11use std:: path:: Path ;
2+ use std:: sync:: { Arc , Mutex } ;
23
34use anyhow:: { Context , Result } ;
45use serde_json:: Value ;
@@ -12,30 +13,70 @@ use crate::types::AgentEvent;
1213pub struct ClaudeSession {
1314 child : Child ,
1415 stdin_tx : mpsc:: UnboundedSender < String > ,
16+ /// The Claude CLI session ID captured from the `system.init` event.
17+ claude_session_id : Arc < Mutex < Option < String > > > ,
1518}
1619
1720impl ClaudeSession {
1821 pub async fn start (
1922 cwd : & Path ,
2023 model : Option < & str > ,
24+ permission_mode : Option < & str > ,
2125 ) -> Result < ( Self , mpsc:: UnboundedReceiver < AgentEvent > ) > {
26+ let perm_mode = permission_mode. unwrap_or ( "bypassPermissions" ) ;
27+ let mut args = vec ! [
28+ "-p" ,
29+ "--output-format" , "stream-json" ,
30+ "--verbose" ,
31+ "--input-format" , "stream-json" ,
32+ "--include-partial-messages" ,
33+ "--permission-mode" , perm_mode,
34+ ] ;
35+ if let Some ( m) = model {
36+ args. push ( "--model" ) ;
37+ args. push ( m) ;
38+ }
39+
40+ Self :: spawn_with_args ( cwd, & args) . await
41+ }
42+
43+ /// Resume a previous Claude CLI session using `--resume`.
44+ pub async fn resume (
45+ cwd : & Path ,
46+ claude_session_id : & str ,
47+ model : Option < & str > ,
48+ ) -> Result < ( Self , mpsc:: UnboundedReceiver < AgentEvent > ) > {
49+ let resume_id = claude_session_id. to_string ( ) ;
2250 let mut args = vec ! [
2351 "-p" ,
2452 "--output-format" , "stream-json" ,
2553 "--verbose" ,
2654 "--input-format" , "stream-json" ,
2755 "--include-partial-messages" ,
28- // bypassPermissions since there's no TTY for Claude Code's
29- // own approval TUI — commands would silently hang otherwise.
3056 "--permission-mode" , "bypassPermissions" ,
57+ "--resume" ,
3158 ] ;
59+ // Push the resume ID; we need the String to live long enough.
60+ args. push ( & resume_id) ;
3261 if let Some ( m) = model {
3362 args. push ( "--model" ) ;
3463 args. push ( m) ;
3564 }
3665
66+ Self :: spawn_with_args ( cwd, & args) . await
67+ }
68+
69+ /// Return the captured Claude CLI session ID, if available.
70+ pub fn claude_session_id ( & self ) -> Option < String > {
71+ self . claude_session_id . lock ( ) . ok ( ) . and_then ( |g| g. clone ( ) )
72+ }
73+
74+ async fn spawn_with_args (
75+ cwd : & Path ,
76+ args : & [ & str ] ,
77+ ) -> Result < ( Self , mpsc:: UnboundedReceiver < AgentEvent > ) > {
3778 let mut child = Command :: new ( "claude" )
38- . args ( & args)
79+ . args ( args)
3980 . current_dir ( cwd)
4081 . stdin ( std:: process:: Stdio :: piped ( ) )
4182 . stdout ( std:: process:: Stdio :: piped ( ) )
@@ -49,6 +90,7 @@ impl ClaudeSession {
4990
5091 let ( event_tx, event_rx) = mpsc:: unbounded_channel ( ) ;
5192 let ( stdin_tx, mut stdin_rx) = mpsc:: unbounded_channel :: < String > ( ) ;
93+ let claude_session_id: Arc < Mutex < Option < String > > > = Arc :: new ( Mutex :: new ( None ) ) ;
5294
5395 // Stdin writer
5496 tokio:: spawn ( async move {
@@ -62,6 +104,7 @@ impl ClaudeSession {
62104
63105 // Stdout reader
64106 let tx = event_tx;
107+ let session_id_clone = claude_session_id. clone ( ) ;
65108 tokio:: spawn ( async move {
66109 let reader = BufReader :: new ( stdout) ;
67110 let mut lines = reader. lines ( ) ;
@@ -72,6 +115,10 @@ impl ClaudeSession {
72115 let mut got_stream_deltas = false ;
73116 // Track active content blocks by their stream index for tool use / thinking.
74117 let mut active_blocks = std:: collections:: HashMap :: < u64 , BlockInfo > :: new ( ) ;
118+ // Map tool_id -> tool_name persisted across the turn so we can
119+ // look up names when the "user" tool_result event arrives (after
120+ // content_block_end has already removed the block from active_blocks).
121+ let mut tool_names = std:: collections:: HashMap :: < String , String > :: new ( ) ;
75122 // Track model name from message_start so we can use it in result.
76123 let mut current_model: Option < String > = None ;
77124
@@ -89,13 +136,26 @@ impl ClaudeSession {
89136 let events: Vec < AgentEvent > = match event_type {
90137 "system" => {
91138 if obj. get ( "subtype" ) . and_then ( |s| s. as_str ( ) ) == Some ( "init" ) {
92- vec ! [ AgentEvent :: SessionReady ]
139+ // Capture the session_id from the init event
140+ let sid = obj. get ( "session_id" ) . and_then ( |s| s. as_str ( ) ) . map ( |s| s. to_string ( ) ) ;
141+ if let Some ( ref s) = sid {
142+ if let Ok ( mut guard) = session_id_clone. lock ( ) {
143+ * guard = Some ( s. clone ( ) ) ;
144+ }
145+ }
146+ vec ! [ AgentEvent :: SessionReady { claude_session_id: sid } ]
93147 } else {
94148 vec ! [ ]
95149 }
96150 }
97151 "stream_event" => {
98152 let evts = handle_stream_event ( & obj, & mut turn_started, & mut active_blocks, & mut current_model) ;
153+ // Record tool_id -> tool_name for later lookup
154+ for evt in & evts {
155+ if let AgentEvent :: ToolUseStart { tool_id, tool_name } = evt {
156+ tool_names. insert ( tool_id. clone ( ) , tool_name. clone ( ) ) ;
157+ }
158+ }
99159 // Mark that we got real streaming deltas so we skip
100160 // the `assistant` snapshot which would duplicate them.
101161 if evts. iter ( ) . any ( |e| matches ! ( e, AgentEvent :: ContentDelta { .. }
@@ -141,6 +201,7 @@ impl ClaudeSession {
141201 turn_started = false ;
142202 got_stream_deltas = false ;
143203 active_blocks. clear ( ) ;
204+ tool_names. clear ( ) ;
144205 let evts = handle_result ( & obj, & current_model) ;
145206 current_model = None ;
146207 evts
@@ -155,34 +216,59 @@ impl ClaudeSession {
155216
156217 if let Some ( blocks) = content {
157218 let mut evts = Vec :: new ( ) ;
219+
220+ // Extract stdout/stderr from top-level tool_use_result
221+ let tool_use_result = obj. get ( "tool_use_result" ) ;
222+ let stdout = tool_use_result
223+ . and_then ( |r| r. get ( "stdout" ) )
224+ . and_then ( |s| s. as_str ( ) )
225+ . unwrap_or ( "" ) ;
226+ let stderr = tool_use_result
227+ . and_then ( |r| r. get ( "stderr" ) )
228+ . and_then ( |s| s. as_str ( ) )
229+ . unwrap_or ( "" ) ;
230+
158231 for block in blocks {
159232 if block. get ( "type" ) . and_then ( |t| t. as_str ( ) ) == Some ( "tool_result" ) {
160233 let tool_use_id = block
161234 . get ( "tool_use_id" )
162235 . and_then ( |id| id. as_str ( ) )
163236 . unwrap_or ( "" )
164237 . 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 ( ) ;
238+
239+ // Build the result content: prefer stdout/stderr
240+ // from tool_use_result, fall back to block content
241+ let result_content = if !stdout. is_empty ( ) || !stderr. is_empty ( ) {
242+ let mut parts = Vec :: new ( ) ;
243+ if !stdout. is_empty ( ) {
244+ parts. push ( stdout. to_string ( ) ) ;
245+ }
246+ if !stderr. is_empty ( ) {
247+ parts. push ( format ! ( "[stderr]\n {stderr}" ) ) ;
248+ }
249+ parts. join ( "\n " )
250+ } else {
251+ block
252+ . get ( "content" )
253+ . map ( |c| {
254+ if let Some ( s) = c. as_str ( ) {
255+ s. to_string ( )
256+ } else {
257+ serde_json:: to_string_pretty ( c) . unwrap_or_default ( )
258+ }
259+ } )
260+ . unwrap_or_default ( )
261+ } ;
262+
175263 let is_error = block
176264 . get ( "is_error" )
177265 . and_then ( |e| e. as_bool ( ) )
178266 . unwrap_or ( false ) ;
179267
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 ( ) )
268+ // Look up tool name from the turn's tool_names map
269+ let tool_name = tool_names
270+ . get ( & tool_use_id)
271+ . cloned ( )
186272 . unwrap_or_default ( ) ;
187273
188274 evts. push ( AgentEvent :: ToolResult {
@@ -213,7 +299,7 @@ impl ClaudeSession {
213299 debug ! ( "Claude stdout reader finished" ) ;
214300 } ) ;
215301
216- Ok ( ( Self { child, stdin_tx } , event_rx) )
302+ Ok ( ( Self { child, stdin_tx, claude_session_id } , event_rx) )
217303 }
218304
219305 pub fn send_message ( & self , text : & str ) -> Result < ( ) > {
0 commit comments