diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/integration/assets/opencode/herdr-agent-state.js | 14 | ||||
| -rw-r--r-- | src/integration/assets/opencode/herdr-agent-state.test.ts | 39 | ||||
| -rw-r--r-- | src/integration/assets/opencode/herdr-tui-session.js | 2 | ||||
| -rw-r--r-- | src/integration/mod.rs | 2 | ||||
| -rw-r--r-- | src/terminal/state.rs | 54 |
5 files changed, 98 insertions, 13 deletions
diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js index 2840c221..45032335 100644 --- a/src/integration/assets/opencode/herdr-agent-state.js +++ b/src/integration/assets/opencode/herdr-agent-state.js @@ -2,7 +2,7 @@ // managed by herdr; reinstalling or updating the integration overwrites this file. // add custom hooks/plugins beside this file instead of editing it. // HERDR_INTEGRATION_ID=opencode -// HERDR_INTEGRATION_VERSION=10 +// HERDR_INTEGRATION_VERSION=11 import net from "node:net"; @@ -13,8 +13,8 @@ let requestChain = Promise.resolve(); let reportedRootSessionID; // Track child sessions so their events cannot replace the pane's root session. -// Their user prompts still project state without attaching the child session id. -const childSessions = new Set(); +// User prompts carry the root id to preserve its identity and cross-talk guard. +const childSessions = new Map(); const CHILD_EVENT_STATES = new Map([ ["permission.asked", "blocked"], ["question.asked", "blocked"], @@ -141,12 +141,16 @@ export const HerdrAgentStatePlugin = async () => { const info = properties.info; if (info?.id && info.parentID) { - childSessions.add(info.id); + childSessions.set(info.id, info.parentID); } if (sessionID && childSessions.has(sessionID)) { const state = CHILD_EVENT_STATES.get(type); if (state) { - await reportState(state); + let rootSessionID = sessionID; + while (childSessions.has(rootSessionID)) { + rootSessionID = childSessions.get(rootSessionID); + } + await reportState(state, rootSessionID); } return; } diff --git a/src/integration/assets/opencode/herdr-agent-state.test.ts b/src/integration/assets/opencode/herdr-agent-state.test.ts index 6f8ee590..a3eea848 100644 --- a/src/integration/assets/opencode/herdr-agent-state.test.ts +++ b/src/integration/assets/opencode/herdr-agent-state.test.ts @@ -167,7 +167,6 @@ test("reports child prompts without replacing the root session", async () => { event: { type: "session.created", properties: { - sessionID: "child-session", info: { id: "child-session", parentID: "root-session" }, }, }, @@ -188,11 +187,39 @@ test("reports child prompts without replacing the root session", async () => { "working", ]); expect(requests.map(requestSessionID)).toEqual([ - undefined, - undefined, - undefined, - undefined, - undefined, + "root-session", + "root-session", + "root-session", + "root-session", + "root-session", + ]); +}); + +test("routes nested child prompts to their own root, not the last active root", async () => { + const plugin = await loadPlugin(); + for (const info of [ + { id: "child-session", parentID: "root-session" }, + { id: "nested-session", parentID: "child-session" }, + ]) { + await plugin.event({ event: { type: "session.created", properties: { info } } }); + } + await plugin["chat.message"]({ sessionID: "other-root" }); + await plugin.event({ + event: { type: "permission.asked", properties: { sessionID: "nested-session" } }, + }); + await plugin.event({ + event: { type: "permission.replied", properties: { sessionID: "nested-session" } }, + }); + await plugin.event({ + event: { type: "session.idle", properties: { sessionID: "nested-session" } }, + }); + await plugin["chat.message"]({ sessionID: "nested-session" }); + + expect(requests.map(requestState)).toEqual(["working", "blocked", "working"]); + expect(requests.map(requestSessionID)).toEqual([ + "other-root", + "root-session", + "root-session", ]); }); diff --git a/src/integration/assets/opencode/herdr-tui-session.js b/src/integration/assets/opencode/herdr-tui-session.js index b5be5bbc..f4f08d57 100644 --- a/src/integration/assets/opencode/herdr-tui-session.js +++ b/src/integration/assets/opencode/herdr-tui-session.js @@ -1,7 +1,7 @@ // installed by herdr // managed by herdr; reinstalling or updating the integration overwrites this file. // HERDR_INTEGRATION_ID=opencode-tui -// HERDR_INTEGRATION_VERSION=10 +// HERDR_INTEGRATION_VERSION=11 import net from "node:net"; diff --git a/src/integration/mod.rs b/src/integration/mod.rs index fd0839f4..3f7176b3 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -170,7 +170,7 @@ const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-st const OPENCODE_TUI_PLUGIN_INSTALL_NAME: &str = "herdr-tui-session.js"; const OPENCODE_TUI_PLUGIN_SPEC: &str = "./herdr-tui-session.js"; const OPENCODE_TUI_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-tui-session.js"); -const OPENCODE_INTEGRATION_VERSION: u32 = 10; +const OPENCODE_INTEGRATION_VERSION: u32 = 11; const KILO_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js"; const KILO_PLUGIN_ASSET: &str = include_str!("assets/kilo/herdr-agent-state.js"); const KILO_INTEGRATION_VERSION: u32 = 4; diff --git a/src/terminal/state.rs b/src/terminal/state.rs index ee170198..dc28af10 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -4786,6 +4786,60 @@ mod tests { } #[test] + fn opencode_child_prompt_reports_with_root_id_preserve_lifecycle_authority() { + let mut terminal = test_terminal(); + let root = crate::agent_resume::AgentSessionRef::id("opencode-root").unwrap(); + anchor_full_lifecycle_session( + &mut terminal, + Agent::OpenCode, + "herdr:opencode", + "opencode", + root.clone(), + ); + + // The plugin projects child permission/question events onto their root. + for (seq, state) in [ + (20, AgentState::Working), + (21, AgentState::Blocked), + (22, AgentState::Working), + (23, AgentState::Idle), + ] { + let mutation = terminal + .set_hook_authority_with_session_ref( + "herdr:opencode".into(), + "opencode".into(), + state, + None, + Some(root.clone()), + Some(seq), + ) + .expect("root-scoped lifecycle report should be accepted"); + assert!(!mutation.session_ref_changed); + assert_eq!(terminal.state, state); + assert_eq!( + terminal + .hook_authority + .as_ref() + .unwrap() + .session_ref + .as_ref(), + Some(&root) + ); + } + + let foreign_child_prompt = terminal.set_hook_authority_with_session_ref( + "herdr:opencode".into(), + "opencode".into(), + AgentState::Blocked, + None, + crate::agent_resume::AgentSessionRef::id("opencode-other-root"), + Some(24), + ); + assert!(foreign_child_prompt.is_none()); + assert_eq!(terminal.state, AgentState::Idle); + } + + #[test] fn opencode_tui_selection_reanchors_full_lifecycle_authority() { let mut terminal = test_terminal(); let old_session = crate::agent_resume::AgentSessionRef::id("opencode-newer").unwrap(); |
