@0xmaxma/claude-gateway 1.6.8 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +149 -20
  2. package/config.template.json +59 -1
  3. package/dist/agent/dreaming/accept.d.ts +43 -0
  4. package/dist/agent/dreaming/accept.d.ts.map +1 -0
  5. package/dist/agent/dreaming/accept.js +230 -0
  6. package/dist/agent/dreaming/accept.js.map +1 -0
  7. package/dist/agent/dreaming/applier.d.ts +77 -0
  8. package/dist/agent/dreaming/applier.d.ts.map +1 -0
  9. package/dist/agent/dreaming/applier.js +406 -0
  10. package/dist/agent/dreaming/applier.js.map +1 -0
  11. package/dist/agent/dreaming/audit.d.ts +27 -0
  12. package/dist/agent/dreaming/audit.d.ts.map +1 -0
  13. package/dist/agent/dreaming/audit.js +100 -0
  14. package/dist/agent/dreaming/audit.js.map +1 -0
  15. package/dist/agent/dreaming/compactor.d.ts +66 -0
  16. package/dist/agent/dreaming/compactor.d.ts.map +1 -0
  17. package/dist/agent/dreaming/compactor.js +306 -0
  18. package/dist/agent/dreaming/compactor.js.map +1 -0
  19. package/dist/agent/dreaming/config.d.ts +19 -0
  20. package/dist/agent/dreaming/config.d.ts.map +1 -0
  21. package/dist/agent/dreaming/config.js +107 -0
  22. package/dist/agent/dreaming/config.js.map +1 -0
  23. package/dist/agent/dreaming/episodic.d.ts +42 -0
  24. package/dist/agent/dreaming/episodic.d.ts.map +1 -0
  25. package/dist/agent/dreaming/episodic.js +116 -0
  26. package/dist/agent/dreaming/episodic.js.map +1 -0
  27. package/dist/agent/dreaming/gather.d.ts +31 -0
  28. package/dist/agent/dreaming/gather.d.ts.map +1 -0
  29. package/dist/agent/dreaming/gather.js +55 -0
  30. package/dist/agent/dreaming/gather.js.map +1 -0
  31. package/dist/agent/dreaming/index.d.ts +87 -0
  32. package/dist/agent/dreaming/index.d.ts.map +1 -0
  33. package/dist/agent/dreaming/index.js +377 -0
  34. package/dist/agent/dreaming/index.js.map +1 -0
  35. package/dist/agent/dreaming/migrate-cli.d.ts +17 -0
  36. package/dist/agent/dreaming/migrate-cli.d.ts.map +1 -0
  37. package/dist/agent/dreaming/migrate-cli.js +42 -0
  38. package/dist/agent/dreaming/migrate-cli.js.map +1 -0
  39. package/dist/agent/dreaming/migrate.d.ts +72 -0
  40. package/dist/agent/dreaming/migrate.d.ts.map +1 -0
  41. package/dist/agent/dreaming/migrate.js +374 -0
  42. package/dist/agent/dreaming/migrate.js.map +1 -0
  43. package/dist/agent/dreaming/report.d.ts +39 -0
  44. package/dist/agent/dreaming/report.d.ts.map +1 -0
  45. package/dist/agent/dreaming/report.js +121 -0
  46. package/dist/agent/dreaming/report.js.map +1 -0
  47. package/dist/agent/dreaming/reviewer.d.ts +43 -0
  48. package/dist/agent/dreaming/reviewer.d.ts.map +1 -0
  49. package/dist/agent/dreaming/reviewer.js +249 -0
  50. package/dist/agent/dreaming/reviewer.js.map +1 -0
  51. package/dist/agent/dreaming/staleness.d.ts +68 -0
  52. package/dist/agent/dreaming/staleness.d.ts.map +1 -0
  53. package/dist/agent/dreaming/staleness.js +281 -0
  54. package/dist/agent/dreaming/staleness.js.map +1 -0
  55. package/dist/agent/dreaming/types.d.ts +130 -0
  56. package/dist/agent/dreaming/types.d.ts.map +1 -0
  57. package/dist/agent/dreaming/types.js +10 -0
  58. package/dist/agent/dreaming/types.js.map +1 -0
  59. package/dist/agent/knowledge/archive-db.d.ts +114 -0
  60. package/dist/agent/knowledge/archive-db.d.ts.map +1 -0
  61. package/dist/agent/knowledge/archive-db.js +474 -0
  62. package/dist/agent/knowledge/archive-db.js.map +1 -0
  63. package/dist/agent/knowledge/chunk.d.ts +21 -0
  64. package/dist/agent/knowledge/chunk.d.ts.map +1 -0
  65. package/dist/agent/knowledge/chunk.js +67 -0
  66. package/dist/agent/knowledge/chunk.js.map +1 -0
  67. package/dist/agent/knowledge/config.d.ts +20 -0
  68. package/dist/agent/knowledge/config.d.ts.map +1 -0
  69. package/dist/agent/knowledge/config.js +143 -0
  70. package/dist/agent/knowledge/config.js.map +1 -0
  71. package/dist/agent/knowledge/graph-demo.d.ts +25 -0
  72. package/dist/agent/knowledge/graph-demo.d.ts.map +1 -0
  73. package/dist/agent/knowledge/graph-demo.js +132 -0
  74. package/dist/agent/knowledge/graph-demo.js.map +1 -0
  75. package/dist/agent/knowledge/index.d.ts +24 -0
  76. package/dist/agent/knowledge/index.d.ts.map +1 -0
  77. package/dist/agent/knowledge/index.js +50 -0
  78. package/dist/agent/knowledge/index.js.map +1 -0
  79. package/dist/agent/knowledge/indexer.d.ts +34 -0
  80. package/dist/agent/knowledge/indexer.d.ts.map +1 -0
  81. package/dist/agent/knowledge/indexer.js +271 -0
  82. package/dist/agent/knowledge/indexer.js.map +1 -0
  83. package/dist/agent/knowledge/lifecycle.d.ts +91 -0
  84. package/dist/agent/knowledge/lifecycle.d.ts.map +1 -0
  85. package/dist/agent/knowledge/lifecycle.js +205 -0
  86. package/dist/agent/knowledge/lifecycle.js.map +1 -0
  87. package/dist/agent/knowledge/provenance.d.ts +15 -0
  88. package/dist/agent/knowledge/provenance.d.ts.map +1 -0
  89. package/dist/agent/knowledge/provenance.js +37 -0
  90. package/dist/agent/knowledge/provenance.js.map +1 -0
  91. package/dist/agent/knowledge/reindex-cli.d.ts +13 -0
  92. package/dist/agent/knowledge/reindex-cli.d.ts.map +1 -0
  93. package/dist/agent/knowledge/reindex-cli.js +53 -0
  94. package/dist/agent/knowledge/reindex-cli.js.map +1 -0
  95. package/dist/agent/knowledge/reindex-spawn.d.ts +30 -0
  96. package/dist/agent/knowledge/reindex-spawn.d.ts.map +1 -0
  97. package/dist/agent/knowledge/reindex-spawn.js +97 -0
  98. package/dist/agent/knowledge/reindex-spawn.js.map +1 -0
  99. package/dist/agent/knowledge/shared-promote.d.ts +18 -0
  100. package/dist/agent/knowledge/shared-promote.d.ts.map +1 -0
  101. package/dist/agent/knowledge/shared-promote.js +29 -0
  102. package/dist/agent/knowledge/shared-promote.js.map +1 -0
  103. package/dist/agent/knowledge/shared-writer.d.ts +24 -0
  104. package/dist/agent/knowledge/shared-writer.d.ts.map +1 -0
  105. package/dist/agent/knowledge/shared-writer.js +97 -0
  106. package/dist/agent/knowledge/shared-writer.js.map +1 -0
  107. package/dist/agent/knowledge/types.d.ts +83 -0
  108. package/dist/agent/knowledge/types.d.ts.map +1 -0
  109. package/dist/agent/knowledge/types.js +12 -0
  110. package/dist/agent/knowledge/types.js.map +1 -0
  111. package/dist/agent/knowledge/wiki.d.ts +107 -0
  112. package/dist/agent/knowledge/wiki.d.ts.map +1 -0
  113. package/dist/agent/knowledge/wiki.js +415 -0
  114. package/dist/agent/knowledge/wiki.js.map +1 -0
  115. package/dist/agent/memory-metrics.d.ts +48 -0
  116. package/dist/agent/memory-metrics.d.ts.map +1 -0
  117. package/dist/agent/memory-metrics.js +156 -0
  118. package/dist/agent/memory-metrics.js.map +1 -0
  119. package/dist/agent/runner.d.ts +24 -1
  120. package/dist/agent/runner.d.ts.map +1 -1
  121. package/dist/agent/runner.js +82 -6
  122. package/dist/agent/runner.js.map +1 -1
  123. package/dist/agent/skill-learning/config.d.ts +18 -0
  124. package/dist/agent/skill-learning/config.d.ts.map +1 -0
  125. package/dist/agent/skill-learning/config.js +71 -0
  126. package/dist/agent/skill-learning/config.js.map +1 -0
  127. package/dist/agent/skill-learning/curator.d.ts +45 -0
  128. package/dist/agent/skill-learning/curator.d.ts.map +1 -0
  129. package/dist/agent/skill-learning/curator.js +171 -0
  130. package/dist/agent/skill-learning/curator.js.map +1 -0
  131. package/dist/agent/skill-learning/index.d.ts +79 -0
  132. package/dist/agent/skill-learning/index.d.ts.map +1 -0
  133. package/dist/agent/skill-learning/index.js +308 -0
  134. package/dist/agent/skill-learning/index.js.map +1 -0
  135. package/dist/agent/skill-learning/metrics.d.ts +13 -0
  136. package/dist/agent/skill-learning/metrics.d.ts.map +1 -0
  137. package/dist/agent/skill-learning/metrics.js +109 -0
  138. package/dist/agent/skill-learning/metrics.js.map +1 -0
  139. package/dist/agent/skill-learning/notifier.d.ts +111 -0
  140. package/dist/agent/skill-learning/notifier.d.ts.map +1 -0
  141. package/dist/agent/skill-learning/notifier.js +398 -0
  142. package/dist/agent/skill-learning/notifier.js.map +1 -0
  143. package/dist/agent/skill-learning/reviewer.d.ts +61 -0
  144. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -0
  145. package/dist/agent/skill-learning/reviewer.js +271 -0
  146. package/dist/agent/skill-learning/reviewer.js.map +1 -0
  147. package/dist/agent/skill-learning/telemetry.d.ts +22 -0
  148. package/dist/agent/skill-learning/telemetry.d.ts.map +1 -0
  149. package/dist/agent/skill-learning/telemetry.js +90 -0
  150. package/dist/agent/skill-learning/telemetry.js.map +1 -0
  151. package/dist/agent/skill-learning/trigger.d.ts +23 -0
  152. package/dist/agent/skill-learning/trigger.d.ts.map +1 -0
  153. package/dist/agent/skill-learning/trigger.js +44 -0
  154. package/dist/agent/skill-learning/trigger.js.map +1 -0
  155. package/dist/agent/skill-learning/types.d.ts +165 -0
  156. package/dist/agent/skill-learning/types.d.ts.map +1 -0
  157. package/dist/agent/skill-learning/types.js +11 -0
  158. package/dist/agent/skill-learning/types.js.map +1 -0
  159. package/dist/agent/skill-learning/writer.d.ts +37 -0
  160. package/dist/agent/skill-learning/writer.d.ts.map +1 -0
  161. package/dist/agent/skill-learning/writer.js +223 -0
  162. package/dist/agent/skill-learning/writer.js.map +1 -0
  163. package/dist/agent/workspace-loader.d.ts +83 -6
  164. package/dist/agent/workspace-loader.d.ts.map +1 -1
  165. package/dist/agent/workspace-loader.js +207 -14
  166. package/dist/agent/workspace-loader.js.map +1 -1
  167. package/dist/api/gateway-router.d.ts +2 -0
  168. package/dist/api/gateway-router.d.ts.map +1 -1
  169. package/dist/api/gateway-router.js +293 -6
  170. package/dist/api/gateway-router.js.map +1 -1
  171. package/dist/api/share-router.d.ts.map +1 -1
  172. package/dist/api/share-router.js +27 -1
  173. package/dist/api/share-router.js.map +1 -1
  174. package/dist/api/skills-router.d.ts +2 -2
  175. package/dist/api/skills-router.d.ts.map +1 -1
  176. package/dist/api/skills-router.js +61 -1
  177. package/dist/api/skills-router.js.map +1 -1
  178. package/dist/apps/agent-manager.d.ts +7 -0
  179. package/dist/apps/agent-manager.d.ts.map +1 -1
  180. package/dist/apps/agent-manager.js +13 -1
  181. package/dist/apps/agent-manager.js.map +1 -1
  182. package/dist/config/migrator.d.ts +2 -0
  183. package/dist/config/migrator.d.ts.map +1 -1
  184. package/dist/config/migrator.js +72 -1
  185. package/dist/config/migrator.js.map +1 -1
  186. package/dist/history/cleanup.d.ts +7 -0
  187. package/dist/history/cleanup.d.ts.map +1 -1
  188. package/dist/history/cleanup.js +27 -0
  189. package/dist/history/cleanup.js.map +1 -1
  190. package/dist/history/db.d.ts +65 -0
  191. package/dist/history/db.d.ts.map +1 -1
  192. package/dist/history/db.js +261 -0
  193. package/dist/history/db.js.map +1 -1
  194. package/dist/index.js +122 -20
  195. package/dist/index.js.map +1 -1
  196. package/dist/session/process.d.ts +10 -0
  197. package/dist/session/process.d.ts.map +1 -1
  198. package/dist/session/process.js +86 -1
  199. package/dist/session/process.js.map +1 -1
  200. package/dist/share/share-store.d.ts +11 -0
  201. package/dist/share/share-store.d.ts.map +1 -1
  202. package/dist/share/share-store.js +3 -1
  203. package/dist/share/share-store.js.map +1 -1
  204. package/dist/skills/loader.d.ts +8 -0
  205. package/dist/skills/loader.d.ts.map +1 -1
  206. package/dist/skills/loader.js +29 -1
  207. package/dist/skills/loader.js.map +1 -1
  208. package/dist/types.d.ts +116 -0
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/ui/web-ui.d.ts.map +1 -1
  211. package/dist/ui/web-ui.js +1126 -3
  212. package/dist/ui/web-ui.js.map +1 -1
  213. package/mcp/package.json +2 -1
  214. package/mcp/server.ts +2 -0
  215. package/mcp/tools/image/module.ts +114 -9
  216. package/mcp/tools/memory/archive-reader.test.ts +212 -0
  217. package/mcp/tools/memory/archive-reader.ts +253 -0
  218. package/mcp/tools/memory/module.ts +152 -0
  219. package/mcp/tools/memory/retrieval-recorder.ts +66 -0
  220. package/mcp/tools/shared/share-client.ts +20 -1
  221. package/mcp/tools/skills/module.ts +28 -0
  222. package/package.json +1 -1
  223. package/resource/claude_gateway.svg +1038 -112
