@oratis/lisa 0.9.0 → 0.11.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 (296) hide show
  1. package/README.md +123 -16
  2. package/README.zh-CN.md +101 -15
  3. package/completions/_lisa +56 -0
  4. package/completions/lisa.bash +35 -1
  5. package/completions/lisa.fish +18 -1
  6. package/dist/advisor/engine.d.ts +7 -0
  7. package/dist/advisor/engine.d.ts.map +1 -1
  8. package/dist/advisor/engine.js +12 -0
  9. package/dist/advisor/engine.js.map +1 -1
  10. package/dist/agent.d.ts +14 -0
  11. package/dist/agent.d.ts.map +1 -1
  12. package/dist/agent.js +58 -7
  13. package/dist/agent.js.map +1 -1
  14. package/dist/agents/managed.d.ts +108 -0
  15. package/dist/agents/managed.d.ts.map +1 -0
  16. package/dist/agents/managed.js +241 -0
  17. package/dist/agents/managed.js.map +1 -0
  18. package/dist/agents/pty.d.ts +155 -0
  19. package/dist/agents/pty.d.ts.map +1 -0
  20. package/dist/agents/pty.js +299 -0
  21. package/dist/agents/pty.js.map +1 -0
  22. package/dist/approval.d.ts +13 -0
  23. package/dist/approval.d.ts.map +1 -1
  24. package/dist/approval.js +25 -2
  25. package/dist/approval.js.map +1 -1
  26. package/dist/autonomy/runs.d.ts +33 -0
  27. package/dist/autonomy/runs.d.ts.map +1 -0
  28. package/dist/autonomy/runs.js +106 -0
  29. package/dist/autonomy/runs.js.map +1 -0
  30. package/dist/channels/feishu.d.ts.map +1 -1
  31. package/dist/channels/feishu.js +65 -0
  32. package/dist/channels/feishu.js.map +1 -1
  33. package/dist/channels/router.d.ts +7 -0
  34. package/dist/channels/router.d.ts.map +1 -1
  35. package/dist/channels/router.js +1 -1
  36. package/dist/channels/router.js.map +1 -1
  37. package/dist/cli/agents-pty.d.ts +18 -0
  38. package/dist/cli/agents-pty.d.ts.map +1 -0
  39. package/dist/cli/agents-pty.js +182 -0
  40. package/dist/cli/agents-pty.js.map +1 -0
  41. package/dist/cli/agents.d.ts +2 -0
  42. package/dist/cli/agents.d.ts.map +1 -0
  43. package/dist/cli/agents.js +65 -0
  44. package/dist/cli/agents.js.map +1 -0
  45. package/dist/cli/consent.d.ts +2 -0
  46. package/dist/cli/consent.d.ts.map +1 -0
  47. package/dist/cli/consent.js +57 -0
  48. package/dist/cli/consent.js.map +1 -0
  49. package/dist/cli/model.d.ts +2 -0
  50. package/dist/cli/model.d.ts.map +1 -0
  51. package/dist/cli/model.js +121 -0
  52. package/dist/cli/model.js.map +1 -0
  53. package/dist/cli/sense.d.ts +2 -0
  54. package/dist/cli/sense.d.ts.map +1 -0
  55. package/dist/cli/sense.js +36 -0
  56. package/dist/cli/sense.js.map +1 -0
  57. package/dist/cli.js +117 -12
  58. package/dist/cli.js.map +1 -1
  59. package/dist/consent/blacklist.d.ts +28 -0
  60. package/dist/consent/blacklist.d.ts.map +1 -0
  61. package/dist/consent/blacklist.js +78 -0
  62. package/dist/consent/blacklist.js.map +1 -0
  63. package/dist/consent/store.d.ts +45 -0
  64. package/dist/consent/store.d.ts.map +1 -0
  65. package/dist/consent/store.js +126 -0
  66. package/dist/consent/store.js.map +1 -0
  67. package/dist/control/policy.d.ts +13 -0
  68. package/dist/control/policy.d.ts.map +1 -0
  69. package/dist/control/policy.js +57 -0
  70. package/dist/control/policy.js.map +1 -0
  71. package/dist/heartbeat/runner.d.ts.map +1 -1
  72. package/dist/heartbeat/runner.js +70 -14
  73. package/dist/heartbeat/runner.js.map +1 -1
  74. package/dist/idle/runner.d.ts.map +1 -1
  75. package/dist/idle/runner.js +87 -17
  76. package/dist/idle/runner.js.map +1 -1
  77. package/dist/integrations/aider/observer.d.ts +21 -1
  78. package/dist/integrations/aider/observer.d.ts.map +1 -1
  79. package/dist/integrations/aider/observer.js +110 -1
  80. package/dist/integrations/aider/observer.js.map +1 -1
  81. package/dist/integrations/claude-code/liveness.d.ts +10 -0
  82. package/dist/integrations/claude-code/liveness.d.ts.map +1 -0
  83. package/dist/integrations/claude-code/liveness.js +62 -0
  84. package/dist/integrations/claude-code/liveness.js.map +1 -0
  85. package/dist/integrations/claude-code/watcher.d.ts +39 -4
  86. package/dist/integrations/claude-code/watcher.d.ts.map +1 -1
  87. package/dist/integrations/claude-code/watcher.js +94 -57
  88. package/dist/integrations/claude-code/watcher.js.map +1 -1
  89. package/dist/integrations/codex/observer.d.ts +19 -6
  90. package/dist/integrations/codex/observer.d.ts.map +1 -1
  91. package/dist/integrations/codex/observer.js +228 -4
  92. package/dist/integrations/codex/observer.js.map +1 -1
  93. package/dist/integrations/dispatch-ledger.d.ts +30 -1
  94. package/dist/integrations/dispatch-ledger.d.ts.map +1 -1
  95. package/dist/integrations/dispatch-ledger.js +75 -9
  96. package/dist/integrations/dispatch-ledger.js.map +1 -1
  97. package/dist/integrations/git/branch.d.ts +11 -0
  98. package/dist/integrations/git/branch.d.ts.map +1 -0
  99. package/dist/integrations/git/branch.js +49 -0
  100. package/dist/integrations/git/branch.js.map +1 -0
  101. package/dist/integrations/git/observer.d.ts +39 -0
  102. package/dist/integrations/git/observer.d.ts.map +1 -0
  103. package/dist/integrations/git/observer.js +252 -0
  104. package/dist/integrations/git/observer.js.map +1 -0
  105. package/dist/integrations/github-pr/observer.d.ts.map +1 -1
  106. package/dist/integrations/github-pr/observer.js +7 -2
  107. package/dist/integrations/github-pr/observer.js.map +1 -1
  108. package/dist/integrations/hub.d.ts +8 -0
  109. package/dist/integrations/hub.d.ts.map +1 -1
  110. package/dist/integrations/hub.js +39 -1
  111. package/dist/integrations/hub.js.map +1 -1
  112. package/dist/integrations/managed/observer.d.ts +17 -0
  113. package/dist/integrations/managed/observer.d.ts.map +1 -0
  114. package/dist/integrations/managed/observer.js +46 -0
  115. package/dist/integrations/managed/observer.js.map +1 -0
  116. package/dist/integrations/opencode/observer.d.ts +53 -5
  117. package/dist/integrations/opencode/observer.d.ts.map +1 -1
  118. package/dist/integrations/opencode/observer.js +242 -20
  119. package/dist/integrations/opencode/observer.js.map +1 -1
  120. package/dist/integrations/pty/observer.d.ts +25 -0
  121. package/dist/integrations/pty/observer.d.ts.map +1 -0
  122. package/dist/integrations/pty/observer.js +48 -0
  123. package/dist/integrations/pty/observer.js.map +1 -0
  124. package/dist/integrations/registry.d.ts.map +1 -1
  125. package/dist/integrations/registry.js +5 -0
  126. package/dist/integrations/registry.js.map +1 -1
  127. package/dist/integrations/shell/observer.d.ts +33 -0
  128. package/dist/integrations/shell/observer.d.ts.map +1 -0
  129. package/dist/integrations/shell/observer.js +223 -0
  130. package/dist/integrations/shell/observer.js.map +1 -0
  131. package/dist/integrations/takoapi/a2a.d.ts +38 -0
  132. package/dist/integrations/takoapi/a2a.d.ts.map +1 -0
  133. package/dist/integrations/takoapi/a2a.js +70 -0
  134. package/dist/integrations/takoapi/a2a.js.map +1 -0
  135. package/dist/integrations/takoapi/ledger.d.ts +32 -0
  136. package/dist/integrations/takoapi/ledger.d.ts.map +1 -0
  137. package/dist/integrations/takoapi/ledger.js +96 -0
  138. package/dist/integrations/takoapi/ledger.js.map +1 -0
  139. package/dist/integrations/takoapi/observer.d.ts +39 -0
  140. package/dist/integrations/takoapi/observer.d.ts.map +1 -0
  141. package/dist/integrations/takoapi/observer.js +102 -0
  142. package/dist/integrations/takoapi/observer.js.map +1 -0
  143. package/dist/integrations/types.d.ts +17 -0
  144. package/dist/integrations/types.d.ts.map +1 -1
  145. package/dist/mcp/client.d.ts +5 -0
  146. package/dist/mcp/client.d.ts.map +1 -1
  147. package/dist/mcp/client.js +1 -1
  148. package/dist/mcp/client.js.map +1 -1
  149. package/dist/memory/embedding.d.ts +45 -0
  150. package/dist/memory/embedding.d.ts.map +1 -0
  151. package/dist/memory/embedding.js +139 -0
  152. package/dist/memory/embedding.js.map +1 -0
  153. package/dist/memory/search_tool.d.ts.map +1 -1
  154. package/dist/memory/search_tool.js +6 -2
  155. package/dist/memory/search_tool.js.map +1 -1
  156. package/dist/memory/vector.d.ts +14 -3
  157. package/dist/memory/vector.d.ts.map +1 -1
  158. package/dist/memory/vector.js +50 -0
  159. package/dist/memory/vector.js.map +1 -1
  160. package/dist/model/local.d.ts +43 -0
  161. package/dist/model/local.d.ts.map +1 -0
  162. package/dist/model/local.js +118 -0
  163. package/dist/model/local.js.map +1 -0
  164. package/dist/model/plan-usage.d.ts +41 -0
  165. package/dist/model/plan-usage.d.ts.map +1 -0
  166. package/dist/model/plan-usage.js +168 -0
  167. package/dist/model/plan-usage.js.map +1 -0
  168. package/dist/model/plans.d.ts +73 -0
  169. package/dist/model/plans.d.ts.map +1 -0
  170. package/dist/model/plans.js +246 -0
  171. package/dist/model/plans.js.map +1 -0
  172. package/dist/orchestrator/recent-recap.d.ts +2 -0
  173. package/dist/orchestrator/recent-recap.d.ts.map +1 -0
  174. package/dist/orchestrator/recent-recap.js +20 -0
  175. package/dist/orchestrator/recent-recap.js.map +1 -0
  176. package/dist/providers/anthropic.d.ts +1 -0
  177. package/dist/providers/anthropic.d.ts.map +1 -1
  178. package/dist/providers/anthropic.js +10 -2
  179. package/dist/providers/anthropic.js.map +1 -1
  180. package/dist/providers/fallback.d.ts +23 -0
  181. package/dist/providers/fallback.d.ts.map +1 -0
  182. package/dist/providers/fallback.js +29 -0
  183. package/dist/providers/fallback.js.map +1 -0
  184. package/dist/providers/gemini.d.ts +2 -0
  185. package/dist/providers/gemini.d.ts.map +1 -1
  186. package/dist/providers/gemini.js +24 -10
  187. package/dist/providers/gemini.js.map +1 -1
  188. package/dist/providers/openai.d.ts.map +1 -1
  189. package/dist/providers/openai.js +4 -1
  190. package/dist/providers/openai.js.map +1 -1
  191. package/dist/providers/registry.d.ts +18 -2
  192. package/dist/providers/registry.d.ts.map +1 -1
  193. package/dist/providers/registry.js +56 -9
  194. package/dist/providers/registry.js.map +1 -1
  195. package/dist/reflect.d.ts +55 -0
  196. package/dist/reflect.d.ts.map +1 -1
  197. package/dist/reflect.js +148 -54
  198. package/dist/reflect.js.map +1 -1
  199. package/dist/sense/log.d.ts +9 -0
  200. package/dist/sense/log.d.ts.map +1 -0
  201. package/dist/sense/log.js +63 -0
  202. package/dist/sense/log.js.map +1 -0
  203. package/dist/sense/screen.d.ts +43 -0
  204. package/dist/sense/screen.d.ts.map +1 -0
  205. package/dist/sense/screen.js +107 -0
  206. package/dist/sense/screen.js.map +1 -0
  207. package/dist/sense/service.d.ts +16 -0
  208. package/dist/sense/service.d.ts.map +1 -0
  209. package/dist/sense/service.js +33 -0
  210. package/dist/sense/service.js.map +1 -0
  211. package/dist/sense/types.d.ts +35 -0
  212. package/dist/sense/types.d.ts.map +1 -0
  213. package/dist/sense/types.js +11 -0
  214. package/dist/sense/types.js.map +1 -0
  215. package/dist/sense/voice.d.ts +28 -0
  216. package/dist/sense/voice.d.ts.map +1 -0
  217. package/dist/sense/voice.js +64 -0
  218. package/dist/sense/voice.js.map +1 -0
  219. package/dist/soul/git.d.ts.map +1 -1
  220. package/dist/soul/git.js +35 -16
  221. package/dist/soul/git.js.map +1 -1
  222. package/dist/soul/store.d.ts +22 -0
  223. package/dist/soul/store.d.ts.map +1 -1
  224. package/dist/soul/store.js +69 -14
  225. package/dist/soul/store.js.map +1 -1
  226. package/dist/soul/summary.d.ts +10 -0
  227. package/dist/soul/summary.d.ts.map +1 -0
  228. package/dist/soul/summary.js +57 -0
  229. package/dist/soul/summary.js.map +1 -0
  230. package/dist/soul/tools.d.ts.map +1 -1
  231. package/dist/soul/tools.js +13 -24
  232. package/dist/soul/tools.js.map +1 -1
  233. package/dist/soul/types.d.ts +9 -0
  234. package/dist/soul/types.d.ts.map +1 -1
  235. package/dist/soul/types.js.map +1 -1
  236. package/dist/subagent.d.ts +7 -0
  237. package/dist/subagent.d.ts.map +1 -1
  238. package/dist/subagent.js +3 -1
  239. package/dist/subagent.js.map +1 -1
  240. package/dist/tools/dispatch_agent.d.ts +13 -1
  241. package/dist/tools/dispatch_agent.d.ts.map +1 -1
  242. package/dist/tools/dispatch_agent.js +56 -12
  243. package/dist/tools/dispatch_agent.js.map +1 -1
  244. package/dist/tools/dispatch_status.d.ts +22 -0
  245. package/dist/tools/dispatch_status.d.ts.map +1 -0
  246. package/dist/tools/dispatch_status.js +57 -0
  247. package/dist/tools/dispatch_status.js.map +1 -0
  248. package/dist/tools/registry.d.ts +29 -0
  249. package/dist/tools/registry.d.ts.map +1 -1
  250. package/dist/tools/registry.js +67 -0
  251. package/dist/tools/registry.js.map +1 -1
  252. package/dist/tools/run_on_plan.d.ts +46 -0
  253. package/dist/tools/run_on_plan.d.ts.map +1 -0
  254. package/dist/tools/run_on_plan.js +89 -0
  255. package/dist/tools/run_on_plan.js.map +1 -0
  256. package/dist/tools/takoapi.d.ts +60 -0
  257. package/dist/tools/takoapi.d.ts.map +1 -0
  258. package/dist/tools/takoapi.js +181 -0
  259. package/dist/tools/takoapi.js.map +1 -0
  260. package/dist/tools/validate.d.ts +22 -0
  261. package/dist/tools/validate.d.ts.map +1 -0
  262. package/dist/tools/validate.js +50 -0
  263. package/dist/tools/validate.js.map +1 -0
  264. package/dist/web/agent-roster.d.ts +56 -0
  265. package/dist/web/agent-roster.d.ts.map +1 -0
  266. package/dist/web/agent-roster.js +109 -0
  267. package/dist/web/agent-roster.js.map +1 -0
  268. package/dist/web/devices.d.ts +27 -0
  269. package/dist/web/devices.d.ts.map +1 -0
  270. package/dist/web/devices.js +113 -0
  271. package/dist/web/devices.js.map +1 -0
  272. package/dist/web/island.d.ts +5 -1
  273. package/dist/web/island.d.ts.map +1 -1
  274. package/dist/web/island.js +410 -63
  275. package/dist/web/island.js.map +1 -1
  276. package/dist/web/lisa-client.d.ts +20 -0
  277. package/dist/web/lisa-client.d.ts.map +1 -0
  278. package/dist/web/lisa-client.js +1323 -0
  279. package/dist/web/lisa-client.js.map +1 -0
  280. package/dist/web/lisa-css.d.ts +13 -0
  281. package/dist/web/lisa-css.d.ts.map +1 -0
  282. package/dist/web/lisa-css.js +1163 -0
  283. package/dist/web/lisa-css.js.map +1 -0
  284. package/dist/web/lisa-html.d.ts +1 -1
  285. package/dist/web/lisa-html.d.ts.map +1 -1
  286. package/dist/web/lisa-html.js +15 -2136
  287. package/dist/web/lisa-html.js.map +1 -1
  288. package/dist/web/push.d.ts +76 -0
  289. package/dist/web/push.d.ts.map +1 -0
  290. package/dist/web/push.js +163 -0
  291. package/dist/web/push.js.map +1 -0
  292. package/dist/web/server.d.ts +19 -0
  293. package/dist/web/server.d.ts.map +1 -1
  294. package/dist/web/server.js +878 -64
  295. package/dist/web/server.js.map +1 -1
  296. package/package.json +5 -2
