11'use client'
22
3- import { useMemo } from 'react'
3+ import { useEffect , useMemo , useRef } from 'react'
44import { PillsRing } from '@/components/emcn'
55import type { ToolCallResult , ToolCallStatus } from '../../../../types'
66import { getToolIcon } from '../../utils'
@@ -20,10 +20,31 @@ const CARD_TOOLS = new Set<string>([
2020 'fast_edit' ,
2121 'custom_tool' ,
2222 'research' ,
23- 'agent' ,
2423 'job' ,
2524] )
2625
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+
2748function CircleCheck ( { className } : { className ?: string } ) {
2849 return (
2950 < svg
@@ -110,9 +131,16 @@ interface ToolCallItemProps {
110131 displayTitle : string
111132 status : ToolCallStatus
112133 result ?: ToolCallResult
134+ streamingArgs ?: string
113135}
114136
115- export function ToolCallItem ( { toolName, displayTitle, status, result } : ToolCallItemProps ) {
137+ export function ToolCallItem ( {
138+ toolName,
139+ displayTitle,
140+ status,
141+ result,
142+ streamingArgs,
143+ } : ToolCallItemProps ) {
116144 const showCard =
117145 CARD_TOOLS . has ( toolName ) &&
118146 status === 'success' &&
@@ -123,9 +151,62 @@ export function ToolCallItem({ toolName, displayTitle, status, result }: ToolCal
123151 return < ToolCallCard toolName = { toolName } displayTitle = { displayTitle } result = { result ! } />
124152 }
125153
154+ if ( streamingArgs && status === 'executing' ) {
155+ return (
156+ < StreamingToolCard
157+ toolName = { toolName }
158+ displayTitle = { displayTitle }
159+ streamingArgs = { streamingArgs }
160+ />
161+ )
162+ }
163+
126164 return < FlatToolLine toolName = { toolName } displayTitle = { displayTitle } status = { status } />
127165}
128166
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+
129210function ToolCallCard ( {
130211 toolName,
131212 displayTitle,
0 commit comments