Skip to content

Commit 23d74a1

Browse files
committed
improvement(tools): tools
1 parent 2eaf4ce commit 23d74a1

11 files changed

Lines changed: 287 additions & 184 deletions

File tree

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

Lines changed: 3 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,6 @@ import { useMemo } from 'react'
22
import { ShimmerText } from '@/components/ui'
33
import { WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1'
44
import type { ToolCallStatus } from '../../../../types'
5-
import { getToolIcon, resolveToolDisplayState } from '../../utils'
6-
7-
function CircleCheck({ className }: { className?: string }) {
8-
return (
9-
<svg
10-
width='16'
11-
height='16'
12-
viewBox='0 0 16 16'
13-
fill='none'
14-
xmlns='http://www.w3.org/2000/svg'
15-
className={className}
16-
>
17-
<circle cx='8' cy='8' r='6.5' stroke='currentColor' strokeWidth='1.25' />
18-
<path
19-
d='M5.5 8.5L7 10L10.5 6.5'
20-
stroke='currentColor'
21-
strokeWidth='1.25'
22-
strokeLinecap='round'
23-
strokeLinejoin='round'
24-
/>
25-
</svg>
26-
)
27-
}
285

296
export function CircleStop({ className }: { className?: string }) {
307
return (
@@ -42,58 +19,6 @@ export function CircleStop({ className }: { className?: string }) {
4219
)
4320
}
4421

45-
function Hyphen({ className }: { className?: string }) {
46-
return (
47-
<svg
48-
width='16'
49-
height='16'
50-
viewBox='0 0 16 16'
51-
fill='none'
52-
xmlns='http://www.w3.org/2000/svg'
53-
className={className}
54-
>
55-
<path d='M4 8H12' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' />
56-
</svg>
57-
)
58-
}
59-
60-
function CircleOutline({ className }: { className?: string }) {
61-
return (
62-
<svg
63-
width='16'
64-
height='16'
65-
viewBox='0 0 16 16'
66-
fill='none'
67-
xmlns='http://www.w3.org/2000/svg'
68-
className={className}
69-
>
70-
<circle cx='8' cy='8' r='6.5' stroke='currentColor' strokeWidth='1.25' />
71-
</svg>
72-
)
73-
}
74-
75-
function StatusIcon({ status, toolName }: { status: ToolCallStatus; toolName: string }) {
76-
const display = resolveToolDisplayState(status)
77-
if (display === 'spinner') {
78-
const Icon = getToolIcon(toolName)
79-
if (Icon) {
80-
return <Icon className='size-[15px] text-[var(--text-tertiary)]' />
81-
}
82-
return <CircleOutline className='size-[15px] text-[var(--text-tertiary)]' />
83-
}
84-
if (display === 'cancelled') {
85-
return <CircleStop className='size-[15px] text-[var(--text-tertiary)]' />
86-
}
87-
if (display === 'interrupted') {
88-
return <Hyphen className='size-[15px] text-[var(--text-tertiary)]' />
89-
}
90-
const Icon = getToolIcon(toolName)
91-
if (Icon) {
92-
return <Icon className='size-[15px] text-[var(--text-tertiary)]' />
93-
}
94-
return <CircleCheck className='size-[15px] text-[var(--text-tertiary)]' />
95-
}
96-
9722
interface ToolCallItemProps {
9823
toolName: string
9924
displayTitle: string
@@ -131,14 +56,11 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }:
13156
return `${verb} ${unescaped}`
13257
}, [toolName, streamingArgs])
13358

134-
const isExecuting = resolveToolDisplayState(status) === 'spinner'
135-
const title = liveWorkspaceFileTitle || displayTitle
59+
const isExecuting = status === 'executing'
60+
const title = (isExecuting ? liveWorkspaceFileTitle : null) || displayTitle
13661

