Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

Expand Down
20 changes: 20 additions & 0 deletions apps/sim/lib/copilot/tools/client/store-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
11 changes: 10 additions & 1 deletion apps/sim/lib/copilot/tools/client/store-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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')
Expand Down
52 changes: 52 additions & 0 deletions apps/sim/lib/copilot/tools/tool-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
51 changes: 43 additions & 8 deletions apps/sim/lib/copilot/tools/tool-display.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 `<section>/<page>` (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
Expand Down Expand Up @@ -805,12 +839,10 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
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')
Expand Down Expand Up @@ -893,9 +925,9 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
}
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
Expand Down Expand Up @@ -998,8 +1030,10 @@ const COMPLETED_VERB_REWRITES: Record<string, string> = {
Editing: 'Edited',
Enabling: 'Enabled',
Executing: 'Executed',
Exploring: 'Explored',
Extracting: 'Extracted',
Fading: 'Faded',
Fetching: 'Fetched',
Finding: 'Found',
Gathering: 'Gathered',
Generating: 'Generated',
Expand Down Expand Up @@ -1035,6 +1069,7 @@ const COMPLETED_VERB_REWRITES: Record<string, string> = {
Selecting: 'Selected',
Setting: 'Set',
Sharing: 'Shared',
Skimming: 'Skimmed',
Stopping: 'Stopped',
Summarizing: 'Summarized',
Switching: 'Switched',
Expand Down