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,
session_id: &str,
) -> Result<(), RuntimeError> {
let command = build_spawn_command(cwd, task, session_id);
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()))?;
if let Err(e) = self
.tmux
.set_environment(tmux_name, "TM_MANAGED_SESSION_ID", session_id)
{
debug!(
session = %tmux_name,
"tmux set-environment TM_MANAGED_SESSION_ID failed (non-fatal): {e}"
);
}
Ok(())
}
}
fn build_spawn_command(cwd: &Path, task: &str, session_id: &str) -> String {
format!(
"export TM_MANAGED_SESSION_ID={}; {TCODE_BINARY} run-task {DEFAULT_AGENT} {} --project {}",
shell_single_quote(session_id),
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,
session_id: &str,
gh_env: &[(String, String)],
) -> Result<(), RuntimeError> {
let _ = gh_env;
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, session_id)
}
fn identify(&self) -> &str {
"tcode"
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::FakeTmux;
use super::*;
const TEST_SESSION_ID: &str = "11111111-2222-3333-4444-555555555555";
#[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", TEST_SESSION_ID);
assert!(
cmd.contains("tcode run-task "),
"command must invoke the tcode binary: {cmd}"
);
}
#[test]
fn tcode_build_spawn_command_exports_managed_session_id() {
let cmd = build_spawn_command(Path::new("/tmp/ws"), "do a thing", TEST_SESSION_ID);
let expected_prefix = format!("export TM_MANAGED_SESSION_ID='{TEST_SESSION_ID}'; ");
assert!(
cmd.starts_with(&expected_prefix),
"spawn command must start with the session-id export: {cmd}"
);
assert!(
cmd.find("export TM_MANAGED_SESSION_ID").unwrap() < cmd.find("tcode run-task").unwrap(),
"export must precede the tcode invocation: {cmd}"
);
}
#[test]
fn tcode_build_spawn_command_uses_default_agent() {
let cmd = build_spawn_command(Path::new("/tmp/ws"), "do a thing", TEST_SESSION_ID);
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", TEST_SESSION_ID);
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", TEST_SESSION_ID);
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", TEST_SESSION_ID)
.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_SESSION_ID)
);
let env_sets = fake.env_sets.lock().expect("env-set log mutex");
assert!(
env_sets.iter().any(|(name, key, value)| name == "tmpm-test"
&& key == "TM_MANAGED_SESSION_ID"
&& value == TEST_SESSION_ID),
"send_spawn_command must call tmux set-environment with TM_MANAGED_SESSION_ID: {env_sets:?}"
);
}
#[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",
TEST_SESSION_ID,
&[],
);
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"
);
}
}
}