Skip to content

Commit b61089d

Browse files
committed
Fixes
1 parent d550934 commit b61089d

16 files changed

Lines changed: 310 additions & 15 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { sanitizeChatDisplayContent } from './chat-sanitize'
6+
7+
describe('sanitizeChatDisplayContent', () => {
8+
it('unwraps workspace resource tags from inline code spans', () => {
9+
const content =
10+
'`I updated <workspace_resource>{"type":"workflow","id":"wf-1","title":"Workflow"}</workspace_resource>.`'
11+
12+
expect(sanitizeChatDisplayContent(content)).toBe(
13+
'I updated <workspace_resource>{"type":"workflow","id":"wf-1","title":"Workflow"}</workspace_resource>.'
14+
)
15+
})
16+
17+
it('removes hidden internal references wrapped in inline code', () => {
18+
const content = 'Read `internal/tool-results/read-1.md` and found the issue.'
19+
20+
expect(sanitizeChatDisplayContent(content)).toBe('Read and found the issue.')
21+
})
22+
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SpecialTags,
1919
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
2020
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
21+
import { sanitizeChatDisplayContent } from './chat-sanitize'
2122

2223
const LANG_ALIASES: Record<string, string> = {
2324
js: 'javascript',
@@ -270,6 +271,8 @@ function ChatContentInner({
270271
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
271272
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
272273

274+
const displayContent = useMemo(() => sanitizeChatDisplayContent(content), [content])
275+
273276
useEffect(() => {
274277
const handler = (e: Event) => {
275278
const { type, id, title } = (e as CustomEvent).detail
@@ -279,7 +282,10 @@ function ChatContentInner({
279282
return () => window.removeEventListener('wsres-click', handler)
280283
}, [])
281284

282-
const parsed = useMemo(() => parseSpecialTags(content, isStreaming), [content, isStreaming])
285+
const parsed = useMemo(
286+
() => parseSpecialTags(displayContent, isStreaming),
287+
[displayContent, isStreaming]
288+
)
283289
const hasSpecialContent = parsed.hasPendingTag || parsed.segments.some((s) => s.type !== 'text')
284290

285291
if (hasSpecialContent) {
@@ -354,7 +360,7 @@ function ChatContentInner({
354360
return (
355361
<div className={cn(PROSE_CLASSES, '[&>:first-child]:mt-0 [&>:last-child]:mb-0')}>
356362
<Streamdown mode={isStreaming ? undefined : 'static'} components={MARKDOWN_COMPONENTS}>
357-
{content}
363+
{displayContent}
358364
</Streamdown>
359365
</div>
360366
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const HIDDEN_INLINE_REFERENCE_PATTERN =
2+
/`[^`\n]*(?:internal\/tool-results\/|internal\/blocktips\/|components\/integrations\/[^`\n]*README)[^`\n]*`/g
3+
const WORKSPACE_RESOURCE_CODE_SPAN_PATTERN =
4+
/`([^`\n]*<workspace_resource>[\s\S]*?<\/workspace_resource>[^`\n]*)`/g
5+
6+
export function sanitizeChatDisplayContent(content: string): string {
7+
return content
8+
.replace(WORKSPACE_RESOURCE_CODE_SPAN_PATTERN, '$1')
9+
.replace(HIDDEN_INLINE_REFERENCE_PATTERN, '')
10+
.replace(/`(\s*<workspace_resource>)/g, '$1')
11+
.replace(/(<\/workspace_resource>\s*)`/g, '$1')
12+
}

apps/sim/lib/copilot/generated/tool-catalog-v1.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,25 +3067,31 @@ export const TouchPlan: ToolCatalogEntry = {
30673067
description:
30683068
'Plan file name or relative path under .plans, e.g. "implementation.md" or "phase-1/implementation.md". If no extension is supplied, ".md" is appended.',
30693069
},
3070+
scope: {
3071+
type: 'string',
3072+
description:
3073+
'Plan scope. Use "workspace" for root .plans/** main-agent plans. Use "workflow" for workflows/{workflow}/.plans/** subplans. If omitted with workflowPath, workflow scope is assumed; otherwise workspace scope is assumed.',
3074+
enum: ['workspace', 'workflow'],
3075+
},
30703076
title: {
30713077
type: 'string',
30723078
description: 'Optional short user-visible label for the plan creation.',
30733079
},
30743080
workflowPath: {
30753081
type: 'string',
30763082
description:
3077-
'Canonical workflow VFS path, e.g. "workflows/My%20Workflow" or "workflows/Folder/My%20Workflow". Copy from glob/read output; do not use workflow IDs.',
3083+
'Required for scope "workflow". Canonical workflow VFS path, e.g. "workflows/My%20Workflow" or "workflows/Folder/My%20Workflow". Copy from glob/read output; do not use workflow IDs.',
30783084
},
30793085
},
3080-
required: ['workflowPath', 'name'],
3086+
required: ['name'],
30813087
},
30823088
resultSchema: {
30833089
type: 'object',
30843090
properties: {
30853091
data: {
30863092
type: 'object',
30873093
description:
3088-
'Contains id, name, vfsPath, backingVfsPath, and workflowId. Use vfsPath for follow-up workspace_file calls.',
3094+
'Contains id, name, scope, vfsPath, backingVfsPath, and workflowId for workflow plans. Use vfsPath for follow-up workspace_file calls.',
30893095
},
30903096
message: { type: 'string', description: 'Human-readable outcome.' },
30913097
success: { type: 'boolean', description: 'Whether the plan file was created.' },

apps/sim/lib/copilot/generated/tool-schemas-v1.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2816,25 +2816,31 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
28162816
description:
28172817
'Plan file name or relative path under .plans, e.g. "implementation.md" or "phase-1/implementation.md". If no extension is supplied, ".md" is appended.',
28182818
},
2819+
scope: {
2820+
type: 'string',
2821+
description:
2822+
'Plan scope. Use "workspace" for root .plans/** main-agent plans. Use "workflow" for workflows/{workflow}/.plans/** subplans. If omitted with workflowPath, workflow scope is assumed; otherwise workspace scope is assumed.',
2823+
enum: ['workspace', 'workflow'],
2824+
},
28192825
title: {
28202826
type: 'string',
28212827
description: 'Optional short user-visible label for the plan creation.',
28222828
},
28232829
workflowPath: {
28242830
type: 'string',
28252831
description:
2826-
'Canonical workflow VFS path, e.g. "workflows/My%20Workflow" or "workflows/Folder/My%20Workflow". Copy from glob/read output; do not use workflow IDs.',
2832+
'Required for scope "workflow". Canonical workflow VFS path, e.g. "workflows/My%20Workflow" or "workflows/Folder/My%20Workflow". Copy from glob/read output; do not use workflow IDs.',
28272833
},
28282834
},
2829-
required: ['workflowPath', 'name'],
2835+
required: ['name'],
28302836
},
28312837
resultSchema: {
28322838
type: 'object',
28332839
properties: {
28342840
data: {
28352841
type: 'object',
28362842
description:
2837-
'Contains id, name, vfsPath, backingVfsPath, and workflowId. Use vfsPath for follow-up workspace_file calls.',
2843+
'Contains id, name, scope, vfsPath, backingVfsPath, and workflowId for workflow plans. Use vfsPath for follow-up workspace_file calls.',
28382844
},
28392845
message: {
28402846
type: 'string',

apps/sim/lib/copilot/request/context/request-context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export function createStreamingContext(overrides?: Partial<StreamingContext>): S
1212
runId: undefined,
1313
messageId: generateId(),
1414
accumulatedContent: '',
15+
finalAssistantContent: '',
16+
sawMainToolCall: false,
1517
contentBlocks: [],
1618
toolCalls: new Map(),
1719
pendingToolPromises: new Map(),

apps/sim/lib/copilot/request/context/result.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function makeContext(): StreamingContext {
1616
runId: undefined,
1717
messageId: 'msg-1',
1818
accumulatedContent: '',
19+
finalAssistantContent: '',
20+
sawMainToolCall: false,
1921
contentBlocks: [],
2022
toolCalls: new Map(),
2123
pendingToolPromises: new Map(),

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ vi.mock('@/lib/copilot/request/session', async () => {
1818
return {
1919
...actual,
2020
hasAbortMarker: vi.fn().mockResolvedValue(false),
21+
upsertFilePreviewSession: vi.fn(async (session) => session),
2122
}
2223
})
2324

@@ -27,6 +28,16 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
2728
resolveWorkspaceFileReference: resolveWorkspaceFileReferenceMock,
2829
}))
2930

31+
vi.mock('@/lib/copilot/tools/server/files/file-preview', async () => {
32+
const actual = await vi.importActual<
33+
typeof import('@/lib/copilot/tools/server/files/file-preview')
34+
>('@/lib/copilot/tools/server/files/file-preview')
35+
return {
36+
...actual,
37+
loadWorkspaceFileTextForPreview: vi.fn().mockResolvedValue(''),
38+
}
39+
})
40+
3041
import {
3142
buildPreviewContentUpdate,
3243
decodeJsonStringPrefix,
@@ -77,6 +88,8 @@ function createStreamingContext(): StreamingContext {
7788
return {
7889
messageId: 'msg-1',
7990
accumulatedContent: '',
91+
finalAssistantContent: '',
92+
sawMainToolCall: false,
8093
contentBlocks: [],
8194
toolCalls: new Map(),
8295
pendingToolPromises: new Map(),
@@ -263,7 +276,10 @@ describe('copilot go stream helpers', () => {
263276

264277
const previewEvents = onEvent.mock.calls
265278
.map(([event]) => event)
266-
.filter((event) => event.type === MothershipStreamV1EventType.tool && 'previewPhase' in event.payload)
279+
.filter(
280+
(event) =>
281+
event.type === MothershipStreamV1EventType.tool && 'previewPhase' in event.payload
282+
)
267283

268284
expect(previewEvents.map((event) => event.payload.previewPhase)).toEqual([
269285
'file_preview_start',
@@ -379,7 +395,8 @@ describe('copilot go stream helpers', () => {
379395
const previewEvents = onEvent.mock.calls
380396
.map(([event]) => event)
381397
.filter(
382-
(event) => event.type === MothershipStreamV1EventType.tool && 'previewPhase' in event.payload
398+
(event) =>
399+
event.type === MothershipStreamV1EventType.tool && 'previewPhase' in event.payload
383400
)
384401

385402
expect(previewEvents.map((event) => event.payload.previewPhase)).toEqual([

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ describe('sse-handlers tool lifecycle', () => {
8484
chatId: undefined,
8585
messageId: 'msg-1',
8686
accumulatedContent: '',
87+
finalAssistantContent: '',
88+
sawMainToolCall: false,
8789
trace: new TraceCollector(),
8890
contentBlocks: [],
8991
toolCalls: new Map(),
@@ -105,6 +107,54 @@ describe('sse-handlers tool lifecycle', () => {
105107
}
106108
})
107109

110+
it('keeps only the latest post-tool assistant text for headless final content', async () => {
111+
await sseHandlers.text(
112+
{
113+
type: MothershipStreamV1EventType.text,
114+
payload: {
115+
channel: MothershipStreamV1TextChannel.assistant,
116+
text: 'I will check that.',
117+
},
118+
} satisfies StreamEvent,
119+
context,
120+
execContext,
121+
{ interactive: false }
122+
)
123+
124+
await sseHandlers.tool(
125+
{
126+
type: MothershipStreamV1EventType.tool,
127+
payload: {
128+
toolCallId: 'tool-1',
129+
toolName: ReadTool.id,
130+
arguments: { path: 'foo.txt' },
131+
executor: MothershipStreamV1ToolExecutor.sim,
132+
mode: MothershipStreamV1ToolMode.async,
133+
phase: MothershipStreamV1ToolPhase.call,
134+
},
135+
} satisfies StreamEvent,
136+
context,
137+
execContext,
138+
{ interactive: false, autoExecuteTools: false }
139+
)
140+
141+
await sseHandlers.text(
142+
{
143+
type: MothershipStreamV1EventType.text,
144+
payload: {
145+
channel: MothershipStreamV1TextChannel.assistant,
146+
text: 'Final answer only.',
147+
},
148+
} satisfies StreamEvent,
149+
context,
150+
execContext,
151+
{ interactive: false }
152+
)
153+
154+
expect(context.accumulatedContent).toBe('I will check that.Final answer only.')
155+
expect(context.finalAssistantContent).toBe('Final answer only.')
156+
})
157+
108158
it('executes tool_call and emits tool_result', async () => {
109159
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })
110160
const onEvent = vi.fn()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function handleTextEvent(scope: ToolScope): StreamHandler {
6868
flushThinkingBlock(context)
6969
}
7070
context.accumulatedContent += chunk
71+
context.finalAssistantContent += chunk
7172
addContentBlock(context, { type: 'text', content: chunk })
7273
}
7374
}

0 commit comments

Comments
 (0)