Revert "style: OpenCode Todo-style right agent panel (bg fill, checklist, pinned footer)"

This reverts commit e0f08097ec.
This commit is contained in:
Emil
2026-08-01 00:01:09 +03:00
parent e0f08097ec
commit 8f308ecea3
4 changed files with 14 additions and 111 deletions
-10
View File
@@ -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
-43
View File
@@ -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<AgentRecord, "id" | "status" | "startedAt" | "finishedAt"> = {
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<string, AgentRecord>();
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"));
+13 -57
View File
@@ -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 */ },
+1 -1
View File
@@ -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 },
});
}