use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use crate::approval::types::PermissionReply;
pub struct OpenCodeBridge {
client: Client,
base_url: String,
password: Option<String>,
}
impl OpenCodeBridge {
pub fn new(url: &str, port: u16, password: Option<String>) -> Self {
let base_url = format!("{}:{}", url.trim_end_matches('/'), port);
OpenCodeBridge {
client: Client::new(),
base_url,
password,
}
}
pub fn get_base_url(&self) -> &str {
&self.base_url
}
fn auth_header(&self) -> Option<String> {
self.password.as_ref().map(|pw| {
let credentials = format!(":{}", pw);
format!("Basic {}", STANDARD.encode(credentials))
})
}
async fn post_json(&self, path: &str, body: serde_json::Value) -> Result<()> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.client.post(&url).json(&body);
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req
.send()
.await
.with_context(|| friendly_connection_error(&self.base_url))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("OpenCode API error {}: {}", status, body);
}
Ok(())
}
pub async fn append_prompt(
&self,
text: &str,
directory: Option<&str>,
workspace: Option<&str>,
) -> Result<()> {
let mut url = format!("{}/tui/append-prompt", self.base_url);
let mut params = Vec::new();
if let Some(dir) = directory {
params.push(format!("directory={}", urlencoding_encode(dir)));
}
if let Some(ws) = workspace {
params.push(format!("workspace={}", urlencoding_encode(ws)));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let mut req = self.client.post(&url).json(&json!({"text": text}));
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
let resp = req
.send()
.await
.with_context(|| friendly_connection_error(&self.base_url))?;
if !resp.status().is_success() {
let status = resp.status();
anyhow::bail!("append_prompt failed: {}", status);
}
Ok(())
}
pub async fn submit_prompt(&self) -> Result<()> {
self.post_json("/tui/submit-prompt", json!({})).await
}
pub async fn is_connected(&self) -> bool {
let url = format!("{}/", self.base_url);
let client = Client::builder()
.timeout(Duration::from_secs(2))
.build()
.unwrap_or_else(|_| Client::new());
let mut req = client.get(&url);
if let Some(auth) = self.auth_header() {
req = req.header("Authorization", auth);
}
req.send().await.is_ok()
}
pub async fn reply_permission(
&self,
id: &str,
reply: PermissionReply,
message: Option<&str>,
) -> Result<()> {
let path = format!("/permission/{}/reply", id);
let mut body = json!({"reply": reply});
if let Some(msg) = message {
body["message"] = json!(msg);
}
self.post_json(&path, body).await
}
pub async fn reply_question(&self, id: &str, answers: Vec<Vec<String>>) -> Result<()> {
let path = format!("/question/{}/reply", id);
self.post_json(&path, json!({"answers": answers})).await
}
pub async fn reject_question(&self, id: &str) -> Result<()> {
let path = format!("/question/{}/reject", id);
self.post_json(&path, json!({})).await
}
}
fn friendly_connection_error(base_url: &str) -> String {
format!(
"Cannot connect to OpenCode at {}. Make sure OpenCode is running with --port flag: opencode --port <port>",
base_url
)
}
fn urlencoding_encode(s: &str) -> String {
s.chars()
.flat_map(|c| {
if c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
vec![c]
} else {
format!("%{:02X}", c as u32).chars().collect::<Vec<_>>()
}
})
.collect()
}