use std::path::Path;
use std::sync::Arc;
use tracing::debug;
use crate::session_manager::ManagedTmuxDriver;
use super::RuntimeAdapter;
use super::RuntimeError;
const TCODE_BINARY: &str = "tcode";
const DEFAULT_AGENT: &str = "engineer";
pub struct TcodeAdapter {
tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>,
}
impl TcodeAdapter {
pub fn new(tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>) -> Self {
Self { tmux }
}
fn tcode_available() -> bool {
which::which(TCODE_BINARY).is_ok()
}
fn send_spawn_command(
&self,
tmux_name: &str,
cwd: &Path,
task: &str,
) -> Result<(), RuntimeError> {
let command = build_spawn_command(cwd, task);
debug!(
session = %tmux_name,
cwd = %cwd.display(),
task = %task,
"spawning tcode in tmux pane"
);
self.tmux
.send_line(tmux_name, &command)
.map_err(|e| RuntimeError::TmuxUnavailable(e.to_string()))
}
}
fn build_spawn_command(cwd: &Path, task: &str) -> String {
format!(
"{TCODE_BINARY} run-task {DEFAULT_AGENT} {} --project {}",
shell_single_quote(task),
shell_single_quote(&cwd.to_string_lossy()),
)
}
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
impl RuntimeAdapter for TcodeAdapter {
fn spawn(&self, tmux_name: &str, cwd: &Path, task: &str) -> Result<(), RuntimeError> {
if !Self::tcode_available() {
return Err(RuntimeError::BinaryNotFound(
"tcode binary not found on PATH — install trusty-code first".into(),
));
}
self.send_spawn_command(tmux_name, cwd, task)
}
fn identify(&self) -> &str {
"tcode"
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::FakeTmux;
use super::*;
#[test]
fn tcode_adapter_identifies() {
let fake = FakeTmux::new();
let adapter = TcodeAdapter::new(fake);
assert_eq!(adapter.identify(), "tcode");
}
#[test]
fn tcode_build_spawn_command_starts_with_binary() {
let cmd = build_spawn_command(Path::new("/tmp/ws"), "do a thing");
assert!(
cmd.starts_with("tcode run-task "),
"command must invoke the tcode binary: {cmd}"
);
}
#[test]
fn tcode_build_spawn_command_uses_default_agent() {
let cmd = build_spawn_command(Path::new("/tmp/ws"), "do a thing");
assert!(
cmd.contains(&format!("run-task {DEFAULT_AGENT} ")),
"command must target the default agent: {cmd}"
);
}
#[test]
fn tcode_build_spawn_command_includes_project_and_task() {
let cmd = build_spawn_command(Path::new("/work/space dir"), "fix bug #12");
assert!(
cmd.contains("--project '/work/space dir'"),
"command must root tcode at the (quoted) workspace cwd: {cmd}"
);
assert!(
cmd.contains("'fix bug #12'"),
"command must carry the (quoted) task description: {cmd}"
);
}
#[test]
fn tcode_build_spawn_command_does_not_scrub_api_key() {
let cmd = build_spawn_command(Path::new("/tmp/ws"), "task");
assert!(
!cmd.contains("ANTHROPIC_API_KEY"),
"tcode adapter must preserve ANTHROPIC_API_KEY (no env scrub): {cmd}"
);
assert!(
!cmd.contains("env -u"),
"tcode adapter must not scrub the environment: {cmd}"
);
}
#[test]
fn tcode_shell_quote_escapes_embedded_quote() {
let quoted = shell_single_quote("it's broken");
assert_eq!(quoted, "'it'\\''s broken'");
}
#[test]
fn tcode_adapter_binary_check_returns_bool() {
let _ = TcodeAdapter::tcode_available();
}
#[test]
fn tcode_adapter_spawn_sends_run_task() {
let fake = FakeTmux::new();
let adapter = TcodeAdapter::new(fake.clone());
adapter
.send_spawn_command("tmpm-test", Path::new("/tmp"), "some task")
.expect("send_spawn_command");
let sends = fake.sends.lock().expect("send log mutex");
assert_eq!(sends.len(), 1);
assert_eq!(sends[0].0, "tmpm-test");
assert_eq!(
sends[0].1,
build_spawn_command(Path::new("/tmp"), "some task")
);
}
#[test]
fn tcode_adapter_spawn_errors_when_binary_missing() {
let fake = FakeTmux::new();
let adapter = TcodeAdapter::new(fake.clone());
let result = adapter.spawn("tmpm-test", Path::new("/tmp"), "some task");
if TcodeAdapter::tcode_available() {
assert!(result.is_ok(), "spawn should succeed when tcode is on PATH");
assert_eq!(fake.sends.lock().expect("send log mutex").len(), 1);
} else {
assert!(
matches!(result, Err(RuntimeError::BinaryNotFound(_))),
"spawn must report BinaryNotFound when tcode is absent: {result:?}"
);
assert!(
fake.sends.lock().expect("send log mutex").is_empty(),
"no command may be sent when the binary is missing"
);
}
}
}