Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/plannotator-tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)?
};
Expand All @@ -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()
})
Expand Down
6 changes: 6 additions & 0 deletions crates/plannotator-tui/src/herdr/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub(crate) struct HerdrEnv {
pub(crate) message_pid: Option<u32>,
/// `PLANNOTATOR_TUI_HOST`: which agent's transcript format to read.
pub(crate) host: Option<String>,
/// `PLANNOTATOR_TUI_SESSION`: the agent's transcript path, when Herdr knew it.
pub(crate) session: Option<PathBuf>,
/// `PLANNOTATOR_TUI_SESSION_ID`: the agent's session id, for hosts without transcript files.
pub(crate) session_id: Option<String>,
}

impl HerdrEnv {
Expand All @@ -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"),
}
}

Expand Down
39 changes: 39 additions & 0 deletions crates/plannotator-tui/src/herdr/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentSession>,
}

/// `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 <pane>` JSON for its `agent_session`, when present.
pub(crate) fn agent_session(agent_get_json: &str) -> Option<AgentSession> {
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 <id>` JSON: the
Expand Down Expand Up @@ -80,6 +101,7 @@ pub(crate) fn plan_last(
args: OpenArgs,
cwd: &Path,
process_info_json: &str,
agent_get_json: Option<&str>,
) -> Result<Launch> {
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());
Expand All @@ -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)
}

Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -211,6 +235,15 @@ pub(crate) fn argv(launch: &Launch) -> Vec<String> {
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())]),
}
Expand All @@ -235,6 +268,12 @@ pub(crate) fn process_info(env: &HerdrEnv, pane: &str) -> Result<String> {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}

/// `herdr agent get <pane>`, raw JSON; `None` when the pane has no agent Herdr can describe.
pub(crate) fn agent_get(env: &HerdrEnv, pane: &str) -> Option<String> {
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 {
Expand Down
45 changes: 45 additions & 0 deletions crates/plannotator-tui/src/herdr/launch/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())));
Expand All @@ -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:?}");
}
Loading