@@ -7,7 +7,12 @@
7
7
  * unread flag). See docs/MAC_ISLAND_PLAN.md.
8
8
  *
9
9
  * No build step, no framework — single string of inline HTML/CSS/JS.
10
+ *
11
+ * D4a: the multi-agent roster logic (merge/aggregate) lives in agent-roster.ts
12
+ * and is SOURCE-INJECTED into the inline <script> below via `${mergeAgentSession}`
13
+ * so the browser runs the exact unit-tested code instead of a drifting copy.
10
14
  */
15
+ import { mergeAgentSession, aggregateAgentState, rosterLabel, formatActivity } from "./agent-roster.js";
11
16
  export const ISLAND_HTML = `<!doctype html>
12
17
  <html lang="en">
13
18
  <head>
@@ -271,6 +276,24 @@ export const ISLAND_HTML = `<!doctype html>
271
276
  background: rgba(255, 255, 255, 0.05);
272
277
  border: 1px solid rgba(255, 255, 255, 0.07);
273
278
  }
279
+ /* D4a — agent-kind chip (codex / opencode / git / …) so the multi-agent
280
+ roster reads at a glance which tool each row belongs to. */
281
+ #claude-list .agent-badge {
282
+ flex-shrink: 0;
283
+ font-size: 9px;
284
+ font-weight: 600;
285
+ letter-spacing: 0.02em;
286
+ text-transform: lowercase;
287
+ color: var(--accent-claude);
288
+ padding: 1px 6px;
289
+ border-radius: 999px;
290
+ background: rgba(255, 140, 66, 0.12);
291
+ border: 1px solid rgba(255, 140, 66, 0.22);
292
+ max-width: 84px;
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+ white-space: nowrap;
296
+ }
274
297
  #claude-list .empty { padding: 8px 12px; color: var(--fg-faint); font-style: italic; }
