@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.
- package/README.md +123 -16
- package/README.zh-CN.md +101 -15
- package/completions/_lisa +56 -0
- package/completions/lisa.bash +35 -1
- package/completions/lisa.fish +18 -1
- package/dist/advisor/engine.d.ts +7 -0
- package/dist/advisor/engine.d.ts.map +1 -1
- package/dist/advisor/engine.js +12 -0
- package/dist/advisor/engine.js.map +1 -1
- package/dist/agent.d.ts +14 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +58 -7
- package/dist/agent.js.map +1 -1
- package/dist/agents/managed.d.ts +108 -0
- package/dist/agents/managed.d.ts.map +1 -0
- package/dist/agents/managed.js +241 -0
- package/dist/agents/managed.js.map +1 -0
- package/dist/agents/pty.d.ts +155 -0
- package/dist/agents/pty.d.ts.map +1 -0
- package/dist/agents/pty.js +299 -0
- package/dist/agents/pty.js.map +1 -0
- package/dist/approval.d.ts +13 -0
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +25 -2
- package/dist/approval.js.map +1 -1
- package/dist/autonomy/runs.d.ts +33 -0
- package/dist/autonomy/runs.d.ts.map +1 -0
- package/dist/autonomy/runs.js +106 -0
- package/dist/autonomy/runs.js.map +1 -0
- package/dist/channels/feishu.d.ts.map +1 -1
- package/dist/channels/feishu.js +65 -0
- package/dist/channels/feishu.js.map +1 -1
- package/dist/channels/router.d.ts +7 -0
- package/dist/channels/router.d.ts.map +1 -1
- package/dist/channels/router.js +1 -1
- package/dist/channels/router.js.map +1 -1
- package/dist/cli/agents-pty.d.ts +18 -0
- package/dist/cli/agents-pty.d.ts.map +1 -0
- package/dist/cli/agents-pty.js +182 -0
- package/dist/cli/agents-pty.js.map +1 -0
- package/dist/cli/agents.d.ts +2 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +65 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/consent.d.ts +2 -0
- package/dist/cli/consent.d.ts.map +1 -0
- package/dist/cli/consent.js +57 -0
- package/dist/cli/consent.js.map +1 -0
- package/dist/cli/model.d.ts +2 -0
- package/dist/cli/model.d.ts.map +1 -0
- package/dist/cli/model.js +121 -0
- package/dist/cli/model.js.map +1 -0
- package/dist/cli/sense.d.ts +2 -0
- package/dist/cli/sense.d.ts.map +1 -0
- package/dist/cli/sense.js +36 -0
- package/dist/cli/sense.js.map +1 -0
- package/dist/cli.js +117 -12
- package/dist/cli.js.map +1 -1
- package/dist/consent/blacklist.d.ts +28 -0
- package/dist/consent/blacklist.d.ts.map +1 -0
- package/dist/consent/blacklist.js +78 -0
- package/dist/consent/blacklist.js.map +1 -0
- package/dist/consent/store.d.ts +45 -0
- package/dist/consent/store.d.ts.map +1 -0
- package/dist/consent/store.js +126 -0
- package/dist/consent/store.js.map +1 -0
- package/dist/control/policy.d.ts +13 -0
- package/dist/control/policy.d.ts.map +1 -0
- package/dist/control/policy.js +57 -0
- package/dist/control/policy.js.map +1 -0
- package/dist/heartbeat/runner.d.ts.map +1 -1
- package/dist/heartbeat/runner.js +70 -14
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/idle/runner.d.ts.map +1 -1
- package/dist/idle/runner.js +87 -17
- package/dist/idle/runner.js.map +1 -1
- package/dist/integrations/aider/observer.d.ts +21 -1
- package/dist/integrations/aider/observer.d.ts.map +1 -1
- package/dist/integrations/aider/observer.js +110 -1
- package/dist/integrations/aider/observer.js.map +1 -1
- package/dist/integrations/claude-code/liveness.d.ts +10 -0
- package/dist/integrations/claude-code/liveness.d.ts.map +1 -0
- package/dist/integrations/claude-code/liveness.js +62 -0
- package/dist/integrations/claude-code/liveness.js.map +1 -0
- package/dist/integrations/claude-code/watcher.d.ts +39 -4
- package/dist/integrations/claude-code/watcher.d.ts.map +1 -1
- package/dist/integrations/claude-code/watcher.js +94 -57
- package/dist/integrations/claude-code/watcher.js.map +1 -1
- package/dist/integrations/codex/observer.d.ts +19 -6
- package/dist/integrations/codex/observer.d.ts.map +1 -1
- package/dist/integrations/codex/observer.js +228 -4
- package/dist/integrations/codex/observer.js.map +1 -1
- package/dist/integrations/dispatch-ledger.d.ts +30 -1
- package/dist/integrations/dispatch-ledger.d.ts.map +1 -1
- package/dist/integrations/dispatch-ledger.js +75 -9
- package/dist/integrations/dispatch-ledger.js.map +1 -1
- package/dist/integrations/git/branch.d.ts +11 -0
- package/dist/integrations/git/branch.d.ts.map +1 -0
- package/dist/integrations/git/branch.js +49 -0
- package/dist/integrations/git/branch.js.map +1 -0
- package/dist/integrations/git/observer.d.ts +39 -0
- package/dist/integrations/git/observer.d.ts.map +1 -0
- package/dist/integrations/git/observer.js +252 -0
- package/dist/integrations/git/observer.js.map +1 -0
- package/dist/integrations/github-pr/observer.d.ts.map +1 -1
- package/dist/integrations/github-pr/observer.js +7 -2
- package/dist/integrations/github-pr/observer.js.map +1 -1
- package/dist/integrations/hub.d.ts +8 -0
- package/dist/integrations/hub.d.ts.map +1 -1
- package/dist/integrations/hub.js +39 -1
- package/dist/integrations/hub.js.map +1 -1
- package/dist/integrations/managed/observer.d.ts +17 -0
- package/dist/integrations/managed/observer.d.ts.map +1 -0
- package/dist/integrations/managed/observer.js +46 -0
- package/dist/integrations/managed/observer.js.map +1 -0
- package/dist/integrations/opencode/observer.d.ts +53 -5
- package/dist/integrations/opencode/observer.d.ts.map +1 -1
- package/dist/integrations/opencode/observer.js +242 -20
- package/dist/integrations/opencode/observer.js.map +1 -1
- package/dist/integrations/pty/observer.d.ts +25 -0
- package/dist/integrations/pty/observer.d.ts.map +1 -0
- package/dist/integrations/pty/observer.js +48 -0
- package/dist/integrations/pty/observer.js.map +1 -0
- package/dist/integrations/registry.d.ts.map +1 -1
- package/dist/integrations/registry.js +5 -0
- package/dist/integrations/registry.js.map +1 -1
- package/dist/integrations/shell/observer.d.ts +33 -0
- package/dist/integrations/shell/observer.d.ts.map +1 -0
- package/dist/integrations/shell/observer.js +223 -0
- package/dist/integrations/shell/observer.js.map +1 -0
- package/dist/integrations/takoapi/a2a.d.ts +38 -0
- package/dist/integrations/takoapi/a2a.d.ts.map +1 -0
- package/dist/integrations/takoapi/a2a.js +70 -0
- package/dist/integrations/takoapi/a2a.js.map +1 -0
- package/dist/integrations/takoapi/ledger.d.ts +32 -0
- package/dist/integrations/takoapi/ledger.d.ts.map +1 -0
- package/dist/integrations/takoapi/ledger.js +96 -0
- package/dist/integrations/takoapi/ledger.js.map +1 -0
- package/dist/integrations/takoapi/observer.d.ts +39 -0
- package/dist/integrations/takoapi/observer.d.ts.map +1 -0
- package/dist/integrations/takoapi/observer.js +102 -0
- package/dist/integrations/takoapi/observer.js.map +1 -0
- package/dist/integrations/types.d.ts +17 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/mcp/client.d.ts +5 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/client.js.map +1 -1
- package/dist/memory/embedding.d.ts +45 -0
- package/dist/memory/embedding.d.ts.map +1 -0
- package/dist/memory/embedding.js +139 -0
- package/dist/memory/embedding.js.map +1 -0
- package/dist/memory/search_tool.d.ts.map +1 -1
- package/dist/memory/search_tool.js +6 -2
- package/dist/memory/search_tool.js.map +1 -1
- package/dist/memory/vector.d.ts +14 -3
- package/dist/memory/vector.d.ts.map +1 -1
- package/dist/memory/vector.js +50 -0
- package/dist/memory/vector.js.map +1 -1
- package/dist/model/local.d.ts +43 -0
- package/dist/model/local.d.ts.map +1 -0
- package/dist/model/local.js +118 -0
- package/dist/model/local.js.map +1 -0
- package/dist/model/plan-usage.d.ts +41 -0
- package/dist/model/plan-usage.d.ts.map +1 -0
- package/dist/model/plan-usage.js +168 -0
- package/dist/model/plan-usage.js.map +1 -0
- package/dist/model/plans.d.ts +73 -0
- package/dist/model/plans.d.ts.map +1 -0
- package/dist/model/plans.js +246 -0
- package/dist/model/plans.js.map +1 -0
- package/dist/orchestrator/recent-recap.d.ts +2 -0
- package/dist/orchestrator/recent-recap.d.ts.map +1 -0
- package/dist/orchestrator/recent-recap.js +20 -0
- package/dist/orchestrator/recent-recap.js.map +1 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +10 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/fallback.d.ts +23 -0
- package/dist/providers/fallback.d.ts.map +1 -0
- package/dist/providers/fallback.js +29 -0
- package/dist/providers/fallback.js.map +1 -0
- package/dist/providers/gemini.d.ts +2 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +24 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +4 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/registry.d.ts +18 -2
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +56 -9
- package/dist/providers/registry.js.map +1 -1
- package/dist/reflect.d.ts +55 -0
- package/dist/reflect.d.ts.map +1 -1
- package/dist/reflect.js +148 -54
- package/dist/reflect.js.map +1 -1
- package/dist/sense/log.d.ts +9 -0
- package/dist/sense/log.d.ts.map +1 -0
- package/dist/sense/log.js +63 -0
- package/dist/sense/log.js.map +1 -0
- package/dist/sense/screen.d.ts +43 -0
- package/dist/sense/screen.d.ts.map +1 -0
- package/dist/sense/screen.js +107 -0
- package/dist/sense/screen.js.map +1 -0
- package/dist/sense/service.d.ts +16 -0
- package/dist/sense/service.d.ts.map +1 -0
- package/dist/sense/service.js +33 -0
- package/dist/sense/service.js.map +1 -0
- package/dist/sense/types.d.ts +35 -0
- package/dist/sense/types.d.ts.map +1 -0
- package/dist/sense/types.js +11 -0
- package/dist/sense/types.js.map +1 -0
- package/dist/sense/voice.d.ts +28 -0
- package/dist/sense/voice.d.ts.map +1 -0
- package/dist/sense/voice.js +64 -0
- package/dist/sense/voice.js.map +1 -0
- package/dist/soul/git.d.ts.map +1 -1
- package/dist/soul/git.js +35 -16
- package/dist/soul/git.js.map +1 -1
- package/dist/soul/store.d.ts +22 -0
- package/dist/soul/store.d.ts.map +1 -1
- package/dist/soul/store.js +69 -14
- package/dist/soul/store.js.map +1 -1
- package/dist/soul/summary.d.ts +10 -0
- package/dist/soul/summary.d.ts.map +1 -0
- package/dist/soul/summary.js +57 -0
- package/dist/soul/summary.js.map +1 -0
- package/dist/soul/tools.d.ts.map +1 -1
- package/dist/soul/tools.js +13 -24
- package/dist/soul/tools.js.map +1 -1
- package/dist/soul/types.d.ts +9 -0
- package/dist/soul/types.d.ts.map +1 -1
- package/dist/soul/types.js.map +1 -1
- package/dist/subagent.d.ts +7 -0
- package/dist/subagent.d.ts.map +1 -1
- package/dist/subagent.js +3 -1
- package/dist/subagent.js.map +1 -1
- package/dist/tools/dispatch_agent.d.ts +13 -1
- package/dist/tools/dispatch_agent.d.ts.map +1 -1
- package/dist/tools/dispatch_agent.js +56 -12
- package/dist/tools/dispatch_agent.js.map +1 -1
- package/dist/tools/dispatch_status.d.ts +22 -0
- package/dist/tools/dispatch_status.d.ts.map +1 -0
- package/dist/tools/dispatch_status.js +57 -0
- package/dist/tools/dispatch_status.js.map +1 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +67 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/run_on_plan.d.ts +46 -0
- package/dist/tools/run_on_plan.d.ts.map +1 -0
- package/dist/tools/run_on_plan.js +89 -0
- package/dist/tools/run_on_plan.js.map +1 -0
- package/dist/tools/takoapi.d.ts +60 -0
- package/dist/tools/takoapi.d.ts.map +1 -0
- package/dist/tools/takoapi.js +181 -0
- package/dist/tools/takoapi.js.map +1 -0
- package/dist/tools/validate.d.ts +22 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +50 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/web/agent-roster.d.ts +56 -0
- package/dist/web/agent-roster.d.ts.map +1 -0
- package/dist/web/agent-roster.js +109 -0
- package/dist/web/agent-roster.js.map +1 -0
- package/dist/web/devices.d.ts +27 -0
- package/dist/web/devices.d.ts.map +1 -0
- package/dist/web/devices.js +113 -0
- package/dist/web/devices.js.map +1 -0
- package/dist/web/island.d.ts +5 -1
- package/dist/web/island.d.ts.map +1 -1
- package/dist/web/island.js +410 -63
- package/dist/web/island.js.map +1 -1
- package/dist/web/lisa-client.d.ts +20 -0
- package/dist/web/lisa-client.d.ts.map +1 -0
- package/dist/web/lisa-client.js +1323 -0
- package/dist/web/lisa-client.js.map +1 -0
- package/dist/web/lisa-css.d.ts +13 -0
- package/dist/web/lisa-css.d.ts.map +1 -0
- package/dist/web/lisa-css.js +1163 -0
- package/dist/web/lisa-css.js.map +1 -0
- package/dist/web/lisa-html.d.ts +1 -1
- package/dist/web/lisa-html.d.ts.map +1 -1
- package/dist/web/lisa-html.js +15 -2136
- package/dist/web/lisa-html.js.map +1 -1
- package/dist/web/push.d.ts +76 -0
- package/dist/web/push.d.ts.map +1 -0
- package/dist/web/push.js +163 -0
- package/dist/web/push.js.map +1 -0
- package/dist/web/server.d.ts +19 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +878 -64
- package/dist/web/server.js.map +1 -1
- package/package.json +5 -2
package/dist/web/island.js
CHANGED
|
@@ -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">
|
|
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
|
-
|
|
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
|
|
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(
|
|
704
|
+
lastNotifyAt.set(nk, Date.now());
|
|
561
705
|
const reasonLabel = info.stateReason === 'permission' ? 'needs permission' : 'is waiting';
|
|
562
|
-
const
|
|
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.
|
|
731
|
+
return state.agentSessions.filter(
|
|
587
732
|
(s) => new Date(s.lastMtime).getTime() >= cutoff
|
|
588
733
|
);
|
|
589
734
|
}
|
|
590
735
|
/**
|
|
591
|
-
*
|
|
592
|
-
* "loudest signal wins": an error anywhere dominates
|
|
593
|
-
*
|
|
594
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
655
|
-
//
|
|
656
|
-
//
|
|
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,
|
|
908
|
+
}).slice(0, 8);
|
|
696
909
|
for (const s of rows) {
|
|
697
910
|
const li = document.createElement('li');
|
|
698
|
-
if (rowOpen.has(s
|
|
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
|
-
|
|
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
|
-
|
|
744
|
-
|
|
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
|
|
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 '
|
|
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
|
-
|
|
999
|
-
|
|
1236
|
+
agent: m.agent,
|
|
1237
|
+
project: m.project,
|
|
1000
1238
|
sessionId: m.sessionId,
|
|
1001
|
-
lastMtime: m.
|
|
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
|
|
1024
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1066
|
-
recordStateHistory(s
|
|
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(() => {
|