Files
orca/agent-dashboard-mocks/index.html
T
Brennan Benson 3468b434d6 feat(dashboard): add agent dashboard popout (#9604)
* feat(dashboard): add agent dashboard popout

* fix(dashboard): gate and harden agent popout

* test(ipc): isolate dashboard handler registration

* fix(dashboard): drop diff status from bucket counts

* fix(dashboard): address review feedback

* perf(dashboard): ignore unrelated store churn
2026-07-20 17:11:23 -07:00

789 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>