275
298
  /* O2 — Tier-2 activity line: what the session is structurally doing. */
276
299
  #claude-list .act {
@@ -353,6 +376,77 @@ export const ISLAND_HTML = `<!doctype html>
353
376
  #claude-list .trail .tdot.error { background: #ff5577; }
354
377
  /* (older stub removed — .head is a real flex strip now, see above) */
355
378
 
379
+ /* ── Sense consent (FOUNDATIONS §1) — "always visible + one-tap stop" ── */
380
+ #sense-list { list-style: none; margin: 6px 0 0; padding: 0; }
381
+ #sense-list li {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 8px;
385
+ padding: 6px 4px;
386
+ font-size: 11.5px;
387
+ }
388
+ #sense-list li + li { border-top: 1px solid rgba(255, 255, 255, 0.05); }
389
+ #sense-list .s-name { font-weight: 600; flex-shrink: 0; }
390
+ #sense-list .s-desc {
391
+ color: var(--fg-faint);
392
+ flex: 1;
393
+ min-width: 0;
394
+ overflow: hidden;
395
+ text-overflow: ellipsis;
396
+ white-space: nowrap;
397
+ font-size: 10px;
398
+ }
399
+ /* Toggle pill: off is muted, on is amber. Click flips it (grant shows the
400
+ description inline so the user sees what they're enabling). */
401
+ #sense-list .s-toggle {
402
+ flex-shrink: 0;
403
+ cursor: pointer;
404
+ font-size: 9px;
405
+ font-weight: 700;
406
+ letter-spacing: 0.04em;
407
+ text-transform: uppercase;
408
+ padding: 2px 8px;
409
+ border-radius: 999px;
410
+ border: 1px solid rgba(255, 255, 255, 0.12);
411
+ background: rgba(255, 255, 255, 0.05);
412
+ color: var(--fg-dim);
413
+ }
414
+ #sense-list .s-toggle.on {
415
+ color: #0b0e18;
416
+ background: var(--accent-warm);
417
+ border-color: var(--accent-warm);
418
+ }
419
+ /* The label glows amber while anything is being sensed. */
420
+ body.sensing #sense-label { color: var(--accent-warm); }
421
+ #sense-stop {
422
+ display: none;
423
+ margin-top: 8px;
424
+ width: 100%;
425
+ padding: 7px 12px;
426
+ border-radius: 10px;
427
+ border: 1px solid rgba(255, 85, 119, 0.35);
428
+ background: rgba(255, 85, 119, 0.12);
429
+ color: #ffd6de;
430
+ font-size: 11px;
431
+ font-weight: 600;
432
+ cursor: pointer;
433
+ }
434
+ #sense-stop:hover { background: rgba(255, 85, 119, 0.2); }
435
+ body.sensing #sense-stop { display: block; }
436
+ /* Live "recently sensed" feed — structural summaries only. */
437
+ #sense-events { list-style: none; margin: 8px 0 0; padding: 0; }
438
+ #sense-events li {
439
+ display: flex;
440
+ gap: 8px;
441
+ align-items: baseline;
442
+ padding: 3px 4px;
443
+ font-size: 10.5px;
444
+ color: var(--fg-dim);
445
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
446
+ }
447
+ #sense-events .se-when { color: var(--fg-faint); flex-shrink: 0; font-variant-numeric: tabular-nums; }
448
+ #sense-events .se-sum { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
449
+
356
450
  /* Phase 3 — notification opt-in chip */
357
451
  #notify-cta {
358
452
  display: none;
@@ -408,6 +502,23 @@ export const ISLAND_HTML = `<!doctype html>
408
502
  body.offline #avatar { filter: grayscale(1); opacity: 0.5; }
