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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ placement = "overlay" # overlay (full tab, default) | split | popup
`plannotator-tui config` prints the file's path and the values in effect. The `herdr/`
directory in this repo is the development manifest; users should install Herdr Annotate.

Actions forwarded by Herdr Mirror default to a split beside the invoking remote
pane. Mirror does not preserve overlay presentation, and Herdr 0.8.2 opens an
overlay in its server's active tab, which can differ from the tab you are viewing.
An explicit `--placement` or `PLANNOTATOR_TUI_PLACEMENT` still takes precedence.

## Agent replies

`plannotator-tui last` finds the transcript of the agent that launched your shell and shows a
Expand Down
5 changes: 4 additions & 1 deletion crates/plannotator-tui/src/herdr/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl HerdrEnv {
/// The focused pane from the context, only when Herdr saw an agent in it.
pub(crate) fn focused_agent_pane(&self) -> Option<Target> {
let context = self.context.as_ref()?;
let agent = context.focused_pane_agent.clone()?;
let agent = context.focused_pane_agent.clone().filter(|agent| !agent.trim().is_empty())?;
Some(Target { pane: context.focused_pane_id.clone()?, agent: Some(agent) })
}

Expand Down Expand Up @@ -171,6 +171,9 @@ mod tests {
let shell =
env(&[("HERDR_ENV", "1"), ("HERDR_PLUGIN_CONTEXT_JSON", r#"{"focused_pane_id":"w1:p2"}"#)]);
assert_eq!(shell.delivery_target(), None);
let mirror_shell =
env(&[("HERDR_PLUGIN_CONTEXT_JSON", r#"{"focused_pane_id":"w1:p2","focused_pane_agent":""}"#)]);
assert_eq!(mirror_shell.delivery_target(), None);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions crates/plannotator-tui/src/herdr/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ pub(crate) fn plan(env: &HerdrEnv, config: &Config, args: OpenArgs, cwd: &Path)
let placement = match (args.placement, env.placement.as_deref()) {
(Some(p), _) => p,
(None, Some(text)) => text.parse().context("PLANNOTATOR_TUI_PLACEMENT")?,
// Mirror imports ordinary panes, not the remote client's overlays. Herdr
// 0.8.2 also places overlays in its globally active tab, which may differ
// from the invoking mirror. A targeted split reaches the intended tab.
(None, None)
if config.herdr.placement == Placement::Overlay
&& context.and_then(|c| c.invocation_source.as_deref()) == Some("mirror") =>
{
Placement::Split
}
(None, None) => config.herdr.placement,
};

Expand Down
34 changes: 34 additions & 0 deletions crates/plannotator-tui/src/herdr/launch/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,40 @@ fn agent_skill_delivers_to_and_splits_beside_the_calling_pane() {
std::fs::remove_dir_all(&root).expect("cleanup");
}

#[test]
fn mirror_defaults_to_a_split_next_to_the_remote_invoking_pane() {
let context = HerdrContext {
focused_pane_id: Some("w2:p4".into()),
focused_pane_agent: Some("codex".into()),
invocation_source: Some("mirror".into()),
..HerdrContext::default()
};
let env = env(None, Some(context));
let launch = plan(&env, &Config::default(), OpenArgs::default(), Path::new("/"))
.expect("plans a mirrored document");
assert_eq!(launch.placement, Placement::Split);
assert!(argv(&launch).windows(2).any(|pair| pair == ["--target-pane", "w2:p4"]));
let agent = r#"{"result":{"agent":{"agent":"codex","agent_session":{"kind":"path","value":"/sessions/exact.jsonl"}}}}"#;
let last = plan_last(&env, &Config::default(), OpenArgs::default(), Path::new("/"), None, Some(agent))
.expect("plans the same pane's reply");
assert_eq!(last.placement, Placement::Split);
assert_eq!(last.target_pane.as_deref(), Some("w2:p4"));
assert_eq!(last.session, Some(AgentSession::Path("/sessions/exact.jsonl".into())));
}

#[test]
fn explicit_placement_still_wins_over_the_mirror_default() {
let mut env =
env(None, Some(HerdrContext { invocation_source: Some("mirror".into()), ..HerdrContext::default() }));
env.placement = Some("popup".into());
let launch =
plan(&env, &Config::default(), OpenArgs::default(), Path::new("/")).expect("environment placement");
assert_eq!(launch.placement, Placement::Popup);
let args = OpenArgs { placement: Some(Placement::Overlay), ..OpenArgs::default() };
let launch = plan(&env, &Config::default(), args, Path::new("/")).expect("explicit argument placement");
assert_eq!(launch.placement, Placement::Overlay);
}

#[test]
fn ctrl_click_opens_the_linked_file() {
let root = temp_folder("click");
Expand Down
Loading