diff --git a/crates/plannotator-tui/src/cli.rs b/crates/plannotator-tui/src/cli.rs index 594b863..c9f01c1 100644 --- a/crates/plannotator-tui/src/cli.rs +++ b/crates/plannotator-tui/src/cli.rs @@ -141,7 +141,7 @@ fn show_config() -> Result<()> { /// `plannotator-tui herdr open [PATH] [--placement P] [--deliver-to PANE]`. fn herdr_command(args: &[String]) -> Result<()> { - use crate::herdr::launch::{OpenArgs, plan, plan_last, process_info, run}; + use crate::herdr::launch::{OpenArgs, agent_get, plan, plan_last, process_info, run}; let sub = args.first().map(String::as_str); if sub == Some("pane") { return herdr_pane(); @@ -172,7 +172,8 @@ fn herdr_command(args: &[String]) -> Result<()> { let probe = plan(&env, &config, OpenArgs { path: None, ..open.clone() }, &cwd)?; let pane = probe.deliver.as_ref().map(|t| t.pane.clone()).or(probe.target_pane); let pane = pane.context("no agent pane to read: not focused on one and no --deliver-to")?; - plan_last(&env, &config, open, &cwd, &process_info(&env, &pane)?)? + let agent = agent_get(&env, &pane); + plan_last(&env, &config, open, &cwd, &process_info(&env, &pane)?, agent.as_deref())? } else { plan(&env, &config, open, &cwd)? }; @@ -186,6 +187,7 @@ fn herdr_pane() -> Result<()> { crate::last::run(&crate::last::LastOptions { host: env.host.clone(), pid: Some(pid), + session: env.session.clone(), pick: 25, ..crate::last::LastOptions::default() }) diff --git a/crates/plannotator-tui/src/herdr/context.rs b/crates/plannotator-tui/src/herdr/context.rs index 7157730..f0767f8 100644 --- a/crates/plannotator-tui/src/herdr/context.rs +++ b/crates/plannotator-tui/src/herdr/context.rs @@ -49,6 +49,10 @@ pub(crate) struct HerdrEnv { pub(crate) message_pid: Option, /// `PLANNOTATOR_TUI_HOST`: which agent's transcript format to read. pub(crate) host: Option, + /// `PLANNOTATOR_TUI_SESSION`: the agent's transcript path, when Herdr knew it. + pub(crate) session: Option, + /// `PLANNOTATOR_TUI_SESSION_ID`: the agent's session id, for hosts without transcript files. + pub(crate) session_id: Option, } impl HerdrEnv { @@ -72,6 +76,8 @@ impl HerdrEnv { plugin_id: non_empty("HERDR_PLUGIN_ID"), message_pid: non_empty("PLANNOTATOR_TUI_MESSAGE_PID").and_then(|v| v.parse().ok()), host: non_empty("PLANNOTATOR_TUI_HOST"), + session: non_empty("PLANNOTATOR_TUI_SESSION").map(PathBuf::from), + session_id: non_empty("PLANNOTATOR_TUI_SESSION_ID"), } } diff --git a/crates/plannotator-tui/src/herdr/launch.rs b/crates/plannotator-tui/src/herdr/launch.rs index 69ddb18..010d4dd 100644 --- a/crates/plannotator-tui/src/herdr/launch.rs +++ b/crates/plannotator-tui/src/herdr/launch.rs @@ -36,6 +36,27 @@ pub(crate) struct Launch { pub(crate) plugin: String, /// Open an agent's last message instead of `file`: (pid, host label). pub(crate) message: Option<(u32, String)>, + /// The agent's session as Herdr reports it: a transcript path or a host-specific id. + pub(crate) session: Option, +} + +/// `agent_session` from `herdr agent get`: the exact transcript when Herdr knows it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum AgentSession { + Path(String), + Id(String), +} + +/// Parse `herdr agent get ` JSON for its `agent_session`, when present. +pub(crate) fn agent_session(agent_get_json: &str) -> Option { + let json: serde_json::Value = serde_json::from_str(agent_get_json).ok()?; + let session = json.pointer("/result/agent/agent_session")?; + let value = session.get("value")?.as_str()?.to_owned(); + match session.get("kind")?.as_str()? { + "path" => Some(AgentSession::Path(value)), + "id" => Some(AgentSession::Id(value)), + _ => None, + } } /// The agent process behind a pane, from `herdr pane process-info --pane ` JSON: the @@ -80,6 +101,7 @@ pub(crate) fn plan_last( args: OpenArgs, cwd: &Path, process_info_json: &str, + agent_get_json: Option<&str>, ) -> Result { let mut launch = plan(env, config, OpenArgs { path: None, ..args }, cwd)?; let pane = launch.deliver.as_ref().map(|t| t.pane.clone()).or_else(|| launch.target_pane.clone()); @@ -91,6 +113,7 @@ pub(crate) fn plan_last( }; launch.file.clone_from(&launch.cwd); launch.message = Some(message); + launch.session = agent_get_json.and_then(agent_session); Ok(launch) } @@ -181,6 +204,7 @@ pub(crate) fn plan(env: &HerdrEnv, config: &Config, args: OpenArgs, cwd: &Path) deliver, plugin: env.plugin_id.clone().unwrap_or_else(|| "plannotator-tui".to_owned()), message: None, + session: None, }) } @@ -211,6 +235,15 @@ pub(crate) fn argv(launch: &Launch) -> Vec { out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_MESSAGE_PID={pid}")]); out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_HOST={host}")]); out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_CWD={}", launch.cwd.display())]); + match &launch.session { + Some(AgentSession::Path(p)) => { + out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_SESSION={p}")]); + } + Some(AgentSession::Id(id)) => { + out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_SESSION_ID={id}")]); + } + None => {} + } } None => out.extend(["--env".to_owned(), format!("PLANNOTATOR_TUI_FILE={}", launch.file.display())]), } @@ -235,6 +268,12 @@ pub(crate) fn process_info(env: &HerdrEnv, pane: &str) -> Result { Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } +/// `herdr agent get `, raw JSON; `None` when the pane has no agent Herdr can describe. +pub(crate) fn agent_get(env: &HerdrEnv, pane: &str) -> Option { + let output = Command::new(&env.bin).args(["agent", "get", pane]).output().ok()?; + output.status.success().then(|| String::from_utf8_lossy(&output.stdout).into_owned()) +} + /// Run the launch through `bin`. Herdr's own stdout/stderr pass through. pub(crate) fn run(env: &HerdrEnv, launch: &Launch) -> Result<()> { if !env.in_herdr { diff --git a/crates/plannotator-tui/src/herdr/launch/tests.rs b/crates/plannotator-tui/src/herdr/launch/tests.rs index 07d5910..080058b 100644 --- a/crates/plannotator-tui/src/herdr/launch/tests.rs +++ b/crates/plannotator-tui/src/herdr/launch/tests.rs @@ -187,6 +187,7 @@ fn a_last_launch_carries_the_pid_and_host_instead_of_a_file() { OpenArgs::default(), Path::new("/"), PROCESS_INFO, + None, ) .expect("plans"); assert_eq!(launch.message, Some((91279, "claude".into()))); @@ -196,3 +197,47 @@ fn a_last_launch_carries_the_pid_and_host_instead_of_a_file() { assert!(args.contains(&"PLANNOTATOR_TUI_HOST=claude".to_owned())); assert!(!args.iter().any(|a| a.starts_with("PLANNOTATOR_TUI_FILE="))); } + +const AGENT_GET_PATH: &str = r#"{"id":"cli:agent:get","result":{"agent":{"agent":"omp","agent_session":{"agent":"omp","kind":"path","source":"herdr:omp","value":"~/.omp/agent/sessions/--w--/2026-08-29T01-00-00-000Z_0199.jsonl"},"pane_id":"w1:p1"},"type":"agent_info"}}"#; +const AGENT_GET_ID: &str = r#"{"id":"cli:agent:get","result":{"agent":{"agent":"hermes","agent_session":{"agent":"hermes","kind":"id","source":"herdr:hermes","value":"sess_abc123"},"pane_id":"w1:p1"},"type":"agent_info"}}"#; + +#[test] +fn herdrs_agent_session_is_passed_to_the_pane_as_a_path_or_an_id() { + assert_eq!( + agent_session(AGENT_GET_PATH), + Some(AgentSession::Path("~/.omp/agent/sessions/--w--/2026-08-29T01-00-00-000Z_0199.jsonl".into())) + ); + assert_eq!(agent_session(AGENT_GET_ID), Some(AgentSession::Id("sess_abc123".into()))); + assert_eq!(agent_session(r#"{"result":{"agent":{"agent":"pi"}}}"#), None); + + let context = HerdrContext { + focused_pane_id: Some("w1:p1".into()), + focused_pane_agent: Some("omp".into()), + focused_pane_cwd: Some("/w".into()), + ..HerdrContext::default() + }; + let launch = plan_last( + &env(None, Some(context.clone())), + &Config::default(), + OpenArgs::default(), + Path::new("/"), + &PROCESS_INFO.replace(r#""name":"claude""#, r#""name":"omp""#), + Some(AGENT_GET_PATH), + ) + .expect("plans"); + let args = argv(&launch); + assert!(args.iter().any(|a| a.starts_with("PLANNOTATOR_TUI_SESSION=~/.omp/agent/sessions/")), "{args:?}"); + assert!(args.contains(&"PLANNOTATOR_TUI_HOST=omp".to_owned())); + + let launch = plan_last( + &env(None, Some(context)), + &Config::default(), + OpenArgs::default(), + Path::new("/"), + PROCESS_INFO, + Some(AGENT_GET_ID), + ) + .expect("plans"); + let args = argv(&launch); + assert!(args.contains(&"PLANNOTATOR_TUI_SESSION_ID=sess_abc123".to_owned()), "{args:?}"); +}