diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 2abb9ef6a7f..2e6e00c87be 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -237,8 +237,7 @@ function decodeStreamingString(value: string): string { } function matchStreamingStringArg(streamingArgs: string, key: string): string | undefined { - const match = streamingArgs.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`, 'm')) - return match?.[1] ? decodeStreamingString(match[1]) : undefined + return extractStreamingStringArgument(streamingArgs, key) } function resolveStreamingManagedResourceTitle( @@ -305,13 +304,14 @@ export function resolveStreamingToolDisplayTitle( } if (name === Grep.id) { - const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') - return toolTitle ? `Searching for ${toolTitle}` : undefined + const pattern = matchStreamingStringArg(streamingArgs, 'pattern') + if (!pattern) return undefined + const path = matchStreamingStringArg(streamingArgs, 'path') + return getToolDisplayTitle(name, { pattern, path }) } if (name === Glob.id) { - const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') - return toolTitle ? `Finding ${toolTitle}` : undefined + return getToolDisplayTitle(name) } if (name === 'mv') { diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts index a764b32fa9b..a0eb8b27bf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts @@ -64,6 +64,21 @@ describe('streaming resource titles', () => { expect(resolveStreamingToolDisplayTitle('rm', '{"toolTitle":"Old Report.pdf"}')).toBe( 'Deleting Old Report.pdf' ) + expect(resolveStreamingToolDisplayTitle('glob', '{"pattern":"docs/**"}')).toBe( + 'Exploring Internal Knowledge Base' + ) + expect( + resolveStreamingToolDisplayTitle('grep', '{"path":"docs/self-hosting.mdx","pattern":"BYOK"}') + ).toBe('Skimming Docs Page: self-hosting') + expect(resolveStreamingToolDisplayTitle('grep', '{"pattern":"BYOK"}')).toBe( + 'Searching Internal Knowledge Base for BYOK' + ) + expect( + resolveStreamingToolDisplayTitle( + 'grep', + JSON.stringify({ path: 'docs/self-hosting.mdx', pattern: '"BYOK"' }) + ) + ).toBe('Skimming Docs Page: self-hosting') }) }) diff --git a/apps/sim/lib/copilot/tools/client/store-utils.test.ts b/apps/sim/lib/copilot/tools/client/store-utils.test.ts index 788b42781e6..4970be668be 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.test.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.test.ts @@ -49,6 +49,26 @@ describe('resolveToolDisplay', () => { ).toBe('Read RET XYZ') }) + it('formats docs corpus reads as Docs Page: Section/page', () => { + expect( + resolveToolDisplay(ReadTool.id, ClientToolCallState.success, { + path: 'docs/workflows/blocks/agent.mdx', + })?.text + ).toBe('Read Docs Page: workflows/agent') + + expect( + resolveToolDisplay(ReadTool.id, ClientToolCallState.executing, { + path: 'docs/integrations/gmail.mdx', + })?.text + ).toBe('Reading Docs Page: integrations/gmail') + + expect( + resolveToolDisplay(ReadTool.id, ClientToolCallState.error, { + path: 'docs/getting-started.mdx', + })?.text + ).toBe('Attempted to read Docs Page: getting-started') + }) + it('decodes percent-encoded VFS path segments for display', () => { expect( resolveToolDisplay(ReadTool.id, ClientToolCallState.executing, { diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts index 343c9e2712d..7f5fd79cd13 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.ts @@ -6,7 +6,11 @@ import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' -import { humanizeDisplayIdentifier, humanizeToolName } from '@/lib/copilot/tools/tool-display' +import { + docsPageLabel, + humanizeDisplayIdentifier, + humanizeToolName, +} from '@/lib/copilot/tools/tool-display' import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils' /** Respond tools are internal handoff tools shown with a friendly generic label. */ @@ -97,6 +101,11 @@ function describeReadTarget(path: string | undefined): string | undefined { if (segments.length === 0) return undefined + if (segments[0] === 'docs') { + const label = docsPageLabel(path) + return label ? `Docs Page: ${label}` : 'docs' + } + const resourceType = VFS_DIR_TO_RESOURCE[segments[0]] if (!resourceType) { return humanizeDisplayIdentifier(stripExtension(segments[segments.length - 1]), 'sentence') diff --git a/apps/sim/lib/copilot/tools/tool-display.test.ts b/apps/sim/lib/copilot/tools/tool-display.test.ts index 31520688721..628e4f6c39d 100644 --- a/apps/sim/lib/copilot/tools/tool-display.test.ts +++ b/apps/sim/lib/copilot/tools/tool-display.test.ts @@ -78,6 +78,58 @@ describe('getToolDisplayTitle natural-language coverage', () => { expect(getToolDisplayTitle('diff_workflows')).toBe('Comparing workflows') }) + it('uses a deterministic internal-knowledge title for glob', () => { + expect(getToolDisplayTitle('glob')).toBe('Exploring Internal Knowledge Base') + expect(getToolDisplayTitle('glob', { toolTitle: 'docs corpus manifest' })).toBe( + 'Exploring Internal Knowledge Base' + ) + expect(getToolCompletedTitle(getToolDisplayTitle('glob'))).toBe( + 'Explored Internal Knowledge Base' + ) + }) + + it('titles grep from its path and pattern rather than a generated summary', () => { + expect( + getToolDisplayTitle('grep', { + path: 'docs/self-hosting.mdx', + pattern: 'BYOK', + toolTitle: 'BYOK in documentation', + }) + ).toBe('Skimming Docs Page: self-hosting') + expect( + getToolDisplayTitle('grep', { + path: 'docs/workflows/blocks/agent.mdx', + pattern: 'apiKey', + }) + ).toBe('Skimming Docs Page: workflows/agent') + expect( + getToolDisplayTitle('grep', { + path: 'files/Q4%20Report.pdf/content', + pattern: 'revenue', + }) + ).toBe('Searching Q4 Report for revenue') + expect(getToolDisplayTitle('grep', { pattern: 'BYOK' })).toBe( + 'Searching Internal Knowledge Base for BYOK' + ) + expect(getToolDisplayTitle('grep', { path: 'workflows/', pattern: 'slack' })).toBe( + 'Searching workflows for slack' + ) + expect( + getToolCompletedTitle( + getToolDisplayTitle('grep', { path: 'docs/self-hosting.mdx', pattern: 'BYOK' }) + ) + ).toBe('Skimmed Docs Page: self-hosting') + }) + + it('uses fetch wording for page-content retrieval', () => { + const title = getToolDisplayTitle('web_fetch', { + urls: ['https://example.com', 'https://example.org'], + }) + + expect(title).toBe('Fetching 2 pages') + expect(getToolStatusDisplayTitle(title, 'success')).toBe('Fetched 2 pages') + }) + it('falls back to running code for run_function without a title', () => { expect(getToolDisplayTitle('run_function')).toBe('Running code') expect(getToolDisplayTitle('run_function', { title: 'Crunching numbers' })).toBe( diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts index 61a3096cb32..ee7c286faba 100644 --- a/apps/sim/lib/copilot/tools/tool-display.ts +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -1,4 +1,4 @@ -import { stripVersionSuffix } from '@sim/utils/string' +import { stripVersionSuffix, truncate } from '@sim/utils/string' /** * Single source of truth for copilot tool-call display titles. @@ -115,6 +115,40 @@ function pathLeaf(path: string): string { return decodePathSegment(leaf) } +/** Returns the final path segment without a file extension or virtual content suffix. */ +function pathStem(path: string): string { + const normalized = path.replace(/\/+$/, '').replace(/\/content$/, '') + const leaf = pathLeaf(normalized) + const extensionIndex = leaf.lastIndexOf('.') + return extensionIndex > 0 ? leaf.slice(0, extensionIndex) : leaf +} + +/** + * Labels a docs corpus page as `
/` (docs/workflows/blocks/agent.mdx + * → `workflows/agent`); a top-level page is just its stem (docs/getting-started.mdx + * → `getting-started`). Returns null when the path is not a docs page. + */ +export function docsPageLabel(path: string): string | null { + const segments = path + .split('/') + .map((segment) => segment.trim()) + .filter(Boolean) + .map(decodePathSegment) + if (segments[0] !== 'docs' || segments.length < 2) return null + const leaf = pathStem(path) + if (segments.length === 2) return leaf + return `${segments[1]}/${leaf}` +} + +function grepTitle(args: ToolArgs): string { + const path = stringArg(args, 'path') + const docsPage = docsPageLabel(path) + if (docsPage) return `Skimming Docs Page: ${docsPage}` + const target = pathStem(path) || 'Internal Knowledge Base' + const pattern = truncate(stringArg(args, 'pattern').replace(/\s+/g, ' '), 60) + return pattern ? `Searching ${target} for ${pattern}` : `Searching ${target}` +} + function summarizeTargets(targets: string[], fallback: string): string { const normalized = targets.map((target) => target.trim()).filter(Boolean) if (normalized.length === 0) return fallback @@ -805,12 +839,10 @@ export function getToolDisplayTitle(name: string, args?: Record return target ? `Searching online for ${target}` : 'Searching online' } case 'grep': { - const target = firstStringArg(args, 'toolTitle', 'title') - return target ? `Searching for ${target}` : 'Searching' + return grepTitle(args) } case 'glob': { - const target = firstStringArg(args, 'toolTitle', 'title') - return target ? `Finding ${target}` : 'Finding files' + return 'Exploring Internal Knowledge Base' } case 'mv': { const sources = stringArrayArg(args, 'sources') @@ -893,9 +925,9 @@ export function getToolDisplayTitle(name: string, args?: Record } case 'web_fetch': { const urls = stringArrayArg(args, 'urls') - if (urls.length === 1) return `Getting ${urls[0]}` - if (urls.length > 1) return `Getting ${urls.length} pages` - return 'Getting page contents' + if (urls.length === 1) return `Fetching ${urls[0]}` + if (urls.length > 1) return `Fetching ${urls.length} pages` + return 'Fetching page contents' } case 'manage_custom_tool': { const schema = args?.schema @@ -998,8 +1030,10 @@ const COMPLETED_VERB_REWRITES: Record = { Editing: 'Edited', Enabling: 'Enabled', Executing: 'Executed', + Exploring: 'Explored', Extracting: 'Extracted', Fading: 'Faded', + Fetching: 'Fetched', Finding: 'Found', Gathering: 'Gathered', Generating: 'Generated', @@ -1035,6 +1069,7 @@ const COMPLETED_VERB_REWRITES: Record = { Selecting: 'Selected', Setting: 'Set', Sharing: 'Shared', + Skimming: 'Skimmed', Stopping: 'Stopped', Summarizing: 'Summarized', Switching: 'Switched',