Skip to main content

claude_codex/providers/cursor/
tool_bridge.rs

1//! Cursor tool bridge: state machine, result builders, pending tool tracking,
2//! stream re-entry, and SSE pause/resume.
3//!
4//! The bridge coordinates the pause-and-continue lifecycle when the Cursor
5//! upstream emits a `<tool_use>` text block. The bridge pauses the SSE stream,
6//! stores the pending tool, and waits for Claude's `tool_result` in the next
7//! client request. On resume it builds Cursor protocol result messages and
8//! continues producing SSE output from stored upstream events.
9
10use std::collections::BTreeSet;
11use std::sync::Mutex;
12
13use once_cell::sync::Lazy;
14
15use crate::anthropic::schema::MessagesRequest;
16use crate::providers::cursor::response::CursorStreamEvent;
17use crate::providers::cursor::sse::CursorSseFramer;
18use crate::providers::cursor::tool_use_xml::{CursorToolUseXmlParser, RecoveredCursorEvent};
19
20// ---------------------------------------------------------------------------
21// Types
22// ---------------------------------------------------------------------------
23
24/// Execution context for a Cursor tool.
25#[derive(Debug, Clone, PartialEq)]
26pub struct CursorExec {
27    pub id: Option<u64>,
28    pub exec_id: Option<String>,
29    pub args: serde_json::Value,
30}
31
32/// A tool result produced by Claude.
33#[derive(Debug, Clone, PartialEq)]
34pub struct CursorNativeToolResult {
35    pub content: String,
36    pub is_error: bool,
37}
38
39/// A pending Cursor tool that Claude must fulfill.
40#[derive(Debug, Clone)]
41pub enum PendingCursorTool {
42    Read {
43        tool_use_id: String,
44        path: String,
45    },
46    Write {
47        tool_use_id: String,
48        path: String,
49        content: String,
50    },
51    Bash {
52        tool_use_id: String,
53        command: String,
54        working_directory: String,
55        timeout_ms: u64,
56    },
57}
58
59impl PendingCursorTool {
60    pub fn name(&self) -> &'static str {
61        match self {
62            Self::Read { .. } => "Read",
63            Self::Write { .. } => "Write",
64            Self::Bash { .. } => "Bash",
65        }
66    }
67
68    pub fn tool_use_id(&self) -> &str {
69        match self {
70            Self::Read { tool_use_id, .. }
71            | Self::Write { tool_use_id, .. }
72            | Self::Bash { tool_use_id, .. } => tool_use_id,
73        }
74    }
75
76    /// Build the JSON input that matches the Claude tool_use block.
77    pub fn input_json(&self) -> serde_json::Value {
78        match self {
79            Self::Read { path, .. } => {
80                serde_json::json!({ "file_path": path })
81            }
82            Self::Write { path, content, .. } => {
83                serde_json::json!({ "file_path": path, "content": content })
84            }
85            Self::Bash {
86                command,
87                working_directory,
88                timeout_ms,
89                ..
90            } => {
91                let cmd = if working_directory.is_empty() {
92                    command.clone()
93                } else {
94                    format!("cd '{}' && {command}", working_directory)
95                };
96                serde_json::json!({
97                    "command": cmd,
98                    "timeout": timeout_ms,
99                    "description": "Run Cursor-requested shell command",
100                    "run_in_background": false,
101                    "dangerouslyDisableSandbox": false
102                })
103            }
104        }
105    }
106}
107
108/// Bridge state stored per session.
109#[derive(Debug)]
110pub struct CursorBridgeState {
111    pub session_id: String,
112    pub message_id: String,
113    pub model: String,
114    pub pending_tool: Option<PendingCursorTool>,
115    pub remaining_events: Vec<CursorStreamEvent>,
116    pub event_cursor: usize,
117    pub input_tokens: u64,
118    pub output_tokens: u64,
119    pub allowed_tool_names: Option<BTreeSet<String>>,
120    pub xml_parser: CursorToolUseXmlParser,
121}
122
123impl CursorBridgeState {
124    fn new(
125        session_id: String,
126        message_id: String,
127        model: String,
128        allowed_tool_names: Option<BTreeSet<String>>,
129        id_factory: Box<dyn FnMut() -> String + Send>,
130    ) -> Self {
131        Self {
132            session_id,
133            message_id,
134            model,
135            pending_tool: None,
136            remaining_events: Vec::new(),
137            event_cursor: 0,
138            input_tokens: 0,
139            output_tokens: 0,
140            allowed_tool_names: allowed_tool_names.clone(),
141            xml_parser: CursorToolUseXmlParser::new_with_id_factory(allowed_tool_names, id_factory),
142        }
143    }
144}
145
146// ---------------------------------------------------------------------------
147// Global bridge registry
148// ---------------------------------------------------------------------------
149
150static BRIDGE_REGISTRY: Lazy<Mutex<BridgeRegistryInner>> =
151    Lazy::new(|| Mutex::new(BridgeRegistryInner::new()));
152
153struct BridgeRegistryInner {
154    sessions: Vec<CursorBridgeState>,
155}
156
157impl BridgeRegistryInner {
158    fn new() -> Self {
159        Self {
160            sessions: Vec::new(),
161        }
162    }
163}
164
165/// Global registry of active bridge sessions.
166pub struct BridgeRegistry;
167
168impl BridgeRegistry {
169    /// Insert a new bridge state for a session.
170    pub fn insert(state: CursorBridgeState) {
171        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
172        reg.sessions.push(state);
173    }
174
175    /// Get the bridge state for a session.
176    pub fn get(session_id: &str) -> Option<usize> {
177        let reg = BRIDGE_REGISTRY.lock().unwrap();
178        reg.sessions.iter().position(|s| s.session_id == session_id)
179    }
180
181    /// Get the pending tool for a session (if any).
182    pub fn pending_tool(session_id: &str) -> Option<PendingCursorTool> {
183        let reg = BRIDGE_REGISTRY.lock().unwrap();
184        reg.sessions
185            .iter()
186            .find(|s| s.session_id == session_id)
187            .and_then(|s| s.pending_tool.clone())
188    }
189
190    /// Take the bridge state for a session (removes it).
191    pub fn take(session_id: &str) -> Option<CursorBridgeState> {
192        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
193        let pos = reg
194            .sessions
195            .iter()
196            .position(|s| s.session_id == session_id)?;
197        Some(reg.sessions.swap_remove(pos))
198    }
199
200    /// Remove a bridge state for a session.
201    pub fn remove(session_id: &str) {
202        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
203        reg.sessions.retain(|s| s.session_id != session_id);
204    }
205
206    /// Insert or update the pending tool for a session.
207    pub fn set_pending_tool(session_id: &str, tool: PendingCursorTool) {
208        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
209        if let Some(state) = reg.sessions.iter_mut().find(|s| s.session_id == session_id) {
210            state.pending_tool = Some(tool);
211        }
212    }
213
214    /// Update usage for a session.
215    pub fn record_usage(session_id: &str, input_tokens: u64, output_tokens: u64) {
216        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
217        if let Some(state) = reg.sessions.iter_mut().find(|s| s.session_id == session_id) {
218            state.input_tokens = input_tokens.max(state.input_tokens);
219            state.output_tokens = output_tokens.max(state.output_tokens);
220        }
221    }
222
223    /// Clear all bridge state.
224    pub fn clear() {
225        let mut reg = BRIDGE_REGISTRY.lock().unwrap();
226        reg.sessions.clear();
227    }
228
229    /// Number of active sessions.
230    pub fn active_count() -> usize {
231        let reg = BRIDGE_REGISTRY.lock().unwrap();
232        reg.sessions.len()
233    }
234}
235
236// ---------------------------------------------------------------------------
237// Tool detection helpers
238// ---------------------------------------------------------------------------
239
240/// Extract advertised tool names from a MessagesRequest.
241pub fn advertised_tool_names(body: &MessagesRequest) -> Option<BTreeSet<String>> {
242    let tools = body.extra.get("tools")?.as_array()?;
243    if tools.is_empty() {
244        return None;
245    }
246    let names: BTreeSet<String> = tools
247        .iter()
248        .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
249        .map(|n| n.to_string())
250        .collect();
251    if names.is_empty() { None } else { Some(names) }
252}
253
254/// Whether the request can use the Cursor native tool bridge.
255///
256/// Returns `true` when the request is streaming, has a session id, and
257/// advertises at least one of Read, Write, or Bash.
258pub fn can_bridge_cursor_native_tools(body: &MessagesRequest, session_id: Option<&str>) -> bool {
259    let _sid = match session_id {
260        Some(id) if !id.is_empty() => id,
261        _ => return false,
262    };
263    if !body.stream {
264        return false;
265    }
266    let names = match advertised_tool_names(body) {
267        Some(n) => n,
268        None => return false,
269    };
270    names.contains("Read") || names.contains("Write") || names.contains("Bash")
271}
272
273// ---------------------------------------------------------------------------
274// Result helpers
275// ---------------------------------------------------------------------------
276
277/// Find the last `tool_result` block matching `tool_use_id` in the request.
278pub fn find_tool_result<'a>(
279    body: &'a MessagesRequest,
280    tool_use_id: &str,
281) -> Option<&'a serde_json::Value> {
282    for message in body.messages.iter().rev() {
283        if message.role != "user" {
284            continue;
285        }
286        let blocks = match &message.content {
287            serde_json::Value::Array(arr) => arr,
288            _ => continue,
289        };
290        for block in blocks.iter().rev() {
291            if block.get("type").and_then(|t| t.as_str()) == Some("tool_result")
292                && block.get("tool_use_id").and_then(|t| t.as_str()) == Some(tool_use_id)
293            {
294                return Some(block);
295            }
296        }
297    }
298    None
299}
300
301/// Render the content of a `tool_result` block into a string.
302pub fn render_tool_result_content(result: &serde_json::Value) -> String {
303    let content = match result.get("content") {
304        Some(serde_json::Value::String(s)) => return s.clone(),
305        Some(serde_json::Value::Array(arr)) => arr.clone(),
306        _ => return String::new(),
307    };
308    let parts: Vec<String> = content
309        .iter()
310        .map(|block| match block.get("type").and_then(|t| t.as_str()) {
311            Some("text") => block
312                .get("text")
313                .and_then(|t| t.as_str())
314                .unwrap_or("")
315                .to_string(),
316            Some("image") => "[image result omitted]".to_string(),
317            Some("thinking") => block
318                .get("thinking")
319                .and_then(|t| t.as_str())
320                .unwrap_or("")
321                .to_string(),
322            _ => serde_json::to_string(block).unwrap_or_default(),
323        })
324        .collect();
325    parts.join("\n")
326}
327
328/// Whether a `tool_result` block indicates an error.
329pub fn tool_result_is_error(result: &serde_json::Value) -> bool {
330    result
331        .get("is_error")
332        .and_then(|e| e.as_bool())
333        .unwrap_or(false)
334}
335
336/// Build the partial JSON string for a pending tool's input (for the
337/// input_json_delta in the tool_use content block).
338pub fn build_tool_use_input_json(tool: &PendingCursorTool) -> String {
339    serde_json::to_string(&tool.input_json()).unwrap_or_else(|_| "{}".to_string())
340}
341
342// ---------------------------------------------------------------------------
343// Cursor protocol message builders
344// ---------------------------------------------------------------------------
345
346/// Inject `id` and `execId` fields into a JSON payload.
347pub fn with_exec_ids(
348    exec: &CursorExec,
349    mut payload: serde_json::Map<String, serde_json::Value>,
350) -> serde_json::Value {
351    if let Some(id) = exec.id {
352        payload.insert("id".into(), id.into());
353    }
354    if let Some(ref exec_id) = exec.exec_id {
355        payload.insert("execId".into(), exec_id.clone().into());
356    }
357    serde_json::Value::Object(payload)
358}
359
360/// Build the Cursor `readResult` message from a Claude `tool_result`.
361pub fn build_read_result_from_native(
362    exec: &CursorExec,
363    result: &CursorNativeToolResult,
364) -> serde_json::Value {
365    let path = exec
366        .args
367        .get("file_path")
368        .and_then(|v| v.as_str())
369        .unwrap_or("");
370    let content = &result.content;
371    let lines = if content.is_empty() {
372        0
373    } else {
374        content.lines().count()
375    };
376    let file_size = content.len().to_string();
377
378    let read_result = if result.is_error {
379        serde_json::json!({
380            "success": {
381                "path": path,
382                "content": content,
383                "totalLines": lines,
384                "fileSize": file_size
385            }
386        })
387    } else {
388        serde_json::json!({
389            "success": {
390                "path": path,
391                "content": content,
392                "totalLines": lines,
393                "fileSize": file_size
394            }
395        })
396    };
397
398    let mut map = serde_json::Map::new();
399    map.insert("readResult".into(), read_result);
400    with_exec_ids(exec, map)
401}
402
403/// Build the Cursor `writeResult` message from a Claude `tool_result`.
404pub fn build_write_result_from_native(
405    exec: &CursorExec,
406    result: &CursorNativeToolResult,
407) -> serde_json::Value {
408    let path = exec
409        .args
410        .get("file_path")
411        .and_then(|v| v.as_str())
412        .unwrap_or("");
413
414    let write_result = if result.is_error {
415        serde_json::json!({
416            "error": {
417                "path": path,
418                "error": result.content
419            }
420        })
421    } else {
422        let lines = if result.content.is_empty() {
423            0
424        } else {
425            result.content.lines().count()
426        };
427        serde_json::json!({
428            "success": {
429                "path": path,
430                "linesCreated": lines,
431                "fileSize": result.content.len()
432            }
433        })
434    };
435
436    let mut map = serde_json::Map::new();
437    map.insert("writeResult".into(), write_result);
438    with_exec_ids(exec, map)
439}
440
441/// Build the collection of Cursor `shellStream` messages from a Claude
442/// `tool_result`.
443///
444/// Returns: start, stdout/stderr, exit, streamClose.
445pub fn build_shell_stream_result(
446    exec: &CursorExec,
447    result: &CursorNativeToolResult,
448    local_execution_time: std::time::Duration,
449    cwd: &str,
450) -> Vec<serde_json::Value> {
451    let mut messages: Vec<serde_json::Value> = Vec::new();
452
453    // Start
454    let start_msg = with_exec_ids(
455        exec,
456        serde_json::json!({ "shellStream": { "start": {} } })
457            .as_object()
458            .cloned()
459            .unwrap_or_default(),
460    );
461    messages.push(start_msg);
462
463    // Content (stdout or stderr)
464    if !result.content.is_empty() {
465        let stream_key = if result.is_error { "stderr" } else { "stdout" };
466        let content_msg = with_exec_ids(
467            exec,
468            serde_json::json!({ "shellStream": { stream_key: { "data": result.content } } })
469                .as_object()
470                .cloned()
471                .unwrap_or_default(),
472        );
473        messages.push(content_msg);
474    }
475
476    // Exit
477    let exit_code: u32 = if result.is_error { 1 } else { 0 };
478    let exit_msg = with_exec_ids(
479        exec,
480        serde_json::json!({
481            "shellStream": {
482                "exit": {
483                    "code": exit_code,
484                    "cwd": cwd,
485                    "localExecutionTimeMs": local_execution_time.as_millis() as u64,
486                }
487            }
488        })
489        .as_object()
490        .cloned()
491        .unwrap_or_default(),
492    );
493    messages.push(exit_msg);
494
495    // Stream close
496    if let Some(id) = exec.id {
497        let close_msg = serde_json::json!({
498            "execClientControlMessage": {
499                "streamClose": {
500                    "id": id
501                }
502            }
503        });
504        messages.push(close_msg);
505    } else {
506        let close_msg = serde_json::json!({
507            "execClientControlMessage": {
508                "streamClose": {}
509            }
510        });
511        messages.push(close_msg);
512    }
513
514    messages
515}
516
517// ---------------------------------------------------------------------------
518// Bridge start and resume
519// ---------------------------------------------------------------------------
520
521/// Start a new tool bridge session.
522///
523/// Processes upstream events through XML recovery. When a `<tool_use>` is
524/// recovered, emits the SSE pause (tool_use content block + message_stop with
525/// stop_reason="tool_use") and stores the bridge state for resume.
526///
527/// Returns the SSE bytes and whether a tool_use pause was emitted.
528pub fn start_cursor_tool_bridge(
529    message_id: &str,
530    model: &str,
531    session_id: &str,
532    events: &[CursorStreamEvent],
533    allowed_tool_names: Option<BTreeSet<String>>,
534    id_factory: Box<dyn FnMut() -> String + Send>,
535) -> (Vec<u8>, bool) {
536    let mut sse = Vec::new();
537    let mut framer = CursorSseFramer::new(&mut sse, message_id, model);
538
539    let mut state = CursorBridgeState::new(
540        session_id.to_string(),
541        message_id.to_string(),
542        model.to_string(),
543        allowed_tool_names,
544        id_factory,
545    );
546
547    let mut paused = false;
548
549    for event in events {
550        if paused {
551            state.remaining_events.push(event.clone());
552            continue;
553        }
554
555        match event {
556            CursorStreamEvent::ThinkingDelta { text } => {
557                framer.emit_thinking_delta(text);
558            }
559            CursorStreamEvent::TextDelta { text } => {
560                let recovered = state.xml_parser.push(text);
561                for recovered_event in &recovered {
562                    if paused {
563                        if let RecoveredCursorEvent::Text(t) = recovered_event {
564                            state
565                                .remaining_events
566                                .push(CursorStreamEvent::TextDelta { text: t.clone() });
567                        }
568                        continue;
569                    }
570                    match recovered_event {
571                        RecoveredCursorEvent::Text(t) => {
572                            framer.emit_text_delta(t);
573                        }
574                        RecoveredCursorEvent::ToolUse(tool_use) => {
575                            let input_json = serde_json::to_string(&tool_use.input)
576                                .unwrap_or_else(|_| "{}".to_string());
577                            framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
578
579                            if let Some(pending) = pending_from_recovered_tool(tool_use) {
580                                state.pending_tool = Some(pending);
581                            }
582
583                            paused = true;
584                        }
585                    }
586                }
587            }
588            CursorStreamEvent::Usage {
589                input_tokens,
590                output_tokens,
591                ..
592            } => {
593                framer.record_usage(*input_tokens, *output_tokens, 0, 0);
594                state.input_tokens = *input_tokens;
595                state.output_tokens = *output_tokens;
596            }
597            CursorStreamEvent::Session { .. } => {
598                // Session info is not mapped to SSE events
599            }
600            CursorStreamEvent::End => {
601                // If we haven't paused, finalize normally
602                if !paused {
603                    // Process any remaining XML before finalizing
604                    let flushed = state.xml_parser.flush();
605                    for evt in &flushed {
606                        if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
607                            let input_json = serde_json::to_string(&tool_use.input)
608                                .unwrap_or_else(|_| "{}".to_string());
609                            framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
610                            if let Some(pending) = pending_from_recovered_tool(tool_use) {
611                                state.pending_tool = Some(pending);
612                            }
613                            paused = true;
614                        }
615                    }
616                    if !paused {
617                        framer.finalize();
618                    }
619                }
620            }
621        }
622    }
623
624    if paused {
625        let remaining = state.remaining_events.clone();
626        let mut stored_state = CursorBridgeState::new(
627            session_id.to_string(),
628            message_id.to_string(),
629            model.to_string(),
630            state.allowed_tool_names.clone(),
631            Box::new(|| {
632                format!(
633                    "call_cursor_{}",
634                    uuid::Uuid::new_v4().to_string().replace('-', "")
635                )
636            }),
637        );
638        stored_state.pending_tool = state.pending_tool.clone();
639        stored_state.remaining_events = remaining;
640        stored_state.event_cursor = 0;
641        stored_state.input_tokens = state.input_tokens;
642        stored_state.output_tokens = state.output_tokens;
643        BridgeRegistry::insert(stored_state);
644    }
645
646    if !paused {
647        // Flush any remaining text from XML parser
648        let flushed = state.xml_parser.flush();
649        for evt in &flushed {
650            if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
651                let input_json =
652                    serde_json::to_string(&tool_use.input).unwrap_or_else(|_| "{}".to_string());
653                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
654                if let Some(pending) = pending_from_recovered_tool(tool_use) {
655                    state.pending_tool = Some(pending);
656                }
657                paused = true;
658            }
659        }
660        if !paused {
661            framer.finalize();
662        }
663    }
664
665    (sse, paused)
666}
667
668/// Resume a paused tool bridge session.
669///
670/// Finds the stored state by session_id, resolves the pending tool with
671/// Claude's `tool_result`, and continues producing SSE from remaining events.
672pub fn resume_cursor_tool_bridge(
673    session_id: &str,
674    new_message_id: &str,
675    new_model: &str,
676    result: &serde_json::Value,
677    pending_tool: &PendingCursorTool,
678) -> (Vec<serde_json::Value>, Vec<u8>) {
679    let native_result = CursorNativeToolResult {
680        content: render_tool_result_content(result),
681        is_error: tool_result_is_error(result),
682    };
683
684    // Build Cursor protocol messages for the resolved tool
685    let exec = CursorExec {
686        id: None,
687        exec_id: None,
688        args: pending_tool.input_json(),
689    };
690    let result_messages = match pending_tool {
691        PendingCursorTool::Read { .. } => {
692            let msg = build_read_result_from_native(&exec, &native_result);
693            vec![msg]
694        }
695        PendingCursorTool::Write { .. } => {
696            let msg = build_write_result_from_native(&exec, &native_result);
697            vec![msg]
698        }
699        PendingCursorTool::Bash {
700            working_directory, ..
701        } => build_shell_stream_result(
702            &exec,
703            &native_result,
704            std::time::Duration::from_millis(0),
705            working_directory,
706        ),
707    };
708
709    // Generate SSE continuation from remaining events
710    let mut sse = Vec::new();
711    let mut framer = CursorSseFramer::new(&mut sse, new_message_id, new_model);
712
713    // Retrieve stored state for remaining events
714    let remaining: Vec<CursorStreamEvent> = BridgeRegistry::pending_tool(session_id)
715        .and_then(|_| BridgeRegistry::take(session_id))
716        .map(|state| state.remaining_events)
717        .unwrap_or_default();
718
719    if remaining.is_empty() {
720        // No remaining events: just finalize
721        framer.finalize();
722    } else {
723        let mut xml_parser = CursorToolUseXmlParser::new(None);
724        let mut paused_again = false;
725
726        for event in &remaining {
727            match event {
728                CursorStreamEvent::ThinkingDelta { text } => {
729                    if !paused_again {
730                        framer.emit_thinking_delta(text);
731                    }
732                }
733                CursorStreamEvent::TextDelta { text } => {
734                    if paused_again {
735                        continue;
736                    }
737                    let recovered = xml_parser.push(text);
738                    for evt in &recovered {
739                        match evt {
740                            RecoveredCursorEvent::Text(t) => {
741                                framer.emit_text_delta(t);
742                            }
743                            RecoveredCursorEvent::ToolUse(tool_use) => {
744                                let input_json = serde_json::to_string(&tool_use.input)
745                                    .unwrap_or_else(|_| "{}".to_string());
746                                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
747                                paused_again = true;
748                            }
749                        }
750                    }
751                }
752                CursorStreamEvent::Usage {
753                    input_tokens,
754                    output_tokens,
755                    ..
756                } => {
757                    if !paused_again {
758                        framer.record_usage(*input_tokens, *output_tokens, 0, 0);
759                    }
760                }
761                CursorStreamEvent::Session { .. } => {}
762                CursorStreamEvent::End => {
763                    if !paused_again {
764                        // Flush before finalizing
765                        let flushed = xml_parser.flush();
766                        for evt in &flushed {
767                            if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
768                                let input_json = serde_json::to_string(&tool_use.input)
769                                    .unwrap_or_else(|_| "{}".to_string());
770                                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
771                                paused_again = true;
772                            }
773                        }
774                        if !paused_again {
775                            framer.finalize();
776                        }
777                    }
778                }
779            }
780        }
781
782        if !paused_again {
783            let flushed = xml_parser.flush();
784            for evt in &flushed {
785                if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
786                    let input_json =
787                        serde_json::to_string(&tool_use.input).unwrap_or_else(|_| "{}".to_string());
788                    framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
789                    paused_again = true;
790                }
791            }
792            if !paused_again {
793                framer.finalize();
794            }
795        }
796
797        if paused_again && !remaining.is_empty() {
798            let state = CursorBridgeState::new(
799                session_id.to_string(),
800                new_message_id.to_string(),
801                new_model.to_string(),
802                None,
803                Box::new(|| {
804                    format!(
805                        "call_cursor_{}",
806                        uuid::Uuid::new_v4().to_string().replace('-', "")
807                    )
808                }),
809            );
810            BridgeRegistry::insert(state);
811        }
812    }
813
814    (result_messages, sse)
815}
816
817// ---------------------------------------------------------------------------
818// Internal helpers
819// ---------------------------------------------------------------------------
820
821/// Create a `PendingCursorTool` from a recovered XML tool_use event.
822fn pending_from_recovered_tool(
823    tool_use: &crate::providers::cursor::tool_use_xml::RecoveredCursorToolUse,
824) -> Option<PendingCursorTool> {
825    match tool_use.name.as_str() {
826        "Read" => {
827            let file_path = tool_use
828                .input
829                .get("file_path")
830                .and_then(|v| v.as_str())
831                .unwrap_or("")
832                .to_string();
833            Some(PendingCursorTool::Read {
834                tool_use_id: tool_use.id.clone(),
835                path: file_path,
836            })
837        }
838        "Write" => {
839            let file_path = tool_use
840                .input
841                .get("file_path")
842                .and_then(|v| v.as_str())
843                .unwrap_or("")
844                .to_string();
845            let content = tool_use
846                .input
847                .get("content")
848                .and_then(|v| v.as_str())
849                .unwrap_or("")
850                .to_string();
851            Some(PendingCursorTool::Write {
852                tool_use_id: tool_use.id.clone(),
853                path: file_path,
854                content,
855            })
856        }
857        "Bash" => {
858            let command = tool_use
859                .input
860                .get("command")
861                .and_then(|v| v.as_str())
862                .unwrap_or("")
863                .to_string();
864            let working_directory = String::new();
865            let timeout_ms = tool_use
866                .input
867                .get("timeout")
868                .and_then(|v| v.as_u64())
869                .unwrap_or(30_000);
870            Some(PendingCursorTool::Bash {
871                tool_use_id: tool_use.id.clone(),
872                command,
873                working_directory,
874                timeout_ms,
875            })
876        }
877        _ => None,
878    }
879}
880
881// ---------------------------------------------------------------------------
882// Tests
883// ---------------------------------------------------------------------------
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use crate::anthropic::schema::MessagesRequest;
889    use std::sync::Mutex;
890
891    /// Serialize tests that share the global bridge registry.
892    static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
893
894    // -----------------------------------------------------------------------
895    // PendingCursorTool tests
896    // -----------------------------------------------------------------------
897
898    #[test]
899    fn pending_read_input_matches_claude_read_tool() {
900        let tool = PendingCursorTool::Read {
901            tool_use_id: "call_cursor_1".into(),
902            path: "/tmp/a".into(),
903        };
904        let json = tool.input_json();
905        assert_eq!(json["file_path"], "/tmp/a");
906        assert_eq!(tool.name(), "Read");
907        assert_eq!(tool.tool_use_id(), "call_cursor_1");
908    }
909
910    #[test]
911    fn pending_write_input_matches_claude_write_tool() {
912        let tool = PendingCursorTool::Write {
913            tool_use_id: "call_cursor_2".into(),
914            path: "/tmp/b".into(),
915            content: "hello".into(),
916        };
917        let json = tool.input_json();
918        assert_eq!(json["file_path"], "/tmp/b");
919        assert_eq!(json["content"], "hello");
920        assert_eq!(tool.name(), "Write");
921    }
922
923    #[test]
924    fn pending_bash_input_matches_claude_bash_tool() {
925        let tool = PendingCursorTool::Bash {
926            tool_use_id: "call_cursor_3".into(),
927            command: "pwd".into(),
928            working_directory: "/tmp".into(),
929            timeout_ms: 30_000,
930        };
931        let json = tool.input_json();
932        assert_eq!(json["command"], "cd '/tmp' && pwd");
933        assert_eq!(json["timeout"], 30_000);
934        assert_eq!(json["description"], "Run Cursor-requested shell command");
935        assert_eq!(tool.name(), "Bash");
936    }
937
938    #[test]
939    fn pending_bash_no_working_directory() {
940        let tool = PendingCursorTool::Bash {
941            tool_use_id: "call_cursor_4".into(),
942            command: "ls".into(),
943            working_directory: "".into(),
944            timeout_ms: 10_000,
945        };
946        let json = tool.input_json();
947        // Without a working directory, command is passed as-is
948        assert_eq!(json["command"], "ls");
949    }
950
951    // -----------------------------------------------------------------------
952    // Result builder tests
953    // -----------------------------------------------------------------------
954
955    #[test]
956    fn with_exec_ids_adds_id_and_exec_id() {
957        let exec = CursorExec {
958            id: Some(7),
959            exec_id: Some("exec-1".into()),
960            args: serde_json::json!({}),
961        };
962        let mut payload = serde_json::Map::new();
963        payload.insert("test".into(), serde_json::json!("value"));
964        let result = with_exec_ids(&exec, payload);
965        assert_eq!(result["id"], 7);
966        assert_eq!(result["execId"], "exec-1");
967        assert_eq!(result["test"], "value");
968    }
969
970    #[test]
971    fn with_exec_ids_omits_missing_fields() {
972        let exec = CursorExec {
973            id: None,
974            exec_id: None,
975            args: serde_json::json!({}),
976        };
977        let mut payload = serde_json::Map::new();
978        payload.insert("test".into(), serde_json::json!("v"));
979        let result = with_exec_ids(&exec, payload);
980        assert!(result.get("id").is_none());
981        assert!(result.get("execId").is_none());
982        assert_eq!(result["test"], "v");
983    }
984
985    #[test]
986    fn read_result_from_successful_result() {
987        let exec = CursorExec {
988            id: Some(1),
989            exec_id: None,
990            args: serde_json::json!({"file_path": "/tmp/a"}),
991        };
992        let result = CursorNativeToolResult {
993            content: "file content".into(),
994            is_error: false,
995        };
996        let msg = build_read_result_from_native(&exec, &result);
997        assert_eq!(msg["id"], 1);
998        assert_eq!(msg["readResult"]["success"]["path"], "/tmp/a");
999        assert_eq!(msg["readResult"]["success"]["content"], "file content");
1000        assert_eq!(msg["readResult"]["success"]["totalLines"], 1);
1001    }
1002
1003    #[test]
1004    fn write_result_from_successful_result() {
1005        let exec = CursorExec {
1006            id: Some(2),
1007            exec_id: None,
1008            args: serde_json::json!({"file_path": "/tmp/b", "content": "hi"}),
1009        };
1010        let result = CursorNativeToolResult {
1011            content: "success".into(),
1012            is_error: false,
1013        };
1014        let msg = build_write_result_from_native(&exec, &result);
1015        assert_eq!(msg["id"], 2);
1016        assert_eq!(msg["writeResult"]["success"]["path"], "/tmp/b");
1017        assert_eq!(msg["writeResult"]["success"]["linesCreated"], 1);
1018    }
1019
1020    #[test]
1021    fn write_result_from_error_result() {
1022        let exec = CursorExec {
1023            id: Some(3),
1024            exec_id: None,
1025            args: serde_json::json!({"file_path": "/tmp/c"}),
1026        };
1027        let result = CursorNativeToolResult {
1028            content: "permission denied".into(),
1029            is_error: true,
1030        };
1031        let msg = build_write_result_from_native(&exec, &result);
1032        assert_eq!(msg["writeResult"]["error"]["path"], "/tmp/c");
1033        assert_eq!(msg["writeResult"]["error"]["error"], "permission denied");
1034    }
1035
1036    #[test]
1037    fn shell_stream_result_emits_start_output_exit_and_close() {
1038        let exec = CursorExec {
1039            id: Some(7),
1040            exec_id: Some("e".into()),
1041            args: serde_json::json!({}),
1042        };
1043        let messages = build_shell_stream_result(
1044            &exec,
1045            &CursorNativeToolResult {
1046                content: "hi".into(),
1047                is_error: false,
1048            },
1049            std::time::Duration::from_millis(3),
1050            "/tmp",
1051        );
1052        assert_eq!(messages.len(), 4);
1053        // Start
1054        assert!(
1055            messages[0]
1056                .get("shellStream")
1057                .and_then(|s| s.get("start"))
1058                .is_some()
1059        );
1060        // Stdout content
1061        assert_eq!(messages[1]["shellStream"]["stdout"]["data"], "hi");
1062        // Exit
1063        assert_eq!(messages[2]["shellStream"]["exit"]["code"], 0);
1064        assert_eq!(messages[2]["shellStream"]["exit"]["cwd"], "/tmp");
1065        // Stream close
1066        assert_eq!(
1067            messages[3]["execClientControlMessage"]["streamClose"]["id"],
1068            7
1069        );
1070    }
1071
1072    #[test]
1073    fn shell_stream_handles_error_result() {
1074        let exec = CursorExec {
1075            id: Some(8),
1076            exec_id: None,
1077            args: serde_json::json!({}),
1078        };
1079        let messages = build_shell_stream_result(
1080            &exec,
1081            &CursorNativeToolResult {
1082                content: "error msg".into(),
1083                is_error: true,
1084            },
1085            std::time::Duration::from_millis(5),
1086            "/tmp",
1087        );
1088        assert_eq!(messages.len(), 4);
1089        assert_eq!(messages[1]["shellStream"]["stderr"]["data"], "error msg");
1090        assert_eq!(messages[2]["shellStream"]["exit"]["code"], 1);
1091    }
1092
1093    // -----------------------------------------------------------------------
1094    // find_tool_result tests
1095    // -----------------------------------------------------------------------
1096
1097    #[test]
1098    fn finds_tool_result_in_request() {
1099        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1100            "model": "cursor:gpt-5.5",
1101            "messages": [
1102                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "result text"}]}
1103            ]
1104        }))
1105        .unwrap();
1106        let result = find_tool_result(&body, "call_1");
1107        assert!(result.is_some());
1108        assert_eq!(
1109            result.unwrap().get("content").and_then(|c| c.as_str()),
1110            Some("result text")
1111        );
1112    }
1113
1114    #[test]
1115    fn find_tool_result_returns_none_when_not_found() {
1116        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1117            "model": "cursor:gpt-5.5",
1118            "messages": [
1119                {"role": "user", "content": [{"type": "text", "text": "hello"}]}
1120            ]
1121        }))
1122        .unwrap();
1123        assert!(find_tool_result(&body, "call_1").is_none());
1124    }
1125
1126    #[test]
1127    fn find_tool_result_scans_newest_first() {
1128        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1129            "model": "cursor:gpt-5.5",
1130            "messages": [
1131                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "old"}]},
1132                {"role": "assistant", "content": "ok"},
1133                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "new"}]}
1134            ]
1135        }))
1136        .unwrap();
1137        let result = find_tool_result(&body, "call_1");
1138        assert!(result.is_some());
1139        assert_eq!(
1140            result.unwrap().get("content").and_then(|c| c.as_str()),
1141            Some("new")
1142        );
1143    }
1144
1145    // -----------------------------------------------------------------------
1146    // advertised_tool_names tests
1147    // -----------------------------------------------------------------------
1148
1149    #[test]
1150    fn advertised_tool_names_extracts_read_write_bash() {
1151        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1152            "model": "cursor:gpt-5.5",
1153            "messages": [{"role": "user", "content": "hi"}],
1154            "tools": [
1155                {"name": "Read", "description": "read", "input_schema": {}},
1156                {"name": "Write", "description": "write", "input_schema": {}},
1157                {"name": "Bash", "description": "bash", "input_schema": {}}
1158            ]
1159        }))
1160        .unwrap();
1161        let names = advertised_tool_names(&body).unwrap();
1162        assert!(names.contains("Read"));
1163        assert!(names.contains("Write"));
1164        assert!(names.contains("Bash"));
1165    }
1166
1167    #[test]
1168    fn advertised_tool_names_no_tools_returns_none() {
1169        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1170            "model": "cursor:gpt-5.5",
1171            "messages": [{"role": "user", "content": "hi"}]
1172        }))
1173        .unwrap();
1174        assert!(advertised_tool_names(&body).is_none());
1175    }
1176
1177    #[test]
1178    fn can_bridge_returns_true_for_stream_with_read_tool() {
1179        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1180            "model": "cursor:gpt-5.5",
1181            "stream": true,
1182            "messages": [{"role": "user", "content": "hi"}],
1183            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1184        }))
1185        .unwrap();
1186        assert!(can_bridge_cursor_native_tools(&body, Some("session-1")));
1187    }
1188
1189    #[test]
1190    fn can_bridge_returns_false_for_non_streaming() {
1191        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1192            "model": "cursor:gpt-5.5",
1193            "stream": false,
1194            "messages": [{"role": "user", "content": "hi"}],
1195            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1196        }))
1197        .unwrap();
1198        assert!(!can_bridge_cursor_native_tools(&body, Some("session-1")));
1199    }
1200
1201    #[test]
1202    fn can_bridge_returns_false_without_session_id() {
1203        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1204            "model": "cursor:gpt-5.5",
1205            "stream": true,
1206            "messages": [{"role": "user", "content": "hi"}],
1207            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1208        }))
1209        .unwrap();
1210        assert!(!can_bridge_cursor_native_tools(&body, None));
1211        assert!(!can_bridge_cursor_native_tools(&body, Some("")));
1212    }
1213
1214    // -----------------------------------------------------------------------
1215    // BridgeRegistry tests
1216    // -----------------------------------------------------------------------
1217
1218    #[test]
1219    fn bridge_registry_manages_sessions() {
1220        let _lock = REGISTRY_LOCK.lock().unwrap();
1221        BridgeRegistry::clear();
1222        assert_eq!(BridgeRegistry::active_count(), 0);
1223
1224        let state = CursorBridgeState::new(
1225            "session-test".into(),
1226            "msg-1".into(),
1227            "cursor-test".into(),
1228            None,
1229            Box::new(|| "id".into()),
1230        );
1231        BridgeRegistry::insert(state);
1232        assert_eq!(BridgeRegistry::active_count(), 1);
1233        assert!(BridgeRegistry::get("session-test").is_some());
1234
1235        let state = BridgeRegistry::take("session-test");
1236        assert!(state.is_some());
1237        assert_eq!(BridgeRegistry::active_count(), 0);
1238    }
1239
1240    #[test]
1241    fn bridge_registry_set_and_get_pending_tool() {
1242        let _lock = REGISTRY_LOCK.lock().unwrap();
1243        BridgeRegistry::clear();
1244        let state = CursorBridgeState::new(
1245            "session-pt".into(),
1246            "msg-1".into(),
1247            "cursor-test".into(),
1248            None,
1249            Box::new(|| "id".into()),
1250        );
1251        BridgeRegistry::insert(state);
1252
1253        let tool = PendingCursorTool::Read {
1254            tool_use_id: "call_1".into(),
1255            path: "/tmp/a".into(),
1256        };
1257        BridgeRegistry::set_pending_tool("session-pt", tool);
1258
1259        let retrieved = BridgeRegistry::pending_tool("session-pt");
1260        assert!(retrieved.is_some());
1261        assert_eq!(retrieved.unwrap().name(), "Read");
1262
1263        BridgeRegistry::clear();
1264    }
1265
1266    // -----------------------------------------------------------------------
1267    // render_tool_result_content tests
1268    // -----------------------------------------------------------------------
1269
1270    #[test]
1271    fn renders_string_content() {
1272        let result = serde_json::json!({
1273            "type": "tool_result",
1274            "content": "plain string"
1275        });
1276        assert_eq!(render_tool_result_content(&result), "plain string");
1277    }
1278
1279    #[test]
1280    fn renders_array_content() {
1281        let result = serde_json::json!({
1282            "type": "tool_result",
1283            "content": [
1284                {"type": "text", "text": "part one"},
1285                {"type": "text", "text": "part two"}
1286            ]
1287        });
1288        let rendered = render_tool_result_content(&result);
1289        assert!(rendered.contains("part one"));
1290        assert!(rendered.contains("part two"));
1291    }
1292
1293    #[test]
1294    fn renders_mixed_content_types() {
1295        let result = serde_json::json!({
1296            "type": "tool_result",
1297            "content": [
1298                {"type": "text", "text": "text result"},
1299                {"type": "image", "source": {"type": "base64", "data": "AAAA"}}
1300            ]
1301        });
1302        let rendered = render_tool_result_content(&result);
1303        assert!(rendered.contains("text result"));
1304        assert!(rendered.contains("[image result omitted]"));
1305    }
1306
1307    #[test]
1308    fn render_empty_tool_result() {
1309        let result = serde_json::json!({"type": "tool_result"});
1310        assert_eq!(render_tool_result_content(&result), "");
1311    }
1312
1313    #[test]
1314    fn detects_error_from_tool_result() {
1315        let result = serde_json::json!({"type": "tool_result", "is_error": true});
1316        assert!(tool_result_is_error(&result));
1317
1318        let result = serde_json::json!({"type": "tool_result", "is_error": false});
1319        assert!(!tool_result_is_error(&result));
1320
1321        let result = serde_json::json!({"type": "tool_result"});
1322        assert!(!tool_result_is_error(&result));
1323    }
1324}