1use 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#[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#[derive(Debug, Clone, PartialEq)]
34pub struct CursorNativeToolResult {
35 pub content: String,
36 pub is_error: bool,
37}
38
39#[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 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#[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
146static 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
165pub struct BridgeRegistry;
167
168impl BridgeRegistry {
169 pub fn insert(state: CursorBridgeState) {
171 let mut reg = BRIDGE_REGISTRY.lock().unwrap();
172 reg.sessions.push(state);
173 }
174
175 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 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 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 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 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 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 pub fn clear() {
225 let mut reg = BRIDGE_REGISTRY.lock().unwrap();
226 reg.sessions.clear();
227 }
228
229 pub fn active_count() -> usize {
231 let reg = BRIDGE_REGISTRY.lock().unwrap();
232 reg.sessions.len()
233 }
234}
235
236pub 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
254pub 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
273pub 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
301pub 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
328pub 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
336pub fn build_tool_use_input_json(tool: &PendingCursorTool) -> String {
339 serde_json::to_string(&tool.input_json()).unwrap_or_else(|_| "{}".to_string())
340}
341
342pub 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
360pub 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
392pub 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
430pub 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 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 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 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 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
506pub 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 }
589 CursorStreamEvent::End => {
590 if !paused {
592 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 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
657pub 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 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 let mut sse = Vec::new();
700 let mut framer = CursorSseFramer::new(&mut sse, new_message_id, new_model);
701
702 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 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 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
806fn 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#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::anthropic::schema::MessagesRequest;
878 use std::sync::Mutex;
879
880 static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
882
883 #[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 assert_eq!(json["command"], "ls");
938 }
939
940 #[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 assert!(
1044 messages[0]
1045 .get("shellStream")
1046 .and_then(|s| s.get("start"))
1047 .is_some()
1048 );
1049 assert_eq!(messages[1]["shellStream"]["stdout"]["data"], "hi");
1051 assert_eq!(messages[2]["shellStream"]["exit"]["code"], 0);
1053 assert_eq!(messages[2]["shellStream"]["exit"]["cwd"], "/tmp");
1054 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 #[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 #[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 #[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 #[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}