Skip to content

Commit 950992b

Browse files
committed
improvement(copilot): clarify VFS tool-call titles
1 parent 73ff4b7 commit 950992b

6 files changed

Lines changed: 146 additions & 15 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,7 @@ function decodeStreamingString(value: string): string {
237237
}
238238

239239
function matchStreamingStringArg(streamingArgs: string, key: string): string | undefined {
240-
const match = streamingArgs.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`, 'm'))
241-
return match?.[1] ? decodeStreamingString(match[1]) : undefined
240+
return extractStreamingStringArgument(streamingArgs, key)
242241
}
243242

244243
function resolveStreamingManagedResourceTitle(
@@ -305,13 +304,14 @@ export function resolveStreamingToolDisplayTitle(
305304
}
306305

307306
if (name === Grep.id) {
308-
const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle')
309-
return toolTitle ? `Searching for ${toolTitle}` : undefined
307+
const pattern = matchStreamingStringArg(streamingArgs, 'pattern')
308+
if (!pattern) return undefined
309+
const path = matchStreamingStringArg(streamingArgs, 'path')
310+
return getToolDisplayTitle(name, { pattern, path })
310311
}
311312

312313
if (name === Glob.id) {
313-
const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle')
314-
return toolTitle ? `Finding ${toolTitle}` : undefined
314+
return getToolDisplayTitle(name)
315315
}
316316

317317
if (name === 'mv') {

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ describe('streaming resource titles', () => {
6464
expect(resolveStreamingToolDisplayTitle('rm', '{"toolTitle":"Old Report.pdf"}')).toBe(
6565
'Deleting Old Report.pdf'
6666
)
67+
expect(resolveStreamingToolDisplayTitle('glob', '{"pattern":"docs/**"}')).toBe(
68+
'Exploring Internal Knowledge Base'
69+
)
70+
expect(
71+
resolveStreamingToolDisplayTitle('grep', '{"path":"docs/self-hosting.mdx","pattern":"BYOK"}')
72+
).toBe('Skimming Docs Page: self-hosting')
73+
expect(resolveStreamingToolDisplayTitle('grep', '{"pattern":"BYOK"}')).toBe(
74+
'Searching Internal Knowledge Base for BYOK'
75+
)
76+
expect(
77+
resolveStreamingToolDisplayTitle(
78+
'grep',
79+
JSON.stringify({ path: 'docs/self-hosting.mdx', pattern: '"BYOK"' })
80+
)
81+
).toBe('Skimming Docs Page: self-hosting')
6782
})
6883
})
6984

apps/sim/lib/copilot/tools/client/store-utils.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ describe('resolveToolDisplay', () => {
4949
).toBe('Read RET XYZ')
5050
})
5151

52+
it('formats docs corpus reads as Docs Page: Section/page', () => {
53+
expect(
54+
resolveToolDisplay(ReadTool.id, ClientToolCallState.success, {
55+
path: 'docs/workflows/blocks/agent.mdx',
56+
})?.text
57+
).toBe('Read Docs Page: workflows/agent')
58+
59+
expect(
60+
resolveToolDisplay(ReadTool.id, ClientToolCallState.executing, {
61+
path: 'docs/integrations/gmail.mdx',
62+
})?.text
63+
).toBe('Reading Docs Page: integrations/gmail')
64+
65+
expect(
66+
resolveToolDisplay(ReadTool.id, ClientToolCallState.error, {
67+
path: 'docs/getting-started.mdx',
68+
})?.text
69+
).toBe('Attempted to read Docs Page: getting-started')
70+
})
71+
5272
it('decodes percent-encoded VFS path segments for display', () => {
5373
expect(
5474
resolveToolDisplay(ReadTool.id, ClientToolCallState.executing, {

apps/sim/lib/copilot/tools/client/store-utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types'
66
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
77
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'
88
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
9-
import { humanizeDisplayIdentifier, humanizeToolName } from '@/lib/copilot/tools/tool-display'
9+
import {
10+
docsPageLabel,
11+
humanizeDisplayIdentifier,
12+
humanizeToolName,
13+
} from '@/lib/copilot/tools/tool-display'
1014
import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils'
1115

1216
/** Respond tools are internal handoff tools shown with a friendly generic label. */
@@ -97,6 +101,11 @@ function describeReadTarget(path: string | undefined): string | undefined {
97101

98102
if (segments.length === 0) return undefined
99103

104+
if (segments[0] === 'docs') {
105+
const label = docsPageLabel(path)
106+
return label ? `Docs Page: ${label}` : 'docs'
107+
}
108+
100109
const resourceType = VFS_DIR_TO_RESOURCE[segments[0]]
101110
if (!resourceType) {
102111
return humanizeDisplayIdentifier(stripExtension(segments[segments.length - 1]), 'sentence')

apps/sim/lib/copilot/tools/tool-display.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,58 @@ describe('getToolDisplayTitle natural-language coverage', () => {
7878
expect(getToolDisplayTitle('diff_workflows')).toBe('Comparing workflows')
7979
})
8080

81+
it('uses a deterministic internal-knowledge title for glob', () => {
82+
expect(getToolDisplayTitle('glob')).toBe('Exploring Internal Knowledge Base')
83+
expect(getToolDisplayTitle('glob', { toolTitle: 'docs corpus manifest' })).toBe(
84+
'Exploring Internal Knowledge Base'
85+
)
86+
expect(getToolCompletedTitle(getToolDisplayTitle('glob'))).toBe(
87+
'Explored Internal Knowledge Base'
88+
)
89+
})
90+
91+
it('titles grep from its path and pattern rather than a generated summary', () => {
92+
expect(
93+
getToolDisplayTitle('grep', {
94+
path: 'docs/self-hosting.mdx',
95+
pattern: 'BYOK',
96+
toolTitle: 'BYOK in documentation',
97+
})
98+
).toBe('Skimming Docs Page: self-hosting')
99+
expect(
100+
getToolDisplayTitle('grep', {
101+
path: 'docs/workflows/blocks/agent.mdx',
102+
pattern: 'apiKey',
103+
})
104+
).toBe('Skimming Docs Page: workflows/agent')
105+
expect(
106+
getToolDisplayTitle('grep', {
107+
path: 'files/Q4%20Report.pdf/content',
108+
pattern: 'revenue',
109+
})
110+
).toBe('Searching Q4 Report for revenue')
111+
expect(getToolDisplayTitle('grep', { pattern: 'BYOK' })).toBe(
112+
'Searching Internal Knowledge Base for BYOK'
113+
)
114+
expect(getToolDisplayTitle('grep', { path: 'workflows/', pattern: 'slack' })).toBe(
115+
'Searching workflows for slack'
116+
)
117+
expect(
118+
getToolCompletedTitle(
119+
getToolDisplayTitle('grep', { path: 'docs/self-hosting.mdx', pattern: 'BYOK' })
120+
)
121+
).toBe('Skimmed Docs Page: self-hosting')
122+
})
123+
124+
it('uses fetch wording for page-content retrieval', () => {
125+
const title = getToolDisplayTitle('web_fetch', {
126+
urls: ['https://example.com', 'https://example.org'],
127+
})
128+
129+
expect(title).toBe('Fetching 2 pages')
130+
expect(getToolStatusDisplayTitle(title, 'success')).toBe('Fetched 2 pages')
131+
})
132+
81133
it('falls back to running code for run_function without a title', () => {
82134
expect(getToolDisplayTitle('run_function')).toBe('Running code')
83135
expect(getToolDisplayTitle('run_function', { title: 'Crunching numbers' })).toBe(

apps/sim/lib/copilot/tools/tool-display.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { stripVersionSuffix } from '@sim/utils/string'
1+
import { stripVersionSuffix, truncate } from '@sim/utils/string'
22

33
/**
44
* Single source of truth for copilot tool-call display titles.
@@ -115,6 +115,40 @@ function pathLeaf(path: string): string {
115115
return decodePathSegment(leaf)
116116
}
117117

118+
/** Returns the final path segment without a file extension or virtual content suffix. */
119+
function pathStem(path: string): string {
120+
const normalized = path.replace(/\/+$/, '').replace(/\/content$/, '')
121+
const leaf = pathLeaf(normalized)
122+
const extensionIndex = leaf.lastIndexOf('.')
123+
return extensionIndex > 0 ? leaf.slice(0, extensionIndex) : leaf
124+
}
125+
126+
/**
127+
* Labels a docs corpus page as `<section>/<page>` (docs/workflows/blocks/agent.mdx
128+
* → `workflows/agent`); a top-level page is just its stem (docs/getting-started.mdx
129+
* → `getting-started`). Returns null when the path is not a docs page.
130+
*/
131+
export function docsPageLabel(path: string): string | null {
132+
const segments = path
133+
.split('/')
134+
.map((segment) => segment.trim())
135+
.filter(Boolean)
136+
.map(decodePathSegment)
137+
if (segments[0] !== 'docs' || segments.length < 2) return null
138+
const leaf = pathStem(path)
139+
if (segments.length === 2) return leaf
140+
return `${segments[1]}/${leaf}`
141+
}
142+
143+
function grepTitle(args: ToolArgs): string {
144+
const path = stringArg(args, 'path')
145+
const docsPage = docsPageLabel(path)
146+
if (docsPage) return `Skimming Docs Page: ${docsPage}`
147+
const target = pathStem(path) || 'Internal Knowledge Base'
148+
const pattern = truncate(stringArg(args, 'pattern').replace(/\s+/g, ' '), 60)
149+
return pattern ? `Searching ${target} for ${pattern}` : `Searching ${target}`
150+
}
151+
118152
function summarizeTargets(targets: string[], fallback: string): string {
119153
const normalized = targets.map((target) => target.trim()).filter(Boolean)
120154
if (normalized.length === 0) return fallback
@@ -805,12 +839,10 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
805839
return target ? `Searching online for ${target}` : 'Searching online'
806840
}
807841
case 'grep': {
808-
const target = firstStringArg(args, 'toolTitle', 'title')
809-
return target ? `Searching for ${target}` : 'Searching'
842+
return grepTitle(args)
810843
}
811844
case 'glob': {
812-
const target = firstStringArg(args, 'toolTitle', 'title')
813-
return target ? `Finding ${target}` : 'Finding files'
845+
return 'Exploring Internal Knowledge Base'
814846
}
815847
case 'mv': {
816848
const sources = stringArrayArg(args, 'sources')
@@ -893,9 +925,9 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
893925
}
894926
case 'web_fetch': {
895927
const urls = stringArrayArg(args, 'urls')
896-
if (urls.length === 1) return `Getting ${urls[0]}`
897-
if (urls.length > 1) return `Getting ${urls.length} pages`
898-
return 'Getting page contents'
928+
if (urls.length === 1) return `Fetching ${urls[0]}`
929+
if (urls.length > 1) return `Fetching ${urls.length} pages`
930+
return 'Fetching page contents'
899931
}
900932
case 'manage_custom_tool': {
901933
const schema = args?.schema
@@ -998,8 +1030,10 @@ const COMPLETED_VERB_REWRITES: Record<string, string> = {
9981030
Editing: 'Edited',
9991031
Enabling: 'Enabled',
10001032
Executing: 'Executed',
1033+
Exploring: 'Explored',
10011034
Extracting: 'Extracted',
10021035
Fading: 'Faded',
1036+
Fetching: 'Fetched',
10031037
Finding: 'Found',
10041038
Gathering: 'Gathered',
10051039
Generating: 'Generated',
@@ -1035,6 +1069,7 @@ const COMPLETED_VERB_REWRITES: Record<string, string> = {
10351069
Selecting: 'Selected',
10361070
Setting: 'Set',
10371071
Sharing: 'Shared',
1072+
Skimming: 'Skimmed',
10381073
Stopping: 'Stopped',
10391074
Summarizing: 'Summarized',
10401075
Switching: 'Switched',

0 commit comments

Comments
 (0)