13762
return (
138-
<div className='flex items-center gap-[8px] pl-[24px]'>
139-
<div className='flex size-[16px] flex-shrink-0 items-center justify-center'>
140-
<StatusIcon status={status} toolName={toolName} />
141-
</div>
63+
<div className='flex items-center pl-[24px]'>
14264
{isExecuting ? (
14365
<ShimmerText className='text-[13px] [--shimmer-rest:var(--text-secondary)]'>
14466
{title}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,33 @@ describe('parseBlocks span-identity tree', () => {
6363
if (group.type !== 'agent_group') throw new Error('expected mothership group')
6464
const tool = group.items[0]
6565
if (tool?.type !== 'tool') throw new Error('expected credential tool')
66-
expect(tool.data.displayTitle).toBe('Renaming Stripe to Production Stripe')
66+
expect(tool.data.displayTitle).toBe('Renamed Stripe to Production Stripe')
67+
})
68+
69+
it('replaces a stale generic search title with the target and settles its tense', () => {
70+
const searchCall = (status: 'executing' | 'success'): ContentBlock => ({
71+
type: 'tool_call',
72+
toolCall: {
73+
id: `search-${status}`,
74+
name: 'grep',
75+
status,
76+
displayTitle: 'Searching',
77+
params: { pattern: 'Elder|Pulse' },
78+
},
79+
timestamp: 1,
80+
})
81+
82+
for (const [status, expected] of [
83+
['executing', 'Searching for Elder|Pulse'],
84+
['success', 'Searched for Elder|Pulse'],
85+
] as const) {
86+
const segments = parseBlocks([searchCall(status)])
87+
const group = segments[0]
88+
if (group?.type !== 'agent_group') throw new Error('expected mothership group')
89+
const tool = group.items[0]
90+
if (tool?.type !== 'tool') throw new Error('expected search tool')
91+
expect(tool.data.displayTitle).toBe(expected)
92+
}
6793
})
6894

6995
it('nests a deploy subagent inside the workflow subagent that spawned it', () => {

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-ca
55
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
66
import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils'
77
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
8-
import { getToolDisplayTitle, humanizeToolName } from '@/lib/copilot/tools/tool-display'
8+
import {
9+
getToolDisplayTitle,
10+
humanizeToolName,
11+
resolveToolDisplayTitleForStatus,
12+
} from '@/lib/copilot/tools/tool-display'
913
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
1014
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
1115
import { SUBAGENT_LABELS } from '../../types'
@@ -158,13 +162,31 @@ function getOverrideDisplayTitle(tc: NonNullable<ContentBlock['toolCall']>): str
158162
})
159163
}
160164
}
165+
if (
166+
tc.params &&
167+
[
168+
'search_online',
169+
'grep',
170+
'glob',
171+
'search_library_docs',
172+
'search_knowledge_base',
173+
'enrichment_run',
174+
].includes(tc.name)
175+
) {
176+
const resolvedTitle = getToolDisplayTitle(tc.name, tc.params)
177+
// During argument streaming, params can temporarily be an empty object.
178+
// Do not replace a richer streamed/persisted title with the no-argument
179+
// fallback until the target-bearing argument has actually arrived.
180+
if (resolvedTitle !== getToolDisplayTitle(tc.name)) return resolvedTitle
181+
}
161182
return undefined
162183
}
163184

164185
function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
165186
const overrideDisplayTitle = getOverrideDisplayTitle(tc)
166-
const displayTitle =
187+
const runningDisplayTitle =
167188
overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params)
189+
const displayTitle = resolveToolDisplayTitleForStatus(runningDisplayTitle, tc.status)
168190

