From 18dc4cbb16975738bc1c722950d59200b5a8fc5a Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 31 Jul 2026 23:45:39 +0300 Subject: [PATCH] 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. --- CHANGELOG.md | 4 ++++ src/agent-tree-ui.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/supervisor.ts | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1cfd72..1ae0652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agent-tree-ui.ts b/src/agent-tree-ui.ts index d056539..133b70b 100644 --- a/src/agent-tree-ui.ts +++ b/src/agent-tree-ui.ts @@ -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(); + +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(); + 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 | 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)); diff --git a/src/supervisor.ts b/src/supervisor.ts index 44f8f4c..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: "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 }, }); }