409
503
  body.offline #label { color: var(--fg-faint); }
410
504
 
505
+ /* Agents-advisor card (cross-agent suggestions with actions) */
506
+ #advisor-section { display: none; }
507
+ body.has-advisor #advisor-section { display: block; }
508
+ #advisor-list { list-style: none; margin: 0; padding: 0; }
509
+ #advisor-list li { display: flex; flex-direction: column; gap: 4px; padding: 4px 0; }
510
+ #advisor-list .advisor-text { font-size: 11px; line-height: 1.35; }
511
+ #advisor-list .advisor-text.urgent { font-weight: 600; }
512
+ #advisor-list .advisor-actions { display: flex; gap: 6px; }
513
+ #advisor-list .advisor-actions button {
514
+ font: inherit; font-size: 10px; padding: 2px 8px; border-radius: 6px;
515
+ border: 1px solid var(--line, rgba(255,255,255,0.15));
516
+ background: transparent; color: inherit; cursor: pointer;
517
+ }
518
+ #advisor-list .advisor-actions button.primary {
519
+ background: var(--accent, #5b8cff); border-color: transparent; color: #fff;
520
+ }
521
+ #advisor-list .advisor-actions button:hover { filter: brightness(1.1); }
411
522
  /* Screen-advisor suggestion card */
412
523
  #suggestion-section { display: none; }
413
524
  body.has-suggestion #suggestion-section { display: block; }
@@ -444,11 +555,21 @@ export const ISLAND_HTML = `<!doctype html>
444
555
  <div id="suggestion-rationale"></div>
445
556
  <button id="suggestion-act" type="button">Optimize ▸</button>
446
557
  </div>
558
+ <div id="advisor-section">
559
+ <div class="section-label">🛰 across your agents</div>
560
+ <ul id="advisor-list"></ul>
561
+ </div>
447
562
  <div id="claude-section">
448
- <div class="section-label">claude code · <span id="claude-count">0</span> active</div>
563
+ <div class="section-label">agents · <span id="claude-count">0</span> active</div>
449
564
  <ul id="claude-list"></ul>
450
565
  <div id="notify-cta" role="button" tabindex="0">🔔 Notify me when Claude is waiting</div>
451
566
  </div>
567
+ <div id="sense-section">
568
+ <div class="section-label" id="sense-label">👁 sense · <span id="sense-summary">all off</span></div>
569
+ <ul id="sense-list"></ul>
570
+ <button id="sense-stop" type="button">Stop all sensing</button>
571
+ <ul id="sense-events"></ul>
572
+ </div>
452
573
  <div id="actions">
453
574
  <button id="btn-open">Open chat</button>
454
575
  <button id="btn-dismiss" class="muted">Dismiss ★</button>
@@ -457,6 +578,21 @@ export const ISLAND_HTML = `<!doctype html>
457
578
 
458
579
  <script>
