Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
6.8 kB · 174 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175import { type Component, type Focusable, getKeybindings, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";import type { AgentConnectionRlmChildAgentSnapshot } from "../../agent-connection/index.js";import { collectSubagentDescendantSummaries } from "../../agents-view/agents-view-state.js";import { type AgentRosterStatus, classifyAgentStatus } from "../../daemon/agent-roster.js";import { classifySessionRosterStatus, type SessionSummary } from "../../daemon/daemon-session-list.js";import { theme } from "../theme/theme.js";import { keyText } from "./keybinding-hints.js";
export interface SubagentSummaryCounts { total: number; running: number; idle: number; inactive: number;}
export function classifySubagentSnapshotStatus(child: AgentConnectionRlmChildAgentSnapshot): AgentRosterStatus { // Activity implies a live session; the in-process connection never stamps activeSessionId. const resident = child.activeSessionId !== undefined || child.activity !== undefined; const busy = child.status === "running" || child.status === "queued" || child.activity !== undefined; return classifyAgentStatus({ resident, queuedChild: !resident && busy, busy, });}
/** Status counts over every snapshot descending from `parentId`; the recursive roster carries the whole subtree. */export function countSubtreeSubagentStatuses( children: Iterable<AgentConnectionRlmChildAgentSnapshot>, parentId: string | undefined,): SubagentSummaryCounts { const counts: SubagentSummaryCounts = { total: 0, running: 0, idle: 0, inactive: 0 }; const knownNodes = new Set<string | undefined>([parentId]); const pending = [...children].filter((child) => child.status !== "cancelled"); let matched = true; while (matched) { matched = false; for (let index = pending.length - 1; index >= 0; index--) { const child = pending[index]!; if (!knownNodes.has(child.parentId)) continue; pending.splice(index, 1); matched = true; counts.total += 1; counts[classifySubagentSnapshotStatus(child)] += 1; knownNodes.add(child.id); } } return counts;}
export function countRosterSubagentStatuses( summaries: Iterable<SessionSummary>, parent: { activeSessionId?: string | undefined; sessionId?: string | undefined; sessionFile?: string | undefined },): SubagentSummaryCounts { const counts: SubagentSummaryCounts = { total: 0, running: 0, idle: 0, inactive: 0 }; // The daemon roster spans every tree: count live rows in this session's subtree, at any depth. for (const child of collectSubagentDescendantSummaries(summaries, parent)) { if (child.lifecycle !== "live") continue; counts.total += 1; counts[child.rosterStatus ?? classifySessionRosterStatus(child)] += 1; } return counts;}
/** One-line entry into the current session's scoped agents view. */export class SubagentSummaryLine implements Component, Focusable { focused = false; private counts: SubagentSummaryCounts = { total: 0, running: 0, idle: 0, inactive: 0 }; private openable = false;
onOpen?: () => void; onCancel?: () => void; onChatAction?: (data: string) => void;
constructor( private readonly getLocationLabel: () => string | undefined = () => undefined, private readonly getContextLabel: () => string | undefined = () => undefined, private readonly getOverrideLabel: () => string | undefined = () => undefined, private readonly getPickerOpen: () => boolean = () => false, ) {}
setSubagentCounts(counts: SubagentSummaryCounts): void { this.counts = counts; }
setOpenable(openable: boolean): void { this.openable = openable; }
isSelectable(): boolean { return !this.getPickerOpen() && this.counts.total > 0 && this.openable; }
handleInput(data: string): void { const keybindings = getKeybindings(); if (keybindings.matches(data, "tui.select.confirm") || keybindings.matches(data, "app.agents.open")) { if (this.isSelectable()) this.onOpen?.(); return; } if ( keybindings.matches(data, "tui.select.up") || keybindings.matches(data, "tui.select.cancel") || keybindings.matches(data, "app.agents.back") ) { this.onCancel?.(); return; } this.onChatAction?.(data); }
render(width: number): string[] { if (this.getPickerOpen()) return []; const lines = this.renderInfoLine(width); if (this.counts.total === 0) return lines; if (width < 2) return lines; const safeWidth = width; const inner = safeWidth - 2; const label = theme.fg("accent", "subagents"); const top = truncateToWidth( `${theme.fg("border", "╭─ ")}${label}${theme.fg("border", ` ${"─".repeat(Math.max(0, inner - 3 - visibleWidth(label)))}╮`)}`, safeWidth, "…", ); const counts = theme.fg("success", `● ${this.counts.running} running`) + " " + theme.fg("warning", `◐ ${this.counts.idle} idle`) + " " + theme.fg("dim", `○ ${this.counts.inactive} inactive`); const openHint = this.openable ? this.focused ? `${keyText("tui.select.confirm")}/${keyText("app.agents.open")} open` : `${keyText("tui.editor.cursorDown", { primaryOnly: true })} select` : ""; const gap = Math.max(1, inner - 2 - visibleWidth(counts) - visibleWidth(openHint)); const body = truncateToWidth(` ${counts}${" ".repeat(gap)}${theme.fg("dim", openHint)} `, inner, "…"); const pad = " ".repeat(Math.max(0, inner - visibleWidth(body))); // Truncation may inject full ANSI resets; wrap each segment so the // selection background survives past them (custom-editor precedent). const content = this.focused ? `${body}${pad}` .split("\x1b[0m") .map((segment) => theme.bg("selectedBg", segment)) .join("\x1b[0m") : `${body}${pad}`; lines.push( top, `${theme.fg("border", "│")}${content}${theme.fg("border", "│")}`, theme.fg("border", `╰${"─".repeat(inner)}╯`), ); return lines; }
private renderInfoLine(width: number): string[] { const overrideLabel = this.getOverrideLabel()?.trim(); const locationLabel = this.getLocationLabel()?.trim(); const contextLabel = this.getContextLabel()?.trim(); const left = overrideLabel || locationLabel || ""; if (!left && !contextLabel) return []; const safeWidth = Math.max(1, width); const right = contextLabel ?? ""; const gap = left && right ? 2 : 0; const rightWidth = Math.min(visibleWidth(right), Math.max(0, safeWidth - gap)); const leftWidth = Math.max(0, safeWidth - rightWidth - gap); const renderedLeft = truncateToWidth(left, leftWidth, "…"); const renderedRight = truncateToWidth(right, rightWidth, "…"); const padding = Math.max(0, safeWidth - visibleWidth(renderedLeft) - visibleWidth(renderedRight)); return [theme.fg("muted", `${renderedLeft}${" ".repeat(padding)}${renderedRight}`)]; }
invalidate(): void { // Render output is derived from counts and focus state. }}