From 07433ccbb1b9895dfb0275c0d55df29495b6da29 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 10 Jan 2026 15:17:05 -0800 Subject: [PATCH 1/6] Fix loading --- apps/sim/app/api/copilot/chat/route.ts | 51 +++++------------- .../api/copilot/chat/update-messages/route.ts | 12 +++++ apps/sim/stores/panel/copilot/store.ts | 53 +++++++++++++++++-- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index b14feb495d..d723ac6dd0 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -802,49 +802,26 @@ export async function POST(req: NextRequest) { toolNames: toolCalls.map((tc) => tc?.name).filter(Boolean), }) - // Save messages to database after streaming completes (including aborted messages) + // NOTE: Messages are saved by the client via update-messages endpoint with full contentBlocks. + // Server only updates conversationId here to avoid overwriting client's richer save. if (currentChat) { - const updatedMessages = [...conversationHistory, userMessage] - - // Save assistant message if there's any content or tool calls (even partial from abort) - if (assistantContent.trim() || toolCalls.length > 0) { - const assistantMessage = { - id: crypto.randomUUID(), - role: 'assistant', - content: assistantContent, - timestamp: new Date().toISOString(), - ...(toolCalls.length > 0 && { toolCalls }), - } - updatedMessages.push(assistantMessage) - logger.info( - `[${tracker.requestId}] Saving assistant message with content (${assistantContent.length} chars) and ${toolCalls.length} tool calls` - ) - } else { - logger.info( - `[${tracker.requestId}] No assistant content or tool calls to save (aborted before response)` - ) - } - // Persist only a safe conversationId to avoid continuing from a state that expects tool outputs const previousConversationId = currentChat?.conversationId as string | undefined const responseId = lastSafeDoneResponseId || previousConversationId || undefined - // Update chat in database immediately (without title) - await db - .update(copilotChats) - .set({ - messages: updatedMessages, - updatedAt: new Date(), - ...(responseId ? { conversationId: responseId } : {}), + if (responseId) { + await db + .update(copilotChats) + .set({ + updatedAt: new Date(), + conversationId: responseId, + }) + .where(eq(copilotChats.id, actualChatId!)) + + logger.info(`[${tracker.requestId}] Updated conversationId for chat ${actualChatId}`, { + updatedConversationId: responseId, }) - .where(eq(copilotChats.id, actualChatId!)) - - logger.info(`[${tracker.requestId}] Updated chat ${actualChatId} with new messages`, { - messageCount: updatedMessages.length, - savedUserMessage: true, - savedAssistantMessage: assistantContent.trim().length > 0, - updatedConversationId: responseId || null, - }) + } } } catch (error) { logger.error(`[${tracker.requestId}] Error processing stream:`, error) diff --git a/apps/sim/app/api/copilot/chat/update-messages/route.ts b/apps/sim/app/api/copilot/chat/update-messages/route.ts index adb040bcce..292983a5e7 100644 --- a/apps/sim/app/api/copilot/chat/update-messages/route.ts +++ b/apps/sim/app/api/copilot/chat/update-messages/route.ts @@ -76,6 +76,18 @@ export async function POST(req: NextRequest) { } const { chatId, messages, planArtifact, config } = UpdateMessagesSchema.parse(body) + + // Debug: Log what we're about to save + const lastMsgParsed = messages[messages.length - 1] + if (lastMsgParsed?.role === 'assistant') { + logger.info(`[${tracker.requestId}] Parsed messages to save`, { + messageCount: messages.length, + lastMsgId: lastMsgParsed.id, + lastMsgContentLength: lastMsgParsed.content?.length || 0, + lastMsgContentBlockCount: lastMsgParsed.contentBlocks?.length || 0, + lastMsgContentBlockTypes: lastMsgParsed.contentBlocks?.map((b: any) => b?.type) || [], + }) + } // Verify that the chat belongs to the user const [chat] = await db diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 9c49d38041..4a29f05e26 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -572,8 +572,30 @@ function stripTodoTags(text: string): string { */ function deepClone(obj: T): T { try { - return JSON.parse(JSON.stringify(obj)) - } catch { + const json = JSON.stringify(obj) + if (!json || json === 'undefined') { + logger.warn('[deepClone] JSON.stringify returned empty for object', { + type: typeof obj, + isArray: Array.isArray(obj), + length: Array.isArray(obj) ? obj.length : undefined, + }) + return obj + } + const parsed = JSON.parse(json) + // Verify the clone worked + if (Array.isArray(obj) && (!Array.isArray(parsed) || parsed.length !== obj.length)) { + logger.warn('[deepClone] Array clone mismatch', { + originalLength: obj.length, + clonedLength: Array.isArray(parsed) ? parsed.length : 'not array', + }) + } + return parsed + } catch (err) { + logger.error('[deepClone] Failed to clone object', { + error: String(err), + type: typeof obj, + isArray: Array.isArray(obj), + }) return obj } } @@ -587,11 +609,18 @@ function serializeMessagesForDB(messages: CopilotMessage[]): any[] { const result = messages .map((msg) => { // Deep clone the entire message to ensure all nested data is serializable + // Ensure timestamp is always a string (Zod schema requires it) + let timestamp: string = msg.timestamp + if (typeof timestamp !== 'string') { + const ts = timestamp as any + timestamp = ts instanceof Date ? ts.toISOString() : new Date().toISOString() + } + const serialized: any = { id: msg.id, role: msg.role, content: msg.content || '', - timestamp: msg.timestamp, + timestamp, } // Deep clone contentBlocks (the main rendering data) @@ -3151,7 +3180,7 @@ export const useCopilotStore = create()( model: selectedModel, } - await fetch('/api/copilot/chat/update-messages', { + const saveResponse = await fetch('/api/copilot/chat/update-messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -3162,6 +3191,18 @@ export const useCopilotStore = create()( }), }) + if (!saveResponse.ok) { + const errorText = await saveResponse.text().catch(() => '') + logger.error('[Stream Done] Failed to save messages to DB', { + status: saveResponse.status, + error: errorText, + }) + } else { + logger.info('[Stream Done] Successfully saved messages to DB', { + messageCount: dbMessages.length, + }) + } + // Update local chat object with plan artifact and config set({ currentChat: { @@ -3170,7 +3211,9 @@ export const useCopilotStore = create()( config, }, }) - } catch {} + } catch (err) { + logger.error('[Stream Done] Exception saving messages', { error: String(err) }) + } } // Post copilot_stats record (input/output tokens can be null for now) From e3b849ad74b7f15e3017aaac40ce4f216dd11298 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 10 Jan 2026 15:29:52 -0800 Subject: [PATCH 2/6] Fix Lint --- apps/sim/app/api/copilot/chat/route.ts | 9 ++++++--- .../app/api/copilot/chat/update-messages/route.ts | 2 +- .../components/copilot-message/copilot-message.tsx | 13 ++----------- apps/sim/stores/panel/copilot/store.ts | 8 ++++---- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index d723ac6dd0..8d92bdbb7b 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -818,9 +818,12 @@ export async function POST(req: NextRequest) { }) .where(eq(copilotChats.id, actualChatId!)) - logger.info(`[${tracker.requestId}] Updated conversationId for chat ${actualChatId}`, { - updatedConversationId: responseId, - }) + logger.info( + `[${tracker.requestId}] Updated conversationId for chat ${actualChatId}`, + { + updatedConversationId: responseId, + } + ) } } } catch (error) { diff --git a/apps/sim/app/api/copilot/chat/update-messages/route.ts b/apps/sim/app/api/copilot/chat/update-messages/route.ts index 292983a5e7..217ba0b058 100644 --- a/apps/sim/app/api/copilot/chat/update-messages/route.ts +++ b/apps/sim/app/api/copilot/chat/update-messages/route.ts @@ -76,7 +76,7 @@ export async function POST(req: NextRequest) { } const { chatId, messages, planArtifact, config } = UpdateMessagesSchema.parse(body) - + // Debug: Log what we're about to save const lastMsgParsed = messages[messages.length - 1] if (lastMsgParsed?.role === 'assistant') { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx index 2ff11db336..2cba10be86 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx @@ -470,17 +470,8 @@ const CopilotMessage: FC = memo( {/* Content blocks in chronological order */} {memoizedContentBlocks} - {/* Show streaming indicator if streaming but no text content yet after tool calls */} - {isStreaming && - !message.content && - message.contentBlocks?.every((block) => block.type === 'tool_call') && ( - - )} - - {/* Streaming indicator when no content yet */} - {!cleanTextContent && !message.contentBlocks?.length && isStreaming && ( - - )} + {/* Always show streaming indicator at the end while streaming */} + {isStreaming && } {message.errorType === 'usage_limit' && (
diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 4a29f05e26..2075ea49b7 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -574,7 +574,7 @@ function deepClone(obj: T): T { try { const json = JSON.stringify(obj) if (!json || json === 'undefined') { - logger.warn('[deepClone] JSON.stringify returned empty for object', { + logger.warn('[deepClone] JSON.stringify returned empty for object', { type: typeof obj, isArray: Array.isArray(obj), length: Array.isArray(obj) ? obj.length : undefined, @@ -591,8 +591,8 @@ function deepClone(obj: T): T { } return parsed } catch (err) { - logger.error('[deepClone] Failed to clone object', { - error: String(err), + logger.error('[deepClone] Failed to clone object', { + error: String(err), type: typeof obj, isArray: Array.isArray(obj), }) @@ -615,7 +615,7 @@ function serializeMessagesForDB(messages: CopilotMessage[]): any[] { const ts = timestamp as any timestamp = ts instanceof Date ? ts.toISOString() : new Date().toISOString() } - + const serialized: any = { id: msg.id, role: msg.role, From 0350321d1b2327874f11aae1ef5f763b8e754e77 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 10 Jan 2026 15:36:24 -0800 Subject: [PATCH 3/6] Scroll stickiness --- .../w/[workflowId]/hooks/use-scroll-management.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts index e8299998c7..62ba34dac1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts @@ -74,7 +74,7 @@ export function useScrollManagement( const { scrollTop, scrollHeight, clientHeight } = scrollContainer const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const nearBottom = distanceFromBottom <= 100 + const nearBottom = distanceFromBottom <= 80 setIsNearBottom(nearBottom) if (isSendingMessage) { @@ -174,7 +174,7 @@ export function useScrollManagement( const { scrollTop, scrollHeight, clientHeight } = scrollContainer const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const nearBottom = distanceFromBottom <= 120 + const nearBottom = distanceFromBottom <= 80 if (nearBottom) { scrollToBottom() } From 47209aee32e5bc0abeba2e9883830fac38d38be0 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 10 Jan 2026 15:49:15 -0800 Subject: [PATCH 4/6] Scroll stickiness --- .../panel/components/copilot/copilot.tsx | 6 ++++-- .../w/[workflowId]/hooks/use-scroll-management.ts | 15 +++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx index 550a5d9a54..03053ccf3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx @@ -124,8 +124,10 @@ export const Copilot = forwardRef(({ panelWidth }, ref isSendingMessage, }) - // Handle scroll management - const { scrollAreaRef, scrollToBottom } = useScrollManagement(messages, isSendingMessage) + // Handle scroll management (80px stickiness for copilot) + const { scrollAreaRef, scrollToBottom } = useScrollManagement(messages, isSendingMessage, { + stickinessThreshold: 80, + }) // Handle chat history grouping const { groupedChats, handleHistoryDropdownOpen: handleHistoryDropdownOpenHook } = useChatHistory( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts index 62ba34dac1..9a6587d9c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts @@ -12,6 +12,12 @@ interface UseScrollManagementOptions { * - `auto`: immediate scroll to bottom (used by floating chat to avoid jitter). */ behavior?: 'auto' | 'smooth' + /** + * Distance from bottom (in pixels) within which auto-scroll stays active. + * Lower values = less sticky (user can scroll away easier). + * Default is 100px. + */ + stickinessThreshold?: number } /** @@ -34,6 +40,7 @@ export function useScrollManagement( const programmaticScrollInProgressRef = useRef(false) const lastScrollTopRef = useRef(0) const scrollBehavior: 'auto' | 'smooth' = options?.behavior ?? 'smooth' + const stickinessThreshold = options?.stickinessThreshold ?? 100 /** * Scrolls the container to the bottom with smooth animation @@ -74,7 +81,7 @@ export function useScrollManagement( const { scrollTop, scrollHeight, clientHeight } = scrollContainer const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const nearBottom = distanceFromBottom <= 80 + const nearBottom = distanceFromBottom <= stickinessThreshold setIsNearBottom(nearBottom) if (isSendingMessage) { @@ -95,7 +102,7 @@ export function useScrollManagement( // Track last scrollTop for direction detection lastScrollTopRef.current = scrollTop - }, [getScrollContainer, isSendingMessage, userHasScrolledDuringStream]) + }, [getScrollContainer, isSendingMessage, userHasScrolledDuringStream, stickinessThreshold]) // Attach scroll listener useEffect(() => { @@ -174,14 +181,14 @@ export function useScrollManagement( const { scrollTop, scrollHeight, clientHeight } = scrollContainer const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const nearBottom = distanceFromBottom <= 80 + const nearBottom = distanceFromBottom <= stickinessThreshold if (nearBottom) { scrollToBottom() } }, 100) return () => window.clearInterval(intervalId) - }, [isSendingMessage, userHasScrolledDuringStream, getScrollContainer, scrollToBottom]) + }, [isSendingMessage, userHasScrolledDuringStream, getScrollContainer, scrollToBottom, stickinessThreshold]) return { scrollAreaRef, From fd2c4b6a7cc48a0130804afd4d7994560cb15a51 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Sat, 10 Jan 2026 17:35:17 -0800 Subject: [PATCH 5/6] improvement: diff controls and notifications positioning --- .../diff-controls/diff-controls.tsx | 4 +-- .../components/tool-call/tool-call.tsx | 6 +++-- .../hooks/use-scroll-management.ts | 8 +++++- apps/sim/stores/panel/copilot/store.ts | 26 ++++++++++++++++--- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx index 6d1ebac811..c34b7f73ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx @@ -303,8 +303,8 @@ export const DiffControls = memo(function DiffControls() { !isResizing && 'transition-[bottom,right] duration-100 ease-out' )} style={{ - bottom: 'calc(var(--terminal-height) + 8px)', - right: 'calc(var(--panel-width) + 8px)', + bottom: 'calc(var(--terminal-height) + 16px)', + right: 'calc(var(--panel-width) + 16px)', }} >
{code && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts index 9a6587d9c0..5959368e3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-scroll-management.ts @@ -188,7 +188,13 @@ export function useScrollManagement( }, 100) return () => window.clearInterval(intervalId) - }, [isSendingMessage, userHasScrolledDuringStream, getScrollContainer, scrollToBottom, stickinessThreshold]) + }, [ + isSendingMessage, + userHasScrolledDuringStream, + getScrollContainer, + scrollToBottom, + stickinessThreshold, + ]) return { scrollAreaRef, diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 2075ea49b7..d4e926c91e 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -271,11 +271,31 @@ function resolveToolDisplay( if (cand?.text || cand?.icon) return { text: cand.text, icon: cand.icon } } } catch {} - // Humanized fallback as last resort + // Humanized fallback as last resort - include state verb for proper verb-noun styling try { if (toolName) { - const text = toolName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - return { text, icon: undefined as any } + const formattedName = toolName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + // Add state verb prefix for verb-noun rendering in tool-call component + let stateVerb: string + switch (state) { + case ClientToolCallState.pending: + case ClientToolCallState.executing: + stateVerb = 'Executing' + break + case ClientToolCallState.success: + stateVerb = 'Executed' + break + case ClientToolCallState.error: + stateVerb = 'Failed' + break + case ClientToolCallState.rejected: + case ClientToolCallState.aborted: + stateVerb = 'Skipped' + break + default: + stateVerb = 'Executing' + } + return { text: `${stateVerb} ${formattedName}`, icon: undefined as any } } } catch {} return undefined From 077a9023254b74ce68034f86dd348b88ee03ae65 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Sat, 10 Jan 2026 17:59:49 -0800 Subject: [PATCH 6/6] feat(copilot): editable input component --- .../components/tool-call/tool-call.tsx | 277 ++++++++++++------ 1 file changed, 187 insertions(+), 90 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 80f956dbb4..4f921c898e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -3,7 +3,8 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { ChevronUp, LayoutList } from 'lucide-react' -import { Button, Code } from '@/components/emcn' +import Editor from 'react-simple-code-editor' +import { Button, Code, getCodeEditorProps, highlight, languages } from '@/components/emcn' import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { getClientTool } from '@/lib/copilot/tools/client/manager' import { getRegisteredTools } from '@/lib/copilot/tools/client/registry' @@ -753,36 +754,70 @@ function SubAgentToolCall({ toolCall: toolCallProp }: { toolCall: CopilotToolCal const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} const inputEntries = Object.entries(safeInputs) if (inputEntries.length === 0) return null + + /** + * Format a value for display - handles objects, arrays, and primitives + */ + const formatValue = (value: unknown): string => { + if (value === null || value === undefined) return '-' + if (typeof value === 'string') return value || '-' + if (typeof value === 'number' || typeof value === 'boolean') return String(value) + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + } + + /** + * Check if a value is a complex type (object or array) + */ + const isComplex = (value: unknown): boolean => { + return typeof value === 'object' && value !== null + } + return ( -
- - - - - - - - - {inputEntries.map(([key, value]) => ( - - - - - ))} - -
- Input - - Value -
- - {key} - - - - {String(value)} +
+ {/* Header */} +
+ Input + + {inputEntries.length} + +
+ {/* Input entries */} +
+ {inputEntries.map(([key, value], index) => { + const formattedValue = formatValue(value) + const needsCodeViewer = isComplex(value) + + return ( +
0 && 'border-[var(--border-1)] border-t' + )} + > + {/* Input key */} + {key} + {/* Value display */} + {needsCodeViewer ? ( + + ) : ( + + {formattedValue} -
+ )} +
+ ) + })} +
) } @@ -2292,74 +2327,136 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} const inputEntries = Object.entries(safeInputs) - // Don't show the table if there are no inputs + // Don't show the section if there are no inputs if (inputEntries.length === 0) { return null } + /** + * Format a value for display - handles objects, arrays, and primitives + */ + const formatValueForDisplay = (value: unknown): string => { + if (value === null || value === undefined) return '' + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean') return String(value) + // For objects and arrays, use JSON.stringify with formatting + try { + return JSON.stringify(value, null, 2) + } catch { + return String(value) + } + } + + /** + * Parse a string value back to its original type if possible + */ + const parseInputValue = (value: string, originalValue: unknown): unknown => { + // If original was a primitive, keep as string + if (typeof originalValue !== 'object' || originalValue === null) { + return value + } + // Try to parse as JSON for objects/arrays + try { + return JSON.parse(value) + } catch { + return value + } + } + + /** + * Check if a value is a complex type (object or array) + */ + const isComplexValue = (value: unknown): boolean => { + return typeof value === 'object' && value !== null + } + return ( -
- - - - - - - - - {inputEntries.map(([key, value]) => ( - + {/* Header */} +
+ Edit Input + + {inputEntries.length} + +
+ {/* Input entries */} +
+ {inputEntries.map(([key, value], index) => { + const isComplex = isComplexValue(value) + const displayValue = formatValueForDisplay(value) + + return ( +
0 && 'border-[var(--border-1)] border-t' + )} > -
- - - ))} - -
- Input - - Value -
-
- - {key} - -
-
-
- { - const newInputs = { ...safeInputs, [key]: e.target.value } - - // Determine how to update based on original structure - if (isNestedInWorkflowInput) { - // Update workflow_input - setEditedParams({ ...editedParams, workflow_input: newInputs }) - } else if (typeof editedParams.input === 'string') { - // Input was a JSON string, serialize back - setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) - } else if (editedParams.input && typeof editedParams.input === 'object') { - // Input is an object - setEditedParams({ ...editedParams, input: newInputs }) - } else if ( - editedParams.inputs && - typeof editedParams.inputs === 'object' - ) { - // Inputs is an object - setEditedParams({ ...editedParams, inputs: newInputs }) - } else { - // Flat structure - update at base level - setEditedParams({ ...editedParams, [key]: e.target.value }) - } - }} - className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]' - /> -
-
+ {/* Input key */} + {key} + {/* Value editor */} + {isComplex ? ( + + + { + const parsedValue = parseInputValue(newCode, value) + const newInputs = { ...safeInputs, [key]: parsedValue } + + if (isNestedInWorkflowInput) { + setEditedParams({ ...editedParams, workflow_input: newInputs }) + } else if (typeof editedParams.input === 'string') { + setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) + } else if ( + editedParams.input && + typeof editedParams.input === 'object' + ) { + setEditedParams({ ...editedParams, input: newInputs }) + } else if ( + editedParams.inputs && + typeof editedParams.inputs === 'object' + ) { + setEditedParams({ ...editedParams, inputs: newInputs }) + } else { + setEditedParams({ ...editedParams, [key]: parsedValue }) + } + }} + highlight={(code) => highlight(code, languages.json, 'json')} + {...getCodeEditorProps()} + className={clsx(getCodeEditorProps().className, 'min-h-[40px]')} + style={{ minHeight: '40px' }} + /> + + + ) : ( + { + const parsedValue = parseInputValue(e.target.value, value) + const newInputs = { ...safeInputs, [key]: parsedValue } + + if (isNestedInWorkflowInput) { + setEditedParams({ ...editedParams, workflow_input: newInputs }) + } else if (typeof editedParams.input === 'string') { + setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) + } else if (editedParams.input && typeof editedParams.input === 'object') { + setEditedParams({ ...editedParams, input: newInputs }) + } else if (editedParams.inputs && typeof editedParams.inputs === 'object') { + setEditedParams({ ...editedParams, inputs: newInputs }) + } else { + setEditedParams({ ...editedParams, [key]: parsedValue }) + } + }} + className='w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium font-mono text-[13px] text-[var(--text-primary)] outline-none transition-colors placeholder:text-[var(--text-muted)] focus:outline-none' + /> + )} +
+ ) + })} + ) }