459
580
  (() => {
581
+ // ── Multi-agent roster reducer (Dispatch D4a) ──────────────────────
582
+ // Source-injected from agent-roster.ts so the browser runs the exact,
583
+ // unit-tested merge/aggregate logic instead of a hand-maintained copy.
584
+ // These must stay self-contained (no imports / closure refs) — the
585
+ // injection-safety test in agent-roster.test.ts guards that.
586
+ ${mergeAgentSession}
587
+ ${aggregateAgentState}
588
+ ${rosterLabel}
589
+ ${formatActivity}
590
+ // Composite identity for a roster row: the same sessionId seen under two
591
+ // agents (e.g. a git + a shell observer) are DISTINCT rows. Mirrors the
592
+ // key mergeAgentSession uses; the UI keys per-row history, open-state, and
593
+ // notify throttling by it too.
594
+ function agentKey(s) { return (s.agent || 'agent') + '::' + s.sessionId; }
595
+
460
596
  const pill = document.getElementById('pill');
461
597
  const avatar = document.getElementById('avatar');
462
598
  const dot = document.getElementById('dot');
@@ -471,6 +607,10 @@ export const ISLAND_HTML = `<!doctype html>
471
607
  const suggTitle = document.getElementById('suggestion-title');
472
608
  const suggRationale= document.getElementById('suggestion-rationale');
473
609
  const suggAct = document.getElementById('suggestion-act');
610
+ const senseList = document.getElementById('sense-list');
611
+ const senseSummary = document.getElementById('sense-summary');
612
+ const senseStop = document.getElementById('sense-stop');
613
+ const senseEvents = document.getElementById('sense-events');
474
614
  const body = document.body;
475
615
 
476
616
  const state = {
@@ -481,8 +621,11 @@ export const ISLAND_HTML = `<!doctype html>
481
621
  desire: null,
482
622
  thinking: false,
483
623
  dreaming: false,
484
- claudeSessions: [], // [{project, sessionId, lastMtime}, …]
624
+ agentSessions: [], // [{agent, project, sessionId, lastMtime, state, activity}, …] — all agents, not just Claude
485
625
  suggestion: null, // {title, rationale, task, at} from the screen advisor
626
+ advisor: [], // [{id, category, urgency, text, action}] from the cross-agent advisor
627
+ sense: [], // [{signal, granted, grantedAt}] from /api/consent (FOUNDATIONS §1)
628
+ senseEvents: [], // [{signal, summary, ts}] recent ambient events (S2)
486
629
  };
487
630
 
488
631
  // 30-min activity window matches the watcher's ACTIVE_WINDOW_MS.
@@ -555,11 +698,13 @@ export const ISLAND_HTML = `<!doctype html>
555
698
  function maybeNotifyWaiting(prevState, info) {
556
699
  if (info.state !== 'waiting') return;
557
700
  if (prevState === 'waiting') return; // already in this state
558
- const last = lastNotifyAt.get(info.sessionId) || 0;
701
+ const nk = agentKey(info);
702
+ const last = lastNotifyAt.get(nk) || 0;
559
703
  if (Date.now() - last < NOTIFY_THROTTLE_MS) return;
560
- lastNotifyAt.set(info.sessionId, Date.now());
704
+ lastNotifyAt.set(nk, Date.now());
561
705
  const reasonLabel = info.stateReason === 'permission' ? 'needs permission' : 'is waiting';
562
- const title = 'Claude ' + reasonLabel + ' in ' + info.project;
706
+ const who = info.agent && info.agent !== 'claude-code' ? info.agent : 'Claude';
707
+ const title = who + ' ' + reasonLabel + ' in ' + info.project;
563
708
  const bodyText = info.sessionId.slice(0, 8) + ' · click to open Lisa';
564
709
  if (hasBridge) {
565
710
  window.webkit.messageHandlers.island.postMessage({
@@ -583,24 +728,18 @@ export const ISLAND_HTML = `<!doctype html>
583
728
  }
