From c69d710ef5c05832a02bfa64fc3bed541b344e74 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 28 May 2026 11:04:29 -0700 Subject: [PATCH 1/2] ff --- .../tools/handlers/function-execute.ts | 9 +- .../tools/server/files/touch-plan.test.ts | 4 + .../copilot/tools/server/files/touch-plan.ts | 4 + apps/sim/lib/copilot/tools/server/router.ts | 16 +- .../copilot/vfs/workflow-alias-resolver.ts | 2 + apps/sim/lib/copilot/vfs/workspace-vfs.ts | 284 ++++++++++-------- apps/sim/lib/core/config/env.ts | 1 + apps/sim/lib/core/config/feature-flags.ts | 5 + 8 files changed, 190 insertions(+), 135 deletions(-) diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 54f835e0e2b..0a780167241 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils' import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' import { getTableById, listTables, queryRows } from '@/lib/table/service' import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { @@ -71,7 +72,9 @@ async function resolveInputFiles( let totalSize = 0 if (inputFiles?.length && workspaceId) { - const allFiles = await listWorkspaceFiles(workspaceId, { includeReservedSystemFiles: true }) + const allFiles = await listWorkspaceFiles(workspaceId, { + includeReservedSystemFiles: isMothershipBetaFeaturesEnabled, + }) for (const fileRef of inputFiles) { const filePath = typeof fileRef === 'string' @@ -124,11 +127,11 @@ async function resolveInputFiles( if (inputDirectories?.length && workspaceId) { const folders = await listWorkspaceFileFolders(workspaceId, { - includeReservedSystemFolders: true, + includeReservedSystemFolders: isMothershipBetaFeaturesEnabled, }) const allFiles = await listWorkspaceFiles(workspaceId, { folders, - includeReservedSystemFiles: true, + includeReservedSystemFiles: isMothershipBetaFeaturesEnabled, }) for (const dirRef of inputDirectories) { const dirPath = diff --git a/apps/sim/lib/copilot/tools/server/files/touch-plan.test.ts b/apps/sim/lib/copilot/tools/server/files/touch-plan.test.ts index e974c467d4a..3134cb2c9fd 100644 --- a/apps/sim/lib/copilot/tools/server/files/touch-plan.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/touch-plan.test.ts @@ -18,6 +18,10 @@ vi.mock('@/lib/copilot/vfs/resource-writer', () => ({ writeWorkspaceFileByPath: mocks.writeWorkspaceFileByPath, })) +vi.mock('@/lib/core/config/feature-flags', () => ({ + isMothershipBetaFeaturesEnabled: true, +})) + import { touchPlanServerTool } from './touch-plan' describe('touch_plan server tool', () => { diff --git a/apps/sim/lib/copilot/tools/server/files/touch-plan.ts b/apps/sim/lib/copilot/tools/server/files/touch-plan.ts index 6e0e6c949ec..3a975c0fb73 100644 --- a/apps/sim/lib/copilot/tools/server/files/touch-plan.ts +++ b/apps/sim/lib/copilot/tools/server/files/touch-plan.ts @@ -12,6 +12,7 @@ import { } from '@/lib/copilot/vfs/path-utils' import { writeWorkspaceFileByPath } from '@/lib/copilot/vfs/resource-writer' import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' const logger = createLogger('TouchPlanServerTool') const TOUCH_PLAN_TOOL_ID = 'touch_plan' @@ -64,6 +65,9 @@ function normalizePlanRelativePath(name: string): string { export const touchPlanServerTool: BaseServerTool = { name: TOUCH_PLAN_TOOL_ID, async execute(params: TouchPlanArgs, context?: ServerToolContext): Promise { + if (!isMothershipBetaFeaturesEnabled) { + return { success: false, message: 'touch_plan is not available' } + } if (!context?.userId) { throw new Error('Authentication required') } diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index f7eee510942..0817f96c34a 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -25,6 +25,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool' import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks' import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation' @@ -130,7 +131,7 @@ function isWriteAction(toolName: string, action: string | undefined): boolean { } /** Registry of all server tools. Tools self-declare their validation schemas. */ -const serverToolRegistry: Record = { +const baseServerToolRegistry: Record = { [getBlocksMetadataServerTool.name]: getBlocksMetadataServerTool, [getTriggerBlocksServerTool.name]: getTriggerBlocksServerTool, [editWorkflowServerTool.name]: editWorkflowServerTool, @@ -159,8 +160,17 @@ const serverToolRegistry: Record = { [generateImageServerTool.name]: generateImageServerTool, } +function getServerToolRegistry(): Record { + if (isMothershipBetaFeaturesEnabled) { + return baseServerToolRegistry + } + const registry = { ...baseServerToolRegistry } + delete registry[touchPlanServerTool.name] + return registry +} + export function getRegisteredServerToolNames(): string[] { - return Object.keys(serverToolRegistry) + return Object.keys(getServerToolRegistry()) } export async function routeExecution( @@ -168,7 +178,7 @@ export async function routeExecution( payload: unknown, context?: ServerToolContext ): Promise { - const tool = serverToolRegistry[toolName] + const tool = getServerToolRegistry()[toolName] if (!tool) { throw new Error(`Unknown server tool: ${toolName}`) } diff --git a/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts b/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts index c289d2eeccd..d5f2d4e6c15 100644 --- a/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts +++ b/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts @@ -8,12 +8,14 @@ import { resolveWorkflowAliasPath, type WorkflowAliasTarget, } from '@/lib/copilot/vfs/workflow-aliases' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' import { canonicalizeVfsPath } from './path-utils' export async function resolveWorkflowAliasForWorkspace(args: { workspaceId: string path: string }): Promise { + if (!isMothershipBetaFeaturesEnabled) return null if (!isPlanAliasPath(args.path)) return null let canonicalPath: string diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 93404ffe38a..8bf743f2b6f 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -80,6 +80,7 @@ import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/executi import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task' import { getKnowledgeBases } from '@/lib/knowledge/service' import { validateMermaidSource } from '@/lib/mermaid/validate' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' import { buildMothershipToolsForRequest } from '@/lib/mothership/settings/runtime' import { listTables } from '@/lib/table/service' import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' @@ -490,6 +491,9 @@ export class WorkspaceVFS { path: string, suffix: 'style' | 'compiled-check' | 'compiled' ): Promise { + if (!isMothershipBetaFeaturesEnabled && isWorkflowAliasBackingPath(path)) { + return null + } const byIdMatch = path.match(new RegExp(`^files/by-id/([^/]+)/${suffix}$`)) if (byIdMatch?.[1]) { return getWorkspaceFile(this._workspaceId, byIdMatch[1]) @@ -657,6 +661,9 @@ export class WorkspaceVFS { .replace(/\/content$/, '') .replace(/^\/+/, '') + if (!isMothershipBetaFeaturesEnabled && isWorkflowAliasBackingPath(fileReference)) { + return null + } if (fileReference.endsWith('/meta.json') || path.endsWith('/meta.json')) return null const scope = deletedMatch ? 'archived' : 'active' @@ -664,7 +671,7 @@ export class WorkspaceVFS { try { const files = await listWorkspaceFiles(this._workspaceId, { scope, - includeReservedSystemFiles: true, + includeReservedSystemFiles: isMothershipBetaFeaturesEnabled, }) const record = findWorkspaceFileRecord(files, fileReference) if (!record) return null @@ -721,6 +728,7 @@ export class WorkspaceVFS { workspaceId: string, userId: string ): Promise { + const workflowArtifactsEnabled = isMothershipBetaFeaturesEnabled const [workflowRows, folderRows] = await Promise.all([ listWorkflows(workspaceId), listFolders(workspaceId), @@ -728,17 +736,21 @@ export class WorkspaceVFS { const folderPaths = this.buildFolderPaths(folderRows) - await Promise.all( - workflowRows.map((wf) => - ensureWorkflowAliasBackingQuietly({ - workspaceId, - userId, - workflowId: wf.id, - workflowName: wf.name, - }) + if (workflowArtifactsEnabled) { + await Promise.all( + workflowRows.map((wf) => + ensureWorkflowAliasBackingQuietly({ + workspaceId, + userId, + workflowId: wf.id, + workflowName: wf.name, + }) + ) ) - ) - const workspaceFiles = await listWorkspaceFiles(workspaceId, { includeReservedSystemFiles: true }) + } + const workspaceFiles = workflowArtifactsEnabled + ? await listWorkspaceFiles(workspaceId, { includeReservedSystemFiles: true }) + : [] // Register all folders in the VFS so empty folders are discoverable. for (const { folderId } of folderRows) { @@ -759,71 +771,73 @@ export class WorkspaceVFS { this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf)) - const changelog = findWorkspaceFileRecord( - workspaceFiles, - workflowChangelogBackingPath(wf.id) - ) - let changelogContent = '' - if (changelog) { - try { - changelogContent = (await readFileRecord(changelog))?.content ?? '' - } catch (err) { - logger.warn('Failed to read workflow changelog alias backing file', { - workspaceId, - workflowId: wf.id, - fileId: changelog.id, - error: toError(err).message, - }) - } - } - if (changelog) { - this.files.set(`${prefix}${WORKFLOW_CHANGELOG_ALIAS_NAME}`, changelogContent) - } - this.files.set(`${prefix}${WORKFLOW_PLANS_ALIAS_DIR}/.folder`, '') - - const planFiles = workspaceFiles.filter((file) => { - if (!file.folderPath) return false - return ( - file.folderPath === `${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}` || - file.folderPath.startsWith(`${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}/`) + if (workflowArtifactsEnabled) { + const changelog = findWorkspaceFileRecord( + workspaceFiles, + workflowChangelogBackingPath(wf.id) ) - }) - for (const planFile of planFiles) { - const relativeFolder = planFile.folderPath - ?.replace(`${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}`, '') - .replace(/^\/+/, '') - const aliasPlanPath = [ - prefix, - `${WORKFLOW_PLANS_ALIAS_DIR}/`, - relativeFolder ? `${encodeVfsPathSegments(relativeFolder.split('/'))}/` : '', - normalizeVfsSegment(planFile.name), - ].join('') - try { - this.files.set(aliasPlanPath, (await readFileRecord(planFile))?.content ?? '') - } catch (err) { - logger.warn('Failed to read workflow plan alias backing file', { - workspaceId, - workflowId: wf.id, - fileId: planFile.id, - error: toError(err).message, - }) + let changelogContent = '' + if (changelog) { + try { + changelogContent = (await readFileRecord(changelog))?.content ?? '' + } catch (err) { + logger.warn('Failed to read workflow changelog alias backing file', { + workspaceId, + workflowId: wf.id, + fileId: changelog.id, + error: toError(err).message, + }) + } } - } - this.files.set( - `${prefix}${WORKFLOW_ALIAS_LINKS_NAME}`, - JSON.stringify( - { - aliases: buildWorkflowAliasLinks({ - workflowPath, + if (changelog) { + this.files.set(`${prefix}${WORKFLOW_CHANGELOG_ALIAS_NAME}`, changelogContent) + } + this.files.set(`${prefix}${WORKFLOW_PLANS_ALIAS_DIR}/.folder`, '') + + const planFiles = workspaceFiles.filter((file) => { + if (!file.folderPath) return false + return ( + file.folderPath === `${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}` || + file.folderPath.startsWith(`${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}/`) + ) + }) + for (const planFile of planFiles) { + const relativeFolder = planFile.folderPath + ?.replace(`${WORKFLOW_PLANS_BACKING_FOLDER}/${wf.id}`, '') + .replace(/^\/+/, '') + const aliasPlanPath = [ + prefix, + `${WORKFLOW_PLANS_ALIAS_DIR}/`, + relativeFolder ? `${encodeVfsPathSegments(relativeFolder.split('/'))}/` : '', + normalizeVfsSegment(planFile.name), + ].join('') + try { + this.files.set(aliasPlanPath, (await readFileRecord(planFile))?.content ?? '') + } catch (err) { + logger.warn('Failed to read workflow plan alias backing file', { + workspaceId, workflowId: wf.id, - changelog, - planFiles, - }), - }, - null, - 2 + fileId: planFile.id, + error: toError(err).message, + }) + } + } + this.files.set( + `${prefix}${WORKFLOW_ALIAS_LINKS_NAME}`, + JSON.stringify( + { + aliases: buildWorkflowAliasLinks({ + workflowPath, + workflowId: wf.id, + changelog, + planFiles, + }), + }, + null, + 2 + ) ) - ) + } let normalized: Awaited> = null try { @@ -1057,6 +1071,7 @@ export class WorkspaceVFS { */ private async materializeFiles(workspaceId: string): Promise { try { + const workflowArtifactsEnabled = isMothershipBetaFeaturesEnabled const folders = await listWorkspaceFileFolders(workspaceId, { includeReservedSystemFolders: true, }) @@ -1065,6 +1080,12 @@ export class WorkspaceVFS { includeReservedSystemFiles: true, }) for (const folder of folders) { + if ( + !workflowArtifactsEnabled && + isWorkflowAliasBackingPath(`files/${encodeVfsPathSegments(folder.path.split('/'))}`) + ) { + continue + } this.files.set(`files/${encodeVfsPathSegments(folder.path.split('/'))}/.folder`, '') } @@ -1073,6 +1094,9 @@ export class WorkspaceVFS { folderPath: file.folderPath, name: file.name, }) + if (!workflowArtifactsEnabled && isWorkflowAliasBackingPath(filePath)) { + continue + } this.files.set( filePath, serializeFileMeta({ @@ -1088,66 +1112,68 @@ export class WorkspaceVFS { ) } - this.files.set(`${WORKFLOW_PLANS_ALIAS_DIR}/.folder`, '') - const workspacePlanFiles = files.filter((file) => { - if (!file.folderPath) return false - return ( - file.folderPath === `${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}` || - file.folderPath.startsWith(`${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}/`) - ) - }) - const workspacePlanLinks = [] - for (const planFile of workspacePlanFiles) { - const relativeFolder = planFile.folderPath - ?.replace(`${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}`, '') - .replace(/^\/+/, '') - const aliasRelativePath = [ - relativeFolder ? `${encodeVfsPathSegments(relativeFolder.split('/'))}/` : '', - normalizeVfsSegment(planFile.name), - ].join('') - const aliasPlanPath = `${WORKFLOW_PLANS_ALIAS_DIR}/${aliasRelativePath}` - const relativeSegments = aliasRelativePath.split('/').slice(0, -1) - for (let index = 0; index < relativeSegments.length; index++) { - this.files.set( - `${WORKFLOW_PLANS_ALIAS_DIR}/${relativeSegments.slice(0, index + 1).join('/')}/.folder`, - '' + if (workflowArtifactsEnabled) { + this.files.set(`${WORKFLOW_PLANS_ALIAS_DIR}/.folder`, '') + const workspacePlanFiles = files.filter((file) => { + if (!file.folderPath) return false + return ( + file.folderPath === `${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}` || + file.folderPath.startsWith(`${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}/`) ) + }) + const workspacePlanLinks = [] + for (const planFile of workspacePlanFiles) { + const relativeFolder = planFile.folderPath + ?.replace(`${WORKFLOW_PLANS_BACKING_FOLDER}/${WORKSPACE_PLANS_BACKING_FOLDER}`, '') + .replace(/^\/+/, '') + const aliasRelativePath = [ + relativeFolder ? `${encodeVfsPathSegments(relativeFolder.split('/'))}/` : '', + normalizeVfsSegment(planFile.name), + ].join('') + const aliasPlanPath = `${WORKFLOW_PLANS_ALIAS_DIR}/${aliasRelativePath}` + const relativeSegments = aliasRelativePath.split('/').slice(0, -1) + for (let index = 0; index < relativeSegments.length; index++) { + this.files.set( + `${WORKFLOW_PLANS_ALIAS_DIR}/${relativeSegments.slice(0, index + 1).join('/')}/.folder`, + '' + ) + } + try { + this.files.set(aliasPlanPath, (await readFileRecord(planFile))?.content ?? '') + workspacePlanLinks.push({ + kind: 'plan_file', + scope: 'workspace', + aliasPath: aliasPlanPath, + backingPath: workspacePlanBackingPath(aliasRelativePath), + backingFileId: planFile.id, + }) + } catch (err) { + logger.warn('Failed to read workspace plan alias backing file', { + workspaceId, + fileId: planFile.id, + error: toError(err).message, + }) + } } - try { - this.files.set(aliasPlanPath, (await readFileRecord(planFile))?.content ?? '') - workspacePlanLinks.push({ - kind: 'plan_file', - scope: 'workspace', - aliasPath: aliasPlanPath, - backingPath: workspacePlanBackingPath(aliasRelativePath), - backingFileId: planFile.id, - }) - } catch (err) { - logger.warn('Failed to read workspace plan alias backing file', { - workspaceId, - fileId: planFile.id, - error: toError(err).message, - }) - } - } - this.files.set( - `${WORKFLOW_PLANS_ALIAS_DIR}/${WORKFLOW_ALIAS_LINKS_NAME}`, - JSON.stringify( - { - aliases: [ - { - kind: 'plans_dir', - scope: 'workspace', - aliasPath: WORKFLOW_PLANS_ALIAS_DIR, - backingPath: workspacePlansBackingFolderPath(), - }, - ...workspacePlanLinks, - ], - }, - null, - 2 + this.files.set( + `${WORKFLOW_PLANS_ALIAS_DIR}/${WORKFLOW_ALIAS_LINKS_NAME}`, + JSON.stringify( + { + aliases: [ + { + kind: 'plans_dir', + scope: 'workspace', + aliasPath: WORKFLOW_PLANS_ALIAS_DIR, + backingPath: workspacePlansBackingFolderPath(), + }, + ...workspacePlanLinks, + ], + }, + null, + 2 + ) ) - ) + } return files .filter( diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 7f408439f16..2fe067e8deb 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -373,6 +373,7 @@ export const env = createEnv({ // Invitations - for self-hosted deployments DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments) DISABLE_PUBLIC_API: z.boolean().optional(), // Disable public API access globally (for self-hosted deployments) + MOTHERSHIP_BETA_FEATURES: z.boolean().optional(), // Enable beta Mothership planning/changelog artifact surfaces // Development Tools REACT_GRAB_ENABLED: z.boolean().optional(), // Enable React Grab for UI element debugging in Cursor/AI agents (dev only) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index f1d6e959f36..7a2928f7648 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -150,6 +150,11 @@ export const isWorkflowColumnsEnabledClient = isTruthy( getEnv('NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED') ) +/** + * Enables beta Mothership plan/changelog artifact surfaces. + */ +export const isMothershipBetaFeaturesEnabled = isTruthy(env.MOTHERSHIP_BETA_FEATURES) + /** * Is E2B enabled for remote code execution */ From afa497a43016c7e22d8b61044dcae735b574ad05 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 28 May 2026 13:42:21 -0700 Subject: [PATCH 2/2] System role in cache --- apps/sim/app/api/mcp/copilot/route.ts | 2 +- apps/sim/lib/copilot/chat/payload.test.ts | 30 ++++++++++++++++++++++- apps/sim/lib/copilot/chat/post.test.ts | 10 +++++--- apps/sim/lib/copilot/chat/post.ts | 13 ++++++---- apps/sim/providers/models.ts | 21 +++++++++++++++- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 694009c94c0..5eee40e7f97 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -38,7 +38,7 @@ import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' const logger = createLogger('CopilotMcpAPI') const mcpRateLimiter = new RateLimiter() -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' +const DEFAULT_COPILOT_MODEL = 'claude-opus-4-8' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' diff --git a/apps/sim/lib/copilot/chat/payload.test.ts b/apps/sim/lib/copilot/chat/payload.test.ts index ab94e1b4719..4dd1b4c6740 100644 --- a/apps/sim/lib/copilot/chat/payload.test.ts +++ b/apps/sim/lib/copilot/chat/payload.test.ts @@ -57,7 +57,7 @@ vi.mock('@/tools/params', () => ({ createUserToolSchema: mockCreateUserToolSchema, })) -import { buildIntegrationToolSchemas } from './payload' +import { buildCopilotRequestPayload, buildIntegrationToolSchemas } from './payload' describe('buildIntegrationToolSchemas', () => { beforeEach(() => { @@ -123,3 +123,31 @@ describe('buildIntegrationToolSchemas', () => { ) }) }) + +describe('buildCopilotRequestPayload', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes workspaceContext through to the Go request payload', async () => { + const payload = await buildCopilotRequestPayload( + { + message: 'debug workspace', + userId: 'user-1', + userMessageId: 'msg-1', + mode: 'agent', + model: 'claude-opus-4-8', + workspaceId: 'ws-1', + workspaceContext: 'workspace inventory', + }, + { selectedModel: 'claude-opus-4-8' } + ) + + expect(payload).toEqual( + expect.objectContaining({ + workspaceId: 'ws-1', + workspaceContext: 'workspace inventory', + }) + ) + }) +}) diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts index fe9f9444fc5..44d5a7b7680 100644 --- a/apps/sim/lib/copilot/chat/post.test.ts +++ b/apps/sim/lib/copilot/chat/post.test.ts @@ -167,15 +167,17 @@ describe('handleUnifiedChatPost', () => { ) expect(response.status).toBe(200) + expect(generateWorkspaceContext).toHaveBeenCalledWith('ws-1', 'user-1') expect(buildCopilotRequestPayload).toHaveBeenCalledWith( expect.objectContaining({ - model: 'claude-opus-4-7', + model: 'claude-opus-4-8', + workspaceContext: 'workspace context', }), - { selectedModel: 'claude-opus-4-7' } + { selectedModel: 'claude-opus-4-8' } ) expect(createSSEStream).toHaveBeenCalledWith( expect.objectContaining({ - titleModel: 'claude-opus-4-7', + titleModel: 'claude-opus-4-8', workspaceId: 'ws-1', orchestrateOptions: expect.objectContaining({ workflowId: 'wf-1', @@ -213,7 +215,7 @@ describe('handleUnifiedChatPost', () => { ) expect(createSSEStream).toHaveBeenCalledWith( expect.objectContaining({ - titleModel: 'claude-opus-4-7', + titleModel: 'claude-opus-4-8', workspaceId: 'ws-1', orchestrateOptions: expect.objectContaining({ workspaceId: 'ws-1', diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index ac216efa0cc..a788b6aa870 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -50,7 +50,7 @@ import type { ChatContext } from '@/stores/panel' export const maxDuration = 3600 const logger = createLogger('UnifiedChatAPI') -const DEFAULT_MODEL = 'claude-opus-4-7' +const DEFAULT_MODEL = 'claude-opus-4-8' const FileAttachmentSchema = z.object({ id: z.string(), @@ -172,6 +172,7 @@ type UnifiedChatBranch = commands?: string[] prefetch?: boolean implicitFeedback?: string + workspaceContext?: string }) => Promise> buildExecutionContext: (params: { userId: string @@ -556,7 +557,7 @@ async function resolveBranch(params: { workflowId: resolvedWorkflowId, workflowName: resolved.workflowName, workspaceId: resolvedWorkspaceId, - effectiveModel: selectedModel, + effectiveModel: selectedModel, selectedModel, mode: mode ?? 'agent', provider, @@ -582,6 +583,7 @@ async function resolveBranch(params: { chatId: payloadParams.chatId, prefetch: payloadParams.prefetch, implicitFeedback: payloadParams.implicitFeedback, + workspaceContext: payloadParams.workspaceContext, userPermission: payloadParams.userPermission, userTimezone: payloadParams.userTimezone, }, @@ -852,11 +854,11 @@ export async function handleUnifiedChatPost(req: NextRequest) { // apparent "gap" before the model call. Each promise is its own // span; they run concurrently under Promise.all below. const workspaceContextPromise = - branch.kind === 'workspace' + workspaceId ? withCopilotSpan( TraceSpan.CopilotChatBuildWorkspaceContext, - { [TraceAttr.WorkspaceId]: branch.workspaceId }, - () => generateWorkspaceContext(branch.workspaceId, authenticatedUserId), + { [TraceAttr.WorkspaceId]: workspaceId }, + () => generateWorkspaceContext(workspaceId, authenticatedUserId), activeOtelRoot.context ) : Promise.resolve(undefined) @@ -950,6 +952,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { commands: body.commands, prefetch: body.prefetch, implicitFeedback: body.implicitFeedback, + workspaceContext, }) : branch.buildPayload({ message: body.message, diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 82497c87d04..a7e11087dcb 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -567,6 +567,26 @@ export const PROVIDER_DEFINITIONS: Record = { toolUsageControl: true, }, models: [ + { + id: 'claude-opus-4-8', + pricing: { + input: 5.0, + cachedInput: 0.5, + output: 25.0, + updatedAt: '2026-05-28', + }, + capabilities: { + nativeStructuredOutputs: true, + maxOutputTokens: 128000, + thinking: { + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + default: 'high', + }, + }, + contextWindow: 1000000, + releaseDate: '2026-05-28', + recommended: true, + }, { id: 'claude-opus-4-7', pricing: { @@ -585,7 +605,6 @@ export const PROVIDER_DEFINITIONS: Record = { }, contextWindow: 1000000, releaseDate: '2026-04-16', - recommended: true, }, { id: 'claude-opus-4-6',