@oratis/lisa 0.8.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 (320) 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/mcp/config.d.ts +4 -0
  150. package/dist/mcp/config.d.ts.map +1 -1
  151. package/dist/mcp/config.js +34 -0
  152. package/dist/mcp/config.js.map +1 -1
  153. package/dist/memory/embedding.d.ts +45 -0
  154. package/dist/memory/embedding.d.ts.map +1 -0
  155. package/dist/memory/embedding.js +139 -0
  156. package/dist/memory/embedding.js.map +1 -0
  157. package/dist/memory/search_tool.d.ts.map +1 -1
  158. package/dist/memory/search_tool.js +6 -2
  159. package/dist/memory/search_tool.js.map +1 -1
  160. package/dist/memory/vector.d.ts +14 -3
  161. package/dist/memory/vector.d.ts.map +1 -1
  162. package/dist/memory/vector.js +50 -0
  163. package/dist/memory/vector.js.map +1 -1
  164. package/dist/model/local.d.ts +43 -0
  165. package/dist/model/local.d.ts.map +1 -0
  166. package/dist/model/local.js +118 -0
  167. package/dist/model/local.js.map +1 -0
  168. package/dist/model/plan-usage.d.ts +41 -0
  169. package/dist/model/plan-usage.d.ts.map +1 -0
  170. package/dist/model/plan-usage.js +168 -0
  171. package/dist/model/plan-usage.js.map +1 -0
  172. package/dist/model/plans.d.ts +73 -0
  173. package/dist/model/plans.d.ts.map +1 -0
  174. package/dist/model/plans.js +246 -0
  175. package/dist/model/plans.js.map +1 -0
  176. package/dist/orchestrator/recent-recap.d.ts +2 -0
  177. package/dist/orchestrator/recent-recap.d.ts.map +1 -0
  178. package/dist/orchestrator/recent-recap.js +20 -0
  179. package/dist/orchestrator/recent-recap.js.map +1 -0
  180. package/dist/providers/anthropic.d.ts +1 -0
  181. package/dist/providers/anthropic.d.ts.map +1 -1
  182. package/dist/providers/anthropic.js +10 -2
  183. package/dist/providers/anthropic.js.map +1 -1
  184. package/dist/providers/fallback.d.ts +23 -0
  185. package/dist/providers/fallback.d.ts.map +1 -0
  186. package/dist/providers/fallback.js +29 -0
  187. package/dist/providers/fallback.js.map +1 -0
  188. package/dist/providers/gemini.d.ts +2 -0
  189. package/dist/providers/gemini.d.ts.map +1 -1
  190. package/dist/providers/gemini.js +24 -10
  191. package/dist/providers/gemini.js.map +1 -1
  192. package/dist/providers/openai.d.ts.map +1 -1
  193. package/dist/providers/openai.js +4 -1
  194. package/dist/providers/openai.js.map +1 -1
  195. package/dist/providers/registry.d.ts +18 -2
  196. package/dist/providers/registry.d.ts.map +1 -1
  197. package/dist/providers/registry.js +56 -9
  198. package/dist/providers/registry.js.map +1 -1
  199. package/dist/reflect.d.ts +55 -0
  200. package/dist/reflect.d.ts.map +1 -1
  201. package/dist/reflect.js +148 -54
  202. package/dist/reflect.js.map +1 -1
  203. package/dist/sense/log.d.ts +9 -0
  204. package/dist/sense/log.d.ts.map +1 -0
  205. package/dist/sense/log.js +63 -0
  206. package/dist/sense/log.js.map +1 -0
  207. package/dist/sense/screen.d.ts +43 -0
  208. package/dist/sense/screen.d.ts.map +1 -0
  209. package/dist/sense/screen.js +107 -0
  210. package/dist/sense/screen.js.map +1 -0
  211. package/dist/sense/service.d.ts +16 -0
  212. package/dist/sense/service.d.ts.map +1 -0
  213. package/dist/sense/service.js +33 -0
  214. package/dist/sense/service.js.map +1 -0
  215. package/dist/sense/types.d.ts +35 -0
  216. package/dist/sense/types.d.ts.map +1 -0
  217. package/dist/sense/types.js +11 -0
  218. package/dist/sense/types.js.map +1 -0
  219. package/dist/sense/voice.d.ts +28 -0
  220. package/dist/sense/voice.d.ts.map +1 -0
  221. package/dist/sense/voice.js +64 -0
  222. package/dist/sense/voice.js.map +1 -0
  223. package/dist/soul/git.d.ts.map +1 -1
  224. package/dist/soul/git.js +35 -16
  225. package/dist/soul/git.js.map +1 -1
  226. package/dist/soul/store.d.ts +22 -0
  227. package/dist/soul/store.d.ts.map +1 -1
  228. package/dist/soul/store.js +69 -14
  229. package/dist/soul/store.js.map +1 -1
  230. package/dist/soul/summary.d.ts +10 -0
  231. package/dist/soul/summary.d.ts.map +1 -0
  232. package/dist/soul/summary.js +57 -0
  233. package/dist/soul/summary.js.map +1 -0
  234. package/dist/soul/tools.d.ts.map +1 -1
  235. package/dist/soul/tools.js +13 -24
  236. package/dist/soul/tools.js.map +1 -1
  237. package/dist/soul/types.d.ts +9 -0
  238. package/dist/soul/types.d.ts.map +1 -1
  239. package/dist/soul/types.js.map +1 -1
  240. package/dist/subagent.d.ts +7 -0
  241. package/dist/subagent.d.ts.map +1 -1
  242. package/dist/subagent.js +3 -1
  243. package/dist/subagent.js.map +1 -1
  244. package/dist/tools/dispatch_agent.d.ts +13 -1
  245. package/dist/tools/dispatch_agent.d.ts.map +1 -1
  246. package/dist/tools/dispatch_agent.js +56 -12
  247. package/dist/tools/dispatch_agent.js.map +1 -1
  248. package/dist/tools/dispatch_status.d.ts +22 -0
  249. package/dist/tools/dispatch_status.d.ts.map +1 -0
  250. package/dist/tools/dispatch_status.js +57 -0
  251. package/dist/tools/dispatch_status.js.map +1 -0
  252. package/dist/tools/github.d.ts +34 -0
  253. package/dist/tools/github.d.ts.map +1 -0
  254. package/dist/tools/github.js +140 -0
  255. package/dist/tools/github.js.map +1 -0
  256. package/dist/tools/github_link.d.ts +36 -0
  257. package/dist/tools/github_link.d.ts.map +1 -0
  258. package/dist/tools/github_link.js +127 -0
  259. package/dist/tools/github_link.js.map +1 -0
  260. package/dist/tools/mcp.d.ts +28 -0
  261. package/dist/tools/mcp.d.ts.map +1 -0
  262. package/dist/tools/mcp.js +55 -0
  263. package/dist/tools/mcp.js.map +1 -0
  264. package/dist/tools/npm_info.d.ts +21 -0
  265. package/dist/tools/npm_info.d.ts.map +1 -0
  266. package/dist/tools/npm_info.js +102 -0
  267. package/dist/tools/npm_info.js.map +1 -0
  268. package/dist/tools/registry.d.ts +29 -0
  269. package/dist/tools/registry.d.ts.map +1 -1
  270. package/dist/tools/registry.js +75 -0
  271. package/dist/tools/registry.js.map +1 -1
  272. package/dist/tools/run_on_plan.d.ts +46 -0
  273. package/dist/tools/run_on_plan.d.ts.map +1 -0
  274. package/dist/tools/run_on_plan.js +89 -0
  275. package/dist/tools/run_on_plan.js.map +1 -0
  276. package/dist/tools/takoapi.d.ts +60 -0
  277. package/dist/tools/takoapi.d.ts.map +1 -0
  278. package/dist/tools/takoapi.js +181 -0
  279. package/dist/tools/takoapi.js.map +1 -0
  280. package/dist/tools/validate.d.ts +22 -0
  281. package/dist/tools/validate.d.ts.map +1 -0
  282. package/dist/tools/validate.js +50 -0
  283. package/dist/tools/validate.js.map +1 -0
  284. package/dist/voice/dictation.d.ts +51 -0
  285. package/dist/voice/dictation.d.ts.map +1 -0
  286. package/dist/voice/dictation.js +70 -0
  287. package/dist/voice/dictation.js.map +1 -0
  288. package/dist/web/agent-roster.d.ts +56 -0
  289. package/dist/web/agent-roster.d.ts.map +1 -0
  290. package/dist/web/agent-roster.js +109 -0
  291. package/dist/web/agent-roster.js.map +1 -0
  292. package/dist/web/devices.d.ts +27 -0
  293. package/dist/web/devices.d.ts.map +1 -0
  294. package/dist/web/devices.js +113 -0
  295. package/dist/web/devices.js.map +1 -0
  296. package/dist/web/island.d.ts +5 -1
  297. package/dist/web/island.d.ts.map +1 -1
  298. package/dist/web/island.js +427 -67
  299. package/dist/web/island.js.map +1 -1
  300. package/dist/web/lisa-client.d.ts +20 -0
  301. package/dist/web/lisa-client.d.ts.map +1 -0
  302. package/dist/web/lisa-client.js +1323 -0
  303. package/dist/web/lisa-client.js.map +1 -0
  304. package/dist/web/lisa-css.d.ts +13 -0
  305. package/dist/web/lisa-css.d.ts.map +1 -0
  306. package/dist/web/lisa-css.js +1163 -0
  307. package/dist/web/lisa-css.js.map +1 -0
  308. package/dist/web/lisa-html.d.ts +1 -1
  309. package/dist/web/lisa-html.d.ts.map +1 -1
  310. package/dist/web/lisa-html.js +16 -2091
  311. package/dist/web/lisa-html.js.map +1 -1
  312. package/dist/web/push.d.ts +76 -0
  313. package/dist/web/push.d.ts.map +1 -0
  314. package/dist/web/push.js +163 -0
  315. package/dist/web/push.js.map +1 -0
  316. package/dist/web/server.d.ts +19 -0
  317. package/dist/web/server.d.ts.map +1 -1
  318. package/dist/web/server.js +898 -65
  319. package/dist/web/server.js.map +1 -1
  320. 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>
