Skip to content

Commit fead01b

Browse files
committed
UX hill-climb: progressive CLI titles, resource names, lane polish
The generic-title flash dies at its root on both consumer paths: the live turn model and the server handler now refine a streaming CLI row's name from the partial argv (registry-gated — a half-streamed token or unknown prefix never matches), and the upsert guard accepts cli-family refinements instead of only the sim_cli placeholder. CLI rows name their resource by inverting the worker's name derivation to the first positional and resolving through the live stores ("Editing workflow" → "Editing Onboarding", "Updating table row" → "Updating Leads row"). Agent-CLI augmentations get real titles; web_search reads the query the worker actually sends; generate_* names the output file; task rows carry their delegation title while streaming and persisted; subagent lanes get their own icon instead of the main-lane Blimp; the collapsed lane status line can no longer surface a raw tool name. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent 4e272d0 commit fead01b

8 files changed

Lines changed: 211 additions & 12 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ChevronDown, cn, Expandable, ExpandableContent, OverflowText } from '@s
55
import { ShimmerText } from '@/components/ui'
66
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
77
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/mothership/tools/retired-tools'
8+
import { getToolDisplayTitle } from '@/lib/mothership/tools/tool-display'
89
import { useSmoothText } from '@/hooks/use-smooth-text'
910
import { type ToolCallData, ToolCallStatus } from '../../../../types'
1011
import { getAgentIcon, isToolDone } from '../../utils'
@@ -44,7 +45,9 @@ interface AgentGroupProps {
4445
}
4546

4647
function toolStatusTitle(tool: ToolCallData): string {
47-
return tool.displayTitle || String(tool.toolName ?? '')
48+
// Raw tool names must never surface — derive a human title when no display
49+
// title was resolved upstream.
50+
return tool.displayTitle || getToolDisplayTitle(String(tool.toolName ?? ''), undefined)
4851
}
4952

