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 b3e422d5328..b662036f2ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -74,7 +74,6 @@ import { TERMINAL_SESSION_RESOURCE_ID, } from '@/lib/copilot/resources/types' import { executeBrowserToolOnClient } from '@/lib/copilot/tools/client/browser-tool-execution' -import { executeLocalFilesystemTool } from '@/lib/copilot/tools/client/local-filesystem' import { bindRunToolToExecution, cancelRunToolExecution, @@ -2009,11 +2008,49 @@ export function useChat( return } handledClientLocalFilesystemToolIdsRef.current.add(toolCallId) - executeLocalFilesystemTool(toolCallId, toolName, toolArgs, { + const options = { workspaceId, chatId: chatIdRef.current ?? selectedChatIdRef.current, signal: abortControllerRef.current?.signal, - }) + } + /** + * Dynamic on purpose: the local-filesystem executor only runs for desktop-local + * VFS tool calls, and a static import kept it in the shared chat chunk on every + * surface that mounts the composer. The guard, the dedupe add, and the option + * capture above stay synchronous, so re-entrancy behaviour is unchanged. If the + * chunk fails to load (deploy skew), the server-side tool call must still settle: + * report an error completion rather than leaving it hanging with the dedupe ref + * already marked handled. + */ + import('@/lib/copilot/tools/client/local-filesystem').then( + (m) => m.executeLocalFilesystemTool(toolCallId, toolName, toolArgs, options), + async (error) => { + logger.error('Failed to load local filesystem tool executor', { error }) + /** + * The recovery itself can reject (the helper chunks or the completion POST can + * fail for the same reason the executor chunk did). Contain it: an unhandled + * rejection here would settle nothing and surface as a console error, exactly + * like the executor's own report-failure path, which also degrades to a log. + */ + try { + const [{ reportClientToolCompletion }, { ASYNC_TOOL_CONFIRMATION_STATUS }] = + await Promise.all([ + import('@/lib/copilot/tools/client/completion'), + import('@/lib/copilot/async-runs/lifecycle'), + ]) + await reportClientToolCompletion( + toolCallId, + ASYNC_TOOL_CONFIRMATION_STATUS.error, + 'Local filesystem tool failed to load' + ) + } catch (reportError) { + logger.error('Failed to report local filesystem tool load failure', { + toolCallId, + error: reportError, + }) + } + } + ) }, [workspaceId] ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 32a08870988..7e5d1e45483 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -77,7 +77,13 @@ import { useFolderAncestors, } from '@/app/workspace/[workspaceId]/components/folders' import { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state' -import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components' +/** + * Deep import on purpose: the `[documentId]/components` barrel also exports `ChunkEditor`, + * which needs exact token counts and therefore `js-tiktoken` (~2.5 MB gzip of BPE rank + * tables). Importing the modal through the barrel shipped the tokenizer to the document + * LIST route, which never edits chunks. + */ +import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal' import { ActionBar, AddConnectorModal, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts index c8b8e357e15..b374e4f738c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts @@ -1,6 +1,5 @@ export { Dashboard } from './dashboard' export { LogDetails, LogDetailsContent } from './log-details' -export { ExecutionSnapshot } from './log-details/components/execution-snapshot' export { FileCards } from './log-details/components/file-download' export { TraceView } from './log-details/components/trace-view' export { LogRowContextMenu } from './log-row-context-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts deleted file mode 100644 index a80bf4e337d..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ExecutionSnapshot } from './execution-snapshot' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary.test.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary.test.tsx new file mode 100644 index 00000000000..221aa5892e4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary.test.tsx @@ -0,0 +1,116 @@ +/** + * @vitest-environment jsdom + */ +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockToastError } = vi.hoisted(() => ({ + mockToastError: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ + Loader: () =>