@@ -125,8 +130,10 @@ export const ISLAND_HTML = `<!doctype html>
125
130
  #dot.unread { background: var(--accent-warm); }
126
131
  /* Phase 2: pill dot reflects the strongest signal across all
127
132
  Claude sessions. */
128
- #dot.claude-working { background: var(--accent-claude); animation: pulse 1.8s ease-in-out infinite; }
129
- #dot.claude-waiting { background: var(--accent-claude); } /* solid "needs you" */
133
+ /* working = a calm slow breathe (it's running, not actionable);
134
+ waiting = solid + a soft halo that draws the eye ("needs you"). */
135
+ #dot.claude-working { background: var(--accent-claude); opacity: 1; animation: breathe 2.6s ease-in-out infinite; }
136
+ #dot.claude-waiting { background: var(--accent-claude); opacity: 1; animation: needsYou 2s ease-in-out infinite; }
130
137
  #dot.claude-error { background: #ff5577; animation: pulse 0.8s ease-in-out infinite; }
131
138
  #dot.offline { background: var(--fg-faint); }
132
139
 
@@ -134,6 +141,17 @@ export const ISLAND_HTML = `<!doctype html>
134
141
  0%, 100% { opacity: 0.35; }
135
142
  50% { opacity: 1; }
136
143
  }