584
729
  function recentSessions() {
585
730
  const cutoff = Date.now() - CLAUDE_ACTIVE_WINDOW_MS;
586
- return state.claudeSessions.filter(
731
+ return state.agentSessions.filter(
587
732
  (s) => new Date(s.lastMtime).getTime() >= cutoff
588
733
  );
589
734
  }
590
735
  /**
591
- * Phase 2: aggregate Claude state for the pill dot. Priority is
592
- * "loudest signal wins": an error anywhere dominates everything;
593
- * otherwise a "waiting" session beats a "working" session (because
594
- * "waiting" means Claude needs the user more attention-worthy
595
- * than "working" which is passive observing).
736
+ * Aggregate state across all agent sessions for the pill dot. Priority is
737
+ * "loudest signal wins": an error anywhere dominates; otherwise "waiting"
738
+ * (an agent needs the user) beats "working" (passive observing). Delegates
739
+ * to the source-injected, unit-tested reducer so the dot and the tests agree.
596
740
  */
597
741
  function aggregateClaudeState() {
598
- const recent = recentSessions();
599
- if (recent.length === 0) return null;
600
- if (recent.some((s) => s.state === 'error')) return 'error';
601
- if (recent.some((s) => s.state === 'waiting')) return 'waiting';
602
- if (recent.some((s) => s.state === 'working')) return 'working';
603
- return null;
742
+ return aggregateAgentState(state.agentSessions, Date.now(), CLAUDE_ACTIVE_WINDOW_MS);
604
743
  }
605
744
 
606
745
  function setAvatar(slug) {
@@ -630,7 +769,7 @@ export const ISLAND_HTML = `<!doctype html>
630
769
  function refreshPanel() {
631
770
  body.classList.toggle('offline', !state.online);
632
771
  body.classList.toggle('has-unread', state.unread);
633
- body.classList.toggle('has-claude', state.claudeSessions.length > 0);
772
+ body.classList.toggle('has-claude', state.agentSessions.length > 0);
634
773
  desireBody.textContent = state.desire || '(nothing actively pursued)';
635
774
  idleBody.textContent = state.idleText || '';
636
775
  btnDismiss.classList.toggle('muted', !state.unread);
@@ -640,7 +779,100 @@ export const ISLAND_HTML = `<!doctype html>
640
779
  suggTitle.textContent = state.suggestion.title || '';
641
780
  suggRationale.textContent = state.suggestion.rationale || '';
642
781
  }
782
+ body.classList.toggle('has-advisor', state.advisor.length > 0);
783
+ renderAdvisorList();
643
784
  renderClaudeList();
785
+ renderSense();
786
+ }
787
+
788
+ // ── Cross-agent advisor card ───────────────────────────────────────
789
+ // Each suggestion gets at most two buttons: the action (never auto-runs —
790
+ // "open" reveals the folder, everything else prefills the chat composer
791
+ // with a concrete ask so the user confirms by sending) and ✕ dismiss
792
+ // (persisted server-side; repeated dismissals down-weight the category).
793
+ function advisorPrefill(s) {
794
+ const a = s.action || {};
795
+ const arg = a.arg || '';
796
+ switch (a.kind) {
797
+ case 'approve':
798
+ return 'One of my agent sessions is waiting for permission: "' + s.text + '". ' +
799
+ 'Inspect it' + (arg ? ' (inspect_agent ' + arg + ')' : '') +
800
+ ' and tell me exactly what it wants to run and whether it looks safe.';
801
+ case 'serialize':
802
+ return 'Two of my agents may be about to collide: "' + s.text + '". ' +
803
+ 'Propose how to serialize them — do not schedule or cancel anything until I confirm.';
804
+ case 'dispatch':
805
+ return s.text + ' — list the pending actionable desires and propose a dispatch plan. ' +
806
+ 'Do not dispatch anything until I confirm.';
807
+ case 'cancel':
808
+ return 'Consider cancelling this: "' + s.text + '". Show me the dispatch details first ' +
809
+ 'and wait for my confirmation.';
810
+ default: // 'look' and anything new
811
+ return 'Look into this for me: "' + s.text + '"' +
812
+ (arg && arg !== 'stuck' ? ' (session ' + arg + ')' : '') + ' — and report back.';
813
+ }
814
+ }
815
+
816
+ function renderAdvisorList() {
817
+ const list = document.getElementById('advisor-list');
818
+ if (!list) return;
819
+ while (list.firstChild) list.removeChild(list.firstChild);
820
+ for (const s of state.advisor.slice(0, 3)) {
821
+ const li = document.createElement('li');
822
+ const text = document.createElement('div');
823
+ text.className = 'advisor-text' + (s.urgency === 'urgent' ? ' urgent' : '');
824
+ text.textContent = (s.urgency === 'urgent' ? '⚠ ' : '') + s.text;
825
+ li.appendChild(text);
826
+ const actions = document.createElement('div');
827
+ actions.className = 'advisor-actions';
828
+ const a = s.action || null;
829
+ if (a && a.kind === 'open' && a.arg && String(a.arg).startsWith('/')) {
830
+ const openBtn = document.createElement('button');
831
+ openBtn.type = 'button';
832
+ openBtn.className = 'primary';
833
+ openBtn.textContent = '📁 ' + (a.label || 'Open');
834
+ openBtn.title = a.arg;
835
+ openBtn.addEventListener('click', (e) => {
836
+ e.stopPropagation();
837
+ if (hasBridge) {
838
+ window.webkit.messageHandlers.island.postMessage({ type: 'open_path', path: a.arg });
839
+ } else {
840
+ navigator.clipboard.writeText(a.arg).catch(() => {});
841
+ }
842
+ });
843
+ actions.appendChild(openBtn);
844
+ } else if (a) {
845
+ const actBtn = document.createElement('button');
846
+ actBtn.type = 'button';
847
+ actBtn.className = 'primary';
848
+ actBtn.textContent = (a.label || 'Ask Lisa') + ' ▸';
849
+ actBtn.title = 'Prefills the chat — nothing runs until you send';
850
+ actBtn.addEventListener('click', (e) => {
851
+ e.stopPropagation();
852
+ openFull(advisorPrefill(s));
853
+ });
854
+ actions.appendChild(actBtn);
855
+ }
856
+ const dismissBtn = document.createElement('button');
857
+ dismissBtn.type = 'button';
858
+ dismissBtn.textContent = '✕';
859
+ dismissBtn.title = 'Dismiss — repeated dismissals teach the advisor to quiet this category';
860
+ dismissBtn.addEventListener('click', async (e) => {
861
+ e.stopPropagation();
862
+ state.advisor = state.advisor.filter((x) => x.id !== s.id);
863
+ refreshPanel();
864
+ try {
865
+ await fetch('/api/advisor/dismiss', {
866
+ method: 'POST',
867
+ headers: { 'content-type': 'application/json' },
868
+ body: JSON.stringify({ id: s.id, category: s.category }),
869
+ });
870
+ } catch (_) {}
871
+ });
872
+ actions.appendChild(dismissBtn);
873
+ li.appendChild(actions);
874
+ list.appendChild(li);
875
+ }
644
876
  }
645
877
 
