use serde::Deserialize;
use crate::commands::project::resolve_dir;
use crate::formatters::banner::{
fallback_session_name, normalize_workdir, print_launch_banner,
print_launch_banner_reconnecting, tmux_has_session,
};
pub(crate) async fn launch(
client: &reqwest::Client,
url: &str,
dir: Option<String>,
) -> anyhow::Result<()> {
let path = resolve_dir(dir)?;
let path = path.canonicalize().unwrap_or(path);
let workdir = path.to_string_lossy().to_string();
if let Some(existing) = find_existing_session(client, url, &workdir).await
&& !existing.is_empty()
&& tmux_has_session(&existing)
{
print_launch_banner_reconnecting(&workdir, &existing);
let status = std::process::Command::new("tmux")
.args(["attach-session", "-t", &existing])
.status()?;
if !status.success() {
anyhow::bail!("tmux attach-session exited with failure");
}
return Ok(());
}
let fw = trusty_mpm::core::paths::FrameworkPaths::default();
if let Err(err) = trusty_mpm::core::session_launch::prepare_session(&fw, &path) {
eprintln!("warning: session preparation failed: {err}");
}
#[derive(Deserialize)]
struct Body {
#[serde(default)]
name: String,
id: Option<trusty_mpm::core::session::SessionId>,
}
let folder_name = fallback_session_name(&path);
let (tmux_name, session_id) = match client
.post(format!("{url}/sessions"))
.json(&serde_json::json!({
"project": path,
"project_path": path,
"name": folder_name,
}))
.send()
.await
{
Ok(resp) => match resp.error_for_status() {
Ok(resp) => match resp.json::<Body>().await {
Ok(body) if !body.name.is_empty() => (body.name, body.id),
Ok(body) => (folder_name.clone(), body.id),
_ => (folder_name.clone(), None),
},
Err(err) => {
eprintln!("warning: daemon rejected session registration: {err}");
(folder_name.clone(), None)
}
},
Err(err) => {
eprintln!("warning: daemon unreachable ({err}); launching without registration");
(folder_name.clone(), None)
}
};
let instructions_path = match trusty_mpm::core::instruction_pipeline::install_system_prompt() {
Ok(path) => Some(path),
Err(err) => {
eprintln!("warning: failed to install system prompt: {err}");
None
}
};
let mpm_cfg = trusty_mpm::core::config::MpmConfig::load_default();
let pm_model = trusty_mpm::core::model_inject::resolve_pm_model(&mpm_cfg, None);
let prompt = trusty_mpm::core::session_launch::build_system_prompt_for(&path);
let prompt_path = trusty_mpm::core::model_inject::write_prompt_file(&prompt);
if prompt_path.is_none() {
eprintln!("warning: failed to write system prompt file; launching without prompt");
}
let claude_cmd = trusty_mpm::core::model_inject::build_claude_command(
Some(&pm_model),
prompt_path.as_deref(),
);
print_launch_banner(
&workdir,
&tmux_name,
instructions_path.as_deref().or(prompt_path.as_deref()),
);
let new_session = std::process::Command::new("tmux")
.args(["new-session", "-d", "-s", &tmux_name, "-c", &workdir])
.status();
if !matches!(new_session, Ok(s) if s.success()) {
anyhow::bail!("failed to create tmux session {tmux_name} in {workdir}");
}
let send = std::process::Command::new("tmux")
.args(["send-keys", "-t", &tmux_name, &claude_cmd, "Enter"])
.status();
if !matches!(send, Ok(s) if s.success()) {
anyhow::bail!("tmux session {tmux_name} created but failed to start claude");
}
if let Some(session_id) = session_id {
let claude_pid = trusty_mpm::core::process::find_claude_pid_in_tmux(
&tmux_name,
10, std::time::Duration::from_millis(500), );
if let Some(pid) = claude_pid {
let _ = client
.patch(format!("{url}/sessions/{}/pid", session_id.0))
.json(&serde_json::json!({ "pid": pid }))
.send()
.await;
tracing::info!(
"claude process PID {pid} registered for session {}",
session_id.0
);
} else {
tracing::warn!("could not find claude PID for session {tmux_name} after retries");
}
}
let status = std::process::Command::new("tmux")
.args(["attach-session", "-t", &tmux_name])
.status()?;
if !status.success() {
anyhow::bail!("tmux attach-session exited with failure");
}
Ok(())
}
pub(crate) async fn connect(
client: &reqwest::Client,
url: &str,
dir: Option<String>,
) -> anyhow::Result<()> {
let path = resolve_dir(dir)?;
let path = path.canonicalize().unwrap_or(path);
let workdir = path.to_string_lossy().to_string();
if let Some(existing) = find_existing_session(client, url, &workdir).await
&& !existing.is_empty()
&& tmux_has_session(&existing)
{
print_launch_banner_reconnecting(&workdir, &existing);
let status = std::process::Command::new("tmux")
.args(["attach-session", "-t", &existing])
.status()?;
if !status.success() {
anyhow::bail!("tmux attach-session exited with failure");
}
return Ok(());
}
#[derive(Deserialize)]
struct Body {
#[serde(default)]
name: String,
}
let folder_name = fallback_session_name(&path);
let tmux_name = match client
.post(format!("{url}/api/v1/sessions/connect"))
.json(&serde_json::json!({
"project": path,
"project_path": path,
"name": folder_name,
}))
.send()
.await
{
Ok(resp) => match resp.error_for_status() {
Ok(resp) => match resp.json::<Body>().await {
Ok(body) if !body.name.is_empty() => body.name,
_ => folder_name.clone(),
},
Err(err) => {
eprintln!("warning: daemon rejected session registration: {err}");
folder_name.clone()
}
},
Err(err) => {
eprintln!("warning: daemon unreachable ({err}); connecting without registration");
folder_name.clone()
}
};
print_launch_banner(&workdir, &tmux_name, None);
let already_running = tmux_has_session(&tmux_name);
let new_session = std::process::Command::new("tmux")
.args(["new-session", "-A", "-d", "-s", &tmux_name, "-c", &workdir])
.status();
if !matches!(new_session, Ok(s) if s.success()) {
anyhow::bail!("failed to create tmux session {tmux_name} in {workdir}");
}
if !already_running {
let send = std::process::Command::new("tmux")
.args(["send-keys", "-t", &tmux_name, "claude", "Enter"])
.status();
if !matches!(send, Ok(s) if s.success()) {
anyhow::bail!("tmux session {tmux_name} created but failed to start claude");
}
}
let status = std::process::Command::new("tmux")
.args(["attach-session", "-t", &tmux_name])
.status()?;
if !status.success() {
anyhow::bail!("tmux attach-session exited with failure");
}
Ok(())
}
async fn find_existing_session(
client: &reqwest::Client,
url: &str,
workdir: &str,
) -> Option<String> {
#[derive(Deserialize)]
struct Row {
#[serde(default)]
workdir: String,
#[serde(default)]
tmux_name: String,
}
let target = normalize_workdir(workdir);
let resp = client.get(format!("{url}/sessions")).send().await.ok()?;
let rows: Vec<Row> = resp.error_for_status().ok()?.json().await.ok()?;
rows.into_iter()
.find(|r| !r.tmux_name.is_empty() && normalize_workdir(&r.workdir) == target)
.map(|r| r.tmux_name)
}