144
+ /* Gentle breathing for "working" — present but not jumpy. */
145
+ @keyframes breathe {
146
+ 0%, 100% { opacity: 0.55; }
147
+ 50% { opacity: 1; }
148
+ }
149
+ /* "needs you" — solid dot with a pulsing warm halo, more prominent than
150
+ working without the harsh on/off flash. */
151
+ @keyframes needsYou {
152
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 140, 66, 0); }
153
+ 50% { box-shadow: 0 0 7px 2px rgba(255, 140, 66, 0.65); }
154
+ }
137
155
 
138
156
  /* Expanded panel — appears below the pill on hover/click.
139
157
  The native LisaIsland.app window is a fixed 360×440 pt; the pill
@@ -258,6 +276,24 @@ export const ISLAND_HTML = `<!doctype html>
258
276
  background: rgba(255, 255, 255, 0.05);
259
277
  border: 1px solid rgba(255, 255, 255, 0.07);
260
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
+ }
261
297
  #claude-list .empty { padding: 8px 12px; color: var(--fg-faint); font-style: italic; }
262
298
  /* O2 — Tier-2 activity line: what the session is structurally doing. */
263
299
  #claude-list .act {
@@ -286,8 +322,8 @@ export const ISLAND_HTML = `<!doctype html>
286
322
  background: var(--fg-faint);
287
323
  margin-right: 4px;
288
324
  }
289
- #claude-list .pip.working { background: var(--accent-claude); animation: pulse 1.8s ease-in-out infinite; }
290
- #claude-list .pip.waiting { background: var(--accent-claude); }
325
+ #claude-list .pip.working { background: var(--accent-claude); opacity: 1; animation: breathe 2.6s ease-in-out infinite; }
326
+ #claude-list .pip.waiting { background: var(--accent-claude); opacity: 1; animation: needsYou 2s ease-in-out infinite; }
291
327
  #claude-list .pip.error { background: #ff5577; }
292
328
  #claude-list .pip.unknown { background: var(--fg-faint); }
293
329
 
@@ -340,6 +376,77 @@ export const ISLAND_HTML = `<!doctype html>
340
376
  #claude-list .trail .tdot.error { background: #ff5577; }
