@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.
Files changed (294) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +125 -450
  3. package/dist/activity-cli.d.ts +3 -0
  4. package/dist/activity-cli.d.ts.map +1 -0
  5. package/dist/activity-cli.js +78 -0
  6. package/dist/activity-cli.js.map +1 -0
  7. package/dist/adapters/index.d.ts +2 -1
  8. package/dist/adapters/index.d.ts.map +1 -1
  9. package/dist/adapters/index.js +3 -2
  10. package/dist/adapters/index.js.map +1 -1
  11. package/dist/brain/activity-journal.d.ts +82 -0
  12. package/dist/brain/activity-journal.d.ts.map +1 -0
  13. package/dist/brain/activity-journal.js +315 -0
  14. package/dist/brain/activity-journal.js.map +1 -0
  15. package/dist/brain/activity-reporting.d.ts +14 -0
  16. package/dist/brain/activity-reporting.d.ts.map +1 -0
  17. package/dist/brain/activity-reporting.js +17 -0
  18. package/dist/brain/activity-reporting.js.map +1 -0
  19. package/dist/brain/auto-update.js +1 -1
  20. package/dist/brain/brain-portability.js +1 -1
  21. package/dist/brain/client-config.d.ts +19 -0
  22. package/dist/brain/client-config.d.ts.map +1 -0
  23. package/dist/brain/client-config.js +40 -0
  24. package/dist/brain/client-config.js.map +1 -0
  25. package/dist/brain/client-integrations.d.ts +63 -0
  26. package/dist/brain/client-integrations.d.ts.map +1 -0
  27. package/dist/brain/client-integrations.js +79 -0
  28. package/dist/brain/client-integrations.js.map +1 -0
  29. package/dist/brain/context-assembly.d.ts +84 -0
  30. package/dist/brain/context-assembly.d.ts.map +1 -0
  31. package/dist/brain/context-assembly.js +529 -0
  32. package/dist/brain/context-assembly.js.map +1 -0
  33. package/dist/brain/context-request.d.ts +4 -0
  34. package/dist/brain/context-request.d.ts.map +1 -0
  35. package/dist/brain/context-request.js +22 -0
  36. package/dist/brain/context-request.js.map +1 -0
  37. package/dist/brain/context-retrieval.d.ts +88 -0
  38. package/dist/brain/context-retrieval.d.ts.map +1 -0
  39. package/dist/brain/context-retrieval.js +300 -0
  40. package/dist/brain/context-retrieval.js.map +1 -0
  41. package/dist/brain/feature-settings.d.ts +58 -0
  42. package/dist/brain/feature-settings.d.ts.map +1 -0
  43. package/dist/brain/feature-settings.js +242 -0
  44. package/dist/brain/feature-settings.js.map +1 -0
  45. package/dist/brain/file-identity.d.ts +4 -0
  46. package/dist/brain/file-identity.d.ts.map +1 -0
  47. package/dist/brain/file-identity.js +27 -0
  48. package/dist/brain/file-identity.js.map +1 -0
  49. package/dist/brain/global-brain.d.ts +18 -1
  50. package/dist/brain/global-brain.d.ts.map +1 -1
  51. package/dist/brain/global-brain.js +77 -10
  52. package/dist/brain/global-brain.js.map +1 -1
  53. package/dist/brain/mcp-server.d.ts +30 -1
  54. package/dist/brain/mcp-server.d.ts.map +1 -1
  55. package/dist/brain/mcp-server.js +378 -28
  56. package/dist/brain/mcp-server.js.map +1 -1
  57. package/dist/brain/mcp-stdio.d.ts +12 -0
  58. package/dist/brain/mcp-stdio.d.ts.map +1 -0
  59. package/dist/brain/mcp-stdio.js +140 -0
  60. package/dist/brain/mcp-stdio.js.map +1 -0
  61. package/dist/brain/memory-annotations.d.ts +97 -0
  62. package/dist/brain/memory-annotations.d.ts.map +1 -0
  63. package/dist/brain/memory-annotations.js +389 -0
  64. package/dist/brain/memory-annotations.js.map +1 -0
  65. package/dist/brain/memory-graph.d.ts.map +1 -1
  66. package/dist/brain/memory-graph.js +12 -23
  67. package/dist/brain/memory-graph.js.map +1 -1
  68. package/dist/brain/memory-inspection.d.ts +72 -0
  69. package/dist/brain/memory-inspection.d.ts.map +1 -0
  70. package/dist/brain/memory-inspection.js +131 -0
  71. package/dist/brain/memory-inspection.js.map +1 -0
  72. package/dist/brain/memory-mcp-content.d.ts +44 -0
  73. package/dist/brain/memory-mcp-content.d.ts.map +1 -0
  74. package/dist/brain/memory-mcp-content.js +91 -0
  75. package/dist/brain/memory-mcp-content.js.map +1 -0
  76. package/dist/brain/memory-sources.d.ts +8 -0
  77. package/dist/brain/memory-sources.d.ts.map +1 -0
  78. package/dist/brain/memory-sources.js +27 -0
  79. package/dist/brain/memory-sources.js.map +1 -0
  80. package/dist/brain/orchestrator.js +1 -1
  81. package/dist/brain/project-feedback.d.ts +63 -0
  82. package/dist/brain/project-feedback.d.ts.map +1 -0
  83. package/dist/brain/project-feedback.js +289 -0
  84. package/dist/brain/project-feedback.js.map +1 -0
  85. package/dist/brain/project-hub-contract.d.ts +326 -0
  86. package/dist/brain/project-hub-contract.d.ts.map +1 -0
  87. package/dist/brain/project-hub-contract.js +56 -0
  88. package/dist/brain/project-hub-contract.js.map +1 -0
  89. package/dist/brain/project-hub-mcp.d.ts +168 -0
  90. package/dist/brain/project-hub-mcp.d.ts.map +1 -0
  91. package/dist/brain/project-hub-mcp.js +41 -0
  92. package/dist/brain/project-hub-mcp.js.map +1 -0
  93. package/dist/brain/project-hub-pdf-worker.d.ts +13 -0
  94. package/dist/brain/project-hub-pdf-worker.d.ts.map +1 -0
  95. package/dist/brain/project-hub-pdf-worker.js +84 -0
  96. package/dist/brain/project-hub-pdf-worker.js.map +1 -0
  97. package/dist/brain/project-hub-store.d.ts +38 -0
  98. package/dist/brain/project-hub-store.d.ts.map +1 -0
  99. package/dist/brain/project-hub-store.js +189 -0
  100. package/dist/brain/project-hub-store.js.map +1 -0
  101. package/dist/brain/project-hub.d.ts +127 -0
  102. package/dist/brain/project-hub.d.ts.map +1 -0
  103. package/dist/brain/project-hub.js +381 -0
  104. package/dist/brain/project-hub.js.map +1 -0
  105. package/dist/brain/project-kit-inspection.d.ts +28 -0
  106. package/dist/brain/project-kit-inspection.d.ts.map +1 -0
  107. package/dist/brain/project-kit-inspection.js +200 -0
  108. package/dist/brain/project-kit-inspection.js.map +1 -0
  109. package/dist/brain/project-kit-paths.d.ts +13 -0
  110. package/dist/brain/project-kit-paths.d.ts.map +1 -0
  111. package/dist/brain/project-kit-paths.js +141 -0
  112. package/dist/brain/project-kit-paths.js.map +1 -0
  113. package/dist/brain/project-kit.d.ts +41 -0
  114. package/dist/brain/project-kit.d.ts.map +1 -0
  115. package/dist/brain/project-kit.js +255 -0
  116. package/dist/brain/project-kit.js.map +1 -0
  117. package/dist/brain/project-memory.d.ts +38 -0
  118. package/dist/brain/project-memory.d.ts.map +1 -0
  119. package/dist/brain/project-memory.js +76 -0
  120. package/dist/brain/project-memory.js.map +1 -0
  121. package/dist/brain/subconscious.d.ts +21 -24
  122. package/dist/brain/subconscious.d.ts.map +1 -1
  123. package/dist/brain/subconscious.js +82 -218
  124. package/dist/brain/subconscious.js.map +1 -1
  125. package/dist/brain/workflow-skills.d.ts +5 -5
  126. package/dist/brain/workflow-skills.d.ts.map +1 -1
  127. package/dist/brain/workflow-skills.js +3 -2
  128. package/dist/brain/workflow-skills.js.map +1 -1
  129. package/dist/cli.js +40 -4
  130. package/dist/cli.js.map +1 -1
  131. package/dist/context-cli.d.ts +3 -0
  132. package/dist/context-cli.d.ts.map +1 -0
  133. package/dist/context-cli.js +90 -0
  134. package/dist/context-cli.js.map +1 -0
  135. package/dist/curation-cli.d.ts +4 -0
  136. package/dist/curation-cli.d.ts.map +1 -0
  137. package/dist/curation-cli.js +118 -0
  138. package/dist/curation-cli.js.map +1 -0
  139. package/dist/dashboard/brain-hub-actions.d.ts +2 -0
  140. package/dist/dashboard/brain-hub-actions.d.ts.map +1 -0
  141. package/dist/dashboard/brain-hub-actions.js +137 -0
  142. package/dist/dashboard/brain-hub-actions.js.map +1 -0
  143. package/dist/dashboard/brain-hub-client.d.ts +2 -0
  144. package/dist/dashboard/brain-hub-client.d.ts.map +1 -0
  145. package/dist/dashboard/brain-hub-client.js +335 -0
  146. package/dist/dashboard/brain-hub-client.js.map +1 -0
  147. package/dist/dashboard/brain-hub-graph.d.ts +2 -0
  148. package/dist/dashboard/brain-hub-graph.d.ts.map +1 -0
  149. package/dist/dashboard/brain-hub-graph.js +176 -0
  150. package/dist/dashboard/brain-hub-graph.js.map +1 -0
  151. package/dist/dashboard/brain-hub-html.d.ts +2 -0
  152. package/dist/dashboard/brain-hub-html.d.ts.map +1 -0
  153. package/dist/dashboard/brain-hub-html.js +30 -0
  154. package/dist/dashboard/brain-hub-html.js.map +1 -0
  155. package/dist/dashboard/brain-hub-project-views.d.ts +2 -0
  156. package/dist/dashboard/brain-hub-project-views.d.ts.map +1 -0
  157. package/dist/dashboard/brain-hub-project-views.js +79 -0
  158. package/dist/dashboard/brain-hub-project-views.js.map +1 -0
  159. package/dist/dashboard/brain-hub-views.d.ts +2 -0
  160. package/dist/dashboard/brain-hub-views.d.ts.map +1 -0
  161. package/dist/dashboard/brain-hub-views.js +150 -0
  162. package/dist/dashboard/brain-hub-views.js.map +1 -0
  163. package/dist/dashboard/demo-workspace.d.ts +11 -0
  164. package/dist/dashboard/demo-workspace.d.ts.map +1 -0
  165. package/dist/dashboard/demo-workspace.js +178 -0
  166. package/dist/dashboard/demo-workspace.js.map +1 -0
  167. package/dist/dashboard/live-workspace-html.d.ts +2 -0
  168. package/dist/dashboard/live-workspace-html.d.ts.map +1 -0
  169. package/dist/dashboard/live-workspace-html.js +1042 -0
  170. package/dist/dashboard/live-workspace-html.js.map +1 -0
  171. package/dist/dashboard/project-feedback-html.d.ts +5 -0
  172. package/dist/dashboard/project-feedback-html.d.ts.map +1 -0
  173. package/dist/dashboard/project-feedback-html.js +216 -0
  174. package/dist/dashboard/project-feedback-html.js.map +1 -0
  175. package/dist/dashboard/project-hub-api.d.ts +5 -0
  176. package/dist/dashboard/project-hub-api.d.ts.map +1 -0
  177. package/dist/dashboard/project-hub-api.js +121 -0
  178. package/dist/dashboard/project-hub-api.js.map +1 -0
  179. package/dist/dashboard/project-kit-html.d.ts +4 -0
  180. package/dist/dashboard/project-kit-html.d.ts.map +1 -0
  181. package/dist/dashboard/project-kit-html.js +359 -0
  182. package/dist/dashboard/project-kit-html.js.map +1 -0
  183. package/dist/dashboard/project-workspace.d.ts +16 -0
  184. package/dist/dashboard/project-workspace.d.ts.map +1 -0
  185. package/dist/dashboard/project-workspace.js +106 -0
  186. package/dist/dashboard/project-workspace.js.map +1 -0
  187. package/dist/dashboard/server.d.ts +2 -0
  188. package/dist/dashboard/server.d.ts.map +1 -1
  189. package/dist/dashboard/server.js +65 -19
  190. package/dist/dashboard/server.js.map +1 -1
  191. package/dist/dashboard/v6-dashboard-html.d.ts +1 -1
  192. package/dist/dashboard/v6-dashboard-html.d.ts.map +1 -1
  193. package/dist/dashboard/v6-dashboard-html.js +34 -56
  194. package/dist/dashboard/v6-dashboard-html.js.map +1 -1
  195. package/dist/dashboard/workspace-api.d.ts +22 -0
  196. package/dist/dashboard/workspace-api.d.ts.map +1 -0
  197. package/dist/dashboard/workspace-api.js +429 -0
  198. package/dist/dashboard/workspace-api.js.map +1 -0
  199. package/dist/dashboard/workspace-html.d.ts +2 -0
  200. package/dist/dashboard/workspace-html.d.ts.map +1 -0
  201. package/dist/dashboard/workspace-html.js +2538 -0
  202. package/dist/dashboard/workspace-html.js.map +1 -0
  203. package/dist/dashboard/workspace-status.d.ts +50 -0
  204. package/dist/dashboard/workspace-status.d.ts.map +1 -0
  205. package/dist/dashboard/workspace-status.js +22 -0
  206. package/dist/dashboard/workspace-status.js.map +1 -0
  207. package/dist/index.d.ts +20 -0
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +15 -0
  210. package/dist/index.js.map +1 -1
  211. package/dist/integration-cli.d.ts +3 -0
  212. package/dist/integration-cli.d.ts.map +1 -0
  213. package/dist/integration-cli.js +16 -0
  214. package/dist/integration-cli.js.map +1 -0
  215. package/dist/project-cli.d.ts +3 -0
  216. package/dist/project-cli.d.ts.map +1 -0
  217. package/dist/project-cli.js +100 -0
  218. package/dist/project-cli.js.map +1 -0
  219. package/dist/types.d.ts +12 -3
  220. package/dist/types.d.ts.map +1 -1
  221. package/docs/COMPARISON.md +22 -0
  222. package/docs/COMPONENTS.md +66 -0
  223. package/docs/CONTEXT_LAB.md +79 -0
  224. package/docs/FEATURES_AND_LIMITS.md +24 -0
  225. package/docs/INTEGRATIONS.md +68 -0
  226. package/docs/LEARNING.md +46 -0
  227. package/docs/PROJECT_KIT.md +93 -0
  228. package/docs/QUICKSTART.md +44 -0
  229. package/docs/README.md +33 -0
  230. package/docs/ROADMAP-v7.md +147 -0
  231. package/docs/SHARING.md +13 -0
  232. package/docs/SHOWCASE.md +87 -0
  233. package/docs/STATS.md +21 -0
  234. package/docs/TROUBLESHOOTING.md +42 -0
  235. package/docs/UPGRADE_PLAN_v7.0.0.md +203 -0
  236. package/docs/VIRAL-PLAYBOOK.md +259 -0
  237. package/docs/VISUAL_GUIDE_v7.1.4.md +70 -0
  238. package/docs/WORKFLOW_INTEGRATION.md +19 -1
  239. package/docs/WORKSPACE.md +37 -0
  240. package/docs/assets/7.1.4-collaboration-design-preview.png +0 -0
  241. package/docs/assets/7.1.4-comparison.png +0 -0
  242. package/docs/assets/7.1.4-context-lab.png +0 -0
  243. package/docs/assets/7.1.4-control-room-dense-preview.png +0 -0
  244. package/docs/assets/7.1.4-control-room-focus-preview.png +0 -0
  245. package/docs/assets/7.1.4-features.png +0 -0
  246. package/docs/assets/7.1.4-graph.png +0 -0
  247. package/docs/assets/7.1.4-hub-control-room.png +0 -0
  248. package/docs/assets/7.1.4-hub-empty.png +0 -0
  249. package/docs/assets/7.1.4-hub-knowledge.png +0 -0
  250. package/docs/assets/7.1.4-hub-learning.png +0 -0
  251. package/docs/assets/7.1.4-hub-mobile.png +0 -0
  252. package/docs/assets/7.1.4-hub-public-docs-source.png +0 -0
  253. package/docs/assets/7.1.4-hub-public-docs.gif +0 -0
  254. package/docs/assets/7.1.4-hub-public-docs.png +0 -0
  255. package/docs/assets/7.1.4-hub-setup.png +0 -0
  256. package/docs/assets/7.1.4-hub-walkthrough.gif +0 -0
  257. package/docs/assets/7.1.4-integrations.png +0 -0
  258. package/docs/assets/7.1.4-knowledge-design-preview.png +0 -0
  259. package/docs/assets/7.1.4-learning-design-preview.png +0 -0
  260. package/docs/assets/7.1.4-learning.png +0 -0
  261. package/docs/assets/7.1.4-live.png +0 -0
  262. package/docs/assets/7.1.4-memory-review.png +0 -0
  263. package/docs/assets/7.1.4-mobile.png +0 -0
  264. package/docs/assets/7.1.4-models-design-preview.png +0 -0
  265. package/docs/assets/7.1.4-outcome-ranking.png +0 -0
  266. package/docs/assets/7.1.4-overview.png +0 -0
  267. package/docs/assets/7.1.4-project-kit.png +0 -0
  268. package/docs/assets/7.1.4-project-skill-preview.png +0 -0
  269. package/docs/assets/7.1.4-replay.png +0 -0
  270. package/docs/assets/7.1.4-setup-design-preview.png +0 -0
  271. package/docs/assets/7.1.4-usage-design-preview.png +0 -0
  272. package/docs/assets/context-flow-v7.2.png +0 -0
  273. package/docs/assets/context-v7.1.3.png +0 -0
  274. package/docs/assets/features-v7.1.3.png +0 -0
  275. package/docs/assets/graph-v7.1.3.png +0 -0
  276. package/docs/assets/integrations-v7.1.3.png +0 -0
  277. package/docs/assets/live-v7.1.3.png +0 -0
  278. package/docs/assets/live-v7.1.4.gif +0 -0
  279. package/docs/assets/memory-detail-v7.1.3.png +0 -0
  280. package/docs/assets/overview-v7.1.3.png +0 -0
  281. package/docs/assets/workspace-graph-v7.2.png +0 -0
  282. package/docs/assets/workspace-overview-v7.2.png +0 -0
  283. package/docs/evaluation/CONTEXT_EVALUATION_v7.1.4.json +1818 -0
  284. package/docs/evaluation/CONTEXT_EVALUATION_v7.1.4.md +36 -0
  285. package/docs/launch/COMPONENT_MAP_v7.1.4.md +307 -0
  286. package/docs/launch/PROJECT_KIT_PLAN_v7.1.4.md +110 -0
  287. package/docs/launch/PROJECT_LEARNING_PLAN_v7.1.4.md +63 -0
  288. package/docs/launch/RELEASE_NOTES_v7.1.3.md +40 -0
  289. package/docs/launch/RELEASE_NOTES_v7.1.4.md +30 -0
  290. package/docs/launch/RELEASE_NOTES_v7.2.0.md +24 -0
  291. package/docs/launch/RESEARCH_AND_PLAN_v7.1.4.md +255 -0
  292. package/package.json +43 -126
  293. package/skills/shadow-research/SKILL.md +2 -0
  294. package/skills/shadow-workflow/SKILL.md +6 -0
