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 = 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
403pub 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
441pub 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 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 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 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 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
517pub 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 }
600 CursorStreamEvent::End => {
601 if !paused {
603 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 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
668pub 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 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 let mut sse = Vec::new();
711 let mut framer = CursorSseFramer::new(&mut sse, new_message_id, new_model);
712
713 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 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 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
817fn 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#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::anthropic::schema::MessagesRequest;
889 use std::sync::Mutex;
890
891 static REGISTRY_LOCK: Mutex<()> = Mutex::new(());
893
894 #[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 assert_eq!(json["command"], "ls");
949 }
950
951 #[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 assert!(
1055 messages[0]
1056 .get("shellStream")
1057 .and_then(|s| s.get("start"))
1058 .is_some()
1059 );
1060 assert_eq!(messages[1]["shellStream"]["stdout"]["data"], "hi");
1062 assert_eq!(messages[2]["shellStream"]["exit"]["code"], 0);
1064 assert_eq!(messages[2]["shellStream"]["exit"]["cwd"], "/tmp");
1065 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 #[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 #[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 #[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 #[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}