Skip to content

Commit 7c58fe1

Browse files
committed
Name the target resource in the remaining tool titles
Table tools keep their operands nested under args and identify the table by id, so their rows said 'Adding rows' with no hint where: enrichment now lifts the nested args and resolves tableId against the cached workspace table list, giving 'Adding rows to Runtimes', 'Adding column status in Runtimes', 'Reading views of Runtimes'. Also: a block-schema read names the block instead of the file ('Loading Slack', 'Loading Google Sheets tips'); browser type/insert show the text they send, middle-ellipsized; downloads name the file; library-docs searches name the library and query; knowledge-base searches include the query; generated media names its output file; and diff_workflows, list_deployment_versions, and publish_custom_block joined the workflow-name enrichment set.
1 parent 0524a1a commit 7c58fe1

3 files changed

Lines changed: 211 additions & 38 deletions

File tree

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import {
2929
} from '@/lib/copilot/generated/tool-catalog-v1'
3030
import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args'
3131
import { getToolDisplayTitle, mvDisplayVerb } from '@/lib/copilot/tools/tool-display'
32+
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
3233
import type { ContentBlock } from '@/app/workspace/[workspaceId]/home/types'
3334
import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
35+
import { tableKeys } from '@/hooks/queries/utils/table-keys'
3436
import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache'
3537
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
3638
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -123,6 +125,33 @@ function resolveTargetWorkflowName(args: Record<string, unknown> | undefined): s
123125
return resolveWorkflowNameForDisplay(args?.workflowId ?? registry.hydration.workflowId)
124126
}
125127

