From d25c7fef3a140ea009e5791a8157a97b015013b6 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 17 Mar 2026 19:16:44 -0700 Subject: [PATCH 01/36] Improve --- .../copilot/client-sse/run-tool-execution.ts | 2 + .../orchestrator/tool-executor/index.ts | 4 +- .../tools/client/tool-display-registry.ts | 92 ------------------- 3 files changed, 4 insertions(+), 94 deletions(-) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts index c62627075ad..1368ff31306 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -271,6 +271,7 @@ function buildResultData(result: unknown): Record | undefined { return { success: r.success, output: r.output, + logs: r.logs, error: r.error, } } @@ -280,6 +281,7 @@ function buildResultData(result: unknown): Record | undefined { return { success: exec.success, output: exec.output, + logs: exec.logs, error: exec.error, } } diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index fac22162fd9..ec77233dab5 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -1023,8 +1023,8 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record< /** * Check whether a tool can be executed on the Sim (TypeScript) side. * - * Tools that are only available on the Go backend (e.g. search_patterns, - * search_errors, remember_debug) will return false. The subagent tool_call + * Tools that are only available on the Go backend (e.g. search_patterns) + * will return false. The subagent tool_call * handler uses this to decide whether to execute a tool locally or let the * Go backend's own tool_result SSE event handle it. */ diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index 5ddeba098b3..fbfb10d4ef4 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -1318,63 +1318,7 @@ const META_redeploy: ToolMetadata = { interrupt: undefined, } -const META_remember_debug: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Validated fix', icon: CheckCircle2 }, - [ClientToolCallState.error]: { text: 'Failed to validate', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted validation', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped validation', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - const operation = params?.operation - if (operation === 'add' || operation === 'edit') { - // For add/edit, show from problem or solution - const text = params?.problem || params?.solution - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Validated fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Validating fix ${text}` - case ClientToolCallState.error: - return `Failed to validate fix ${text}` - case ClientToolCallState.aborted: - return `Aborted validating fix ${text}` - case ClientToolCallState.rejected: - return `Skipped validating fix ${text}` - } - } - } else if (operation === 'delete') { - // For delete, show from problem or solution (or id as fallback) - const text = params?.problem || params?.solution || params?.id - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Adjusted fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Adjusting fix ${text}` - case ClientToolCallState.error: - return `Failed to adjust fix ${text}` - case ClientToolCallState.aborted: - return `Aborted adjusting fix ${text}` - case ClientToolCallState.rejected: - return `Skipped adjusting fix ${text}` - } - } - } - - return undefined - }, -} const META_research: ToolMetadata = { displayNames: { @@ -1784,40 +1728,6 @@ const META_search_documentation: ToolMetadata = { }, } -const META_search_errors: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, - [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted debugging', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped debugging', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Debugged ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Debugging ${query}` - case ClientToolCallState.error: - return `Failed to debug ${query}` - case ClientToolCallState.aborted: - return `Aborted debugging ${query}` - case ClientToolCallState.rejected: - return `Skipped debugging ${query}` - } - } - return undefined - }, -} - const META_search_library_docs: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 }, @@ -2466,7 +2376,6 @@ const TOOL_METADATA_BY_ID: Record = { read: META_read, redeploy: META_redeploy, rename_workflow: META_rename_workflow, - remember_debug: META_remember_debug, research: META_research, run: META_run, run_block: META_run_block, @@ -2475,7 +2384,6 @@ const TOOL_METADATA_BY_ID: Record = { run_workflow_until_block: META_run_workflow_until_block, scrape_page: META_scrape_page, search_documentation: META_search_documentation, - search_errors: META_search_errors, search_library_docs: META_search_library_docs, search_online: META_search_online, search_patterns: META_search_patterns, From e063a9d95fd114c3156f1c1985726ef43cb23947 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 17 Mar 2026 19:22:29 -0700 Subject: [PATCH 02/36] Hide is hosted --- .../components/bottom-controls/bottom-controls.tsx | 13 ++++++++----- .../copilot/tools/client/tool-display-registry.ts | 3 --- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx index f01b583c84d..2b0dfb859a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx @@ -2,6 +2,7 @@ import { ArrowUp, Image, Loader2 } from 'lucide-react' import { Badge, Button } from '@/components/emcn' +import { isHosted } from '@/lib/core/config/feature-flags' import { cn } from '@/lib/core/utils/cn' import { ModeSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector' import { ModelSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector' @@ -56,11 +57,13 @@ export function BottomControls({ /> )} - + {!isHosted && ( + + )} {/* Right side: Attach Button + Send Button */} diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index fbfb10d4ef4..effc6fb339b 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -4,7 +4,6 @@ import { Bug, Check, CheckCircle, - CheckCircle2, ClipboardList, Database, Eye, @@ -1318,8 +1317,6 @@ const META_redeploy: ToolMetadata = { interrupt: undefined, } - - const META_research: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Researching', icon: Loader2 }, From 46ed61cab35b7b363885fb3bfad560f3d6ac10fb Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 17 Mar 2026 19:24:28 -0700 Subject: [PATCH 03/36] Remove hardcoded --- apps/sim/stores/panel/copilot/store.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index a2c3249441f..9d0b9101500 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { type CopilotChat, sendStreamingMessage } from '@/lib/copilot/api' +import { isHosted } from '@/lib/core/config/feature-flags' import { applySseEvent, sseHandlers } from '@/lib/copilot/client-sse' import { appendContinueOption, @@ -588,8 +589,7 @@ async function initiateStream( chatId: prepared.currentChat?.id, workflowId: prepared.workflowId || undefined, mode: apiMode, - model: selectedModelId, - provider: selectedProvider || undefined, + ...(isHosted ? {} : { model: selectedModelId, provider: selectedProvider || undefined }), prefetch: get().agentPrefetch, createNewChat: !prepared.currentChat, stream: prepared.stream, @@ -967,8 +967,7 @@ async function resumeFromLiveStream( workflowId: resume.nextStream.workflowId, chatId: resume.nextStream.chatId || get().currentChat?.id || undefined, mode: get().mode === 'ask' ? 'ask' : get().mode === 'plan' ? 'plan' : 'agent', - model: resumeModelId, - provider: resumeProvider || undefined, + ...(isHosted ? {} : { model: resumeModelId, provider: resumeProvider || undefined }), prefetch: get().agentPrefetch, stream: true, resumeFromEventId: resume.resumeFromEventId, @@ -1544,8 +1543,7 @@ export const useCopilotStore = create()( chatId: currentChat?.id, workflowId, mode: apiMode, - model: fbModelId, - provider: fbProvider || undefined, + ...(isHosted ? {} : { model: fbModelId, provider: fbProvider || undefined }), prefetch: get().agentPrefetch, createNewChat: !currentChat, stream: true, From bd7e090e88be0c6370f2cf21fdb9b97b59d4715a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 17 Mar 2026 19:42:26 -0700 Subject: [PATCH 04/36] fix --- apps/sim/app/api/mcp/copilot/route.ts | 4 ++-- apps/sim/app/api/v1/copilot/chat/route.ts | 2 +- apps/sim/lib/copilot/constants.ts | 8 ++++---- apps/sim/lib/copilot/orchestrator/tool-executor/index.ts | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 58c124c00c5..5a269e0407e 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -517,7 +517,7 @@ async function handleMcpRequestWithSdk( try { await transport.handleRequest(requestAdapter as any, responseCapture as any, parsedBody) await responseCapture.waitForHeaders() - // Must exceed the longest possible tool execution (build = 5 min). + // Must exceed the longest possible tool execution. // Using ORCHESTRATION_TIMEOUT_MS + 60 s buffer so the orchestrator can // finish or time-out on its own before the transport is torn down. await responseCapture.waitForEnd(ORCHESTRATION_TIMEOUT_MS + 60_000) @@ -729,7 +729,7 @@ async function handleBuildToolCall( chatId, goRoute: '/api/mcp', autoExecuteTools: true, - timeout: 300000, + timeout: ORCHESTRATION_TIMEOUT_MS, interactive: false, abortSignal, }) diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index e3acb50a10a..32fab46db0f 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -17,7 +17,7 @@ const RequestSchema = z.object({ mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'), model: z.string().optional(), autoExecuteTools: z.boolean().optional().default(true), - timeout: z.number().optional().default(300000), + timeout: z.number().optional().default(3_600_000), }) /** diff --git a/apps/sim/lib/copilot/constants.ts b/apps/sim/lib/copilot/constants.ts index c5d65e97841..09359f52ca2 100644 --- a/apps/sim/lib/copilot/constants.ts +++ b/apps/sim/lib/copilot/constants.ts @@ -24,11 +24,11 @@ export const REDIS_COPILOT_STREAM_PREFIX = 'copilot_stream:' // Timeouts // --------------------------------------------------------------------------- -/** Default timeout for the copilot orchestration stream loop (5 min). */ -export const ORCHESTRATION_TIMEOUT_MS = 300_000 +/** Default timeout for the copilot orchestration stream loop (60 min). */ +export const ORCHESTRATION_TIMEOUT_MS = 3_600_000 -/** Timeout for the client-side streaming response handler (10 min). */ -export const STREAM_TIMEOUT_MS = 600_000 +/** Timeout for the client-side streaming response handler (60 min). */ +export const STREAM_TIMEOUT_MS = 3_600_000 /** TTL for Redis tool call confirmation entries (24 h). */ export const REDIS_TOOL_CALL_TTL_SECONDS = 86_400 diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index ec77233dab5..03768311d9e 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -698,7 +698,6 @@ const SERVER_TOOLS = new Set([ 'edit_workflow', 'get_workflow_logs', 'search_documentation', - 'search_online', 'set_environment_variables', 'make_api_request', 'knowledge_base', From a36454d7f0c3cb81e09dad51150c13aa829439df Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 18 Mar 2026 11:08:55 -0700 Subject: [PATCH 05/36] Fixes --- apps/sim/app/api/copilot/chat/route.ts | 2 ++ apps/sim/app/api/copilot/chat/stream/route.ts | 4 +++- apps/sim/app/api/mcp/copilot/route.ts | 2 +- apps/sim/app/api/mothership/chat/route.ts | 2 ++ apps/sim/app/api/mothership/execute/route.ts | 2 ++ apps/sim/app/api/v1/copilot/chat/route.ts | 2 ++ apps/sim/lib/core/config/feature-flags.ts | 6 +++--- bun.lock | 1 - 8 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 305280b8f0c..bfc1d6bac32 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -31,6 +31,8 @@ import { getUserEntityPermissions, } from '@/lib/workspaces/permissions/utils' +export const maxDuration = 3600 + const logger = createLogger('CopilotChatAPI') const FileAttachmentSchema = z.object({ diff --git a/apps/sim/app/api/copilot/chat/stream/route.ts b/apps/sim/app/api/copilot/chat/stream/route.ts index 70e34c07fc4..d83c9afd449 100644 --- a/apps/sim/app/api/copilot/chat/stream/route.ts +++ b/apps/sim/app/api/copilot/chat/stream/route.ts @@ -8,9 +8,11 @@ import { import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers' import { SSE_HEADERS } from '@/lib/core/utils/sse' +export const maxDuration = 3600 + const logger = createLogger('CopilotChatStreamAPI') const POLL_INTERVAL_MS = 250 -const MAX_STREAM_MS = 10 * 60 * 1000 +const MAX_STREAM_MS = 60 * 60 * 1000 function encodeEvent(event: Record): Uint8Array { return new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 5a269e0407e..125e34894ba 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -40,7 +40,7 @@ const DEFAULT_COPILOT_MODEL = 'claude-opus-4-5' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' -export const maxDuration = 300 +export const maxDuration = 3600 interface CopilotKeyAuthResult { success: boolean diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index c1478c172fb..3c3efaeb25b 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -22,6 +22,8 @@ import { getUserEntityPermissions, } from '@/lib/workspaces/permissions/utils' +export const maxDuration = 3600 + const logger = createLogger('MothershipChatAPI') const FileAttachmentSchema = z.object({ diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index f7f2e72d71d..6ec8bc33e24 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -10,6 +10,8 @@ import { getUserEntityPermissions, } from '@/lib/workspaces/permissions/utils' +export const maxDuration = 3600 + const logger = createLogger('MothershipExecuteAPI') const MessageSchema = z.object({ diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index 32fab46db0f..8b2d899803d 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -6,6 +6,8 @@ import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator' import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' import { authenticateV1Request } from '@/app/api/v1/auth' +export const maxDuration = 3600 + const logger = createLogger('CopilotHeadlessAPI') const DEFAULT_COPILOT_MODEL = 'claude-opus-4-5' diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index b1e3b148d61..f9b36bdf65b 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' +export const isHosted = true + // getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || + // getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled diff --git a/bun.lock b/bun.lock index 61df0c93763..83d43243e51 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", From 28ad4386d76b2303196b56c5df6534e4d5b8d482 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 18 Mar 2026 11:30:31 -0700 Subject: [PATCH 06/36] v0 --- apps/sim/app/api/files/upload/route.ts | 15 +- .../[workspaceId]/home/hooks/use-chat.ts | 34 +- .../settings/[section]/settings.tsx | 9 - .../components/copilot/copilot-skeleton.tsx | 14 - .../settings/components/copilot/copilot.tsx | 398 --- .../[workspaceId]/settings/navigation.ts | 9 - .../components/chat-message/chat-message.tsx | 5 +- .../diff-controls/diff-controls.tsx | 75 +- .../notifications/notifications.tsx | 7 +- .../checkpoint-confirmation.tsx | 79 - .../checkpoint-confirmation/index.ts | 1 - .../components/file-display/file-display.tsx | 135 - .../components/file-display/index.ts | 1 - .../copilot-message/components/index.ts | 6 - .../components/markdown-renderer/index.ts | 1 - .../markdown-renderer/markdown-renderer.tsx | 331 --- .../components/smooth-streaming/index.ts | 1 - .../smooth-streaming/smooth-streaming.tsx | 107 - .../components/thinking-block/index.ts | 1 - .../thinking-block/thinking-block.tsx | 364 --- .../components/usage-limit-actions/index.ts | 1 - .../usage-limit-actions.tsx | 105 - .../copilot-message/copilot-message.tsx | 568 ---- .../components/copilot-message/hooks/index.ts | 2 - .../hooks/use-checkpoint-management.ts | 264 -- .../hooks/use-message-editing.ts | 256 -- .../components/copilot-message/index.ts | 1 - .../components/copilot/components/index.ts | 8 - .../components/plan-mode-section/index.ts | 1 - .../plan-mode-section/plan-mode-section.tsx | 283 -- .../components/queued-messages/index.ts | 1 - .../queued-messages/queued-messages.tsx | 103 - .../copilot/components/todo-list/index.ts | 1 - .../components/todo-list/todo-list.tsx | 159 - .../copilot/components/tool-call/index.ts | 1 - .../components/tool-call/tool-call.tsx | 2166 -------------- .../attached-files-display.tsx | 122 - .../attached-files-display/index.ts | 1 - .../bottom-controls/bottom-controls.tsx | 130 - .../components/bottom-controls/index.ts | 1 - .../context-pills/context-pills.tsx | 51 - .../components/context-pills/index.ts | 1 - .../components/user-input/components/index.ts | 7 - .../mention-menu/folder-content.tsx | 161 -- .../components/mention-menu/index.ts | 1 - .../components/mention-menu/mention-menu.tsx | 333 --- .../components/mode-selector/index.ts | 1 - .../mode-selector/mode-selector.tsx | 143 - .../components/model-selector/index.ts | 1 - .../model-selector/model-selector.tsx | 189 -- .../user-input/components/slash-menu/index.ts | 1 - .../components/slash-menu/slash-menu.tsx | 207 -- .../copilot/components/user-input/index.ts | 1 - .../components/user-input/user-input.tsx | 901 ------ .../copilot/components/welcome/index.ts | 1 - .../copilot/components/welcome/welcome.tsx | 72 - .../panel/components/copilot/copilot.tsx | 561 ---- .../panel/components/copilot/hooks/index.ts | 3 - .../copilot/hooks/use-chat-history.ts | 98 - .../hooks/use-copilot-initialization.ts | 148 - .../copilot/hooks/use-todo-management.ts | 44 - .../components/panel/components/index.ts | 1 - .../w/[workflowId]/components/panel/panel.tsx | 146 +- .../components/terminal/terminal.tsx | 9 +- .../training-modal/training-modal.tsx | 806 ------ .../[workspaceId]/w/[workflowId]/workflow.tsx | 13 +- apps/sim/hooks/queries/copilot-keys.ts | 148 - apps/sim/lib/copilot/api.ts | 242 -- .../lib/copilot/client-sse/content-blocks.ts | 147 - apps/sim/lib/copilot/client-sse/handlers.ts | 983 ------- apps/sim/lib/copilot/client-sse/index.ts | 3 - .../copilot/client-sse/run-tool-execution.ts | 32 +- .../copilot/client-sse/subagent-handlers.ts | 421 --- apps/sim/lib/copilot/client-sse/types.ts | 47 - apps/sim/lib/copilot/messages/checkpoints.ts | 130 - .../copilot/messages/credential-masking.ts | 28 - apps/sim/lib/copilot/messages/index.ts | 4 - apps/sim/lib/copilot/messages/persist.ts | 41 - .../sim/lib/copilot/messages/serialization.ts | 210 -- apps/sim/lib/copilot/store-utils.ts | 111 - apps/sim/lib/core/config/feature-flags.ts | 6 +- apps/sim/stores/copilot-training/index.ts | 2 - apps/sim/stores/copilot-training/store.ts | 196 -- apps/sim/stores/copilot-training/types.ts | 44 - apps/sim/stores/index.ts | 5 +- apps/sim/stores/notifications/index.ts | 2 +- apps/sim/stores/notifications/utils.ts | 49 - apps/sim/stores/panel/copilot/index.ts | 13 - apps/sim/stores/panel/copilot/store.ts | 2576 ----------------- apps/sim/stores/panel/copilot/types.ts | 283 -- apps/sim/stores/panel/index.ts | 16 +- apps/sim/stores/panel/types.ts | 14 + apps/sim/stores/workflow-diff/store.ts | 35 - apps/sim/stores/workflow-diff/utils.ts | 40 - 94 files changed, 199 insertions(+), 15296 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/checkpoint-confirmation.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/todo-list.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/attached-files-display.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/context-pills.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/folder-content.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/mention-menu.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/slash-menu.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-chat-history.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-copilot-initialization.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks/use-todo-management.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx delete mode 100644 apps/sim/hooks/queries/copilot-keys.ts delete mode 100644 apps/sim/lib/copilot/api.ts delete mode 100644 apps/sim/lib/copilot/client-sse/content-blocks.ts delete mode 100644 apps/sim/lib/copilot/client-sse/handlers.ts delete mode 100644 apps/sim/lib/copilot/client-sse/index.ts delete mode 100644 apps/sim/lib/copilot/client-sse/subagent-handlers.ts delete mode 100644 apps/sim/lib/copilot/client-sse/types.ts delete mode 100644 apps/sim/lib/copilot/messages/checkpoints.ts delete mode 100644 apps/sim/lib/copilot/messages/credential-masking.ts delete mode 100644 apps/sim/lib/copilot/messages/index.ts delete mode 100644 apps/sim/lib/copilot/messages/persist.ts delete mode 100644 apps/sim/lib/copilot/messages/serialization.ts delete mode 100644 apps/sim/stores/copilot-training/index.ts delete mode 100644 apps/sim/stores/copilot-training/store.ts delete mode 100644 apps/sim/stores/copilot-training/types.ts delete mode 100644 apps/sim/stores/panel/copilot/index.ts delete mode 100644 apps/sim/stores/panel/copilot/store.ts delete mode 100644 apps/sim/stores/panel/copilot/types.ts diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index e97aeeb3707..1400361c8c8 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -5,7 +5,7 @@ import '@/lib/uploads/core/setup.server' import { getSession } from '@/lib/auth' import type { StorageContext } from '@/lib/uploads/config' import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils' +import { isImageFileType } from '@/lib/uploads/utils/file-utils' import { SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_DOCUMENT_EXTENSIONS, @@ -280,19 +280,8 @@ export async function POST(request: NextRequest) { continue } - // Handle copilot, chat, profile-pictures contexts if (context === 'copilot' || context === 'chat' || context === 'profile-pictures') { - if (context === 'copilot') { - const { isSupportedFileType: isCopilotSupported } = await import( - '@/lib/uploads/contexts/copilot/copilot-file-manager' - ) - const resolvedType = resolveFileType(file) - if (!isImageFileType(resolvedType) && !isCopilotSupported(resolvedType)) { - throw new InvalidRequestError( - 'Unsupported file type. Allowed: images, PDF, and text files (TXT, CSV, MD, HTML, JSON, XML).' - ) - } - } else if (!isImageFileType(file.type)) { + if (context !== 'copilot' && !isImageFileType(file.type)) { throw new InvalidRequestError( `Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for ${context} uploads` ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index c320a6a6d7c..11a80675891 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -249,6 +249,10 @@ function extractResourceFromReadResult( export interface UseChatOptions { onResourceEvent?: () => void + apiPath?: string + stopPath?: string + workflowId?: string + onToolResult?: (toolName: string, success: boolean, result: unknown) => void } export function useChat( @@ -267,6 +271,14 @@ export function useChat( const [activeResourceId, setActiveResourceId] = useState(null) const onResourceEventRef = useRef(options?.onResourceEvent) onResourceEventRef.current = options?.onResourceEvent + const apiPathRef = useRef(options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH) + apiPathRef.current = options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH + const stopPathRef = useRef(options?.stopPath ?? '/api/mothership/chat/stop') + stopPathRef.current = options?.stopPath ?? '/api/mothership/chat/stop' + const workflowIdRef = useRef(options?.workflowId) + workflowIdRef.current = options?.workflowId + const onToolResultRef = useRef(options?.onToolResult) + onToolResultRef.current = options?.onToolResult const resourcesRef = useRef(resources) resourcesRef.current = resources const activeResourceIdRef = useRef(activeResourceId) @@ -355,6 +367,7 @@ export function useChat( }, [initialChatId]) useEffect(() => { + if (workflowIdRef.current) return if (!isHomePage || !chatIdRef.current) return streamGenRef.current++ chatIdRef.current = undefined @@ -418,7 +431,7 @@ export function useChat( if (batchEvents.length === 0 && streamStatus === 'unknown') { const cid = chatIdRef.current if (cid) { - fetch('/api/mothership/chat/stop', { + fetch(stopPathRef.current, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chatId: cid, streamId: activeStreamId, content: '' }), @@ -597,11 +610,13 @@ export function useChat( resources: [], }) } - window.history.replaceState( - null, - '', - `/workspace/${workspaceId}/task/${parsed.chatId}` - ) + if (!workflowIdRef.current) { + window.history.replaceState( + null, + '', + `/workspace/${workspaceId}/task/${parsed.chatId}` + ) + } } } break @@ -786,6 +801,8 @@ export function useChat( invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id) } } + + onToolResultRef.current?.(tc.name, tc.status === 'success', tc.result?.output) } break @@ -909,7 +926,7 @@ export function useChat( } try { - const res = await fetch('/api/mothership/chat/stop', { + const res = await fetch(stopPathRef.current, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -1061,7 +1078,7 @@ export function useChat( })) : undefined - const response = await fetch(MOTHERSHIP_CHAT_API_PATH, { + const response = await fetch(apiPathRef.current, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -1073,6 +1090,7 @@ export function useChat( ...(fileAttachments && fileAttachments.length > 0 ? { fileAttachments } : {}), ...(resourceAttachments ? { resourceAttachments } : {}), ...(contexts && contexts.length > 0 ? { contexts } : {}), + ...(workflowIdRef.current ? { workflowId: workflowIdRef.current } : {}), userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }), signal: abortController.signal, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index a9a007303c8..98e32f268e6 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -7,7 +7,6 @@ import { useSession } from '@/lib/auth/auth-client' import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton' import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton' import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' -import { CopilotSkeleton } from '@/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton' import { CredentialSetsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets-skeleton' import { CredentialsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton' import { CustomToolsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton' @@ -96,13 +95,6 @@ const BYOK = dynamic( () => import('@/app/workspace/[workspaceId]/settings/components/byok/byok').then((m) => m.BYOK), { loading: () => } ) -const Copilot = dynamic( - () => - import('@/app/workspace/[workspaceId]/settings/components/copilot/copilot').then( - (m) => m.Copilot - ), - { loading: () => } -) const MCP = dynamic( () => import('@/app/workspace/[workspaceId]/settings/components/mcp/mcp').then((m) => m.MCP), { loading: () => } @@ -185,7 +177,6 @@ export function SettingsPage({ section }: SettingsPageProps) { {isBillingEnabled && effectiveSection === 'team' && } {effectiveSection === 'sso' && } {effectiveSection === 'byok' && } - {effectiveSection === 'copilot' && } {effectiveSection === 'mcp' && } {effectiveSection === 'custom-tools' && } {effectiveSection === 'skills' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton.tsx deleted file mode 100644 index 000b1e02691..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { - ApiKeySkeleton, - ApiKeysSkeleton, -} from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton' - -/** - * Re-export ApiKeySkeleton as CopilotKeySkeleton since both share identical markup. - */ -export const CopilotKeySkeleton = ApiKeySkeleton - -/** - * Skeleton for the Copilot section shown during dynamic import loading. - */ -export const CopilotSkeleton = ApiKeysSkeleton diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx deleted file mode 100644 index 0f8ae7adc95..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx +++ /dev/null @@ -1,398 +0,0 @@ -'use client' - -import { useMemo, useState } from 'react' -// import { useParams } from 'next/navigation' -import { createLogger } from '@sim/logger' -import { Check, Copy, Plus, Search } from 'lucide-react' -import { - Button, - Input as EmcnInput, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, - // Switch, -} from '@/components/emcn' -import { Input } from '@/components/ui' -import { formatDate } from '@/lib/core/utils/formatting' -// import { useMcpServers, useUpdateMcpServer } from '@/hooks/queries/mcp' -import { CopilotKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton' -import { - type CopilotKey, - useCopilotKeys, - useDeleteCopilotKey, - useGenerateCopilotKey, -} from '@/hooks/queries/copilot-keys' - -const logger = createLogger('CopilotSettings') - -/** - * Copilot Keys management component for handling API keys used with the Copilot feature. - * Provides functionality to create, view, and delete copilot API keys. - */ -// function McpServerSkeleton() { -// return ( -//
-//
-// -// -//
-// -//
-// ) -// } - -export function Copilot() { - // const params = useParams() - // const workspaceId = params.workspaceId as string - - const { data: keys = [], isLoading } = useCopilotKeys() - const generateKey = useGenerateCopilotKey() - const deleteKeyMutation = useDeleteCopilotKey() - - // const { data: mcpServers = [], isLoading: mcpLoading } = useMcpServers(workspaceId) - // const updateServer = useUpdateMcpServer() - - const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) - const [newKeyName, setNewKeyName] = useState('') - const [newKey, setNewKey] = useState(null) - const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) - const [copySuccess, setCopySuccess] = useState(false) - const [deleteKey, setDeleteKey] = useState(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [searchTerm, setSearchTerm] = useState('') - const [createError, setCreateError] = useState(null) - - // const enabledServers = mcpServers.filter((s) => s.enabled) - - // const handleToggleCopilot = async (serverId: string, enabled: boolean) => { - // try { - // await updateServer.mutateAsync({ - // workspaceId, - // serverId, - // updates: { copilotEnabled: enabled }, - // }) - // } catch (error) { - // logger.error('Failed to toggle MCP server for Mothership', { error }) - // } - // } - - const filteredKeys = useMemo(() => { - if (!searchTerm.trim()) return keys - const term = searchTerm.toLowerCase() - return keys.filter( - (key) => - key.name?.toLowerCase().includes(term) || key.displayKey?.toLowerCase().includes(term) - ) - }, [keys, searchTerm]) - - const handleCreateKey = async () => { - if (!newKeyName.trim()) return - - const trimmedName = newKeyName.trim() - const isDuplicate = keys.some((k) => k.name === trimmedName) - if (isDuplicate) { - setCreateError( - `A Copilot API key named "${trimmedName}" already exists. Please choose a different name.` - ) - return - } - - setCreateError(null) - try { - const data = await generateKey.mutateAsync({ name: trimmedName }) - if (data?.key?.apiKey) { - setNewKey(data.key.apiKey) - setShowNewKeyDialog(true) - setNewKeyName('') - setCreateError(null) - setIsCreateDialogOpen(false) - } - } catch (error) { - logger.error('Failed to generate copilot API key', { error }) - setCreateError('Failed to create API key. Please check your connection and try again.') - } - } - - const copyToClipboard = (key: string) => { - navigator.clipboard.writeText(key) - setCopySuccess(true) - setTimeout(() => setCopySuccess(false), 2000) - } - - const handleDeleteKey = async () => { - if (!deleteKey) return - try { - setShowDeleteDialog(false) - const keyToDelete = deleteKey - setDeleteKey(null) - - await deleteKeyMutation.mutateAsync({ keyId: keyToDelete.id }) - } catch (error) { - logger.error('Failed to delete copilot API key', { error }) - } - } - - const formatLastUsed = (dateString?: string | null) => { - if (!dateString) return 'Never' - return formatDate(new Date(dateString)) - } - - const hasKeys = keys.length > 0 - const showEmptyState = !hasKeys - const showNoResults = searchTerm.trim() && filteredKeys.length === 0 && keys.length > 0 - - return ( - <> -
- {/* MCP Tools Section — uncomment when ready to allow users to toggle MCP servers for Mothership -
-
- MCP Tools -
- {mcpLoading ? ( -
- - -
- ) : enabledServers.length === 0 ? ( -
- No MCP servers configured. Add servers in the MCP Tools tab. -
- ) : ( -
- {enabledServers.map((server) => ( -
-
- {server.name} -

- {server.toolCount ?? 0} tool{server.toolCount === 1 ? '' : 's'} -

-
- handleToggleCopilot(server.id, checked)} - /> -
- ))} -
- )} -
- */} - - {/* Search Input and Create Button */} -
-
- - setSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
- -
- - {/* Scrollable Content */} -
- {isLoading ? ( -
- - - -
- ) : showEmptyState ? ( -
- Click "Create" above to get started -
- ) : ( -
- {filteredKeys.map((key) => ( -
-
-
- - {key.name || 'Unnamed Key'} - - - (last used: {formatLastUsed(key.lastUsed).toLowerCase()}) - -
-

- {key.displayKey} -

-
- -
- ))} - {showNoResults && ( -
- No API keys found matching "{searchTerm}" -
- )} -
- )} -
-
- - {/* Create API Key Dialog */} - - - Create new API key - -

- This key will allow access to Copilot features. Make sure to copy it after creation as - you won't be able to see it again. -

- -
-

- Enter a name for your API key to help you identify it later. -

- { - setNewKeyName(e.target.value) - if (createError) setCreateError(null) - }} - placeholder='e.g., Development, Production' - className='h-9' - autoFocus - /> - {createError && ( -

{createError}

- )} -
-
- - - - - -
-
- - {/* New API Key Dialog */} - { - setShowNewKeyDialog(open) - if (!open) { - setNewKey(null) - setCopySuccess(false) - } - }} - > - - Your API key has been created - -

- This is the only time you will see your API key.{' '} - - Copy it now and store it securely. - -

- - {newKey && ( -
-
- - {newKey} - -
- -
- )} -
-
-
- - {/* Delete Confirmation Dialog */} - - - Delete API key - -

- Deleting{' '} - - {deleteKey?.name || 'Unnamed Key'} - {' '} - will immediately revoke access for any integrations using it.{' '} - This action cannot be undone. -

-
- - - - -
-
- - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index e333e52808a..dd519fcf141 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -2,7 +2,6 @@ import { BookOpen, Card, Connections, - HexSimple, Key, KeySquare, Lock, @@ -33,7 +32,6 @@ export type SettingsSection = | 'subscription' | 'team' | 'sso' - | 'copilot' | 'mcp' | 'custom-tools' | 'skills' @@ -124,13 +122,6 @@ export const allNavigationItems: NavigationItem[] = [ section: 'system', requiresHosted: true, }, - { - id: 'copilot', - label: 'Copilot Keys', - icon: HexSimple, - section: 'system', - requiresHosted: true, - }, { id: 'inbox', label: 'Sim Mailer', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx index 5fe856c557f..08ab0b8f608 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx @@ -1,8 +1,11 @@ import { useMemo } from 'react' import { FileText } from 'lucide-react' -import { StreamingIndicator } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming' import { useThrottledValue } from '@/hooks/use-throttled-value' +function StreamingIndicator() { + return +} + interface ChatAttachment { id: string name: string 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 65f1ef2eb6a..800fdf1d1c3 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 @@ -4,7 +4,6 @@ import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/provide import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useNotificationStore } from '@/stores/notifications' -import { useCopilotStore } from '@/stores/panel' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -25,15 +24,6 @@ export const DiffControls = memo(function DiffControls() { ) ) - const { updatePreviewToolCallState } = useCopilotStore( - useCallback( - (state) => ({ - updatePreviewToolCallState: state.updatePreviewToolCallState, - }), - [] - ) - ) - const { activeWorkflowId } = useWorkflowRegistry( useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), []) ) @@ -46,81 +36,21 @@ export const DiffControls = memo(function DiffControls() { const handleAccept = useCallback(() => { logger.info('Accepting proposed changes with backup protection') - - // Resolve target toolCallId for build/edit and update to terminal success state in the copilot store - // This happens synchronously first for instant UI feedback - try { - const { toolCallsById, messages } = useCopilotStore.getState() - let id: string | undefined - outer: for (let mi = messages.length - 1; mi >= 0; mi--) { - const m = messages[mi] - if (m.role !== 'assistant' || !m.contentBlocks) continue - const blocks = m.contentBlocks as any[] - for (let bi = blocks.length - 1; bi >= 0; bi--) { - const b = blocks[bi] - if (b?.type === 'tool_call') { - const tn = b.toolCall?.name - if (tn === 'edit_workflow') { - id = b.toolCall?.id - break outer - } - } - } - } - if (!id) { - const candidates = Object.values(toolCallsById).filter((t) => t.name === 'edit_workflow') - id = candidates.length ? candidates[candidates.length - 1].id : undefined - } - if (id) updatePreviewToolCallState('accepted', id) - } catch {} - - // Accept changes without blocking the UI; errors will be logged by the store handler acceptChanges().catch((error) => { logger.error('Failed to accept changes (background):', error) }) - - // Create checkpoint in the background (fire-and-forget) so it doesn't block UI logger.info('Accept triggered; UI will update optimistically') - }, [updatePreviewToolCallState, acceptChanges]) + }, [acceptChanges]) const handleReject = useCallback(() => { logger.info('Rejecting proposed changes (optimistic)') - - // Resolve target toolCallId for build/edit and update to terminal rejected state in the copilot store - try { - const { toolCallsById, messages } = useCopilotStore.getState() - let id: string | undefined - outer: for (let mi = messages.length - 1; mi >= 0; mi--) { - const m = messages[mi] - if (m.role !== 'assistant' || !m.contentBlocks) continue - const blocks = m.contentBlocks as any[] - for (let bi = blocks.length - 1; bi >= 0; bi--) { - const b = blocks[bi] - if (b?.type === 'tool_call') { - const tn = b.toolCall?.name - if (tn === 'edit_workflow') { - id = b.toolCall?.id - break outer - } - } - } - } - if (!id) { - const candidates = Object.values(toolCallsById).filter((t) => t.name === 'edit_workflow') - id = candidates.length ? candidates[candidates.length - 1].id : undefined - } - if (id) updatePreviewToolCallState('rejected', id) - } catch {} - - // Reject changes optimistically rejectChanges().catch((error) => { logger.error('Failed to reject changes (background):', error) }) - }, [updatePreviewToolCallState, rejectChanges]) + }, [rejectChanges]) const preventZoomRef = usePreventZoom() - // Register global command to accept changes (Cmd/Ctrl + Shift + Enter) const acceptCommand = useMemo( () => createCommand({ @@ -135,7 +65,6 @@ export const DiffControls = memo(function DiffControls() { ) useRegisterGlobalCommands([acceptCommand]) - // Don't show anything if no diff is available or diff is not ready if (!hasActiveDiff || !isDiffReady) { return null } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index 834d83054f2..1cd9712306b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -8,7 +8,6 @@ import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hoo import { type Notification, type NotificationAction, - openCopilotWithMessage, sendMothershipMessage, useNotificationStore, } from '@/stores/notifications' @@ -117,11 +116,7 @@ export const Notifications = memo(function Notifications({ embedded }: Notificat switch (action.type) { case 'copilot': - if (embedded) { - sendMothershipMessage(action.message) - } else { - openCopilotWithMessage(action.message) - } + sendMothershipMessage(action.message) break case 'refresh': window.location.reload() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/checkpoint-confirmation.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/checkpoint-confirmation.tsx deleted file mode 100644 index 4475058c832..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/checkpoint-confirmation.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Button } from '@/components/emcn' - -type CheckpointConfirmationVariant = 'restore' | 'discard' - -interface CheckpointConfirmationProps { - /** Confirmation variant - 'restore' for reverting, 'discard' for edit with checkpoint options */ - variant: CheckpointConfirmationVariant - /** Whether an action is currently processing */ - isProcessing: boolean - /** Callback when cancel is clicked */ - onCancel: () => void - /** Callback when revert is clicked */ - onRevert: () => void - /** Callback when continue is clicked (only for 'discard' variant) */ - onContinue?: () => void -} - -/** - * Inline confirmation for checkpoint operations - * Supports two variants: - * - 'restore': Simple revert confirmation with warning - * - 'discard': Edit with checkpoint options (revert or continue without revert) - */ -export function CheckpointConfirmation({ - variant, - isProcessing, - onCancel, - onRevert, - onContinue, -}: CheckpointConfirmationProps) { - const isRestoreVariant = variant === 'restore' - - return ( -
-

- {isRestoreVariant ? ( - <> - Revert to checkpoint? This will restore your workflow to the state saved at this - checkpoint.{' '} - This action cannot be undone. - - ) : ( - 'Continue from a previous message?' - )} -

-
- - - {!isRestoreVariant && onContinue && ( - - )} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/index.ts deleted file mode 100644 index 612120a4f7d..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-confirmation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './checkpoint-confirmation' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx deleted file mode 100644 index d3ce40390ce..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { memo, useState } from 'react' -import { FileText, Image } from 'lucide-react' -import type { MessageFileAttachment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' - -/** - * File size units for formatting - */ -const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB'] as const - -/** - * Kilobyte multiplier - */ -const KILOBYTE = 1024 - -/** - * Props for the FileAttachmentDisplay component - */ -interface FileAttachmentDisplayProps { - /** Array of file attachments to display */ - fileAttachments: MessageFileAttachment[] -} - -/** - * FileAttachmentDisplay shows thumbnails or icons for attached files - * Displays image previews or appropriate icons based on file type - * - * @param props - Component props - * @returns Grid of file attachment thumbnails - */ -export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDisplayProps) => { - const [fileUrls, setFileUrls] = useState>({}) - const [failedImages, setFailedImages] = useState>(() => new Set()) - - /** - * Formats file size in bytes to human-readable format - * @param bytes - File size in bytes - * @returns Formatted string (e.g., "2.5 MB") - */ - const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 B' - const i = Math.floor(Math.log(bytes) / Math.log(KILOBYTE)) - return `${Math.round((bytes / KILOBYTE ** i) * 10) / 10} ${FILE_SIZE_UNITS[i]}` - } - - /** - * Returns appropriate icon based on file media type - * @param mediaType - MIME type of the file - * @returns Icon component - */ - const getFileIcon = (mediaType: string) => { - if (mediaType.startsWith('image/')) { - return - } - if (mediaType.includes('pdf')) { - return - } - if (mediaType.includes('text') || mediaType.includes('json') || mediaType.includes('xml')) { - return - } - return - } - - /** - * Gets or generates the file URL from cache - * @param file - File attachment object - * @returns URL to serve the file - */ - const getFileUrl = (file: MessageFileAttachment) => { - const cacheKey = file.key - if (fileUrls[cacheKey]) { - return fileUrls[cacheKey] - } - - const url = `/api/files/serve/${encodeURIComponent(file.key)}?context=copilot` - setFileUrls((prev) => ({ ...prev, [cacheKey]: url })) - return url - } - - /** - * Handles click on a file attachment - opens in new tab - * @param file - File attachment object - */ - const handleFileClick = (file: MessageFileAttachment) => { - const serveUrl = getFileUrl(file) - window.open(serveUrl, '_blank') - } - - /** - * Checks if a file is an image based on media type - * @param mediaType - MIME type of the file - * @returns True if file is an image - */ - const isImageFile = (mediaType: string) => { - return mediaType.startsWith('image/') - } - - /** - * Handles image loading errors - * @param fileId - ID of the file that failed to load - */ - const handleImageError = (fileId: string) => { - setFailedImages((prev) => new Set(prev).add(fileId)) - } - - return ( - <> - {fileAttachments.map((file) => ( -
handleFileClick(file)} - title={`${file.filename} (${formatFileSize(file.size)})`} - > - {isImageFile(file.media_type) && !failedImages.has(file.id) ? ( - {file.filename} handleImageError(file.id)} - /> - ) : ( -
- {getFileIcon(file.media_type)} -
- )} - - {/* Hover overlay effect */} -
-
- ))} - - ) -}) - -FileAttachmentDisplay.displayName = 'FileAttachmentDisplay' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts deleted file mode 100644 index feaf05e59ec..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './file-display' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts deleted file mode 100644 index 96b6244e926..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './checkpoint-confirmation' -export * from './file-display' -export { CopilotMarkdownRenderer } from './markdown-renderer' -export * from './smooth-streaming' -export * from './thinking-block' -export * from './usage-limit-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts deleted file mode 100644 index 62e0a916cd5..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CopilotMarkdownRenderer } from './markdown-renderer' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx deleted file mode 100644 index 2f134d03baa..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx +++ /dev/null @@ -1,331 +0,0 @@ -'use client' - -import React, { memo, useCallback, useState } from 'react' -import { Check, Copy } from 'lucide-react' -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import { Code, Tooltip } from '@/components/emcn' - -const REMARK_PLUGINS = [remarkGfm] - -/** - * Recursively extracts text content from React elements - * @param element - React node to extract text from - * @returns Concatenated text content - */ -const getTextContent = (element: React.ReactNode): string => { - if (typeof element === 'string') { - return element - } - if (typeof element === 'number') { - return String(element) - } - if (React.isValidElement(element)) { - const elementProps = element.props as { children?: React.ReactNode } - return getTextContent(elementProps.children) - } - if (Array.isArray(element)) { - return element.map(getTextContent).join('') - } - return '' -} - -/** - * Maps common language aliases to supported viewer languages - */ -const LANGUAGE_MAP: Record = { - js: 'javascript', - javascript: 'javascript', - jsx: 'javascript', - ts: 'javascript', - typescript: 'javascript', - tsx: 'javascript', - json: 'json', - python: 'python', - py: 'python', - code: 'javascript', -} - -/** - * Normalizes a language string to a supported viewer language - */ -function normalizeLanguage(lang: string): 'javascript' | 'json' | 'python' { - const normalized = (lang || '').toLowerCase() - return LANGUAGE_MAP[normalized] || 'javascript' -} - -/** - * Props for the CodeBlock component - */ -interface CodeBlockProps { - /** Code content to display */ - code: string - /** Language identifier from markdown */ - language: string -} - -/** - * CodeBlock component with isolated copy state - * Prevents full markdown re-renders when copy button is clicked - */ -const CodeBlock = memo(function CodeBlock({ code, language }: CodeBlockProps) { - const [copied, setCopied] = useState(false) - - const handleCopy = useCallback(() => { - if (code) { - navigator.clipboard.writeText(code) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - }, [code]) - - const viewerLanguage = normalizeLanguage(language) - const displayLanguage = language === 'code' ? viewerLanguage : language - - return ( -
-
- {displayLanguage} - -
- -
- ) -}) - -/** - * Link component with hover preview tooltip - */ -const LinkWithPreview = memo(function LinkWithPreview({ - href, - children, -}: { - href: string - children: React.ReactNode -}) { - return ( - - - - {children} - - - - {href} - - - ) -}) - -/** - * Props for the CopilotMarkdownRenderer component - */ -interface CopilotMarkdownRendererProps { - /** Markdown content to render */ - content: string -} - -/** - * Static markdown component definitions - optimized for LLM chat spacing - * Tighter spacing compared to traditional prose for better chat UX - */ -const markdownComponents = { - p: ({ children }: React.HTMLAttributes) => ( -

- {children} -

- ), - - h1: ({ children }: React.HTMLAttributes) => ( -

- {children} -

- ), - h2: ({ children }: React.HTMLAttributes) => ( -

- {children} -

- ), - h3: ({ children }: React.HTMLAttributes) => ( -

- {children} -

- ), - h4: ({ children }: React.HTMLAttributes) => ( -

- {children} -

- ), - - ul: ({ children }: React.HTMLAttributes) => ( -
    - {children} -
- ), - ol: ({ children }: React.HTMLAttributes) => ( -
    - {children} -
- ), - li: ({ children }: React.LiHTMLAttributes) => ( -
  • - {children} -
  • - ), - - pre: ({ children }: React.HTMLAttributes) => { - let codeContent: React.ReactNode = children - let language = 'code' - - if ( - React.isValidElement<{ className?: string; children?: React.ReactNode }>(children) && - children.type === 'code' - ) { - const childElement = children as React.ReactElement<{ - className?: string - children?: React.ReactNode - }> - codeContent = childElement.props.children - language = childElement.props.className?.replace('language-', '') || 'code' - } - - let actualCodeText = '' - if (typeof codeContent === 'string') { - actualCodeText = codeContent - } else if (React.isValidElement(codeContent)) { - actualCodeText = getTextContent(codeContent) - } else if (Array.isArray(codeContent)) { - actualCodeText = codeContent - .map((child) => - typeof child === 'string' - ? child - : React.isValidElement(child) - ? getTextContent(child) - : '' - ) - .join('') - } else { - actualCodeText = String(codeContent || '') - } - - return - }, - - code: ({ - className, - children, - ...props - }: React.HTMLAttributes & { className?: string }) => ( - - {children} - - ), - - strong: ({ children }: React.HTMLAttributes) => ( - {children} - ), - b: ({ children }: React.HTMLAttributes) => ( - {children} - ), - em: ({ children }: React.HTMLAttributes) => ( - {children} - ), - i: ({ children }: React.HTMLAttributes) => ( - {children} - ), - - blockquote: ({ children }: React.HTMLAttributes) => ( -
    - {children} -
    - ), - - hr: () =>
    , - - a: ({ href, children }: React.AnchorHTMLAttributes) => ( - {children} - ), - - table: ({ children }: React.TableHTMLAttributes) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }: React.HTMLAttributes) => ( - {children} - ), - tbody: ({ children }: React.HTMLAttributes) => ( - {children} - ), - tr: ({ children }: React.HTMLAttributes) => ( - {children} - ), - th: ({ children }: React.ThHTMLAttributes) => ( - - {children} - - ), - td: ({ children }: React.TdHTMLAttributes) => ( - - {children} - - ), - - img: ({ src, alt, ...props }: React.ImgHTMLAttributes) => ( - {alt - ), -} - -/** - * CopilotMarkdownRenderer renders markdown content with custom styling - * Optimized for LLM chat: tight spacing, memoized components, isolated state - * - * @param props - Component props - * @returns Rendered markdown content - */ -function CopilotMarkdownRenderer({ content }: CopilotMarkdownRendererProps) { - return ( -
    - - {content} - -
    - ) -} - -export default memo(CopilotMarkdownRenderer) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts deleted file mode 100644 index 96c0d8364f7..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './smooth-streaming' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx deleted file mode 100644 index c0965808e8d..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { memo, useEffect, useRef, useState } from 'react' -import { cn } from '@/lib/core/utils/cn' -import { CopilotMarkdownRenderer } from '../markdown-renderer' - -/** Character animation delay in milliseconds */ -const CHARACTER_DELAY = 3 - -/** Props for the StreamingIndicator component */ -interface StreamingIndicatorProps { - /** Optional class name for layout adjustments */ - className?: string -} - -/** Shows animated dots during message streaming when no content has arrived */ -export const StreamingIndicator = memo(({ className }: StreamingIndicatorProps) => ( -
    -
    -
    -
    -
    -
    -
    -)) - -StreamingIndicator.displayName = 'StreamingIndicator' - -/** Props for the SmoothStreamingText component */ -interface SmoothStreamingTextProps { - /** Content to display with streaming animation */ - content: string - /** Whether the content is actively streaming */ - isStreaming: boolean -} - -/** Displays text with character-by-character animation for smooth streaming */ -export const SmoothStreamingText = memo( - ({ content, isStreaming }: SmoothStreamingTextProps) => { - const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content)) - const contentRef = useRef(content) - const timeoutRef = useRef(null) - const indexRef = useRef(isStreaming ? 0 : content.length) - const isAnimatingRef = useRef(false) - - useEffect(() => { - contentRef.current = content - - if (content.length === 0) { - setDisplayedContent('') - indexRef.current = 0 - return - } - - if (isStreaming) { - if (indexRef.current < content.length) { - const animateText = () => { - const currentContent = contentRef.current - const currentIndex = indexRef.current - - if (currentIndex < currentContent.length) { - const newDisplayed = currentContent.slice(0, currentIndex + 1) - setDisplayedContent(newDisplayed) - indexRef.current = currentIndex + 1 - timeoutRef.current = setTimeout(animateText, CHARACTER_DELAY) - } else { - isAnimatingRef.current = false - } - } - - if (!isAnimatingRef.current) { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - isAnimatingRef.current = true - animateText() - } - } - } else { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - setDisplayedContent(content) - indexRef.current = content.length - isAnimatingRef.current = false - } - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - isAnimatingRef.current = false - } - }, [content, isStreaming]) - - return ( -
    - -
    - ) - }, - (prevProps, nextProps) => { - return ( - prevProps.content === nextProps.content && prevProps.isStreaming === nextProps.isStreaming - ) - } -) - -SmoothStreamingText.displayName = 'SmoothStreamingText' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts deleted file mode 100644 index 515f72bb041..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx deleted file mode 100644 index 3c95d83d433..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx +++ /dev/null @@ -1,364 +0,0 @@ -'use client' - -import { memo, useEffect, useMemo, useRef, useState } from 'react' -import clsx from 'clsx' -import { ChevronUp } from 'lucide-react' -import { formatDuration } from '@/lib/core/utils/formatting' -import { CopilotMarkdownRenderer } from '../markdown-renderer' - -/** Removes thinking tags (raw or escaped) and special tags from streamed content */ -function stripThinkingTags(text: string): string { - return text - .replace(/<\/?thinking[^>]*>/gi, '') - .replace(/<\/?thinking[^&]*>/gi, '') - .replace(/[\s\S]*?<\/options>/gi, '') - .replace(/[\s\S]*$/gi, '') - .replace(/[\s\S]*?<\/plan>/gi, '') - .replace(/[\s\S]*$/gi, '') - .trim() -} - -/** Interval for auto-scroll during streaming (ms) */ -const SCROLL_INTERVAL = 50 - -/** Timer update interval in milliseconds */ -const TIMER_UPDATE_INTERVAL = 100 - -/** Thinking text streaming delay - faster than main text */ -const THINKING_DELAY = 0.5 -const THINKING_CHARS_PER_FRAME = 3 - -/** Props for the SmoothThinkingText component */ -interface SmoothThinkingTextProps { - content: string - isStreaming: boolean -} - -/** - * Renders thinking content with fast streaming animation. - */ -const SmoothThinkingText = memo( - ({ content, isStreaming }: SmoothThinkingTextProps) => { - const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content)) - const contentRef = useRef(content) - const textRef = useRef(null) - const rafRef = useRef(null) - const indexRef = useRef(isStreaming ? 0 : content.length) - const lastFrameTimeRef = useRef(0) - const isAnimatingRef = useRef(false) - - useEffect(() => { - contentRef.current = content - - if (content.length === 0) { - setDisplayedContent('') - indexRef.current = 0 - return - } - - if (isStreaming) { - if (indexRef.current < content.length && !isAnimatingRef.current) { - isAnimatingRef.current = true - lastFrameTimeRef.current = performance.now() - - const animateText = (timestamp: number) => { - const currentContent = contentRef.current - const currentIndex = indexRef.current - const elapsed = timestamp - lastFrameTimeRef.current - - if (elapsed >= THINKING_DELAY) { - if (currentIndex < currentContent.length) { - const newIndex = Math.min( - currentIndex + THINKING_CHARS_PER_FRAME, - currentContent.length - ) - const newDisplayed = currentContent.slice(0, newIndex) - setDisplayedContent(newDisplayed) - indexRef.current = newIndex - lastFrameTimeRef.current = timestamp - } - } - - if (indexRef.current < currentContent.length) { - rafRef.current = requestAnimationFrame(animateText) - } else { - isAnimatingRef.current = false - } - } - - rafRef.current = requestAnimationFrame(animateText) - } - } else { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - setDisplayedContent(content) - indexRef.current = content.length - isAnimatingRef.current = false - } - - return () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - isAnimatingRef.current = false - } - }, [content, isStreaming]) - - return ( -
    - -
    - ) - }, - (prevProps, nextProps) => { - return ( - prevProps.content === nextProps.content && prevProps.isStreaming === nextProps.isStreaming - ) - } -) - -SmoothThinkingText.displayName = 'SmoothThinkingText' - -/** Props for the ThinkingBlock component */ -interface ThinkingBlockProps { - /** Content of the thinking block */ - content: string - /** Whether the block is currently streaming */ - isStreaming?: boolean - /** Whether there are more content blocks after this one (e.g., tool calls) */ - hasFollowingContent?: boolean - /** Custom label for the thinking block (e.g., "Thinking", "Exploring"). Defaults to "Thought" */ - label?: string - /** Whether special tags (plan, options) are present - triggers collapse */ - hasSpecialTags?: boolean -} - -/** - * Displays AI reasoning/thinking process with collapsible content and duration timer. - * Auto-expands during streaming and collapses when complete. - */ -export function ThinkingBlock({ - content, - isStreaming = false, - hasFollowingContent = false, - label = 'Thought', - hasSpecialTags = false, -}: ThinkingBlockProps) { - const cleanContent = useMemo(() => stripThinkingTags(content || ''), [content]) - - const [isExpanded, setIsExpanded] = useState(false) - const [duration, setDuration] = useState(0) - const [userHasScrolledAway, setUserHasScrolledAway] = useState(false) - const userCollapsedRef = useRef(false) - const scrollContainerRef = useRef(null) - const startTimeRef = useRef(Date.now()) - const lastScrollTopRef = useRef(0) - const programmaticScrollRef = useRef(false) - - /** Auto-expands during streaming, auto-collapses when streaming ends or following content arrives */ - useEffect(() => { - if (!isStreaming || hasFollowingContent || hasSpecialTags) { - setIsExpanded(false) - userCollapsedRef.current = false - setUserHasScrolledAway(false) - return - } - - if (!userCollapsedRef.current && cleanContent && cleanContent.length > 0) { - setIsExpanded(true) - } - }, [isStreaming, cleanContent, hasFollowingContent, hasSpecialTags]) - - useEffect(() => { - if (isStreaming && !hasFollowingContent) { - startTimeRef.current = Date.now() - setDuration(0) - setUserHasScrolledAway(false) - } - }, [isStreaming, hasFollowingContent]) - - useEffect(() => { - if (!isStreaming || hasFollowingContent) return - - const interval = setInterval(() => { - setDuration(Date.now() - startTimeRef.current) - }, TIMER_UPDATE_INTERVAL) - - return () => clearInterval(interval) - }, [isStreaming, hasFollowingContent]) - - useEffect(() => { - const container = scrollContainerRef.current - if (!container || !isExpanded) return - - const handleScroll = () => { - if (programmaticScrollRef.current) return - - const { scrollTop, scrollHeight, clientHeight } = container - const distanceFromBottom = scrollHeight - scrollTop - clientHeight - const isNearBottom = distanceFromBottom <= 20 - - const delta = scrollTop - lastScrollTopRef.current - const movedUp = delta < -1 - - if (movedUp && !isNearBottom) { - setUserHasScrolledAway(true) - } - - if (userHasScrolledAway && isNearBottom && delta > 10) { - setUserHasScrolledAway(false) - } - - lastScrollTopRef.current = scrollTop - } - - container.addEventListener('scroll', handleScroll, { passive: true }) - lastScrollTopRef.current = container.scrollTop - - return () => container.removeEventListener('scroll', handleScroll) - }, [isExpanded, userHasScrolledAway]) - - useEffect(() => { - if (!isStreaming || !isExpanded || userHasScrolledAway) return - - const intervalId = window.setInterval(() => { - const container = scrollContainerRef.current - if (!container) return - - programmaticScrollRef.current = true - container.scrollTo({ - top: container.scrollHeight, - behavior: 'auto', - }) - window.setTimeout(() => { - programmaticScrollRef.current = false - }, 16) - }, SCROLL_INTERVAL) - - return () => window.clearInterval(intervalId) - }, [isStreaming, isExpanded, userHasScrolledAway]) - - const hasContent = cleanContent.length > 0 - const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags - // Round to nearest second (minimum 1s) to match original behavior - const roundedMs = Math.max(1000, Math.round(duration / 1000) * 1000) - const durationText = `${label} for ${formatDuration(roundedMs)}` - - const getStreamingLabel = (lbl: string) => { - if (lbl === 'Thought') return 'Thinking' - if (lbl.endsWith('ed')) return `${lbl.slice(0, -2)}ing` - return lbl - } - const streamingLabel = getStreamingLabel(label) - - if (!isThinkingDone) { - return ( -
    - - - -
    - -
    -
    - ) - } - - return ( -
    - - -
    -
    - -
    -
    -
    - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts deleted file mode 100644 index 0d34dbbe5f1..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './usage-limit-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx deleted file mode 100644 index 17dd3ea5ebd..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Loader2 } from 'lucide-react' -import { Button } from '@/components/emcn' -import { formatCredits } from '@/lib/billing/credits/conversion' -import { canEditUsageLimit } from '@/lib/billing/subscriptions/utils' -import { getEnv, isTruthy } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' -import { useSubscriptionData, useUpdateUsageLimit } from '@/hooks/queries/subscription' -import { useSettingsNavigation } from '@/hooks/use-settings-navigation' -import { useCopilotStore } from '@/stores/panel' - -const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) -const LIMIT_INCREMENTS = [0, 50, 100] as const - -function roundUpToNearest50(value: number): number { - return Math.ceil(value / 50) * 50 -} - -export function UsageLimitActions() { - const { navigateToSettings } = useSettingsNavigation() - const { data: subscriptionData } = useSubscriptionData({ enabled: isBillingEnabled }) - const updateUsageLimitMutation = useUpdateUsageLimit() - - const subscription = subscriptionData?.data - const canEdit = subscription ? canEditUsageLimit(subscription) : false - - const [selectedAmount, setSelectedAmount] = useState(null) - const [isHidden, setIsHidden] = useState(false) - - const currentLimit = subscription?.usageLimit ?? 0 - const baseLimit = roundUpToNearest50(currentLimit) || 50 - const limitOptions = LIMIT_INCREMENTS.map((increment) => baseLimit + increment) - - const handleUpdateLimit = async (newLimit: number) => { - setSelectedAmount(newLimit) - try { - await updateUsageLimitMutation.mutateAsync({ limit: newLimit }) - - setIsHidden(true) - - const { messages, sendMessage } = useCopilotStore.getState() - const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user') - - if (lastUserMessage) { - const filteredMessages = messages.filter( - (m) => !(m.role === 'assistant' && m.errorType === 'usage_limit') - ) - useCopilotStore.setState({ messages: filteredMessages }) - - await sendMessage(lastUserMessage.content, { - fileAttachments: lastUserMessage.fileAttachments, - contexts: lastUserMessage.contexts, - messageId: lastUserMessage.id, - }) - } - } catch { - setIsHidden(false) - } finally { - setSelectedAmount(null) - } - } - - const handleNavigateToUpgrade = () => { - if (isHosted) { - navigateToSettings({ section: 'subscription' }) - } else { - window.open('https://www.sim.ai', '_blank') - } - } - - if (isHidden) { - return null - } - - if (!isHosted || !canEdit) { - return ( - - ) - } - - return ( - <> - {limitOptions.map((limit) => { - const isLoading = updateUsageLimitMutation.isPending && selectedAmount === limit - const isDisabled = updateUsageLimitMutation.isPending - - return ( - - ) - })} - - ) -} 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 deleted file mode 100644 index 0fc34449dec..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx +++ /dev/null @@ -1,568 +0,0 @@ -'use client' - -import { type FC, memo, useCallback, useMemo, useRef, useState } from 'react' -import { RotateCcw } from 'lucide-react' -import { Button } from '@/components/emcn' -import { MessageActions } from '@/app/workspace/[workspaceId]/components' -import { - OptionsSelector, - parseSpecialTags, - ToolCall, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components' -import { - CheckpointConfirmation, - FileAttachmentDisplay, - SmoothStreamingText, - StreamingIndicator, - ThinkingBlock, - UsageLimitActions, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components' -import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' -import { - useCheckpointManagement, - useMessageEditing, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks' -import { UserInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input' -import { buildMentionHighlightNodes } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' -import type { CopilotMessage as CopilotMessageType } from '@/stores/panel' -import { useCopilotStore } from '@/stores/panel' - -/** - * Props for the CopilotMessage component - */ -interface CopilotMessageProps { - /** Message object containing content and metadata */ - message: CopilotMessageType - /** Whether the message is currently streaming */ - isStreaming?: boolean - /** Width of the panel in pixels */ - panelWidth?: number - /** Whether the message should appear dimmed */ - isDimmed?: boolean - /** Number of checkpoints for this message */ - checkpointCount?: number - /** Callback when edit mode changes */ - onEditModeChange?: (isEditing: boolean, cancelCallback?: () => void) => void - /** Callback when revert mode changes */ - onRevertModeChange?: (isReverting: boolean) => void - /** Whether this is the last message in the conversation */ - isLastMessage?: boolean -} - -/** - * CopilotMessage component displays individual chat messages - * Handles both user and assistant messages with different rendering and interactions - * Supports editing, checkpoints, feedback, and file attachments - * - * @param props - Component props - * @returns Message component with appropriate role-based rendering - */ -const CopilotMessage: FC = memo( - ({ - message, - isStreaming, - panelWidth = 308, - isDimmed = false, - checkpointCount = 0, - onEditModeChange, - onRevertModeChange, - isLastMessage = false, - }) => { - const isUser = message.role === 'user' - const isAssistant = message.role === 'assistant' - - const { - messageCheckpoints: allMessageCheckpoints, - messages, - isSendingMessage, - abortMessage, - mode, - setMode, - isAborting, - } = useCopilotStore() - - const maskCredentialValue = useCopilotStore((s) => s.maskCredentialValue) - - const messageCheckpoints = isUser ? allMessageCheckpoints[message.id] || [] : [] - const hasCheckpoints = messageCheckpoints.length > 0 && messageCheckpoints.some((cp) => cp?.id) - - const isLastUserMessage = useMemo(() => { - if (!isUser) return false - const userMessages = messages.filter((m) => m.role === 'user') - return userMessages.length > 0 && userMessages[userMessages.length - 1]?.id === message.id - }, [isUser, messages, message.id]) - - const [isHoveringMessage, setIsHoveringMessage] = useState(false) - const cancelEditRef = useRef<(() => void) | null>(null) - - const { - showRestoreConfirmation, - showCheckpointDiscardModal, - isReverting, - isProcessingDiscard, - pendingEditRef, - setShowCheckpointDiscardModal, - handleRevertToCheckpoint, - handleConfirmRevert, - handleCancelRevert, - handleCancelCheckpointDiscard, - handleContinueWithoutRevert, - handleContinueAndRevert, - } = useCheckpointManagement( - message, - messages, - messageCheckpoints, - onRevertModeChange, - onEditModeChange, - () => cancelEditRef.current?.() - ) - - const { - isEditMode, - isExpanded, - editedContent, - needsExpansion, - editContainerRef, - messageContentRef, - userInputRef, - setEditedContent, - handleCancelEdit, - handleMessageClick, - handleSubmitEdit, - } = useMessageEditing({ - message, - messages, - isLastUserMessage, - hasCheckpoints, - onEditModeChange: (isEditing) => { - onEditModeChange?.(isEditing, handleCancelEdit) - }, - disableDocumentClickOutside: true, - showCheckpointDiscardModal, - setShowCheckpointDiscardModal, - pendingEditRef, - }) - - cancelEditRef.current = handleCancelEdit - - const cleanTextContent = useMemo(() => { - if (!message.content) return '' - return message.content.replace(/\n{3,}/g, '\n\n') - }, [message.content]) - - const parsedTags = useMemo(() => { - if (isUser) return null - - if (message.content) { - const parsed = parseSpecialTags(message.content) - if (parsed.options || parsed.plan) return parsed - } - - if (message.contentBlocks && message.contentBlocks.length > 0) { - for (const block of message.contentBlocks) { - if (block.type === 'text' && block.content) { - const parsed = parseSpecialTags(block.content) - if (parsed.options || parsed.plan) return parsed - } - } - } - - return null - }, [message.content, message.contentBlocks, isUser]) - - const selectedOptionKey = useMemo(() => { - if (!parsedTags?.options || isStreaming) return null - - const currentIndex = messages.findIndex((m) => m.id === message.id) - if (currentIndex === -1 || currentIndex >= messages.length - 1) return null - - const nextMessage = messages[currentIndex + 1] - if (!nextMessage || nextMessage.role !== 'user') return null - - const nextContent = nextMessage.content?.trim() - if (!nextContent) return null - - for (const [key, option] of Object.entries(parsedTags.options)) { - const optionTitle = typeof option === 'string' ? option : option.title - if (nextContent === optionTitle) { - return key - } - } - - return null - }, [parsedTags?.options, messages, message.id, isStreaming]) - - const sendMessage = useCopilotStore((s) => s.sendMessage) - - const handleOptionSelect = useCallback( - (_optionKey: string, optionText: string) => { - sendMessage(optionText) - }, - [sendMessage] - ) - - const isActivelyStreaming = isLastMessage && isStreaming - - const memoizedContentBlocks = useMemo(() => { - if (!message.contentBlocks || message.contentBlocks.length === 0) { - return null - } - - return message.contentBlocks.map((block, index) => { - if (block.type === 'text') { - const isLastTextBlock = - index === message.contentBlocks!.length - 1 && block.type === 'text' - const parsed = parseSpecialTags(block.content ?? '') - // Mask credential IDs in the displayed content - const cleanBlockContent = maskCredentialValue( - parsed.cleanContent.replace(/\n{3,}/g, '\n\n') - ) - - if (!cleanBlockContent.trim()) return null - - const shouldUseSmoothing = isActivelyStreaming && isLastTextBlock - const blockKey = `text-${index}-${block.timestamp || index}` - - return ( -
    - {shouldUseSmoothing ? ( - - ) : ( - - )} -
    - ) - } - if (block.type === 'thinking') { - const hasFollowingContent = index < message.contentBlocks!.length - 1 - const hasSpecialTags = !!(parsedTags?.options || parsedTags?.plan) - const blockKey = `thinking-${index}-${block.timestamp || index}` - - return ( -
    - -
    - ) - } - if (block.type === 'tool_call' && block.toolCall) { - const blockKey = `tool-${block.toolCall.id}` - - return ( -
    - -
    - ) - } - return null - }) - }, [message.contentBlocks, isActivelyStreaming, parsedTags, isLastMessage]) - - if (isUser) { - return ( -
    - {isEditMode ? ( -
    - { - if (isSendingMessage && isLastUserMessage) { - abortMessage() - } - }} - isLoading={isSendingMessage && isLastUserMessage} - isAborting={isAborting} - disabled={showCheckpointDiscardModal} - value={editedContent} - onChange={setEditedContent} - placeholder='Edit your message...' - mode={mode} - onModeChange={setMode} - panelWidth={panelWidth} - clearOnSubmit={false} - initialContexts={message.contexts} - /> - - {/* Inline checkpoint confirmation - shown below input in edit mode */} - {showCheckpointDiscardModal && ( - - )} -
    - ) : ( -
    - {/* File attachments displayed above the message box */} - {message.fileAttachments && message.fileAttachments.length > 0 && ( -
    - -
    - )} - - {/* Message box - styled like input, clickable to edit */} -
    setIsHoveringMessage(true)} - onMouseLeave={() => setIsHoveringMessage(false)} - className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-4)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-7)] hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]' - > -
    - {buildMentionHighlightNodes( - message.content || '', - message.contexts || [], - (token, key) => ( - - {token} - - ) - )} -
    - - {/* Gradient fade when truncated - applies to entire message box */} - {!isExpanded && needsExpansion && ( -
    - )} - - {/* Abort button when hovering and response is generating (only on last user message) */} - {isSendingMessage && isHoveringMessage && isLastUserMessage && ( -
    - -
    - )} - - {/* Revert button on hover (only when has checkpoints and not generating) */} - {!isSendingMessage && hasCheckpoints && isHoveringMessage && ( -
    - -
    - )} -
    -
    - )} - - {/* Inline restore checkpoint confirmation */} - {showRestoreConfirmation && ( - - )} -
    - ) - } - - if (isAssistant) { - return ( -
    - {!isStreaming && (message.content || message.contentBlocks?.length) && ( -
    - -
    - )} -
    - {/* Content blocks in chronological order */} - {memoizedContentBlocks || (isStreaming &&
    )} - - {isStreaming && } - - {message.errorType === 'usage_limit' && ( -
    - -
    - )} - - {/* Citations if available */} - {message.citations && message.citations.length > 0 && ( -
    -
    Sources:
    -
    - {message.citations.map((citation) => ( - - {citation.title} - - ))} -
    -
    - )} - - {/* Options selector when agent presents choices - streams in but disabled until complete */} - {/* Disabled for previous messages (not isLastMessage) so only the latest options are interactive */} - {parsedTags?.options && Object.keys(parsedTags.options).length > 0 && ( - - )} -
    -
    - ) - } - - return null - }, - (prevProps, nextProps) => { - const prevMessage = prevProps.message - const nextMessage = nextProps.message - - if (prevMessage.id !== nextMessage.id) return false - if (prevProps.isStreaming !== nextProps.isStreaming) return false - if (prevProps.isDimmed !== nextProps.isDimmed) return false - if (prevProps.panelWidth !== nextProps.panelWidth) return false - if (prevProps.checkpointCount !== nextProps.checkpointCount) return false - if (prevProps.isLastMessage !== nextProps.isLastMessage) return false - - if (nextProps.isStreaming) { - const prevBlocks = prevMessage.contentBlocks || [] - const nextBlocks = nextMessage.contentBlocks || [] - - if (prevBlocks.length !== nextBlocks.length) return false - - const getLastBlockContent = (blocks: any[], type: 'text' | 'thinking'): string | null => { - for (let i = blocks.length - 1; i >= 0; i--) { - const block = blocks[i] - if (block && block.type === type) { - return (block as any).content ?? '' - } - } - return null - } - - const prevLastTextContent = getLastBlockContent(prevBlocks as any[], 'text') - const nextLastTextContent = getLastBlockContent(nextBlocks as any[], 'text') - if ( - prevLastTextContent !== null && - nextLastTextContent !== null && - prevLastTextContent !== nextLastTextContent - ) { - return false - } - - const prevLastThinkingContent = getLastBlockContent(prevBlocks as any[], 'thinking') - const nextLastThinkingContent = getLastBlockContent(nextBlocks as any[], 'thinking') - if ( - prevLastThinkingContent !== null && - nextLastThinkingContent !== null && - prevLastThinkingContent !== nextLastThinkingContent - ) { - return false - } - - const prevToolCalls = prevMessage.toolCalls || [] - const nextToolCalls = nextMessage.toolCalls || [] - - if (prevToolCalls.length !== nextToolCalls.length) return false - - for (let i = 0; i < nextToolCalls.length; i++) { - if (prevToolCalls[i]?.state !== nextToolCalls[i]?.state) return false - } - - return true - } - - if ( - prevMessage.content !== nextMessage.content || - prevMessage.role !== nextMessage.role || - (prevMessage.toolCalls?.length || 0) !== (nextMessage.toolCalls?.length || 0) || - (prevMessage.contentBlocks?.length || 0) !== (nextMessage.contentBlocks?.length || 0) - ) { - return false - } - - const prevToolCalls = prevMessage.toolCalls || [] - const nextToolCalls = nextMessage.toolCalls || [] - for (let i = 0; i < nextToolCalls.length; i++) { - if (prevToolCalls[i]?.state !== nextToolCalls[i]?.state) return false - } - - const prevContentBlocks = prevMessage.contentBlocks || [] - const nextContentBlocks = nextMessage.contentBlocks || [] - for (let i = 0; i < nextContentBlocks.length; i++) { - const prevBlock = prevContentBlocks[i] - const nextBlock = nextContentBlocks[i] - if ( - prevBlock?.type === 'tool_call' && - nextBlock?.type === 'tool_call' && - prevBlock.toolCall?.state !== nextBlock.toolCall?.state - ) { - return false - } - } - - return true - } -) - -CopilotMessage.displayName = 'CopilotMessage' - -export { CopilotMessage } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/index.ts deleted file mode 100644 index 2c501a1c9ab..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useCheckpointManagement } from './use-checkpoint-management' -export { useMessageEditing } from './use-message-editing' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts deleted file mode 100644 index 6605582e1bb..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-checkpoint-management.ts +++ /dev/null @@ -1,264 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' -import type { CopilotMessage } from '@/stores/panel' -import { useCopilotStore } from '@/stores/panel' - -const logger = createLogger('useCheckpointManagement') - -/** - * Custom hook to handle checkpoint-related operations for messages - * - * @param message - The copilot message - * @param messages - Array of all messages in the chat - * @param messageCheckpoints - Checkpoints for this message - * @param onRevertModeChange - Callback for revert mode changes - * @param onEditModeChange - Callback for edit mode changes - * @param onCancelEdit - Callback when edit is cancelled - * @returns Checkpoint management utilities - */ -export function useCheckpointManagement( - message: CopilotMessage, - messages: CopilotMessage[], - messageCheckpoints: any[], - onRevertModeChange?: (isReverting: boolean) => void, - onEditModeChange?: (isEditing: boolean) => void, - onCancelEdit?: () => void -) { - const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false) - const [showCheckpointDiscardModal, setShowCheckpointDiscardModal] = useState(false) - const [isReverting, setIsReverting] = useState(false) - const [isProcessingDiscard, setIsProcessingDiscard] = useState(false) - const pendingEditRef = useRef<{ - message: string - fileAttachments?: any[] - contexts?: any[] - } | null>(null) - - const { revertToCheckpoint, currentChat } = useCopilotStore() - - /** Initiates checkpoint revert confirmation */ - const handleRevertToCheckpoint = useCallback(() => { - setShowRestoreConfirmation(true) - onRevertModeChange?.(true) - }, [onRevertModeChange]) - - /** Confirms and executes checkpoint revert */ - const handleConfirmRevert = useCallback(async () => { - if (messageCheckpoints.length > 0) { - const latestCheckpoint = messageCheckpoints[0] - setIsReverting(true) - try { - await revertToCheckpoint(latestCheckpoint.id) - - const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState() - const updatedCheckpoints = { - ...currentCheckpoints, - [message.id]: [], - } - useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints }) - - const currentMessages = messages - const revertIndex = currentMessages.findIndex((m) => m.id === message.id) - if (revertIndex !== -1) { - const truncatedMessages = currentMessages.slice(0, revertIndex + 1) - useCopilotStore.setState({ messages: truncatedMessages }) - - if (currentChat?.id) { - try { - await fetch('/api/copilot/chat/update-messages', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chatId: currentChat.id, - messages: truncatedMessages.map((m) => ({ - id: m.id, - role: m.role, - content: m.content, - timestamp: m.timestamp, - ...(m.contentBlocks && { contentBlocks: m.contentBlocks }), - ...(m.fileAttachments && { fileAttachments: m.fileAttachments }), - ...((m as any).contexts && { contexts: (m as any).contexts }), - })), - }), - }) - } catch (error) { - logger.error('Failed to update messages in DB after revert:', error) - } - } - } - - setShowRestoreConfirmation(false) - onRevertModeChange?.(false) - - logger.info('Checkpoint reverted and removed from message', { - messageId: message.id, - checkpointId: latestCheckpoint.id, - }) - } catch (error) { - logger.error('Failed to revert to checkpoint:', error) - setShowRestoreConfirmation(false) - onRevertModeChange?.(false) - } finally { - setIsReverting(false) - } - } - }, [ - messageCheckpoints, - revertToCheckpoint, - message.id, - messages, - currentChat, - onRevertModeChange, - ]) - - /** Cancels checkpoint revert */ - const handleCancelRevert = useCallback(() => { - setShowRestoreConfirmation(false) - onRevertModeChange?.(false) - }, [onRevertModeChange]) - - /** Reverts to checkpoint then proceeds with pending edit */ - const handleContinueAndRevert = useCallback(async () => { - setIsProcessingDiscard(true) - try { - if (messageCheckpoints.length > 0) { - const latestCheckpoint = messageCheckpoints[0] - try { - await revertToCheckpoint(latestCheckpoint.id) - - const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState() - const updatedCheckpoints = { - ...currentCheckpoints, - [message.id]: [], - } - useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints }) - - logger.info('Reverted to checkpoint before editing message', { - messageId: message.id, - checkpointId: latestCheckpoint.id, - }) - } catch (error) { - logger.error('Failed to revert to checkpoint:', error) - } - } - - setShowCheckpointDiscardModal(false) - onEditModeChange?.(false) - onCancelEdit?.() - - const { sendMessage } = useCopilotStore.getState() - if (pendingEditRef.current) { - const { message: msg, fileAttachments, contexts } = pendingEditRef.current - const editIndex = messages.findIndex((m) => m.id === message.id) - if (editIndex !== -1) { - const truncatedMessages = messages.slice(0, editIndex) - const updatedMessage = { - ...message, - content: msg, - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, - } - useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] }) - - await sendMessage(msg, { - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, - messageId: message.id, - queueIfBusy: false, - }) - } - pendingEditRef.current = null - } - } finally { - setIsProcessingDiscard(false) - } - }, [messageCheckpoints, revertToCheckpoint, message, messages, onEditModeChange, onCancelEdit]) - - /** Cancels checkpoint discard and clears pending edit */ - const handleCancelCheckpointDiscard = useCallback(() => { - setShowCheckpointDiscardModal(false) - onEditModeChange?.(false) - onCancelEdit?.() - pendingEditRef.current = null - }, [onEditModeChange, onCancelEdit]) - - /** Continues with edit without reverting checkpoint */ - const handleContinueWithoutRevert = useCallback(async () => { - setShowCheckpointDiscardModal(false) - onEditModeChange?.(false) - onCancelEdit?.() - - if (pendingEditRef.current) { - const { message: msg, fileAttachments, contexts } = pendingEditRef.current - const { sendMessage } = useCopilotStore.getState() - const editIndex = messages.findIndex((m) => m.id === message.id) - if (editIndex !== -1) { - const truncatedMessages = messages.slice(0, editIndex) - const updatedMessage = { - ...message, - content: msg, - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, - } - useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] }) - - await sendMessage(msg, { - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, - messageId: message.id, - queueIfBusy: false, - }) - } - pendingEditRef.current = null - } - }, [message, messages, onEditModeChange, onCancelEdit]) - - /** Handles keyboard events for confirmation dialogs */ - useEffect(() => { - const isActive = showRestoreConfirmation || showCheckpointDiscardModal - if (!isActive) return - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.defaultPrevented) return - - if (event.key === 'Escape') { - if (showRestoreConfirmation) handleCancelRevert() - else handleCancelCheckpointDiscard() - } else if (event.key === 'Enter') { - event.preventDefault() - if (showRestoreConfirmation) handleConfirmRevert() - else handleContinueAndRevert() - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [ - showRestoreConfirmation, - showCheckpointDiscardModal, - handleCancelRevert, - handleConfirmRevert, - handleCancelCheckpointDiscard, - handleContinueAndRevert, - ]) - - return { - // State - showRestoreConfirmation, - showCheckpointDiscardModal, - isReverting, - isProcessingDiscard, - pendingEditRef, - - // Operations - setShowCheckpointDiscardModal, - handleRevertToCheckpoint, - handleConfirmRevert, - handleCancelRevert, - handleCancelCheckpointDiscard, - handleContinueWithoutRevert, - handleContinueAndRevert, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts deleted file mode 100644 index 48427779714..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts +++ /dev/null @@ -1,256 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' -import type { ChatContext, CopilotMessage, MessageFileAttachment } from '@/stores/panel' -import { useCopilotStore } from '@/stores/panel' - -const logger = createLogger('useMessageEditing') - -/** Ref interface for UserInput component */ -interface UserInputRef { - focus: () => void -} - -/** Message truncation height in pixels */ -const MESSAGE_TRUNCATION_HEIGHT = 60 - -/** Delay before attaching click-outside listener to avoid immediate trigger */ -const CLICK_OUTSIDE_DELAY = 100 - -/** Delay before aborting when editing during stream */ -const ABORT_DELAY = 100 - -interface UseMessageEditingProps { - message: CopilotMessage - messages: CopilotMessage[] - isLastUserMessage: boolean - hasCheckpoints: boolean - onEditModeChange?: (isEditing: boolean) => void - showCheckpointDiscardModal: boolean - setShowCheckpointDiscardModal: (show: boolean) => void - pendingEditRef: React.MutableRefObject<{ - message: string - fileAttachments?: MessageFileAttachment[] - contexts?: ChatContext[] - } | null> - /** - * When true, disables the internal document click-outside handler. - * Use when a parent component provides its own click-outside handling. - */ - disableDocumentClickOutside?: boolean -} - -/** - * Custom hook to manage message editing functionality - * Handles edit mode state, expansion, click handlers, and edit submission - * - * @param props - Message editing configuration - * @returns Message editing state and handlers - */ -export function useMessageEditing(props: UseMessageEditingProps) { - const { - message, - messages, - isLastUserMessage, - hasCheckpoints, - onEditModeChange, - showCheckpointDiscardModal, - setShowCheckpointDiscardModal, - pendingEditRef, - disableDocumentClickOutside = false, - } = props - - const [isEditMode, setIsEditMode] = useState(false) - const [isExpanded, setIsExpanded] = useState(false) - const [editedContent, setEditedContent] = useState(message.content) - const [needsExpansion, setNeedsExpansion] = useState(false) - - const editContainerRef = useRef(null) - const messageContentRef = useRef(null) - const userInputRef = useRef(null) - - const { sendMessage, isSendingMessage, abortMessage, currentChat } = useCopilotStore() - - /** Checks if message content needs expansion based on height */ - useEffect(() => { - if (messageContentRef.current && message.role === 'user') { - const scrollHeight = messageContentRef.current.scrollHeight - setNeedsExpansion(scrollHeight > MESSAGE_TRUNCATION_HEIGHT) - } - }, [message.content, message.role]) - - /** Enters edit mode */ - const handleEditMessage = useCallback(() => { - setIsEditMode(true) - setIsExpanded(false) - setEditedContent(message.content) - onEditModeChange?.(true) - - setTimeout(() => { - userInputRef.current?.focus() - }, 0) - }, [message.content, onEditModeChange]) - - /** Cancels edit mode */ - const handleCancelEdit = useCallback(() => { - setIsEditMode(false) - setEditedContent(message.content) - onEditModeChange?.(false) - }, [message.content, onEditModeChange]) - - /** Handles message click to enter edit mode */ - const handleMessageClick = useCallback(() => { - if (needsExpansion && !isExpanded) { - setIsExpanded(true) - } - handleEditMessage() - }, [needsExpansion, isExpanded, handleEditMessage]) - - /** Performs the edit operation - truncates messages after edited message and resends */ - const performEdit = useCallback( - async ( - editedMessage: string, - fileAttachments?: MessageFileAttachment[], - contexts?: ChatContext[] - ) => { - const currentMessages = messages - const editIndex = currentMessages.findIndex((m) => m.id === message.id) - - if (editIndex !== -1) { - setIsEditMode(false) - onEditModeChange?.(false) - - const truncatedMessages = currentMessages.slice(0, editIndex) - const updatedMessage = { - ...message, - content: editedMessage, - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || message.contexts, - } - - useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] }) - - if (currentChat?.id) { - try { - await fetch('/api/copilot/chat/update-messages', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chatId: currentChat.id, - messages: truncatedMessages.map((m) => ({ - id: m.id, - role: m.role, - content: m.content, - timestamp: m.timestamp, - ...(m.contentBlocks && { contentBlocks: m.contentBlocks }), - ...(m.fileAttachments && { fileAttachments: m.fileAttachments }), - ...(m.contexts && { contexts: m.contexts }), - })), - }), - }) - } catch (error) { - logger.error('Failed to update messages in DB after edit:', error) - } - } - - await sendMessage(editedMessage, { - fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || message.contexts, - messageId: message.id, - queueIfBusy: false, - }) - } - }, - [messages, message, currentChat, sendMessage, onEditModeChange] - ) - - /** Submits edited message, checking for checkpoints first */ - const handleSubmitEdit = useCallback( - async ( - editedMessage: string, - fileAttachments?: MessageFileAttachment[], - contexts?: ChatContext[] - ) => { - if (!editedMessage.trim()) return - - if (isSendingMessage) { - abortMessage() - await new Promise((resolve) => setTimeout(resolve, ABORT_DELAY)) - } - - if (hasCheckpoints) { - pendingEditRef.current = { message: editedMessage, fileAttachments, contexts } - setShowCheckpointDiscardModal(true) - return - } - - await performEdit(editedMessage, fileAttachments, contexts) - }, - [ - isSendingMessage, - hasCheckpoints, - abortMessage, - performEdit, - pendingEditRef, - setShowCheckpointDiscardModal, - ] - ) - - /** Keyboard-only exit (Esc) */ - useEffect(() => { - if (!isEditMode) return - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - handleCancelEdit() - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => { - document.removeEventListener('keydown', handleKeyDown) - } - }, [isEditMode, handleCancelEdit]) - - /** Optional document-level click-outside handler */ - useEffect(() => { - if (!isEditMode || disableDocumentClickOutside) return - - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement - if (editContainerRef.current?.contains(target)) { - return - } - handleCancelEdit() - } - - const timeoutId = setTimeout(() => { - document.addEventListener('click', handleClickOutside, true) - }, CLICK_OUTSIDE_DELAY) - - return () => { - clearTimeout(timeoutId) - document.removeEventListener('click', handleClickOutside, true) - } - }, [isEditMode, disableDocumentClickOutside, handleCancelEdit]) - - return { - // State - isEditMode, - isExpanded, - editedContent, - needsExpansion, - - // Refs - editContainerRef, - messageContentRef, - userInputRef, - - // Operations - setEditedContent, - handleCancelEdit, - handleMessageClick, - handleSubmitEdit, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts deleted file mode 100644 index d2cf90344a1..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './copilot-message' 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 deleted file mode 100644 index 632dae1424f..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './chat-history-skeleton' -export * from './copilot-message' -export * from './plan-mode-section' -export * from './queued-messages' -export * from './todo-list' -export * from './tool-call' -export * from './user-input' -export * from './welcome' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts deleted file mode 100644 index fb80d1dda5e..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './plan-mode-section' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx deleted file mode 100644 index c4f17704ff5..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Plan Mode Section component with resizable markdown content display. - * Displays markdown content in a separate section at the top of the copilot panel. - * Follows emcn design principles with consistent spacing, typography, and color scheme. - * - * @example - * ```tsx - * import { PlanModeSection } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components' - * - * function CopilotPanel() { - * const plan = "# My Plan\n\nThis is a plan description..." - * - * return ( - * - * ) - * } - * ``` - */ - -'use client' - -import * as React from 'react' -import { Check, GripHorizontal, Pencil, X } from 'lucide-react' -import { Button, Textarea } from '@/components/emcn' -import { Trash } from '@/components/emcn/icons/trash' -import { cn } from '@/lib/core/utils/cn' -import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' - -/** - * Shared border and background styles - */ -const SURFACE_5 = 'bg-[var(--surface-4)]' -const SURFACE_9 = 'bg-[var(--surface-5)]' -const BORDER_STRONG = 'border-[var(--border-1)]' - -export interface PlanModeSectionProps { - /** - * Markdown content to display - */ - content: string - /** - * Optional class name for additional styling - */ - className?: string - /** - * Initial height of the section in pixels - * @default 180 - */ - initialHeight?: number - /** - * Minimum height in pixels - * @default 80 - */ - minHeight?: number - /** - * Maximum height in pixels - * @default 600 - */ - maxHeight?: number - /** - * Callback function when clear button is clicked - */ - onClear?: () => void - /** - * Callback function when save button is clicked - * Receives the current content as parameter - */ - onSave?: (content: string) => void - /** - * Callback when Build Plan button is clicked - */ - onBuildPlan?: () => void -} - -/** - * Plan Mode Section component for displaying markdown content with resizable height. - * Features: pinned position, resizable height with drag handle, internal scrolling. - */ -const PlanModeSection: React.FC = ({ - content, - className, - initialHeight, - minHeight = 80, - maxHeight = 600, - onClear, - onSave, - onBuildPlan, -}) => { - // Default to 75% of max height - const defaultHeight = initialHeight ?? Math.floor(maxHeight * 0.75) - const [height, setHeight] = React.useState(defaultHeight) - const [isResizing, setIsResizing] = React.useState(false) - const [isEditing, setIsEditing] = React.useState(false) - const [editedContent, setEditedContent] = React.useState(content) - const resizeStartRef = React.useRef({ y: 0, startHeight: 0 }) - const textareaRef = React.useRef(null) - - // Update edited content when content prop changes - React.useEffect(() => { - if (!isEditing) { - setEditedContent(content) - } - }, [content, isEditing]) - - const handleResizeStart = React.useCallback( - (e: React.MouseEvent) => { - e.preventDefault() - setIsResizing(true) - resizeStartRef.current = { - y: e.clientY, - startHeight: height, - } - }, - [height] - ) - - const handleResizeMove = React.useCallback( - (e: MouseEvent) => { - if (!isResizing) return - - const deltaY = e.clientY - resizeStartRef.current.y - const newHeight = Math.max( - minHeight, - Math.min(maxHeight, resizeStartRef.current.startHeight + deltaY) - ) - setHeight(newHeight) - }, - [isResizing, minHeight, maxHeight] - ) - - const handleResizeEnd = React.useCallback(() => { - setIsResizing(false) - }, []) - - React.useEffect(() => { - if (isResizing) { - document.addEventListener('mousemove', handleResizeMove) - document.addEventListener('mouseup', handleResizeEnd) - document.body.style.cursor = 'ns-resize' - document.body.style.userSelect = 'none' - - return () => { - document.removeEventListener('mousemove', handleResizeMove) - document.removeEventListener('mouseup', handleResizeEnd) - document.body.style.cursor = '' - document.body.style.userSelect = '' - } - } - }, [isResizing, handleResizeMove, handleResizeEnd]) - - const handleEdit = React.useCallback(() => { - setIsEditing(true) - setEditedContent(content) - setTimeout(() => { - textareaRef.current?.focus() - }, 50) - }, [content]) - - const handleSave = React.useCallback(() => { - if (onSave && editedContent.trim() !== content.trim()) { - onSave(editedContent.trim()) - } - setIsEditing(false) - }, [editedContent, content, onSave]) - - const handleCancel = React.useCallback(() => { - setEditedContent(content) - setIsEditing(false) - }, [content]) - - if (!content || !content.trim()) { - return null - } - - return ( -
    - {/* Header with build/edit/save/clear buttons */} -
    - - Workflow Plan - -
    - {isEditing ? ( - <> - - - - ) : ( - <> - {onBuildPlan && ( - - )} - {onSave && ( - - )} - {onClear && ( - - )} - - )} -
    -
    - - {/* Scrollable content area */} -
    - {isEditing ? ( -