chore: remove the /agents interactive overlay; keep the live widget

This commit is contained in:
Emil
2026-08-01 00:04:19 +03:00
parent 4f630183fd
commit 2c2a8f58a2
6 changed files with 6 additions and 110 deletions
+3 -7
View File
@@ -31,18 +31,14 @@
tree as paused, and resume requires the loop to actually be paused. tree as paused, and resume requires the loop to actually be paused.
- `read_artifact` now resolves symlinks before the confinement check, so a - `read_artifact` now resolves symlinks before the confinement check, so a
symlink inside the project cannot leak files from outside it. symlink inside the project cannot leak files from outside it.
- Added a live subagent dashboard: a TUI widget above the editor shows the - The live widget above the editor shows the research run id, loop status,
research run id, loop status, iteration, active agents with elapsed time, iteration, active agents with elapsed time, and recently finished agents
and recently finished agents (updated every 1.5 s, hidden when idle). (updated every 1.5 s, hidden when idle).
- Subagents no longer stall the main chat on serial model backends: new - Subagents no longer stall the main chat on serial model backends: new
`agent_concurrency` (default 3) caps how many subagent sessions stream at `agent_concurrency` (default 3) caps how many subagent sessions stream at
once, and optional `subagent_model` routes subagents to a different once, and optional `subagent_model` routes subagents to a different
model/backend (e.g. a local Ollama model) so they never contend with the model/backend (e.g. a local Ollama model) so they never contend with the
main session. main session.
- New `/agents` command opens an interactive overlay with the full agent tree:
tree glyphs (├─/└─/│), colored statuses and icons, live elapsed time,
keyboard navigation (↑/↓), and a detail pane (Enter) showing each agent's
task and result. The live widget header now hints at `/agents`.
## 0.1.1 — 2026-07-31 ## 0.1.1 — 2026-07-31
@@ -0,0 +1 @@
{"nodes": [{"id": "home_emil_desktop_hypothesis_machine_changelog_md", "label": "CHANGELOG.md", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L1"}, {"id": "hypothesis_machine_changelog_changelog", "label": "Changelog", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L1"}, {"id": "hypothesis_machine_changelog_unreleased", "label": "Unreleased", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L3"}, {"id": "hypothesis_machine_changelog_0_1_1_2026_07_31", "label": "0.1.1 \u2014 2026-07-31", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L47"}, {"id": "hypothesis_machine_changelog_0_1_0_2026_07_31", "label": "0.1.0 \u2014 2026-07-31", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L60"}], "edges": [{"source": "home_emil_desktop_hypothesis_machine_changelog_md", "target": "hypothesis_machine_changelog_changelog", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L1", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_unreleased", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L3", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_0_1_1_2026_07_31", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L47", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_0_1_0_2026_07_31", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L60", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
+1 -1
View File
@@ -1 +1 @@
{"/home/emil/Desktop/hypothesis-machine/package.json":{"size":1331,"mtime_ns":1785526465209535381,"hash":"f7514b7033a121f12b8960b05cad5a29bb4636b9641740561adc3b75352f95cb"},"/home/emil/Desktop/hypothesis-machine/tsconfig.build.json":{"size":171,"mtime_ns":1785523498961599333,"hash":"25868e1e77377761e5f756724739440927cdecf3d48c2800816407ef81b79222"},"/home/emil/Desktop/hypothesis-machine/tsconfig.json":{"size":430,"mtime_ns":1785523498961599333,"hash":"49863ef1ecd0b134189964c301f95090bf082241233c16055decd9aca6a9a314"},"/home/emil/Desktop/hypothesis-machine/.github/pull_request_template.md":{"size":534,"mtime_ns":1785526202969401851,"hash":"d76aeee1b42b19491e04604e657c0115b9b34df747f02b126e162df16538df1a"},"/home/emil/Desktop/hypothesis-machine/CHANGELOG.md":{"size":4559,"mtime_ns":1785531669243473488,"hash":"769d7b6bd162d14987cbe61d8f4563221c8450f44f201224c92c874efb2081c6"},"/home/emil/Desktop/hypothesis-machine/CONTRIBUTING.md":{"size":1522,"mtime_ns":1785526465210535393,"hash":"991a374ba1a32e74466f14e615736dec364ef2e22af71d3721a13e9f7779fd2c"},"/home/emil/Desktop/hypothesis-machine/README.md":{"size":6441,"mtime_ns":1785526465209535381,"hash":"d0ba5495074caf139af024082008ec136d4609995f58d431d716d24953bd7c07"},"/home/emil/Desktop/hypothesis-machine/SECURITY.md":{"size":974,"mtime_ns":1785526202966401815,"hash":"7870d4b34102270e45e1453c6052b25af09f1fe13bfdc4f244108b66a1e9a34b"},"/home/emil/Desktop/hypothesis-machine/docs/architecture.md":{"size":2002,"mtime_ns":1785524693984370854,"hash":"c04eb13e5a1728dc27f72e94bf88c179d26719c684e7f5f6129222910ae795cf"},"/home/emil/Desktop/hypothesis-machine/docs/development.md":{"size":1283,"mtime_ns":1785526465211535405,"hash":"f4af7af03f18576be7dbfec448e1443911d7fe43eabe9dc8ea0240052ff3383e"},"/home/emil/Desktop/hypothesis-machine/docs/pi-capabilities.md":{"size":4716,"mtime_ns":1785525665668981602,"hash":"32246de16c8fb36b826cb70b489f91ee0b3e5073bcbde729853ca04813fda9e4"},"/home/emil/Desktop/hypothesis-machine/docs/releasing.md":{"size":1269,"mtime_ns":1785526465213535429,"hash":"902bf6e8f541fbce500197fb303fb0ccfa2571998ee5bc57d7227e0f8d059b5a"},"/home/emil/Desktop/hypothesis-machine/docs/security.md":{"size":2817,"mtime_ns":1785526465213535429,"hash":"b8e428dd89560c6487d0177ae3f0b168dec24ef7727ad959e06f58dff206c5a1"},"/home/emil/Desktop/hypothesis-machine/prompts/agent-base.md":{"size":309,"mtime_ns":1785524192916593472,"hash":"6290b9307d35d28616e10d0043fd69150fac94ec4489b7a27a7fa1bbe830edf0"},"/home/emil/Desktop/hypothesis-machine/prompts/retrospective.md":{"size":244,"mtime_ns":1785524192917593484,"hash":"e16601b436e781f625c271fe5637d73c23a61780d3022ba0368503fc3b0516a9"},"/home/emil/Desktop/hypothesis-machine/prompts/supervisor.md":{"size":326,"mtime_ns":1785524192916593472,"hash":"78358e240ac6321433f93b14f138b6de917bba392078cb2e831f0276721968f3"},"/home/emil/Desktop/hypothesis-machine/prompts/synthesis.md":{"size":203,"mtime_ns":1785524192916593472,"hash":"33a91ff7d9ddc51110b59f673dea6bf954c48a6fc880f9a06b713dacb6c2c109"},"/home/emil/Desktop/hypothesis-machine/services/browser-worker/README.md":{"size":945,"mtime_ns":1785524192915593461,"hash":"eac3a08976ae79cb79300181be5b1b6cb8d0dd4a27aa8940e4e473a8429a15b8"}} {"/home/emil/Desktop/hypothesis-machine/package.json":{"size":1331,"mtime_ns":1785526465209535381,"hash":"f7514b7033a121f12b8960b05cad5a29bb4636b9641740561adc3b75352f95cb"},"/home/emil/Desktop/hypothesis-machine/tsconfig.build.json":{"size":171,"mtime_ns":1785523498961599333,"hash":"25868e1e77377761e5f756724739440927cdecf3d48c2800816407ef81b79222"},"/home/emil/Desktop/hypothesis-machine/tsconfig.json":{"size":430,"mtime_ns":1785523498961599333,"hash":"49863ef1ecd0b134189964c301f95090bf082241233c16055decd9aca6a9a314"},"/home/emil/Desktop/hypothesis-machine/.github/pull_request_template.md":{"size":534,"mtime_ns":1785526202969401851,"hash":"d76aeee1b42b19491e04604e657c0115b9b34df747f02b126e162df16538df1a"},"/home/emil/Desktop/hypothesis-machine/CHANGELOG.md":{"size":3998,"mtime_ns":1785531774587687095,"hash":"8b8f8ebbd94cd9c50f0597f06a6cd68d53e6605d682f260a087c9fc7176f7a2b"},"/home/emil/Desktop/hypothesis-machine/CONTRIBUTING.md":{"size":1522,"mtime_ns":1785526465210535393,"hash":"991a374ba1a32e74466f14e615736dec364ef2e22af71d3721a13e9f7779fd2c"},"/home/emil/Desktop/hypothesis-machine/README.md":{"size":6441,"mtime_ns":1785526465209535381,"hash":"d0ba5495074caf139af024082008ec136d4609995f58d431d716d24953bd7c07"},"/home/emil/Desktop/hypothesis-machine/SECURITY.md":{"size":974,"mtime_ns":1785526202966401815,"hash":"7870d4b34102270e45e1453c6052b25af09f1fe13bfdc4f244108b66a1e9a34b"},"/home/emil/Desktop/hypothesis-machine/docs/architecture.md":{"size":2002,"mtime_ns":1785524693984370854,"hash":"c04eb13e5a1728dc27f72e94bf88c179d26719c684e7f5f6129222910ae795cf"},"/home/emil/Desktop/hypothesis-machine/docs/development.md":{"size":1283,"mtime_ns":1785526465211535405,"hash":"f4af7af03f18576be7dbfec448e1443911d7fe43eabe9dc8ea0240052ff3383e"},"/home/emil/Desktop/hypothesis-machine/docs/pi-capabilities.md":{"size":4716,"mtime_ns":1785525665668981602,"hash":"32246de16c8fb36b826cb70b489f91ee0b3e5073bcbde729853ca04813fda9e4"},"/home/emil/Desktop/hypothesis-machine/docs/releasing.md":{"size":1269,"mtime_ns":1785526465213535429,"hash":"902bf6e8f541fbce500197fb303fb0ccfa2571998ee5bc57d7227e0f8d059b5a"},"/home/emil/Desktop/hypothesis-machine/docs/security.md":{"size":2817,"mtime_ns":1785526465213535429,"hash":"b8e428dd89560c6487d0177ae3f0b168dec24ef7727ad959e06f58dff206c5a1"},"/home/emil/Desktop/hypothesis-machine/prompts/agent-base.md":{"size":309,"mtime_ns":1785524192916593472,"hash":"6290b9307d35d28616e10d0043fd69150fac94ec4489b7a27a7fa1bbe830edf0"},"/home/emil/Desktop/hypothesis-machine/prompts/retrospective.md":{"size":244,"mtime_ns":1785524192917593484,"hash":"e16601b436e781f625c271fe5637d73c23a61780d3022ba0368503fc3b0516a9"},"/home/emil/Desktop/hypothesis-machine/prompts/supervisor.md":{"size":326,"mtime_ns":1785524192916593472,"hash":"78358e240ac6321433f93b14f138b6de917bba392078cb2e831f0276721968f3"},"/home/emil/Desktop/hypothesis-machine/prompts/synthesis.md":{"size":203,"mtime_ns":1785524192916593472,"hash":"33a91ff7d9ddc51110b59f673dea6bf954c48a6fc880f9a06b713dacb6c2c109"},"/home/emil/Desktop/hypothesis-machine/services/browser-worker/README.md":{"size":945,"mtime_ns":1785524192915593461,"hash":"eac3a08976ae79cb79300181be5b1b6cb8d0dd4a27aa8940e4e473a8429a15b8"}}
-90
View File
@@ -1,90 +0,0 @@
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
import type { Theme } from "@earendil-works/pi-coding-agent";
import type { AgentTree } from "./agent-tree.js";
import type { AgentRecord } from "./types.js";
interface TreeRow { record: AgentRecord; prefix: string; depth: number }
/** Flatten the agent tree into rows with tree glyphs (├─/└─/│). */
function buildRows(tree: AgentTree): TreeRow[] {
const rows: TreeRow[] = [];
const root = tree.inspect(tree.rootId);
const walk = (record: AgentRecord, ancestors: boolean[], last: boolean, depth: number): void => {
const indent = ancestors.map((isLast) => (isLast ? " " : "│ ")).join("");
const connector = ancestors.length === 0 ? "" : last ? "└─ " : "├─ ";
rows.push({ record, prefix: indent + connector, depth });
record.children.forEach((childId, index) => walk(tree.inspect(childId), [...ancestors, last], index === record.children.length - 1, depth + 1));
};
walk(root, [], false, 0);
return rows;
}
const STATUS_COLOR: Record<AgentRecord["status"], (theme: Theme, text: string) => string> = {
running: (t, s) => t.fg("accent", s),
waiting: (t, s) => t.fg("accent", s),
completed: (t, s) => t.fg("success", s),
failed: (t, s) => t.fg("error", s),
cancelled: (t, s) => t.fg("dim", s),
interrupted: (t, s) => t.fg("warning", s),
created: (t, s) => t.fg("text", s),
archived: (t, s) => t.fg("dim", s),
};
const STATUS_ICON: Record<AgentRecord["status"], string> = {
running: "▸", waiting: "⏸", completed: "✓", failed: "✖", cancelled: "·", interrupted: "⚠", created: "·", archived: "·",
};
function shortName(id: string): string { return id.replace(/-[a-f0-9]{8}$/, ""); }
function elapsed(record: AgentRecord, now: number): string {
if (!record.startedAt) return "";
const start = Date.parse(record.startedAt);
const end = record.finishedAt ? Date.parse(record.finishedAt) : (record.status === "running" || record.status === "waiting") ? now : undefined;
if (!end) return "";
const span = Math.max(0, Math.round((end - start) / 1000));
return `${String(Math.floor(span / 60)).padStart(2, "0")}:${String(span % 60).padStart(2, "0")}`;
}
/** Interactive overlay showing the full agent tree with live statuses, keyboard navigation, and a detail pane. */
export function createAgentTreeOverlay(
tree: AgentTree,
tui: { requestRender(): void },
theme: Theme,
done: () => void,
): { render(width: number): string[]; invalidate(): void; handleInput(data: string): void; dispose(): void } {
let selected = 0;
let detail = false;
const timer = setInterval(() => tui.requestRender(), 1500);
return {
render(width) {
const rows = buildRows(tree);
if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
const now = Date.now();
const lines: string[] = [theme.fg("accent", `◆ Agent tree · ${tree.runId} · ${rows.length - 1} agents`), ""];
for (let i = 0; i < rows.length; i++) {
const { record, prefix } = rows[i]!;
const marker = i === selected ? theme.fg("accent", ">") : " ";
const icon = STATUS_ICON[record.status];
const status = STATUS_COLOR[record.status](theme, record.status);
const time = elapsed(record, now);
lines.push(`${marker} ${prefix}${theme.fg("text", icon)} ${theme.fg("text", shortName(record.id))} ${status}${time ? ` ${theme.fg("dim", time)}` : ""}`);
}
if (detail && rows[selected]) {
const { record } = rows[selected]!;
lines.push("", theme.fg("dim", "task:"), ...wrapTextWithAnsi(record.task, width - 2).slice(0, 4).map((line) => theme.fg("text", line)));
if (record.result?.summary) lines.push(theme.fg("dim", "result:"), ...wrapTextWithAnsi(record.result.summary, width - 2).slice(0, 5).map((line) => theme.fg("muted", line)));
if (record.error) lines.push(theme.fg("error", `error: ${truncateToWidth(record.error, width - 2)}`));
}
lines.push("", theme.fg("dim", "↑↓ navigate · enter toggle details · esc close"));
return lines.map((line) => truncateToWidth(line, width));
},
invalidate() { /* rebuilt fresh on every render */ },
handleInput(data) {
const rows = buildRows(tree);
if (matchesKey(data, Key.up)) { if (selected > 0) { selected--; tui.requestRender(); } }
else if (matchesKey(data, Key.down)) { if (selected < rows.length - 1) { selected++; tui.requestRender(); } }
else if (matchesKey(data, Key.enter)) { detail = !detail; tui.requestRender(); }
else if (matchesKey(data, Key.escape)) { done(); }
},
dispose() { clearInterval(timer); },
};
}
-1
View File
@@ -11,7 +11,6 @@ export default function hypothesisMachine(pi: ExtensionAPI): void {
pi.on("session_shutdown", async () => { await supervisor.shutdown(); }); pi.on("session_shutdown", async () => { await supervisor.shutdown(); });
pi.registerCommand("team", { description: "Show the recursive research team", handler: async (_args, ctx) => { ctx.ui.notify(supervisor.team(), "info"); } }); pi.registerCommand("team", { description: "Show the recursive research team", handler: async (_args, ctx) => { ctx.ui.notify(supervisor.team(), "info"); } });
pi.registerCommand("agents", { description: "Show the interactive agent tree overlay", handler: async (_args, ctx) => { await supervisor.showAgentTree(ctx); } });
pi.registerCommand("research", { description: "Start a bounded research run", handler: async (args, ctx) => { const goal = args.trim(); if (!goal) { ctx.ui.notify("Usage: /research <goal>", "warning"); return; } const state = supervisor.loop; if (!state || !supervisor.tree) throw new Error("Not initialized"); supervisor.tree.setGoal(goal); state.setGoal(goal); state.start(); const prompt = `Research goal: ${goal}\nUse research_control and the recursive agent tools. Create specialized children only when useful. Record each iteration and stop on the coded conditions. Report important progress without flooding the chat.`; if (ctx.isIdle()) pi.sendUserMessage(prompt); else pi.sendUserMessage(prompt, { deliverAs: "followUp" }); } }); pi.registerCommand("research", { description: "Start a bounded research run", handler: async (args, ctx) => { const goal = args.trim(); if (!goal) { ctx.ui.notify("Usage: /research <goal>", "warning"); return; } const state = supervisor.loop; if (!state || !supervisor.tree) throw new Error("Not initialized"); supervisor.tree.setGoal(goal); state.setGoal(goal); state.start(); const prompt = `Research goal: ${goal}\nUse research_control and the recursive agent tools. Create specialized children only when useful. Record each iteration and stop on the coded conditions. Report important progress without flooding the chat.`; if (ctx.isIdle()) pi.sendUserMessage(prompt); else pi.sendUserMessage(prompt, { deliverAs: "followUp" }); } });
pi.registerCommand("research-status", { description: "Show research loop state", handler: async (_args, ctx) => { ctx.ui.notify(JSON.stringify(supervisor.loop?.snapshot() ?? {}, null, 2), "info"); } }); pi.registerCommand("research-status", { description: "Show research loop state", handler: async (_args, ctx) => { ctx.ui.notify(JSON.stringify(supervisor.loop?.snapshot() ?? {}, null, 2), "info"); } });
pi.registerCommand("research-pause", { description: "Pause spawning and the loop", handler: async (_args, ctx) => { if (supervisor.loop?.snapshot().status !== "running") { ctx.ui.notify("Cannot pause a non-running loop", "warning"); return; } supervisor.loop?.pause(); supervisor.tree?.pause(); ctx.ui.notify("Research paused", "info"); } }); pi.registerCommand("research-pause", { description: "Pause spawning and the loop", handler: async (_args, ctx) => { if (supervisor.loop?.snapshot().status !== "running") { ctx.ui.notify("Cannot pause a non-running loop", "warning"); return; } supervisor.loop?.pause(); supervisor.tree?.pause(); ctx.ui.notify("Research paused", "info"); } });
+1 -11
View File
@@ -1,7 +1,6 @@
import { resolve } from "node:path"; import { resolve } from "node:path";
import { defineTool, ModelRuntime, type ExtensionAPI, type ExtensionContext, type ModelRegistry, type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import { defineTool, ModelRuntime, type ExtensionAPI, type ExtensionContext, type ModelRegistry, type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent";
import { Text, truncateToWidth } from "@earendil-works/pi-tui"; import { Text, truncateToWidth } from "@earendil-works/pi-tui";
import { createAgentTreeOverlay } from "./agent-tree-ui.js";
import { Type } from "typebox"; import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai"; import { StringEnum } from "@earendil-works/pi-ai";
import { AgentTree } from "./agent-tree.js"; import { AgentTree } from "./agent-tree.js";
@@ -64,15 +63,6 @@ export class SupervisorIntegration {
team(): string { return this.required().tree.render(); } team(): string { return this.required().tree.render(); }
findings(kind?: string): unknown { return this.required().memory.list(kind); } findings(kind?: string): unknown { return this.required().memory.list(kind); }
/** Open the interactive agent-tree overlay. */
async showAgentTree(ctx: ExtensionContext): Promise<void> {
const tree = this.required().tree;
await ctx.ui.custom<undefined>((tui, theme, _kb, done) => createAgentTreeOverlay(tree, tui, theme, () => done(undefined)), {
overlay: true,
overlayOptions: { width: "55%", minWidth: 60, maxHeight: "80%", anchor: "top-right", margin: 1, visible: (termWidth) => termWidth >= 60 },
});
}
/** Live subagent dashboard widget above the editor, refreshed on a light timer. */ /** Live subagent dashboard widget above the editor, refreshed on a light timer. */
private installAgentWidget(ctx: ExtensionContext): void { private installAgentWidget(ctx: ExtensionContext): void {
ctx.ui.setWidget("hm-agents", (tui, theme) => { ctx.ui.setWidget("hm-agents", (tui, theme) => {
@@ -93,7 +83,7 @@ export class SupervisorIntegration {
const active = agents.filter((agent) => agent.status === "running" || agent.status === "waiting"); const active = agents.filter((agent) => agent.status === "running" || agent.status === "waiting");
if (active.length === 0 && state.status !== "running") return []; if (active.length === 0 && state.status !== "running") return [];
const now = Date.now(); const now = Date.now();
const lines: string[] = [theme.fg("accent", `◆ ${state.runId} · ${state.status} · iter ${state.iteration} · ${active.length} active`) + theme.fg("dim", " /agents")]; const lines: string[] = [theme.fg("accent", `◆ ${state.runId} · ${state.status} · iter ${state.iteration} · ${active.length} active`)];
for (const agent of active) { for (const agent of active) {
const elapsed = agent.startedAt ? Math.max(0, Math.round((now - Date.parse(agent.startedAt)) / 1000)) : 0; const elapsed = agent.startedAt ? Math.max(0, Math.round((now - Date.parse(agent.startedAt)) / 1000)) : 0;
const stamp = `${String(Math.floor(elapsed / 60)).padStart(2, "0")}:${String(elapsed % 60).padStart(2, "0")}`; const stamp = `${String(Math.floor(elapsed / 60)).padStart(2, "0")}:${String(elapsed % 60).padStart(2, "0")}`;