From d4e379131c81ecc1e65bafd85f2f9cf1ed1035cd Mon Sep 17 00:00:00 2001 From: Iury Souza Date: Wed, 05 Aug 2026 11:15:19 +0000 Subject: [PATCH] feat(herdr): report interactive attention states --- package.json | 1 + packages/pi-ext/package.json | 3 ++- packages/pi-ext/extensions/herdr-attention/index.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/pi-ext/tests/herdr-attention/herdr-attention.test.mjs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 file(s) changed, 151 insertion(s)(+), 1 deletion(s)(-) diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "./packages/pi-ext/extensions/session-store/index.ts", "./packages/pi-ext/extensions/cmux/index.ts", "./packages/pi-ext/extensions/handoff/index.ts", + "./packages/pi-ext/extensions/herdr-attention/index.ts", "./packages/pi-ext/extensions/session-snap/index.ts", "./packages/pi-ext/extensions/pi-telescope/index.ts", "./packages/pi-ext/extensions/pi-sem/index.ts", diff --git a/packages/pi-ext/package.json b/packages/pi-ext/package.json --- a/packages/pi-ext/package.json +++ b/packages/pi-ext/package.json @@ -61,7 +61,7 @@ "homepage": "https://github.com/iurysza/pi-extensions/tree/main/packages/pi-ext#readme", "scripts": { "sem:evaluate": "node extensions/pi-sem/bin/sem-eval.mjs", - "test": "npm run test:pi-sem && npm run test:tool-presentation && node --test tests/leader-key/favourite-models.test.mjs tests/leader-key/skill-editor.test.mjs tests/chat-to-md/chat-to-md.test.mjs tests/handoff/dependency-contract.test.mjs tests/startup-screen/startup-screen.test.mjs tests/file-search/*.test.mjs", + "test": "npm run test:pi-sem && npm run test:tool-presentation && node --test tests/leader-key/favourite-models.test.mjs tests/leader-key/skill-editor.test.mjs tests/chat-to-md/chat-to-md.test.mjs tests/handoff/dependency-contract.test.mjs tests/startup-screen/startup-screen.test.mjs tests/file-search/*.test.mjs tests/herdr-attention/herdr-attention.test.mjs", "test:pi-sem": "node --test tests/pi-sem/core.test.mjs tests/review/sem-guidance.test.mjs", "test:tool-presentation": "node scripts/test-tool-presentation.mjs", "typecheck": "tsc --noEmit -p tsconfig.json", @@ -86,6 +86,7 @@ "./extensions/session-store/index.ts", "./extensions/cmux/index.ts", "./extensions/handoff/index.ts", + "./extensions/herdr-attention/index.ts", "./extensions/session-snap/index.ts", "./extensions/pi-telescope/index.ts", "./extensions/pi-sem/index.ts" diff --git a/packages/pi-ext/extensions/herdr-attention/index.ts b/packages/pi-ext/extensions/herdr-attention/index.ts new file mode 100644 --- /dev/null +++ b/packages/pi-ext/extensions/herdr-attention/index.ts @@ -0,0 +1,79 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +const ATTENTION_TOOL_LABELS = new Map([ + ["ask_user", "question"], + ["ask_user_question", "question"], + ["choose_visual_artifact_direction", "choose visual direction"], + ["plannotator_submit_plan", "review plan"], + ["cursor_ask_question", "question"], +]); + +const PERMISSION_REQUEST_EVENT = "permissions:request"; +const PERMISSION_RESOLVED_EVENT = "permissions:resolved"; +const PERMISSION_KEY = "permission"; + +type ToolExecutionEvent = { + toolCallId: string; + toolName: string; + args?: unknown; +}; + +type PermissionEvent = { + sessionId: string; +}; + +export function attentionLabelForTool(event: ToolExecutionEvent): string | undefined { + const label = ATTENTION_TOOL_LABELS.get(event.toolName); + if (label) return label; + + if ( + event.toolName === "subagent" && + event.args !== null && + typeof event.args === "object" && + (event.args as { clarify?: unknown }).clarify === true + ) { + return "configure subagents"; + } + + return undefined; +} + +export default function herdrAttention(pi: ExtensionAPI): void { + const activeWaits = new Set(); + + function begin(key: string, label: string): void { + if (activeWaits.has(key)) return; + activeWaits.add(key); + pi.events.emit("herdr:blocked", { active: true, label }); + } + + function end(key: string): void { + if (!activeWaits.delete(key)) return; + pi.events.emit("herdr:blocked", { active: false }); + } + + pi.on("tool_execution_start", (event, ctx) => { + if (ctx.mode !== "tui") return; + const toolEvent = event as ToolExecutionEvent; + const label = attentionLabelForTool(toolEvent); + if (label) begin(`tool:${toolEvent.toolCallId}`, label); + }); + + pi.on("tool_execution_end", (event) => { + end(`tool:${(event as ToolExecutionEvent).toolCallId}`); + }); + + pi.events.on(PERMISSION_REQUEST_EVENT, (event) => { + const { sessionId } = event as PermissionEvent; + begin(`${PERMISSION_KEY}:${sessionId}`, "permission"); + }); + + pi.events.on(PERMISSION_RESOLVED_EVENT, (event) => { + const { sessionId } = event as PermissionEvent; + end(`${PERMISSION_KEY}:${sessionId}`); + }); + + pi.on("session_shutdown", () => { + for (const key of activeWaits) end(key); + }); +} diff --git a/packages/pi-ext/tests/herdr-attention/herdr-attention.test.mjs b/packages/pi-ext/tests/herdr-attention/herdr-attention.test.mjs new file mode 100644 --- /dev/null +++ b/packages/pi-ext/tests/herdr-attention/herdr-attention.test.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import herdrAttention, { attentionLabelForTool } from "../../extensions/herdr-attention/index.ts"; + +function createHarness() { + const handlers = new Map(); + const events = new Map(); + const emitted = []; + const pi = { + on: (name, handler) => handlers.set(name, handler), + events: { + on: (name, handler) => events.set(name, handler), + emit: (name, data) => emitted.push({ name, data }), + }, + }; + + herdrAttention(pi); + return { handlers, events, emitted }; +} + +describe("Herdr attention", () => { + it("labels known interactive tools and explicit subagent clarification", () => { + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "ask_user" }), "question"); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "choose_visual_artifact_direction" }), "choose visual direction"); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "plannotator_submit_plan" }), "review plan"); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "cursor_ask_question" }), "question"); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "subagent", args: { clarify: true } }), "configure subagents"); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "subagent", args: { clarify: false } }), undefined); + assert.equal(attentionLabelForTool({ toolCallId: "1", toolName: "bash" }), undefined); + }); + + it("blocks while an interactive tool is open and clears when it finishes", () => { + const { handlers, emitted } = createHarness(); + const ctx = { mode: "tui" }; + + handlers.get("tool_execution_start")({ toolCallId: "ask-1", toolName: "ask_user" }, ctx); + handlers.get("tool_execution_end")({ toolCallId: "ask-1", toolName: "ask_user" }, ctx); + + assert.deepEqual(emitted, [ + { name: "herdr:blocked", data: { active: true, label: "question" } }, + { name: "herdr:blocked", data: { active: false } }, + ]); + }); + + it("does not report interactive waits outside a terminal UI", () => { + const { handlers, emitted } = createHarness(); + + handlers.get("tool_execution_start")({ toolCallId: "ask-1", toolName: "ask_user" }, { mode: "print" }); + handlers.get("tool_execution_end")({ toolCallId: "ask-1", toolName: "ask_user" }, { mode: "print" }); + + assert.deepEqual(emitted, []); + }); + + it("uses permission lifecycle events and clears unfinished waits on shutdown", () => { + const { handlers, events, emitted } = createHarness(); + + events.get("permissions:request")({ sessionId: "session-1" }); + events.get("permissions:resolved")({ sessionId: "session-1" }); + handlers.get("tool_execution_start")({ toolCallId: "plan-1", toolName: "plannotator_submit_plan" }, { mode: "tui" }); + handlers.get("session_shutdown")({}, { mode: "tui" }); + + assert.deepEqual(emitted, [ + { name: "herdr:blocked", data: { active: true, label: "permission" } }, + { name: "herdr:blocked", data: { active: false } }, + { name: "herdr:blocked", data: { active: true, label: "review plan" } }, + { name: "herdr:blocked", data: { active: false } }, + ]); + }); +}); -- tangled.sh