Skip to main content

agent_code_lib/services/
bridge.rs

1//! IDE bridge protocol.
2//!
3//! HTTP server that allows IDE extensions (VS Code, JetBrains, etc.)
4//! to communicate with the running agent. The bridge exposes endpoints
5//! for sending messages, reading conversation state, and streaming
6//! events.
7//!
8//! # Protocol
9//!
10//! - `POST /message` — send a user message to the agent
11//! - `GET /messages` — retrieve conversation history
12//! - `GET /status` — agent status (idle/active, model, session)
13//! - `GET /events` — SSE stream of real-time events
14//!
15//! The bridge runs on localhost with a random high port. The port
16//! is written to a lock file so IDE extensions can discover it.
17
18use std::path::PathBuf;
19
20use serde::{Deserialize, Serialize};
21use tracing::info;
22
23/// Bridge server state shared across request handlers.
24pub struct BridgeState {
25    /// Port the bridge is listening on.
26    pub port: u16,
27    /// Path to the lock file (for IDE discovery).
28    pub lock_file: PathBuf,
29}
30
31/// Status response returned by GET /status.
32#[derive(Debug, Serialize)]
33pub struct BridgeStatus {
34    pub session_id: String,
35    pub model: String,
36    pub cwd: String,
37    pub is_active: bool,
38    pub turn_count: usize,
39    pub tool_count: usize,
40}
41
42/// Message request body for POST /message.
43#[derive(Debug, Deserialize)]
44pub struct BridgeMessageRequest {
45    pub content: String,
46}
47
48/// Write the bridge lock file so IDE extensions can find us.
49pub fn write_lock_file(port: u16, cwd: &str) -> Result<PathBuf, String> {
50    let lock_dir = dirs::cache_dir()
51        .unwrap_or_else(|| PathBuf::from("/tmp"))
52        .join("agent-code")
53        .join("bridge");
54
55    std::fs::create_dir_all(&lock_dir)
56        .map_err(|e| format!("Failed to create bridge lock dir: {e}"))?;
57
58    let lock_file = lock_dir.join(format!("{}.lock", std::process::id()));
59
60    let content = serde_json::json!({
61        "port": port,
62        "pid": std::process::id(),
63        "cwd": cwd,
64        "started_at": chrono::Utc::now().to_rfc3339(),
65    });
66
67    std::fs::write(&lock_file, serde_json::to_string_pretty(&content).unwrap())
68        .map_err(|e| format!("Failed to write lock file: {e}"))?;
69
70    info!("Bridge lock file: {}", lock_file.display());
71    Ok(lock_file)
72}
73
74/// Remove the bridge lock file on shutdown.
75pub fn remove_lock_file(lock_file: &PathBuf) {
76    if lock_file.exists() {
77        let _ = std::fs::remove_file(lock_file);
78    }
79}
80
81/// Discover running bridge instances by scanning lock files.
82pub fn discover_bridges() -> Vec<BridgeInstance> {
83    let lock_dir = match dirs::cache_dir() {
84        Some(d) => d.join("agent-code").join("bridge"),
85        None => return Vec::new(),
86    };
87
88    if !lock_dir.is_dir() {
89        return Vec::new();
90    }
91
92    std::fs::read_dir(&lock_dir)
93        .ok()
94        .into_iter()
95        .flatten()
96        .flatten()
97        .filter_map(|entry| {
98            let content = std::fs::read_to_string(entry.path()).ok()?;
99            let data: serde_json::Value = serde_json::from_str(&content).ok()?;
100
101            let pid = data.get("pid")?.as_u64()? as u32;
102            let port = data.get("port")?.as_u64()? as u16;
103            let cwd = data.get("cwd")?.as_str()?.to_string();
104
105            // Check if the process is still running.
106            let alive = std::process::Command::new("kill")
107                .args(["-0", &pid.to_string()])
108                .output()
109                .map(|o| o.status.success())
110                .unwrap_or(false);
111
112            if !alive {
113                // Stale lock file — clean it up.
114                let _ = std::fs::remove_file(entry.path());
115                return None;
116            }
117
118            Some(BridgeInstance { pid, port, cwd })
119        })
120        .collect()
121}
122
123/// A discovered running bridge instance.
124#[derive(Debug)]
125pub struct BridgeInstance {
126    pub pid: u32,
127    pub port: u16,
128    pub cwd: String,
129}