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 = serde_json::json!({
379        "success": {
380            "path": path,
381            "content": content,
382            "totalLines": lines,
383            "fileSize": file_size
384        }
385    });
386
387    let mut map = serde_json::Map::new();
388    map.insert("readResult".into(), read_result);
389    with_exec_ids(exec, map)
390}
391
392/// Build the Cursor `writeResult` message from a Claude `tool_result`.
393pub fn build_write_result_from_native(
394    exec: &CursorExec,
395    result: &CursorNativeToolResult,
396) -> serde_json::Value {
397    let path = exec
398        .args
399        .get("file_path")
400        .and_then(|v| v.as_str())
401        .unwrap_or("");
402
403    let write_result = if result.is_error {
404        serde_json::json!({
405            "error": {
406                "path": path,
407                "error": result.content
408            }
409        })
410    } else {
411        let lines = if result.content.is_empty() {
412            0
413        } else {
414            result.content.lines().count()
415        };
416        serde_json::json!({
417            "success": {
418                "path": path,
419                "linesCreated": lines,
420                "fileSize": result.content.len()
421            }
422        })
423    };
424
425    let mut map = serde_json::Map::new();
426    map.insert("writeResult".into(), write_result);
427    with_exec_ids(exec, map)
428}
429
430/// Build the collection of Cursor `shellStream` messages from a Claude
431/// `tool_result`.
432///
433/// Returns: start, stdout/stderr, exit, streamClose.
434pub fn build_shell_stream_result(
435    exec: &CursorExec,
436    result: &CursorNativeToolResult,
437    local_execution_time: std::time::Duration,
438    cwd: &str,
439) -> Vec<serde_json::Value> {
440    let mut messages: Vec<serde_json::Value> = Vec::new();
441
442    // Start
443    let start_msg = with_exec_ids(
444        exec,
445        serde_json::json!({ "shellStream": { "start": {} } })
446            .as_object()
447            .cloned()
448            .unwrap_or_default(),
449    );
450    messages.push(start_msg);
451
452    // Content (stdout or stderr)
453    if !result.content.is_empty() {
454        let stream_key = if result.is_error { "stderr" } else { "stdout" };
455        let content_msg = with_exec_ids(
456            exec,
457            serde_json::json!({ "shellStream": { stream_key: { "data": result.content } } })
458                .as_object()
459                .cloned()
460                .unwrap_or_default(),
461        );
462        messages.push(content_msg);
463    }
464
465    // Exit
466    let exit_code: u32 = if result.is_error { 1 } else { 0 };
467    let exit_msg = with_exec_ids(
468        exec,
469        serde_json::json!({
470            "shellStream": {
471                "exit": {
472                    "code": exit_code,
473                    "cwd": cwd,
474                    "localExecutionTimeMs": local_execution_time.as_millis() as u64,
475                }
476            }
477        })
478        .as_object()
479        .cloned()
480        .unwrap_or_default(),
481    );
482    messages.push(exit_msg);
483
484    // Stream close
485    if let Some(id) = exec.id {
486        let close_msg = serde_json::json!({
487            "execClientControlMessage": {
488                "streamClose": {
489                    "id": id
490                }
491            }
492        });
493        messages.push(close_msg);
494    } else {
495        let close_msg = serde_json::json!({
496            "execClientControlMessage": {
497                "streamClose": {}
498            }
499        });
500        messages.push(close_msg);
501    }
502
503    messages
504}
505
506// ---------------------------------------------------------------------------
507// Bridge start and resume
508// ---------------------------------------------------------------------------
509
510/// Start a new tool bridge session.
511///
512/// Processes upstream events through XML recovery. When a `<tool_use>` is
513/// recovered, emits the SSE pause (tool_use content block + message_stop with
514/// stop_reason="tool_use") and stores the bridge state for resume.
515///
516/// Returns the SSE bytes and whether a tool_use pause was emitted.
517pub fn start_cursor_tool_bridge(
518    message_id: &str,
519    model: &str,
520    session_id: &str,
521    events: &[CursorStreamEvent],
522    allowed_tool_names: Option<BTreeSet<String>>,
523    id_factory: Box<dyn FnMut() -> String + Send>,
524) -> (Vec<u8>, bool) {
525    let mut sse = Vec::new();
526    let mut framer = CursorSseFramer::new(&mut sse, message_id, model);
527
528    let mut state = CursorBridgeState::new(
529        session_id.to_string(),
530        message_id.to_string(),
531        model.to_string(),
532        allowed_tool_names,
533        id_factory,
534    );
535
536    let mut paused = false;
537
538    for event in events {
539        if paused {
540            state.remaining_events.push(event.clone());
541            continue;
542        }
543
544        match event {
545            CursorStreamEvent::ThinkingDelta { text } => {
546                framer.emit_thinking_delta(text);
547            }
548            CursorStreamEvent::TextDelta { text } => {
549                let recovered = state.xml_parser.push(text);
550                for recovered_event in &recovered {
551                    if paused {
552                        if let RecoveredCursorEvent::Text(t) = recovered_event {
553                            state
554                                .remaining_events
555                                .push(CursorStreamEvent::TextDelta { text: t.clone() });
556                        }
557                        continue;
558                    }
559                    match recovered_event {
560                        RecoveredCursorEvent::Text(t) => {
561                            framer.emit_text_delta(t);
562                        }
563                        RecoveredCursorEvent::ToolUse(tool_use) => {
564                            let input_json = serde_json::to_string(&tool_use.input)
565                                .unwrap_or_else(|_| "{}".to_string());
566                            framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
567
568                            if let Some(pending) = pending_from_recovered_tool(tool_use) {
569                                state.pending_tool = Some(pending);
570                            }
571
572                            paused = true;
573                        }
574                    }
575                }
576            }
577            CursorStreamEvent::Usage {
578                input_tokens,
579                output_tokens,
580                ..
581            } => {
582                framer.record_usage(*input_tokens, *output_tokens, 0, 0);
583                state.input_tokens = *input_tokens;
584                state.output_tokens = *output_tokens;
585            }
586            CursorStreamEvent::Session { .. } => {
587                // Session info is not mapped to SSE events
588            }
589            CursorStreamEvent::End => {
590                // If we haven't paused, finalize normally
591                if !paused {
592                    // Process any remaining XML before finalizing
593                    let flushed = state.xml_parser.flush();
594                    for evt in &flushed {
595                        if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
596                            let input_json = serde_json::to_string(&tool_use.input)
597                                .unwrap_or_else(|_| "{}".to_string());
598                            framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
599                            if let Some(pending) = pending_from_recovered_tool(tool_use) {
600                                state.pending_tool = Some(pending);
601                            }
602                            paused = true;
603                        }
604                    }
605                    if !paused {
606                        framer.finalize();
607                    }
608                }
609            }
610        }
611    }
612
613    if paused {
614        let remaining = state.remaining_events.clone();
615        let mut stored_state = CursorBridgeState::new(
616            session_id.to_string(),
617            message_id.to_string(),
618            model.to_string(),
619            state.allowed_tool_names.clone(),
620            Box::new(|| {
621                format!(
622                    "call_cursor_{}",
623                    uuid::Uuid::new_v4().to_string().replace('-', "")
624                )
625            }),
626        );
627        stored_state.pending_tool = state.pending_tool.clone();
628        stored_state.remaining_events = remaining;
629        stored_state.event_cursor = 0;
630        stored_state.input_tokens = state.input_tokens;
631        stored_state.output_tokens = state.output_tokens;
632        BridgeRegistry::insert(stored_state);
633    }
634
635    if !paused {
636        // Flush any remaining text from XML parser
637        let flushed = state.xml_parser.flush();
638        for evt in &flushed {
639            if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
640                let input_json =
641                    serde_json::to_string(&tool_use.input).unwrap_or_else(|_| "{}".to_string());
642                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
643                if let Some(pending) = pending_from_recovered_tool(tool_use) {
644                    state.pending_tool = Some(pending);
645                }
646                paused = true;
647            }
648        }
649        if !paused {
650            framer.finalize();
651        }
652    }
653
654    (sse, paused)
655}
656
657/// Resume a paused tool bridge session.
658///
659/// Finds the stored state by session_id, resolves the pending tool with
660/// Claude's `tool_result`, and continues producing SSE from remaining events.
661pub fn resume_cursor_tool_bridge(
662    session_id: &str,
663    new_message_id: &str,
664    new_model: &str,
665    result: &serde_json::Value,
666    pending_tool: &PendingCursorTool,
667) -> (Vec<serde_json::Value>, Vec<u8>) {
668    let native_result = CursorNativeToolResult {
669        content: render_tool_result_content(result),
670        is_error: tool_result_is_error(result),
671    };
672
673    // Build Cursor protocol messages for the resolved tool
674    let exec = CursorExec {
675        id: None,
676        exec_id: None,
677        args: pending_tool.input_json(),
678    };
679    let result_messages = match pending_tool {
680        PendingCursorTool::Read { .. } => {
681            let msg = build_read_result_from_native(&exec, &native_result);
682            vec![msg]
683        }
684        PendingCursorTool::Write { .. } => {
685            let msg = build_write_result_from_native(&exec, &native_result);
686            vec![msg]
687        }
688        PendingCursorTool::Bash {
689            working_directory, ..
690        } => build_shell_stream_result(
691            &exec,
692            &native_result,
693            std::time::Duration::from_millis(0),
694            working_directory,
695        ),
696    };
697
698    // Generate SSE continuation from remaining events
699    let mut sse = Vec::new();
700    let mut framer = CursorSseFramer::new(&mut sse, new_message_id, new_model);
701
702    // Retrieve stored state for remaining events
703    let remaining: Vec<CursorStreamEvent> = BridgeRegistry::pending_tool(session_id)
704        .and_then(|_| BridgeRegistry::take(session_id))
705        .map(|state| state.remaining_events)
706        .unwrap_or_default();
707
708    if remaining.is_empty() {
709        // No remaining events: just finalize
710        framer.finalize();
711    } else {
712        let mut xml_parser = CursorToolUseXmlParser::new(None);
713        let mut paused_again = false;
714
715        for event in &remaining {
716            match event {
717                CursorStreamEvent::ThinkingDelta { text } => {
718                    if !paused_again {
719                        framer.emit_thinking_delta(text);
720                    }
721                }
722                CursorStreamEvent::TextDelta { text } => {
723                    if paused_again {
724                        continue;
725                    }
726                    let recovered = xml_parser.push(text);
727                    for evt in &recovered {
728                        match evt {
729                            RecoveredCursorEvent::Text(t) => {
730                                framer.emit_text_delta(t);
731                            }
732                            RecoveredCursorEvent::ToolUse(tool_use) => {
733                                let input_json = serde_json::to_string(&tool_use.input)
734                                    .unwrap_or_else(|_| "{}".to_string());
735                                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
736                                paused_again = true;
737                            }
738                        }
739                    }
740                }
741                CursorStreamEvent::Usage {
742                    input_tokens,
743                    output_tokens,
744                    ..
745                } => {
746                    if !paused_again {
747                        framer.record_usage(*input_tokens, *output_tokens, 0, 0);
748                    }
749                }
750                CursorStreamEvent::Session { .. } => {}
751                CursorStreamEvent::End => {
752                    if !paused_again {
753                        // Flush before finalizing
754                        let flushed = xml_parser.flush();
755                        for evt in &flushed {
756                            if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
757                                let input_json = serde_json::to_string(&tool_use.input)
758                                    .unwrap_or_else(|_| "{}".to_string());
759                                framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
760                                paused_again = true;
761                            }
762                        }
763                        if !paused_again {
764                            framer.finalize();
765                        }
766                    }
767                }
768            }
769        }
770
771        if !paused_again {
772            let flushed = xml_parser.flush();
773            for evt in &flushed {
774                if let RecoveredCursorEvent::ToolUse(tool_use) = evt {
775                    let input_json =
776                        serde_json::to_string(&tool_use.input).unwrap_or_else(|_| "{}".to_string());
777                    framer.emit_tool_pause(&tool_use.id, &tool_use.name, &input_json);
778                    paused_again = true;
779                }
780            }
781            if !paused_again {
782                framer.finalize();
783            }
784        }
785
786        if paused_again && !remaining.is_empty() {
787            let state = CursorBridgeState::new(
788                session_id.to_string(),
789                new_message_id.to_string(),
790                new_model.to_string(),
791                None,
792                Box::new(|| {
793                    format!(
794                        "call_cursor_{}",
795                        uuid::Uuid::new_v4().to_string().replace('-', "")
796                    )
797                }),
798            );
799            BridgeRegistry::insert(state);
800        }
801    }
802
803    (result_messages, sse)
804}
805
806// ---------------------------------------------------------------------------
807// Internal helpers
808// ---------------------------------------------------------------------------
809
810/// Create a `PendingCursorTool` from a recovered XML tool_use event.
811fn pending_from_recovered_tool(
812    tool_use: &crate::providers::cursor::tool_use_xml::RecoveredCursorToolUse,
813) -> Option<PendingCursorTool> {
814    match tool_use.name.as_str() {
815        "Read" => {
816            let file_path = tool_use
817                .input
818                .get("file_path")
819                .and_then(|v| v.as_str())
820                .unwrap_or("")
821                .to_string();
822            Some(PendingCursorTool::Read {
823                tool_use_id: tool_use.id.clone(),
824                path: file_path,
825            })
826        }
827        "Write" => {
828            let file_path = tool_use
829                .input
830                .get("file_path")
831                .and_then(|v| v.as_str())
832                .unwrap_or("")
833                .to_string();
834            let content = tool_use
835                .input
836                .get("content")
837                .and_then(|v| v.as_str())
838                .unwrap_or("")
839                .to_string();
840            Some(PendingCursorTool::Write {
841                tool_use_id: tool_use.id.clone(),
842                path: file_path,
843                content,
844            })
845        }
846        "Bash" => {
847            let command = tool_use
848                .input
849                .get("command")
850                .and_then(|v| v.as_str())
851                .unwrap_or("")
852                .to_string();
853            let working_directory = String::new();
854            let timeout_ms = tool_use
855                .input
856                .get("timeout")
857                .and_then(|v| v.as_u64())
858                .unwrap_or(30_000);
859            Some(PendingCursorTool::Bash {
860                tool_use_id: tool_use.id.clone(),
861                command,
862                working_directory,
863                timeout_ms,
864            })
865        }
866        _ => None,
867    }
868}
869
870// ---------------------------------------------------------------------------
871// Tests
872// ---------------------------------------------------------------------------
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use crate::anthropic::schema::MessagesRequest;
878    use std::sync::Mutex;
879
880    /// Serialize tests that share the global bridge registry.
881    static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
882
883    // -----------------------------------------------------------------------
884    // PendingCursorTool tests
885    // -----------------------------------------------------------------------
886
887    #[test]
888    fn pending_read_input_matches_claude_read_tool() {
889        let tool = PendingCursorTool::Read {
890            tool_use_id: "call_cursor_1".into(),
891            path: "/tmp/a".into(),
892        };
893        let json = tool.input_json();
894        assert_eq!(json["file_path"], "/tmp/a");
895        assert_eq!(tool.name(), "Read");
896        assert_eq!(tool.tool_use_id(), "call_cursor_1");
897    }
898
899    #[test]
900    fn pending_write_input_matches_claude_write_tool() {
901        let tool = PendingCursorTool::Write {
902            tool_use_id: "call_cursor_2".into(),
903            path: "/tmp/b".into(),
904            content: "hello".into(),
905        };
906        let json = tool.input_json();
907        assert_eq!(json["file_path"], "/tmp/b");
908        assert_eq!(json["content"], "hello");
909        assert_eq!(tool.name(), "Write");
910    }
911
912    #[test]
913    fn pending_bash_input_matches_claude_bash_tool() {
914        let tool = PendingCursorTool::Bash {
915            tool_use_id: "call_cursor_3".into(),
916            command: "pwd".into(),
917            working_directory: "/tmp".into(),
918            timeout_ms: 30_000,
919        };
920        let json = tool.input_json();
921        assert_eq!(json["command"], "cd '/tmp' && pwd");
922        assert_eq!(json["timeout"], 30_000);
923        assert_eq!(json["description"], "Run Cursor-requested shell command");
924        assert_eq!(tool.name(), "Bash");
925    }
926
927    #[test]
928    fn pending_bash_no_working_directory() {
929        let tool = PendingCursorTool::Bash {
930            tool_use_id: "call_cursor_4".into(),
931            command: "ls".into(),
932            working_directory: "".into(),
933            timeout_ms: 10_000,
934        };
935        let json = tool.input_json();
936        // Without a working directory, command is passed as-is
937        assert_eq!(json["command"], "ls");
938    }
939
940    // -----------------------------------------------------------------------
941    // Result builder tests
942    // -----------------------------------------------------------------------
943
944    #[test]
945    fn with_exec_ids_adds_id_and_exec_id() {
946        let exec = CursorExec {
947            id: Some(7),
948            exec_id: Some("exec-1".into()),
949            args: serde_json::json!({}),
950        };
951        let mut payload = serde_json::Map::new();
952        payload.insert("test".into(), serde_json::json!("value"));
953        let result = with_exec_ids(&exec, payload);
954        assert_eq!(result["id"], 7);
955        assert_eq!(result["execId"], "exec-1");
956        assert_eq!(result["test"], "value");
957    }
958
959    #[test]
960    fn with_exec_ids_omits_missing_fields() {
961        let exec = CursorExec {
962            id: None,
963            exec_id: None,
964            args: serde_json::json!({}),
965        };
966        let mut payload = serde_json::Map::new();
967        payload.insert("test".into(), serde_json::json!("v"));
968        let result = with_exec_ids(&exec, payload);
969        assert!(result.get("id").is_none());
970        assert!(result.get("execId").is_none());
971        assert_eq!(result["test"], "v");
972    }
973
974    #[test]
975    fn read_result_from_successful_result() {
976        let exec = CursorExec {
977            id: Some(1),
978            exec_id: None,
979            args: serde_json::json!({"file_path": "/tmp/a"}),
980        };
981        let result = CursorNativeToolResult {
982            content: "file content".into(),
983            is_error: false,
984        };
985        let msg = build_read_result_from_native(&exec, &result);
986        assert_eq!(msg["id"], 1);
987        assert_eq!(msg["readResult"]["success"]["path"], "/tmp/a");
988        assert_eq!(msg["readResult"]["success"]["content"], "file content");
989        assert_eq!(msg["readResult"]["success"]["totalLines"], 1);
990    }
991
992    #[test]
993    fn write_result_from_successful_result() {
994        let exec = CursorExec {
995            id: Some(2),
996            exec_id: None,
997            args: serde_json::json!({"file_path": "/tmp/b", "content": "hi"}),
998        };
999        let result = CursorNativeToolResult {
1000            content: "success".into(),
1001            is_error: false,
1002        };
1003        let msg = build_write_result_from_native(&exec, &result);
1004        assert_eq!(msg["id"], 2);
1005        assert_eq!(msg["writeResult"]["success"]["path"], "/tmp/b");
1006        assert_eq!(msg["writeResult"]["success"]["linesCreated"], 1);
1007    }
1008
1009    #[test]
1010    fn write_result_from_error_result() {
1011        let exec = CursorExec {
1012            id: Some(3),
1013            exec_id: None,
1014            args: serde_json::json!({"file_path": "/tmp/c"}),
1015        };
1016        let result = CursorNativeToolResult {
1017            content: "permission denied".into(),
1018            is_error: true,
1019        };
1020        let msg = build_write_result_from_native(&exec, &result);
1021        assert_eq!(msg["writeResult"]["error"]["path"], "/tmp/c");
1022        assert_eq!(msg["writeResult"]["error"]["error"], "permission denied");
1023    }
1024
1025    #[test]
1026    fn shell_stream_result_emits_start_output_exit_and_close() {
1027        let exec = CursorExec {
1028            id: Some(7),
1029            exec_id: Some("e".into()),
1030            args: serde_json::json!({}),
1031        };
1032        let messages = build_shell_stream_result(
1033            &exec,
1034            &CursorNativeToolResult {
1035                content: "hi".into(),
1036                is_error: false,
1037            },
1038            std::time::Duration::from_millis(3),
1039            "/tmp",
1040        );
1041        assert_eq!(messages.len(), 4);
1042        // Start
1043        assert!(
1044            messages[0]
1045                .get("shellStream")
1046                .and_then(|s| s.get("start"))
1047                .is_some()
1048        );
1049        // Stdout content
1050        assert_eq!(messages[1]["shellStream"]["stdout"]["data"], "hi");
1051        // Exit
1052        assert_eq!(messages[2]["shellStream"]["exit"]["code"], 0);
1053        assert_eq!(messages[2]["shellStream"]["exit"]["cwd"], "/tmp");
1054        // Stream close
1055        assert_eq!(
1056            messages[3]["execClientControlMessage"]["streamClose"]["id"],
1057            7
1058        );
1059    }
1060
1061    #[test]
1062    fn shell_stream_handles_error_result() {
1063        let exec = CursorExec {
1064            id: Some(8),
1065            exec_id: None,
1066            args: serde_json::json!({}),
1067        };
1068        let messages = build_shell_stream_result(
1069            &exec,
1070            &CursorNativeToolResult {
1071                content: "error msg".into(),
1072                is_error: true,
1073            },
1074            std::time::Duration::from_millis(5),
1075            "/tmp",
1076        );
1077        assert_eq!(messages.len(), 4);
1078        assert_eq!(messages[1]["shellStream"]["stderr"]["data"], "error msg");
1079        assert_eq!(messages[2]["shellStream"]["exit"]["code"], 1);
1080    }
1081
1082    // -----------------------------------------------------------------------
1083    // find_tool_result tests
1084    // -----------------------------------------------------------------------
1085
1086    #[test]
1087    fn finds_tool_result_in_request() {
1088        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1089            "model": "cursor:gpt-5.5",
1090            "messages": [
1091                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "result text"}]}
1092            ]
1093        }))
1094        .unwrap();
1095        let result = find_tool_result(&body, "call_1");
1096        assert!(result.is_some());
1097        assert_eq!(
1098            result.unwrap().get("content").and_then(|c| c.as_str()),
1099            Some("result text")
1100        );
1101    }
1102
1103    #[test]
1104    fn find_tool_result_returns_none_when_not_found() {
1105        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1106            "model": "cursor:gpt-5.5",
1107            "messages": [
1108                {"role": "user", "content": [{"type": "text", "text": "hello"}]}
1109            ]
1110        }))
1111        .unwrap();
1112        assert!(find_tool_result(&body, "call_1").is_none());
1113    }
1114
1115    #[test]
1116    fn find_tool_result_scans_newest_first() {
1117        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1118            "model": "cursor:gpt-5.5",
1119            "messages": [
1120                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "old"}]},
1121                {"role": "assistant", "content": "ok"},
1122                {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "call_1", "content": "new"}]}
1123            ]
1124        }))
1125        .unwrap();
1126        let result = find_tool_result(&body, "call_1");
1127        assert!(result.is_some());
1128        assert_eq!(
1129            result.unwrap().get("content").and_then(|c| c.as_str()),
1130            Some("new")
1131        );
1132    }
1133
1134    // -----------------------------------------------------------------------
1135    // advertised_tool_names tests
1136    // -----------------------------------------------------------------------
1137
1138    #[test]
1139    fn advertised_tool_names_extracts_read_write_bash() {
1140        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1141            "model": "cursor:gpt-5.5",
1142            "messages": [{"role": "user", "content": "hi"}],
1143            "tools": [
1144                {"name": "Read", "description": "read", "input_schema": {}},
1145                {"name": "Write", "description": "write", "input_schema": {}},
1146                {"name": "Bash", "description": "bash", "input_schema": {}}
1147            ]
1148        }))
1149        .unwrap();
1150        let names = advertised_tool_names(&body).unwrap();
1151        assert!(names.contains("Read"));
1152        assert!(names.contains("Write"));
1153        assert!(names.contains("Bash"));
1154    }
1155
1156    #[test]
1157    fn advertised_tool_names_no_tools_returns_none() {
1158        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1159            "model": "cursor:gpt-5.5",
1160            "messages": [{"role": "user", "content": "hi"}]
1161        }))
1162        .unwrap();
1163        assert!(advertised_tool_names(&body).is_none());
1164    }
1165
1166    #[test]
1167    fn can_bridge_returns_true_for_stream_with_read_tool() {
1168        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1169            "model": "cursor:gpt-5.5",
1170            "stream": true,
1171            "messages": [{"role": "user", "content": "hi"}],
1172            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1173        }))
1174        .unwrap();
1175        assert!(can_bridge_cursor_native_tools(&body, Some("session-1")));
1176    }
1177
1178    #[test]
1179    fn can_bridge_returns_false_for_non_streaming() {
1180        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1181            "model": "cursor:gpt-5.5",
1182            "stream": false,
1183            "messages": [{"role": "user", "content": "hi"}],
1184            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1185        }))
1186        .unwrap();
1187        assert!(!can_bridge_cursor_native_tools(&body, Some("session-1")));
1188    }
1189
1190    #[test]
1191    fn can_bridge_returns_false_without_session_id() {
1192        let body: MessagesRequest = serde_json::from_value(serde_json::json!({
1193            "model": "cursor:gpt-5.5",
1194            "stream": true,
1195            "messages": [{"role": "user", "content": "hi"}],
1196            "tools": [{"name": "Read", "description": "read", "input_schema": {}}]
1197        }))
1198        .unwrap();
1199        assert!(!can_bridge_cursor_native_tools(&body, None));
1200        assert!(!can_bridge_cursor_native_tools(&body, Some("")));
1201    }
1202
1203    // -----------------------------------------------------------------------
1204    // BridgeRegistry tests
1205    // -----------------------------------------------------------------------
1206
1207    #[test]
1208    fn bridge_registry_manages_sessions() {
1209        let _lock = REGISTRY_LOCK.lock().unwrap();
1210        BridgeRegistry::clear();
1211        assert_eq!(BridgeRegistry::active_count(), 0);
1212
1213        let state = CursorBridgeState::new(
1214            "session-test".into(),
1215            "msg-1".into(),
1216            "cursor-test".into(),
1217            None,
1218            Box::new(|| "id".into()),
1219        );
1220        BridgeRegistry::insert(state);
1221        assert_eq!(BridgeRegistry::active_count(), 1);
1222        assert!(BridgeRegistry::get("session-test").is_some());
1223
1224        let state = BridgeRegistry::take("session-test");
1225        assert!(state.is_some());
1226        assert_eq!(BridgeRegistry::active_count(), 0);
1227    }
1228
1229    #[test]
1230    fn bridge_registry_set_and_get_pending_tool() {
1231        let _lock = REGISTRY_LOCK.lock().unwrap();
1232        BridgeRegistry::clear();
1233        let state = CursorBridgeState::new(
1234            "session-pt".into(),
1235            "msg-1".into(),
1236            "cursor-test".into(),
1237            None,
1238            Box::new(|| "id".into()),
1239        );
1240        BridgeRegistry::insert(state);
1241
1242        let tool = PendingCursorTool::Read {
1243            tool_use_id: "call_1".into(),
1244            path: "/tmp/a".into(),
1245        };
1246        BridgeRegistry::set_pending_tool("session-pt", tool);
1247
1248        let retrieved = BridgeRegistry::pending_tool("session-pt");
1249        assert!(retrieved.is_some());
1250        assert_eq!(retrieved.unwrap().name(), "Read");
1251
1252        BridgeRegistry::clear();
1253    }
1254
1255    // -----------------------------------------------------------------------
1256    // render_tool_result_content tests
1257    // -----------------------------------------------------------------------
1258
1259    #[test]
1260    fn renders_string_content() {
1261        let result = serde_json::json!({
1262            "type": "tool_result",
1263            "content": "plain string"
1264        });
1265        assert_eq!(render_tool_result_content(&result), "plain string");
1266    }
1267
1268    #[test]
1269    fn renders_array_content() {
1270        let result = serde_json::json!({
1271            "type": "tool_result",
1272            "content": [
1273                {"type": "text", "text": "part one"},
1274                {"type": "text", "text": "part two"}
1275            ]
1276        });
1277        let rendered = render_tool_result_content(&result);
1278        assert!(rendered.contains("part one"));
1279        assert!(rendered.contains("part two"));
1280    }
1281
1282    #[test]
1283    fn renders_mixed_content_types() {
1284        let result = serde_json::json!({
1285            "type": "tool_result",
1286            "content": [
1287                {"type": "text", "text": "text result"},
1288                {"type": "image", "source": {"type": "base64", "data": "AAAA"}}
1289            ]
1290        });
1291        let rendered = render_tool_result_content(&result);
1292        assert!(rendered.contains("text result"));
1293        assert!(rendered.contains("[image result omitted]"));
1294    }
1295
1296    #[test]
1297    fn render_empty_tool_result() {
1298        let result = serde_json::json!({"type": "tool_result"});
1299        assert_eq!(render_tool_result_content(&result), "");
1300    }
1301
1302    #[test]
1303    fn detects_error_from_tool_result() {
1304        let result = serde_json::json!({"type": "tool_result", "is_error": true});
1305        assert!(tool_result_is_error(&result));
1306
1307        let result = serde_json::json!({"type": "tool_result", "is_error": false});
1308        assert!(!tool_result_is_error(&result));
1309
1310        let result = serde_json::json!({"type": "tool_result"});
1311        assert!(!tool_result_is_error(&result));
1312    }
1313}