aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.js61
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.test.ts51
2 files changed, 77 insertions, 35 deletions
diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js
index 27ea7ee5..76265050 100644
--- a/src/integration/assets/opencode/herdr-agent-state.js
+++ b/src/integration/assets/opencode/herdr-agent-state.js
@@ -12,10 +12,16 @@ let reportSeq = Date.now() * 1000;
let requestChain = Promise.resolve();
let reportedRootSessionID;
-// Subagent (task tool) sessions carry a parentID; the main agent session does
-// not. Their lifecycle events would otherwise clobber the pane's real state, so
-// learn child session ids from session.created/updated and drop their reports.
+// 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();
+const CHILD_EVENT_STATES = new Map([
+ ["permission.asked", "blocked"],
+ ["question.asked", "blocked"],
+ ["permission.replied", "working"],
+ ["question.replied", "working"],
+ ["question.rejected", "working"],
+]);
function nextReportSeq() {
reportSeq += 1;
@@ -28,24 +34,22 @@ function sessionIDFromProperties(properties) {
: undefined;
}
+const SESSION_STATE_BY_STATUS = new Map([
+ ["idle", "idle"],
+ ["active", "working"],
+ ["busy", "working"],
+ ["pending", "working"],
+ ["retry", "working"],
+ ["running", "working"],
+ ["streaming", "working"],
+ ["working", "working"],
+]);
+
function stateFromSessionStatus(status) {
- // session.status carries { type: "idle" | "busy" | "retry" }; older builds used a bare string.
const kind = typeof status === "string" ? status : status?.type;
- if (typeof kind !== "string") return undefined;
- switch (kind.toLowerCase()) {
- case "idle":
- return "idle";
- case "active":
- case "busy":
- case "pending":
- case "running":
- case "streaming":
- case "working":
- case "retry":
- return "working";
- default:
- return undefined;
- }
+ return typeof kind === "string"
+ ? SESSION_STATE_BY_STATUS.get(kind.toLowerCase())
+ : undefined;
}
function request(method, params) {
@@ -144,22 +148,9 @@ export const HerdrAgentStatePlugin = async () => {
childSessions.add(info.id);
}
if (sessionID && childSessions.has(sessionID)) {
- // Child session events are dropped so they cannot clobber the pane's
- // root-agent state, but a subagent waiting on the user must still
- // surface as blocked (and clear once answered). Report state only,
- // without an agent_session_id, so the pane keeps the root session.
- switch (type) {
- case "permission.asked":
- case "question.asked":
- await reportState("blocked");
- break;
- case "permission.replied":
- case "question.replied":
- case "question.rejected":
- await reportState("working");
- break;
- default:
- break;
+ const state = CHILD_EVENT_STATES.get(type);
+ if (state) {
+ await reportState(state);
}
return;
}
diff --git a/src/integration/assets/opencode/herdr-agent-state.test.ts b/src/integration/assets/opencode/herdr-agent-state.test.ts
index d8d6d14d..be21fd71 100644
--- a/src/integration/assets/opencode/herdr-agent-state.test.ts
+++ b/src/integration/assets/opencode/herdr-agent-state.test.ts
@@ -114,6 +114,57 @@ test("suppresses redundant same-session updates", async () => {
expect(requests.map(requestSessionID)).toEqual(["root-session", "replacement-session"]);
});
+test("reports retry status as working", async () => {
+ const plugin = await loadPlugin();
+
+ await plugin.event({
+ event: {
+ type: "session.status",
+ properties: { sessionID: "root-session", status: { type: "retry" } },
+ },
+ });
+
+ expect(requests.map(requestMethod)).toEqual(["pane.report_agent"]);
+ expect(requests.map(requestState)).toEqual(["working"]);
+ expect(requests.map(requestSessionID)).toEqual(["root-session"]);
+});
+
+test("reports child prompts without replacing the root session", async () => {
+ const plugin = await loadPlugin();
+
+ await plugin.event({
+ event: {
+ type: "session.created",
+ properties: {
+ sessionID: "child-session",
+ info: { id: "child-session", parentID: "root-session" },
+ },
+ },
+ });
+
+ for (const type of ["permission.asked", "question.asked"]) {
+ await plugin.event({ event: { type, properties: { sessionID: "child-session" } } });
+ }
+ for (const type of ["permission.replied", "question.replied", "question.rejected"]) {
+ await plugin.event({ event: { type, properties: { sessionID: "child-session" } } });
+ }
+
+ expect(requests.map(requestState)).toEqual([
+ "blocked",
+ "blocked",
+ "working",
+ "working",
+ "working",
+ ]);
+ expect(requests.map(requestSessionID)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ ]);
+});
+
function requestMethod(request: unknown): unknown {
return isRecord(request) ? request.method : undefined;
}