diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a31da5..1ae0652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,6 @@ ## Unreleased -- Restyled the persistent right-side agent panel (`/agents-panel`) to match - OpenCode's Todo/plan sidebar: a full-height background column (no box border, - panel background painted across the whole overlay width) with a bold `▼ agents` - title, a flat OpenCode-style checklist (`[•]` in-progress in warning color, - `[ ]` pending and `[✓]` completed in muted, `[✖]` failed in error), live - per-agent activity lines under running agents, and the footer hint pinned to - the bottom of the column. The panel now sits flush against the right edge of - the terminal and stops two rows above the bottom so the editor and status bar - stay visible. - - Fixed the research loop and agent tree getting permanently stuck after `research_control stop`: `start` now restarts a stopped/completed run in the same session (`AgentTree.restart` revives the run, archives old branches, and diff --git a/_panel-preview.ts b/_panel-preview.ts deleted file mode 100644 index c48edb1..0000000 --- a/_panel-preview.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Theme } from "@earendil-works/pi-coding-agent"; -import { createAgentPanelComponent, agentLastActivity } from "./src/agent-tree-ui.js"; -import type { AgentRecord } from "./src/types.js"; - -const theme = new Theme( - { accent: "#8abeb7", border: "#5f87ff", borderAccent: "#00d7ff", borderMuted: "#505050", success: "#b5bd68", error: "#cc6666", warning: "#ffff00", muted: "#808080", dim: "#666666", text: "#d4d4d4", thinkingText: "#808080", userMessageText: "#d4d4d4", customMessageText: "#d4d4d4", customMessageLabel: "#9575cd", toolTitle: "#d4d4d4", toolOutput: "#808080", mdHeading: "#f0c674", mdLink: "#81a2be", mdLinkUrl: "#666666", mdCode: "#8abeb7", mdCodeBlock: "#b5bd68", mdCodeBlockBorder: "#808080", mdQuote: "#808080", mdQuoteBorder: "#808080", mdHr: "#808080", mdListBullet: "#8abeb7", toolDiffAdded: "#b5bd68", toolDiffRemoved: "#cc6666", toolDiffContext: "#808080", syntaxComment: "#808080", syntaxKeyword: "#cc6666", syntaxFunction: "#8abeb7", syntaxVariable: "#d4d4d4", syntaxString: "#b5bd68", syntaxNumber: "#f0c674", syntaxType: "#5f87ff", syntaxOperator: "#d4d4d4", syntaxPunctuation: "#808080", thinkingOff: "#666666", thinkingMinimal: "#666666", thinkingLow: "#666666", thinkingMedium: "#666666", thinkingHigh: "#666666", thinkingXhigh: "#666666", thinkingMax: "#666666", bashMode: "#cc6666" }, - { selectedBg: "#3a3a4a", userMessageBg: "#343541", customMessageBg: "#2d2838", toolPendingBg: "#282832", toolSuccessBg: "#283228", toolErrorBg: "#3c2828" }, - "truecolor", -); - -const base: Omit = { - runId: "run-132965dd", parentId: "root", children: [], lineage: [], depth: 1, - task: "x", taskFingerprint: "x", expectedOutput: "", completionCriteria: "", specPath: "", - createdAt: new Date(Date.now() - 20 * 60000).toISOString(), -}; -const mk = (id: string, status: AgentRecord["status"], startedMin: number, finishedMin?: number): AgentRecord => ({ - ...base, id, status, - startedAt: new Date(Date.now() - startedMin * 60000).toISOString(), - finishedAt: finishedMin !== undefined ? new Date(Date.now() - finishedMin * 60000).toISOString() : undefined, -}); - -// Fake tree -const agents = new Map(); -agents.set("root", { ...base, id: "root", parentId: null, depth: 0, status: "completed", startedAt: new Date(Date.now() - 30 * 60000).toISOString(), finishedAt: new Date(Date.now() - 25 * 60000).toISOString() }); -agents.set("market-competitors-bb389dda", mk("market-competitors-bb389dda", "running", 3)); -agents.set("regulatory-gov-support-3b922f86", mk("regulatory-gov-support-3b922f86", "waiting", 3)); -agents.set("llm-infrastructure-niches-b1308f72", mk("llm-infrastructure-niches-b1308f72", "created", 0)); -agents.set("science-education-niches-293d399a", mk("science-education-niches-293d399a", "completed", 55, 12)); -agents.set("demand-b2b-b2g-57637d25", mk("demand-b2b-b2g-57637d25", "failed", 40, 9)); -const tree = { - rootId: "root", runId: "run-132965dd", - list: () => [...agents.values()], -} as never; - -const tui = { requestRender() {}, terminal: { rows: 40 } } as never; -const panel = createAgentPanelComponent(tree, tui, theme, () => {}); -const out = panel.render(50); -// strip ANSI, show visibly -const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ""); -console.log("+" + "-".repeat(50) + "+"); -for (const line of out) console.log("|" + strip(line) + "|"); -console.log("+" + "-".repeat(50) + "+"); -console.log("lines:", out.length, "| last line ends with reset:", out[out.length-1]!.endsWith("\x1b[0m")); diff --git a/src/agent-tree-ui.ts b/src/agent-tree-ui.ts index 7c6d295..133b70b 100644 --- a/src/agent-tree-ui.ts +++ b/src/agent-tree-ui.ts @@ -85,76 +85,32 @@ export function agentLastActivity(sessionFile: string | undefined): string | und return line || undefined; } -const PANEL_BG = "toolPendingBg"; - -/** - * OpenCode-style persistent right-side panel: a full-height background column - * (no box border) holding a flat checklist of agents, mirroring the Todo/plan - * sidebar of OpenCode's TUI: `[•]` in-progress (warning), `[ ]` pending and - * `[✓]` completed (muted), with a bold title and a footer pinned to the bottom. - */ +/** Persistent non-capturing right-side panel: live agent tree without keyboard navigation. */ export function createAgentPanelComponent( tree: AgentTree, - tui: { requestRender(): void; terminal: { rows: number } }, + tui: { requestRender(): void }, theme: Theme, done: () => void, ): { render(width: number): string[]; invalidate(): void; handleInput(data: string): void; dispose(): void } { const timer = setInterval(() => tui.requestRender(), 1500); - /** Paint a line with the panel background, padded with spaces to the full overlay width (padding must be inside the bg ANSI codes, otherwise the TUI compositor resets it). */ - const paint = (line: string, width: number): string => theme.bg(PANEL_BG, truncateToWidth(line, width, "…", true)); - const blank = (width: number): string => theme.bg(PANEL_BG, " ".repeat(width)); - const marker = (record: AgentRecord): { glyph: string; color: (s: string) => string } => { - switch (record.status) { - case "running": return { glyph: "•", color: (s) => theme.fg("warning", s) }; - case "waiting": return { glyph: " ", color: (s) => theme.fg("muted", s) }; - case "completed": return { glyph: "✓", color: (s) => theme.fg("muted", s) }; - case "failed": return { glyph: "✖", color: (s) => theme.fg("error", s) }; - case "interrupted": return { glyph: "!", color: (s) => theme.fg("warning", s) }; - default: return { glyph: " ", color: (s) => theme.fg("dim", s) }; - } - }; - const row = (record: AgentRecord, width: number, now: number): string => { - const { glyph, color } = marker(record); - const time = elapsed(record, now); - return paint(`${color(`[${glyph}]`)} ${color(shortName(record.id))}${time ? ` ${theme.fg("dim", time)}` : ""}`, width); - }; return { render(width) { + const rows = buildRows(tree).filter((row) => row.depth > 0); const now = Date.now(); - const all = tree.list().filter((agent) => agent.id !== tree.rootId); - const isActive = (agent: AgentRecord): boolean => agent.status === "running" || agent.status === "waiting" || agent.status === "created"; - const active = all.filter(isActive); - const finished = all.filter((agent) => !isActive(agent)).sort((a, b) => (b.finishedAt ?? "").localeCompare(a.finishedAt ?? "")); - const visibleActive = active.slice(0, 8); - const visibleFinished = finished.slice(0, 3); - const hidden = Math.max(0, active.length - visibleActive.length + finished.length - visibleFinished.length); - - const lines: string[] = [ - paint(theme.fg("text", theme.bold("▼ agents")) + theme.fg("dim", ` · ${tree.runId}`), width), - paint(theme.fg("dim", `${active.length} active · ${all.length} agents`), width), - blank(width), - ]; - for (const record of visibleActive) { - lines.push(row(record, width, now)); + const active = rows.filter((row) => row.record.status === "running" || row.record.status === "waiting").length; + const lines: string[] = [theme.fg("accent", `◆ agents`) + theme.fg("dim", ` · ${tree.runId}`), theme.fg("dim", `${active} active · ${rows.length} agents`), ""]; + for (const { record, prefix } of rows) { + const icon = STATUS_ICON[record.status]; + const status = STATUS_COLOR[record.status](theme, record.status); + const time = elapsed(record, now); + lines.push(`${prefix}${theme.fg("text", icon)} ${theme.fg("text", shortName(record.id))} ${status}${time ? ` ${theme.fg("dim", time)}` : ""}`); if (record.status === "running" || record.status === "waiting") { const activity = agentLastActivity(record.sessionFile); - if (activity) lines.push(paint(theme.fg("dim", ` ${truncateToWidth(activity, Math.max(20, width - 6))}`), width)); + if (activity) lines.push(theme.fg("dim", ` ${truncateToWidth(activity, Math.max(20, width - 8))}`)); } } - if (visibleFinished.length > 0) { - lines.push(blank(width)); - for (const record of visibleFinished) lines.push(row(record, width, now)); - } - if (hidden > 0) lines.push(paint(theme.fg("dim", `+${hidden} more…`), width)); - - // Fill the rest of the right column with the panel background, keeping the - // editor + status rows at the bottom visible, and pin the footer at the bottom. - const total = Math.max(1, tui.terminal.rows - 2); - if (lines.length < total) { - while (lines.length < total - 1) lines.push(blank(width)); - lines.push(paint(theme.fg("dim", "/agents — details · /agents-panel — hide"), width)); - } - return lines; + lines.push("", theme.fg("dim", "/agents — details · /agents-panel — hide")); + return lines.map((line) => truncateToWidth(line, width)); }, invalidate() { /* rebuilt fresh on every render */ }, handleInput() { /* non-capturing: keys go to the editor */ }, diff --git a/src/supervisor.ts b/src/supervisor.ts index 57b924f..2064ef7 100644 --- a/src/supervisor.ts +++ b/src/supervisor.ts @@ -83,7 +83,7 @@ export class SupervisorIntegration { return createAgentPanelComponent(tree, tui, theme, () => { this.agentPanelClose = undefined; done(undefined); }); }, { overlay: true, - overlayOptions: { width: "40%", minWidth: 44, maxHeight: "100%", anchor: "top-right", margin: { top: 0, right: 0, bottom: 0, left: 1 }, nonCapturing: true, visible: (termWidth) => termWidth >= 100 }, + overlayOptions: { width: "40%", minWidth: 44, maxHeight: "100%", anchor: "top-right", margin: 1, nonCapturing: true, visible: (termWidth) => termWidth >= 100 }, }); }