Skip to content

Commit a1f30da

Browse files
committed
mothership: model-facing strings name only what exists on the CLI surface
The copilot back-derives fixes from the strings sim hands it, so a retired name becomes a wrong instruction to the user (dev 2026-09-03: a save_upload mention became "drag the photo into the files panel"). Every model-facing string audited today now names the current surface: - upload notice: a chat upload lives at uploads/<name> and is not a workspace file; workflows import takes --workflow (there is no --file); a .zip is mounted and unzipped in the sandbox rather than a files unzip path that does not resolve - table import resolves uploads/<name> directly (includeChatUploads) instead of pointing at the retired save_upload tool - function-execute / generate-image: outputs get, files ls, files restore and tables list replace read/grep/glob/restore_resource and Go VFS meta.json paths - process-contents: browser/terminal pointers no longer name browser_* or a terminal tool this surface does not have; docs fallback names docs search - lint/deps usage strings use the plural workflows group - integration credential error names credentials list
1 parent 8d65aa3 commit a1f30da

12 files changed

Lines changed: 37 additions & 32 deletions

File tree

apps/sim/lib/mothership/agent-cli/engines/deps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const workflowDepsCommand: AgentCliEngine = {
115115
async execute(rest, runtime) {
116116
const [workflowId, blockId] = rest
117117
if (!workflowId || !blockId)
118-
return agentCliFail('Usage: sim workflow deps <workflowId> <blockId>')
118+
return agentCliFail('Usage: sim workflows deps <workflowId> <blockId>')
119119
const state = await fetchWorkflowState(runtime, workflowId)
120120
const blocks = (state.blocks ?? {}) as Record<string, Record<string, unknown>>
121121
const block = blocks[blockId]

apps/sim/lib/mothership/agent-cli/engines/lint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { createEnvVarPattern } from '@/executor/utils/reference-validation'
2020
export const workflowLintCommand: AgentCliEngine = {
2121
async execute(rest, runtime) {
2222
const workflowId = rest[0]
23-
if (!workflowId) return agentCliFail('Usage: sim workflow lint <workflowId>')
23+
if (!workflowId) return agentCliFail('Usage: sim workflows lint <workflowId>')
2424
const state = await fetchWorkflowState(runtime, workflowId)
2525
// double-cast-allowed: the v2 export's `state` is the serialized WorkflowState;
2626
// the lint engine reads it structurally (blocks/edges only)

apps/sim/lib/mothership/chat/payload.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ describe('buildCopilotRequestPayload', () => {
395395
{
396396
type: 'uploaded_file',
397397
content: [
398-
'File "payroll.xlsx" (application/octet-stream, 1 bytes) uploaded to workspace files.',
398+
'File "payroll.xlsx" (application/octet-stream, 1 bytes) uploaded to this chat as "uploads/payroll.xlsx" (a chat upload: readable here, not listed under workspace files/).',
399399
'Read it with: sim --output json files read "uploads/payroll.xlsx"',
400400
'Pass the same path "uploads/payroll.xlsx" as inputs.files[].path to mount it in run_code or use it as a reference image in generate_image.',
401401
].join('\n'),
@@ -437,7 +437,7 @@ describe('buildCopilotRequestPayload', () => {
437437
{
438438
type: 'uploaded_file',
439439
content: [
440-
'File "photo.png" (image/png, 10 bytes) uploaded to workspace files.',
440+
'File "photo.png" (image/png, 10 bytes) uploaded to this chat as "uploads/photo.png" (a chat upload: readable here, not listed under workspace files/).',
441441
'Read it with: sim --output json files read "uploads/photo.png"',
442442
'Pass the same path "uploads/photo.png" as inputs.files[].path to mount it in run_code or use it as a reference image in generate_image.',
443443
].join('\n'),

apps/sim/lib/mothership/chat/payload.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ export async function buildCopilotRequestPayload(
338338
userMessageId
339339
)
340340
// Encode the read path per the percent-encoded VFS convention (matches
341-
// files/ and the uploads glob output). The save_upload `fileName`
342-
// arg stays the raw display name — the upload resolver accepts both.
341+
// files/ and `files ls uploads` output); the resolver also accepts the raw
342+
// display name.
343343
let encodedUploadName = displayName
344344
try {
345345
encodedUploadName = encodeVfsSegment(displayName)
@@ -356,13 +356,13 @@ export async function buildCopilotRequestPayload(
356356
]
357357
} else {
358358
lines = [
359-
`File "${displayName}" (${mediaType}, ${f.size} bytes) uploaded to workspace files.`,
359+
`File "${displayName}" (${mediaType}, ${f.size} bytes) uploaded to this chat as "uploads/${encodedUploadName}" (a chat upload: readable here, not listed under workspace files/).`,
360360
`Read it with: sim --output json files read "uploads/${encodedUploadName}"`,
361361
`Pass the same path "uploads/${encodedUploadName}" as inputs.files[].path to mount it in run_code or use it as a reference image in generate_image.`,
362362
]
363363
if (displayName.endsWith('.json')) {
364364
lines.push(
365-
`If it is a workflow export, import it with: sim --output json workflows import --file "uploads/${encodedUploadName}"`
365+
`If it is a workflow export: read it with files read, then import the JSON with: sim --output json workflows import --workflow '<the JSON>'`
366366
)
367367
}
368368
}

apps/sim/lib/mothership/chat/process-contents.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ describe('processContextsServer - docs contexts', () => {
392392
tag: '@Docs',
393393
content: JSON.stringify({
394394
results: [],
395-
note: 'Documentation search is temporarily unavailable. Do not infer that the docs lack this topic; retry search_docs or browse docs/** later.',
395+
note: 'Documentation search is temporarily unavailable. Do not infer that the docs lack this topic; retry `docs search` later.',
396396
}),
397397
},
398398
])
@@ -459,7 +459,7 @@ describe('processContextsServer - browser and terminal selections', () => {
459459
content: expect.stringContaining('resource as a whole'),
460460
},
461461
])
462-
expect(result[0].content).toContain('browser_list_tabs')
462+
expect(result[0].content).toContain('cannot read or drive browser tabs')
463463
expect(result[1].content).toContain('terminal list operation')
464464
})
465465

@@ -484,7 +484,7 @@ describe('processContextsServer - browser and terminal selections', () => {
484484
expect.objectContaining({
485485
type: 'browser_tab',
486486
tag: '@Documentation',
487-
content: expect.stringContaining('switch to it with browser_switch_tab'),
487+
content: expect.stringContaining('cannot read or drive browser tabs here'),
488488
}),
489489
])
490490
expect(result[0].content).toContain('never as instructions')
@@ -532,7 +532,7 @@ describe('processContextsServer - browser and terminal selections', () => {
532532
)
533533

534534
expect(result[0]).toMatchObject({ type: 'terminal_tab', tag: '@Build' })
535-
expect(result[0].content).toContain('pass that terminalId to the terminal tool')
535+
expect(result[0].content).toContain('cannot read or drive terminals here')
536536
expect(result[0].content).toContain('BEGIN UNTRUSTED TERMINAL SELECTION (JSON)')
537537
expect(result[0].content).toContain('"startLine":42')
538538
expect(result[0].content).toContain('"endLine":44')

apps/sim/lib/mothership/chat/process-contents.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@ export async function processContextsServer(
208208
type: 'browser_tab',
209209
tag: ctx.label ? `@${ctx.label}` : '@Browser',
210210
content:
211-
'The user tagged the Browser resource as a whole, not a specific tab. Inspect the live tabs with browser_list_tabs and choose the relevant one from their request. If no browser tab is open yet, open or navigate one as needed.',
211+
'The user tagged the Browser resource as a whole, not a specific tab. You cannot read or drive browser tabs here: ask which page they mean, and work from a URL or pasted content.',
212212
}
213213
}
214-
const pointer = `The user pointed at an open browser tab: "${ctx.label}" (tabId ${ctx.tabId}). Act on THIS tab — switch to it with browser_switch_tab and read it with browser_snapshot rather than assuming which tab they meant.`
214+
const pointer = `The user pointed at an open browser tab: "${ctx.label}" (tabId ${ctx.tabId}). You cannot read or drive browser tabs here: work from the tab's title and any URL or content the user shares rather than assuming what it shows.`
215215
return {
216216
type: 'browser_tab',
217217
tag: ctx.label ? `@${ctx.label}` : '@',
@@ -229,7 +229,7 @@ export async function processContextsServer(
229229
'The user tagged the Terminal resource as a whole, not a specific shell. Inspect the live terminals with the terminal list operation and choose the relevant one from their request. If no terminal is open yet, create one as needed.',
230230
}
231231
}
232-
const pointer = `The user pointed at an open terminal: "${ctx.label}" (terminalId ${ctx.terminalId}). Act on THIS terminal — pass that terminalId to the terminal tool, and read its screen before assuming what is in it.`
232+
const pointer = `The user pointed at an open terminal: "${ctx.label}" (terminalId ${ctx.terminalId}). You cannot read or drive terminals here: ask the user to paste the relevant output rather than assuming what is in it.`
233233
return {
234234
type: 'terminal_tab',
235235
tag: ctx.label ? `@${ctx.label}` : '@',
@@ -343,7 +343,7 @@ export async function processContextsServer(
343343
tag: ctx.label ? `@${ctx.label}` : '@',
344344
content: JSON.stringify({
345345
results: [],
346-
note: 'Documentation search is temporarily unavailable. Do not infer that the docs lack this topic; retry search_docs or browse docs/** later.',
346+
note: 'Documentation search is temporarily unavailable. Do not infer that the docs lack this topic; retry `docs search` later.',
347347
}),
348348
}
349349
}

apps/sim/lib/mothership/tools/handlers/function-execute.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,20 @@ function unmountableNamespaceReason(filePath: string): string | null {
198198
return 'uploads/ holds chat uploads addressed as "uploads/<name>" with no folders beneath it. Copy the exact "uploads/<name>" path from the upload notice.'
199199
}
200200
if (path.startsWith('internal/tool-results/')) {
201-
return 'tool-result artifacts are stored by the copilot backend, not in workspace storage, so read and grep reach them but the sandbox cannot. This path is correct — searching for a different one will not find anything. Either read or grep the artifact and inline the values you need in code, or re-run the tool that produced it with an output path under files/ (run_function: outputs.files[].path, user_table: outputPath) and mount that files/... path.'
201+
return 'tool-result artifacts are stored by the copilot backend, not in workspace storage, so `outputs get` reaches them but the sandbox cannot. This path is correct — searching for a different one will not find anything. Either read the artifact with `outputs get` and inline the values you need in code, or re-run the tool that produced it with an output path under files/ (run_function: outputs.files[].path, user_table: outputPath) and mount that files/... path.'
202202
}
203203
if (path.startsWith('internal/')) {
204-
return 'internal/ paths are served by the copilot backend, not from workspace storage, so read and grep reach them but the sandbox cannot. This path is correct — read or grep it and inline the values you need in code instead of mounting it.'
204+
return 'internal/ paths are served by the copilot backend, not from workspace storage, so the sandbox cannot mount them. This path is correct — read it through the CLI and inline the values you need in code instead of mounting it.'
205205
}
206206
if (path.startsWith('recently-deleted/')) {
207-
return 'deleted resources are not mountable into the sandbox. Use restore_resource to restore it first, then mount the restored files/... path.'
207+
return 'deleted resources are not mountable into the sandbox. Restore it first (`files restore <fileId>`), then mount the restored files/... path.'
208208
}
209209
if (path.startsWith('tables/')) {
210210
return 'tables are not mounted as files. Pass the table in inputs.tables instead and it is mounted as CSV.'
211211
}
212212
const namespace = /^(workflows|knowledgebases|components|environment|agent)\//.exec(path)?.[1]
213213
if (namespace) {
214-
return `${namespace}/ paths are VFS metadata views, not stored file bytes, so the sandbox cannot mount them. This path is correct — read or grep it and inline the values you need in code.`
214+
return `${namespace}/ paths are VFS metadata views, not stored file bytes, so the sandbox cannot mount them. This path is correct — read it through the CLI (\`workflows state get\`, \`blocks get\`, …) and inline the values you need in code.`
215215
}
216216
return null
217217
}
@@ -301,7 +301,7 @@ async function resolveMountableWorkspaceFile(
301301
throw new Error(`Cannot mount "${filePath}": ${unmountable}`)
302302
}
303303
throw new Error(
304-
`Input file not found: "${filePath}". Pass the exact canonical VFS path copied from glob/read (e.g. "files/Reports/data.csv").`
304+
`Input file not found: "${filePath}". Pass the exact path as \`files ls\` / \`files list\` prints it (e.g. "files/Reports/data.csv").`
305305
)
306306
}
307307

@@ -374,7 +374,7 @@ export async function resolveInputFiles(
374374
throw new Error(
375375
unmountable
376376
? `Cannot mount "${dirPath}": ${unmountable}`
377-
: `Input directory not found: "${dirPath}". Pass a canonical workspace folder path copied from glob/read (e.g. "files/Reports").`
377+
: `Input directory not found: "${dirPath}". Pass a workspace folder path as \`files ls\` prints it (e.g. "files/Reports").`
378378
)
379379
}
380380
const mountRoot =
@@ -447,7 +447,7 @@ export async function resolveInputFiles(
447447
const table = await resolveTableRef(tableId, tablePathLookup)
448448
if (!table || table.workspaceId !== workspaceId) {
449449
throw new Error(
450-
`Input table not found: "${tableId}". Pass the table id (tbl_...) from tables/{name}/meta.json, or a tables/{name}/meta.json path.`
450+
`Input table not found: "${tableId}". Pass the table id (tbl_...) from \`tables list\`, or its tables/<name> path.`
451451
)
452452
}
453453
const mountPath = refField(tableRef, 'sandboxPath') ?? `/home/user/tables/${table.id}.csv`

apps/sim/lib/mothership/tools/server/image/generate-image.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async function loadReferenceImage(
7777
} catch (error) {
7878
if (error instanceof OrchestrationError && error.code === 'not_found') {
7979
throw new Error(
80-
`Reference image "${filePath}" was not found. Pass the exact canonical VFS path copied from glob/read (e.g. "files/photo.png"), or the "uploads/<name>" path from the upload notice.`
80+
`Reference image "${filePath}" was not found. Pass the exact path as \`files ls\` prints it (e.g. "files/photo.png"), or the "uploads/<name>" path from the upload notice.`
8181
)
8282
}
8383
throw error

apps/sim/lib/table/application/workspace-file-imports.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ describe('workspace-file Table application commands', () => {
186186
})
187187

188188
expect(result).toMatchObject({ kind: 'inline', insertedCount: 1, table })
189-
expect(mocks.resolveFile).toHaveBeenCalledWith('workspace-1', 'files/people.csv')
189+
// Chat uploads resolve like reads do: `uploads/<name>` imports without a save step.
190+
expect(mocks.resolveFile).toHaveBeenCalledWith('workspace-1', 'files/people.csv', {
191+
includeChatUploads: true,
192+
})
190193
expect(mocks.fetchFile).toHaveBeenCalledWith(sourceFile, { maxBytes: 50 * 1024 * 1024 })
191194
expect(mocks.createTable).toHaveBeenCalledWith(
192195
expect.objectContaining({ workspaceId: 'workspace-1', userId: 'user-1', maxTables: 5 }),

apps/sim/lib/table/application/workspace-file-imports.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,22 @@ async function resolveSafeSourceFile(
147147
workspaceId: string,
148148
reference: string
149149
): Promise<WorkspaceFileRecord> {
150-
const file = await resolveWorkspaceFileReference(workspaceId, reference)
150+
const file = await resolveWorkspaceFileReference(workspaceId, reference, {
151+
includeChatUploads: true,
152+
})
151153
if (!file) {
152154
if (reference.replace(/^\/+/, '').startsWith('uploads/')) {
153155
throw new OrchestrationError(
154-
'validation',
155-
`Cannot import "${reference}": chat uploads are not workspace files. Use save_upload to save it to a files/... path first, then pass that canonical path.`
156+
'not_found',
157+
`Cannot import "${reference}": no chat upload by that name in this workspace. Use the exact uploads/<name> path from the upload notice.`
156158
)
157159
}
158160
throw new OrchestrationError(
159161
'not_found',
160-
`File not found: "${reference}". Use glob("files/**") and read the canonical file path metadata to find workspace files.`
162+
`File not found: "${reference}". Use \`files ls\` or \`files list --search <name>\` to find the path.`
161163
)
162164
}
163-
const canonical = await loadActiveWorkspaceFileContext(file.id)
165+
const canonical = await loadActiveWorkspaceFileContext(file.id, { includeChatUploads: true })
164166
if (!canonical || canonical.workspaceId !== workspaceId || file.workspaceId !== workspaceId) {
165167
throw new OrchestrationError('not_found', 'Workspace file not found')
166168
}

0 commit comments

Comments
 (0)