@@ -136,6 +136,41 @@ async function handleQuery(cmd) {
136136 options . resume = sessionId ;
137137 }
138138
139+ // canUseTool callback — sends approval requests to Rust, waits for response
140+ options . canUseTool = async ( toolName , input ) => {
141+ const requestId = String ( ++ approvalCounter ) ;
142+
143+ // AskUserQuestion — forward to frontend
144+ if ( toolName === "AskUserQuestion" ) {
145+ emit ( {
146+ type : "ask_user_question" ,
147+ requestId,
148+ questions : input . questions || [ ] ,
149+ } ) ;
150+ } else {
151+ // Regular tool approval
152+ emit ( {
153+ type : "approval_request" ,
154+ requestId,
155+ toolName,
156+ input,
157+ } ) ;
158+ }
159+
160+ // Wait for the response from Rust
161+ return new Promise ( ( resolve ) => {
162+ pendingApprovals . set ( requestId , {
163+ resolve : ( resp ) => {
164+ if ( resp . decision === "allow" ) {
165+ resolve ( { behavior : "allow" , updatedInput : input } ) ;
166+ } else {
167+ resolve ( { behavior : "deny" , message : resp . message || "User denied this action" } ) ;
168+ }
169+ } ,
170+ } ) ;
171+ } ) ;
172+ } ;
173+
139174 // Create an AbortController for this query.
140175 const abort = new AbortController ( ) ;
141176 currentAbort = abort ;
@@ -144,13 +179,18 @@ async function handleQuery(cmd) {
144179 let capturedSessionId = sessionId || null ;
145180 let turnEmitted = false ;
146181
182+ // Emit turn_started so the frontend shows "generating" state
183+ emit ( { type : "turn_started" } ) ;
184+
147185 try {
148186 for await ( const message of query ( { prompt, options } ) ) {
149187 // Abort was requested while iterating.
150188 if ( abort . signal . aborted ) break ;
151189
190+ const msgType = message . type ;
191+
152192 // ── system messages ──
153- if ( message . type === "system" ) {
193+ if ( msgType === "system" ) {
154194 if ( message . subtype === "init" && message . session_id ) {
155195 capturedSessionId = message . session_id ;
156196 emit ( { type : "session_ready" , sessionId : message . session_id } ) ;
@@ -159,17 +199,44 @@ async function handleQuery(cmd) {
159199 }
160200
161201 // ── result message (final) ──
162- if ( "result" in message ) {
163- // Extract usage if available.
164- if ( message . usage ) {
202+ if ( msgType === "result" || "result" in message ) {
203+ // Extract usage from various possible locations
204+ const usage = message . usage || message . modelUsage ;
205+ const costUsd = message . total_cost_usd ?? message . cost_usd ?? 0 ;
206+ const modelName = message . model ?? model ?? "unknown" ;
207+
208+ if ( usage ) {
209+ // SDK may nest usage per-model or flat
210+ let inputTokens = 0 , outputTokens = 0 , cacheRead = 0 , cacheWrite = 0 ;
211+
212+ if ( typeof usage === "object" && ! Array . isArray ( usage ) ) {
213+ // Check if it's a flat usage object or per-model
214+ if ( usage . input_tokens != null || usage . inputTokens != null ) {
215+ inputTokens = usage . input_tokens ?? usage . inputTokens ?? 0 ;
216+ outputTokens = usage . output_tokens ?? usage . outputTokens ?? 0 ;
217+ cacheRead = usage . cache_read_input_tokens ?? usage . cacheReadInputTokens ?? 0 ;
218+ cacheWrite = usage . cache_creation_input_tokens ?? usage . cacheCreationInputTokens ?? 0 ;
219+ } else {
220+ // Per-model usage: { "claude-...": { inputTokens, ... } }
221+ for ( const [ , modelUsage ] of Object . entries ( usage ) ) {
222+ if ( typeof modelUsage === "object" ) {
223+ inputTokens += modelUsage . inputTokens ?? 0 ;
224+ outputTokens += modelUsage . outputTokens ?? 0 ;
225+ cacheRead += modelUsage . cacheReadInputTokens ?? 0 ;
226+ cacheWrite += modelUsage . cacheCreationInputTokens ?? 0 ;
227+ }
228+ }
229+ }
230+ }
231+
165232 emit ( {
166233 type : "usage" ,
167- inputTokens : message . usage . input_tokens ?? 0 ,
168- outputTokens : message . usage . output_tokens ?? 0 ,
169- cacheRead : message . usage . cache_read_input_tokens ?? message . usage . cache_read_tokens ?? 0 ,
170- cacheWrite : message . usage . cache_creation_input_tokens ?? message . usage . cache_write_tokens ?? 0 ,
171- costUsd : message . cost_usd ?? message . total_cost_usd ?? 0 ,
172- model : message . model ?? model ?? "unknown" ,
234+ inputTokens,
235+ outputTokens,
236+ cacheRead,
237+ cacheWrite,
238+ costUsd,
239+ model : modelName ,
173240 } ) ;
174241 }
175242
@@ -181,17 +248,53 @@ async function handleQuery(cmd) {
181248 // ── streaming content messages ──
182249 // The SDK yields various message shapes. We normalise them to our protocol.
183250
184- const msgType = message . type ;
185-
186251 // stream_event wrapping Anthropic API SSE events
187252 if ( msgType === "stream_event" && message . event ) {
188253 handleStreamEvent ( message . event ) ;
189254 continue ;
190255 }
191256
192- // assistant message snapshots (full content)
257+ // assistant message — extract content blocks
193258 if ( msgType === "assistant" && message . message ?. content ) {
194- // We prefer stream_event deltas; skip snapshot processing.
259+ for ( const block of message . message . content ) {
260+ if ( block . type === "text" && block . text ) {
261+ emit ( { type : "text_delta" , text : block . text } ) ;
262+ } else if ( block . type === "tool_use" ) {
263+ emit ( {
264+ type : "tool_use_start" ,
265+ toolId : block . id ?? "" ,
266+ toolName : block . name ?? "tool" ,
267+ } ) ;
268+ if ( block . input ) {
269+ emit ( {
270+ type : "tool_use_input" ,
271+ toolId : block . id ?? "" ,
272+ inputJson : JSON . stringify ( block . input ) ,
273+ } ) ;
274+ }
275+ } else if ( block . type === "thinking" && block . thinking ) {
276+ emit ( { type : "thinking_delta" , text : block . thinking } ) ;
277+ }
278+ }
279+ continue ;
280+ }
281+
282+ // user message — contains tool results
283+ if ( msgType === "user" && message . message ?. content ) {
284+ for ( const block of message . message . content ) {
285+ if ( block . type === "tool_result" ) {
286+ const content = typeof block . content === "string"
287+ ? block . content
288+ : JSON . stringify ( block . content ) ;
289+ emit ( {
290+ type : "tool_result" ,
291+ toolId : block . tool_use_id ?? "" ,
292+ toolName : "" ,
293+ content,
294+ isError : ! ! block . is_error ,
295+ } ) ;
296+ }
297+ }
195298 continue ;
196299 }
197300
0 commit comments