341
377
  /* (older stub removed — .head is a real flex strip now, see above) */
342
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
+
343
450
  /* Phase 3 — notification opt-in chip */
344
451
  #notify-cta {
345
452
  display: none;
@@ -395,6 +502,23 @@ export const ISLAND_HTML = `<!doctype html>
395
502
  body.offline #avatar { filter: grayscale(1); opacity: 0.5; }
396
503
  body.offline #label { color: var(--fg-faint); }
397
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); }
398
522
  /* Screen-advisor suggestion card */
399
523
  #suggestion-section { display: none; }
400
524
  body.has-suggestion #suggestion-section { display: block; }
@@ -431,11 +555,21 @@ export const ISLAND_HTML = `<!doctype html>
431
555
  <div id="suggestion-rationale"></div>
432
556
  <button id="suggestion-act" type="button">Optimize ▸</button>
433
557
  </div>
558
+ <div id="advisor-section">
559
+ <div class="section-label">🛰 across your agents</div>
560
+ <ul id="advisor-list"></ul>
561
+ </div>
434
562
  <div id="claude-section">
435
- <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>
436
564
  <ul id="claude-list"></ul>
437
565
  <div id="notify-cta" role="button" tabindex="0">🔔 Notify me when Claude is waiting</div>
438
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>
439
573
  <div id="actions">
440
574
  <button id="btn-open">Open chat</button>
441
575
  <button id="btn-dismiss" class="muted">Dismiss ★</button>
@@ -444,6 +578,21 @@ export const ISLAND_HTML = `<!doctype html>
444
578
 
445
579
  <script>
446
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
+
447
596
  const pill = document.getElementById('pill');
448
597
  const avatar = document.getElementById('avatar');
449
598
  const dot = document.getElementById('dot');
@@ -458,6 +607,10 @@ export const ISLAND_HTML = `<!doctype html>
458
607
  const suggTitle = document.getElementById('suggestion-title');
459
608
  const suggRationale= document.getElementById('suggestion-rationale');
460
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');
461
614
  const body = document.body;
462
615
 