package/dist/ui/web-ui.js CHANGED
@@ -78,7 +78,14 @@ function generateDashboardHtml() {
78
78
  .badge-fable { background: #553052; color: #f687b3; }
79
79
  .badge-model { background: #2d3748; color: #cbd5e0; }
80
80
  .ts { color: #718096; font-size: 0.8rem; }
81
- #refresh-indicator { float: right; font-size: 0.75rem; color: #4a5568; }
81
+ /* Top-right cluster: the Logout button sits beside the auto-refresh status. */
82
+ #top-right { float: right; display: inline-flex; align-items: center; gap: 10px; }
83
+ #refresh-indicator { font-size: 0.75rem; color: #4a5568; }
84
+ #logout-btn {
85
+ font-size: 0.7rem; background: #2d3748; color: #a0aec0; border: 1px solid #4a5568;
86
+ border-radius: 4px; padding: 2px 8px; cursor: pointer;
87
+ }
88
+ #logout-btn:hover { background: #374151; }
82
89
  .error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
83
90
  .btn-stream {
84
91
  background: #44337a;
@@ -217,20 +224,202 @@ function generateDashboardHtml() {
217
224
  h1 { font-size: 1.2rem; }
218
225
  h2 { font-size: 1rem; }
219
226
  .meta { font-size: 0.78rem; }
220
- #refresh-indicator { float: none; display: block; margin-top: 4px; }
227
+ #top-right { float: none; display: flex; margin-top: 4px; }
221
228
  .proc-tree { font-size: 0.72rem; padding: 10px 12px; }
222
229
  /* On phones allow horizontal pan only — no vertical clipping. */
223
230
  }
231
+
232
+ /* --- Tabs (Sessions | Knowledge base) --- */
233
+ .tabs { display: flex; gap: 4px; margin: 14px 0 18px; border-bottom: 1px solid #2d3748; }
234
+ .tab {
235
+ background: none; border: none; border-bottom: 2px solid transparent;
236
+ color: #a0aec0; font-size: 0.9rem; font-weight: 600; padding: 8px 16px;
237
+ cursor: pointer; margin-bottom: -1px;
238
+ }
239
+ .tab:hover { color: #e2e8f0; }
240
+ .tab.active { color: #63b3ed; border-bottom-color: #63b3ed; }
241
+
242
+ /* --- Knowledge Base graph view --- */
243
+ .kb-toolbar {
244
+ display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
245
+ margin-bottom: 10px; font-size: 0.8rem; color: #a0aec0;
246
+ }
247
+ .kb-toolbar button {
248
+ background: #2d3748; color: #e2e8f0; border: 1px solid #4a5568;
249
+ border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 0.8rem;
250
+ }
251
+ .kb-toolbar button:hover { background: #374151; }
252
+ .kb-toolbar label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
253
+ #kb-demo-size, #kb-source, #dreams-agent {
254
+ background: #1a2333; color: #cbd5e0; border: 1px solid #2d3748;
255
+ border-radius: 4px; padding: 2px 4px; font-size: 0.78rem; cursor: pointer;
256
+ }
257
+ .kb-demo-badge {
258
+ background: #744210; color: #fbd38d; border: 1px solid #975a16;
259
+ border-radius: 4px; padding: 2px 8px; font-size: 0.72rem; font-weight: 600;
260
+ }
261
+ .kb-legend { display: inline-flex; gap: 10px; flex-wrap: wrap; }
262
+ .kb-legend span { display: inline-flex; align-items: center; gap: 4px; }
263
+ .kb-legend i { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
264
+ .kb-stage {
265
+ position: relative; width: 100%; height: 620px; overflow: hidden;
266
+ border: 1px solid #2d3748; border-radius: 8px;
267
+ /* Deep-space radial backdrop makes the node glow read as light. */
268
+ background: radial-gradient(ellipse 70% 62% at 50% 42%, #16203a 0%, #0d1322 55%, #070a12 100%);
269
+ }
270
+ /* The graph is a 3D sphere rendered on <canvas> (auto-spins; drag rotates the view;
271
+ always centred by projection). Node/edge/label styling lives in the canvas paint
272
+ code, not CSS. */
273
+ #kb-canvas { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
274
+ #kb-canvas.drag { cursor: grabbing; }
275
+ /* Zoom controls, pinned bottom-right of the graph stage. */
276
+ .kb-zoom {
277
+ position: absolute; right: 12px; bottom: 12px; display: flex; flex-direction: column;
278
+ gap: 6px; z-index: 3;
279
+ }
280
+ .kb-zoom button {
281
+ width: 32px; height: 32px; padding: 0; font-size: 1.15rem; line-height: 1;
282
+ display: flex; align-items: center; justify-content: center;
283
+ background: rgba(26, 35, 51, 0.82); color: #e2e8f0; border: 1px solid #2d3748;
284
+ border-radius: 6px; cursor: pointer; backdrop-filter: blur(3px); user-select: none;
285
+ }
286
+ .kb-zoom button:hover { background: rgba(45, 55, 72, 0.92); border-color: #4a5568; }
287
+ .kb-zoom button:active { background: #374151; }
288
+ .kb-search-wrap { position: relative; display: inline-flex; align-items: center; gap: 6px; }
289
+ #kb-search {
290
+ background: #0d1117; color: #e2e8f0; border: 1px solid #4a5568; border-radius: 4px;
291
+ padding: 3px 8px; font-size: 0.8rem; width: 160px; outline: none;
292
+ }
293
+ #kb-search:focus { border-color: #63b3ed; }
294
+ .kb-search-count { color: #718096; font-size: 0.75rem; min-width: 42px; }
295
+ .kb-empty {
296
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
297
+ color: #718096; font-size: 0.9rem; text-align: center; padding: 20px; pointer-events: none;
298
+ }
299
+ /* --- Nightly dreaming report --- */
300
+ .dreams-list { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 20px; }
301
+ .dreams-empty { color: #718096; font-size: 0.9rem; text-align: center; padding: 40px 20px; }
302
+ .dream-run {
303
+ background: #131a2b; border: 1px solid #222c40; border-radius: 8px; padding: 12px 14px;
304
+ }
305
+ .dream-run-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
306
+ .dream-run-head .agent { color: #63b3ed; font-weight: 600; }
307
+ .dream-run-head .when { color: #718096; font-size: 0.78rem; font-family: monospace; }
308
+ .dream-badge { border-radius: 4px; padding: 1px 7px; font-size: 0.7rem; font-weight: 600; }
309
+ .dream-badge.auto { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; }
310
+ .dream-badge.propose { background: #2a4365; color: #90cdf4; border: 1px solid #2b6cb0; }
311
+ .dream-badge.outcome { background: #2d3748; color: #cbd5e0; border: 1px solid #4a5568; }
312
+ .dream-summary { color: #cbd5e0; font-size: 0.86rem; margin: 4px 0 8px; }
313
+ .dream-meta { color: #718096; font-size: 0.75rem; margin-top: 8px; }
314
+ .dream-props { display: flex; flex-direction: column; gap: 6px; }
315
+ .dream-prop {
316
+ background: #0e1420; border: 1px solid #1e2740; border-radius: 6px; padding: 7px 9px; font-size: 0.82rem;
317
+ }
318
+ .dream-prop .op { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; margin-right: 6px; }
319
+ .dream-prop .op.add { color: #68d391; } .dream-prop .op.replace { color: #f6ad55; } .dream-prop .op.remove { color: #fc8181; }
320
+ .dream-prop .file { color: #90cdf4; font-family: monospace; font-size: 0.76rem; }
321
+ .dream-prop .score { color: #718096; font-size: 0.72rem; float: right; }
322
+ .dream-prop .reason { color: #a0aec0; margin-top: 3px; }
323
+ .dream-prop .content { color: #cbd5e0; margin-top: 5px; white-space: pre-wrap; word-break: break-word;
324
+ background: #070a12; border-radius: 4px; padding: 6px 8px; font-size: 0.78rem; max-height: 140px; overflow: auto; }
325
+ .dream-prop .anchor { color: #f6ad55; font-family: monospace; font-size: 0.72rem; margin-top: 3px; }
326
+ .dream-prop.accepted { border-color: #2f855a; }
327
+ .dream-prop-actions { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
328
+ .dream-accept-btn { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; border-radius: 4px;
329
+ padding: 2px 10px; font-size: 0.74rem; font-weight: 600; cursor: pointer; }
330
+ .dream-accept-btn:hover { background: #276749; }
331
+ .dream-accept-btn:disabled { opacity: 0.55; cursor: default; }
332
+ .prop-status { font-size: 0.72rem; font-weight: 600; }
333
+ .prop-status.applied { color: #9ae6b4; }
334
+ .prop-status.applied-auto { color: #68d391; }
335
+ .prop-status.pending { color: #90cdf4; }
336
+ .prop-status.failed { color: #fc8181; }
337
+ .dream-accept-all { background: #22543d; color: #9ae6b4; border: 1px solid #2f855a; border-radius: 4px;
338
+ padding: 1px 9px; font-size: 0.7rem; font-weight: 600; cursor: pointer; }
339
+ .dream-accept-all:hover { background: #276749; }
340
+ .dream-accept-all:disabled { opacity: 0.55; cursor: default; }
341
+ /* Note detail — full-width card below the graph. Shows the whole file. */
342
+ .kb-note {
343
+ margin-top: 12px; background: #131a2b; border: 1px solid #2d3748; border-radius: 8px;
344
+ padding: 16px 18px; font-size: 0.85rem; color: #cbd5e0;
345
+ }
346
+ .kb-note-head { display: flex; align-items: flex-start; gap: 10px; }
347
+ .kb-note-head h3 { margin: 0; font-size: 1.02rem; color: #e2e8f0; flex: 1; word-break: break-word; }
348
+ .kb-note-close {
349
+ cursor: pointer; color: #718096; background: #1a202c; border: 1px solid #2d3748;
350
+ border-radius: 4px; padding: 1px 8px; font-size: 0.9rem; line-height: 1.4;
351
+ }
352
+ .kb-note-close:hover { color: #e2e8f0; background: #2d3748; }
353
+ /* Metadata chips row. */
354
+ .kb-note-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 4px; }
355
+ .kb-note-chip {
356
+ display: inline-flex; align-items: center; gap: 5px; background: #0e1420;
357
+ border: 1px solid #1e2740; border-radius: 999px; padding: 2px 10px; font-size: 0.72rem; color: #a0aec0;
358
+ }
359
+ .kb-note-chip b { color: #cbd5e0; font-weight: 600; }
360
+ .kb-note-chip i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
361
+ /* File path line under the chips — full width, monospace, wraps on long paths. */
362
+ .kb-note-path {
363
+ margin: 6px 0 2px; font-size: 0.72rem; font-family: ui-monospace, monospace;
364
+ color: #718096; word-break: break-all;
365
+ }
366
+ .kb-note-path b { color: #a0aec0; font-weight: 600; }
367
+ /* Rendered Markdown body. */
368
+ .kb-md {
369
+ margin-top: 12px; padding-top: 12px; border-top: 1px solid #222c40;
370
+ line-height: 1.62; color: #cbd5e0; word-break: break-word;
371
+ }
372
+ .kb-md > :first-child { margin-top: 0; }
373
+ .kb-md h1, .kb-md h2, .kb-md h3, .kb-md h4 { color: #e2e8f0; line-height: 1.3; margin: 18px 0 8px; }
374
+ .kb-md h1 { font-size: 1.25rem; border-bottom: 1px solid #222c40; padding-bottom: 5px; }
375
+ .kb-md h2 { font-size: 1.1rem; border-bottom: 1px solid #1e2740; padding-bottom: 4px; }
376
+ .kb-md h3 { font-size: 1rem; } .kb-md h4 { font-size: 0.9rem; color: #a0aec0; }
377
+ .kb-md p { margin: 8px 0; }
378
+ .kb-md ul, .kb-md ol { margin: 8px 0; padding-left: 22px; }
379
+ .kb-md li { margin: 3px 0; }
380
+ .kb-md a { color: #63b3ed; text-decoration: none; } .kb-md a:hover { text-decoration: underline; }
381
+ .kb-md code {
382
+ background: #0d1117; border: 1px solid #1e2740; border-radius: 4px;
383
+ padding: 1px 5px; font-size: 0.82em; font-family: ui-monospace, monospace; color: #e2e8f0;
384
+ }
385
+ .kb-md pre {
386
+ background: #0a0e17; border: 1px solid #1e2740; border-radius: 6px; padding: 10px 12px;
387
+ overflow-x: auto; margin: 10px 0;
388
+ }
389
+ .kb-md pre code { background: none; border: none; padding: 0; font-size: 0.8rem; line-height: 1.5; }
390
+ .kb-md blockquote {
391
+ margin: 10px 0; padding: 2px 14px; border-left: 3px solid #2d3748; color: #a0aec0;
392
+ }
393
+ .kb-md hr { border: none; border-top: 1px solid #222c40; margin: 14px 0; }
394
+ .kb-md strong { color: #e2e8f0; } .kb-md table { border-collapse: collapse; margin: 0; width: 100%; font-size: 0.82rem; }
395
+ .kb-md th, .kb-md td { border: 1px solid #222c40; padding: 5px 10px; text-align: left; }
396
+ .kb-md th { background: #0e1420; color: #e2e8f0; font-weight: 600; }
397
+ .kb-md tbody tr:nth-child(even) { background: #0e1420; }
398
+ /* Wide tables scroll horizontally instead of breaking the panel. */
399
+ .kb-table-wrap { overflow-x: auto; margin: 10px 0; border-radius: 6px; }
400
+ .kb-md del { color: #718096; }
401
+ /* Task-list items: bullet replaced by a real (disabled) checkbox. */
402
+ .kb-md li.kb-task { list-style: none; margin-left: -18px; }
403
+ .kb-md li.kb-task input { margin-right: 7px; vertical-align: middle; accent-color: #63b3ed; }
404
+ .kb-note-loading { color: #718096; font-size: 0.82rem; margin-top: 12px; }
224
405
  </style>
225
406
  </head>
226
407
  <body>
227
- <h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="refresh-indicator">refreshing...</span> <button id="logout-btn" style="float:right;font-size:0.7rem;background:#2d3748;color:#a0aec0;border:1px solid #4a5568;border-radius:4px;padding:2px 8px;cursor:pointer;">Logout</button></h1>
408
+ <h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="top-right"><button id="logout-btn">Logout</button><span id="refresh-indicator">refreshing...</span></span></h1>
228
409
  <div class="meta">
229
410
  Uptime: <span id="uptime">&mdash;</span> &nbsp;|&nbsp;
230
411
  Started: <span id="started-at">&mdash;</span> &nbsp;|&nbsp;
231
412
  Last updated: <span id="last-updated">&mdash;</span>
232
413
  </div>
233
414
 
415
+ <!-- Tab switcher: Sessions (live PTY + table) | Knowledge base (shared-KB graph) -->
416
+ <div class="tabs">
417
+ <button class="tab active" id="tab-sessions" data-view="view-sessions">Sessions</button>
418
+ <button class="tab" id="tab-kb" data-view="view-kb">Knowledge base</button>
419
+ <button class="tab" id="tab-dreams" data-view="view-dreams">Nightly dreaming</button>
420
+ </div>
421
+
422
+ <div id="view-sessions" class="view">
234
423
  <!-- Row: Processes 70% | Agent badges 30% (collapses on narrow screens) -->
235
424
  <div class="top-grid">
236
425
  <div>
@@ -284,6 +473,61 @@ function generateDashboardHtml() {
284
473
  </tbody>
285
474
  </table>
286
475
  </div>
476
+ </div><!-- /view-sessions -->
477
+
478
+ <!-- Knowledge base — a 3D force-directed graph over the shared vault / an agent's memory.
479
+ Zero external deps: the spherical layout + canvas render + rotation are hand-rolled. -->
480
+ <div id="view-kb" class="view" style="display:none;">
481
+ <div class="kb-toolbar">
482
+ <label id="kb-source-wrap" title="Which memory graph to view: the cross-agent Shared KB, or one agent's own memory">Source
483
+ <select id="kb-source"><option value="shared">Shared Knowledge Base</option></select>
484
+ </label>
485
+ <button id="kb-refresh" title="Recompute the graph from the selected source">&#x21bb; Refresh</button>
486
+ <span class="kb-search-wrap">
487
+ <input id="kb-search" type="search" placeholder="Search notes&hellip;" autocomplete="off" spellcheck="false">
488
+ <span id="kb-search-count" class="kb-search-count"></span>
489
+ </span>
490
+ <label><input type="checkbox" id="kb-demo-toggle" checked> Show demo data when vault is empty</label>
491
+ <label id="kb-demo-size-wrap" title="Synthetic node count for the demo (stress-test the viewer at scale)">Demo size
492
+ <select id="kb-demo-size">
493
+ <option value="0">8 (sample)</option>
494
+ <option value="100">100</option>
495
+ <option value="300" selected>300</option>
496
+ <option value="600">600</option>
497
+ </select>
498
+ </label>
499
+ <span id="kb-demo-badge" class="kb-demo-badge" style="display:none;">DEMO DATA</span>
500
+ <span id="kb-stats"></span>
501
+ <span class="kb-legend" id="kb-legend"></span>
502
+ </div>
503
+ <div class="kb-stage">
504
+ <!-- 3D knowledge graph: a sphere of notes rendered on canvas. Auto-spins; drag
505
+ empty space to rotate the view; wheel to zoom. Centred by projection, so there
506
+ is no pan and no "center" button — the graph can never drift off-screen. -->
507
+ <canvas id="kb-canvas"></canvas>
508
+ <div id="kb-empty" class="kb-empty" style="display:none;"></div>
509
+ <div class="kb-zoom">
510
+ <button id="kb-zoom-in" type="button" title="Zoom in" aria-label="Zoom in">&plus;</button>
511
+ <button id="kb-zoom-out" type="button" title="Zoom out" aria-label="Zoom out">&minus;</button>
512
+ </div>
513
+ </div>
514
+ <!-- Note detail — full width BELOW the graph so the whole file shows (no
515
+ truncation), rendered as formatted Markdown. Populated on node click. -->
516
+ <div id="kb-note" class="kb-note" style="display:none;"></div>
517
+ </div><!-- /view-kb -->
518
+
519
+ <!-- Nightly dreaming — the memory-consolidation audit trail (.dreaming/) per agent. -->
520
+ <div id="view-dreams" class="view" style="display:none;">
521
+ <div class="kb-toolbar">
522
+ <button id="dreams-refresh" title="Reload the dreaming audit trail">&#x21bb; Refresh</button>
523
+ <label id="dreams-agent-wrap">Agent
524
+ <select id="dreams-agent"><option value="">All agents</option></select>
525
+ </label>
526
+ <span id="dreams-stats"></span>
527
+ </div>
528
+ <div id="dreams-list" class="dreams-list"></div>
529
+ <div id="dreams-empty" class="dreams-empty" style="display:none;"></div>
530
+ </div><!-- /view-dreams -->
287
531
 
288
532
  <div id="error-msg" class="error" style="display:none;"></div>
289
533
 
@@ -905,6 +1149,885 @@ function generateDashboardHtml() {
905
1149
  // Process tree (with CPU/mem) is heavier (spawns ps) — refresh a bit slower.
906
1150
  setInterval(refreshProcesses, 6000);
907
1151
  </script>
1152
+
1153
+ <!-- Knowledge base graph: tab switching + hand-rolled force-directed renderer.
1154
+ No template literals here (this file is itself a template literal). -->
1155
+ <script id="kb-graph">
1156
+ (function(){
1157
+ // ---------- Tab switching ----------
1158
+ var tabs = document.querySelectorAll('.tab');
1159
+ var kbLoaded = false;
1160
+ tabs.forEach(function(tab){
1161
+ tab.addEventListener('click', function(){
1162
+ tabs.forEach(function(t){ t.classList.remove('active'); });
1163
+ tab.classList.add('active');
1164
+ document.querySelectorAll('.view').forEach(function(v){ v.style.display = 'none'; });
1165
+ var viewEl = document.getElementById(tab.getAttribute('data-view'));
1166
+ if (viewEl) viewEl.style.display = '';
1167
+ if (tab.getAttribute('data-view') === 'view-kb' && !kbLoaded) { kbLoaded = true; loadSources(); }
1168
+ if (tab.getAttribute('data-view') === 'view-dreams' && window.__loadDreams) { window.__loadDreams(); }
1169
+ });
1170
+ });
1171
+
1172
+ // ---------- Graph elements + refs ----------
1173
+ var canvas = document.getElementById('kb-canvas');
1174
+ var elEmpty = document.getElementById('kb-empty');
1175
+ var elNote = document.getElementById('kb-note');
1176
+ var elStats = document.getElementById('kb-stats');
1177
+ var elLegend = document.getElementById('kb-legend');
1178
+ var elDemoBadge = document.getElementById('kb-demo-badge');
1179
+ var sourceSel = document.getElementById('kb-source');
1180
+ var demoToggle = document.getElementById('kb-demo-toggle');
1181
+ var demoToggleWrap = demoToggle ? demoToggle.closest('label') : null;
1182
+ var demoSize = document.getElementById('kb-demo-size');
1183
+ var demoSizeWrap = document.getElementById('kb-demo-size-wrap');
1184
+ var elSearch = document.getElementById('kb-search');
1185
+ var elSearchCount = document.getElementById('kb-search-count');
1186
+
1187
+ // Curated colours for the memory type: vocabulary that ACTUALLY appears in
1188
+ // notes (user / feedback / project / reference / …) — the old map keyed on a
1189
+ // KB ontology (decision/evidence/claim…) that never matched, so every node
1190
+ // fell to one grey. Any type not listed gets a stable, distinct hue from a
1191
+ // hash so tags never collapse to a single colour again.
1192
+ var TYPE_COLORS = {
1193
+ user: '#63b3ed', feedback: '#f6ad55', project: '#68d391',
1194
+ reference: '#b794f4', session: '#4fd1c5', fact: '#f687b3',
1195
+ decision: '#63b3ed', evidence: '#68d391', claim: '#f6ad55',
1196
+ policy: '#b794f4', infra: '#4fd1c5'
1197
+ };
1198
+ var DEFAULT_COLOR = '#7d8fb3';
1199
+ function hashHue(s){ var h = 0; for (var i = 0; i < s.length; i++){ h = (h * 31 + s.charCodeAt(i)) & 0xffffffff; } return Math.abs(h) % 360; }
1200
+ function colorFor(type){
1201
+ if (!type || type === 'other') return DEFAULT_COLOR;
1202
+ if (TYPE_COLORS[type]) return TYPE_COLORS[type];
1203
+ // Deterministic distinct colour for any unlisted type (mid tone, readable on the dark stage).
1204
+ return 'hsl(' + hashHue(type) + ', 62%, 62%)';
1205
+ }
1206
+
1207
+ // ---------- 3D graph engine (canvas) ----------
1208
+ // Notes are laid out on the surface of a unit sphere (Fibonacci seed + a short 3D
1209
+ // force relax) centred at the origin. Every frame the whole sphere is rotated by
1210
+ // (rotY, rotX) and perspective-projected to the canvas centre — so the graph is
1211
+ // ALWAYS centred by construction: no pan, no "center" button, it cannot drift off
1212
+ // screen. It auto-spins; dragging empty space rotates the view. This 3D rotation is
1213
+ // the "หมุน" a flat 2D SVG (which could only pan) was never able to do.
1214
+ var LABEL_MIN_DEG = 3; // hubs keep their label; others reveal on hover/search
1215
+ var G = null; // { nodes:[{nd,x,y,z,deg,i,hit}], edges:[{a,b}] }
1216
+ var rotY = 0.6, rotX = -0.25, zoom = 1;
1217
+ var dragging = false, moved = false, downX = 0, downY = 0, lastX = 0, lastY = 0;
1218
+ // Angular momentum: the last drag speed carries the sphere on after release so it
1219
+ // keeps orbiting (decays toward the gentle baseline auto-spin) instead of freezing.
1220
+ var velY = 0, velX = 0;
1221
+ var MIN_ZOOM = 0.25, MAX_ZOOM = 8, SPIN = 0.0025; // zoom clamp + baseline orbit speed
1222
+ var hover = null, selected = null, auto = true;
1223
+ var cvW = 0, cvH = 0, cvDpr = 1, needRender = true, lastFrame = 0;
1224
+ var P = []; // last projected screen coords, for hit-testing
1225
+
1226
+ function invalidate(){ needRender = true; }
1227
+ function sizeCanvas(){
1228
+ if (!canvas) return;
1229
+ cvDpr = window.devicePixelRatio || 1;
1230
+ cvW = canvas.clientWidth; cvH = canvas.clientHeight;
1231
+ canvas.width = cvW * cvDpr; canvas.height = cvH * cvDpr;
1232
+ invalidate();
1233
+ }
1234
+
1235
+ // ---------- Build model from API payload ----------
1236
+ // Seed nodes on a Fibonacci sphere, then relax with a 3D repulsion + spring pass so
1237
+ // linked notes pull together into clusters while the whole set stays on the sphere.
1238
+ function buildModel(data){
1239
+ hideNote(); hover = null; selected = null;
1240
+ var raw = (data && data.nodes) || [];
1241
+ var rawEdges = (data && data.edges) || [];
1242
+ var N = raw.length || 1;
1243
+ var byId = {};
1244
+ var nodes = raw.map(function(nd, i){
1245
+ var t = Math.acos(1 - 2 * (i + 0.5) / N), p = Math.PI * (1 + Math.sqrt(5)) * i;
1246
+ return {
1247
+ nd: {
1248
+ id: nd.id, title: nd.title || nd.id, type: nd.type || null,
1249
+ degree: nd.degree || 0,
1250
+ confidence: (nd.confidence === undefined ? null : nd.confidence),
1251
+ updatedAt: nd.updatedAt || null, stale: !!nd.stale,
1252
+ contradiction: !!nd.contradiction, excerpt: nd.excerpt || null
1253
+ },
1254
+ x: Math.sin(t) * Math.cos(p), y: Math.sin(t) * Math.sin(p), z: Math.cos(t),
1255
+ deg: 0, i: i, hit: true
1256
+ };
1257
+ });
1258
+ nodes.forEach(function(o){ byId[o.nd.id] = o; });
1259
+ var edges = [];
1260
+ rawEdges.forEach(function(e){
1261
+ var a = byId[e.source], b = byId[e.target];
1262
+ if (a && b){ edges.push({ a: a, b: b }); a.deg++; b.deg++; }
1263
+ });
1264
+ relax(nodes, edges);
1265
+ G = { nodes: nodes, edges: edges };
1266
+ rotY = 0.6; rotX = -0.25; zoom = 1; auto = true; velY = 0; velX = 0;
1267
+ sizeCanvas(); invalidate();
1268
+ // Re-apply an active search to the freshly-built set (refresh / source switch).
1269
+ if (elSearch && elSearch.value.trim()) runSearch(); else clearSearch();
1270
+ }
1271
+
1272
+ // Short 3D force relax: all-pairs repulsion keeps nodes apart, edge springs pull
1273
+ // linked notes together, then recentre at the origin and renormalise to the unit
1274
+ // sphere so projection stays centred. Iterations scale down for large demo sets.
1275
+ function relax(nodes, edges){
1276
+ var N = nodes.length || 1;
1277
+ var iters = N > 400 ? 60 : 110;
1278
+ for (var it = 0; it < iters; it++){
1279
+ for (var i = 0; i < nodes.length; i++){
1280
+ for (var k = i + 1; k < nodes.length; k++){
1281
+ var dx = nodes[i].x - nodes[k].x, dy = nodes[i].y - nodes[k].y, dz = nodes[i].z - nodes[k].z;
1282
+ var d2 = dx*dx + dy*dy + dz*dz + 0.01, f = 0.018 / d2, inv = 1 / Math.sqrt(d2);
1283
+ dx *= inv; dy *= inv; dz *= inv;
1284
+ nodes[i].x += dx*f; nodes[i].y += dy*f; nodes[i].z += dz*f;
1285
+ nodes[k].x -= dx*f; nodes[k].y -= dy*f; nodes[k].z -= dz*f;
1286
+ }
1287
+ }
1288
+ for (var e = 0; e < edges.length; e++){
1289
+ var A = edges[e].a, B = edges[e].b;
1290
+ var ex = B.x - A.x, ey = B.y - A.y, ez = B.z - A.z, sf = 0.012;
1291
+ A.x += ex*sf; A.y += ey*sf; A.z += ez*sf; B.x -= ex*sf; B.y -= ey*sf; B.z -= ez*sf;
1292
+ }
1293
+ }
1294
+ var cx = 0, cy = 0, cz = 0;
1295
+ nodes.forEach(function(n){ cx += n.x; cy += n.y; cz += n.z; });
1296
+ cx /= N; cy /= N; cz /= N;
1297
+ var R = 0;
1298
+ nodes.forEach(function(n){ n.x -= cx; n.y -= cy; n.z -= cz; R = Math.max(R, Math.hypot(n.x, n.y, n.z)); });
1299
+ R = R || 1;
1300
+ nodes.forEach(function(n){ n.x /= R; n.y /= R; n.z /= R; });
1301
+ }
1302
+
1303
+ // ---------- Render ----------
1304
+ // Rotate each node by (rotY around the vertical axis, rotX around the horizontal),
1305
+ // perspective-divide, and paint. Depth (post-rotation z) drives alpha and paint
1306
+ // order so the near hemisphere reads brighter and overpaints the far one.
1307
+ function render(){
1308
+ if (!canvas || !G || !cvW) return;
1309
+ var ctx = canvas.getContext('2d');
1310
+ ctx.setTransform(cvDpr, 0, 0, cvDpr, 0, 0);
1311
+ ctx.clearRect(0, 0, cvW, cvH);
1312
+ var nodes = G.nodes;
1313
+ if (!nodes.length){ P = []; return; }
1314
+ var cyw = Math.cos(rotY), syw = Math.sin(rotY), cxp = Math.cos(rotX), sxp = Math.sin(rotX);
1315
+ var scale = Math.min(cvW, cvH) * 0.34 * zoom, ox = cvW / 2, oy = cvH / 2, cam = 3;
1316
+ var searching = !!(elSearch && elSearch.value.trim());
1317
+ P = nodes.map(function(o){
1318
+ var x = o.x * cyw - o.z * syw, z = o.x * syw + o.z * cyw;
1319
+ var y = o.y * cxp - z * sxp; z = o.y * sxp + z * cxp;
1320
+ var q = cam / (cam - z);
1321
+ return { sx: ox + x * scale * q, sy: oy + y * scale * q, z: z,
1322
+ r: Math.max(2.5, (4 + Math.sqrt(o.deg) * 2.2) * q), o: o };
1323
+ });
1324
+ // Edges first, depth-shaded (far side dimmer); dimmed hard when searching unless
1325
+ // both ends match the query, so the hit set reads as a connected subgraph.
1326
+ ctx.lineWidth = 1;
1327
+ G.edges.forEach(function(ed){
1328
+ var a = P[ed.a.i], b = P[ed.b.i];
1329
+ var depth = ((a.z + b.z) / 2 + 1) / 2;
1330
+ var lit = !searching || (ed.a.hit && ed.b.hit);
1331
+ ctx.strokeStyle = 'rgba(120,150,210,' + (lit ? (0.10 + 0.32 * depth) : 0.03) + ')';
1332
+ ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
1333
+ });
1334
+ // Nodes back-to-front so near nodes overpaint far ones.
1335
+ var order = P.map(function(_, i){ return i; }).sort(function(i, k){ return P[i].z - P[k].z; });
1336
+ order.forEach(function(i){
1337
+ var p = P[i], o = p.o, nd = o.nd, near = (p.z + 1) / 2;
1338
+ var isHov = hover === i, isSel = selected === i;
1339
+ var dim = searching && !o.hit;
1340
+ var alpha = dim ? 0.08 : (0.4 + 0.6 * near);
1341
+ var col = colorFor(nd.type); // keep the node's own type colour, even when hovered/selected
1342
+ // Soft additive bloom so each node reads as a point of LIGHT (fuzzy halo),
1343
+ // not a flat disc with a hard edge. Radial gradient fading to transparent,
1344
+ // painted with 'lighter' so overlapping glows accumulate like real light.
1345
+ if (!dim){
1346
+ var glowR = p.r * ((isHov || isSel) ? 6.5 : 4.2);
1347
+ var gi = ((isHov || isSel) ? 0.55 : 0.26) * (0.45 + 0.55 * near);
1348
+ var grad = ctx.createRadialGradient(p.sx, p.sy, 0, p.sx, p.sy, glowR);
1349
+ grad.addColorStop(0, col);
1350
+ grad.addColorStop(1, 'rgba(0,0,0,0)');
1351
+ ctx.globalCompositeOperation = 'lighter';
1352
+ ctx.globalAlpha = gi;
1353
+ ctx.fillStyle = grad;
1354
+ ctx.beginPath(); ctx.arc(p.sx, p.sy, glowR, 0, 7); ctx.fill();
1355
+ ctx.globalCompositeOperation = 'source-over';
1356
+ ctx.globalAlpha = 1;
1357
+ }
1358
+ // Bright solid core on top of its glow.
1359
+ ctx.beginPath(); ctx.arc(p.sx, p.sy, p.r, 0, 7);
1360
+ ctx.globalAlpha = alpha;
1361
+ ctx.fillStyle = col;
1362
+ ctx.fill();
1363
+ ctx.globalAlpha = 1;
1364
+ if (searching && o.hit){ ctx.strokeStyle = '#f6e05e'; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
1365
+ else if (nd.contradiction){ ctx.strokeStyle = '#fc8181'; ctx.lineWidth = 2; ctx.stroke(); ctx.lineWidth = 1; }
1366
+ else if (nd.stale){ ctx.strokeStyle = '#f0883e'; ctx.lineWidth = 1.2; ctx.stroke(); ctx.lineWidth = 1; }
1367
+ // Hover / selection ring: a white halo OUTSIDE the node so the node keeps
1368
+ // its own type colour. Selected = bright + thick, hover = softer + thinner.
1369
+ if ((isSel || isHov) && !dim){
1370
+ ctx.beginPath(); ctx.arc(p.sx, p.sy, p.r + 3, 0, 7);
1371
+ ctx.strokeStyle = isSel ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.5)';
1372
+ ctx.lineWidth = isSel ? 2 : 1.4; ctx.stroke(); ctx.lineWidth = 1;
1373
+ }
1374
+ // Labels: hubs always; hovered / selected / search-hit reveal on demand.
1375
+ var showLabel = (o.deg >= LABEL_MIN_DEG) || isHov || isSel || (searching && o.hit);
1376
+ if (showLabel && !dim){
1377
+ ctx.fillStyle = (isHov || isSel) ? '#e6edf3' : 'rgba(213,222,236,' + (0.25 + 0.6 * near) + ')';
1378
+ ctx.font = (isHov || isSel ? '12px ' : '11px ') + 'ui-sans-serif, system-ui, sans-serif';
1379
+ ctx.fillText(nd.title, p.sx + p.r + 4, p.sy + 3);
1380
+ }
1381
+ });
1382
+ }
1383
+
1384
+ // ---------- Hit-testing + interactions ----------
1385
+ function hitTest(mx, my){
1386
+ var best = null, bd = 16;
1387
+ for (var i = 0; i < P.length; i++){
1388
+ var p = P[i]; var d = Math.hypot(p.sx - mx, p.sy - my);
1389
+ if (d < bd && d < p.r + 8){ bd = d; best = i; }
1390
+ }
1391
+ return best;
1392
+ }
1393
+ function stopSpin(){ auto = false; }
1394
+ function initInteractions(){
1395
+ if (!canvas) return;
1396
+ sizeCanvas();
1397
+ window.addEventListener('resize', sizeCanvas);
1398
+ canvas.addEventListener('mousedown', function(e){
1399
+ dragging = true; moved = false; downX = lastX = e.clientX; downY = lastY = e.clientY;
1400
+ velY = 0; velX = 0; // grabbing halts any residual momentum
1401
+ canvas.classList.add('drag');
1402
+ });
1403
+ // A press that never crossed the drag threshold is a tap: on a node open its
1404
+ // panel, on empty space dismiss it. Bound on window so a drag that leaves the
1405
+ // canvas still ends cleanly.
1406
+ window.addEventListener('mouseup', function(e){
1407
+ if (dragging && !moved && G){
1408
+ var r = canvas.getBoundingClientRect();
1409
+ var hit = hitTest(e.clientX - r.left, e.clientY - r.top);
1410
+ if (hit != null){ selected = hit; showNote(G.nodes[hit].nd); }
1411
+ else { selected = null; hideNote(); }
1412
+ invalidate();
1413
+ }
1414
+ // Releasing after a rotate resumes the orbit — the sphere keeps spinning,
1415
+ // carried on by the momentum captured during the drag (see the frame loop).
1416
+ if (dragging) auto = true;
1417
+ dragging = false; canvas.classList.remove('drag');
1418
+ });
1419
+ canvas.addEventListener('mousemove', function(e){
1420
+ var r = canvas.getBoundingClientRect();
1421
+ if (dragging){
1422
+ // Empty-space drag ROTATES the sphere (X drag → spin, Y drag → tilt); this is
1423
+ // the interaction the old flat-SVG pan could never provide.
1424
+ if (Math.abs(e.clientX - downX) + Math.abs(e.clientY - downY) > 4){ moved = true; }
1425
+ var dY = (e.clientX - lastX) * 0.01, dX = (e.clientY - lastY) * 0.01;
1426
+ rotY += dY; rotX += dX;
1427
+ velY = dY; velX = dX; // remember the latest speed → release momentum
1428
+ if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
1429
+ lastX = e.clientX; lastY = e.clientY; invalidate();
1430
+ } else {
1431
+ var hit = hitTest(e.clientX - r.left, e.clientY - r.top);
1432
+ if (hit !== hover){ hover = hit; canvas.style.cursor = (hit != null) ? 'pointer' : 'grab'; invalidate(); }
1433
+ }
1434
+ });
1435
+ // Touch: one-finger drag rotates.
1436
+ canvas.addEventListener('touchstart', function(e){
1437
+ if (e.touches.length === 1){ dragging = true; moved = false; velY = 0; velX = 0;
1438
+ var t = e.touches[0]; downX = lastX = t.clientX; downY = lastY = t.clientY; }
1439
+ }, { passive: true });
1440
+ canvas.addEventListener('touchmove', function(e){
1441
+ if (dragging && e.touches.length === 1){
1442
+ var t = e.touches[0];
1443
+ var dY = (t.clientX - lastX) * 0.01, dX = (t.clientY - lastY) * 0.01;
1444
+ rotY += dY; rotX += dX; velY = dY; velX = dX;
1445
+ if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
1446
+ lastX = t.clientX; lastY = t.clientY; moved = true;
1447
+ invalidate(); e.preventDefault();
1448
+ }
1449
+ }, { passive: false });
1450
+ canvas.addEventListener('touchend', function(){ if (dragging) auto = true; dragging = false; });
1451
+ // Zoom: wheel + the on-screen +/- buttons share one clamp. Range widened so the
1452
+ // sphere can be pushed much closer / further than before.
1453
+ function zoomBy(f){
1454
+ zoom *= f;
1455
+ if (zoom < MIN_ZOOM) zoom = MIN_ZOOM; else if (zoom > MAX_ZOOM) zoom = MAX_ZOOM;
1456
+ invalidate();
1457
+ }
1458
+ canvas.addEventListener('wheel', function(e){
1459
+ e.preventDefault();
1460
+ zoomBy(e.deltaY < 0 ? 1.1 : 0.9);
1461
+ }, { passive: false });
1462
+ var zin = document.getElementById('kb-zoom-in'), zout = document.getElementById('kb-zoom-out');
1463
+ if (zin) zin.addEventListener('click', function(){ zoomBy(1.25); });
1464
+ if (zout) zout.addEventListener('click', function(){ zoomBy(0.8); });
1465
+ // Single throttled rAF loop: ~30fps, pauses when hidden or nothing changed. Also
1466
+ // recovers the canvas size the first time the tab becomes visible (clientWidth is
1467
+ // 0 while the view is display:none, so the initial sizeCanvas() reads nothing).
1468
+ function frame(t){
1469
+ requestAnimationFrame(frame);
1470
+ if (document.hidden || !G) return;
1471
+ if (!cvW){ sizeCanvas(); if (!cvW) return; }
1472
+ if (!dragging && G.nodes.length){
1473
+ // Post-release momentum decays smoothly; the baseline auto-spin keeps the
1474
+ // sphere orbiting forever once the fling has bled off.
1475
+ if (Math.abs(velY) > 0.00008 || Math.abs(velX) > 0.00008){
1476
+ rotY += velY; rotX += velX;
1477
+ if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
1478
+ velY *= 0.95; velX *= 0.95; needRender = true;
1479
+ } else { velY = 0; velX = 0; }
1480
+ if (auto){ rotY += SPIN; needRender = true; }
1481
+ }
1482
+ if (!needRender || t - lastFrame < 28) return;
1483
+ lastFrame = t; needRender = false; render();
1484
+ }
1485
+ requestAnimationFrame(frame);
1486
+ }
1487
+
1488
+ // ---------- Search ----------
1489
+ // Mark matching notes (title / type / excerpt). render() dims the rest and rings the
1490
+ // hits; a hit's label is revealed even under the declutter threshold.
1491
+ function clearSearch(){
1492
+ if (G) G.nodes.forEach(function(o){ o.hit = true; });
1493
+ if (elSearchCount) elSearchCount.textContent = '';
1494
+ invalidate();
1495
+ }
1496
+ function runSearch(){
1497
+ if (!elSearch || !G) return [];
1498
+ var q = elSearch.value.trim().toLowerCase();
1499
+ if (!q){ clearSearch(); return []; }
1500
+ var hits = [];
1501
+ G.nodes.forEach(function(o){
1502
+ var nd = o.nd;
1503
+ var hay = (nd.title + ' ' + (nd.type || '') + ' ' + (nd.excerpt || '')).toLowerCase();
1504
+ o.hit = hay.indexOf(q) >= 0;
1505
+ if (o.hit) hits.push(o);
1506
+ });
1507
+ if (elSearchCount) elSearchCount.textContent = hits.length + ' / ' + G.nodes.length;
1508
+ invalidate();
1509
+ return hits;
1510
+ }
1511
+ // Enter → rotate the sphere so the first match faces the camera (front & centre) and
1512
+ // open its panel — the 3D analogue of "recentre on the first hit".
1513
+ function focusFirstMatch(){
1514
+ var hits = runSearch();
1515
+ if (!hits.length) return;
1516
+ var n0 = hits[0];
1517
+ stopSpin(); velY = 0; velX = 0;
1518
+ rotY = Math.atan2(n0.x, n0.z);
1519
+ var z1 = Math.hypot(n0.x, n0.z);
1520
+ rotX = Math.atan2(n0.y, z1);
1521
+ if (rotX > 1.5) rotX = 1.5; else if (rotX < -1.5) rotX = -1.5;
1522
+ selected = n0.i; showNote(n0.nd);
1523
+ invalidate();
1524
+ }
1525
+
1526
+ // ---------- Detail note (full-width, below the graph) ----------
1527
+ function el(tag, txt){ var e = document.createElement(tag); if (txt !== undefined) e.textContent = txt; return e; }
1528
+ function chip(label, value, dotColor){
1529
+ var c = el('span'); c.setAttribute('class', 'kb-note-chip');
1530
+ if (dotColor){ var i = el('i'); i.style.background = dotColor; c.appendChild(i); }
1531
+ c.appendChild(el('span', label + ' ')); c.appendChild(el('b', value));
1532
+ return c;
1533
+ }
1534
+
1535
+ // ---------- Minimal, XSS-safe Markdown renderer ----------
1536
+ // Builds DOM nodes only (textContent everywhere — never assigns raw HTML), so
1537
+ // an untrusted note body cannot inject markup. Handles headings, fenced/inline
1538
+ // code, lists, blockquotes, hr, and inline bold/italic/code/links.
1539
+ // NOTE: this whole script lives inside a TS template literal, so every regex
1540
+ // backslash MUST be doubled (\\s not \s) — a single backslash is stripped at
1541
+ // build time. Literal backticks are written as \` (kept single).
1542
+ function sanitizeHref(h){
1543
+ var s = String(h || '').trim();
1544
+ // Allow only http(s), mailto, and in-vault relative links; block javascript:, data:, etc.
1545
+ if (/^https?:\\/\\//i.test(s) || /^mailto:/i.test(s)) return s;
1546
+ if (/^[\\w./#?=&%-]+$/.test(s) && !/^[a-z]+:/i.test(s)) return s;
1547
+ return null;
1548
+ }
1549
+ function renderInline(container, text){
1550
+ // Tokenise on: inline code, bold, strikethrough, italic, and [label](href) links.
1551
+ var re = /(\`[^\`]+\`)|(\\*\\*[^*]+\\*\\*)|(~~[^~]+~~)|(\\*[^*]+\\*|_[^_]+_)|(\\[[^\\]]+\\]\\([^)]+\\))/;
1552
+ var rest = String(text);
1553
+ while (rest.length){
1554
+ var m = re.exec(rest);
1555
+ if (!m){ container.appendChild(document.createTextNode(rest)); break; }
1556
+ if (m.index > 0) container.appendChild(document.createTextNode(rest.slice(0, m.index)));
1557
+ var tok = m[0];
1558
+ if (tok.charAt(0) === '\`'){
1559
+ container.appendChild(el('code', tok.slice(1, -1)));
1560
+ } else if (tok.slice(0, 2) === '**'){
1561
+ container.appendChild(el('strong', tok.slice(2, -2)));
1562
+ } else if (tok.slice(0, 2) === '~~'){
1563
+ container.appendChild(el('del', tok.slice(2, -2)));
1564
+ } else if (tok.charAt(0) === '*' || tok.charAt(0) === '_'){
1565
+ container.appendChild(el('em', tok.slice(1, -1)));
1566
+ } else {
1567
+ var lm = /^\\[([^\\]]+)\\]\\(([^)]+)\\)$/.exec(tok);
1568
+ var href = lm ? sanitizeHref(lm[2]) : null;
1569
+ if (lm && href){
1570
+ var a = el('a', lm[1]); a.setAttribute('href', href);
1571
+ a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer');
1572
+ container.appendChild(a);
1573
+ } else {
1574
+ container.appendChild(document.createTextNode(tok));
1575
+ }
1576
+ }
1577
+ rest = rest.slice(m.index + tok.length);
1578
+ }
1579
+ }
1580
+ function renderMarkdown(root, md){
1581
+ root.textContent = '';
1582
+ var lines = String(md).replace(/\\r\\n?/g, '\\n').split('\\n');
1583
+ var i = 0;
1584
+ // Nested-list stack: each entry = { list:<ul|ol>, ordered, indent }.
1585
+ var stack = [];
1586
+ function endList(){ stack.length = 0; }
1587
+ function listTarget(indent, ordered){
1588
+ while (stack.length && stack[stack.length - 1].indent > indent) stack.pop();
1589
+ var top = stack.length ? stack[stack.length - 1] : null;
1590
+ if (top && top.indent === indent){
1591
+ if (top.ordered === ordered) return top.list;
1592
+ stack.pop(); top = stack.length ? stack[stack.length - 1] : null;
1593
+ }
1594
+ var listEl = el(ordered ? 'ol' : 'ul');
1595
+ if (top && top.indent < indent && top.list.lastChild) top.list.lastChild.appendChild(listEl);
1596
+ else root.appendChild(listEl);
1597
+ stack.push({ list: listEl, ordered: ordered, indent: indent });
1598
+ return listEl;
1599
+ }
1600
+ // Split a table row into trimmed cells (drop the optional edge pipes).
1601
+ function tableCells(row){
1602
+ return row.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(function(s){ return s.trim(); });
1603
+ }
1604
+ while (i < lines.length){
1605
+ var line = lines[i];
1606
+ // Fenced code block.
1607
+ var fence = /^\\s*\`\`\`(.*)$/.exec(line);
1608
+ if (fence){
1609
+ endList(); i++;
1610
+ var buf = [];
1611
+ while (i < lines.length && !/^\\s*\`\`\`\\s*$/.test(lines[i])){ buf.push(lines[i]); i++; }
1612
+ i++; // closing fence
1613
+ var pre = el('pre'); pre.appendChild(el('code', buf.join('\\n')));
1614
+ root.appendChild(pre); continue;
1615
+ }
1616
+ // Horizontal rule.
1617
+ if (/^\\s*([-*_])(\\s*\\1){2,}\\s*$/.test(line)){ endList(); root.appendChild(el('hr')); i++; continue; }
1618
+ // Heading.
1619
+ var h = /^(#{1,6})\\s+(.*)$/.exec(line);
1620
+ if (h){ endList(); var hEl = el('h' + h[1].length); renderInline(hEl, h[2].trim()); root.appendChild(hEl); i++; continue; }
1621
+ // Blockquote (collapse consecutive > lines).
1622
+ if (/^\\s*>\\s?/.test(line)){
1623
+ endList(); var q = el('blockquote'); var qbuf = [];
1624
+ while (i < lines.length && /^\\s*>\\s?/.test(lines[i])){ qbuf.push(lines[i].replace(/^\\s*>\\s?/, '')); i++; }
1625
+ renderInline(q, qbuf.join(' ')); root.appendChild(q); continue;
1626
+ }
1627
+ // GFM pipe table: a header row immediately followed by a |---|:--:| separator.
1628
+ if (/\\|/.test(line) && i + 1 < lines.length &&
1629
+ /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*$/.test(lines[i + 1])){
1630
+ endList();
1631
+ var aligns = tableCells(lines[i + 1]).map(function(s){
1632
+ var lft = s.charAt(0) === ':', rgt = s.charAt(s.length - 1) === ':';
1633
+ return (lft && rgt) ? 'center' : rgt ? 'right' : lft ? 'left' : '';
1634
+ });
1635
+ var table = el('table'); var thead = el('thead'); var htr = el('tr');
1636
+ tableCells(line).forEach(function(c, idx){
1637
+ var th = el('th'); if (aligns[idx]) th.style.textAlign = aligns[idx];
1638
+ renderInline(th, c); htr.appendChild(th);
1639
+ });
1640
+ thead.appendChild(htr); table.appendChild(thead);
1641
+ var tbody = el('tbody'); i += 2;
1642
+ while (i < lines.length && /\\|/.test(lines[i]) && lines[i].trim() !== ''){
1643
+ var tr = el('tr');
1644
+ tableCells(lines[i]).forEach(function(c, idx){
1645
+ var td = el('td'); if (aligns[idx]) td.style.textAlign = aligns[idx];
1646
+ renderInline(td, c); tr.appendChild(td);
1647
+ });
1648
+ tbody.appendChild(tr); i++;
1649
+ }
1650
+ table.appendChild(tbody);
1651
+ var wrap = el('div'); wrap.setAttribute('class', 'kb-table-wrap'); wrap.appendChild(table);
1652
+ root.appendChild(wrap); continue;
1653
+ }
1654
+ // List item (ordered/unordered), indent-nested, with optional task checkbox.
1655
+ var li = /^(\\s*)(?:([-*+])|(\\d+)[.)])\\s+(.*)$/.exec(line);
1656
+ if (li){
1657
+ var indent = li[1].length;
1658
+ var ordered = li[3] !== undefined;
1659
+ var content = li[4];
1660
+ var target = listTarget(indent, ordered);
1661
+ var item = el('li');
1662
+ var task = /^\\[([ xX])\\]\\s+(.*)$/.exec(content);
1663
+ if (task){
1664
+ item.setAttribute('class', 'kb-task');
1665
+ var box = el('input'); box.setAttribute('type', 'checkbox'); box.setAttribute('disabled', '');
1666
+ if (task[1] !== ' ') box.setAttribute('checked', '');
1667
+ item.appendChild(box);
1668
+ renderInline(item, ' ' + task[2]);
1669
+ } else {
1670
+ renderInline(item, content);
1671
+ }
1672
+ target.appendChild(item); i++; continue;
1673
+ }
1674
+ // Blank line ends a list / paragraph.
1675
+ if (/^\\s*$/.test(line)){ endList(); i++; continue; }
1676
+ // Paragraph: gather consecutive plain lines.
1677
+ endList(); var pbuf = [line];
1678
+ i++;
1679
+ while (i < lines.length && !/^\\s*$/.test(lines[i]) &&
1680
+ !/^(#{1,6})\\s/.test(lines[i]) && !/^\\s*>\\s?/.test(lines[i]) &&
1681
+ !/^\\s*(?:[-*+]|\\d+[.)])\\s+/.test(lines[i]) && !/^\\s*\`\`\`/.test(lines[i])){
1682
+ pbuf.push(lines[i]); i++;
1683
+ }
1684
+ var p = el('p'); renderInline(p, pbuf.join('\\n')); root.appendChild(p);
1685
+ }
1686
+ if (!root.childNodes.length) root.appendChild(el('p', '(empty note)'));
1687
+ }
1688
+
1689
+ var noteReq = 0; // guards against out-of-order fetch responses
1690
+ function showNote(nd){
1691
+ noteReq++;
1692
+ var myReq = noteReq;
1693
+ elNote.textContent = '';
1694
+ // Head: title + close.
1695
+ var head = el('div'); head.setAttribute('class', 'kb-note-head');
1696
+ head.appendChild(el('h3', nd.title));
1697
+ var close = el('button', '✕'); close.setAttribute('class', 'kb-note-close'); close.setAttribute('title', 'Close');
1698
+ close.addEventListener('click', function(){ selected = null; hideNote(); invalidate(); });
1699
+ head.appendChild(close);
1700
+ elNote.appendChild(head);
1701
+ // Metadata chips.
1702
+ var meta = el('div'); meta.setAttribute('class', 'kb-note-meta');
1703
+ meta.appendChild(chip('type', nd.type || '—', colorFor(nd.type)));
1704
+ meta.appendChild(chip('links', String(nd.degree)));
1705
+ if (nd.confidence !== null && nd.confidence !== undefined) meta.appendChild(chip('confidence', String(nd.confidence)));
1706
+ if (nd.updatedAt) meta.appendChild(chip('updated', nd.updatedAt));
1707
+ if (nd.stale) meta.appendChild(chip('status', 'stale ≥ 90d'));
1708
+ if (nd.contradiction) meta.appendChild(chip('flag', 'contradiction'));
1709
+ meta.appendChild(chip('id', nd.id));
1710
+ elNote.appendChild(meta);
1711
+ // File path line (full location) — filled once the note fetch returns.
1712
+ var pathLine = el('div'); pathLine.setAttribute('class', 'kb-note-path');
1713
+ pathLine.style.display = 'none'; elNote.appendChild(pathLine);
1714
+ // Body — start from the excerpt, then fetch and render the FULL file.
1715
+ var body = el('div'); body.setAttribute('class', 'kb-md');
1716
+ if (nd.excerpt) renderMarkdown(body, nd.excerpt);
1717
+ var loading = el('div', 'Loading full note…'); loading.setAttribute('class', 'kb-note-loading');
1718
+ elNote.appendChild(body); elNote.appendChild(loading);
1719
+ elNote.style.display = '';
1720
+ elNote.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1721
+ // Fetch the whole markdown body for the current source scope.
1722
+ var url = apiUrl('/knowledge/note') + '?scope=' + encodeURIComponent(currentScope()) +
1723
+ '&id=' + encodeURIComponent(nd.id);
1724
+ fetch(url, { headers: { 'Accept': 'application/json' } }).then(function(res){
1725
+ if (res.status === 401){ onUnauthorized(); return null; }
1726
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1727
+ return res.json();
1728
+ }).then(function(data){
1729
+ if (myReq !== noteReq) return; // a newer note was opened — ignore
1730
+ if (loading.parentNode) loading.parentNode.removeChild(loading);
1731
+ if (!data) return;
1732
+ // Header enrichments from the backend: last-modified date + full path.
1733
+ if (typeof data.updated === 'string' && data.updated){
1734
+ meta.appendChild(chip('updated', String(data.updated).slice(0, 10)));
1735
+ }
1736
+ if (typeof data.path === 'string' && data.path){
1737
+ pathLine.textContent = '';
1738
+ pathLine.appendChild(el('span', '📄 ')); // page icon
1739
+ pathLine.appendChild(el('b', data.path));
1740
+ pathLine.style.display = '';
1741
+ }
1742
+ if (typeof data.body === 'string' && data.body.trim()) renderMarkdown(body, data.body);
1743
+ }).catch(function(){
1744
+ if (myReq !== noteReq) return;
1745
+ // Demo/synthetic ids have no file — keep the excerpt, just drop the spinner.
1746
+ if (loading.parentNode) loading.parentNode.removeChild(loading);
1747
+ if (!nd.excerpt) renderMarkdown(body, '(full note unavailable)');
1748
+ });
1749
+ }
1750
+ function hideNote(){ noteReq++; elNote.style.display = 'none'; elNote.textContent = ''; }
1751
+
1752
+ // ---------- Legend + stats ----------
1753
+ function renderLegend(){
1754
+ elLegend.textContent = '';
1755
+ var present = {};
1756
+ G.nodes.forEach(function(o){ present[o.nd.type || 'other'] = true; });
1757
+ Object.keys(present).forEach(function(type){
1758
+ var s = el('span');
1759
+ var dot = el('i'); dot.style.background = colorFor(type === 'other' ? null : type);
1760
+ s.appendChild(dot); s.appendChild(el('span', type));
1761
+ elLegend.appendChild(s);
1762
+ });
1763
+ }
1764
+
1765
+ // ---------- Load (data layer — unchanged) ----------
1766
+ function currentScope(){ return (sourceSel && sourceSel.value) || 'shared'; }
1767
+ // Demo controls only make sense for the Shared KB — an agent's own memory has no
1768
+ // synthetic demo. Hide them when an agent is picked.
1769
+ function syncSourceControls(){
1770
+ var isShared = currentScope() === 'shared';
1771
+ if (demoToggleWrap) demoToggleWrap.style.display = isShared ? '' : 'none';
1772
+ if (demoSizeWrap) demoSizeWrap.style.display = (isShared && demoToggle.checked) ? '' : 'none';
1773
+ if (!isShared) elDemoBadge.style.display = 'none';
1774
+ }
1775
+
1776
+ // Populate the source selector from /knowledge/sources (Shared KB + agents that
1777
+ // have Lane-2 memory notes), preserving the current selection, then load.
1778
+ function loadSources(){
1779
+ fetch(apiUrl('/knowledge/sources'), { headers: { 'Accept': 'application/json' } }).then(function(res){
1780
+ if (res.status === 401) { onUnauthorized(); return null; }
1781
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1782
+ return res.json();
1783
+ }).then(function(data){
1784
+ if (!data || !sourceSel) { loadGraph(); return; }
1785
+ var prev = sourceSel.value || 'shared';
1786
+ sourceSel.textContent = '';
1787
+ (data.sources || [{ id: 'shared', label: 'Shared Knowledge Base', count: 0 }]).forEach(function(s){
1788
+ var o = document.createElement('option');
1789
+ o.value = s.id; o.textContent = s.label + (typeof s.count === 'number' ? ' (' + s.count + ')' : '');
1790
+ sourceSel.appendChild(o);
1791
+ });
1792
+ // Restore prior selection if it still exists, else fall back to Shared KB.
1793
+ sourceSel.value = prev;
1794
+ if (sourceSel.value !== prev) sourceSel.value = 'shared';
1795
+ loadGraph();
1796
+ }).catch(function(){ loadGraph(); }); // selector best-effort — the graph still loads
1797
+ }
1798
+
1799
+ function loadGraph(){
1800
+ syncSourceControls();
1801
+ var scope = currentScope();
1802
+ var q = '';
1803
+ if (scope !== 'shared') {
1804
+ q = '?scope=' + encodeURIComponent(scope);
1805
+ } else if (!demoToggle.checked) {
1806
+ q = '?demo=off';
1807
+ } else if (demoSize && demoSize.value !== '0') {
1808
+ q = '?demo=' + encodeURIComponent(demoSize.value);
1809
+ }
1810
+ var url = apiUrl('/knowledge/graph') + q;
1811
+ elStats.textContent = 'Loading…'; elEmpty.style.display = 'none';
1812
+ fetch(url, { headers: { 'Accept': 'application/json' } }).then(function(res){
1813
+ if (res.status === 401) { onUnauthorized(); return null; }
1814
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1815
+ return res.json();
1816
+ }).then(function(data){
1817
+ if (!data) return;
1818
+ var isDemo = !!data.demo;
1819
+ elDemoBadge.style.display = isDemo ? '' : 'none';
1820
+ if (!data.nodes || data.nodes.length === 0){
1821
+ buildModel({ nodes: [], edges: [] });
1822
+ elStats.textContent = '';
1823
+ elEmpty.textContent = scope === 'shared'
1824
+ ? 'No notes in the shared Knowledge Base yet. Notes appear here once agents promote memories to the shared vault (dreaming auto mode), or enable the demo toggle above.'
1825
+ : 'This agent has no memory notes yet.';
1826
+ elEmpty.style.display = '';
1827
+ elLegend.textContent = '';
1828
+ return;
1829
+ }
1830
+ buildModel(data);
1831
+ renderLegend();
1832
+ elStats.textContent = data.nodes.length + ' notes, ' + data.edges.length + ' links';
1833
+ }).catch(function(err){
1834
+ elStats.textContent = ''; elEmpty.textContent = 'Failed to load graph: ' + err.message; elEmpty.style.display = '';
1835
+ });
1836
+ }
1837
+
1838
+ document.getElementById('kb-refresh').addEventListener('click', loadGraph);
1839
+ if (sourceSel) sourceSel.addEventListener('change', loadGraph);
1840
+ demoToggle.addEventListener('change', loadGraph);
1841
+ if (demoSize) demoSize.addEventListener('change', loadGraph);
1842
+ if (elSearch){
1843
+ elSearch.addEventListener('input', runSearch);
1844
+ elSearch.addEventListener('keydown', function(ev){
1845
+ if (ev.key === 'Enter') focusFirstMatch();
1846
+ else if (ev.key === 'Escape') { elSearch.value = ''; clearSearch(); }
1847
+ });
1848
+ }
1849
+ initInteractions();
1850
+ })();
1851
+ </script>
1852
+
1853
+ <script id="kb-dreams">
1854
+ // Nightly dreaming report renderer. Fetches /knowledge/dreams (parsed audit
1855
+ // trail across all agents), renders a newest-first timeline with per-run
1856
+ // proposals, and an agent filter. All text goes through textContent (no HTML
1857
+ // injection). Uses the global apiUrl/onUnauthorized helpers.
1858
+ (function(){
1859
+ var listEl = document.getElementById('dreams-list');
1860
+ var emptyEl = document.getElementById('dreams-empty');
1861
+ var statsEl = document.getElementById('dreams-stats');
1862
+ var agentSel = document.getElementById('dreams-agent');
1863
+ var refreshBtn = document.getElementById('dreams-refresh');
1864
+ var allRuns = [];
1865
+ var loaded = false;
1866
+ // Notice to show once, after the next render (a reload overwrites statsEl,
1867
+ // so an accept result set directly on statsEl would be lost immediately).
1868
+ var pendingNotice = '';
1869
+
1870
+ function el(tag, cls, text){
1871
+ var e = document.createElement(tag);
1872
+ if (cls) e.className = cls;
1873
+ if (text != null) e.textContent = text;
1874
+ return e;
1875
+ }
1876
+
1877
+ // Apply one or more proposals of a run to the agent's memory via the K4
1878
+ // applier. indexes === null ⇒ accept every pending proposal in the run.
1879
+ function acceptDreams(agent, ts, indexes, btn){
1880
+ var origText = btn ? btn.textContent : '';
1881
+ if (btn){ btn.disabled = true; btn.textContent = 'Applying…'; }
1882
+ var payload = { agentId: agent, ts: ts };
1883
+ if (indexes) payload.indexes = indexes;
1884
+ fetch(apiUrl('/knowledge/dreams/apply'), {
1885
+ method: 'POST',
1886
+ headers: { 'Content-Type': 'application/json' },
1887
+ body: JSON.stringify(payload)
1888
+ }).then(function(res){
1889
+ if (res.status === 401){ onUnauthorized(); return null; }
1890
+ return res.json().then(function(data){ return { ok: res.ok, data: data || {} }; });
1891
+ }).then(function(r){
1892
+ if (!r) return;
1893
+ if (!r.ok){
1894
+ if (btn){ btn.disabled = false; btn.textContent = origText || 'Accept'; }
1895
+ statsEl.textContent = 'Apply failed: ' + (r.data.error || 'error');
1896
+ return;
1897
+ }
1898
+ // Reload so applied proposals show ✓ and any anchor-stale skips stay
1899
+ // pending; the memory files themselves were already updated on disk.
1900
+ // The message is stashed in pendingNotice because loadDreams → render
1901
+ // rewrites statsEl.
1902
+ pendingNotice = 'Applied ' + (r.data.applied || 0)
1903
+ + (r.data.skipped ? ', skipped ' + r.data.skipped + ' (anchor changed / net-negative)' : '')
1904
+ + (r.data.alreadyAccepted ? ', ' + r.data.alreadyAccepted + ' already applied' : '');
1905
+ loadDreams(true);
1906
+ }).catch(function(e){
1907
+ if (btn){ btn.disabled = false; btn.textContent = origText || 'Accept'; }
1908
+ statsEl.textContent = 'Apply error: ' + e.message;
1909
+ });
1910
+ }
1911
+
1912
+ function renderRun(run){
1913
+ var box = el('div', 'dream-run');
1914
+ var head = el('div', 'dream-run-head');
1915
+ if (run.agent) head.appendChild(el('span', 'agent', run.agent));
1916
+ var modeCls = run.mode === 'auto' ? 'auto' : 'propose';
1917
+ head.appendChild(el('span', 'dream-badge ' + modeCls, run.mode));
1918
+ head.appendChild(el('span', 'dream-badge outcome', run.outcome));
1919
+ head.appendChild(el('span', 'when', run.iso));
1920
+
1921
+ // Pending = a propose-run proposal not yet manually accepted. Accept-all
1922
+ // shows only when there is something to apply.
1923
+ var pending = (run.proposals || []).filter(function(p){ return run.mode !== 'auto' && !p.accepted; });
1924
+ if (pending.length){
1925
+ var allBtn = el('button', 'dream-accept-all', 'Accept all (' + pending.length + ')');
1926
+ allBtn.title = 'Apply every pending proposal in this run to memory';
1927
+ allBtn.addEventListener('click', function(){
1928
+ acceptDreams(run.agent, run.ts, pending.map(function(p){ return p.index; }), allBtn);
1929
+ });
1930
+ head.appendChild(allBtn);
1931
+ }
1932
+ box.appendChild(head);
1933
+
1934
+ if (run.summary) box.appendChild(el('div', 'dream-summary', run.summary));
1935
+
1936
+ if (run.proposals && run.proposals.length){
1937
+ var props = el('div', 'dream-props');
1938
+ run.proposals.forEach(function(p){
1939
+ var pe = el('div', 'dream-prop' + (p.accepted ? ' accepted' : ''));
1940
+ var top = el('div');
1941
+ top.appendChild(el('span', 'op ' + (p.op || ''), p.op || '?'));
1942
+ top.appendChild(el('span', 'file', p.file || ''));
1943
+ top.appendChild(el('span', 'score', 'score ' + (p.score != null ? p.score : '?') + ' · recall ' + (p.recallCount != null ? p.recallCount : '?')));
1944
+ pe.appendChild(top);
1945
+ if (p.reason) pe.appendChild(el('div', 'reason', p.reason));
1946
+ if (p.target) pe.appendChild(el('div', 'anchor', 'anchor: ' + p.target));
1947
+ if (p.content) pe.appendChild(el('div', 'content', p.content));
1948
+
1949
+ // Per-proposal status + accept action.
1950
+ var actions = el('div', 'dream-prop-actions');
1951
+ if (p.accepted){
1952
+ actions.appendChild(el('span', 'prop-status applied', '✓ applied'));
1953
+ } else if (run.mode === 'auto'){
1954
+ actions.appendChild(el('span', 'prop-status applied-auto', '✓ applied (auto)'));
1955
+ } else {
1956
+ actions.appendChild(el('span', 'prop-status pending', 'pending'));
1957
+ var accBtn = el('button', 'dream-accept-btn', 'Accept');
1958
+ accBtn.title = 'Apply this proposal to ' + (p.file || 'memory');
1959
+ accBtn.addEventListener('click', function(){
1960
+ acceptDreams(run.agent, run.ts, [p.index], accBtn);
1961
+ });
1962
+ actions.appendChild(accBtn);
1963
+ }
1964
+ pe.appendChild(actions);
1965
+ props.appendChild(pe);
1966
+ });
1967
+ box.appendChild(props);
1968
+ }
1969
+
1970
+ var applied = (run.applied != null) ? (', applied ' + run.applied) : '';
1971
+ box.appendChild(el('div', 'dream-meta', 'tokens ' + (run.tokens||0) + ' · sessions ' + (run.sessions||0) + applied));
1972
+ return box;
1973
+ }
1974
+
1975
+ function render(){
1976
+ var filter = agentSel ? agentSel.value : '';
1977
+ var runs = filter ? allRuns.filter(function(r){ return r.agent === filter; }) : allRuns;
1978
+ listEl.textContent = '';
1979
+ if (!runs.length){
1980
+ emptyEl.textContent = allRuns.length ? 'No dream runs for this agent.' : 'No dream runs yet. The nightly dreaming pass writes here after it first runs (auto or propose mode).';
1981
+ emptyEl.style.display = '';
1982
+ statsEl.textContent = '';
1983
+ return;
1984
+ }
1985
+ emptyEl.style.display = 'none';
1986
+ var pendingTotal = 0;
1987
+ runs.forEach(function(r){
1988
+ if (r.mode === 'auto') return;
1989
+ (r.proposals || []).forEach(function(p){ if (!p.accepted) pendingTotal++; });
1990
+ });
1991
+ statsEl.textContent = runs.length + ' run' + (runs.length === 1 ? '' : 's')
1992
+ + (pendingTotal ? ' · ' + pendingTotal + ' pending' : '');
1993
+ if (pendingNotice){
1994
+ statsEl.textContent += ' — ' + pendingNotice;
1995
+ pendingNotice = '';
1996
+ }
1997
+ runs.forEach(function(r){ listEl.appendChild(renderRun(r)); });
1998
+ }
1999
+
2000
+ function loadDreams(force){
2001
+ if (loaded && !force) return;
2002
+ loaded = true;
2003
+ statsEl.textContent = 'Loading…';
2004
+ fetch(apiUrl('/knowledge/dreams'), { headers: { 'Accept': 'application/json' } }).then(function(res){
2005
+ if (res.status === 401) { onUnauthorized(); return null; }
2006
+ if (!res.ok) throw new Error('HTTP ' + res.status);
2007
+ return res.json();
2008
+ }).then(function(data){
2009
+ if (!data) return;
2010
+ allRuns = data.runs || [];
2011
+ // Populate the agent filter, preserving the current pick.
2012
+ if (agentSel){
2013
+ var prev = agentSel.value;
2014
+ agentSel.textContent = '';
2015
+ agentSel.appendChild(el('option', null, 'All agents')); agentSel.lastChild.value = '';
2016
+ (data.agents || []).forEach(function(a){ var o = el('option', null, a); o.value = a; agentSel.appendChild(o); });
2017
+ agentSel.value = prev; if (agentSel.value !== prev) agentSel.value = '';
2018
+ }
2019
+ render();
2020
+ }).catch(function(err){
2021
+ statsEl.textContent = ''; emptyEl.textContent = 'Failed to load dreams: ' + err.message; emptyEl.style.display = '';
2022
+ });
2023
+ }
2024
+
2025
+ if (agentSel) agentSel.addEventListener('change', render);
2026
+ if (refreshBtn) refreshBtn.addEventListener('click', function(){ loadDreams(true); });
2027
+ // Exposed so the tab switcher (in the kb-graph script) can lazy-load on first open.
2028
+ window.__loadDreams = loadDreams;
2029
+ })();
2030
+ </script>
908
2031
  </body>
909
2032
  </html>`;
910
2033
  }