mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
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:
@@ -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)
|
||||
@@ -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 => ({'&':'&','<':'<','>':'>'}[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> 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>
|
||||
Reference in New Issue
Block a user