From acc00df6a5010a971e7d920b45d52bf56020d2dd Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sat, 4 Apr 2026 12:31:23 -0700 Subject: [PATCH] feat(mothership): append --- .../api/files/serve/[...path]/route.test.ts | 10 +++ .../tools/server/files/workspace-file.ts | 79 +++++++++++++++++++ apps/sim/lib/copilot/tools/shared/schemas.ts | 2 +- 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/files/serve/[...path]/route.test.ts b/apps/sim/app/api/files/serve/[...path]/route.test.ts index bc5b35647c..3c0c1fa7f6 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -75,6 +75,16 @@ vi.mock('@/lib/uploads/utils/file-utils', () => ({ vi.mock('@/lib/uploads/setup.server', () => ({})) +vi.mock('@/lib/execution/doc-vm', () => ({ + generatePdfFromCode: vi.fn().mockResolvedValue(Buffer.from('%PDF-compiled')), + generateDocxFromCode: vi.fn().mockResolvedValue(Buffer.from('PK\x03\x04compiled')), + generatePptxFromCode: vi.fn().mockResolvedValue(Buffer.from('PK\x03\x04compiled')), +})) + +vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({ + parseWorkspaceFileKey: vi.fn().mockReturnValue(undefined), +})) + vi.mock('@/app/api/files/utils', () => ({ FileNotFoundError, createFileResponse: mockCreateFileResponse, diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index b258180cfe..94f4ec0a3a 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -301,6 +301,85 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined + const content = (args as Record).content as string | undefined + + if (!fileId) { + return { success: false, message: 'fileId is required for append operation' } + } + if (!content) { + return { success: false, message: 'content is required for append operation' } + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return { success: false, message: `File with ID "${fileId}" not found` } + } + + const currentBuffer = await downloadWsFile(fileRecord) + const combined = `${currentBuffer.toString('utf-8')}\n${content}` + + const appendLowerName = fileRecord.name?.toLowerCase() ?? '' + const isPptxAppend = appendLowerName.endsWith('.pptx') + const isDocxAppend = appendLowerName.endsWith('.docx') + const isPdfAppend = appendLowerName.endsWith('.pdf') + const isDocAppend = isPptxAppend || isDocxAppend || isPdfAppend + + if (isDocAppend) { + const formatName = isPptxAppend ? 'PPTX' : isDocxAppend ? 'DOCX' : 'PDF' + const generator = isPptxAppend + ? generatePptxFromCode + : isDocxAppend + ? generateDocxFromCode + : generatePdfFromCode + try { + await generator(combined, workspaceId) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return { + success: false, + message: `Appended ${formatName} code failed to compile: ${msg}. Fix the content and retry.`, + } + } + } + + const appendSourceMime = isPptxAppend + ? PPTX_SOURCE_MIME + : isDocxAppend + ? DOCX_SOURCE_MIME + : isPdfAppend + ? PDF_SOURCE_MIME + : undefined + const appendBuffer = Buffer.from(combined, 'utf-8') + assertServerToolNotAborted(context) + await updateWorkspaceFileContent( + workspaceId, + fileId, + context.userId, + appendBuffer, + appendSourceMime + ) + + logger.info('Workspace file appended via copilot', { + fileId, + name: fileRecord.name, + appendedSize: content.length, + totalSize: appendBuffer.length, + userId: context.userId, + }) + + return { + success: true, + message: `Content appended to "${fileRecord.name}" (${content.length} bytes added, ${appendBuffer.length} bytes total)`, + data: { + id: fileId, + name: fileRecord.name, + size: appendBuffer.length, + }, + } + } + case 'patch': { const fileId = (args as Record).fileId as string | undefined const edits = (args as Record).edits as diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index c59200e846..579a4b6b85 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -176,7 +176,7 @@ export type UserTableResult = z.infer // workspace_file - shared schema used by server tool and Go catalog export const WorkspaceFileArgsSchema = z.object({ - operation: z.enum(['write', 'update', 'delete', 'rename', 'patch']), + operation: z.enum(['write', 'update', 'append', 'delete', 'rename', 'patch']), args: z .object({ fileId: z.string().optional(),