@theihtisham/agent-shadow-brain 7.1.0 → 7.1.4
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 +42 -0
- package/README.md +125 -450
- package/dist/activity-cli.d.ts +3 -0
- package/dist/activity-cli.d.ts.map +1 -0
- package/dist/activity-cli.js +78 -0
- package/dist/activity-cli.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/brain/activity-journal.d.ts +82 -0
- package/dist/brain/activity-journal.d.ts.map +1 -0
- package/dist/brain/activity-journal.js +315 -0
- package/dist/brain/activity-journal.js.map +1 -0
- package/dist/brain/activity-reporting.d.ts +14 -0
- package/dist/brain/activity-reporting.d.ts.map +1 -0
- package/dist/brain/activity-reporting.js +17 -0
- package/dist/brain/activity-reporting.js.map +1 -0
- package/dist/brain/auto-update.js +1 -1
- package/dist/brain/brain-portability.js +1 -1
- package/dist/brain/client-config.d.ts +19 -0
- package/dist/brain/client-config.d.ts.map +1 -0
- package/dist/brain/client-config.js +40 -0
- package/dist/brain/client-config.js.map +1 -0
- package/dist/brain/client-integrations.d.ts +63 -0
- package/dist/brain/client-integrations.d.ts.map +1 -0
- package/dist/brain/client-integrations.js +79 -0
- package/dist/brain/client-integrations.js.map +1 -0
- package/dist/brain/context-assembly.d.ts +84 -0
- package/dist/brain/context-assembly.d.ts.map +1 -0
- package/dist/brain/context-assembly.js +529 -0
- package/dist/brain/context-assembly.js.map +1 -0
- package/dist/brain/context-request.d.ts +4 -0
- package/dist/brain/context-request.d.ts.map +1 -0
- package/dist/brain/context-request.js +22 -0
- package/dist/brain/context-request.js.map +1 -0
- package/dist/brain/context-retrieval.d.ts +88 -0
- package/dist/brain/context-retrieval.d.ts.map +1 -0
- package/dist/brain/context-retrieval.js +300 -0
- package/dist/brain/context-retrieval.js.map +1 -0
- package/dist/brain/feature-settings.d.ts +58 -0
- package/dist/brain/feature-settings.d.ts.map +1 -0
- package/dist/brain/feature-settings.js +242 -0
- package/dist/brain/feature-settings.js.map +1 -0
- package/dist/brain/file-identity.d.ts +4 -0
- package/dist/brain/file-identity.d.ts.map +1 -0
- package/dist/brain/file-identity.js +27 -0
- package/dist/brain/file-identity.js.map +1 -0
- package/dist/brain/global-brain.d.ts +18 -1
- package/dist/brain/global-brain.d.ts.map +1 -1
- package/dist/brain/global-brain.js +77 -10
- package/dist/brain/global-brain.js.map +1 -1
- package/dist/brain/mcp-server.d.ts +30 -1
- package/dist/brain/mcp-server.d.ts.map +1 -1
- package/dist/brain/mcp-server.js +378 -28
- package/dist/brain/mcp-server.js.map +1 -1
- package/dist/brain/mcp-stdio.d.ts +12 -0
- package/dist/brain/mcp-stdio.d.ts.map +1 -0
- package/dist/brain/mcp-stdio.js +140 -0
- package/dist/brain/mcp-stdio.js.map +1 -0
- package/dist/brain/memory-annotations.d.ts +97 -0
- package/dist/brain/memory-annotations.d.ts.map +1 -0
- package/dist/brain/memory-annotations.js +389 -0
- package/dist/brain/memory-annotations.js.map +1 -0
- package/dist/brain/memory-graph.d.ts.map +1 -1
- package/dist/brain/memory-graph.js +12 -23
- package/dist/brain/memory-graph.js.map +1 -1
- package/dist/brain/memory-inspection.d.ts +72 -0
- package/dist/brain/memory-inspection.d.ts.map +1 -0
- package/dist/brain/memory-inspection.js +131 -0
- package/dist/brain/memory-inspection.js.map +1 -0
- package/dist/brain/memory-mcp-content.d.ts +44 -0
- package/dist/brain/memory-mcp-content.d.ts.map +1 -0
- package/dist/brain/memory-mcp-content.js +91 -0
- package/dist/brain/memory-mcp-content.js.map +1 -0
- package/dist/brain/memory-sources.d.ts +8 -0
- package/dist/brain/memory-sources.d.ts.map +1 -0
- package/dist/brain/memory-sources.js +27 -0
- package/dist/brain/memory-sources.js.map +1 -0
- package/dist/brain/orchestrator.js +1 -1
- package/dist/brain/project-feedback.d.ts +63 -0
- package/dist/brain/project-feedback.d.ts.map +1 -0
- package/dist/brain/project-feedback.js +289 -0
- package/dist/brain/project-feedback.js.map +1 -0
- package/dist/brain/project-hub-contract.d.ts +326 -0
- package/dist/brain/project-hub-contract.d.ts.map +1 -0
- package/dist/brain/project-hub-contract.js +56 -0
- package/dist/brain/project-hub-contract.js.map +1 -0
- package/dist/brain/project-hub-mcp.d.ts +168 -0
- package/dist/brain/project-hub-mcp.d.ts.map +1 -0
- package/dist/brain/project-hub-mcp.js +41 -0
- package/dist/brain/project-hub-mcp.js.map +1 -0
- package/dist/brain/project-hub-pdf-worker.d.ts +13 -0
- package/dist/brain/project-hub-pdf-worker.d.ts.map +1 -0
- package/dist/brain/project-hub-pdf-worker.js +84 -0
- package/dist/brain/project-hub-pdf-worker.js.map +1 -0
- package/dist/brain/project-hub-store.d.ts +38 -0
- package/dist/brain/project-hub-store.d.ts.map +1 -0
- package/dist/brain/project-hub-store.js +189 -0
- package/dist/brain/project-hub-store.js.map +1 -0
- package/dist/brain/project-hub.d.ts +127 -0
- package/dist/brain/project-hub.d.ts.map +1 -0
- package/dist/brain/project-hub.js +381 -0
- package/dist/brain/project-hub.js.map +1 -0
- package/dist/brain/project-kit-inspection.d.ts +28 -0
- package/dist/brain/project-kit-inspection.d.ts.map +1 -0
- package/dist/brain/project-kit-inspection.js +200 -0
- package/dist/brain/project-kit-inspection.js.map +1 -0
- package/dist/brain/project-kit-paths.d.ts +13 -0
- package/dist/brain/project-kit-paths.d.ts.map +1 -0
- package/dist/brain/project-kit-paths.js +141 -0
- package/dist/brain/project-kit-paths.js.map +1 -0
- package/dist/brain/project-kit.d.ts +41 -0
- package/dist/brain/project-kit.d.ts.map +1 -0
- package/dist/brain/project-kit.js +255 -0
- package/dist/brain/project-kit.js.map +1 -0
- package/dist/brain/project-memory.d.ts +38 -0
- package/dist/brain/project-memory.d.ts.map +1 -0
- package/dist/brain/project-memory.js +76 -0
- package/dist/brain/project-memory.js.map +1 -0
- package/dist/brain/subconscious.d.ts +21 -24
- package/dist/brain/subconscious.d.ts.map +1 -1
- package/dist/brain/subconscious.js +82 -218
- package/dist/brain/subconscious.js.map +1 -1
- package/dist/brain/workflow-skills.d.ts +5 -5
- package/dist/brain/workflow-skills.d.ts.map +1 -1
- package/dist/brain/workflow-skills.js +3 -2
- package/dist/brain/workflow-skills.js.map +1 -1
- package/dist/cli.js +40 -4
- package/dist/cli.js.map +1 -1
- package/dist/context-cli.d.ts +3 -0
- package/dist/context-cli.d.ts.map +1 -0
- package/dist/context-cli.js +90 -0
- package/dist/context-cli.js.map +1 -0
- package/dist/curation-cli.d.ts +4 -0
- package/dist/curation-cli.d.ts.map +1 -0
- package/dist/curation-cli.js +118 -0
- package/dist/curation-cli.js.map +1 -0
- package/dist/dashboard/brain-hub-actions.d.ts +2 -0
- package/dist/dashboard/brain-hub-actions.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-actions.js +137 -0
- package/dist/dashboard/brain-hub-actions.js.map +1 -0
- package/dist/dashboard/brain-hub-client.d.ts +2 -0
- package/dist/dashboard/brain-hub-client.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-client.js +335 -0
- package/dist/dashboard/brain-hub-client.js.map +1 -0
- package/dist/dashboard/brain-hub-graph.d.ts +2 -0
- package/dist/dashboard/brain-hub-graph.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-graph.js +176 -0
- package/dist/dashboard/brain-hub-graph.js.map +1 -0
- package/dist/dashboard/brain-hub-html.d.ts +2 -0
- package/dist/dashboard/brain-hub-html.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-html.js +30 -0
- package/dist/dashboard/brain-hub-html.js.map +1 -0
- package/dist/dashboard/brain-hub-project-views.d.ts +2 -0
- package/dist/dashboard/brain-hub-project-views.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-project-views.js +79 -0
- package/dist/dashboard/brain-hub-project-views.js.map +1 -0
- package/dist/dashboard/brain-hub-views.d.ts +2 -0
- package/dist/dashboard/brain-hub-views.d.ts.map +1 -0
- package/dist/dashboard/brain-hub-views.js +150 -0
- package/dist/dashboard/brain-hub-views.js.map +1 -0
- package/dist/dashboard/demo-workspace.d.ts +11 -0
- package/dist/dashboard/demo-workspace.d.ts.map +1 -0
- package/dist/dashboard/demo-workspace.js +178 -0
- package/dist/dashboard/demo-workspace.js.map +1 -0
- package/dist/dashboard/live-workspace-html.d.ts +2 -0
- package/dist/dashboard/live-workspace-html.d.ts.map +1 -0
- package/dist/dashboard/live-workspace-html.js +1042 -0
- package/dist/dashboard/live-workspace-html.js.map +1 -0
- package/dist/dashboard/project-feedback-html.d.ts +5 -0
- package/dist/dashboard/project-feedback-html.d.ts.map +1 -0
- package/dist/dashboard/project-feedback-html.js +216 -0
- package/dist/dashboard/project-feedback-html.js.map +1 -0
- package/dist/dashboard/project-hub-api.d.ts +5 -0
- package/dist/dashboard/project-hub-api.d.ts.map +1 -0
- package/dist/dashboard/project-hub-api.js +121 -0
- package/dist/dashboard/project-hub-api.js.map +1 -0
- package/dist/dashboard/project-kit-html.d.ts +4 -0
- package/dist/dashboard/project-kit-html.d.ts.map +1 -0
- package/dist/dashboard/project-kit-html.js +359 -0
- package/dist/dashboard/project-kit-html.js.map +1 -0
- package/dist/dashboard/project-workspace.d.ts +16 -0
- package/dist/dashboard/project-workspace.d.ts.map +1 -0
- package/dist/dashboard/project-workspace.js +106 -0
- package/dist/dashboard/project-workspace.js.map +1 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +65 -19
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/v6-dashboard-html.d.ts +1 -1
- package/dist/dashboard/v6-dashboard-html.d.ts.map +1 -1
- package/dist/dashboard/v6-dashboard-html.js +34 -56
- package/dist/dashboard/v6-dashboard-html.js.map +1 -1
- package/dist/dashboard/workspace-api.d.ts +22 -0
- package/dist/dashboard/workspace-api.d.ts.map +1 -0
- package/dist/dashboard/workspace-api.js +429 -0
- package/dist/dashboard/workspace-api.js.map +1 -0
- package/dist/dashboard/workspace-html.d.ts +2 -0
- package/dist/dashboard/workspace-html.d.ts.map +1 -0
- package/dist/dashboard/workspace-html.js +2538 -0
- package/dist/dashboard/workspace-html.js.map +1 -0
- package/dist/dashboard/workspace-status.d.ts +50 -0
- package/dist/dashboard/workspace-status.d.ts.map +1 -0
- package/dist/dashboard/workspace-status.js +22 -0
- package/dist/dashboard/workspace-status.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-cli.d.ts +3 -0
- package/dist/integration-cli.d.ts.map +1 -0
- package/dist/integration-cli.js +16 -0
- package/dist/integration-cli.js.map +1 -0
- package/dist/project-cli.d.ts +3 -0
- package/dist/project-cli.d.ts.map +1 -0
- package/dist/project-cli.js +100 -0
- package/dist/project-cli.js.map +1 -0
- package/dist/types.d.ts +12 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/COMPARISON.md +22 -0
- package/docs/COMPONENTS.md +66 -0
- package/docs/CONTEXT_LAB.md +79 -0
- package/docs/FEATURES_AND_LIMITS.md +24 -0
- package/docs/INTEGRATIONS.md +68 -0
- package/docs/LEARNING.md +46 -0
- package/docs/PROJECT_KIT.md +93 -0
- package/docs/QUICKSTART.md +44 -0
- package/docs/README.md +33 -0
- package/docs/ROADMAP-v7.md +147 -0
- package/docs/SHARING.md +13 -0
- package/docs/SHOWCASE.md +87 -0
- package/docs/STATS.md +21 -0
- package/docs/TROUBLESHOOTING.md +42 -0
- package/docs/UPGRADE_PLAN_v7.0.0.md +203 -0
- package/docs/VIRAL-PLAYBOOK.md +259 -0
- package/docs/VISUAL_GUIDE_v7.1.4.md +70 -0
- package/docs/WORKFLOW_INTEGRATION.md +19 -1
- package/docs/WORKSPACE.md +37 -0
- package/docs/assets/7.1.4-collaboration-design-preview.png +0 -0
- package/docs/assets/7.1.4-comparison.png +0 -0
- package/docs/assets/7.1.4-context-lab.png +0 -0
- package/docs/assets/7.1.4-control-room-dense-preview.png +0 -0
- package/docs/assets/7.1.4-control-room-focus-preview.png +0 -0
- package/docs/assets/7.1.4-features.png +0 -0
- package/docs/assets/7.1.4-graph.png +0 -0
- package/docs/assets/7.1.4-hub-control-room.png +0 -0
- package/docs/assets/7.1.4-hub-empty.png +0 -0
- package/docs/assets/7.1.4-hub-knowledge.png +0 -0
- package/docs/assets/7.1.4-hub-learning.png +0 -0
- package/docs/assets/7.1.4-hub-mobile.png +0 -0
- package/docs/assets/7.1.4-hub-public-docs-source.png +0 -0
- package/docs/assets/7.1.4-hub-public-docs.gif +0 -0
- package/docs/assets/7.1.4-hub-public-docs.png +0 -0
- package/docs/assets/7.1.4-hub-setup.png +0 -0
- package/docs/assets/7.1.4-hub-walkthrough.gif +0 -0
- package/docs/assets/7.1.4-integrations.png +0 -0
- package/docs/assets/7.1.4-knowledge-design-preview.png +0 -0
- package/docs/assets/7.1.4-learning-design-preview.png +0 -0
- package/docs/assets/7.1.4-learning.png +0 -0
- package/docs/assets/7.1.4-live.png +0 -0
- package/docs/assets/7.1.4-memory-review.png +0 -0
- package/docs/assets/7.1.4-mobile.png +0 -0
- package/docs/assets/7.1.4-models-design-preview.png +0 -0
- package/docs/assets/7.1.4-outcome-ranking.png +0 -0
- package/docs/assets/7.1.4-overview.png +0 -0
- package/docs/assets/7.1.4-project-kit.png +0 -0
- package/docs/assets/7.1.4-project-skill-preview.png +0 -0
- package/docs/assets/7.1.4-replay.png +0 -0
- package/docs/assets/7.1.4-setup-design-preview.png +0 -0
- package/docs/assets/7.1.4-usage-design-preview.png +0 -0
- package/docs/assets/context-flow-v7.2.png +0 -0
- package/docs/assets/context-v7.1.3.png +0 -0
- package/docs/assets/features-v7.1.3.png +0 -0
- package/docs/assets/graph-v7.1.3.png +0 -0
- package/docs/assets/integrations-v7.1.3.png +0 -0
- package/docs/assets/live-v7.1.3.png +0 -0
- package/docs/assets/live-v7.1.4.gif +0 -0
- package/docs/assets/memory-detail-v7.1.3.png +0 -0
- package/docs/assets/overview-v7.1.3.png +0 -0
- package/docs/assets/workspace-graph-v7.2.png +0 -0
- package/docs/assets/workspace-overview-v7.2.png +0 -0
- package/docs/evaluation/CONTEXT_EVALUATION_v7.1.4.json +1818 -0
- package/docs/evaluation/CONTEXT_EVALUATION_v7.1.4.md +36 -0
- package/docs/launch/COMPONENT_MAP_v7.1.4.md +307 -0
- package/docs/launch/PROJECT_KIT_PLAN_v7.1.4.md +110 -0
- package/docs/launch/PROJECT_LEARNING_PLAN_v7.1.4.md +63 -0
- package/docs/launch/RELEASE_NOTES_v7.1.3.md +40 -0
- package/docs/launch/RELEASE_NOTES_v7.1.4.md +30 -0
- package/docs/launch/RELEASE_NOTES_v7.2.0.md +24 -0
- package/docs/launch/RESEARCH_AND_PLAN_v7.1.4.md +255 -0
- package/package.json +43 -126
- package/skills/shadow-research/SKILL.md +2 -0
- package/skills/shadow-workflow/SKILL.md +6 -0
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
// Local, read-only operations console for the Shadow Brain event journal.
|
|
2
|
+
export const LIVE_WORKSPACE_HTML = String.raw `<!doctype html>
|
|
3
|
+
<html lang="en" data-motion="system">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<meta name="color-scheme" content="dark">
|
|
8
|
+
<title>Live operations · Agent Shadow Brain</title>
|
|
9
|
+
<script>
|
|
10
|
+
try {
|
|
11
|
+
if (localStorage.getItem('shadow-brain:workspace:motion') === 'reduce') document.documentElement.dataset.motion = 'reduce';
|
|
12
|
+
} catch {}
|
|
13
|
+
</script>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
color-scheme: dark;
|
|
17
|
+
--canvas: #101923; --header: #131e2a; --surface: #162230; --inset: #111d29; --hover: #213345;
|
|
18
|
+
--line: #314153; --ink: #edf5fb; --muted: #aabed1; --control: #738ba1; --focus: #9bdff3;
|
|
19
|
+
--cyan: #86dbe3; --cyan-ink: #102f38; --cyan-tint: #1d3b46;
|
|
20
|
+
--amber: #f5ce91; --amber-tint: #3a3027; --rose: #ffacba; --rose-tint: #432936;
|
|
21
|
+
--node: #203b4c; --graph-line: #567589; --grid: #2c4153;
|
|
22
|
+
--ease: 190ms cubic-bezier(.2,.75,.25,1); --radius: 9px;
|
|
23
|
+
font: 15px/1.5 Inter, "Segoe UI", system-ui, sans-serif;
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
[hidden] { display: none !important; }
|
|
27
|
+
body { margin: 0; background: var(--canvas); color: var(--ink); }
|
|
28
|
+
h1,h2,h3,p,ul,dl,dd { margin: 0; }
|
|
29
|
+
h1,h2,h3 { line-height: 1.22; text-wrap: balance; overflow-wrap: anywhere; }
|
|
30
|
+
h1 { font-size: 2rem; letter-spacing: -.035em; font-weight: 710; }
|
|
31
|
+
h2 { font-size: 1.1rem; font-weight: 680; letter-spacing: -.013em; }
|
|
32
|
+
h3 { font-size: .94rem; font-weight: 650; }
|
|
33
|
+
p { overflow-wrap: anywhere; max-width: 69ch; }
|
|
34
|
+
a { color: var(--cyan); text-decoration: none; }
|
|
35
|
+
a:hover { text-decoration: underline; }
|
|
36
|
+
:is(button,a,select,summary,[tabindex]):focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
|
|
37
|
+
.muted { color: var(--muted); }
|
|
38
|
+
.small { font-size: .86rem; }
|
|
39
|
+
.mono { font: .82rem/1.45 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; }
|
|
40
|
+
.icon { width: 18px; height: 18px; flex: none; vertical-align: middle; }
|
|
41
|
+
.icon.compact { width: 15px; height: 15px; }
|
|
42
|
+
.skip { position: fixed; top: 7px; left: 8px; z-index: 3; transform: translateY(-150%); }
|
|
43
|
+
.skip:focus { transform: none; }
|
|
44
|
+
.topbar { background: var(--header); border-bottom: 1px solid var(--line); }
|
|
45
|
+
.top-inner { max-width: 1560px; margin: 0 auto; min-height: 59px; padding: 9px 27px; display: flex; align-items: center; gap: 22px; }
|
|
46
|
+
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font-weight: 690; white-space: nowrap; }
|
|
47
|
+
.brand:hover { text-decoration: none; }
|
|
48
|
+
.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; background: var(--cyan-tint); color: var(--cyan); border-radius: 7px; }
|
|
49
|
+
.main-nav { display: flex; align-items: center; gap: 7px; }
|
|
50
|
+
.main-nav a { display: inline-flex; min-height: 34px; align-items: center; padding: 5px 9px; border-radius: 5px; color: var(--muted); font-size: .88rem; }
|
|
51
|
+
.main-nav a[aria-current="page"] { color: var(--cyan); background: var(--cyan-tint); font-weight: 650; }
|
|
52
|
+
.top-state { display: flex; align-items: center; gap: 10px; margin-left: auto; }
|
|
53
|
+
.tag { display: inline-flex; align-items: center; gap: 6px; font-size: .77rem; font-weight: 620; line-height: 1.25; padding: 4px 7px; border-radius: 5px; background: var(--hover); color: var(--muted); max-width: 100%; overflow-wrap: anywhere; }
|
|
54
|
+
.tag.ok { background: var(--cyan-tint); color: var(--cyan); }
|
|
55
|
+
.tag.warn { background: var(--amber-tint); color: var(--amber); }
|
|
56
|
+
.tag.bad { background: var(--rose-tint); color: var(--rose); }
|
|
57
|
+
.dot { display: inline-block; width: 7px; height: 7px; background: currentColor; border-radius: 50%; flex: none; }
|
|
58
|
+
main { max-width: 1560px; padding: 26px 27px 44px; margin: 0 auto; min-width: 0; }
|
|
59
|
+
.head { display: flex; justify-content: space-between; align-items: start; gap: 15px; margin-bottom: 17px; }
|
|
60
|
+
.head p { margin-top: 6px; }
|
|
61
|
+
.controls { display: flex; flex-wrap: wrap; justify-content: end; gap: 7px; align-items: end; }
|
|
62
|
+
.button,select {
|
|
63
|
+
color: var(--ink); background: var(--surface); border: 1px solid var(--control); border-radius: 6px;
|
|
64
|
+
min-height: 36px; padding: 6px 10px; font: inherit; font-size: .85rem; font-weight: 650; transition: background var(--ease), border-color var(--ease);
|
|
65
|
+
}
|
|
66
|
+
.button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; line-height: 1.2; }
|
|
67
|
+
.button:hover, select:hover { background: var(--hover); text-decoration: none; }
|
|
68
|
+
.button:disabled { opacity: .56; cursor: default; }
|
|
69
|
+
.button.primary { border-color: var(--cyan); color: var(--cyan-ink); background: var(--cyan); }
|
|
70
|
+
.button.primary:hover { background: #b3ebee; }
|
|
71
|
+
.button.quiet { background: transparent; border-color: transparent; color: var(--cyan); }
|
|
72
|
+
.button.quiet:hover, .button[aria-pressed="true"] { background: var(--cyan-tint); }
|
|
73
|
+
.label { display: grid; gap: 3px; min-width: 0; font-size: .79rem; color: var(--muted); font-weight: 630; }
|
|
74
|
+
.notice { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 9px 12px; min-height: 40px; border-radius: 6px; border: 1px solid var(--line); background: var(--inset); color: var(--muted); font-size: .84rem; margin-bottom: 14px; }
|
|
75
|
+
.notice.error { background: var(--rose-tint); border-color: transparent; color: var(--rose); }
|
|
76
|
+
.stats { display: grid; grid-template-columns: repeat(5,minmax(0,1fr)); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); margin-bottom: 15px; }
|
|
77
|
+
.stat { display: grid; gap: 4px; min-width: 0; padding: 13px 15px; }
|
|
78
|
+
.stat + .stat { border-left: 1px solid var(--line); }
|
|
79
|
+
.stat dt { color: var(--muted); font-size: .82rem; font-weight: 590; }
|
|
80
|
+
.stat dd { font-size: 1.37rem; font-variant-numeric: tabular-nums; font-weight: 710; line-height: 1.15; overflow-wrap: anywhere; }
|
|
81
|
+
.stat .danger { color: var(--rose); }
|
|
82
|
+
.panel { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-width: 0; overflow: hidden; }
|
|
83
|
+
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 55px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
|
|
84
|
+
.panel-head p { margin-top: 3px; }
|
|
85
|
+
.panel-body { padding: 12px 14px; min-width: 0; }
|
|
86
|
+
.blank { min-height: 112px; display: grid; align-content: start; gap: 6px; padding: 17px 14px; color: var(--muted); }
|
|
87
|
+
.blank h3 { color: var(--ink); }
|
|
88
|
+
.blank .icon { color: var(--cyan); margin-bottom: 3px; }
|
|
89
|
+
.ops-grid { display: grid; grid-template-columns: minmax(190px,.72fr) minmax(0,2.55fr) minmax(233px,.95fr); gap: 14px; margin-bottom: 15px; align-items: stretch; }
|
|
90
|
+
.list { list-style: none; padding: 0; }
|
|
91
|
+
.list > li + li { border-top: 1px solid var(--line); }
|
|
92
|
+
.session-list, .inspect-body { max-height: 445px; overflow: auto; }
|
|
93
|
+
.session-row { width: 100%; border: 0; background: transparent; color: var(--ink); display: grid; grid-template-columns: 29px minmax(0,1fr); align-items: start; text-align: left; gap: 8px; padding: 11px 12px; font: inherit; cursor: pointer; }
|
|
94
|
+
.session-row:hover, .session-row[aria-pressed="true"] { background: var(--hover); }
|
|
95
|
+
.session-row[aria-pressed="true"] .session-glyph { background: var(--cyan-tint); color: var(--cyan); }
|
|
96
|
+
.session-glyph { width: 29px; height: 29px; border-radius: 6px; background: var(--inset); color: var(--muted); display: grid; place-items: center; }
|
|
97
|
+
.session-label { display: block; font-weight: 630; overflow-wrap: anywhere; }
|
|
98
|
+
.session-detail { display: grid; gap: 4px; justify-items: start; min-width: 0; }
|
|
99
|
+
.network-body { min-height: 405px; position: relative; display: grid; align-items: center; background: var(--inset); }
|
|
100
|
+
#network-svg { display: block; width: 100%; height: 405px; }
|
|
101
|
+
.topology-ring { fill: none; stroke: var(--graph-line); stroke-width: 1; stroke-opacity: .29; stroke-dasharray: 2 5; }
|
|
102
|
+
.network-edge { fill: none; stroke: var(--graph-line); stroke-width: 1.2; opacity: .62; transition: opacity var(--ease), stroke var(--ease); }
|
|
103
|
+
.network-edge.active { stroke: var(--cyan); stroke-width: 1.8; opacity: .9; }
|
|
104
|
+
.network-edge.dimmed { opacity: .3; }
|
|
105
|
+
.network-edge.reported { stroke: var(--amber); stroke-dasharray: 3 4; }
|
|
106
|
+
.network-node { cursor: pointer; color: var(--muted); }
|
|
107
|
+
.network-node .node-surface { fill: var(--node); stroke: var(--graph-line); stroke-width: 1.25; }
|
|
108
|
+
.network-node .node-indicator { fill: currentColor; }
|
|
109
|
+
.network-node.live { color: var(--cyan); }
|
|
110
|
+
.network-node.reported { color: var(--amber); }
|
|
111
|
+
.network-node.failed { color: var(--rose); }
|
|
112
|
+
.network-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { stroke: currentColor; stroke-width: 2.4; }
|
|
113
|
+
.network-node:focus-visible { outline: none; }
|
|
114
|
+
.network-node text, .network-core text { fill: var(--ink); font-size: 12px; font-weight: 650; pointer-events: none; }
|
|
115
|
+
.network-node .node-subtitle, .network-core .node-subtitle { fill: var(--muted); font-size: 10px; font-weight: 550; }
|
|
116
|
+
.network-core circle { fill: var(--cyan-tint); stroke: var(--cyan); stroke-width: 1.4; }
|
|
117
|
+
.network-core .core-inner { fill: var(--surface); stroke: var(--graph-line); }
|
|
118
|
+
.pulse { pointer-events: none; fill: var(--cyan); }
|
|
119
|
+
.pulse.reported { fill: var(--amber); }
|
|
120
|
+
.pulse.failed { fill: var(--rose); }
|
|
121
|
+
.network-caption { padding: 8px 13px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 5px 12px; color: var(--muted); font-size: .78rem; }
|
|
122
|
+
.network-caption span { display: inline-flex; align-items: center; gap: 5px; }
|
|
123
|
+
.legend-line { width: 17px; border-top: 2px solid var(--cyan); }
|
|
124
|
+
.legend-line.reported { border-color: var(--amber); border-top-style: dashed; }
|
|
125
|
+
.inspect-body .panel-body { display: grid; gap: 10px; }
|
|
126
|
+
.facts { display: grid; gap: 7px; }
|
|
127
|
+
.facts div { display: flex; align-items: start; justify-content: space-between; gap: 8px; }
|
|
128
|
+
.facts dt { color: var(--muted); font-size: .83rem; }
|
|
129
|
+
.facts dd { text-align: right; font-size: .83rem; overflow-wrap: anywhere; }
|
|
130
|
+
.stages { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
131
|
+
.stage { border: 1px solid var(--line); color: var(--muted); border-radius: 4px; padding: 4px 6px; font-size: .78rem; }
|
|
132
|
+
.stage.seen { border-color: var(--amber); color: var(--amber); }
|
|
133
|
+
.stage.current { background: var(--amber-tint); font-weight: 680; }
|
|
134
|
+
.charts { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 14px; margin-bottom: 15px; }
|
|
135
|
+
.chart-svg { width: 100%; height: 150px; display: block; }
|
|
136
|
+
.chart-grid { stroke: var(--line); stroke-width: 1; }
|
|
137
|
+
.chart-label { fill: var(--muted); font-size: 11px; }
|
|
138
|
+
.chart-bar { fill: var(--cyan); }
|
|
139
|
+
.chart-error { fill: var(--rose); }
|
|
140
|
+
.chart-duration { stroke: var(--amber); fill: none; stroke-width: 2; }
|
|
141
|
+
.chart-point { fill: var(--amber); }
|
|
142
|
+
.chart-data summary { cursor: pointer; color: var(--cyan); }
|
|
143
|
+
.chart-data table { border-collapse: collapse; width: 100%; margin-top: 6px; font-size: .82rem; }
|
|
144
|
+
.chart-data th,.chart-data td { text-align: left; padding: 4px 7px; border-bottom: 1px solid var(--line); }
|
|
145
|
+
.events { max-height: 550px; overflow: auto; }
|
|
146
|
+
.event-row { display: grid; grid-template-columns: 105px minmax(135px,.7fr) minmax(0,1.2fr) auto; gap: 9px; align-items: start; padding: 9px 14px; }
|
|
147
|
+
.event-row.new { animation: arrive var(--ease) both; }
|
|
148
|
+
.event-meta { overflow-wrap: anywhere; }
|
|
149
|
+
.event-title { font-weight: 630; overflow-wrap: anywhere; }
|
|
150
|
+
.event-link { border: 0; background: transparent; color: var(--ink); padding: 0; text-align: left; font: inherit; cursor: pointer; }
|
|
151
|
+
.event-link:hover, .event-link[aria-pressed="true"] { color: var(--cyan); text-decoration: underline; }
|
|
152
|
+
.event-row .tag { justify-self: end; }
|
|
153
|
+
h1 { font-size: 2.25rem; }
|
|
154
|
+
.stats { background: #172b38; }
|
|
155
|
+
.stat { padding: 15px 16px; }
|
|
156
|
+
.stat dd { font-size: 1.55rem; }
|
|
157
|
+
.stat:first-child dd { color: var(--cyan); }
|
|
158
|
+
.network-body { background: radial-gradient(ellipse at 50% 50%, #1c3c4b, #121f2d 64%); }
|
|
159
|
+
.network-panel { border-color: #456679; }
|
|
160
|
+
.topology-ring { stroke-opacity: .44; }
|
|
161
|
+
.network-core .core-ring { fill: none; stroke: var(--cyan); stroke-width: 1; stroke-opacity: .36; stroke-dasharray: 2 5; }
|
|
162
|
+
.network-edge.failed { stroke: var(--rose); }
|
|
163
|
+
.network-edge.active.failed { stroke: var(--rose); }
|
|
164
|
+
.network-edge.active.reported { stroke: var(--amber); }
|
|
165
|
+
.network-node.selected-event .node-surface { stroke-width: 2.6; stroke: currentColor; }
|
|
166
|
+
.session-row[aria-pressed="true"] { box-shadow: inset 0 0 0 1px #526e80; }
|
|
167
|
+
.legend-line.failed { border-color: var(--rose); }
|
|
168
|
+
.replay-bar { display: grid; grid-template-columns: auto minmax(150px,1fr) auto; align-items: end; gap: 11px; margin-bottom: 15px; padding: 12px 15px; background: #192d3b; }
|
|
169
|
+
.replay-bar h2 { color: var(--cyan); margin-bottom: 4px; }
|
|
170
|
+
.replay-bar .replay-caption { grid-column: 1 / -1; color: var(--muted); }
|
|
171
|
+
.replay-scrub { display: grid; gap: 3px; min-width: 0; }
|
|
172
|
+
.replay-scrub label { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 4px; color: var(--muted); }
|
|
173
|
+
.replay-scrub input { width: 100%; min-height: 30px; margin: 0; accent-color: var(--cyan); }
|
|
174
|
+
.replay-buttons { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
175
|
+
.replay-buttons .icon-button { width: 36px; padding: 0; }
|
|
176
|
+
.demo-notice { display: none; }
|
|
177
|
+
html[data-demo="true"] .demo-notice { display: flex; gap: 7px 10px; flex-wrap: wrap; padding: 8px 12px; margin-bottom: 14px; background: var(--amber-tint); color: var(--amber); border-radius: 7px; font-size: .83rem; }
|
|
178
|
+
.project-journal-notice { display: none; }
|
|
179
|
+
html[data-project-local="true"] .project-journal-notice { display: flex; gap: 4px 10px; flex-wrap: wrap; padding: 8px 12px; margin-bottom: 14px; background: var(--cyan-tint); color: var(--cyan); border-radius: 7px; font-size: .83rem; }
|
|
180
|
+
.project-journal-notice a { text-decoration: underline; text-underline-offset: 3px; }
|
|
181
|
+
.event-row[data-selected="true"] { background: var(--cyan-tint); }
|
|
182
|
+
.event-row[data-selected="true"] .event-link { color: var(--cyan); }
|
|
183
|
+
@keyframes arrive { from { background: var(--cyan-tint); opacity: .8; } to { background: transparent; opacity: 1; } }
|
|
184
|
+
html[data-motion="reduce"] *, html[data-motion="reduce"] *::before, html[data-motion="reduce"] *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
185
|
+
@media (prefers-reduced-motion: reduce) {
|
|
186
|
+
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
187
|
+
}
|
|
188
|
+
@media (max-width: 1150px) {
|
|
189
|
+
.ops-grid { grid-template-columns: minmax(175px,.7fr) minmax(0,2.3fr); }
|
|
190
|
+
.inspect-panel { grid-column: 1 / -1; }
|
|
191
|
+
.inspect-body { max-height: 300px; }
|
|
192
|
+
}
|
|
193
|
+
@media (max-width: 750px) {
|
|
194
|
+
:root { font-size: 14px; }
|
|
195
|
+
.top-inner,main { padding-left: 12px; padding-right: 12px; }
|
|
196
|
+
.top-inner { gap: 9px; flex-wrap: wrap; }
|
|
197
|
+
.main-nav { gap: 2px; }
|
|
198
|
+
.top-version { display: none; }
|
|
199
|
+
.head { flex-wrap: wrap; }
|
|
200
|
+
h1 { font-size: 1.82rem; }
|
|
201
|
+
.controls { justify-content: start; }
|
|
202
|
+
.replay-bar { grid-template-columns: minmax(0,1fr); }
|
|
203
|
+
.stats { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
|
204
|
+
.stat + .stat { border-left: 0; }
|
|
205
|
+
.stat:nth-child(even) { border-left: 1px solid var(--line); }
|
|
206
|
+
.stat:nth-child(n+3) { border-top: 1px solid var(--line); }
|
|
207
|
+
.stat:last-child { grid-column: 1 / -1; }
|
|
208
|
+
.ops-grid,.charts { grid-template-columns: minmax(0,1fr); }
|
|
209
|
+
.network-panel { grid-row: 1; }
|
|
210
|
+
.network-body { min-height: 280px; }
|
|
211
|
+
#network-svg { height: 280px; }
|
|
212
|
+
.session-list,.inspect-body { max-height: 320px; }
|
|
213
|
+
.event-row { grid-template-columns: minmax(0,1fr) auto; }
|
|
214
|
+
.event-row .event-meta { grid-column: 1; }
|
|
215
|
+
.event-row .tag { grid-column: 2; grid-row: 1; }
|
|
216
|
+
.event-row .event-title { grid-column: 1 / -1; }
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
219
|
+
</head>
|
|
220
|
+
<body>
|
|
221
|
+
<a class="skip button" href="#live-main">Skip to content</a>
|
|
222
|
+
<header class="topbar">
|
|
223
|
+
<div class="top-inner">
|
|
224
|
+
<a class="brand" href="/" aria-label="Agent Shadow Brain workspace"><span class="brand-mark" data-icon="brand"></span><span>Agent Shadow Brain</span></a>
|
|
225
|
+
<nav class="main-nav" aria-label="Navigation"><a href="/">Workspace</a><a href="/live" aria-current="page">Live</a></nav>
|
|
226
|
+
<div class="top-state"><span class="small muted mono top-version">7.1.4</span><span id="demo-tag" class="tag warn" hidden>Synthetic demo</span><span id="stream-tag" class="tag"><span class="dot"></span><span id="stream-label">Connecting</span></span></div>
|
|
227
|
+
</div>
|
|
228
|
+
</header>
|
|
229
|
+
<main id="live-main" tabindex="-1">
|
|
230
|
+
<div class="demo-notice"><strong>Synthetic demo</strong><span>Scripted local MCP calls use sample data. Changes affect this temporary demo only.</span></div>
|
|
231
|
+
<div class="project-journal-notice"><strong>Project journal</strong><span>Live activity comes from this project’s local store.</span><a href="/#project-kit">Open Project Kit</a></div>
|
|
232
|
+
<div class="head">
|
|
233
|
+
<div><h1>Live operations</h1><p class="muted small">Follow recorded agent sessions, tool calls, and reported task phases.</p></div>
|
|
234
|
+
<div class="controls" aria-label="Live view controls">
|
|
235
|
+
<label for="time-window" class="label">Window<select id="time-window"><option value="1">1 minute</option><option value="5">5 minutes</option><option value="15" selected>15 minutes</option><option value="60">1 hour</option></select></label>
|
|
236
|
+
<button class="button" type="button" id="motion-control" aria-pressed="false"><span data-icon="motion"></span>Reduce motion</button>
|
|
237
|
+
<button class="button" type="button" id="refresh"><span data-icon="refresh"></span>Refresh</button>
|
|
238
|
+
<button class="button" type="button" id="replay-start" disabled><span data-icon="replay"></span>Replay loaded events</button>
|
|
239
|
+
<button class="button primary" type="button" id="pause"><span data-icon="pause"></span><span id="pause-label">Pause stream</span></button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="notice" id="stream-notice" role="status">Connecting to the local event journal…</div>
|
|
243
|
+
<section class="panel replay-bar" id="replay-bar" aria-labelledby="replay-heading" hidden>
|
|
244
|
+
<div><h2 id="replay-heading" tabindex="-1">Journal replay</h2><div class="replay-buttons" aria-label="Replay controls">
|
|
245
|
+
<button class="button icon-button" type="button" id="replay-prev" aria-label="Previous event" title="Previous event"><span data-icon="prev"></span></button>
|
|
246
|
+
<button class="button" type="button" id="replay-play">Play</button>
|
|
247
|
+
<button class="button icon-button" type="button" id="replay-next" aria-label="Next event" title="Next event"><span data-icon="next"></span></button>
|
|
248
|
+
</div></div>
|
|
249
|
+
<div class="replay-scrub"><label for="replay-range"><span>Event position</span><output for="replay-range" class="small mono" id="replay-position" aria-live="off"></output></label>
|
|
250
|
+
<input type="range" min="0" max="0" value="0" id="replay-range" aria-describedby="replay-caption"></div>
|
|
251
|
+
<button class="button primary" type="button" id="replay-exit">Return to live</button>
|
|
252
|
+
<p class="small replay-caption" id="replay-caption"></p>
|
|
253
|
+
</section>
|
|
254
|
+
<dl class="stats" id="live-stats" aria-label="Tool metrics in the selected window"></dl>
|
|
255
|
+
<div class="ops-grid">
|
|
256
|
+
<section class="panel">
|
|
257
|
+
<div class="panel-head"><h2>Sessions</h2><span id="session-count" class="tag">0</span></div>
|
|
258
|
+
<div id="sessions" class="session-list"></div>
|
|
259
|
+
</section>
|
|
260
|
+
<section class="panel network-panel">
|
|
261
|
+
<div class="panel-head"><div><h2>Activity network</h2><p class="small muted" id="network-count">Recorded session activity</p></div><span id="network-status" class="tag"></span></div>
|
|
262
|
+
<div class="network-body"><svg id="network-svg" viewBox="0 0 720 405" role="group" aria-label="Activity network. Use arrow keys to navigate sessions."></svg></div>
|
|
263
|
+
<div class="network-caption"><span><i class="legend-line"></i>Measured activity</span><span><i class="legend-line reported"></i>Host-reported activity</span><span><i class="legend-line failed"></i>Error or cancellation</span></div>
|
|
264
|
+
</section>
|
|
265
|
+
<section class="panel inspect-panel">
|
|
266
|
+
<div class="panel-head"><h2>Session details</h2></div><div id="inspection" class="inspect-body"></div>
|
|
267
|
+
</section>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="charts">
|
|
270
|
+
<section class="panel"><div class="panel-head"><div><h2>Tool throughput</h2><p class="small muted">Recorded tool results per interval</p></div></div><div class="panel-body" id="throughput"></div></section>
|
|
271
|
+
<section class="panel"><div class="panel-head"><div><h2>Tool duration</h2><p class="small muted">Median measured duration per interval</p></div></div><div class="panel-body" id="duration"></div></section>
|
|
272
|
+
</div>
|
|
273
|
+
<section class="panel">
|
|
274
|
+
<div class="panel-head"><div><h2>Event timeline</h2><p class="small muted" id="timeline-scope">All sessions</p></div><div class="controls">
|
|
275
|
+
<label class="label" for="event-filter">Event source<select id="event-filter"><option value="">All sources</option><option value="measured">Measured</option><option value="reported">Reported</option></select></label>
|
|
276
|
+
<label class="label" for="transport-filter">Transport<select id="transport-filter"><option value="">All transports</option><option value="mcp">MCP</option><option value="cli">CLI</option><option value="host">Host</option></select></label>
|
|
277
|
+
<button class="button quiet" type="button" id="clear-session" hidden>Show all sessions</button></div>
|
|
278
|
+
</div><div id="events" class="events"></div>
|
|
279
|
+
</section>
|
|
280
|
+
</main>
|
|
281
|
+
<script>
|
|
282
|
+
'use strict';
|
|
283
|
+
const $ = (id) => document.getElementById(id);
|
|
284
|
+
const glyphs = {
|
|
285
|
+
brand: '<path d="M12 5 5 9v6l7 4 7-4V9l-7-4v7m-7-3 7 3 7-3m-7 3v7"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/>',
|
|
286
|
+
motion: '<path d="M3 8h14M5 12h16M3 16h12"/><circle cx="19" cy="8" r="1.4"/><circle cx="17" cy="16" r="1.4"/>',
|
|
287
|
+
refresh: '<path d="M19.5 9a8 8 0 1 0 .3 5.5M20 4v5h-5"/>',
|
|
288
|
+
pause: '<path d="M9 5v14m6-14v14"/>',
|
|
289
|
+
play: '<path d="m8 5 11 7-11 7z"/>',
|
|
290
|
+
replay: '<path d="M4 11a8 8 0 1 1 2 6M4 5v6h6m2-3v5l3 2"/>',
|
|
291
|
+
prev: '<path d="M6 5v14m12-13-9 6 9 6z"/>',
|
|
292
|
+
next: '<path d="M18 5v14M6 6l9 6-9 6z"/>',
|
|
293
|
+
agent: '<rect x="4.5" y="7" width="15" height="12" rx="3"/><path d="M12 7V4m-8 9H2m20 0h-2m-11-.5h.02m5.98 0H15m-5 3h4"/>',
|
|
294
|
+
activity: '<path d="M3 12h4l3-7 4 14 3-7h4"/>',
|
|
295
|
+
warning: '<path d="m12 3 10 17H2L12 3zm0 6v4m0 3h.01"/>'
|
|
296
|
+
};
|
|
297
|
+
const phases = { understand: 'Understand', plan: 'Plan', work: 'Work', verify: 'Verify' };
|
|
298
|
+
const transports = { mcp: 'MCP', cli: 'CLI', host: 'Host' };
|
|
299
|
+
const outcomes = { success: 'Succeeded', error: 'Error', cancelled: 'Cancelled' };
|
|
300
|
+
const names = { codex: 'Codex', 'claude-code': 'Claude Code', opencode: 'OpenCode', deepseek: 'DeepSeek', 'deepseek-harness': 'DeepSeek Harness' };
|
|
301
|
+
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
302
|
+
const icon = (name, compact = false) => '<svg aria-hidden="true" class="icon' + (compact ? ' compact' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round">' + (glyphs[name] || '') + '</svg>';
|
|
303
|
+
const clip = (value, length) => value.length > length ? value.slice(0, length - 1) + '…' : value;
|
|
304
|
+
const number = (value) => Number(value).toLocaleString();
|
|
305
|
+
const clock = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit', second: '2-digit' });
|
|
306
|
+
const tick = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
307
|
+
const tag = (label, kind = '') => '<span class="tag ' + kind + '">' + esc(label) + '</span>';
|
|
308
|
+
const MAX_EVENTS = 500;
|
|
309
|
+
const ACTIVE_AGE_MS = 120_000;
|
|
310
|
+
const motionPreference = matchMedia('(prefers-reduced-motion: reduce)');
|
|
311
|
+
const state = {
|
|
312
|
+
events: [], journalId: null, cursor: null, retained: 0, dropped: 0, gap: false, reset: false,
|
|
313
|
+
loaded: false, paused: false, pending: false, controller: null, problem: '', checkedAt: 0, recording: null, delivery: null, demo: false,
|
|
314
|
+
selected: '', selectedEvent: '', renderKey: '', model: null, newIds: new Set(), replay: null
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
document.querySelectorAll('[data-icon]').forEach((element) => { element.innerHTML = icon(element.dataset.icon); });
|
|
318
|
+
|
|
319
|
+
function labelAgent(value, transport) {
|
|
320
|
+
if (value) return Object.hasOwn(names, value) ? names[value] : value;
|
|
321
|
+
return (transports[transport] || 'Tool') + ' session';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function clockText(value, formatter = clock) {
|
|
325
|
+
const time = typeof value === 'number' ? value : Date.parse(value);
|
|
326
|
+
return Number.isFinite(time) ? formatter.format(time) : '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function timeMarkup(value) {
|
|
330
|
+
const time = Date.parse(value);
|
|
331
|
+
if (!Number.isFinite(time)) return '';
|
|
332
|
+
return '<time class="mono muted" datetime="' + esc(value) + '" title="' + esc(new Date(time).toLocaleString()) + '">' + esc(clockText(time)) + '</time>';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function durationText(value) {
|
|
336
|
+
if (!Number.isFinite(value)) return '—';
|
|
337
|
+
if (value >= 1000) return (value / 1000).toLocaleString(undefined, { maximumFractionDigits: 2 }) + ' s';
|
|
338
|
+
if (value > 0 && value < .01) return '<0.01 ms';
|
|
339
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: value < 1 ? 2 : 1 }) + ' ms';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function median(values) {
|
|
343
|
+
if (!values.length) return null;
|
|
344
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
345
|
+
const middle = Math.floor(sorted.length / 2);
|
|
346
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function blank(image, title, detail) {
|
|
350
|
+
return '<div class="blank">' + icon(image) + '<h3>' + esc(title) + '</h3><p class="small">' + esc(detail) + '</p></div>';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function sessionKey(event) {
|
|
354
|
+
return JSON.stringify([event.projectId || '', event.sessionId || '', event.sessionId ? '' : event.agent || '', event.sessionId ? '' : event.transport]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function spanKey(event) {
|
|
358
|
+
return JSON.stringify([sessionKey(event), event.correlationId]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function describeEvent(event) {
|
|
362
|
+
const tool = event.tool || 'Tool';
|
|
363
|
+
switch (event.kind) {
|
|
364
|
+
case 'session-start': return 'Session started';
|
|
365
|
+
case 'session-end': return 'Session ended';
|
|
366
|
+
case 'phase': return 'Reported phase · ' + (phases[event.phase] || 'Unknown');
|
|
367
|
+
case 'tool-start': return tool + ' started';
|
|
368
|
+
case 'tool-end': return tool + ' · ' + (outcomes[event.outcome] || 'Result recorded');
|
|
369
|
+
default: return 'Journal event';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function eventClass(event) {
|
|
374
|
+
if (event.outcome === 'error' || event.outcome === 'cancelled') return 'failed';
|
|
375
|
+
return event.source === 'reported' ? 'reported' : 'live';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildModel(now, minutes, events = state.events) {
|
|
379
|
+
const start = now - minutes * 60_000;
|
|
380
|
+
const measuredStarts = new Map();
|
|
381
|
+
const matched = [];
|
|
382
|
+
const sessions = new Map();
|
|
383
|
+
const ended = new Set();
|
|
384
|
+
const visible = [];
|
|
385
|
+
let unmatchedEnds = 0;
|
|
386
|
+
for (const event of events) {
|
|
387
|
+
const timestamp = Date.parse(event.timestamp);
|
|
388
|
+
if (!Number.isFinite(timestamp) || timestamp > now) continue;
|
|
389
|
+
const key = sessionKey(event);
|
|
390
|
+
if (event.kind === 'session-end') ended.add(key);
|
|
391
|
+
if (event.kind === 'session-start') ended.delete(key);
|
|
392
|
+
if (event.source === 'measured' && event.correlationId) {
|
|
393
|
+
const correlation = spanKey(event);
|
|
394
|
+
if (event.kind === 'tool-start') measuredStarts.set(correlation, event);
|
|
395
|
+
if (event.kind === 'tool-end') {
|
|
396
|
+
const began = measuredStarts.get(correlation);
|
|
397
|
+
measuredStarts.delete(correlation);
|
|
398
|
+
if (timestamp >= start) {
|
|
399
|
+
if (began) matched.push(event);
|
|
400
|
+
else unmatchedEnds++;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (timestamp < start) continue;
|
|
405
|
+
visible.push(event);
|
|
406
|
+
const session = sessions.get(key) || {
|
|
407
|
+
key, project: event.projectId || '', sessionId: event.sessionId || '', agent: '', transport: event.transport,
|
|
408
|
+
events: [], phases: new Map(), transports: new Set(), results: [], pending: [], stale: 0, latest: null, ended: false
|
|
409
|
+
};
|
|
410
|
+
if (event.agent) session.agent = event.agent;
|
|
411
|
+
if (transports[event.transport]) session.transports.add(transports[event.transport]);
|
|
412
|
+
session.events.push(event);
|
|
413
|
+
session.latest = event;
|
|
414
|
+
if (event.kind === 'phase' && event.source === 'reported' && phases[event.phase]) session.phases.set(event.phase, event);
|
|
415
|
+
sessions.set(key, session);
|
|
416
|
+
}
|
|
417
|
+
matched.forEach((event) => sessions.get(sessionKey(event))?.results.push(event));
|
|
418
|
+
let pending = 0;
|
|
419
|
+
let stale = 0;
|
|
420
|
+
for (const event of measuredStarts.values()) {
|
|
421
|
+
const key = sessionKey(event);
|
|
422
|
+
const session = sessions.get(key);
|
|
423
|
+
if (Date.parse(event.timestamp) < start || !session) continue;
|
|
424
|
+
if (ended.has(key) || now - Date.parse(event.timestamp) > ACTIVE_AGE_MS) {
|
|
425
|
+
session.stale++;
|
|
426
|
+
stale++;
|
|
427
|
+
} else {
|
|
428
|
+
session.pending.push(event);
|
|
429
|
+
pending++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const session of sessions.values()) session.ended = ended.has(session.key);
|
|
433
|
+
const ordered = [...sessions.values()].sort((a, b) => b.latest.sequence - a.latest.sequence);
|
|
434
|
+
const errors = matched.filter((event) => event.outcome === 'error').length;
|
|
435
|
+
const durations = matched.map((event) => event.durationMs).filter(Number.isFinite);
|
|
436
|
+
return { now, start, minutes, visible, sessions: ordered, matched, pending, stale, errors, unmatchedEnds, median: median(durations) };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sessionStatus(session) {
|
|
440
|
+
if (session.pending.length) return ['Awaiting tool result', 'warn'];
|
|
441
|
+
if (session.ended) return ['Ended', ''];
|
|
442
|
+
if (session.latest?.outcome === 'error') return ['Latest result error', 'bad'];
|
|
443
|
+
if (session.stale) return ['Result unavailable', 'warn'];
|
|
444
|
+
if (session.latest?.source === 'reported') return ['Phase or status reported', 'warn'];
|
|
445
|
+
return ['Activity recorded', 'ok'];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function sessionTransports(session) {
|
|
449
|
+
return [...session.transports].sort().join(' · ');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function renderStats(model) {
|
|
453
|
+
const cells = [
|
|
454
|
+
['Observed sessions', model.sessions.length], ['Matched tool results', model.matched.length],
|
|
455
|
+
['Awaiting result', model.pending], ['Tool errors', model.errors], ['Median duration', durationText(model.median)]
|
|
456
|
+
];
|
|
457
|
+
$('live-stats').innerHTML = cells.map(([label, value], index) => '<div class="stat"><dt>' + label + '</dt><dd' +
|
|
458
|
+
(index === 3 && value ? ' class="danger"' : '') + '>' + esc(state.loaded ? typeof value === 'number' ? number(value) : value : '—') + '</dd></div>').join('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderSessions(model) {
|
|
462
|
+
$('session-count').textContent = number(model.sessions.length);
|
|
463
|
+
if (!model.sessions.length) {
|
|
464
|
+
$('sessions').innerHTML = blank('agent', state.loaded ? 'No sessions in this window' : 'Loading sessions',
|
|
465
|
+
state.loaded ? 'Sessions appear when Shadow Brain records a supported action.' : 'Reading retained journal events.');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
$('sessions').innerHTML = '<ul class="list">' + model.sessions.map((session) => {
|
|
469
|
+
const [status, kind] = sessionStatus(session);
|
|
470
|
+
return '<li><button class="session-row" type="button" data-session="' + esc(session.key) + '" aria-pressed="' + (session.key === state.selected) +
|
|
471
|
+
'"><span class="session-glyph">' + icon('agent', true) + '</span><span class="session-detail"><span class="session-label">' +
|
|
472
|
+
esc(labelAgent(session.agent, session.transport)) + '</span><span class="small muted">' +
|
|
473
|
+
esc([session.project, sessionTransports(session)].filter(Boolean).join(' · ')) + '</span>' + tag(status, kind) + '</span></button></li>';
|
|
474
|
+
}).join('') + '</ul>';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function layoutNetwork(sessions) {
|
|
478
|
+
const width = 720;
|
|
479
|
+
const height = 405;
|
|
480
|
+
const core = { x: width / 2, y: height / 2 };
|
|
481
|
+
const ordered = [...sessions].sort((a, b) => a.key.localeCompare(b.key));
|
|
482
|
+
const phase = ordered.length <= 2 ? 0 : Math.PI / 4;
|
|
483
|
+
return { core, nodes: ordered.map((session, index) => {
|
|
484
|
+
const angle = index * Math.PI * 2 / ordered.length + phase;
|
|
485
|
+
const point = { x: core.x + Math.cos(angle) * 202, y: core.y + Math.sin(angle) * 113 };
|
|
486
|
+
const satellites = session.events.slice(-12).map((event, i) => {
|
|
487
|
+
const turn = (i % 6 - 2.5) * .27;
|
|
488
|
+
const ray = angle + turn;
|
|
489
|
+
const distance = i < 6 ? 76 : 96;
|
|
490
|
+
return { event, x: point.x + Math.cos(ray) * distance, y: point.y + Math.sin(ray) * distance * .72 };
|
|
491
|
+
});
|
|
492
|
+
return { session, point, satellites };
|
|
493
|
+
}) };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function pointPath(a, b, control = 0) {
|
|
497
|
+
const x = (a.x + b.x) / 2;
|
|
498
|
+
const y = (a.y + b.y) / 2 - control;
|
|
499
|
+
return 'M' + a.x.toFixed(1) + ' ' + a.y.toFixed(1) + ' Q' + x.toFixed(1) + ' ' + y.toFixed(1) + ' ' +
|
|
500
|
+
b.x.toFixed(1) + ' ' + b.y.toFixed(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function motionAllowed() {
|
|
504
|
+
return document.documentElement.dataset.motion !== 'reduce' && !motionPreference.matches && !state.paused && !state.replay && !document.hidden;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function pulse(path, tone, index) {
|
|
508
|
+
const delay = (index % 5) * .085;
|
|
509
|
+
return '<circle class="pulse ' + tone + '" r="3.9" opacity="0" data-delay="' + delay +
|
|
510
|
+
'"><animate attributeName="opacity" values="0;1;1;0" keyTimes="0;.13;.68;1" dur=".95s" begin="indefinite"/>' +
|
|
511
|
+
'<animateMotion path="' + path + '" dur=".95s" begin="indefinite"/></circle>';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderNetwork(model, animate) {
|
|
515
|
+
const svg = $('network-svg');
|
|
516
|
+
const selectedSession = model.sessions.find((session) => session.key === state.selected);
|
|
517
|
+
const mapSessions = model.sessions.slice(0, 10);
|
|
518
|
+
if (selectedSession && !mapSessions.includes(selectedSession)) mapSessions[mapSessions.length - 1] = selectedSession;
|
|
519
|
+
const { core, nodes } = layoutNetwork(mapSessions);
|
|
520
|
+
const displayedEvents = nodes.reduce((sum, node) => sum + node.satellites.length, 0);
|
|
521
|
+
$('network-count').textContent = displayedEvents ? number(displayedEvents) + ' recent events across ' + number(mapSessions.length) + ' displayed sessions' :
|
|
522
|
+
'Recorded session activity';
|
|
523
|
+
const omitted = model.sessions.length - mapSessions.length;
|
|
524
|
+
$('network-status').textContent = omitted > 0 ? number(omitted) + ' more in Sessions' : state.replay ? 'Replay' : state.paused ? 'Paused' : '';
|
|
525
|
+
$('network-status').hidden = !omitted && !state.paused && !state.replay;
|
|
526
|
+
const first = state.selected || nodes[0]?.session.key;
|
|
527
|
+
const primary = [];
|
|
528
|
+
const links = [];
|
|
529
|
+
const satellites = [];
|
|
530
|
+
const pulses = [];
|
|
531
|
+
const firstEvent = state.selectedEvent || nodes[0]?.satellites[0]?.event.id;
|
|
532
|
+
for (const { session, point, satellites: events } of nodes) {
|
|
533
|
+
const selected = session.key === state.selected;
|
|
534
|
+
const tone = session.pending.length ? 'live' : eventClass(session.latest);
|
|
535
|
+
const source = session.latest.source === 'reported' ? ' reported' : tone === 'failed' ? ' failed' : '';
|
|
536
|
+
links.push('<path class="network-edge' + source + (selected ? ' active' : state.selected ? ' dimmed' : '') +
|
|
537
|
+
'" d="' + pointPath(core, point, 10) + '"/>');
|
|
538
|
+
for (const item of events) {
|
|
539
|
+
const event = item.event;
|
|
540
|
+
const eventTone = eventClass(event);
|
|
541
|
+
const path = pointPath(point, item, 4);
|
|
542
|
+
links.push('<path class="network-edge' + (eventTone === 'reported' ? ' reported' : eventTone === 'failed' ? ' failed' : '') +
|
|
543
|
+
(event.id === state.selectedEvent ? ' active' : state.selected && !selected ? ' dimmed' : '') + '" d="' + path + '"/>');
|
|
544
|
+
if (animate && state.newIds.has(event.id)) {
|
|
545
|
+
pulses.push(pulse(pointPath(core, point, 10), eventTone, pulses.length));
|
|
546
|
+
pulses.push(pulse(path, eventTone, pulses.length));
|
|
547
|
+
}
|
|
548
|
+
const eventLabel = describeEvent(event) + ' · ' + labelAgent(session.agent, session.transport);
|
|
549
|
+
satellites.push('<g class="network-node ' + eventTone + (event.id === state.selectedEvent ? ' selected-event' : '') + '" data-live-event="' + esc(event.id) + '" role="button" tabindex="' +
|
|
550
|
+
(event.id === firstEvent ? 0 : -1) + '" aria-pressed="' + (event.id === state.selectedEvent) + '" aria-label="' + esc(eventLabel) +
|
|
551
|
+
'" transform="translate(' + item.x.toFixed(1) + ' ' + item.y.toFixed(1) + ')"><title>' + esc(eventLabel) +
|
|
552
|
+
'</title><circle r="8" fill="transparent"/><circle class="node-surface" r="5.5"/><circle class="node-indicator" r="2.3"/></g>');
|
|
553
|
+
}
|
|
554
|
+
const label = labelAgent(session.agent, session.transport);
|
|
555
|
+
const [status] = sessionStatus(session);
|
|
556
|
+
const full = [label, session.project, status].filter(Boolean).join(' · ');
|
|
557
|
+
const subtitle = clip(session.project || transports[session.transport] || 'Recorded session', 18);
|
|
558
|
+
primary.push('<g class="network-node ' + tone + '" data-session="' + esc(session.key) + '" role="button" tabindex="' +
|
|
559
|
+
(session.key === first ? 0 : -1) + '" aria-pressed="' + selected + '" aria-label="' + esc(full) + '" transform="translate(' +
|
|
560
|
+
(point.x - 68).toFixed(1) + ' ' + (point.y - 18).toFixed(1) + ')"><title>' + esc(full) +
|
|
561
|
+
'</title><rect class="node-surface" width="136" height="36" rx="6"/><circle class="node-indicator" cx="13" cy="13" r="3.6"/><text x="23" y="16">' +
|
|
562
|
+
esc(clip(label, 17)) + '</text><text class="node-subtitle" x="23" y="29">' + esc(subtitle) + '</text></g>');
|
|
563
|
+
}
|
|
564
|
+
const base = '<defs><pattern id="network-grid" width="16" height="16" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r=".8" fill="var(--grid)"/></pattern></defs>' +
|
|
565
|
+
'<rect width="720" height="405" fill="url(#network-grid)"/><ellipse class="topology-ring" cx="360" cy="202.5" rx="239" ry="143"/>' +
|
|
566
|
+
'<ellipse class="topology-ring" cx="360" cy="202.5" rx="202" ry="113"/><ellipse class="topology-ring" cx="360" cy="202.5" rx="131" ry="79"/>';
|
|
567
|
+
const hub = '<g class="network-core" aria-label="Local event journal"><circle class="core-ring" cx="360" cy="202.5" r="64"/><circle cx="360" cy="202.5" r="54"/>' +
|
|
568
|
+
'<circle class="core-inner" cx="360" cy="202.5" r="47"/><text text-anchor="middle" x="360" y="201">Shadow Brain</text>' +
|
|
569
|
+
'<text class="node-subtitle" text-anchor="middle" x="360" y="217">Event journal</text></g>';
|
|
570
|
+
svg.innerHTML = base + '<g aria-hidden="true">' + links.join('') + '</g>' + hub + satellites.join('') + primary.join('') +
|
|
571
|
+
'<g id="network-pulses" aria-hidden="true">' + pulses.slice(0, 20).join('') + '</g>';
|
|
572
|
+
svg.querySelectorAll('#network-pulses [data-delay]').forEach((element) => {
|
|
573
|
+
for (const animation of element.children) animation.beginElementAt?.(Number(element.dataset.delay));
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function fact(label, value) {
|
|
578
|
+
return value ? '<div><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>' : '';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderInspector(model) {
|
|
582
|
+
const session = model.sessions.find((item) => item.key === state.selected);
|
|
583
|
+
const target = $('inspection');
|
|
584
|
+
if (!session) {
|
|
585
|
+
target.innerHTML = blank('activity', 'Select a session or event', 'Inspect the recorded action, phase, and timing details.');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const event = session.events.find((item) => item.id === state.selectedEvent);
|
|
589
|
+
const [status, kind] = sessionStatus(session);
|
|
590
|
+
const current = [...session.phases.values()].sort((a, b) => b.sequence - a.sequence)[0];
|
|
591
|
+
const phaseMarkup = Object.entries(phases).map(([key, label]) => {
|
|
592
|
+
const observation = session.phases.get(key);
|
|
593
|
+
return '<span class="stage' + (observation ? ' seen' : '') + (current?.phase === key ? ' current' : '') +
|
|
594
|
+
'"' + (observation ? ' title="' + esc('Reported at ' + clockText(observation.timestamp)) + '"' : '') + '>' + label + '</span>';
|
|
595
|
+
}).join('');
|
|
596
|
+
const eventDetails = event ? '<div><h3>Selected event</h3><p class="small">' + esc(describeEvent(event)) + '</p></div><dl class="facts">' +
|
|
597
|
+
fact('Source', event.source === 'reported' ? 'Host report' : 'Measured') +
|
|
598
|
+
fact('Recorded at', clockText(event.timestamp)) + fact('Transport', transports[event.transport]) +
|
|
599
|
+
fact('Duration', Number.isFinite(event.durationMs) ? durationText(event.durationMs) : '') +
|
|
600
|
+
fact('Correlation ID', event.correlationId) +
|
|
601
|
+
fact('Sequence', String(event.sequence)) + '</dl>' : '';
|
|
602
|
+
const projectLink = session.project ? '<a class="button quiet" href="/#memories?project=' + esc(encodeURIComponent(session.project)) +
|
|
603
|
+
'">Open project memories</a>' : '';
|
|
604
|
+
target.innerHTML = '<div class="panel-body"><div><h3>' + esc(labelAgent(session.agent, session.transport)) + '</h3>' + tag(status, kind) +
|
|
605
|
+
'</div><dl class="facts">' + fact('Project', session.project || 'Unscoped') + fact('Transports', sessionTransports(session)) +
|
|
606
|
+
fact('Session ID', session.sessionId) + fact('Latest event', clockText(session.latest.timestamp)) +
|
|
607
|
+
fact('Matched results', String(session.results.length)) + fact('Awaiting result', session.pending.length ? String(session.pending.length) : '') +
|
|
608
|
+
fact('Unresolved starts', session.stale ? String(session.stale) : '') + '</dl>' +
|
|
609
|
+
'<div><h3>Task phases</h3>' + (current ? '<p class="small muted">Latest report at ' + esc(clockText(current.timestamp)) +
|
|
610
|
+
'</p><div class="stages">' + phaseMarkup + '</div>' : '<p class="small muted">No phase reports in this window.</p>') + '</div>' +
|
|
611
|
+
eventDetails + projectLink + '</div>';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function chartBuckets(model) {
|
|
615
|
+
const size = 10;
|
|
616
|
+
const width = (model.now - model.start) / size;
|
|
617
|
+
const buckets = Array.from({ length: size }, (_, index) => ({ time: model.start + index * width, results: 0, errors: 0, durations: [] }));
|
|
618
|
+
for (const result of model.matched) {
|
|
619
|
+
const position = Math.min(size - 1, Math.max(0, Math.floor((Date.parse(result.timestamp) - model.start) / width)));
|
|
620
|
+
const bucket = buckets[position];
|
|
621
|
+
bucket.results++;
|
|
622
|
+
if (result.outcome === 'error') bucket.errors++;
|
|
623
|
+
if (Number.isFinite(result.durationMs)) bucket.durations.push(result.durationMs);
|
|
624
|
+
}
|
|
625
|
+
return buckets.map(({ durations, ...bucket }) => ({ ...bucket, duration: median(durations) }));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function chartData(buckets, type, open, formatter) {
|
|
629
|
+
const timing = type === 'duration';
|
|
630
|
+
const headings = timing ? '<th scope="col">Median duration</th>' : '<th scope="col">Results</th><th scope="col">Errors</th>';
|
|
631
|
+
return '<details class="chart-data"' + (open ? ' open' : '') + '><summary class="small">View chart data</summary>' +
|
|
632
|
+
'<table><thead><tr><th scope="col">Interval start</th>' + headings + '</tr></thead><tbody>' + buckets.map((bucket) => {
|
|
633
|
+
const values = timing ? '<td>' + esc(durationText(bucket.duration)) + '</td>' : '<td>' + number(bucket.results) + '</td><td>' + number(bucket.errors) + '</td>';
|
|
634
|
+
return '<tr><th scope="row">' + esc(clockText(bucket.time, formatter)) + '</th>' + values + '</tr>';
|
|
635
|
+
}).join('') + '</tbody></table></details>';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function renderChart(id, buckets) {
|
|
639
|
+
const target = $(id);
|
|
640
|
+
const timing = id === 'duration';
|
|
641
|
+
const open = target.querySelector('details')?.open;
|
|
642
|
+
const formatter = buckets[1].time - buckets[0].time < 60_000 ? clock : tick;
|
|
643
|
+
const values = buckets.map((bucket) => timing ? bucket.duration : bucket.results).filter(Number.isFinite);
|
|
644
|
+
if (timing ? !values.length : !values.some(Boolean)) {
|
|
645
|
+
target.innerHTML = blank('activity', timing ? 'No measured durations in this window' : 'No matched tool results in this window',
|
|
646
|
+
'Matching start and result events will populate this chart.');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const max = Math.max(1, ...values);
|
|
650
|
+
const left = 63;
|
|
651
|
+
const right = 529;
|
|
652
|
+
const baseline = 119;
|
|
653
|
+
const top = 20;
|
|
654
|
+
const step = (right - left) / buckets.length;
|
|
655
|
+
const height = (value) => (baseline - top) * value / max;
|
|
656
|
+
let previous = null;
|
|
657
|
+
const marks = buckets.map((bucket, index) => {
|
|
658
|
+
const center = left + step * (index + .5);
|
|
659
|
+
if (!timing) {
|
|
660
|
+
const regular = bucket.results - bucket.errors;
|
|
661
|
+
const w = step * .65;
|
|
662
|
+
return '<g><title>' + esc(clockText(bucket.time, formatter) + ' · ' + bucket.results + ' results · ' + bucket.errors + ' errors') +
|
|
663
|
+
'</title><rect class="chart-bar" x="' + (center - w / 2) + '" y="' + (baseline - height(regular)) + '" width="' + w +
|
|
664
|
+
'" height="' + height(regular) + '"/><rect class="chart-error" x="' + (center - w / 2) + '" y="' +
|
|
665
|
+
(baseline - height(bucket.results)) + '" width="' + w + '" height="' + height(bucket.errors) + '"/></g>';
|
|
666
|
+
}
|
|
667
|
+
if (!Number.isFinite(bucket.duration)) {
|
|
668
|
+
previous = null;
|
|
669
|
+
return '';
|
|
670
|
+
}
|
|
671
|
+
const y = baseline - height(bucket.duration);
|
|
672
|
+
const line = previous ? '<path class="chart-duration" d="M' + previous.x + ' ' + previous.y + ' L' + center + ' ' + y + '"/>' : '';
|
|
673
|
+
previous = { x: center, y };
|
|
674
|
+
return line + '<circle class="chart-point" cx="' + center + '" cy="' + y + '" r="3.7"><title>' +
|
|
675
|
+
esc(clockText(bucket.time, formatter) + ' · ' + durationText(bucket.duration)) + '</title></circle>';
|
|
676
|
+
}).join('');
|
|
677
|
+
const maximum = timing ? durationText(max) : number(max);
|
|
678
|
+
const axis = [top, (top + baseline) / 2, baseline].map((y) => '<path class="chart-grid" d="M' + left + ' ' + y + 'H' + right + '"/>').join('');
|
|
679
|
+
const captions = [0, 4, 9].map((index) => '<text class="chart-label" x="' + (left + step * (index + .5)) +
|
|
680
|
+
'" y="139" text-anchor="middle">' + esc(clockText(buckets[index].time, formatter)) + '</text>').join('');
|
|
681
|
+
target.innerHTML = '<svg class="chart-svg" aria-hidden="true" viewBox="0 0 548 148">' + axis + marks + captions +
|
|
682
|
+
'<text class="chart-label" x="5" y="24">' + esc(maximum) + '</text><text class="chart-label" x="5" y="123">0</text></svg>' +
|
|
683
|
+
(!timing ? '<p class="small muted"><span style="color:var(--cyan)">■</span> Succeeded or cancelled <span style="color:var(--rose)">■</span> Errors</p>' : '') +
|
|
684
|
+
chartData(buckets, id, open, formatter);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function renderEvents(model) {
|
|
688
|
+
const session = model.sessions.find((item) => item.key === state.selected);
|
|
689
|
+
const filter = $('event-filter').value;
|
|
690
|
+
const transport = $('transport-filter').value;
|
|
691
|
+
const data = (session ? session.events : model.visible).filter((event) => (!filter || event.source === filter) && (!transport || event.transport === transport));
|
|
692
|
+
$('timeline-scope').textContent = (session ? labelAgent(session.agent, session.transport) + ' session' : 'All sessions') +
|
|
693
|
+
' · ' + number(data.length) + ' recorded events';
|
|
694
|
+
$('clear-session').hidden = !session;
|
|
695
|
+
const target = $('events');
|
|
696
|
+
if (!data.length) {
|
|
697
|
+
target.innerHTML = blank('activity', 'No matching events', state.loaded ? 'Choose a different window, source, or transport.' : 'Reading retained journal events.');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
target.innerHTML = '<ul class="list">' + [...data].reverse().slice(0, 150).map((event) => {
|
|
701
|
+
const tone = eventClass(event);
|
|
702
|
+
const source = event.source === 'reported' ? 'Reported' : 'Measured';
|
|
703
|
+
const extra = Number.isFinite(event.durationMs) ? ' · ' + durationText(event.durationMs) : '';
|
|
704
|
+
const meta = [event.agent ? labelAgent(event.agent, event.transport) : '', transports[event.transport], event.projectId].filter(Boolean).join(' · ');
|
|
705
|
+
return '<li class="event-row' + (state.newIds.has(event.id) ? ' new' : '') + '" data-selected="' + (event.id === state.selectedEvent) + '">' + timeMarkup(event.timestamp) +
|
|
706
|
+
'<span class="event-meta small muted">' + esc(meta) + '</span><div class="event-title"><button class="event-link" type="button" data-live-event="' +
|
|
707
|
+
esc(event.id) + '" aria-pressed="' + (event.id === state.selectedEvent) + '">' + esc(describeEvent(event)) +
|
|
708
|
+
'</button><span class="small muted">' + esc(extra) + '</span></div>' + tag(source, tone === 'reported' ? 'warn' : tone === 'failed' ? 'bad' : 'ok') + '</li>';
|
|
709
|
+
}).join('') + '</ul>' + (data.length > 150 ? '<p class="panel-body small muted">Showing the latest 150 events in this view.</p>' : '');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function renderStatus(model) {
|
|
713
|
+
const badge = $('stream-tag');
|
|
714
|
+
const label = state.replay ? 'Replay' : state.problem ? 'Disconnected' : !state.loaded ? 'Connecting' : state.paused ? 'Paused' : !state.delivery ? 'Delivery off' : 'Live';
|
|
715
|
+
badge.className = 'tag ' + (state.replay ? 'warn' : state.problem ? 'bad' : state.loaded && state.delivery && !state.paused ? 'ok' : 'warn');
|
|
716
|
+
badge.title = state.checkedAt ? 'Last checked ' + clockText(state.checkedAt) : '';
|
|
717
|
+
$('stream-label').textContent = label;
|
|
718
|
+
const demoBanner = document.documentElement.dataset.demo === 'true';
|
|
719
|
+
$('demo-tag').hidden = !state.demo || demoBanner;
|
|
720
|
+
$('pause-label').textContent = state.paused ? 'Resume stream' : 'Pause stream';
|
|
721
|
+
$('pause').querySelector('[data-icon]').innerHTML = icon(state.paused ? 'play' : 'pause');
|
|
722
|
+
$('pause').hidden = !!state.replay;
|
|
723
|
+
$('pause').disabled = !state.loaded || state.delivery === false;
|
|
724
|
+
$('refresh').hidden = !!state.replay;
|
|
725
|
+
$('refresh').disabled = state.pending || !!state.replay;
|
|
726
|
+
$('replay-start').hidden = !!state.replay;
|
|
727
|
+
$('replay-start').disabled = !state.events.length || !!state.replay || state.delivery === false;
|
|
728
|
+
const messages = [];
|
|
729
|
+
if (state.replay) messages.push('Replaying a frozen snapshot of ' + number(state.replay.events.length) + ' loaded events.');
|
|
730
|
+
else if (state.problem) messages.push(state.problem);
|
|
731
|
+
else if (!state.loaded) messages.push('Connecting to the local event journal…');
|
|
732
|
+
else {
|
|
733
|
+
messages.push(number(model.visible.length) + ' loaded events · Last ' + model.minutes + (model.minutes === 1 ? ' minute' : ' minutes'));
|
|
734
|
+
if (state.demo && !demoBanner) messages.push('Scripted local activity.');
|
|
735
|
+
if (state.recording === false) messages.push('Activity recording is off.');
|
|
736
|
+
if (state.delivery === false) messages.push('Live delivery is off.');
|
|
737
|
+
if (state.paused) messages.push('Stream paused.');
|
|
738
|
+
const oldest = Date.parse(state.events[0]?.timestamp);
|
|
739
|
+
const incomplete = state.gap || (state.dropped > 0 || state.retained > state.events.length) &&
|
|
740
|
+
(!Number.isFinite(oldest) || oldest > model.start);
|
|
741
|
+
if (incomplete) messages.push('The window may be partial because older events are outside the loaded journal.');
|
|
742
|
+
if (state.reset) messages.push('The journal restarted.');
|
|
743
|
+
if (model.unmatchedEnds || model.stale) messages.push('Unmatched spans are excluded from measured result metrics.');
|
|
744
|
+
}
|
|
745
|
+
const notice = $('stream-notice');
|
|
746
|
+
notice.className = 'notice' + (state.problem && !state.replay ? ' error' : '');
|
|
747
|
+
const settings = state.loaded && (state.recording === false || state.delivery === false);
|
|
748
|
+
const markup = messages.map((message) => '<span>' + esc(message) + '</span>').join('') +
|
|
749
|
+
(settings ? '<a class="button quiet" href="/#features">Open Features</a>' : '');
|
|
750
|
+
if (notice.innerHTML !== markup) notice.innerHTML = markup;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function renderReplay() {
|
|
754
|
+
const replay = state.replay;
|
|
755
|
+
$('replay-bar').hidden = !replay;
|
|
756
|
+
if (!replay) return;
|
|
757
|
+
const { events, index, timer, incomplete } = replay;
|
|
758
|
+
const event = events[index];
|
|
759
|
+
const slider = $('replay-range');
|
|
760
|
+
slider.max = String(events.length - 1);
|
|
761
|
+
slider.value = String(index);
|
|
762
|
+
slider.disabled = events.length < 2;
|
|
763
|
+
const position = number(index + 1) + ' / ' + number(events.length) + ' · ' + clockText(event.timestamp);
|
|
764
|
+
slider.setAttribute('aria-valuetext', position + ', ' + describeEvent(event));
|
|
765
|
+
$('replay-position').textContent = position;
|
|
766
|
+
$('replay-prev').disabled = index === 0;
|
|
767
|
+
$('replay-next').disabled = index === events.length - 1;
|
|
768
|
+
$('replay-play').disabled = events.length < 2;
|
|
769
|
+
$('replay-play').textContent = timer ? 'Pause replay' : index === events.length - 1 ? 'Play from start' : 'Play replay';
|
|
770
|
+
$('replay-caption').textContent = 'Loaded sequence ' + number(events[0].sequence) + '–' + number(events.at(-1).sequence) +
|
|
771
|
+
' · Playback advances two events per second. The time window ends at the selected event.' +
|
|
772
|
+
(incomplete ? ' Earlier events may be outside this snapshot.' : '');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function stopReplayPlayback() {
|
|
776
|
+
if (!state.replay?.timer) return;
|
|
777
|
+
clearInterval(state.replay.timer);
|
|
778
|
+
state.replay.timer = null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function moveReplay(index) {
|
|
782
|
+
const replay = state.replay;
|
|
783
|
+
if (!replay || !Number.isInteger(index)) return;
|
|
784
|
+
replay.index = Math.max(0, Math.min(replay.events.length - 1, index));
|
|
785
|
+
if (replay.index === replay.events.length - 1) stopReplayPlayback();
|
|
786
|
+
const event = replay.events[replay.index];
|
|
787
|
+
state.selected = sessionKey(event);
|
|
788
|
+
state.selectedEvent = event.id;
|
|
789
|
+
render(true);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function startReplay() {
|
|
793
|
+
if (!state.events.length || state.replay || state.delivery === false) return;
|
|
794
|
+
state.controller?.abort();
|
|
795
|
+
state.replay = { events: [...state.events], index: 0, timer: null,
|
|
796
|
+
incomplete: state.gap || state.dropped > 0 || state.retained > state.events.length || state.events[0].sequence > 1 };
|
|
797
|
+
moveReplay(state.replay.events.length - 1);
|
|
798
|
+
$('replay-heading').focus();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function exitReplay() {
|
|
802
|
+
stopReplayPlayback();
|
|
803
|
+
state.replay = null;
|
|
804
|
+
state.paused = false;
|
|
805
|
+
render(true);
|
|
806
|
+
$('replay-start').focus();
|
|
807
|
+
void poll();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function focusKey() {
|
|
811
|
+
const active = document.activeElement;
|
|
812
|
+
const value = active?.dataset.session || active?.dataset.liveEvent;
|
|
813
|
+
if (!value) return null;
|
|
814
|
+
const section = active.closest('#network-svg,#sessions,#events');
|
|
815
|
+
return section ? { section, key: active.dataset.session ? 'data-session' : 'data-live-event', value } : null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function render(force = false, animate = false) {
|
|
819
|
+
const replay = state.replay;
|
|
820
|
+
const replayEvent = replay?.events[replay.index];
|
|
821
|
+
const now = replayEvent ? Date.parse(replayEvent.timestamp) : Math.max(state.checkedAt || Date.now(), Date.parse(state.events.at(-1)?.timestamp) || 0);
|
|
822
|
+
const minutes = Number($('time-window').value);
|
|
823
|
+
const model = buildModel(now, minutes, replay ? replay.events.slice(0, replay.index + 1) : state.events);
|
|
824
|
+
state.model = model;
|
|
825
|
+
renderStatus(model);
|
|
826
|
+
renderReplay();
|
|
827
|
+
const key = [state.loaded, state.journalId, state.cursor, Math.floor(now / 15000), minutes, replayEvent?.sequence].join('|');
|
|
828
|
+
if (!force && key === state.renderKey) return;
|
|
829
|
+
state.renderKey = key;
|
|
830
|
+
if (!model.sessions.some((session) => session.key === state.selected)) state.selected = '';
|
|
831
|
+
if (!model.visible.some((event) => event.id === state.selectedEvent)) state.selectedEvent = '';
|
|
832
|
+
const focus = focusKey();
|
|
833
|
+
const scrolls = ['sessions', 'inspection', 'events'].map((id) => [$(id), $(id).scrollTop]);
|
|
834
|
+
renderStats(model);
|
|
835
|
+
renderSessions(model);
|
|
836
|
+
renderNetwork(model, animate && motionAllowed());
|
|
837
|
+
renderInspector(model);
|
|
838
|
+
const buckets = chartBuckets(model);
|
|
839
|
+
renderChart('throughput', buckets);
|
|
840
|
+
renderChart('duration', buckets);
|
|
841
|
+
renderEvents(model);
|
|
842
|
+
for (const [target, scroll] of scrolls) target.scrollTop = scroll;
|
|
843
|
+
if (focus) focus.section.querySelector('[' + focus.key + '="' + CSS.escape(focus.value) + '"]')?.focus({ preventScroll: true });
|
|
844
|
+
state.newIds.clear();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function loadPage(signal) {
|
|
848
|
+
const query = new URLSearchParams({ limit: '200' });
|
|
849
|
+
if (state.cursor !== null) query.set('cursor', String(state.cursor));
|
|
850
|
+
if (state.journalId) query.set('journalId', state.journalId);
|
|
851
|
+
const response = await fetch('/api/workspace/telemetry?' + query, { cache: 'no-store', headers: { Accept: 'application/json' }, signal });
|
|
852
|
+
const data = await response.json().catch(() => null);
|
|
853
|
+
if (!response.ok || !Array.isArray(data?.events) || !Number.isSafeInteger(data.nextAfterSequence ?? data.cursor)) {
|
|
854
|
+
throw new Error(data?.error || 'The local event journal could not be read.');
|
|
855
|
+
}
|
|
856
|
+
return data;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function acceptPage(page, animate) {
|
|
860
|
+
if (page.liveEnabled === false) {
|
|
861
|
+
state.events = [];
|
|
862
|
+
state.journalId = null;
|
|
863
|
+
state.cursor = null;
|
|
864
|
+
state.retained = 0;
|
|
865
|
+
state.dropped = 0;
|
|
866
|
+
state.gap = false;
|
|
867
|
+
state.reset = false;
|
|
868
|
+
state.recording = page.recordingEnabled;
|
|
869
|
+
state.delivery = false;
|
|
870
|
+
state.demo = page.demo === true;
|
|
871
|
+
state.checkedAt = Date.parse(page.checkedAt) || Date.now();
|
|
872
|
+
state.loaded = true;
|
|
873
|
+
state.newIds.clear();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const reset = page.reset || (state.journalId && page.journalId !== state.journalId);
|
|
877
|
+
state.reset = !!reset;
|
|
878
|
+
if (reset) {
|
|
879
|
+
state.events = [];
|
|
880
|
+
state.gap = false;
|
|
881
|
+
}
|
|
882
|
+
if (page.gap) state.gap = true;
|
|
883
|
+
const merged = new Map(state.events.map((event) => [event.sequence, event]));
|
|
884
|
+
for (const event of page.events) {
|
|
885
|
+
if (!Number.isSafeInteger(event.sequence) || typeof event.id !== 'string' || !Number.isFinite(Date.parse(event.timestamp))) continue;
|
|
886
|
+
if (animate && !merged.has(event.sequence)) state.newIds.add(event.id);
|
|
887
|
+
merged.set(event.sequence, event);
|
|
888
|
+
}
|
|
889
|
+
state.events = [...merged.values()].sort((a, b) => a.sequence - b.sequence).slice(-MAX_EVENTS);
|
|
890
|
+
state.journalId = page.journalId;
|
|
891
|
+
state.cursor = page.nextAfterSequence ?? page.cursor;
|
|
892
|
+
state.retained = page.retainedCount;
|
|
893
|
+
state.dropped = page.droppedCount;
|
|
894
|
+
state.recording = page.recordingEnabled;
|
|
895
|
+
state.delivery = page.liveEnabled;
|
|
896
|
+
state.demo = page.demo === true;
|
|
897
|
+
state.checkedAt = Date.parse(page.checkedAt) || Date.now();
|
|
898
|
+
state.loaded = true;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function poll(manual = false) {
|
|
902
|
+
if (state.pending || state.replay || (!manual && (state.paused || state.delivery === false || document.hidden))) return;
|
|
903
|
+
state.pending = true;
|
|
904
|
+
$('refresh').disabled = true;
|
|
905
|
+
const controller = new AbortController();
|
|
906
|
+
state.controller = controller;
|
|
907
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]);
|
|
908
|
+
const animate = state.loaded && state.delivery !== false && motionAllowed();
|
|
909
|
+
try {
|
|
910
|
+
for (let i = 0; i < 3; i++) {
|
|
911
|
+
const page = await loadPage(signal);
|
|
912
|
+
if (signal.aborted) return;
|
|
913
|
+
acceptPage(page, animate);
|
|
914
|
+
if (!page.hasMore) break;
|
|
915
|
+
}
|
|
916
|
+
state.problem = '';
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (!controller.signal.aborted) state.problem = error.message || 'The event journal is unavailable.';
|
|
919
|
+
} finally {
|
|
920
|
+
state.pending = false;
|
|
921
|
+
state.controller = null;
|
|
922
|
+
render(manual, animate);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function chooseSession(key) {
|
|
927
|
+
state.selected = key;
|
|
928
|
+
state.selectedEvent = '';
|
|
929
|
+
render(true);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function chooseEvent(id) {
|
|
933
|
+
const event = state.model?.visible.find((item) => item.id === id);
|
|
934
|
+
if (!event) return;
|
|
935
|
+
state.selected = sessionKey(event);
|
|
936
|
+
state.selectedEvent = id;
|
|
937
|
+
render(true);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function keyboardNetwork(event) {
|
|
941
|
+
const target = event.target.closest('[data-session],[data-live-event]');
|
|
942
|
+
if (!target) return;
|
|
943
|
+
const eventKey = target.dataset.liveEvent;
|
|
944
|
+
const attr = eventKey ? 'data-live-event' : 'data-session';
|
|
945
|
+
const items = [...$('network-svg').querySelectorAll('[' + attr + ']')];
|
|
946
|
+
const index = items.indexOf(target);
|
|
947
|
+
const activate = (item) => {
|
|
948
|
+
if (eventKey) chooseEvent(item.dataset.liveEvent);
|
|
949
|
+
else chooseSession(item.dataset.session);
|
|
950
|
+
};
|
|
951
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
952
|
+
event.preventDefault();
|
|
953
|
+
activate(target);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const next = { ArrowLeft: index - 1, ArrowUp: index - 1, ArrowRight: index + 1, ArrowDown: index + 1, Home: 0, End: items.length - 1 }[event.key];
|
|
957
|
+
if (!Number.isInteger(next)) return;
|
|
958
|
+
event.preventDefault();
|
|
959
|
+
const selected = items[(next + items.length) % items.length];
|
|
960
|
+
const value = eventKey ? selected.dataset.liveEvent : selected.dataset.session;
|
|
961
|
+
activate(selected);
|
|
962
|
+
$('network-svg').querySelector('[' + attr + '="' + CSS.escape(value) + '"]')?.focus({ preventScroll: true });
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function syncMotion() {
|
|
966
|
+
const reduced = document.documentElement.dataset.motion === 'reduce' || motionPreference.matches;
|
|
967
|
+
$('motion-control').setAttribute('aria-pressed', String(reduced));
|
|
968
|
+
$('motion-control').title = motionPreference.matches ? 'Your system preference reduces motion' : '';
|
|
969
|
+
if (reduced) $('network-pulses')?.replaceChildren();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
document.addEventListener('click', (event) => {
|
|
973
|
+
const target = event.target.closest('[data-session],[data-live-event]');
|
|
974
|
+
if (target?.dataset.session) chooseSession(target.dataset.session);
|
|
975
|
+
else if (target?.dataset.liveEvent) chooseEvent(target.dataset.liveEvent);
|
|
976
|
+
});
|
|
977
|
+
$('network-svg').addEventListener('keydown', keyboardNetwork);
|
|
978
|
+
$('time-window').addEventListener('change', () => render(true));
|
|
979
|
+
$('event-filter').addEventListener('change', () => renderEvents(state.model));
|
|
980
|
+
$('transport-filter').addEventListener('change', () => renderEvents(state.model));
|
|
981
|
+
$('clear-session').addEventListener('click', () => chooseSession(''));
|
|
982
|
+
$('refresh').addEventListener('click', () => { void poll(true); });
|
|
983
|
+
$('replay-start').addEventListener('click', startReplay);
|
|
984
|
+
$('replay-exit').addEventListener('click', exitReplay);
|
|
985
|
+
$('replay-range').addEventListener('input', (event) => {
|
|
986
|
+
stopReplayPlayback();
|
|
987
|
+
moveReplay(Number(event.target.value));
|
|
988
|
+
});
|
|
989
|
+
$('replay-prev').addEventListener('click', () => {
|
|
990
|
+
stopReplayPlayback();
|
|
991
|
+
moveReplay(state.replay.index - 1);
|
|
992
|
+
});
|
|
993
|
+
$('replay-next').addEventListener('click', () => {
|
|
994
|
+
stopReplayPlayback();
|
|
995
|
+
moveReplay(state.replay.index + 1);
|
|
996
|
+
});
|
|
997
|
+
$('replay-play').addEventListener('click', () => {
|
|
998
|
+
const replay = state.replay;
|
|
999
|
+
if (!replay) return;
|
|
1000
|
+
if (replay.timer) {
|
|
1001
|
+
stopReplayPlayback();
|
|
1002
|
+
renderReplay();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (replay.index === replay.events.length - 1) moveReplay(0);
|
|
1006
|
+
replay.timer = setInterval(() => moveReplay(replay.index + 1), 500);
|
|
1007
|
+
renderReplay();
|
|
1008
|
+
});
|
|
1009
|
+
$('pause').addEventListener('click', () => {
|
|
1010
|
+
state.paused = !state.paused;
|
|
1011
|
+
if (state.paused) {
|
|
1012
|
+
state.controller?.abort();
|
|
1013
|
+
$('network-pulses')?.replaceChildren();
|
|
1014
|
+
render(true);
|
|
1015
|
+
} else {
|
|
1016
|
+
render(true);
|
|
1017
|
+
void poll();
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
$('motion-control').addEventListener('click', () => {
|
|
1021
|
+
const value = document.documentElement.dataset.motion === 'reduce' ? 'system' : 'reduce';
|
|
1022
|
+
document.documentElement.dataset.motion = value;
|
|
1023
|
+
try { localStorage.setItem('shadow-brain:workspace:motion', value); } catch {}
|
|
1024
|
+
syncMotion();
|
|
1025
|
+
});
|
|
1026
|
+
motionPreference.addEventListener('change', syncMotion);
|
|
1027
|
+
document.addEventListener('visibilitychange', () => {
|
|
1028
|
+
if (document.hidden) {
|
|
1029
|
+
$('network-pulses')?.replaceChildren();
|
|
1030
|
+
stopReplayPlayback();
|
|
1031
|
+
renderReplay();
|
|
1032
|
+
}
|
|
1033
|
+
else void poll();
|
|
1034
|
+
});
|
|
1035
|
+
syncMotion();
|
|
1036
|
+
render(true);
|
|
1037
|
+
void poll(true);
|
|
1038
|
+
setInterval(() => { void poll(); }, 1900);
|
|
1039
|
+
</script>
|
|
1040
|
+
</body>
|
|
1041
|
+
</html>`;
|
|
1042
|
+
//# sourceMappingURL=live-workspace-html.js.map
|