@@ -0,0 +1,2538 @@
1
+ // Dependency-free local workspace. The advanced dashboard is served separately.
2
+ import { PROJECT_KIT_MARKUP, PROJECT_KIT_SCRIPT, PROJECT_KIT_STYLE } from './project-kit-html.js';
3
+ export const WORKSPACE_HTML = String.raw `<!doctype html>
4
+ <html lang="en" data-theme="light" data-motion="system">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <meta name="color-scheme" content="light dark">
9
+ <title>Agent Shadow Brain · Workspace</title>
10
+ <script>
11
+ try {
12
+ const theme = localStorage.getItem('shadow-brain:workspace:theme');
13
+ const motion = localStorage.getItem('shadow-brain:workspace:motion');
14
+ if (theme === 'dark') document.documentElement.dataset.theme = theme;
15
+ if (motion === 'reduce') document.documentElement.dataset.motion = motion;
16
+ } catch {}
17
+ </script>
18
+ <style>
19
+ :root {
20
+ color-scheme: light;
21
+ --canvas: #f4f6f9; --surface: #fff; --inset: #eef2f6; --hover: #f2f5f8;
22
+ --ink: #182539; --muted: #4e6176; --line: #dbe2e9; --control: #8a9bae;
23
+ --teal: #096459; --teal-fill: #076b5e; --teal-hover: #05584d; --tint: #dff1ec;
24
+ --bad: #a32940; --bad-bg: #fbeaf0; --focus: #167b9d; --amber: #815116; --amber-bg: #f9edd8;
25
+ --rail: #172539; --rail-ink: #f0f5fc; --rail-muted: #b5c5d7; --rail-line: #35475b;
26
+ --rail-selected: #284657; --rail-highlight: #8ed9cb;
27
+ --agent: #57539a; --agent-bg: #eeecfa; --project: var(--teal); --project-bg: var(--tint);
28
+ --memory: #475f7e; --memory-bg: #eaf0f8;
29
+ --graph-line: #6d8195; --graph-project: #478779; --graph-agent: #746aa5; --graph-dot: #ccd7e0; --accent-ink: #fff;
30
+ --skeleton: #e5ebf0; --r: 9px; --ease: 175ms cubic-bezier(.2,.75,.25,1);
31
+ --header: 62px; --sidebar: 222px; --sticky: 2; --notice: 3;
32
+ font-family: Inter, "Segoe UI", system-ui, sans-serif;
33
+ font-size: 15px;
34
+ }
35
+ html[data-theme="dark"] {
36
+ color-scheme: dark;
37
+ --canvas: #111b2a; --surface: #172437; --inset: #1e2e43; --hover: #23344a;
38
+ --ink: #edf4fb; --muted: #b3c1d1; --line: #334459; --control: #74889e;
39
+ --teal: #94e1d3; --teal-fill: #9be6d6; --teal-hover: #baf2e6; --tint: #203f43;
40
+ --bad: #ffadbd; --bad-bg: #492a3a; --focus: #8cd6f3; --amber: #f4cb89; --amber-bg: #43352a;
41
+ --rail: #101a28; --rail-line: #2e4157; --rail-selected: #233e4f;
42
+ --agent: #c7bffc; --agent-bg: #333451; --memory: #bccfea; --memory-bg: #2a3a50;
43
+ --graph-line: #8295ac; --graph-project: #82bcb1; --graph-agent: #aaa0db; --graph-dot: #34465b; --accent-ink: #15372e;
44
+ --skeleton: #2b3a4d;
45
+ }
46
+ * { box-sizing: border-box; }
47
+ [hidden] { display: none !important; }
48
+ body { margin: 0; color: var(--ink); background: var(--canvas); font-size: .94rem; line-height: 1.5; }
49
+ a { color: var(--teal); text-decoration: none; }
50
+ p, h1, h2, h3, ul, dl, dd { margin: 0; }
51
+ p { max-width: 69ch; overflow-wrap: anywhere; }
52
+ h1, h2, h3 { line-height: 1.22; text-wrap: balance; overflow-wrap: anywhere; }
53
+ h1 { font-size: 2rem; letter-spacing: -.035em; font-weight: 710; }
54
+ h2 { font-size: 1.14rem; letter-spacing: -.015em; font-weight: 690; }
55
+ h3 { font-size: 1rem; font-weight: 650; }
56
+ .muted { color: var(--muted); }
57
+ .small { font-size: .86rem; }
58
+ .mono { font: .81rem/1.52 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; }
59
+ .icon { width: 18px; height: 18px; flex: none; vertical-align: middle; }
60
+ .icon.small-icon { width: 15px; height: 15px; }
61
+ :is(a,button,input,textarea,select,summary,[tabindex]):focus-visible {
62
+ outline: 2px solid var(--focus); outline-offset: 3px;
63
+ }
64
+ .skip { position: fixed; top: 10px; left: 10px; z-index: var(--notice); transform: translateY(-160%); }
65
+ .skip:focus { transform: none; }
66
+ .shell { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); min-height: 100vh; }
67
+ .rail {
68
+ position: sticky; top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column;
69
+ align-items: stretch; gap: 32px; padding: 24px 15px 18px; background: var(--rail); color: var(--rail-ink);
70
+ }
71
+ .brand { color: var(--rail-ink); display: flex; align-items: center; gap: 11px; padding: 0 7px; font-weight: 710; }
72
+ .brand-mark { width: 33px; height: 33px; display: grid; place-items: center; background: #294354; color: #9ce6d6; border-radius: 8px; }
73
+ .brand-mark .icon { width: 21px; height: 21px; }
74
+ .brand-name { letter-spacing: -.02em; line-height: 1.13; }
75
+ .rail-nav { display: grid; gap: 4px; }
76
+ .nav-link { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 7px; color: var(--rail-muted); font-weight: 580; }
77
+ .nav-link:hover { color: var(--rail-ink); background: #24384d; }
78
+ .nav-link[aria-current="page"] { color: var(--rail-ink); background: var(--rail-selected); }
79
+ .nav-link[aria-current="page"] .icon { color: var(--rail-highlight); }
80
+ .rail :focus-visible { outline-color: #9ce6d6; }
81
+ .rail-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--rail-line); }
82
+ .rail-foot .arrow { margin-left: auto; width: 14px; height: 14px; }
83
+ .rail-build { color: var(--rail-muted); padding: 10px 10px 0; font-size: .78rem; }
84
+ .main { min-width: 0; }
85
+ .top {
86
+ min-height: var(--header); display: flex; align-items: center; justify-content: space-between; gap: 16px;
87
+ padding: 10px 34px; border-bottom: 1px solid var(--line); background: var(--surface);
88
+ }
89
+ .crumb { display: flex; align-items: center; gap: 9px; min-width: 0; }
90
+ .crumb .icon { width: 14px; height: 14px; color: var(--muted); }
91
+ .top-actions { display: flex; align-items: center; gap: 14px; }
92
+ .live-link { white-space: nowrap; }
93
+ .connection { display: inline-flex; gap: 6px; align-items: center; white-space: nowrap; }
94
+ .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: currentColor; }
95
+ .connection[data-state="connected"] .dot { color: var(--teal); }
96
+ .connection[data-state="error"] .dot { color: var(--bad); }
97
+ .content { max-width: 1510px; margin: 0 auto; padding: 33px 34px 52px; min-width: 0; }
98
+ .page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 24px; }
99
+ .page-head p { margin-top: 8px; }
100
+ .page-head .button { align-self: center; }
101
+ .panel { border: 1px solid var(--line); background: var(--surface); border-radius: var(--r); min-width: 0; }
102
+ .panel-head { display: flex; justify-content: space-between; gap: 14px; align-items: center; padding: 17px 19px; border-bottom: 1px solid var(--line); }
103
+ .panel-head p { margin-top: 3px; }
104
+ .panel-body { padding: 18px 19px; min-width: 0; }
105
+ .stack { display: grid; gap: 17px; align-content: start; min-width: 0; }
106
+ .split { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); align-items: start; gap: 18px; }
107
+ .button {
108
+ appearance: none; color: var(--ink); background: var(--surface); border: 1px solid var(--control);
109
+ min-height: 37px; padding: 6px 11px; font: inherit; font-size: .88rem; font-weight: 650; border-radius: 7px;
110
+ display: inline-flex; align-items: center; justify-content: center; gap: 7px; text-align: center; line-height: 1.22;
111
+ text-decoration: none; cursor: pointer; transition: background var(--ease), color var(--ease), border-color var(--ease);
112
+ }
113
+ .button:hover, .button[aria-pressed="true"] { background: var(--hover); }
114
+ .button[aria-pressed="true"] { color: var(--teal); border-color: var(--teal); }
115
+ .button.primary { background: var(--teal-fill); color: var(--accent-ink); border-color: var(--teal-fill); }
116
+ .button.primary:hover { background: var(--teal-hover); border-color: var(--teal-hover); }
117
+ .button.quiet { background: transparent; border-color: transparent; color: var(--teal); }
118
+ .button.quiet:hover { background: var(--hover); }
119
+ .button.icon-button { width: 37px; padding: 0; }
120
+ .button:disabled { opacity: .65; cursor: default; }
121
+ .button:active:not(:disabled) { transform: translateY(1px); }
122
+ .label { display: grid; gap: 5px; font-size: .88rem; font-weight: 630; min-width: 0; }
123
+ :is(input:not([type=checkbox]),select,textarea) {
124
+ min-width: 0; width: 100%; color: var(--ink); background: var(--surface); font: inherit; font-weight: 450;
125
+ border: 1px solid var(--control); border-radius: 7px; padding: 8px 10px; min-height: 38px;
126
+ }
127
+ :is(input,textarea)::placeholder { color: var(--muted); opacity: 1; }
128
+ textarea { resize: vertical; }
129
+ .search-field { position: relative; min-width: 0; }
130
+ .search-field > [data-icon] { position: absolute; bottom: 10px; left: 10px; color: var(--muted); pointer-events: none; line-height: 1; }
131
+ .search-field input { padding-left: 34px; }
132
+ .inline-check { display: flex; align-items: flex-start; gap: 7px; min-height: 28px; }
133
+ .inline-check input { accent-color: var(--teal-fill); margin-top: 4px; }
134
+ .form-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
135
+ .filters { display: grid; grid-template-columns: minmax(155px,1.45fr) repeat(4,minmax(96px,1fr)) auto; gap: 10px; align-items: end; }
136
+ .filter-bar { display: flex; align-items: end; gap: 10px; flex-wrap: wrap; }
137
+ .filter-bar .label { flex: 1 1 155px; }
138
+ .status { padding: 13px 17px; border: 1px solid var(--line); border-radius: 7px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; background: var(--surface); }
139
+ .status.error { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
140
+ .status.error .button { color: var(--bad); }
141
+ .status.error .button:hover { background: var(--surface); }
142
+ .blank { display: grid; gap: 6px; padding: 29px 19px; }
143
+ .blank > .icon { color: var(--teal); margin-bottom: 4px; }
144
+ .blank a { justify-self: start; margin-top: 4px; }
145
+ .skeleton { display: grid; gap: 12px; padding: 19px; }
146
+ .skeleton i { width: 82%; height: 12px; display: block; border-radius: 4px; background: var(--skeleton); }
147
+ .skeleton i:nth-child(2) { width: 63%; }
148
+ .skeleton i:nth-child(3) { width: 71%; }
149
+ .tag {
150
+ background: var(--inset); color: var(--muted); padding: 3px 7px; border-radius: 5px;
151
+ font-size: .79rem; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere;
152
+ }
153
+ .tag.project { background: var(--project-bg); color: var(--project); }
154
+ .tag.agent { background: var(--agent-bg); color: var(--agent); }
155
+ .tag.memory { background: var(--memory-bg); color: var(--memory); }
156
+ .tag.state-active, .tag.check-ok { background: var(--tint); color: var(--teal); }
157
+ .tag.state-contested, .tag.check-warn { background: var(--amber-bg); color: var(--amber); }
158
+ .tag.state-superseded { background: var(--agent-bg); color: var(--agent); }
159
+ .tag.check-error { background: var(--bad-bg); color: var(--bad); }
160
+ .tags { display: flex; align-items: center; flex-wrap: wrap; gap: 5px 7px; }
161
+ .summary {
162
+ display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); background: var(--surface);
163
+ border: 1px solid var(--line); border-radius: var(--r); margin-bottom: 18px;
164
+ }
165
+ .summary-item { display: flex; align-items: center; gap: 13px; min-width: 0; padding: 18px 19px; }
166
+ .summary-item + .summary-item { border-left: 1px solid var(--line); }
167
+ .tile-icon { display: grid; place-items: center; width: 37px; height: 37px; border-radius: 7px; flex: none; background: var(--tint); color: var(--teal); }
168
+ .summary dt { color: var(--muted); font-size: .84rem; font-weight: 600; }
169
+ .summary dd { font-size: 1.3rem; font-weight: 700; line-height: 1.12; font-variant-numeric: tabular-nums; margin-top: 4px; }
170
+ .list { list-style: none; padding: 0; }
171
+ .list > li + li { border-top: 1px solid var(--line); }
172
+ .project-row { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(65px,.55fr) auto; align-items: center; gap: 12px; padding: 12px 19px; }
173
+ .project-name { display: flex; gap: 7px; align-items: center; min-width: 0; font-weight: 650; overflow-wrap: anywhere; }
174
+ .project-name:hover { text-decoration: underline; }
175
+ .project-detail { margin: 3px 0 0 22px; }
176
+ .project-count { color: var(--muted); font-size: .81rem; text-align: right; font-variant-numeric: tabular-nums; }
177
+ .project-meter { display: block; width: 100%; height: 5px; margin-top: 4px; border-radius: 3px; overflow: hidden; background: var(--inset); }
178
+ .project-meter span { display: block; height: 100%; background: var(--teal); border-radius: inherit; opacity: .75; }
179
+ .event { display: grid; grid-template-columns: 27px minmax(0,1fr) auto; gap: 9px; align-items: start; padding: 12px 19px; }
180
+ .event .tile-icon { width: 26px; height: 26px; }
181
+ .event strong { display: block; font-size: .89rem; font-weight: 630; overflow-wrap: anywhere; }
182
+ .event time { color: var(--muted); font-size: .77rem; }
183
+ .event-meta { margin-top: 2px; }
184
+ .overview-memory { display: grid; gap: 5px; padding: 12px 19px; }
185
+ .overview-memory strong { overflow-wrap: anywhere; }
186
+ .toolbar-end { margin-left: auto; }
187
+ .list-footer { border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 12px 18px; }
188
+ .memory-row { padding: 14px 19px; display: grid; gap: 8px; }
189
+ .memory-head { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
190
+ .memory-head time { white-space: nowrap; }
191
+ .record-link { color: var(--ink); font-weight: 680; text-decoration: underline; text-decoration-color: var(--control); text-underline-offset: 3px; }
192
+ .record-link:hover { color: var(--teal); text-decoration-color: currentColor; }
193
+ .memory-content { white-space: pre-wrap; overflow-wrap: anywhere; max-width: none; }
194
+ .detail-head { display: grid; gap: 9px; }
195
+ .detail-head > .button { justify-self: start; }
196
+ .detail-layout { grid-template-columns: minmax(0,1.45fr) minmax(265px,1fr); }
197
+ .detail-form, .detail-sources, .check-result { display: grid; gap: 9px; }
198
+ .detail-note { white-space: pre-wrap; overflow-wrap: anywhere; }
199
+ .detail-row { display: grid; gap: 7px; padding: 13px 19px; }
200
+ .link-search { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: end; gap: 8px; }
201
+ .save-status:empty { display: none; }
202
+ .inline-error { color: var(--bad); }
203
+ .quality-track { display: flex; width: 100%; height: 12px; background: var(--inset); border-radius: 5px; overflow: hidden; }
204
+ .quality-track > span { flex: none; }
205
+ .quality-active { background: var(--teal-fill); }
206
+ .quality-contested { background: var(--amber); }
207
+ .quality-archived { background: var(--control); }
208
+ .quality-superseded { background: var(--agent); }
209
+ .quality-labels { display: flex; flex-wrap: wrap; gap: 8px 13px; }
210
+ .quality-labels a { display: inline-flex; align-items: center; gap: 6px; color: var(--ink); font-size: .82rem; }
211
+ .quality-labels a:hover { text-decoration: underline; }
212
+ .quality-labels i { width: 8px; height: 8px; border-radius: 2px; }
213
+ .quality-source { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
214
+ .check-row { display: grid; grid-template-columns: minmax(130px,.9fr) minmax(0,1.7fr) auto; gap: 10px; align-items: start; padding: 13px 19px; }
215
+ .check-row .tag { justify-self: end; }
216
+ .memory-row p { max-width: 90ch; white-space: pre-wrap; }
217
+ .memory-row details summary { color: var(--teal); font-size: .85rem; font-weight: 650; cursor: pointer; width: fit-content; max-width: 100%; }
218
+ .memory-row details ul { padding-left: 19px; margin-top: 4px; }
219
+ .memory-row details li { overflow-wrap: anywhere; }
220
+ .source { display: flex; align-items: baseline; gap: 6px; color: var(--muted); min-width: 0; }
221
+ .source .icon { align-self: center; }
222
+ .graph-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(225px, 27%); min-width: 0; }
223
+ .graph-stage { position: relative; min-width: 0; height: 480px; background: var(--canvas); overflow: hidden; }
224
+ #graph-svg { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; user-select: none; }
225
+ #graph-svg.dragging { cursor: grabbing; }
226
+ .cluster-area { fill: var(--project-bg); fill-opacity: .49; stroke: var(--graph-project); stroke-opacity: .25; }
227
+ .graph-node { color: var(--memory); cursor: pointer; }
228
+ .graph-node.project { color: var(--project); }
229
+ .graph-node.agent { color: var(--agent); }
230
+ .graph-node .node-surface { fill: var(--surface); stroke: var(--line); stroke-width: 1.25; }
231
+ .graph-node.project .node-surface { fill: var(--project-bg); stroke: var(--graph-project); }
232
+ .graph-node.agent .node-surface { fill: var(--agent-bg); stroke: var(--graph-agent); }
233
+ .graph-node.dot-node .node-surface { stroke: var(--memory); }
234
+ .graph-node .node-glyph { fill: currentColor; }
235
+ .graph-node text { fill: var(--ink); font-size: 12px; font-weight: 600; }
236
+ .graph-node .node-count { fill: var(--muted); font-size: 11px; font-weight: 550; }
237
+ .graph-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { stroke: currentColor; stroke-width: 2.2; }
238
+ .graph-node.dot-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { fill: var(--memory-bg); }
239
+ .graph-node:focus-visible { outline: none; }
240
+ .graph-node.unrelated { opacity: .34; }
241
+ .graph-edge { fill: none; stroke: var(--graph-line); stroke-width: 1.15; opacity: .53; transition: opacity var(--ease); }
242
+ .graph-edge.project-edge { stroke: var(--graph-project); }
243
+ .graph-edge.agent-edge { stroke: var(--graph-agent); stroke-dasharray: 4 3; opacity: .35; }
244
+ .graph-edge.active { stroke: var(--teal); stroke-width: 2.1; opacity: 1; }
245
+ .graph-edge.agent-edge.active { stroke: var(--agent); }
246
+ .graph-edge.recorded-edge { stroke: var(--agent); stroke-width: 1.8; opacity: .8; }
247
+ .graph-edge.contradicts { stroke: var(--bad); stroke-dasharray: 5 3; }
248
+ .graph-edge.supersedes { stroke: var(--teal); stroke-dasharray: 2 2; }
249
+ .graph-edge.recorded-edge.active { stroke-width: 3; opacity: 1; }
250
+ .graph-edge.unrelated { opacity: .13; }
251
+ .graph-edge-hit { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; }
252
+ .graph-link-group:is(:hover,:focus-visible) .graph-edge.recorded-edge { opacity: 1; stroke-width: 3; }
253
+ .graph-link-group:focus-visible { outline: none; }
254
+ .graph-arrow { fill: var(--agent); }
255
+ .graph-tooltip { pointer-events: none; }
256
+ .graph-tooltip rect { fill: var(--surface); stroke: var(--memory); }
257
+ .graph-tooltip text { fill: var(--ink); font-size: 12px; font-weight: 650; }
258
+ .graph-tools { position: absolute; bottom: 12px; left: 12px; display: flex; gap: 5px; align-items: center; }
259
+ .graph-tools .button { min-height: 32px; }
260
+ .graph-tools .icon-button { width: 32px; }
261
+ .graph-tools output { padding: 4px 6px; background: var(--surface); color: var(--muted); border-radius: 5px; }
262
+ .graph-inspector { min-width: 0; border-left: 1px solid var(--line); max-height: 480px; overflow-y: auto; }
263
+ .graph-inspector .panel-body { display: grid; gap: 11px; }
264
+ .graph-inspector p { white-space: pre-wrap; }
265
+ .related { display: grid; gap: 5px; }
266
+ .related .button { justify-content: start; overflow-wrap: anywhere; text-align: left; }
267
+ .legend { display: flex; align-items: center; flex-wrap: wrap; gap: 7px 14px; padding: 11px 18px; border-top: 1px solid var(--line); color: var(--muted); font-size: .82rem; }
268
+ .legend > span { display: inline-flex; align-items: center; gap: 5px; }
269
+ .legend-glyph { display: inline-block; width: 9px; height: 9px; background: currentColor; flex: none; }
270
+ .legend-glyph.project { border-radius: 2px; color: var(--project); }
271
+ .legend-glyph.agent { border-radius: 50%; color: var(--agent); }
272
+ .legend-glyph.memory { width: 7px; height: 7px; transform: rotate(45deg); color: var(--memory); margin-right: 2px; }
273
+ .legend-line { display: inline-block; width: 16px; border-top: 2px solid var(--graph-line); }
274
+ .legend-line.project-edge { border-color: var(--graph-project); }
275
+ .legend-line.agent-edge { border-color: var(--graph-agent); border-top-style: dashed; }
276
+ .legend-line.recorded-edge { border-color: var(--agent); }
277
+ .graph-list { min-width: 0; max-height: 480px; overflow-y: auto; }
278
+ .graph-list .graph-row { padding: 9px 12px; }
279
+ .graph-list .button { justify-content: flex-start; text-align: left; width: 100%; }
280
+ .graph-row .tag { margin-left: auto; }
281
+ .context-layout { grid-template-columns: minmax(250px,.9fr) minmax(0,1.35fr); }
282
+ .context-config { display: grid; gap: 12px; }
283
+ .context-preview { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--inset); border-radius: 7px; padding: 13px; margin: 0; max-height: 420px; overflow: auto; }
284
+ .context-reasons { display: grid; gap: 10px; }
285
+ .context-reasons summary { cursor: pointer; font-weight: 650; }
286
+ .context-reasons li { padding: 8px 0; }
287
+ .context-reasons li strong { overflow-wrap: anywhere; }
288
+ .context-reasons li p { margin-top: 2px; }
289
+ .settings { max-width: 930px; }
290
+ .setting { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding: 16px 19px; }
291
+ .setting p { margin-top: 4px; }
292
+ .switch { display: inline-flex; align-items: center; justify-content: end; gap: 7px; min-width: 87px; min-height: 36px; border-color: transparent; background: transparent; }
293
+ .switch-track { width: 32px; height: 18px; border-radius: 11px; position: relative; background: var(--control); }
294
+ .switch-track::after {
295
+ content: ""; width: 12px; height: 12px; position: absolute; left: 3px; top: 3px; border-radius: 50%;
296
+ background: white; transition: transform var(--ease);
297
+ }
298
+ .switch[aria-checked="true"] .switch-track { background: var(--teal-fill); }
299
+ .switch[aria-checked="true"] .switch-track::after { transform: translateX(14px); background: var(--accent-ink); }
300
+ .setting select { width: 131px; flex: none; }
301
+ .save-status { padding: 8px 19px; min-height: 35px; border-top: 1px solid var(--line); }
302
+ .save-status[data-error="true"] { color: var(--bad); }
303
+ .notice { margin: 0 0 16px; }
304
+ :root { --signal: #8fe5d3; --instrument: #142d3b; --instrument-deep: #102333; --instrument-ink: #f3f9fd; --instrument-muted: #b6cbd5; --instrument-line: #395663; }
305
+ .rail { gap: 35px; }
306
+ .nav-link[aria-current="page"] { box-shadow: inset 0 0 0 1px #426371; }
307
+ .rail-live { margin-top: 11px; }
308
+ .rail-live .icon { color: var(--rail-highlight); }
309
+ .page-head h1 { font-size: 2.27rem; }
310
+ .page-head .head-actions { align-items: end; }
311
+ .page-head .head-actions .button { align-self: end; }
312
+ .demo-notice { display: none; }
313
+ html[data-demo="true"] .demo-notice { display: flex; gap: 7px 10px; flex-wrap: wrap; padding: 8px 12px; margin-bottom: 16px; background: var(--amber-bg); color: var(--amber); border-radius: 7px; font-size: .83rem; }
314
+ html[data-demo="true"] .advanced-link { display: none; }
315
+ .overview-summary { display: grid; grid-template-columns: minmax(0,1.1fr) minmax(0,1fr); color: var(--instrument-ink); background: var(--instrument); border-radius: 12px; overflow: hidden; margin-bottom: 17px; }
316
+ .overview-summary .summary { grid-template-columns: repeat(3,minmax(0,1fr)); background: var(--instrument-deep); color: var(--instrument-ink); border: 0; border-radius: 0; margin: 0; }
317
+ .overview-summary .summary-item { flex-direction: column; align-items: start; justify-content: center; gap: 11px; padding: 20px 16px; }
318
+ .overview-summary .summary-item + .summary-item { border-color: var(--instrument-line); }
319
+ .overview-summary .summary dt { color: var(--instrument-muted); font-size: .83rem; }
320
+ .overview-summary .summary dd { font-size: 1.72rem; }
321
+ .overview-summary .tile-icon { color: var(--signal); background: #234550; }
322
+ .overview-lead { display: flex; align-items: center; gap: 19px; padding: 21px; min-width: 0; }
323
+ .overview-lead h2 { font-size: 1.28rem; }
324
+ .overview-lead p { color: var(--instrument-muted); margin-top: 5px; }
325
+ .overview-lead .button { color: var(--signal); border-color: var(--instrument-line); margin-top: 12px; }
326
+ .overview-lead .button:hover { background: #254350; }
327
+ .coverage-ring { flex: none; display: grid; place-items: center; width: 98px; aspect-ratio: 1; border-radius: 50%; background: conic-gradient(var(--signal) var(--coverage), #3b5665 0); }
328
+ .coverage-ring span { display: grid; place-items: center; width: 80px; aspect-ratio: 1; border-radius: 50%; background: var(--instrument); font-size: 1.46rem; font-weight: 720; font-variant-numeric: tabular-nums; }
329
+ .overview-columns { grid-template-columns: minmax(0,1.33fr) minmax(0,1fr); }
330
+ .overview-activity .panel-head { background: var(--inset); }
331
+ .attention-list { max-height: 355px; overflow: auto; }
332
+ .attention-row { display: grid; grid-template-columns: 30px minmax(0,1fr); gap: 11px; align-items: start; padding: 12px 19px; }
333
+ .attention-row .tile-icon { width: 30px; height: 30px; background: var(--amber-bg); color: var(--amber); }
334
+ .attention-row .tags { margin-top: 5px; }
335
+ .graph-stage {
336
+ --canvas: #142534; --surface: #203546; --ink: #f0f7fc; --muted: #bed0db; --line: #4b6679;
337
+ --project: #a3e8d5; --project-bg: #234651; --agent: #d3c5ff; --agent-bg: #3a3e5e; --memory: #c2daf2; --memory-bg: #344d64;
338
+ --graph-line: #8ba9bd; --graph-project: #82b8ab; --graph-agent: #aa9ed0; --graph-dot: #40566a; --teal: #9ce9d8; --bad: #ff9eab;
339
+ height: 520px; color: var(--ink); color-scheme: dark;
340
+ }
341
+ .graph-stage .button:hover { background: #30485a; }
342
+ .graph-stage .cluster-area { fill-opacity: .47; stroke-opacity: .34; }
343
+ .graph-node { transition: opacity var(--ease); }
344
+ .graph-node.selected-memory .node-surface { stroke: var(--teal); }
345
+ .graph-stage .graph-edge.supports { stroke: var(--teal); }
346
+ .graph-inspector, .graph-list { max-height: 520px; }
347
+ .graph-body[data-lens="true"] .graph-stage { height: 390px; }
348
+ .graph-body[data-lens="true"] :is(.graph-inspector,.graph-list) { max-height: 390px; }
349
+ .graph-lens { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--line); }
350
+ .legend-line.supports { border-color: var(--teal); }
351
+ .legend-line.contradicts { border-color: var(--bad); border-top-style: dashed; }
352
+ .legend-line.supersedes { border-color: var(--teal); border-top-style: dotted; }
353
+ .context-layout { grid-template-columns: minmax(255px,.68fr) minmax(0,1.75fr); }
354
+ .context-config { gap: 16px; }
355
+ .context-config .form-actions .primary { width: 100%; }
356
+ .context-settings-pair { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 10px; }
357
+ .context-result-head { background: var(--inset); }
358
+ .context-output-head { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: start; gap: 10px; }
359
+ .context-output-head .form-actions { justify-content: end; }
360
+ .context-metrics { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); border-block: 1px solid var(--line); }
361
+ .context-metrics > div { padding: 11px 10px; min-width: 0; }
362
+ .context-metrics > div + div { border-left: 1px solid var(--line); }
363
+ .context-metrics dt { font-size: .78rem; color: var(--muted); }
364
+ .context-metrics dd { font-size: 1.21rem; font-weight: 710; overflow-wrap: anywhere; }
365
+ .context-budget-track { height: 6px; border-radius: 4px; background: var(--line); overflow: hidden; margin-top: 5px; }
366
+ .context-budget-track span { display: block; height: 100%; background: var(--teal-fill); border-radius: inherit; }
367
+ .context-section { display: grid; gap: 11px; min-width: 0; }
368
+ .context-section > summary, .candidate > summary { cursor: pointer; font-weight: 680; }
369
+ .context-section > summary { color: var(--teal); }
370
+ .context-section[open] > summary { margin-bottom: 10px; }
371
+ .retrieval-flow { display: grid; grid-template-columns: repeat(auto-fit,minmax(97px,1fr)); gap: 1px; list-style: none; padding: 1px; border-radius: 8px; background: var(--instrument-line); overflow: hidden; }
372
+ .retrieval-flow li { display: grid; grid-template-rows: auto 1fr auto; gap: 5px; min-width: 0; background: var(--instrument-deep); color: var(--instrument-ink); padding: 10px; }
373
+ .retrieval-flow strong { font-size: 1.28rem; font-variant-numeric: tabular-nums; }
374
+ .retrieval-flow span { font-size: .78rem; color: var(--instrument-muted); overflow-wrap: anywhere; }
375
+ .retrieval-flow li:last-child { background: var(--instrument); }
376
+ .retrieval-flow li:last-child strong { color: var(--signal); }
377
+ .retrieval-stage-bar { height: 3px; background: var(--instrument-line); border-radius: 2px; overflow: hidden; }
378
+ .retrieval-stage-bar i { display: block; height: 100%; background: var(--signal); opacity: .74; }
379
+ .candidate-list { max-height: 415px; overflow: auto; border-block: 1px solid var(--line); }
380
+ .candidate { padding: 9px 9px 9px 12px; }
381
+ .candidate > summary { color: var(--ink); }
382
+ .candidate > summary:hover { color: var(--teal); }
383
+ .candidate-title { display: inline-grid; grid-template-columns: auto minmax(0,1fr) auto; align-items: baseline; gap: 7px; width: calc(100% - 20px); vertical-align: top; }
384
+ .candidate-title .rank { color: var(--muted); font: .8rem Consolas,monospace; }
385
+ .candidate-body { display: grid; gap: 7px; padding: 9px 0 4px; }
386
+ .candidate-signals { display: grid; grid-template-columns: repeat(auto-fit,minmax(110px,1fr)); gap: 5px; }
387
+ .candidate-signals div { background: var(--inset); border-radius: 5px; padding: 6px; }
388
+ .candidate-signals dt { color: var(--muted); font-size: .75rem; }
389
+ .candidate-signals dd { font-size: .89rem; font-variant-numeric: tabular-nums; }
390
+ .context-compare { display: grid; gap: 8px; }
391
+ .compare-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
392
+ .compare-grid > div { padding: 10px; min-width: 0; }
393
+ .compare-grid > div + div { border-left: 1px solid var(--line); }
394
+ .compare-grid ul { padding-left: 16px; }
395
+ .compare-grid h3 { margin-bottom: 5px; }
396
+ .integration-meta { display: flex; flex-wrap: wrap; gap: 7px; padding: 12px 19px; border-bottom: 1px solid var(--line); }
397
+ .integration-config { display: grid; gap: 8px; }
398
+ .integration-config > summary { cursor: pointer; color: var(--teal); font-weight: 650; }
399
+ .integration-config[open] > summary { margin-bottom: 8px; }
400
+ .integration-config .context-preview { margin-block: 7px; max-height: 280px; }
401
+ .setting .settings-description { display: flex; align-items: start; gap: 11px; }
402
+ .setting .tile-icon { width: 32px; height: 32px; }
403
+ html[data-motion="reduce"] *, html[data-motion="reduce"] *::before, html[data-motion="reduce"] *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
404
+ @media (prefers-reduced-motion: reduce) {
405
+ *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
406
+ }
407
+ @media (max-width: 1140px) {
408
+ :root { --sidebar: 188px; }
409
+ .top, .content { padding-left: 24px; padding-right: 24px; }
410
+ .filters { grid-template-columns: repeat(4,minmax(0,1fr)); }
411
+ .filters .search-field { grid-column: span 3; }
412
+ .filters .button { grid-column: 4; grid-row: 1; }
413
+ .overview-columns { grid-template-columns: minmax(0,1.25fr) minmax(0,1fr); }
414
+ .event { grid-template-columns: 27px minmax(0,1fr); }
415
+ .event time { grid-column: 2; }
416
+ }
417
+ @media (max-width: 900px) {
418
+ :root { --sidebar: 66px; }
419
+ .rail { padding-left: 10px; padding-right: 10px; }
420
+ .rail .brand { justify-content: center; padding: 0; }
421
+ .nav-link { justify-content: center; min-height: 41px; }
422
+ .brand-name, .nav-label, .rail-foot .arrow, .rail-build { display: none; }
423
+ .context-layout { grid-template-columns: minmax(0,1fr); }
424
+ }
425
+ @media (max-width: 690px) {
426
+ :root { --header: 48px; font-size: 14px; }
427
+ .shell { display: block; }
428
+ .rail { z-index: var(--sticky); height: auto; flex-direction: row; align-items: center; gap: 7px; padding: 5px 10px; }
429
+ .rail-live { display: none; }
430
+ .rail-nav { display: flex; gap: 3px; }
431
+ .nav-link { padding: 7px 9px; min-height: 35px; }
432
+ .rail-foot { margin: 0 0 0 auto; padding: 0; border: 0; }
433
+ .brand-mark { width: 30px; height: 30px; }
434
+ .top { padding: 7px 13px; }
435
+ .top-actions { gap: 8px; }
436
+ .content { padding: 22px 12px 32px; }
437
+ h1, .page-head h1 { font-size: 1.68rem; }
438
+ .page-head { gap: 8px; margin-bottom: 17px; flex-wrap: wrap; }
439
+ .summary { margin-bottom: 13px; }
440
+ .summary-item { gap: 6px; padding: 11px 9px; }
441
+ .summary-item .tile-icon { display: none; }
442
+ .split { grid-template-columns: minmax(0,1fr); gap: 13px; }
443
+ .filters { grid-template-columns: repeat(2,minmax(0,1fr)); }
444
+ .filters .search-field { grid-column: 1; }
445
+ .filters .button { grid-column: 2; grid-row: 1; }
446
+ .check-row { grid-template-columns: minmax(0,1fr) auto; padding-left: 12px; padding-right: 12px; }
447
+ .check-row .check-result { grid-column: 1 / -1; grid-row: 2; }
448
+ .check-row .tag { grid-column: 2; grid-row: 1; }
449
+ .graph-body { grid-template-columns: minmax(0,1fr); }
450
+ .graph-stage, .graph-list { height: 365px; }
451
+ .graph-body[data-lens="true"] .graph-stage { height: 310px; }
452
+ .graph-inspector { border-left: 0; border-top: 1px solid var(--line); max-height: 340px; }
453
+ .panel-head, .panel-body, .memory-row, .setting { padding-left: 12px; padding-right: 12px; }
454
+ .project-row, .event { padding-left: 12px; padding-right: 12px; }
455
+ .list-footer { flex-wrap: wrap; }
456
+ .graph-mode { margin-left: 0; }
457
+ .overview-summary { grid-template-columns: minmax(0,1fr); }
458
+ .overview-summary .summary-item { padding: 11px 10px; }
459
+ .overview-summary .summary dd { font-size: 1.42rem; }
460
+ .overview-lead { padding: 15px; gap: 12px; }
461
+ .coverage-ring { width: 77px; }
462
+ .coverage-ring span { width: 63px; font-size: 1.16rem; }
463
+ .context-output-head { grid-template-columns: minmax(0,1fr); }
464
+ .context-output-head .form-actions { justify-content: start; }
465
+ #context-result, #attention-heading { scroll-margin-top: 55px; }
466
+ .context-metrics > div { padding: 8px 6px; }
467
+ .context-metrics dd { font-size: 1rem; }
468
+ .retrieval-flow { grid-template-columns: repeat(3,minmax(0,1fr)); }
469
+ .candidate-title { grid-template-columns: auto minmax(0,1fr); }
470
+ .candidate-title .tag { grid-column: 2; justify-self: start; }
471
+ }
472
+ @media (max-width: 355px) {
473
+ .rail { gap: 3px; }
474
+ .nav-link { padding-left: 5px; padding-right: 5px; }
475
+ .crumb > .muted, .crumb .icon { display: none; }
476
+ .filters { grid-template-columns: repeat(2,minmax(0,1fr)); }
477
+ .filters .search-field { grid-column: 1 / -1; }
478
+ .filters .button { grid-row: auto; grid-column: auto; }
479
+ }
480
+ ${PROJECT_KIT_STYLE}
481
+ </style>
482
+ </head>
483
+ <body>
484
+ <a class="skip button" href="#main-content">Skip to content</a>
485
+ <div class="shell">
486
+ <aside class="rail">
487
+ <a class="brand" href="#overview" aria-label="Agent Shadow Brain overview">
488
+ <span class="brand-mark" data-icon="brand"></span><span class="brand-name">Agent Shadow Brain</span>
489
+ </a>
490
+ <nav class="rail-nav" aria-label="Workspace">
491
+ <a class="nav-link" href="#overview" aria-label="Overview" title="Overview" data-view="overview" aria-current="page"><span data-icon="overview"></span><span class="nav-label">Overview</span></a>
492
+ <a class="nav-link" href="#memories" aria-label="Memories" title="Memories" data-view="memories"><span data-icon="memory"></span><span class="nav-label">Memories</span></a>
493
+ <a class="nav-link" href="#graph" aria-label="Graph" title="Graph" data-view="graph"><span data-icon="graph"></span><span class="nav-label">Graph</span></a>
494
+ <a class="nav-link" href="#context" aria-label="Context Lab" title="Context Lab" data-view="context"><span data-icon="context"></span><span class="nav-label">Context Lab</span></a>
495
+ <a class="nav-link" href="#project-kit" aria-label="Project Kit" title="Project Kit" data-view="project-kit"><span data-icon="project"></span><span class="nav-label">Project Kit</span></a>
496
+ <a class="nav-link" href="#integrations" aria-label="Integrations and health" title="Integrations and health" data-view="integrations"><span data-icon="integrations"></span><span class="nav-label">Integrations</span></a>
497
+ <a class="nav-link" href="#features" aria-label="Features" title="Features" data-view="features"><span data-icon="settings"></span><span class="nav-label">Features</span></a>
498
+ <a class="nav-link rail-live" href="/live" aria-label="Live operations" title="Live operations"><span data-icon="activity"></span><span class="nav-label">Live operations</span></a>
499
+ </nav>
500
+ <div class="rail-foot">
501
+ <a class="nav-link advanced-link" href="/advanced" aria-label="Advanced dashboard" title="Advanced dashboard"><span data-icon="advanced"></span><span class="nav-label">Advanced tools</span><span class="arrow" data-icon="diagonal"></span></a>
502
+ <p class="rail-build" id="rail-version">Local workspace</p>
503
+ </div>
504
+ </aside>
505
+ <div class="main">
506
+ <header class="top">
507
+ <div class="crumb"><span class="muted">Workspace</span><span data-icon="chevron"></span><strong id="crumb">Overview</strong></div>
508
+ <div class="top-actions">
509
+ <a class="button quiet live-link" href="/live"><span data-icon="activity"></span>Live</a>
510
+ <span class="connection muted small" id="connection" data-state="loading"><span class="dot"></span><span id="connection-label">Connecting</span></span>
511
+ <button type="button" class="button icon-button quiet" id="theme-toggle" aria-label="Switch to dark theme" title="Switch to dark theme"><span data-icon="moon"></span></button>
512
+ </div>
513
+ </header>
514
+ <main id="main-content" class="content" tabindex="-1">
515
+ <div class="demo-notice"><strong>Synthetic demo</strong><span>Scripted local MCP calls use sample data. Changes affect this temporary demo only.</span></div>
516
+ <div class="project-local-notice"><strong>Project workspace</strong><span>Memory and settings are stored with this project.</span><a href="#project-kit">Open Project Kit</a></div>
517
+ <div class="notice" id="workspace-notice" role="status" hidden></div>
518
+ <section id="page-overview" aria-labelledby="title-overview">
519
+ <div class="page-head">
520
+ <div><h1 id="title-overview" tabindex="-1">Memory workspace</h1><p class="muted">Review saved knowledge and prepare a brief for your next task.</p></div>
521
+ <div class="form-actions head-actions"><label class="label" for="overview-quality-project">Project<select id="overview-quality-project"><option value="">All projects</option></select></label>
522
+ <a href="#context" id="overview-context" class="button primary"><span data-icon="context"></span>Open Context Lab</a></div>
523
+ </div>
524
+ <div id="overview-body" aria-busy="true"></div>
525
+ </section>
526
+ <section id="page-memories" aria-labelledby="title-memories" hidden>
527
+ <div class="page-head"><div><h1 id="title-memories" tabindex="-1">Memories</h1><p class="muted">Search saved lessons and see why they were recalled.</p></div></div>
528
+ <div class="stack">
529
+ <form id="memory-form" class="panel panel-body filters" role="search" aria-label="Search memories">
530
+ <label class="label search-field" for="memory-query">Search<span data-icon="search"></span><input id="memory-query" type="search" maxlength="500" placeholder="Search memories"></label>
531
+ <label class="label" for="memory-project">Project<select id="memory-project"><option value="">All projects</option></select></label>
532
+ <label class="label" for="memory-category">Category<select id="memory-category"><option value="">All categories</option></select></label>
533
+ <label class="label" for="memory-agent">Agent<select id="memory-agent"><option value="">All agents</option></select></label>
534
+ <label class="label" for="memory-state">Status<select id="memory-state"><option value="">All statuses</option><option value="active">Active</option><option value="contested">Needs review</option><option value="archived">Archived</option><option value="superseded">Superseded</option></select></label>
535
+ <button class="button primary" id="memory-search" type="submit"><span data-icon="search"></span>Search</button>
536
+ </form>
537
+ <div id="memory-results" class="panel" aria-busy="true"></div>
538
+ </div>
539
+ </section>
540
+ <section id="page-memory" aria-labelledby="title-memory" hidden>
541
+ <div class="page-head">
542
+ <div class="detail-head"><a id="detail-back" href="#memories" class="button quiet"><span data-icon="left"></span>Memories</a><h1 id="title-memory" tabindex="-1">Memory detail</h1><div id="detail-meta"></div></div>
543
+ <div class="form-actions" id="detail-actions" hidden>
544
+ <a id="detail-graph" class="button" href="#graph"><span data-icon="graph"></span>View on graph</a>
545
+ <a id="detail-context" class="button" href="#context"><span data-icon="context"></span>Assemble context</a>
546
+ <a id="detail-outcome" class="button" href="#project-kit" hidden><span data-icon="activity"></span>Record outcome</a>
547
+ </div>
548
+ </div>
549
+ <div id="detail-body" aria-busy="true"></div>
550
+ </section>
551
+ <section id="page-graph" aria-labelledby="title-graph" hidden>
552
+ <div class="page-head"><div><h1 id="title-graph" tabindex="-1">Graph</h1><p class="muted">Explore project membership, recording agents, and links between memories.</p></div></div>
553
+ <div class="stack">
554
+ <div class="panel panel-body filter-bar">
555
+ <label class="label search-field" for="graph-query">Find a node<span data-icon="search"></span><input id="graph-query" type="search" maxlength="200" placeholder="Filter by name"></label>
556
+ <label class="label" for="graph-project">Project<select id="graph-project"><option value="">All projects</option></select></label>
557
+ <label class="label" for="graph-kind">Node type<select id="graph-kind"><option value="">All types</option><option value="project">Projects</option><option value="agent">Agents</option><option value="memory">Memories</option></select></label>
558
+ <label class="label" for="graph-relation">Relationship<select id="graph-relation"><option value="">All relationships</option><option value="recorded">Memory links</option><option value="belongs-to">Project membership</option><option value="recorded-by">Recording agent</option><option value="related">Related</option><option value="supports">Supports</option><option value="contradicts">Contradicts</option><option value="supersedes">Supersedes</option></select></label>
559
+ <div class="form-actions toolbar-end graph-mode" role="group" aria-label="Graph view">
560
+ <button type="button" class="button" id="graph-mode-map" aria-pressed="true"><span data-icon="graph"></span>Map</button>
561
+ <button type="button" class="button" id="graph-mode-list" aria-pressed="false"><span data-icon="list"></span>List</button>
562
+ </div>
563
+ </div>
564
+ <div class="panel" id="graph-panel" aria-busy="true">
565
+ <div class="panel-head"><h2>Memory relationships</h2><span id="graph-count" class="muted small" role="status"></span></div>
566
+ <div id="graph-lens" class="graph-lens" hidden><p class="small muted" id="graph-lens-label"></p><button class="button" type="button" id="graph-lens-clear">Show full graph</button></div>
567
+ <div id="graph-status"></div>
568
+ <div class="graph-body" id="graph-body" hidden>
569
+ <div class="graph-stage" id="graph-stage">
570
+ <svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" role="group" aria-label="Memory relationship map. Use arrow keys to navigate nodes or links."></svg>
571
+ <div class="graph-tools" role="group" aria-label="Map zoom">
572
+ <button class="button icon-button" type="button" id="graph-out" aria-label="Zoom out" title="Zoom out"><span data-icon="minus"></span></button>
573
+ <button class="button icon-button" type="button" id="graph-in" aria-label="Zoom in" title="Zoom in"><span data-icon="plus"></span></button>
574
+ <button class="button" type="button" id="graph-fit">Fit</button>
575
+ <output id="graph-scale" class="small" aria-label="Zoom level">100%</output>
576
+ </div>
577
+ </div>
578
+ <div id="graph-list" class="graph-list" hidden></div>
579
+ <aside id="graph-inspector" class="graph-inspector" aria-label="Selected node details"></aside>
580
+ </div>
581
+ <div id="graph-legend" class="legend" hidden>
582
+ <span><i class="legend-glyph project"></i> Project</span><span><i class="legend-glyph agent"></i> Agent</span><span><i class="legend-glyph memory"></i> Memory</span>
583
+ <span><i class="legend-line project-edge"></i> Belongs to project</span><span><i class="legend-line agent-edge"></i> Recorded by agent</span>
584
+ <span><i class="legend-line recorded-edge"></i> Related</span><span><i class="legend-line supports"></i> Supports</span>
585
+ <span><i class="legend-line contradicts"></i> Contradicts</span><span><i class="legend-line supersedes"></i> Supersedes</span>
586
+ </div>
587
+ </div>
588
+ </div>
589
+ </section>
590
+ <section id="page-context" aria-labelledby="title-context" hidden>
591
+ <div class="page-head"><div><h1 id="title-context" tabindex="-1">Context Lab</h1><p class="muted">Assemble a task brief, inspect its evidence, and compare retrieval profiles.</p></div></div>
592
+ <div class="split context-layout">
593
+ <div class="panel">
594
+ <div class="panel-head"><h2>Build a brief</h2></div>
595
+ <form class="panel-body context-config" id="context-form">
596
+ <label class="label" for="context-project">Project<select id="context-project" required><option value="">Select a project</option></select></label>
597
+ <label class="label" for="context-task">Task<textarea id="context-task" rows="5" maxlength="4096" required placeholder="Describe what the agent will work on."></textarea></label>
598
+ <div class="context-settings-pair">
599
+ <label class="label" for="context-budget">Token budget<input id="context-budget" type="number" inputmode="numeric" min="0" max="12000" step="1" value="2000" required aria-describedby="context-estimate"></label>
600
+ <label class="label" for="context-profile">Retrieval<select id="context-profile"><option value="adaptive">Adaptive</option><option value="legacy">Legacy</option></select></label>
601
+ </div>
602
+ <p class="small muted" id="context-estimate">Token usage is estimated for the assembled brief.</p>
603
+ <label class="inline-check small" id="context-related-row"><input type="checkbox" id="context-related" checked>Expand through recorded supporting and related links</label>
604
+ <label class="inline-check small" id="context-cross-row" hidden><input type="checkbox" id="context-cross">Include eligible memories from other projects</label>
605
+ <div id="context-gate"></div>
606
+ <div class="form-actions"><button class="button primary" id="context-submit" type="submit" disabled><span data-icon="context"></span>Assemble context</button></div>
607
+ </form>
608
+ </div>
609
+ <div class="panel" id="context-result"><div class="panel-head context-result-head"><h2>Brief and evidence</h2><span class="small muted" id="context-result-state" role="status">Ready for a task</span></div><div id="context-output"></div></div>
610
+ </div>
611
+ </section>
612
+ ${PROJECT_KIT_MARKUP}
613
+ <section id="page-integrations" aria-labelledby="title-integrations" hidden>
614
+ <div class="page-head">
615
+ <div><h1 id="title-integrations" tabindex="-1">Integrations &amp; health</h1><p class="muted">Check local storage and see which capabilities each agent supports.</p></div>
616
+ <button class="button" type="button" id="integrations-refresh"><span data-icon="refresh"></span>Run checks</button>
617
+ </div>
618
+ <div id="integrations-body" aria-busy="true"></div>
619
+ </section>
620
+ <section id="page-features" aria-labelledby="title-features" hidden>
621
+ <div class="page-head"><div><h1 id="title-features" tabindex="-1">Features</h1><p class="muted">Control how Shadow Brain assembles context and updates the workspace.</p></div></div>
622
+ <div class="stack settings">
623
+ <div class="panel"><div class="panel-head"><h2>Workspace features</h2></div><div id="feature-list" aria-busy="true"></div><p class="save-status small muted" role="status" id="feature-status"></p></div>
624
+ <div class="panel">
625
+ <div class="panel-head"><h2>Display</h2></div>
626
+ <ul class="list">
627
+ <li class="setting"><div><h3><label for="theme-setting">Theme</label></h3><p class="small muted">Choose the workspace color scheme on this device.</p></div><select id="theme-setting"><option value="light">Light</option><option value="dark">Dark</option></select></li>
628
+ <li class="setting"><div><h3><label for="motion-setting">Motion</label></h3><p class="small muted">Follow your system preference or reduce interface motion.</p></div><select id="motion-setting"><option value="system">System</option><option value="reduce">Reduced</option></select></li>
629
+ </ul>
630
+ <p id="display-status" class="save-status small muted" role="status" hidden></p>
631
+ </div>
632
+ </div>
633
+ </section>
634
+ </main>
635
+ </div>
636
+ </div>
637
+ <script>
638
+ 'use strict';
639
+
640
+ const glyphs = {
641
+ brand: '<path d="M12 5 5 9v6l7 4 7-4V9l-7-4v7m-7-3 7 3 7-3m-7 3v7"/><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/>',
642
+ overview: '<rect x="3.5" y="4" width="17" height="16" rx="2"/><path d="M3.5 9.5h17m-11 0V20"/>',
643
+ memory: '<path d="M4 7.5h16M6 7.5v11a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5v-11M9 12h6"/><rect x="3" y="4" width="18" height="4" rx="1"/>',
644
+ graph: '<path d="m6 6 6 6m0 0 6-6m-6 6v6"/><circle cx="5" cy="5" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="12" cy="19" r="2"/><circle cx="12" cy="12" r="1.6" fill="currentColor"/>',
645
+ context: '<path d="M9 8h6m-6 4h6m-6 4h4M6 3h9l4 4v14H6zM15 3v5h4"/>',
646
+ settings: '<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="2.4" fill="var(--icon-bg, var(--rail))"/><circle cx="15" cy="17" r="2.4" fill="var(--icon-bg, var(--rail))"/>',
647
+ advanced: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="m7.5 9 3 3-3 3m5.5 0h3.5"/>',
648
+ diagonal: '<path d="M6 18 18 6M8 6h10v10"/>',
649
+ chevron: '<path d="m9 6 6 6-6 6"/>',
650
+ left: '<path d="m14 6-6 6 6 6"/>',
651
+ moon: '<path d="M20 15a8.5 8.5 0 0 1-11-11 8.5 8.5 0 1 0 11 11Z"/>',
652
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19m0-14-1.5 1.5m-11 11L5 19"/>',
653
+ project: '<path d="M3.5 7V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18Z"/>',
654
+ 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"/>',
655
+ activity: '<path d="M3 12h4l3-7 4 14 3-7h4"/>',
656
+ integrations: '<path d="M9 3v4m6-4v4m-8 0h10v4a5 5 0 0 1-5 5h0a5 5 0 0 1-5-5zm5 9v5"/>',
657
+ search: '<circle cx="10.5" cy="10.5" r="6"/><path d="m15 15 5 5"/>',
658
+ source: '<path d="M9.5 14.5 14.5 9m-6 8.5-1 1a3.5 3.5 0 0 1-5-5L7 9a3.5 3.5 0 0 1 5 0m3.5-2.5 1-1a3.5 3.5 0 0 1 5 5L17 15a3.5 3.5 0 0 1-5 0"/>',
659
+ list: '<path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/>',
660
+ plus: '<path d="M12 5v14M5 12h14"/>',
661
+ minus: '<path d="M5 12h14"/>',
662
+ copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
663
+ warning: '<path d="m12 3 10 17H2L12 3zm0 6v4m0 3h.01"/>',
664
+ refresh: '<path d="M19.5 9a8 8 0 1 0 .3 5.5M20 4v5h-5"/>'
665
+ };
666
+ const viewNames = { overview: 'Overview', memories: 'Memories', memory: 'Memory detail', graph: 'Graph', context: 'Context Lab', 'project-kit': 'Project Kit', integrations: 'Integrations', features: 'Features' };
667
+ const reviewStates = { active: 'Active', contested: 'Needs review', archived: 'Archived', superseded: 'Superseded' };
668
+ const linkTypes = { related: 'Related', supports: 'Supports', contradicts: 'Contradicts', supersedes: 'Supersedes' };
669
+ const definitions = [
670
+ { id: 'contextAssembly', label: 'Context assembly', description: 'Build a task-specific brief from saved project memories.' },
671
+ { id: 'crossProjectContext', label: 'Cross-project context', description: 'Allow context assembly to include eligible memories from other projects when requested.' },
672
+ { id: 'automaticBriefing', label: 'Automatic briefing', description: 'Include a context brief when a supported agent starts work in a project.' },
673
+ { id: 'liveActivity', label: 'Live activity', description: 'Refresh the workspace activity feed automatically.' },
674
+ { id: 'activityRecording', label: 'Record agent activity', description: 'Record bounded tool timing and explicitly reported session phases.' }
675
+ ];
676
+ const $ = (id) => document.getElementById(id);
677
+ const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
678
+ const icon = (name, small = false) => '<svg class="icon' + (small ? ' small-icon' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (glyphs[name] || '') + '</svg>';
679
+ const list = (value) => Array.isArray(value) ? value : [];
680
+ const compact = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
681
+ const shorten = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + '…' : value;
682
+ const number = (value) => Number(value || 0).toLocaleString();
683
+ const countLabel = (value, singular, plural = singular + 's') => number(value) + ' ' + (Number(value) === 1 ? singular : plural);
684
+ const nice = (value) => String(value ?? '').replace(/[-_]/g, ' ').replace(/^./, (char) => char.toUpperCase());
685
+ const tags = (items) => '<div class="tags">' + items.filter(([, value]) => value).map(([kind, value]) =>
686
+ '<span class="tag ' + esc(kind) + '">' + esc(value) + '</span>').join('') + '</div>';
687
+ const DATE = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
688
+ const TIME = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
689
+ const state = {
690
+ view: '', route: '', projects: [], overviewLoaded: false, overviewPending: null, overviewScope: '', overviewRequest: 0, overviewRequestScope: '',
691
+ activity: [], activityEnabled: null, activityPending: false, features: null, featurePending: null, featureSaving: false,
692
+ health: { snapshot: null, project: '', filter: '', request: 0 }, projectKit: null,
693
+ memory: { query: null, request: 0, controller: null, limit: 20, offset: 0 },
694
+ detail: { snapshot: null, id: '', project: '', back: '#memories', request: 0, targetRequest: 0, saving: false },
695
+ graph: { snapshot: null, project: null, mode: matchMedia('(max-width: 690px)').matches ? 'list' : 'map', selected: '', selectedEdge: '', focusMemory: '', lens: '', nodes: [], edges: [], layout: null, zoom: 1, panX: 0, panY: 0, request: 0, controller: null, drag: null },
696
+ context: { pack: null, params: null, comparison: null, comparing: false, pending: false, dirty: false, routeProject: '' },
697
+ integrations: { snapshot: null, pending: false }
698
+ };
699
+
700
+ document.querySelectorAll('[data-icon]').forEach((element) => { element.innerHTML = icon(element.dataset.icon); });
701
+
702
+ function dateMarkup(value, full = false) {
703
+ if (!value) return '';
704
+ const date = new Date(value);
705
+ if (!Number.isFinite(date.getTime())) return '';
706
+ const today = new Date();
707
+ const dateText = full || date.getFullYear() !== today.getFullYear() ? DATE.format(date) : TIME.format(date);
708
+ return '<time class="small muted" datetime="' + esc(date.toISOString()) + '" title="' + esc(date.toLocaleString()) + '">' + esc(dateText) + '</time>';
709
+ }
710
+
711
+ function blank(iconName, title, detail, action = '') {
712
+ return '<div class="blank">' + icon(iconName) + '<h3>' + esc(title) + '</h3><p class="small muted">' + esc(detail) + '</p>' + action + '</div>';
713
+ }
714
+
715
+ function skeleton(label) {
716
+ return '<div class="skeleton" role="status"><span class="muted small">' + esc(label) + '</span><i></i><i></i><i></i></div>';
717
+ }
718
+
719
+ function errorMarkup(message, retry) {
720
+ return '<div class="status error" role="alert">' + icon('warning') + '<span>' + esc(message) + '</span>' +
721
+ (retry ? '<button class="button quiet" data-retry="' + retry + '" type="button">Try again</button>' : '') + '</div>';
722
+ }
723
+
724
+ function setBusy(element, isBusy) {
725
+ element.setAttribute('aria-busy', String(isBusy));
726
+ }
727
+
728
+ function connection(value) {
729
+ $('connection').dataset.state = value;
730
+ $('connection-label').textContent = value === 'connected' ? 'Local server' : value === 'error' ? 'Disconnected' : 'Connecting';
731
+ }
732
+
733
+ async function api(path, { body, signal } = {}) {
734
+ let response;
735
+ try {
736
+ response = await fetch('/api/workspace/' + path, {
737
+ headers: body ? { Accept: 'application/json', 'Content-Type': 'application/json' } : { Accept: 'application/json' },
738
+ method: body ? 'POST' : 'GET',
739
+ body: body ? JSON.stringify(body) : undefined,
740
+ cache: 'no-store',
741
+ signal: signal || AbortSignal.timeout(12000)
742
+ });
743
+ } catch (error) {
744
+ if (error.name === 'AbortError') throw error;
745
+ connection('error');
746
+ throw new Error('Could not reach the local Shadow Brain server.');
747
+ }
748
+ connection('connected');
749
+ const data = await response.json().catch(() => null);
750
+ if (!response.ok || !data) {
751
+ const error = new Error(response.ok ? 'The server returned an unreadable response.' : data?.error || 'The server could not complete this request.');
752
+ error.status = response.status;
753
+ throw error;
754
+ }
755
+ return data;
756
+ }
757
+
758
+ function optionSet(id, placeholder, values, preferred) {
759
+ const select = $(id);
760
+ const previous = preferred ?? select.value;
761
+ const choices = values.filter((item) => item && item.id);
762
+ const signature = JSON.stringify(choices);
763
+ if (select.dataset.signature !== signature) {
764
+ select.replaceChildren(new Option(placeholder, ''), ...choices.map((item) => new Option(item.name || item.id, item.id)));
765
+ select.dataset.signature = signature;
766
+ }
767
+ if (previous && Array.from(select.options).some((option) => option.value === previous)) select.value = previous;
768
+ else select.value = '';
769
+ }
770
+
771
+ function queryPath(name, params) {
772
+ const search = new URLSearchParams();
773
+ Object.entries(params).forEach(([key, value]) => { if (value !== '' && value != null) search.set(key, String(value)); });
774
+ return name + (search.size ? '?' + search : '');
775
+ }
776
+
777
+ function routeLink(view, params) {
778
+ return '#' + queryPath(view, params);
779
+ }
780
+
781
+ function memoryLink(id, project, label, className = 'record-link') {
782
+ if (!id || !project) return esc(label);
783
+ return '<a class="' + className + '" href="' + esc(routeLink('memory', { id, project })) + '">' + esc(label) + '</a>';
784
+ }
785
+
786
+ function reviewLabel(value) {
787
+ return Object.hasOwn(reviewStates, value) ? reviewStates[value] : '';
788
+ }
789
+
790
+ function reviewTag(value) {
791
+ const label = reviewLabel(value);
792
+ return label ? '<span class="tag state-' + value + '">' + label + '</span>' : '';
793
+ }
794
+
795
+ function renderSummary(totals) {
796
+ return '<dl class="summary">' + [
797
+ ['memory', 'Total stored memories', totals.memories], ['project', 'Projects', totals.projects], ['agent', 'Contributing agents', totals.agents]
798
+ ].map(([image, label, value]) => '<div class="summary-item"><span class="tile-icon">' + icon(image) +
799
+ '</span><div><dt>' + label + '</dt><dd>' + number(value) + '</dd></div></div>').join('') + '</dl>';
800
+ }
801
+
802
+ function renderOverviewSummary(data) {
803
+ const counts = data.quality || {};
804
+ const total = Number(counts.total) || 0;
805
+ const sources = Number(counts.sourceBacked) || 0;
806
+ const percent = total ? Math.round(Math.min(1, sources / total) * 100) : 0;
807
+ const project = data.scope?.projectId;
808
+ const scope = project ? state.projects.find((item) => item.id === project)?.name || project : 'All projects';
809
+ return '<div class="overview-summary"><div class="overview-lead"><div class="coverage-ring" aria-hidden="true" style="--coverage:' + percent + '%"><span>' +
810
+ (total ? percent + '%' : '—') + '</span></div><div><h2>Recorded source coverage</h2><p class="small">' +
811
+ esc(scope) + ' · ' + number(sources) + ' of ' + number(total) + (data.truncated ? ' sampled memories' : ' memories') +
812
+ '</p><button class="button quiet" type="button" id="attention-jump">Review attention queue ' + icon('chevron', true) + '</button></div></div>' + renderSummary(data.totals || {}) + '</div>';
813
+ }
814
+
815
+ function renderProjects(projects) {
816
+ if (!projects.length) return blank('project', 'No projects yet', 'Projects appear after an agent stores a memory with a project scope.');
817
+ const max = Math.max(1, ...projects.map((project) => Number(project.memories) || 0));
818
+ return '<ul class="list">' + [...projects].sort((a, b) => b.memories - a.memories).slice(0, 8).map((project) => {
819
+ const size = Math.max(0, Math.min(100, Math.round((Number(project.memories) || 0) / max * 100)));
820
+ const href = routeLink('memories', { project: project.id });
821
+ return '<li class="project-row"><div><a class="project-name" href="' + esc(href) + '">' + icon('project', true) +
822
+ '<span>' + esc(project.name || project.id) + '</span></a>' +
823
+ (project.updatedAt ? '<div class="project-detail">' + dateMarkup(project.updatedAt, true) + '</div>' : '') +
824
+ '</div><div class="project-count">' + countLabel(project.memories, 'memory', 'memories') +
825
+ '<span class="project-meter" aria-hidden="true"><span style="width:' + size + '%"></span></span></div>' +
826
+ '<a class="button icon-button quiet" aria-label="' + esc('View memories for ' + (project.name || project.id)) + '" href="' + esc(href) + '">' + icon('chevron') + '</a></li>';
827
+ }).join('') + '</ul>';
828
+ }
829
+
830
+ function renderOverviewMemories(memories) {
831
+ if (!memories.length) return '';
832
+ return '<div class="panel"><div class="panel-head"><h2>Latest memories</h2><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
833
+ '</a></div><ul class="list">' + memories.map((item) => '<li class="overview-memory"><div class="memory-head"><strong>' +
834
+ memoryLink(item.id, item.project, item.title || nice(item.category) || 'Memory') + '</strong>' + dateMarkup(item.updatedAt, true) + '</div>' +
835
+ tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)], ['state-' + (item.effectiveState || ''), reviewLabel(item.effectiveState)]]) + '</li>').join('') + '</ul></div>';
836
+ }
837
+
838
+ function renderQuality(data) {
839
+ const quality = data.quality;
840
+ if (!quality || !Number.isFinite(quality.total)) return '';
841
+ const project = data.scope?.projectId || '';
842
+ const scope = data.truncated ? 'Based on the latest ' + number(data.inventoryLimit) + ' stored memories.' :
843
+ project ? 'Stored memories in this project.' : 'Stored memories across projects.';
844
+ const statusCounts = Object.entries(reviewStates).map(([key, label]) => ({ key, label, count: Number(quality[key]) || 0 }));
845
+ const coverage = quality.total ? Math.max(0, Math.min(100, Number(quality.sourceBacked || 0) / quality.total * 100)) : 0;
846
+ const statusBars = statusCounts.map(({ key, count }) =>
847
+ '<span class="quality-' + key + '" style="width:' + Math.max(0, Math.min(100, count / quality.total * 100)) + '%"></span>').join('');
848
+ const statusLinks = statusCounts.map(({ key, label, count }) => '<a href="' + esc(routeLink('memories', { project, state: key })) +
849
+ '"><i aria-hidden="true" class="quality-' + key + '"></i>' + esc(label) + ' <strong>' + number(count) + '</strong></a>').join('');
850
+ return '<div class="panel"><div class="panel-head"><div><h2>Review and sources</h2><p class="small muted">' + esc(scope) +
851
+ '</p></div></div>' + (quality.total ? '<div class="panel-body stack"><div class="stack" style="gap:8px"><h3>Memory status</h3>' +
852
+ '<div class="quality-track" aria-hidden="true">' + statusBars + '</div><div class="quality-labels">' + statusLinks + '</div></div>' +
853
+ '<div class="stack" style="gap:8px"><div class="quality-source"><h3>Recorded sources</h3><span class="small muted">' +
854
+ number(quality.sourceBacked) + ' of ' + number(quality.total) + ' memories</span></div>' +
855
+ '<div class="quality-track" aria-hidden="true"><span class="quality-active" style="width:' + coverage + '%"></span></div></div>' +
856
+ (Number.isFinite(quality.reviewed) ? '<p class="small muted">' + number(quality.reviewed) + ' with a recorded review</p>' : '') +
857
+ '</div>' : blank('memory', 'No memories in scope', 'Save a memory to see review and source counts here.')) + '</div>';
858
+ }
859
+
860
+ const healthIssues = {
861
+ contested: 'Needs review', contradiction: 'Contradictory link', 'orphaned-link': 'Missing linked memory',
862
+ duplicate: 'Repeated content', 'no-source': 'No recorded source', 'older-memory': 'Older than 90 days'
863
+ };
864
+
865
+ function renderAttention() {
866
+ const target = $('attention-results');
867
+ const store = state.health;
868
+ if (!target || !store.snapshot || store.project !== state.overviewScope) return;
869
+ const data = store.snapshot;
870
+ const counts = data.counts || {};
871
+ const rows = list(data.queue).filter((item) => !store.filter || list(item.issues).some((issue) => issue.kind === store.filter));
872
+ const options = Object.entries(healthIssues).map(([value, label]) => '<option value="' + value + '">' + label + '</option>').join('');
873
+ const controls = $('attention-filter');
874
+ if (!controls.childElementCount) controls.innerHTML = '<option value="">All issues</option>' + options;
875
+ controls.value = store.filter;
876
+ controls.disabled = !list(data.queue).length;
877
+ const scopeNote = (data.coverage?.truncated ? 'Checked the latest ' + number(data.coverage.limit) + ' memories in scope.' : 'Checked ' + number(counts.total) + ' memories in scope.');
878
+ $('attention-scope').textContent = scopeNote;
879
+ const warnings = list(data.warnings).map((warning) => '<p class="small muted panel-body">' + esc(warning) + '</p>').join('');
880
+ const count = '<p class="small muted panel-body">Showing ' + number(rows.length) + ' of ' + number(data.queueTotal) + ' flagged memories' +
881
+ (store.filter ? ' · filter applied to loaded items' : '') + '.</p>';
882
+ const items = rows.map((item) => '<li class="attention-row"><span class="tile-icon">' + icon('warning', true) + '</span><div>' +
883
+ memoryLink(item.id, item.projectId, item.title || item.id) + '<div class="tags">' +
884
+ tags([['project', state.projects.find((project) => project.id === item.projectId)?.name || item.projectId], ['memory', nice(item.category)]]) +
885
+ list(item.issues).map((issue) => '<span class="tag check-' + (issue.severity === 'review' ? 'warn' : '') + '">' +
886
+ esc(issue.label || healthIssues[issue.kind] || 'Review') + '</span>').join('') + '</div></div></li>').join('');
887
+ target.innerHTML = rows.length ? count + '<ul class="list attention-list" tabindex="0" aria-label="Flagged memories">' + items + '</ul>' + warnings :
888
+ blank('memory', store.filter ? 'No loaded items match' : 'No attention items', store.filter ? 'Choose another issue or show all loaded items.' : 'Review flags will appear here as the memory store changes.') + warnings;
889
+ }
890
+
891
+ async function loadHealth(project) {
892
+ const target = $('attention-results');
893
+ if (!target) return;
894
+ const store = state.health;
895
+ const request = ++store.request;
896
+ if (store.project !== project) store.filter = '';
897
+ store.project = project;
898
+ store.snapshot = null;
899
+ $('attention-filter').disabled = true;
900
+ $('attention-scope').textContent = '';
901
+ target.innerHTML = skeleton('Checking memory health…');
902
+ setBusy(target, true);
903
+ try {
904
+ const snapshot = await api(queryPath('memory-health', { project, limit: 30 }));
905
+ if (request !== store.request || target !== $('attention-results')) return;
906
+ store.snapshot = snapshot;
907
+ renderAttention();
908
+ } catch (error) {
909
+ if (request === store.request && target === $('attention-results')) target.innerHTML = errorMarkup(error.message, 'health');
910
+ } finally {
911
+ if (request === store.request && target === $('attention-results')) setBusy(target, false);
912
+ }
913
+ }
914
+
915
+ function renderActivity() {
916
+ const target = $('activity-body');
917
+ if (!target) return;
918
+ const enabled = state.activityEnabled;
919
+ $('activity-status').textContent = enabled === true ? 'Live' : enabled === false ? 'Paused' : '';
920
+ if (enabled === false) {
921
+ target.innerHTML = blank('activity', 'Live activity is paused', 'Turn it back on in Features to see recent events.',
922
+ '<a class="button quiet" href="#features">Open Features ' + icon('chevron', true) + '</a>');
923
+ return;
924
+ }
925
+ if (!state.activity.length) {
926
+ target.innerHTML = blank('activity', 'No recent activity', 'Memory and agent events appear here as work happens.');
927
+ return;
928
+ }
929
+ target.innerHTML = '<ul class="list">' + state.activity.slice(0, 8).map((event) => {
930
+ const image = String(event.type ?? '').includes('memory') ? 'memory' : 'activity';
931
+ const meta = [event.agent, event.project].filter(Boolean).join(' · ');
932
+ return '<li class="event"><span class="tile-icon">' + icon(image, true) + '</span><div><strong>' +
933
+ esc(event.label || nice(event.type) || 'Activity') + '</strong>' +
934
+ (meta ? '<p class="event-meta small muted">' + esc(meta) + '</p>' : '') + '</div>' + dateMarkup(event.timestamp) + '</li>';
935
+ }).join('') + '</ul>';
936
+ }
937
+
938
+ function renderOverview(data) {
939
+ const sample = data.truncated ? '<p class="status small muted notice">Project and agent counts reflect the latest ' +
940
+ number(data.inventoryLimit) + ' stored memories.</p>' : '';
941
+ const pending = data.storageHealth?.status === 'pending' ? '<p class="status small muted notice">The memory store has pending writes.</p>' : '';
942
+ const projectNote = state.projects.length > 8 ? 'Top projects by saved memory count' : 'Saved memories by project';
943
+ optionSet('overview-quality-project', 'All projects', state.projects, data.scope?.projectId || '');
944
+ $('overview-context').href = routeLink('context', { project: data.scope?.projectId });
945
+ $('overview-body').innerHTML = '<div id="overview-refresh"></div>' + pending + renderOverviewSummary(data) + sample +
946
+ '<div class="split overview-columns"><div class="stack"><section class="panel" aria-labelledby="attention-heading"><div class="panel-head"><div>' +
947
+ '<h2 id="attention-heading" tabindex="-1">Attention queue</h2><p class="small muted" id="attention-scope"></p></div>' +
948
+ '<label class="label" for="attention-filter">Issue<select id="attention-filter" disabled></select></label></div>' +
949
+ '<div id="attention-results"></div></section>' + renderQuality(data) + '<div class="panel"><div class="panel-head"><div><h2>Project inventory</h2>' +
950
+ '<p class="muted small">' + projectNote + '</p></div><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
951
+ '</a></div>' + renderProjects(state.projects) + '</div>' + renderOverviewMemories(list(data.recentMemories)) +
952
+ '</div><div class="panel overview-activity"><div class="panel-head"><div><h2>Recent activity</h2><span id="activity-status" class="small muted"></span></div><a class="button quiet" href="/live">Open live operations ' + icon('chevron', true) + '</a></div><div id="activity-body"></div></div></div>';
953
+ renderActivity();
954
+ if (state.view === 'overview') void loadHealth(data.scope?.projectId || '');
955
+ }
956
+
957
+ function syncProjectOptions() {
958
+ const contextProject = state.context.routeProject || $('context-project').value;
959
+ const graphProject = state.view === 'graph' ? routeParams().get('project') : null;
960
+ optionSet('context-project', 'Select a project', state.projects, contextProject);
961
+ if ($('context-project').value === state.context.routeProject) state.context.routeProject = '';
962
+ optionSet('graph-project', 'All projects', state.projects, $('graph-project').value || graphProject);
963
+ }
964
+
965
+ async function loadOverview(force = false, project = '') {
966
+ if (state.overviewPending && state.overviewRequestScope === project) return state.overviewPending;
967
+ if (state.overviewLoaded && !force && state.overviewScope === project) return;
968
+ const request = ++state.overviewRequest;
969
+ state.overviewRequestScope = project;
970
+ const target = $('overview-body');
971
+ if (!state.overviewLoaded) target.innerHTML = '<div class="panel">' + skeleton('Loading workspace…') + '</div>';
972
+ else if (state.overviewScope !== project && $('overview-refresh')) $('overview-refresh').innerHTML =
973
+ '<div class="notice status small muted" role="status">Loading the selected project. Showing the previous scope.</div>';
974
+ setBusy(target, true);
975
+ state.overviewPending = (async () => {
976
+ try {
977
+ const data = await api(queryPath('overview', { project }));
978
+ if (request !== state.overviewRequest) return;
979
+ state.overviewLoaded = true;
980
+ state.overviewScope = project;
981
+ state.projects = list(data.projects);
982
+ state.projectKit = data.projectKit ?? null;
983
+ state.activity = list(data.activity);
984
+ if (typeof data.features?.liveActivity === 'boolean') state.activityEnabled = data.features.liveActivity;
985
+ if (data.version) $('rail-version').textContent = 'Version ' + data.version;
986
+ syncProjectOptions();
987
+ renderOverview(data);
988
+ renderContextControls();
989
+ } catch (error) {
990
+ if (request !== state.overviewRequest) return;
991
+ if (!state.overviewLoaded) target.innerHTML = errorMarkup(error.message, 'overview');
992
+ else if ($('overview-refresh')) $('overview-refresh').innerHTML = '<div class="notice">' + errorMarkup('Could not refresh the workspace. Showing the last loaded overview.', 'overview') + '</div>';
993
+ renderContextControls();
994
+ } finally {
995
+ if (request === state.overviewRequest) {
996
+ setBusy(target, false);
997
+ state.overviewPending = null;
998
+ }
999
+ }
1000
+ })();
1001
+ return state.overviewPending;
1002
+ }
1003
+
1004
+ async function refreshActivity() {
1005
+ if (document.hidden || state.view !== 'overview' || state.activityEnabled === false || state.activityPending || !state.overviewLoaded) return;
1006
+ state.activityPending = true;
1007
+ try {
1008
+ const data = await api('activity');
1009
+ if (state.features?.features?.liveActivity === false) return;
1010
+ state.activityEnabled = data.enabled;
1011
+ state.activity = list(data.activity);
1012
+ renderActivity();
1013
+ } catch {
1014
+ const status = $('activity-status');
1015
+ if (status) status.textContent = 'Refresh unavailable';
1016
+ } finally {
1017
+ state.activityPending = false;
1018
+ }
1019
+ }
1020
+
1021
+ function sourceMarkup(values) {
1022
+ const sources = Array.from(new Set(list(values).filter((source) => typeof source === 'string' && source.trim())));
1023
+ if (!sources.length) return '<p class="source small">' + icon('source', true) + '<span>No source recorded</span></p>';
1024
+ return sources.map((source) => '<p class="source small">' + icon('source', true) + '<span class="mono">' + esc(source) + '</span></p>').join('');
1025
+ }
1026
+
1027
+ function memoryQuery() {
1028
+ return {
1029
+ q: $('memory-query').value.trim(),
1030
+ project: $('memory-project').value,
1031
+ category: $('memory-category').value,
1032
+ agent: $('memory-agent').value,
1033
+ state: $('memory-state').value
1034
+ };
1035
+ }
1036
+
1037
+ function renderMemories(data) {
1038
+ const target = $('memory-results');
1039
+ const items = list(data.items);
1040
+ const total = Number.isFinite(data.total) ? data.total : items.length;
1041
+ const { offset } = state.memory;
1042
+ const activeFilters = Object.values(state.memory.query || {}).some(Boolean);
1043
+ const subtitle = state.memory.query?.q ? 'Ranked search results' : 'Most recent first';
1044
+ const heading = '<div class="panel-head"><div><h2>' + esc(activeFilters ? 'Results' : 'Recent memories') +
1045
+ '</h2><p class="small muted">' + subtitle + '</p></div><span class="small muted" role="status">' + countLabel(total, 'memory', 'memories') + '</span></div>' +
1046
+ (data.truncated ? '<p class="panel-body small muted">Filters and counts may be limited to ' + number(data.inventoryLimit) + ' memories.</p>' : '');
1047
+ if (!items.length) {
1048
+ target.innerHTML = heading + (activeFilters
1049
+ ? blank('search', 'No matching memories', 'Try a different query or clear one of the filters.', '<button class="button quiet" data-memory-clear type="button">Clear filters</button>')
1050
+ : blank('memory', 'No memories yet', 'Saved memories appear here after an agent stores a lesson or observation.'));
1051
+ return;
1052
+ }
1053
+ target.innerHTML = heading + '<ul class="list">' + items.map((item) => {
1054
+ const title = item.title || nice(item.category) || 'Memory';
1055
+ const reasons = list(item.reasons);
1056
+ const detail = reasons.length ? '<details><summary>Why this matched</summary><ul class="small muted">' +
1057
+ reasons.map((reason) => '<li>' + esc(reason) + '</li>').join('') + '</ul></details>' : '';
1058
+ const snippet = compact(item.snippet) && compact(item.snippet) !== compact(title) ? '<p>' + esc(item.snippet) + '</p>' : '';
1059
+ return '<li class="memory-row"><div class="memory-head"><h3>' + memoryLink(item.id, item.project, title) + '</h3>' + dateMarkup(item.updatedAt, true) + '</div>' +
1060
+ tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)], ['state-' + (item.effectiveState || ''), reviewLabel(item.effectiveState)]]) + snippet +
1061
+ sourceMarkup([...list(item.sources), item.source]) + detail + '</li>';
1062
+ }).join('') + '</ul>' + (offset || total > items.length ? '<div class="list-footer"><span class="muted small">' +
1063
+ number(offset + 1) + '–' + number(offset + items.length) + ' of ' + number(total) + '</span><div class="form-actions">' +
1064
+ '<button type="button" class="button" data-memory-page="prev"' + (offset ? '' : ' disabled') + '>' + icon('left', true) +
1065
+ 'Previous</button><button type="button" class="button" data-memory-page="next"' + (offset + items.length < total ? '' : ' disabled') +
1066
+ '>Next' + icon('chevron', true) + '</button></div></div>' : '');
1067
+ }
1068
+
1069
+ async function loadMemories(query, offset = 0) {
1070
+ const target = $('memory-results');
1071
+ const store = state.memory;
1072
+ const seq = ++store.request;
1073
+ store.controller?.abort();
1074
+ const controller = new AbortController();
1075
+ store.controller = controller;
1076
+ store.query = query;
1077
+ store.offset = offset;
1078
+ setBusy(target, true);
1079
+ target.innerHTML = skeleton('Loading memories…');
1080
+ try {
1081
+ const data = await api(queryPath('memories', { ...query, limit: store.limit, offset }), {
1082
+ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)])
1083
+ });
1084
+ if (seq !== store.request) return;
1085
+ const facets = data.facets || {};
1086
+ const toOptions = (values) => list(values).map((value) => ({ id: value, name: nice(value) }));
1087
+ const projects = list(facets.projects);
1088
+ optionSet('memory-project', 'All projects', projects.length ? projects : state.projects, query.project);
1089
+ optionSet('memory-category', 'All categories', toOptions(facets.categories), query.category);
1090
+ optionSet('memory-agent', 'All agents', toOptions(facets.agents), query.agent);
1091
+ renderMemories(data);
1092
+ } catch (error) {
1093
+ if (seq === store.request && error.name !== 'AbortError') target.innerHTML = errorMarkup(error.message, 'memories');
1094
+ } finally {
1095
+ if (seq === store.request) setBusy(target, false);
1096
+ }
1097
+ }
1098
+
1099
+ function linkCaption(link, memoryId) {
1100
+ if (link.type === 'related') return 'Related to';
1101
+ if (link.fromId === memoryId) return linkTypes[link.type] || nice(link.type);
1102
+ return { supports: 'Supported by', contradicts: 'Contradicted by', supersedes: 'Superseded by' }[link.type] || nice(link.type);
1103
+ }
1104
+
1105
+ function renderReviewHistory(events) {
1106
+ const recent = [...list(events)].sort((a, b) => Number(b.revision) - Number(a.revision)).slice(0, 12);
1107
+ if (!recent.length) return '';
1108
+ const rows = recent.map((event) => {
1109
+ const command = event.command || {};
1110
+ const label = command.kind === 'review' ? 'Recorded review · ' + reviewLabel(command.state) :
1111
+ command.kind === 'link' ? 'Added link · ' + (linkTypes[command.type] || nice(command.type)) :
1112
+ command.kind === 'unlink' ? 'Removed link' : 'Memory change';
1113
+ return '<li class="detail-row"><div class="memory-head"><strong>' + esc(label) + '</strong>' + dateMarkup(event.at) + '</div>' +
1114
+ (command.note ? '<p class="detail-note small muted">' + esc(command.note) + '</p>' : '') + '</li>';
1115
+ }).join('');
1116
+ return '<section class="panel"><div class="panel-head"><h2>Recent changes</h2></div><ul class="list">' + rows + '</ul></section>';
1117
+ }
1118
+
1119
+ function renderDetail(snapshot) {
1120
+ const { memory, review, links } = snapshot;
1121
+ const project = memory.project || memory.projectId || state.detail.project;
1122
+ const effective = snapshot.effectiveState || memory.effectiveState || review?.state;
1123
+ const title = memory.title || nice(memory.category) || 'Memory detail';
1124
+ const content = memory.content ?? memory.snippet;
1125
+ const ownState = review?.state || 'active';
1126
+ const reviewOptions = Object.entries(reviewStates).filter(([key]) => key !== 'superseded')
1127
+ .map(([key, label]) => '<option value="' + key + '"' + (key === ownState ? ' selected' : '') + '>' + label + '</option>').join('');
1128
+ const relationOptions = Object.entries(linkTypes).map(([key, label]) => '<option value="' + key + '">' + label + '</option>').join('');
1129
+ const replacement = list(links).find((link) => link.type === 'supersedes' && link.toId === memory.id);
1130
+ const titleEl = $('title-memory');
1131
+ titleEl.textContent = title;
1132
+ $('detail-meta').innerHTML = tags([
1133
+ ['project', memory.projectName || project], ['agent', memory.agent], ['memory', nice(memory.category)],
1134
+ ['state-' + (effective || ''), reviewLabel(effective)]
1135
+ ]) + (memory.createdAt || memory.updatedAt ? '<div class="small muted">' + dateMarkup(memory.createdAt || memory.updatedAt, true) + '</div>' : '');
1136
+ $('detail-graph').href = routeLink('graph', { project, memory: memory.id });
1137
+ $('detail-context').href = routeLink('context', { project });
1138
+ const projectFeedback = document.documentElement.dataset.projectLocal === 'true' || state.projectKit?.projectId === project;
1139
+ $('detail-outcome').hidden = !projectFeedback;
1140
+ $('detail-outcome').href = routeLink('project-kit', { memory: memory.id });
1141
+ $('detail-actions').hidden = false;
1142
+ const linkRows = list(links).map((link) => {
1143
+ const outgoing = link.fromId === memory.id;
1144
+ const endpoint = outgoing ? link.toId : link.fromId;
1145
+ const endpointTitle = (outgoing ? link.toTitle : link.fromTitle) || endpoint;
1146
+ const endpointLink = link.orphaned ? esc(endpointTitle) : memoryLink(endpoint, project, endpointTitle);
1147
+ return '<li class="detail-row"><div class="tags"><span class="tag memory">' + esc(linkCaption(link, memory.id)) + '</span>' +
1148
+ endpointLink + '</div>' +
1149
+ (link.note ? '<p class="detail-note small muted">' + esc(link.note) + '</p>' : '') +
1150
+ '<details><summary class="small muted">Remove link</summary><div class="form-actions"><button class="button" type="button" data-unlink="' +
1151
+ esc(link.id) + '">Remove this link</button></div></details></li>';
1152
+ }).join('');
1153
+ const warnings = list(snapshot.warnings).map((warning) => '<p class="status small muted notice">' + esc(warning) + '</p>').join('');
1154
+ $('detail-body').innerHTML = warnings + '<div class="split detail-layout"><div class="stack">' +
1155
+ '<section class="panel"><div class="panel-head"><h2>Saved memory</h2></div><div class="panel-body">' +
1156
+ '<p class="memory-content">' + esc(content || 'No saved text.') + '</p></div></section>' +
1157
+ '<section class="panel"><div class="panel-head"><h2>Recorded sources</h2></div><div class="panel-body detail-sources">' +
1158
+ sourceMarkup([...list(memory.sources), ...list(snapshot.sources), memory.source]) + '</div></section>' +
1159
+ '<section class="panel"><div class="panel-head"><h2>Connections</h2><span class="small muted">' + countLabel(list(links).length, 'link') +
1160
+ '</span></div>' + (linkRows ? '<ul class="list">' + linkRows + '</ul>' :
1161
+ '<div class="panel-body"><p class="small muted">No links recorded for this memory.</p></div>') +
1162
+ '<div class="panel-body"><details id="link-editor"><summary>Add a memory link</summary><div class="detail-form" style="margin-top:10px">' +
1163
+ '<form id="link-search" class="link-search"><label class="label" for="link-query">Find a memory in this project<input id="link-query" type="search" maxlength="500" placeholder="Search by keyword"></label>' +
1164
+ '<button type="submit" class="button">Find</button></form>' +
1165
+ '<form id="link-form" class="detail-form"><label class="label" for="link-target">Target memory<select id="link-target" required><option value="">Choose a memory</option></select></label>' +
1166
+ '<p id="link-search-status" class="small muted" role="status"></p>' +
1167
+ '<label class="label" for="link-type">Relationship from this memory<select id="link-type">' + relationOptions + '</select></label>' +
1168
+ '<label class="label" for="link-note">Note<textarea id="link-note" maxlength="1000" rows="2" placeholder="Describe the relationship"></textarea></label>' +
1169
+ '<div class="form-actions"><button class="button primary" id="link-submit" type="submit">Save link</button></div></form></div></details></div>' +
1170
+ '<p id="link-edit-status" class="save-status small muted" role="status"></p></section>' +
1171
+ '</div><div class="stack"><section class="panel"><div class="panel-head"><h2>Recorded review</h2>' + reviewTag(effective) + '</div>' +
1172
+ '<div class="panel-body detail-form">' +
1173
+ (effective === 'superseded' ? '<p class="small muted">This memory is superseded' +
1174
+ (replacement?.fromId && !replacement.orphaned ? ' by ' + memoryLink(replacement.fromId, project, replacement.fromTitle || replacement.fromId) : '') + '.</p>' : '') +
1175
+ (review?.updatedAt ? '<div class="small muted">Reviewed ' + dateMarkup(review.updatedAt, true) + '</div>' :
1176
+ '<p class="small muted">No review recorded.</p>') +
1177
+ '<form id="review-form" class="detail-form"><label class="label" for="review-state">Manual status<select id="review-state">' +
1178
+ reviewOptions + '</select></label><label class="label" for="review-note">Review note<textarea id="review-note" maxlength="1000" rows="3" placeholder="Record what you checked">' +
1179
+ esc(review?.note) + '</textarea></label><div class="form-actions"><button class="button primary" id="review-submit" type="submit">Save review</button></div></form>' +
1180
+ '</div><p id="detail-status" class="save-status small muted" role="status"></p></section>' +
1181
+ renderReviewHistory(snapshot.history) + '</div></div>';
1182
+ }
1183
+
1184
+ async function loadDetail(id, project, failurePrefix = '') {
1185
+ const store = state.detail;
1186
+ const request = ++store.request;
1187
+ store.id = id;
1188
+ store.project = project;
1189
+ store.snapshot = null;
1190
+ $('detail-actions').hidden = true;
1191
+ $('title-memory').textContent = 'Memory detail';
1192
+ $('detail-meta').replaceChildren();
1193
+ const target = $('detail-body');
1194
+ setBusy(target, true);
1195
+ target.innerHTML = skeleton('Loading memory…');
1196
+ try {
1197
+ if (!id || !project) {
1198
+ target.innerHTML = '<div class="panel">' + blank('memory', 'Choose a memory', 'Open a record from your memories, context, or graph.',
1199
+ '<a href="#memories" class="button quiet">Browse memories</a>') + '</div>';
1200
+ return false;
1201
+ }
1202
+ const snapshot = await api(queryPath('memory', { id, project }));
1203
+ if (request !== store.request) return false;
1204
+ if (snapshot.memory?.id !== id || (snapshot.memory.project || snapshot.memory.projectId) !== project ||
1205
+ !Number.isSafeInteger(snapshot.revision)) throw new Error('The server returned an unreadable memory.');
1206
+ store.snapshot = snapshot;
1207
+ renderDetail(snapshot);
1208
+ return true;
1209
+ } catch (error) {
1210
+ if (request !== store.request) return false;
1211
+ target.innerHTML = errorMarkup(failurePrefix + error.message, error.status === 404 ? null : 'detail');
1212
+ return false;
1213
+ } finally {
1214
+ if (request === store.request) setBusy(target, false);
1215
+ }
1216
+ }
1217
+
1218
+ async function searchLinkTargets() {
1219
+ const store = state.detail;
1220
+ if (!store.snapshot || store.saving) return;
1221
+ const request = ++store.targetRequest;
1222
+ const detailRequest = store.request;
1223
+ const target = $('link-search-status');
1224
+ target.textContent = 'Looking for memories…';
1225
+ try {
1226
+ const q = $('link-query').value.trim();
1227
+ const result = await api(queryPath('memories', { q, project: store.project, limit: 15 }));
1228
+ if (request !== store.targetRequest || detailRequest !== store.request) return;
1229
+ const choices = list(result.items).filter((item) => item.id !== store.id && (item.project || item.projectId) === store.project)
1230
+ .map((item) => ({ id: item.id, name: item.title || item.id }));
1231
+ optionSet('link-target', 'Choose a memory', choices, '');
1232
+ target.textContent = choices.length ? countLabel(choices.length, 'memory', 'memories') + ' found.' : 'No other memories matched.';
1233
+ } catch (error) {
1234
+ if (request === store.targetRequest && detailRequest === store.request) target.textContent = error.message;
1235
+ }
1236
+ }
1237
+
1238
+ function detailStatus(id, message, error = false) {
1239
+ const status = $(id);
1240
+ if (!status) return;
1241
+ status.textContent = message;
1242
+ status.dataset.error = String(error);
1243
+ }
1244
+
1245
+ async function saveDetail(kind, payload, focus) {
1246
+ const store = state.detail;
1247
+ if (!store.snapshot || store.saving) return;
1248
+ const { id, project, request } = store;
1249
+ const status = kind === 'memory-review' ? 'detail-status' : 'link-edit-status';
1250
+ const controls = () => $('detail-body').querySelectorAll('button, select, textarea');
1251
+ store.saving = true;
1252
+ controls().forEach((element) => { element.disabled = true; });
1253
+ detailStatus(status, 'Saving…');
1254
+ try {
1255
+ await api(kind, { body: { projectId: project, expectedRevision: store.snapshot.revision, ...payload } });
1256
+ state.graph.snapshot = null;
1257
+ state.overviewLoaded = false;
1258
+ if (state.context.pack || state.context.pending) {
1259
+ state.context.dirty = true;
1260
+ if (!state.context.pending) renderContext();
1261
+ }
1262
+ if (request !== store.request || state.view !== 'memory') return;
1263
+ if (await loadDetail(id, project, 'Saved. Could not reload the memory. ')) detailStatus(status, 'Saved.');
1264
+ } catch (error) {
1265
+ if (request !== store.request || state.view !== 'memory') return;
1266
+ if (error.status === 409) {
1267
+ if (await loadDetail(id, project, 'This memory changed elsewhere. ')) detailStatus(status, 'This memory changed elsewhere. Review the latest state and try again.', true);
1268
+ } else {
1269
+ detailStatus(status, error.message, true);
1270
+ }
1271
+ } finally {
1272
+ store.saving = false;
1273
+ if (state.view === 'memory' && id === store.id && project === store.project) {
1274
+ controls().forEach((element) => { element.disabled = false; });
1275
+ if (focus === 'link-editor' && $(focus)) $(focus).querySelector('summary')?.focus();
1276
+ else ($(focus) || $('title-memory')).focus();
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ const graphKinds = { project: 'Project', agent: 'Agent', memory: 'Memory' };
1282
+ const graphEdgeLabels = { 'belongs-to': 'Belongs to project', 'recorded-by': 'Recorded by agent', ...linkTypes };
1283
+ const graphEdgeClass = { 'belongs-to': ' project-edge', 'recorded-by': ' agent-edge' };
1284
+ const recordedEdge = (edge) => edge?.id && Object.hasOwn(linkTypes, edge.kind);
1285
+ const graphMemoryId = (node) => node.memoryId || (node.kind === 'memory' && node.id.startsWith('memory:') ? node.id.slice(7) : '');
1286
+
1287
+ function graphNeighbors(edges, id) {
1288
+ return edges.filter((edge) => edge.source === id || edge.target === id).map((edge) => ({
1289
+ id: edge.source === id ? edge.target : edge.source,
1290
+ edge
1291
+ }));
1292
+ }
1293
+
1294
+ function graphPillGrid(positions, nodes, width, top, pitch, nodeWidth = pitch - 18) {
1295
+ const columns = Math.max(1, Math.floor((width - 28) / pitch));
1296
+ for (let index = 0; index < nodes.length; index += columns) {
1297
+ const row = nodes.slice(index, index + columns);
1298
+ const offset = (width - row.length * pitch) / 2 + (pitch - nodeWidth) / 2;
1299
+ row.forEach((node, column) => {
1300
+ positions.set(node.id, { x: offset + column * pitch, y: top + Math.floor(index / columns) * 45, width: nodeWidth, height: 33 });
1301
+ });
1302
+ }
1303
+ return Math.ceil(nodes.length / columns) * 45;
1304
+ }
1305
+
1306
+ function projectCluster(hub, memories) {
1307
+ const rings = [];
1308
+ for (let start = 0, ring = 0; start < memories.length; ring++) {
1309
+ const capacity = 13 + ring * 8;
1310
+ const members = memories.slice(start, start + capacity);
1311
+ rings.push({ members, rx: 86 + ring * 32, ry: 61 + ring * 29 });
1312
+ start += members.length;
1313
+ }
1314
+ const outer = rings.at(-1);
1315
+ return { hub, rings, count: memories.length, rx: (outer?.rx || 75) + 17, ry: (outer?.ry || 36) + 17 };
1316
+ }
1317
+
1318
+ function packClusters(clusters, viewport, agentBands) {
1319
+ let best;
1320
+ for (let columns = 1; columns <= Math.min(clusters.length, 8); columns++) {
1321
+ const rows = [];
1322
+ for (let index = 0; index < clusters.length; index += columns) rows.push(clusters.slice(index, index + columns));
1323
+ const rowWidths = rows.map((row) => row.reduce((sum, cluster) => sum + cluster.rx * 2 + 19, 0));
1324
+ const rowHeights = rows.map((row) => Math.max(...row.map((cluster) => cluster.ry)) * 2 + 20);
1325
+ const width = Math.max(agentBands.width, 36 + Math.max(...rowWidths));
1326
+ const height = 46 + agentBands.height + rowHeights.reduce((sum, value) => sum + value, 0);
1327
+ const scale = Math.min(viewport.width / width, (viewport.height - 45) / height);
1328
+ if (!best || scale > best.scale) best = { rows, rowWidths, rowHeights, width, height, scale };
1329
+ }
1330
+ return best;
1331
+ }
1332
+
1333
+ function graphLayout(nodes, edges) {
1334
+ const positions = new Map();
1335
+ const clusters = [];
1336
+ const order = (a, b) => String(a.label || '').localeCompare(String(b.label || '')) || String(a.id).localeCompare(String(b.id));
1337
+ const projects = nodes.filter((node) => node.kind === 'project').sort(order);
1338
+ const agents = nodes.filter((node) => node.kind === 'agent').sort(order);
1339
+ const memories = nodes.filter((node) => node.kind === 'memory').sort(order);
1340
+ const byId = new Map(memories.map((node) => [node.id, node]));
1341
+ const expanded = state.graph.project && projects.length === 1 && memories.length <= 25;
1342
+ if (!projects.length || !memories.length || expanded) {
1343
+ const width = Math.max(450, Math.min(5, Math.ceil(Math.sqrt(memories.length * 1.1))) * 184 + 30);
1344
+ let height = 28;
1345
+ height += graphPillGrid(positions, projects, width, height, 178, 155);
1346
+ if (expanded) height += 14;
1347
+ height += graphPillGrid(positions, memories, width, height, 184);
1348
+ if (expanded) height += 14;
1349
+ height += graphPillGrid(positions, agents, width, height, 140, 118);
1350
+ return { positions, clusters, width, height: height + 18 };
1351
+ }
1352
+
1353
+ const groupMap = new Map(projects.map((node) => [node.id, []]));
1354
+ const grouped = new Set();
1355
+ for (const edge of edges) {
1356
+ if (edge.kind !== 'belongs-to') continue;
1357
+ const memory = byId.get(edge.target);
1358
+ const group = groupMap.get(edge.source);
1359
+ if (!memory || !group || grouped.has(memory.id)) continue;
1360
+ group.push(memory);
1361
+ grouped.add(memory.id);
1362
+ }
1363
+ const groups = projects.map((node) => projectCluster(node, groupMap.get(node.id).sort(order)));
1364
+ const ungrouped = memories.filter((node) => !grouped.has(node.id));
1365
+ const split = Math.ceil(agents.length / 2);
1366
+ const topAgents = agents.slice(0, split);
1367
+ const bottomAgents = agents.slice(split);
1368
+ const agentWidth = Math.min(split, 6) * 132 + 28;
1369
+ const agentHeight = (Math.ceil(split / 6) + Math.ceil(bottomAgents.length / 6)) * 45;
1370
+ const packed = packClusters(groups, graphViewport(), { width: agentWidth, height: agentHeight });
1371
+ const { width, rows, rowWidths, rowHeights } = packed;
1372
+ let y = 14 + graphPillGrid(positions, topAgents, width, 14, 132, 112) + 18;
1373
+ rows.forEach((row, rowIndex) => {
1374
+ let x = (width - rowWidths[rowIndex]) / 2 + 9.5;
1375
+ for (const group of row) {
1376
+ const cx = x + group.rx;
1377
+ const cy = y + rowHeights[rowIndex] / 2;
1378
+ clusters.push({ cx, cy, rx: group.rx, ry: group.ry });
1379
+ positions.set(group.hub.id, { x: cx - 66, y: cy - 21, width: 132, height: 42, count: group.count });
1380
+ group.rings.forEach(({ members, rx, ry }, ringIndex) => {
1381
+ members.forEach((node, index) => {
1382
+ const angle = (index + (ringIndex % 2 ? .5 : 0)) * Math.PI * 2 / members.length - Math.PI / 2;
1383
+ positions.set(node.id, { x: cx + Math.cos(angle) * rx - 11, y: cy + Math.sin(angle) * ry - 11, width: 22, height: 22, dot: true });
1384
+ });
1385
+ });
1386
+ x += group.rx * 2 + 19;
1387
+ }
1388
+ y += rowHeights[rowIndex];
1389
+ });
1390
+ y += graphPillGrid(positions, ungrouped, width, y + 3, 184);
1391
+ y += graphPillGrid(positions, bottomAgents, width, y + 8, 132, 112);
1392
+ return { positions, clusters, width, height: y + 16 };
1393
+ }
1394
+
1395
+ function graphEdgePath(edge, layout) {
1396
+ const a = layout.positions.get(edge.source);
1397
+ const b = layout.positions.get(edge.target);
1398
+ if (!a || !b) return '';
1399
+ const ax = a.x + a.width / 2;
1400
+ const ay = a.y + a.height / 2;
1401
+ const bx = b.x + b.width / 2;
1402
+ const by = b.y + b.height / 2;
1403
+ const start = 'M' + ax + ' ' + ay;
1404
+ if (edge.kind === 'belongs-to') return start + ' L' + bx + ' ' + by;
1405
+ if (recordedEdge(edge)) {
1406
+ const dx = bx - ax;
1407
+ const dy = by - ay;
1408
+ const length = Math.hypot(dx, dy) || 1;
1409
+ const pad = Math.min(b.width / 2 / Math.abs(dx), b.height / 2 / Math.abs(dy), .38);
1410
+ const lift = Math.min(63, Math.max(20, length * .25));
1411
+ return start + ' Q' + ((ax + bx) / 2 - dy / length * lift) + ' ' + ((ay + by) / 2 + dx / length * lift) +
1412
+ ' ' + (bx - dx * pad) + ' ' + (by - dy * pad);
1413
+ }
1414
+ const bend = (ay + by) / 2;
1415
+ return start + ' C' + ax + ' ' + bend + ' ' + bx + ' ' + bend + ' ' + bx + ' ' + by;
1416
+ }
1417
+
1418
+ function graphNodeMarkup(node, position, tabIndex) {
1419
+ const { x, y, width, height } = position;
1420
+ const label = compact(node.label || node.id);
1421
+ const hasCount = Number.isFinite(position.count);
1422
+ const mark = node.kind === 'project' ? '<rect class="node-glyph" x="11" y="' + (hasCount ? 10 : 12) + '" width="8" height="8" rx="1"/>'
1423
+ : node.kind === 'agent' ? '<circle class="node-glyph" cx="15" cy="16" r="4.5"/>' : '<path class="node-glyph" d="m15 11 5 5-5 5-5-5z"/>';
1424
+ const display = shorten(label, Math.floor((width - 30) / 6.1));
1425
+ const shape = position.dot
1426
+ ? '<circle class="node-surface" cx="11" cy="11" r="9.5"/><path class="node-glyph" d="m11 7 4 4-4 4-4-4z"/>'
1427
+ : '<rect class="node-surface" width="' + width + '" height="' + height + '" rx="6"/>' + mark +
1428
+ '<text x="26" y="' + (hasCount ? 18 : 21) + '">' + esc(display) + '</text>' +
1429
+ (hasCount ? '<text class="node-count" x="26" y="33">' + countLabel(position.count, 'memory', 'memories') + '</text>' : '');
1430
+ return '<g class="graph-node ' + node.kind + (position.dot ? ' dot-node' : '') + '" role="button" aria-label="' + esc(graphKinds[node.kind] + ': ' + label) +
1431
+ '" data-node="' + esc(node.id) + '" aria-pressed="false" tabindex="' + tabIndex + '" transform="translate(' + x + ' ' + y + ')"><title>' +
1432
+ esc(label) + '</title>' + shape + '</g>';
1433
+ }
1434
+
1435
+ function renderGraphList() {
1436
+ const graph = state.graph;
1437
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
1438
+ const links = graph.edges.filter(recordedEdge).map((edge) => '<li class="graph-row"><button type="button" class="button" data-edge-record="' +
1439
+ esc(edge.id) + '" aria-pressed="false"><span>' + esc(byId.get(edge.source)?.label || edge.source) + ' · ' +
1440
+ esc(graphEdgeLabels[edge.kind]) + ' · ' + esc(byId.get(edge.target)?.label || edge.target) + '</span></button></li>').join('');
1441
+ $('graph-list').innerHTML = (links ? '<details open class="panel-body"><summary>Recorded memory links</summary><ul class="list">' + links + '</ul></details>' : '') +
1442
+ '<ul class="list">' + graph.nodes.map((node) => '<li class="graph-row"><button type="button" class="button" data-node="' +
1443
+ esc(node.id) + '" aria-pressed="false">' + icon(node.kind, true) + '<span>' + esc(node.label || node.id) +
1444
+ '</span><span class="tag ' + node.kind + '">' + graphKinds[node.kind] + '</span></button></li>').join('') + '</ul>';
1445
+ }
1446
+
1447
+ function renderGraphMap() {
1448
+ const graph = state.graph;
1449
+ const layout = graph.layout = graphLayout(graph.nodes, graph.edges);
1450
+ const svg = $('graph-svg');
1451
+ const clusters = layout.clusters.map(({ cx, cy, rx, ry }) => '<ellipse class="cluster-area" cx="' + cx + '" cy="' + cy +
1452
+ '" rx="' + rx + '" ry="' + ry + '"/>').join('');
1453
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
1454
+ const structural = [];
1455
+ const recordedLinks = [];
1456
+ graph.edges.forEach((edge, i) => {
1457
+ const path = graphEdgePath(edge, layout);
1458
+ const recorded = recordedEdge(edge);
1459
+ const visual = '<path class="graph-edge' + (recorded ? ' recorded-edge ' + edge.kind : graphEdgeClass[edge.kind] || '') +
1460
+ '" data-edge="' + i + '" d="' + path + '"' + (recorded && edge.kind !== 'related' ? ' marker-end="url(#graph-arrow)"' : '') + '/>';
1461
+ if (recorded) {
1462
+ const from = byId.get(edge.source)?.label || edge.source;
1463
+ const to = byId.get(edge.target)?.label || edge.target;
1464
+ const label = from + ' · ' + graphEdgeLabels[edge.kind] + ' · ' + to;
1465
+ recordedLinks.push('<g class="graph-link-group" data-edge-record="' + esc(edge.id) + '" role="button" tabindex="-1" aria-pressed="false" aria-label="' +
1466
+ esc(label) + '"><title>' + esc(label) + '</title>' + visual + '<path class="graph-edge-hit" d="' + path + '"/></g>');
1467
+ } else structural.push(visual);
1468
+ });
1469
+ const nodes = graph.nodes.map((node, i) => graphNodeMarkup(node, layout.positions.get(node.id), i === 0 ? 0 : -1)).join('');
1470
+ svg.innerHTML = '<defs><pattern id="map-dots" width="18" height="18" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1" fill="var(--graph-dot)"/></pattern>' +
1471
+ '<marker id="graph-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="graph-arrow" d="M0 1 9 5 0 9z"/></marker></defs>' +
1472
+ '<rect width="100%" height="100%" fill="url(#map-dots)"/><g id="graph-transform"><g aria-hidden="true">' + clusters + structural.join('') + '</g>' +
1473
+ recordedLinks.join('') + nodes + '<g class="graph-tooltip" id="graph-tooltip" aria-hidden="true"></g></g>';
1474
+ fitGraph();
1475
+ }
1476
+
1477
+ function graphViewport() {
1478
+ const rect = $('graph-svg').getBoundingClientRect();
1479
+ return { width: Math.max(rect.width, 1), height: Math.max(rect.height, 1) };
1480
+ }
1481
+
1482
+ function updateGraphTransform() {
1483
+ const graph = state.graph;
1484
+ const transform = $('graph-transform');
1485
+ if (!transform || graph.mode !== 'map') return;
1486
+ const { width, height } = graphViewport();
1487
+ $('graph-svg').setAttribute('viewBox', '0 0 ' + width + ' ' + height);
1488
+ transform.setAttribute('transform', 'translate(' + graph.panX + ' ' + graph.panY + ') scale(' + graph.zoom + ')');
1489
+ $('graph-scale').textContent = Math.round(graph.zoom * 100) + '%';
1490
+ $('graph-out').disabled = graph.zoom <= .06;
1491
+ $('graph-in').disabled = graph.zoom >= 2.75;
1492
+ }
1493
+
1494
+ function fitGraph() {
1495
+ const graph = state.graph;
1496
+ if (!graph.layout || graph.mode !== 'map' || $('graph-body').hidden || state.view !== 'graph') return;
1497
+ const { width, height } = graphViewport();
1498
+ const { width: mapWidth, height: mapHeight } = graph.layout;
1499
+ const widthScale = Math.max(1, width - 24) / mapWidth;
1500
+ const heightScale = Math.max(1, height - 54) / mapHeight;
1501
+ const zoom = Math.min(1.35, widthScale, heightScale);
1502
+ graph.zoom = zoom;
1503
+ graph.panX = (width - mapWidth * zoom) / 2;
1504
+ graph.panY = Math.max(0, (height - 38 - mapHeight * zoom) / 2);
1505
+ updateGraphTransform();
1506
+ }
1507
+
1508
+ function zoomGraph(factor, anchor) {
1509
+ const graph = state.graph;
1510
+ if (!graph.layout) return;
1511
+ const viewport = graphViewport();
1512
+ const center = anchor || { x: viewport.width / 2, y: viewport.height / 2 };
1513
+ const previous = graph.zoom;
1514
+ const target = Math.min(2.75, Math.max(Math.min(.06, previous), previous * factor));
1515
+ const ratio = target / previous;
1516
+ graph.panX = center.x - (center.x - graph.panX) * ratio;
1517
+ graph.panY = center.y - (center.y - graph.panY) * ratio;
1518
+ graph.zoom = target;
1519
+ updateGraphTransform();
1520
+ }
1521
+
1522
+ function centerGraphNode(id) {
1523
+ const graph = state.graph;
1524
+ const position = graph.layout?.positions.get(id);
1525
+ if (!position || graph.mode !== 'map') return;
1526
+ const { width, height } = graphViewport();
1527
+ const left = graph.panX + position.x * graph.zoom;
1528
+ const top = graph.panY + position.y * graph.zoom;
1529
+ if (graph.zoom >= .8 && left >= 14 && top >= 14 && left + position.width * graph.zoom <= width - 14 &&
1530
+ top + position.height * graph.zoom <= height - 52) return;
1531
+ const zoom = Math.max(.8, graph.zoom);
1532
+ graph.zoom = zoom;
1533
+ graph.panX = width / 2 - (position.x + position.width / 2) * zoom;
1534
+ graph.panY = (height - 38) / 2 - (position.y + position.height / 2) * zoom;
1535
+ updateGraphTransform();
1536
+ }
1537
+
1538
+ function renderGraphTooltip(id) {
1539
+ const graph = state.graph;
1540
+ const tooltip = $('graph-tooltip');
1541
+ if (!tooltip) return;
1542
+ const point = graph.layout.positions.get(id);
1543
+ const node = graph.nodes.find((item) => item.id === id);
1544
+ if (!point?.dot || !node) {
1545
+ tooltip.replaceChildren();
1546
+ return;
1547
+ }
1548
+ const label = shorten(compact(node.label || node.id), 48);
1549
+ const width = Math.max(70, label.length * 6.7 + 18);
1550
+ const x = Math.min(graph.layout.width - width - 2, Math.max(2, point.x + 11 - width / 2));
1551
+ const y = Math.max(1, point.y - 32);
1552
+ tooltip.setAttribute('transform', 'translate(' + x + ' ' + y + ')');
1553
+ tooltip.innerHTML = '<rect width="' + width + '" height="26" rx="5"/><text x="9" y="17">' + esc(label) + '</text>';
1554
+ }
1555
+
1556
+ function renderGraphInspector() {
1557
+ const graph = state.graph;
1558
+ const all = new Map(list(graph.snapshot?.nodes).map((node) => [node.id, node]));
1559
+ const selected = all.get(graph.selected);
1560
+ const inspector = $('graph-inspector');
1561
+ const selectedEdge = graph.edges.find((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge);
1562
+ if (selectedEdge) {
1563
+ const from = all.get(selectedEdge.source);
1564
+ const to = all.get(selectedEdge.target);
1565
+ const record = (node) => node ? memoryLink(graphMemoryId(node), node.project, node.label || node.id) : '';
1566
+ const endpoint = (label, node) => (selectedEdge.kind === 'related' ? '' : '<p class="small muted">' + label + '</p>') + '<p>' + record(node) + '</p>';
1567
+ inspector.innerHTML = '<div class="panel-head"><h3>Link details</h3></div><div class="panel-body">' +
1568
+ '<div>' + endpoint('From memory', from) + '</div>' + tags([['memory', graphEdgeLabels[selectedEdge.kind]], ['agent', 'Recorded memory link']]) +
1569
+ '<div>' + endpoint('To memory', to) + '</div>' + (selectedEdge.note ? '<p class="small muted detail-note">' + esc(selectedEdge.note) + '</p>' : '') + '</div>';
1570
+ return;
1571
+ }
1572
+ if (!selected) {
1573
+ inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div>' +
1574
+ blank('graph', 'Select a node', 'See its stored details and follow its recorded connections.');
1575
+ return;
1576
+ }
1577
+ const adjacent = graphNeighbors(graph.edges, selected.id).filter((item) => all.has(item.id));
1578
+ const connected = adjacent.length ? '<details open><summary>' + countLabel(adjacent.length, 'connection') + '</summary><div class="related">' +
1579
+ adjacent.map(({ id, edge }) => {
1580
+ const node = all.get(id);
1581
+ if (recordedEdge(edge)) return '<button type="button" class="button quiet" data-edge-record="' + esc(edge.id) +
1582
+ '"><span>' + esc(linkCaption({ fromId: edge.source, type: edge.kind }, selected.id)) + ' · ' + esc(node.label || node.id) + '</span></button>';
1583
+ return '<button type="button" class="button quiet" data-related="' + esc(id) + '" title="' +
1584
+ esc(graphEdgeLabels[edge.kind] || nice(edge.kind) || 'Connection') + '">' + icon(node.kind, true) + '<span>' +
1585
+ esc(node.label || node.id) + '</span></button>';
1586
+ }).join('') + '</div></details>' : '<p class="muted small">No connections in this view.</p>';
1587
+ const project = state.projects.find((item) => item.id === selected.project)?.name || selected.project;
1588
+ const snippet = compact(selected.snippet) && compact(selected.snippet) !== compact(selected.label)
1589
+ ? '<p class="small">' + esc(selected.snippet) + '</p>' : '';
1590
+ const canFocus = selected.kind === 'memory' && adjacent.some(({ edge }) => recordedEdge(edge)) && graph.lens !== selected.id;
1591
+ inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div><div class="panel-body">' +
1592
+ tags([[selected.kind, graphKinds[selected.kind]], ['memory', nice(selected.category)], ['state-' + (selected.effectiveState || ''), reviewLabel(selected.effectiveState)]]) +
1593
+ '<h3>' + (selected.kind === 'memory' ? memoryLink(graphMemoryId(selected), selected.project, selected.label || selected.id) : esc(selected.label || selected.id)) + '</h3>' +
1594
+ (project && selected.kind !== 'project' ? '<p class="muted small">' + esc(project) + '</p>' : '') + snippet +
1595
+ (selected.kind === 'memory' ? sourceMarkup([...list(selected.sources), selected.source]) : '') +
1596
+ (canFocus ? '<button class="button" type="button" data-graph-lens="' + esc(selected.id) + '">' + icon('graph', true) + 'Focus recorded links</button>' : '') +
1597
+ connected + '</div>';
1598
+ }
1599
+
1600
+ function updateGraphSelection() {
1601
+ const graph = state.graph;
1602
+ const id = graph.selected;
1603
+ const selectedEdge = graph.edges.find((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge);
1604
+ const connected = new Set(graphNeighbors(graph.edges, id).map((item) => item.id));
1605
+ connected.add(id);
1606
+ if (selectedEdge) { connected.add(selectedEdge.source); connected.add(selectedEdge.target); }
1607
+ const hasSelection = !!id || !!selectedEdge;
1608
+ const tabStop = id || selectedEdge?.source || graph.nodes[0]?.id;
1609
+ $('graph-body').querySelectorAll('[data-node]').forEach((element) => {
1610
+ const nodeId = element.dataset.node;
1611
+ element.setAttribute('aria-pressed', String(id === nodeId));
1612
+ if (element instanceof SVGElement) {
1613
+ element.classList.toggle('unrelated', hasSelection && !connected.has(nodeId));
1614
+ element.classList.toggle('selected-memory', id === nodeId && nodeId.startsWith('memory:'));
1615
+ element.setAttribute('tabindex', nodeId === tabStop ? '0' : '-1');
1616
+ }
1617
+ });
1618
+ if (graph.mode === 'map') {
1619
+ $('graph-svg').querySelectorAll('[data-edge]').forEach((element) => {
1620
+ const edge = graph.edges[Number(element.dataset.edge)];
1621
+ const active = edge === selectedEdge || edge.source === id || edge.target === id;
1622
+ element.classList.toggle('active', active);
1623
+ element.classList.toggle('unrelated', hasSelection && !active);
1624
+ });
1625
+ const linkTabStop = graph.selectedEdge || graph.edges.find(recordedEdge)?.id;
1626
+ $('graph-svg').querySelectorAll('[data-edge-record]').forEach((element) => {
1627
+ element.setAttribute('aria-pressed', String(element.dataset.edgeRecord === graph.selectedEdge));
1628
+ element.setAttribute('tabindex', element.dataset.edgeRecord === linkTabStop ? '0' : '-1');
1629
+ });
1630
+ renderGraphTooltip(id);
1631
+ }
1632
+ $('graph-list').querySelectorAll('[data-edge-record]').forEach((element) => {
1633
+ element.setAttribute('aria-pressed', String(element.dataset.edgeRecord === graph.selectedEdge));
1634
+ });
1635
+ renderGraphInspector();
1636
+ }
1637
+
1638
+ function selectGraphNode(id, { center = false, focus = false } = {}) {
1639
+ const graph = state.graph;
1640
+ graph.selected = id;
1641
+ graph.selectedEdge = '';
1642
+ updateGraphSelection();
1643
+ if (center) centerGraphNode(id);
1644
+ if (focus) {
1645
+ const target = graph.mode === 'map' ? $('graph-svg') : $('graph-list');
1646
+ target.querySelector('[data-node="' + CSS.escape(id) + '"]')?.focus();
1647
+ }
1648
+ }
1649
+
1650
+ function selectGraphEdge(id, fromInspector) {
1651
+ const graph = state.graph;
1652
+ if (!graph.edges.some((edge) => recordedEdge(edge) && edge.id === id)) return;
1653
+ graph.selected = '';
1654
+ graph.selectedEdge = id;
1655
+ updateGraphSelection();
1656
+ if (fromInspector) $('graph-inspector').querySelector('a')?.focus();
1657
+ }
1658
+
1659
+ function renderGraph() {
1660
+ const graph = state.graph;
1661
+ if (!graph.snapshot) return;
1662
+ const search = $('graph-query').value.trim().toLowerCase();
1663
+ const kind = $('graph-kind').value;
1664
+ const relation = $('graph-relation').value;
1665
+ const allNodes = list(graph.snapshot.nodes).filter((node) => Object.hasOwn(graphKinds, node.kind));
1666
+ const allEdges = list(graph.snapshot.edges);
1667
+ const anchor = allNodes.find((node) => node.id === graph.lens);
1668
+ const edges = anchor ? allEdges.filter((edge) => recordedEdge(edge) && (edge.source === anchor.id || edge.target === anchor.id)) : allEdges;
1669
+ const lensIds = anchor ? new Set([anchor.id, ...edges.flatMap((edge) => [edge.source, edge.target])]) : null;
1670
+ const nodes = lensIds ? allNodes.filter((node) => lensIds.has(node.id)) : allNodes;
1671
+ $('graph-lens').hidden = !anchor;
1672
+ $('graph-body').dataset.lens = String(!!anchor);
1673
+ $('graph-lens-label').textContent = anchor ? 'Recorded links for ' + shorten(compact(anchor.label || anchor.id), 94) : '';
1674
+ const matches = nodes.filter((node) => (!kind || node.kind === kind) &&
1675
+ (!search || [node.label, node.snippet, node.project, node.category].filter(Boolean).join(' ').toLowerCase().includes(search)));
1676
+ const ids = new Set(matches.map((node) => node.id));
1677
+ if (search && !kind) {
1678
+ matches.forEach((node) => graphNeighbors(edges, node.id).forEach((item) => ids.add(item.id)));
1679
+ }
1680
+ const matchingEdges = edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target) &&
1681
+ (!relation || (relation === 'recorded' ? recordedEdge(edge) : edge.kind === relation)));
1682
+ if (relation) {
1683
+ const endpoints = new Set(matchingEdges.flatMap((edge) => [edge.source, edge.target]));
1684
+ for (const id of ids) if (!endpoints.has(id)) ids.delete(id);
1685
+ }
1686
+ graph.nodes = nodes.filter((node) => ids.has(node.id));
1687
+ graph.edges = matchingEdges;
1688
+ if (graph.selected && !ids.has(graph.selected)) graph.selected = '';
1689
+ if (graph.selectedEdge && !graph.edges.some((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge)) graph.selectedEdge = '';
1690
+ const matchingCount = search && matches.length !== graph.nodes.length ? countLabel(matches.length, 'match', 'matches') + ' · ' : '';
1691
+ $('graph-count').textContent = matchingCount + countLabel(graph.nodes.length, 'node') + ' · ' + countLabel(graph.edges.length, 'connection');
1692
+ $('graph-body').hidden = !graph.nodes.length;
1693
+ $('graph-legend').hidden = !graph.nodes.length;
1694
+ const note = graph.snapshot.truncated ? '<div class="panel-body small muted">This snapshot includes ' +
1695
+ number(graph.snapshot.shownMemories) + ' memories. Filter by project to explore a narrower set.</div>' : '';
1696
+ const focused = graph.focusMemory && graph.nodes.find((node) => graphMemoryId(node) === graph.focusMemory);
1697
+ const missing = graph.focusMemory && !focused ? '<p class="panel-body small muted">This memory is outside the current graph view.</p>' : '';
1698
+ $('graph-status').innerHTML = graph.nodes.length ? note + missing : missing + blank('graph', search || kind || relation ? 'No matching nodes' : 'No relationships yet',
1699
+ search || kind || relation ? 'Change the name, node type, or relationship filter.' : 'The graph fills in as project and agent memories are stored.');
1700
+ if (!graph.nodes.length) return;
1701
+ $('graph-stage').hidden = graph.mode !== 'map';
1702
+ $('graph-list').hidden = graph.mode !== 'list';
1703
+ $('graph-mode-map').setAttribute('aria-pressed', String(graph.mode === 'map'));
1704
+ $('graph-mode-list').setAttribute('aria-pressed', String(graph.mode === 'list'));
1705
+ renderGraphList();
1706
+ if (graph.mode === 'map') renderGraphMap();
1707
+ if (focused) {
1708
+ graph.focusMemory = '';
1709
+ selectGraphNode(focused.id);
1710
+ } else updateGraphSelection();
1711
+ }
1712
+
1713
+ async function loadGraph(project, force = false, memory = '') {
1714
+ const graph = state.graph;
1715
+ graph.focusMemory = memory;
1716
+ if (!force && !memory && graph.snapshot && graph.project === project) {
1717
+ renderGraph();
1718
+ return;
1719
+ }
1720
+ const seq = ++graph.request;
1721
+ graph.controller?.abort();
1722
+ const controller = new AbortController();
1723
+ graph.controller = controller;
1724
+ graph.snapshot = null;
1725
+ if (graph.project !== project || memory) graph.lens = '';
1726
+ graph.project = project;
1727
+ graph.selected = '';
1728
+ graph.selectedEdge = '';
1729
+ setBusy($('graph-panel'), true);
1730
+ $('graph-status').innerHTML = skeleton('Loading relationships…');
1731
+ $('graph-count').textContent = '';
1732
+ $('graph-body').hidden = true;
1733
+ $('graph-lens').hidden = true;
1734
+ $('graph-legend').hidden = true;
1735
+ try {
1736
+ const snapshot = await api(queryPath('graph', { project, memory }), { signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]) });
1737
+ if (seq !== graph.request) return;
1738
+ graph.snapshot = snapshot;
1739
+ renderGraph();
1740
+ } catch (error) {
1741
+ if (seq === graph.request && error.name !== 'AbortError') $('graph-status').innerHTML = errorMarkup(error.message, 'graph');
1742
+ } finally {
1743
+ if (seq === graph.request) setBusy($('graph-panel'), false);
1744
+ }
1745
+ }
1746
+
1747
+ function renderContextControls() {
1748
+ const input = $('context-cross');
1749
+ const toggle = state.features?.features;
1750
+ const canCross = document.documentElement.dataset.projectLocal !== 'true' && toggle?.contextAssembly === true && toggle?.crossProjectContext === true;
1751
+ $('context-cross-row').hidden = !canCross;
1752
+ if (!canCross) input.checked = false;
1753
+ $('context-submit').disabled = !toggle?.contextAssembly || !state.projects.length || state.context.pending;
1754
+ $('context-related-row').hidden = $('context-profile').value !== 'adaptive';
1755
+ const gate = $('context-gate');
1756
+ if (!toggle && state.featurePending) {
1757
+ gate.innerHTML = '<p class="muted small" role="status">Checking feature settings…</p>';
1758
+ } else if (!toggle) {
1759
+ gate.innerHTML = errorMarkup('Could not check context assembly settings.', 'features');
1760
+ } else if (!toggle.contextAssembly) {
1761
+ gate.innerHTML = '<p class="muted small">Context assembly is turned off. <a href="#features">Manage in Features</a></p>';
1762
+ } else if (!state.overviewLoaded && state.overviewPending) {
1763
+ gate.innerHTML = '<p class="muted small" role="status">Loading projects…</p>';
1764
+ } else if (!state.overviewLoaded) {
1765
+ gate.innerHTML = errorMarkup('Could not load projects.', 'overview');
1766
+ } else if (!state.projects.length) {
1767
+ gate.innerHTML = '<p class="muted small">Store a project memory before assembling context.</p>';
1768
+ } else {
1769
+ gate.replaceChildren();
1770
+ }
1771
+ }
1772
+
1773
+ function contextReasonList(items, format) {
1774
+ return '<ul class="list">' + items.map(format).join('') + '</ul>';
1775
+ }
1776
+
1777
+ const contextReasons = {
1778
+ selected: 'Included in the brief.', duplicate: 'Matches a memory already considered.', budget: 'Would exceed the selected budget.',
1779
+ limit: 'Beyond the brief’s memory limit.', 'candidate-limit': 'Beyond the evaluated candidate limit.', empty: 'Contains no memory text.',
1780
+ lookback: 'Falls outside the selected time range.', archived: 'Archived in a recorded review.', contested: 'Marked as needing review.',
1781
+ superseded: 'Superseded by a recorded replacement.'
1782
+ };
1783
+ const rankScore = (value) => Number.isFinite(value) ? value.toLocaleString(undefined, { maximumFractionDigits: 3 }) : '—';
1784
+
1785
+ function renderRetrieval(pack) {
1786
+ const retrieval = pack.retrieval;
1787
+ if (!retrieval || !pack.enabled) return '';
1788
+ const counts = list(retrieval.stages);
1789
+ const max = Math.max(1, ...counts.map((stage) => Number(stage.count) || 0));
1790
+ const stages = counts.map((stage) => '<li><strong>' + number(stage.count) + '</strong><span>' + esc(stage.label) +
1791
+ '</span><div class="retrieval-stage-bar" aria-hidden="true"><i style="width:' + Math.max(0, (Number(stage.count) || 0) / max * 100) + '%"></i></div></li>').join('');
1792
+ const candidates = list(retrieval.candidates);
1793
+ const labels = new Map([...list(pack.entries), ...candidates].map((item) => [item.id, item.title || shorten(compact(item.excerpt), 95) || item.id]));
1794
+ const rows = candidates.map((candidate) => {
1795
+ const signals = list(candidate.signals).map((signal) => {
1796
+ const sign = signal.label === 'Project task feedback' && signal.value > 0 ? '+' : '';
1797
+ return '<div><dt>' + esc(signal.label) + '</dt><dd>' + sign + esc(rankScore(signal.value)) + '</dd></div>';
1798
+ }).join('');
1799
+ const terms = list(candidate.matchedTerms);
1800
+ const fields = list(candidate.matchedFields);
1801
+ const paths = list(candidate.paths).map((path) => '<li><span class="tag agent">' + esc(linkTypes[path.relationType] || nice(path.relationType)) +
1802
+ '</span> ' + memoryLink(path.fromId, candidate.projectId, labels.get(path.fromId) || path.fromId) + ' ' + icon('chevron', true) + ' ' +
1803
+ memoryLink(path.toId, candidate.projectId, labels.get(path.toId) || path.toId) + '</li>').join('');
1804
+ const feedback = candidate.feedback;
1805
+ const reporters = list(feedback?.reporterKinds).map((kind) => kind === 'agent' ? 'Agent' : 'User');
1806
+ const feedbackSource = feedback ? '<p class="small muted">' + countLabel(list(feedback.recordIds).length, 'matching task report') +
1807
+ (reporters.length ? ' · ' + esc(reporters.join(' and ')) : '') +
1808
+ (state.projectKit?.projectId === candidate.projectId ? ' · <a href="' + esc(routeLink('project-kit', { memory: candidate.id })) + '">Review project outcomes</a>' : '') + '</p>' : '';
1809
+ const included = candidate.decision === 'selected';
1810
+ return '<li><details class="candidate"><summary><span class="candidate-title"><span class="rank">#' + number(candidate.rank) + '</span>' +
1811
+ '<span>' + esc(candidate.title || candidate.id) + '</span><span class="tag check-' + (included ? 'ok' : 'warn') + '">' +
1812
+ (included ? 'Included' : 'Excluded') + '</span></span></summary><div class="candidate-body"><p class="small muted">' +
1813
+ esc(contextReasons[candidate.reason] || nice(candidate.reason)) + ' Rank score ' + esc(rankScore(candidate.score)) + '.</p>' +
1814
+ (terms.length ? '<p class="small"><strong>Matched terms</strong> ' + esc(terms.join(', ')) + '</p>' : '') +
1815
+ (fields.length ? '<p class="small"><strong>Matched fields</strong> ' + esc(fields.map(nice).join(', ')) + '</p>' : '') +
1816
+ (signals ? '<dl class="candidate-signals" aria-label="Ranking contributions">' + signals + '</dl>' : '') + feedbackSource + sourceMarkup(candidate.sources) +
1817
+ (paths ? '<div><h3>Recorded link paths</h3><ul class="list">' + paths + '</ul></div>' : '') +
1818
+ '<p>' + memoryLink(candidate.id, candidate.projectId, 'Open memory') + '</p></div></details></li>';
1819
+ }).join('');
1820
+ const reportCount = retrieval.truncated ? ' Candidate diagnostics are bounded to ' + number(retrieval.candidateLimit) + ' records.' : '';
1821
+ return '<section class="context-section" aria-labelledby="retrieval-heading"><h3 id="retrieval-heading">Retrieval pipeline</h3>' +
1822
+ '<ol class="retrieval-flow" aria-label="Retrieval stages">' + stages + '</ol><p class="small muted">Scanned ' + number(retrieval.scanned) +
1823
+ ' memories. Ranking contributions explain ordering.' + esc(reportCount) + '</p>' +
1824
+ (rows ? '<details class="context-section" open><summary>Candidate decisions · ' + number(candidates.length) + '</summary>' +
1825
+ '<ul class="list candidate-list" aria-label="Candidate decisions">' + rows + '</ul></details>' : '') + '</section>';
1826
+ }
1827
+
1828
+ function renderComparison() {
1829
+ const target = $('context-comparison');
1830
+ if (!target) return;
1831
+ const { comparison, comparing, dirty } = state.context;
1832
+ if (dirty || !comparison && !comparing) {
1833
+ target.replaceChildren();
1834
+ return;
1835
+ }
1836
+ if (comparing) {
1837
+ target.innerHTML = skeleton('Comparing retrieval profiles…');
1838
+ return;
1839
+ }
1840
+ if (comparison.error) {
1841
+ target.innerHTML = errorMarkup(comparison.error);
1842
+ return;
1843
+ }
1844
+ const { legacy, adaptive, delta } = comparison;
1845
+ const shared = list(delta?.sharedIds);
1846
+ const column = (label, pack, ids) => {
1847
+ const unique = new Set(ids);
1848
+ const entries = list(pack.entries);
1849
+ const differences = entries.filter((entry) => unique.has(entry.id));
1850
+ return '<div><h3>' + label + '</h3><p class="small muted">' + countLabel(entries.length, 'included memory', 'included memories') + ' · ' +
1851
+ number(pack.budget?.estimatedTokens) + ' estimated tokens</p><p class="small"><strong>' + number(differences.length) + ' unique to ' + label + '</strong></p>' +
1852
+ (differences.length ? '<ul class="small">' + differences.map((entry) => '<li>' +
1853
+ memoryLink(entry.id, entry.projectId, shorten(compact(entry.excerpt), 95) || entry.id) + '</li>').join('') + '</ul>' : '') + '</div>';
1854
+ };
1855
+ target.innerHTML = '<h3>Profile comparison</h3><p class="small muted">Same task, scope, and requested budget · ' +
1856
+ countLabel(shared.length, 'shared memory', 'shared memories') + '</p><div class="compare-grid">' +
1857
+ column('Adaptive', adaptive, list(delta?.addedIds)) + column('Legacy', legacy, list(delta?.removedIds)) + '</div>' +
1858
+ list(comparison.warnings).map((warning) => '<p class="small muted">' + esc(warning) + '</p>').join('');
1859
+ }
1860
+
1861
+ function renderContext() {
1862
+ const output = $('context-output');
1863
+ const { pack, dirty, params } = state.context;
1864
+ const status = $('context-result-state');
1865
+ if (!pack) {
1866
+ const enabled = state.features?.features?.contextAssembly !== false;
1867
+ status.textContent = enabled ? '' : 'Disabled';
1868
+ output.innerHTML = enabled ? blank('context', 'Ready for a task', 'Choose a project and describe the work to see the context and its sources here.') :
1869
+ blank('context', 'Context assembly is turned off', 'Enable it in Features to build a brief.');
1870
+ return;
1871
+ }
1872
+ const entries = list(pack.entries);
1873
+ const excluded = list(pack.excluded);
1874
+ const budget = pack.budget || {};
1875
+ const warnings = list(pack.warnings);
1876
+ const allowed = state.features?.features?.contextAssembly === true;
1877
+ const canInspect = allowed && !dirty && pack.enabled;
1878
+ const canCopy = canInspect && !!pack.markdown && entries.length > 0;
1879
+ status.textContent = dirty ? 'Rebuild needed' : pack.enabled ? 'Assembled' : 'Disabled';
1880
+ const warningMarkup = warnings.map((warning) => '<p class="small muted">' + esc(warning) + '</p>').join('');
1881
+ const selection = contextReasonList(entries, (entry) => {
1882
+ const title = shorten(compact(entry.excerpt), 120) || nice(entry.category) || entry.id;
1883
+ const reason = list(entry.reasons).map((detail) => '<p class="small muted">' + esc(detail) + '</p>').join('');
1884
+ const links = list(entry.related).map((relation) => {
1885
+ const directional = { fromId: relation.direction === 'outgoing' ? entry.id : relation.otherMemoryId, type: relation.type };
1886
+ return '<p class="small">' + esc(linkCaption(directional, entry.id)) + ' ' +
1887
+ memoryLink(relation.otherMemoryId, entry.projectId, relation.title || relation.otherMemoryId) + '</p>';
1888
+ }).join('');
1889
+ return '<li><strong>' + memoryLink(entry.id, entry.projectId, title) + '</strong><div class="small muted">' +
1890
+ esc([entry.projectName, entry.agentTool, nice(entry.category)].filter(Boolean).join(' · ')) +
1891
+ '</div>' + reason + sourceMarkup(entry.sources) + links + '</li>';
1892
+ });
1893
+ const rejections = contextReasonList(excluded, (entry) => '<li><strong class="mono">' +
1894
+ memoryLink(entry.id, entry.projectId || (pack.scope === 'project' ? pack.projectId : ''), entry.title || entry.id) +
1895
+ '</strong><p class="small muted">' + esc(contextReasons[entry.reason] || nice(entry.reason)) + '</p></li>');
1896
+ const projectName = state.projects.find((item) => item.id === pack.projectId)?.name || pack.projectId;
1897
+ const scope = pack.scope === 'cross-project' ? 'Cross-project · ' + projectName : projectName;
1898
+ const usage = budget.tokenBudget > 0 ? Math.min(100, Math.max(0, budget.estimatedTokens / budget.tokenBudget * 100)) : 0;
1899
+ output.innerHTML = '<div class="panel-body stack">' + (dirty ? '<p class="status small" role="status">This preview is out of date. Assemble again to refresh it.</p>' : '') +
1900
+ '<div class="context-output-head">' + tags([['project', scope], ['memory', params?.profile === 'adaptive' ? 'Adaptive' : 'Legacy']]) +
1901
+ '<div class="form-actions">' + (canInspect && params ? '<button type="button" class="button" id="context-compare-button"' +
1902
+ (state.context.comparing ? ' disabled' : '') + '>Compare profiles</button>' : '') +
1903
+ (canCopy ? '<button class="button" id="context-download" type="button">Download .md</button>' +
1904
+ '<button class="button primary" id="context-copy" type="button">' + icon('copy') + 'Copy brief</button>' : '') + '</div></div>' +
1905
+ '<p id="copy-status" class="small muted" role="status" hidden></p><dl class="context-metrics">' +
1906
+ '<div><dt>Estimated tokens</dt><dd>' + number(budget.estimatedTokens) + ' / ' + number(budget.tokenBudget) + '</dd><div class="context-budget-track" aria-hidden="true"><span style="width:' + usage + '%"></span></div></div>' +
1907
+ '<div><dt>Characters</dt><dd>' + number(budget.usedChars) + ' / ' + number(budget.maxChars) + '</dd></div>' +
1908
+ '<div><dt>Included / excluded</dt><dd>' + number(entries.length) + ' / ' + number(excluded.length) + '</dd></div></dl>' +
1909
+ warningMarkup + '<section class="context-compare" id="context-comparison" aria-live="polite"></section>' + renderRetrieval(pack) +
1910
+ '<div class="context-reasons">' +
1911
+ (entries.length ? '<details class="context-section"><summary>Included memories · ' + number(entries.length) + '</summary>' + selection + '</details>' : '') +
1912
+ (excluded.length ? '<details class="context-section"><summary>Excluded memories · ' + number(excluded.length) + '</summary>' + rejections + '</details>' : '') +
1913
+ '</div>' + (pack.markdown && pack.enabled && entries.length ? '<details class="context-section" open><summary>Assembled brief</summary><pre class="context-preview mono" id="context-text" tabindex="0" aria-label="Assembled context markdown">' + esc(pack.markdown) + '</pre></details>' :
1914
+ blank('context', pack.enabled ? 'No memories fit this task' : 'Context assembly is turned off', pack.enabled ? 'Review any exclusions or try a different task.' : 'Enable it in Features to build a brief.')) + '</div>';
1915
+ renderComparison();
1916
+ }
1917
+
1918
+ async function assembleContext() {
1919
+ const store = state.context;
1920
+ const toggle = state.features?.features;
1921
+ const query = $('context-task').value.trim();
1922
+ const projectId = $('context-project').value;
1923
+ const tokenBudget = Number($('context-budget').value);
1924
+ if (store.pending || !toggle?.contextAssembly || !projectId) return;
1925
+ if (!query) {
1926
+ $('context-task').setCustomValidity('Enter a task.');
1927
+ $('context-task').reportValidity();
1928
+ return;
1929
+ }
1930
+ const profile = $('context-profile').value;
1931
+ const body = { query, projectId, tokenBudget, includeCrossProject: toggle.crossProjectContext && $('context-cross').checked,
1932
+ profile, ...(profile === 'adaptive' ? { expandRelated: $('context-related').checked } : {}) };
1933
+ store.pending = true;
1934
+ store.dirty = false;
1935
+ store.pack = null;
1936
+ store.params = body;
1937
+ store.comparison = null;
1938
+ store.comparing = false;
1939
+ renderContextControls();
1940
+ setBusy($('context-result'), true);
1941
+ $('context-result-state').textContent = 'Assembling';
1942
+ $('context-output').innerHTML = skeleton('Assembling context…');
1943
+ try {
1944
+ const pack = await api('context', { body });
1945
+ const current = state.features?.features;
1946
+ if (store.params === body && current?.contextAssembly && (pack.scope !== 'cross-project' || current.crossProjectContext)) store.pack = pack;
1947
+ renderContext();
1948
+ } catch (error) {
1949
+ if (store.params === body) {
1950
+ $('context-result-state').textContent = 'Request failed';
1951
+ $('context-output').innerHTML = errorMarkup(error.message, 'context');
1952
+ }
1953
+ } finally {
1954
+ store.pending = false;
1955
+ setBusy($('context-result'), false);
1956
+ renderContextControls();
1957
+ }
1958
+ }
1959
+
1960
+ async function compareContext() {
1961
+ const store = state.context;
1962
+ if (!store.params || store.dirty || store.pending || store.comparing || !store.pack?.enabled) return;
1963
+ const params = store.params;
1964
+ const { profile, ...body } = params;
1965
+ store.comparing = true;
1966
+ $('context-compare-button').disabled = true;
1967
+ renderComparison();
1968
+ try {
1969
+ const result = await api('context-compare', { body });
1970
+ if (!result.adaptive || !result.legacy || !result.delta) throw new Error('The server returned an unreadable comparison.');
1971
+ if (store.params === params && !store.dirty) store.comparison = result;
1972
+ } catch (error) {
1973
+ if (store.params === params && !store.dirty) store.comparison = { error: error.message };
1974
+ } finally {
1975
+ if (store.params === params) {
1976
+ store.comparing = false;
1977
+ if ($('context-compare-button')) $('context-compare-button').disabled = false;
1978
+ renderComparison();
1979
+ }
1980
+ }
1981
+ }
1982
+
1983
+ function downloadContext() {
1984
+ const { pack, dirty } = state.context;
1985
+ if (!pack?.markdown || !pack.enabled || dirty || !state.features?.features.contextAssembly) return;
1986
+ const url = URL.createObjectURL(new Blob([pack.markdown], { type: 'text/markdown;charset=utf-8' }));
1987
+ const link = document.createElement('a');
1988
+ link.href = url;
1989
+ link.download = 'shadow-brain-context.md';
1990
+ link.click();
1991
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1992
+ }
1993
+
1994
+ async function copyContext() {
1995
+ const store = state.context;
1996
+ const status = $('copy-status');
1997
+ if (!store.pack?.markdown || !store.pack.enabled || store.dirty || !status || !state.features?.features.contextAssembly) return;
1998
+ try {
1999
+ await navigator.clipboard.writeText(store.pack.markdown);
2000
+ status.textContent = 'Brief copied.';
2001
+ } catch {
2002
+ status.textContent = 'Clipboard access is unavailable. Select and copy the preview text.';
2003
+ }
2004
+ status.hidden = false;
2005
+ }
2006
+
2007
+ const checkStates = { pass: ['ok', 'Passed'], warn: ['warn', 'Review'], unknown: ['', 'Unknown'], error: ['error', 'Error'] };
2008
+ const installStates = {
2009
+ complete: ['ok', 'Files present'], partial: ['warn', 'Some files present'], missing: ['', 'Files missing'],
2010
+ 'not-checked': ['', 'Unchecked'], blocked: ['error', 'Check blocked']
2011
+ };
2012
+
2013
+ function checkTag(value, labels) {
2014
+ const [kind, label] = Object.hasOwn(labels, value) ? labels[value] : ['', 'Unknown'];
2015
+ return '<span class="tag check-' + kind + '">' + label + '</span>';
2016
+ }
2017
+
2018
+ function commandMarkup(command) {
2019
+ if (typeof command !== 'string' || !command.trim()) return '';
2020
+ return '<div class="form-actions"><code class="mono">' + esc(command) + '</code><button type="button" class="button quiet" data-copy-command="' +
2021
+ esc(command) + '" aria-label="Copy command">' + icon('copy', true) + 'Copy</button><span class="small muted" role="status"></span></div>';
2022
+ }
2023
+
2024
+ function docsLink(value) {
2025
+ if (typeof value !== 'string') return '';
2026
+ try {
2027
+ const url = new URL(value);
2028
+ if (url.protocol !== 'https:') return '';
2029
+ return '<a class="button quiet" href="' + esc(url.href) + '" target="_blank" rel="noopener noreferrer">Documentation ' +
2030
+ icon('diagonal', true) + '</a>';
2031
+ } catch { return ''; }
2032
+ }
2033
+
2034
+ function renderIntegrations(data) {
2035
+ const checks = list(data.checks);
2036
+ const agents = list(data.agents);
2037
+ const checkRows = checks.map((check) => '<li class="check-row"><h3>' + esc(check.label || check.id) + '</h3><div class="check-result"><p class="small muted">' +
2038
+ esc(check.detail) + '</p>' + commandMarkup(check.command) + '</div>' + checkTag(check.status, checkStates) + '</li>').join('');
2039
+ const agentRows = agents.map((agent) => '<li class="check-row"><h3>' + esc(agent.label || agent.id) + '</h3><div class="check-result">' +
2040
+ tags(list(agent.capabilities).map((value) => ['memory', value])) + (agent.detail ? '<p class="small muted">' + esc(agent.detail) + '</p>' : '') +
2041
+ (agent.note ? '<p class="small muted">' + esc(agent.note) + '</p>' : '') + commandMarkup(agent.command) +
2042
+ (typeof agent.config?.content === 'string' ? '<details class="integration-config"><summary>View MCP setup example</summary>' +
2043
+ '<p class="small muted">' + esc([agent.config.format, agent.config.filename].filter(Boolean).join(' · ')) + '</p>' +
2044
+ '<pre class="context-preview mono" tabindex="0">' + esc(agent.config.content) + '</pre>' +
2045
+ list(agent.config.notes).map((note) => '<p class="small muted">' + esc(note) + '</p>').join('') +
2046
+ '<div class="form-actions"><button type="button" class="button quiet" data-copy-command="' + esc(agent.config.content) + '">' +
2047
+ icon('copy', true) + 'Copy example</button><span class="small muted" role="status"></span></div></details>' : '') +
2048
+ '<div class="form-actions">' + docsLink(agent.docs) + '</div></div>' + checkTag(agent.installation, installStates) + '</li>').join('');
2049
+ $('integrations-body').innerHTML = '<div class="stack"><section class="panel"><div class="panel-head"><div><h2>Local diagnostics</h2>' +
2050
+ (data.checkedAt ? '<p class="small muted">Last checked ' + dateMarkup(data.checkedAt) + '</p>' : '') + '</div>' +
2051
+ (typeof data.diagnostics === 'string' && data.diagnostics ? '<div class="form-actions"><button class="button" type="button" id="diagnostics-copy">' + icon('copy', true) +
2052
+ 'Copy report</button><span class="small muted" role="status"></span></div>' : '') + '</div>' +
2053
+ (checkRows ? '<ul class="list">' + checkRows + '</ul>' : blank('activity', 'No diagnostic results', 'Run checks to refresh the local status.')) +
2054
+ '</section><section class="panel"><div class="panel-head"><div><h2>Supported agents</h2>' +
2055
+ '<p class="small muted">Installation status reflects project skill files. Use Live operations to inspect recorded sessions.</p></div></div>' +
2056
+ '<div class="integration-meta"><span class="tag project">Project skill files</span><span class="tag memory">MCP setup examples</span><a class="small" href="/live">Recorded sessions ' + icon('chevron', true) + '</a></div>' +
2057
+ (agentRows ? '<ul class="list">' + agentRows + '</ul>' : blank('integrations', 'No agent data', 'Run checks to load the support catalog.')) +
2058
+ '</section></div>';
2059
+ }
2060
+
2061
+ async function loadIntegrations() {
2062
+ const store = state.integrations;
2063
+ if (store.pending) return;
2064
+ store.pending = true;
2065
+ const target = $('integrations-body');
2066
+ $('integrations-refresh').disabled = true;
2067
+ setBusy(target, true);
2068
+ target.innerHTML = '<div class="panel">' + skeleton('Running local checks…') + '</div>';
2069
+ try {
2070
+ store.snapshot = await api('integrations');
2071
+ renderIntegrations(store.snapshot);
2072
+ } catch (error) {
2073
+ target.innerHTML = errorMarkup(error.message, 'integrations');
2074
+ } finally {
2075
+ store.pending = false;
2076
+ $('integrations-refresh').disabled = false;
2077
+ setBusy(target, false);
2078
+ }
2079
+ }
2080
+
2081
+ async function copyIntegrationText(value, message, button) {
2082
+ const status = button.nextElementSibling;
2083
+ if (!value || !status) return;
2084
+ try {
2085
+ await navigator.clipboard.writeText(value);
2086
+ status.textContent = message;
2087
+ } catch {
2088
+ status.textContent = 'Clipboard access is unavailable.';
2089
+ }
2090
+ }
2091
+
2092
+ function projectFeatureLimit(id) {
2093
+ if (document.documentElement.dataset.projectLocal !== 'true') return undefined;
2094
+ return { crossProjectContext: 'Project memory stays scoped to this project.', automaticBriefing: 'Automatic briefing requires a legacy analysis session.' }[id];
2095
+ }
2096
+
2097
+ function renderFeatures() {
2098
+ const target = $('feature-list');
2099
+ if (!state.features) return;
2100
+ const current = state.features.features;
2101
+ const overrides = new Map(list(state.features.definitions).map((item) => [item.id, item]));
2102
+ const icons = { contextAssembly: 'context', crossProjectContext: 'graph', automaticBriefing: 'agent', liveActivity: 'activity', activityRecording: 'memory' };
2103
+ target.innerHTML = '<ul class="list">' + definitions.map((definition) => {
2104
+ const display = overrides.get(definition.id) || definition;
2105
+ const reason = projectFeatureLimit(definition.id);
2106
+ const on = !reason && current[definition.id] === true;
2107
+ return '<li class="setting"><div class="settings-description"><span class="tile-icon">' + icon(icons[definition.id], true) + '</span><div><h3 id="label-' + definition.id + '">' + esc(display.label) + '</h3><p class="muted small" id="desc-' +
2108
+ definition.id + '">' + esc(reason || display.description) + '</p></div></div><button type="button" class="button switch" role="switch" aria-checked="' +
2109
+ on + '" aria-labelledby="label-' + definition.id + '" aria-describedby="desc-' + definition.id + '" data-feature="' + definition.id + '"' +
2110
+ (state.featureSaving || reason ? ' disabled' : '') + '><span aria-hidden="true">' + (reason ? 'Unavailable' : on ? 'On' : 'Off') + '</span><span class="switch-track" aria-hidden="true"></span></button></li>';
2111
+ }).join('') + '</ul>';
2112
+ }
2113
+
2114
+ function applyFeatures(snapshot) {
2115
+ const before = state.features?.features;
2116
+ const current = snapshot.features;
2117
+ if (!current || !Number.isSafeInteger(snapshot.revision) || !definitions.every(({ id }) => typeof current[id] === 'boolean')) {
2118
+ throw new Error('The server returned unreadable feature settings.');
2119
+ }
2120
+ if (state.features && snapshot.revision < state.features.revision) return;
2121
+ state.features = snapshot;
2122
+ state.activityEnabled = current.liveActivity;
2123
+ const context = state.context;
2124
+ if (!current.contextAssembly || !current.crossProjectContext && context.params?.includeCrossProject) {
2125
+ context.pack = null;
2126
+ context.params = null;
2127
+ context.comparison = null;
2128
+ context.comparing = false;
2129
+ }
2130
+ renderFeatures();
2131
+ renderActivity();
2132
+ renderContextControls();
2133
+ if (!context.pending) renderContext();
2134
+ if (current.liveActivity && before?.liveActivity !== true) void refreshActivity();
2135
+ }
2136
+
2137
+ function featureStatus(message, error = false) {
2138
+ const target = $('feature-status');
2139
+ target.textContent = message;
2140
+ target.dataset.error = String(error);
2141
+ }
2142
+
2143
+ async function loadFeatures(force = false) {
2144
+ if (state.featurePending) return state.featurePending;
2145
+ if (state.features && !force) return;
2146
+ const target = $('feature-list');
2147
+ if (!state.features) target.innerHTML = skeleton('Loading feature settings…');
2148
+ setBusy(target, true);
2149
+ state.featurePending = (async () => {
2150
+ try {
2151
+ applyFeatures(await api('features'));
2152
+ return true;
2153
+ } catch (error) {
2154
+ if (!state.features) target.innerHTML = errorMarkup(error.message, 'features');
2155
+ else if (!state.featureSaving) featureStatus(error.message, true);
2156
+ return false;
2157
+ } finally {
2158
+ state.featurePending = null;
2159
+ setBusy(target, false);
2160
+ renderContextControls();
2161
+ }
2162
+ })();
2163
+ renderContextControls();
2164
+ return state.featurePending;
2165
+ }
2166
+
2167
+ async function toggleFeature(key) {
2168
+ if (state.featureSaving || !state.features || projectFeatureLimit(key) || !definitions.some((definition) => definition.id === key)) return;
2169
+ state.featureSaving = true;
2170
+ $('feature-list').querySelectorAll('[data-feature]').forEach((button) => { button.disabled = true; });
2171
+ featureStatus('Saving…');
2172
+ try {
2173
+ const body = { patch: { [key]: !state.features.features[key] }, expectedRevision: state.features.revision };
2174
+ applyFeatures(await api('features', { body }));
2175
+ featureStatus('Saved.');
2176
+ } catch (error) {
2177
+ if (error.status === 409) {
2178
+ const refreshed = await loadFeatures(true);
2179
+ featureStatus(refreshed ? 'Settings changed elsewhere. Review the current settings and try again.' :
2180
+ 'Settings changed elsewhere. Reload Features to see the current settings.', true);
2181
+ } else {
2182
+ featureStatus(error.message, true);
2183
+ }
2184
+ } finally {
2185
+ state.featureSaving = false;
2186
+ renderFeatures();
2187
+ if (state.view === 'features') $('feature-list').querySelector('[data-feature="' + key + '"]')?.focus();
2188
+ }
2189
+ }
2190
+
2191
+ function setPreference(key, value, announce = true) {
2192
+ document.documentElement.dataset[key] = value;
2193
+ let persisted = true;
2194
+ if (announce) {
2195
+ try {
2196
+ localStorage.setItem('shadow-brain:workspace:' + key, value);
2197
+ } catch { persisted = false; }
2198
+ const status = $('display-status');
2199
+ status.hidden = false;
2200
+ status.textContent = persisted ? 'Display preference saved on this device.' : 'Display preference applied for this session.';
2201
+ }
2202
+ $('theme-setting').value = document.documentElement.dataset.theme;
2203
+ $('motion-setting').value = document.documentElement.dataset.motion;
2204
+ const dark = document.documentElement.dataset.theme === 'dark';
2205
+ const label = dark ? 'Switch to light theme' : 'Switch to dark theme';
2206
+ const button = $('theme-toggle');
2207
+ button.innerHTML = icon(dark ? 'sun' : 'moon');
2208
+ button.title = label;
2209
+ button.setAttribute('aria-label', label);
2210
+ if (announce && !persisted && state.view !== 'features') {
2211
+ $('workspace-notice').hidden = false;
2212
+ $('workspace-notice').textContent = 'Display preference applied for this session.';
2213
+ }
2214
+ }
2215
+
2216
+ function routeParams() {
2217
+ return new URLSearchParams(location.hash.split('?')[1] || '');
2218
+ }
2219
+
2220
+ function changeMemorySearch() {
2221
+ const query = memoryQuery();
2222
+ const route = routeLink('memories', query);
2223
+ if (location.hash !== route) location.hash = route;
2224
+ else void loadMemories(query);
2225
+ }
2226
+
2227
+ function changeView(moveFocus = false) {
2228
+ const hash = location.hash.slice(1).split('?')[0];
2229
+ const view = Object.hasOwn(viewNames, hash) ? hash : 'overview';
2230
+ const previous = state.view;
2231
+ const previousRoute = state.route;
2232
+ state.view = view;
2233
+ state.route = location.hash || '#overview';
2234
+ $('crumb').textContent = viewNames[view];
2235
+ document.title = viewNames[view] + ' · Agent Shadow Brain';
2236
+ for (const name of Object.keys(viewNames)) {
2237
+ const section = $('page-' + name);
2238
+ section.hidden = name !== view;
2239
+ const link = document.querySelector('[data-view="' + name + '"]');
2240
+ if (name === view || name === 'memories' && view === 'memory') link?.setAttribute('aria-current', 'page');
2241
+ else link?.removeAttribute('aria-current');
2242
+ }
2243
+ if (moveFocus && (previous !== view || view === 'memory')) {
2244
+ window.scrollTo(0, 0);
2245
+ $('title-' + view).focus({ preventScroll: true });
2246
+ }
2247
+ const params = routeParams();
2248
+ const project = params.get('project') || '';
2249
+ switch (view) {
2250
+ case 'overview':
2251
+ void loadOverview(previous !== '' && previous !== 'overview', project);
2252
+ void loadFeatures();
2253
+ break;
2254
+ case 'memories': {
2255
+ const query = { q: params.get('q') || '', project, category: params.get('category') || '', agent: params.get('agent') || '', state: params.get('state') || '' };
2256
+ $('memory-query').value = query.q;
2257
+ if (state.projects.length) optionSet('memory-project', 'All projects', state.projects, query.project);
2258
+ $('memory-state').value = query.state;
2259
+ void loadMemories(query);
2260
+ break;
2261
+ }
2262
+ case 'memory': {
2263
+ if (previous && previous !== 'memory') state.detail.back = previousRoute;
2264
+ const back = $('detail-back');
2265
+ const name = state.detail.back.slice(1).split('?')[0];
2266
+ back.href = state.detail.back;
2267
+ back.innerHTML = icon('left', true) + esc(viewNames[name] || 'Memories');
2268
+ void loadDetail(params.get('id') || '', project);
2269
+ break;
2270
+ }
2271
+ case 'graph': {
2272
+ void loadOverview();
2273
+ if (project) optionSet('graph-project', 'All projects', state.projects, project);
2274
+ else $('graph-project').value = '';
2275
+ const memory = params.get('memory') || '';
2276
+ if (memory) ['graph-query', 'graph-kind', 'graph-relation'].forEach((id) => { $(id).value = ''; });
2277
+ void loadGraph(project, previous !== '' && previous !== 'graph', memory);
2278
+ break;
2279
+ }
2280
+ case 'context':
2281
+ if (project) {
2282
+ state.context.routeProject = project;
2283
+ syncProjectOptions();
2284
+ if (state.context.params && state.context.params.projectId !== project) {
2285
+ state.context.dirty = true;
2286
+ if (!state.context.pending) renderContext();
2287
+ }
2288
+ }
2289
+ void loadOverview();
2290
+ void loadFeatures(previous !== '');
2291
+ renderContextControls();
2292
+ break;
2293
+ case 'features':
2294
+ featureStatus('');
2295
+ void loadFeatures(true);
2296
+ break;
2297
+ case 'project-kit':
2298
+ void loadProjectKit();
2299
+ break;
2300
+ case 'integrations':
2301
+ if (state.integrations.snapshot) renderIntegrations(state.integrations.snapshot);
2302
+ else void loadIntegrations();
2303
+ break;
2304
+ }
2305
+ }
2306
+
2307
+ function graphSelectFromList(id, fromInspector) {
2308
+ if (!state.graph.nodes.some((node) => node.id === id)) {
2309
+ $('graph-query').value = '';
2310
+ $('graph-kind').value = '';
2311
+ $('graph-relation').value = '';
2312
+ renderGraph();
2313
+ }
2314
+ selectGraphNode(id, { center: fromInspector, focus: fromInspector });
2315
+ }
2316
+
2317
+ function handleGraphKeyboard(event) {
2318
+ const edge = event.target.closest('[data-edge-record]');
2319
+ if (edge) {
2320
+ const links = Array.from($('graph-svg').querySelectorAll('[data-edge-record]'));
2321
+ const index = links.indexOf(edge);
2322
+ const nextIndex = { ArrowLeft: Math.max(index - 1, 0), ArrowRight: Math.min(index + 1, links.length - 1), Home: 0, End: links.length - 1 }[event.key];
2323
+ if (event.key === 'Enter' || event.key === ' ') {
2324
+ event.preventDefault();
2325
+ selectGraphEdge(edge.dataset.edgeRecord);
2326
+ } else if (Number.isInteger(nextIndex)) {
2327
+ event.preventDefault();
2328
+ const next = links[nextIndex];
2329
+ selectGraphEdge(next.dataset.edgeRecord);
2330
+ next.focus();
2331
+ }
2332
+ return;
2333
+ }
2334
+ const target = event.target.closest('[data-node]');
2335
+ if (!target) return;
2336
+ if (event.key === 'Enter' || event.key === ' ') {
2337
+ event.preventDefault();
2338
+ selectGraphNode(target.dataset.node, { center: true });
2339
+ return;
2340
+ }
2341
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
2342
+ event.preventDefault();
2343
+ const graph = state.graph;
2344
+ const { positions } = graph.layout;
2345
+ const points = [...positions.entries()].map(([id, point]) => ({ id, x: point.x + point.width / 2, y: point.y + point.height / 2 }));
2346
+ let next;
2347
+ if (event.key === 'Home' || event.key === 'End') {
2348
+ points.sort((a, b) => a.y - b.y || a.x - b.x);
2349
+ next = event.key === 'Home' ? points[0] : points.at(-1);
2350
+ } else {
2351
+ const current = points.find((point) => point.id === target.dataset.node);
2352
+ const horizontal = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
2353
+ const sign = event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1;
2354
+ const axis = horizontal ? 'x' : 'y';
2355
+ const other = horizontal ? 'y' : 'x';
2356
+ const score = (point) => Math.abs(point[axis] - current[axis]) + Math.abs(point[other] - current[other]) * 1.4;
2357
+ next = points.filter((point) => (point[axis] - current[axis]) * sign > 3).sort((a, b) => score(a) - score(b))[0];
2358
+ }
2359
+ if (next) selectGraphNode(next.id, { center: true, focus: true });
2360
+ }
2361
+
2362
+ $('theme-toggle').addEventListener('click', () => setPreference('theme', document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
2363
+ $('theme-setting').addEventListener('change', (event) => setPreference('theme', event.target.value));
2364
+ $('motion-setting').addEventListener('change', (event) => setPreference('motion', event.target.value));
2365
+ $('memory-form').addEventListener('submit', (event) => { event.preventDefault(); changeMemorySearch(); });
2366
+ ['memory-project', 'memory-category', 'memory-agent', 'memory-state'].forEach((id) => $(id).addEventListener('change', changeMemorySearch));
2367
+ $('overview-quality-project').addEventListener('change', (event) => {
2368
+ const project = event.target.value;
2369
+ const route = routeLink('overview', { project });
2370
+ window.history.replaceState(null, '', route);
2371
+ state.route = route;
2372
+ void loadOverview(true, project).then(() => {
2373
+ if (state.view === 'overview' && state.route === route) $('overview-quality-project')?.focus();
2374
+ });
2375
+ });
2376
+ $('overview-body').addEventListener('change', (event) => {
2377
+ if (event.target.id !== 'attention-filter') return;
2378
+ state.health.filter = event.target.value;
2379
+ renderAttention();
2380
+ });
2381
+ $('overview-body').addEventListener('click', (event) => {
2382
+ if (event.target.closest('#attention-jump')) $('attention-heading')?.focus();
2383
+ });
2384
+ $('integrations-refresh').addEventListener('click', () => { void loadIntegrations(); });
2385
+ $('graph-query').addEventListener('input', renderGraph);
2386
+ $('graph-kind').addEventListener('change', renderGraph);
2387
+ $('graph-relation').addEventListener('change', renderGraph);
2388
+ $('graph-project').addEventListener('change', (event) => {
2389
+ const route = routeLink('graph', { project: event.target.value });
2390
+ if (location.hash !== route) location.hash = route;
2391
+ else void loadGraph(event.target.value);
2392
+ });
2393
+ ['map', 'list'].forEach((mode) => $('graph-mode-' + mode).addEventListener('click', () => {
2394
+ state.graph.mode = mode;
2395
+ $('graph-mode-map').setAttribute('aria-pressed', String(mode === 'map'));
2396
+ $('graph-mode-list').setAttribute('aria-pressed', String(mode === 'list'));
2397
+ renderGraph();
2398
+ }));
2399
+ $('graph-fit').addEventListener('click', () => fitGraph());
2400
+ $('graph-lens-clear').addEventListener('click', () => {
2401
+ const anchor = state.graph.lens;
2402
+ state.graph.lens = '';
2403
+ renderGraph();
2404
+ if (state.graph.nodes.some((node) => node.id === anchor)) selectGraphNode(anchor, { focus: true });
2405
+ else $('graph-query').focus();
2406
+ });
2407
+ $('graph-in').addEventListener('click', () => zoomGraph(1.35));
2408
+ $('graph-out').addEventListener('click', () => zoomGraph(1 / 1.35));
2409
+ $('graph-svg').addEventListener('keydown', handleGraphKeyboard);
2410
+ $('graph-svg').addEventListener('focusin', (event) => {
2411
+ const node = event.target.closest('[data-node]');
2412
+ if (node) renderGraphTooltip(node.dataset.node);
2413
+ });
2414
+ $('graph-svg').addEventListener('focusout', () => renderGraphTooltip(state.graph.selected));
2415
+ $('graph-svg').addEventListener('pointerdown', (event) => {
2416
+ if (event.button !== 0 || !event.isPrimary || event.target.closest('[data-node],[data-edge-record]')) return;
2417
+ const graph = state.graph;
2418
+ graph.drag = { id: event.pointerId, x: event.clientX - graph.panX, y: event.clientY - graph.panY };
2419
+ $('graph-svg').setPointerCapture(event.pointerId);
2420
+ $('graph-svg').classList.add('dragging');
2421
+ });
2422
+ $('graph-svg').addEventListener('pointermove', (event) => {
2423
+ const graph = state.graph;
2424
+ if (graph.drag?.id !== event.pointerId) return;
2425
+ graph.panX = event.clientX - graph.drag.x;
2426
+ graph.panY = event.clientY - graph.drag.y;
2427
+ updateGraphTransform();
2428
+ });
2429
+ $('graph-svg').addEventListener('lostpointercapture', () => {
2430
+ state.graph.drag = null;
2431
+ $('graph-svg').classList.remove('dragging');
2432
+ });
2433
+ window.addEventListener('resize', () => {
2434
+ if (state.view === 'graph' && state.graph.mode === 'map' && state.graph.nodes.length) {
2435
+ renderGraphMap();
2436
+ updateGraphSelection();
2437
+ }
2438
+ });
2439
+
2440
+ $('context-form').addEventListener('submit', (event) => { event.preventDefault(); void assembleContext(); });
2441
+ $('context-form').addEventListener('input', () => {
2442
+ $('context-task').setCustomValidity('');
2443
+ $('context-related-row').hidden = $('context-profile').value !== 'adaptive';
2444
+ const store = state.context;
2445
+ if (store.pack || store.pending) {
2446
+ if (!store.dirty) {
2447
+ store.dirty = true;
2448
+ if (!store.pending) renderContext();
2449
+ }
2450
+ }
2451
+ });
2452
+
2453
+ $('detail-body').addEventListener('submit', (event) => {
2454
+ const { id } = state.detail;
2455
+ switch (event.target.id) {
2456
+ case 'review-form':
2457
+ event.preventDefault();
2458
+ void saveDetail('memory-review', { memoryId: id, state: $('review-state').value, note: $('review-note').value.trim() }, 'review-submit');
2459
+ break;
2460
+ case 'link-search':
2461
+ event.preventDefault();
2462
+ void searchLinkTargets();
2463
+ break;
2464
+ case 'link-form':
2465
+ event.preventDefault();
2466
+ void saveDetail('memory-link', { fromId: id, toId: $('link-target').value, type: $('link-type').value, note: $('link-note').value.trim() }, 'link-editor');
2467
+ break;
2468
+ }
2469
+ });
2470
+
2471
+ document.addEventListener('click', (event) => {
2472
+ const element = event.target.closest('[data-retry],[data-memory-clear],[data-memory-page],[data-node],[data-related],[data-edge-record],[data-graph-lens],[data-feature],[data-unlink],[data-copy-command],#diagnostics-copy,#context-copy,#context-download,#context-compare-button,.skip');
2473
+ if (!element) return;
2474
+ const data = element.dataset;
2475
+ if (data.retry) {
2476
+ switch (data.retry) {
2477
+ case 'overview': void loadOverview(true, state.view === 'overview' ? routeParams().get('project') || '' : ''); break;
2478
+ case 'health': void loadHealth(state.overviewScope); break;
2479
+ case 'kit-profile': void loadProjectKit(true); break;
2480
+ case 'memories': void loadMemories(state.memory.query || memoryQuery(), state.memory.offset); break;
2481
+ case 'graph': void loadGraph(state.graph.project, true, state.graph.focusMemory); break;
2482
+ case 'detail': void loadDetail(state.detail.id, state.detail.project); break;
2483
+ case 'integrations': void loadIntegrations(); break;
2484
+ case 'context': $('context-form').requestSubmit(); break;
2485
+ case 'features': void loadFeatures(true); break;
2486
+ }
2487
+ } else if (Object.hasOwn(data, 'memoryClear')) {
2488
+ ['memory-query', 'memory-project', 'memory-category', 'memory-agent', 'memory-state'].forEach((id) => { $(id).value = ''; });
2489
+ $('memory-query').focus();
2490
+ changeMemorySearch();
2491
+ } else if (data.memoryPage) {
2492
+ const { query, offset, limit } = state.memory;
2493
+ const step = data.memoryPage === 'next' ? limit : -limit;
2494
+ $('memory-search').focus();
2495
+ void loadMemories(query || memoryQuery(), Math.max(0, offset + step));
2496
+ } else if (data.node) {
2497
+ selectGraphNode(data.node);
2498
+ } else if (data.related) {
2499
+ graphSelectFromList(data.related, true);
2500
+ } else if (data.edgeRecord) {
2501
+ selectGraphEdge(data.edgeRecord, $('graph-inspector').contains(element));
2502
+ } else if (data.graphLens) {
2503
+ state.graph.lens = data.graphLens;
2504
+ ['graph-query', 'graph-kind', 'graph-relation'].forEach((id) => { $(id).value = ''; });
2505
+ renderGraph();
2506
+ $('graph-lens-clear').focus();
2507
+ } else if (data.unlink) {
2508
+ void saveDetail('memory-unlink', { memoryId: state.detail.id, linkId: data.unlink }, 'link-editor');
2509
+ } else if (data.copyCommand) {
2510
+ void copyIntegrationText(data.copyCommand, 'Command copied.', element);
2511
+ } else if (data.feature) {
2512
+ void toggleFeature(data.feature);
2513
+ } else if (element.id === 'context-copy') {
2514
+ void copyContext();
2515
+ } else if (element.id === 'context-download') {
2516
+ downloadContext();
2517
+ } else if (element.id === 'context-compare-button') {
2518
+ void compareContext();
2519
+ } else if (element.id === 'diagnostics-copy') {
2520
+ void copyIntegrationText(state.integrations.snapshot?.diagnostics, 'Diagnostic report copied.', element);
2521
+ } else if (element.classList.contains('skip')) {
2522
+ event.preventDefault();
2523
+ $('main-content').focus();
2524
+ }
2525
+ });
2526
+
2527
+ ${PROJECT_KIT_SCRIPT}
2528
+
2529
+ window.addEventListener('hashchange', () => changeView(true));
2530
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) void refreshActivity(); });
2531
+ setInterval(() => { void refreshActivity(); }, 10000);
2532
+ setPreference('theme', document.documentElement.dataset.theme, false);
2533
+ renderContext();
2534
+ changeView();
2535
+ </script>
2536
+ </body>
2537
+ </html>`;
2538
+ //# sourceMappingURL=workspace-html.js.map