@staix/agent-hub 0.12.44 → 0.13.0
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 +40 -0
- package/README.md +83 -116
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +102 -14
- package/docs/agent-notes/README.md +15 -0
- package/docs/agent-notes/adapters.md +63 -46
- package/docs/agent-notes/budget.md +7 -2
- package/docs/agent-notes/bus.md +6 -4
- package/docs/agent-notes/daemon.md +15 -8
- package/docs/agent-notes/local-worker.md +6 -4
- package/docs/agent-notes/tasks.md +18 -5
- package/docs/agent-notes/tests.md +11 -9
- package/docs/dogfood.md +92 -0
- package/docs/events.md +14 -1
- package/docs/operations.md +349 -26
- package/docs/profiles.md +2 -2
- package/docs/quickstart.md +12 -2
- package/docs/sdlc.md +61 -13
- package/docs/security.md +24 -12
- package/docs/smoke.md +9 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +873 -13
- package/docs/verification/2026-10-19-reviewer-isolation.md +2 -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-22-0.12.45.md +60 -0
- package/docs/verification/2026-10-22-code-facts.md +78 -0
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +47 -0
- package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
- package/docs/verification/2026-10-23-0.13.0.md +89 -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/verification/dogfood/2026-10-22.md +3109 -0
- package/docs/verified.json +19 -16
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +10120 -9623
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +3 -1
- package/src/adapters/agy/entry.ts +5 -4
- package/src/adapters/claude-channel.ts +32 -12
- package/src/adapters/codex-appserver.ts +19 -2
- package/src/adapters/codex-review-isolation.ts +12 -5
- package/src/adapters/compat.ts +140 -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 +25 -7
- package/src/adapters/definitions/native-cli-doctor.ts +12 -5
- package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
- package/src/adapters/definitions/native-cli-entry-kimi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
- package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/qwen-native.ts +7 -3
- package/src/adapters/definitions/qwen.ts +5 -1
- 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 +72 -31
- package/src/adapters/kimi-review.ts +6 -3
- package/src/adapters/pi.ts +2 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +13 -1
- package/src/adapters/registry.ts +52 -15
- package/src/cli/agents.ts +40 -0
- package/src/cli/console-review.ts +54 -0
- package/src/cli/console-state.ts +124 -29
- package/src/cli/console-symbols.ts +74 -0
- package/src/cli/console-usage.ts +89 -0
- package/src/cli/console.ts +26 -1
- package/src/cli/help.ts +36 -21
- package/src/cli/identity.ts +30 -7
- package/src/cli/init.ts +109 -11
- package/src/cli/main.ts +145 -16
- package/src/cli/output.ts +44 -1
- package/src/cli/preview.ts +1 -1
- package/src/cli/sdlc-release-guard.ts +82 -0
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +4 -1
- package/src/hub/action-class.ts +362 -0
- package/src/hub/action-policy.ts +106 -0
- package/src/hub/board.ts +72 -11
- package/src/hub/budget.ts +71 -2
- package/src/hub/bus.ts +43 -3
- package/src/hub/checks.ts +80 -31
- package/src/hub/code-facts.ts +223 -0
- package/src/hub/config-apply.ts +28 -11
- package/src/hub/config-reload.ts +8 -5
- package/src/hub/config-trust.ts +7 -4
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +332 -61
- package/src/hub/dogfood-read.ts +199 -0
- package/src/hub/dogfood.ts +163 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +11 -2
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +45 -7
- package/src/hub/inbox-polling.ts +25 -0
- package/src/hub/instance-config.ts +17 -5
- package/src/hub/instance-execution.ts +20 -8
- package/src/hub/instances.ts +75 -2
- package/src/hub/peers.ts +9 -0
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/profiles.ts +7 -2
- package/src/hub/release-worktree.ts +56 -0
- package/src/hub/review-brief.ts +36 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +31 -5
- package/src/hub/sdlc-queue.ts +48 -11
- package/src/hub/sdlc-release-authorization.ts +173 -0
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +71 -18
- package/src/hub/settings.ts +10 -3
- package/src/hub/snapshots.ts +101 -14
- package/src/hub/symbol-normalizer.ts +26 -0
- package/src/hub/task-code-symbols.ts +90 -0
- package/src/hub/task-sweep.ts +3 -2
- package/src/hub/tasks.ts +556 -88
- package/src/hub/test-integrity.ts +162 -0
- package/src/hub/ui.ts +2 -2
- package/src/hub/usage-rollup.ts +361 -0
- package/src/local/sandbox.ts +5 -3
- package/src/ui/index.html +560 -78
- 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/dev.json +6 -1
- package/templates/profiles/general.json +6 -1
- package/templates/profiles/research.json +6 -1
- package/templates/profiles/writing.json +6 -1
- package/templates/release-guard.block.md +6 -0
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,6 +723,7 @@ 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'));
|
|
@@ -399,7 +801,7 @@ function renderSettings(settings, roster = []) {
|
|
|
399
801
|
// are rebuilt only when their own data changed, so a value being typed in one row survives a save of another.
|
|
400
802
|
update('settings', settings.rows.map((row) => row.key), (target) => {
|
|
401
803
|
settingSlots.clear();
|
|
402
|
-
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);
|
|
403
805
|
const undo = el('div'); settingSlots.set('', { node: undo, signature: null }); target.append(undo);
|
|
404
806
|
for (const group of SETTING_GROUPS) {
|
|
405
807
|
const rows = settings.rows.filter((row) => row.group === group);
|
|
@@ -418,7 +820,7 @@ function renderSettings(settings, roster = []) {
|
|
|
418
820
|
if (reloadSupported) {
|
|
419
821
|
reloadSlot.node.append(button('Reload configuration', {action:'setting_reload'}));
|
|
420
822
|
if (settings.pendingRestart.length) {
|
|
421
|
-
|
|
823
|
+
const pendingHead = el('p', 'Pending restart', 'label'); pendingHead.append(el('span', ' '), badge(String(settings.pendingRestart.length), 'warn')); reloadSlot.node.append(pendingHead);
|
|
422
824
|
for (const pending of settings.pendingRestart) reloadSlot.node.append(el('p', pending.key + ': ' + pending.reason, 'meta'));
|
|
423
825
|
}
|
|
424
826
|
if (Array.isArray(settings.retainedConfig) && settings.retainedConfig.length) {
|
|
@@ -474,6 +876,26 @@ function renderStarts(target, starts) {
|
|
|
474
876
|
}
|
|
475
877
|
}
|
|
476
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
|
+
}
|
|
477
899
|
const BOARD_FOCUS_COLUMNS = ['backlog','todo','progress','review','done'];
|
|
478
900
|
let columnFocus, taskDetailId = null;
|
|
479
901
|
let planningEdit = null, releaseEdit = null;
|
|
@@ -549,6 +971,30 @@ function peerMark(peer, reviewer = false) {
|
|
|
549
971
|
const mark = el('span', undefined, 'peer-mark peer-' + key), color = el('span', undefined, 'peer-color-dot' + (reviewer ? ' reviewer' : ''));
|
|
550
972
|
color.setAttribute('aria-hidden', 'true'); mark.append(color, el('span', (reviewer ? 'reviewer: ' : 'owner: ') + (peer || 'unassigned'))); return mark;
|
|
551
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
|
+
}
|
|
552
998
|
function boardContext() {
|
|
553
999
|
const current=context(), snapshot=boardViewState?.snapshot || {};
|
|
554
1000
|
return {...current,origin:location.origin,projectId:typeof snapshot.projectId==='string' ? snapshot.projectId : current.projectId,instanceId:typeof snapshot.instanceId==='string' ? snapshot.instanceId : current.instanceId};
|
|
@@ -586,24 +1032,32 @@ function taskDetail(task, card, stage, ids, overdue) {
|
|
|
586
1032
|
const row = el('article', undefined, 'task-detail'), head = el('div', undefined, 'row-head');
|
|
587
1033
|
row.setAttribute('aria-label', '#' + task.id + ' ' + task.title + ', ' + task.state.replaceAll('_', ' ') + ', owner ' + (task.owner || 'unassigned') + ', priority P' + card.priority);
|
|
588
1034
|
head.append(el('span', '#' + task.id + ' ' + task.title, 'label'), badge('P' + card.priority));
|
|
589
|
-
if (card.starred) { const star = el('span', '
|
|
590
|
-
row.append(head
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if
|
|
594
|
-
if (
|
|
595
|
-
if (
|
|
596
|
-
if (task.
|
|
597
|
-
if (task.
|
|
598
|
-
if (task.lifecycle)
|
|
599
|
-
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);
|
|
600
1051
|
if (task.detail) row.append(el('p', task.detail, 'body'));
|
|
601
|
-
|
|
602
|
-
if(task.
|
|
603
|
-
if(task.
|
|
604
|
-
if(
|
|
605
|
-
|
|
606
|
-
|
|
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);
|
|
607
1061
|
const controls = el('div', undefined, 'actions');
|
|
608
1062
|
if (card.pull && boardViewState?.snapshot.boardCapabilities?.backlog===true) controls.append(boardButton('Pull into To Do', {action:'backlog_pull',id:task.id}));
|
|
609
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'}));
|
|
@@ -626,15 +1080,16 @@ function taskCard(task, card, stage, ids, overdue, compact = true) {
|
|
|
626
1080
|
if (compact) {
|
|
627
1081
|
row.setAttribute('role', 'button');
|
|
628
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');
|
|
629
1085
|
if (task.role) row.setAttribute('aria-label', row.getAttribute('aria-label') + ', Task guidance: ' + task.role + ' (grants no authority)');
|
|
630
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)')); }
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
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);
|
|
638
1093
|
row.addEventListener('click', () => openTaskDetail(task.id));
|
|
639
1094
|
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(task.id); } });
|
|
640
1095
|
}
|
|
@@ -707,7 +1162,8 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
707
1162
|
if (!ids.length) empty(target,'No peer status available.');
|
|
708
1163
|
});
|
|
709
1164
|
update('tasks',[snapshot.tasks,peers,snapshot.boardCapabilities,taskFilter,ownerFilter,releaseFilter,lanes,showAllDone,[...overdue],columnFocus],target=>{
|
|
710
|
-
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'));
|
|
711
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)));
|
|
712
1168
|
const visibleIds=new Set(visible.map(task=>task.id));
|
|
713
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];
|
|
@@ -734,7 +1190,7 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
734
1190
|
if(hidden) ready=ready.slice(0,10);
|
|
735
1191
|
for(const [label,list] of [['Ready',ready],['Waiting',waiting]]) {
|
|
736
1192
|
if(column.id==='todo') box.append(el('h4',label+' ('+list.length+')'));
|
|
737
|
-
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)}
|
|
738
1194
|
}
|
|
739
1195
|
if(!ready.length && !waiting.length) empty(box,'No tasks');
|
|
740
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)}
|
|
@@ -742,7 +1198,7 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
742
1198
|
}
|
|
743
1199
|
lane.append(grid);target.append(lane);
|
|
744
1200
|
}
|
|
745
|
-
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'));
|
|
746
1202
|
});
|
|
747
1203
|
refreshTaskDetail();
|
|
748
1204
|
}
|
|
@@ -779,35 +1235,46 @@ function render(snapshot) {
|
|
|
779
1235
|
syncStarts(snapshot);
|
|
780
1236
|
update('peers', [peers, starts, starts.map(start => { const pending = pendingStarts.get(startKey(start.peer)); return pending ? [pending.phase, pending.retryAt || null] : null; })], (target) => {
|
|
781
1237
|
peerRows.clear();
|
|
782
|
-
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');
|
|
783
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]), () => {
|
|
784
1242
|
const row = el('div', undefined, 'row'), head = el('div', undefined, 'row-head'), name = el('div', undefined, 'peername');
|
|
785
1243
|
name.append(dot(peerDot[peer.state] || ''), el('span', id, 'label'));
|
|
786
1244
|
head.append(name, badge(peer.rostered === false ? peer.state + ' off roster' : peer.state, peerDot[peer.state] || ''));
|
|
787
1245
|
if (peer.planner) name.append(badge('Planner' + (peer.planner === 'setting' ? '' : ' (' + peer.planner + ')'), ''));
|
|
788
|
-
row
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (peer.
|
|
794
|
-
|
|
795
|
-
if (peer.
|
|
796
|
-
if (
|
|
797
|
-
if (peer.
|
|
798
|
-
if (peer.
|
|
799
|
-
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'));
|
|
1263
|
+
if (typeof peer.modelSource === 'string' && peer.modelSource) more.append(el('p', 'Model source: ' + peer.modelSource, 'meta'));
|
|
800
1264
|
for (const backend of peer.backends || []) {
|
|
801
|
-
if (backend.requestedModel)
|
|
1265
|
+
if (backend.requestedModel) more.append(el('p', backend.kind.toUpperCase() + ': ' + (backend.actualModel || 'model not reported') + ' (' + backend.state + ')', 'meta'));
|
|
802
1266
|
}
|
|
1267
|
+
if (more.children.length) { const fold = el('details', undefined, 'peer-fold'); fold.append(el('summary', 'Details (' + more.children.length + ')'), more); row.append(fold); }
|
|
803
1268
|
const paused = peer.state === 'paused' || peer.paused;
|
|
804
1269
|
row.tabIndex = 0; peerRows.set(id, row);
|
|
805
1270
|
const trackDesk = (on) => { const desk = officeDesks.get(id); if (desk) desk.classList.toggle('office-spotlight', on); };
|
|
806
1271
|
row.addEventListener('mouseenter', () => trackDesk(true)); row.addEventListener('mouseleave', () => trackDesk(false));
|
|
807
1272
|
row.addEventListener('focus', () => trackDesk(true)); row.addEventListener('blur', () => trackDesk(false));
|
|
808
|
-
row.append(button(paused ? 'Resume' : 'Pause', {action:paused ? 'resume' : 'pause', peer:id}));
|
|
1273
|
+
row.append(button(paused ? 'Resume' : 'Pause', {action:paused ? 'resume' : 'pause', peer:id})); return row;
|
|
1274
|
+
});
|
|
1275
|
+
peerRows.set(id, row); target.append(row);
|
|
809
1276
|
}
|
|
810
|
-
renderStarts(target, starts);
|
|
1277
|
+
keyed.end(); renderStarts(target, starts);
|
|
811
1278
|
});
|
|
812
1279
|
renderOffice(snapshot);
|
|
813
1280
|
const assignable = ids.filter((id) => peers[id].state !== 'offline' && peers[id].attached !== false);
|
|
@@ -850,7 +1317,7 @@ function render(snapshot) {
|
|
|
850
1317
|
const event = entry.event, node = el('div', undefined, 'event'), kind = event.env ? event.env.kind : 'state';
|
|
851
1318
|
node.dataset.kind = kind; applyStreamFilter(node);
|
|
852
1319
|
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);
|
|
853
|
-
meta.append(stamp, el('span', ' / ' + env.from + '
|
|
1320
|
+
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 : '')));
|
|
854
1321
|
node.append(meta, el('div', env.body, 'body')); }
|
|
855
1322
|
else node.append(el('div', event.peer + ' / ' + event.state, 'meta'));
|
|
856
1323
|
stream.append(node); eventCount++; while (stream.children.length > 200) stream.firstElementChild.remove();
|
|
@@ -869,10 +1336,10 @@ async function poll() {
|
|
|
869
1336
|
const input = {after:cursor, memory:memoryInput()}; if (managerMode) { input.projectId = captured.projectId; input.instanceId = captured.instanceId; }
|
|
870
1337
|
const result = await post('/snapshot', input);
|
|
871
1338
|
if (!managerMode || (result.projectId === captured.projectId && result.instanceId === captured.instanceId && captured.generation === viewGeneration)) {
|
|
872
|
-
snapshotValid = true; lastSnapshot = result; render(result);
|
|
1339
|
+
snapshotValid = true; lastSnapshot = result; lastSnapshotAt = Date.now(); render(result); setConnection('Live / updated ' + new Date().toLocaleTimeString(), 'live'); setDisconnected(false);
|
|
873
1340
|
}
|
|
874
1341
|
}
|
|
875
|
-
catch (error) { if (!stopped)
|
|
1342
|
+
catch (error) { if (!stopped) { setConnection('Disconnected / retrying: ' + error.message, 'down'); setDisconnected(true); } }
|
|
876
1343
|
if (!stopped) timer = setTimeout(poll, 1000);
|
|
877
1344
|
}
|
|
878
1345
|
// office-scene-begin (issue 264): the office, drawn Flat or Isometric from the shared officeModel as inline
|
|
@@ -1005,6 +1472,11 @@ function officeTileCards(tile, centerX, startY, lift = 0) {
|
|
|
1005
1472
|
function officeRoomCards(room) { return officeTileCards(room, room.w / 2, 8, officeRoomOffset); }
|
|
1006
1473
|
function officeDeskCards(desk) { return officeTileCards(desk, desk.w / 2 - 0.5, 9); }
|
|
1007
1474
|
|
|
1475
|
+
// A sprite icon inside the Office scene, sized in scene units; the stroke is the scene's own hairline, not the page's 2px.
|
|
1476
|
+
function officeIcon(id, x, y, size, cls) {
|
|
1477
|
+
const g = officeNode('g', { class: cls, transform: 'translate(' + x + ' ' + y + ') scale(' + (size / 24) + ')', 'aria-hidden': 'true' });
|
|
1478
|
+
g.append(officeNode('use', { href: '#' + id })); return g;
|
|
1479
|
+
}
|
|
1008
1480
|
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; }
|
|
1009
1481
|
function officeKeys(event, activate) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); activate(); } }
|
|
1010
1482
|
let spotlitRow = null;
|
|
@@ -1037,7 +1509,7 @@ function officeCardPaint(node, card) {
|
|
|
1037
1509
|
const children = [officeNode('title', {}, card.label), clip,
|
|
1038
1510
|
officeNode('rect', { x: 0, y: 0, width: card.w, height: card.h, rx: 0.5, class: 'office-card' }),
|
|
1039
1511
|
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];
|
|
1040
|
-
if (card.checking) children.push(
|
|
1512
|
+
if (card.checking) children.push(officeIcon('i-check', card.w - 1.5, card.h / 2 - 0.46, 0.92, 'office-checking'));
|
|
1041
1513
|
node.replaceChildren(...children);
|
|
1042
1514
|
}
|
|
1043
1515
|
function officeDeskPaint(node, desk) {
|
|
@@ -1288,7 +1760,7 @@ function drawOffice(target, model) {
|
|
|
1288
1760
|
const latch = model.latched[desk.id];
|
|
1289
1761
|
if (desk.condition !== 'idle' || !latch || latch.until <= now) continue;
|
|
1290
1762
|
const { node, fresh } = keep('latch:' + desk.id, String(latch.seq), scene.layers.marks,
|
|
1291
|
-
(n) => { officePlace(n, desk.x, desk.y); n.setAttribute('class', 'office-latchmark'); n.setAttribute('aria-hidden', 'true'); n.replaceChildren(officeNode('text', { x:
|
|
1763
|
+
(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);
|
|
1292
1764
|
if (fresh) officeFade(node, latch.until, now);
|
|
1293
1765
|
}
|
|
1294
1766
|
for (const key of [...els.keys()]) if (!seen.has(key)) officeLeave(scene, key);
|
|
@@ -1414,14 +1886,16 @@ function renderMemory(memory) {
|
|
|
1414
1886
|
tiers.append(chip);
|
|
1415
1887
|
}
|
|
1416
1888
|
for (const chip of tiers.children) chip.setAttribute('aria-pressed', String(chip.dataset.tier === memoryQuery.tier));
|
|
1417
|
-
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; }
|
|
1889
|
+
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; }
|
|
1418
1890
|
$('memory-count').textContent = memory.counts.long + ' long-term / ' + memory.counts.short + ' short-term / ' + (memory.counts.goals || 0) + ' open tasks';
|
|
1419
1891
|
if (memory.goals) {
|
|
1420
1892
|
// Long-term tasks: state, who holds the lease and until when (a conflict when two machines claimed it), linked board tasks.
|
|
1421
1893
|
const who = (m) => m === memory.machine ? 'this machine' : 'machine ' + m.slice(0, 8);
|
|
1422
1894
|
update('memory-list', {goals: memory.goals, node: memoryQuery.node}, (target) => {
|
|
1423
1895
|
if (!memory.goals.length) return empty(target, 'No long-term tasks. A person creates one with ahub goal new <title>.');
|
|
1896
|
+
const keyed = keyedStart('memory');
|
|
1424
1897
|
for (const g of memory.goals) {
|
|
1898
|
+
const row = keyed('goal:' + g.id, JSON.stringify([g, memoryQuery.node === 'goal:' + g.id]), () => {
|
|
1425
1899
|
const row = el('button', undefined, 'memory-row'); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'goal:' + g.id));
|
|
1426
1900
|
const head = el('div', undefined, 'row-head');
|
|
1427
1901
|
head.append(el('span', g.title, 'label'), badge(g.state, g.state === 'done' ? 'ok' : g.state === 'blocked' ? 'err' : g.state === 'active' ? 'info' : ''));
|
|
@@ -1429,22 +1903,30 @@ function renderMemory(memory) {
|
|
|
1429
1903
|
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'));
|
|
1430
1904
|
if (g.lease.conflict.length) row.append(badge('lease conflict', 'warn'));
|
|
1431
1905
|
row.addEventListener('click', () => selectMemoryNode('goal:' + g.id));
|
|
1906
|
+
return row;
|
|
1907
|
+
});
|
|
1432
1908
|
target.append(row);
|
|
1433
1909
|
}
|
|
1910
|
+
keyed.end();
|
|
1434
1911
|
});
|
|
1435
1912
|
}
|
|
1436
1913
|
memoryOptions('memory-peer', memory.peers, 'All peers', 'peer');
|
|
1437
1914
|
memoryOptions('memory-machine', memory.machines, 'All machines', 'machine');
|
|
1438
1915
|
if (!memory.goals) update('memory-list', {entries: memory.entries, node: memoryQuery.node}, (target) => {
|
|
1439
|
-
if (!memory.entries.length) return empty(target, 'No ' + memory.tier + '-term entries match.');
|
|
1916
|
+
if (!memory.entries.length) return empty(target, 'No ' + memory.tier + '-term entries match. Save one with ahub remember <text>.');
|
|
1917
|
+
const keyed = keyedStart('memory');
|
|
1440
1918
|
for (const entry of memory.entries) {
|
|
1919
|
+
const row = keyed('mem:' + entry.id, JSON.stringify([entry, memoryQuery.node === 'mem:' + entry.id]), () => {
|
|
1441
1920
|
const row = el('button', undefined, 'memory-row'); row.type = 'button'; row.setAttribute('aria-pressed', String(memoryQuery.node === 'mem:' + entry.id));
|
|
1442
1921
|
const head = el('div', undefined, 'row-head');
|
|
1443
1922
|
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' : ''));
|
|
1444
1923
|
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'));
|
|
1445
1924
|
row.addEventListener('click', () => selectMemoryNode('mem:' + entry.id));
|
|
1925
|
+
return row;
|
|
1926
|
+
});
|
|
1446
1927
|
target.append(row);
|
|
1447
1928
|
}
|
|
1929
|
+
keyed.end();
|
|
1448
1930
|
});
|
|
1449
1931
|
update('memory-graph', memory.graph || null, (target) => {
|
|
1450
1932
|
const graph = memory.graph;
|
|
@@ -1525,7 +2007,7 @@ window.addEventListener('resize', () => { if (!stopped && officeView.get() !== '
|
|
|
1525
2007
|
$('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 = ''; });
|
|
1526
2008
|
$('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 = ''; } });
|
|
1527
2009
|
initializeDashboardNavigation();
|
|
1528
|
-
(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);
|
|
2010
|
+
(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'); } } })();
|
|
1529
2011
|
</script>
|
|
1530
2012
|
</body>
|
|
1531
2013
|
</html>
|