diff --git a/extensions/background-terminals/src/ui/ps.ts b/extensions/background-terminals/src/ui/ps.ts index 0d10761e..529897fd 100644 --- a/extensions/background-terminals/src/ui/ps.ts +++ b/extensions/background-terminals/src/ui/ps.ts @@ -21,6 +21,7 @@ import { panelFrame, screenTitleLine, } from "../../../shared/screen-chrome.ts"; +import { spinnerFrame } from "../../../shared/spinner.ts"; import { formatDuration, formatElapsed, @@ -43,18 +44,30 @@ function configuredKeys( return keybindings.getKeys(binding).join("/") || "unbound"; } -function statusGlyph(snap: TerminalSnapshot, theme: Theme) { +/** + * One status indicator per state, shared by the picker rows and the detail + * header. Running spins, in step with every other OpenPI surface. A selected + * row keeps its state glyph and borrows the accent tone. + */ +function statusGlyph( + snap: TerminalSnapshot, + theme: Theme, + now = Date.now(), + selected = false, +) { + const tone = (color: "warning" | "success" | "error" | "muted") => + selected ? ("accent" as const) : color; switch (snap.status) { case "running": - return theme.fg("warning", "■"); + return theme.fg(tone("warning"), spinnerFrame(now)); case "done": - return theme.fg("success", "■"); + return theme.fg(tone("success"), "✓"); case "failed": - return theme.fg("error", "■"); + return theme.fg(tone("error"), "✗"); case "killed": - return theme.fg("muted", "■"); + return theme.fg(tone("muted"), "✓"); case "timed_out": - return theme.fg("error", "■"); + return theme.fg(tone("error"), "✗"); } } @@ -92,13 +105,13 @@ export async function openTerminalPicker( new TerminalDashboard(tui, theme, keybindings, view, selection, done), { overlay: true, - // Dock the picker just above the editor (editor + strip + footer ≈ 6 + // Dock the picker just above the editor (editor + strip + footer ≈ 5 // rows) like a command palette, instead of blanking the conversation. overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "60%", - margin: { bottom: 6 }, + margin: { bottom: 5 }, }, }, ); @@ -246,6 +259,8 @@ class TerminalDashboard implements Component { configuredKeys(this.keybindings, binding); return [ + // One empty row of air between the conversation and the docked panel. + "", screenTitleLine( theme, "Background terminals", @@ -294,12 +309,13 @@ class TerminalDashboard implements Component { const index = start + i; const isSelected = index === this.selection.index; - // Left: marker, status square, title, dim id - const marker = isSelected ? theme.fg("accent", "❯") : " "; + // Left: one glyph — a selected row tints its status glyph instead of + // stacking a marker — then title and dim id. + const glyph = statusGlyph(snap, theme, Date.now(), isSelected); const title = isSelected ? theme.fg("accent", oneLine(snap.title)) : theme.fg("text", oneLine(snap.title)); - const left = ` ${marker} ${statusGlyph(snap, theme)} ${title} ${theme.fg("dim", snap.id)}`; + const left = ` ${glyph} ${title} ${theme.fg("dim", snap.id)}`; // Right: pid · elapsed · exit/status const dot = theme.fg("dim", " · "); diff --git a/extensions/subagents/index.ts b/extensions/subagents/index.ts index 2245be50..9cabfc9f 100644 --- a/extensions/subagents/index.ts +++ b/extensions/subagents/index.ts @@ -810,10 +810,10 @@ export default function (pi: ExtensionAPI) { const meta = [details.harness, details.model] .filter(Boolean) .join(" \u00b7 "); - // A spawn is a beginning, not a success: keep the marker quiet and let - // the strip's spinner carry the running state from here on. + // A spawn adds an agent to the pool: a quiet plus, then the strip's + // spinner carries the running state from here on. return new Text( - `${theme.fg("dim", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`, + `${theme.fg("dim", "+")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`, 0, 0, ); diff --git a/extensions/subagents/navigation.ts b/extensions/subagents/navigation.ts index 30050f92..becf7b45 100644 --- a/extensions/subagents/navigation.ts +++ b/extensions/subagents/navigation.ts @@ -68,7 +68,7 @@ function statusGlyph(snapshot: SubagentSnapshot, theme: Theme, now: number) { if (snapshot.status === "running") return theme.fg("warning", spinnerFrame(now)); if (snapshot.status === "done") return theme.fg("success", "✓"); - return theme.fg("error", "x"); + return theme.fg("error", "✗"); } /** One-line subagent manager entry with the same affordance as Workflow. */ diff --git a/extensions/subagents/src/ui/takeover.ts b/extensions/subagents/src/ui/takeover.ts index aadd644c..43768ba2 100644 --- a/extensions/subagents/src/ui/takeover.ts +++ b/extensions/subagents/src/ui/takeover.ts @@ -49,14 +49,19 @@ function statusGlyph( snap: SubagentSnapshot, theme: Theme, now = Date.now(), + selected = false, ): string { + // A selected row keeps its state glyph and borrows the accent tone, so the + // list never hides what is still running behind a selection marker. + const tone = (color: "warning" | "success" | "error") => + selected ? ("accent" as const) : color; switch (snap.status) { case "running": - return theme.fg("warning", spinnerFrame(now)); + return theme.fg(tone("warning"), spinnerFrame(now)); case "done": - return theme.fg("success", "✓"); + return theme.fg(tone("success"), "✓"); case "error": - return theme.fg("error", "✗"); + return theme.fg(tone("error"), "✗"); } } @@ -132,13 +137,13 @@ export async function openSubagentPicker( new SubagentDashboard(tui, theme, keybindings, view, selection, done), { overlay: true, - // Dock the picker just above the editor (editor + strip + footer ≈ 6 + // Dock the picker just above the editor (editor + strip + footer ≈ 5 // rows) like a command palette, instead of covering the conversation. overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "60%", - margin: { bottom: 6 }, + margin: { bottom: 5 }, }, }, ); @@ -306,6 +311,8 @@ export class SubagentDashboard implements Component { // /workflows. The frame is padded to the rows it was given, which keeps // this view's content-fit height rather than reintroducing a fixed one. return [ + // One empty row of air between the conversation and the docked panel. + "", ...panelFrame(theme, { label: `Subagents · ${summary}`, rows: rowLines, @@ -353,13 +360,15 @@ export class SubagentDashboard implements Component { const index = start + i; const isSelected = index === this.selection.index; - const marker = isSelected ? theme.fg("accent", "❯") : " "; + // One glyph column: a selected row tints its status glyph with the + // accent tone instead of stacking a second marker. + const glyph = statusGlyph(snap, theme, now, isSelected); const safeTitle = sanitizeSubagentDisplayLine(snap.title) || snap.id; const title = isSelected ? theme.fg("accent", safeTitle) : theme.fg("text", safeTitle); const activity = runningActivity(snap); - const prefix = ` ${marker} ${statusGlyph(snap, theme, now)} `; + const prefix = ` ${glyph} `; const utilization = formatContextUtilization(snap.usage); const metadata = [ diff --git a/extensions/subagents/takeover.test.ts b/extensions/subagents/takeover.test.ts index 64b7f4df..596a2292 100644 --- a/extensions/subagents/takeover.test.ts +++ b/extensions/subagents/takeover.test.ts @@ -117,7 +117,7 @@ test("dashboard box height follows its subagents and retains the old maximum", ( const one = dashboard([snap("one")], 10); try { const lines = one.render(100); - assert.equal(lines.length, 4); // border, one agent, border, hints + assert.equal(lines.length, 5); // air, border, one agent, border, hints assert.equal(lines.filter((line) => line.includes("agent one")).length, 1); } finally { one.dispose(); diff --git a/extensions/workflows/navigation.ts b/extensions/workflows/navigation.ts index 313cd82b..42922bbc 100644 --- a/extensions/workflows/navigation.ts +++ b/extensions/workflows/navigation.ts @@ -42,7 +42,7 @@ function cleanLine(value: string) { function statusGlyph(status: WorkflowStatus, theme: Theme, now: number) { if (status === "completed") return theme.fg("success", "✓"); if (status === "running") return theme.fg("warning", spinnerFrame(now)); - return theme.fg("error", "x"); + return theme.fg("error", "✗"); } /** Live, one-line Claude-style workflow entry rendered below the editor. */