646
878
  function relativeTime(iso) {
@@ -651,28 +883,9 @@ export const ISLAND_HTML = `<!doctype html>
651
883
  return Math.round(ms / 3600_000) + 'h ago';
652
884
  }
653
885
 
654
- // O2 — compact one-line summary of a session's Tier-2 activity. Structural
655
- // only (tool names, last command, basename of a touched file). Returns ''
656
- // when there's no activity (e.g. visibility=metadata).
657
- function basename(p) {
658
- if (!p) return '';
659
- const parts = String(p).split('/');
660
- return parts[parts.length - 1] || p;
661
- }
662
- function formatActivity(s) {
663
- const a = s.activity;
664
- if (!a) return '';
665
- if (a.pendingPermission) return '⚠ wants to run ' + a.pendingPermission;
666
- const bits = [];
667
- if (a.lastError) bits.push('✗ ' + a.lastError);
668
- if (a.lastCommandName) bits.push('$ ' + a.lastCommandName);
669
- const tool = a.lastTools && a.lastTools.length ? a.lastTools[a.lastTools.length - 1] : '';
670
- const file = a.filesTouched && a.filesTouched.length ? basename(a.filesTouched[a.filesTouched.length - 1]) : '';
671
- if (tool && file) bits.push(tool + ' ' + file);
672
- else if (tool) bits.push(tool);
673
- else if (file) bits.push(file);
674
- return bits.join(' · ');
675
- }
886
+ // O2 — compact one-line Tier-2 activity summary is the source-injected
887
+ // formatActivity (from agent-roster.ts, above) structural only, now also
888
+ // surfacing turn count + tokens. Shared with the GUI sidebar.
676
889
 
677
890
  function renderClaudeList() {
678
891
  const recent = recentSessions();
@@ -692,10 +905,10 @@ export const ISLAND_HTML = `<!doctype html>
692
905
  const rb = stateRank[b.state] ?? 9;
693
906
  if (ra !== rb) return ra - rb;
694
907
  return new Date(b.lastMtime).getTime() - new Date(a.lastMtime).getTime();
695
- }).slice(0, 5);
908
+ }).slice(0, 8);
696
909
  for (const s of rows) {
697
910
  const li = document.createElement('li');
698
- if (rowOpen.has(s.sessionId)) li.classList.add('row-open');
911
+ if (rowOpen.has(agentKey(s))) li.classList.add('row-open');
699
912
  // pip + project + relative-time render as a single horizontal
700
913
  // .head strip. The trail + actions render BELOW the head when
701
914
  // the row is open (li is flex-column).
@@ -705,11 +918,22 @@ export const ISLAND_HTML = `<!doctype html>
705
918
  pip.className = 'pip ' + (s.state || 'unknown');
706
919
  const proj = document.createElement('span');
707
920
  proj.className = 'proj';
708
- proj.textContent = s.project;
921
+ // Label by git branch when available (more meaningful than a worktree
922
+ // hash); the project/cwd stays visible in the row tooltip below.
923
+ proj.textContent = rosterLabel(s);
709
924
  const when = document.createElement('span');
710
925
  when.className = 'when';
711
926
  when.textContent = relativeTime(s.lastMtime);
712
927
  head.appendChild(pip);
928
+ // D4a — agent-kind chip; omitted for plain Claude so existing rows read
929
+ // unchanged, shown for every other agent (codex / opencode / git / …).
930
+ if (s.agent && s.agent !== 'claude-code') {
931
+ const badge = document.createElement('span');
932
+ badge.className = 'agent-badge';
933
+ badge.textContent = s.agent;
934
+ badge.title = s.agent;
935
+ head.appendChild(badge);
936
+ }
713
937
  head.appendChild(proj);
714
938
  head.appendChild(when);
715
939
  li.appendChild(head);
@@ -737,11 +961,13 @@ export const ISLAND_HTML = `<!doctype html>
737
961
  li.appendChild(actions);
738
962
 
739
963
  li.title = s.state + (s.stateReason ? ' (' + s.stateReason + ')' : '')
964
+ + ' · ' + s.project
740
965
  + ' · ' + s.sessionId
741
966
  + '\\nclick: expand timeline · double-click: copy sessionId';
742
967
  li.addEventListener('click', () => {
743
- if (rowOpen.has(s.sessionId)) rowOpen.delete(s.sessionId);
744
- else rowOpen.add(s.sessionId);
968
+ const k = agentKey(s);
969
+ if (rowOpen.has(k)) rowOpen.delete(k);
970
+ else rowOpen.add(k);
745
971
  renderClaudeList();
746
972
  });
747
973
  li.addEventListener('dblclick', async (ev) => {
@@ -804,7 +1030,7 @@ export const ISLAND_HTML = `<!doctype html>
804
1030
  }
805
1031
 
806
1032
  function renderTrail(container, s) {
807
- const h = stateHistory.get(s.sessionId) || [];
1033
+ const h = stateHistory.get(agentKey(s)) || [];
808
1034
  if (h.length === 0) {
809
1035
  container.textContent = '(no transitions recorded yet)';
810
1036
  return;
@@ -993,15 +1219,28 @@ export const ISLAND_HTML = `<!doctype html>
993
1219
  );
994
1220
  }
995
1221
  break;
996
- case 'claude_session_update':
1222
+ case 'advisor_suggestions':
1223
+ state.advisor = Array.isArray(m.suggestions) ? m.suggestions : [];
1224
+ refreshPanel();
1225
+ break;
1226
+ case 'sense_event':
1227
+ // Newest first; cap the in-memory feed.
1228
+ state.senseEvents = [m].concat(state.senseEvents || []).slice(0, 30);
1229
+ renderSenseEvents();
1230
+ break;
1231
+ case 'agent_session_update':
1232
+ // D4a — generalized multi-agent event (claude-code + codex + …).
1233
+ // Payload is the normalized AgentSession (lastMtime is epoch ms;
1234
+ // the reducer's new Date() handles that and the ISO fetch shape).
997
1235
  upsertClaudeSession({
998
- project: m.projectLabel,
999
- projectEncoded: m.projectEncoded,
1236
+ agent: m.agent,
1237
+ project: m.project,
1000
1238
  sessionId: m.sessionId,
1001
- lastMtime: m.ts,
1239
+ lastMtime: m.lastMtime,
1002
1240
  state: m.state || 'unknown',
1003
1241
  stateReason: m.stateReason || '',
1004
1242
  cwd: m.cwd || '',
1243
+ activity: m.activity || null,
1005
1244
  });
1006
1245
  refreshDot();
1007
1246
  refreshPanel();
@@ -1020,16 +1259,17 @@ export const ISLAND_HTML = `<!doctype html>
1020
1259
  // ── Claude Code session helpers ────────────────────────────────────
1021
1260
 
1022
1261
  function upsertClaudeSession(s) {
1023
- const idx = state.claudeSessions.findIndex(
1024
- (x) => x.sessionId === s.sessionId
1262
+ const k = agentKey(s);
1263
+ const prev = state.agentSessions.find((x) => agentKey(x) === k);
1264
+ const prevState = prev ? prev.state : null;
1265
+ // Source-injected reducer: upserts by (agent, sessionId) and prunes the
1266
+ // window in one pass, returning a fresh array.
1267
+ state.agentSessions = mergeAgentSession(
1268
+ state.agentSessions, s, Date.now(), CLAUDE_ACTIVE_WINDOW_MS,
1025
1269
  );
1026
- const prevState = idx >= 0 ? state.claudeSessions[idx].state : null;
1027
- if (idx >= 0) state.claudeSessions[idx] = s;
1028
- else state.claudeSessions.push(s);
1029
- pruneClaudeSessions();
1030
1270
 
1031
1271
  // Phase 3 — record transition + maybe notify on entering "waiting".
1032
- const transitioned = recordStateHistory(s.sessionId, {
1272
+ const transitioned = recordStateHistory(k, {
1033
1273
  state: s.state,
1034
1274
  reason: s.stateReason,
1035
1275
  lastMtime: s.lastMtime,
@@ -1039,31 +1279,33 @@ export const ISLAND_HTML = `<!doctype html>
1039
1279
 
1040
1280
  function pruneClaudeSessions() {
1041
1281
  const cutoff = Date.now() - CLAUDE_ACTIVE_WINDOW_MS;
1042
- state.claudeSessions = state.claudeSessions.filter(
1282
+ state.agentSessions = state.agentSessions.filter(
1043
1283
  (s) => new Date(s.lastMtime).getTime() >= cutoff
1044
1284
  );
1045
1285
  }
1046
1286
 
1047
1287
  async function fetchClaudeSessions() {
1048
1288
  try {
1049
- const r = await fetch('/api/claude/sessions', { cache: 'no-store' });
1289
+ // D4a the multi-agent snapshot (all agents), lastMtime as ISO string.
1290
+ const r = await fetch('/api/agents/sessions', { cache: 'no-store' });
1050
1291
  if (!r.ok) return;
1051
1292
  const j = await r.json();
1052
1293
  if (Array.isArray(j.sessions)) {
1053
- state.claudeSessions = j.sessions.map((s) => ({
1294
+ state.agentSessions = j.sessions.map((s) => ({
1295
+ agent: s.agent,
1054
1296
  project: s.project,
1055
- projectEncoded: s.projectEncoded,
1056
1297
  sessionId: s.sessionId,
1057
1298
  lastMtime: s.lastMtime,
1058
1299
  state: s.state || 'unknown',
1059
1300
  stateReason: s.stateReason || '',
1060
1301
  cwd: s.cwd || '',
1302
+ activity: s.activity || null,
1061
1303
  }));
1062
1304
  // Phase 3 — seed each session's history with its current state
1063
1305
  // so the trail isn't empty on first open. Doesn't notify (no
1064
1306
  // transition implied by initial load).
1065
- for (const s of state.claudeSessions) {
1066
- recordStateHistory(s.sessionId, {
1307
+ for (const s of state.agentSessions) {
1308
+ recordStateHistory(agentKey(s), {
1067
1309
  state: s.state,
1068
1310
  reason: s.stateReason,
1069
1311
  lastMtime: s.lastMtime,
@@ -1075,6 +1317,97 @@ export const ISLAND_HTML = `<!doctype html>
1075
1317
  }
1076
1318
  }
1077
1319
 
1320
+ // ── Sense consent (FOUNDATIONS §1) ─────────────────────────────────
1321
+ // "Always visible + one-tap stop": list each sensitive signal with a
1322
+ // toggle; the label + Stop-all button light up while anything is on.
1323
+ function renderSense() {
1324
+ const grants = state.sense || [];
1325
+ const onCount = grants.filter((g) => g.granted).length;
1326
+ senseSummary.textContent = onCount > 0 ? (onCount + ' on') : 'all off';
1327
+ body.classList.toggle('sensing', onCount > 0);
1328
+ while (senseList.firstChild) senseList.removeChild(senseList.firstChild);
1329
+ for (const g of grants) {
1330
+ const li = document.createElement('li');
1331
+ const name = document.createElement('span');
1332
+ name.className = 's-name';
1333
+ name.textContent = g.signal;
1334
+ const desc = document.createElement('span');
1335
+ desc.className = 's-desc';
1336
+ desc.textContent = g.description || '';
1337
+ desc.title = g.description || '';
1338
+ const toggle = document.createElement('span');
1339
+ toggle.className = 's-toggle' + (g.granted ? ' on' : '');
1340
+ toggle.textContent = g.granted ? 'on' : 'off';
1341
+ toggle.setAttribute('role', 'button');
1342
+ toggle.tabIndex = 0;
1343
+ toggle.addEventListener('click', (e) => {
1344
+ e.stopPropagation();
1345
+ setSenseSignal(g.signal, !g.granted);
1346
+ });
1347
+ li.appendChild(name);
1348
+ li.appendChild(desc);
1349
+ li.appendChild(toggle);
1350
+ senseList.appendChild(li);
1351
+ }
1352
+ }
1353
+
1354
+ function applyConsent(j) {
1355
+ if (j && Array.isArray(j.grants)) { state.sense = j.grants; renderSense(); }
1356
+ }
1357
+
1358
+ async function fetchConsent() {
1359
+ try {
1360
+ const r = await fetch('/api/consent', { cache: 'no-store' });
1361
+ if (r.ok) applyConsent(await r.json());
1362
+ } catch (_) { /* older server without /api/consent — silent */ }
1363
+ }
1364
+
1365
+ async function setSenseSignal(signal, on) {
1366
+ try {
1367
+ const r = await fetch('/api/consent/' + (on ? 'grant' : 'revoke'), {
1368
+ method: 'POST',
1369
+ headers: { 'content-type': 'application/json' },
1370
+ body: JSON.stringify({ signal: signal }),
1371
+ });
1372
+ if (r.ok) applyConsent(await r.json());
1373
+ } catch (_) {}
1374
+ }
1375
+
1376
+ async function stopAllSensing() {
1377
+ try {
1378
+ const r = await fetch('/api/consent/revoke-all', { method: 'POST' });
1379
+ if (r.ok) applyConsent(await r.json());
1380
+ } catch (_) {}
1381
+ }
1382
+ senseStop.addEventListener('click', (e) => { e.stopPropagation(); stopAllSensing(); });
1383
+
1384
+ // Recently-sensed feed (structural summaries only).
1385
+ function renderSenseEvents() {
1386
+ while (senseEvents.firstChild) senseEvents.removeChild(senseEvents.firstChild);
1387
+ for (const e of (state.senseEvents || []).slice(0, 8)) {
1388
+ const li = document.createElement('li');
1389
+ const when = document.createElement('span');
1390
+ when.className = 'se-when';
1391
+ when.textContent = relativeTime(e.ts);
1392
+ const sum = document.createElement('span');
1393
+ sum.className = 'se-sum';
1394
+ sum.textContent = '[' + (e.signal || '?') + '] ' + (e.summary || '');
1395
+ sum.title = sum.textContent;
1396
+ li.appendChild(when);
1397
+ li.appendChild(sum);
1398
+ senseEvents.appendChild(li);
1399
+ }
1400
+ }
1401
+
1402
+ async function fetchSenseEvents() {
1403
+ try {
1404
+ const r = await fetch('/api/sense/recent', { cache: 'no-store' });
1405
+ if (!r.ok) return;
1406
+ const j = await r.json();
1407
+ if (Array.isArray(j.events)) { state.senseEvents = j.events; renderSenseEvents(); }
1408
+ } catch (_) { /* older server without the endpoint — silent */ }
1409
+ }
1410
+
1078
1411
  // Phase 3 — notification permission opt-in. The CTA is only visible
1079
1412
  // when permission is in the default (un-asked) state.
1080
1413
  notifyCta.addEventListener('click', (e) => {
@@ -1089,6 +1422,8 @@ export const ISLAND_HTML = `<!doctype html>
1089
1422
  setAvatar('neutral');
1090
1423
  pollPing();
1091
1424
  fetchClaudeSessions().then(() => { refreshDot(); refreshPanel(); });
1425
+ fetchConsent();
1426
+ fetchSenseEvents();
1092
1427
  subscribe();
1093
1428
  refreshNotifyCta();
1094
1429
  // A fresh island picks up the most recent screen-advisor suggestion (if the
@@ -1097,10 +1432,22 @@ export const ISLAND_HTML = `<!doctype html>
1097
1432
  .then((r) => r.ok ? r.json() : null)
1098
1433
  .then((j) => { if (j && j.suggestion) { state.suggestion = j.suggestion; refreshPanel(); } })
1099
1434
  .catch(() => {});
1435
+ // …and the latest cross-agent advisor suggestions.
1436
+ fetch('/api/advisor/latest', { cache: 'no-store' })
1437
+ .then((r) => r.ok ? r.json() : null)
1438
+ .then((j) => {
1439
+ if (j && Array.isArray(j.suggestions) && j.suggestions.length) {
1440
+ state.advisor = j.suggestions;
1441
+ refreshPanel();
1442
+ }
1443
+ })
1444
+ .catch(() => {});
1100
1445
  // Lightweight resync — covers cases where SSE silently disconnected
1101
1446
  // (laptop sleep, network blip) and we need to refresh state.
1102
1447
  setInterval(pollPing, 30_000);
1103
1448
  setInterval(fetchClaudeSessions, 60_000);
1449
+ // Pick up consent changes made elsewhere (e.g. the lisa consent CLI).
1450
+ setInterval(fetchConsent, 30_000);
1104
1451
  // Re-render every 15s so "Xs ago" / "Xm ago" labels stay fresh and
1105
1452
  // stale sessions fall off the list without a fresh update event.
1106
1453
  setInterval(() => {