chore: remove stray agent-dashboard-mocks folder

Design mockups (index.html + RESEARCH.md) accidentally committed alongside
the agent dashboard popout feature in #9604. These are scratch design
artifacts and don't belong in the shipped repo.
This commit is contained in:
Brennan Benson
2026-07-20 18:45:30 -07:00
parent aad34cbb32
commit fa585c235b
2 changed files with 0 additions and 821 deletions
-33
View File
@@ -1,33 +0,0 @@
# How people like to see all their agents at once — research + how it shaped the mocks
Grounded in shipping tools (Warp Agent Panel, herdr, Claude Code Agent View, Claude Squad, uzi, Conductor/Crystal, Vibe Kanban, k9s/Lens, Datadog Host Map, Buildkite, pm2/btop) and notification/triage UX literature. This drives `index.html`.
## The core finding
**"Waiting for input" is the whole reason this view exists.** Everything else is ambient. Every serious tool makes the needs-a-human state the loudest thing on screen and lets you resolve it without a full context switch (Warp "jump to pane", Claude Agent View "reply without attaching"). Practical parallelism tops out around **4–8 agents** (occasionally ~30), so the default should optimize glanceability for a handful, with a density fallback for many.
## Design principles applied to the mocks
1. **Waiting-for-input is loudest.** Amber + gentle pulse, top-of-sort priority, an always-visible aggregate ("2 need you"), a one-line *reason label*, and inline **Answer**/**Open** actions. → the **Needs you** inbox is the purest form; every other view surfaces it too.
2. **State is the primary channel, encoded redundantly.** Color + icon + text label (never color alone — accessibility). Colors map to Orca's `main.css` tokens (success green, destructive red) with a mock-local amber/blue for waiting/working.
3. **Motion = "needs you or is changing," never decoration.** Only *working* (subtle) and *waiting* (pulse) animate; done/idle/error are static. Respects `prefers-reduced-motion`.
4. **Density adapts to count.** Cards (≤8) → table (8–20) → tiles/grid (20+). The switcher lets you feel each.
5. **Group by project with roll-up, but let urgency flatten it.** Gallery/tree group by repo; the inbox is the flat, urgency-sorted triage view.
6. **Per-agent info priority:** state → reason (when blocked) → what it's working on → last activity → elapsed → provider badge → branch. Shown progressively (1–4 always, 5–7 on hover/detail).
7. **Live preview on hover, dots at rest.** Don't mount 30 live xterms (perf) — rest = status; **hover → terminal tail popover** with a jump-in affordance. This is the shared popover in every variant.
8. **Notify on transitions, tiered.** waiting/error = interruptive (toast + unread badge, and Orca's mobile push); done = ambient; working/idle = silent.
## The seven variants ↔ research concepts
| Variant | Maps to | Best for |
|---|---|---|
| **Repo › worktree › agent** (nested, default) | herdr state roll-up; k9s XRay tree; your own idea | Mirrors Orca's real hierarchy; roll-up dot on each repo/worktree tells you if anything inside needs you; agents sorted by urgency |
| **Needs you** (inbox) | Inbox-zero triage; Warp attention state | Any count — the "just tell me what needs me" view |
| **Card gallery** (default) | Conductor/Lens cards + progressive disclosure | Typical 5–12 — best glanceability/info balance |
| **Kanban board** | Vibe Kanban (columns = state) | 8–15; managing rather than co-coding |
| **Mission control** (table) | uzi `ls -w`, k9s | 15–30; terminal-native power users |
| **Terminal wall** (mosaic) | tmux wall, Claude Squad panes | 2–6; comparison runs / ambient monitor |
| **Tree + focus** (master-detail) | k9s XRay tree, Conductor, herdr roll-up | Supervise + dive-in; strong general default |
| **Radial map** | Datadog Host Map / your original concentric dashboard | Stretch/overview concept; homage to the movable-UI branch |
**Recommendation from the research:** ship **Card gallery** as default + **Tree + focus** as the working surface, with **Needs you** as a one-key filter and **Mission control** as the "I have 25 agents" density toggle. Wall/radial are focus/overview modes.
## Sources
Warp [managing-agents](https://docs.warp.dev/agents/using-agents/managing-agents) · herdr [github](https://github.com/ogulcancelik/herdr), [docs](https://herdr.dev/docs/agents/) · Claude Code [agent-teams](https://code.claude.com/docs/en/agent-teams), [Agent View cockpit](https://www.contextstudios.ai/blog/claude-code-agent-view-multi-agent-cockpit) · Claude Squad [github](https://github.com/smtg-ai/claude-squad) · uzi [github](https://github.com/devflowinc/uzi) · Conductor [docs](https://www.conductor.build/docs/guides/parallel-agents/run-multiple-claude-code-sessions) · Vibe Kanban [github](https://github.com/BloopAI/vibe-kanban) · k9s [site](https://k9scli.io/), [palark](https://palark.com/blog/k9s-the-powerful-terminal-ui-for-kubernetes/) · Datadog [host map](https://docs.datadoghq.com/dashboards/widgets/hostmap/), [widget colors](https://docs.datadoghq.com/dashboards/guide/widget_colors/) · Buildkite [dashboard](https://buildkite.com/docs/pipelines/dashboard-walkthrough) · pm2/btop [monitoring](https://pm2.keymetrics.io/docs/usage/monitoring/) · Triage UX [Smart Interface Design Patterns](https://smart-interface-design-patterns.com/articles/notifications/), [Missive inbox zero](https://missiveapp.com/blog/inbox-zero)
-788
View File
@@ -1,788 +0,0 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Orca · Agent Dashboard — UI explorations</title>
<style>
/*
Orca "all my agents at once" — UI mock gallery.
Six distinct layouts share one dataset + one terminal-popover system.
Colors mirror Orca tokens (src/renderer/src/assets/main.css); state colors
are mock-local since Orca reserves color for state and has no agent-state palette yet.
*/
:root {
--font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: "Geist Mono", ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--radius: 0.625rem;
/* Orca light tokens */
--background: #fff;
--foreground: #0a0a0a;
--card: #fff;
--card-foreground: #0a0a0a;
--popover: #fff;
--popover-foreground: #0a0a0a;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #171717;
--border: #e5e5e5;
--ring: #a1a1a1;
--primary: #171717;
/* Agent-state palette (mock-local, adapts to theme) */
--st-attention: #b45309; /* needs you — loudest */
--st-attention-bg: #fef3c7;
--st-working: #2563eb; /* streaming / running */
--st-working-bg: #dbeafe;
--st-idle: #737373; /* alive but quiet */
--st-idle-bg: #f5f5f5;
--st-done: #15803d; /* finished, unseen */
--st-done-bg: #dcfce7;
--st-error: #e40014; /* crashed / failed */
--st-error-bg: #fee2e2;
/* terminal surface */
--term-bg: #0b0b0c;
--term-fg: #d6d6d6;
--term-dim: #8a8a8a;
--shadow-float: 0 10px 30px rgba(0,0,0,0.22), 0 2px 8px rgba(0,0,0,0.12);
}
html.dark {
--background: #0a0a0a;
--foreground: #fafafa;
--card: #171717;
--card-foreground: #fafafa;
--popover: #171717;
--popover-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a1a1a1;
--accent: #404040;
--accent-foreground: #fafafa;
--border: rgba(255,255,255,0.09);
--ring: #737373;
--primary: #e5e5e5;
--st-attention: #fbbf24;
--st-attention-bg: rgba(251,191,36,0.14);
--st-working: #60a5fa;
--st-working-bg: rgba(96,165,250,0.15);
--st-idle: #a1a1a1;
--st-idle-bg: rgba(255,255,255,0.05);
--st-done: #86efac;
--st-done-bg: rgba(134,239,172,0.13);
--st-error: #ff6568;
--st-error-bg: rgba(255,101,104,0.14);
--term-bg: #08090b;
--term-fg: #d6d6d6;
--term-dim: #7d7d7d;
--shadow-float: 0 12px 34px rgba(0,0,0,0.55), 0 2px 10px rgba(0,0,0,0.4);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: var(--font-sans);
letter-spacing: 0.01em;
background: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
}
button { font-family: inherit; }
/* ---------- top chrome ---------- */
.topbar {
display: flex; align-items: center; gap: 14px;
padding: 10px 18px; border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 40; background: var(--background);
}
.brand { display: flex; align-items: baseline; gap: 9px; }
.brand b { font-size: 14px; font-weight: 650; }
.brand span { font-size: 12px; color: var(--muted-foreground); }
.spacer { flex: 1; }
.tabs { display: flex; gap: 2px; background: var(--muted); padding: 3px; border-radius: 9px; }
.tabs button {
border: 0; background: transparent; color: var(--muted-foreground);
font-size: 12.5px; font-weight: 550; padding: 6px 11px; border-radius: 7px; cursor: pointer;
white-space: nowrap;
}
.tabs button:hover { color: var(--foreground); }
.tabs button.active { background: var(--card); color: var(--foreground); box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.iconbtn {
border: 1px solid var(--border); background: var(--card); color: var(--foreground);
width: 34px; height: 34px; border-radius: 8px; cursor: pointer; font-size: 15px;
display: grid; place-items: center;
}
.iconbtn:hover { background: var(--accent); }
/* ---------- summary strip ---------- */
.summary { display: flex; gap: 8px; align-items: center; padding: 12px 18px 0; flex-wrap: wrap; }
.pill {
display: inline-flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 550;
padding: 5px 10px 5px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--card);
cursor: default;
}
.pill .dot { width: 8px; height: 8px; border-radius: 50%; }
.pill b { font-variant-numeric: tabular-nums; }
.pill.attn { border-color: color-mix(in srgb, var(--st-attention) 45%, var(--border)); background: var(--st-attention-bg); color: var(--st-attention); }
.intro { padding: 8px 18px 0; color: var(--muted-foreground); font-size: 12.5px; max-width: 880px; line-height: 1.5; }
.intro code { font-family: var(--font-mono); font-size: 11.5px; background: var(--muted); padding: 1px 5px; border-radius: 5px; }
.stage { padding: 16px 18px 60px; }
/* ---------- state atoms ---------- */
.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.s-attention { background: var(--st-attention); }
.s-working { background: var(--st-working); }
.s-idle { background: var(--st-idle); }
.s-done { background: var(--st-done); }
.s-error { background: var(--st-error); }
.pulse { box-shadow: 0 0 0 0 currentColor; animation: pulse 1.8s infinite; color: var(--st-attention); }
@keyframes pulse { 0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--st-attention) 55%,transparent)} 70%{box-shadow:0 0 0 7px transparent} 100%{box-shadow:0 0 0 0 transparent} }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .pulse,.spin,.caret { animation: none !important; } }
.badge {
display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 600;
padding: 2px 7px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.04em;
}
.badge.attention { background: var(--st-attention-bg); color: var(--st-attention); }
.badge.working { background: var(--st-working-bg); color: var(--st-working); }
.badge.idle { background: var(--st-idle-bg); color: var(--st-idle); }
.badge.done { background: var(--st-done-bg); color: var(--st-done); }
.badge.error { background: var(--st-error-bg); color: var(--st-error); }
.prov { width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center; font-size: 11px; font-weight: 700; color: #fff; flex: none; }
.prov.claude { background: #d97757; }
.prov.codex { background: #10a37f; }
.prov.gemini { background: linear-gradient(135deg,#4f86f7,#9b6cf5); }
.prov.opencode { background: #0ea5a4; }
.meta { color: var(--muted-foreground); font-size: 11.5px; }
.mono { font-family: var(--font-mono); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* an agent target: anything hoverable that opens the terminal popover */
[data-agent] { cursor: pointer; }
[data-agent].hot { outline: 2px solid var(--ring); outline-offset: 2px; }
/* =================================================================== */
/* VARIANT: Kanban board (by state) */
/* =================================================================== */
.kanban { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(240px, 1fr); gap: 12px; align-items: start; overflow-x: auto; }
.kcol { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 10px; min-height: 120px; }
.kcol.attn { border-color: color-mix(in srgb, var(--st-attention) 40%, var(--border)); box-shadow: 0 0 0 1px color-mix(in srgb,var(--st-attention) 22%, transparent); }
.kcol h3 { margin: 2px 4px 10px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); display: flex; align-items: center; gap: 7px; }
.kcol h3 .count { margin-left: auto; background: var(--muted); color: var(--foreground); border-radius: 999px; font-size: 10.5px; padding: 1px 7px; }
.kcard { background: var(--background); border: 1px solid var(--border); border-radius: 10px; padding: 9px 10px; margin-bottom: 8px; }
.kcard:hover { border-color: var(--ring); }
.kcard .row1 { display: flex; align-items: center; gap: 8px; }
.kcard .name { font-size: 12.5px; font-weight: 600; }
.kcard .task { margin: 6px 0 7px; font-size: 12px; color: var(--foreground); line-height: 1.35; }
.kcard .row2 { display: flex; align-items: center; gap: 8px; }
.kcard .wt { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); }
/* =================================================================== */
/* VARIANT: Card gallery (grouped by project) */
/* =================================================================== */
.projgroup { margin-bottom: 22px; }
.projhead { display: flex; align-items: center; gap: 9px; margin: 0 2px 10px; }
.projhead .pname { font-size: 12.5px; font-weight: 650; }
.projhead .pmeta { font-size: 11.5px; color: var(--muted-foreground); }
.projhead .line { flex: 1; height: 1px; background: var(--border); }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.gcard { position: relative; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px; overflow: hidden; }
.gcard:hover { border-color: var(--ring); box-shadow: 0 4px 14px rgba(0,0,0,0.08); }
.gcard .accent { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }
.gcard.attention .accent { background: var(--st-attention); }
.gcard.working .accent { background: var(--st-working); }
.gcard.idle .accent { background: var(--st-idle); }
.gcard.done .accent { background: var(--st-done); }
.gcard.error .accent { background: var(--st-error); }
.gcard .top { display: flex; align-items: center; gap: 9px; }
.gcard .name { font-size: 13px; font-weight: 620; }
.gcard .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); }
.gcard .task { margin: 10px 0 12px; font-size: 12.5px; line-height: 1.4; min-height: 34px; }
.gcard .foot { display: flex; align-items: center; gap: 10px; }
.spark { display: flex; align-items: flex-end; gap: 2px; height: 16px; }
.spark i { width: 3px; background: color-mix(in srgb, var(--st-working) 60%, transparent); border-radius: 1px; }
.attnbar { margin-top: 10px; background: var(--st-attention-bg); color: var(--st-attention); border-radius: 8px; padding: 7px 9px; font-size: 11.5px; display: flex; gap: 7px; align-items: flex-start; }
/* =================================================================== */
/* VARIANT: Mission control (dense table) */
/* =================================================================== */
.mc { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.mc th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); font-weight: 600; padding: 8px 10px; border-bottom: 1px solid var(--border); position: sticky; top: 56px; background: var(--background); cursor: pointer; user-select: none; }
.mc th.num, .mc td.num { text-align: right; font-variant-numeric: tabular-nums; }
.mc td { padding: 9px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.mc tr:hover td { background: var(--accent); }
.mc tr.attention td { box-shadow: inset 3px 0 0 var(--st-attention); }
.mc tr.attention td:first-child { background: var(--st-attention-bg); }
.mc .cell-name { display: flex; align-items: center; gap: 8px; }
.mc .cell-name b { font-weight: 600; }
.mc .wt { font-family: var(--font-mono); font-size: 11px; color: var(--muted-foreground); }
.mc .tasktext { max-width: 360px; }
/* =================================================================== */
/* VARIANT: Terminal wall (live mosaic) */
/* =================================================================== */
.wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: 12px; }
.tile { border: 1px solid var(--border); border-radius: 11px; overflow: hidden; background: var(--term-bg); }
.tile:hover { border-color: var(--ring); }
.tile.attention { border-color: color-mix(in srgb, var(--st-attention) 55%, var(--border)); }
.tile .tbar { display: flex; align-items: center; gap: 8px; padding: 7px 9px; background: var(--card); border-bottom: 1px solid var(--border); }
.tile .tbar .name { font-size: 11.5px; font-weight: 600; color: var(--card-foreground); }
.tile .tbar .wt { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); margin-left: auto; }
.tile .screen { font-family: var(--font-mono); font-size: 10.5px; line-height: 1.5; color: var(--term-fg); padding: 9px 10px; height: 128px; overflow: hidden; }
.tile .screen .l { white-space: pre; overflow: hidden; text-overflow: ellipsis; }
/* =================================================================== */
/* VARIANT: Radial / concentric */
/* =================================================================== */
.radialwrap { position: relative; width: 100%; height: 620px; }
.radial { position: absolute; inset: 0; }
.ring { position: absolute; border: 1px dashed var(--border); border-radius: 50%; left: 50%; top: 50%; transform: translate(-50%,-50%); }
.ring .rlabel { position: absolute; top: -10px; left: 50%; transform: translateX(-50%); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); background: var(--background); padding: 0 6px; }
.rcenter { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); text-align: center; width: 120px; }
.rcenter .big { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
.rcenter .lab { font-size: 11px; color: var(--muted-foreground); }
.node { position: absolute; transform: translate(-50%,-50%); display: grid; place-items: center; }
.node .chip { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; font-size: 12px; font-weight: 700; color: #fff; border: 2px solid var(--background); box-shadow: 0 0 0 2px transparent; }
.node[data-st="attention"] .chip { box-shadow: 0 0 0 3px var(--st-attention); }
.node[data-st="working"] .chip { box-shadow: 0 0 0 3px var(--st-working); }
.node[data-st="idle"] .chip { box-shadow: 0 0 0 3px var(--st-idle); }
.node[data-st="done"] .chip { box-shadow: 0 0 0 3px var(--st-done); }
.node[data-st="error"] .chip { box-shadow: 0 0 0 3px var(--st-error); }
.node .nlab { font-size: 10px; color: var(--muted-foreground); margin-top: 3px; max-width: 78px; text-align: center; }
/* =================================================================== */
/* VARIANT: Focus (tree + detail) */
/* =================================================================== */
.focus { display: grid; grid-template-columns: 320px 1fr; gap: 14px; align-items: start; }
.tree { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 8px; }
.tree .prow { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); padding: 8px 8px 4px; display: flex; gap: 7px; align-items: center; }
.tree .arow { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 8px; margin: 1px 2px; }
.tree .arow:hover { background: var(--accent); }
.tree .arow.sel { background: var(--accent); box-shadow: inset 0 0 0 1px var(--border); }
.tree .arow .nm { font-size: 12.5px; font-weight: 550; }
.tree .arow .wt { font-family: var(--font-mono); font-size: 10px; color: var(--muted-foreground); margin-left: auto; }
.detail { background: var(--card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.detail .dhead { display: flex; align-items: center; gap: 10px; padding: 13px 14px; border-bottom: 1px solid var(--border); }
.detail .dhead .name { font-size: 14px; font-weight: 650; }
.detail .dbody { display: grid; grid-template-columns: 1fr 220px; }
.detail .screenbig { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.55; color: var(--term-fg); background: var(--term-bg); padding: 12px 14px; height: 340px; overflow: auto; }
.detail .side { padding: 13px 14px; border-left: 1px solid var(--border); font-size: 12px; }
.detail .side .k { color: var(--muted-foreground); font-size: 11px; margin-top: 12px; }
.detail .side .k:first-child { margin-top: 0; }
.detail .side .v { margin-top: 2px; }
/* =================================================================== */
/* VARIANT: Nested cards — repo > worktree > agent */
/* Surfaces alternate (card > muted > card) so 3 levels read in both */
/* themes without relying on shadows. */
/* =================================================================== */
.repogrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); gap: 14px; align-items: start; }
.repo { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 12px; }
.repohead { display: flex; align-items: center; gap: 9px; margin: 2px 3px 12px; }
.repohead .rn { font-size: 13.5px; font-weight: 680; }
.repohead .rmeta { font-size: 11px; color: var(--muted-foreground); margin-left: auto; font-variant-numeric: tabular-nums; }
.wtpanel { background: var(--muted); border: 1px solid var(--border); border-radius: 11px; padding: 9px; margin-bottom: 10px; }
.wtpanel:last-child { margin-bottom: 0; }
.wthead { display: flex; align-items: center; gap: 8px; margin: 1px 3px 8px; }
.wthead .wn { font-family: var(--font-mono); font-size: 11px; font-weight: 600; }
.wthead .wc { font-size: 10px; color: var(--muted-foreground); background: var(--card); border: 1px solid var(--border); border-radius: 999px; padding: 1px 6px; }
.wthead .wd { margin-left: auto; }
.acard { position: relative; display: flex; align-items: flex-start; gap: 8px; background: var(--card); border: 1px solid var(--border); border-radius: 9px; padding: 8px 9px 8px 12px; margin-bottom: 6px; overflow: hidden; }
.acard:last-child { margin-bottom: 0; }
.acard:hover { border-color: var(--ring); }
.acard .aacc { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }
.acard.attention .aacc { background: var(--st-attention); }
.acard.working .aacc { background: var(--st-working); }
.acard.idle .aacc { background: var(--st-idle); }
.acard.done .aacc { background: var(--st-done); }
.acard.error .aacc { background: var(--st-error); }
.acard .abody { flex: 1; min-width: 0; }
.acard .arow { display: flex; align-items: center; gap: 7px; }
.acard .an { font-size: 12px; font-weight: 600; }
.acard .at { font-size: 11.5px; margin-top: 3px; line-height: 1.35; }
.acard .am { font-size: 10.5px; color: var(--muted-foreground); margin-top: 4px; }
/* =================================================================== */
/* VARIANT: Needs-you inbox (triage-first) */
/* =================================================================== */
.inbox { max-width: 860px; margin: 0 auto; }
.inbox .lead { font-size: 12.5px; color: var(--muted-foreground); margin: 0 2px 12px; }
.irow { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 11px; margin-bottom: 9px; background: var(--card); }
.irow.attention { border-color: color-mix(in srgb, var(--st-attention) 45%, var(--border)); background: color-mix(in srgb, var(--st-attention-bg) 60%, var(--card)); }
.irow.error { border-color: color-mix(in srgb, var(--st-error) 40%, var(--border)); }
.irow:hover { box-shadow: 0 3px 12px rgba(0,0,0,0.09); }
.irow .who { min-width: 150px; }
.irow .who .nm { font-size: 12.5px; font-weight: 620; display: flex; align-items: center; gap: 7px; }
.irow .who .wt { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); margin-top: 2px; }
.irow .reason { flex: 1; font-size: 12.5px; line-height: 1.4; }
.irow .reason .lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 650; margin-right: 6px; }
.irow.attention .reason .lbl { color: var(--st-attention); }
.irow.error .reason .lbl { color: var(--st-error); }
.irow .acts { display: flex; gap: 7px; }
.quiet { margin-top: 22px; }
.quiet h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground); margin: 0 2px 8px; }
.qrow { display: flex; align-items: center; gap: 9px; padding: 7px 10px; border-radius: 8px; font-size: 12px; }
.qrow:hover { background: var(--accent); }
.qrow .wt { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); }
.emptyz { text-align: center; padding: 40px; color: var(--muted-foreground); }
/* ---------- terminal popover ---------- */
#pop {
position: fixed; z-index: 100; width: 440px; max-width: 92vw;
background: var(--popover); color: var(--popover-foreground);
border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-float);
opacity: 0; transform: translateY(4px) scale(0.99); pointer-events: none;
transition: opacity .12s ease, transform .12s ease; overflow: hidden;
}
#pop.show { opacity: 1; transform: none; pointer-events: auto; }
#pop .phead { display: flex; align-items: center; gap: 9px; padding: 10px 12px; border-bottom: 1px solid var(--border); }
#pop .phead .name { font-size: 13px; font-weight: 620; }
#pop .phead .wt { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-foreground); }
#pop .pscreen { font-family: var(--font-mono); font-size: 11.5px; line-height: 1.55; background: var(--term-bg); color: var(--term-fg); padding: 11px 13px; height: 240px; overflow: auto; }
#pop .pscreen .l { white-space: pre-wrap; word-break: break-word; }
#pop .cmd { color: #9ece6a; }
#pop .path { color: #7aa2f7; }
#pop .warn { color: #e0af68; }
#pop .err { color: #f7768e; }
#pop .dim { color: var(--term-dim); }
#pop .ok { color: #9ece6a; }
.caret { display: inline-block; width: 7px; height: 14px; background: var(--term-fg); vertical-align: text-bottom; animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
#pop .askbar { background: var(--st-attention-bg); color: var(--st-attention); padding: 9px 13px; font-size: 12px; display: flex; gap: 8px; align-items: flex-start; border-top: 1px solid var(--border); }
#pop .pfoot { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-top: 1px solid var(--border); }
#pop .pfoot input { flex: 1; background: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px; color: var(--foreground); font-size: 12px; font-family: var(--font-mono); }
.btn { border: 1px solid var(--border); background: var(--card); color: var(--foreground); font-size: 12px; font-weight: 550; padding: 7px 11px; border-radius: 8px; cursor: pointer; white-space: nowrap; }
.btn:hover { background: var(--accent); }
.btn.primary { background: var(--primary); color: var(--background); border-color: var(--primary); }
</style>
</head>
<body>
<div class="topbar">
<div class="brand"><b>Orca</b><span>Agents</span></div>
<div class="spacer"></div>
<div class="tabs" id="tabs"></div>
<button class="iconbtn" id="themeToggle" title="Toggle light / dark">◐</button>
</div>
<div class="summary" id="summary"></div>
<div class="intro" id="intro">
Eight ways to see every agent across all your worktrees at once. <b>Hover any agent</b> to peek its live terminal
in a popover (move into the popover to interact). Layout switcher is top-right; <code>◐</code> toggles light/dark.
</div>
<div class="stage" id="stage"></div>
<!-- shared terminal popover -->
<div id="pop" role="dialog" aria-label="Agent terminal preview"></div>
<script>
/* ------------------------------------------------------------------ */
/* Mock data: agents across 4 projects / worktrees, weighted states. */
/* ------------------------------------------------------------------ */
const AGENTS = [
{ id:'a1', name:'Claude', prov:'claude', project:'orca', worktree:'agent-canvas-view', branch:'brennanb2025/agent-canvas', state:'attention',
task:'Refactor the concentric dashboard into a movable panel', elapsed:'7m', last:'waiting 40s', add:214, del:63, files:9,
ask:'Move AgentDashboard mount to the right rail, or keep it as a floating window?',
term:[['dim','$ '],['cmd','claude'],['',' Read src/renderer/.../AgentDashboard.tsx'],['',' Edit dashboard-panel.tsx (+214 −63)'],['',' 9 files staged'],['warn',' ? I need a decision before continuing:'],['warn',' Right-rail mount vs floating window?']] },
{ id:'a2', name:'Codex', prov:'codex', project:'orca', worktree:'pty-backpressure', branch:'brennanb2025/pty-queue', state:'working',
task:'Bound PTY output queues under backpressure', elapsed:'12m', last:'2s ago', add:88, del:12, files:4,
term:[['dim','$ '],['cmd','codex exec'],['path',' daemon/pty-queue.ts'],['',' applying patch… hunk 3/5'],['ok',' ✓ typecheck passed'],['',' running vitest pty-queue.test.ts'],['dim',' › 14 passed, streaming…']] },
{ id:'a3', name:'Gemini', prov:'gemini', project:'orca', worktree:'git-graph-view', branch:'brennanb2025/git-graph', state:'idle',
task:'Add Git Graph view showing all branches', elapsed:'1h 3m', last:'idle 9m', add:402, del:120, files:15,
term:[['dim','$ '],['cmd','gemini'],['ok',' ✓ built GitGraphView'],['dim',' waiting for next instruction'],['dim',' (idle — no output for 9m)']] },
{ id:'a4', name:'Claude', prov:'claude', project:'orca-internal', worktree:'skill-freshness', branch:'skill-freshness-rail', state:'done',
task:'Safe skill freshness detection + update rail', elapsed:'22m', last:'done 3m', add:640, del:210, files:18,
term:[['dim','$ '],['cmd','claude'],['ok',' ✓ all checks green'],['ok',' ✓ 217 tests passed'],['',' Summary: added freshness rail, 5 fixes'],['dim',' ── agent finished ──']] },
{ id:'a5', name:'Codex', prov:'codex', project:'orca-internal', worktree:'posthog-dash', branch:'posthog-dashboard-edit', state:'error',
task:'Add chart-density design rules', elapsed:'4m', last:'error 30s', add:12, del:4, files:2,
term:[['dim','$ '],['cmd','codex exec'],['',' writing references/palette.md'],['err',' ✗ ENOENT: references/ does not exist'],['err',' Error: build step failed (exit 1)'],['dim',' process exited — needs retry']] },
{ id:'a6', name:'OpenCode', prov:'opencode', project:'mobile', worktree:'quick-commands', branch:'mobile-quick-cmds', state:'working',
task:'Terminal Quick Commands launcher', elapsed:'18m', last:'just now', add:333, del:41, files:11,
term:[['dim','$ '],['cmd','opencode run'],['path',' mobile/src/terminal/QuickCommands.tsx'],['',' rendering tab-strip launcher'],['dim',' hot-reloading sim…'],['ok',' ✓ mounted on iPhone 15 sim']] },
{ id:'a7', name:'Claude', prov:'claude', project:'mobile', worktree:'native-chat-toggle', branch:'mobile-native-chat', state:'attention',
task:'Per-device native chat default-view toggle', elapsed:'9m', last:'waiting 1m', add:120, del:30, files:6,
ask:'Default the toggle ON for new installs, or leave OFF until opt-in?',
term:[['dim','$ '],['cmd','claude'],['',' Edit Settings/NativeChat.tsx'],['warn',' ? Default new installs to ON or OFF?'],['warn',' (tri-state map ready either way)']] },
{ id:'a8', name:'Gemini', prov:'gemini', project:'mobile', worktree:'ws-accept-wedge', branch:'mobile-ws-6768', state:'working',
task:'Fix :6768 WS accept wedge (no notifications)', elapsed:'31m', last:'5s ago', add:76, del:22, files:3,
term:[['dim','$ '],['cmd','gemini'],['path',' runtime/ws-server.ts'],['',' adding maxConnections cap + accept-error handler'],['dim',' restarting :6768 listener…'],['ok',' ✓ heartbeat liveness log added']] },
{ id:'a9', name:'Codex', prov:'codex', project:'orca', worktree:'dialog-maxw', branch:'dialog-content-maxw', state:'idle',
task:'Let caller max-width override DialogContent cap', elapsed:'2h 11m', last:'idle 44m', add:9, del:3, files:1,
term:[['dim','$ '],['cmd','codex exec'],['ok',' ✓ patch applied'],['dim',' awaiting review'],['dim',' (idle 44m)']] },
{ id:'a10', name:'Claude', prov:'claude', project:'orca', worktree:'prune-registrations', branch:'space-prune', state:'done',
task:'Prune stale worktree registrations', elapsed:'14m', last:'done 20m', add:180, del:64, files:7,
term:[['dim','$ '],['cmd','claude'],['ok',' ✓ prune action wired to relay op'],['ok',' ✓ i18n auto-keys synced'],['dim',' ── agent finished ──']] },
{ id:'a11', name:'OpenCode', prov:'opencode', project:'orca-internal', worktree:'pr-review-alert', branch:'pr-review-scope', state:'working',
task:'Alert on changes outside PR stated feature', elapsed:'6m', last:'1s ago', add:54, del:8, files:2,
term:[['dim','$ '],['cmd','opencode run'],['',' scanning diff for scope creep'],['dim',' 3 files outside stated feature…'],['warn',' flagging status-bar change']] },
{ id:'a12', name:'Gemini', prov:'gemini', project:'mobile', worktree:'create-ws-dropdown', branch:'mobile-create-ws', state:'error',
task:'Fix Create Workspace dropdown unresponsive', elapsed:'3m', last:'error 1m', add:20, del:15, files:2,
term:[['dim','$ '],['cmd','gemini'],['',' wiring BottomDrawerModalHost'],['err',' ✗ esbuild: cannot resolve ./probe'],['err',' Error: bundling failed'],['dim',' exited — retry needed']] },
// split-pane cases: a second agent sharing a worktree (proves the innermost nesting)
{ id:'a2b', name:'Claude', prov:'claude', project:'orca', worktree:'pty-backpressure', branch:'brennanb2025/pty-queue', state:'attention',
task:'Review the queue-bound patch in a split pane', elapsed:'5m', last:'waiting 25s', add:0, del:0, files:0,
ask:'Cap the queue at 64KiB, or make it a setting?',
term:[['dim','$ '],['cmd','claude'],['',' reviewing daemon/pty-queue.ts'],['warn',' ? Cap at 64KiB or expose as a setting?']] },
{ id:'a8b', name:'OpenCode', prov:'opencode', project:'mobile', worktree:'ws-accept-wedge', branch:'mobile-ws-6768', state:'working',
task:'Run the :6768 socket soak test', elapsed:'12m', last:'3s ago', add:18, del:2, files:1,
term:[['dim','$ '],['cmd','opencode run'],['',' soak: 500 connect/disconnect cycles'],['dim',' cycle 213/500…'],['ok',' ✓ no wedge so far']] },
];
// urgency rank for sorting + roll-up (attention & error bubble to the top)
const URG = { attention:0, error:1, working:2, done:3, idle:4 };
const STATE_ORDER = ['attention','working','idle','done','error'];
const STATE_LABEL = { attention:'Needs you', working:'Working', idle:'Idle', done:'Done', error:'Error' };
const STATE_ICON = { attention:'✋', working:'⟳', idle:'○', done:'✓', error:'✗' };
const byState = s => AGENTS.filter(a => a.state === s);
const projects = [...new Set(AGENTS.map(a => a.project))];
const initials = p => ({claude:'C',codex:'Cx',gemini:'G',opencode:'oc'})[p] || '?';
/* ------------------------------------------------------------------ */
/* Terminal popover */
/* ------------------------------------------------------------------ */
const pop = document.getElementById('pop');
let hideTimer = null, showTimer = null, streamTimer = null, activeId = null;
function termHTML(a){
const lines = a.term.map(([cls,txt]) => `<div class="l"><span class="${cls}">${escapeHTML(txt)}</span></div>`).join('');
return lines;
}
function escapeHTML(s){ return s.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
function renderPop(a){
const askbar = a.ask ? `<div class="askbar">✋ <div><b>Waiting for you.</b> ${escapeHTML(a.ask)}</div></div>` : '';
pop.innerHTML = `
<div class="phead">
<div class="prov ${a.prov}">${initials(a.prov)}</div>
<div>
<div class="name">${a.name} · <span class="wt">${escapeHTML(a.worktree)}</span></div>
<div class="meta">${a.project} · ${a.elapsed}</div>
</div>
<div style="margin-left:auto"><span class="badge ${a.state}">${STATE_LABEL[a.state]}</span></div>
</div>
<div class="pscreen" id="pscreen">${termHTML(a)}<div class="l"><span class="dim">$ </span><span class="caret"></span></div></div>
${askbar}
<div class="pfoot">
<input placeholder="Type a message to ${a.name}…" />
<button class="btn">Focus worktree</button>
<button class="btn primary">Open terminal</button>
</div>`;
}
function positionPop(target){
const r = target.getBoundingClientRect();
const pw = pop.offsetWidth, ph = pop.offsetHeight;
const gap = 10;
let left = r.right + gap;
if (left + pw > window.innerWidth - 8) left = r.left - pw - gap; // flip left
if (left < 8) left = Math.min(Math.max(8, r.left), window.innerWidth - pw - 8); // clamp
let top = r.top + r.height/2 - ph/2;
top = Math.max(8, Math.min(top, window.innerHeight - ph - 8));
pop.style.left = left + 'px';
pop.style.top = top + 'px';
}
function openPop(target){
const a = AGENTS.find(x => x.id === target.dataset.agent);
if (!a) return;
clearTimeout(hideTimer);
activeId = a.id;
renderPop(a);
pop.classList.add('show');
positionPop(target);
// fake a little "live" streaming for working agents
clearInterval(streamTimer);
if (a.state === 'working') {
const extra = [' · touched '+a.worktree, ' · '+a.last, ' › streaming output…', ' · '+a.files+' files changed'];
let i = 0;
streamTimer = setInterval(() => {
const sc = document.getElementById('pscreen'); if(!sc) return;
const line = document.createElement('div'); line.className='l';
line.innerHTML = `<span class="dim">${escapeHTML(extra[i % extra.length])}</span>`;
sc.insertBefore(line, sc.lastElementChild);
sc.scrollTop = sc.scrollHeight; i++;
}, 1400);
}
}
function scheduleHide(){
clearTimeout(hideTimer);
hideTimer = setTimeout(() => { pop.classList.remove('show'); clearInterval(streamTimer); activeId=null; }, 220);
}
function wireHovers(root){
root.querySelectorAll('[data-agent]').forEach(el => {
el.addEventListener('mouseenter', () => { clearTimeout(showTimer); showTimer = setTimeout(() => openPop(el), 120); el.classList.add('hot'); });
el.addEventListener('mouseleave', () => { clearTimeout(showTimer); el.classList.remove('hot'); scheduleHide(); });
});
}
pop.addEventListener('mouseenter', () => clearTimeout(hideTimer));
pop.addEventListener('mouseleave', scheduleHide);
window.addEventListener('scroll', () => { if(activeId){ const t=document.querySelector(`[data-agent="${activeId}"]`); if(t) positionPop(t);} }, true);
/* ------------------------------------------------------------------ */
/* Summary strip */
/* ------------------------------------------------------------------ */
function renderSummary(){
const el = document.getElementById('summary');
const parts = STATE_ORDER.map(s => {
const n = byState(s).length;
const attn = s === 'attention';
return `<div class="pill ${attn?'attn':''}"><span class="dot s-${s} ${attn?'pulse':''}"></span>${STATE_LABEL[s]} <b>${n}</b></div>`;
}).join('');
el.innerHTML = `<div class="pill" style="font-weight:650"><b>${AGENTS.length}</b>&nbsp;agents · ${projects.length} projects</div>` + parts;
}
/* ------------------------------------------------------------------ */
/* Variant renderers */
/* ------------------------------------------------------------------ */
function vKanban(stage){
stage.innerHTML = `<div class="kanban">` + STATE_ORDER.map(s => {
const list = byState(s);
const cards = list.map(a => `
<div class="kcard" data-agent="${a.id}">
<div class="row1"><div class="prov ${a.prov}">${initials(a.prov)}</div><span class="name">${a.name}</span>
<span style="margin-left:auto" class="dot s-${s} ${s==='attention'?'pulse':''}"></span></div>
<div class="task">${a.task}</div>
<div class="row2"><span class="wt">${a.project}/${a.worktree}</span></div>
</div>`).join('') || `<div class="meta" style="padding:6px 4px">None</div>`;
return `<div class="kcol ${s==='attention'?'attn':''}"><h3><span class="dot s-${s}"></span>${STATE_LABEL[s]}<span class="count">${list.length}</span></h3>${cards}</div>`;
}).join('') + `</div>`;
wireHovers(stage);
}
function vGallery(stage){
stage.innerHTML = projects.map(p => {
const list = AGENTS.filter(a => a.project === p);
const attn = list.filter(a => a.state==='attention').length;
const cards = list.map(a => {
const spark = `<div class="spark">`+Array.from({length:12},()=>`<i style="height:${4+Math.round(Math.random()*14)}px"></i>`).join('')+`</div>`;
const ask = a.ask ? `<div class="attnbar">✋ <div>${escapeHTML(a.ask)}</div></div>` : '';
return `<div class="gcard ${a.state}" data-agent="${a.id}">
<div class="accent"></div>
<div class="top"><div class="prov ${a.prov}">${initials(a.prov)}</div>
<div><div class="name">${a.name}</div><div class="sub">${a.worktree}</div></div>
<div style="margin-left:auto"><span class="badge ${a.state}">${STATE_LABEL[a.state]}</span></div></div>
<div class="task">${a.task}</div>
${ask}
<div class="foot"><span class="meta">${a.last}</span>
<span style="margin-left:auto">${a.state==='working'?spark:`<span class="meta">${a.elapsed}</span>`}</span></div>
</div>`;
}).join('');
return `<div class="projgroup"><div class="projhead"><span class="pname">${p}</span>
<span class="pmeta">${list.length} agents${attn?` · ${attn} need you`:''}</span><span class="line"></span></div>
<div class="gallery">${cards}</div></div>`;
}).join('');
wireHovers(stage);
}
function vMissionControl(stage){
const rows = [...AGENTS].sort((a,b)=>STATE_ORDER.indexOf(a.state)-STATE_ORDER.indexOf(b.state)).map(a => `
<tr class="${a.state}" data-agent="${a.id}">
<td><div class="cell-name"><span class="dot s-${a.state} ${a.state==='attention'?'pulse':''}"></span><div class="prov ${a.prov}" style="width:18px;height:18px;font-size:9px;border-radius:5px">${initials(a.prov)}</div><b>${a.name}</b></div></td>
<td><span class="wt">${a.project}/${a.worktree}</span></td>
<td class="tasktext truncate">${a.ask ? '✋ '+escapeHTML(a.ask) : a.task}</td>
<td class="num meta">${a.elapsed}</td>
<td class="num meta">${a.last}</td>
</tr>`).join('');
stage.innerHTML = `<table class="mc"><thead><tr>
<th>Agent</th><th>Project / Worktree</th><th>Task</th><th class="num">Elapsed</th><th class="num">Last activity</th>
</tr></thead><tbody>${rows}</tbody></table>`;
wireHovers(stage);
}
function vWall(stage){
stage.innerHTML = `<div class="wall">` + AGENTS.map(a => {
const screen = a.term.slice(-6).map(([cls,txt])=>`<div class="l"><span class="${cls}" style="color:inherit">${escapeHTML(txt)}</span></div>`).join('');
return `<div class="tile ${a.state}" data-agent="${a.id}">
<div class="tbar"><span class="dot s-${a.state} ${a.state==='attention'?'pulse':''}"></span>
<div class="prov ${a.prov}" style="width:18px;height:18px;font-size:9px;border-radius:5px">${initials(a.prov)}</div>
<span class="name">${a.name}</span><span class="wt">${a.worktree}</span></div>
<div class="screen">${screen}<div class="l"><span style="color:var(--term-dim)">$ </span><span class="caret"></span></div></div>
</div>`;
}).join('') + `</div>`;
wireHovers(stage);
}
function vRadial(stage){
stage.innerHTML = `<div class="radialwrap"><div class="radial" id="radial"></div></div>`;
const wrap = document.getElementById('radial');
const W = wrap.clientWidth, H = 620, cx = W/2, cy = H/2;
// radii kept < cy (310) so no ring clips into the header
const RAD = { attention:95, working:170, quiet:235, error:288 };
[[RAD.attention,'Needs you'],[RAD.working,'Working'],[RAD.quiet,'Idle / Done'],[RAD.error,'Error']].forEach(([rad,lab])=>{
const d=document.createElement('div'); d.className='ring'; d.style.width=d.style.height=(rad*2)+'px';
d.innerHTML=`<span class="rlabel">${lab}</span>`; wrap.appendChild(d);
});
const center=document.createElement('div'); center.className='rcenter';
const attn=byState('attention').length;
center.innerHTML=`<div class="big">${AGENTS.length}</div><div class="lab">agents<br>${attn} need you</div>`;
wrap.appendChild(center);
// per-ring angular offset so small groups don't collapse onto the vertical axis
const groups = [
['attention', byState('attention'), RAD.attention, 30],
['working', byState('working'), RAD.working, -20],
['quiet', [...byState('idle'),...byState('done')], RAD.quiet, 15],
['error', byState('error'), RAD.error, -45],
];
groups.forEach(([g,list,rad,off])=>{
list.forEach((a,i)=>{
const ang = (off + (360/Math.max(list.length,1))*i) * Math.PI/180;
const x = cx + rad*Math.cos(ang), y = cy + rad*Math.sin(ang);
const n=document.createElement('div'); n.className='node'; n.dataset.agent=a.id; n.dataset.st=a.state;
n.style.left=x+'px'; n.style.top=y+'px';
n.innerHTML=`<div class="chip ${a.prov}">${initials(a.prov)}</div><div class="nlab">${a.worktree}</div>`;
wrap.appendChild(n);
});
});
wireHovers(wrap);
}
function vNested(stage){
// build repo -> worktree -> [agents]
const repos = {};
AGENTS.forEach(a => { (repos[a.project] ??= {}); (repos[a.project][a.worktree] ??= []).push(a); });
const rollup = list => list.slice().sort((x,y)=>URG[x.state]-URG[y.state])[0].state; // most-urgent child
const byUrg = key => (a,b) => URG[key(a)]-URG[key(b)];
const repoEntries = Object.entries(repos)
.map(([p,wts]) => { const all = Object.values(wts).flat(); return { p, wts, all, roll: rollup(all) }; })
.sort((a,b) => URG[a.roll]-URG[b.roll] || a.p.localeCompare(b.p));
stage.innerHTML = `<div class="repogrid">` + repoEntries.map(({p,wts,all,roll}) => {
const attn = all.filter(a=>a.state==='attention').length;
const wtEntries = Object.entries(wts)
.map(([w,list]) => ({ w, list, roll: rollup(list) }))
.sort((a,b) => URG[a.roll]-URG[b.roll] || a.w.localeCompare(b.w));
const wtHTML = wtEntries.map(({w,list,roll}) => {
const agents = list.slice().sort(byUrg(a=>a.state)).map(a => `
<div class="acard ${a.state}" data-agent="${a.id}">
<div class="aacc"></div>
<div class="prov ${a.prov}" style="width:20px;height:20px;font-size:10px;border-radius:6px">${initials(a.prov)}</div>
<div class="abody">
<div class="arow"><span class="an">${a.name}</span>
<span style="margin-left:auto" class="badge ${a.state}">${STATE_LABEL[a.state]}</span></div>
<div class="at truncate">${a.ask ? '✋ '+escapeHTML(a.ask) : a.task}</div>
<div class="am">${a.last}</div>
</div>
</div>`).join('');
return `<div class="wtpanel">
<div class="wthead"><span class="dot s-${roll} ${roll==='attention'?'pulse':''}"></span>
<span class="wn">${w}</span>${list.length>1?`<span class="wc">${list.length} agents</span>`:''}</div>
${agents}</div>`;
}).join('');
return `<div class="repo">
<div class="repohead"><span class="dot s-${roll} ${roll==='attention'?'pulse':''}"></span>
<span class="rn">${p}</span>
<span class="rmeta">${all.length} agents${attn?` · <span style="color:var(--st-attention);font-weight:600">${attn} need you</span>`:''}</span></div>
${wtHTML}</div>`;
}).join('') + `</div>`;
wireHovers(stage);
}
function vInbox(stage){
const urgent = [...byState('attention'), ...byState('error')];
const quiet = AGENTS.filter(a => !['attention','error'].includes(a.state))
.sort((a,b)=>STATE_ORDER.indexOf(a.state)-STATE_ORDER.indexOf(b.state));
const urgentHTML = urgent.length ? urgent.map(a=>{
const isErr = a.state==='error';
const reason = isErr
? `<span class="lbl">Error</span>${escapeHTML(a.term.find(t=>t[0]==='err')?.[1]?.trim() || 'process exited — needs retry')}`
: `<span class="lbl">Waiting</span>${escapeHTML(a.ask || 'needs your input')}`;
const acts = isErr
? `<button class="btn">View log</button><button class="btn primary">Retry</button>`
: `<button class="btn">Open</button><button class="btn primary">Answer</button>`;
return `<div class="irow ${a.state}" data-agent="${a.id}">
<span class="dot s-${a.state} ${a.state==='attention'?'pulse':''}"></span>
<div class="who"><div class="nm"><div class="prov ${a.prov}" style="width:18px;height:18px;font-size:9px;border-radius:5px">${initials(a.prov)}</div>${a.name}</div>
<div class="wt">${a.project}/${a.worktree}</div></div>
<div class="reason">${reason}</div>
<div class="acts">${acts}</div></div>`;
}).join('') : `<div class="emptyz">✓ Nothing needs you right now. ${AGENTS.length} agents running.</div>`;
const quietHTML = quiet.map(a=>`<div class="qrow" data-agent="${a.id}">
<span class="dot s-${a.state}"></span><span class="badge ${a.state}">${STATE_LABEL[a.state]}</span>
<span style="font-weight:550">${a.name}</span><span class="wt">${a.worktree}</span>
<span style="margin-left:auto" class="meta">${a.last}</span></div>`).join('');
stage.innerHTML = `<div class="inbox">
<p class="lead">${urgent.length} of ${AGENTS.length} agents need a human — sorted by urgency. Everything else is running fine below.</p>
${urgentHTML}
<div class="quiet"><h4>Running quietly · ${quiet.length}</h4>${quietHTML}</div>
</div>`;
wireHovers(stage);
}
function vFocus(stage){
let sel = AGENTS.find(a=>a.state==='attention') || AGENTS[0];
const treeHTML = projects.map(p=>{
const list = AGENTS.filter(a=>a.project===p);
return `<div class="prow"><span>${p}</span><span style="margin-left:auto" class="meta">${list.length}</span></div>` +
list.map(a=>`<div class="arow" data-agent="${a.id}" data-sel="${a.id}">
<span class="dot s-${a.state} ${a.state==='attention'?'pulse':''}"></span>
<span class="nm">${a.name}</span><span class="wt">${a.worktree}</span></div>`).join('');
}).join('');
stage.innerHTML = `<div class="focus"><div class="tree">${treeHTML}</div><div class="detail" id="detail"></div></div>`;
const renderDetail = (a)=>{
const scr = a.term.map(([cls,txt])=>`<div><span class="${cls}" style="color:inherit">${escapeHTML(txt)}</span></div>`).join('');
document.getElementById('detail').innerHTML = `
<div class="dhead"><div class="prov ${a.prov}">${initials(a.prov)}</div>
<div><div class="name">${a.name}</div><div class="meta mono">${a.project}/${a.worktree}</div></div>
<div style="margin-left:auto" class="badge ${a.state}">${STATE_LABEL[a.state]}</div></div>
<div class="dbody"><div class="screenbig">${scr}<div><span class="dim">$ </span><span class="caret"></span></div></div>
<div class="side">
<div class="k">Task</div><div class="v">${a.task}</div>
${a.ask?`<div class="k">Waiting on you</div><div class="v" style="color:var(--st-attention)">${escapeHTML(a.ask)}</div>`:''}
<div class="k">Branch</div><div class="v mono" style="font-size:11px">${a.branch}</div>
<div class="k">Elapsed</div><div class="v">${a.elapsed} · ${a.last}</div>
<div style="margin-top:14px;display:flex;gap:8px"><button class="btn">Focus worktree</button><button class="btn primary">Open</button></div>
</div></div>`;
};
renderDetail(sel);
stage.querySelectorAll('.arow').forEach(row=>{
row.addEventListener('click',()=>{ stage.querySelectorAll('.arow').forEach(r=>r.classList.remove('sel')); row.classList.add('sel');
renderDetail(AGENTS.find(a=>a.id===row.dataset.sel)); });
});
stage.querySelector(`.arow[data-sel="${sel.id}"]`)?.classList.add('sel');
wireHovers(stage);
}
/* ------------------------------------------------------------------ */
/* Tabs / routing */
/* ------------------------------------------------------------------ */
const VARIANTS = [
['nested','Repo › worktree › agent', vNested],
['inbox','Needs you', vInbox],
['gallery','Card gallery', vGallery],
['board','Kanban board', vKanban],
['mission','Mission control', vMissionControl],
['wall','Terminal wall', vWall],
['focus','Tree + focus', vFocus],
['radial','Radial map', vRadial],
];
let current = 'nested';
function renderTabs(){
document.getElementById('tabs').innerHTML = VARIANTS.map(([k,l]) =>
`<button data-k="${k}" class="${k===current?'active':''}">${l}</button>`).join('');
document.querySelectorAll('#tabs button').forEach(b => b.addEventListener('click',()=>{ current=b.dataset.k; renderTabs(); renderStage(); }));
}
function renderStage(){
pop.classList.remove('show'); clearInterval(streamTimer);
const stage = document.getElementById('stage');
VARIANTS.find(v=>v[0]===current)[2](stage);
}
document.getElementById('themeToggle').addEventListener('click',()=>{ document.documentElement.classList.toggle('dark'); if(current==='radial') renderStage(); });
window.addEventListener('resize',()=>{ if(current==='radial') renderStage(); });
renderSummary(); renderTabs(); renderStage();
</script>
</body>
</html>