Files
simstudioai--sim/apps/sim/lib/copilot/tools/tool-display.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

224 lines
7.4 KiB
TypeScript

import { stripVersionSuffix } from '@sim/utils/string'
/**
* Single source of truth for copilot tool-call display titles.
*
* The mothership (Go) no longer emits any presentation metadata on the stream —
* tool-call titles are derived entirely here, keyed by tool name (plus arguments
* for the dynamic cases). The live client render layer (see
* `home/hooks/stream/stream-helpers.ts`) wraps this with workspace/block-name
* enrichment for the run_* tools; every other surface (server persistence,
* transcript replay, fallback rendering) calls `getToolDisplayTitle` directly.
*
* Icons are likewise client-owned — see `getToolIcon` in the message-content
* utils. Nothing about tool presentation lives on the Go side anymore.
*/
type ToolArgs = Record<string, unknown> | undefined
function stringArg(args: ToolArgs, key: string): string {
const value = args?.[key]
return typeof value === 'string' ? value.trim() : ''
}
function firstStringArg(args: ToolArgs, ...keys: string[]): string {
for (const key of keys) {
const value = stringArg(args, key)
if (value) return value
}
return ''
}
function stringArrayArg(args: ToolArgs, key: string): string[] {
const value = args?.[key]
if (!Array.isArray(value)) return []
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
}
function nestedStringArg(args: ToolArgs, parentKey: string, ...keys: string[]): string {
const parent = args?.[parentKey]
if (!parent || typeof parent !== 'object') return ''
return firstStringArg(parent as Record<string, unknown>, ...keys)
}
function operationTitle(
args: ToolArgs,
placeholder: string,
labels: Record<string, string>
): string {
const operation = stringArg(args, 'operation')
return labels[operation] ?? placeholder
}
function isWorkflowArtifactPath(path: string, filename: string): boolean {
const trimmed = path.trim()
return trimmed.startsWith('workflows/') && trimmed.endsWith(`/${filename}`)
}
function workspaceFileTitle(args: ToolArgs): string {
const title = stringArg(args, 'title')
if (!title) return ''
const verbByOperation: Record<string, string> = {
create: 'Creating',
append: 'Adding',
patch: 'Editing',
update: 'Writing',
rename: 'Renaming',
delete: 'Deleting',
}
const verb = verbByOperation[stringArg(args, 'operation')] ?? 'Writing'
return `${verb} ${title}`
}
/** Static fallback titles for tools without an argument-aware title. */
const TOOL_TITLES: Record<string, string> = {
read: 'Reading file',
search_library_docs: 'Searching library docs',
user_memory: 'Accessing memory',
user_table: 'Managing table',
workspace_file: 'Editing file',
edit_content: 'Applying file content',
create_workflow: 'Creating workflow',
edit_workflow: 'Editing workflow',
knowledge_base: 'Managing knowledge base',
open_resource: 'Opening resource',
generate_image: 'Generating image',
generate_video: 'Generating video',
generate_audio: 'Generating audio',
ffmpeg: 'Processing media',
manage_folder: 'Folder action',
// Subagent trigger tools, when surfaced as a tool call.
workflow: 'Workflow Agent',
run: 'Run Agent',
deploy: 'Deploy Agent',
auth: 'Auth Agent',
knowledge: 'Knowledge Agent',
table: 'Table Agent',
scheduled_task: 'Scheduled Task Agent',
agent: 'Tools Agent',
research: 'Research Agent',
media: 'Media Agent',
superagent: 'Executing action',
}
/**
* Final fallback: humanize a raw tool name (e.g. `manage_folder` -> "Manage
* Folder"), matching the legacy client humanizer so labels never render blank.
*/
export function humanizeToolName(name: string): string {
const words = stripVersionSuffix(name).split('_').filter(Boolean)
if (words.length === 0) return name
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}
/**
* Resolve a tool-call display title from its name and arguments. Argument-aware
* cases come first, then the static map, then a humanized fallback. This never
* returns an empty string.
*/
export function getToolDisplayTitle(name: string, args?: Record<string, unknown>): string {
switch (name) {
case 'search_online': {
const target = firstStringArg(args, 'toolTitle', 'title')
return target ? `Searching online for ${target}` : 'Searching online'
}
case 'grep': {
const target = firstStringArg(args, 'toolTitle', 'title')
return target ? `Searching for ${target}` : 'Searching'
}
case 'glob': {
const target = firstStringArg(args, 'toolTitle', 'title')
return target ? `Finding ${target}` : 'Finding files'
}
case 'enrichment_run': {
const subject = nestedStringArg(
args,
'inputs',
'fullName',
'companyName',
'domain',
'email',
'companyDomain'
)
return subject ? `Searching for ${subject}` : 'Searching'
}
case 'scrape_page': {
const url = stringArg(args, 'url')
return url ? `Scraping ${url}` : 'Scraping page'
}
case 'crawl_website': {
const url = stringArg(args, 'url')
return url ? `Crawling ${url}` : 'Crawling website'
}
case 'get_page_contents': {
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'
}
case 'manage_custom_tool':
return operationTitle(args, 'Custom tool action', {
add: 'Creating custom tool',
edit: 'Updating custom tool',
delete: 'Deleting custom tool',
list: 'Listing custom tools',
})
case 'manage_mcp_tool':
return operationTitle(args, 'MCP server action', {
add: 'Creating MCP server',
edit: 'Updating MCP server',
delete: 'Deleting MCP server',
list: 'Listing MCP servers',
})
case 'manage_skill':
return operationTitle(args, 'Skill action', {
add: 'Creating skill',
edit: 'Updating skill',
delete: 'Deleting skill',
list: 'Listing skills',
})
case 'manage_scheduled_task':
return operationTitle(args, 'Scheduled task action', {
create: 'Creating scheduled task',
get: 'Getting scheduled task',
update: 'Updating scheduled task',
delete: 'Deleting scheduled task',
list: 'Listing scheduled tasks',
})
case 'manage_credential':
return operationTitle(args, 'Credential action', {
rename: 'Renaming credential',
delete: 'Deleting credential',
})
case 'manage_folder':
return operationTitle(args, 'Folder action', {
create: 'Creating folder',
rename: 'Renaming folder',
move: 'Moving folder',
delete: 'Deleting folder',
})
case 'run_workflow':
case 'run_from_block':
case 'run_workflow_until_block':
return 'Running workflow'
case 'query_logs': {
const workflowName = stringArg(args, 'workflowName')
return workflowName ? `Querying logs for ${workflowName}` : 'Querying logs'
}
case 'read': {
if (isWorkflowArtifactPath(stringArg(args, 'path'), 'lint.json')) {
return 'Validating workflow state'
}
break
}
case 'workspace_file':
case 'function_execute': {
const title = name === 'workspace_file' ? workspaceFileTitle(args) : stringArg(args, 'title')
if (title) return title
break
}
}
return TOOL_TITLES[name] ?? humanizeToolName(name)
}