128+
/**
129+
* Table name for a nested `args.tableId`. Tables reach the client through
130+
* React Query rather than a Zustand store, so the cached workspace list is
131+
* the synchronous source a title can read; an uncached id simply stays
132+
* unnamed rather than blocking the row.
133+
*/
134+
function resolveTableNameForDisplay(tableId: unknown): string | undefined {
135+
const id = stringParam(tableId)
136+
if (!id) return undefined
137+
const cache = getQueryClient().getQueryCache()
138+
for (const query of cache.findAll({ queryKey: tableKeys.lists() })) {
139+
const data = query.state.data
140+
const tables = Array.isArray(data)
141+
? data
142+
: isRecordLike(data) && Array.isArray((data as { tables?: unknown }).tables)
143+
? ((data as { tables: unknown[] }).tables as unknown[])
144+
: []
145+
for (const table of tables) {
146+
if (!isRecordLike(table)) continue
147+
if (stringParam(table.id) !== id) continue
148+
const name = stringParam(table.name)
149+
if (name) return name
150+
}
151+
}
152+
return undefined
153+
}
154+
126155
function resolveBlockNameForDisplay(blockId: unknown): string | undefined {
127156
const id = stringParam(blockId)
128157
if (!id) return undefined
@@ -191,8 +220,20 @@ export function resolveIntegrationToolDisplayTitle(tool: {
191220
* the current workflow), so their titles can only name the workflow once the
192221
* client resolves the id against the workflow registry.
193222
*/
223+
const TABLE_SCOPED_TOOL_IDS = new Set<string>([
224+
'table_automations',
225+
'table_columns',
226+
'table_enrichments',
227+
'table_manage',
228+
'table_rows',
229+
'table_views',
230+
])
231+
194232
const WORKFLOW_SCOPED_TOOL_IDS = new Set<string>([
195233
'deploy_as_api',
234+
'diff_workflows',
235+
'list_deployment_versions',
236+
'publish_custom_block',
196237
'deploy_as_chat',
197238
'deploy_as_mcp',
198239
'get_block_outputs',
@@ -250,6 +291,22 @@ export function resolveToolDisplayTitle(name: string, args?: Record<string, unkn
250291
// defaulting to the current workflow. Resolve the name here and hand it to
251292
// the shared resolver as `workflowName`, which every workflow title already
252293
// reads, so deployments, reads, and block work all say WHICH workflow.
294+
// Table tools keep their operands nested under `args`, and identify the
295+
// table by id — so the shared resolver sees neither the column being added
296+
// nor which table it belongs to. Lift both here.
297+
if (TABLE_SCOPED_TOOL_IDS.has(name)) {
298+
const nested = isRecordLike(args?.args) ? (args?.args as Record<string, unknown>) : undefined
299+
const tableName =
300+
stringParam(args?.tableName) ?? resolveTableNameForDisplay(nested?.tableId ?? args?.tableId)
301+
if (nested || tableName) {
302+
return getToolDisplayTitle(name, {
303+
...args,
304+
...(nested ?? {}),
305+
...(tableName ? { tableName } : {}),
306+
})
307+
}
308+
}
309+
253310
if (WORKFLOW_SCOPED_TOOL_IDS.has(name) && !stringParam(args?.workflowName)) {
254311
const workflowName = resolveTargetWorkflowName(args)
255312
// Block-scoped tools carry a blockId for the same reason; resolve it too,

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,53 @@ describe('terminal-title projection is idempotent', () => {
700700
)
701701
})
702702
})
703+
704+
describe('resource-naming titles', () => {
705+
it('names the table a row/column operation targets', () => {
706+
expect(getToolDisplayTitle('table_rows', { operation: 'insert', tableName: 'Runtimes' })).toBe(
707+
'Adding rows to Runtimes'
708+
)
709+
expect(
710+
getToolDisplayTitle('table_columns', {
711+
operation: 'add',
712+
columnName: 'status',
713+
tableName: 'Runtimes',
714+
})
715+
).toBe('Adding column status in Runtimes')
716+
expect(getToolDisplayTitle('table_views', { operation: 'list', tableName: 'Runtimes' })).toBe(
717+
'Reading views of Runtimes'
718+
)
719+
})
720+
721+
it('falls back cleanly when the table is unnamed', () => {
722+
expect(getToolDisplayTitle('table_rows', { operation: 'update' })).toBe('Updating rows')
723+
})
724+
725+
it('names the block behind a block-schema read', () => {
726+
expect(getToolDisplayTitle('read', { path: 'components/blocks/slack_v2.json' })).toBe(
727+
'Loading Slack'
728+
)
729+
expect(
730+
getToolDisplayTitle('read', { path: 'components/blocks/google_sheets_v2/README.md' })
731+
).toBe('Loading Google Sheets tips')
732+
})
733+
734+
it('shows the text a browser type/insert call sends', () => {
735+
expect(getToolDisplayTitle('browser_type', { text: 'hello there' })).toBe(
736+
'Typing "hello there"'
737+
)
738+
expect(getToolDisplayTitle('browser_insert_text', {})).toBe('Inserting text')
739+
})
740+
741+
it('names downloads, docs searches, and generated files', () => {
742+
expect(getToolDisplayTitle('download_file', { fileName: 'report.csv' })).toBe(
743+
'Downloading report.csv'
744+
)
745+
expect(
746+
getToolDisplayTitle('search_library_docs', { library_name: 'React', query: 'useEffect' })
747+
).toBe('Searching React docs for useEffect')
748+
expect(getToolDisplayTitle('generate_image', { path: 'files/hero.png' })).toBe(
749+
'Generating hero.png'
750+
)
751+
})
752+
})

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

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -84,41 +84,47 @@ function stringOrNumberArg(args: ToolArgs, key: string): string {
8484
*/
8585
function splitTableTitle(name: string, args: ToolArgs): string {
8686
const op = stringArg(args, 'operation')
87-
const target = firstStringArg(args, 'name', 'columnName', 'viewName', 'tableName', 'title')
87+
const target = firstStringArg(args, 'columnName', 'viewName', 'name', 'title')
8888
const suffix = target ? ` ${target}` : ''
89+
// "in Runtimes" / "of Runtimes" — enrichment resolves the nested tableId.
90+
const table = stringArg(args, 'tableName')
91+
const inTable = table ? ` in ${table}` : ''
92+
const ofTable = table ? ` of ${table}` : ''
8993
switch (name) {
9094
case 'table_manage':
91-
if (op === 'create') return `Creating table${suffix}`
92-
if (op === 'delete') return `Deleting table${suffix}`
93-
if (op === 'read' || op === 'get' || op === 'list') return 'Reading table'
94-
return 'Updating table'
95+
if (op === 'create') return `Creating table${suffix || (table ? ` ${table}` : '')}`
96+
if (op === 'delete') return `Deleting table${table ? ` ${table}` : suffix}`
97+
if (op === 'read' || op === 'get' || op === 'list')
98+
return `Reading${table ? ` ${table}` : ' table'}`
99+
return `Updating${table ? ` ${table}` : ' table'}`
95100
case 'table_rows':
96-
if (op === 'insert' || op === 'add' || op === 'create') return 'Adding rows'
97-
if (op === 'update') return 'Updating rows'
98-
if (op === 'delete') return 'Deleting rows'
99-
if (op === 'read' || op === 'list' || op === 'query') return 'Reading rows'
100-
return 'Editing rows'
101+
if (op === 'insert' || op === 'add' || op === 'create')
102+
return `Adding rows${inTable ? ` to ${table}` : ''}`
103+
if (op === 'update') return `Updating rows${inTable}`
104+
if (op === 'delete') return `Deleting rows${inTable}`
105+
if (op === 'read' || op === 'list' || op === 'query') return `Reading rows${ofTable}`
106+
return `Editing rows${ofTable}`
101107
case 'table_columns':
102-
if (op === 'add' || op === 'create') return `Adding column${suffix}`
103-
if (op === 'update') return `Updating column${suffix}`
104-
if (op === 'delete') return `Deleting column${suffix}`
105-
if (op === 'read' || op === 'list') return 'Reading columns'
106-
return 'Editing columns'
108+
if (op === 'add' || op === 'create') return `Adding column${suffix}${inTable}`
109+
if (op === 'update') return `Updating column${suffix}${inTable}`
110+
if (op === 'delete') return `Deleting column${suffix}${inTable}`
111+
if (op === 'read' || op === 'list') return `Reading columns${ofTable}`
112+
return `Editing columns${ofTable}`
107113
case 'table_automations':
108-
if (op === 'read' || op === 'list') return 'Reading automations'
109-
if (op === 'delete') return 'Removing automation'
110-
return 'Wiring automation'
114+
if (op === 'read' || op === 'list') return `Reading automations${ofTable}`
115+
if (op === 'delete') return `Removing automation${inTable}`
116+
return `Wiring automation${inTable}`
111117
case 'table_enrichments':
112-
if (op === 'read' || op === 'list') return 'Reading enrichments'
113-
if (op === 'delete') return 'Removing enrichment'
114-
return 'Configuring enrichment'
118+
if (op === 'read' || op === 'list') return `Reading enrichments${ofTable}`
119+
if (op === 'delete') return `Removing enrichment${inTable}`
120+
return `Configuring enrichment${suffix}${inTable}`
115121
case 'table_views':
116-
if (op === 'create') return `Creating view${suffix}`
117-
if (op === 'delete') return `Deleting view${suffix}`
118-
if (op === 'read' || op === 'list') return 'Reading views'
119-
return 'Editing views'
122+
if (op === 'create') return `Creating view${suffix}${inTable}`
123+
if (op === 'delete') return `Deleting view${suffix}${inTable}`
124+
if (op === 'read' || op === 'list') return `Reading views${ofTable}`
125+
return `Editing views${ofTable}`
120126
default:
121-
return 'Updating table'
127+
return `Updating${table ? ` ${table}` : ' table'}`
122128
}
123129
}
124130

@@ -165,6 +171,29 @@ function displayUrl(raw: string): string {
165171
}
166172
}
167173

174+
/**
175+
* Human name for a block type id: strip the version suffix and title-case the
176+
* snake_case stem (`slack_v2` -> `Slack`, `google_sheets_v2` -> `Google Sheets`).
177+
*/
178+
export function blockDisplayName(blockType: string): string {
179+
const stem = stripVersionSuffix(blockType.trim())
180+
if (!stem) return blockType
181+
return stem
182+
.split('_')
183+
.filter(Boolean)
184+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
185+
.join(' ')
186+
}
187+
188+
/** Ellipsizes the middle so both ends of a value stay recognizable. */
189+
function truncateMiddle(value: string, maxChars: number): string {
190+
const text = value.replace(/\s+/g, ' ').trim()
191+
if (text.length <= maxChars) return text
192+
const head = Math.ceil((maxChars - 1) / 2)
193+
const tail = Math.floor((maxChars - 1) / 2)
194+
return `${text.slice(0, head)}${text.slice(text.length - tail)}`
195+
}
196+
168197
function isWorkflowArtifactPath(path: string, filename: string): boolean {
169198
const trimmed = path.trim()
170199
return trimmed.startsWith('workflows/') && trimmed.endsWith(`/${filename}`)
@@ -305,12 +334,12 @@ function queryUserTableTitle(args: ToolArgs): string {
305334
}
306335

307336
function searchKnowledgeBaseTitle(args: ToolArgs): string {
308-
const titles: Record<string, string> = {
309-
get: 'Reading knowledge base',
310-
query: 'Searching knowledge base',
311-
list_tags: 'Listing knowledge base tags',
312-
}
313-
return titles[stringArg(args, 'operation')] ?? 'Searching knowledge base'
337+
const query = stringArg(args, 'query')
338+
const operation = stringArg(args, 'operation')
339+
if (operation === 'get') return 'Reading knowledge base'
340+
if (operation === 'list_tags') return 'Listing knowledge base tags'
341+
// A search row is far more useful with the question it asked.
342+
return query ? `Searching knowledge base for ${query}` : 'Searching knowledge base'
314343
}
315344

316345
function manageSandboxTitle(args: ToolArgs): string {
@@ -524,9 +553,7 @@ const TOOL_TITLES: Record<string, string> = {
524553
manage_knowledge_base: 'Managing knowledge base',
525554
search_knowledge_base: 'Searching knowledge base',
526555
open_resource: 'Opening resource',
527-
generate_image: 'Generating image',
528-
generate_video: 'Generating video',
529-
generate_audio: 'Generating audio',
556+
530557
ffmpeg: 'Processing media',
531558
get_deployment_status: 'Checking deployment status',
532559
create_empty_file: 'Creating file',
@@ -538,7 +565,7 @@ const TOOL_TITLES: Record<string, string> = {
538565
publish_custom_block: 'Publishing custom block',
539566
deploy_as_mcp: 'Deploying as MCP tool',
540567
diff_workflows: 'Comparing workflows',
541-
download_file: 'Downloading file',
568+
542569
run_function: 'Running code',
543570
generate_api_key: 'Generating API key',
544571
// Retired in favor of the account/ and organization/ VFS namespaces. Kept so
@@ -589,8 +616,7 @@ const TOOL_TITLES: Record<string, string> = {
589616
browser_screenshot: 'Taking screenshot',
590617
browser_click: 'Clicking element',
591618
browser_click_at: 'Clicking point',
592-
browser_type: 'Typing text',
593-
browser_insert_text: 'Inserting text',
619+
594620
browser_drag: 'Dragging element',
595621
browser_select_option: 'Selecting option',
596622
browser_hover: 'Hovering element',
@@ -996,6 +1022,40 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
9961022
const text = stringArg(args, 'text')
9971023
return text ? `Waiting for "${text}"` : 'Waiting for page'
9981024
}
1025+
case 'generate_image':
1026+
case 'generate_video':
1027+
case 'generate_audio': {
1028+
const kind =
1029+
name === 'generate_image' ? 'image' : name === 'generate_video' ? 'video' : 'audio'
1030+
const target =
1031+
firstStringArg(args, 'toolTitle', 'title') ||
1032+
(stringArg(args, 'path') ? pathLeaf(stringArg(args, 'path')) : '')
1033+
return target ? `Generating ${target}` : `Generating ${kind}`
1034+
}
1035+
case 'download_file': {
1036+
const target =
1037+
firstStringArg(args, 'fileName', 'toolTitle', 'title') ||
1038+
(stringArg(args, 'path') ? pathLeaf(stringArg(args, 'path')) : '') ||
1039+
(stringArg(args, 'url') ? displayUrl(stringArg(args, 'url')) : '')
1040+
return target ? `Downloading ${target}` : 'Downloading file'
1041+
}
1042+
case 'search_library_docs': {
1043+
const library = firstStringArg(args, 'library_name', 'libraryName', 'library')
1044+
const query = stringArg(args, 'query')
1045+
if (library && query) return `Searching ${library} docs for ${query}`
1046+
if (library) return `Searching ${library} docs`
1047+
return query ? `Searching library docs for ${query}` : 'Searching library docs'
1048+
}
1049+
case 'run_code': {
1050+
const title = stringArg(args, 'title')
1051+
return title || 'Running code'
1052+
}
1053+
case 'browser_type':
1054+
case 'browser_insert_text': {
1055+
const verb = name === 'browser_type' ? 'Typing' : 'Inserting'
1056+
const text = stringArg(args, 'text')
1057+
return text ? `${verb} "${truncateMiddle(text, 32)}"` : `${verb} text`
1058+
}
9991059
case 'browser_press_key': {
10001060
const key = stringArg(args, 'key')
10011061
return key ? `Pressing ${key}` : 'Pressing key'
@@ -1161,6 +1221,12 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
11611221
}
11621222
// Workflow artifacts name BOTH the workflow and which part, so five
11631223
// reads in a row differentiate instead of all saying the same thing.
1224+
// A block schema read is the model looking up how a block works; name
1225+
// the block, not the file. The row's icon is chosen from the same id.
1226+
const blockSchema = path.match(/^components\/blocks\/([^/]+)\.json$/)
1227+
if (blockSchema) return `Loading ${blockDisplayName(decodePathSegment(blockSchema[1]))}`
1228+
const blockTips = path.match(/^components\/blocks\/([^/]+)\/README\.md$/)
1229+
if (blockTips) return `Loading ${blockDisplayName(decodePathSegment(blockTips[1]))} tips`
11641230
const workflowArtifact = path.match(/^workflows\/([^/]+)\/([^/]+)$/)
11651231
if (workflowArtifact) {
11661232
const part =

0 commit comments

Comments
 (0)