169191
return {
170192
id: tc.id,

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import { deriveMessagePhase, resolveToolDisplayState } from './utils'
5+
import { deriveMessagePhase } from './utils'
66

77
describe('deriveMessagePhase', () => {
88
it('is streaming whenever the transport is live', () => {
@@ -18,21 +18,3 @@ describe('deriveMessagePhase', () => {
1818
expect(deriveMessagePhase({ isStreaming: false, isRevealing: false })).toBe('settled')
1919
})
2020
})
21-
22-
describe('resolveToolDisplayState', () => {
23-
it('spins iff the tool is executing — a pure projection of its own status', () => {
24-
expect(resolveToolDisplayState('executing')).toBe('spinner')
25-
})
26-
27-
it('maps cancelled and interrupted to their own glyphs', () => {
28-
expect(resolveToolDisplayState('cancelled')).toBe('cancelled')
29-
expect(resolveToolDisplayState('interrupted')).toBe('interrupted')
30-
})
31-
32-
it('renders terminal successes and errors as the tool icon', () => {
33-
expect(resolveToolDisplayState('success')).toBe('icon')
34-
expect(resolveToolDisplayState('error')).toBe('icon')
35-
expect(resolveToolDisplayState('skipped')).toBe('icon')
36-
expect(resolveToolDisplayState('rejected')).toBe('icon')
37-
})
38-
})

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

Lines changed: 4 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,45 @@
11
import type { ComponentType, SVGProps } from 'react'
22
import {
3-
Asterisk,
43
Blimp,
54
Bug,
65
Database,
7-
Eye,
86
File,
9-
FolderCode,
107
Hammer,
118
Integration,
12-
Layout,
13-
Library,
14-
Pencil,
159
PlayOutline,
1610
Rocket,
1711
Search,
18-
Settings,
19-
TerminalWindow,
2012
Wrench,
2113
} from '@sim/emcn'
2214
import { Calendar, Table as TableIcon } from '@sim/emcn/icons'
23-
import { AgentIcon, ImageIcon, TTSIcon, VideoIcon } from '@/components/icons'
24-
import type { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
15+
import { AgentIcon, VideoIcon } from '@/components/icons'
16+
import type { ToolCallStatus } from '../../types'
2517

2618
export type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
2719

28-
const TOOL_ICONS: Record<string, IconComponent> = {
20+
const AGENT_ICONS: Record<string, IconComponent> = {
2921
mothership: Blimp,
30-
glob: FolderCode,
31-
grep: Search,
32-
read: File,
33-
mv: FolderCode,
34-
cp: Layout,
35-
mkdir: FolderCode,
36-
search_online: Search,
37-
scrape_page: Search,
38-
get_page_contents: Search,
39-
search_library_docs: Library,
40-
manage_mcp_tool: Settings,
41-
manage_skill: Asterisk,
42-
user_memory: Database,
43-
function_execute: TerminalWindow,
44-
run_code: TerminalWindow,
4522
superagent: Blimp,
46-
user_table: TableIcon,
47-
workspace_file: File,
48-
edit_content: File,
49-
create_workflow: Layout,
50-
edit_workflow: Pencil,
5123
workflow: Hammer,
5224
debug: Bug,
5325
run: PlayOutline,
5426
deploy: Rocket,
5527
auth: Integration,
5628
knowledge: Database,
57-
knowledge_base: Database,
58-
search_knowledge_base: Database,
5929
table: TableIcon,
60-
query_user_table: TableIcon,
6130
scheduled_task: Calendar,
6231
job: Calendar,
6332
agent: AgentIcon,
6433
custom_tool: Wrench,
6534
research: Search,
6635
scout: Search,
6736
search: Search,
68-
context_compaction: Asterisk,
69-
open_resource: Eye,
7037
file: File,
7138
media: VideoIcon,
72-
generate_image: ImageIcon,
73-
generate_video: VideoIcon,
74-
generate_audio: TTSIcon,
75-
ffmpeg: Wrench,
7639
}
7740

7841
export function getAgentIcon(name: string): IconComponent {
79-
return TOOL_ICONS[name as keyof typeof TOOL_ICONS] ?? Blimp
80-
}
81-
82-
export function getToolIcon(name: string): IconComponent | undefined {
83-
const icon = TOOL_ICONS[name as keyof typeof TOOL_ICONS]
84-
return icon === Blimp ? undefined : icon
42+
return AGENT_ICONS[name as keyof typeof AGENT_ICONS] ?? Blimp
8543
}
8644

8745
export type MessagePhase = 'streaming' | 'revealing' | 'settled'
@@ -100,19 +58,6 @@ export function deriveMessagePhase({
10058
return 'settled'
10159
}
10260

103-
type ToolDisplayState = 'spinner' | 'cancelled' | 'interrupted' | 'icon'
104-
105-
export function resolveToolDisplayState(status: ToolCallStatus): ToolDisplayState {
106-
// Pure projection of the tool's own status. A row spins iff it is genuinely
107-
// executing; every terminal status maps to a glyph. No transport/turn-live
108-
// gating — deterministic terminals (tool `result`, turn propagation) guarantee
109-
// a row never lingers `executing` after its work is done.
110-
if (status === 'executing') return 'spinner'
111-
if (status === 'cancelled') return 'cancelled'
112-
if (status === 'interrupted') return 'interrupted'
113-
return 'icon'
114-
}
115-
11661
export function isToolDone(status: ToolCallStatus): boolean {
11762
return (
11863
status === 'success' ||

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,18 +335,24 @@ export function resolveStreamingToolDisplayTitle(
335335
}
336336

337337
if (name === SearchOnline.id) {
338-
const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle')
339-
return toolTitle ? `Searching online for ${toolTitle}` : undefined
338+
const target =
339+
matchStreamingStringArg(streamingArgs, 'toolTitle') ??
340+
matchStreamingStringArg(streamingArgs, 'query')
341+
return target ? `Searching online for ${target}` : undefined
340342
}
341343

342344
if (name === Grep.id) {
343-
const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle')
344-
return toolTitle ? `Searching for ${toolTitle}` : undefined
345+
const target =
346+
matchStreamingStringArg(streamingArgs, 'toolTitle') ??
347+
matchStreamingStringArg(streamingArgs, 'pattern')
348+
return target ? `Searching for ${target}` : undefined
345349
}
346350

347351
if (name === Glob.id) {
348352
const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle')
349-
return toolTitle ? `Finding ${toolTitle}` : undefined
353+
if (toolTitle) return `Finding ${toolTitle}`
354+
const pattern = matchStreamingStringArg(streamingArgs, 'pattern')
355+
return pattern ? `Finding files matching ${pattern}` : undefined
350356
}
351357

352358
if (name === 'mv') {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ describe('streaming resource titles', () => {
6262
)
6363
).toBe('Renaming Old Name to New Name')
6464
})
65+
66+
it('uses search query and pattern fallbacks before toolTitle arrives', () => {
67+
expect(resolveStreamingToolDisplayTitle('grep', '{"pattern":"Elder|Pulse"}')).toBe(
68+
'Searching for Elder|Pulse'
69+
)
70+
expect(
71+
resolveStreamingToolDisplayTitle('search_online', '{"query":"Pulse converter differences"}')
72+
).toBe('Searching online for Pulse converter differences')
73+
expect(resolveStreamingToolDisplayTitle('glob', '{"pattern":"workflows/**"}')).toBe(
74+
'Finding files matching workflows/**'
75+
)
76+
})
6577
})
6678

6779
// A main-agent file delegation: trigger tool (main lane), subagent span, inner

apps/sim/lib/copilot/tools/client/store-utils.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ describe('resolveToolDisplay', () => {
118118
path: 'files/deck.pptx/style',
119119
})?.text
120120
).toBe('Read style details for deck.pptx')
121+
122+
expect(
123+
resolveToolDisplay(ReadTool.id, ClientToolCallState.aborted, {
124+
path: 'files/report.pdf/content',
125+
})?.text
126+
).toBe('Skipped: Read report.pdf')
121127
})
122128

123129
it('falls back to a humanized tool label for generic tools', () => {

0 commit comments

Comments
 (0)