From ddd32191267e605e2ab44e018e6d3e500faa2373 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:22:08 -0800 Subject: [PATCH] improvement(copilot): v0.2 (#2086) * Edit workflow returns workflow * Condense workflkow conosle * Limit console results to 2 * mark checkoff v1 * Mark and checkoff todo * Fixes * Plan mode * fix agent output * broken * Fixes * Fix diff mode persist * Fix diff coloring * Undo/redo * Checkpoint udno redo * fix(templates): fix templates details page (#1942) * Fix template details * Fix deps * fix(templates-details): restore approval feature, and keep details UI consistent, smoothen out creation of profile (#1943) * fix(templates): view current ui * update UI to be less cluttered * make state management for creating user profile smoother * fix autoselect logic * fix lint * fix(landing): need to propagate landing page copilot prompt (#1944) * fix(wand): subblocks should not be overwritten after wand gen (#1946) * fix(settings): fix broken api keys, help modal, logs, workflow renaming (#1945) * fix(settings): fix broken api keys, help modal, logs, workflow renaming * fix build * cleanup * use emcn * fix(files): changed file input value sample from string -> object (#1947) * improvement: usage-indicator UI (#1948) * fix(deploy): fix button (#1949) * fix(executor): consolidate execution hooks (#1950) * fix(autoconnect): should check if triggermode is set from the toolbar drag event directly (#1951) * improvement: templates styling (#1952) * improvement: template use button (#1954) * feat(newgifs): added new gifs (#1953) * new gifs * changed wording * changed wording * lowercase * changed wording * remove blog stuff --------- Co-authored-by: aadamgough Co-authored-by: waleed * feat(drizzle): added ods for analytics from drizzle (#1956) * feat(drizzle): added ods for analytics from drizzle * clean * fix(sheets): file selector had incorrect provider (#1958) * feat(docs): added docs analytics drizzle ods (#1957) * feat(docs): added docs analytics drizzle ods * fix build * fix provider for docs selector (#1959) * fix(custom-tools): updates to legacy + copilot generated custom tools (#1960) * fix(custom-tools): updates to existing tools * don't reorder custom tools in modal based on edit time * restructure custom tools to persist copilot generated tools * fix tests * fix(slack): remove update message incorrect canonical param (#1964) * fix(slack): send message tool * add logging * add more logs * add more logs * fix canonical param * improvement(templates): add share button, serve public templates routes for unauthenticated users and workspace one for authenticated users, improve settings style and organization (#1962) * improvement(templates): add share button, serve public templates routes for unauthenticated users and workspace one for authenticated users, improve settings style and organization * fix lint --------- Co-authored-by: Vikhyath Mondreti * fix(onedrive): incorrect canonical param (#1966) * fix(onedrive): incorrect canonical param * fix download file * fix * fix * Revert "fix" This reverts commit f68ccd75fdf9f66ff9e3e07840145ba952d3f851. * Revert "fix" This reverts commit f3d8acee7d84e41cdac4ad7943da6dda34c51cfa. * fix(executor): streaming after tool calls (#1963) * Provider changes * Fix lint * fix(code): readd wand to code subblock (#1969) * fix(cmd-k): z-index + reoder tools, triggers (#1970) * fix(cmd-k): z-index + reoder tools, triggers * fix more z-index styling * fix(executor): streaming response format (#1972) * fix(workflow-block): fix redeploy header to not repeatedly show redeploy when redeploy is not necessary (#1973) * fix(workflow-block): fix redeploy header to not repeatedly show redeploy when redeploy is not necessary * cleanup * improvement(platform): chat, emcn, terminal, usage-limit (#1974) * improvement(usage-indicator): layout * improvement: expand default terminal height * fix: swap workflow block ports * improvement: chat initial positioning * improvement(chat): display; improvement(emcn): popover attributes * fix lint * feat(settings): added reactquery for settings, removed zustand stores, added apollo, added workflow block selector dropdown search, added add environment variable option to empty env var dropdown (#1971) * feat(settings): added reactquery for settings, removed zustand stores, added apollo, added workflow block selector dropdown search, added add environment variable option to empty env var dropdown * fix delete dialog for copilot keys * simplify combobox * fix more z indices * consolidated duplicate hooks --------- Co-authored-by: Vikhyath Mondreti * fix(copilot-subflows): copilot-added subflows id mismatch (#1977) * feat(i18n): update translations (#1978) * feat(i18n): update translations * fix build --------- Co-authored-by: waleedlatif1 * fix(logs): show block inputs (#1979) * Fix executor lgos block inputs * Fix Comment * fix(onedrive): parse array values correctly (#1981) * fix(onedrive): parse array values correctly * fix onedrive * fix * fix onedrive input parsing by reusing code subblock * fix type * feat(files): add presigned URL generation support for execution files (#1980) * fix(popovers): billed account + async example command (#1982) * fix(settings): update usage data in settings > subs to use reactquery hooks (#1983) * fix(settings): update usage data in settings > subs to use reactquery hooks * standardize usage pills calculation * fix(output-selector): z-index in chat deploy modal (#1984) * improvement(logs): improved logs search (#1985) * improvement(logs): improved logs search * more * ack PR comments * feat(slack): added slack full message object in response (#1987) * feat(slack): add better error messages, reminder to add bot to app (#1990) * feat(i18n): update translations (#1989) Co-authored-by: waleedlatif1 * fix(landing): hero stripe icon (#1988) * Temp * Update * Update * Update * Progress * Diff store fixes * simplify sockets ops for diff store * Search patterns tool * Better tool calls * Fix sanitizationg * Context window display * fix(variables): fix double stringification (#1991) * improvement(variables): support dot notation for nested objects (#1992) * improvement(tanstack): migrate multiple stores (#1994) * improvement(tanstack): migrate folders, knowledge to tanstack * fix types * fix(folders): duplicate (#1996) * fix(variables): fix variables block json resolution (#1997) * Improvement(ui/ux): signup, command-list, cursors, search modal, workflow runs, usage indicator (#1998) * improvement: signup loading, command-list, cursors, search modal ordering * improvement: workflow runs, search modal * improvement(usage-indicator): ui/ux * test(pr): hackathon (#1999) * test(pr): github trigger (#2000) * fix(usage-indicator): conditional rendering, upgrade, and ui/ux (#2001) * fix: usage-limit indicator and render conditonally on is billing enabled * fix: upgrade render * fix(notes): fix notes, tighten spacing, update deprecated zustand function, update use mention data to ignore block positon (#2002) * fix(pdfs): use unpdf instead of pdf-parse (#2004) * fix(modals): fix z-index for various modals and output selector and variables (#2005) * fix(condition): treat condition input the same as the code subblock (#2006) * feat(models): added gpt-5.1 (#2007) * improvement: runpath edges, blocks, active (#2008) * feat(i18n): update translations (#2009) * fix(triggers): check triggermode and consolidate block type (#2011) * fix(triggers): disabled trigger shouldn't be added to dag (#2012) * Fix disabled blocks * Comments * Fix api/chat trigger not found message * fix(tags): only show start block upstream if is ancestor (#2013) * fix(variables): Fix resolution on double < (#2016) * Fix variable <> * Ling * Clean * feat(billing): add notif for first failed payment, added upgrade email from free, updated providers that supported granular tool control to support them, fixed envvar popover, fixed redirect to wrong workspace after oauth connect (#2015) * feat(billing): add notif for first failed payment, added upgrade email from free, updated providers that supported granular tool control to support them, fixed envvar popover, fixed redirect to wrong workspace after oauth connect * fix build * ack PR comments * feat(performance): added reactquery hooks for workflow operations, for logs, fixed logs reloading, fix subscription UI (#2017) * feat(performance): added reactquery hooks for workflow operations, for logs, fixed logs reloading, fix subscription UI * use useInfiniteQuery for logs fetching * fix(copilot): run workflow supports input format and fix run id (#2018) * fix(router): fix error edge in router block + fix source handle problem (#2019) * Fix router block error port handling * Remove comment * Fix edge execution * improvement: code subblock, action bar, connections (#2024) * improvement: action bar, connections * fix: code block draggable resize * fix(response): fix response block http format (#2027) * Fix response block * Lint * fix(notes): fix notes block spacing, additional logs for billing transfer route (#2029) * fix(usage-data): refetch on usage limit update in settings (#2032) * fix(overage): fix pill calculation in the usage indicator to be consistent across views (#2034) * fix(workflows): fixed workflow loading in without start block, added templates RQ hook, cleaned up unused templates code (#2035) * fix(triggers): dedup + not surfacing deployment status log (#2033) * fix(triggers): dedup + not surfacing deployment status log * fix ms teams * change to microsoftteams * Revert "change to microsoftteams" This reverts commit 217f808641acf16cd64eae2ba7a2d6aee72f500b. * fix * fix * fix provider name * fix oauth for msteams * improvement(undo-redo): expand undo-redo store to store 100 ops instead of 15 (#2036) * improvement(undo-redo): expand undo-redo store to store 100 ops instead of 15 * prevent undo-redo from interfering with subblock browser text undo * improvement(docs): remove copy page from mobile view on docs (#2037) * improvement(docs): remove copy page from mobile view on docs * bring title and pagenav lower on mobile * added cursor pointer to clickable components in docs * fix(workflow-block): clearing child workflow input format field must lazy cascade parent workflow state deletion (#2038) * Error tools * Ui tools * Navigate ui tool * Combine env vars and oauth tools * Plan mode section * Plan mode v1 * Plan mode v1 * Plan mode improvements * Build plan button * Make workflow plan editable * Tool improvements * Fix build plan button * Lint * Fix * Fix lint * Fix plan * Remove migrations * fix undo/redo settling of ops * Add migratinos back * Smoothen out diff store * Hide plan mode * Fix lint * Edit run workflow params * Fix lint * Fix tests * Fix typing * Fix build errors and lint * Fix build * Fix tests --------- Co-authored-by: Vikhyath Mondreti Co-authored-by: Waleed Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com> Co-authored-by: aadamgough Co-authored-by: Vikhyath Mondreti Co-authored-by: Emir Karabeg Co-authored-by: waleedlatif1 --- .../app/api/chat/manage/[id]/route.test.ts | 17 +- apps/sim/app/api/copilot/chat/route.test.ts | 4 + apps/sim/app/api/copilot/chat/route.ts | 6 +- .../api/copilot/chat/update-messages/route.ts | 37 +- .../api/templates/approved/sanitized/route.ts | 140 + .../sim/app/api/workflows/[id]/state/route.ts | 21 + .../output-select/output-select.tsx | 23 +- .../diff-controls/diff-controls.tsx | 45 +- .../components/copilot/components/index.ts | 1 + .../inline-tool-call/inline-tool-call.tsx | 336 +- .../plan-mode-section/plan-mode-section.tsx | 284 + .../context-usage-indicator.tsx | 76 + .../components/user-input/components/index.ts | 1 + .../mode-selector/mode-selector.tsx | 22 +- .../components/user-input/user-input.tsx | 184 +- .../copilot/components/welcome/welcome.tsx | 4 +- .../panel/components/copilot/copilot.tsx | 45 +- .../components/deploy-modal/deploy-modal.tsx | 115 +- .../sub-block/hooks/use-sub-block-value.ts | 32 +- .../panel/components/editor/editor.tsx | 4 +- .../hooks/use-editor-block-properties.ts | 16 +- .../hooks/use-editor-subblock-layout.ts | 45 +- .../workflow-block/workflow-block.tsx | 25 +- .../hooks/use-current-workflow.ts | 20 +- .../hooks/use-workflow-execution.ts | 32 +- .../utils/workflow-execution-utils.ts | 19 - .../[workspaceId]/w/[workflowId]/workflow.tsx | 22 +- .../workspace/providers/socket-provider.tsx | 11 +- apps/sim/hooks/use-collaborative-workflow.ts | 138 +- apps/sim/hooks/use-undo-redo.ts | 2124 +++-- apps/sim/lib/copilot/api.ts | 12 +- apps/sim/lib/copilot/registry.ts | 130 +- .../sim/lib/copilot/tools/client/base-tool.ts | 16 + .../client/blocks/get-blocks-metadata.ts | 40 +- .../tools/client/examples/get-examples-rag.ts | 22 + .../examples/get-operations-examples.ts | 22 + .../copilot/tools/client/gdrive/list-files.ts | 21 + .../copilot/tools/client/gdrive/read-file.ts | 22 + .../tools/client/navigation/navigate-ui.ts | 241 + .../tools/client/other/make-api-request.ts | 38 + .../tools/client/other/remember-debug.ts | 80 + .../client/other/search-documentation.ts | 22 + .../tools/client/other/search-errors.ts | 53 + .../tools/client/other/search-online.ts | 22 + .../tools/client/other/search-patterns.ts | 53 + ...auth-credentials.ts => get-credentials.ts} | 37 +- .../client/user/get-environment-variables.ts | 77 - .../client/user/set-environment-variables.ts | 24 + .../workflow/check-deployment-status.ts | 100 + .../tools/client/workflow/deploy-workflow.ts | 333 + .../tools/client/workflow/edit-workflow.ts | 181 +- .../client/workflow/get-user-workflow.ts | 87 +- .../client/workflow/get-workflow-console.ts | 36 +- .../client/workflow/get-workflow-from-name.ts | 35 +- .../tools/client/workflow/run-workflow.ts | 27 + .../workflow/set-global-workflow-variables.ts | 32 + apps/sim/lib/copilot/tools/server/router.ts | 6 +- ...auth-credentials.ts => get-credentials.ts} | 47 +- .../server/user/get-environment-variables.ts | 56 - .../server/workflow/get-workflow-console.ts | 61 +- apps/sim/lib/oauth/oauth.test.ts | 22 +- apps/sim/lib/workflows/deployment-utils.ts | 131 + apps/sim/lib/workflows/diff/diff-engine.ts | 73 +- apps/sim/lib/workflows/diff/index.ts | 2 +- apps/sim/lib/workflows/json-sanitizer.ts | 6 +- apps/sim/lib/workflows/socket-operations.ts | 93 + apps/sim/scripts/export-workflow.ts | 125 + apps/sim/socket-server/database/operations.ts | 101 + apps/sim/socket-server/handlers/operations.ts | 38 + apps/sim/socket-server/handlers/workflow.ts | 23 - .../socket-server/middleware/permissions.ts | 2 + apps/sim/socket-server/validation/schemas.ts | 11 + apps/sim/stores/operation-queue/store.ts | 5 +- apps/sim/stores/panel/copilot/store.ts | 496 +- apps/sim/stores/panel/copilot/types.ts | 20 +- apps/sim/stores/undo-redo/index.ts | 2 +- apps/sim/stores/undo-redo/store.ts | 69 + apps/sim/stores/undo-redo/types.ts | 35 + apps/sim/stores/undo-redo/utils.ts | 59 + apps/sim/stores/workflow-diff/store.ts | 974 ++- apps/sim/stores/workflows/subblock/store.ts | 8 + apps/sim/stores/workflows/subblock/types.ts | 1 + apps/sim/stores/workflows/workflow/store.ts | 37 + apps/sim/stores/workflows/workflow/types.ts | 4 + .../db/migrations/0111_solid_dreadnoughts.sql | 2 + .../db/migrations/meta/0111_snapshot.json | 7684 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 2 + 88 files changed, 13675 insertions(+), 2139 deletions(-) create mode 100644 apps/sim/app/api/templates/approved/sanitized/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-usage-indicator/context-usage-indicator.tsx create mode 100644 apps/sim/lib/copilot/tools/client/navigation/navigate-ui.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/remember-debug.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/search-errors.ts create mode 100644 apps/sim/lib/copilot/tools/client/other/search-patterns.ts rename apps/sim/lib/copilot/tools/client/user/{get-oauth-credentials.ts => get-credentials.ts} (58%) delete mode 100644 apps/sim/lib/copilot/tools/client/user/get-environment-variables.ts create mode 100644 apps/sim/lib/copilot/tools/client/workflow/check-deployment-status.ts create mode 100644 apps/sim/lib/copilot/tools/client/workflow/deploy-workflow.ts rename apps/sim/lib/copilot/tools/server/user/{get-oauth-credentials.ts => get-credentials.ts} (70%) delete mode 100644 apps/sim/lib/copilot/tools/server/user/get-environment-variables.ts create mode 100644 apps/sim/lib/workflows/deployment-utils.ts create mode 100644 apps/sim/lib/workflows/socket-operations.ts create mode 100755 apps/sim/scripts/export-workflow.ts create mode 100644 packages/db/migrations/0111_solid_dreadnoughts.sql create mode 100644 packages/db/migrations/meta/0111_snapshot.json 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 ? ( +