diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index 84e297507..9cd7a9a4e 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -19,6 +19,7 @@ describe('Chat Edit API Route', () => { const mockCreateErrorResponse = vi.fn() const mockEncryptSecret = vi.fn() const mockCheckChatAccess = vi.fn() + const mockGetSession = vi.fn() beforeEach(() => { vi.resetModules() @@ -42,6 +43,10 @@ describe('Chat Edit API Route', () => { chat: { id: 'id', identifier: 'identifier', userId: 'userId' }, })) + vi.doMock('@/lib/auth', () => ({ + getSession: mockGetSession, + })) + vi.doMock('@/lib/logs/console/logger', () => ({ createLogger: vi.fn().mockReturnValue({ info: vi.fn(), @@ -89,9 +94,7 @@ describe('Chat Edit API Route', () => { describe('GET', () => { it('should return 401 when user is not authenticated', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(null), - })) + mockGetSession.mockResolvedValueOnce(null) const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123') const { GET } = await import('@/app/api/chat/manage/[id]/route') @@ -102,11 +105,9 @@ describe('Chat Edit API Route', () => { }) it('should return 404 when chat not found or access denied', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-id' }, - }), - })) + mockGetSession.mockResolvedValueOnce({ + user: { id: 'user-id' }, + }) mockCheckChatAccess.mockResolvedValue({ hasAccess: false }) diff --git a/apps/sim/app/api/copilot/chat/route.test.ts b/apps/sim/app/api/copilot/chat/route.test.ts index e2950cced..80b30dabf 100644 --- a/apps/sim/app/api/copilot/chat/route.test.ts +++ b/apps/sim/app/api/copilot/chat/route.test.ts @@ -563,6 +563,8 @@ describe('Copilot Chat API Route', () => { ], messageCount: 4, previewYaml: null, + config: null, + planArtifact: null, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-02T00:00:00.000Z', }, @@ -576,6 +578,8 @@ describe('Copilot Chat API Route', () => { ], messageCount: 2, previewYaml: null, + config: null, + planArtifact: null, createdAt: '2024-01-03T00:00:00.000Z', updatedAt: '2024-01-04T00:00:00.000Z', }, diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 5a90b167b..5fc8a563f 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -53,7 +53,7 @@ const ChatMessageSchema = z.object({ ]) .optional() .default('claude-4.5-sonnet'), - mode: z.enum(['ask', 'agent']).optional().default('agent'), + mode: z.enum(['ask', 'agent', 'plan']).optional().default('agent'), prefetch: z.boolean().optional(), createNewChat: z.boolean().optional().default(false), stream: z.boolean().optional().default(true), @@ -880,6 +880,8 @@ export async function GET(req: NextRequest) { title: copilotChats.title, model: copilotChats.model, messages: copilotChats.messages, + planArtifact: copilotChats.planArtifact, + config: copilotChats.config, createdAt: copilotChats.createdAt, updatedAt: copilotChats.updatedAt, }) @@ -897,6 +899,8 @@ export async function GET(req: NextRequest) { messages: Array.isArray(chat.messages) ? chat.messages : [], messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0, previewYaml: null, // Not needed for chat list + planArtifact: chat.planArtifact || null, + config: chat.config || null, createdAt: chat.createdAt, updatedAt: chat.updatedAt, })) 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 d4e0ebfae..f58e2f7a5 100644 --- a/apps/sim/app/api/copilot/chat/update-messages/route.ts +++ b/apps/sim/app/api/copilot/chat/update-messages/route.ts @@ -37,6 +37,14 @@ const UpdateMessagesSchema = z.object({ .optional(), }) ), + planArtifact: z.string().nullable().optional(), + config: z + .object({ + mode: z.enum(['ask', 'build', 'plan']).optional(), + model: z.string().optional(), + }) + .nullable() + .optional(), }) export async function POST(req: NextRequest) { @@ -49,7 +57,7 @@ export async function POST(req: NextRequest) { } const body = await req.json() - const { chatId, messages } = UpdateMessagesSchema.parse(body) + const { chatId, messages, planArtifact, config } = UpdateMessagesSchema.parse(body) // Verify that the chat belongs to the user const [chat] = await db @@ -62,18 +70,27 @@ export async function POST(req: NextRequest) { return createNotFoundResponse('Chat not found or unauthorized') } - // Update chat with new messages - await db - .update(copilotChats) - .set({ - messages: messages, - updatedAt: new Date(), - }) - .where(eq(copilotChats.id, chatId)) + // Update chat with new messages, plan artifact, and config + const updateData: Record = { + messages: messages, + updatedAt: new Date(), + } - logger.info(`[${tracker.requestId}] Successfully updated chat messages`, { + if (planArtifact !== undefined) { + updateData.planArtifact = planArtifact + } + + if (config !== undefined) { + updateData.config = config + } + + await db.update(copilotChats).set(updateData).where(eq(copilotChats.id, chatId)) + + logger.info(`[${tracker.requestId}] Successfully updated chat`, { chatId, newMessageCount: messages.length, + hasPlanArtifact: !!planArtifact, + hasConfig: !!config, }) return NextResponse.json({ diff --git a/apps/sim/app/api/templates/approved/sanitized/route.ts b/apps/sim/app/api/templates/approved/sanitized/route.ts new file mode 100644 index 000000000..7615f145b --- /dev/null +++ b/apps/sim/app/api/templates/approved/sanitized/route.ts @@ -0,0 +1,140 @@ +import { db } from '@sim/db' +import { templates } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { checkInternalApiKey } from '@/lib/copilot/utils' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' +import { sanitizeForCopilot } from '@/lib/workflows/json-sanitizer' + +const logger = createLogger('TemplatesSanitizedAPI') + +export const revalidate = 0 + +/** + * GET /api/templates/approved/sanitized + * Returns all approved templates with their sanitized JSONs, names, and descriptions + * Requires internal API secret authentication via X-API-Key header + */ +export async function GET(request: NextRequest) { + const requestId = generateRequestId() + + try { + const url = new URL(request.url) + const hasApiKey = !!request.headers.get('x-api-key') + + // Check internal API key authentication + const authResult = checkInternalApiKey(request) + if (!authResult.success) { + logger.warn(`[${requestId}] Authentication failed for approved sanitized templates`, { + error: authResult.error, + hasApiKey, + howToUse: 'Add header: X-API-Key: ', + }) + return NextResponse.json( + { + error: authResult.error, + hint: 'Include X-API-Key header with INTERNAL_API_SECRET value', + }, + { status: 401 } + ) + } + + // Fetch all approved templates + const approvedTemplates = await db + .select({ + id: templates.id, + name: templates.name, + details: templates.details, + state: templates.state, + tags: templates.tags, + requiredCredentials: templates.requiredCredentials, + }) + .from(templates) + .where(eq(templates.status, 'approved')) + + // Process each template to sanitize for copilot + const sanitizedTemplates = approvedTemplates + .map((template) => { + try { + const copilotSanitized = sanitizeForCopilot(template.state as any) + + if (copilotSanitized?.blocks) { + Object.values(copilotSanitized.blocks).forEach((block: any) => { + if (block && typeof block === 'object') { + block.outputs = undefined + block.position = undefined + block.height = undefined + block.layout = undefined + block.horizontalHandles = undefined + + // Also clean nested nodes recursively + if (block.nestedNodes) { + Object.values(block.nestedNodes).forEach((nestedBlock: any) => { + if (nestedBlock && typeof nestedBlock === 'object') { + nestedBlock.outputs = undefined + nestedBlock.position = undefined + nestedBlock.height = undefined + nestedBlock.layout = undefined + nestedBlock.horizontalHandles = undefined + } + }) + } + } + }) + } + + const details = template.details as { tagline?: string; about?: string } | null + const description = details?.tagline || details?.about || '' + + return { + id: template.id, + name: template.name, + description, + tags: template.tags, + requiredCredentials: template.requiredCredentials, + sanitizedJson: copilotSanitized, + } + } catch (error) { + logger.error(`[${requestId}] Error sanitizing template ${template.id}`, { + error: error instanceof Error ? error.message : String(error), + }) + return null + } + }) + .filter((t): t is NonNullable => t !== null) + + const response = { + templates: sanitizedTemplates, + count: sanitizedTemplates.length, + } + + return NextResponse.json(response) + } catch (error) { + logger.error(`[${requestId}] Error fetching approved sanitized templates`, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + return NextResponse.json( + { + error: 'Internal server error', + requestId, + }, + { status: 500 } + ) + } +} + +// Add a helpful OPTIONS handler for CORS preflight +export async function OPTIONS(request: NextRequest) { + const requestId = generateRequestId() + logger.info(`[${requestId}] OPTIONS request received for /api/templates/approved/sanitized`) + + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'X-API-Key, Content-Type', + }, + }) +} diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index a84afc495..cbd07cf41 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -4,6 +4,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { generateRequestId } from '@/lib/utils' import { extractAndPersistCustomTools } from '@/lib/workflows/custom-tools-persistence' @@ -248,6 +249,26 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const elapsed = Date.now() - startTime logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`) + try { + const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' + const notifyResponse = await fetch(`${socketUrl}/api/workflow-updated`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workflowId }), + }) + + if (!notifyResponse.ok) { + logger.warn( + `[${requestId}] Failed to notify Socket.IO server about workflow ${workflowId} update` + ) + } + } catch (notificationError) { + logger.warn( + `[${requestId}] Error notifying Socket.IO server about workflow ${workflowId} update`, + notificationError + ) + } + return NextResponse.json({ success: true, warnings }, { status: 200 }) } catch (error: any) { const elapsed = Date.now() - startTime diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index bf960b3a3..cceeb2514 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -70,7 +70,7 @@ export function OutputSelect({ const popoverRef = useRef(null) const contentRef = useRef(null) const blocks = useWorkflowStore((state) => state.blocks) - const { isShowingDiff, isDiffReady, diffWorkflow } = useWorkflowDiffStore() + const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore() const subBlockValues = useSubBlockStore((state) => workflowId ? state.workflowValues[workflowId] : null ) @@ -78,7 +78,9 @@ export function OutputSelect({ /** * Uses diff blocks when in diff mode, otherwise main blocks */ - const workflowBlocks = isShowingDiff && isDiffReady && diffWorkflow ? diffWorkflow.blocks : blocks + const shouldUseBaseline = hasActiveDiff && isDiffReady && !isShowingDiff && baselineWorkflow + const workflowBlocks = + shouldUseBaseline && baselineWorkflow ? baselineWorkflow.blocks : (blocks as any) /** * Extracts all available workflow outputs for the dropdown @@ -100,7 +102,7 @@ export function OutputSelect({ const blockArray = Object.values(workflowBlocks) if (blockArray.length === 0) return outputs - blockArray.forEach((block) => { + blockArray.forEach((block: any) => { if (block.type === 'starter' || !block?.id || !block?.type) return const blockName = @@ -110,8 +112,8 @@ export function OutputSelect({ const blockConfig = getBlock(block.type) const responseFormatValue = - isShowingDiff && isDiffReady && diffWorkflow - ? diffWorkflow.blocks[block.id]?.subBlocks?.responseFormat?.value + shouldUseBaseline && baselineWorkflow + ? baselineWorkflow.blocks?.[block.id]?.subBlocks?.responseFormat?.value : subBlockValues?.[block.id]?.responseFormat const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) @@ -164,7 +166,16 @@ export function OutputSelect({ }) return outputs - }, [workflowBlocks, workflowId, isShowingDiff, isDiffReady, diffWorkflow, blocks, subBlockValues]) + }, [ + workflowBlocks, + workflowId, + isShowingDiff, + isDiffReady, + baselineWorkflow, + blocks, + subBlockValues, + shouldUseBaseline, + ]) /** * Checks if an output is currently selected by comparing both ID and label 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 a375ed86e..2e7e5c328 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 @@ -12,20 +12,28 @@ const logger = createLogger('DiffControls') export const DiffControls = memo(function DiffControls() { // Optimized: Single diff store subscription - const { isShowingDiff, isDiffReady, diffWorkflow, toggleDiffView, acceptChanges, rejectChanges } = - useWorkflowDiffStore( - useCallback( - (state) => ({ - isShowingDiff: state.isShowingDiff, - isDiffReady: state.isDiffReady, - diffWorkflow: state.diffWorkflow, - toggleDiffView: state.toggleDiffView, - acceptChanges: state.acceptChanges, - rejectChanges: state.rejectChanges, - }), - [] - ) + const { + isShowingDiff, + isDiffReady, + hasActiveDiff, + toggleDiffView, + acceptChanges, + rejectChanges, + baselineWorkflow, + } = useWorkflowDiffStore( + useCallback( + (state) => ({ + isShowingDiff: state.isShowingDiff, + isDiffReady: state.isDiffReady, + hasActiveDiff: state.hasActiveDiff, + toggleDiffView: state.toggleDiffView, + acceptChanges: state.acceptChanges, + rejectChanges: state.rejectChanges, + baselineWorkflow: state.baselineWorkflow, + }), + [] ) + ) // Optimized: Single copilot store subscription for needed values const { updatePreviewToolCallState, clearPreviewYaml, currentChat, messages } = useCopilotStore( @@ -61,10 +69,11 @@ export const DiffControls = memo(function DiffControls() { try { logger.info('Creating checkpoint before accepting changes') - // Get current workflow state from the store and ensure it's complete - const rawState = useWorkflowStore.getState().getWorkflowState() + // Use the baseline workflow (state before diff) instead of current state + // This ensures reverting to the checkpoint restores the pre-diff state + const rawState = baselineWorkflow || useWorkflowStore.getState().getWorkflowState() - // Merge subblock values from the SubBlockStore to get complete state + // The baseline already has merged subblock values, but we'll merge again to be safe // This ensures all user inputs and subblock data are captured const blocksWithSubblockValues = mergeSubblockState(rawState.blocks, activeWorkflowId) @@ -199,7 +208,7 @@ export const DiffControls = memo(function DiffControls() { logger.error('Failed to create checkpoint:', error) return false } - }, [activeWorkflowId, currentChat, messages]) + }, [activeWorkflowId, currentChat, messages, baselineWorkflow]) const handleAccept = useCallback(async () => { logger.info('Accepting proposed changes with backup protection') @@ -297,7 +306,7 @@ export const DiffControls = memo(function DiffControls() { }, [clearPreviewYaml, updatePreviewToolCallState, rejectChanges]) // Don't show anything if no diff is available or diff is not ready - if (!diffWorkflow || !isDiffReady) { + if (!hasActiveDiff || !isDiffReady) { return null } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts index d1def975e..a151aef4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts @@ -1,5 +1,6 @@ export * from './copilot-message/copilot-message' export * from './inline-tool-call/inline-tool-call' +export * from './plan-mode-section/plan-mode-section' export * from './todo-list/todo-list' export * from './user-input/user-input' export * from './welcome/welcome' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx index 154fc23fc..270775c38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/inline-tool-call/inline-tool-call.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Loader2 } from 'lucide-react' import useDrivePicker from 'react-google-drive-picker' import { Button } from '@/components/emcn' @@ -52,8 +52,17 @@ const ACTION_VERBS = [ 'Editing', 'Edited', 'Running', + 'Ran', 'Designing', 'Designed', + 'Searching', + 'Searched', + 'Debugging', + 'Debugged', + 'Validating', + 'Validated', + 'Adjusting', + 'Adjusted', 'Summarizing', 'Summarized', 'Marking', @@ -70,6 +79,27 @@ const ACTION_VERBS = [ 'Evaluating', 'Evaluated', 'Finished', + 'Setting', + 'Set', + 'Applied', + 'Applying', + 'Rejected', + 'Deploy', + 'Deploying', + 'Deployed', + 'Redeploying', + 'Redeployed', + 'Redeploy', + 'Undeploy', + 'Undeploying', + 'Undeployed', + 'Checking', + 'Checked', + 'Opening', + 'Opened', + 'Create', + 'Creating', + 'Created', ] as const /** @@ -198,10 +228,15 @@ function ShimmerOverlayText({ /** * Determines if a tool call is "special" and should display with gradient styling. - * Only workflow operation tools (edit, build, run) get the purple gradient. + * Only workflow operation tools (edit, build, run, deploy) get the purple gradient. */ function isSpecialToolCall(toolCall: CopilotToolCall): boolean { - const workflowOperationTools = ['edit_workflow', 'build_workflow', 'run_workflow'] + const workflowOperationTools = [ + 'edit_workflow', + 'build_workflow', + 'run_workflow', + 'deploy_workflow', + ] return workflowOperationTools.includes(toolCall.name) } @@ -223,12 +258,21 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { return hasInterrupt && toolCall.state === 'pending' } -async function handleRun(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) { +async function handleRun( + toolCall: CopilotToolCall, + setToolCallState: any, + onStateChange?: any, + editedParams?: any +) { const instance = getClientTool(toolCall.id) if (!instance) return try { const mergedParams = - (toolCall as any).params || (toolCall as any).parameters || (toolCall as any).input || {} + editedParams || + (toolCall as any).params || + (toolCall as any).parameters || + (toolCall as any).input || + {} await instance.handleAccept?.(mergedParams) onStateChange?.('executing') } catch (e) { @@ -262,9 +306,11 @@ function getDisplayName(toolCall: CopilotToolCall): string { function RunSkipButtons({ toolCall, onStateChange, + editedParams, }: { toolCall: CopilotToolCall onStateChange?: (state: any) => void + editedParams?: any }) { const [isProcessing, setIsProcessing] = useState(false) const [buttonsHidden, setButtonsHidden] = useState(false) @@ -280,7 +326,7 @@ function RunSkipButtons({ setIsProcessing(true) setButtonsHidden(true) try { - await handleRun(toolCall, setToolCallState, onStateChange) + await handleRun(toolCall, setToolCallState, onStateChange, editedParams) } finally { setIsProcessing(false) } @@ -418,14 +464,29 @@ export function InlineToolCall({ ) const toolCall = liveToolCall || toolCallProp + // Guard: nothing to render without a toolCall + if (!toolCall) return null + const isExpandablePending = toolCall?.state === 'pending' && - (toolCall.name === 'make_api_request' || toolCall.name === 'set_global_workflow_variables') + (toolCall.name === 'make_api_request' || + toolCall.name === 'set_global_workflow_variables' || + toolCall.name === 'run_workflow') const [expanded, setExpanded] = useState(isExpandablePending) - // Guard: nothing to render without a toolCall - if (!toolCall) return null + // State for editable parameters + const params = (toolCall as any).parameters || (toolCall as any).input || toolCall.params || {} + const [editedParams, setEditedParams] = useState(params) + const paramsRef = useRef(params) + + // Update edited params when toolCall params change (deep comparison to avoid resetting user edits on ref change) + useEffect(() => { + if (JSON.stringify(params) !== JSON.stringify(paramsRef.current)) { + setEditedParams(params) + paramsRef.current = params + } + }, [params]) // Skip rendering tools that are not in the registry or are explicitly omitted try { @@ -436,7 +497,9 @@ export function InlineToolCall({ return null } const isExpandableTool = - toolCall.name === 'make_api_request' || toolCall.name === 'set_global_workflow_variables' + toolCall.name === 'make_api_request' || + toolCall.name === 'set_global_workflow_variables' || + toolCall.name === 'run_workflow' const showButtons = shouldShowRunSkipButtons(toolCall) const showMoveToBackground = @@ -450,7 +513,6 @@ export function InlineToolCall({ } const displayName = getDisplayName(toolCall) - const params = (toolCall as any).parameters || (toolCall as any).input || toolCall.params || {} const isLoadingState = toolCall.state === ClientToolCallState.pending || @@ -460,8 +522,8 @@ export function InlineToolCall({ const renderPendingDetails = () => { if (toolCall.name === 'make_api_request') { - const url = params.url || '' - const method = (params.method || '').toUpperCase() + const url = editedParams.url || '' + const method = (editedParams.method || '').toUpperCase() return (
@@ -479,19 +541,23 @@ export function InlineToolCall({ @@ -503,17 +569,20 @@ export function InlineToolCall({ if (toolCall.name === 'set_environment_variables') { const variables = - params.variables && typeof params.variables === 'object' ? params.variables : {} + editedParams.variables && typeof editedParams.variables === 'object' + ? editedParams.variables + : {} // Normalize variables - handle both direct key-value and nested {name, value} format - const normalizedEntries: Array<[string, string]> = [] + // Store [originalKey, displayName, displayValue] + const normalizedEntries: Array<[string, string, string]> = [] Object.entries(variables).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && 'name' in value && 'value' in value) { - // Handle { name: "KEY", value: "VAL" } format - normalizedEntries.push([String((value as any).name), String((value as any).value)]) + // Handle { name: "KEY", value: "VAL" } format (common in arrays or structured objects) + normalizedEntries.push([key, String((value as any).name), String((value as any).value)]) } else { // Handle direct key-value format - normalizedEntries.push([key, String(value)]) + normalizedEntries.push([key, key, String(value)]) } }) @@ -538,21 +607,75 @@ export function InlineToolCall({ ) : ( - normalizedEntries.map(([name, value]) => ( + normalizedEntries.map(([originalKey, name, value]) => ( @@ -565,7 +688,7 @@ export function InlineToolCall({ } if (toolCall.name === 'set_global_workflow_variables') { - const ops = Array.isArray(params.operations) ? (params.operations as any[]) : [] + const ops = Array.isArray(editedParams.operations) ? (editedParams.operations as any[]) : [] return (
@@ -588,9 +711,16 @@ export function InlineToolCall({ {ops.map((op, idx) => (
- - {String(op.name || '')} - + { + const newOps = [...ops] + newOps[idx] = { ...op, name: e.target.value } + setEditedParams({ ...editedParams, operations: newOps }) + }} + className='w-full bg-transparent font-season text-amber-800 text-xs outline-none dark:text-amber-200' + />
@@ -599,9 +729,16 @@ export function InlineToolCall({
{op.value !== undefined ? ( - - {String(op.value)} - + { + const newOps = [...ops] + newOps[idx] = { ...op, value: e.target.value } + setEditedParams({ ...editedParams, operations: newOps }) + }} + className='w-full bg-transparent font-[470] font-mono text-amber-700 text-xs outline-none focus:text-amber-800 dark:text-amber-300 dark:focus:text-amber-200' + /> ) : ( — @@ -616,6 +753,111 @@ export function InlineToolCall({ ) } + if (toolCall.name === 'run_workflow') { + // Get inputs - could be in multiple locations + let inputs = editedParams.input || editedParams.inputs || editedParams.workflow_input + let isNestedInWorkflowInput = false + + // If input is a JSON string, parse it + if (typeof inputs === 'string') { + try { + inputs = JSON.parse(inputs) + } catch { + inputs = {} + } + } + + // Check if workflow_input exists and contains the actual inputs + if (editedParams.workflow_input && typeof editedParams.workflow_input === 'object') { + inputs = editedParams.workflow_input + isNestedInWorkflowInput = true + } + + // If no inputs object found, treat base editedParams as inputs (excluding system fields) + if (!inputs || typeof inputs !== 'object') { + const { workflowId, workflow_input, ...rest } = editedParams + inputs = rest + } + + const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} + const inputEntries = Object.entries(safeInputs) + + return ( +
+
- - {method || 'GET'} - + setEditedParams({ ...editedParams, method: e.target.value })} + className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground' + />
- - {url || 'URL not provided'} - + setEditedParams({ ...editedParams, url: e.target.value })} + placeholder='URL not provided' + className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground' + />
- {name} + { + const newName = e.target.value + const newVariables = Array.isArray(variables) + ? [...variables] + : { ...variables } + + if (Array.isArray(newVariables)) { + // Array format: update .name property + const idx = Number(originalKey) + const item = newVariables[idx] + if (typeof item === 'object' && item !== null && 'name' in item) { + newVariables[idx] = { ...item, name: newName } + } + } else { + // Object format: rename key + // We need to preserve the value but change the key + const value = newVariables[originalKey as keyof typeof newVariables] + delete newVariables[originalKey as keyof typeof newVariables] + newVariables[newName as keyof typeof newVariables] = value + } + setEditedParams({ ...editedParams, variables: newVariables }) + }} + className='w-full bg-transparent font-medium text-foreground text-xs outline-none' + />
- - {value} - + { + // Clone the variables container (works for both Array and Object) + const newVariables = Array.isArray(variables) + ? [...variables] + : { ...variables } + + const currentVal = + newVariables[originalKey as keyof typeof newVariables] + + if ( + typeof currentVal === 'object' && + currentVal !== null && + 'value' in currentVal + ) { + // Update value in object structure + newVariables[originalKey as keyof typeof newVariables] = { + ...(currentVal as any), + value: e.target.value, + } + } else { + // Update direct value + newVariables[originalKey as keyof typeof newVariables] = e.target + .value as any + } + setEditedParams({ ...editedParams, variables: newVariables }) + }} + className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground' + />
+ + + + + + + + {inputEntries.length === 0 ? ( + + + + ) : ( + inputEntries.map(([key, value]) => ( + + + + + )) + )} + +
+ Input + + Value +
+ No inputs provided +
+
+ {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-muted-foreground text-xs outline-none focus:text-foreground' + /> +
+
+
+ ) + } + return null } @@ -630,7 +872,13 @@ export function InlineToolCall({ className='font-[470] font-season text-[#939393] text-sm dark:text-[#939393]' />
{renderPendingDetails()}
- {showButtons && } + {showButtons && ( + + )} ) } @@ -652,7 +900,11 @@ export function InlineToolCall({ {isExpandableTool && expanded &&
{renderPendingDetails()}
} {showButtons ? ( - + ) : showMoveToBackground ? (
+ + + ) : ( + <> + {onBuildPlan && ( + + )} + {onSave && ( + + )} + {onClear && ( + + )} + + )} +
+ + + {/* Scrollable content area */} +
+ {isEditing ? ( +