11'use client'
22
3- import { useEffect , useMemo , useRef } from 'react'
43import { PillsRing } from '@/components/emcn'
5- import type { ToolCallResult , ToolCallStatus } from '../../../../types'
4+ import type { ToolCallStatus } from '../../../../types'
65import { getToolIcon } from '../../utils'
76
8- /** Tools that render as cards with result data on success. */
9- const CARD_TOOLS = new Set < string > ( [
10- 'function_execute' ,
11- 'search_online' ,
12- 'scrape_page' ,
13- 'get_page_contents' ,
14- 'search_library_docs' ,
15- 'superagent' ,
16- 'run' ,
17- 'plan' ,
18- 'debug' ,
19- 'edit' ,
20- 'fast_edit' ,
21- 'custom_tool' ,
22- 'research' ,
23- 'job' ,
24- ] )
25-
26- /**
27- * Extract a readable preview from partial tool-call JSON.
28- * For workspace_file, pulls out the "content" field value.
29- * For other tools, returns the raw accumulated JSON.
30- */
31- function extractStreamingPreview ( toolName : string , raw : string ) : string {
32- if ( toolName === 'workspace_file' ) {
33- const marker = '"content":'
34- const idx = raw . indexOf ( marker )
35- if ( idx === - 1 ) return ''
36- let rest = raw . slice ( idx + marker . length ) . trimStart ( )
37- if ( rest . startsWith ( '"' ) ) rest = rest . slice ( 1 )
38- // Unescape common JSON escape sequences for display
39- return rest
40- . replace ( / \\ n / g, '\n' )
41- . replace ( / \\ t / g, '\t' )
42- . replace ( / \\ " / g, '"' )
43- . replace ( / \\ \\ / g, '\\' )
44- }
45- return raw
46- }
47-
487function CircleCheck ( { className } : { className ?: string } ) {
498 return (
509 < svg
@@ -97,15 +56,13 @@ function StatusIcon({ status, toolName }: { status: ToolCallStatus; toolName: st
9756 return < CircleCheck className = 'h-[15px] w-[15px] text-[var(--text-tertiary)]' />
9857}
9958
100- function FlatToolLine ( {
101- toolName,
102- displayTitle,
103- status,
104- } : {
59+ interface ToolCallItemProps {
10560 toolName : string
10661 displayTitle : string
10762 status : ToolCallStatus
108- } ) {
63+ }
64+
65+ export function ToolCallItem ( { toolName, displayTitle, status } : ToolCallItemProps ) {
10966 return (
11067 < div className = 'flex items-center gap-[8px] pl-[24px]' >
11168 < div className = 'flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center' >
@@ -115,128 +72,3 @@ function FlatToolLine({
11572 </ div >
11673 )
11774}
118-
119- function formatToolOutput ( output : unknown ) : string {
120- if ( output === null || output === undefined ) return ''
121- if ( typeof output === 'string' ) return output
122- try {
123- return JSON . stringify ( output , null , 2 )
124- } catch {
125- return String ( output )
126- }
127- }
128-
129- interface ToolCallItemProps {
130- toolName : string
131- displayTitle : string
132- status : ToolCallStatus
133- result ?: ToolCallResult
134- streamingArgs ?: string
135- }
136-
137- export function ToolCallItem ( {
138- toolName,
139- displayTitle,
140- status,
141- result,
142- streamingArgs,
143- } : ToolCallItemProps ) {
144- const showCard =
145- CARD_TOOLS . has ( toolName ) &&
146- status === 'success' &&
147- result ?. output !== undefined &&
148- result ?. output !== null
149-
150- if ( showCard ) {
151- return < ToolCallCard toolName = { toolName } displayTitle = { displayTitle } result = { result ! } />
152- }
153-
154- if ( streamingArgs && status === 'executing' ) {
155- return (
156- < StreamingToolCard
157- toolName = { toolName }
158- displayTitle = { displayTitle }
159- streamingArgs = { streamingArgs }
160- />
161- )
162- }
163-
164- return < FlatToolLine toolName = { toolName } displayTitle = { displayTitle } status = { status } />
165- }
166-
167- function StreamingToolCard ( {
168- toolName,
169- displayTitle,
170- streamingArgs,
171- } : {
172- toolName : string
173- displayTitle : string
174- streamingArgs : string
175- } ) {
176- const preview = useMemo (
177- ( ) => extractStreamingPreview ( toolName , streamingArgs ) ,
178- [ toolName , streamingArgs ]
179- )
180- const scrollRef = useRef < HTMLPreElement > ( null )
181-
182- useEffect ( ( ) => {
183- const el = scrollRef . current
184- if ( el ) el . scrollTop = el . scrollHeight
185- } , [ preview ] )
186-
187- return (
188- < div className = 'pl-[24px]' >
189- < div className = 'overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-3)]' >
190- < div className = 'flex items-center gap-[8px] px-[10px] py-[6px]' >
191- < PillsRing className = 'h-[15px] w-[15px] text-[var(--text-tertiary)]' animate />
192- < span className = 'font-base text-[13px] text-[var(--text-secondary)]' > { displayTitle } </ span >
193- </ div >
194- { preview && (
195- < div className = 'border-[var(--border)] border-t px-[10px] py-[6px]' >
196- < pre
197- ref = { scrollRef }
198- className = 'max-h-[200px] overflow-y-auto whitespace-pre-wrap break-all font-mono text-[12px] text-[var(--text-body)] leading-[1.5]'
199- >
200- { preview }
201- < span className = 'inline-block h-[14px] w-[1px] animate-pulse bg-[var(--text-tertiary)]' />
202- </ pre >
203- </ div >
204- ) }
205- </ div >
206- </ div >
207- )
208- }
209-
210- function ToolCallCard ( {
211- toolName,
212- displayTitle,
213- result,
214- } : {
215- toolName : string
216- displayTitle : string
217- result : ToolCallResult
218- } ) {
219- const body = useMemo ( ( ) => formatToolOutput ( result . output ) , [ result . output ] )
220- const Icon = getToolIcon ( toolName )
221- const ResolvedIcon = Icon ?? CircleCheck
222-
223- return (
224- < div className = 'animate-stream-fade-in pl-[24px]' >
225- < div className = 'overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-3)]' >
226- < div className = 'flex items-center gap-[8px] px-[10px] py-[6px]' >
227- < div className = 'flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center' >
228- < ResolvedIcon className = 'h-[15px] w-[15px] text-[var(--text-tertiary)]' />
229- </ div >
230- < span className = 'font-base text-[13px] text-[var(--text-secondary)]' > { displayTitle } </ span >
231- </ div >
232- { body && (
233- < div className = 'border-[var(--border)] border-t px-[10px] py-[6px]' >
234- < pre className = 'max-h-[200px] overflow-y-auto whitespace-pre-wrap break-all font-mono text-[12px] text-[var(--text-body)] leading-[1.5]' >
235- { body }
236- </ pre >
237- </ div >
238- ) }
239- </ div >
240- </ div >
241- )
242- }
0 commit comments