aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.js14
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.test.ts39
-rw-r--r--src/integration/assets/opencode/herdr-tui-session.js2
-rw-r--r--src/integration/mod.rs2
-rw-r--r--src/terminal/state.rs54
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();