Skip to content

Commit cec7b0e

Browse files
committed
VFS update
1 parent c0b1657 commit cec7b0e

14 files changed

Lines changed: 448 additions & 46 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
DeployApi,
5353
DeployChat,
5454
DeployMcp,
55+
FunctionExecute,
5556
GetPageContents,
5657
GetWorkflowLogs,
5758
Glob,
@@ -581,9 +582,20 @@ function resolveOperationDisplayTitle(
581582
return label ?? fallback
582583
}
583584

585+
function prefixFunctionExecuteTitle(title: string | undefined, language: unknown): string {
586+
const modePrefix = language === 'shell' ? 'CLI' : 'Code'
587+
const baseTitle = title ?? 'Running code'
588+
if (baseTitle.startsWith(`${modePrefix}:`)) return baseTitle
589+
return `${modePrefix}: ${baseTitle}`
590+
}
591+
584592
function resolveToolDisplayTitle(name: string, args?: Record<string, unknown>): string | undefined {
585593
if (!args) return undefined
586594

595+
if (name === FunctionExecute.id) {
596+
return prefixFunctionExecuteTitle(stringParam(args.title), args.language)
597+
}
598+
587599
if (name === WorkspaceFile.id) {
588600
const target = asPayloadRecord(args.target)
589601
return resolveWorkspaceFileDisplayTitle(args.operation, args.title, target?.fileName)
@@ -731,6 +743,13 @@ function matchStreamingStringArg(streamingArgs: string, key: string): string | u
731743
}
732744

733745
function resolveStreamingToolDisplayTitle(name: string, streamingArgs: string): string | undefined {
746+
if (name === FunctionExecute.id) {
747+
return prefixFunctionExecuteTitle(
748+
matchStreamingStringArg(streamingArgs, 'title'),
749+
matchStreamingStringArg(streamingArgs, 'language')
750+
)
751+
}
752+
734753
if (name === WorkspaceFile.id) {
735754
return resolveWorkspaceFileDisplayTitle(
736755
matchStreamingStringArg(streamingArgs, 'operation'),
@@ -1799,7 +1818,11 @@ export function useChat(
17991818
if (session.fileId && hasRenderableFilePreviewContent(session)) {
18001819
setResources((current) => {
18011820
const withoutStreaming = current.filter((resource) => resource.id !== 'streaming-file')
1802-
if (withoutStreaming.some((resource) => resource.type === 'file' && resource.id === session.fileId)) {
1821+
if (
1822+
withoutStreaming.some(
1823+
(resource) => resource.type === 'file' && resource.id === session.fileId
1824+
)
1825+
) {
18031826
return withoutStreaming
18041827
}
18051828
return [

apps/sim/lib/copilot/request/go/stream.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe('copilot go stream helpers', () => {
161161
requestId: 'req-1',
162162
type: MothershipStreamV1EventType.tool,
163163
payload: {
164-
toolCallId: 'workspace-file-1',
164+
toolCallId: 'workspace-file-path-1',
165165
toolName: 'workspace_file',
166166
executor: MothershipStreamV1ToolExecutor.sim,
167167
mode: MothershipStreamV1ToolMode.async,
@@ -180,7 +180,7 @@ describe('copilot go stream helpers', () => {
180180
requestId: 'req-1',
181181
type: MothershipStreamV1EventType.tool,
182182
payload: {
183-
toolCallId: 'workspace-file-1',
183+
toolCallId: 'workspace-file-path-1',
184184
toolName: 'workspace_file',
185185
executor: MothershipStreamV1ToolExecutor.sim,
186186
mode: MothershipStreamV1ToolMode.async,
@@ -199,7 +199,7 @@ describe('copilot go stream helpers', () => {
199199
requestId: 'req-1',
200200
type: MothershipStreamV1EventType.tool,
201201
payload: {
202-
toolCallId: 'edit-content-1',
202+
toolCallId: 'edit-content-path-1',
203203
toolName: 'edit_content',
204204
executor: MothershipStreamV1ToolExecutor.sim,
205205
mode: MothershipStreamV1ToolMode.async,
@@ -214,7 +214,7 @@ describe('copilot go stream helpers', () => {
214214
requestId: 'req-1',
215215
type: MothershipStreamV1EventType.tool,
216216
payload: {
217-
toolCallId: 'edit-content-1',
217+
toolCallId: 'edit-content-path-1',
218218
toolName: 'edit_content',
219219
executor: MothershipStreamV1ToolExecutor.sim,
220220
mode: MothershipStreamV1ToolMode.async,
@@ -301,7 +301,7 @@ describe('copilot go stream helpers', () => {
301301
requestId: 'req-1',
302302
type: MothershipStreamV1EventType.tool,
303303
payload: {
304-
toolCallId: 'workspace-file-1',
304+
toolCallId: 'workspace-file-alias-1',
305305
toolName: 'workspace_file',
306306
executor: MothershipStreamV1ToolExecutor.sim,
307307
mode: MothershipStreamV1ToolMode.async,
@@ -320,7 +320,7 @@ describe('copilot go stream helpers', () => {
320320
requestId: 'req-1',
321321
type: MothershipStreamV1EventType.tool,
322322
payload: {
323-
toolCallId: 'edit-content-1',
323+
toolCallId: 'edit-content-alias-1',
324324
toolName: 'edit_content',
325325
executor: MothershipStreamV1ToolExecutor.sim,
326326
mode: MothershipStreamV1ToolMode.async,
@@ -335,7 +335,7 @@ describe('copilot go stream helpers', () => {
335335
requestId: 'req-1',
336336
type: MothershipStreamV1EventType.tool,
337337
payload: {
338-
toolCallId: 'edit-content-1',
338+
toolCallId: 'edit-content-alias-1',
339339
toolName: 'edit_content',
340340
executor: MothershipStreamV1ToolExecutor.sim,
341341
mode: MothershipStreamV1ToolMode.async,

apps/sim/lib/copilot/request/handlers/handlers.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,95 @@ describe('sse-handlers tool lifecycle', () => {
320320
expect(context.toolCalls.get('tool-hidden')?.name).toBe('load_agent_skill')
321321
})
322322

323+
it('does not add ui-hidden tool calls to content blocks', async () => {
324+
await sseHandlers.tool(
325+
{
326+
type: MothershipStreamV1EventType.tool,
327+
payload: {
328+
toolCallId: 'tool-ui-hidden',
329+
toolName: 'read',
330+
arguments: { path: 'components/integrations/slack/README.md' },
331+
executor: MothershipStreamV1ToolExecutor.go,
332+
mode: MothershipStreamV1ToolMode.sync,
333+
phase: MothershipStreamV1ToolPhase.call,
334+
ui: { hidden: true },
335+
},
336+
} satisfies StreamEvent,
337+
context,
338+
execContext,
339+
{ interactive: false, timeout: 1000 }
340+
)
341+
342+
expect(context.contentBlocks).toEqual([])
343+
expect(context.toolCalls.get('tool-ui-hidden')?.name).toBe('read')
344+
})
345+
346+
it('removes an existing content block when a later frame marks the tool hidden', async () => {
347+
await sseHandlers.tool(
348+
{
349+
type: MothershipStreamV1EventType.tool,
350+
payload: {
351+
toolCallId: 'tool-hidden-after-partial',
352+
toolName: 'read',
353+
executor: MothershipStreamV1ToolExecutor.go,
354+
mode: MothershipStreamV1ToolMode.sync,
355+
phase: MothershipStreamV1ToolPhase.call,
356+
status: 'generating',
357+
arguments: { path: 'components/integrations' },
358+
},
359+
} satisfies StreamEvent,
360+
context,
361+
execContext,
362+
{ interactive: false, timeout: 1000 }
363+
)
364+
expect(context.contentBlocks).toHaveLength(1)
365+
366+
await sseHandlers.tool(
367+
{
368+
type: MothershipStreamV1EventType.tool,
369+
payload: {
370+
toolCallId: 'tool-hidden-after-partial',
371+
toolName: 'read',
372+
executor: MothershipStreamV1ToolExecutor.go,
373+
mode: MothershipStreamV1ToolMode.sync,
374+
phase: MothershipStreamV1ToolPhase.call,
375+
arguments: { path: 'components/integrations/slack/README.md' },
376+
ui: { hidden: true },
377+
},
378+
} satisfies StreamEvent,
379+
context,
380+
execContext,
381+
{ interactive: false, timeout: 1000 }
382+
)
383+
384+
expect(context.contentBlocks).toEqual([])
385+
})
386+
387+
it('does not show pathless read or glob generating placeholders', async () => {
388+
for (const toolName of ['read', 'glob'] as const) {
389+
await sseHandlers.tool(
390+
{
391+
type: MothershipStreamV1EventType.tool,
392+
payload: {
393+
toolCallId: `${toolName}-generating`,
394+
toolName,
395+
executor: MothershipStreamV1ToolExecutor.go,
396+
mode: MothershipStreamV1ToolMode.sync,
397+
phase: MothershipStreamV1ToolPhase.call,
398+
status: 'generating',
399+
},
400+
} satisfies StreamEvent,
401+
context,
402+
execContext,
403+
{ interactive: false, timeout: 1000 }
404+
)
405+
}
406+
407+
expect(context.contentBlocks).toEqual([])
408+
expect(context.toolCalls.has('read-generating')).toBe(false)
409+
expect(context.toolCalls.has('glob-generating')).toBe(false)
410+
})
411+
323412
it('updates stored params when a subagent generating event is followed by the final tool call', async () => {
324413
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })
325414
context.subAgentParentToolCallId = 'parent-1'

apps/sim/lib/copilot/request/handlers/tool.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const logger = createLogger('CopilotToolHandler')
5151

5252
function applyToolDisplay(
5353
toolCall: ToolCallState | undefined,
54-
ui: { title?: string; phaseLabel?: string }
54+
ui: { title?: string; phaseLabel?: string; hidden?: boolean }
5555
): void {
5656
if (!toolCall) return
5757
const displayTitle = ui.title || ui.phaseLabel
@@ -235,6 +235,8 @@ async function handleCallPhase(
235235
const isSubagent = scope === 'subagent'
236236
const ui = getToolCallUI(data)
237237

238+
if (isPartial && shouldDelayVfsPlaceholder(toolName, args)) return
239+
238240
if (isSubagent) {
239241
if (wasToolResultSeen(toolCallId) || existing?.endTime) {
240242
if (existing && !existing.name && toolName) existing.name = toolName
@@ -308,23 +310,40 @@ async function handleCallPhase(
308310
)
309311
}
310312

313+
function shouldDelayVfsPlaceholder(
314+
toolName: string,
315+
args: Record<string, unknown> | undefined
316+
): boolean {
317+
return (toolName === 'read' || toolName === 'glob') && !args
318+
}
319+
320+
function removeToolCallContentBlock(context: StreamingContext, toolCallId: string): void {
321+
for (let i = context.contentBlocks.length - 1; i >= 0; i--) {
322+
const block = context.contentBlocks[i]
323+
if (block.type === 'tool_call' && block.toolCall?.id === toolCallId) {
324+
context.contentBlocks.splice(i, 1)
325+
}
326+
}
327+
}
328+
311329
function registerSubagentToolCall(
312330
context: StreamingContext,
313331
toolCallId: string,
314332
toolName: string,
315333
args: Record<string, unknown> | undefined,
316334
parentToolCallId: string,
317-
ui: { title?: string; phaseLabel?: string }
335+
ui: { title?: string; phaseLabel?: string; hidden?: boolean }
318336
): void {
319337
if (!context.subAgentToolCalls[parentToolCallId]) {
320338
context.subAgentToolCalls[parentToolCallId] = []
321339
}
322-
const hideFromUi = isToolHiddenInUi(toolName)
340+
const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true
323341
let toolCall = context.toolCalls.get(toolCallId)
324342
if (toolCall) {
325343
if (!toolCall.name && toolName) toolCall.name = toolName
326344
if (args && !toolCall.params) toolCall.params = args
327345
applyToolDisplay(toolCall, ui)
346+
if (hideFromUi) removeToolCallContentBlock(context, toolCallId)
328347
} else {
329348
toolCall = {
330349
id: toolCallId,
@@ -363,12 +382,16 @@ function registerMainToolCall(
363382
toolName: string,
364383
args: Record<string, unknown> | undefined,
365384
existing: ToolCallState | undefined,
366-
ui: { title?: string; phaseLabel?: string }
385+
ui: { title?: string; phaseLabel?: string; hidden?: boolean }
367386
): void {
368-
const hideFromUi = isToolHiddenInUi(toolName)
387+
const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true
369388
if (existing) {
370389
if (args && !existing.params) existing.params = args
371390
applyToolDisplay(existing, ui)
391+
if (hideFromUi) {
392+
removeToolCallContentBlock(context, toolCallId)
393+
return
394+
}
372395
if (
373396
!hideFromUi &&
374397
!context.contentBlocks.some((b) => b.type === 'tool_call' && b.toolCall?.id === toolCallId)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import {
3+
MothershipStreamV1EventType,
4+
MothershipStreamV1ToolExecutor,
5+
MothershipStreamV1ToolMode,
6+
MothershipStreamV1ToolPhase,
7+
} from '@/lib/copilot/generated/mothership-stream-v1'
8+
import { TOOL_CALL_STATUS } from '@/lib/copilot/request/session'
9+
import type { StreamEvent } from '@/lib/copilot/request/types'
10+
import { shouldSkipToolCallEvent } from './sse-utils'
11+
12+
describe('shouldSkipToolCallEvent', () => {
13+
it('skips pathless read and glob generating placeholders without marking the call seen', () => {
14+
const readEvent = toolCallEvent('read-generating-placeholder', 'read', undefined, true)
15+
const globEvent = toolCallEvent('glob-generating-placeholder', 'glob', undefined, true)
16+
17+
expect(shouldSkipToolCallEvent(readEvent)).toBe(true)
18+
expect(shouldSkipToolCallEvent(globEvent)).toBe(true)
19+
20+
expect(
21+
shouldSkipToolCallEvent(
22+
toolCallEvent('read-generating-placeholder', 'read', {
23+
path: 'components/integrations/slack/README.md',
24+
})
25+
)
26+
).toBe(false)
27+
expect(
28+
shouldSkipToolCallEvent(
29+
toolCallEvent('glob-generating-placeholder', 'glob', {
30+
pattern: 'components/blocks/*/README.md',
31+
})
32+
)
33+
).toBe(false)
34+
})
35+
36+
it('keeps non-vfs generating placeholders visible', () => {
37+
expect(
38+
shouldSkipToolCallEvent(toolCallEvent('search-generating-placeholder', 'search_online', undefined, true))
39+
).toBe(false)
40+
})
41+
})
42+
43+
function toolCallEvent(
44+
toolCallId: string,
45+
toolName: string,
46+
args?: Record<string, unknown>,
47+
generating = false
48+
): StreamEvent {
49+
return {
50+
type: MothershipStreamV1EventType.tool,
51+
payload: {
52+
toolCallId,
53+
toolName,
54+
executor: MothershipStreamV1ToolExecutor.go,
55+
mode: MothershipStreamV1ToolMode.sync,
56+
phase: MothershipStreamV1ToolPhase.call,
57+
...(generating ? { status: TOOL_CALL_STATUS.generating } : {}),
58+
...(args ? { arguments: args } : {}),
59+
},
60+
} satisfies StreamEvent
61+
}

apps/sim/lib/copilot/request/sse-utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function wasToolResultSeen(toolCallId: string): boolean {
5555

5656
export function shouldSkipToolCallEvent(event: StreamEvent): boolean {
5757
if (!isToolCallStreamEvent(event)) return false
58+
if (isPathlessVfsGeneratingEvent(event)) return true
5859
if (event.payload.status === TOOL_CALL_STATUS.generating) return false
5960
const toolCallId = getToolCallIdFromCallEvent(event)
6061
if (event.payload.partial === true) return false
@@ -63,6 +64,12 @@ export function shouldSkipToolCallEvent(event: StreamEvent): boolean {
6364
return false
6465
}
6566

67+
function isPathlessVfsGeneratingEvent(event: ToolCallStreamEvent): boolean {
68+
if (event.payload.status !== TOOL_CALL_STATUS.generating) return false
69+
if (event.payload.toolName !== 'read' && event.payload.toolName !== 'glob') return false
70+
return event.payload.arguments === undefined
71+
}
72+
6673
export function shouldSkipToolResultEvent(event: StreamEvent): boolean {
6774
return isToolResultStreamEvent(event) && wasToolResultSeen(getToolCallIdFromResultEvent(event))
6875
}

0 commit comments

Comments
 (0)