@@ -25,6 +25,9 @@ impl ClaudeSession {
2525 "--verbose" ,
2626 "--input-format" , "stream-json" ,
2727 "--include-partial-messages" ,
28+ // bypassPermissions since there's no TTY for Claude Code's
29+ // own approval TUI — commands would silently hang otherwise.
30+ "--permission-mode" , "bypassPermissions" ,
2831 ] ;
2932 if let Some ( m) = model {
3033 args. push ( "--model" ) ;
@@ -67,6 +70,10 @@ impl ClaudeSession {
6770 // Track whether we've received stream_event deltas for the current turn.
6871 // If so, skip `assistant` text processing to avoid duplicate content.
6972 let mut got_stream_deltas = false ;
73+ // Track active content blocks by their stream index for tool use / thinking.
74+ let mut active_blocks = std:: collections:: HashMap :: < u64 , BlockInfo > :: new ( ) ;
75+ // Track model name from message_start so we can use it in result.
76+ let mut current_model: Option < String > = None ;
7077
7178 while let Ok ( Some ( line) ) = lines. next_line ( ) . await {
7279 if line. trim ( ) . is_empty ( ) {
@@ -88,10 +95,12 @@ impl ClaudeSession {
8895 }
8996 }
9097 "stream_event" => {
91- let evts = handle_stream_event ( & obj, & mut turn_started) ;
98+ let evts = handle_stream_event ( & obj, & mut turn_started, & mut active_blocks , & mut current_model ) ;
9299 // Mark that we got real streaming deltas so we skip
93100 // the `assistant` snapshot which would duplicate them.
94- if evts. iter ( ) . any ( |e| matches ! ( e, AgentEvent :: ContentDelta { .. } ) ) {
101+ if evts. iter ( ) . any ( |e| matches ! ( e, AgentEvent :: ContentDelta { .. }
102+ | AgentEvent :: ToolUseStart { .. }
103+ | AgentEvent :: ThinkingDelta { .. } ) ) {
95104 got_stream_deltas = true ;
96105 }
97106 evts
@@ -131,9 +140,68 @@ impl ClaudeSession {
131140 last_len = 0 ;
132141 turn_started = false ;
133142 got_stream_deltas = false ;
134- handle_result ( & obj)
143+ active_blocks. clear ( ) ;
144+ let evts = handle_result ( & obj, & current_model) ;
145+ current_model = None ;
146+ evts
147+ }
148+ "user" => {
149+ // Tool results come back as "user" type messages
150+ // with content array containing tool_result blocks
151+ let content = obj
152+ . get ( "message" )
153+ . and_then ( |m| m. get ( "content" ) )
154+ . and_then ( |c| c. as_array ( ) ) ;
155+
156+ if let Some ( blocks) = content {
157+ let mut evts = Vec :: new ( ) ;
158+ for block in blocks {
159+ if block. get ( "type" ) . and_then ( |t| t. as_str ( ) ) == Some ( "tool_result" ) {
160+ let tool_use_id = block
161+ . get ( "tool_use_id" )
162+ . and_then ( |id| id. as_str ( ) )
163+ . unwrap_or ( "" )
164+ . to_string ( ) ;
165+ let result_content = block
166+ . get ( "content" )
167+ . map ( |c| {
168+ if let Some ( s) = c. as_str ( ) {
169+ s. to_string ( )
170+ } else {
171+ serde_json:: to_string_pretty ( c) . unwrap_or_default ( )
172+ }
173+ } )
174+ . unwrap_or_default ( ) ;
175+ let is_error = block
176+ . get ( "is_error" )
177+ . and_then ( |e| e. as_bool ( ) )
178+ . unwrap_or ( false ) ;
179+
180+ // Try to get the tool name from tool_use_result
181+ let tool_name = obj
182+ . get ( "tool_use_result" )
183+ . and_then ( |r| r. get ( "stdout" ) )
184+ . and_then ( |s| s. as_str ( ) )
185+ . map ( |_| "Bash" . to_string ( ) )
186+ . unwrap_or_default ( ) ;
187+
188+ evts. push ( AgentEvent :: ToolResult {
189+ tool_id : tool_use_id,
190+ tool_name,
191+ content : result_content,
192+ is_error,
193+ } ) ;
194+ }
195+ }
196+ evts
197+ } else {
198+ vec ! [ ]
199+ }
200+ }
201+ _ => {
202+ debug ! ( "Unhandled Claude event type: {event_type}" ) ;
203+ vec ! [ ]
135204 }
136- _ => vec ! [ ] ,
137205 } ;
138206
139207 for event in events {
@@ -224,7 +292,20 @@ fn handle_assistant(obj: &Value, last_len: &mut usize, turn_started: &mut bool)
224292 events
225293}
226294
227- fn handle_stream_event ( obj : & Value , turn_started : & mut bool ) -> Vec < AgentEvent > {
295+ /// Tracks active content blocks by their stream index.
296+ #[ derive( Debug , Clone ) ]
297+ struct BlockInfo {
298+ block_type : String ,
299+ tool_id : Option < String > ,
300+ tool_name : Option < String > ,
301+ }
302+
303+ fn handle_stream_event (
304+ obj : & Value ,
305+ turn_started : & mut bool ,
306+ active_blocks : & mut std:: collections:: HashMap < u64 , BlockInfo > ,
307+ current_model : & mut Option < String > ,
308+ ) -> Vec < AgentEvent > {
228309 let event = match obj. get ( "event" ) {
229310 Some ( e) => e,
230311 None => return vec ! [ ] ,
@@ -233,6 +314,15 @@ fn handle_stream_event(obj: &Value, turn_started: &mut bool) -> Vec<AgentEvent>
233314
234315 match inner {
235316 "message_start" => {
317+ // Capture model from message_start for later use in usage report
318+ if let Some ( model) = event
319+ . get ( "message" )
320+ . and_then ( |m| m. get ( "model" ) )
321+ . and_then ( |m| m. as_str ( ) )
322+ {
323+ * current_model = Some ( model. to_string ( ) ) ;
324+ }
325+
236326 if !* turn_started {
237327 * turn_started = true ;
238328 let id = event
@@ -246,33 +336,92 @@ fn handle_stream_event(obj: &Value, turn_started: &mut bool) -> Vec<AgentEvent>
246336 }
247337 }
248338 "content_block_start" => {
339+ let index = event. get ( "index" ) . and_then ( |i| i. as_u64 ( ) ) . unwrap_or ( 0 ) ;
249340 let block = event. get ( "content_block" ) . unwrap_or ( & Value :: Null ) ;
250- if block. get ( "type" ) . and_then ( |t| t. as_str ( ) ) == Some ( "tool_use" ) {
251- let name = block. get ( "name" ) . and_then ( |n| n. as_str ( ) ) . unwrap_or ( "tool" ) ;
252- vec ! [ AgentEvent :: ContentDelta {
253- text: format!( "\n > *Using {name}...*\n " ) ,
254- } ]
255- } else {
256- vec ! [ ]
341+ let block_type = block. get ( "type" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "text" ) ;
342+
343+ match block_type {
344+ "tool_use" => {
345+ let tool_id = block. get ( "id" ) . and_then ( |id| id. as_str ( ) ) . unwrap_or ( "" ) . to_string ( ) ;
346+ let tool_name = block. get ( "name" ) . and_then ( |n| n. as_str ( ) ) . unwrap_or ( "tool" ) . to_string ( ) ;
347+ active_blocks. insert ( index, BlockInfo {
348+ block_type : "tool_use" . into ( ) ,
349+ tool_id : Some ( tool_id. clone ( ) ) ,
350+ tool_name : Some ( tool_name. clone ( ) ) ,
351+ } ) ;
352+ vec ! [ AgentEvent :: ToolUseStart { tool_id, tool_name } ]
353+ }
354+ "thinking" => {
355+ active_blocks. insert ( index, BlockInfo {
356+ block_type : "thinking" . into ( ) ,
357+ tool_id : None ,
358+ tool_name : None ,
359+ } ) ;
360+ vec ! [ ]
361+ }
362+ _ => {
363+ active_blocks. insert ( index, BlockInfo {
364+ block_type : block_type. into ( ) ,
365+ tool_id : None ,
366+ tool_name : None ,
367+ } ) ;
368+ vec ! [ ]
369+ }
257370 }
258371 }
259372 "content_block_delta" => {
373+ let index = event. get ( "index" ) . and_then ( |i| i. as_u64 ( ) ) . unwrap_or ( 0 ) ;
260374 let delta = event. get ( "delta" ) . unwrap_or ( & Value :: Null ) ;
261375 let dtype = delta. get ( "type" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "" ) ;
262- if dtype == "text_delta" {
263- let text = delta. get ( "text" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "" ) ;
264- if text. is_empty ( ) { vec ! [ ] } else {
265- vec ! [ AgentEvent :: ContentDelta { text: text. to_string( ) } ]
376+
377+ match dtype {
378+ "text_delta" => {
379+ let text = delta. get ( "text" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "" ) ;
380+ if text. is_empty ( ) {
381+ vec ! [ ]
382+ } else {
383+ vec ! [ AgentEvent :: ContentDelta { text: text. to_string( ) } ]
384+ }
385+ }
386+ "input_json_delta" => {
387+ let json_str = delta. get ( "partial_json" ) . and_then ( |j| j. as_str ( ) ) . unwrap_or ( "" ) ;
388+ if let Some ( block) = active_blocks. get ( & index) {
389+ if let Some ( tool_id) = & block. tool_id {
390+ return vec ! [ AgentEvent :: ToolInputDelta {
391+ tool_id: tool_id. clone( ) ,
392+ input_json: json_str. to_string( ) ,
393+ } ] ;
394+ }
395+ }
396+ vec ! [ ]
397+ }
398+ "thinking_delta" => {
399+ let text = delta. get ( "thinking" ) . and_then ( |t| t. as_str ( ) ) . unwrap_or ( "" ) ;
400+ if text. is_empty ( ) {
401+ vec ! [ ]
402+ } else {
403+ vec ! [ AgentEvent :: ThinkingDelta { text: text. to_string( ) } ]
404+ }
405+ }
406+ _ => vec ! [ ] ,
407+ }
408+ }
409+ "content_block_end" => {
410+ let index = event. get ( "index" ) . and_then ( |i| i. as_u64 ( ) ) . unwrap_or ( 0 ) ;
411+ if let Some ( block) = active_blocks. remove ( & index) {
412+ if block. block_type == "tool_use" {
413+ if let Some ( tool_id) = block. tool_id {
414+ return vec ! [ AgentEvent :: ToolUseEnd { tool_id } ] ;
415+ }
266416 }
267- } else {
268- vec ! [ ]
269417 }
418+ vec ! [ ]
270419 }
271420 _ => vec ! [ ] ,
272421 }
273422}
274423
275- fn handle_result ( obj : & Value ) -> Vec < AgentEvent > {
424+ fn handle_result ( obj : & Value , current_model : & Option < String > ) -> Vec < AgentEvent > {
276425 let mut events = Vec :: new ( ) ;
277426
278427 // Extract usage data if present
@@ -300,9 +449,11 @@ fn handle_result(obj: &Value) -> Vec<AgentEvent> {
300449 . or_else ( || usage. and_then ( |u| u. get ( "cache_write_tokens" ) ) )
301450 . and_then ( |v| v. as_u64 ( ) )
302451 . unwrap_or ( 0 ) ;
452+ // Use model from result, fall back to model captured from message_start
303453 let model = obj
304454 . get ( "model" )
305455 . and_then ( |m| m. as_str ( ) )
456+ . or_else ( || current_model. as_deref ( ) )
306457 . unwrap_or ( "unknown" )
307458 . to_string ( ) ;
308459
0 commit comments