5053
/**

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export function collectMessageSources(texts: readonly string[]): SourceTagData[]
4848

4949
const TOOL_ICONS: Record<string, IconComponent> = {
5050
mothership: Blimp,
51+
// Delegated subagent lanes: visually distinct from the main Sim lane.
52+
task: AgentIcon,
5153
glob: FolderCode,
5254
grep: Search,
5355
read: File,

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,56 @@ export function resolveToolDisplayTitle(name: string, args?: Record<string, unkn
320320
if (workflowName || blockName) return getToolDisplayTitle(name, enriched)
321321
}
322322

323+
// CLI rows: name the resource. The command's first positional is the id;
324+
// recover it by inverting the worker's name derivation (path tokens joined
325+
// with '_'), then resolve through the live stores. "Editing workflow"
326+
// becomes "Editing Onboarding"; "Updating table row" becomes "Updating
327+
// Leads row". Only a successful name lookup changes anything.
328+
if (name.startsWith('cli_workflow') || name.startsWith('cli_tables')) {
329+
const positional = cliFirstPositional(name, args)
330+
if (positional) {
331+
const base = getToolDisplayTitle(name, args)
332+
const resourceName = name.startsWith('cli_tables')
333+
? resolveTableNameForDisplay(positional)
334+
: resolveWorkflowNameForDisplay(positional)
335+
if (resourceName) {
336+
const noun = name.startsWith('cli_tables') ? /\btable\b/ : /\bworkflow\b/
337+
if (noun.test(base)) return base.replace(noun, resourceName)
338+
return `${base}: ${resourceName}`
339+
}
340+
}
341+
}
342+
323343
return getToolDisplayTitle(name, args)
324344
}
325345

346+
/**
347+
* The first positional argument after a CLI command path — the resource id.
348+
* Inverts the worker's `cli_<path tokens joined by _>` naming to find where the
349+
* command path ends in argv, mirroring its flag handling exactly.
350+
*/
351+
function cliFirstPositional(name: string, args?: Record<string, unknown>): string | undefined {
352+
const argv = Array.isArray(args?.args) ? (args.args as unknown[]) : undefined
353+
if (!argv) return undefined
354+
const tokens: string[] = []
355+
for (let i = 0; i < argv.length; i++) {
356+
const token = argv[i]
357+
if (typeof token !== 'string') break
358+
if (token === '--output') {
359+
i++
360+
continue
361+
}
362+
if (token.startsWith('-')) break
363+
tokens.push(token)
364+
}
365+
let joined = 'cli'
366+
for (let i = 0; i < tokens.length; i++) {
367+
joined += `_${(tokens[i] ?? '').replace(/-/g, '_')}`
368+
if (joined === name) return tokens[i + 1]
369+
}
370+
return undefined
371+
}
372+
326373
function decodeStreamingString(value: string): string {
327374
return value
328375
.replace(/\\u([0-9a-fA-F]{4})/g, (_: string, hex: string) =>
@@ -370,6 +417,11 @@ export function resolveStreamingToolDisplayTitle(
370417
return functionExecuteTitle(matchStreamingStringArg(streamingArgs, 'title'))
371418
}
372419

420+
if (name === 'task') {
421+
const title = matchStreamingStringArg(streamingArgs, 'title')
422+
if (title) return `Delegating: ${title}`
423+
}
424+
373425
if (name === PrepareFileEdit.id) {
374426
return resolveWorkspaceFileDisplayTitle(
375427
matchStreamingStringArg(streamingArgs, 'operation'),

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
import { CallIntegrationTool } from '@/lib/mothership/generated/tool-catalog-v1'
1212
import type { PersistedStreamEventEnvelope } from '@/lib/mothership/request/session/contract'
1313
import { extractStreamingStringArgument } from '@/lib/mothership/tools/streaming-args'
14-
import { CONTEXT_COMPACTION_DISPLAY_TITLE } from '@/lib/mothership/tools/tool-display'
14+
import {
15+
CONTEXT_COMPACTION_DISPLAY_TITLE,
16+
refineStreamingCliToolName,
17+
} from '@/lib/mothership/tools/tool-display'
1518

1619
/**
1720
* The single deterministic model of one assistant turn, derived purely from the
@@ -363,12 +366,14 @@ function upsertToolNode(
363366
): ToolNode {
364367
const existing = model.nodes.get(id)
365368
if (existing && existing.kind === 'tool') {
366-
// Fill blanks, and replace exactly the CLI placeholder: the worker's partial frame
367-
// names CLI rows `sim_cli` (args unknowable mid-stream) and the finalized frame
368-
// carries the real verb (`cli_workflows_list`) — without this every live CLI row
369-
// read "Running CLI command" until reload. Scoped to the placeholder so the gateway
370-
// rebind's model-authored branding is never clobbered by a later frame.
371-
if (name && (!existing.name || (existing.name === 'sim_cli' && name !== 'sim_cli'))) {
369+
// Fill blanks, and refine CLI names: the worker's partial frame names CLI rows
370+
// `sim_cli` (args unknowable mid-stream), streaming deltas may refine that to a
371+
// provisional `cli_*`, and the finalized frame carries the authoritative verb —
372+
// so any cli-family name accepts a different cli-family (or authoritative)
373+
// successor. Scoped to the cli family so the gateway rebind's model-authored
374+
// branding is never clobbered by a later frame.
375+
const cliFamily = existing.name === 'sim_cli' || existing.name.startsWith('cli_')
376+
if (name && (!existing.name || (cliFamily && name !== existing.name && name !== 'sim_cli'))) {
372377
existing.name = name
373378
}
374379
return existing
@@ -553,6 +558,13 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
553558
)
554559
const delta = asString(payload.argumentsDelta)
555560
if (delta) node.streamingArgs = (node.streamingArgs ?? '') + delta
561+
// Progressive CLI title: upgrade the placeholder to the specific verb as
562+
// soon as enough argv tokens have streamed to name the command — the
563+
// browser mirror of the server handler's refinement.
564+
if (delta && (node.name === 'sim_cli' || node.name.startsWith('cli_'))) {
565+
const refined = refineStreamingCliToolName(node.streamingArgs ?? '')
566+
if (refined && refined !== node.name) node.name = refined
567+
}
556568
} else if (phase === MothershipStreamV1ToolPhase.result) {
557569
applyToolResult(
558570
model,

‎apps/sim/lib/mothership/request/handlers/tool.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ import { getToolEntry, isSimExecuted } from '@/lib/mothership/tool-executor'
4949
import { isToolHiddenInUi } from '@/lib/mothership/tools/client/hidden-tools'
5050
import { isUserLocalVfsToolCall } from '@/lib/mothership/tools/local-filesystem'
5151
import { extractStreamingStringArgument } from '@/lib/mothership/tools/streaming-args'
52-
import { getToolDisplayTitle } from '@/lib/mothership/tools/tool-display'
52+
import {
53+
getToolDisplayTitle,
54+
refineStreamingCliToolName,
55+
} from '@/lib/mothership/tools/tool-display'
5356
import {
5457
isWorkflowToolName,
5558
resolveWorkflowToolTargetId,
@@ -115,6 +118,18 @@ function handleToolArgsDelta(
115118
const toolCall = context.toolCalls.get(data.toolCallId)
116119
if (!toolCall) return
117120
toolCall.streamingArgs = `${toolCall.streamingArgs ?? ''}${data.argumentsDelta}`
121+
122+
// Progressive CLI title: the row upgrades from "Running CLI command" to the
123+
// specific verb as soon as enough argv tokens have streamed to name it —
124+
// no wait for the full (possibly huge) argument payload.
125+
if (toolCall.name === 'sim_cli' || toolCall.name.startsWith('cli_')) {
126+
const refined = refineStreamingCliToolName(toolCall.streamingArgs)
127+
if (refined && refined !== toolCall.name) {
128+
toolCall.name = refined
129+
applyToolDisplay(toolCall)
130+
}
131+
return
132+
}
118133
if (toolCall.name !== INTEGRATION_GATEWAY_TOOL) return
119134

120135
const toolId = extractStreamingStringArgument(toolCall.streamingArgs, 'toolId')

‎apps/sim/lib/mothership/tools/cli-tool-display.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,21 @@ export const CLI_TOOL_TITLES: Record<string, string> = {
222222
cli_workspaces_get: 'Reading workspace',
223223
cli_workspaces_list: 'Listing workspaces',
224224
cli_workspaces_members: 'Listing workspace members',
225+
// Agent-only CLI augmentations
226+
cli_files_grep: 'Searching file contents',
227+
cli_workflow_blocks: 'Listing workflow blocks',
228+
cli_workflow_deps: 'Tracing block inputs',
229+
cli_workflow_edges: 'Reading workflow wiring',
230+
cli_workflow_grep: 'Searching workflow',
231+
cli_workflow_lint: 'Validating workflow',
232+
cli_workflow_trace: 'Analyzing run trace',
233+
cli_workflows_grep: 'Searching workflows',
225234
// Non-CLI copilot worker tools
226235
cli_help: 'Checking CLI reference',
227236
sim_cli: 'Running CLI command',
228237
run_code: 'Running code',
229238
load_skill: 'Loading skill',
239+
read_output: 'Reading full output',
240+
remember: 'Updating memory',
230241
task: 'Delegating task',
231242
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
getToolDisplayTitle,
7+
refineStreamingCliToolName,
8+
} from '@/lib/mothership/tools/tool-display'
9+
10+
describe('refineStreamingCliToolName', () => {
11+
it('names the command from a partial prefix, skipping --output json', () => {
12+
expect(refineStreamingCliToolName('{"args":["--output","json","workflows","list"')).toBe(
13+
'cli_workflows_list'
14+
)
15+
})
16+
17+
it('stays generic on an intermediate prefix, then resolves the full command', () => {
18+
const partial = '{"args":["workflows","operations"'
19+
const fuller = '{"args":["workflows","operations","apply","wf-1","--operations","[{'
20+
expect(refineStreamingCliToolName(partial)).toBeNull()
21+
expect(refineStreamingCliToolName(fuller)).toBe('cli_workflows_operations_apply')
22+
})
23+
24+
it('never matches on a half-streamed token', () => {
25+
expect(refineStreamingCliToolName('{"args":["workflows","li')).toBeNull()
26+
})
27+
28+
it('names agent augmentations', () => {
29+
expect(refineStreamingCliToolName('{"args":["workflow","trace","abc-123"')).toBe(
30+
'cli_workflow_trace'
31+
)
32+
expect(refineStreamingCliToolName('{"args":["workflow","deps"')).toBe('cli_workflow_deps')
33+
})
34+
35+
it('returns null before the args array or on unknown commands', () => {
36+
expect(refineStreamingCliToolName('{"ar')).toBeNull()
37+
expect(refineStreamingCliToolName('{"args":["frobnicate","things"')).toBeNull()
38+
})
39+
40+
it('recognizes --help wherever it appears', () => {
41+
expect(refineStreamingCliToolName('{"args":["tables","--help"')).toBe('cli_help')
42+
})
43+
})
44+
45+
describe('cli display integration', () => {
46+
it('maps the new augmentation names to titles', () => {
47+
expect(getToolDisplayTitle('cli_workflow_trace', {})).toBe('Analyzing run trace')
48+
expect(getToolDisplayTitle('cli_workflow_deps', {})).toBe('Tracing block inputs')
49+
expect(getToolDisplayTitle('remember', {})).toBe('Updating memory')
50+
expect(getToolDisplayTitle('web_search', { query: 'latest sim release' })).toContain(
51+
'latest sim release'
52+
)
53+
expect(getToolDisplayTitle('task', { title: 'Inventory workspace' })).toBe(
54+
'Delegating: Inventory workspace'
55+
)
56+
})
57+
})

‎apps/sim/lib/mothership/tools/tool-display.ts‎

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -948,8 +948,16 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
948948
return `Deleting ${target || 'MCP server'}`
949949
}
950950
case 'web_search': {
951-
const target = firstStringArg(args, 'toolTitle', 'title')
952-
return target ? `Searching online for ${target}` : 'Searching online'
951+
const target = firstStringArg(args, 'toolTitle', 'title', 'query')
952+
return target
953+
? `Searching online for "${truncate(target, MAX_QUOTED_TITLE_VALUE_LENGTH)}"`
954+
: 'Searching online'
955+
}
956+
case 'task': {
957+
const target = firstStringArg(args, 'title')
958+
return target
959+
? `Delegating: ${truncate(target, MAX_QUOTED_TITLE_VALUE_LENGTH)}`
960+
: 'Delegating task'
953961
}
954962
case 'search_docs': {
955963
const target = firstStringArg(args, 'toolTitle', 'title', 'query')
@@ -1066,7 +1074,8 @@ export function getToolDisplayTitle(name: string, args?: Record<string, unknown>
10661074
name === 'generate_image' ? 'image' : name === 'generate_video' ? 'video' : 'audio'
10671075
const target =
10681076
firstStringArg(args, 'toolTitle', 'title') ||
1069-
(stringArg(args, 'path') ? pathLeaf(stringArg(args, 'path')) : '')
1077+
(stringArg(args, 'path') ? pathLeaf(stringArg(args, 'path')) : '') ||
1078+
firstOutputFilePath(args)
10701079
return target ? `Generating ${target}` : `Generating ${kind}`
10711080
}
10721081
case 'download_file': {
@@ -1500,3 +1509,41 @@ export function getToolStatusDisplayTitle(
15001509
if (status === 'cancelled' || status === 'aborted') return getToolStoppedTitle(title)
15011510
return title
15021511
}
1512+
1513+
/**
1514+
* Refines a streaming `sim_cli` call's derived name from its PARTIAL argument
1515+
* JSON, so the row's title upgrades from "Running CLI command" to the specific
1516+
* verb ("Listing workflows") while the arguments are still generating — the
1517+
* same progressive pattern the integration gateway rows use. Only complete
1518+
* quoted tokens count (a half-streamed token never matches), flags end the
1519+
* command path exactly as the worker's own matcher does, and a candidate is
1520+
* accepted only when the title registry knows it — an unknown prefix stays on
1521+
* the generic name rather than inventing one.
1522+
*/
1523+
const STREAMING_ARGS_ARRAY = /"args"\s*:\s*\[([^\]]*)/
1524+
const COMPLETE_STRING_TOKEN = /"((?:[^"\\]|\\.)*)"/g
1525+
1526+
export function refineStreamingCliToolName(streamingArgs: string): string | null {
1527+
const argsMatch = STREAMING_ARGS_ARRAY.exec(streamingArgs)
1528+
if (!argsMatch?.[1]) return null
1529+
const tokens: string[] = []
1530+
for (const match of argsMatch[1].matchAll(COMPLETE_STRING_TOKEN)) {
1531+
tokens.push(match[1] ?? '')
1532+
}
1533+
if (tokens.includes('--help') || tokens.includes('-h')) return 'cli_help'
1534+
const path: string[] = []
1535+
for (let i = 0; i < tokens.length; i++) {
1536+
const token = tokens[i] ?? ''
1537+
if (token === '--output') {
1538+
i++
1539+
continue
1540+
}
1541+
if (token.startsWith('-')) break
1542+
path.push(token)
1543+
}
1544+
for (let length = Math.min(path.length, 4); length >= 1; length--) {
1545+
const candidate = `cli_${path.slice(0, length).join('_').replace(/-/g, '_')}`
1546+
if (CLI_TOOL_TITLES[candidate]) return candidate
1547+
}
1548+
return null
1549+
}

0 commit comments

Comments
 (0)