463
616
  const state = {
@@ -468,8 +621,11 @@ export const ISLAND_HTML = `<!doctype html>
468
621
  desire: null,
469
622
  thinking: false,
470
623
  dreaming: false,
471
- claudeSessions: [], // [{project, sessionId, lastMtime}, …]
624
+ agentSessions: [], // [{agent, project, sessionId, lastMtime, state, activity}, …] — all agents, not just Claude
472
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)
473
629
  };
474
630
 
475
631
  // 30-min activity window matches the watcher's ACTIVE_WINDOW_MS.
@@ -542,11 +698,13 @@ export const ISLAND_HTML = `<!doctype html>
542
698
  function maybeNotifyWaiting(prevState, info) {
543
699
  if (info.state !== 'waiting') return;
544
700
  if (prevState === 'waiting') return; // already in this state
545
- const last = lastNotifyAt.get(info.sessionId) || 0;
701
+ const nk = agentKey(info);
702
+ const last = lastNotifyAt.get(nk) || 0;
546
703
  if (Date.now() - last < NOTIFY_THROTTLE_MS) return;
547
- lastNotifyAt.set(info.sessionId, Date.now());
704
+ lastNotifyAt.set(nk, Date.now());
548
705
  const reasonLabel = info.stateReason === 'permission' ? 'needs permission' : 'is waiting';
549
- 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;
550
708
  const bodyText = info.sessionId.slice(0, 8) + ' · click to open Lisa';
551
709
  if (hasBridge) {
552
710
  window.webkit.messageHandlers.island.postMessage({
@@ -570,24 +728,18 @@ export const ISLAND_HTML = `<!doctype html>
570
728
  }
571
729
  function recentSessions() {
572
730
  const cutoff = Date.now() - CLAUDE_ACTIVE_WINDOW_MS;
573
- return state.claudeSessions.filter(
731
+ return state.agentSessions.filter(
574
732
  (s) => new Date(s.lastMtime).getTime() >= cutoff
575
733
  );
576
734
  }
577
735
  /**
578
- * Phase 2: aggregate Claude state for the pill dot. Priority is
579
- * "loudest signal wins": an error anywhere dominates everything;
580
- * otherwise a "waiting" session beats a "working" session (because
581
- * "waiting" means Claude needs the user more attention-worthy
582
- * 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.
583
740
  */
584
741
  function aggregateClaudeState() {
585
- const recent = recentSessions();
586
- if (recent.length === 0) return null;
587
- if (recent.some((s) => s.state === 'error')) return 'error';
588
- if (recent.some((s) => s.state === 'waiting')) return 'waiting';
589
- if (recent.some((s) => s.state === 'working')) return 'working';
590
- return null;
742
+ return aggregateAgentState(state.agentSessions, Date.now(), CLAUDE_ACTIVE_WINDOW_MS);
591
743
  }
592
744
 
593
745
  function setAvatar(slug) {
@@ -617,7 +769,7 @@ export const ISLAND_HTML = `<!doctype html>
617
769
  function refreshPanel() {
618
770
  body.classList.toggle('offline', !state.online);
619
771
  body.classList.toggle('has-unread', state.unread);
620
- body.classList.toggle('has-claude', state.claudeSessions.length > 0);
772
+ body.classList.toggle('has-claude', state.agentSessions.length > 0);
621
773
  desireBody.textContent = state.desire || '(nothing actively pursued)';
622
774
  idleBody.textContent = state.idleText || '';
623
775
  btnDismiss.classList.toggle('muted', !state.unread);
@@ -627,7 +779,100 @@ export const ISLAND_HTML = `<!doctype html>
627
779
  suggTitle.textContent = state.suggestion.title || '';
628
780
  suggRationale.textContent = state.suggestion.rationale || '';
629
781
  }
782
+ body.classList.toggle('has-advisor', state.advisor.length > 0);
783
+ renderAdvisorList();
630
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
+ }
631
876
  }
632
877
 
633
878
  function relativeTime(iso) {
@@ -638,28 +883,9 @@ export const ISLAND_HTML = `<!doctype html>
638
883
  return Math.round(ms / 3600_000) + 'h ago';
639
884
  }
640
885
 
641
- // O2 — compact one-line summary of a session's Tier-2 activity. Structural
642
- // only (tool names, last command, basename of a touched file). Returns ''
643
- // when there's no activity (e.g. visibility=metadata).
644
- function basename(p) {
645
- if (!p) return '';
646
- const parts = String(p).split('/');
647
- return parts[parts.length - 1] || p;
648
- }
649
- function formatActivity(s) {
650
- const a = s.activity;
651
- if (!a) return '';
652
- if (a.pendingPermission) return '⚠ wants to run ' + a.pendingPermission;
653
- const bits = [];
654
- if (a.lastError) bits.push('✗ ' + a.lastError);
655
- if (a.lastCommandName) bits.push('$ ' + a.lastCommandName);
656
- const tool = a.lastTools && a.lastTools.length ? a.lastTools[a.lastTools.length - 1] : '';
657
- const file = a.filesTouched && a.filesTouched.length ? basename(a.filesTouched[a.filesTouched.length - 1]) : '';
658
- if (tool && file) bits.push(tool + ' ' + file);
659
- else if (tool) bits.push(tool);
660
- else if (file) bits.push(file);
661
- return bits.join(' · ');
662
- }
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.
663
889
 
664
890
  function renderClaudeList() {
665
891
  const recent = recentSessions();
@@ -679,10 +905,10 @@ export const ISLAND_HTML = `<!doctype html>
679
905
  const rb = stateRank[b.state] ?? 9;
680
906
  if (ra !== rb) return ra - rb;
681
907
  return new Date(b.lastMtime).getTime() - new Date(a.lastMtime).getTime();
682
- }).slice(0, 5);
908
+ }).slice(0, 8);
683
909
  for (const s of rows) {
684
910
  const li = document.createElement('li');
685
- if (rowOpen.has(s.sessionId)) li.classList.add('row-open');
911
+ if (rowOpen.has(agentKey(s))) li.classList.add('row-open');
686
912
  // pip + project + relative-time render as a single horizontal
687
913
  // .head strip. The trail + actions render BELOW the head when
688
914
  // the row is open (li is flex-column).
@@ -692,11 +918,22 @@ export const ISLAND_HTML = `<!doctype html>
692
918
  pip.className = 'pip ' + (s.state || 'unknown');
693
919
  const proj = document.createElement('span');
694
920
  proj.className = 'proj';
695
- 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);
696
924
  const when = document.createElement('span');
697
925
  when.className = 'when';
698
926
  when.textContent = relativeTime(s.lastMtime);
699
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
+ }
700
937
  head.appendChild(proj);
701
938
  head.appendChild(when);
702
939
  li.appendChild(head);
@@ -724,11 +961,13 @@ export const ISLAND_HTML = `<!doctype html>
724
961
  li.appendChild(actions);
725
962
 
726
963
  li.title = s.state + (s.stateReason ? ' (' + s.stateReason + ')' : '')
964
+ + ' · ' + s.project
727
965
  + ' · ' + s.sessionId
728
966
  + '\\nclick: expand timeline · double-click: copy sessionId';
729
967
  li.addEventListener('click', () => {
730
- if (rowOpen.has(s.sessionId)) rowOpen.delete(s.sessionId);
731
- else rowOpen.add(s.sessionId);
968
+ const k = agentKey(s);
969
+ if (rowOpen.has(k)) rowOpen.delete(k);
970
+ else rowOpen.add(k);
732
971
  renderClaudeList();
733
972
  });
734
973
  li.addEventListener('dblclick', async (ev) => {
@@ -791,7 +1030,7 @@ export const ISLAND_HTML = `<!doctype html>
791
1030
  }
792
1031
 
793
1032
  function renderTrail(container, s) {
794
- const h = stateHistory.get(s.sessionId) || [];
1033
+ const h = stateHistory.get(agentKey(s)) || [];
795
1034
  if (h.length === 0) {
796
1035
  container.textContent = '(no transitions recorded yet)';
797
1036
  return;
@@ -980,15 +1219,28 @@ export const ISLAND_HTML = `<!doctype html>
980
1219
  );
981
1220
  }
982
1221
  break;
983
- 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).
984
1235
  upsertClaudeSession({
985
- project: m.projectLabel,
986
- projectEncoded: m.projectEncoded,
1236
+ agent: m.agent,
1237
+ project: m.project,
987
1238
  sessionId: m.sessionId,
988
- lastMtime: m.ts,
1239
+ lastMtime: m.lastMtime,
989
1240
  state: m.state || 'unknown',
990
1241
  stateReason: m.stateReason || '',
991
1242
  cwd: m.cwd || '',
1243
+ activity: m.activity || null,
992
1244
  });
993
1245
  refreshDot();
994
1246
  refreshPanel();
@@ -1007,16 +1259,17 @@ export const ISLAND_HTML = `<!doctype html>
1007
1259
  // ── Claude Code session helpers ────────────────────────────────────
1008
1260
 
1009
1261
  function upsertClaudeSession(s) {
1010
- const idx = state.claudeSessions.findIndex(
1011
- (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,
1012
1269
  );
1013
- const prevState = idx >= 0 ? state.claudeSessions[idx].state : null;
1014
- if (idx >= 0) state.claudeSessions[idx] = s;
1015
- else state.claudeSessions.push(s);
1016
- pruneClaudeSessions();
1017
1270
 
1018
1271
  // Phase 3 — record transition + maybe notify on entering "waiting".
1019
- const transitioned = recordStateHistory(s.sessionId, {
1272
+ const transitioned = recordStateHistory(k, {
1020
1273
  state: s.state,
1021
1274
  reason: s.stateReason,
1022
1275
  lastMtime: s.lastMtime,
@@ -1026,31 +1279,33 @@ export const ISLAND_HTML = `<!doctype html>
1026
1279
 
1027
1280
  function pruneClaudeSessions() {
1028
1281
  const cutoff = Date.now() - CLAUDE_ACTIVE_WINDOW_MS;
1029
- state.claudeSessions = state.claudeSessions.filter(
1282
+ state.agentSessions = state.agentSessions.filter(
1030
1283
  (s) => new Date(s.lastMtime).getTime() >= cutoff
1031
1284
  );
1032
1285
  }
1033
1286
 
1034
1287
  async function fetchClaudeSessions() {
1035
1288
  try {
1036
- 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' });
1037
1291
  if (!r.ok) return;
1038
1292
  const j = await r.json();
1039
1293
  if (Array.isArray(j.sessions)) {
1040
- state.claudeSessions = j.sessions.map((s) => ({
1294
+ state.agentSessions = j.sessions.map((s) => ({
1295
+ agent: s.agent,
1041
1296
  project: s.project,
1042
- projectEncoded: s.projectEncoded,
1043
1297
  sessionId: s.sessionId,
1044
1298
  lastMtime: s.lastMtime,
1045
1299
  state: s.state || 'unknown',
1046
1300
  stateReason: s.stateReason || '',
1047
1301
  cwd: s.cwd || '',
1302
+ activity: s.activity || null,
1048
1303
  }));
1049
1304
  // Phase 3 — seed each session's history with its current state
1050
1305
  // so the trail isn't empty on first open. Doesn't notify (no
1051
1306
  // transition implied by initial load).
1052
- for (const s of state.claudeSessions) {
1053
- recordStateHistory(s.sessionId, {
1307
+ for (const s of state.agentSessions) {
1308
+ recordStateHistory(agentKey(s), {
1054
1309
  state: s.state,
1055
1310
  reason: s.stateReason,
1056
1311
  lastMtime: s.lastMtime,
@@ -1062,6 +1317,97 @@ export const ISLAND_HTML = `<!doctype html>
1062
1317
  }
1063
1318
  }
1064
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
+
1065
1411
  // Phase 3 — notification permission opt-in. The CTA is only visible
1066
1412
  // when permission is in the default (un-asked) state.
1067
1413
  notifyCta.addEventListener('click', (e) => {
@@ -1076,6 +1422,8 @@ export const ISLAND_HTML = `<!doctype html>
1076
1422
  setAvatar('neutral');
1077
1423
  pollPing();
1078
1424
  fetchClaudeSessions().then(() => { refreshDot(); refreshPanel(); });
1425
+ fetchConsent();
1426
+ fetchSenseEvents();
1079
1427
  subscribe();
1080
1428
  refreshNotifyCta();
1081
1429
  // A fresh island picks up the most recent screen-advisor suggestion (if the
@@ -1084,10 +1432,22 @@ export const ISLAND_HTML = `<!doctype html>
1084
1432
  .then((r) => r.ok ? r.json() : null)
1085
1433
  .then((j) => { if (j && j.suggestion) { state.suggestion = j.suggestion; refreshPanel(); } })
1086
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(() => {});
1087
1445
  // Lightweight resync — covers cases where SSE silently disconnected
1088
1446
  // (laptop sleep, network blip) and we need to refresh state.
1089
1447
  setInterval(pollPing, 30_000);
1090
1448
  setInterval(fetchClaudeSessions, 60_000);
1449
+ // Pick up consent changes made elsewhere (e.g. the lisa consent CLI).
1450
+ setInterval(fetchConsent, 30_000);
1091
1451
  // Re-render every 15s so "Xs ago" / "Xm ago" labels stay fresh and
1092
1452
  // stale sessions fall off the list without a fresh update event.
1093
1453
  setInterval(() => {