@staix/agent-hub 0.12.43 → 0.12.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -3
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +228 -0
- package/docs/agent-notes/README.md +14 -0
- package/docs/agent-notes/adapters.md +61 -42
- package/docs/agent-notes/budget.md +2 -2
- package/docs/agent-notes/daemon.md +16 -10
- package/docs/agent-notes/local-worker.md +4 -3
- package/docs/agent-notes/tasks.md +13 -6
- package/docs/agent-notes/tests.md +9 -7
- package/docs/operations.md +163 -12
- package/docs/quickstart.md +10 -2
- package/docs/sdlc.md +26 -8
- package/docs/security.md +11 -6
- package/docs/specs/2026-09-19-agent-hub-design.md +273 -8
- package/docs/verification/2026-10-21-0.12.44.md +54 -0
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-21-grok-config-acp-t0.md +30 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verified.json +38 -20
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +10287 -9560
- package/src/adapters/acp-owned-group.ts +124 -0
- package/src/adapters/acp.ts +92 -25
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +4 -4
- package/src/adapters/agy/entry.ts +4 -1
- package/src/adapters/channel-peer.ts +143 -0
- package/src/adapters/claude-channel.ts +21 -12
- package/src/adapters/codex-appserver.ts +17 -2
- package/src/adapters/configured-acp.ts +124 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +60 -39
- package/src/adapters/definitions/native-channel-state.ts +41 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +9 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-preview-qwen.ts +1 -1
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/native-identity-defaults.ts +14 -0
- package/src/adapters/definitions/qwen.ts +7 -9
- package/src/adapters/definitions/reviewer-variants.ts +3 -7
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/jcode.ts +4 -0
- package/src/adapters/local-worker.ts +4 -0
- package/src/adapters/pi.ts +6 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +28 -16
- package/src/adapters/registry.ts +115 -17
- package/src/adapters/reviewer-print.ts +4 -0
- package/src/cli/agy-mcp.ts +3 -2
- package/src/cli/configured-acp-entry.ts +58 -0
- package/src/cli/console-state.ts +9 -4
- package/src/cli/facts-hook.ts +2 -1
- package/src/cli/help.ts +27 -16
- package/src/cli/identity.ts +8 -7
- package/src/cli/launch.ts +5 -4
- package/src/cli/main.ts +101 -32
- package/src/cli/memory.ts +13 -10
- package/src/cli/output.ts +1 -0
- package/src/cli/preview.ts +9 -0
- package/src/cli/recovery-guidance.ts +1 -1
- package/src/cli/terminal-recovery.ts +16 -13
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +6 -4
- package/src/cli/upgrade-runtime.ts +25 -7
- package/src/cli/upgrade.ts +3 -2
- package/src/hub/board.ts +37 -8
- package/src/hub/budget.ts +9 -2
- package/src/hub/bus.ts +6 -2
- package/src/hub/claude-state.ts +2 -37
- package/src/hub/conductor.ts +8 -6
- package/src/hub/config-apply.ts +37 -17
- package/src/hub/config-reload.ts +42 -11
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +6 -0
- package/src/hub/control-client.ts +4 -1
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +412 -328
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +57 -5
- package/src/hub/instance-config.ts +33 -12
- package/src/hub/instance-execution.ts +23 -11
- package/src/hub/instances.ts +47 -22
- package/src/hub/lifecycle-schema.ts +5 -3
- package/src/hub/memory-profile.ts +16 -3
- package/src/hub/no-writes.ts +50 -0
- package/src/hub/peers.ts +9 -0
- package/src/hub/permission-mode.ts +9 -7
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +35 -6
- package/src/hub/routing.ts +21 -20
- package/src/hub/sdlc-queue.ts +19 -4
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +27 -12
- package/src/hub/settings.ts +54 -16
- package/src/hub/snapshots.ts +32 -6
- package/src/hub/start-mode.ts +10 -8
- package/src/hub/task-sweep.ts +8 -2
- package/src/hub/tasks.ts +358 -60
- package/src/hub/ui.ts +2 -2
- package/src/memory/capture.ts +2 -1
- package/src/ui/index.html +563 -77
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
package/src/ui/index.html
CHANGED
|
@@ -18,11 +18,225 @@ const dashboardTheme = (() => {
|
|
|
18
18
|
})();
|
|
19
19
|
</script>
|
|
20
20
|
<style>
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/* Design tokens (#469 R1). Every colour lives in these three blocks and nowhere else. The dark values appear twice because
|
|
22
|
+
the theme can be chosen (data-theme) or follow the system; the two blocks must stay identical (a test compares them).
|
|
23
|
+
Neutrals are one cool family. The accent is for primary actions, focus and selection. Green, amber, blue and red keep
|
|
24
|
+
their state meaning. Spacing is on a 4px base. Radius rule: controls 6px, surfaces (panels, cards, dialogs) 10px,
|
|
25
|
+
chips and avatars full; a flush list row keeps 0. Two elevations, used only on raised elements (--shadow-1 is for the board cards in R3). The Office scene rules below
|
|
26
|
+
are unchanged until R5; keep a comment out from directly before them, because a test finds those rules by their selector text. */
|
|
27
|
+
:root{color-scheme:light;
|
|
28
|
+
--paper:#eeeef1;--panel:#f8f8fa;--raised:#fcfcfd;--panel-soft:#f1f1f4;--control:#fcfcfd;--hover:#ececf0;
|
|
29
|
+
--ink:#1f2024;--muted:#5a5c66;--faint:#6a6c76;--line:#dcdce2;--control-border:#c4c4cc;
|
|
30
|
+
--accent:#4338ca;--accent-soft:#e6e7fb;--on-accent:#fafafc;--on-peer:#121316;--focus:#4338ca;
|
|
31
|
+
--green:#245f45;--green-soft:#e4efe9;--amber:#8a5b12;--amber-soft:#f8eed6;--blue:#245a93;--blue-soft:#e6eef8;--red:#a2392f;--red-soft:#f6e5e2;--waiting:#795399;
|
|
32
|
+
--dot:#80828c;--dot-ok:#2f7a52;--dot-warn:#b07a16;--dot-err:#b24539;
|
|
33
|
+
--scrim:rgba(18,19,22,.4);--shadow-1:0 1px 2px rgba(31,32,36,.08),0 2px 8px rgba(31,32,36,.06);--shadow-2:0 8px 24px rgba(31,32,36,.16),0 2px 6px rgba(31,32,36,.1);
|
|
34
|
+
--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-6:24px;--space-8:32px;
|
|
35
|
+
--text-xs:12px;--text-sm:13px;--text-base:14px;--text-lg:16px;--text-xl:20px;
|
|
36
|
+
--font-sans:ui-sans-serif,-apple-system,"Segoe UI Variable",system-ui,sans-serif;--font-mono:ui-monospace,"SF Mono","Cascadia Code",monospace;
|
|
37
|
+
--radius-control:6px;--radius-surface:10px;--radius-full:999px}
|
|
38
|
+
:root[data-theme="dark"]{color-scheme:dark;
|
|
39
|
+
--paper:#121316;--panel:#1a1b1f;--raised:#222328;--panel-soft:#16171a;--control:#1a1b1f;--hover:#25262c;
|
|
40
|
+
--ink:#e7e7ea;--muted:#a3a5ae;--faint:#8d8f99;--line:#2a2b31;--control-border:#3a3b43;
|
|
41
|
+
--accent:#a5b4fc;--accent-soft:#25283f;--on-accent:#121316;--on-peer:#121316;--focus:#a5b4fc;
|
|
42
|
+
--green:#6fc2a0;--green-soft:#1e3a2e;--amber:#d9a64f;--amber-soft:#3a2f14;--blue:#8ab8ea;--blue-soft:#1d2f44;--red:#e28b80;--red-soft:#3d2320;--waiting:#b9a0df;
|
|
43
|
+
--dot:#767985;--dot-ok:#4cb77f;--dot-warn:#d9a64f;--dot-err:#e28b80;
|
|
44
|
+
--scrim:rgba(0,0,0,.55);--shadow-1:0 1px 2px rgba(0,0,0,.4),0 2px 8px rgba(0,0,0,.3);--shadow-2:0 8px 24px rgba(0,0,0,.55),0 2px 6px rgba(0,0,0,.4)}
|
|
45
|
+
@media(prefers-color-scheme:dark){:root:not([data-theme="light"]):not([data-theme="dark"]){color-scheme:dark;
|
|
46
|
+
--paper:#121316;--panel:#1a1b1f;--raised:#222328;--panel-soft:#16171a;--control:#1a1b1f;--hover:#25262c;
|
|
47
|
+
--ink:#e7e7ea;--muted:#a3a5ae;--faint:#8d8f99;--line:#2a2b31;--control-border:#3a3b43;
|
|
48
|
+
--accent:#a5b4fc;--accent-soft:#25283f;--on-accent:#121316;--on-peer:#121316;--focus:#a5b4fc;
|
|
49
|
+
--green:#6fc2a0;--green-soft:#1e3a2e;--amber:#d9a64f;--amber-soft:#3a2f14;--blue:#8ab8ea;--blue-soft:#1d2f44;--red:#e28b80;--red-soft:#3d2320;--waiting:#b9a0df;
|
|
50
|
+
--dot:#767985;--dot-ok:#4cb77f;--dot-warn:#d9a64f;--dot-err:#e28b80;
|
|
51
|
+
--scrim:rgba(0,0,0,.55);--shadow-1:0 1px 2px rgba(0,0,0,.4),0 2px 8px rgba(0,0,0,.3);--shadow-2:0 8px 24px rgba(0,0,0,.55),0 2px 6px rgba(0,0,0,.4)}}
|
|
52
|
+
|
|
53
|
+
/* Base */
|
|
54
|
+
*{box-sizing:border-box}
|
|
55
|
+
body{margin:0;background:var(--paper);color:var(--ink);font:var(--text-base)/1.5 var(--font-sans)}
|
|
56
|
+
h1{font-size:var(--text-xl);letter-spacing:-.02em;margin:0}
|
|
57
|
+
h1 span{font-weight:400;color:var(--muted)}
|
|
58
|
+
h2{font-size:var(--text-lg);margin:0}
|
|
59
|
+
p{margin:var(--space-2) 0}
|
|
60
|
+
.eyebrow{overflow-wrap:anywhere;font-size:var(--text-xs);color:var(--faint);letter-spacing:.12em;text-transform:uppercase}
|
|
61
|
+
.meta{overflow-wrap:anywhere;font-size:var(--text-xs);color:var(--muted);font-variant-numeric:tabular-nums}
|
|
62
|
+
.connection{font-size:var(--text-xs);color:var(--muted)}
|
|
63
|
+
.stack{display:flex;flex-direction:column;gap:var(--space-6);min-width:0}
|
|
64
|
+
footer{max-width:1440px;margin:auto;padding:var(--space-4) var(--space-8) var(--space-6);color:var(--faint);font-size:var(--text-xs)}
|
|
65
|
+
|
|
66
|
+
/* Surfaces and rows */
|
|
67
|
+
section{border:1px solid var(--line);background:var(--panel);border-radius:var(--radius-surface);overflow:hidden}
|
|
68
|
+
.section-head{display:flex;justify-content:space-between;align-items:center;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line);gap:var(--space-3)}
|
|
69
|
+
.section-head .filters{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}
|
|
70
|
+
.content{padding:var(--space-4)}
|
|
71
|
+
.empty{color:var(--muted);padding:var(--space-4)}
|
|
72
|
+
.row{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
73
|
+
.row:last-child{border-bottom:0}
|
|
74
|
+
.row-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)}
|
|
75
|
+
.peername{display:flex;align-items:center;gap:var(--space-2);min-width:0}
|
|
76
|
+
.label{font-weight:600;overflow-wrap:anywhere}
|
|
77
|
+
.body{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:var(--space-2)}
|
|
78
|
+
.split{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)}
|
|
79
|
+
.stream{height:360px;overflow:auto}
|
|
80
|
+
.event{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
81
|
+
.event .body{font:var(--text-sm)/1.55 var(--font-mono)}
|
|
82
|
+
|
|
83
|
+
/* Badges, dots, chips */
|
|
84
|
+
.badge{display:inline-block;font:var(--text-xs) var(--font-mono);font-variant-numeric:tabular-nums;padding:2px var(--space-2);background:var(--panel-soft);color:var(--muted);border-radius:var(--radius-full);white-space:nowrap}
|
|
85
|
+
.badge.ok{background:var(--green-soft);color:var(--green)}
|
|
86
|
+
.badge.warn{color:var(--amber);background:var(--amber-soft)}
|
|
87
|
+
.badge.info{color:var(--blue);background:var(--blue-soft)}
|
|
88
|
+
.badge.err{color:var(--red);background:var(--red-soft)}
|
|
89
|
+
.dot{flex:none;width:9px;height:9px;border-radius:var(--radius-full);background:var(--dot)}
|
|
90
|
+
.dot.ok{background:var(--dot-ok)}
|
|
91
|
+
.dot.warn{background:var(--dot-warn)}
|
|
92
|
+
.dot.err{background:var(--dot-err)}
|
|
93
|
+
.chips{display:flex;gap:var(--space-2);flex-wrap:wrap}
|
|
94
|
+
.chip{background:var(--control);color:var(--muted);border:1px solid var(--control-border);border-radius:var(--radius-full);padding:var(--space-1) var(--space-3);font-size:var(--text-xs);font-variant-numeric:tabular-nums;cursor:pointer;line-height:1.2}
|
|
95
|
+
.chip:hover{background:var(--hover)}
|
|
96
|
+
.chip[aria-pressed="true"]{background:var(--accent);color:var(--on-accent);border-color:var(--accent);font-weight:600}
|
|
97
|
+
|
|
98
|
+
/* Controls */
|
|
99
|
+
button,input,select,textarea{font:inherit}
|
|
100
|
+
button{background:var(--control);color:var(--ink);border:1px solid var(--control-border);border-radius:var(--radius-control);padding:var(--space-2) var(--space-3);cursor:pointer}
|
|
101
|
+
button:hover{background:var(--hover)}
|
|
102
|
+
button.primary{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
|
|
103
|
+
button.danger{color:var(--red)}
|
|
104
|
+
button:disabled{opacity:.5;cursor:default}
|
|
105
|
+
:focus-visible{outline:2px solid var(--focus);outline-offset:3px}
|
|
106
|
+
input,select,textarea{width:100%;border:1px solid var(--control-border);border-radius:var(--radius-control);padding:var(--space-2);background:var(--control);color:var(--ink)}
|
|
107
|
+
input[type="checkbox"]{width:auto;margin-right:var(--space-2)}
|
|
108
|
+
textarea{resize:vertical;min-height:80px}
|
|
109
|
+
label{display:block;font-size:var(--text-xs);color:var(--muted);margin-bottom:var(--space-1)}
|
|
110
|
+
form .field{margin-bottom:var(--space-3)}
|
|
111
|
+
.actions{display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap;margin-top:var(--space-3)}
|
|
112
|
+
.actions select{width:auto;max-width:220px}
|
|
113
|
+
.actions input[type="text"]{width:auto;flex:1 1 160px;max-width:320px}
|
|
114
|
+
.section-head .filters select{width:auto;max-width:220px}
|
|
115
|
+
.theme-choice{width:auto}
|
|
116
|
+
details summary{cursor:pointer;padding:var(--space-3) var(--space-4);color:var(--accent);font-weight:600}
|
|
117
|
+
details form{padding:0 var(--space-4) var(--space-4)}
|
|
118
|
+
details.settings-group>summary{padding:var(--space-3) var(--space-4);cursor:pointer;border-bottom:1px solid var(--line);font-weight:600}
|
|
119
|
+
details.settings-group:last-child:not([open])>summary{border-bottom:0}
|
|
120
|
+
.preview{margin-top:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--panel-soft);border-radius:var(--radius-control);font:var(--text-xs) var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
121
|
+
.preview:empty{display:none}
|
|
122
|
+
|
|
123
|
+
/* Budgets */
|
|
124
|
+
progress{width:100%;height:8px;accent-color:var(--green);display:block;margin:var(--space-2) 0}
|
|
125
|
+
progress.warn{accent-color:var(--amber)}
|
|
126
|
+
progress.warn::-webkit-progress-value{background:var(--amber)}
|
|
127
|
+
progress.critical{accent-color:var(--red)}
|
|
128
|
+
progress.critical::-webkit-progress-value{background:var(--red)}
|
|
129
|
+
progress::-moz-progress-bar{background:var(--green)}
|
|
130
|
+
progress.warn::-moz-progress-bar{background:var(--amber)}
|
|
131
|
+
progress.critical::-moz-progress-bar{background:var(--red)}
|
|
132
|
+
.budget-window+.budget-window{margin-top:var(--space-4)}
|
|
133
|
+
|
|
134
|
+
/* Task progress strip: the state colours keep their meaning */
|
|
135
|
+
.task-progress{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
136
|
+
.task-progress svg{display:block;width:100%;height:12px;margin:var(--space-2) 0}
|
|
137
|
+
.progress-proposed{fill:var(--dot)}
|
|
138
|
+
.progress-unknown{fill:var(--dot)}
|
|
139
|
+
.progress-backlog{fill:var(--dot)}
|
|
140
|
+
.progress-cancelled{fill:var(--muted)}
|
|
141
|
+
.progress-waiting{fill:var(--waiting)}
|
|
142
|
+
.progress-in_progress{fill:var(--blue)}
|
|
143
|
+
.progress-in_review{fill:var(--amber)}
|
|
144
|
+
.progress-changes_requested{fill:var(--red)}
|
|
145
|
+
.progress-approved{fill:var(--green)}
|
|
146
|
+
.task-stage{font:var(--text-xs) var(--font-mono);margin:var(--space-2) 0;color:var(--muted)}
|
|
147
|
+
.task-stage .reached{color:var(--green)}
|
|
148
|
+
.task-stage.changes{color:var(--red)}
|
|
149
|
+
.task-stage.cancelled{color:var(--muted)}
|
|
150
|
+
|
|
151
|
+
/* Memory */
|
|
152
|
+
.memory-controls{display:flex;gap:var(--space-2);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
153
|
+
.memory-controls input,.memory-controls select{width:auto;flex:1 1 130px}
|
|
154
|
+
.memory-row{display:block;width:100%;text-align:left;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:var(--space-3) var(--space-4);background:var(--panel)}
|
|
155
|
+
.memory-row[aria-pressed="true"]{background:var(--accent-soft);box-shadow:inset 3px 0 0 var(--accent)}
|
|
156
|
+
.memory-row:hover{background:var(--hover)}
|
|
157
|
+
.memory-row[aria-pressed="true"]:hover{background:var(--accent-soft)}
|
|
158
|
+
.memory-row .label{font-weight:500}
|
|
159
|
+
.memory-row .meta{margin-top:var(--space-1)}
|
|
160
|
+
.memory-row:last-child{border-bottom:0}
|
|
161
|
+
#memory-list{max-height:420px;overflow:auto}
|
|
162
|
+
#memory-detail:not(:empty){border-top:1px solid var(--line)}
|
|
163
|
+
#memory-detail .body{font-size:var(--text-sm);line-height:1.55}
|
|
164
|
+
.memory-graph svg{display:block;width:100%;height:320px;background:var(--panel-soft)}
|
|
165
|
+
.medge{stroke:var(--dot);stroke-width:1.5}
|
|
166
|
+
.medge.rules_out{stroke:var(--red)}
|
|
167
|
+
.medge.supersedes{stroke-dasharray:4 3}
|
|
168
|
+
.medge.suggested{stroke-dasharray:2 4;opacity:.75}
|
|
169
|
+
.medge.contradicts{stroke:var(--amber)}
|
|
170
|
+
.mnode{cursor:pointer}
|
|
171
|
+
.mnode circle{fill:var(--dot);stroke:var(--panel);stroke-width:2}
|
|
172
|
+
.mnode.memory circle{fill:var(--blue)}
|
|
173
|
+
.mnode.fail circle{fill:var(--red)}
|
|
174
|
+
.mnode.decision circle{fill:var(--green)}
|
|
175
|
+
.mnode.contract circle{fill:var(--amber)}
|
|
176
|
+
.mnode.selected circle{stroke:var(--focus);stroke-width:3}
|
|
177
|
+
.mnode:focus-visible circle{stroke:var(--focus);stroke-width:4}
|
|
178
|
+
.mnode text{font:var(--text-xs) var(--font-sans);fill:var(--ink)}
|
|
179
|
+
.memory-edges{margin:0;padding:var(--space-2) var(--space-4) var(--space-3) var(--space-8);font:var(--text-xs) var(--font-mono);color:var(--muted);max-height:160px;overflow:auto}
|
|
180
|
+
/* Message (#469 R4): the shared form styles (label above field, .field spacing); only the width is capped so a line of text stays readable. */
|
|
181
|
+
#send{max-width:640px}
|
|
182
|
+
#send textarea{min-height:140px}
|
|
183
|
+
|
|
184
|
+
/* Agents (#469 R4): a summary line per peer, the diagnostics folded into one disclosure, approvals first (their section is first in the DOM). */
|
|
185
|
+
.peer-summary{display:flex;flex-wrap:wrap;gap:var(--space-1) var(--space-4);margin:var(--space-2) 0 0}
|
|
186
|
+
.peer-fold{margin-top:var(--space-2)}
|
|
187
|
+
.peer-fold>summary{padding:var(--space-1) 0;color:var(--muted);font-weight:500;font-size:var(--text-xs)}
|
|
188
|
+
.peer-more{padding:var(--space-1) 0 0 var(--space-3);border-left:2px solid var(--line)}
|
|
189
|
+
.notice-warn{color:var(--amber)}
|
|
190
|
+
/* Stream: aligned rows. The filter chips and Follow latest sit in the section head, outside the scrolling list, so they stay in view while it scrolls. */
|
|
191
|
+
.stream{padding:0}
|
|
192
|
+
.event{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-1);align-items:baseline}
|
|
193
|
+
.event>.meta{display:flex;flex-wrap:wrap;align-items:center;gap:0 var(--space-1);color:var(--muted)}
|
|
194
|
+
.event .icon{width:14px;height:14px}
|
|
195
|
+
.event:nth-child(even){background:var(--panel-soft)}
|
|
196
|
+
/* Settings: a group is a titled section; a restart-pending value carries a badge, not only a line of text. */
|
|
197
|
+
.settings-reload{padding:var(--space-3) var(--space-4)}
|
|
198
|
+
.settings-reload:empty{display:none}
|
|
199
|
+
.settings-reload .meta{margin-top:var(--space-1)}
|
|
200
|
+
details.settings-group{border-bottom:1px solid var(--line)}
|
|
201
|
+
details.settings-group>summary{background:var(--panel-soft)}
|
|
202
|
+
details.settings-group .row{border-bottom:1px dashed var(--line)}
|
|
203
|
+
details.settings-group .row:last-child{border-bottom:0}
|
|
204
|
+
/* Peer colours: the registry's hue, one variant per theme, generated by peer-palette.ts */
|
|
23
205
|
/* PEER_PALETTE_CSS */
|
|
24
|
-
|
|
25
|
-
|
|
206
|
+
|
|
207
|
+
/* Task board */
|
|
208
|
+
.task-board-section{min-width:0}
|
|
209
|
+
.board-columns{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:var(--space-3);padding:var(--space-4)}
|
|
210
|
+
.board-column{min-width:0;background:var(--panel-soft);border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3)}
|
|
211
|
+
.board-column h3{font-size:var(--text-base);margin:0 0 var(--space-3)}
|
|
212
|
+
.board-column h4{font-size:var(--text-xs);color:var(--muted);margin:var(--space-3) 0 var(--space-2)}
|
|
213
|
+
.task-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3);margin:0 0 var(--space-3);overflow-wrap:anywhere}
|
|
214
|
+
.task-card .row-head{align-items:flex-start}
|
|
215
|
+
.task-card .label{font-size:var(--text-base)}
|
|
216
|
+
.task-card .actions{display:grid;gap:var(--space-2)}
|
|
217
|
+
.task-card .actions select{max-width:none;width:100%}
|
|
218
|
+
.task-card button,.task-card select,.agent-legend button{min-height:44px}
|
|
219
|
+
.task-card .body{font-size:var(--text-sm)}
|
|
220
|
+
.task-card .task-stage{font-size:var(--text-xs)}
|
|
221
|
+
.board-columns .empty{padding:var(--space-2) 0}
|
|
222
|
+
.board-lane{border-top:1px solid var(--line)}
|
|
223
|
+
.board-lane>h3{padding:var(--space-3) var(--space-4) 0;font-size:var(--text-lg)}
|
|
224
|
+
.peer-mark{display:inline-flex;align-items:center;gap:var(--space-2)}
|
|
225
|
+
.peer-color-dot{width:9px;height:9px;display:inline-block;border-radius:var(--radius-full);background:var(--peer-color);flex-shrink:0}
|
|
226
|
+
.peer-color-dot.reviewer{background:transparent;border:2px solid var(--peer-color)}
|
|
227
|
+
.agent-legend{display:flex;flex-wrap:wrap;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--line)}
|
|
228
|
+
.agent-legend .agent-slot{border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3);flex:1 1 180px;min-width:0}
|
|
229
|
+
.board-drop-active{outline:2px solid var(--accent);outline-offset:2px}
|
|
230
|
+
.board-toolbar{padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-4);flex-wrap:wrap;align-items:center}
|
|
231
|
+
.board-toolbar label{margin:0}
|
|
232
|
+
.board-toolbar select{width:auto;max-width:220px}
|
|
233
|
+
.board-unknown{padding:var(--space-3) var(--space-4);color:var(--amber)}
|
|
234
|
+
@media(max-width:1100px){.board-columns{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
235
|
+
@media(max-width:900px){.stack{gap:var(--space-4)}.stream{height:300px}footer{padding:var(--space-4)}}
|
|
236
|
+
@media(max-width:600px){.split{grid-template-columns:minmax(0,1fr)}.board-columns{grid-template-columns:1fr;padding:var(--space-3)}.board-toolbar{align-items:stretch}.board-toolbar select{max-width:none;width:100%}.board-toolbar label{width:100%}}
|
|
237
|
+
/* Reduced motion (#469 R6): one rule for the whole page. Every transition and animation is off, whatever rule declares it. */
|
|
238
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
|
|
239
|
+
|
|
26
240
|
.office-scene{display:block;max-width:100%;height:auto;margin-inline:auto;font-family:system-ui,sans-serif;font-size:1px}.office-scene :is(rect,path,line,polygon,circle){vector-effect:non-scaling-stroke;stroke-width:1px}.office-room{fill:var(--panel-soft);stroke:var(--line)}.office-room-title{fill:var(--ink);font-size:1.083333px;font-weight:600}.office-room-count{fill:var(--ink);font-size:1px;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.office-header-surface{fill:var(--panel);stroke:var(--line)}.office-count-pill{fill:var(--panel);stroke:var(--line)}.office-desk-surface{fill:var(--panel);stroke:var(--line)}.office-desk.office-dimmed .office-avatar{opacity:.55}.office-name{fill:var(--ink);font-size:1.083333px;font-weight:600}.office-state,.office-meta,.office-empty{fill:var(--muted);font-size:1px}.office-glyph,.office-hand{font-size:1px}.office-idle .office-glyph{fill:var(--green)}.office-busy .office-glyph{fill:var(--amber)}.office-offline .office-glyph,.office-unknown .office-glyph{fill:var(--muted)}.office-paused .office-glyph{fill:var(--amber)}.office-waiting .office-glyph{fill:var(--red)}.office-hand{fill:var(--red)}.office-bubble{fill:var(--panel);stroke:var(--blue)}.office-bubble-text{fill:var(--ink);font-size:1px}.office-mode{fill:var(--muted);font-size:1px;font-family:ui-monospace,monospace}.office-card{fill:var(--control);stroke:var(--control-border)}.office-card-text{fill:var(--ink);font-size:1px}.office-card-id{font-family:ui-monospace,monospace}.office-card-accent{fill:var(--line)}.office-waiting-card .office-card-accent{fill:var(--waiting)}.office-rework-card .office-card-accent{fill:var(--red)}.office-card-checking .office-card-accent{fill:var(--blue)}.office-more{fill:var(--blue);font-size:1px}.office-scene .office-spotlight .office-desk-surface,.office-scene .office-spotlight .office-card,.office-scene [role="button"]:focus-visible > .office-card{stroke:var(--focus);stroke-width:2px}.office-card-hit{fill:transparent;stroke:none;pointer-events:all}.office-card-focus:focus-visible{outline:none}.row.spotlight{background:var(--hover)}
|
|
27
241
|
|
|
28
242
|
.office-movable{transition:transform .8s ease}.office-arc .office-arc-path{fill:none;stroke:var(--blue);stroke-width:1px}.office-arc .office-arc-head{fill:var(--blue)}.office-arc-important .office-arc-path{stroke:var(--amber)}.office-arc-important .office-arc-head{fill:var(--amber)}.office-arc-dropped .office-arc-path{stroke:var(--red);stroke-dasharray:.6 .4}.office-arc-dropped .office-arc-head{fill:var(--red)}.office-arc-label{fill:var(--muted);font-size:1px}.office-alert{fill:var(--red);font-size:1px}.office-latch{fill:var(--amber);font-size:1px}.office-link-line{fill:none;stroke-width:1px;stroke-dasharray:.8 .5}.office-link-overlap .office-link-line{stroke:var(--amber)}.office-link-conflict .office-link-line{stroke:var(--red)}.office-link-cohort .office-link-line{stroke:var(--blue)}.office-link-label{font-size:1px;fill:var(--muted)}.office-link-conflict .office-link-label{fill:var(--red)}.office-link-overlap .office-link-label{fill:var(--amber)}.office-link-cohort .office-link-label{fill:var(--blue)}.office-checking{fill:var(--blue);font-size:.916667px}
|
|
@@ -30,25 +244,154 @@ const dashboardTheme = (() => {
|
|
|
30
244
|
@keyframes office-pulse{50%{opacity:.45}}@keyframes office-appear{from{opacity:0}}@keyframes office-fade{to{opacity:0}}
|
|
31
245
|
@media(prefers-reduced-motion:reduce){.office-scene,.office-scene *{transition:none!important;animation:none!important}}
|
|
32
246
|
|
|
247
|
+
/* View navigation, task detail and dialog */
|
|
248
|
+
[hidden]{display:none!important}
|
|
249
|
+
/* App shell (#469 R2). From 1024px: a sticky top bar over a left sidebar and the content. Below 1024px: the same nav is one
|
|
250
|
+
row of tabs that scrolls sideways inside itself, so the page never scrolls sideways. The nav buttons, their order and the
|
|
251
|
+
data-view-target markers are the ones the navigation tests pin. */
|
|
252
|
+
.sprite{position:absolute;width:0;height:0;overflow:hidden}
|
|
253
|
+
.icon{width:20px;height:20px;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
254
|
+
.skip-link{position:absolute;left:var(--space-2);top:-48px;z-index:30;background:var(--accent);color:var(--on-accent);padding:var(--space-2) var(--space-3);border-radius:var(--radius-control)}
|
|
255
|
+
.skip-link:focus{top:var(--space-2)}
|
|
256
|
+
.shell{display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto minmax(0,1fr);min-height:100vh}
|
|
257
|
+
.topbar{grid-row:1;display:flex;align-items:center;gap:var(--space-4);padding:var(--space-2) var(--space-4);min-height:56px;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
|
|
258
|
+
.brand{min-width:0}
|
|
259
|
+
.brand .eyebrow{line-height:1.2}
|
|
260
|
+
.brand h1{font-size:var(--text-lg);line-height:1.2;white-space:nowrap}
|
|
261
|
+
.topbar #project-context{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
262
|
+
.topbar-end{margin-left:auto;display:flex;align-items:center;gap:var(--space-3);min-width:0}
|
|
263
|
+
.topbar-end .connection{white-space:nowrap}
|
|
264
|
+
.connection::before{content:"";display:inline-block;width:8px;height:8px;margin-right:var(--space-2);border-radius:var(--radius-full);background:var(--dot);vertical-align:baseline}
|
|
265
|
+
.connection[data-state="live"]{color:var(--green)}.connection[data-state="live"]::before{background:var(--dot-ok)}
|
|
266
|
+
.connection[data-state="down"]{color:var(--red)}.connection[data-state="down"]::before{background:var(--dot-err)}
|
|
267
|
+
.theme-field{display:flex;align-items:center;gap:var(--space-2)}
|
|
268
|
+
.theme-field label{margin:0}
|
|
269
|
+
.theme-field select{width:auto}
|
|
270
|
+
.icon-button{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0}
|
|
271
|
+
.attention-badge{min-height:44px}
|
|
272
|
+
.view-nav{grid-row:2;display:flex;gap:var(--space-1);padding:var(--space-2) var(--space-4);overflow-x:auto;overscroll-behavior-x:contain;background:var(--panel);border-bottom:1px solid var(--line);scrollbar-width:thin}
|
|
273
|
+
.view-nav button{display:inline-flex;align-items:center;gap:var(--space-2);flex:none;min-height:44px;border-color:transparent;background:transparent;white-space:nowrap}
|
|
274
|
+
.view-nav button:hover{background:var(--hover)}
|
|
275
|
+
.view-nav button[aria-current="page"]{background:var(--accent-soft);color:var(--accent);border-color:var(--accent);font-weight:600}
|
|
276
|
+
.view-nav .badge{margin-left:var(--space-1)}
|
|
277
|
+
.shell-body{grid-row:3;min-width:0}
|
|
278
|
+
main{max-width:1440px;margin:auto;padding:var(--space-6) var(--space-8);display:block;min-width:0}
|
|
279
|
+
main:focus{outline:none}
|
|
280
|
+
.keys-list{margin:0 0 var(--space-3);display:grid;gap:var(--space-2)}
|
|
281
|
+
.keys-list>div{display:grid;grid-template-columns:96px 1fr;gap:var(--space-3);align-items:baseline}
|
|
282
|
+
.keys-list dt{font-weight:600}.keys-list dd{margin:0}
|
|
283
|
+
kbd{display:inline-block;min-width:24px;padding:1px var(--space-2);border:1px solid var(--control-border);border-bottom-width:2px;border-radius:var(--radius-control);background:var(--control);font:var(--text-xs) var(--font-mono);text-align:center}
|
|
284
|
+
#keys-dialog{width:min(480px,calc(100% - 32px));border:1px solid var(--line);border-radius:var(--radius-surface);background:var(--raised);color:var(--ink);padding:var(--space-6);box-shadow:var(--shadow-2)}
|
|
285
|
+
#keys-dialog::backdrop{background:var(--scrim)}
|
|
286
|
+
#keys-dialog .dialog-head button{min-width:44px;padding:0}
|
|
287
|
+
@media(min-width:1024px){
|
|
288
|
+
.shell{grid-template-columns:200px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
|
|
289
|
+
.topbar{grid-column:1/-1}
|
|
290
|
+
.view-nav{grid-row:2;grid-column:1;flex-direction:column;overflow-x:visible;overflow-y:auto;padding:var(--space-3);border-bottom:0;border-right:1px solid var(--line);position:sticky;top:56px;align-self:start;height:calc(100vh - 56px)}
|
|
291
|
+
.view-nav button{width:100%;justify-content:flex-start}
|
|
292
|
+
.view-nav .badge{margin-left:auto}
|
|
293
|
+
.shell-body{grid-row:2;grid-column:2}
|
|
294
|
+
}
|
|
295
|
+
@media(max-width:1023px){.topbar{position:static;flex-wrap:wrap;gap:var(--space-1) var(--space-3);padding-block:var(--space-2)}.topbar #project-context{order:3;flex-basis:100%}.topbar-end{margin-left:0;flex-wrap:wrap;width:100%;gap:var(--space-2) var(--space-3)}.topbar-end .connection{flex:1 0 100%;white-space:normal}.topbar-end .theme-field{margin-left:auto}main{padding:var(--space-4)}.brand h1 span{display:none}}
|
|
296
|
+
@media(max-width:600px){main{padding:var(--space-3)}.topbar{padding:var(--space-2) var(--space-3)}.theme-field label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}}
|
|
297
|
+
main>.stack{margin-top:var(--space-6)}
|
|
298
|
+
.task-card.compact{cursor:pointer;min-height:44px;transition:transform .15s ease-out}
|
|
299
|
+
.task-card.compact:hover,.task-card.compact:focus-visible{border-color:var(--focus);box-shadow:var(--shadow-1);transform:translateY(-1px)}
|
|
300
|
+
.task-card.compact[draggable="true"]:active{box-shadow:var(--shadow-2);transform:translateY(-2px)}
|
|
301
|
+
.task-card.compact .card-head{display:flex;justify-content:flex-start;align-items:flex-start;gap:var(--space-2)}
|
|
302
|
+
.task-card.compact .card-title{flex:1 1 auto;min-width:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
|
|
303
|
+
.card-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin-top:var(--space-2)}
|
|
304
|
+
.release-chip{font-size:var(--text-xs);color:var(--muted);border:1px solid var(--line);border-radius:var(--radius-full);padding:0 var(--space-2);overflow-wrap:anywhere}
|
|
305
|
+
.owner-avatar{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:var(--radius-full);background:var(--peer-color);color:var(--on-peer);font-size:var(--text-xs);font-weight:700;line-height:1}
|
|
306
|
+
@media(prefers-reduced-motion:reduce){.task-card.compact{transition:none}.task-card.compact:hover,.task-card.compact:focus-visible,.task-card.compact[draggable="true"]:active{transform:none}}
|
|
307
|
+
.task-detail .actions{display:flex;flex-wrap:wrap}
|
|
308
|
+
.task-detail .actions select{width:auto;max-width:260px}
|
|
309
|
+
.task-detail button,.task-detail select{min-height:44px}
|
|
310
|
+
.board-focus{grid-template-columns:minmax(0,1fr)}
|
|
311
|
+
.column-head{display:flex;justify-content:space-between;gap:var(--space-2);align-items:center;margin-bottom:var(--space-3)}
|
|
312
|
+
.column-head h3{margin:0}
|
|
313
|
+
.column-head button{min-height:44px;font-size:var(--text-xs)}
|
|
314
|
+
/* Side sheets (#469 R3): the task detail and the Plan sheet are native dialogs pinned to the right edge. Full height; on narrow
|
|
315
|
+
screens they fill the width. They slide in on transform and fade on opacity only, and not at all under reduced motion. */
|
|
316
|
+
#task-detail-dialog,#plan-dialog{position:fixed;inset:0 0 0 auto;margin:0;width:min(520px,100%);max-width:100%;height:100vh;max-height:100vh;overflow:auto;border:0;border-left:1px solid var(--line);border-radius:0;background:var(--raised);color:var(--ink);padding:var(--space-6);box-shadow:var(--shadow-2)}
|
|
317
|
+
#plan-dialog{width:min(560px,100%)}
|
|
318
|
+
#task-detail-dialog[open],#plan-dialog[open]{animation:sheet-in .18s ease-out}
|
|
319
|
+
@keyframes sheet-in{from{transform:translateX(24px);opacity:0}}
|
|
320
|
+
@media(prefers-reduced-motion:reduce){#task-detail-dialog[open],#plan-dialog[open]{animation:none}}
|
|
321
|
+
@media(max-width:600px){#task-detail-dialog,#plan-dialog{padding:var(--space-4);border-left:0}}
|
|
322
|
+
#plan-dialog::backdrop{background:var(--scrim)}
|
|
323
|
+
.plan-block{border:1px solid var(--line);border-radius:var(--radius-surface);background:var(--panel);margin:0 0 var(--space-4);padding:var(--space-4);overflow:visible}
|
|
324
|
+
.plan-block>h3{font-size:var(--text-base);margin:0 0 var(--space-3)}
|
|
325
|
+
.plan-block form,.plan-block .content{padding:0}
|
|
326
|
+
/* A group is a plain heading and its lines: the global section{} box rule must not wrap each one in a card. */
|
|
327
|
+
.detail-group{margin:var(--space-4) 0;border:0;background:none;border-radius:0;overflow:visible}
|
|
328
|
+
.detail-heading{font-size:var(--text-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--faint);margin:0 0 var(--space-2);border-bottom:1px solid var(--line);padding-bottom:var(--space-1)}
|
|
329
|
+
#task-detail-dialog::backdrop{background:var(--scrim)}
|
|
330
|
+
.dialog-head{display:flex;justify-content:space-between;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4)}
|
|
331
|
+
.dialog-head button{min-height:44px}
|
|
332
|
+
@media(max-width:600px){.board-focus{grid-template-columns:minmax(0,1fr)}}
|
|
33
333
|
|
|
34
|
-
|
|
334
|
+
/* Quiet motion (#469 R6). Only transform and opacity move, 120 to 200 ms, ease-out. The Office keeps its own pinned motion above. */
|
|
335
|
+
button{transition:transform .12s ease-out}
|
|
336
|
+
button:active:not(:disabled){transform:translateY(1px)}
|
|
337
|
+
main section[data-view]:not([hidden]){animation:view-in .16s ease-out}
|
|
338
|
+
.task-card.is-new{animation:card-in .16s ease-out}
|
|
339
|
+
@keyframes view-in{from{opacity:0}}
|
|
340
|
+
@keyframes card-in{from{opacity:0;transform:translateY(4px)}}
|
|
341
|
+
@keyframes toast-in{from{opacity:0;transform:translateY(8px)}}
|
|
342
|
+
/* Toasts: a stack in the bottom corner, newest last. An error is dismissed by hand; a success fades. */
|
|
343
|
+
.notice{position:fixed;right:var(--space-4);bottom:var(--space-4);z-index:20;display:flex;flex-direction:column;gap:var(--space-2);width:min(420px,calc(100vw - 2 * var(--space-4)));margin:0;pointer-events:none}
|
|
344
|
+
.notice:empty{display:none}
|
|
345
|
+
.toast{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3) var(--space-4);background:var(--raised);box-shadow:var(--shadow-2);overflow-wrap:anywhere;pointer-events:auto;animation:toast-in .16s ease-out}
|
|
346
|
+
.toast.error{color:var(--red);border-color:var(--red)}
|
|
347
|
+
.toast-out{opacity:0;transition:opacity .16s ease-out}
|
|
348
|
+
.toast-text{flex:1 1 auto;min-width:0}
|
|
349
|
+
.toast-dismiss{flex:none;min-height:32px;padding:var(--space-1) var(--space-2);font-size:var(--text-xs)}
|
|
350
|
+
/* Disconnected: the connection status is already err; the content dims, stays readable, and one line says when the last snapshot came. */
|
|
351
|
+
.disconnected{margin:0 0 var(--space-4);padding:var(--space-2) var(--space-4);border:1px solid var(--red);border-radius:var(--radius-control);color:var(--red);background:var(--panel);font-size:var(--text-sm)}
|
|
352
|
+
main[data-stale]>:not(#notice):not(#disconnected){opacity:.6;transition:opacity .16s ease-out}
|
|
353
|
+
/* First load: blocks shaped like the board columns and the peer rows, until the first snapshot replaces them. They do not animate. */
|
|
354
|
+
.skeleton{display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-4)}
|
|
355
|
+
.skeleton-board{flex-direction:row;align-items:flex-start}
|
|
356
|
+
.skeleton-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--line);border-radius:var(--radius-surface);background:var(--panel-soft)}
|
|
357
|
+
.skeleton-bar{display:block;height:var(--space-4);border-radius:var(--radius-control);background:var(--line)}
|
|
358
|
+
.skeleton-tall{height:var(--space-8)}
|
|
359
|
+
@media(max-width:600px){.skeleton-board{flex-direction:column}}
|
|
35
360
|
</style>
|
|
36
361
|
</head>
|
|
37
362
|
<body>
|
|
38
|
-
|
|
363
|
+
<!-- Icons: Tabler Icons, outline set, path data copied unchanged. The MIT notice is in THIRD_PARTY_NOTICES.md. At most 20 may live in this sprite. -->
|
|
364
|
+
<svg class="sprite" width="0" height="0" aria-hidden="true" focusable="false"><defs>
|
|
365
|
+
<symbol id="i-board" viewBox="0 0 24 24"><path d="M4 4l6 0" /><path d="M14 4l6 0" /><path d="M4 10a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2l0 -8" /><path d="M14 10a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2l0 -2" /></symbol>
|
|
366
|
+
<symbol id="i-office" viewBox="0 0 24 24"><path d="M3 21l18 0" /><path d="M3 7v1a3 3 0 0 0 6 0v-1m0 1a3 3 0 0 0 6 0v-1m0 1a3 3 0 0 0 6 0v-1h-18l2 -4h14l2 4" /><path d="M5 21l0 -10.15" /><path d="M19 21l0 -10.15" /><path d="M9 21v-4a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v4" /></symbol>
|
|
367
|
+
<symbol id="i-stream" viewBox="0 0 24 24"><path d="M3 12h4l3 8l4 -16l3 8h4" /></symbol>
|
|
368
|
+
<symbol id="i-agents" viewBox="0 0 24 24"><path d="M5 7a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /><path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /></symbol>
|
|
369
|
+
<symbol id="i-message" viewBox="0 0 24 24"><path d="M8 9h8" /><path d="M8 13h6" /><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12" /></symbol>
|
|
370
|
+
<symbol id="i-memory" viewBox="0 0 24 24"><path d="M15.5 13a3.5 3.5 0 0 0 -3.5 3.5v1a3.5 3.5 0 0 0 7 0v-1.8" /><path d="M8.5 13a3.5 3.5 0 0 1 3.5 3.5v1a3.5 3.5 0 0 1 -7 0v-1.8" /><path d="M17.5 16a3.5 3.5 0 0 0 0 -7h-.5" /><path d="M19 9.3v-2.8a3.5 3.5 0 0 0 -7 0" /><path d="M6.5 16a3.5 3.5 0 0 1 0 -7h.5" /><path d="M5 9.3v-2.8a3.5 3.5 0 0 1 7 0v10" /></symbol>
|
|
371
|
+
<symbol id="i-settings" viewBox="0 0 24 24"><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></symbol>
|
|
372
|
+
<symbol id="i-help" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 17l0 .01" /><path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" /></symbol>
|
|
373
|
+
<symbol id="i-close" viewBox="0 0 24 24"><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></symbol>
|
|
374
|
+
<symbol id="i-star" viewBox="0 0 24 24"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873l-6.158 -3.245" /></symbol>
|
|
375
|
+
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" /></symbol>
|
|
376
|
+
<symbol id="i-check" viewBox="0 0 24 24"><path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /></symbol>
|
|
377
|
+
</defs></svg>
|
|
378
|
+
<a class="skip-link" href="#main-content">Skip to content</a>
|
|
379
|
+
<div class="shell">
|
|
380
|
+
<header class="topbar"><div class="brand"><div class="eyebrow">Local control room</div><h1>agent-hub <span>/ dashboard</span></h1></div><div id="project-context" class="meta"></div><div class="topbar-end"><div id="connection" class="connection" role="status">Connecting...</div><button type="button" id="attention-badge" class="attention-badge" aria-label="Global attention" hidden>(0)</button><div class="theme-field"><label for="theme">Theme</label><select id="theme" class="theme-choice"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></div><button type="button" id="keys-open" class="icon-button" aria-label="Keyboard shortcuts" aria-haspopup="dialog"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-help"/></svg></button></div></header>
|
|
39
381
|
<nav id="dashboard-nav" class="view-nav" aria-label="Dashboard views">
|
|
40
|
-
<button type="button" data-view-target="board" aria-current="page">Board</button>
|
|
41
|
-
<button type="button" data-view-target="office">Office</button>
|
|
42
|
-
<button type="button" data-view-target="stream">Stream</button>
|
|
43
|
-
<button type="button" data-view-target="agents">Agents <span id="agents-approval-count" class="badge warn" hidden></span></button>
|
|
44
|
-
<button type="button" data-view-target="message">Message</button>
|
|
45
|
-
<button type="button" data-view-target="memory">Memory</button>
|
|
46
|
-
<button type="button" data-view-target="settings">Settings</button>
|
|
47
|
-
<button type="button" id="attention-badge" class="attention-badge" aria-label="Global attention" hidden>(0)</button>
|
|
382
|
+
<button type="button" data-view-target="board" aria-current="page"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-board"/></svg><span class="nav-label">Board</span></button>
|
|
383
|
+
<button type="button" data-view-target="office"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-office"/></svg><span class="nav-label">Office</span></button>
|
|
384
|
+
<button type="button" data-view-target="stream"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-stream"/></svg><span class="nav-label">Stream</span></button>
|
|
385
|
+
<button type="button" data-view-target="agents"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-agents"/></svg><span class="nav-label">Agents</span> <span id="agents-approval-count" class="badge warn" hidden></span></button>
|
|
386
|
+
<button type="button" data-view-target="message"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-message"/></svg><span class="nav-label">Message</span></button>
|
|
387
|
+
<button type="button" data-view-target="memory"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-memory"/></svg><span class="nav-label">Memory</span></button>
|
|
388
|
+
<button type="button" data-view-target="settings"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-settings"/></svg><span class="nav-label">Settings</span></button>
|
|
48
389
|
</nav>
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<
|
|
390
|
+
<div class="shell-body">
|
|
391
|
+
<main id="main-content" tabindex="-1">
|
|
392
|
+
<p id="disconnected" class="disconnected" role="status" hidden></p>
|
|
393
|
+
<div id="notice" class="notice" role="status" aria-live="polite" aria-label="Notices"></div>
|
|
394
|
+
<section class="task-board-section" aria-labelledby="tasks-title" data-view="board"><div class="section-head"><h2 id="tasks-title">Task board</h2><div class="filters"><select id="task-filter" aria-label="Filter tasks by state"><option value="">All states</option><option value="backlog">Backlog</option><option value="proposed">To Do</option><option value="work">Work (including rework)</option><option value="in_progress">In progress</option><option value="in_review">In review</option><option value="approved">Approved</option><option value="cancelled">Cancelled</option><option value="changes_requested">Changes requested</option></select><span id="task-count" class="meta"></span></div></div><div id="task-progress" class="task-progress" aria-live="polite"></div><div class="board-toolbar"><button id="plan-open" type="button" class="primary" aria-haspopup="dialog">Plan</button><button id="board-back" type="button" hidden>Back to board</button><span id="board-focus-title" class="meta" hidden></span><label for="task-peer-filter">Agent<select id="task-peer-filter"><option value="">All agents</option></select></label><label for="task-release-filter">Release<select id="task-release-filter"><option value="">All releases</option></select></label><label for="task-lanes">View<select id="task-lanes"><option value="columns">By state</option><option value="agents">By agent</option><option value="releases">By release</option></select></label></div><div id="task-legend" class="agent-legend" role="group" aria-label="Agent legend and assignment targets"></div><div id="tasks" class="task-list"><div class="skeleton skeleton-board" aria-hidden="true"><div class="skeleton-col"><span class="skeleton-bar"></span><span class="skeleton-bar skeleton-tall"></span><span class="skeleton-bar skeleton-tall"></span></div><div class="skeleton-col"><span class="skeleton-bar"></span><span class="skeleton-bar skeleton-tall"></span></div><div class="skeleton-col"><span class="skeleton-bar"></span><span class="skeleton-bar skeleton-tall"></span></div><div class="skeleton-col"><span class="skeleton-bar"></span></div><div class="skeleton-col"><span class="skeleton-bar"></span><span class="skeleton-bar skeleton-tall"></span></div></div><p class="empty">Loading the board...</p></div></section>
|
|
52
395
|
<div class="stack">
|
|
53
396
|
<section id="office-section" aria-labelledby="office-title" data-view="office" hidden><div class="section-head"><h2 id="office-title">Office</h2><div class="filters"><label for="office-view">Office mode</label><select id="office-view"><option value="off">Off</option><option value="flat">Flat</option><option value="isometric">Isometric</option></select></div></div><div id="office" class="content"></div></section>
|
|
54
397
|
<section aria-labelledby="stream-title" data-view="stream" hidden><div class="section-head"><h2 id="stream-title">Live stream</h2><div class="filters"><div id="stream-filters" class="chips" role="group" aria-label="Filter events by kind"></div><label><input id="follow" type="checkbox" checked> Follow latest</label></div></div><div id="stream" class="stream" tabindex="0" aria-label="Recent hub events"><p class="empty">Waiting for hub activity...</p></div><p id="stream-empty" class="empty" hidden>No events match this filter.</p></section>
|
|
@@ -59,13 +402,21 @@ const dashboardTheme = (() => {
|
|
|
59
402
|
</div>
|
|
60
403
|
<div class="stack">
|
|
61
404
|
<section id="projects-panel" aria-labelledby="projects-title" data-view="agents" hidden><div class="section-head"><h2 id="projects-title">Projects</h2><span id="project-count" class="meta"></span></div><div id="projects"></div></section>
|
|
62
|
-
<section aria-labelledby="peers-title" data-view="agents" hidden><div class="section-head"><h2 id="peers-title">Peers & queues</h2><span id="peer-count" class="meta"></span></div><div id="peers"></div></section>
|
|
405
|
+
<section aria-labelledby="peers-title" data-view="agents" hidden><div class="section-head"><h2 id="peers-title">Peers & queues</h2><span id="peer-count" class="meta"></span></div><div id="peers"><div class="skeleton" aria-hidden="true"><span class="skeleton-bar skeleton-tall"></span><span class="skeleton-bar skeleton-tall"></span><span class="skeleton-bar skeleton-tall"></span></div><p class="empty">Loading peers...</p></div></section>
|
|
63
406
|
<section aria-labelledby="permissions-title" data-view="agents" hidden><div class="section-head"><h2 id="permissions-title">Pending approvals</h2><span id="permission-count" class="meta"></span></div><div id="permissions"></div></section>
|
|
64
407
|
<section aria-labelledby="budget-title" data-view="agents" hidden><div class="section-head"><h2 id="budget-title">Usage budgets</h2></div><div id="budget"></div></section>
|
|
65
408
|
<section aria-labelledby="bench-title" data-view="agents" hidden><div class="section-head"><h2 id="bench-title">Benchmarks</h2><span id="bench-count" class="meta"></span></div><div id="bench"></div></section>
|
|
66
409
|
</div>
|
|
67
410
|
</main>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<dialog id="plan-dialog" aria-labelledby="plan-title"><div class="dialog-head"><h2 id="plan-title">Plan</h2><button type="button" id="plan-close" aria-label="Close plan sheet"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-close"/></svg></button></div>
|
|
414
|
+
<section class="plan-block" aria-labelledby="plan-h-plan"><h3 id="plan-h-plan">Plan a task</h3><form id="planning"><div class="field"><label for="planning-task">Task</label><select id="planning-task"><option value="">Select task</option></select></div><div class="split"><label>Priority<select id="planning-priority"><option value="">Default</option><option value="0">P0</option><option value="1">P1</option><option value="2">P2</option><option value="3">P3</option></select></label><label>Order<input id="planning-rank" type="number" min="0" step="1"></label><label>Release<select id="planning-release"><option value="">No release</option></select></label><label>Planned start<input id="planning-start" type="datetime-local" step="any"></label><label>Due<input id="planning-due" type="datetime-local" step="any"></label></div><label><input id="planning-pinned" type="checkbox">Pin against planner changes</label><label>Reason<input id="planning-reason" maxlength="1000" required></label><div class="actions"><button id="planning-save" type="submit" data-mutation="project">Save plan</button><button id="planning-undo" type="button" data-mutation="project">Undo latest plan</button><button id="planning-reload" type="button">Reload current values</button></div><p id="planning-version" class="meta"></p></form></section><section id="split-panel" class="plan-block" aria-labelledby="plan-h-split" hidden><h3 id="plan-h-split">Split the selected backlog task</h3><form id="split-backlog"><label>Part titles, one per line<textarea id="split-titles" maxlength="2408" required></textarea></label><label><input id="split-sequential" type="checkbox">Each part waits for the previous part</label><label>Reason<input id="split-reason" maxlength="1000" required></label><p class="meta">Two to eight parts. The original task retains integration and acceptance. Unused parts can be undone from its current plan.</p><button id="split-save" type="submit" data-mutation="project">Stage parts</button></form></section><section class="plan-block" aria-labelledby="plan-h-releases"><h3 id="plan-h-releases">Releases</h3><div id="releases" class="content"></div><form id="release-new"><div class="split"><label>Name<input id="release-name" maxlength="64" required></label><label>Order<input id="release-order" type="number" min="0" value="0" step="1" required></label></div><label>Goal<input id="release-goal" maxlength="1000" required></label><button type="submit" data-mutation="project">Create release</button></form><form id="release-plan"><label>Release<select id="release-edit"><option value="">Select release</option></select></label><div class="split"><label>Planned start<input id="release-start" type="datetime-local" step="any"></label><label>Cut<input id="release-cut" type="datetime-local" step="any"></label><label>Ship<input id="release-ship" type="datetime-local" step="any"></label></div><button id="release-save" type="submit" data-mutation="project">Save dates</button><button id="release-reload" type="button">Reload current release</button><p id="release-version" class="meta"></p></form></section><section id="delivery" class="plan-block" aria-labelledby="plan-h-delivery"><h3 id="plan-h-delivery">Delivery evidence (read-only)</h3><div class="field"><label for="delivery-repo">GitHub repository</label><input id="delivery-repo" maxlength="200" placeholder="owner/repository"></div><div class="field"><label for="delivery-live">Live verification issue IDs (optional)</label><input id="delivery-live" maxlength="300" placeholder="Comma-separated issue numbers"></div><div class="actions"><button id="delivery-queue" type="button">Read merge queue</button><button id="delivery-release" type="button">Read release checklist</button></div><pre id="delivery-report" class="body" aria-live="polite"></pre></section><section class="plan-block" aria-labelledby="plan-h-propose"><h3 id="plan-h-propose">Propose a task</h3><form id="propose"><div class="field"><label for="title">Title</label><input id="title" name="title" required maxlength="300" placeholder="What should the team do?"></div><div class="field"><label for="detail">Details (optional)</label><textarea id="detail" name="detail" maxlength="8000"></textarea></div><div class="split"><div class="field"><label for="class">Class</label><select id="class" name="class"><option>implement</option><option>plan</option><option>bulk_edit</option><option>test</option><option>review</option><option>summarize</option><option>triage</option></select></div><div class="field"><label for="owner">Owner</label><select id="owner" name="owner"><option value="">Automatic</option></select></div></div><label><input id="backlog-proposal" type="checkbox"> Add to Backlog without offering</label><button class="primary" data-mutation="project" type="submit">Propose task</button></form></section>
|
|
415
|
+
</dialog>
|
|
68
416
|
<dialog id="task-detail-dialog" aria-labelledby="task-detail-title"><div class="dialog-head"><h2 id="task-detail-title">Task details</h2><button id="task-detail-close" type="button" autofocus>Close details</button></div><div id="task-detail-content"></div></dialog>
|
|
417
|
+
<dialog id="keys-dialog" aria-labelledby="keys-title"><div class="dialog-head"><h2 id="keys-title">Keyboard shortcuts</h2><button type="button" id="keys-close" aria-label="Close shortcuts"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-close"/></svg></button></div>
|
|
418
|
+
<dl class="keys-list"><div><dt><kbd>1</kbd>-<kbd>7</kbd></dt><dd>Switch view: Board, Office, Stream, Agents, Message, Memory, Settings</dd></div><div><dt><kbd>?</kbd></dt><dd>Show this list</dd></div><div><dt><kbd>Esc</kbd></dt><dd>Close the open sheet or task detail</dd></div></dl>
|
|
419
|
+
<p class="meta">Keys are ignored while you type in a field, and with Ctrl, Alt or Cmd held.</p></dialog>
|
|
69
420
|
<footer>Loopback session. Private task content stays in the terminal. Reopen with <code>ahub ui</code> when your session expires.</footer>
|
|
70
421
|
<script>
|
|
71
422
|
'use strict';
|
|
@@ -95,8 +446,28 @@ function switchView(view) {
|
|
|
95
446
|
if (!DASHBOARD_VIEWS.includes(view)) return;
|
|
96
447
|
currentView = view; dashboardCookie.set('agent-hub-view', view); applyDashboardView();
|
|
97
448
|
}
|
|
449
|
+
// Keys 1-7 switch views, ? lists the keys, Esc closes (a native <dialog> closes itself on Esc). View keys never fire while typing:
|
|
450
|
+
// not in an input, a textarea, a select or an editable node, and not with Ctrl, Alt or Cmd held.
|
|
451
|
+
const TYPING_TARGETS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
|
452
|
+
function dashboardKey(event) {
|
|
453
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
454
|
+
const target = event.target || {};
|
|
455
|
+
if (TYPING_TARGETS.includes(String(target.tagName || '').toUpperCase()) || target.isContentEditable) return false;
|
|
456
|
+
const dialog = $('keys-dialog');
|
|
457
|
+
if (event.key === '?') { if (!dialog.open) dialog.showModal?.(); event.preventDefault?.(); return true; }
|
|
458
|
+
if (dialog.open || $('task-detail-dialog').open || $('plan-dialog').open) return false;
|
|
459
|
+
const index = Number(event.key) - 1;
|
|
460
|
+
if (!Number.isInteger(index) || index < 0 || index >= DASHBOARD_VIEWS.length || event.key.length !== 1) return false;
|
|
461
|
+
switchView(DASHBOARD_VIEWS[index]); event.preventDefault?.(); return true;
|
|
462
|
+
}
|
|
98
463
|
function initializeDashboardNavigation() {
|
|
99
464
|
for (const button of document.querySelectorAll('button[data-view-target]')) button.addEventListener('click', () => switchView(button.dataset.viewTarget));
|
|
465
|
+
document.addEventListener('keydown', dashboardKey);
|
|
466
|
+
$('keys-open').addEventListener('click', () => { if (!$('keys-dialog').open) $('keys-dialog').showModal?.(); });
|
|
467
|
+
$('keys-close').addEventListener('click', () => $('keys-dialog').close?.());
|
|
468
|
+
// The Plan sheet is a native <dialog>: Esc closes it, and the forms inside keep their ids and handlers.
|
|
469
|
+
$('plan-open').addEventListener('click', () => { if (!$('plan-dialog').open) $('plan-dialog').showModal?.(); });
|
|
470
|
+
$('plan-close').addEventListener('click', () => $('plan-dialog').close?.());
|
|
100
471
|
$('attention-badge').addEventListener('click', reachAttention);
|
|
101
472
|
$('board-back').addEventListener('click', () => setColumnFocus(null));
|
|
102
473
|
$('task-detail-close').addEventListener('click', closeTaskDetail);
|
|
@@ -109,6 +480,14 @@ history.replaceState(null, '', location.pathname);
|
|
|
109
480
|
const $ = (id) => document.getElementById(id);
|
|
110
481
|
const el = (tag, text, cls) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = String(text); if (cls) node.className = cls; return node; };
|
|
111
482
|
let stopped = false, cursor = 0, timer, projectsTimer, eventCount = 0;
|
|
483
|
+
const setConnection = (text, state) => { const node = $('connection'); node.textContent = text; node.dataset.state = state; };
|
|
484
|
+
// Disconnected (#469 R6): the content dims (opacity only) but stays readable, and one line says when the last snapshot arrived.
|
|
485
|
+
let lastSnapshotAt = 0;
|
|
486
|
+
function setDisconnected(down) {
|
|
487
|
+
const main = $('main-content'), line = $('disconnected');
|
|
488
|
+
if (down) { main.dataset.stale = 'true'; line.hidden = false; setText(line, (lastSnapshotAt ? 'Last snapshot at ' + new Date(lastSnapshotAt).toLocaleTimeString() : 'No snapshot received yet') + '. Polling continues every second.'); }
|
|
489
|
+
else { delete main.dataset.stale; line.hidden = true; setText(line, ''); }
|
|
490
|
+
}
|
|
112
491
|
let managerMode = false, projectList = [], selectedProjectId = '', selectedInstanceId = '', snapshotValid = false, viewGeneration = 0, projectRequest = 0;
|
|
113
492
|
let lastSnapshot = null;
|
|
114
493
|
let officePrev = null;
|
|
@@ -133,8 +512,21 @@ const taskBadge = { proposed: '', in_progress: 'warn', in_review: 'info', approv
|
|
|
133
512
|
const peerDot = { idle: 'ok', busy: 'warn', paused: 'warn', offline: '' };
|
|
134
513
|
const projectBadge = { running: 'ok', starting: 'warn', stopping: 'warn', stopped: '', unavailable: 'err', incompatible: 'err', missing: 'err' };
|
|
135
514
|
function refreshRelativeTimes(root) { for (const node of root.querySelectorAll('[data-ts]')) node.textContent = rel(Number(node.dataset.ts)); }
|
|
136
|
-
|
|
137
|
-
|
|
515
|
+
// Toasts (#469 R6): #notice is a stack of at most four. An empty text clears it. The same text again replaces its toast. An error stays
|
|
516
|
+
// until it is dismissed; a success fades out after 4 s (opacity only) and stays when the system asks for reduced motion.
|
|
517
|
+
function notice(text, error = false) {
|
|
518
|
+
const TOAST_MAX = 4, TOAST_MS = 4000, TOAST_FADE_MS = 160, stack = $('notice');
|
|
519
|
+
if (!text) { stack.replaceChildren(); return; }
|
|
520
|
+
const gone = (node) => node.remove(); // only the toast that leaves: a rebuild would detach a focused Dismiss button of one that stays (#296)
|
|
521
|
+
const toast = el('div', undefined, 'toast' + (error ? ' error' : '')), body = el('span', text, 'toast-text'), dismiss = el('button', 'Dismiss', 'toast-dismiss');
|
|
522
|
+
dismiss.type = 'button'; dismiss.setAttribute('aria-label', 'Dismiss notice: ' + String(text).slice(0, 60)); dismiss.addEventListener('click', () => gone(toast));
|
|
523
|
+
toast.append(body, dismiss); toast.dataset.key = (error ? 'error:' : 'ok:') + text;
|
|
524
|
+
for (const child of [...stack.children]) if (child.dataset.key === toast.dataset.key) child.remove(); // the same text replaces its own toast
|
|
525
|
+
while (stack.children.length > TOAST_MAX - 1) stack.children[0].remove(); // over four: the oldest goes, nothing is rebuilt
|
|
526
|
+
stack.append(toast);
|
|
527
|
+
if (!error && !(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true)) setTimeout(() => { toast.className += ' toast-out'; setTimeout(() => gone(toast), TOAST_FADE_MS); }, TOAST_MS);
|
|
528
|
+
}
|
|
529
|
+
function expire() { stopped = true; clearTimeout(timer); clearTimeout(projectsTimer); setConnection('Session closed', 'down'); notice('Session missing or expired. Run ahub ui in your terminal to reopen the dashboard.', true); document.querySelectorAll('button').forEach((button) => { button.disabled = true; }); }
|
|
138
530
|
async function post(path, body) {
|
|
139
531
|
const response = await fetch(path, {method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
|
|
140
532
|
if (response.status === 401) { expire(); throw new Error('Run ahub ui to start a new session.'); }
|
|
@@ -181,7 +573,7 @@ async function action(payload, button, managerAction = false) {
|
|
|
181
573
|
finally { if (button) { pendingButtons.delete(button); if (!stopped) button.disabled = Boolean(key && pendingStarts.has(key)) || (!managerAction && managerMode && !snapshotValid); } if (key) redrawStartControls(); }
|
|
182
574
|
}
|
|
183
575
|
function button(text, payload, cls, managerAction = false) { const node = el('button', text, cls); node.type = 'button'; node.dataset.mutation = managerAction ? 'manager' : 'project'; if (payload.action === 'start_peer') node.dataset.startKey = startKey(payload.peer); node.disabled = Boolean(node.dataset.startKey && pendingStarts.has(node.dataset.startKey)) || (!managerAction && managerMode && !snapshotValid); node.addEventListener('click', () => void action(payload, node, managerAction)); return node; }
|
|
184
|
-
function update(id, data, render) { const signature = JSON.stringify(data); if (signatures.get(id) === signature) return; signatures.set(id, signature); const target = $(id); target.replaceChildren(); render(target); }
|
|
576
|
+
function update(id, data, render) { const signature = JSON.stringify(data); if (signatures.get(id) === signature) return; signatures.set(id, signature); const target = $(id); update.keep = {node: (typeof document === 'undefined' ? null : document.activeElement) || null, to: null}; target.replaceChildren(); render(target); const kept = update.keep; update.keep = null; const back = kept.to || kept.node; if (back && back.isConnected && typeof document !== 'undefined' && document.activeElement !== back) back.focus({preventScroll: true}); }
|
|
185
577
|
function empty(target, text) { target.append(el('p', text, 'empty')); }
|
|
186
578
|
function count(value) { return Array.isArray(value) ? value.length : value && typeof value === 'object' ? Object.keys(value).length : value; }
|
|
187
579
|
function taskCount(value) { return Array.isArray(value) ? value.length : value && typeof value === 'object' ? Object.values(value).reduce((total, item) => total + (typeof item === 'number' ? item : 0), 0) : value; }
|
|
@@ -199,6 +591,9 @@ function restoreDraft(choices = false) {
|
|
|
199
591
|
} else { $('message').value = draft.message || ''; $('title').value = draft.title || ''; $('detail').value = draft.detail || ''; $('class').value = draft.class || 'implement'; }
|
|
200
592
|
}
|
|
201
593
|
function syncMutationControls() { document.querySelectorAll('button[data-mutation="project"]').forEach((b) => { b.disabled = pendingButtons.has(b) || Boolean(b.dataset.startKey && pendingStarts.has(b.dataset.startKey)) || (managerMode && !snapshotValid); }); }
|
|
594
|
+
// The first-load blocks the page ships in #tasks and #peers (#296): a project switch puts copies of them back and update() replaces them
|
|
595
|
+
// when that project's first snapshot renders, so a slow or failed first /snapshot reads as loading, not as an empty project.
|
|
596
|
+
const firstLoad = Object.fromEntries(['tasks', 'peers'].map((name) => [name, [...$(name).childNodes].map((node) => node.cloneNode(true))]));
|
|
202
597
|
function resetView(id, instance) {
|
|
203
598
|
saveDraft();
|
|
204
599
|
selectedProjectId = id || ''; selectedInstanceId = instance || ''; snapshotValid = false; cursor = 0; eventCount = 0; viewGeneration++;
|
|
@@ -206,7 +601,7 @@ function resetView(id, instance) {
|
|
|
206
601
|
officePrev = null; officeDesks.clear(); officeCards.clear(); officeScene = null;
|
|
207
602
|
$('office').replaceChildren(); $('office-section').hidden = true;
|
|
208
603
|
closeTaskDetail(); renderNeedsAttention(null); applyDashboardView();
|
|
209
|
-
signatures.clear(); $('stream').replaceChildren(); empty($('stream'), managerMode ? 'Select a running project...' : '
|
|
604
|
+
signatures.clear(); if (typeof keyedCaches !== 'undefined') keyedCaches.clear(); $('stream').replaceChildren(); empty($('stream'), managerMode ? 'Select a running project...' : 'No events yet. Messages, task changes and peer states appear here. Send one with ahub say <text>.');
|
|
210
605
|
if (streamFilter) { streamFilter = ''; for (const child of $('stream-filters').children) child.setAttribute('aria-pressed', 'false'); }
|
|
211
606
|
$('stream-empty').hidden = true; $('task-filter').value = '';
|
|
212
607
|
if(typeof loadedPlanningDates!=='undefined')loadedPlanningDates.clear();
|
|
@@ -215,7 +610,13 @@ function resetView(id, instance) {
|
|
|
215
610
|
$('split-titles').value='';$('split-reason').value='';$('split-sequential').checked=false;
|
|
216
611
|
$('release-name').value='';$('release-goal').value='';$('release-order').value='0';
|
|
217
612
|
$('delivery-report').textContent = ''; $('delivery-repo').value = ''; $('delivery-live').value = '';
|
|
218
|
-
|
|
613
|
+
// A project with a running instance is polled next, so the board and the peers show loading; "No project selected" is for no project.
|
|
614
|
+
const loading = Boolean(id && instance) && typeof firstLoad !== 'undefined';
|
|
615
|
+
for (const [id, text] of [['memory-list','No project selected.'],['memory-graph',''],['memory-detail',''],['peers','No project selected.'],['tasks','No project selected.'],['task-progress','No project selected.'],['permissions','No project selected.'],['budget','No project selected.'],['settings','No project selected.']]) {
|
|
616
|
+
$(id).replaceChildren();
|
|
617
|
+
if (loading && firstLoad[id]?.length) $(id).replaceChildren(...firstLoad[id].map((node) => node.cloneNode(true)));
|
|
618
|
+
else if (text) empty($(id), text);
|
|
619
|
+
}
|
|
219
620
|
$('settings-session').textContent = '';
|
|
220
621
|
$('peer-count').textContent = ''; $('task-count').textContent = ''; $('permission-count').textContent = ''; $('project-context').textContent = '';
|
|
221
622
|
notice('');
|
|
@@ -260,7 +661,7 @@ async function refreshProjects() {
|
|
|
260
661
|
const selected = projectList.find((p) => p && p.id === selectedProjectId), nextInstance = selected && typeof selected.instanceId === 'string' ? selected.instanceId : '';
|
|
261
662
|
if (!selected || selected.state !== 'running' || nextInstance !== selectedInstanceId) resetView(selected?.id || '', nextInstance);
|
|
262
663
|
renderProjects();
|
|
263
|
-
} catch (error) { if (!stopped)
|
|
664
|
+
} catch (error) { if (!stopped) setConnection('Projects unavailable: ' + error.message, 'down'); }
|
|
264
665
|
if (!stopped) projectsTimer = setTimeout(refreshProjects, 3000);
|
|
265
666
|
}
|
|
266
667
|
function peerOptions(select, ids, first) { const selected = select.value; select.replaceChildren(); const initial = el('option', first); initial.value = ''; select.append(initial); for (const id of ids) { const option = el('option', id); option.value = id; select.append(option); } if (ids.includes(selected)) select.value = selected; }
|
|
@@ -288,7 +689,7 @@ function renderApprovals(permissions) {
|
|
|
288
689
|
const terminalWaiting = permissions.filter((permission) => adapterDisplay[permission.peer]?.terminalApproval).length;
|
|
289
690
|
$('permission-count').textContent = permissions.length + ' waiting' + (terminalWaiting ? ', ' + terminalWaiting + ' Pi/local: answer in the console or ahub tail / ahub permit' : '');
|
|
290
691
|
update('permissions', permissions, (target) => {
|
|
291
|
-
if (!permissions.length) return empty(target, 'No approvals waiting.');
|
|
692
|
+
if (!permissions.length) return empty(target, 'No approvals waiting. A peer that asks for a permission appears here; answer with ahub permit.');
|
|
292
693
|
for (const permission of permissions) {
|
|
293
694
|
const row = el('div', undefined, 'row'); row.dataset.permissionId=permission.id;row.tabIndex=0;row.append(el('div', permission.peer, 'label'), el('p', permission.title, 'body'));
|
|
294
695
|
if (permission.terminalOnly) row.append(el('p', 'Review in the console or ahub tail; answer with ahub permit. You can deny here.', 'meta'));
|
|
@@ -322,12 +723,17 @@ function settingRow(row, raise) {
|
|
|
322
723
|
const title = el('label', row.label, 'label'); title.htmlFor = id;
|
|
323
724
|
const fileValue = row.fileValue !== undefined ? row.fileValue : row.pending !== undefined ? row.pending : row.value;
|
|
324
725
|
head.append(title, badge(settingText(row.value), row.group === 'Permissions' && row.value !== null && row.value !== 'ask' ? 'warn' : ''));
|
|
726
|
+
if (row.pending !== undefined && ['hub start', 'peer start'].includes(row.applies)) head.append(badge('restart pending', 'warn'));
|
|
325
727
|
node.append(head, el('p', row.key + ' / from ' + row.source + ' / ' + (row.applies === 'live' ? 'applies at once' : row.applies === 'next use' ? 'applies at next use' : row.applies === 'restart' ? 'applies after restart' : row.applies === 'peer start' ? "applies at the peer's next start" : 'read at hub start') + ' / written to ' + row.file, 'meta'));
|
|
326
728
|
node.append(el('p', 'Running: ' + settingText(row.value) + ' / File: ' + settingText(fileValue), 'meta'));
|
|
327
729
|
if (['live', 'next use', 'restart'].includes(row.applies)) node.append(el('p', 'Apply: ' + row.applies + (row.restartReason ? ' / ' + row.restartReason : ''), 'meta'));
|
|
328
730
|
if (row.pending !== undefined) node.append(el('p', ['hub start', 'peer start'].includes(row.applies) ? 'Pending restart: ' + settingText(row.pending) + ' at the next ' + (row.applies === 'peer start' ? 'peer' : 'hub') + ' start.' : 'Pending ' + row.applies + ': ' + settingText(row.pending), 'meta'));
|
|
329
731
|
if (row.hint) node.append(el('p', 'Bounds: ' + row.hint, 'meta'));
|
|
330
732
|
if (row.note) node.append(el('p', row.note, 'meta'));
|
|
733
|
+
if (row.locked) {
|
|
734
|
+
if (row.note !== row.locked) node.append(el('p', row.locked, 'meta'));
|
|
735
|
+
const locked = el('button', 'Locked'); locked.disabled = true; locked.title = row.locked; node.append(locked); return node;
|
|
736
|
+
}
|
|
331
737
|
if (row.risk === 'raises' && !raise) node.append(el('p', 'Raising this needs a settings session: run ahub ui --settings.' + (row.floor === undefined ? '' : ' Setting it to ' + row.floor + ' works here.'), 'meta'));
|
|
332
738
|
const actions = el('div', undefined, 'actions'), stored = row.file !== 'this hub run';
|
|
333
739
|
let read, confirm, force;
|
|
@@ -395,7 +801,7 @@ function renderSettings(settings, roster = []) {
|
|
|
395
801
|
// are rebuilt only when their own data changed, so a value being typed in one row survives a save of another.
|
|
396
802
|
update('settings', settings.rows.map((row) => row.key), (target) => {
|
|
397
803
|
settingSlots.clear();
|
|
398
|
-
const reload = el('div'); settingSlots.set('$reload', { node: reload, signature: null }); target.append(reload);
|
|
804
|
+
const reload = el('div', undefined, 'settings-reload'); settingSlots.set('$reload', { node: reload, signature: null }); target.append(reload);
|
|
399
805
|
const undo = el('div'); settingSlots.set('', { node: undo, signature: null }); target.append(undo);
|
|
400
806
|
for (const group of SETTING_GROUPS) {
|
|
401
807
|
const rows = settings.rows.filter((row) => row.group === group);
|
|
@@ -414,7 +820,7 @@ function renderSettings(settings, roster = []) {
|
|
|
414
820
|
if (reloadSupported) {
|
|
415
821
|
reloadSlot.node.append(button('Reload configuration', {action:'setting_reload'}));
|
|
416
822
|
if (settings.pendingRestart.length) {
|
|
417
|
-
|
|
823
|
+
const pendingHead = el('p', 'Pending restart', 'label'); pendingHead.append(el('span', ' '), badge(String(settings.pendingRestart.length), 'warn')); reloadSlot.node.append(pendingHead);
|
|
418
824
|
for (const pending of settings.pendingRestart) reloadSlot.node.append(el('p', pending.key + ': ' + pending.reason, 'meta'));
|
|
419
825
|
}
|
|
420
826
|
if (Array.isArray(settings.retainedConfig) && settings.retainedConfig.length) {
|
|
@@ -470,6 +876,26 @@ function renderStarts(target, starts) {
|
|
|
470
876
|
}
|
|
471
877
|
}
|
|
472
878
|
let boardViewState = null, showAllDone = false;
|
|
879
|
+
// Keyed rendering (#469 R6). A pass hands out one node per key: the cached node when its signature is unchanged (so it keeps its
|
|
880
|
+
// DOM node, an open <details>, hover and, with update(), focus), a fresh node when it changed or is new, and end() drops the keys
|
|
881
|
+
// that are gone. A changed item is replaced by a fresh node, not patched: its listeners close over its data. The focused node of
|
|
882
|
+
// update() is carried to the fresh one. With enter, an item that is new after the first pass, or has moved to another group (a
|
|
883
|
+
// column), fades in (class is-new, removed on animationend). A card that disappears is removed at once: there is no leave animation.
|
|
884
|
+
const keyedCaches = new Map();
|
|
885
|
+
function keyedStart(name, enter = false) {
|
|
886
|
+
let cache = keyedCaches.get(name); if (!cache) keyedCaches.set(name, cache = {nodes: new Map(), primed: false});
|
|
887
|
+
const used = new Set();
|
|
888
|
+
const get = (key, signature, build, group = '') => {
|
|
889
|
+
used.add(key); const hit = cache.nodes.get(key);
|
|
890
|
+
if (hit && hit.signature === signature && hit.group === group) return hit.node;
|
|
891
|
+
const node = build(), keep = update.keep;
|
|
892
|
+
if (hit && keep && keep.node && (hit.node === keep.node)) keep.to = node;
|
|
893
|
+
if (enter && cache.primed && (!hit || hit.group !== group) && node.classList?.add) { node.classList.add('is-new'); node.addEventListener('animationend', () => node.classList.remove('is-new'), {once: true}); }
|
|
894
|
+
cache.nodes.set(key, {signature, node, group}); return node;
|
|
895
|
+
};
|
|
896
|
+
get.end = () => { for (const key of [...cache.nodes.keys()]) if (!used.has(key)) cache.nodes.delete(key); cache.primed = true; };
|
|
897
|
+
return get;
|
|
898
|
+
}
|
|
473
899
|
const BOARD_FOCUS_COLUMNS = ['backlog','todo','progress','review','done'];
|
|
474
900
|
let columnFocus, taskDetailId = null;
|
|
475
901
|
let planningEdit = null, releaseEdit = null;
|
|
@@ -545,6 +971,30 @@ function peerMark(peer, reviewer = false) {
|
|
|
545
971
|
const mark = el('span', undefined, 'peer-mark peer-' + key), color = el('span', undefined, 'peer-color-dot' + (reviewer ? ' reviewer' : ''));
|
|
546
972
|
color.setAttribute('aria-hidden', 'true'); mark.append(color, el('span', (reviewer ? 'reviewer: ' : 'owner: ') + (peer || 'unassigned'))); return mark;
|
|
547
973
|
}
|
|
974
|
+
// A sprite icon in the page (#469 R4). Decorative unless a label is given; the text beside it carries the meaning otherwise.
|
|
975
|
+
const icon = (id, label) => {
|
|
976
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
977
|
+
svg.setAttribute('class', 'icon'); svg.setAttribute('focusable', 'false'); use.setAttribute('href', '#' + id); svg.append(use);
|
|
978
|
+
if (label) { svg.setAttribute('role', 'img'); svg.setAttribute('aria-label', label); } else svg.setAttribute('aria-hidden', 'true');
|
|
979
|
+
return svg;
|
|
980
|
+
};
|
|
981
|
+
// #469 R3: the owner as a small round avatar in the peer colour with its initial. The text name stays reachable
|
|
982
|
+
// (aria-label and title), so the colour is never the only signal. A task with no owner shows a neutral avatar.
|
|
983
|
+
function ownerAvatar(peer) {
|
|
984
|
+
const key = Object.hasOwn(peerPalette, peer || '') ? peer : 'other', name = peer || 'unassigned';
|
|
985
|
+
const avatar = el('span', String(name).slice(0, 1).toUpperCase(), 'owner-avatar peer-' + key);
|
|
986
|
+
avatar.setAttribute('role', 'img'); avatar.setAttribute('aria-label', 'owner: ' + name); avatar.title = 'owner: ' + name;
|
|
987
|
+
return avatar;
|
|
988
|
+
}
|
|
989
|
+
// One badge per card: the state, with the priority folded into its text ("in review P1"). Overdue and a running check fold into the
|
|
990
|
+
// same text (#293: "in progress P2, overdue, check running"), never a second badge. The tones match taskBadge (declared above the
|
|
991
|
+
// board code, so a board-only slice cannot see it); a state with no tone gets the neutral badge. A state that is already err keeps
|
|
992
|
+
// it; otherwise overdue makes the badge warn, and a running check makes it info unless the state already has a tone.
|
|
993
|
+
const CARD_BADGE_TONE = { in_progress: 'warn', in_review: 'info', approved: 'ok', changes_requested: 'err' };
|
|
994
|
+
function cardBadge(task, card, overdue = false) {
|
|
995
|
+
const own = CARD_BADGE_TONE[task.state] || '', tone = own === 'err' ? own : overdue ? 'warn' : own || (task.checking ? 'info' : '');
|
|
996
|
+
return badge(String(task.state).replaceAll('_', ' ') + ' P' + card.priority + (overdue ? ', overdue' : '') + (task.checking ? ', check running' : ''), tone);
|
|
997
|
+
}
|
|
548
998
|
function boardContext() {
|
|
549
999
|
const current=context(), snapshot=boardViewState?.snapshot || {};
|
|
550
1000
|
return {...current,origin:location.origin,projectId:typeof snapshot.projectId==='string' ? snapshot.projectId : current.projectId,instanceId:typeof snapshot.instanceId==='string' ? snapshot.instanceId : current.instanceId};
|
|
@@ -582,24 +1032,32 @@ function taskDetail(task, card, stage, ids, overdue) {
|
|
|
582
1032
|
const row = el('article', undefined, 'task-detail'), head = el('div', undefined, 'row-head');
|
|
583
1033
|
row.setAttribute('aria-label', '#' + task.id + ' ' + task.title + ', ' + task.state.replaceAll('_', ' ') + ', owner ' + (task.owner || 'unassigned') + ', priority P' + card.priority);
|
|
584
1034
|
head.append(el('span', '#' + task.id + ' ' + task.title, 'label'), badge('P' + card.priority));
|
|
585
|
-
if (card.starred) { const star = el('span', '
|
|
586
|
-
row.append(head
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
592
|
-
if (task.
|
|
593
|
-
if (task.
|
|
594
|
-
if (task.lifecycle)
|
|
595
|
-
if
|
|
1035
|
+
if (card.starred) { const star = el('span', undefined, 'star'); star.title = 'starred'; star.append(icon('i-star', 'starred')); head.append(star); }
|
|
1036
|
+
row.append(head);
|
|
1037
|
+
const status = el('section', undefined, 'detail-group'); status.append(el('h4', 'Status', 'detail-heading'));
|
|
1038
|
+
status.append(el('p', task.profileClass ? task.profileClass + ' (base ' + task.class + ')' : task.class, 'meta'));
|
|
1039
|
+
if (stage) status.append(renderTaskStage(stage));
|
|
1040
|
+
if (card.waiting.length) status.append(el('p', 'Waiting: ' + card.waiting.join('; '), 'meta'));
|
|
1041
|
+
if (task.reworkRound) status.append(el('p', 'Rework round ' + task.reworkRound, 'meta'));
|
|
1042
|
+
if (task.checking) status.append(el('p', 'Check running', 'meta'));
|
|
1043
|
+
if (task.lifecycle) status.append(el('p', 'SDLC ' + task.lifecycle.mode + ' / ' + task.lifecycle.stage + (task.lifecycle.playbook ? ' (' + task.lifecycle.playbook + ')' : '') + ': ' + task.lifecycle.next + ' / next: ' + task.lifecycle.actor, 'meta'));
|
|
1044
|
+
if (task.lifecycle?.definition) status.append(el('p', 'Stage base: ' + task.lifecycle.definition.base + ' / entry: ' + (task.lifecycle.entry?.length ? task.lifecycle.entry.join('; ') : 'ready') + ' / exit: ' + (task.lifecycle.exit?.length ? task.lifecycle.exit.join('; ') : 'ready'), 'meta'));
|
|
1045
|
+
if(overdue)status.append(badge('Overdue','warn'));
|
|
1046
|
+
row.append(status);
|
|
1047
|
+
const people = el('section', undefined, 'detail-group'); people.append(el('h4', 'People', 'detail-heading'));
|
|
1048
|
+
people.append(peerMark(task.owner)); if (task.reviewer) people.append(peerMark(task.reviewer, true));
|
|
1049
|
+
if(task.role){const guidance='Task guidance: '+task.role+' (grants no authority)';people.append(el('p',guidance,'meta'));row.setAttribute('aria-label',row.getAttribute('aria-label')+', '+guidance);}
|
|
1050
|
+
row.append(people);
|
|
596
1051
|
if (task.detail) row.append(el('p', task.detail, 'body'));
|
|
597
|
-
|
|
598
|
-
if(task.
|
|
599
|
-
if(task.
|
|
600
|
-
if(
|
|
601
|
-
|
|
602
|
-
|
|
1052
|
+
const dates = el('section', undefined, 'detail-group'); dates.append(el('h4', 'Dates', 'detail-heading'));
|
|
1053
|
+
if(task.plannedStart != null || task.due != null)dates.append(el('p','Planned '+dateText(task.plannedStart)+' / due '+dateText(task.due),'meta'));
|
|
1054
|
+
if(task.actualDates)dates.append(el('p','Actual start '+dateText(task.actualDates.startedAt)+' / review '+dateText(task.actualDates.reviewAt)+' / done '+dateText(task.actualDates.doneAt),'meta'));
|
|
1055
|
+
if (dates.children.length > 1) row.append(dates);
|
|
1056
|
+
const links = el('section', undefined, 'detail-group'); links.append(el('h4', 'Links', 'detail-heading'));
|
|
1057
|
+
if(task.release!=null){const release=boardViewState?.snapshot.releases?.find(release=>release.id===task.release),label='Release #'+task.release+' '+(release?.name || '(label unavailable)');links.append(el('p',label,'meta'));row.setAttribute('aria-label',row.getAttribute('aria-label')+', '+label);}
|
|
1058
|
+
if(task.external?.provider==='github'){const source=el('a','GitHub #'+task.external.issue);source.href=task.external.url;source.rel='noreferrer';source.target='_blank';links.append(source);}
|
|
1059
|
+
if(task.pinned)links.append(el('p','Pinned','meta'));
|
|
1060
|
+
if (links.children.length > 1) row.append(links);
|
|
603
1061
|
const controls = el('div', undefined, 'actions');
|
|
604
1062
|
if (card.pull && boardViewState?.snapshot.boardCapabilities?.backlog===true) controls.append(boardButton('Pull into To Do', {action:'backlog_pull',id:task.id}));
|
|
605
1063
|
if (card.pull && boardViewState?.snapshot.planningCapabilities?.wipPull===true) controls.append(boardButton('Pull within WIP', {action:'planner_pull',id:task.id,revision:task.planningRevision ?? 0,reason:'person requested a WIP-constrained pull'}));
|
|
@@ -622,15 +1080,16 @@ function taskCard(task, card, stage, ids, overdue, compact = true) {
|
|
|
622
1080
|
if (compact) {
|
|
623
1081
|
row.setAttribute('role', 'button');
|
|
624
1082
|
row.setAttribute('aria-label', '#' + task.id + ' ' + task.title + ', ' + task.state.replaceAll('_', ' ') + ', owner ' + (task.owner || 'unassigned') + ', priority P' + card.priority);
|
|
1083
|
+
if (overdue) row.setAttribute('aria-label', row.getAttribute('aria-label') + ', overdue');
|
|
1084
|
+
if (task.checking) row.setAttribute('aria-label', row.getAttribute('aria-label') + ', check running');
|
|
625
1085
|
if (task.role) row.setAttribute('aria-label', row.getAttribute('aria-label') + ', Task guidance: ' + task.role + ' (grants no authority)');
|
|
626
1086
|
if (task.release != null) { const release=boardViewState?.snapshot.releases?.find(release=>release.id===task.release);row.setAttribute('aria-label',row.getAttribute('aria-label')+', Release #'+task.release+' '+(release?.name || '(label unavailable)')); }
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (task.checking) row.append(badge('Check running','info'));
|
|
1087
|
+
// Title (two lines at most, clamped in CSS), the owner avatar, one state/priority badge and the release. Everything else is in the detail.
|
|
1088
|
+
const head = el('div', undefined, 'row-head card-head'); head.append(ownerAvatar(task.owner), el('span', '#' + task.id + ' ' + task.title, 'label card-title'));
|
|
1089
|
+
if (card.starred) { const star = el('span', undefined, 'star'); star.title = 'starred'; star.append(icon('i-star', 'starred')); head.append(star); }
|
|
1090
|
+
const meta = el('div', undefined, 'card-meta'); meta.append(cardBadge(task, card, overdue));
|
|
1091
|
+
if (task.release != null) { const release=boardViewState?.snapshot.releases?.find(release=>release.id===task.release); meta.append(el('span', 'Release #' + task.release + (release?.name ? ' ' + release.name : ''), 'release-chip')); }
|
|
1092
|
+
row.append(head, meta);
|
|
634
1093
|
row.addEventListener('click', () => openTaskDetail(task.id));
|
|
635
1094
|
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(task.id); } });
|
|
636
1095
|
}
|
|
@@ -703,7 +1162,8 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
703
1162
|
if (!ids.length) empty(target,'No peer status available.');
|
|
704
1163
|
});
|
|
705
1164
|
update('tasks',[snapshot.tasks,peers,snapshot.boardCapabilities,taskFilter,ownerFilter,releaseFilter,lanes,showAllDone,[...overdue],columnFocus],target=>{
|
|
706
|
-
taskRows.clear();
|
|
1165
|
+
taskRows.clear(); const keyed=keyedStart('cards',true);
|
|
1166
|
+
if(!snapshot.tasks.length)target.append(el('p','No tasks yet. Plan one with the Plan button, or run ahub task propose <title>.','empty'));
|
|
707
1167
|
const visible=snapshot.tasks.filter(task=>(!taskFilter || (taskFilter==='work' ? ['in_progress','changes_requested'].includes(task.state) : task.state===taskFilter)) && (!ownerFilter || task.owner===ownerFilter || task.reviewer===ownerFilter) && (!releaseFilter || task.release===Number(releaseFilter)));
|
|
708
1168
|
const visibleIds=new Set(visible.map(task=>task.id));
|
|
709
1169
|
const owners=columnFocus ? [null] : lanes==='agents' ? [...new Set(visible.map(task=>task.owner || ''))] : lanes==='releases' ? [...new Set(visible.map(task=>task.release ?? 0))] : [null];
|
|
@@ -730,7 +1190,7 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
730
1190
|
if(hidden) ready=ready.slice(0,10);
|
|
731
1191
|
for(const [label,list] of [['Ready',ready],['Waiting',waiting]]) {
|
|
732
1192
|
if(column.id==='todo') box.append(el('h4',label+' ('+list.length+')'));
|
|
733
|
-
for(const card of list)
|
|
1193
|
+
for(const card of list){const task=byId.get(card.id),compact=columnFocus === null,release=snapshot.releases?.find(release=>release.id===task.release)?.name,node=keyed(card.id,JSON.stringify([task,card,stages.get(card.id),card.assignable?eligible:null,overdue.has(card.id),compact,release,boardContext(),snapshot.boardCapabilities,snapshot.planningCapabilities]),()=>taskCard(task,card,stages.get(card.id),eligible,overdue.has(card.id),compact),column.id);taskRows.set(card.id,node);box.append(node)}
|
|
734
1194
|
}
|
|
735
1195
|
if(!ready.length && !waiting.length) empty(box,'No tasks');
|
|
736
1196
|
if(hidden){const more=el('button','Show '+hidden+' older Done tasks');more.type='button';more.addEventListener('click',()=>{if(!boardViewState)return;showAllDone=true;renderBoard(boardViewState.snapshot,boardViewState.peers,boardViewState.ids)});box.append(more)}
|
|
@@ -738,7 +1198,7 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
738
1198
|
}
|
|
739
1199
|
lane.append(grid);target.append(lane);
|
|
740
1200
|
}
|
|
741
|
-
const unknown=model.unknown.filter(id=>visibleIds.has(id));if(unknown.length) target.append(el('p','Unsupported task states: '+unknown.map(id=>'#'+id).join(', '),'board-unknown'));
|
|
1201
|
+
keyed.end();const unknown=model.unknown.filter(id=>visibleIds.has(id));if(unknown.length) target.append(el('p','Unsupported task states: '+unknown.map(id=>'#'+id).join(', '),'board-unknown'));
|
|
742
1202
|
});
|
|
743
1203
|
refreshTaskDetail();
|
|
744
1204
|
}
|
|
@@ -775,34 +1235,45 @@ function render(snapshot) {
|
|
|
775
1235
|
syncStarts(snapshot);
|
|
776
1236
|
update('peers', [peers, starts, starts.map(start => { const pending = pendingStarts.get(startKey(start.peer)); return pending ? [pending.phase, pending.retryAt || null] : null; })], (target) => {
|
|
777
1237
|
peerRows.clear();
|
|
778
|
-
if (!ids.length) { empty(target, 'No peers connected. Start one below, or
|
|
1238
|
+
if (!ids.length) { empty(target, 'No peers connected. Start one below, or run ahub claude or ahub codex in a terminal.'); return renderStarts(target, starts); }
|
|
1239
|
+
const keyed = keyedStart('peers');
|
|
779
1240
|
for (const [id, peer] of Object.entries(peers)) {
|
|
1241
|
+
const row = keyed(id, JSON.stringify([peer, (snapshot.boardLegend || []).find(item => item.peer === id)?.activeTasks]), () => {
|
|
780
1242
|
const row = el('div', undefined, 'row'), head = el('div', undefined, 'row-head'), name = el('div', undefined, 'peername');
|
|
781
1243
|
name.append(dot(peerDot[peer.state] || ''), el('span', id, 'label'));
|
|
782
1244
|
head.append(name, badge(peer.rostered === false ? peer.state + ' off roster' : peer.state, peerDot[peer.state] || ''));
|
|
783
1245
|
if (peer.planner) name.append(badge('Planner' + (peer.planner === 'setting' ? '' : ' (' + peer.planner + ')'), ''));
|
|
784
|
-
row
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (peer.
|
|
790
|
-
|
|
791
|
-
if (peer.
|
|
792
|
-
if (peer.
|
|
793
|
-
if (peer.
|
|
794
|
-
if (peer.
|
|
1246
|
+
// Summary row (#469 R4): name, state, current task and queue. The diagnostic lines below fold into one disclosure.
|
|
1247
|
+
const current = (snapshot.boardLegend || []).find(item => item.peer === id)?.activeTasks, summary = el('p', undefined, 'peer-summary');
|
|
1248
|
+
summary.append(el('span', peer.queued + ' queued' + (peer.queuedImportant ? ' (' + peer.queuedImportant + ' important)' : ''), 'meta'), el('span', Array.isArray(current) && current.length ? 'Working on ' + current.map(task => '#' + task).join(', ') : 'No active task', 'meta'));
|
|
1249
|
+
row.append(head, summary);
|
|
1250
|
+
// A status notice asks a person to act (for example, reattach a native session): it stays visible, never folded away.
|
|
1251
|
+
if (typeof peer.statusNotice?.message === 'string' && peer.statusNotice.message) row.append(el('p', peer.statusNotice.message, 'meta notice-warn'));
|
|
1252
|
+
const more = el('div', undefined, 'peer-more');
|
|
1253
|
+
if (peer.flowRoles) more.append(el('p', 'Flow roles: ' + (peer.flowRoles.join(', ') || 'none'), 'meta'));
|
|
1254
|
+
if (peer.catalogRoles) { more.append(el('p', 'Catalog guidance: ' + (Array.isArray(peer.catalogRoles.value) ? peer.catalogRoles.value.join(', ') || 'none' : 'unconfirmed'), 'meta')); if (peer.catalogRoles.pending !== undefined) more.append(el('p', 'Pending at next start: ' + (peer.catalogRoles.pending.join(', ') || 'none'), 'meta')); }
|
|
1255
|
+
if (peer.needsReview) more.append(el('p', peer.needsReview + ' need review: use ahub queue list', 'meta'));
|
|
1256
|
+
if (peer.oldestQueuedAt !== undefined) more.append(el('p', 'Oldest pending: ' + Math.max(0, Math.floor((Date.now() - peer.oldestQueuedAt) / 1000)) + 's', 'meta'));
|
|
1257
|
+
if (peer.attached === false) more.append(el('p', 'Disconnected recipient; work is retained.', 'meta'));
|
|
1258
|
+
if (peer.toolsOnly) more.append(el('p', peer.toolsOnly, 'meta'));
|
|
1259
|
+
if (peer.permissionNote) more.append(el('p', peer.permissionNote, 'meta'));
|
|
1260
|
+
if (peer.pullOnly && adapterDisplay[id]?.pullOnlyRead) more.append(el('p', 'Last hub_inbox read: ' + (typeof peer.lastInboxAt === 'number' ? Math.max(0, Math.floor((Date.now() - peer.lastInboxAt) / 1000)) + 's ago' : 'never'), 'meta'));
|
|
1261
|
+
if (peer.paused) more.append(el('p', peer.paused, 'meta'));
|
|
1262
|
+
if (peer.requestedModel) more.append(el('p', 'Requested: ' + peer.requestedModel, 'meta'));
|
|
795
1263
|
for (const backend of peer.backends || []) {
|
|
796
|
-
if (backend.requestedModel)
|
|
1264
|
+
if (backend.requestedModel) more.append(el('p', backend.kind.toUpperCase() + ': ' + (backend.actualModel || 'model not reported') + ' (' + backend.state + ')', 'meta'));
|
|
797
1265
|
}
|
|
1266
|
+
if (more.children.length) { const fold = el('details', undefined, 'peer-fold'); fold.append(el('summary', 'Details (' + more.children.length + ')'), more); row.append(fold); }
|
|
798
1267
|
const paused = peer.state === 'paused' || peer.paused;
|
|
799
1268
|
row.tabIndex = 0; peerRows.set(id, row);
|
|
800
1269
|
const trackDesk = (on) => { const desk = officeDesks.get(id); if (desk) desk.classList.toggle('office-spotlight', on); };
|
|
801
1270
|
row.addEventListener('mouseenter', () => trackDesk(true)); row.addEventListener('mouseleave', () => trackDesk(false));
|
|
802
1271
|
row.addEventListener('focus', () => trackDesk(true)); row.addEventListener('blur', () => trackDesk(false));
|
|
803
|
-
row.append(button(paused ? 'Resume' : 'Pause', {action:paused ? 'resume' : 'pause', peer:id}));
|
|
1272
|
+
row.append(button(paused ? 'Resume' : 'Pause', {action:paused ? 'resume' : 'pause', peer:id})); return row;
|
|
1273
|
+
});
|
|
1274
|
+
peerRows.set(id, row); target.append(row);
|
|
804
1275
|
}
|
|
805
|
-
renderStarts(target, starts);
|
|
1276
|
+
keyed.end(); renderStarts(target, starts);
|
|
806
1277
|
});
|
|
807
1278
|
renderOffice(snapshot);
|
|
808
1279
|
const assignable = ids.filter((id) => peers[id].state !== 'offline' && peers[id].attached !== false);
|
|
@@ -845,7 +1316,7 @@ function render(snapshot) {
|
|
|
845
1316
|
const event = entry.event, node = el('div', undefined, 'event'), kind = event.env ? event.env.kind : 'state';
|
|
846
1317
|
node.dataset.kind = kind; applyStreamFilter(node);
|
|
847
1318
|
if (event.env) { const env = event.env, meta = el('div', '', 'meta'), stamp = el('span'); stamp.dataset.ts = env.ts; stamp.title = time(env.ts); stamp.textContent = rel(env.ts);
|
|
848
|
-
meta.append(stamp, el('span', ' / ' + env.from + '
|
|
1319
|
+
meta.append(stamp, el('span', ' / ' + env.from + ' '), icon('i-arrow', 'to'), el('span', ' ' + (env.to ? env.to.join(', ') : 'all') + ' / ' + env.kind + (event.t !== 'envelope' ? ' / ' + event.t : '') + (event.dropped ? ' / ' + event.dropped : '')));
|
|
849
1320
|
node.append(meta, el('div', env.body, 'body')); }
|
|
850
1321
|
else node.append(el('div', event.peer + ' / ' + event.state, 'meta'));
|
|
851
1322
|
stream.append(node); eventCount++; while (stream.children.length > 200) stream.firstElementChild.remove();
|
|
@@ -864,10 +1335,10 @@ async function poll() {
|
|
|
864
1335
|
const input = {after:cursor, memory:memoryInput()}; if (managerMode) { input.projectId = captured.projectId; input.instanceId = captured.instanceId; }
|
|
865
1336
|
const result = await post('/snapshot', input);
|
|
866
1337
|
if (!managerMode || (result.projectId === captured.projectId && result.instanceId === captured.instanceId && captured.generation === viewGeneration)) {
|
|
867
|
-
snapshotValid = true; lastSnapshot = result; render(result);
|
|
1338
|
+
snapshotValid = true; lastSnapshot = result; lastSnapshotAt = Date.now(); render(result); setConnection('Live / updated ' + new Date().toLocaleTimeString(), 'live'); setDisconnected(false);
|
|
868
1339
|
}
|
|
869
1340
|
}
|
|
870
|
-
catch (error) { if (!stopped)
|
|
1341
|
+
catch (error) { if (!stopped) { setConnection('Disconnected / retrying: ' + error.message, 'down'); setDisconnected(true); } }
|
|
871
1342
|
if (!stopped) timer = setTimeout(poll, 1000);
|
|
872
1343
|
}
|
|
873
1344
|
// office-scene-begin (issue 264): the office, drawn Flat or Isometric from the shared officeModel as inline
|
|
@@ -1000,6 +1471,11 @@ function officeTileCards(tile, centerX, startY, lift = 0) {
|
|
|
1000
1471
|
function officeRoomCards(room) { return officeTileCards(room, room.w / 2, 8, officeRoomOffset); }
|
|
1001
1472
|
function officeDeskCards(desk) { return officeTileCards(desk, desk.w / 2 - 0.5, 9); }
|
|
1002
1473
|
|
|
1474
|
+
// A sprite icon inside the Office scene, sized in scene units; the stroke is the scene's own hairline, not the page's 2px.
|
|
1475
|
+
function officeIcon(id, x, y, size, cls) {
|
|
1476
|
+
const g = officeNode('g', { class: cls, transform: 'translate(' + x + ' ' + y + ') scale(' + (size / 24) + ')', 'aria-hidden': 'true' });
|
|
1477
|
+
g.append(officeNode('use', { href: '#' + id })); return g;
|
|
1478
|
+
}
|
|
1003
1479
|
function officeNode(tag, attrs, text) { const node = document.createElementNS('http://www.w3.org/2000/svg', tag); for (const key in attrs) node.setAttribute(key, String(attrs[key])); if (text !== undefined) node.textContent = text; return node; }
|
|
1004
1480
|
function officeKeys(event, activate) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); activate(); } }
|
|
1005
1481
|
let spotlitRow = null;
|
|
@@ -1032,7 +1508,7 @@ function officeCardPaint(node, card) {
|
|
|
1032
1508
|
const children = [officeNode('title', {}, card.label), clip,
|
|
1033
1509
|
officeNode('rect', { x: 0, y: 0, width: card.w, height: card.h, rx: 0.5, class: 'office-card' }),
|
|
1034
1510
|
officeNode('rect', { x: 0.15, y: 0.25, width: 0.2, height: Math.max(0.4, card.h - 0.5), rx: 0.1, class: 'office-card-accent' }), text];
|
|
1035
|
-
if (card.checking) children.push(
|
|
1511
|
+
if (card.checking) children.push(officeIcon('i-check', card.w - 1.5, card.h / 2 - 0.46, 0.92, 'office-checking'));
|
|
1036
1512
|
node.replaceChildren(...children);
|
|
1037
1513
|
}
|
|
1038
1514
|
function officeDeskPaint(node, desk) {
|
|
@@ -1283,7 +1759,7 @@ function drawOffice(target, model) {
|
|
|
1283
1759
|
const latch = model.latched[desk.id];
|
|
1284
1760
|
if (desk.condition !== 'idle' || !latch || latch.until <= now) continue;
|
|
1285
1761
|
const { node, fresh } = keep('latch:' + desk.id, String(latch.seq), scene.layers.marks,
|
|
1286
|
-
(n) => { officePlace(n, desk.x, desk.y); n.setAttribute('class', 'office-latchmark'); n.setAttribute('aria-hidden', 'true'); n.replaceChildren(officeNode('text', { x:
|
|
1762
|
+
(n) => { officePlace(n, desk.x, desk.y); n.setAttribute('class', 'office-latchmark'); n.setAttribute('aria-hidden', 'true'); n.replaceChildren(officeNode('circle', { cx: 5.5, cy: 3.55, r: 0.45, class: 'office-latch-dot' }), officeNode('text', { x: 6.3, y: 3.9, class: 'office-latch' }, 'busy')); }, false);
|
|
1287
1763
|
if (fresh) officeFade(node, latch.until, now);
|
|
1288
1764
|
}
|
|
1289
1765
|
for (const key of [...els.keys()]) if (!seen.has(key)) officeLeave(scene, key);
|
|
@@ -1409,14 +1885,16 @@ function renderMemory(memory) {
|
|
|
1409
1885
|
tiers.append(chip);
|
|
1410
1886
|
}
|
|
1411
1887
|
for (const chip of tiers.children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier));
|
|
1412
|
-
if (!memory) { $('memory-count').textContent = ''; update('memory-list', null, (target) => empty(target, 'Hub memory is off for this project, or nothing is stored yet.')); update('memory-graph', null, () => {}); update('memory-detail', null, () => {}); return; }
|
|
1888
|
+
if (!memory) { $('memory-count').textContent = ''; update('memory-list', null, (target) => empty(target, 'Hub memory is off for this project, or nothing is stored yet. Save a note with ahub remember <text>.')); update('memory-graph', null, () => {}); update('memory-detail', null, () => {}); return; }
|
|
1413
1889
|
$('memory-count').textContent = memory.counts.long + ' long-term / ' + memory.counts.short + ' short-term / ' + (memory.counts.goals || 0) + ' open tasks';
|
|
1414
1890
|
if (memory.goals) {
|
|
1415
1891
|
// Long-term tasks: state, who holds the lease and until when (a conflict when two machines claimed it), linked board tasks.
|
|
1416
1892
|
const who = (m) => m === memory.machine ? 'this machine' : 'machine ' + m.slice(0, 8);
|
|
1417
1893
|
update('memory-list', {goals: memory.goals, node: memoryQuery.node}, (target) => {
|
|
1418
1894
|
if (!memory.goals.length) return empty(target, 'No long-term tasks. A person creates one with ahub goal new <title>.');
|
|
1895
|
+
const keyed = keyedStart('memory');
|
|
1419
1896
|
for (const g of memory.goals) {
|
|
1897
|
+
const row = keyed('goal:' + g.id, JSON.stringify([g, memoryQuery.node === 'goal:' + g.id]), () => {
|
|
1420
1898
|
const row = el('button', undefined, 'memory-row'); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'goal:' + g.id));
|
|
1421
1899
|
const head = el('div', undefined, 'row-head');
|
|
1422
1900
|
head.append(el('span', g.title, 'label'), badge(g.state, g.state === 'done' ? 'ok' : g.state === 'blocked' ? 'err' : g.state === 'active' ? 'info' : ''));
|
|
@@ -1424,22 +1902,30 @@ function renderMemory(memory) {
|
|
|
1424
1902
|
row.append(head, el('div', g.id.slice(0, 8) + ' / ' + lease + ' / ' + g.links.length + ' board task' + (g.links.length === 1 ? '' : 's') + (g.milestones.length ? ' / milestones: ' + g.milestones.join('; ') : ''), 'meta'));
|
|
1425
1903
|
if (g.lease.conflict.length) row.append(badge('lease conflict', 'warn'));
|
|
1426
1904
|
row.addEventListener('click', () => selectMemoryNode('goal:' + g.id));
|
|
1905
|
+
return row;
|
|
1906
|
+
});
|
|
1427
1907
|
target.append(row);
|
|
1428
1908
|
}
|
|
1909
|
+
keyed.end();
|
|
1429
1910
|
});
|
|
1430
1911
|
}
|
|
1431
1912
|
memoryOptions('memory-peer', memory.peers, 'All peers', 'peer');
|
|
1432
1913
|
memoryOptions('memory-machine', memory.machines, 'All machines', 'machine');
|
|
1433
1914
|
if (!memory.goals) update('memory-list', {entries: memory.entries, node: memoryQuery.node}, (target) => {
|
|
1434
|
-
if (!memory.entries.length) return empty(target, 'No ' + memory.tier + '-term entries match.');
|
|
1915
|
+
if (!memory.entries.length) return empty(target, 'No ' + memory.tier + '-term entries match. Save one with ahub remember <text>.');
|
|
1916
|
+
const keyed = keyedStart('memory');
|
|
1435
1917
|
for (const entry of memory.entries) {
|
|
1918
|
+
const row = keyed('mem:' + entry.id, JSON.stringify([entry, memoryQuery.node === 'mem:' + entry.id]), () => {
|
|
1436
1919
|
const row = el('button', undefined, 'memory-row'); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'mem:' + entry.id));
|
|
1437
1920
|
const head = el('div', undefined, 'row-head');
|
|
1438
1921
|
head.append(el('span', (entry.title ? entry.title + ': ' : '') + entry.text.split('\n')[0].slice(0, 140), 'label'), badge(entry.kind, entry.kind === 'fail' ? 'err' : entry.kind === 'decision' ? 'ok' : entry.kind === 'contract' ? 'warn' : ''));
|
|
1439
1922
|
row.append(head, el('div', entry.id.slice(0, 8) + ' / ' + entry.peer + ' / ' + time(entry.at) + (entry.scope === 'shared' ? ' / shared' : '') + (entry.supersededBy ? ' / superseded' : '') + (entry.machine !== memory.machine ? ' / from machine ' + entry.machine.slice(0, 6) : ''), 'meta'));
|
|
1440
1923
|
row.addEventListener('click', () => selectMemoryNode('mem:' + entry.id));
|
|
1924
|
+
return row;
|
|
1925
|
+
});
|
|
1441
1926
|
target.append(row);
|
|
1442
1927
|
}
|
|
1928
|
+
keyed.end();
|
|
1443
1929
|
});
|
|
1444
1930
|
update('memory-graph', memory.graph || null, (target) => {
|
|
1445
1931
|
const graph = memory.graph;
|
|
@@ -1520,7 +2006,7 @@ window.addEventListener('resize', () => { if (!stopped && officeView.get() !== '
|
|
|
1520
2006
|
$('send').addEventListener('submit', async (event) => { event.preventDefault(); const generation = viewGeneration, body = $('message').value.trim(); if (!body) return; const to = $('recipient').value, payload = {action:'send', body}; if (to) payload.to = [to]; if (await action(payload, event.submitter) && generation === viewGeneration) $('message').value = ''; });
|
|
1521
2007
|
$('propose').addEventListener('submit', async (event) => { event.preventDefault(); const generation = viewGeneration, title = $('title').value.trim(); if (!title) return; const payload = {action:'propose', title, class:$('class').value}; if ($('detail').value.trim()) payload.detail = $('detail').value.trim(); if ($('owner').value) payload.owner = $('owner').value; if ($('backlog-proposal').checked) { if(boardViewState?.snapshot.boardCapabilities?.backlog!==true || !sameBoardContext(boardContext())) return notice('Backlog is unavailable for this project snapshot.',true);payload.backlog=true; } if (await action(payload, event.submitter) && generation === viewGeneration) { $('title').value = ''; $('detail').value = ''; } });
|
|
1522
2008
|
initializeDashboardNavigation();
|
|
1523
|
-
(async () => { try { if (ticket) await post('/session', {ticket}); ticket = ''; renderStreamChips(); await refreshProjects(); await poll(); } catch (error) { ticket = ''; if (!stopped) { stopped = true; notice('Could not start session: ' + error.message + ' Run ahub ui to reopen.', true);
|
|
2009
|
+
(async () => { try { if (ticket) await post('/session', {ticket}); ticket = ''; renderStreamChips(); await refreshProjects(); await poll(); } catch (error) { ticket = ''; if (!stopped) { stopped = true; notice('Could not start session: ' + error.message + ' Run ahub ui to reopen.', true); setConnection('Session unavailable', 'down'); } } })();
|
|
1524
2010
|
</script>
|
|
1525
2011
|
</body>
|
|
1526
2012
|
</html>
|