feat: full-height right agent panel with per-agent last activity

The /agents-panel now fills the whole right column (40% width, 100% height,
top-right anchor). Under each running subagent it shows the agent's most
recent activity read from the tail of its session file: the last tool call
with its query (e.g. web_search: "...") or the last assistant text. Reads are
cached by file size so the 1.5s refresh stays cheap.
This commit is contained in:
Emil
2026-07-31 23:45:39 +03:00
parent 7e4146acbc
commit 18dc4cbb16
3 changed files with 50 additions and 1 deletions
+4
View File
@@ -47,6 +47,10 @@
panel with the live agent tree: non-capturing (typing keeps working),
auto-refreshed every 1.5 s, hidden on terminals narrower than 100 columns,
and closed automatically on shutdown.
- The right-side panel now fills the whole right column (width 40%, full
height) and shows each running subagent's most recent activity under its
row — the last tool call with its query or the last assistant text — read
from the tail of the agent's session file (cached by file size).
## 0.1.1 — 2026-07-31
+45
View File
@@ -1,4 +1,5 @@
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
import { closeSync, openSync, readSync, statSync } from "node:fs";
import type { Theme } from "@earendil-works/pi-coding-agent";
import type { AgentTree } from "./agent-tree.js";
import type { AgentRecord } from "./types.js";
@@ -44,6 +45,46 @@ function elapsed(record: AgentRecord, now: number): string {
return `${String(Math.floor(span / 60)).padStart(2, "0")}:${String(span % 60).padStart(2, "0")}`;
}
/** Cache of the last observed session-file size and its extracted activity line. */
const activityCache = new Map<string, { size: number; line: string }>();
function collapse(text: string): string { return text.replace(/\s+/g, " ").trim(); }
/** Read the tail of an agent session file and extract its most recent activity: last tool call (name + query) or last assistant text. */
export function agentLastActivity(sessionFile: string | undefined): string | undefined {
if (!sessionFile) return undefined;
let size: number;
try { size = statSync(sessionFile).size; } catch { return undefined; }
const cached = activityCache.get(sessionFile);
if (cached && cached.size === size) return cached.line || undefined;
let text = "";
try {
const fd = openSync(sessionFile, "r");
try { const start = Math.max(0, size - 65536); const buf = Buffer.alloc(size - start); readSync(fd, buf, 0, buf.length, start); text = buf.toString("utf8"); } finally { closeSync(fd); }
} catch { return cached?.line || undefined; }
const queries = new Map<string, string | undefined>();
let toolName: string | undefined; let toolId: string | undefined; let lastText: string | undefined;
for (const raw of text.split("\n")) {
let entry: { message?: { role?: string; toolName?: string; toolCallId?: string; content?: Array<{ type?: string; id?: string; name?: string; arguments?: unknown; text?: string }> } };
try { entry = JSON.parse(raw) as typeof entry; } catch { continue; }
const msg = entry?.message; if (!msg) continue;
if (msg.role === "toolResult") { toolName = msg.toolName; toolId = msg.toolCallId; }
if (msg.role === "assistant" && Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part?.type === "toolCall" && typeof part.id === "string") {
const args = part.arguments as Record<string, unknown> | undefined;
const query = typeof args?.query === "string" ? args.query : args ? String(Object.values(args)[0] ?? "") : "";
queries.set(part.id, query);
}
if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) lastText = collapse(part.text);
}
}
}
const line = toolName ? ` ${toolName}${toolId && queries.get(toolId) ? `: \"${queries.get(toolId)}\"` : ""}` : lastText ? ` ${lastText}` : "";
activityCache.set(sessionFile, { size, line });
return line || undefined;
}
/** Persistent non-capturing right-side panel: live agent tree without keyboard navigation. */
export function createAgentPanelComponent(
tree: AgentTree,
@@ -63,6 +104,10 @@ export function createAgentPanelComponent(
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(theme.fg("dim", ` ${truncateToWidth(activity, Math.max(20, width - 8))}`));
}
}
lines.push("", theme.fg("dim", "/agents — details · /agents-panel — hide"));
return lines.map((line) => truncateToWidth(line, width));
+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: "36%", minWidth: 44, maxHeight: "85%", anchor: "top-right", margin: 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 },
});
}