@agentproto/apps 0.20.1 → 0.20.3
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/dist/index.mjs +10 -1
- package/dist/index.mjs.map +1 -1
- package/dist/session-chat/index.d.ts.map +1 -1
- package/dist/session-chat.mjs +10 -1
- package/dist/session-chat.mjs.map +1 -1
- package/package.json +2 -2
- package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +6 -0
- package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +21 -4
- package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +41 -12
- package/session-steward/.agentproto/workflows/session-steward/cron-rules.mjs +113 -19
- package/session-steward/.agentproto/workflows/session-steward/entry.mjs +74 -11
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/code-team/agents/implementer.ts","../src/code-team/agents/reviewer.ts","../src/code-team/agents/fixer.ts","../src/code-team/workflows/deliver-change.ts","../src/code-team/index.ts","../src/content-team/agents/researcher.ts","../src/content-team/agents/writer.ts","../src/content-team/agents/editor.ts","../src/content-team/agents/illustrator.ts","../src/content-team/workflows/produce-content.ts","../src/content-team/workflows/produce-cover.ts","../src/content-team/index.ts","../src/mail-triage/agents/triager.ts","../src/mail-triage/workflows/triage-inbox.ts","../src/mail-triage/ui.ts","../src/mail-triage/index.ts","../src/media-viewer/agents/cataloger.ts","../src/media-viewer/workflows/scan-media.ts","../src/media-viewer/ui.ts","../src/media-viewer/index.ts","../src/ops-panel/agents/manager.ts","../src/ops-panel/agents/watchdog.ts","../src/ops-panel/ui.ts","../src/ops-panel/index.ts","../src/config/fragment.ts","../src/config/ui.ts","../src/config/index.ts","../src/session-viewer/agents/narrator.ts","../src/session-viewer/workflows/narrate-session.ts","../src/session-viewer/ui.ts","../src/session-viewer/index.ts","../src/panel-bridge.ts","../src/sessions-panel/panel.ts","../src/sessions-panel/index.ts","../src/agents-overview/panel.ts","../src/agents-overview/index.ts","../src/bureau-sessions/panel.ts","../src/bureau-sessions/index.ts","../src/session-story/panel.ts","../src/session-story/index.ts","../src/live-session/panel.ts","../src/live-session/index.ts","../src/session-chat/blob-embed.ts","../src/session-chat/panel.ts","../src/session-chat/index.ts","../src/work-board/panel.generated.ts","../src/work-board/panel.ts","../src/work-board/index.ts","../src/store/panel.generated.ts","../src/store/panel.ts","../src/store/index.ts","../src/review-panel/panel.generated.ts","../src/review-panel/panel.ts","../src/review-panel/index.ts"],"names":["defineAgent","defineWorkflow","defineApp","APP_ID","SECTIONS","z"],"mappings":"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AA,CAAK,SAAA,CAAU,uBAAuB,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA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AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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AAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;ACxG9B,IAAM,gBAA2BD,SAAAA,CAAU;AAAA,EAChD,EAAA,EAAI,4BAAA;AAAA,EACJ,IAAA,EAAM,gBAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,WAAA,EAAa,qFAAA;AAAA,EACb,MAAA,EAAQ,CAAC,QAAQ,CAAA;AAAA,EACjB,SAAA,EAAW,CAAC,cAAc,CAAA;AAAA,EAC1B,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,mBAAA;AAAA,IACN,KAAA,EAAO,gBAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,oBAAoB;AAAA;AAEnC,CAAC;AC8DM,SAAS,kBAAkB,OAAA,EAAyB;AACzD,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA,EAkFa,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwC3C,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,GAAA,CAAA;AAM1B;;;ACtMO,IAAM,UAAA,GAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2ExB,iBAAA,CAAkB,2BAA2B,CAAC;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA,EA8StC,iBAAA,CAAkB,gCAAgC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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AA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;A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AM,yBAAA,GAA4B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","file":"index.mjs","sourcesContent":["import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Writes the requested change. */\nexport const implementer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/implementer\",\n description: \"Implements the requested code change.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Change only what the task asks\", \"Match the surrounding style\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You implement the requested change. Read the task and the surrounding code \" +\n \"first, make the smallest correct change that satisfies it, then run the \" +\n \"tests. Do not touch anything the task didn't ask for.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Reviews the diff; reports only. */\nexport const reviewer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/reviewer\",\n description: \"Reviews the diff and reports findings.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Report findings only\", \"Never edit files\", \"Never run gh pr merge\"],\n // Read-only + run_command (to inspect the diff / run tests). No write/edit\n // tools — the \"never edit files\" boundary is enforced by capability too.\n tools: [\"list_dir\", \"read_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You are a rigorous reviewer. Read the diff, and report each real issue as \" +\n \"`file:line — why`. Change nothing; you only report.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Applies the reviewer's findings, precisely and minimally. */\nexport const fixer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/fixer\",\n description: \"Applies the reviewer's findings, precisely and minimally.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Apply only the listed findings\", \"No unrelated refactors\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You apply the reviewer's findings. Address each listed finding exactly, add \" +\n \"nothing extra, and re-run the tests when done.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the code team delivers a change through. Every team agent\n * references it; the app-kit attachment invariant keeps that honest.\n *\n * Three declarative `kind:\"agent\"` steps — implementer → reviewer → fixer —\n * one per team agent, each `agent.ref` resolving (at compile time, against\n * this app's own install record) to that agent's emitted AGENT.md. This\n * replaces an earlier `ToolStep`-based draft (`apply_patch`/`read_diff`/\n * `run_tests`) that dispatched daemon-side, where those tool ids don't\n * exist — the daemon has no `apply_patch` tool, only live agent sessions\n * that can edit files. Agent steps are the adoption fix (WP-B4): each step\n * spawns the named team agent as a real session and lets it act.\n */\nexport const deliverChange = defineWorkflow({\n id: \"deliver-change\",\n name: \"Deliver a code change\",\n description: \"Implement the change, review the diff, apply fixes, verify.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"implement\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/implementer\" },\n prompt: \"Implement the requested change.\",\n },\n {\n id: \"review\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/reviewer\" },\n prompt:\n \"Review the diff the implementer just produced. Report every real issue as `file:line — why`.\",\n },\n {\n id: \"fix\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/fixer\" },\n prompt: \"Apply the reviewer's findings, then re-run the tests.\",\n },\n ],\n})\n","/**\n * `code-team` — a ready-made agentproto app: a small team that ships a code\n * change. Three agents (implementer → reviewer → fixer) bound to one delivery\n * workflow, declared with `@agentproto/app-kit`.\n *\n * Generic on purpose — `@agentproto/…` ids, no product dependency, no home\n * workspace. A host imports it and uses any subset of its agents/workflows:\n * in-process via `codeTeam.toMastraAgents(...)`, or on disk via\n * `codeTeam.emit(dir)`.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { implementer } from \"./agents/implementer.js\"\nimport { reviewer } from \"./agents/reviewer.js\"\nimport { fixer } from \"./agents/fixer.js\"\nimport { deliverChange } from \"./workflows/deliver-change.js\"\n\nexport const codeTeam: AppHandle = defineApp({\n id: \"@agentproto/code-team\",\n name: \"Code Team\",\n version: \"0.1.0\",\n description: \"Implement, review, and fix code changes.\",\n agents: [implementer, reviewer, fixer],\n workflows: [deliverChange],\n})\n\nexport { implementer, reviewer, fixer, deliverChange }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Gathers sources for the piece. */\nexport const researcher: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/researcher\",\n description: \"Reads the provided sources and summarizes them for the writer.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Cite every claim\", \"Prefer primary sources\"],\n tools: [\"list_dir\", \"read_file\", \"run_command\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You read the source material already in the workspace (list_dir, then \" +\n \"read_file the sources), extract the key facts, and hand the writer a tight \" +\n \"brief with a citation for every claim. Use run_command only to fetch a URL \" +\n \"already named in the task; do not invent sources.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Drafts the piece from the brief. */\nexport const writer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/writer\",\n description: \"Drafts the piece from the researcher's brief.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Stay on the brief\", \"One idea per paragraph\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You draft the piece from the brief and its sources. Lead with the point, \" +\n \"keep one idea per paragraph, and don't invent facts the brief doesn't have.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Tightens the draft. */\nexport const editor: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/editor\",\n description: \"Edits the draft for voice, clarity, and length.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Preserve the writer's meaning\", \"Cut, don't pad\"],\n tools: [\"read_file\", \"edit_file\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You edit the draft. Tighten the voice, cut fluff, fix clarity — without \" +\n \"changing what the writer meant. Leave it shorter than you found it.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Art-directs the cover illustration for each article. */\nexport const illustrator: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/illustrator\",\n description: \"Art-directs the cover illustration for each article.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"One cover per article\", \"Text-free prompts only\"],\n tools: [\"read_file\", \"write_file\", \"run_command\"],\n workflows: [{ ref: \"produce-cover\" }],\n }),\n body:\n \"You art-direct a single cover illustration for the article. \" +\n \"Read the article (read_file) to extract the one visual idea that best \" +\n \"represents the piece, then write the art direction (write_file) as a \" +\n \"clean, text-free image prompt. Keep the visual discipline tight: bold \" +\n \"flat shapes, limited risograph-style palettes, strong negative space, and \" +\n \"a single focal subject. Never include text, lettering, logos, watermarks, \" +\n \"or UI chrome in the prompt. Produce exactly one cover per article; if the \" +\n \"article calls for a literal diagram or chart, fall back to a precise \" +\n \"gpt-image-1 style description and treat the diagram itself as the image \" +\n \"subject. Use run_command only to inspect generated assets or move files \" +\n \"during the upload step.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the content team produces a piece through. Every team\n * agent references it (app-kit attachment invariant).\n *\n * Three declarative `kind:\"agent\"` steps — researcher → writer → editor —\n * one per team agent, each `agent.ref` resolving (at compile time, against\n * this app's own install record) to that agent's emitted AGENT.md. This\n * replaces an earlier `ToolStep`-based draft (`read_file`/`write_file`/\n * `edit_file`/`run_command`) that dispatched daemon-side, where those tool\n * ids don't exist — they're agent workspace tools (adapter-side), never\n * daemon tool ids, so `app_install` rejected them. Agent steps are the\n * adoption fix: each step spawns the named team agent as a real session,\n * which uses its own workspace tools to read, draft, and edit.\n */\nexport const produceContent = defineWorkflow({\n id: \"produce-content\",\n name: \"Produce a piece of content\",\n description: \"Research the sources, draft the piece, then edit it.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"research\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/researcher\" },\n prompt:\n \"Read the source material already in the workspace and hand off a tight \" +\n \"brief with a citation for every claim.\",\n },\n {\n id: \"draft\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/writer\" },\n prompt: \"Draft the piece from the researcher's brief and its sources.\",\n },\n {\n id: \"edit\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/editor\" },\n prompt: \"Edit the draft for voice, clarity, and length, then finalize it.\",\n },\n ],\n})\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the illustrator uses to produce a cover.\n *\n * A single declarative `kind:\"agent\"` step — `agent.ref` resolving (at\n * compile time, against this app's own install record) to the illustrator's\n * emitted AGENT.md. This replaces an earlier `ToolStep`-based draft\n * (`content.cover-concept`/`content.cover-base-gen`/`content.cover-cleanup`/\n * `content.cover-upload`) naming tools that don't exist anywhere — no driver\n * implements them. The illustrator agent folds all four phases (concept,\n * base generation, cleanup, upload) into one prompt for a single session\n * turn, matching the tools it already has (`read_file`, `write_file`,\n * `run_command`).\n */\nexport const produceCover = defineWorkflow({\n id: \"produce-cover\",\n name: \"Produce a cover illustration\",\n description:\n \"Read the article, derive the cover concept, generate the base image, \" +\n \"clean it up, and upload the final cover.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"illustrate\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/illustrator\" },\n prompt:\n \"Produce the cover for this article in one pass: (1) read the article \" +\n \"and derive the single visual concept that best represents it, (2) \" +\n \"write the art direction as a clean, text-free image prompt and \" +\n \"generate the base image from it, (3) clean up the generated image \" +\n \"(crop, color, artifacts), then (4) upload the final cover to the \" +\n \"workspace's output location.\",\n },\n ],\n})\n","/**\n * `content-team` — a ready-made agentproto app: a small team that produces a\n * piece of content. Three agents (researcher → writer → editor) bound to one\n * production workflow, declared with `@agentproto/app-kit`.\n *\n * Generic on purpose — `@agentproto/…` ids, no product dependency. A host\n * imports it and uses any subset of its agents/workflows.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { researcher } from \"./agents/researcher.js\"\nimport { writer } from \"./agents/writer.js\"\nimport { editor } from \"./agents/editor.js\"\nimport { illustrator } from \"./agents/illustrator.js\"\nimport { produceContent } from \"./workflows/produce-content.js\"\nimport { produceCover } from \"./workflows/produce-cover.js\"\n\nexport const contentTeam: AppHandle = defineApp({\n id: \"@agentproto/content-team\",\n name: \"Content Team\",\n version: \"0.1.0\",\n description: \"Research, draft, edit, and illustrate content.\",\n agents: [researcher, writer, editor, illustrator],\n workflows: [produceContent, produceCover],\n})\n\nexport { researcher, writer, editor, illustrator, produceContent, produceCover }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Triages your inbox — finds unread mail, categorizes, labels, archives. */\nexport const triager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/triager\",\n description: \"Triages your inbox — finds unread mail, categorizes, labels, archives.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Always show the triage plan before applying\",\n \"Never delete — only archive, label, or mark read\",\n \"Ask before applying bulk actions on more than 20 messages\",\n ],\n tools: [\n \"mailbox_list\",\n \"mailbox_search\",\n \"mailbox_list_threads\",\n \"mailbox_get_thread\",\n \"mailbox_labels_list\",\n \"mailbox_label_create\",\n \"mailbox_triage_plan\",\n \"mailbox_triage_apply\",\n ],\n workflows: [{ ref: \"triage-inbox\" }],\n }),\n body:\n \"You start by calling mailbox_list to discover connected mailboxes. Pick one \" +\n \"with `capabilities.triage: true` (triage requires the gmail.modify scope). \" +\n \"Every subsequent mailbox tool requires the `mailbox` parameter (UUID or alias \" +\n \"from this list).\\n\" +\n \"\\n\" +\n \"Scan the mailbox for unread messages using mailbox_search with \" +\n 'query=\"is:unread\" and the mailbox from step 1. Categorize them ' +\n \"(urgent, needs-reply, newsletter, notification, spam). Create one triage plan \" +\n \"per bulk action with mailbox_triage_plan — its exact contract is \" +\n '`{ mailbox, criteria: { query } | { messageIds }, action: { type: \"markRead\"|\"archive\"|\"trash\"|\"label\", addLabelIds?, removeLabelIds? } }` ' +\n \"— e.g. label each category, archive newsletters+notifications, keep \" +\n \"urgent+needs-reply in inbox. Each plan returns a `plan_id`, a `count`, and a \" +\n \"`sample`; it expires after 15 minutes. Show the plan before applying. Execute \" +\n \"only after user confirmation, with mailbox_triage_apply \" +\n \"`{ plan_id, confirm: true }` — that is the sole mutation path.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/** Single-agent workflow: triage the inbox. */\nexport const triageInbox = defineWorkflow({\n id: \"triage-inbox\",\n name: \"Triage Inbox\",\n description: \"Scan inbox, categorize unread messages, create and apply triage plan.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"triage\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/triager\" },\n prompt:\n \"Call mailbox_list to discover connected mailboxes, then scan the \" +\n \"selected mailbox for unread messages using mailbox_search with \" +\n 'query=\"is:unread\". Categorize them (urgent, needs-reply, newsletter, ' +\n \"notification, spam). Create a triage plan: label each category, archive \" +\n \"newsletters+notifications, keep urgent+needs-reply in inbox. \" +\n \"Show me the plan before applying.\",\n },\n ],\n})\n","/**\n * Mail Triage — a self-contained MCP App dashboard for `mail-triage`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }`. Mailbox access goes\n * through an imported agentpush MCP server (`imported:<alias>/<tool>` ids);\n * running the triager agent itself goes through the daemon's own\n * `app_run` / `app_status` / `agent_output` — a second path alongside the\n * deterministic plan/apply tools, so the panel can either drive the mailbox\n * tools directly or hand the whole job to the agent and watch it work.\n *\n * The agentpush alias is NOT hardcoded to one server: `app_tool_call`\n * enforces an exact-match allowlist (`ui.tools`), so every candidate alias\n * must be baked into `MAIL_TRIAGE_TOOLS` at emit time. The candidate list\n * comes from `MAIL_TRIAGE_MCP_ALIASES` (overridable via the\n * `MAIL_TRIAGE_MCP_ALIASES` env var, comma-separated, read when this module\n * is evaluated — i.e. at emit). At runtime the panel probes each candidate's\n * `mailbox_list` and activates the first alias that answers with mailboxes;\n * a selector appears in the toolbar when several respond.\n */\n\nconst ALIAS_TOOL_NAMES = [\n \"mailbox_list\",\n \"mailbox_search\",\n \"mailbox_triage_plan\",\n \"mailbox_triage_apply\",\n] as const\n\nconst DEFAULT_MCP_ALIASES = [\"agentpush-prod\", \"agentpush\"] as const\n\nfunction envAliases(): string[] | null {\n if (typeof process === \"undefined\" || !process.env) return null\n const raw = process.env[\"MAIL_TRIAGE_MCP_ALIASES\"]\n if (!raw) return null\n const list = raw\n .split(\",\")\n .map(s => s.trim())\n .filter(Boolean)\n return list.length > 0 ? list : null\n}\n\n/** Candidate imported-MCP aliases the dashboard may reach mailboxes through. */\nexport const MAIL_TRIAGE_MCP_ALIASES: readonly string[] = envAliases() ?? [...DEFAULT_MCP_ALIASES]\n\nexport const MAIL_TRIAGE_TOOLS: readonly string[] = [\n ...MAIL_TRIAGE_MCP_ALIASES.flatMap(alias => ALIAS_TOOL_NAMES.map(t => `imported:${alias}/${t}`)),\n \"app_run\",\n \"app_status\",\n \"agent_output\",\n \"app_list\",\n]\n\nconst APP_ID = \"@agentproto/mail-triage\"\nconst AGENT_ID = \"@agentproto/triager\"\nconst CATEGORIES = [\"urgent\", \"needs-reply\", \"newsletter\", \"notification\"]\n\nexport const MAIL_TRIAGE_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Mail Triage</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow-y:auto}\n#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0;flex-wrap:wrap}\n#toolbar .title{font-size:13px;font-weight:600;flex:1}\nselect,input[type=text]{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:4px;font-size:12px;font-family:inherit}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn:disabled{opacity:.4;cursor:default}\n.abtn.primary{background:var(--blue);border-color:var(--blue);color:#04101c}\n.abtn.primary:hover{opacity:.9}\n#caps{padding:6px 14px;display:flex;gap:6px;align-items:center;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n.badge{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--text2)}\n.badge.on{color:var(--green);border-color:var(--green)}\n.badge.off{opacity:.5;text-decoration:line-through}\n#content{padding:14px;display:flex;flex-direction:column;gap:16px}\nsection{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px}\nsection h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}\n#summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}\n.tile{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}\n.tile .count{font-size:22px;font-weight:700}\n.tile .label{font-size:11px;color:var(--text2);text-transform:capitalize}\n.tile.unread .count{color:var(--green)}\n.tile.urgent .count{color:var(--red)}\n.tile.needs-reply .count{color:var(--yellow)}\n.tile.newsletter .count{color:var(--blue)}\n.tile.notification .count{color:var(--purple)}\n#plan-list,#search-results,#log-events,#runs-list,#connect-hint-list{max-height:260px;overflow-y:auto;font-size:12px}\n.plan-item{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}\n.plan-item .subj{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.plan-item .tag{color:var(--text2);font-size:11px;white-space:nowrap}\n.runs-item{display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);cursor:pointer}\n.runs-item:hover{background:var(--bg3)}\n.runs-item .rid{font-family:Menlo,Monaco,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}\n.runs-item .st{font-size:11px;white-space:nowrap}\n.runs-item .st.running{color:var(--yellow)}\n.runs-item .st.ended{color:var(--green)}\n.runs-item .meta{color:var(--text2);font-size:11px;white-space:nowrap}\n#log-tail{font-family:Menlo,Monaco,monospace;white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;max-height:260px;overflow-y:auto}\n.searchbar{display:flex;gap:8px;margin-bottom:8px}\n.searchbar input{flex:1}\n.actions{display:flex;gap:8px;margin-top:10px}\n#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.empty{color:var(--text2);padding:8px 0}\n.mini{font-size:10px;text-transform:none;letter-spacing:0;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit}\n.caps-hint{font-size:11px;color:var(--yellow)}\n.snippet{color:var(--text2);font-size:11px;margin-top:2px}\n.mail-checkbox{accent-color:var(--blue);cursor:pointer;flex-shrink:0;margin-top:1px}\n.mail-open{font-size:10px;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit;white-space:nowrap;padding:0;flex-shrink:0}\n.mail-open:hover{text-decoration:underline}\n#selection-bar{display:none;margin-bottom:8px;gap:8px;align-items:center}\n#selection-bar.show{display:flex}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span class=\"title\">Mail Triage</span>\n <select id=\"alias-select\" style=\"display:none\" title=\"MCP server\"></select>\n <select id=\"mailbox-select\"></select>\n <button class=\"abtn\" id=\"refresh-btn\">Refresh</button>\n </div>\n <div id=\"caps\" style=\"display:none\"></div>\n <div id=\"content\">\n <section id=\"connect-hint\" style=\"display:none\">\n <h3>Connect a mailbox</h3>\n <div id=\"connect-hint-list\"></div>\n <div class=\"empty\">Import/start an agentpush MCP server and connect a Gmail account in agentpush, then retry.</div>\n <div class=\"actions\">\n <button class=\"abtn primary\" id=\"connect-retry-btn\">Retry</button>\n </div>\n </section>\n\n <section>\n <h3>Inbox summary</h3>\n <div id=\"summary\"></div>\n </section>\n\n <section>\n <h3>Search</h3>\n <div class=\"searchbar\">\n <input type=\"text\" id=\"search-input\" placeholder=\"e.g. is:unread\">\n <button class=\"abtn\" id=\"search-btn\">Search</button>\n </div>\n <div id=\"selection-bar\">\n <span id=\"sel-count\" style=\"font-size:12px;color:var(--text2)\">0 selected</span>\n <button class=\"abtn primary\" data-action=\"send-selection\">Send selection to Claude</button>\n </div>\n <div id=\"search-results\"><div class=\"empty\">No search run yet.</div></div>\n </section>\n\n <section>\n <h3>Triage plan</h3>\n <div class=\"searchbar\">\n <input type=\"text\" id=\"plan-query\" placeholder=\"criteria query\" value=\"is:unread category:promotions older_than:7d\">\n <select id=\"plan-action\">\n <option value=\"markRead\">mark read</option>\n <option value=\"archive\">archive</option>\n <option value=\"label\">label…</option>\n <option value=\"trash\">trash</option>\n </select>\n <input type=\"text\" id=\"plan-label\" placeholder=\"label name\" style=\"display:none\">\n </div>\n <div class=\"actions\">\n <button class=\"abtn primary\" id=\"triage-btn\">Build plan</button>\n <button class=\"abtn\" id=\"apply-btn\" disabled>Apply</button>\n </div>\n <div id=\"plan-list\" style=\"margin-top:10px\"><div class=\"empty\">No plan yet — click Build plan.</div></div>\n </section>\n\n <section>\n <h3>Triager agent</h3>\n <div class=\"actions\">\n <button class=\"abtn\" id=\"run-agent-btn\">Run Triager Agent</button>\n </div>\n <div id=\"log-events\" style=\"margin-top:10px\"><div class=\"empty\">Agent has not run yet.</div></div>\n <div id=\"log-tail\" style=\"margin-top:6px\"></div>\n </section>\n\n <section>\n <h3><span>Past runs</span><button class=\"mini\" id=\"runs-refresh-btn\">refresh</button></h3>\n <div id=\"runs-list\"><div class=\"empty\">No runs yet.</div></div>\n </section>\n </div>\n <div id=\"status\">Connecting…</div>\n</div>\n<script>\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar AGENT_ID = ${JSON.stringify(AGENT_ID)};\nvar CATEGORIES = ${JSON.stringify(CATEGORIES)};\nvar ALIASES = ${JSON.stringify(MAIL_TRIAGE_MCP_ALIASES)};\nvar POLL_TIMEOUT_MS = 10 * 60 * 1000;\nvar callTool = null;\nvar updateModelContext = null;\nvar openLink = null;\nvar onTeardown = null;\nvar currentPlan = null;\nvar pollHandle = null;\nvar planExpiryHandle = null;\nvar activeAlias = ALIASES[0];\nvar currentMailboxes = [];\nvar categoryCounts = null;\nvar unreadCount = null;\nvar aliasStates = {};\nvar selectedMessages = {};\n\nfunction setStatus(msg) {\n document.getElementById(\"status\").textContent = msg;\n}\n\n// See media-viewer/ui.ts for why this loop exists: app_tool_call wraps its\n// dispatch result in at least one more {content:[{type:\"text\",text:...}]}\n// shell than the underlying tool returned.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try {\n cur = JSON.parse(cur);\n continue;\n } catch (e) {\n return cur;\n }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n if (!callTool) {\n return Promise.reject(new Error(\"Not connected to host bridge (standalone mode).\"));\n }\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(function (result) {\n var value = unwrapText(result);\n if (value && typeof value === \"object\" && !Array.isArray(value) && typeof value.error === \"string\") {\n throw new Error(value.error);\n }\n return value;\n });\n}\n\nfunction aliasTool(name) {\n return \"imported:\" + activeAlias + \"/\" + name;\n}\n\n// Disables a button and swaps its label to an ellipsis while a call is in\n// flight, so double-submit is impossible; restores the original label after.\nfunction setBusy(btn, busy) {\n if (busy) {\n if (btn.getAttribute(\"data-label\") === null) btn.setAttribute(\"data-label\", btn.textContent);\n btn.disabled = true;\n btn.textContent = \"\\\\u2026\";\n } else {\n btn.disabled = false;\n var label = btn.getAttribute(\"data-label\");\n if (label !== null) btn.textContent = label;\n }\n}\n\nfunction isSelected(id) {\n return selectedMessages[id] ? true : false;\n}\n\nfunction toggleSelection(id, item) {\n if (selectedMessages[id]) {\n delete selectedMessages[id];\n } else {\n selectedMessages[id] = item;\n }\n updateSelectionBar();\n}\n\nfunction updateSelectionBar() {\n var bar = document.getElementById(\"selection-bar\");\n var countEl = document.getElementById(\"sel-count\");\n var count = 0;\n for (var k in selectedMessages) { if (selectedMessages.hasOwnProperty(k)) count++; }\n countEl.textContent = count + \" selected\";\n bar.className = count > 0 ? \"show\" : \"\";\n // Sync checkboxes\n var cbs = document.querySelectorAll(\"#search-results .mail-checkbox\");\n for (var i = 0; i < cbs.length; i++) {\n var mid = cbs[i].getAttribute(\"data-message-id\");\n cbs[i].checked = mid ? isSelected(mid) : false;\n }\n}\n\nfunction sendSelectionToClaude() {\n if (!updateModelContext) {\n setStatus(\"Host does not support updateModelContext.\");\n return;\n }\n var items = [];\n var names = [];\n for (var k in selectedMessages) {\n if (!selectedMessages.hasOwnProperty(k)) continue;\n var msg = selectedMessages[k];\n items.push({\n id: msg.id || k,\n threadId: msg.threadId || \"\",\n from: msg.from || \"\",\n subject: msg.subject || \"\",\n timestamp: msg.internalDate || msg.date || \"\"\n });\n if (msg.subject) names.push(msg.subject);\n }\n if (items.length === 0) {\n setStatus(\"No messages selected.\");\n return;\n }\n var summary = items.length + \" emails selected for triage: \" + names.join(\", \");\n setStatus(\"Sending selection to Claude\\\\u2026\");\n updateModelContext({\n content: [{ type: \"text\", text: summary }],\n structuredContent: { selection: items }\n }).then(function () {\n setStatus(\"Selection sent to Claude (\" + items.length + \" emails).\");\n }).catch(function (err) {\n setStatus(\"Failed to send selection: \" + (err && err.message ? err.message : String(err)));\n });\n}\n\nfunction clearPlanExpiry() {\n if (planExpiryHandle) {\n clearTimeout(planExpiryHandle);\n planExpiryHandle = null;\n }\n}\n\nfunction onPlanExpired() {\n planExpiryHandle = null;\n currentPlan = null;\n document.getElementById(\"apply-btn\").disabled = true;\n setStatus(\"Plan expired \\\\u2014 rebuild it.\");\n}\n\n// Arms a timeout that invalidates the plan at its server-declared expires_at\n// (plans expire after 15 minutes) so Apply never fires against a stale plan.\nfunction armPlanExpiry(expiresAt) {\n clearPlanExpiry();\n if (!expiresAt) return;\n var ms = new Date(expiresAt).getTime() - Date.now();\n if (isNaN(ms)) return;\n if (ms <= 0) {\n onPlanExpired();\n return;\n }\n planExpiryHandle = setTimeout(onPlanExpired, ms);\n}\n\nfunction asList(value, keys) {\n if (Array.isArray(value)) return value;\n if (value && typeof value === \"object\") {\n for (var i = 0; i < keys.length; i++) {\n if (Array.isArray(value[keys[i]])) return value[keys[i]];\n }\n }\n return [];\n}\n\nfunction renderSummary() {\n var el = document.getElementById(\"summary\");\n el.innerHTML = \"\";\n var tiles = [{ key: \"unread\", count: unreadCount }].concat(\n CATEGORIES.map(function (cat) {\n return { key: cat, count: categoryCounts ? categoryCounts[cat] : undefined };\n })\n );\n tiles.forEach(function (t) {\n var tile = document.createElement(\"div\");\n tile.className = \"tile \" + t.key;\n tile.innerHTML = '<div class=\"count\"></div><div class=\"label\"></div>';\n tile.querySelector(\".count\").textContent = t.count === undefined || t.count === null ? \"\\\\u2013\" : String(t.count);\n tile.querySelector(\".label\").textContent = t.key.replace(\"-\", \" \");\n el.appendChild(tile);\n });\n}\n\nfunction mailboxId(mb) {\n if (typeof mb === \"string\") return mb;\n return mb.id || mb.name || JSON.stringify(mb);\n}\n\nfunction mailboxText(mb) {\n if (typeof mb === \"string\") return mb;\n var name = mb.address || mb.label || mb.name || mb.id || JSON.stringify(mb);\n var caps = mb.capabilities;\n if (caps && typeof caps === \"object\") {\n return name + (caps.triage ? \" \\\\u2014 triage \\\\u2713\" : \" \\\\u2014 read-only\");\n }\n return name;\n}\n\nfunction selectedMailbox() {\n var id = document.getElementById(\"mailbox-select\").value;\n for (var i = 0; i < currentMailboxes.length; i++) {\n if (mailboxId(currentMailboxes[i]) === id) return currentMailboxes[i];\n }\n return null;\n}\n\nfunction renderCaps(mb) {\n var el = document.getElementById(\"caps\");\n var caps = mb && typeof mb === \"object\" ? mb.capabilities : null;\n if (!caps || typeof caps !== \"object\") {\n el.style.display = \"none\";\n return;\n }\n el.style.display = \"flex\";\n el.innerHTML = \"\";\n [\"read\", \"send\", \"triage\", \"compose\"].forEach(function (cap) {\n var b = document.createElement(\"span\");\n b.className = \"badge \" + (caps[cap] ? \"on\" : \"off\");\n b.textContent = cap;\n el.appendChild(b);\n });\n if (!caps.triage) {\n var hint = document.createElement(\"span\");\n hint.className = \"caps-hint\";\n hint.textContent = \"triage scope missing \\\\u2014 grant it via agentpush (mailbox_request_elevation)\";\n el.appendChild(hint);\n }\n}\n\n// Gmail category labels -> the dashboard's triage buckets. IMPORTANT wins,\n// so an important promo counts as urgent, not newsletter.\nfunction categoryOf(labelIds) {\n var l = labelIds || [];\n if (l.indexOf(\"IMPORTANT\") !== -1) return \"urgent\";\n if (l.indexOf(\"CATEGORY_PROMOTIONS\") !== -1) return \"newsletter\";\n if (l.indexOf(\"CATEGORY_UPDATES\") !== -1 || l.indexOf(\"CATEGORY_SOCIAL\") !== -1 || l.indexOf(\"CATEGORY_FORUMS\") !== -1) return \"notification\";\n return \"needs-reply\";\n}\n\nfunction onMailboxChange() {\n clearPlanExpiry();\n var mb = selectedMailbox();\n renderCaps(mb);\n var caps = mb && typeof mb === \"object\" ? mb.capabilities : null;\n var noTriage = !!(caps && typeof caps === \"object\" && !caps.triage);\n var scopeHint = noTriage ? \"Mailbox lacks triage capability \\\\u2014 grant it via agentpush (mailbox_request_elevation)\" : \"\";\n document.getElementById(\"triage-btn\").disabled = noTriage;\n document.getElementById(\"triage-btn\").title = scopeHint;\n document.getElementById(\"apply-btn\").disabled = true;\n document.getElementById(\"apply-btn\").title = scopeHint;\n currentPlan = null;\n categoryCounts = null;\n unreadCount = null;\n renderSummary();\n if (!mb) return;\n callApp(aliasTool(\"mailbox_search\"), { mailbox: mailboxId(mb), query: \"is:unread\", limit: 50 })\n .then(function (result) {\n var items = asList(result, [\"items\", \"messages\", \"results\"]);\n if (result && typeof result === \"object\" && typeof result.resultSizeEstimate === \"number\") {\n unreadCount = result.resultSizeEstimate;\n } else {\n unreadCount = items.length;\n }\n // Category breakdown over the newest page of unread — a sample when\n // the mailbox has more unread than one page.\n var counts = {};\n items.forEach(function (item) {\n var cat = categoryOf(item && item.labelIds);\n counts[cat] = (counts[cat] || 0) + 1;\n });\n categoryCounts = items.length > 0 ? counts : null;\n renderSummary();\n })\n .catch(function (err) {\n // KPI is best-effort: keep the dashes, but say why instead of silence.\n setStatus(\"Inbox summary unavailable: \" + err.message);\n });\n}\n\nfunction renderMailboxes(mailboxes) {\n currentMailboxes = mailboxes;\n var select = document.getElementById(\"mailbox-select\");\n select.innerHTML = \"\";\n if (mailboxes.length === 0) {\n var opt = document.createElement(\"option\");\n opt.textContent = \"no mailboxes\";\n opt.value = \"\";\n select.appendChild(opt);\n renderCaps(null);\n return;\n }\n mailboxes.forEach(function (mb) {\n var opt = document.createElement(\"option\");\n opt.value = mailboxId(mb);\n opt.textContent = mailboxText(mb);\n select.appendChild(opt);\n });\n onMailboxChange();\n}\n\nfunction renderAliasSelect(liveAliases) {\n var select = document.getElementById(\"alias-select\");\n select.innerHTML = \"\";\n var shown = liveAliases.length > 0 ? liveAliases : ALIASES;\n shown.forEach(function (alias) {\n var opt = document.createElement(\"option\");\n opt.value = alias;\n opt.textContent = alias;\n select.appendChild(opt);\n });\n select.value = activeAlias;\n select.style.display = shown.length > 1 ? \"\" : \"none\";\n}\n\nfunction loadMailboxes() {\n setStatus(\"Loading mailboxes from \" + activeAlias + \"\\\\u2026\");\n return callApp(aliasTool(\"mailbox_list\"), {})\n .then(function (result) {\n renderMailboxes(asList(result, [\"mailboxes\", \"items\"]));\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n })\n .catch(function (err) {\n setStatus(\"Error loading mailboxes: \" + err.message);\n });\n}\n\nfunction renderConnectHint() {\n var el = document.getElementById(\"connect-hint\");\n var anyLive = false;\n for (var i = 0; i < ALIASES.length; i++) {\n if (aliasStates[ALIASES[i]] && aliasStates[ALIASES[i]].state === \"live\") anyLive = true;\n }\n if (anyLive) {\n el.style.display = \"none\";\n return;\n }\n el.style.display = \"\";\n var list = document.getElementById(\"connect-hint-list\");\n list.innerHTML = \"\";\n ALIASES.forEach(function (alias) {\n var st = aliasStates[alias];\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n var text;\n if (!st) text = alias + \" \\\\u2014 not probed yet\";\n else if (st.state === \"unreachable\") text = alias + \" \\\\u2014 unreachable: \" + st.message;\n else if (st.state === \"empty\") text = alias + \" \\\\u2014 connected, no mailboxes\";\n else text = alias + \" \\\\u2014 live\";\n row.textContent = text;\n list.appendChild(row);\n });\n}\n\nfunction probeAliases() {\n setStatus(\"Detecting mail servers\\\\u2026\");\n return Promise.all(\n ALIASES.map(function (alias) {\n return callApp(\"imported:\" + alias + \"/mailbox_list\", {})\n .then(function (result) {\n var mailboxes = asList(result, [\"mailboxes\", \"items\"]);\n aliasStates[alias] = { state: mailboxes.length > 0 ? \"live\" : \"empty\" };\n return { alias: alias, mailboxes: mailboxes };\n })\n .catch(function (err) {\n aliasStates[alias] = { state: \"unreachable\", message: err && err.message ? err.message : String(err) };\n return { alias: alias, mailboxes: [] };\n });\n })\n ).then(function (results) {\n var live = results.filter(function (r) { return r.mailboxes.length > 0; });\n if (live.length > 0) {\n activeAlias = live[0].alias;\n renderAliasSelect(live.map(function (r) { return r.alias; }));\n renderMailboxes(live[0].mailboxes);\n renderConnectHint();\n setStatus(\"Ready \\\\u2014 \" + activeAlias + (live.length > 1 ? \" (\" + live.length + \" servers available)\" : \"\"));\n } else {\n renderAliasSelect([]);\n renderMailboxes([]);\n renderConnectHint();\n setStatus(\"No mailboxes reachable \\\\u2014 see connection panel below.\");\n }\n });\n}\n\nfunction renderSearchResults(items, raw) {\n var el = document.getElementById(\"search-results\");\n el.innerHTML = \"\";\n if (raw && typeof raw === \"object\" && typeof raw.resultSizeEstimate === \"number\") {\n var count = document.createElement(\"div\");\n count.className = \"empty\";\n count.textContent = items.length + \" shown \\\\u00b7 ~\" + raw.resultSizeEstimate + \" total\";\n el.appendChild(count);\n }\n if (items.length === 0) {\n var none = document.createElement(\"div\");\n none.className = \"empty\";\n none.textContent = \"No results.\";\n el.appendChild(none);\n return;\n }\n items.forEach(function (item) {\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n row.style.flexDirection = \"column\";\n row.style.alignItems = \"stretch\";\n var msgId = item && (item.id || item.messageId || \"\");\n var threadId = item && (item.threadId || \"\");\n var subj = typeof item === \"string\" ? item : item.subject || item.title || JSON.stringify(item);\n var from = item && item.from ? item.from : \"\";\n var snippet = item && item.snippet ? item.snippet : \"\";\n var top = document.createElement(\"div\");\n top.style.display = \"flex\";\n top.style.alignItems = \"flex-start\";\n top.style.gap = \"6px\";\n var cb = document.createElement(\"input\");\n cb.type = \"checkbox\";\n cb.className = \"mail-checkbox\";\n cb.setAttribute(\"data-action\", \"toggle-select\");\n if (msgId) cb.setAttribute(\"data-message-id\", msgId);\n if (isSelected(msgId)) cb.checked = true;\n top.appendChild(cb);\n var subjSpan = document.createElement(\"span\");\n subjSpan.className = \"subj\";\n subjSpan.style.flex = \"1\";\n subjSpan.textContent = subj;\n top.appendChild(subjSpan);\n var tagSpan = document.createElement(\"span\");\n tagSpan.className = \"tag\";\n tagSpan.textContent = from;\n top.appendChild(tagSpan);\n if (threadId) {\n var openBtn = document.createElement(\"button\");\n openBtn.className = \"mail-open\";\n openBtn.textContent = \"Open\";\n openBtn.setAttribute(\"data-action\", \"open-gmail\");\n openBtn.setAttribute(\"data-thread-id\", threadId);\n top.appendChild(openBtn);\n }\n row.appendChild(top);\n if (snippet) {\n var sn = document.createElement(\"div\");\n sn.className = \"snippet\";\n sn.textContent = snippet;\n row.appendChild(sn);\n }\n el.appendChild(row);\n });\n}\n\n// Renders a mailbox_triage_plan result: { plan_id, action, count, sample,\n// truncated, expires_at }. The sample rows are search-shaped messages.\nfunction renderPlan(plan) {\n var el = document.getElementById(\"plan-list\");\n el.innerHTML = \"\";\n if (!plan || typeof plan !== \"object\" || !plan.plan_id) {\n el.innerHTML = '<div class=\"empty\">Plan failed \\\\u2014 unexpected response.</div>';\n return;\n }\n var head = document.createElement(\"div\");\n head.className = \"plan-item\";\n head.innerHTML = '<span class=\"subj\"></span><span class=\"tag\"></span>';\n head.querySelector(\".subj\").textContent =\n plan.count + \" message\" + (plan.count === 1 ? \"\" : \"s\") + \" \\\\u2192 \" + (plan.action && plan.action.type);\n head.querySelector(\".tag\").textContent = plan.expires_at ? \"expires \" + fmtDate(plan.expires_at) : \"\";\n el.appendChild(head);\n var sample = asList(plan, [\"sample\"]);\n sample.forEach(function (item) {\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n row.innerHTML = '<span class=\"subj\"></span><span class=\"tag\"></span>';\n row.querySelector(\".subj\").textContent = (item && (item.subject || item.title)) || JSON.stringify(item);\n row.querySelector(\".tag\").textContent = (item && item.from) || \"\";\n el.appendChild(row);\n });\n if (plan.truncated) {\n var more = document.createElement(\"div\");\n more.className = \"empty\";\n more.textContent = \"\\\\u2026sample truncated \\\\u2014 \" + plan.count + \" total.\";\n el.appendChild(more);\n }\n}\n\nfunction fmtDate(iso) {\n if (!iso) return \"\";\n var d = new Date(iso);\n return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();\n}\n\nfunction renderRuns(runs) {\n var el = document.getElementById(\"runs-list\");\n el.innerHTML = \"\";\n if (runs.length === 0) {\n el.innerHTML = '<div class=\"empty\">No runs yet.</div>';\n return;\n }\n var shown = runs.slice(0, 20);\n shown.forEach(function (run) {\n var row = document.createElement(\"div\");\n row.className = \"runs-item\";\n row.title = \"Show sessions + output for \" + run.appRunId;\n row.innerHTML = '<span class=\"st\"></span><span class=\"rid\"></span><span class=\"meta\"></span>';\n var st = row.querySelector(\".st\");\n st.textContent = run.status || \"?\";\n st.className = \"st \" + (run.status || \"\");\n row.querySelector(\".rid\").textContent = run.appRunId;\n row.querySelector(\".meta\").textContent =\n fmtDate(run.startedAt) + (run.endedAt ? \" \\\\u2192 \" + fmtDate(run.endedAt) : \"\") + \" \\\\u00b7 \" + (run.sessions || 0) + \" session\" + (run.sessions === 1 ? \"\" : \"s\");\n row.addEventListener(\"click\", function () { viewRun(run.appRunId); });\n el.appendChild(row);\n });\n if (runs.length > 20) {\n var more = document.createElement(\"div\");\n more.className = \"empty\";\n more.textContent = \"showing 20 of \" + runs.length;\n el.appendChild(more);\n }\n}\n\nfunction loadRuns() {\n return callApp(\"app_list\", {})\n .then(function (result) {\n var apps = Array.isArray(result) ? result : [];\n var mine = null;\n for (var i = 0; i < apps.length; i++) {\n if (apps[i] && apps[i].appId === APP_ID) { mine = apps[i]; break; }\n }\n var runs = (mine && Array.isArray(mine.runs) ? mine.runs.slice() : []).sort(function (a, b) {\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n renderRuns(runs);\n })\n .catch(function (err) {\n setStatus(\"Could not load runs: \" + err.message);\n });\n}\n\nfunction viewRun(appRunId) {\n setStatus(\"Loading run \" + appRunId + \"\\\\u2026\");\n callApp(\"app_status\", { appRunId: appRunId })\n .then(function (status) {\n var sessions = (status && status.sessions) || [];\n appendLog(\"\\\\u2500\\\\u2500 run \" + appRunId + \" (\" + (status && status.status) + \") \\\\u2500\\\\u2500\");\n if (sessions.length === 0) {\n appendLog(\"(no sessions)\");\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n return;\n }\n sessions.forEach(function (s) {\n appendLog(\"session \" + s.sessionId + \" (\" + (s.agentId || \"?\") + \")\");\n });\n return callApp(\"agent_output\", { sessionId: sessions[0].sessionId, lastN: 200, clean: true })\n .then(function (output) {\n if (output) appendLog(typeof output === \"string\" ? output : JSON.stringify(output, null, 2));\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n });\n })\n .catch(function (err) {\n setStatus(\"Run status failed: \" + err.message);\n });\n}\n\n// Event delegation: search-results (checkboxes + open buttons)\ndocument.getElementById(\"search-results\").addEventListener(\"change\", function (e) {\n var target = e.target;\n if (target.getAttribute(\"data-action\") === \"toggle-select\") {\n var mid = target.getAttribute(\"data-message-id\");\n if (mid) {\n var item = null;\n var el = target.parentNode;\n while (el && el.parentNode && el !== document) {\n if (el.tagName === \"DIV\" && el.className.indexOf(\"plan-item\") !== -1) {\n var subjEl = el.querySelector(\".subj\");\n var tagEl = el.querySelector(\".tag\");\n var subjText = subjEl ? subjEl.textContent : \"\";\n var tagText = tagEl ? tagEl.textContent : \"\";\n // Reconstruct a minimal item from the DOM\n item = { id: mid, threadId: \"\", subject: subjText, from: tagText };\n break;\n }\n el = el.parentNode;\n }\n toggleSelection(mid, item || { id: mid });\n }\n }\n});\ndocument.getElementById(\"search-results\").addEventListener(\"click\", function (e) {\n var target = e.target;\n if (target.getAttribute(\"data-action\") === \"open-gmail\") {\n var tid = target.getAttribute(\"data-thread-id\");\n if (tid && openLink) {\n openLink(\"https://mail.google.com/mail/u/0/#inbox/\" + tid).catch(function (err) {\n setStatus(\"Failed to open link: \" + (err && err.message ? err.message : String(err)));\n });\n } else if (tid) {\n setStatus(\"Host does not support openLink.\");\n }\n }\n});\n// Event delegation: selection-bar (send-selection)\ndocument.getElementById(\"selection-bar\").addEventListener(\"click\", function (e) {\n if (e.target.getAttribute(\"data-action\") === \"send-selection\") {\n sendSelectionToClaude();\n }\n});\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n probeAliases();\n loadRuns();\n});\n\ndocument.getElementById(\"connect-retry-btn\").addEventListener(\"click\", function () {\n probeAliases();\n loadRuns();\n});\n\ndocument.getElementById(\"alias-select\").addEventListener(\"change\", function () {\n activeAlias = this.value;\n clearPlanExpiry();\n currentPlan = null;\n document.getElementById(\"apply-btn\").disabled = true;\n loadMailboxes();\n});\n\ndocument.getElementById(\"mailbox-select\").addEventListener(\"change\", onMailboxChange);\n\ndocument.getElementById(\"runs-refresh-btn\").addEventListener(\"click\", loadRuns);\n\ndocument.getElementById(\"search-btn\").addEventListener(\"click\", function () {\n var btn = this;\n var query = document.getElementById(\"search-input\").value.trim() || \"is:unread\";\n var mailbox = document.getElementById(\"mailbox-select\").value;\n setBusy(btn, true);\n setStatus(\"Searching\\\\u2026\");\n callApp(aliasTool(\"mailbox_search\"), { mailbox: mailbox, query: query })\n .then(function (result) {\n renderSearchResults(asList(result, [\"items\", \"messages\", \"results\"]), result);\n setBusy(btn, false);\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Search failed: \" + err.message);\n });\n});\n\ndocument.getElementById(\"plan-action\").addEventListener(\"change\", function () {\n document.getElementById(\"plan-label\").style.display = this.value === \"label\" ? \"\" : \"none\";\n});\n\ndocument.getElementById(\"triage-btn\").addEventListener(\"click\", function () {\n var btn = this;\n var mailbox = document.getElementById(\"mailbox-select\").value;\n var query = document.getElementById(\"plan-query\").value.trim() || \"is:unread\";\n var actionType = document.getElementById(\"plan-action\").value;\n var action = { type: actionType };\n if (actionType === \"label\") {\n var label = document.getElementById(\"plan-label\").value.trim();\n if (!label) {\n setStatus(\"Enter a label name for the label action.\");\n return;\n }\n action.addLabelIds = [label];\n }\n setBusy(btn, true);\n setStatus(\"Building triage plan\\\\u2026\");\n document.getElementById(\"apply-btn\").disabled = true;\n clearPlanExpiry();\n currentPlan = null;\n callApp(aliasTool(\"mailbox_triage_plan\"), { mailbox: mailbox, criteria: { query: query }, action: action })\n .then(function (plan) {\n setBusy(btn, false);\n currentPlan = plan && plan.plan_id ? plan : null;\n renderPlan(plan);\n if (currentPlan && plan.count > 0) {\n document.getElementById(\"apply-btn\").disabled = false;\n armPlanExpiry(plan.expires_at);\n setStatus(\"Plan ready (\" + plan.count + \" messages) \\\\u2014 review, then Apply.\");\n } else if (currentPlan) {\n setStatus(\"Plan matches no messages \\\\u2014 nothing to apply.\");\n } else {\n setStatus(\"Triage plan failed \\\\u2014 see plan panel.\");\n }\n })\n .catch(function (err) {\n setBusy(btn, false);\n var msg = err && err.message ? err.message : String(err);\n if (msg.indexOf(\"mailbox_scope_missing\") !== -1) {\n setStatus(\"Triage scope missing \\\\u2014 grant it via agentpush (mailbox_request_elevation).\");\n } else {\n setStatus(\"Triage plan failed: \" + msg);\n }\n });\n});\n\ndocument.getElementById(\"apply-btn\").addEventListener(\"click\", function () {\n var btn = this;\n if (!currentPlan || !currentPlan.plan_id) return;\n setBusy(btn, true);\n setStatus(\"Applying triage plan\\\\u2026\");\n callApp(aliasTool(\"mailbox_triage_apply\"), { plan_id: currentPlan.plan_id, confirm: true })\n .then(function (result) {\n setBusy(btn, false);\n var count = currentPlan.count;\n var extra = [];\n if (result && typeof result === \"object\") {\n [\"applied\", \"modified\", \"count\"].forEach(function (key) {\n if (typeof result[key] === \"number\") extra.push(key + \": \" + result[key]);\n });\n }\n setStatus(\n extra.length > 0\n ? \"Plan applied \\\\u2014 \" + extra.join(\", \") + \".\"\n : \"Plan applied (\" + count + \" message\" + (count === 1 ? \"\" : \"s\") + \").\"\n );\n document.getElementById(\"apply-btn\").disabled = true;\n clearPlanExpiry();\n currentPlan = null;\n onMailboxChange();\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Apply failed: \" + err.message);\n });\n});\n\nfunction appendLog(text) {\n var el = document.getElementById(\"log-events\");\n if (el.querySelector(\".empty\")) el.innerHTML = \"\";\n var line = document.createElement(\"div\");\n line.textContent = text;\n el.appendChild(line);\n el.scrollTop = el.scrollHeight;\n}\n\n// Replaces the whole tail on every poll tick (the daemon returns the last\n// N lines, not a delta) — appending it instead would duplicate the log.\nfunction renderLogTail(text) {\n var el = document.getElementById(\"log-tail\");\n el.textContent = text || \"\";\n el.scrollTop = el.scrollHeight;\n}\n\nfunction pollAgentRun(appRunId, startedAt) {\n if (pollHandle) {\n clearTimeout(pollHandle);\n pollHandle = null;\n }\n if (Date.now() - startedAt > POLL_TIMEOUT_MS) {\n setStatus(\"Still running \\\\u2014 check Past runs later.\");\n return;\n }\n callApp(\"app_status\", { appRunId: appRunId })\n .then(function (status) {\n var sessions = (status && status.sessions) || [];\n var sessionId = sessions[0] && sessions[0].sessionId;\n var tail = sessionId\n ? callApp(\"agent_output\", { sessionId: sessionId, lastN: 200, clean: true })\n : Promise.resolve(\"\");\n return tail.then(function (output) {\n renderLogTail(typeof output === \"string\" ? output : output ? JSON.stringify(output, null, 2) : \"\");\n var done = status && (status.status === \"ended\" || status.status === \"done\" || status.endedAt);\n if (!done) {\n pollHandle = setTimeout(function () { pollAgentRun(appRunId, startedAt); }, 2000);\n } else {\n pollHandle = null;\n setStatus(\"Agent run finished.\");\n loadRuns();\n }\n });\n })\n .catch(function (err) {\n setStatus(\"Agent status failed: \" + err.message);\n });\n}\n\ndocument.getElementById(\"run-agent-btn\").addEventListener(\"click\", function () {\n var btn = this;\n if (pollHandle) {\n clearTimeout(pollHandle);\n pollHandle = null;\n }\n document.getElementById(\"log-events\").innerHTML = \"\";\n document.getElementById(\"log-tail\").textContent = \"\";\n setBusy(btn, true);\n setStatus(\"Starting triager agent\\\\u2026\");\n callApp(\"app_run\", { appId: APP_ID, agents: [AGENT_ID], prompt: \"Triage my inbox.\" })\n .then(function (run) {\n setBusy(btn, false);\n var sessions = (run && run.sessions) || [];\n var errors = (run && run.errors) || [];\n if (sessions.length === 0 && errors.length > 0) {\n appendLog(\n \"Run failed to start: \" +\n errors.map(function (e) { return (e.agentId || \"?\") + \": \" + e.error; }).join(\"; \")\n );\n setStatus(\"Run failed to start.\");\n return;\n }\n appendLog(\"Started app run \" + (run && run.appRunId));\n loadRuns();\n pollAgentRun(run.appRunId, Date.now());\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Run failed: \" + err.message);\n });\n});\n\n// window.McpApp is normally injected by the serving layer (postMessage bridge\n// on the MCP resource path, REST bridge on GET /apps/:id/ui). Guard anyway:\n// without it a missing bridge throws here and the panel dies silently on\n// \"Connecting…\" instead of reporting itself.\n(window.McpApp ? window.McpApp.connect() : Promise.reject(new Error(\"window.McpApp bridge missing\")))\n .then(function (bridge) {\n callTool = bridge.callTool;\n updateModelContext = bridge.updateModelContext;\n openLink = bridge.openLink;\n onTeardown = bridge.onTeardown;\n if (onTeardown) {\n onTeardown(function () {\n if (pollHandle) { clearTimeout(pollHandle); pollHandle = null; }\n if (planExpiryHandle) { clearTimeout(planExpiryHandle); planExpiryHandle = null; }\n });\n }\n return Promise.all([probeAliases(), loadRuns()]);\n })\n .catch(function (err) {\n setStatus(\"Standalone mode \\\\u2014 no host bridge (\" + err.message + \")\");\n renderSummary();\n });\n\nrenderSummary();\n</script>\n</body>\n</html>`\n","/**\n * `mail-triage` — a single-agent agentproto app for email triage.\n *\n * One agent (triager) scans inbox, categorizes unread mail, and applies\n * triage actions (label, archive). Proves the APP primitive works end-to-end\n * with a simple, useful real-world task.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { triager } from \"./agents/triager.js\"\nimport { triageInbox } from \"./workflows/triage-inbox.js\"\nimport { MAIL_TRIAGE_HTML, MAIL_TRIAGE_TOOLS } from \"./ui.js\"\n\nexport const mailTriage: AppHandle = defineApp({\n id: \"@agentproto/mail-triage\",\n name: \"Mail Triage\",\n version: \"0.1.0\",\n description: \"Triage your inbox — categorize, label, archive.\",\n agents: [triager],\n workflows: [triageInbox],\n ui: {\n html: MAIL_TRIAGE_HTML,\n title: \"Mail Triage\",\n tools: [...MAIL_TRIAGE_TOOLS],\n },\n})\n\nexport { triager, triageInbox }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\nexport const cataloger: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/media-cataloger\",\n description:\n \"Scans a folder for media files (images, video, audio), collects metadata, and produces a structured catalog.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Read-only — never modify, rename, move, or delete files\",\n \"Only catalog files with recognized media extensions\",\n \"Report exact file sizes and paths, never estimate\",\n ],\n tools: [\"list_dir\", \"read_file\", \"file_info\"],\n workflows: [{ ref: \"scan-media\" }],\n }),\n body: [\n \"You scan a workspace folder for media files and produce a structured JSON catalog.\",\n \"\",\n \"Walk the target directory recursively using list_dir. For each file, check the\",\n \"extension against known media types:\",\n \"- Images: .jpg, .jpeg, .png, .gif, .webp, .svg, .bmp, .ico, .avif\",\n \"- Video: .mp4, .webm, .mov, .avi, .mkv\",\n \"- Audio: .mp3, .wav, .ogg, .flac, .aac, .m4a\",\n \"\",\n \"For each media file found, use file_info to get size and modification date.\",\n \"\",\n \"Output a JSON object with this shape:\",\n \"```json\",\n '{',\n ' \"folder\": \"<scanned path>\",',\n ' \"scannedAt\": \"<ISO timestamp>\",',\n ' \"summary\": { \"total\": N, \"images\": N, \"video\": N, \"audio\": N, \"totalSizeBytes\": N },',\n ' \"files\": [',\n ' {',\n ' \"path\": \"relative/path.jpg\",',\n ' \"name\": \"file.jpg\",',\n ' \"type\": \"image\",',\n ' \"extension\": \".jpg\",',\n ' \"sizeBytes\": 123456,',\n ' \"modifiedAt\": \"2026-08-01T12:00:00Z\"',\n \" }\",\n \" ]\",\n \"}\",\n \"```\",\n \"\",\n \"Sort files by type (images first, then video, then audio), then by name.\",\n ].join(\"\\n\"),\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\nexport const scanMedia = defineWorkflow({\n id: \"scan-media\",\n name: \"Scan Media\",\n description:\n \"Scan a folder for media files, collect metadata, and produce a structured catalog.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"catalog\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/media-cataloger\" },\n prompt:\n \"Scan the workspace root folder for media files (images, video, audio). \" +\n \"Walk all subdirectories recursively. For each media file, collect its path, \" +\n \"name, type, extension, size, and modification date. Output the full catalog \" +\n \"as a single JSON object.\",\n },\n ],\n})\n","/**\n * Media Viewer — a self-contained MCP App gallery widget for `media-viewer`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }`. `app_tool_call`\n * double-wraps its dispatch result (it returns a text-result whose `text` is\n * the JSON-stringified inner tool result, itself an MCP content array) — see\n * `unwrapText` below, which peels that apart regardless of how many layers\n * the host's bridge already stripped.\n */\n\nexport const MEDIA_VIEWER_TOOLS = [\"directory_list\", \"file_info\", \"file_read\"] as const\n\nconst APP_ID = \"@agentproto/media-viewer\"\n\nexport const MEDIA_VIEWER_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Media Viewer</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%}\n#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;background:var(--bg2);flex-shrink:0}\n#toolbar .title{font-size:13px;font-weight:600;flex:1}\n#breadcrumb{font-size:12px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n#body{flex:1;display:flex;overflow:hidden}\n#grid-wrap{flex:1;overflow-y:auto;padding:14px}\n#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}\n.cell{display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);cursor:pointer;text-align:center}\n.cell:hover{background:var(--bg3);border-color:var(--blue)}\n.cell.selected{border-color:var(--blue);background:var(--bg3)}\n.icon{font-size:28px;line-height:1}\n.name{font-size:11px;color:var(--text);word-break:break-word;max-width:100%;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n#empty{color:var(--text2);padding:24px;text-align:center}\n#details{width:280px;border-left:1px solid var(--border);background:var(--bg2);padding:14px;overflow-y:auto;flex-shrink:0}\n#details h3{font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}\n#details .row{display:flex;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--border);font-size:12px}\n#details .row span:first-child{color:var(--text2)}\n#preview{margin-top:10px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow-y:auto}\n#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <button class=\"abtn\" id=\"up-btn\" title=\"Up one level\">↑</button>\n <span class=\"title\">Media Viewer</span>\n <span id=\"breadcrumb\">.</span>\n <button class=\"abtn\" id=\"refresh-btn\">Refresh</button>\n </div>\n <div id=\"body\">\n <div id=\"grid-wrap\">\n <div id=\"grid\"></div>\n <div id=\"empty\" style=\"display:none\">No files in this folder.</div>\n </div>\n <div id=\"details\" style=\"display:none\"></div>\n </div>\n <div id=\"status\">Connecting…</div>\n</div>\n<script>\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar currentPath = \".\";\nvar entries = [];\nvar callTool = null;\n\nfunction setStatus(msg) {\n document.getElementById(\"status\").textContent = msg;\n}\n\n// app_tool_call wraps its dispatch result at least once (its own\n// text-result body is the JSON-stringified inner MCP tool result); peel\n// back through nested {content:[{type:\"text\",text:...}]} shells until the\n// payload stops looking like one, so this works whichever layer the host's\n// own bridge already unwrapped.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try {\n cur = JSON.parse(cur);\n continue;\n } catch (e) {\n return cur;\n }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction parseDirectoryListing(raw) {\n var text = typeof raw === \"string\" ? raw : \"\";\n return text\n .split(\"\\\\n\")\n .map(function (line) { return line.trim(); })\n .filter(function (line) { return line.length > 0; })\n .map(function (line) {\n var isDir = line.indexOf(\"[DIR]\") === 0;\n var name = line.replace(/^\\\\[(FILE|DIR)\\\\]\\\\s*/, \"\");\n return { name: name, isDir: isDir };\n });\n}\n\nfunction iconFor(entry) {\n if (entry.isDir) return \"\\\\uD83D\\\\uDCC1\";\n var ext = (entry.name.split(\".\").pop() || \"\").toLowerCase();\n if ([\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"svg\", \"bmp\", \"ico\", \"avif\"].indexOf(ext) !== -1) return \"\\\\uD83D\\\\uDDBC\\\\uFE0F\";\n if ([\"mp4\", \"webm\", \"mov\", \"avi\", \"mkv\"].indexOf(ext) !== -1) return \"\\\\uD83C\\\\uDFA5\";\n if ([\"mp3\", \"wav\", \"ogg\", \"flac\", \"aac\", \"m4a\"].indexOf(ext) !== -1) return \"\\\\uD83C\\\\uDFB5\";\n return \"\\\\uD83D\\\\uDCC4\";\n}\n\nfunction joinPath(base, name) {\n if (base === \".\" || base === \"\") return name;\n return base.replace(/\\\\/+$/, \"\") + \"/\" + name;\n}\n\nfunction parentPath(path) {\n if (path === \".\" || path.indexOf(\"/\") === -1) return \".\";\n return path.slice(0, path.lastIndexOf(\"/\")) || \".\";\n}\n\nfunction renderGrid() {\n document.getElementById(\"breadcrumb\").textContent = currentPath;\n var grid = document.getElementById(\"grid\");\n var empty = document.getElementById(\"empty\");\n grid.innerHTML = \"\";\n if (entries.length === 0) {\n empty.style.display = \"block\";\n return;\n }\n empty.style.display = \"none\";\n entries.forEach(function (entry, idx) {\n var cell = document.createElement(\"div\");\n cell.className = \"cell\";\n cell.dataset.idx = String(idx);\n cell.innerHTML = '<div class=\"icon\">' + iconFor(entry) + '</div><div class=\"name\"></div>';\n cell.querySelector(\".name\").textContent = entry.name;\n cell.addEventListener(\"click\", function () { onSelect(entry, cell); });\n grid.appendChild(cell);\n });\n}\n\nfunction onSelect(entry, cell) {\n Array.prototype.forEach.call(document.querySelectorAll(\".cell\"), function (c) {\n c.classList.remove(\"selected\");\n });\n cell.classList.add(\"selected\");\n if (entry.isDir) {\n loadDirectory(joinPath(currentPath, entry.name));\n return;\n }\n showFileDetails(joinPath(currentPath, entry.name));\n}\n\nfunction showFileDetails(path) {\n var details = document.getElementById(\"details\");\n details.style.display = \"block\";\n details.innerHTML = \"<h3>Loading…</h3>\";\n setStatus(\"Reading \" + path + \"\\\\u2026\");\n callApp(\"file_info\", { path: path })\n .then(function (info) {\n var rows = \"\";\n if (info && typeof info === \"object\") {\n [\"name\", \"type\", \"size\", \"modified\"].forEach(function (key) {\n if (info[key] !== undefined) {\n rows += '<div class=\"row\"><span>' + key + '</span><span></span></div>';\n }\n });\n }\n details.innerHTML = \"<h3>Details</h3>\" + rows + '<div id=\"preview\">Loading preview…</div>';\n var rowEls = details.querySelectorAll(\".row span:last-child\");\n var keys = [\"name\", \"type\", \"size\", \"modified\"].filter(function (key) {\n return info && info[key] !== undefined;\n });\n keys.forEach(function (key, i) {\n rowEls[i].textContent = String(info[key]);\n });\n return callApp(\"file_read\", { path: path });\n })\n .then(function (content) {\n var pre = document.getElementById(\"preview\");\n if (!pre) return;\n var text = typeof content === \"string\" ? content : JSON.stringify(content);\n pre.textContent = text.length > 2000 ? text.slice(0, 2000) + \"\\\\u2026 (truncated)\" : (text || \"(empty file)\");\n setStatus(\"Ready \\\\u00b7 \" + currentPath);\n })\n .catch(function (err) {\n var pre = document.getElementById(\"preview\");\n if (pre) pre.textContent = \"Preview unavailable (\" + err.message + \")\";\n setStatus(\"Ready \\\\u00b7 \" + currentPath);\n });\n}\n\nfunction loadDirectory(path) {\n document.getElementById(\"details\").style.display = \"none\";\n setStatus(\"Loading \" + path + \"\\\\u2026\");\n return callApp(\"directory_list\", { path: path })\n .then(function (raw) {\n currentPath = path;\n entries = parseDirectoryListing(raw).sort(function (a, b) {\n if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n renderGrid();\n setStatus(\"Ready \\\\u00b7 \" + entries.length + \" entries \\\\u00b7 \" + path);\n })\n .catch(function (err) {\n entries = [];\n renderGrid();\n setStatus(\"Error: \" + err.message);\n });\n}\n\ndocument.getElementById(\"up-btn\").addEventListener(\"click\", function () {\n loadDirectory(parentPath(currentPath));\n});\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadDirectory(currentPath);\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n return loadDirectory(\".\");\n })\n .catch(function (err) {\n setStatus(\"Standalone mode \\\\u2014 no host bridge (\" + err.message + \")\");\n entries = [];\n renderGrid();\n });\n</script>\n</body>\n</html>`\n","/**\n * `media-viewer` — a single-agent agentproto app for media cataloging.\n *\n * One agent (cataloger) scans a workspace folder for images, video, and audio\n * files, collects metadata, and produces a structured JSON catalog. Paired\n * with a Cowork McpApp gallery widget for visual browsing.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { cataloger } from \"./agents/cataloger.js\"\nimport { scanMedia } from \"./workflows/scan-media.js\"\nimport { MEDIA_VIEWER_HTML, MEDIA_VIEWER_TOOLS } from \"./ui.js\"\n\nexport const mediaViewer: AppHandle = defineApp({\n id: \"@agentproto/media-viewer\",\n name: \"Media Viewer\",\n version: \"0.1.0\",\n description: \"Scan a folder for images, video, and audio — gallery view.\",\n agents: [cataloger],\n workflows: [scanMedia],\n ui: {\n html: MEDIA_VIEWER_HTML,\n title: \"Media Viewer\",\n tools: [...MEDIA_VIEWER_TOOLS],\n },\n})\n\nexport { cataloger, scanMedia }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Sessions Manager — the durable \"master of the sessions\" coordinator:\n * the human talks to IT, it talks to the other sessions. Distilled from the\n * hand-run plateau-coordinator session this app productizes.\n *\n * Where the watchdog only OBSERVES (and never touches a live session), the\n * manager DRIVES: it re-prompts idle sessions onto their task, restarts dead\n * ones, and answers \"where is X at?\" by reading transcripts. The Ops Panel\n * spawns it via `agent_start` with {@link MANAGER_LABEL} and `keepAlive`, so\n * the idle-reaper never retires a legitimately-parked coordinator.\n */\n\n/** Session label the panel spawns under — how a live manager is recognized. */\nexport const MANAGER_LABEL = \"Sessions Manager\"\n\nexport const manager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/sessions-manager\",\n description:\n \"Durable coordinator over the daemon's sessions — routes questions to the right session, re-prompts stalled work, restarts dead sessions, reports status.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Never kill a session unless the human explicitly asks for that session by id/label\",\n \"Never merge PRs or bypass a repo's declared review flow from a supervised session\",\n \"Prefer re-prompting an existing session over spawning a duplicate for the same task\",\n ],\n tools: [\n \"session_list\",\n \"conversation_read\",\n \"agent_prompt\",\n \"session_restart\",\n \"agent_start\",\n \"session_events_poll\",\n ],\n }),\n body: [\n \"You are the manager of this agentproto daemon's sessions: the human talks to you, and you talk\",\n \"to the other sessions. You are durable — the same session across many turns — so keep a working\",\n \"memory of who is doing what and don't re-derive it from scratch every turn.\",\n \"\",\n \"What you do:\",\n \"- Locate: given a task or question, find the relevant session (`session_list`, labels, cwd,\",\n \" recency) and read its transcript (`conversation_read`) before answering — never guess status.\",\n \"- Drive: re-prompt an idle/parked session onto its task with `agent_prompt`; restart a\",\n \" killed/exited session with `session_restart` when its work is clearly unfinished.\",\n \"- Spawn: only when no existing session owns the task — check for duplicates first.\",\n \"- Report: answer with short, concrete status (who, doing what, since when, blocked on what).\",\n \"\",\n \"Boundaries:\",\n \"- Never kill a session unless the human explicitly asks for that session.\",\n \"- A session's own repo conventions (review flow, merge policy) always win — you coordinate,\",\n \" you don't override.\",\n \"- If a session awaits a human decision, surface the question to the human instead of answering\",\n \" in their place.\",\n ].join(\"\\n\"),\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Session Watchdog — a durable daemon-health session, re-prompted on a\n * cron tick (~every 5 minutes). Distilled from the hand-run watchdog session\n * this app productizes: same classification rules, same hard boundaries\n * (never kills a live session, never spawns).\n *\n * The Ops Panel spawns it via `agent_start` with {@link WATCHDOG_LABEL} and\n * wires a `prompt-session` cron ({@link WATCHDOG_TICK_LABEL} /\n * {@link WATCHDOG_TICK_SCHEDULE}) pointing at the spawned session — see\n * `ui.ts`. The agent is also usable standalone via `app_run` for a one-shot\n * health pass.\n */\n\n/** Session label the panel spawns under — how a live watchdog is recognized. */\nexport const WATCHDOG_LABEL = \"Session Watchdog\"\n\n/** Label of the cron job that re-prompts the watchdog session. */\nexport const WATCHDOG_TICK_LABEL = \"watchdog-tick\"\n\n/** Every 5 minutes. */\nexport const WATCHDOG_TICK_SCHEDULE = \"*/5 * * * *\"\n\n/** The prompt each cron tick sends to the durable session. */\nexport const WATCHDOG_TICK_PROMPT = \"Run your maintenance check now.\"\n\nexport const watchdog: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-watchdog\",\n description:\n \"Durable daemon-health watchdog — classifies sessions (needs-you, stalled, duplicated, abandonable), corrects flags, archives dead rows. Never kills or spawns.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"NEVER kill a live session, message/interrupt another session's conversation, or spawn new sessions\",\n \"Housekeeping only on terminal-status sessions (exited/killed/error) — never touches a live one\",\n \"Report only what's NEW since the last tick — no padding, no repeats unless something got worse\",\n ],\n tools: [\n \"session_list\",\n \"session_events_poll\",\n \"session_flag_status\",\n \"session_archive\",\n \"session_gc\",\n ],\n }),\n body: [\n \"You are the Session Watchdog for this agentproto daemon. You run on a recurring cron tick\",\n \"(~every 5 minutes) as the SAME durable session, re-prompted each time — you retain memory of\",\n \"what you already flagged, so don't repeat an alert unless something got meaningfully worse or new.\",\n \"\",\n \"Each tick:\",\n \"1. Call `session_list` to see current session state (status, awaitingInput/awaitingQuestion,\",\n \" stalledSinceMs, busy, blockedOn, childrenBusy).\",\n \"2. Call `session_events_poll` for events since your last tick (stalled, exited, awaiting-input)\",\n \" to catch anything between ticks.\",\n \"3. Classify:\",\n \" - Needs you: `awaitingInput: true` with a real unanswered question, older than ~10 minutes.\",\n \" - Stalled: busy but silent well past normal turn latency, no legitimate blockedOn excuse.\",\n \" - Possibly duplicated: two or more live sessions share workspace/cwd and a highly similar\",\n \" recent prompt/label — use judgment, not exact string matching.\",\n \" - Looks done/abandonable: idle/parked with no plausible remaining purpose.\",\n \"4. Correct misclassifications when confident: if `session_flag_status` is available and you can\",\n \" tell a session is actually waiting on a human (or isn't, despite being flagged), call it with\",\n \" a clear `reason`. If that tool is unknown on this daemon build, skip silently.\",\n \"5. Autonomous housekeeping (reversible, so no need to ask): `session_archive` or `session_gc` on\",\n \" sessions already terminal-status (exited/killed/error) and old. Never touches a live session.\",\n \"6. NEVER: kill a live session, message/interrupt another session's conversation, or spawn new\",\n \" sessions. You only have these five tools on purpose — if you'd want another one, report the\",\n \" suggestion instead of working around it.\",\n \"7. Report only what's NEW since last tick — one line per finding. If nothing new, reply with a\",\n \" short \\\"nothing new\\\" — don't pad it.\",\n \"\",\n \"You are not a general coding assistant across ticks — stay scoped to this watchdog role.\",\n ].join(\"\\n\"),\n}\n","/**\n * Ops Panel — a self-contained MCP App panel for `ops-panel`.\n *\n * The daemon operations cockpit: special durable sessions (Session Watchdog,\n * Sessions Manager) as launchable cards, the session list filterable by\n * workspace with lifecycle actions, housekeeping (session GC, worktree GC)\n * and the cron ledger. Talks to the daemon only through the\n * `window.McpApp.connect()` bridge — every action round-trips through\n * `app_tool_call { appId, tool, args }`, gated by {@link OPS_PANEL_TOOLS}\n * (same convention as `session-viewer`).\n *\n * Special-session recognition is by spawn label (`Session Watchdog` /\n * `Sessions Manager` — see `agents/`): the panel finds the newest session\n * wearing the label, shows its status, and offers Launch / Restart / Kill.\n * Launching the watchdog also (re)wires its `watchdog-tick` cron — the\n * `prompt-session` job that re-prompts the durable session every 5 minutes —\n * deleting any stale tick first so a dead session id never keeps a failing\n * cron behind.\n */\n\nimport { manager, MANAGER_LABEL } from \"./agents/manager.js\"\nimport {\n watchdog,\n WATCHDOG_LABEL,\n WATCHDOG_TICK_LABEL,\n WATCHDOG_TICK_PROMPT,\n WATCHDOG_TICK_SCHEDULE,\n} from \"./agents/watchdog.js\"\n\nexport const OPS_PANEL_TOOLS = [\n \"daemon_health\",\n \"session_list\",\n \"agent_start\",\n \"agent_prompt\",\n \"agent_kill\",\n \"session_restart\",\n \"session_archive\",\n \"session_gc\",\n \"cron_list\",\n \"cron_create\",\n \"cron_delete\",\n \"cron_run\",\n \"worktree_gc\",\n] as const\n\nconst APP_ID = \"@agentproto/ops-panel\"\n\n/** What the panel needs to launch each special session, injected below. */\nconst SPECIALS = [\n {\n key: \"watchdog\",\n label: WATCHDOG_LABEL,\n tagline: \"Health ticks every 5 min — flags, archives, never kills.\",\n prompt: watchdog.body ?? \"\",\n cron: {\n label: WATCHDOG_TICK_LABEL,\n schedule: WATCHDOG_TICK_SCHEDULE,\n prompt: WATCHDOG_TICK_PROMPT,\n },\n },\n {\n key: \"manager\",\n label: MANAGER_LABEL,\n tagline: \"You talk to it, it drives the other sessions.\",\n prompt: manager.body ?? \"\",\n cron: null,\n },\n] as const\n\nexport const OPS_PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Ops Panel</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--text3);flex:none}\n.chip.ok .d{background:var(--green)}\n.chip.bad .d{background:var(--red)}\n#ws-select{margin-left:auto;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11.5px;font-family:inherit}\n#refresh-btn{background:none;border:1px solid var(--border);cursor:pointer;color:var(--text2);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:16px}\n.sect h2{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;display:flex;align-items:center;gap:8px}\n.sect h2 .count{font-weight:600;color:var(--text3);text-transform:none;letter-spacing:0}\n\n#specials{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px}\n.card .ct{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700}\n.card .tagline{font-size:11px;color:var(--text2);margin-top:3px}\n.card .meta{font-size:10.5px;color:var(--text3);margin-top:7px;display:flex;flex-wrap:wrap;gap:5px 10px}\n.card .cronline{font-size:10.5px;margin-top:7px;display:flex;align-items:center;gap:6px;color:var(--text2)}\n.card .actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}\n\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-busy{background:rgba(63,185,80,.25);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n.b-warn{background:rgba(210,153,34,.15);color:var(--yellow)}\n\nbutton.act{background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;cursor:pointer;font-family:inherit}\nbutton.act:hover{border-color:var(--text3)}\nbutton.act.primary{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.4);color:var(--blue)}\nbutton.act.danger{color:var(--red)}\nbutton.act.danger:hover{border-color:var(--red)}\nbutton.act.confirm{background:rgba(248,81,73,.2);border-color:var(--red);color:var(--red)}\nbutton.act:disabled{opacity:.45;cursor:default}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--border)}\ntd{font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--bg3);vertical-align:middle}\ntr:hover td{background:var(--bg2)}\ntd .sn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;display:block}\ntd .sid{font-size:10px;color:var(--text3);font-family:Menlo,Monaco,monospace}\n.ws-tag{display:inline-block;font-size:10px;font-weight:600;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:1px 6px;color:var(--text2)}\ntd.actions{white-space:nowrap;text-align:right}\ntd.actions button{margin-left:4px}\n\n.hk{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.hk .card .nums{display:flex;gap:14px;margin-top:8px}\n.hk .num{display:flex;flex-direction:column}\n.hk .num b{font-size:17px;font-weight:700}\n.hk .num span{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.04em}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--red);color:var(--red)}\n.empty{color:var(--text3);font-size:11.5px;padding:8px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>Agentproto Ops</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon…</span></span>\n <select id=\"ws-select\"><option value=\"\">all workspaces</option></select>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻ refresh</button>\n </div>\n <div id=\"body\">\n <div class=\"sect\">\n <h2>Special sessions</h2>\n <div id=\"specials\"></div>\n </div>\n <div class=\"sect\">\n <h2>Sessions <span class=\"count\" id=\"sess-count\"></span></h2>\n <div id=\"sessions\"></div>\n </div>\n <div class=\"sect\">\n <h2>Housekeeping</h2>\n <div class=\"hk\" id=\"housekeeping\"></div>\n </div>\n <div class=\"sect\">\n <h2>Crons <span class=\"count\" id=\"cron-count\"></span></h2>\n <div id=\"crons\"></div>\n </div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SPECIALS = ${JSON.stringify(SPECIALS)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm: first click arms the button, second within 4s fires.\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar crons = [];\nvar wsFilter = \"\";\nvar pollTimer = null;\nvar busyActions = false;\n\nfunction isLive(s) { return !!s && (s.status === \"running\" || s.status === \"starting\"); }\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-busy\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction labelOf(s) {\n return s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id;\n}\n\nfunction specialSessionFor(sp) {\n var best = null;\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if ((s.label || s.title) !== sp.label) continue;\n if (!best) { best = s; continue; }\n // Prefer a live one; among equals prefer the newest.\n if (isLive(s) && !isLive(best)) { best = s; continue; }\n if (isLive(s) === isLive(best) && String(s.startedAt) > String(best.startedAt)) best = s;\n }\n return best;\n}\n\nfunction cronByLabel(label) {\n var hits = [];\n for (var i = 0; i < crons.length; i++) if (crons[i].label === label) hits.push(crons[i]);\n return hits;\n}\n\n// ============================================================\n// data loading\n// ============================================================\n\nfunction loadAll() {\n return Promise.all([\n callApp(\"session_list\", { kind: \"all\" }).then(function (d) {\n sessions = (d && d.sessions) || (Array.isArray(d) ? d : []);\n }),\n callApp(\"cron_list\", {}).then(function (d) {\n crons = Array.isArray(d) ? d : (d && d.jobs) || [];\n }).catch(function () { crons = []; }),\n ]).then(function () {\n renderWorkspaces();\n renderSpecials();\n renderSessions();\n renderCrons();\n updateHousekeepingNums();\n });\n}\n\nfunction loadHealth() {\n return callApp(\"daemon_health\", {}).then(function (d) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip ok\";\n var v = (d && (d.version || (d.build && d.build.sha) || d.status)) || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n }).catch(function (e) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n });\n}\n\n// Housekeeping card is built ONCE (so the worktree plan text and armed\n// confirm buttons survive the 5s poll); only the counters re-render — see\n// updateHousekeepingNums. The worktree plan is a separate dry-run call, on\n// demand only (it walks git state — not something to hammer on every poll).\nfunction buildHousekeeping() {\n var el = document.getElementById(\"housekeeping\");\n el.innerHTML =\n '<div class=\"card\"><div class=\"ct\">Sessions GC</div>'\n + '<div class=\"nums\" id=\"gc-nums\"></div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"gc-archive\">Archive >7d</button>'\n + '<button class=\"act danger\" id=\"gc-forget\">Forget >30d</button>'\n + '</div></div>'\n + '<div class=\"card\"><div class=\"ct\">Worktrees</div>'\n + '<div class=\"meta\" id=\"wt-plan\">No plan yet — run a dry run.</div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"wt-dry\">Dry run</button>'\n + '<button class=\"act danger\" id=\"wt-apply\" disabled>Reclaim now</button>'\n + '</div></div>';\n\n document.getElementById(\"gc-archive\").addEventListener(\"click\", function () {\n runAction(\"session_gc\", { olderThanDays: 7 }, \"GC: archived old terminal sessions\");\n });\n var forgetBtn = document.getElementById(\"gc-forget\");\n forgetBtn.addEventListener(\"click\", function () {\n confirmClick(\"gc-forget\", forgetBtn, function () {\n runAction(\"session_gc\", { olderThanDays: 30, forget: true }, \"GC: forgot >30d terminal sessions\");\n });\n });\n document.getElementById(\"wt-dry\").addEventListener(\"click\", function () { worktreePlan(false); });\n var applyBtn = document.getElementById(\"wt-apply\");\n applyBtn.addEventListener(\"click\", function () {\n confirmClick(\"wt-apply\", applyBtn, function () { worktreePlan(true); });\n });\n updateHousekeepingNums();\n}\n\nfunction updateHousekeepingNums() {\n var el = document.getElementById(\"gc-nums\");\n if (!el) return;\n var terminal = 0, live = 0;\n for (var i = 0; i < sessions.length; i++) {\n if (isLive(sessions[i])) live++;\n else terminal++;\n }\n el.innerHTML =\n '<div class=\"num\"><b>' + live + '</b><span>live</span></div>'\n + '<div class=\"num\"><b>' + terminal + '</b><span>terminal</span></div>';\n}\n\nfunction worktreePlan(apply) {\n var el = document.getElementById(\"wt-plan\");\n el.textContent = apply ? \"reclaiming\\\\u2026\" : \"planning\\\\u2026\";\n callApp(\"worktree_gc\", apply ? { apply: true } : {}).then(function (d) {\n var entries = (d && (d.plan || d.entries || d.worktrees)) || [];\n var counts = { reclaim: 0, salvage: 0, hold: 0 };\n for (var i = 0; i < entries.length; i++) {\n var c = entries[i].classification || entries[i].class || entries[i].action;\n if (counts[c] != null) counts[c]++;\n }\n el.textContent = (apply ? \"applied — \" : \"dry run — \")\n + counts.reclaim + \" reclaim / \" + counts.salvage + \" salvage / \" + counts.hold + \" hold\"\n + (entries.length === 0 ? \" (no linked worktrees)\" : \"\");\n document.getElementById(\"wt-apply\").disabled = counts.reclaim === 0 || apply;\n if (apply) toast(\"Worktree GC applied\");\n }).catch(function (e) {\n el.textContent = \"worktree_gc: \" + e.message;\n });\n}\n\n// ============================================================\n// rendering\n// ============================================================\n\nfunction renderWorkspaces() {\n var sel = document.getElementById(\"ws-select\");\n var seen = {};\n var slugs = [];\n for (var i = 0; i < sessions.length; i++) {\n var w = sessions[i].workspaceSlug;\n if (w && !seen[w]) { seen[w] = true; slugs.push(w); }\n }\n slugs.sort();\n var cur = wsFilter;\n var html = '<option value=\"\">all workspaces</option>';\n for (var j = 0; j < slugs.length; j++) {\n html += '<option value=\"' + escHtml(slugs[j]) + '\"' + (slugs[j] === cur ? \" selected\" : \"\") + '>' + escHtml(slugs[j]) + \"</option>\";\n }\n sel.innerHTML = html;\n}\n\nfunction renderSpecials() {\n var el = document.getElementById(\"specials\");\n var html = \"\";\n for (var i = 0; i < SPECIALS.length; i++) {\n var sp = SPECIALS[i];\n var s = specialSessionFor(sp);\n var badge = s ? displayBadge(s) : { label: \"not running\", cls: \"b-end\" };\n var meta = \"\";\n if (s) {\n meta += \"<span>\" + escHtml(s.id) + \"</span>\";\n if (s.startedAt) meta += \"<span>started \" + escHtml(fmtRelative(s.startedAt)) + \"</span>\";\n if (s.costUsd != null) meta += \"<span>$\" + Number(s.costUsd).toFixed(2) + \"</span>\";\n }\n var cronline = \"\";\n if (sp.cron) {\n var jobs = cronByLabel(sp.cron.label);\n if (jobs.length === 0) {\n cronline = '<span class=\"badge b-end\">no cron</span> tick not scheduled';\n } else {\n var job = jobs[0];\n var ok = !job.lastResult || job.lastResult.ok;\n var pointsAt = job.action && job.action.sessionId;\n var stale = s && isLive(s) && pointsAt && pointsAt !== s.id;\n cronline = '<span class=\"badge ' + (job.active ? (ok && !stale ? \"b-run\" : \"b-warn\") : \"b-end\") + '\">'\n + (job.active ? (ok && !stale ? \"tick ok\" : (stale ? \"tick stale\" : \"tick failing\")) : \"tick off\") + \"</span> \"\n + escHtml(job.schedule) + (pointsAt ? \" → \" + escHtml(pointsAt) : \"\")\n + (!ok && job.lastResult && job.lastResult.summary ? \" · \" + escHtml(String(job.lastResult.summary).slice(0, 60)) : \"\");\n }\n }\n var actions = \"\";\n if (!s || !isLive(s)) {\n actions += '<button class=\"act primary\" data-sp=\"' + sp.key + '\" data-do=\"launch\">Launch</button>';\n if (s) actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"restart\">Restart last</button>';\n } else {\n if (sp.cron) {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Tick now</button>';\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"fixcron\">Re-wire cron</button>';\n } else {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Ping</button>';\n }\n actions += '<button class=\"act danger\" data-sp=\"' + sp.key + '\" data-do=\"kill\">Kill</button>';\n }\n html += '<div class=\"card\">'\n + '<div class=\"ct\">' + escHtml(sp.label) + ' <span class=\"badge ' + badge.cls + '\">' + escHtml(badge.label) + \"</span></div>\"\n + '<div class=\"tagline\">' + escHtml(sp.tagline) + \"</div>\"\n + (meta ? '<div class=\"meta\">' + meta + \"</div>\" : \"\")\n + (cronline ? '<div class=\"cronline\">' + cronline + \"</div>\" : \"\")\n + '<div class=\"actions\">' + actions + \"</div>\"\n + \"</div>\";\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-sp]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { specialAction(btn.getAttribute(\"data-sp\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderSessions() {\n var el = document.getElementById(\"sessions\");\n var rows = [];\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if (wsFilter && s.workspaceSlug !== wsFilter) continue;\n rows.push(s);\n }\n rows.sort(function (a, b) {\n var la = isLive(a) ? 1 : 0, lb = isLive(b) ? 1 : 0;\n if (la !== lb) return lb - la;\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n var liveCount = 0;\n for (var c = 0; c < rows.length; c++) if (isLive(rows[c])) liveCount++;\n document.getElementById(\"sess-count\").textContent = liveCount + \" live / \" + rows.length + \" shown\";\n if (rows.length === 0) {\n el.innerHTML = '<div class=\"empty\">No sessions' + (wsFilter ? \" in \" + escHtml(wsFilter) : \"\") + \".</div>\";\n return;\n }\n var html = \"<table><thead><tr><th>Session</th><th>Workspace</th><th>Status</th><th>Started</th><th>Cost</th><th></th></tr></thead><tbody>\";\n var shown = rows.slice(0, 80);\n for (var j = 0; j < shown.length; j++) {\n var s2 = shown[j];\n var db = displayBadge(s2);\n var acts = \"\";\n if (isLive(s2)) {\n acts += '<button class=\"act danger\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"kill\">Kill</button>';\n } else {\n acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"restart\">Restart</button>';\n if (!s2.archived) acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"archive\">Archive</button>';\n }\n html += \"<tr>\"\n + '<td><span class=\"sn\" title=\"' + escHtml(labelOf(s2)) + '\">' + escHtml(labelOf(s2)) + '</span><span class=\"sid\">' + escHtml(s2.id) + \"</span></td>\"\n + \"<td>\" + (s2.workspaceSlug ? '<span class=\"ws-tag\">' + escHtml(s2.workspaceSlug) + \"</span>\" : \"\") + \"</td>\"\n + '<td><span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span></td>\"\n + \"<td>\" + escHtml(fmtRelative(s2.startedAt)) + \"</td>\"\n + \"<td>\" + (s2.costUsd != null ? \"$\" + Number(s2.costUsd).toFixed(2) : \"\") + \"</td>\"\n + '<td class=\"actions\">' + acts + \"</td>\"\n + \"</tr>\";\n }\n html += \"</tbody></table>\";\n if (rows.length > shown.length) html += '<div class=\"empty\">… ' + (rows.length - shown.length) + \" more (narrow with the workspace filter)</div>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-id]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { sessionAction(btn.getAttribute(\"data-id\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderCrons() {\n var el = document.getElementById(\"crons\");\n document.getElementById(\"cron-count\").textContent = crons.length ? crons.length + \" jobs\" : \"\";\n if (crons.length === 0) {\n el.innerHTML = '<div class=\"empty\">No cron jobs.</div>';\n return;\n }\n var html = \"<table><thead><tr><th>Label</th><th>Schedule</th><th>State</th><th>Last</th><th>Next</th><th></th></tr></thead><tbody>\";\n for (var i = 0; i < crons.length; i++) {\n var c = crons[i];\n var last = c.lastResult == null ? \"\" : (c.lastResult.ok ? \"ok\" : \"failed\");\n var lastCls = c.lastResult == null ? \"b-end\" : (c.lastResult.ok ? \"b-run\" : \"b-err\");\n html += \"<tr>\"\n + '<td><span class=\"sn\">' + escHtml(c.label || c.id) + \"</span></td>\"\n + \"<td>\" + escHtml(c.schedule) + \"</td>\"\n + '<td><span class=\"badge ' + (c.active ? \"b-run\" : \"b-end\") + '\">' + (c.active ? \"active\" : \"off\") + \"</span></td>\"\n + \"<td>\" + (last ? '<span class=\"badge ' + lastCls + '\" title=\"' + escHtml((c.lastResult && c.lastResult.summary) || \"\") + '\">' + last + \"</span>\" : \"\") + \"</td>\"\n + \"<td>\" + escHtml(c.nextRunAt ? fmtRelative(c.nextRunAt).replace(\" ago\", \"\") : \"\") + \"</td>\"\n + '<td class=\"actions\">'\n + '<button class=\"act\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"run\">Run</button>'\n + '<button class=\"act danger\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"delete\">Delete</button>'\n + \"</td></tr>\";\n }\n html += \"</tbody></table>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-cron]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { cronAction(btn.getAttribute(\"data-cron\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\n// ============================================================\n// actions\n// ============================================================\n\nfunction runAction(tool, args, okMsg) {\n if (busyActions) return Promise.resolve();\n busyActions = true;\n return callApp(tool, args).then(function (d) {\n busyActions = false;\n if (okMsg) toast(okMsg);\n return loadAll().then(function () { return d; });\n }).catch(function (e) {\n busyActions = false;\n toast(tool + \": \" + e.message, true);\n throw e;\n });\n}\n\nfunction findSpecial(key) {\n for (var i = 0; i < SPECIALS.length; i++) if (SPECIALS[i].key === key) return SPECIALS[i];\n return null;\n}\n\nfunction rewireTickCron(sp, sessionId) {\n if (!sp.cron) return Promise.resolve();\n var stale = cronByLabel(sp.cron.label);\n var chain = Promise.resolve();\n for (var i = 0; i < stale.length; i++) {\n (function (jobId) {\n chain = chain.then(function () { return callApp(\"cron_delete\", { jobId: jobId }); });\n })(stale[i].id);\n }\n return chain.then(function () {\n return callApp(\"cron_create\", {\n schedule: sp.cron.schedule,\n recurring: true,\n label: sp.cron.label,\n action: { kind: \"prompt-session\", sessionId: sessionId, prompt: sp.cron.prompt },\n });\n });\n}\n\nfunction specialAction(key, what, btn) {\n var sp = findSpecial(key);\n if (!sp) return;\n var s = specialSessionFor(sp);\n if (what === \"launch\") {\n btn.disabled = true;\n callApp(\"agent_start\", {\n adapter: \"claude-code\",\n label: sp.label,\n prompt: sp.prompt,\n keepAlive: true,\n }).then(function (d) {\n var id = d && d.id;\n toast(sp.label + \" launched\" + (id ? \" (\" + id + \")\" : \"\"));\n return id && sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"launch: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"restart\" && s) {\n btn.disabled = true;\n callApp(\"session_restart\", { idOrName: s.id }).then(function (d) {\n var id = (d && d.id) || s.id;\n toast(sp.label + \" restarted\");\n return sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"restart: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"tick\" && s) {\n var prompt = sp.cron ? sp.cron.prompt : \"Status check: report where things stand, briefly.\";\n runAction(\"agent_prompt\", { sessionId: s.id, prompt: prompt }, sp.label + \" prompted\");\n return;\n }\n if (what === \"fixcron\" && s) {\n btn.disabled = true;\n rewireTickCron(sp, s.id).then(function () {\n toast(\"Cron re-wired to \" + s.id);\n return loadAll();\n }).catch(function (e) { toast(\"cron: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"kill\" && s) {\n confirmClick(\"kill-\" + s.id, btn, function () {\n runAction(\"agent_kill\", { sessionId: s.id }, sp.label + \" killed\");\n });\n }\n}\n\nfunction sessionAction(id, what, btn) {\n if (what === \"kill\") {\n confirmClick(\"kill-\" + id, btn, function () {\n runAction(\"agent_kill\", { sessionId: id }, \"Killed \" + id);\n });\n return;\n }\n if (what === \"restart\") { runAction(\"session_restart\", { idOrName: id }, \"Restarted \" + id); return; }\n if (what === \"archive\") { runAction(\"session_archive\", { idOrName: id }, \"Archived \" + id); }\n}\n\nfunction cronAction(jobId, what, btn) {\n if (what === \"run\") { runAction(\"cron_run\", { jobId: jobId }, \"Cron fired\"); return; }\n if (what === \"delete\") {\n confirmClick(\"cron-\" + jobId, btn, function () {\n runAction(\"cron_delete\", { jobId: jobId }, \"Cron deleted\");\n });\n }\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () { loadHealth(); loadAll(); });\ndocument.getElementById(\"ws-select\").addEventListener(\"change\", function (e) {\n wsFilter = e.target.value;\n renderSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n buildHousekeeping();\n loadHealth();\n return loadAll();\n })\n .then(function () {\n pollTimer = setInterval(loadAll, 5000);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `ops-panel` — the agentproto daemon operations cockpit as an app.\n *\n * Two durable \"special sessions\" ship as agents — the Session Watchdog\n * (cron-ticked health checks: classify, flag, archive; never kills) and the\n * Sessions Manager (the coordinator the human talks to, which drives the\n * other sessions). The panel (`ui.ts`) launches/restarts them by label,\n * wires the watchdog's `watchdog-tick` cron, lists every session filterable\n * by workspace with lifecycle actions, and fronts housekeeping: session GC,\n * worktree GC (dry-run first), and the cron ledger.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { manager } from \"./agents/manager.js\"\nimport { watchdog } from \"./agents/watchdog.js\"\nimport { OPS_PANEL_HTML, OPS_PANEL_TOOLS } from \"./ui.js\"\n\nexport const opsPanel: AppHandle = defineApp({\n id: \"@agentproto/ops-panel\",\n name: \"Ops Panel\",\n version: \"0.1.0\",\n description: \"Daemon ops cockpit — watchdog & manager sessions, workspace-filtered session list, GC and cron housekeeping.\",\n agents: [watchdog, manager],\n ui: {\n html: OPS_PANEL_HTML,\n title: \"Ops Panel\",\n tools: [...OPS_PANEL_TOOLS],\n },\n})\n\nexport { watchdog, manager }\nexport { WATCHDOG_LABEL, WATCHDOG_TICK_LABEL, WATCHDOG_TICK_PROMPT, WATCHDOG_TICK_SCHEDULE } from \"./agents/watchdog.js\"\nexport { MANAGER_LABEL } from \"./agents/manager.js\"\n","/**\n * The `@agentproto/config` deep-link fragment grammar (plan §3.4):\n *\n * #<section>[/<id>[/<sub>]]\n * section := wallets | harnesses | models | capabilities | defaults | remote | advanced\n * id := encodeURIComponent(profileId | adapterSlug | modelRef | configKey | \"pairing\")\n *\n * Written in plain ES5-safe syntax (`var`/`function`, no arrows, no\n * destructuring) and inlined verbatim into the panel's `<script>` via\n * `parseConfigFragment.toString()` / `buildConfigFragment.toString()`\n * (see `ui.ts`) — this is the ONE copy of the parser, tested directly here\n * and run unmodified in the browser. Every free variable it needs (the\n * section list) is declared INSIDE the function body so the embedded\n * source has no dependency on this module's closure.\n */\n\nexport const CONFIG_SECTIONS = [\"wallets\", \"harnesses\", \"models\", \"capabilities\", \"defaults\", \"remote\", \"advanced\"] as const\n\nexport type ConfigSection = (typeof CONFIG_SECTIONS)[number]\n\nexport interface ParsedConfigFragment {\n section: ConfigSection\n id?: string\n sub?: string\n}\n\n/** Unknown section -> first section (`wallets`). Unknown/absent id or sub is\n * simply omitted — never guessed, never a blank crash. A malformed\n * percent-encoding in `id`/`sub` falls back to the raw segment rather than\n * throwing. */\nexport function parseConfigFragment(hash: string): ParsedConfigFragment {\n var SECTIONS = [\"wallets\", \"harnesses\", \"models\", \"capabilities\", \"defaults\", \"remote\", \"advanced\"]\n var raw = String(hash || \"\").replace(/^#/, \"\")\n var parts = raw.split(\"/\").filter(function (p) {\n return p.length > 0\n })\n var sectionRaw = parts[0] || \"\"\n var section = SECTIONS.indexOf(sectionRaw) !== -1 ? sectionRaw : SECTIONS[0]!\n var result: { section: string; id?: string; sub?: string } = { section: section }\n var idPart = parts[1]\n if (idPart !== undefined) {\n try {\n result.id = decodeURIComponent(idPart)\n } catch (e) {\n result.id = idPart\n }\n }\n var subPart = parts[2]\n if (subPart !== undefined) {\n try {\n result.sub = decodeURIComponent(subPart)\n } catch (e) {\n result.sub = subPart\n }\n }\n return result as ParsedConfigFragment\n}\n\n/** Inverse of {@link parseConfigFragment} — builds the hash string (with the\n * leading `#`) the panel writes back to `location.hash` on navigation. */\nexport function buildConfigFragment(section: string, id?: string, sub?: string): string {\n var out = \"#\" + section\n if (id !== undefined && id !== null && id !== \"\") out += \"/\" + encodeURIComponent(id)\n if (sub !== undefined && sub !== null && sub !== \"\") out += \"/\" + encodeURIComponent(sub)\n return out\n}\n\n/**\n * MCP-hosted deep links (plan §3.4, PR-5): a browser tab resolves\n * `location.hash` directly, but an MCP host has no URL bar for this panel —\n * it opens `app_ui_config { view }` instead, and the bridge surfaces that as\n * a `ui/notifications/tool-input` (or `-result`) arrival (see `ui.ts`'s\n * `handleToolInput`).\n *\n * Precedence: `location.hash` wins on the panel's very first route — an\n * actual browser deep link must not be overridden by a stale/default tool\n * call. Every arrival after the first is a live \"go to this view\" command\n * from the host and always applies, regardless of the current hash.\n *\n * `arrivalIndex` is 1-based (the 1st, 2nd, ... tool-input notification this\n * boot has seen); `hashPresentAtLoad` is whether `location.hash` was\n * non-empty when the panel's script started running, captured once before\n * any routing happens. Pure — no DOM — so it is unit-testable without a\n * browser or jsdom, same as {@link parseConfigFragment}.\n */\nexport function shouldApplyIncomingView(arrivalIndex: number, hashPresentAtLoad: boolean): boolean {\n return !(arrivalIndex === 1 && hashPresentAtLoad)\n}\n","/**\n * `@agentproto/config` — a self-contained MCP App panel for daemon-wide\n * configuration. Its own app, not a section of `ops-panel` — that app is\n * referenced only for the mechanics this panel shares (\n * `window.McpApp.connect()` -> `app_tool_call { appId, tool, args }`, the\n * nested-envelope `unwrapText`, one card per entity, two-click confirm). See\n * its `ui.ts` for the pattern this file follows.\n *\n * Six sections, each independently loaded/rendered so one failing card never\n * blanks the others: Wallets (auth profiles + spend), Harnesses (adapters +\n * capabilities + presets + roles), Models (the vendor/product/route\n * catalog), Defaults & messaging (daemon config knobs), Remote & pairing,\n * and Advanced (raw config dump). This PR adds the write flows: wallet\n * lifecycle + curation, harness preset CRUD, a generic config_get/config_set\n * editor for Defaults/per-harness defaults/titler.model, and remote/pairing\n * lifecycle.\n *\n * Three daemon tools this panel wants (`config_get`, `config_set`, and\n * every write tool below) may not exist yet on the daemon it talks to\n * (parallel PRs) — `loadTool` turns an \"unknown daemon tool\" failure into a\n * muted, section-local notice instead of an error, and the Defaults section\n * falls back to `daemon_health`'s five effective knobs when `config_get` is\n * absent.\n *\n * Secret discipline: a credential (auth_profile_create's `credential`, a\n * remote bearer, a pairing offer URL) is read from a form field, used ONCE\n * in the same handler, and the DOM field is cleared before the async call\n * even starts. Nothing secret is ever stored in section state, written to\n * `updateModelContext`, or logged.\n *\n * Deep-link contract (plan section 3.4): `#<section>[/<id>[/<sub>]]`, parsed\n * by `parseConfigFragment` (`fragment.ts`) — embedded here via\n * `.toString()` so the exact function tested in `fragment.test.ts` is the\n * one that runs in the browser, not a second hand-copied version. A browser\n * tab resolves this from `location.hash`; an MCP host has no URL bar, so it\n * opens `app_ui_config { view }` instead — `handleToolInput` below turns\n * that into the same fragment via the bridge's `onToolInput` (app-ui-apps.ts).\n * `location.hash` wins when the panel first loads with one already set;\n * every tool-input notification after that re-routes unconditionally.\n */\n\nimport { parseConfigFragment, buildConfigFragment, shouldApplyIncomingView } from \"./fragment.js\"\n\nexport const CONFIG_TOOLS = [\n \"auth_profile_list\",\n \"auth_profile_create\",\n \"auth_profile_delete\",\n \"auth_profile_set_enabled\",\n \"auth_profile_set_models\",\n \"auth_profile_refresh_models\",\n \"auth_profile_import\",\n \"auth_discover_credentials\",\n \"auth_profile_update\",\n \"adapter_list\",\n \"harness_capabilities\",\n \"capabilities_inventory\",\n \"mcp_imported_tool_list\",\n \"mcp_imported_status\",\n \"mcp_import\",\n \"mcp_imported_remove\",\n \"harness_preset_list\",\n \"harness_preset_create\",\n \"harness_preset_delete\",\n \"harness_preset_set_default\",\n \"catalog_models\",\n \"catalog_provider_models\",\n \"role_list\",\n \"usage_rollup\",\n \"config_get\",\n \"config_set\",\n \"daemon_health\",\n \"remote_status\",\n \"remote_enable\",\n \"remote_disable\",\n \"pair_list\",\n \"pair_offer\",\n \"pair_revoke\",\n \"tunnel_list\",\n \"tunnel_status\",\n] as const\n\nconst APP_ID = \"@agentproto/config\"\n\nconst SECTIONS: ReadonlyArray<{ id: string; label: string }> = [\n { id: \"wallets\", label: \"Wallets\" },\n { id: \"harnesses\", label: \"Harnesses\" },\n { id: \"models\", label: \"Models\" },\n { id: \"capabilities\", label: \"Capabilities\" },\n { id: \"defaults\", label: \"Defaults\" },\n { id: \"remote\", label: \"Remote\" },\n { id: \"advanced\", label: \"Advanced\" },\n]\n\nexport const CONFIG_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto config</title>\n<style>\n/* Theme tokens: prefer a live --vscode-* var (VS Code webview host), else\n the brand-neutral standalone default, split light/dark by\n prefers-color-scheme — same mapping convention as work-board/ui/style.css.\n No JS theme detection: a host that swaps its theme live is never frozen. */\n:root{\n --cfg-bg: var(--vscode-editor-background, #f5f6f3);\n --cfg-surface: var(--vscode-sideBar-background, #ffffff);\n --cfg-surface-2: var(--vscode-editorWidget-background, #eef0ec);\n --cfg-ink: var(--vscode-editor-foreground, #171a1e);\n --cfg-ink-faint: var(--vscode-descriptionForeground, #6b7280);\n --cfg-border: var(--vscode-panel-border, #dde0da);\n --cfg-accent: var(--vscode-button-background, #0f62fe);\n --cfg-warning: var(--vscode-charts-yellow, #b45309);\n --cfg-danger: var(--vscode-charts-red, #b91c1c);\n --cfg-success: var(--vscode-charts-green, #15803d);\n --cfg-muted-bg: var(--vscode-input-background, #e5e7eb);\n}\n@media (prefers-color-scheme: dark){\n :root{\n --cfg-bg: var(--vscode-editor-background, #0f1115);\n --cfg-surface: var(--vscode-sideBar-background, #161a20);\n --cfg-surface-2: var(--vscode-editorWidget-background, #1c2129);\n --cfg-ink: var(--vscode-editor-foreground, #e6e9ee);\n --cfg-ink-faint: var(--vscode-descriptionForeground, #8b93a1);\n --cfg-border: var(--vscode-panel-border, #2a2f38);\n --cfg-accent: var(--vscode-button-background, #4589ff);\n --cfg-warning: var(--vscode-charts-yellow, #d29922);\n --cfg-danger: var(--vscode-charts-red, #f85149);\n --cfg-success: var(--vscode-charts-green, #3fb950);\n --cfg-muted-bg: var(--vscode-input-background, #262b33);\n }\n}\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--cfg-bg);color:var(--cfg-ink)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--cfg-ink-faint);flex:none}\n.chip.ok .d{background:var(--cfg-success)}\n.chip.bad .d{background:var(--cfg-danger)}\n#refresh-btn{margin-left:auto;background:none;border:1px solid var(--cfg-border);cursor:pointer;color:var(--cfg-ink-faint);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--cfg-ink);background:var(--cfg-surface-2)}\n\n#nav{display:flex;gap:4px;padding:8px 14px 0;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}\n#nav button{background:none;border:none;border-bottom:2px solid transparent;color:var(--cfg-ink-faint);font-size:12px;font-weight:600;font-family:inherit;padding:7px 10px;cursor:pointer}\n#nav button:hover{color:var(--cfg-ink)}\n#nav button.active{color:var(--cfg-ink);border-bottom-color:var(--cfg-accent)}\n\n#pending-banner{margin:8px 14px 0;padding:6px 10px;background:var(--cfg-surface-2);border:1px solid var(--cfg-warning);color:var(--cfg-warning);border-radius:6px;font-size:11px;flex-shrink:0}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px}\n.section{display:none;flex-direction:column;gap:12px}\n.section.active{display:flex}\n\n.notice{font-size:11.5px;color:var(--cfg-warning);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:6px;padding:6px 10px}\n.muted{color:var(--cfg-ink-faint)}\n.muted-note{color:var(--cfg-ink-faint);font-size:11px;padding:6px 10px}\n.empty{color:var(--cfg-ink-faint);font-size:11.5px;padding:8px}\n\n.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px}\n.card{background:var(--cfg-surface);border:1px solid var(--cfg-border);border-radius:8px;overflow:hidden;scroll-margin-top:12px}\n.card.hl{outline:2px solid var(--cfg-accent);outline-offset:-1px}\n.card .title{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-weight:700;background:var(--cfg-surface-2);border-bottom:1px solid var(--cfg-border)}\n.card .title .aside{font-weight:400;color:var(--cfg-ink-faint);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}\n.card .body{padding:8px 10px}\n.card dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;align-items:baseline}\n.card dt{color:var(--cfg-ink-faint);white-space:nowrap;font-size:11.5px}\n.card dd{text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11.5px}\n.bar{grid-column:1/3;height:4px;background:var(--cfg-muted-bg);border-radius:2px;overflow:hidden;margin:2px 0 4px}\n.bar>span{display:block;height:100%;background:var(--cfg-accent)}\n.bar.warn>span{background:var(--cfg-warning)}\n\n.sect-h{font-size:11px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.pillbar{display:flex;gap:4px}\n.pillbar button{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:999px;cursor:pointer;font-family:inherit}\n.pillbar button.active{color:var(--cfg-ink);border-color:var(--cfg-accent)}\nlabel.inline{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--cfg-ink-faint)}\nselect,input[type=text],input[type=password],input[type=number],textarea{background:var(--cfg-surface-2);color:var(--cfg-ink);border:1px solid var(--cfg-border);border-radius:5px;padding:3px 7px;font-size:11px;font-family:inherit}\ninput:disabled,select:disabled,textarea:disabled{opacity:.5;cursor:not-allowed}\n\n.tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:5px;padding:1px 6px;color:var(--cfg-ink-faint)}\n.tag.ok{color:var(--cfg-success);border-color:var(--cfg-success)}\n.tag.bad{color:var(--cfg-danger);border-color:var(--cfg-danger)}\n.tag.warn-tag{color:var(--cfg-warning);border-color:var(--cfg-warning)}\na.link{color:var(--cfg-accent);text-decoration:none;cursor:pointer}\na.link:hover{text-decoration:underline}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--cfg-border)}\ntd{font-size:11.5px;padding:5px 8px;border-bottom:1px solid var(--cfg-surface-2);vertical-align:middle}\ntr.hl td{background:var(--cfg-surface-2)}\ntd.matrix-cell{text-align:center;cursor:pointer}\ntd.matrix-cell.disabled{opacity:.35;cursor:default}\n\n.btn-sm{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink);font-size:10.5px;font-weight:600;padding:3px 8px;border-radius:5px;cursor:pointer;font-family:inherit}\n.btn-sm:hover{border-color:var(--cfg-accent)}\n.btn-sm:disabled{opacity:.5;cursor:not-allowed}\n.btn-sm.danger{color:var(--cfg-danger);border-color:var(--cfg-danger)}\n.btn-sm.confirm{background:var(--cfg-danger);color:#fff;border-color:var(--cfg-danger)}\n.btn-sm.primary{color:var(--cfg-accent);border-color:var(--cfg-accent)}\n\n.cfg-row{display:contents}\n.cfg-help{font-size:10.5px}\n.cfg-err{color:var(--cfg-danger);font-size:10.5px}\n\n.form-panel{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:10px;margin-top:6px;display:flex;flex-direction:column;gap:6px}\n.form-panel .field{display:flex;flex-direction:column;gap:2px;font-size:11px}\n.form-panel .field label{color:var(--cfg-ink-faint);font-size:10.5px}\n.form-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}\n\n.reveal-box{background:var(--cfg-surface-2);border:1px dashed var(--cfg-warning);border-radius:8px;padding:10px;font-size:11px;display:flex;flex-direction:column;gap:6px}\n.reveal-box code{display:block;word-break:break-all;background:var(--cfg-muted-bg);padding:4px 6px;border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:10.5px}\n\n.mm-list{max-height:160px;overflow-y:auto;margin-top:4px;padding:4px;background:var(--cfg-muted-bg);border-radius:5px}\n.wallet-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--cfg-danger);color:var(--cfg-danger)}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>agentproto config</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon…</span></span>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻ refresh</button>\n </div>\n <div id=\"nav\"></div>\n <div class=\"notice\" id=\"pending-banner\" style=\"display:none\"></div>\n <div id=\"body\">\n <div class=\"notice\" id=\"notfound\" style=\"display:none\"></div>\n <div class=\"section\" data-section=\"wallets\" id=\"sec-wallets\"></div>\n <div class=\"section\" data-section=\"harnesses\" id=\"sec-harnesses\"></div>\n <div class=\"section\" data-section=\"models\" id=\"sec-models\"></div>\n <div class=\"section\" data-section=\"capabilities\" id=\"sec-capabilities\"></div>\n <div class=\"section\" data-section=\"defaults\" id=\"sec-defaults\"></div>\n <div class=\"section\" data-section=\"remote\" id=\"sec-remote\"></div>\n <div class=\"section\" data-section=\"advanced\" id=\"sec-advanced\"></div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar updateModelContext = null;\nvar onTeardownFn = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SECTIONS = ${JSON.stringify(SECTIONS)};\n\n// ── deep-link fragment parser (fragment.ts, embedded verbatim so the\n// browser runs the exact function fragment.test.ts exercises) ──\n${parseConfigFragment.toString()}\n${buildConfigFragment.toString()}\n${shouldApplyIncomingView.toString()}\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as ops-panel/session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\n// Never throws: every card loads independently, so one failing tool must\n// never crash another card's render. An \"unknown daemon tool\" failure (a\n// write tool, or config_get/config_set, on a daemon that predates them) is\n// distinguished from any other failure so the caller can render a muted\n// feature-detect note instead of an error. Used for BOTH reads and writes:\n// every write tool here answers either a JSON object (success) or a plain\n// string (its own error text), the same two shapes a read tool answers.\nfunction loadTool(tool, args) {\n return callApp(tool, args).then(function (d) {\n if (typeof d === \"string\") {\n return { ok: false, message: d, unknown: /unknown daemon tool/.test(d) };\n }\n if (d && typeof d === \"object\" && typeof d.error === \"string\") {\n return { ok: false, message: d.error, unknown: /unknown daemon tool/.test(d.error) };\n }\n return { ok: true, data: d };\n }).catch(function (e) {\n var msg = (e && e.message) || String(e);\n return { ok: false, message: msg, unknown: false };\n });\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction humanize(s) { return String(s || \"\").replace(/[-_]+/g, \" \"); }\n\nfunction formatTokens(n) {\n if (n == null || !isFinite(n)) return \"\";\n var trim = function (t) { return t.replace(/\\\\.0$/, \"\"); };\n if (n >= 1000000) return trim((n / 1000000).toFixed(1)) + \"M\";\n if (n >= 1000) return trim((n / 1000).toFixed(1)) + \"k\";\n return String(n);\n}\n\nfunction formatCost(usd) {\n if (usd == null || !isFinite(usd)) return \"\";\n if (usd > 0 && usd < 0.01) return \"<$0.01\";\n return \"$\" + usd.toFixed(2);\n}\n\nfunction formatDate(iso) {\n if (!iso) return \"\";\n var d = new Date(iso);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleString();\n}\n\nfunction formatMs(ms) {\n if (ms == null || !isFinite(ms)) return \"\";\n if (ms >= 60000) return Math.round(ms / 60000) + \"m\";\n if (ms >= 1000) return Math.round(ms / 1000) + \"s\";\n return ms + \"ms\";\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar BILLING_LABELS = { \"oauth-bearer\": \"Subscription\", \"api-key\": \"API key\" };\n\n// ── card builders (session-header-cards.tsx visual language: card surface,\n// title strip w/ muted aside, two-col dl grid, 4px progress bar) ──\nfunction cardOpen(id, title, aside) {\n return '<div class=\"card\" data-card-id=\"' + escHtml(id) + '\">'\n + '<div class=\"title\"><span>' + escHtml(title) + '</span>'\n + (aside ? '<span class=\"aside\">' + aside + '</span>' : '')\n + '</div><div class=\"body\"><dl>';\n}\nfunction cardClose() { return '</dl></div></div>'; }\nfunction row(label, value, title) {\n return '<dt>' + escHtml(label) + '</dt><dd' + (title ? ' title=\"' + escHtml(title) + '\"' : '') + '>' + value + '</dd>';\n}\nfunction bar(pct) {\n var p = Math.max(0, Math.min(100, pct));\n return '<div class=\"bar' + (p >= 80 ? ' warn' : '') + '\"><span style=\"width:' + p + '%\"></span></div>';\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm (ops-panel's pattern): first click arms the button\n// (\"sure?\"), a second click within 4s fires the callback; anything else\n// disarms it. Shared by every destructive action in this panel (delete\n// wallet, delete preset, disable remote, revoke pairing).\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\nfunction copyToClipboard(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { toast(\"Copied.\", false); }, function () { toast(\"Copy failed.\", true); });\n return;\n }\n try {\n var ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.opacity = \"0\";\n document.body.appendChild(ta);\n ta.focus();\n ta.select();\n document.execCommand(\"copy\");\n document.body.removeChild(ta);\n toast(\"Copied.\", false);\n } catch (e) {\n toast(\"Copy failed.\", true);\n }\n}\nfunction wireCopyButtons(container, valuesByKey) {\n var btns = container.querySelectorAll(\"[data-copy]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var key = btn.getAttribute(\"data-copy\");\n var value = valuesByKey[key];\n if (value) copyToClipboard(value);\n });\n })(btns[i]);\n }\n}\n\n// ============================================================\n// generic config_get / config_set field editor — shared by Defaults,\n// per-harness adapter defaults, and titler.model (Models section).\n// ============================================================\n\n// Last revision seen from ANY config_get/config_set response, reused on\n// every write (the spec: \"always pass the last revision\"). A daemon that\n// predates config_get/config_set never sets this; config_set is simply\n// called without a revision then (server treats that as unconditional).\nvar configRevision = null;\n\n// path -> true for every currently-known pending-restart key, merged from\n// every config_get response seen anywhere in the app. Drives the persistent\n// top banner — never restarts anything itself, only tells the user to.\nvar pendingRestartMap = {};\n\nfunction notePendingRestart(rows) {\n var changed = false;\n for (var i = 0; i < (rows || []).length; i++) {\n var r = rows[i];\n if (r.pendingRestart) {\n if (!pendingRestartMap[r.path]) changed = true;\n pendingRestartMap[r.path] = true;\n } else if (pendingRestartMap[r.path]) {\n delete pendingRestartMap[r.path];\n changed = true;\n }\n }\n if (changed) renderPendingBanner();\n}\n\nfunction renderPendingBanner() {\n var el = document.getElementById(\"pending-banner\");\n if (!el) return;\n var keys = [];\n for (var k in pendingRestartMap) if (Object.prototype.hasOwnProperty.call(pendingRestartMap, k)) keys.push(k);\n if (keys.length === 0) {\n el.style.display = \"none\";\n el.textContent = \"\";\n return;\n }\n keys.sort();\n el.style.display = \"block\";\n el.textContent = \"Restart the daemon yourself to apply: \" + keys.join(\", \") + \". This app never restarts it for you.\";\n}\n\n// Type inference for a config_get row: config-tools.ts's describeKey\n// spreads deriveValueType(entry.schema) onto every row, so row.valueType\n// (\"boolean\" | \"number\" | \"integer\" | \"string\" | \"enum\" | \"string[]\" |\n// \"object\" | \"unknown\") plus row.enum / row.min / row.max are the registry's\n// OWN answer and win outright. The JS-type inference below is only a\n// fallback for a daemon old enough to predate that field entirely (absent,\n// not merely \"unknown\" — \"unknown\" is itself a real, present answer for a\n// schema deriveValueType can't type, e.g. a mixed union, and must NOT be\n// re-guessed from the current value).\nfunction inferValueType(row) {\n if (row.valueType) return row.valueType;\n var v = row.effective !== undefined ? row.effective : row.value;\n if (v === undefined) v = row.default;\n if (typeof v === \"boolean\") return \"boolean\";\n if (typeof v === \"number\") return \"number\";\n if (typeof v === \"string\") return \"string\";\n if (Array.isArray(v)) {\n var allStr = true;\n for (var i = 0; i < v.length; i++) if (typeof v[i] !== \"string\") allStr = false;\n return allStr ? \"string[]\" : \"object\";\n }\n if (v && typeof v === \"object\") return \"object\";\n return \"unknown\";\n}\n\n// defaults.spawn.browser is a union of the literals \"headless\" and false\n// (config-schema.ts) - deriveValueType's enum-from-union heuristic requires\n// EVERY literal to be a string, and false isn't one, so this one specific\n// key comes back \"unknown\" rather than \"enum\" (verified against\n// packages/runtime/src/config-tools.ts's deriveValueType). A checkbox whose\n// two states are not plain true/false is still the right control for it, so\n// it keeps its own tiny special case rather than a general boolean-like-enum\n// mechanism nothing else needs.\nvar BOOL_LIKE_FIELDS = {\n \"defaults.spawn.browser\": { on: \"headless\", off: false, onLabel: \"headless\" },\n};\n\nfunction pathId(path) { return \"cfg-\" + String(path).replace(/[^a-zA-Z0-9]/g, \"-\"); }\n\nfunction writableReasonText(row) {\n if (row.secret) return \"secret, set via wallets or the CLI\";\n return \"read-only, edit the config file by hand\";\n}\n\nfunction configDisplayValue(row) {\n if (row.secret) {\n if (row.secret.set === false) return \"not set\";\n if (typeof row.secret.set === \"boolean\") {\n var bits = [];\n if (row.secret.fingerprint) bits.push(row.secret.fingerprint);\n if (row.secret.last4) bits.push(row.secret.last4);\n return bits.length ? bits.join(\" / \") : \"set\";\n }\n var parts = [];\n for (var k in row.secret) {\n if (Object.prototype.hasOwnProperty.call(row.secret, k)) {\n parts.push(k + \": \" + (row.secret[k] && row.secret[k].set ? \"set\" : \"not set\"));\n }\n }\n return parts.length ? parts.join(\", \") : \"no keys\";\n }\n return row.effective !== undefined ? row.effective : row.value;\n}\n\nfunction numberBoundAttrs(row) {\n var attrs = \"\";\n if (typeof row.min === \"number\") attrs += ' min=\"' + row.min + '\"';\n if (typeof row.max === \"number\") attrs += ' max=\"' + row.max + '\"';\n return attrs;\n}\n\nfunction configControlHtml(row) {\n var vt = inferValueType(row);\n var val = row.effective !== undefined ? row.effective : (row.value !== undefined ? row.value : row.default);\n var boolLike = BOOL_LIKE_FIELDS[row.path];\n var disabledAttr = row.writable ? \"\" : \" disabled\";\n var titleAttr = row.writable ? \"\" : ' title=\"' + escHtml(writableReasonText(row)) + '\"';\n if (boolLike) {\n var checked = val === boolLike.on;\n return '<label class=\"inline\"><input type=\"checkbox\" data-cfg-bool-like=\"' + escHtml(row.path) + '\"' + (checked ? \" checked\" : \"\") + disabledAttr + titleAttr + '> ' + escHtml(boolLike.onLabel || \"on\") + '</label>';\n }\n if (vt === \"boolean\") {\n return '<input type=\"checkbox\" data-cfg-auto=\"' + escHtml(row.path) + '\"' + (val ? \" checked\" : \"\") + disabledAttr + titleAttr + '>';\n }\n if (vt === \"enum\") {\n var enumOpts = row.enum || [];\n var out = '<select data-cfg-auto=\"' + escHtml(row.path) + '\"' + disabledAttr + titleAttr + '>';\n for (var i = 0; i < enumOpts.length; i++) {\n out += '<option value=\"' + escHtml(enumOpts[i]) + '\"' + (enumOpts[i] === val ? \" selected\" : \"\") + '>' + escHtml(enumOpts[i]) + '</option>';\n }\n return out + '</select>';\n }\n if (vt === \"number\" || vt === \"integer\") {\n return '<input type=\"number\" data-cfg-auto=\"' + escHtml(row.path) + '\" value=\"' + escHtml(val == null ? \"\" : String(val)) + '\"'\n + (vt === \"integer\" ? ' step=\"1\"' : \"\") + numberBoundAttrs(row) + disabledAttr + titleAttr + '>';\n }\n if (vt === \"string[]\") {\n return '<input type=\"text\" data-cfg-input=\"' + escHtml(row.path) + '\" value=\"' + escHtml((val || []).join(\", \")) + '\" placeholder=\"comma, separated, list\"' + disabledAttr + titleAttr + '>'\n + ' <button type=\"button\" class=\"btn-sm\" data-cfg-save=\"' + escHtml(row.path) + '\"' + disabledAttr + '>save</button>';\n }\n if (vt === \"object\" || vt === \"unknown\") {\n // \"unknown\" is a real, present answer (a schema deriveValueType can't\n // type, e.g. a mixed boolean/object union) — a raw JSON editor is the\n // one control that can represent any of those shapes without guessing.\n return '<div><textarea data-cfg-input=\"' + escHtml(row.path) + '\" rows=\"4\" style=\"width:100%;font-family:Menlo,Monaco,monospace;font-size:11px\"' + disabledAttr + titleAttr + '>' + escHtml(JSON.stringify(val === undefined ? null : val, null, 2)) + '</textarea>'\n + ' <button type=\"button\" class=\"btn-sm\" data-cfg-save=\"' + escHtml(row.path) + '\"' + disabledAttr + '>save</button></div>';\n }\n return '<input type=\"text\" data-cfg-auto=\"' + escHtml(row.path) + '\" value=\"' + escHtml(val == null ? \"\" : String(val)) + '\"' + disabledAttr + titleAttr + '>';\n}\n\n// One dl \"row\" for a config key. Wrapped in a display:contents div so the\n// dt/dd pairs stay direct grid children (the 2-col dl grid) while still\n// being addressable as one group for wiring/lookup. A writable:false row\n// still renders a real control (checkbox/select/input), just disabled,\n// with the reason in its title, never a second, ad hoc read-only widget.\nfunction configFieldHtml(row) {\n var label = row.label || humanize(String(row.path).split(\".\").slice(-1)[0]);\n var badge = '<span class=\"tag\">' + escHtml(row.apply) + '</span>';\n var extra = \"\";\n if (row.source === \"env\" && row.envOverride) extra += ' <span class=\"tag bad\">env ' + escHtml(row.envOverride) + '</span>';\n if (row.pendingRestart) extra += ' <span class=\"tag warn-tag\">restart pending</span>';\n\n var dd = \"\";\n if (row.secret) {\n dd += escHtml(String(configDisplayValue(row)));\n } else {\n dd += configControlHtml(row);\n }\n dd += \" \" + badge + extra;\n if (row.writable && !row.secret && row.value !== undefined) {\n dd += ' <button type=\"button\" class=\"btn-sm\" data-cfg-reset=\"' + escHtml(row.path) + '\" title=\"Reset to default\">reset</button>';\n }\n\n var html = '<div class=\"cfg-row\" data-cfg-path=\"' + escHtml(row.path) + '\">'\n + '<dt title=\"' + escHtml(row.path) + '\">' + escHtml(label) + '</dt><dd>' + dd + '</dd>';\n if (row.help) html += '<dt></dt><dd class=\"cfg-help muted\">' + escHtml(row.help) + '</dd>';\n html += '<dt></dt><dd class=\"cfg-err\" data-cfg-err=\"' + escHtml(row.path) + '\" style=\"display:none\"></dd>';\n html += '</div>';\n return html;\n}\n\nfunction buildConfigCardHtml(cardId, title, rows) {\n var body = \"\";\n for (var i = 0; i < rows.length; i++) body += configFieldHtml(rows[i]);\n return '<div class=\"card\" data-card-id=\"' + escHtml(cardId) + '\"><div class=\"body\"><dl>' + body + '</dl></div></div>';\n}\n\nfunction extractConfigSetErrorCode(message) {\n var s = String(message || \"\");\n var start = s.indexOf(\"[\");\n var end = start === -1 ? -1 : s.indexOf(\"]\", start);\n if (start === -1 || end === -1) return \"\";\n return s.slice(start + 1, end);\n}\n\nfunction describeConfigSetError(message) {\n var code = extractConfigSetErrorCode(message);\n var HUMAN = {\n invalid_input: \"That request was not valid.\",\n unknown_key: \"This daemon does not know that setting.\",\n not_writable: \"This setting cannot be changed here.\",\n invalid_value: \"That value is not valid for this setting.\",\n invalid_config: \"That change would leave the config file invalid.\",\n stale_revision: \"This section changed elsewhere. Reloading.\",\n };\n return HUMAN[code] || message;\n}\n\n// Attach listeners for every config control inside \"container\". rowsByPath\n// supplies the row metadata a save/reset needs (valueType for parsing).\n// onSaved(path) fires after a successful write (path just written);\n// onStale() fires on a stale_revision rejection (reload the section).\nfunction wireConfigRows(container, rowsByPath, onSaved, onStale) {\n function submit(path, value, isUnset) {\n var args = isUnset ? { key: path, unset: true, revision: configRevision } : { key: path, value: value, revision: configRevision };\n return loadTool(\"config_set\", args).then(function (r) {\n if (!r.ok) {\n var stale = extractConfigSetErrorCode(r.message) === \"stale_revision\";\n toast(describeConfigSetError(r.message), true);\n if (stale && onStale) onStale();\n return;\n }\n var data = r.data;\n if (data && data.revision) configRevision = data.revision;\n var note = \"Saved.\";\n if (data && data.applied === \"restart-required\") note = \"Saved. Restart required to apply.\";\n if (data && data.shadowedByEnv) note += \" Overridden by \" + data.shadowedByEnv + \".\";\n toast(note, false);\n if (onSaved) onSaved(path);\n });\n }\n\n var autos = container.querySelectorAll(\"[data-cfg-auto]\");\n for (var i = 0; i < autos.length; i++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var path = el.getAttribute(\"data-cfg-auto\");\n var value;\n if (el.type === \"checkbox\") value = el.checked;\n else if (el.type === \"number\") value = el.value === \"\" ? null : Number(el.value);\n else value = el.value;\n submit(path, value, false);\n });\n })(autos[i]);\n }\n\n var boolLikes = container.querySelectorAll(\"[data-cfg-bool-like]\");\n for (var b = 0; b < boolLikes.length; b++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var path = el.getAttribute(\"data-cfg-bool-like\");\n var spec = BOOL_LIKE_FIELDS[path];\n submit(path, el.checked ? spec.on : spec.off, false);\n });\n })(boolLikes[b]);\n }\n\n var saves = container.querySelectorAll(\"[data-cfg-save]\");\n for (var s = 0; s < saves.length; s++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var path = btn.getAttribute(\"data-cfg-save\");\n var row = rowsByPath[path];\n var input = container.querySelector('[data-cfg-input=\"' + cssEscape(path) + '\"]');\n var errEl = container.querySelector('[data-cfg-err=\"' + cssEscape(path) + '\"]');\n if (errEl) { errEl.style.display = \"none\"; errEl.textContent = \"\"; }\n var vt = row ? inferValueType(row) : \"string\";\n var value;\n if (vt === \"string[]\") {\n value = input.value.split(\",\").map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });\n } else if (vt === \"object\" || vt === \"unknown\") {\n try {\n value = JSON.parse(input.value);\n } catch (e) {\n if (errEl) { errEl.style.display = \"block\"; errEl.textContent = \"Invalid JSON: \" + e.message; }\n return;\n }\n } else {\n value = input.value;\n }\n submit(path, value, false);\n });\n })(saves[s]);\n }\n\n var resets = container.querySelectorAll(\"[data-cfg-reset]\");\n for (var r = 0; r < resets.length; r++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n submit(btn.getAttribute(\"data-cfg-reset\"), undefined, true);\n });\n })(resets[r]);\n }\n}\n\n// ============================================================\n// nav + deep-link routing\n// ============================================================\n\nvar current = { section: SECTIONS[0].id, id: undefined, sub: undefined };\nvar loaded = {}; // section -> true once its data has been fetched at least once\n\nfunction renderNav() {\n var el = document.getElementById(\"nav\");\n var html = \"\";\n for (var i = 0; i < SECTIONS.length; i++) {\n html += '<button data-section=\"' + SECTIONS[i].id + '\" class=\"' + (SECTIONS[i].id === current.section ? \"active\" : \"\") + '\">' + escHtml(SECTIONS[i].label) + '</button>';\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-section]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n location.hash = buildConfigFragment(btn.getAttribute(\"data-section\"));\n });\n })(btns[k]);\n }\n}\n\nfunction showSection(sectionId) {\n var els = document.querySelectorAll(\".section\");\n for (var i = 0; i < els.length; i++) {\n els[i].classList.toggle(\"active\", els[i].getAttribute(\"data-section\") === sectionId);\n }\n var btns = document.querySelectorAll(\"#nav button[data-section]\");\n for (var j = 0; j < btns.length; j++) {\n btns[j].classList.toggle(\"active\", btns[j].getAttribute(\"data-section\") === sectionId);\n }\n}\n\n// Scroll to + highlight the card matching 'id' within the active section's\n// container. Unknown id means the \"not found\" banner (never a blank page).\nfunction applyDeepLink(sectionId, id, sub) {\n var notfound = document.getElementById(\"notfound\");\n notfound.style.display = \"none\";\n var prevHl = document.querySelectorAll(\".card.hl, tr.hl, th.hl\");\n for (var p = 0; p < prevHl.length; p++) prevHl[p].classList.remove(\"hl\");\n if (id === undefined) return;\n var container = document.getElementById(\"sec-\" + sectionId);\n if (!container) return;\n // #capabilities/<mcp|skills>/<sub>: 'id' selects the TAB, not a card —\n // switch tab (re-rendering the section) before looking for 'sub's card.\n if (sectionId === \"capabilities\") {\n var capTab = id === \"skills\" ? \"skills\" : \"mcp\";\n if (capabilitiesState.tab !== capTab) {\n capabilitiesState.tab = capTab;\n renderCapabilities();\n container = document.getElementById(\"sec-\" + sectionId);\n }\n if (sub === undefined) return;\n var capTarget = container.querySelector('[data-card-id=\"' + cssEscape(sub) + '\"]');\n if (!capTarget) {\n notfound.textContent = \"not found: \" + id + \"/\" + sub;\n notfound.style.display = \"block\";\n return;\n }\n capTarget.classList.add(\"hl\");\n scrollIntoViewSafe(capTarget);\n return;\n }\n var targetId = sectionId === \"remote\" && id === \"pairing\" ? \"pairing\" : id;\n var target = container.querySelector('[data-card-id=\"' + cssEscape(targetId) + '\"]');\n if (!target) {\n notfound.textContent = \"not found: \" + id + (sub ? \"/\" + sub : \"\");\n notfound.style.display = \"block\";\n return;\n }\n target.classList.add(\"hl\");\n scrollIntoViewSafe(target);\n if (sub) {\n var row = target.querySelector('[data-row-id=\"' + cssEscape(sub) + '\"]');\n if (row) {\n row.classList.add(\"hl\");\n scrollIntoViewSafe(row);\n }\n }\n}\n\n// Not every embedding host implements scrollIntoView on a sandboxed/minimal\n// DOM — highlighting must still work even when scrolling can't.\nfunction scrollIntoViewSafe(el) {\n if (el && typeof el.scrollIntoView === \"function\") el.scrollIntoView({ block: \"nearest\" });\n}\n\n// CSS.escape isn't guaranteed in every sandboxed webview — a minimal,\n// dependency-free fallback for the characters our own ids ever contain.\nfunction cssEscape(s) {\n return String(s).replace(/[^a-zA-Z0-9_-]/g, function (c) {\n return \"\\\\\\\\\" + c.charCodeAt(0).toString(16) + \" \";\n });\n}\n\n// Clears every secret this panel ever holds only in memory (a remote-enable\n// reveal, a pairing offer) — called on navigating away from #remote and on\n// host teardown. Never persisted, never in updateModelContext.\nfunction clearRemoteSecrets() {\n remoteReveal = null;\n pairReveal = null;\n clearPairCountdown();\n // The #sec-remote DOM may still show a rendered reveal box from before\n // this call — clearing the variable alone does not touch it. Force the\n // next visit to re-fetch and re-render from scratch (remoteReveal is\n // null by then, so no reveal box comes back) instead of leaving the\n // stale secret sitting in a hidden .section until some other event\n // happens to re-render it.\n loaded.remote = false;\n}\n\nfunction route(pushBack) {\n var parsed = parseConfigFragment(location.hash);\n if (current.section === \"remote\" && parsed.section !== \"remote\") {\n clearRemoteSecrets();\n }\n current = parsed;\n showSection(parsed.section);\n renderNav();\n ensureLoaded(parsed.section).then(function () {\n applyDeepLink(parsed.section, parsed.id, parsed.sub);\n });\n if (pushBack) location.hash = buildConfigFragment(parsed.section, parsed.id, parsed.sub);\n}\n\nwindow.addEventListener(\"hashchange\", function () { route(false); });\n\n// ── MCP-hosted deep links ──\n// A browser tab navigates via location.hash directly; an MCP host (Claude\n// Desktop, Codex, VS Code) has no URL bar for this panel, so it opens\n// app_ui_config with a view argument instead (plan section 3.4). The bridge\n// (app-ui-apps.ts) surfaces that as tool input; this turns it into the same\n// #<section>/<id>/<sub> fragment a browser deep link would use.\n//\n// location.hash still wins on the very first route — an actual browser deep\n// link must not be overridden by a stale/default tool call. Every\n// subsequent tool-input notification (a fresh app_ui_config invocation while\n// the panel is already open) is a live \"go to this view\" command and always\n// re-routes.\nvar toolInputCount = 0;\nvar hashPresentAtLoad = !!(location.hash && location.hash !== \"#\");\n\nfunction routeToView(view) {\n if (typeof view !== \"string\" || !view) return;\n var parsed = parseConfigFragment(view);\n var frag = buildConfigFragment(parsed.section, parsed.id, parsed.sub);\n if (location.hash === frag) { route(false); } else { location.hash = frag; }\n}\n\nfunction handleToolInput(input) {\n toolInputCount += 1;\n if (!shouldApplyIncomingView(toolInputCount, hashPresentAtLoad)) return;\n routeToView(input && input.view);\n}\n\n// ============================================================\n// section data + render dispatch\n// ============================================================\n\nvar SECTION_LOADERS = {\n wallets: loadWallets,\n harnesses: loadHarnesses,\n models: loadModels,\n capabilities: loadCapabilities,\n defaults: loadDefaults,\n remote: loadRemote,\n advanced: loadAdvanced,\n};\n\nfunction ensureLoaded(sectionId) {\n if (loaded[sectionId]) return Promise.resolve();\n var fn = SECTION_LOADERS[sectionId];\n if (!fn) return Promise.resolve();\n return fn().then(function () { loaded[sectionId] = true; });\n}\n\nfunction reloadCurrent() {\n loaded[current.section] = false;\n return ensureLoaded(current.section).then(function () {\n applyDeepLink(current.section, current.id, current.sub);\n });\n}\n\n// ============================================================\n// Wallets\n// ============================================================\n\nvar walletsState = {\n window: \"7d\",\n profilesResult: null,\n rollupResult: null,\n presetsResult: null,\n discoveredResult: null,\n showAddForm: false,\n showDiscovered: false,\n expanded: {}, // profileId -> { edit: bool, curate: bool }\n providerModelsByEndpoint: {}, // endpoint -> catalog_provider_models rows\n};\n\nfunction loadWallets() {\n return Promise.all([\n loadTool(\"auth_profile_list\", { full: true }),\n loadTool(\"usage_rollup\", { window: walletsState.window, groupBy: [\"profile\"] }),\n loadTool(\"harness_preset_list\", {}),\n ]).then(function (results) {\n walletsState.profilesResult = results[0];\n walletsState.rollupResult = results[1];\n walletsState.presetsResult = results[2];\n renderWallets();\n });\n}\n\nfunction reloadWalletsWindow(win) {\n walletsState.window = win;\n fetchRollup(false);\n}\n\nfunction fetchRollup(probe) {\n return loadTool(\"usage_rollup\", { window: walletsState.window, groupBy: [\"profile\"], probe: !!probe }).then(function (r) {\n walletsState.rollupResult = r;\n renderWallets();\n });\n}\n\nfunction byProfileMap(rollupResult) {\n var map = {};\n if (rollupResult.ok && rollupResult.data && Array.isArray(rollupResult.data.byProfile)) {\n for (var i = 0; i < rollupResult.data.byProfile.length; i++) {\n map[rollupResult.data.byProfile[i].profileRef] = rollupResult.data.byProfile[i];\n }\n }\n return map;\n}\n\nfunction presetsReferencing(profileId) {\n var out = [];\n var pr = walletsState.presetsResult;\n if (!pr || !pr.ok) return out;\n var presets = (pr.data && pr.data.presets) || [];\n for (var i = 0; i < presets.length; i++) {\n if (presets[i].profileRef === profileId) out.push(presets[i]);\n }\n return out;\n}\n\nfunction getWalletExpanded(id) {\n if (!walletsState.expanded[id]) walletsState.expanded[id] = { edit: false, curate: false, curateMode: undefined };\n return walletsState.expanded[id];\n}\n\nfunction replaceProfileInState(profile) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;\n var list = pr.data.profiles;\n for (var i = 0; i < list.length; i++) {\n if (list[i].id === profile.id) { list[i] = profile; return; }\n }\n list.push(profile);\n}\n\nfunction removeProfileFromState(id) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;\n pr.data.profiles = pr.data.profiles.filter(function (p) { return p.id !== id; });\n}\n\nfunction renderWallets() {\n var el = document.getElementById(\"sec-wallets\");\n var rr = walletsState.rollupResult;\n var html = '<div class=\"sect-h\">Spend<span class=\"pillbar\" id=\"wallet-window\"></span>'\n + '<button type=\"button\" class=\"btn-sm\" id=\"spend-refresh-live\">refresh live</button></div>'\n + '<div class=\"muted-note\" style=\"padding:0 0 4px\">Refreshing live consumes a small sliver of the rate budget for that wallet.</div>';\n\n if (rr && rr.ok) {\n var t = rr.data.total || {};\n html += '<div class=\"card\"><div class=\"body\"><dl>'\n + row(\"Window\", escHtml(rr.data.window))\n + (typeof t.spentUsd === \"number\" ? row(\"Spend\", formatCost(t.spentUsd)) : \"\")\n + (typeof t.tokensIn === \"number\" || typeof t.tokensOut === \"number\"\n ? row(\"Tokens\", (typeof t.tokensIn === \"number\" ? formatTokens(t.tokensIn) + \" in\" : \"\") + (typeof t.tokensIn === \"number\" && typeof t.tokensOut === \"number\" ? \" · \" : \"\") + (typeof t.tokensOut === \"number\" ? formatTokens(t.tokensOut) + \" out\" : \"\"))\n : \"\")\n + (typeof t.unpricedTokens === \"number\" && t.unpricedTokens > 0 ? row(\"Unpriced tokens\", formatTokens(t.unpricedTokens)) : \"\")\n + '</dl></div></div>';\n } else if (rr) {\n html += '<div class=\"muted-note\">usage_rollup: ' + escHtml(rr.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Wallets'\n + '<button type=\"button\" class=\"btn-sm\" id=\"wallet-add-toggle\">' + (walletsState.showAddForm ? \"close\" : \"+ add wallet\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm\" id=\"wallet-scan-toggle\">' + (walletsState.showDiscovered ? \"close\" : \"find credentials\") + '</button>'\n + '</div>';\n\n if (walletsState.showAddForm) html += addWalletFormHtml();\n if (walletsState.showDiscovered) html += discoveredCredentialsHtml();\n\n html += '<div class=\"grid\" id=\"wallet-cards\"></div>';\n\n el.innerHTML = html;\n\n var pillEl = document.getElementById(\"wallet-window\");\n var windows = [\"5h\", \"7d\", \"30d\"];\n var pillHtml = \"\";\n for (var w = 0; w < windows.length; w++) {\n pillHtml += '<button data-win=\"' + windows[w] + '\" class=\"' + (windows[w] === walletsState.window ? \"active\" : \"\") + '\">' + windows[w] + '</button>';\n }\n pillEl.innerHTML = pillHtml;\n var pillBtns = pillEl.querySelectorAll(\"button\");\n for (var pb = 0; pb < pillBtns.length; pb++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { reloadWalletsWindow(btn.getAttribute(\"data-win\")); });\n })(pillBtns[pb]);\n }\n document.getElementById(\"spend-refresh-live\").addEventListener(\"click\", function () { fetchRollup(true); });\n\n document.getElementById(\"wallet-add-toggle\").addEventListener(\"click\", function () {\n walletsState.showAddForm = !walletsState.showAddForm;\n renderWallets();\n });\n document.getElementById(\"wallet-scan-toggle\").addEventListener(\"click\", function () {\n walletsState.showDiscovered = !walletsState.showDiscovered;\n if (walletsState.showDiscovered && !walletsState.discoveredResult) {\n loadTool(\"auth_discover_credentials\", {}).then(function (r) {\n walletsState.discoveredResult = r;\n renderWallets();\n });\n return;\n }\n renderWallets();\n });\n\n wireAddWalletForm(el);\n wireDiscoveredCredentials(el);\n renderWalletCards();\n}\n\nfunction addWalletFormHtml() {\n return '<div class=\"form-panel\" id=\"wallet-add-form\">'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Id</label><input type=\"text\" id=\"wf-id\" placeholder=\"my-anthropic-key\"></div>'\n + '<div class=\"field\"><label>Endpoint</label><input type=\"text\" id=\"wf-endpoint\" list=\"wf-endpoint-list\" placeholder=\"anthropic\"><datalist id=\"wf-endpoint-list\">'\n + '<option value=\"anthropic\"><option value=\"openrouter\"><option value=\"moonshot\"><option value=\"openai\"></datalist></div>'\n + '<div class=\"field\"><label>Method</label><select id=\"wf-method\"><option value=\"api-key\">API key</option><option value=\"oauth-bearer\">Subscription</option></select></div>'\n + '<div class=\"field\"><label>Label (optional)</label><input type=\"text\" id=\"wf-label\"></div>'\n + '</div>'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Credential</label><input type=\"password\" id=\"wf-credential\" placeholder=\"paste secret\"></div>'\n + '<div class=\"field\" id=\"wf-source-field\" style=\"display:none\"><label>Self-refreshing source (alternative)</label><input type=\"text\" id=\"wf-source\" placeholder=\"claude-code-oauth\"></div>'\n + '<button type=\"button\" class=\"btn-sm primary\" id=\"wf-submit\">Create wallet</button>'\n + '</div>'\n + '<div class=\"cfg-err\" id=\"wf-err\" style=\"display:none\"></div>'\n + '</div>';\n}\n\nfunction wireAddWalletForm(container) {\n var methodEl = container.querySelector(\"#wf-method\");\n var sourceField = container.querySelector(\"#wf-source-field\");\n if (!methodEl) return;\n methodEl.addEventListener(\"change\", function () {\n sourceField.style.display = methodEl.value === \"oauth-bearer\" ? \"\" : \"none\";\n });\n container.querySelector(\"#wf-submit\").addEventListener(\"click\", function () {\n var errEl = container.querySelector(\"#wf-err\");\n errEl.style.display = \"none\";\n var id = container.querySelector(\"#wf-id\").value.trim();\n var endpoint = container.querySelector(\"#wf-endpoint\").value.trim();\n var method = methodEl.value;\n var label = container.querySelector(\"#wf-label\").value.trim();\n var credentialInput = container.querySelector(\"#wf-credential\");\n var sourceInput = container.querySelector(\"#wf-source\");\n var credential = credentialInput.value;\n var source = method === \"oauth-bearer\" ? sourceInput.value.trim() : \"\";\n // Clear the secret from the DOM and this local scope's reference to it\n // immediately, before the async call even starts.\n credentialInput.value = \"\";\n if (!id || !endpoint) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Id and endpoint are required.\";\n credential = \"\";\n return;\n }\n if (!credential && !source) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Give either a credential or a self-refreshing source.\";\n credential = \"\";\n return;\n }\n var args = { id: id, endpoint: endpoint, method: method };\n if (label) args.label = label;\n if (credential) args.credential = credential;\n else if (source) args.source = source;\n credential = \"\";\n loadTool(\"auth_profile_create\", args).then(function (r) {\n if (!r.ok) {\n errEl.style.display = \"block\";\n errEl.textContent = r.message;\n return;\n }\n toast(\"Wallet created.\", false);\n walletsState.showAddForm = false;\n loadWallets();\n });\n });\n}\n\nfunction discoveredCredentialsHtml() {\n var dr = walletsState.discoveredResult;\n var html = '<div class=\"card\"><div class=\"body\">';\n if (!dr) {\n html += '<div class=\"muted-note\">scanning…</div>';\n } else if (!dr.ok) {\n html += '<div class=\"muted-note\">auth_discover_credentials: ' + escHtml(dr.message) + '</div>';\n } else {\n var creds = (dr.data && dr.data.credentials) || [];\n if (creds.length === 0) {\n html += '<div class=\"empty\">No importable credentials found on this host.</div>';\n } else {\n html += '<table><thead><tr><th>Endpoint</th><th>Method</th><th>Origin</th><th>Hint</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < creds.length; i++) {\n var c = creds[i];\n var defaultId = c.origin + \"-\" + c.endpoint;\n html += '<tr><td>' + escHtml(c.endpoint) + '</td><td>' + escHtml(BILLING_LABELS[c.method] || c.method) + '</td><td>' + escHtml(c.origin) + '</td><td>' + escHtml(c.hint) + '</td>'\n + '<td><input type=\"text\" class=\"dc-id\" data-dc-idx=\"' + i + '\" value=\"' + escHtml(defaultId) + '\" style=\"width:120px\">'\n + ' <button type=\"button\" class=\"btn-sm\" data-dc-import=\"' + i + '\">import</button></td></tr>';\n }\n html += '</tbody></table>';\n }\n }\n html += '</div></div>';\n return html;\n}\n\nfunction wireDiscoveredCredentials(container) {\n var btns = container.querySelectorAll(\"[data-dc-import]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var idx = Number(btn.getAttribute(\"data-dc-import\"));\n var dr = walletsState.discoveredResult;\n var c = dr && dr.ok && dr.data.credentials[idx];\n if (!c) return;\n var idInput = container.querySelector('.dc-id[data-dc-idx=\"' + idx + '\"]');\n var id = (idInput && idInput.value.trim()) || (c.origin + \"-\" + c.endpoint);\n loadTool(\"auth_profile_import\", { origin: c.origin, endpoint: c.endpoint, id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Imported.\", false);\n loadWallets();\n });\n });\n })(btns[i]);\n }\n}\n\nfunction renderWalletCards() {\n var cardsEl = document.getElementById(\"wallet-cards\");\n if (!cardsEl) return;\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok) {\n cardsEl.innerHTML = '<div class=\"empty\">' + (pr ? escHtml(pr.message) : \"loading…\") + '</div>';\n return;\n }\n var profiles = (pr.data && pr.data.profiles) || [];\n if (profiles.length === 0) {\n cardsEl.innerHTML = '<div class=\"empty\">No auth profiles configured on this host.</div>';\n return;\n }\n var rr = walletsState.rollupResult;\n var spend = rr && rr.ok ? byProfileMap(rr) : {};\n var out = \"\";\n for (var i = 0; i < profiles.length; i++) {\n out += walletCardHtml(profiles[i], spend[profiles[i].id]);\n }\n cardsEl.innerHTML = out;\n wireWalletCards(cardsEl);\n}\n\nfunction walletCardHtml(p, spend) {\n var title = p.label || p.id;\n var exp = getWalletExpanded(p.id);\n var out = cardOpen(p.id, title, escHtml(p.endpoint));\n out += row(\"Method\", escHtml(BILLING_LABELS[p.method] || humanize(p.method)));\n var keyLine = p.keyStatus === \"stored\"\n ? [p.fingerprint, p.last4].filter(Boolean).join(\" · \") || \"stored\"\n : p.keyStatus === \"self-refreshing\" ? \"self-refreshing\" : \"unavailable\";\n out += row(\"Key\", escHtml(keyLine));\n if (p.origin) out += row(\"Imported from\", escHtml(p.origin));\n out += '<dt>Enabled</dt><dd><label class=\"inline\"><input type=\"checkbox\" data-wallet-enabled=\"' + escHtml(p.id) + '\"' + (p.disabled ? \"\" : \" checked\") + '></label></dd>';\n var curated = !p.models || p.models.mode === \"all\" ? \"All eligible\" : (p.models.ids || []).length + \" curated\";\n out += row(\"Models\", escHtml(curated));\n if (spend) {\n if (typeof spend.spentUsd === \"number\") out += row(\"Spend (window)\", formatCost(spend.spentUsd));\n if (p.costBudget && typeof p.costBudget.maxCostUsd === \"number\") {\n var pct = p.costBudget.maxCostUsd > 0 ? ((spend.spentUsd || 0) / p.costBudget.maxCostUsd) * 100 : 0;\n out += row(\"Budget\", formatCost(p.costBudget.maxCostUsd) + \" / \" + escHtml(p.costBudget.window));\n out += bar(pct);\n }\n if (spend.remaining) {\n out += row(\"Remaining quota\", String(spend.remaining.remaining), \"resets \" + formatDate(spend.remaining.resetsAt));\n }\n if (spend.credits && typeof spend.credits.balanceUsd === \"number\") {\n out += row(\"Credit balance\", formatCost(spend.credits.balanceUsd), spend.credits.asOf ? \"as of \" + formatDate(spend.credits.asOf) : undefined);\n }\n } else if (p.costBudget && typeof p.costBudget.maxCostUsd === \"number\") {\n out += row(\"Budget\", formatCost(p.costBudget.maxCostUsd) + \" / \" + escHtml(p.costBudget.window));\n }\n out += '<div class=\"wallet-card-actions\">'\n + '<button type=\"button\" class=\"btn-sm\" data-wallet-edit-toggle=\"' + escHtml(p.id) + '\">' + (exp.edit ? \"close\" : \"rename / budget\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm\" data-wallet-curate-toggle=\"' + escHtml(p.id) + '\">' + (exp.curate ? \"close\" : \"manage models\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm danger\" data-wallet-delete=\"' + escHtml(p.id) + '\">delete</button>'\n + '</div>';\n if (exp.edit) out += walletEditPanelHtml(p);\n if (exp.curate) out += walletModelsEditorHtml(p, walletsState.providerModelsByEndpoint[p.endpoint], exp.curateMode);\n out += cardClose();\n return out;\n}\n\nfunction walletEditPanelHtml(p) {\n var hasBudget = !!(p.costBudget && typeof p.costBudget.maxCostUsd === \"number\");\n return '<div class=\"form-panel\" data-wallet-edit-panel=\"' + escHtml(p.id) + '\">'\n + '<div class=\"field\"><label>Label</label><input type=\"text\" class=\"we-label\" value=\"' + escHtml(p.label || \"\") + '\"></div>'\n + '<label class=\"inline\"><input type=\"checkbox\" class=\"we-no-budget\"' + (hasBudget ? \"\" : \" checked\") + '> no spend cap</label>'\n + '<div class=\"form-row we-budget-fields\" style=\"' + (hasBudget ? \"\" : \"display:none\") + '\">'\n + '<div class=\"field\"><label>Max cost (USD)</label><input type=\"number\" class=\"we-max-cost\" value=\"' + escHtml(hasBudget ? String(p.costBudget.maxCostUsd) : \"\") + '\"></div>'\n + '<div class=\"field\"><label>Window</label><input type=\"text\" class=\"we-window\" value=\"' + escHtml(hasBudget ? p.costBudget.window : \"7d\") + '\"></div>'\n + '<div class=\"field\"><label>Scope</label><select class=\"we-scope\"><option value=\"profile\"' + (hasBudget && p.costBudget.scope === \"profile\" ? \" selected\" : \"\") + '>profile</option><option value=\"session\"' + (hasBudget && p.costBudget.scope === \"session\" ? \" selected\" : \"\") + '>session</option></select></div>'\n + '</div>'\n + '<button type=\"button\" class=\"btn-sm primary\" data-wallet-edit-save=\"' + escHtml(p.id) + '\">save</button>'\n + '</div>';\n}\n\n// pendingMode is the panel's OWN pending selection (getWalletExpanded's\n// curateMode), not necessarily what the server has stored yet: switching to\n// \"curate\" must reveal the checklist right away so there is something to\n// check, without writing a destructive \"curate to nothing\" the instant the\n// radio moves. The first checked model is what actually persists the\n// mode=allow switch (see the .mm-id change handler).\nfunction walletModelsEditorHtml(p, catalogRows, pendingMode) {\n var mode = pendingMode || (p.models && p.models.mode) || \"all\";\n var ids = (p.models && p.models.ids) || [];\n var html = '<div class=\"form-panel\" data-wallet-models-panel=\"' + escHtml(p.id) + '\">';\n html += '<label class=\"inline\"><input type=\"radio\" name=\"mm-mode-' + escHtml(p.id) + '\" value=\"all\" class=\"mm-mode\" data-mm-profile=\"' + escHtml(p.id) + '\"' + (mode === \"all\" ? \" checked\" : \"\") + '> service all eligible</label> ';\n html += '<label class=\"inline\"><input type=\"radio\" name=\"mm-mode-' + escHtml(p.id) + '\" value=\"allow\" class=\"mm-mode\" data-mm-profile=\"' + escHtml(p.id) + '\"' + (mode === \"allow\" ? \" checked\" : \"\") + '> curate</label>';\n if (mode === \"allow\") html += ' <button type=\"button\" class=\"btn-sm\" data-mm-refresh=\"' + escHtml(p.id) + '\">refresh models</button>';\n html += '<div class=\"mm-list\" data-mm-list=\"' + escHtml(p.id) + '\" style=\"' + (mode === \"allow\" ? \"\" : \"display:none\") + '\">';\n if (mode === \"allow\") {\n if (!catalogRows) {\n html += '<div class=\"muted-note\">loading models…</div>';\n } else if (catalogRows.length === 0) {\n html += '<div class=\"empty\">No models known for this endpoint.</div>';\n } else {\n for (var i = 0; i < catalogRows.length; i++) {\n var m = catalogRows[i];\n var checked = ids.indexOf(m.id) !== -1;\n html += '<label class=\"inline\" style=\"display:block;margin:2px 0\"><input type=\"checkbox\" class=\"mm-id\" data-mm-profile=\"' + escHtml(p.id) + '\" value=\"' + escHtml(m.id) + '\"' + (checked ? \" checked\" : \"\") + '> ' + escHtml(m.label || m.id) + '</label>';\n }\n }\n }\n html += '</div></div>';\n return html;\n}\n\nfunction wireWalletCards(container) {\n var enabledToggles = container.querySelectorAll(\"[data-wallet-enabled]\");\n for (var i = 0; i < enabledToggles.length; i++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-wallet-enabled\");\n var wantEnabled = el.checked;\n el.disabled = true;\n loadTool(\"auth_profile_set_enabled\", { id: id, enabled: wantEnabled }).then(function (r) {\n el.disabled = false;\n if (!r.ok) { toast(r.message, true); el.checked = !wantEnabled; return; }\n replaceProfileInState(r.data.profile);\n toast(wantEnabled ? \"Wallet enabled.\" : \"Wallet disabled.\", false);\n renderWalletCards();\n });\n });\n })(enabledToggles[i]);\n }\n\n var editToggles = container.querySelectorAll(\"[data-wallet-edit-toggle]\");\n for (var e = 0; e < editToggles.length; e++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-edit-toggle\");\n var exp = getWalletExpanded(id);\n exp.edit = !exp.edit;\n renderWalletCards();\n });\n })(editToggles[e]);\n }\n\n var editSaves = container.querySelectorAll(\"[data-wallet-edit-save]\");\n for (var s = 0; s < editSaves.length; s++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-edit-save\");\n var panel = container.querySelector('[data-wallet-edit-panel=\"' + cssEscape(id) + '\"]');\n var label = panel.querySelector(\".we-label\").value.trim();\n var noBudget = panel.querySelector(\".we-no-budget\").checked;\n var patch = { id: id, label: label === \"\" ? null : label };\n if (noBudget) {\n patch.costBudget = null;\n } else {\n patch.costBudget = {\n maxCostUsd: Number(panel.querySelector(\".we-max-cost\").value) || 0,\n window: panel.querySelector(\".we-window\").value.trim() || \"7d\",\n scope: panel.querySelector(\".we-scope\").value,\n };\n }\n loadTool(\"auth_profile_update\", patch).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n toast(\"Wallet updated.\", false);\n getWalletExpanded(id).edit = false;\n renderWalletCards();\n });\n });\n })(editSaves[s]);\n }\n\n var noBudgetToggles = container.querySelectorAll(\".we-no-budget\");\n for (var nb = 0; nb < noBudgetToggles.length; nb++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var fields = el.parentElement.querySelector(\".we-budget-fields\");\n if (fields) fields.style.display = el.checked ? \"none\" : \"\";\n });\n })(noBudgetToggles[nb]);\n }\n\n var curateToggles = container.querySelectorAll(\"[data-wallet-curate-toggle]\");\n for (var c = 0; c < curateToggles.length; c++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-curate-toggle\");\n var exp = getWalletExpanded(id);\n exp.curate = !exp.curate;\n if (exp.curate) {\n var profile = findProfile(id);\n if (profile && !walletsState.providerModelsByEndpoint[profile.endpoint]) {\n loadTool(\"catalog_provider_models\", { endpoint: profile.endpoint, limit: 200 }).then(function (r) {\n walletsState.providerModelsByEndpoint[profile.endpoint] = r.ok ? (r.data.models || []) : [];\n renderWalletCards();\n });\n return;\n }\n }\n renderWalletCards();\n });\n })(curateToggles[c]);\n }\n\n var mmModes = container.querySelectorAll(\".mm-mode\");\n for (var mm = 0; mm < mmModes.length; mm++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-mm-profile\");\n if (el.value === \"all\") {\n getWalletExpanded(id).curateMode = undefined;\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"all\" }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n renderWalletCards();\n });\n } else {\n // Reveal the checklist locally (a pending selection, independent\n // of what the server has stored); nothing is written until a\n // model is actually checked, so switching this radio alone can\n // never silently curate the wallet down to nothing.\n getWalletExpanded(id).curateMode = \"allow\";\n renderWalletCards();\n }\n });\n })(mmModes[mm]);\n }\n\n var mmIds = container.querySelectorAll(\".mm-id\");\n for (var mi = 0; mi < mmIds.length; mi++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-mm-profile\");\n var list = container.querySelector('[data-mm-list=\"' + cssEscape(id) + '\"]');\n var checked = list.querySelectorAll(\".mm-id:checked\");\n var ids = [];\n for (var i2 = 0; i2 < checked.length; i2++) ids.push(checked[i2].value);\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"allow\", ids: ids }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n });\n });\n })(mmIds[mi]);\n }\n\n var mmRefresh = container.querySelectorAll(\"[data-mm-refresh]\");\n for (var mr = 0; mr < mmRefresh.length; mr++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-mm-refresh\");\n loadTool(\"auth_profile_refresh_models\", { id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n var added = (r.data.added || []).length;\n var removed = (r.data.removed || []).length;\n toast(\"Refreshed models (+\" + added + \" / -\" + removed + \").\", false);\n renderWalletCards();\n });\n });\n })(mmRefresh[mr]);\n }\n\n var deleteBtns = container.querySelectorAll(\"[data-wallet-delete]\");\n for (var d = 0; d < deleteBtns.length; d++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-wallet-delete\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"wallet-delete-\" + id, btn, function () {\n var blockedBy = presetsReferencing(id);\n if (blockedBy.length > 0) {\n var names = blockedBy.map(function (p) { return p.name || p.id; }).join(\", \");\n toast(\"Cannot delete: still used by harness preset(s) \" + names + \".\", true);\n return;\n }\n loadTool(\"auth_profile_delete\", { id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n removeProfileFromState(id);\n toast(\"Wallet deleted.\", false);\n renderWalletCards();\n });\n });\n });\n })(deleteBtns[d]);\n }\n}\n\nfunction findProfile(id) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok) return null;\n var list = (pr.data && pr.data.profiles) || [];\n for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];\n return null;\n}\n\n// ============================================================\n// Harnesses\n// ============================================================\n\nvar harnessesState = {\n adaptersResult: null,\n capsResult: null,\n presetsResult: null,\n rolesResult: null,\n profilesResult: null,\n expandedDefaults: {}, // slug -> bool\n defaultsCache: {}, // slug -> row[]\n showPresetForm: false,\n};\n\nfunction loadHarnesses() {\n return Promise.all([\n loadTool(\"adapter_list\", {}),\n loadTool(\"harness_capabilities\", {}),\n loadTool(\"harness_preset_list\", {}),\n loadTool(\"role_list\", {}),\n loadTool(\"auth_profile_list\", {}),\n ]).then(function (results) {\n harnessesState.adaptersResult = results[0];\n harnessesState.capsResult = results[1];\n harnessesState.presetsResult = results[2];\n harnessesState.rolesResult = results[3];\n harnessesState.profilesResult = results[4];\n renderHarnesses();\n });\n}\n\nfunction adapterDefaultsPaths(slug) {\n return [\n \"defaults.adapters.\" + slug + \".skills\",\n \"defaults.adapters.\" + slug + \".options\",\n \"defaults.adapters.\" + slug + \".contextContinuity\",\n \"defaults.adapters.\" + slug + \".auth.mode\",\n \"defaults.adapters.\" + slug + \".auth.source\",\n \"defaults.adapters.\" + slug + \".auth.provider\",\n \"defaults.adapters.\" + slug + \".auth.token\",\n \"defaults.adapters.\" + slug + \".auth.apiKey\",\n ];\n}\n\nfunction renderHarnesses() {\n var el = document.getElementById(\"sec-harnesses\");\n var adaptersR = harnessesState.adaptersResult;\n var capsR = harnessesState.capsResult;\n var presetsR = harnessesState.presetsResult;\n var rolesR = harnessesState.rolesResult;\n if (!adaptersR.ok) {\n el.innerHTML = '<div class=\"empty\">adapter_list: ' + escHtml(adaptersR.message) + '</div>';\n return;\n }\n var adapters = (adaptersR.data && adaptersR.data.adapters) || [];\n var capsBySlug = {};\n if (capsR.ok) {\n var caps = (capsR.data && capsR.data.capabilities) || [];\n for (var i = 0; i < caps.length; i++) capsBySlug[caps[i].adapter] = caps[i];\n }\n var presetsBySlug = {};\n var allPresets = [];\n if (presetsR.ok) {\n allPresets = (presetsR.data && presetsR.data.presets) || [];\n for (var j = 0; j < allPresets.length; j++) {\n var slug = allPresets[j].harnessSlug;\n if (!presetsBySlug[slug]) presetsBySlug[slug] = [];\n presetsBySlug[slug].push(allPresets[j]);\n }\n }\n\n var html = '<div class=\"sect-h\">Adapters</div>';\n if (!capsR.ok && capsR.unknown) html += '<div class=\"muted-note\">Capability detail needs a newer agentproto daemon.</div>';\n else if (!capsR.ok) html += '<div class=\"muted-note\">harness_capabilities: ' + escHtml(capsR.message) + '</div>';\n if (!presetsR.ok) html += '<div class=\"muted-note\">harness_preset_list: ' + escHtml(presetsR.message) + '</div>';\n\n html += '<div class=\"grid\">';\n if (adapters.length === 0) {\n html += '<div class=\"empty\">No adapters installed on this host.</div>';\n }\n for (var k = 0; k < adapters.length; k++) {\n var a = adapters[k];\n var cap = capsBySlug[a.slug];\n var slugPresets = presetsBySlug[a.slug] || [];\n var def = null;\n for (var d = 0; d < slugPresets.length; d++) if (slugPresets[d].isDefault) def = slugPresets[d];\n html += cardOpen(a.slug, a.name || a.slug, escHtml(a.slug));\n if (a.version) html += row(\"Version\", escHtml(a.version));\n if (a.protocol) html += row(\"Protocol\", escHtml(a.protocol));\n html += row(\"Models\", String((a.models || []).length));\n if (cap && Array.isArray(cap.providers) && cap.providers.length > 0) {\n var chips = \"\";\n for (var pi = 0; pi < cap.providers.length; pi++) {\n var prov = cap.providers[pi];\n chips += '<span class=\"tag ' + (prov.cred && prov.cred.present ? \"ok\" : \"\") + '\">' + escHtml(prov.billingEndpoint) + '</span> ';\n }\n html += '<dt>Billing</dt><dd>' + chips + '</dd>';\n }\n if (def) {\n html += row(\"Default preset\", escHtml(def.name) + ' → <a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(def.profileRef) + '\">' + escHtml(def.profileRef) + '</a> / ' + escHtml(def.defaultModel));\n } else if (slugPresets.length > 0) {\n html += row(\"Presets\", String(slugPresets.length) + \" (no default)\");\n }\n html += '<dt></dt><dd><button type=\"button\" class=\"btn-sm\" data-adapter-defaults-toggle=\"' + escHtml(a.slug) + '\">' + (harnessesState.expandedDefaults[a.slug] ? \"hide defaults\" : \"spawn defaults\") + '</button></dd>';\n html += '<div id=\"adapter-defaults-' + escHtml(a.slug) + '\"></div>';\n html += cardClose();\n }\n html += '</div>';\n\n html += '<div class=\"sect-h\">Presets<button type=\"button\" class=\"btn-sm\" id=\"preset-add-toggle\">' + (harnessesState.showPresetForm ? \"close\" : \"+ new preset\") + '</button></div>';\n if (harnessesState.showPresetForm) html += presetFormHtml(adapters);\n html += presetsTableHtml(allPresets);\n\n html += '<div class=\"sect-h\">Roles</div>';\n if (rolesR.ok) {\n var roles = (rolesR.data && rolesR.data.roles) || [];\n if (roles.length === 0) {\n html += '<div class=\"empty\">No roles registered.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Role</th><th>Level</th><th>Delegation</th><th>Can spawn</th></tr></thead><tbody>';\n for (var r = 0; r < roles.length; r++) {\n var role = roles[r];\n html += '<tr><td>' + escHtml(role.name) + '</td><td>' + escHtml(String(role.level)) + '</td><td>' + escHtml(role.delegation) + '</td><td>' + escHtml((role.spawnable || []).join(\", \")) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n } else {\n html += '<div class=\"muted-note\">role_list: ' + escHtml(rolesR.message) + '</div>';\n }\n\n el.innerHTML = html;\n wireGotoLinks(el);\n wireAdapterDefaultsToggles(el);\n wirePresetForm(el, adapters);\n wirePresetsTable(el);\n\n for (var slug2 in harnessesState.expandedDefaults) {\n if (harnessesState.expandedDefaults[slug2] && harnessesState.defaultsCache[slug2]) {\n renderAdapterDefaultsCard(slug2);\n }\n }\n}\n\nfunction presetsTableHtml(presets) {\n if (presets.length === 0) return '<div class=\"empty\">No harness presets configured.</div>';\n var html = '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Id</th><th>Harness</th><th>Name</th><th>Wallet</th><th>Model</th><th>Default</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < presets.length; i++) {\n var p = presets[i];\n html += '<tr data-row-id=\"' + escHtml(p.id) + '\"><td>' + escHtml(p.id) + '</td><td>' + escHtml(p.harnessSlug) + '</td><td>' + escHtml(p.name) + '</td>'\n + '<td><a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(p.profileRef) + '\">' + escHtml(p.profileRef) + '</a>' + (p.profileDisabled ? ' <span class=\"tag bad\">disabled/missing</span>' : '') + '</td>'\n + '<td>' + escHtml(p.defaultModel) + '</td>'\n + '<td>' + (p.isDefault ? '<span class=\"tag ok\">default</span>' : '<button type=\"button\" class=\"btn-sm\" data-preset-set-default=\"' + escHtml(p.id) + '\" data-preset-harness=\"' + escHtml(p.harnessSlug) + '\">set default</button>') + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-preset-delete=\"' + escHtml(p.id) + '\">delete</button></td></tr>';\n }\n html += '</tbody></table></div></div>';\n return html;\n}\n\nfunction presetFormHtml(adapters) {\n var profiles = (harnessesState.profilesResult && harnessesState.profilesResult.ok && harnessesState.profilesResult.data.profiles) || [];\n var html = '<div class=\"form-panel\">'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Id</label><input type=\"text\" id=\"pf-id\"></div>'\n + '<div class=\"field\"><label>Harness</label><select id=\"pf-harness\">';\n for (var i = 0; i < adapters.length; i++) html += '<option value=\"' + escHtml(adapters[i].slug) + '\">' + escHtml(adapters[i].slug) + '</option>';\n html += '</select></div>'\n + '<div class=\"field\"><label>Name</label><input type=\"text\" id=\"pf-name\"></div>'\n + '</div><div class=\"form-row\">'\n + '<div class=\"field\"><label>Wallet</label><select id=\"pf-profile\">';\n for (var j = 0; j < profiles.length; j++) html += '<option value=\"' + escHtml(profiles[j].id) + '\">' + escHtml(profiles[j].label || profiles[j].id) + '</option>';\n html += '</select></div>'\n + '<div class=\"field\"><label>Default model</label><input type=\"text\" id=\"pf-model\" placeholder=\"vendor/model\"></div>'\n + '<label class=\"inline\"><input type=\"checkbox\" id=\"pf-default\"> make default for this harness</label>'\n + '<button type=\"button\" class=\"btn-sm primary\" id=\"pf-submit\">Create preset</button>'\n + '</div><div class=\"cfg-err\" id=\"pf-err\" style=\"display:none\"></div></div>';\n return html;\n}\n\nfunction wirePresetForm(container, adapters) {\n var toggle = container.querySelector(\"#preset-add-toggle\");\n if (toggle) toggle.addEventListener(\"click\", function () {\n harnessesState.showPresetForm = !harnessesState.showPresetForm;\n renderHarnesses();\n });\n var submit = container.querySelector(\"#pf-submit\");\n if (!submit) return;\n submit.addEventListener(\"click\", function () {\n var errEl = container.querySelector(\"#pf-err\");\n errEl.style.display = \"none\";\n var id = container.querySelector(\"#pf-id\").value.trim();\n var harnessSlug = container.querySelector(\"#pf-harness\").value;\n var name = container.querySelector(\"#pf-name\").value.trim();\n var profileRef = container.querySelector(\"#pf-profile\").value;\n var defaultModel = container.querySelector(\"#pf-model\").value.trim();\n var isDefault = container.querySelector(\"#pf-default\").checked;\n if (!id || !name || !defaultModel || !profileRef) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Id, name, wallet, and default model are required.\";\n return;\n }\n loadTool(\"harness_preset_create\", { id: id, harnessSlug: harnessSlug, name: name, profileRef: profileRef, defaultModel: defaultModel, isDefault: isDefault }).then(function (r) {\n if (!r.ok) { errEl.style.display = \"block\"; errEl.textContent = r.message; return; }\n toast(\"Preset saved.\", false);\n harnessesState.showPresetForm = false;\n loadHarnesses();\n });\n });\n}\n\nfunction wirePresetsTable(container) {\n var setDefaults = container.querySelectorAll(\"[data-preset-set-default]\");\n for (var i = 0; i < setDefaults.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var presetId = btn.getAttribute(\"data-preset-set-default\");\n var harnessSlug = btn.getAttribute(\"data-preset-harness\");\n loadTool(\"harness_preset_set_default\", { harnessSlug: harnessSlug, presetId: presetId }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Default preset updated.\", false);\n loadHarnesses();\n });\n });\n })(setDefaults[i]);\n }\n var deletes = container.querySelectorAll(\"[data-preset-delete]\");\n for (var d = 0; d < deletes.length; d++) {\n (function (btn) {\n var presetId = btn.getAttribute(\"data-preset-delete\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"preset-delete-\" + presetId, btn, function () {\n loadTool(\"harness_preset_delete\", { id: presetId }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Preset deleted.\", false);\n loadHarnesses();\n });\n });\n });\n })(deletes[d]);\n }\n}\n\nfunction wireAdapterDefaultsToggles(container) {\n var toggles = container.querySelectorAll(\"[data-adapter-defaults-toggle]\");\n for (var i = 0; i < toggles.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var slug = btn.getAttribute(\"data-adapter-defaults-toggle\");\n harnessesState.expandedDefaults[slug] = !harnessesState.expandedDefaults[slug];\n if (harnessesState.expandedDefaults[slug] && !harnessesState.defaultsCache[slug]) {\n loadTool(\"config_get\", { keys: adapterDefaultsPaths(slug) }).then(function (r) {\n if (r.ok && r.data.revision) configRevision = r.data.revision;\n harnessesState.defaultsCache[slug] = r.ok ? (r.data.keys || []) : [];\n if (r.ok) notePendingRestart(r.data.keys);\n if (!r.ok && r.unknown) {\n var host = document.getElementById(\"adapter-defaults-\" + slug);\n if (host) host.innerHTML = '<div class=\"muted-note\">Per-harness defaults need a newer agentproto daemon.</div>';\n return;\n }\n if (!r.ok) {\n var host2 = document.getElementById(\"adapter-defaults-\" + slug);\n if (host2) host2.innerHTML = '<div class=\"muted-note\">config_get: ' + escHtml(r.message) + '</div>';\n return;\n }\n renderAdapterDefaultsCard(slug);\n });\n }\n btn.textContent = harnessesState.expandedDefaults[slug] ? \"hide defaults\" : \"spawn defaults\";\n var host3 = document.getElementById(\"adapter-defaults-\" + slug);\n if (host3 && !harnessesState.expandedDefaults[slug]) host3.innerHTML = \"\";\n else if (host3 && harnessesState.defaultsCache[slug]) renderAdapterDefaultsCard(slug);\n });\n })(toggles[i]);\n }\n}\n\nfunction renderAdapterDefaultsCard(slug) {\n var host = document.getElementById(\"adapter-defaults-\" + slug);\n if (!host) return;\n var rows = harnessesState.defaultsCache[slug] || [];\n var rowsByPath = {};\n for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];\n host.innerHTML = '<dl>' + rows.map(configFieldHtml).join(\"\") + '</dl>';\n wireConfigRows(host, rowsByPath, function (path) {\n loadTool(\"config_get\", { keys: adapterDefaultsPaths(slug) }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n harnessesState.defaultsCache[slug] = r.data.keys || [];\n notePendingRestart(r.data.keys);\n renderAdapterDefaultsCard(slug);\n });\n }, function () {\n loadHarnesses();\n });\n}\n\nfunction wireGotoLinks(container) {\n var links = container.querySelectorAll(\"a[data-goto]\");\n for (var i = 0; i < links.length; i++) {\n (function (a) {\n a.addEventListener(\"click\", function (e) {\n e.preventDefault();\n location.hash = \"#\" + a.getAttribute(\"data-goto\");\n });\n })(links[i]);\n }\n}\n\n// ============================================================\n// Models\n// ============================================================\n\nvar modelsState = { routes: [], runnableOnly: false, walletFilter: \"\", titlerRow: null };\n\nfunction loadModels() {\n return Promise.all([\n loadTool(\"catalog_models\", { full: true }),\n loadTool(\"auth_profile_list\", { full: true }),\n loadTool(\"config_get\", { keys: [\"titler.model\"] }),\n ]).then(function (results) {\n modelsState.routesResult = results[0];\n modelsState.profilesResult = results[1];\n modelsState.titlerResult = results[2];\n if (results[2].ok && results[2].data.revision) configRevision = results[2].data.revision;\n if (results[2].ok) notePendingRestart(results[2].data.keys);\n renderModels();\n });\n}\n\nfunction renderModels() {\n var el = document.getElementById(\"sec-models\");\n var rr = modelsState.routesResult;\n if (!rr.ok) {\n el.innerHTML = '<div class=\"empty\">catalog_models: ' + escHtml(rr.message) + '</div>';\n return;\n }\n var routes = (rr.data && rr.data.routes) || [];\n var profiles = (modelsState.profilesResult && modelsState.profilesResult.ok && modelsState.profilesResult.data && modelsState.profilesResult.data.profiles) || [];\n\n var html = '<div class=\"sect-h\">Titler model</div><div id=\"titler-model-card\"></div>';\n\n html += '<div class=\"sect-h\">Catalog<label class=\"inline\"><input type=\"checkbox\" id=\"runnable-only\"' + (modelsState.runnableOnly ? \" checked\" : \"\") + '> runnable only</label>'\n + '<label class=\"inline\">wallet <select id=\"wallet-filter\"><option value=\"\">any</option>';\n for (var i = 0; i < profiles.length; i++) {\n html += '<option value=\"' + escHtml(profiles[i].id) + '\"' + (profiles[i].id === modelsState.walletFilter ? \" selected\" : \"\") + '>' + escHtml(profiles[i].label || profiles[i].id) + '</option>';\n }\n html += '</select></label></div>';\n\n var filtered = routes.filter(function (r) {\n if (modelsState.runnableOnly && !r.runnable) return false;\n if (modelsState.walletFilter && (!r.eligibleProfiles || r.eligibleProfiles.indexOf(modelsState.walletFilter) === -1)) return false;\n return true;\n });\n\n var byVendor = {};\n var vendorOrder = [];\n for (var f = 0; f < filtered.length; f++) {\n var v = filtered[f].vendor;\n if (!byVendor[v]) { byVendor[v] = []; vendorOrder.push(v); }\n byVendor[v].push(filtered[f]);\n }\n vendorOrder.sort();\n\n if (vendorOrder.length === 0) {\n html += '<div class=\"empty\">No models match the current filters.</div>';\n }\n for (var vi = 0; vi < vendorOrder.length; vi++) {\n var vendor = vendorOrder[vi];\n var vendorRows = byVendor[vendor];\n var byProduct = {};\n var productOrder = [];\n for (var pr2 = 0; pr2 < vendorRows.length; pr2++) {\n var prod = vendorRows[pr2].product;\n if (!byProduct[prod]) { byProduct[prod] = []; productOrder.push(prod); }\n byProduct[prod].push(vendorRows[pr2]);\n }\n productOrder.sort();\n html += '<div class=\"sect-h\">' + escHtml(vendor) + '</div><div class=\"grid\">';\n for (var pi2 = 0; pi2 < productOrder.length; pi2++) {\n var product = productOrder[pi2];\n var productRoutes = byProduct[product];\n var modelRef = vendor + \"/\" + product;\n html += cardOpen(modelRef, product, \"\");\n for (var ri = 0; ri < productRoutes.length; ri++) {\n var route = productRoutes[ri];\n var priceStr = route.pricing ? \"$\" + route.pricing.inPer1M + \"/$\" + route.pricing.outPer1M + \" per 1M\" : \"\";\n var elig = (route.eligibleProfiles || []).map(function (id) {\n return '<a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(id) + '\">' + escHtml(id) + '</a>';\n }).join(\" \");\n html += row(route.route, (route.runnable ? '<span class=\"tag ok\">runnable</span>' : '<span class=\"tag\">not runnable</span>') + (route.curated ? ' <span class=\"tag\">curated</span>' : \"\"));\n if (priceStr) html += row(\"Price\", escHtml(priceStr));\n if (route.contextWindow) html += row(\"Context\", escHtml(route.contextWindow) + (route.maxOutput ? \" / \" + escHtml(route.maxOutput) + \" out\" : \"\"));\n if (elig) html += '<dt>Wallets</dt><dd>' + elig + '</dd>';\n }\n html += cardClose();\n }\n html += '</div>';\n }\n\n html += '<div class=\"sect-h\">Wallet × model matrix</div>' + walletModelMatrixHtml(routes, profiles);\n\n el.innerHTML = html;\n wireGotoLinks(el);\n wireModelMatrix(el, routes, profiles);\n renderTitlerModelCard();\n\n var runnableEl = document.getElementById(\"runnable-only\");\n if (runnableEl) runnableEl.addEventListener(\"change\", function (e) { modelsState.runnableOnly = e.target.checked; renderModels(); });\n var walletEl = document.getElementById(\"wallet-filter\");\n if (walletEl) walletEl.addEventListener(\"change\", function (e) { modelsState.walletFilter = e.target.value; renderModels(); });\n}\n\nfunction walletModelMatrixHtml(routes, profiles) {\n if (profiles.length === 0) return '<div class=\"empty\">No wallets configured.</div>';\n var eligibleRoutes = routes.filter(function (r) { return r.eligibleProfiles && r.eligibleProfiles.length > 0; }).slice(0, 40);\n if (eligibleRoutes.length === 0) return '<div class=\"empty\">No wallet-reachable models yet.</div>';\n var html = '<div class=\"card\"><div class=\"body\" style=\"overflow-x:auto\"><table><thead><tr><th>Wallet</th>';\n for (var c = 0; c < eligibleRoutes.length; c++) html += '<th title=\"' + escHtml(eligibleRoutes[c].route) + '\">' + escHtml(eligibleRoutes[c].product) + '</th>';\n html += '</tr></thead><tbody>';\n for (var i = 0; i < profiles.length; i++) {\n var p = profiles[i];\n html += '<tr><td>' + escHtml(p.label || p.id) + '</td>';\n for (var j = 0; j < eligibleRoutes.length; j++) {\n var route = eligibleRoutes[j];\n var modelId = route.ref || (route.vendor + \"/\" + route.product);\n var reachable = route.eligibleProfiles.indexOf(p.id) !== -1;\n if (!reachable) {\n html += '<td class=\"matrix-cell disabled\">–</td>';\n continue;\n }\n var mode = (p.models && p.models.mode) || \"all\";\n var checked = mode === \"all\" || (p.models.ids || []).indexOf(modelId) !== -1;\n html += '<td class=\"matrix-cell\" data-matrix-profile=\"' + escHtml(p.id) + '\" data-matrix-model=\"' + escHtml(modelId) + '\" data-matrix-mode=\"' + escHtml(mode) + '\">' + (checked ? \"✓\" : \"–\") + '</td>';\n }\n html += '</tr>';\n }\n html += '</tbody></table></div></div><div class=\"muted-note\">Click a checked cell in an \"all eligible\" wallet to start curating it; click a cell to toggle curation once curating.</div>';\n return html;\n}\n\nfunction wireModelMatrix(container, routes, profiles) {\n var cells = container.querySelectorAll(\"[data-matrix-profile]\");\n for (var i = 0; i < cells.length; i++) {\n (function (cell) {\n cell.addEventListener(\"click\", function () {\n var id = cell.getAttribute(\"data-matrix-profile\");\n var modelId = cell.getAttribute(\"data-matrix-model\");\n var p = null;\n for (var k = 0; k < profiles.length; k++) if (profiles[k].id === id) p = profiles[k];\n if (!p) return;\n var mode = (p.models && p.models.mode) || \"all\";\n var currentIds = (p.models && p.models.ids) || [];\n var nextIds;\n if (mode === \"all\") {\n // Start curating from just this one model.\n nextIds = [modelId];\n } else if (currentIds.indexOf(modelId) !== -1) {\n nextIds = currentIds.filter(function (x) { return x !== modelId; });\n } else {\n nextIds = currentIds.concat([modelId]);\n }\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"allow\", ids: nextIds }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n for (var k2 = 0; k2 < profiles.length; k2++) if (profiles[k2].id === id) profiles[k2] = r.data.profile;\n if (modelsState.profilesResult && modelsState.profilesResult.ok) {\n for (var k3 = 0; k3 < modelsState.profilesResult.data.profiles.length; k3++) {\n if (modelsState.profilesResult.data.profiles[k3].id === id) modelsState.profilesResult.data.profiles[k3] = r.data.profile;\n }\n }\n renderModels();\n });\n });\n })(cells[i]);\n }\n}\n\nfunction renderTitlerModelCard() {\n var host = document.getElementById(\"titler-model-card\");\n if (!host) return;\n var tr = modelsState.titlerResult;\n if (!tr.ok && tr.unknown) {\n host.innerHTML = '<div class=\"muted-note\">Editing titler.model needs a newer agentproto daemon (config_get).</div>';\n return;\n }\n if (!tr.ok) {\n host.innerHTML = '<div class=\"muted-note\">config_get: ' + escHtml(tr.message) + '</div>';\n return;\n }\n var rows = tr.data.keys || [];\n var rowsByPath = {};\n for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];\n host.innerHTML = '<div class=\"card\"><div class=\"body\"><dl>' + rows.map(configFieldHtml).join(\"\") + '</dl></div></div>';\n wireConfigRows(host, rowsByPath, function () {\n loadTool(\"config_get\", { keys: [\"titler.model\"] }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n modelsState.titlerResult = r;\n notePendingRestart(r.data.keys);\n renderTitlerModelCard();\n });\n }, function () {\n loadModels();\n });\n}\n\n// ============================================================\n// Capabilities (MCP & Skills)\n// ============================================================\n\nvar capabilitiesState = {\n tab: \"mcp\", // \"mcp\" | \"skills\"\n invResult: null,\n expandedImports: {}, // importId -> bool\n};\n\nfunction loadCapabilities() {\n return loadTool(\"capabilities_inventory\", {}).then(function (r) {\n capabilitiesState.invResult = r;\n renderCapabilities();\n });\n}\n\nfunction capTabsHtml() {\n return '<span class=\"pillbar\">'\n + '<button type=\"button\" class=\"' + (capabilitiesState.tab === \"mcp\" ? \"active\" : \"\") + '\" data-cap-tab=\"mcp\">MCP</button>'\n + '<button type=\"button\" class=\"' + (capabilitiesState.tab === \"skills\" ? \"active\" : \"\") + '\" data-cap-tab=\"skills\">Skills</button>'\n + '</span>';\n}\n\nfunction capStatusTagHtml(status) {\n var cls = status === \"connected\" ? \"ok\" : status === \"error\" ? \"bad\" : status === \"idle\" ? \"\" : \"warn-tag\";\n return '<span class=\"tag ' + cls + '\">' + escHtml(status) + '</span>';\n}\n\nfunction renderCapabilities() {\n var el = document.getElementById(\"sec-capabilities\");\n var r = capabilitiesState.invResult;\n var html = '<div class=\"sect-h\">Capabilities' + capTabsHtml() + '</div>';\n if (!r) { el.innerHTML = html; return; }\n if (!r.ok) {\n html += r.unknown\n ? '<div class=\"muted-note\">Capabilities inventory needs a newer agentproto daemon (capabilities_inventory).</div>'\n : '<div class=\"muted-note\">capabilities_inventory: ' + escHtml(r.message) + '</div>';\n el.innerHTML = html;\n return;\n }\n var inv = r.data;\n html += capabilitiesState.tab === \"skills\" ? renderSkillsTabHtml(inv.skills || {}) : renderMcpTabHtml(inv.mcp || {});\n el.innerHTML = html;\n wireCapabilitiesTabs(el);\n if (capabilitiesState.tab === \"mcp\") wireMcpTab(el);\n}\n\nfunction wireCapabilitiesTabs(container) {\n var btns = container.querySelectorAll(\"[data-cap-tab]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n capabilitiesState.tab = btn.getAttribute(\"data-cap-tab\");\n renderCapabilities();\n });\n })(btns[i]);\n }\n}\n\nfunction renderMcpTabHtml(mcp) {\n var imported = mcp.imported || [];\n var discovered = mcp.discovered || [];\n var reach = mcp.daemonMountByHarness || {};\n var html = \"\";\n if (mcp.error) html += '<div class=\"muted-note\">mcp: ' + escHtml(mcp.error) + '</div>';\n\n html += '<div class=\"sect-h\">Imported MCP servers</div>';\n if (imported.length === 0) {\n html += '<div class=\"empty\">No MCP servers imported yet.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Source</th><th>Status</th><th>Tools</th><th>Used by</th><th></th><th></th></tr></thead><tbody>';\n for (var i = 0; i < imported.length; i++) {\n var m = imported[i];\n var expanded = !!capabilitiesState.expandedImports[m.id];\n var usedBy = (m.usedBySessions || []).length;\n html += '<tr data-card-id=\"' + escHtml(m.id) + '\" data-row-id=\"' + escHtml(m.id) + '\">'\n + '<td>' + escHtml(m.alias || m.name) + '</td>'\n + '<td>' + escHtml(m.source) + '</td>'\n + '<td>' + capStatusTagHtml(m.status) + (m.error ? ' <span class=\"tag bad\" title=\"' + escHtml(m.error) + '\">!</span>' : '') + '</td>'\n + '<td>' + (m.toolCount != null ? String(m.toolCount) : \"–\") + '</td>'\n + '<td>' + (usedBy > 0 ? \"used by \" + usedBy + \" session\" + (usedBy === 1 ? \"\" : \"s\") : \"unused\") + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm\" data-cap-import-tools=\"' + escHtml(m.id) + '\" data-cap-import-alias=\"' + escHtml(m.alias || m.name) + '\">' + (expanded ? \"hide tools\" : \"show tools\") + '</button></td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-cap-import-remove=\"' + escHtml(m.id) + '\">remove</button></td>'\n + '</tr>'\n + '<tr><td colspan=\"7\"><div id=\"cap-import-tools-' + escHtml(pathId(m.id)) + '\"></div></td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n\n html += '<div class=\"sect-h\">Reach</div>';\n var slugs = Object.keys(reach);\n if (slugs.length === 0) {\n html += '<div class=\"empty\">No adapters installed.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\">';\n for (var s = 0; s < slugs.length; s++) {\n var r2 = reach[slugs[s]];\n var cls = r2 === \"default\" ? \"ok\" : r2 === \"on-request\" ? \"\" : \"bad\";\n html += '<span class=\"tag ' + cls + '\" style=\"margin:2px 4px 2px 0\">' + escHtml(slugs[s]) + \": \" + escHtml(r2) + '</span>';\n }\n html += '<div class=\"muted-note\">\"default\" harnesses get the imported pool automatically; \"on-request\" harnesses can mount it explicitly via <code>mcpServers</code>; \"none\" harnesses cannot reach it at all. Either way, reach is via <code>mcp_imported_tool_list</code> / <code>mcp_imported_call</code>, not a direct per-server mount.</div>';\n html += '</div></div>';\n }\n\n html += '<div class=\"sect-h\">Discovered, not imported</div>';\n var notImported = discovered.filter(function (d) { return !d.imported; });\n if (notImported.length === 0) {\n html += '<div class=\"empty\">Nothing new discovered.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Source</th><th>Type</th><th></th></tr></thead><tbody>';\n for (var d = 0; d < notImported.length; d++) {\n var disc = notImported[d];\n html += '<tr data-card-id=\"' + escHtml(disc.id) + '\" data-row-id=\"' + escHtml(disc.id) + '\"><td>' + escHtml(disc.name) + '</td><td>' + escHtml(disc.source) + '</td><td>' + escHtml(disc.type) + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm primary\" data-cap-import=\"' + escHtml(disc.id) + '\">import</button></td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n return html;\n}\n\nfunction wireMcpTab(container) {\n var toolBtns = container.querySelectorAll(\"[data-cap-import-tools]\");\n for (var i = 0; i < toolBtns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-cap-import-tools\");\n var alias = btn.getAttribute(\"data-cap-import-alias\");\n var expanded = !capabilitiesState.expandedImports[id];\n capabilitiesState.expandedImports[id] = expanded;\n btn.textContent = expanded ? \"hide tools\" : \"show tools\";\n var host = document.getElementById(\"cap-import-tools-\" + pathId(id));\n if (!host) return;\n if (!expanded) { host.innerHTML = \"\"; return; }\n host.innerHTML = '<div class=\"muted-note\">Loading tools…</div>';\n loadTool(\"mcp_imported_tool_list\", { alias: alias }).then(function (r) {\n if (!r.ok) { host.innerHTML = '<div class=\"muted-note\">mcp_imported_tool_list: ' + escHtml(r.message) + '</div>'; return; }\n var tools = (r.data && r.data.tools) || [];\n if (tools.length === 0) { host.innerHTML = '<div class=\"empty\">No tools (server may not be connected yet).</div>'; return; }\n var h = '<div class=\"muted-note\">' + tools.length + ' tool(s):</div><ul>';\n for (var t = 0; t < tools.length; t++) {\n h += '<li><code>' + escHtml(tools[t].name) + '</code>' + (tools[t].description ? \" — \" + escHtml(tools[t].description) : \"\") + '</li>';\n }\n h += '</ul>';\n host.innerHTML = h;\n });\n });\n })(toolBtns[i]);\n }\n\n var removeBtns = container.querySelectorAll(\"[data-cap-import-remove]\");\n for (var r3 = 0; r3 < removeBtns.length; r3++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-cap-import-remove\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"cap-import-remove-\" + id, btn, function () {\n loadTool(\"mcp_imported_remove\", { id: id }).then(function (res) {\n if (!res.ok) { toast(res.message, true); return; }\n toast(\"Removed.\", false);\n loadCapabilities();\n });\n });\n });\n })(removeBtns[r3]);\n }\n\n var importBtns = container.querySelectorAll(\"[data-cap-import]\");\n for (var im = 0; im < importBtns.length; im++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-cap-import\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"cap-import-\" + id, btn, function () {\n loadTool(\"mcp_import\", { sourceMcpId: id }).then(function (res) {\n if (!res.ok) { toast(res.message, true); return; }\n toast(\"Imported.\", false);\n loadCapabilities();\n });\n });\n });\n })(importBtns[im]);\n }\n}\n\nfunction capPackHasSkill(packs, name) {\n for (var i = 0; i < packs.length; i++) {\n if ((packs[i].skills || []).indexOf(name) !== -1) return true;\n }\n return false;\n}\n\nfunction renderSkillsTabHtml(skills) {\n var harnesses = skills.byHarness || [];\n var packs = skills.packs || [];\n var defaults = skills.defaults || { global: [], byAdapter: {} };\n var html = \"\";\n if (skills.error) html += '<div class=\"muted-note\">skills: ' + escHtml(skills.error) + '</div>';\n\n html += '<div class=\"sect-h\">Skills by harness</div>';\n if (harnesses.length === 0) {\n html += '<div class=\"empty\">No adapters installed.</div>';\n return html;\n }\n\n var skillNames = {};\n for (var p = 0; p < packs.length; p++) {\n for (var s = 0; s < (packs[p].skills || []).length; s++) skillNames[packs[p].skills[s]] = true;\n }\n for (var h = 0; h < harnesses.length; h++) {\n for (var i2 = 0; i2 < (harnesses[h].installed || []).length; i2++) skillNames[harnesses[h].installed[i2]] = true;\n for (var n = 0; n < (harnesses[h].native || []).length; n++) skillNames[harnesses[h].native[n]] = true;\n }\n var names = Object.keys(skillNames).sort();\n\n html += '<div class=\"card\"><div class=\"body\" style=\"overflow-x:auto\"><table><thead><tr><th>Skill</th>';\n for (var c = 0; c < harnesses.length; c++) {\n var target = harnesses[c].target;\n html += '<th data-card-id=\"' + escHtml(harnesses[c].adapter) + '\">' + escHtml(harnesses[c].adapter)\n + '<div class=\"cfg-help\">' + (target ? escHtml(target.dir || \"\") : \"unsupported\") + '</div>'\n + (harnesses[c].spawnOption ? '<div class=\"cfg-help\">skills spawn option</div>' : '')\n + '</th>';\n }\n html += '</tr></thead><tbody>';\n\n html += '<tr><td><em>defaults</em></td>';\n for (var d = 0; d < harnesses.length; d++) {\n var adapterDefaults = defaults.byAdapter[harnesses[d].adapter] || [];\n var all = defaults.global.concat(adapterDefaults);\n html += '<td>' + (all.length > 0 ? escHtml(all.join(\", \")) : \"–\") + '</td>';\n }\n html += '</tr>';\n\n if (names.length === 0) {\n html += '<tr><td colspan=\"' + (harnesses.length + 1) + '\">No skills found in any installed pack.</td></tr>';\n }\n for (var nm = 0; nm < names.length; nm++) {\n html += '<tr><td>' + escHtml(names[nm]) + '</td>';\n for (var hc = 0; hc < harnesses.length; hc++) {\n var row = harnesses[hc];\n var cell;\n if (!row.target) {\n cell = '<span class=\"tag\">unsupported</span>';\n } else if ((row.installed || []).indexOf(names[nm]) !== -1) {\n cell = '<span class=\"tag ok\">installed</span>';\n } else if ((row.native || []).indexOf(names[nm]) !== -1) {\n cell = '<span class=\"tag ok\">native</span>';\n } else if (capPackHasSkill(packs, names[nm])) {\n cell = '<span class=\"tag warn-tag\">available</span><div class=\"cfg-help\"><code>agentproto install skill/' + escHtml(names[nm]) + '</code></div>';\n } else {\n cell = '<span class=\"tag\">–</span>';\n }\n html += '<td>' + cell + '</td>';\n }\n html += '</tr>';\n }\n html += '</tbody></table></div></div>';\n\n html += '<div class=\"sect-h\">Packs</div>';\n if (packs.length === 0) {\n html += '<div class=\"empty\">No skill packs found under ~/.agentproto/packs.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Pack</th><th>Version</th><th>Skills</th></tr></thead><tbody>';\n for (var pk = 0; pk < packs.length; pk++) {\n html += '<tr data-card-id=\"' + escHtml(packs[pk].name) + '\"><td>' + escHtml(packs[pk].name) + '</td><td>' + escHtml(packs[pk].version || \"\") + '</td><td>' + escHtml((packs[pk].skills || []).join(\", \")) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n return html;\n}\n\n// ============================================================\n// Defaults & messaging\n// ============================================================\n\nvar DEFAULTS_GROUPS = [\n {\n id: \"messaging\", title: \"Messaging and prompts\",\n match: function (p) {\n return p.indexOf(\"defaults.messaging.\") === 0 || p === \"defaults.agentPromptInterrupt\"\n || p === \"defaults.backgroundTaskWake.enabled\" || p === \"defaults.backgroundTaskWake.graceMs\";\n },\n },\n {\n id: \"spawn\", title: \"Spawn\",\n match: function (p) {\n return p.indexOf(\"spawn.\") === 0 || p.indexOf(\"worktrees.\") === 0 || p.indexOf(\"defaults.spawn.\") === 0\n || p === \"defaults.skills\" || p === \"defaults.options\" || p === \"defaults.defaultRoleDepthCutoff\"\n || p === \"defaults.maxGrantableDelegation\" || p === \"provenance.wrapGh\" || p === \"agentsMd.inlineMaxKb\";\n },\n },\n { id: \"context-continuity\", title: \"Context continuity\", match: function (p) { return p === \"defaults.contextContinuity\"; } },\n {\n id: \"observability\", title: \"Observability\",\n match: function (p) {\n return p.indexOf(\"titler.\") === 0 || p === \"defaults.langfuseTracing\" || p === \"defaults.traceRedactor\"\n || p === \"sessions.attentionDelaySec\" || p === \"defaults.mcp.deferredTools\";\n },\n },\n {\n id: \"daemon\", title: \"Daemon\",\n match: function (p) {\n return p.indexOf(\"daemon.\") === 0 || p === \"sessions.eventsDir\" || p === \"features.pty\" || p === \"features.llmEndpoint\"\n || p === \"profiles\" || p === \"activeProfile\";\n },\n },\n];\n\nfunction classifyDefaultsGroup(path) {\n for (var i = 0; i < DEFAULTS_GROUPS.length; i++) if (DEFAULTS_GROUPS[i].match(path)) return DEFAULTS_GROUPS[i];\n return { id: \"other\", title: \"Other\" };\n}\n\nvar defaultsState = { rowsByPath: {}, healthFallback: null };\n\nfunction loadDefaults() {\n return Promise.all([\n loadTool(\"config_get\", { section: \"defaults\" }),\n loadTool(\"config_get\", { section: \"daemon\" }),\n loadTool(\"daemon_health\", {}),\n ]).then(function (results) {\n renderDefaults(results[0], results[1], results[2]);\n });\n}\n\nfunction renderDefaults(defR, daemonR, healthR) {\n var el = document.getElementById(\"sec-defaults\");\n\n if (!defR.ok && defR.unknown) {\n // Feature-detect fallback for a daemon that predates config_get: the\n // five effective knobs daemon_health already reports, read-only.\n var html = '<div class=\"notice\">Full defaults editor needs a newer agentproto daemon (config_get). Showing the knobs daemon_health reports.</div>';\n if (healthR.ok) {\n var hd = healthR.data;\n var restartBadge = ' <span class=\"tag\">restart</span>';\n html += '<div class=\"card\" data-card-id=\"daemon\"><div class=\"title\"><span>Daemon</span>'\n + (hd.version ? '<span class=\"aside\">' + escHtml(hd.version) + '</span>' : '') + '</div><div class=\"body\"><dl>'\n + row(\"Resume sessions on boot\", (hd.resumeSessionsOnBoot ? \"Yes\" : \"No\") + restartBadge)\n + row(\"Idle reap after\", (hd.idleReapAfterMs ? formatMs(hd.idleReapAfterMs) : \"off\") + restartBadge)\n + row(\"Crash detect interval\", (hd.crashDetectIntervalMs ? formatMs(hd.crashDetectIntervalMs) : \"off\") + restartBadge)\n + row(\"Restart sweep interval\", (hd.restartSweepIntervalMs ? formatMs(hd.restartSweepIntervalMs) : \"off\") + restartBadge)\n + row(\"Turn stall after\", (hd.turnStallAfterMs ? formatMs(hd.turnStallAfterMs) : \"off\") + restartBadge)\n + (hd.build && hd.build.sha ? row(\"Build\", escHtml(hd.build.sha)) : \"\")\n + '</dl></div></div>';\n } else {\n html += '<div class=\"muted-note\">daemon_health: ' + escHtml(healthR.message) + '</div>';\n }\n el.innerHTML = html;\n return;\n }\n if (!defR.ok) {\n el.innerHTML = '<div class=\"empty\">config_get: ' + escHtml(defR.message) + '</div>';\n return;\n }\n\n if (defR.data.revision) configRevision = defR.data.revision;\n var rows = (defR.data.keys || []).concat(daemonR.ok ? (daemonR.data.keys || []) : []);\n notePendingRestart(rows);\n defaultsState.rowsByPath = {};\n for (var i = 0; i < rows.length; i++) defaultsState.rowsByPath[rows[i].path] = rows[i];\n\n el.innerHTML = renderDefaultsGroupsHtml();\n wireConfigRows(el, defaultsState.rowsByPath, function (path) {\n refreshDefaultsGroupFor(path);\n }, function () {\n loadDefaults();\n });\n}\n\nfunction objectValues(obj) {\n var out = [];\n for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) out.push(obj[k]);\n return out;\n}\n\nfunction renderDefaultsGroupsHtml() {\n var byId = {};\n var order = [];\n var rows = objectValues(defaultsState.rowsByPath);\n for (var i = 0; i < rows.length; i++) {\n var g = classifyDefaultsGroup(rows[i].path);\n if (!byId[g.id]) { byId[g.id] = { id: g.id, title: g.title, rows: [] }; order.push(g.id); }\n byId[g.id].rows.push(rows[i]);\n }\n var html = \"\";\n for (var j = 0; j < order.length; j++) {\n var group = byId[order[j]];\n html += '<div class=\"sect-h\">' + escHtml(group.title) + '</div>' + buildConfigCardHtml(group.id, group.title, group.rows);\n }\n return html;\n}\n\nfunction refreshDefaultsGroupFor(path) {\n var group = classifyDefaultsGroup(path);\n var groupPaths = [];\n for (var k in defaultsState.rowsByPath) {\n if (Object.prototype.hasOwnProperty.call(defaultsState.rowsByPath, k) && classifyDefaultsGroup(k).id === group.id) groupPaths.push(k);\n }\n loadTool(\"config_get\", { keys: groupPaths }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n var newRows = r.data.keys || [];\n for (var i = 0; i < newRows.length; i++) defaultsState.rowsByPath[newRows[i].path] = newRows[i];\n notePendingRestart(newRows);\n var el = document.getElementById(\"sec-defaults\");\n var cardEl = el.querySelector('[data-card-id=\"' + cssEscape(group.id) + '\"]');\n if (!cardEl) return;\n cardEl.outerHTML = buildConfigCardHtml(group.id, group.title, newRows);\n var freshEl = el.querySelector('[data-card-id=\"' + cssEscape(group.id) + '\"]');\n wireConfigRows(freshEl, defaultsState.rowsByPath, function (p2) { refreshDefaultsGroupFor(p2); }, function () { loadDefaults(); });\n });\n}\n\n// ============================================================\n// Remote & pairing\n// ============================================================\n\n// Held ONLY in memory, never in section state that survives a re-render\n// across sections, never in updateModelContext. Cleared on navigating away\n// from #remote (route()) and on host teardown (see boot, below).\nvar remoteReveal = null;\nvar pairReveal = null;\nvar pairCountdownTimer = null;\n\nfunction clearPairCountdown() {\n if (pairCountdownTimer) { clearInterval(pairCountdownTimer); pairCountdownTimer = null; }\n}\n\nfunction startPairCountdown(expiresAtIso) {\n clearPairCountdown();\n pairCountdownTimer = setInterval(function () {\n var el = document.getElementById(\"pair-offer-countdown\");\n if (!el) { clearPairCountdown(); return; }\n var ms = new Date(expiresAtIso).getTime() - Date.now();\n if (ms <= 0) { el.textContent = \"expired\"; clearPairCountdown(); return; }\n el.textContent = Math.ceil(ms / 1000) + \"s remaining\";\n }, 1000);\n}\n\nfunction loadRemote() {\n return Promise.all([\n loadTool(\"remote_status\", {}),\n loadTool(\"pair_list\", {}),\n loadTool(\"tunnel_list\", {}),\n ]).then(function (results) {\n renderRemote(results[0], results[1], results[2]);\n });\n}\n\nfunction renderRemote(remoteR, pairR, tunnelR) {\n var el = document.getElementById(\"sec-remote\");\n var html = '<div class=\"sect-h\">Remote gateway</div>';\n if (remoteR.ok) {\n var rs = remoteR.data;\n html += '<div class=\"card\" data-card-id=\"remote\"><div class=\"body\"><dl>'\n + row(\"Enabled\", rs.enabled ? \"Yes\" : \"No\")\n + (rs.provider ? row(\"Provider\", escHtml(rs.provider)) : \"\")\n + (rs.publicUrl ? row(\"Public URL\", escHtml(rs.publicUrl)) : \"\")\n + (rs.pid != null ? row(\"PID\", String(rs.pid)) : \"\")\n + (rs.createdAt ? row(\"Created\", fmtRelative(rs.createdAt)) : \"\")\n + (rs.lastError ? row(\"Last error\", escHtml(rs.lastError)) : \"\")\n + '</dl>'\n + '<div class=\"wallet-card-actions\">'\n + (rs.enabled\n ? '<button type=\"button\" class=\"btn-sm danger\" id=\"remote-disable-btn\">disable</button>'\n : '<button type=\"button\" class=\"btn-sm primary\" id=\"remote-enable-btn\">enable</button>')\n + '</div>';\n if (remoteReveal) {\n html += '<div class=\"reveal-box\">'\n + '<div>Shown once. Store it now, it will not be shown again.</div>'\n + (remoteReveal.bearerToken ? '<div>Bearer token<code>' + escHtml(remoteReveal.bearerToken) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"bearer\">copy token</button></div>' : '')\n + (remoteReveal.phoneUrl ? '<div>Phone link<code>' + escHtml(remoteReveal.phoneUrl) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"phoneUrl\">copy link</button></div>' : '')\n + (remoteReveal.mcpConfigSnippet ? '<div>.mcp.json<code>' + escHtml(remoteReveal.mcpConfigSnippet) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"snippet\">copy snippet</button></div>' : '')\n + '</div>';\n }\n html += '</div></div>';\n } else {\n html += '<div class=\"muted-note\">remote_status: ' + escHtml(remoteR.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Pairing<button type=\"button\" class=\"btn-sm\" id=\"pair-offer-btn\">+ new pairing</button></div>';\n if (pairReveal) {\n html += '<div class=\"reveal-box\">'\n + '<div>Offer URL. Shown once, expires <span id=\"pair-offer-countdown\"></span>.</div>'\n + '<code>' + escHtml(pairReveal.url) + '</code>'\n + '<button type=\"button\" class=\"btn-sm\" data-copy=\"pairOffer\">copy URL</button>'\n + '<div class=\"muted\">fingerprint ' + escHtml(pairReveal.fingerprint) + '</div>'\n + '</div>';\n }\n if (pairR.ok) {\n var pairings = (pairR.data && pairR.data.pairings) || [];\n html += '<div class=\"card\" data-card-id=\"pairing\"><div class=\"body\">';\n if (pairings.length === 0) {\n html += '<div class=\"empty\">No paired clients.</div>';\n } else {\n html += '<table><thead><tr><th>Name</th><th>Fingerprint</th><th>Created</th><th>Last seen</th><th>Rendezvous</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < pairings.length; i++) {\n var p = pairings[i];\n html += '<tr data-row-id=\"' + escHtml(p.fingerprint) + '\"><td>' + escHtml(p.name) + '</td><td>' + escHtml(p.fingerprint) + '</td>'\n + '<td>' + escHtml(fmtRelative(p.createdAt)) + '</td><td>' + escHtml(fmtRelative(p.lastSeen)) + '</td><td>' + escHtml(p.rendezvous || \"\") + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-pair-revoke=\"' + escHtml(p.fingerprint) + '\">revoke</button></td></tr>';\n }\n html += '</tbody></table>';\n }\n html += '</div></div>';\n } else {\n html += '<div class=\"muted-note\">pair_list: ' + escHtml(pairR.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Tunnels</div>';\n if (tunnelR.ok) {\n var tunnels = (tunnelR.data && tunnelR.data.tunnels) || [];\n if (tunnels.length === 0) {\n html += '<div class=\"empty\">No tunnels tracked by this daemon.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Provider</th><th>Port</th><th>URL</th><th>Status</th></tr></thead><tbody>';\n for (var t = 0; t < tunnels.length; t++) {\n var tn = tunnels[t];\n html += '<tr><td>' + escHtml(tn.label || tn.name) + '</td><td>' + escHtml(tn.provider) + '</td><td>' + escHtml(String(tn.targetPort)) + '</td>'\n + '<td>' + escHtml(tn.publicUrl || \"\") + '</td><td>' + escHtml(tn.status) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n } else {\n html += '<div class=\"muted-note\">tunnel_list: ' + escHtml(tunnelR.message) + '</div>';\n }\n\n el.innerHTML = html;\n wireRemote(el);\n if (remoteReveal) {\n wireCopyButtons(el, { bearer: remoteReveal.bearerToken, phoneUrl: remoteReveal.phoneUrl, snippet: remoteReveal.mcpConfigSnippet });\n }\n if (pairReveal) {\n wireCopyButtons(el, { pairOffer: pairReveal.url });\n startPairCountdown(pairReveal.expiresAt);\n }\n}\n\nfunction wireRemote(container) {\n var enableBtn = container.querySelector(\"#remote-enable-btn\");\n if (enableBtn) {\n enableBtn.addEventListener(\"click\", function () {\n confirmClick(\"remote-enable\", enableBtn, function () {\n loadTool(\"remote_enable\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n remoteReveal = { bearerToken: r.data.bearerToken, phoneUrl: r.data.phoneUrl, mcpConfigSnippet: r.data.mcpConfigSnippet };\n toast(\"Remote enabled.\", false);\n loadRemote();\n });\n });\n });\n }\n var disableBtn = container.querySelector(\"#remote-disable-btn\");\n if (disableBtn) {\n disableBtn.addEventListener(\"click\", function () {\n confirmClick(\"remote-disable\", disableBtn, function () {\n loadTool(\"remote_disable\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n remoteReveal = null;\n toast(\"Remote disabled.\", false);\n loadRemote();\n });\n });\n });\n }\n var offerBtn = container.querySelector(\"#pair-offer-btn\");\n if (offerBtn) {\n offerBtn.addEventListener(\"click\", function () {\n loadTool(\"pair_offer\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n pairReveal = { url: r.data.url, fingerprint: r.data.fingerprint, expiresAt: r.data.expiresAt };\n toast(\"Pairing offer created.\", false);\n loadRemote();\n });\n });\n }\n var revokeBtns = container.querySelectorAll(\"[data-pair-revoke]\");\n for (var i = 0; i < revokeBtns.length; i++) {\n (function (btn) {\n var fp = btn.getAttribute(\"data-pair-revoke\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"pair-revoke-\" + fp, btn, function () {\n loadTool(\"pair_revoke\", { target: fp }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Pairing revoked.\", false);\n pairReveal = null;\n loadRemote();\n });\n });\n });\n })(revokeBtns[i]);\n }\n}\n\n// ============================================================\n// Advanced\n// ============================================================\n\nfunction loadAdvanced() {\n return Promise.all([\n loadTool(\"config_get\", {}),\n loadTool(\"daemon_health\", {}),\n ]).then(function (results) {\n renderAdvanced(results[0], results[1]);\n });\n}\n\nfunction renderAdvanced(configR, healthR) {\n var el = document.getElementById(\"sec-advanced\");\n var html = '<div class=\"sect-h\">Raw config dump</div>';\n if (configR.ok) {\n html += '<div class=\"card\"><div class=\"body\"><pre style=\"white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace\">' + escHtml(JSON.stringify(configR.data, null, 2)) + '</pre></div></div>';\n } else if (configR.unknown) {\n html += '<div class=\"notice\">Full config dump needs a newer agentproto daemon (config_get). See Defaults for the knobs daemon_health reports.</div>';\n } else {\n html += '<div class=\"muted-note\">config_get: ' + escHtml(configR.message) + '</div>';\n }\n html += '<div class=\"sect-h\">Daemon health</div>';\n if (healthR.ok) {\n html += '<div class=\"card\"><div class=\"body\"><pre style=\"white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace\">' + escHtml(JSON.stringify(healthR.data, null, 2)) + '</pre></div></div>';\n } else {\n html += '<div class=\"muted-note\">daemon_health: ' + escHtml(healthR.message) + '</div>';\n }\n el.innerHTML = html;\n}\n\n// ============================================================\n// boot\n// ============================================================\n\nfunction loadHealthChip() {\n return loadTool(\"daemon_health\", {}).then(function (r) {\n var chip = document.getElementById(\"daemon-chip\");\n if (r.ok) {\n chip.className = \"chip ok\";\n var v = r.data.version || (r.data.build && r.data.build.sha) || r.data.status || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n } else {\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n }\n });\n}\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadHealthChip();\n reloadCurrent().catch(function (e) { toast(String(e && e.message || e), true); });\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n updateModelContext = bridge.updateModelContext;\n if (bridge.onTeardown) {\n onTeardownFn = bridge.onTeardown;\n onTeardownFn(function () { clearRemoteSecrets(); });\n }\n renderNav();\n loadHealthChip();\n if (bridge.onToolInput) bridge.onToolInput(handleToolInput);\n route(true);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode: no host bridge (' + escHtml(err.message) + ')</div>';\n });\n</script>\n</body>\n</html>`\n","/**\n * `@agentproto/config` — daemon-wide configuration as its own standalone app.\n *\n * A separate app from `ops-panel` (not a section of it): ops-panel is the\n * daemon operations cockpit (sessions, crons, housekeeping); this app is the\n * visual twin of `agentproto config` — wallets (auth profiles + spend),\n * harnesses, the model catalog, defaults/messaging knobs, remote/pairing,\n * and a raw config escape hatch. `ops-panel` is referenced only for its app\n * mechanics (`defineApp`, self-contained panel HTML, `app_tool_call` gated\n * by a `ui.tools` allowlist) — see `ui.ts`.\n *\n * `agents: []`: UI only, no durable sessions to launch. Edit flows live\n * behind explicit confirm/save actions in `ui.ts`; every mutating tool is\n * feature-detected the same way the read tools already are, so this app\n * works against a daemon that predates a given write tool (it just shows a\n * muted \"needs a newer agentproto daemon\" note for that one control).\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { CONFIG_HTML, CONFIG_TOOLS } from \"./ui.js\"\n\nexport const configApp: AppHandle = defineApp({\n id: \"@agentproto/config\",\n name: \"agentproto config\",\n version: \"0.1.0\",\n description:\n \"Daemon-wide configuration: wallets (auth profiles + spend), harnesses, \" +\n \"the model catalog, defaults & messaging knobs, remote & pairing, and a raw config dump.\",\n agents: [],\n ui: {\n html: CONFIG_HTML,\n title: \"agentproto config\",\n tools: [...CONFIG_TOOLS],\n },\n})\n\nexport { CONFIG_TOOLS } from \"./ui.js\"\nexport { parseConfigFragment, buildConfigFragment, CONFIG_SECTIONS } from \"./fragment.js\"\nexport type { ConfigSection, ParsedConfigFragment } from \"./fragment.js\"\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\nexport const narrator: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-narrator\",\n description:\n \"Reads a daemon session's transcript and narrates it in plain English — what happened, what was decided, what's left.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Read-only — never spawn, kill, resume, or write to any session\",\n \"Narrate only what the transcript actually shows, never speculate about intent\",\n ],\n tools: [\"conversation_read\", \"session_list\"],\n workflows: [{ ref: \"narrate-session\" }],\n }),\n body: [\n \"You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.\",\n \"\",\n \"Given a session id or name, call `conversation_read` with `format: \\\"json\\\"` to get the\",\n \"structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the\",\n \"session id is ambiguous or unknown, call `session_list` first to resolve it by label.\",\n \"\",\n \"Write a short plain-English narration:\",\n \"- What the user asked for.\",\n \"- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).\",\n \"- Any open question, decision point, or unfinished step at the end of the transcript.\",\n \"\",\n \"Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real\",\n \"decision). If `conversation.conversation` comes back null, say plainly that this session has\",\n \"no readable conversation (e.g. a bare shell PTY) instead of guessing.\",\n ].join(\"\\n\"),\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\nexport const narrateSession = defineWorkflow({\n id: \"narrate-session\",\n name: \"Narrate Session\",\n description: \"Read a daemon session's transcript and narrate it in plain English.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"narrate\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/session-narrator\" },\n prompt:\n \"Narrate the session named in the run input (or the most recently started session if \" +\n \"none was given): what the user asked for, what the assistant did, and what — if \" +\n \"anything — is still open at the end of the transcript.\",\n },\n ],\n})\n","/**\n * Session Viewer — a self-contained MCP App panel for `session-viewer`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }` — same convention as\n * `media-viewer`/`mail-triage`. Read-only by design: `session_list` picks a\n * session (live or finished), `conversation_read` renders it as a clean\n * conversation — no spawn/kill/resume action lives here.\n *\n * Live sessions are polled (`conversation_read` has no push channel over\n * this bridge — see the PR's feasibility note on a real event/PTY stream);\n * a finished session is fetched once and left alone.\n */\n\nexport const SESSION_VIEWER_TOOLS = [\"session_list\", \"conversation_read\"] as const\n\nconst APP_ID = \"@agentproto/session-viewer\"\n\nexport const SESSION_VIEWER_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Viewer</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;height:100%;overflow:hidden}\n\n/* sidebar */\n#sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}\n#sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}\n#archived-toggle{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--text2);padding:6px 12px;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}\n#archived-toggle input{cursor:pointer}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:14px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n#empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}\n\n/* main */\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n#header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}\n.pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex:none;box-shadow:0 0 0 0 rgba(63,185,80,.4);animation:pulse 2s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 6px rgba(63,185,80,0)}100%{box-shadow:0 0 0 0 rgba(63,185,80,0)}}\n#header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}\n.fact{font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n#empty-main,#loading-main,#error-main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;text-align:center;padding:24px}\n#empty-main .ico,#loading-main .ico{font-size:30px}\n#error-main{color:var(--red)}\n\n/* transcript */\n#transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}\n.msg{display:flex;flex-direction:column;max-width:78%}\n.msg.role-user{align-self:flex-end;align-items:flex-end}\n.msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}\n.msg.role-system{align-self:center;max-width:90%}\n.msg.role-message{align-self:flex-start;align-items:flex-start}\n.bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}\n.role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}\n.role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}\n.role-message .bubble{background:var(--bg2);border:1px dashed var(--blue);border-bottom-left-radius:3px}\n.role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}\n.mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}\n.mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}\n.mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}\n\n.chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}\n.chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}\n.chip .ch .ci{color:var(--yellow);flex:none}\n.chip.tool-result .ch .ci{color:var(--purple)}\n.chip .ch .cname{font-weight:600;color:var(--text)}\n.chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}\n.chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}\n.chip.open .ch .cchev{transform:rotate(90deg)}\n.chip pre{display:none;margin:0;border-top:1px solid var(--border);padding:9px 11px;font-family:Menlo,Monaco,'Courier New',monospace;font-size:11px;line-height:1.5;color:var(--text2);white-space:pre-wrap;word-break:break-word;max-height:320px;overflow:auto}\n.chip.open pre{display:block}\n.reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}\n\n#statusbar{padding:5px 18px;font-size:10.5px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0;display:flex;justify-content:space-between}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻</button>\n </div>\n <label id=\"archived-toggle\"><input type=\"checkbox\" id=\"archived-check\"> show archived</label>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"header\" style=\"display:none\"></div>\n <div id=\"empty-main\"><div class=\"ico\">💬</div><div>Pick a session to read its conversation</div></div>\n <div id=\"transcript\" style=\"display:none\"></div>\n <div id=\"statusbar\" style=\"display:none\">\n <span id=\"status-left\"></span>\n <span id=\"status-right\"></span>\n </div>\n </div>\n</div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — works whichever layer the host's bridge already\n// unwrapped. With conversation_read's format:\"json\" this resolves straight\n// to the { meta, messages } object; with session_list, straight to\n// { sessions: [...] }.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction fmtTime(ts) {\n if (!ts) return \"\";\n try { return new Date(ts).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }); }\n catch (e) { return \"\"; }\n}\n\nfunction fmtClock(iso) {\n if (!iso) return \"\";\n try { return new Date(iso).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" }); }\n catch (e) { return \"\"; }\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nfunction fmtDuration(startIso, endIso) {\n var end = endIso ? new Date(endIso).getTime() : Date.now();\n var start = new Date(startIso).getTime();\n if (!isFinite(start) || !isFinite(end)) return \"\";\n var s = Math.max(0, Math.round((end - start) / 1000));\n if (s < 60) return s + \"s\";\n var m = Math.floor(s / 60);\n if (m < 60) return m + \"m \" + (s % 60) + \"s\";\n var h = Math.floor(m / 60);\n return h + \"h \" + (m % 60) + \"m\";\n}\n\nfunction prettyArgs(raw) {\n if (raw == null) return \"\";\n if (typeof raw !== \"string\") return JSON.stringify(raw, null, 2);\n try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }\n}\n\nfunction firstLine(s, max) {\n var t = String(s == null ? \"\" : s).split(\"\\\\n\")[0];\n return t.length > max ? t.slice(0, max) + \"\\\\u2026\" : t;\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar includeArchived = false;\nvar currentMeta = null;\nvar lastMessageCount = -1;\nvar sidebarTimer = null;\nvar convoTimer = null;\n\nfunction findSession(id) {\n for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];\n return null;\n}\n\nfunction isLive(s) {\n return !!s && (s.status === \"running\" || s.status === \"starting\");\n}\n\n// ============================================================\n// sidebar\n// ============================================================\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-run\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction renderSidebar() {\n var el = document.getElementById(\"session-list\");\n if (sessions.length === 0) {\n el.innerHTML = '<div id=\"empty-sidebar\">No sessions yet.</div>';\n return;\n }\n var html = \"\";\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id.slice(0, 10);\n var db = displayBadge(s);\n var active = s.id === activeId ? \" active\" : \"\";\n html += '<div class=\"si' + active + '\" data-id=\"' + escHtml(s.id) + '\">'\n + '<div class=\"sn\">' + escHtml(label) + \"</div>\"\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span>\"\n + \"<span>\" + escHtml(s.kind || \"\") + \"</span>\"\n + \"<span>\" + escHtml(fmtRelative(s.startedAt)) + \"</span>\"\n + \"</div></div>\";\n }\n el.innerHTML = html;\n var rows = el.querySelectorAll(\".si\");\n for (var j = 0; j < rows.length; j++) {\n rows[j].addEventListener(\"click\", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute(\"data-id\")));\n }\n}\n\nfunction loadSessions() {\n return callApp(\"session_list\", { kind: \"all\", includeArchived: includeArchived }).then(function (data) {\n sessions = (data && data.sessions) || [];\n renderSidebar();\n if (activeId) {\n var hdr = document.getElementById(\"header\");\n if (hdr.style.display !== \"none\") renderHeader();\n }\n }).catch(function (e) {\n document.getElementById(\"status-left\").textContent = \"sessions: \" + e.message;\n });\n}\n\n// ============================================================\n// header\n// ============================================================\n\nfunction renderHeader() {\n var s = findSession(activeId);\n var meta = currentMeta || {};\n var el = document.getElementById(\"header\");\n el.style.display = \"block\";\n\n var title = meta.title || (s && (s.label || s.title)) || activeId;\n var live = s && isLive(s);\n var pulse = live ? '<span class=\"pulse\"></span>' : \"\";\n\n var facts = [];\n if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));\n if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));\n if (s && s.kind) facts.push(escHtml(s.kind));\n if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }\n if (s && s.cwd) facts.push('<span title=\"' + escHtml(s.cwd) + '\">' + escHtml(s.cwd.length > 46 ? \"\\\\u2026\" + s.cwd.slice(-44) : s.cwd) + \"</span>\");\n var started = (meta.startedAt || (s && s.startedAt));\n if (started) facts.push(\"started \" + escHtml(fmtClock(started)) + \" (\" + escHtml(fmtRelative(started)) + \")\");\n var ended = meta.endedAt || (s && s.endedAt);\n if (ended) facts.push(\"ran \" + escHtml(fmtDuration(started, ended)));\n else if (started) facts.push(\"running \" + escHtml(fmtDuration(started)));\n if (meta.messageCount != null) facts.push(meta.messageCount + \" messages\");\n if (meta.toolCallCount != null) facts.push(meta.toolCallCount + \" tool calls\");\n if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {\n facts.push(\"tokens \" + (meta.tokens.input || 0) + \"\\\\u2192\" + (meta.tokens.output || 0));\n }\n if (meta.costUsd != null) facts.push(\"$\" + Number(meta.costUsd).toFixed(4));\n\n el.innerHTML = '<div class=\"htitle\">' + pulse + escHtml(title) + \"</div>\"\n + '<div class=\"hfacts\">' + facts.map(function (f) { return '<span class=\"fact\">' + f + \"</span>\"; }).join(\"\") + \"</div>\";\n}\n\n// ============================================================\n// transcript\n// ============================================================\n\nfunction renderToolCallChip(tc) {\n var args = prettyArgs(tc.args);\n return '<div class=\"chip tool-call\"><div class=\"ch\"><span class=\"ci\">▸</span>'\n + '<span class=\"cname\">' + escHtml(tc.name || \"tool\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(args, 64)) + \"</span>\"\n + '<span class=\"cchev\">▸</span></div><pre>' + escHtml(args) + \"</pre></div>\";\n}\n\nfunction renderToolResultChip(m) {\n var text = m.text || \"\";\n return '<div class=\"chip tool-result\"><div class=\"ch\"><span class=\"ci\">🔧</span>'\n + '<span class=\"cname\">' + escHtml(m.toolName || \"result\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(text, 64)) + \"</span>\"\n + '<span class=\"cchev\">▸</span></div><pre>' + escHtml(text) + \"</pre></div>\";\n}\n\nfunction renderMessage(m) {\n var time = m.ts ? '<span class=\"mtime\">' + fmtTime(m.ts) + \"</span>\" : \"\";\n if (m.role === \"system\") {\n return '<div class=\"msg role-system\"><div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n if (m.role === \"tool\") {\n return '<div class=\"msg role-tool\">' + renderToolResultChip(m) + \"</div>\";\n }\n if (m.role === \"user\" && m.from) {\n // A typed message from another session (session-message record) — never\n // the human: name the daemon-attested sender instead of \"You\".\n var who = m.from.label || (m.from.sessionId ? m.from.sessionId.slice(-6) : m.from.relation);\n return '<div class=\"msg role-message\"><div class=\"mrow\"><span class=\"mrole\">from ' + escHtml(m.from.relation)\n + \" \" + escHtml(who) + \" · \" + escHtml(m.from.kind) + \"</span>\" + time + \"</div>\"\n + '<div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n if (m.role === \"user\") {\n return '<div class=\"msg role-user\"><div class=\"mrow\"><span class=\"mrole\">You</span>' + time + \"</div>\"\n + '<div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n // assistant\n var inner = \"\";\n if (m.reasoning) inner += '<div class=\"reasoning\">' + escHtml(m.reasoning) + \"</div>\";\n if (m.text) inner += escHtml(m.text);\n var body = inner ? '<div class=\"bubble\">' + inner + \"</div>\" : \"\";\n var chips = (m.toolCalls || []).map(renderToolCallChip).join(\"\");\n return '<div class=\"msg role-assistant\"><div class=\"mrow\"><span class=\"mrole\">Assistant</span>' + time + \"</div>\"\n + body + chips + \"</div>\";\n}\n\nfunction renderTranscript(messages) {\n var el = document.getElementById(\"transcript\");\n var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;\n el.style.display = \"flex\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n if (errEl) errEl.style.display = \"none\";\n if (messages.length === 0) {\n el.innerHTML = '<div style=\"color:var(--text2);text-align:center;padding:20px\">(empty conversation)</div>';\n return;\n }\n el.innerHTML = messages.map(renderMessage).join(\"\");\n if (atBottom) el.scrollTop = el.scrollHeight;\n\n var chips = el.querySelectorAll(\".chip\");\n for (var i = 0; i < chips.length; i++) {\n chips[i].querySelector(\".ch\").addEventListener(\"click\", (function (chip) {\n return function () { chip.classList.toggle(\"open\"); };\n })(chips[i]));\n }\n}\n\nfunction renderNoConversation(reason) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var el = document.getElementById(\"error-main\");\n el.style.display = \"flex\";\n el.innerHTML = '<div class=\"ico\">🚫</div><div>No readable conversation for this session'\n + (reason ? '<br><span style=\"font-size:11px;color:var(--text3)\">' + escHtml(reason) + \"</span>\" : \"\")\n + \"</div>\";\n}\n\n// ============================================================\n// selection + polling\n// ============================================================\n\nfunction clearConvoPoll() {\n if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }\n}\n\nfunction scheduleConvoPoll() {\n clearConvoPoll();\n var s = findSession(activeId);\n if (!isLive(s)) return;\n convoTimer = setTimeout(function () {\n loadConversation(activeId, { silent: true });\n }, 3500);\n}\n\nfunction loadConversation(id, opts) {\n opts = opts || {};\n var requestedId = id;\n if (!opts.silent) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n errEl.style.display = \"flex\";\n errEl.innerHTML = '<div class=\"ico\">⌛</div><div>Loading\\\\u2026</div>';\n }\n document.getElementById(\"status-left\").textContent = \"loading\\\\u2026\";\n return callApp(\"conversation_read\", { idOrName: id, format: \"json\" }).then(function (data) {\n if (activeId !== requestedId) return; // switched away meanwhile\n if (!data || !Array.isArray(data.messages)) {\n currentMeta = null;\n renderHeader();\n renderNoConversation(data && data.reason);\n clearConvoPoll();\n document.getElementById(\"status-left\").textContent = \"no conversation\";\n return;\n }\n currentMeta = data.meta || {};\n lastMessageCount = data.messages.length;\n renderHeader();\n renderTranscript(data.messages);\n document.getElementById(\"status-left\").textContent = data.messages.length + \" messages\";\n document.getElementById(\"status-right\").textContent = \"updated \" + fmtTime(Date.now());\n scheduleConvoPoll();\n }).catch(function (e) {\n if (activeId !== requestedId) return;\n document.getElementById(\"status-left\").textContent = \"error: \" + e.message;\n if (!opts.silent) renderNoConversation(e.message);\n });\n}\n\nfunction selectSession(id) {\n if (activeId === id) return;\n activeId = id;\n currentMeta = null;\n lastMessageCount = -1;\n clearConvoPoll();\n renderSidebar();\n document.getElementById(\"statusbar\").style.display = \"flex\";\n renderHeader();\n loadConversation(id, {});\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadSessions();\n if (activeId) loadConversation(activeId, {});\n});\ndocument.getElementById(\"archived-check\").addEventListener(\"change\", function (e) {\n includeArchived = !!e.target.checked;\n loadSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n return loadSessions();\n })\n .then(function () {\n sidebarTimer = setInterval(loadSessions, 5000);\n if (!activeId && sessions.length > 0) selectSession(sessions[0].id);\n })\n .catch(function (err) {\n document.getElementById(\"empty-main\").innerHTML =\n '<div class=\"ico\">⚠</div><div>Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `session-viewer` — a single-agent agentproto app for reading daemon\n * sessions as a clean conversation.\n *\n * The panel (`ui.ts`) picks a session — live or finished — from\n * `session_list` and renders its transcript via `conversation_read`: user/\n * assistant turns, collapsible tool calls, timestamps, a metadata header.\n * Read-only, no spawn/kill/resume action. The bundled `session-narrator`\n * agent is a complementary plain-English narrator over the same\n * `conversation_read` data — usable standalone via `app_run`, independent\n * of the panel.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { narrator } from \"./agents/narrator.js\"\nimport { narrateSession } from \"./workflows/narrate-session.js\"\nimport { SESSION_VIEWER_HTML, SESSION_VIEWER_TOOLS } from \"./ui.js\"\n\nexport const sessionViewer: AppHandle = defineApp({\n id: \"@agentproto/session-viewer\",\n name: \"Session Viewer\",\n version: \"0.1.0\",\n description: \"Read a daemon session as a clean conversation — turns, tool calls, timestamps.\",\n agents: [narrator],\n workflows: [narrateSession],\n ui: {\n html: SESSION_VIEWER_HTML,\n title: \"Session Viewer\",\n tools: [...SESSION_VIEWER_TOOLS],\n },\n})\n\nexport { narrator, narrateSession }\n","/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also mounts two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes.\n * No auto-request: panels stay inline until the user clicks. The buttons\n * themselves are NOT implemented here — `@agentproto/app-client/display-mode`\n * is the one implementation, shared with the `window.McpApp` bridge injected\n * into installed apps (`packages/runtime/src/app-ui-apps.ts`); this module\n * only hands it `getHostContext`/`onHostContext`/`requestDisplayMode`. It\n * also leaves `window.AgentprotoUI.installDisplayMode` defined in the panel,\n * which is what a panel with its own header calls to place the toggle\n * inline instead (see that module's `mountToggle`).\n *\n * Standalone HTTP mode: `GET /apps/:appId/ui` (packages/runtime\n * http-server.ts) serves this exact script with NO postMessage host on the\n * other end of `window.parent` — it's a plain browser tab (or a VS Code\n * *HTTP-iframe* panel), so `window.parent === window` and nothing ever\n * answers `initBridge()`'s `ui/initialize` request, leaving every panel\n * stuck on \"Connecting to bridge…\" forever. `injectStandaloneAppBridge`\n * (app-ui-apps.ts) papers over exactly this for installed apps by injecting\n * `window.McpApp.connect() -> Promise<{callTool, ...}>`, a REST-backed\n * stand-in whose `callTool` POSTs to the sibling `./tool-call` route — but\n * this bridge never consumed `window.McpApp`, so builtin panels (the only\n * consumers of this module) were left hanging regardless.\n *\n * Fixed here by detecting that same standalone shape and short-circuiting\n * both `initBridge()` (resolves locally, no round trip) and `callTool()`\n * (routes through `window.McpApp.connect()` instead of `tools/call` over\n * postMessage). The detection requires BOTH `window.parent === window` (no\n * host to talk to — this is the actual condition standalone mode means) AND\n * `window.McpApp` being present with a `connect` function (the capability\n * this bridge needs from it): `window.McpApp` presence alone was considered\n * and rejected as the sole signal, even though it happens to also hold today\n * — the VS Code webview's `srcdoc` relay (packages/vscode\n * appPanel.ts/appPanelController.ts) answers this bridge's raw postMessage\n * JSON-RPC directly and never defines `window.McpApp` for a builtin panel\n * (only `createUiHtmlCache`'s `injectMcpAppBridge`, used for INSTALLED\n * apps' `ui.path` html, defines it) — but that's an accident of today's\n * wiring, not a guarantee; `window.parent === window` is the direct,\n * load-bearing signal (\"is there anyone to postMessage?\") and doesn't\n * depend on which injector happened to run. There is no host in standalone\n * mode, so `initBridge()` seeds a default `hostContext` of `{displayMode:\n * 'inline', availableDisplayModes: []}` — an empty `availableDisplayModes`\n * is what keeps the display-mode toggle buttons hidden (they only show for\n * modes `hostContext.availableDisplayModes` lists), rather than rendering\n * two buttons with nothing to switch to.\n *\n * The real postMessage-host path (a compliant MCP-Apps host, or the VS Code\n * srcdoc relay) is untouched byte-for-byte: same `rpcRequest`/`rpcNotify`\n * over `window.parent.postMessage`, same handshake, same display-mode\n * plumbing.\n *\n * Asymmetry not fixed here (flagged, not papered over): `window.McpApp`'s\n * `sendMessage`/`updateModelContext` (STANDALONE_REST_BRIDGE_SCRIPT,\n * app-ui-apps.ts) reject with \"no host (standalone mode)\", while\n * `appPanelController.ts`'s `ui/message`/`ui/update-model-context` handlers\n * accept-and-drop (`return {}`). This bridge doesn't currently expose either\n * call, so the difference is latent, but it's the last behavioural delta\n * between the two panel paths and worth a follow-up if this bridge ever\n * grows those methods.\n */\n\nimport { DISPLAY_MODE_SCRIPT_BODY } from \"@agentproto/app-client/display-mode\"\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel — the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext — default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n${DISPLAY_MODE_SCRIPT_BODY}\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});`\n}\n","/**\n * HTML bundle for the agentproto sessions panel — the `PANEL_HTML` served\n * as the `ui://agentproto_sessions/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_sessions` tool defined in ./index.ts.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → session_list + agent_output (3 s poll)\n * – Kill action: tools/call → agent_kill | terminal_kill\n * (goes through Claude's normal tool approval flow)\n *\n * The HTML bundle is fully autonomous (zero CDN, zero external deps).\n * ANSI-to-HTML renderer handles SGR codes (colors 0-15, bold, dim,\n * italic, underline, reset). CR-overwrite sequences are stripped so\n * progress bars don't corrupt the output.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle ─────────────────────────────────────────────────────────────\n\nexport const PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto sessions</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}\n#sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:15px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}\n.badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}\n.br{background:rgba(63,185,80,.15);color:var(--green)}\n.bs{background:rgba(88,166,255,.15);color:var(--blue)}\n.be{background:rgba(139,148,158,.1);color:var(--text2)}\n.bk{background:rgba(248,81,73,.1);color:var(--red)}\n.berr{background:rgba(248,81,73,.2);color:var(--red)}\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}\n#session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn:disabled{opacity:.4;cursor:default}\n.abtn.danger{border-color:var(--red);color:var(--red)}\n.abtn.danger:hover{background:rgba(248,81,73,.1)}\n#output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}\n.line{white-space:pre-wrap;word-break:break-all}\n#empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}\n#empty .ico{font-size:32px}\n#statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.errmsg{color:var(--red);padding:12px}\n/* ANSI SGR */\n.ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}\n.f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}\n.f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}\n.f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}\n.f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"toolbar\" style=\"display:none\">\n <span id=\"session-title\"></span>\n <button class=\"abtn danger\" id=\"kill-btn\" onclick=\"doKill()\">Kill</button>\n </div>\n <div id=\"output\">\n <div id=\"empty\"><div class=\"ico\">⚡</div><div>Select a session</div></div>\n </div>\n <div id=\"statusbar\">Connecting to bridge…</div>\n </div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-sessions-panel\")}\n\n// ============================================================\n// ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)\n// ============================================================\n\nfunction escHtml(s) {\n return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n}\n\nfunction ansiToHtml(raw) {\n // Strip CR overwrite sequences (progress bars: CR without LF)\n var text = raw.replace(/[^\\\\n]*\\\\r([^\\\\n])/g, '$1').replace(/\\\\r/g, '');\n var bold = false, dim = false, italic = false, underline = false, fg = -1;\n var out = '';\n // Split on ESC [ ... m (SGR) sequences; keep delimiters\n var ESC = '\u001b';\n var parts = text.split(/([\u001b]\\\\[[0-9;]*m)/);\n for (var pi = 0; pi < parts.length; pi++) {\n var part = parts[pi];\n if (part.length === 0) continue;\n if (part.charCodeAt(0) === 0x1b && part[1] === '[') {\n // Parse SGR parameters\n var inner = part.slice(2, part.length - 1); // strip ESC[ and m\n var codes = inner === '' ? [0] : inner.split(';').map(Number);\n for (var ci = 0; ci < codes.length; ci++) {\n var c = codes[ci];\n if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }\n else if (c === 1) bold = true;\n else if (c === 2) dim = true;\n else if (c === 3) italic = true;\n else if (c === 4) underline = true;\n else if (c === 22) { bold = false; dim = false; }\n else if (c === 23) italic = false;\n else if (c === 24) underline = false;\n else if (c >= 30 && c <= 37) fg = c - 30;\n else if (c === 39) fg = -1;\n else if (c >= 90 && c <= 97) fg = c - 90 + 8;\n }\n } else {\n // Text segment — strip any remaining non-printable ESC sequences\n var safe = escHtml(part).replace(/\u001b\\\\[[^m]*[A-Za-z]/g, '');\n if (safe === '') continue;\n var cls = '';\n if (bold) cls += ' ab';\n if (dim) cls += ' ad';\n if (italic) cls += ' ai';\n if (underline) cls += ' au';\n if (fg >= 0 && fg <= 15) cls += ' f' + fg;\n cls = cls.trim();\n out += cls ? '<span class=\"' + cls + '\">' + safe + '</span>' : safe;\n }\n }\n return out;\n}\n\n// ============================================================\n// App state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar outputLines = [];\nvar nextCursor = 0;\nvar pollTimer = null;\nvar pollActive = false;\n\nfunction setStatus(msg) {\n document.getElementById('statusbar').textContent = msg;\n}\n\n// ============================================================\n// Session list\n// ============================================================\n\nfunction badgeClass(status) {\n if (status === 'running') return 'br';\n if (status === 'starting') return 'bs';\n if (status === 'killed') return 'bk';\n if (status === 'error') return 'berr';\n return 'be'; // exited\n}\n\n// Derive a turn-aware display badge. A session's 'status' only tracks process\n// liveness, so an agent-cli process stays \"running\" while idle between turns.\n// Read 'busy' / 'awaitingInput' to show real activity instead:\n// working — a turn is in flight (busy)\n// waiting — turn ended on awaiting-input (needs a reply)\n// idle — process alive, no turn running (last turn done)\nfunction displayBadge(s) {\n if (s.status === 'running' && s.kind === 'agent-cli') {\n if (s.busy) return { label: 'working', cls: 'br' };\n if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };\n return { label: 'idle', cls: 'be' };\n }\n return { label: s.status, cls: badgeClass(s.status) };\n}\n\nfunction renderSidebar() {\n var el = document.getElementById('session-list');\n if (sessions.length === 0) {\n el.innerHTML = '<div style=\"padding:12px;color:var(--text2);font-size:11px\">No sessions</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);\n var db = displayBadge(s);\n var active = s.id === activeId ? ' active' : '';\n // blockedOn (set while the turn waits on a spawned sub-agent or a\n // shell command) rides next to the status badge; waiting-on-user is\n // NOT here — that's awaitingInput, a different signal.\n var blocked = '';\n if (s.blockedOn === 'subagent') blocked = '<span class=\"badge bs\">🧩 sous-agent</span>';\n else if (s.blockedOn === 'command') blocked = '<span class=\"badge bs\">⏳ commande</span>';\n else if (s.blockedOn === 'inbox') blocked = '<span class=\"badge bs\">📬 attend messages</span>';\n html += '<div class=\"si' + active + '\" onclick=\"selectSession(\\\\'' + s.id + '\\\\')\">'\n + '<div class=\"sn\">' + escHtml(label) + '</div>'\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + db.label + '</span>'\n + blocked\n + '<span>' + escHtml(s.kind || '') + '</span>'\n + '</div></div>';\n }\n el.innerHTML = html;\n}\n\nfunction loadSessions() {\n // {kind:'all'} is session_list's live-able default — agent-CLI + terminal/\n // PTY only. kind:\"command\" rows (a shell-execution log, not a resumable\n // session) are excluded unless includeCommands is explicitly passed.\n return callTool('session_list', {kind: 'all'}).then(function(data) {\n sessions = data.sessions || [];\n renderSidebar();\n setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' · ' + new Date().toLocaleTimeString());\n }).catch(function(e) {\n setStatus('Error: ' + e.message);\n });\n}\n\n// ============================================================\n// Output panel\n// ============================================================\n\nfunction renderOutputFull() {\n var el = document.getElementById('output');\n if (outputLines.length === 0) {\n el.innerHTML = '<div id=\"empty\"><div class=\"ico\">⚡</div><div>No output yet</div></div>';\n return;\n }\n var html = '';\n for (var i = 0; i < outputLines.length; i++) {\n html += '<div class=\"line\">' + ansiToHtml(outputLines[i]) + '</div>';\n }\n el.innerHTML = html;\n el.scrollTop = el.scrollHeight;\n}\n\nfunction appendLines(lines) {\n var el = document.getElementById('output');\n // Remove empty-state placeholder if present\n var empty = el.querySelector('#empty');\n if (empty) empty.parentNode.removeChild(empty);\n var frag = document.createDocumentFragment();\n for (var i = 0; i < lines.length; i++) {\n var div = document.createElement('div');\n div.className = 'line';\n div.innerHTML = ansiToHtml(lines[i]);\n frag.appendChild(div);\n }\n el.appendChild(frag);\n el.scrollTop = el.scrollHeight;\n}\n\nfunction selectSession(id) {\n activeId = id;\n outputLines = [];\n nextCursor = 0;\n renderSidebar();\n\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === id) { s = sessions[i]; break; }\n }\n\n var toolbar = document.getElementById('toolbar');\n toolbar.style.display = 'flex';\n document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);\n\n var killBtn = document.getElementById('kill-btn');\n killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');\n\n document.getElementById('output').innerHTML = '<div style=\"padding:12px;color:var(--text2)\">Loading…</div>';\n\n // Initial load\n callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n }).catch(function(e) {\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Error: ' + escHtml(e.message) + '</div>';\n });\n}\n\n// ============================================================\n// Kill\n// ============================================================\n\nfunction doKill() {\n if (!activeId) return;\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === activeId) { s = sessions[i]; break; }\n }\n var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';\n callTool(toolName, {sessionId: activeId}).then(function() {\n return doRefresh();\n }).catch(function(e) {\n setStatus('Kill failed: ' + e.message);\n });\n}\n\n// ============================================================\n// Poll\n// ============================================================\n\nfunction doPoll() {\n if (pollActive) return;\n pollActive = true;\n var p = loadSessions();\n if (activeId) {\n var capturedId = activeId;\n var capturedCursor = nextCursor;\n p = p.then(function() {\n return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});\n }).then(function(data) {\n if (capturedId !== activeId) return; // user switched sessions\n var newLines = data.lines || [];\n if (newLines.length > 0) {\n outputLines = outputLines.concat(newLines);\n if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);\n nextCursor = data.nextCursor || nextCursor;\n appendLines(newLines);\n }\n }).catch(function() {});\n }\n p.then(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n }).catch(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n });\n}\n\nfunction doRefresh() {\n return loadSessions().then(function() {\n if (activeId) {\n return callTool('agent_output', {sessionId: activeId, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n });\n }\n });\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n return loadSessions();\n}).then(function() {\n pollTimer = setTimeout(doPoll, 3000);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto sessions panel.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). The host (@agentproto/runtime)\n * wires this factory's output via `registerMcpApps` from its\n * mcp-apps-adapter.ts at boot time — see runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_sessions` is called by the host.\n * 2. execute() returns a sessions snapshot → injected into the HTML as\n * window.__APP_INIT__ (static initial render, no bridge needed).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list` + `agent_output` every 3 s for live data.\n *\n * This module also exports `sessionsPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeSessionsPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { PANEL_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const sessionsPanelInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\n \"Which sessions to return. `running` = only alive; `all` = running + recent (default).\",\n ),\n})\n\nexport type SessionsInput = z.infer<typeof sessionsPanelInputSchema>\nexport type SessionsOutput<TSession = unknown> = { sessions: TSession[] }\n\n/**\n * Generic over the host's own session-descriptor shape — this package\n * doesn't own that type (it lives in @agentproto/runtime's session\n * registry), so the factory stays agnostic to it and the host supplies the\n * concrete type as a type argument when it wires the app up.\n */\nexport interface SessionsPanelOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the sessions registry so execute() doesn't need\n * an AppContext (agentproto has no userId/guildId concept).\n */\nexport function makeSessionsPanelApp<TSession = unknown>(\n ops: SessionsPanelOps<TSession>,\n): AgnoMcpApp<SessionsInput, SessionsOutput<TSession>> {\n return {\n id: \"agentproto_sessions\",\n title: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all \" +\n \"running and recent agent-CLI and terminal/PTY sessions. Raw shell-\" +\n \"command runs are a log, not a resumable session, and don't appear \" +\n \"here — see `command_list`. The panel polls live data and lets you \" +\n \"inspect output or kill sessions.\",\n inputSchema: sessionsPanelInputSchema,\n execute: async (input) => ({\n sessions: ops.listSessions(input.filter),\n }),\n html: PANEL_HTML,\n }\n}\n\nexport const sessionsPanelApp: AppHandle = defineApp({\n id: \"@agentproto/sessions-panel\",\n name: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all running and recent \" +\n \"agent-CLI and terminal/PTY sessions.\",\n agents: [],\n ui: {\n html: PANEL_HTML,\n title: \"Agent Sessions\",\n tools: [\"session_list\", \"agent_output\"],\n },\n})\n","/**\n * HTML bundle for the agentproto \"agents — vue claire\" panel — the\n * `AGENTS_OVERVIEW_HTML` served as the `ui://agentproto_agents_overview/view`\n * resource. Mounted via the `agentproto_agents_overview` tool defined in\n * ./index.ts.\n *\n * The panel polls `session_list` every ~12s, then calls the host's\n * `summarize_session` tool (registered server-side by\n * @agentproto/runtime's summarize-session-tool.ts, NOT part of this\n * package — that tool is tied to the runtime's session ring-buffer/hot\n * path) for each visible session to fetch the plain sentence + state.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle (zero CDN, CSP-safe inline) ──────────────────────────────────\n\nexport const AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agents — vue claire</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}\n#hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}\n#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}\n.card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}\n.card.s-todo{border-left-color:var(--purple)}\n.card.s-work{border-left-color:var(--green)}\n.card.s-wait{border-left-color:var(--yellow)}\n.card.s-done{border-left-color:var(--text2)}\n.card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}\n.state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}\n.state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}\n.state.s-work{background:rgba(63,185,80,.16);color:var(--green)}\n.state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}\n.state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}\n.summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}\n.summary.muted{color:var(--text2);font-style:italic}\n.meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}\n.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}\n#empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}\n#empty .ico{font-size:30px}\n#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"hdr\">\n <div>\n <h1>Agents — vue claire</h1>\n <div class=\"sub\">une carte par agent · résumé + état</div>\n </div>\n <button id=\"refresh-btn\" title=\"Rafraîchir\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"grid\"><div id=\"empty\"><div class=\"ico\">⚡</div><div>Connexion au bridge…</div></div></div>\n <div id=\"statusbar\">Connexion…</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-agents-overview\")}\n\n// ── State ──\nvar REFRESH_MS = 12000;\nvar pollTimer = null, polling = false;\nfunction esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }\nfunction setStatus(m){ document.getElementById('statusbar').textContent = m; }\n\nvar STATE_CLASS = {\n 'à traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'terminé':'s-done'\n};\nfunction stateClass(st){ return STATE_CLASS[st] || 's-wait'; }\n\nfunction fmtAgo(iso){\n if (!iso) return '';\n var t = Date.parse(iso);\n if (isNaN(t)) return '';\n var s = Math.max(0, Math.round((Date.now() - t) / 1000));\n if (s < 60) return 'il y a ' + s + 's';\n var m = Math.round(s/60);\n if (m < 60) return 'il y a ' + m + 'min';\n var h = Math.round(m/60);\n return 'il y a ' + h + 'h';\n}\n\nfunction titleOf(s){\n return s.label || s.name || (s.command ? s.command.split(/\\\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);\n}\n\nfunction render(sessions, summaries){\n var grid = document.getElementById('grid');\n if (!sessions.length){\n grid.innerHTML = '<div id=\"empty\"><div class=\"ico\">😴</div><div>Aucune session d\\\\'agent</div></div>';\n return;\n }\n var html = '';\n for (var i=0;i<sessions.length;i++){\n var s = sessions[i];\n var sum = summaries[s.id] || {};\n var st = sum.state || 'en attente';\n var cls = stateClass(st);\n var summary = sum.summary || 'Résumé indisponible.';\n var muted = !sum.summary ? ' muted' : '';\n html += '<div class=\"card ' + cls + '\">'\n + '<div class=\"card-top\">'\n + '<span class=\"title\">' + esc(titleOf(s)) + '</span>'\n + '<span class=\"state ' + cls + '\">' + esc(st) + '</span>'\n + '</div>'\n + '<div class=\"summary' + muted + '\">' + esc(summary) + '</div>'\n + '<div class=\"meta\">'\n + '<span>' + esc(s.kind || '') + '</span><span class=\"dot\"></span>'\n + '<span>' + esc(s.status || '') + '</span>'\n + (s.lastOutputAt ? '<span class=\"dot\"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')\n + '</div>'\n + '</div>';\n }\n grid.innerHTML = html;\n}\n\nfunction loadAndRender(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n var all = data.sessions || [];\n var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });\n // Render shells immediately, then fill summaries as they arrive.\n var summaries = {};\n render(agents, summaries);\n setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' · ' + new Date().toLocaleTimeString('fr-FR'));\n return Promise.all(agents.map(function(s){\n return callTool('summarize_session', {sessionId: s.id}).then(function(sum){\n summaries[s.id] = sum;\n }).catch(function(){ /* leave shell */ });\n })).then(function(){ render(agents, summaries); });\n }).catch(function(e){ setStatus('Erreur : ' + e.message); });\n}\n\nfunction doPoll(){\n if (polling) return;\n polling = true;\n loadAndRender().then(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n }).catch(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n });\n}\nfunction doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }\n\ninitBridge().then(loadAndRender).then(function(){\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n}).catch(function(e){\n setStatus('Bridge : ' + e.message);\n document.getElementById('grid').innerHTML = '<div id=\"empty\"><div class=\"ico\">⚠</div><div>Échec connexion bridge : ' + esc(e.message) + '</div></div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"agents — vue claire\" panel.\n *\n * A plain-language overview of agent-cli sessions: one card per session\n * with a single human sentence (what the agent is doing/last said) plus a\n * coarse state — `à traiter` / `au travail` / `en attente` / `terminé`.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_agents_overview` is called by the host.\n * 2. execute() returns the agent-session snapshot → injected into the\n * HTML as the initial render via the tool result JSON.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list` every ~12 s, then calls `summarize_session` for\n * each visible session to fetch the plain sentence + state.\n *\n * The per-session summary is generated SERVER-SIDE by the\n * `summarize_session` tool, which stays in @agentproto/runtime\n * (summarize-session-tool.ts) — it's tied to the runtime's session\n * ring-buffer/hot path, not to this panel's UI. Only the panel factory +\n * HTML live here.\n *\n * This module also exports `agentsOverviewApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeAgentsOverviewApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { AGENTS_OVERVIEW_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const agentsOverviewInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\"`running` = only alive agent sessions; `all` = running + recent (default).\"),\n})\n\nexport type AgentsOverviewInput = z.infer<typeof agentsOverviewInputSchema>\nexport type AgentsOverviewOutput<TSession = unknown> = { sessions: TSession[] }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface AgentsOverviewOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeAgentsOverviewApp<TSession = unknown>(\n ops: AgentsOverviewOps<TSession>,\n): AgnoMcpApp<AgentsOverviewInput, AgentsOverviewOutput<TSession>> {\n return {\n id: \"agentproto_agents_overview\",\n title: \"Agents — vue claire\",\n description:\n \"Open the agents overview — a plain-language card per agent session \" +\n \"with one human sentence (what it's doing / last said) and a coarse \" +\n \"state (à traiter / au travail / en attente / terminé). Polls live and \" +\n \"asks the server to summarise each session.\",\n inputSchema: agentsOverviewInputSchema,\n execute: async input => ({ sessions: ops.listSessions(input.filter) }),\n html: AGENTS_OVERVIEW_HTML,\n }\n}\n\nexport const agentsOverviewApp: AppHandle = defineApp({\n id: \"@agentproto/agents-overview\",\n name: \"Agents — vue claire\",\n description:\n \"Open the agents overview — a plain-language card per agent session with one human sentence \" +\n \"and a coarse state (à traiter / au travail / en attente / terminé).\",\n agents: [],\n ui: {\n html: AGENTS_OVERVIEW_HTML,\n title: \"Agents — vue claire\",\n tools: [\"session_list\", \"summarize_session\"],\n },\n})\n","/**\n * HTML bundle for the agentproto \"bureau — sessions navigateur\" panel — the\n * `BUREAU_SESSIONS_HTML` served as the `ui://agentproto_bureau_sessions/view`\n * resource. Mounted via the `agentproto_bureau_sessions` tool defined in\n * ./index.ts.\n *\n * Data only — no server LLM. The whole bundle is inline (zero CDN).\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle (zero CDN, CSP-safe inline) ──────────────────────────────────\n\nexport const BUREAU_SESSIONS_HTML = `<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>bureau — sessions navigateur</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\n*{box-sizing:border-box}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}\n#hdr h1{font-size:13px;font-weight:600}\n#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#list{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}\n.row{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:11px 14px;display:flex;align-items:center;gap:14px}\n.icon{font-size:20px;flex-shrink:0}\n.body{flex:1;min-width:0}\n.r1{display:flex;align-items:center;gap:8px}\n.adapter{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.url{font-family:Menlo,Monaco,'Courier New',monospace;font-size:11.5px;color:var(--blue);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.meta{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}\n.badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap}\n.br{background:rgba(63,185,80,.16);color:var(--green)}\n.bs{background:rgba(88,166,255,.16);color:var(--blue)}\n.bk{background:rgba(248,81,73,.14);color:var(--red)}\n.be{background:rgba(139,148,158,.14);color:var(--text2)}\n.berr{background:rgba(248,81,73,.22);color:var(--red)}\n.port{font-family:Menlo,Monaco,'Courier New',monospace}\n#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}\n#empty .ico{font-size:30px}\n#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"hdr\">\n <div>\n <h1>Bureau — sessions navigateur</h1>\n <div class=\"sub\">services navigateur actifs · URL / port / statut</div>\n </div>\n <button id=\"refresh-btn\" title=\"Rafraîchir\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"list\"><div id=\"empty\"><div class=\"ico\">🌐</div><div>Connexion au bridge…</div></div></div>\n <div id=\"statusbar\">Connexion…</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-bureau-sessions\")}\n\n// ── State ──\nvar REFRESH_MS = 5000;\nvar pollTimer = null, polling = false;\nfunction esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }\nfunction setStatus(m){ document.getElementById('statusbar').textContent = m; }\n\nfunction badgeClass(st){\n if (st === 'running') return 'br';\n if (st === 'starting') return 'bs';\n if (st === 'killed') return 'bk';\n if (st === 'error') return 'berr';\n return 'be';\n}\nfunction fmtUptime(iso, end){\n if (!iso) return '';\n var start = Date.parse(iso);\n if (isNaN(start)) return '';\n var ref = end ? Date.parse(end) : Date.now();\n var s = Math.max(0, Math.round((ref - start) / 1000));\n if (s < 60) return s + 's';\n var m = Math.floor(s/60), rs = s%60;\n if (m < 60) return m + 'm ' + rs + 's';\n var h = Math.floor(m/60), rm = m%60;\n return h + 'h ' + rm + 'm';\n}\n\nfunction render(rows){\n var list = document.getElementById('list');\n if (!rows.length){\n list.innerHTML = '<div id=\"empty\"><div class=\"ico\">🌐</div><div>Aucune session navigateur</div></div>';\n return;\n }\n var html = '';\n for (var i=0;i<rows.length;i++){\n var s = rows[i];\n var bc = badgeClass(s.status);\n var url = s.browserBaseUrl || (s.browserPort ? ('http://127.0.0.1:' + s.browserPort) : '');\n var adapter = s.browserAdapterId || s.label || s.name || 'navigateur';\n var up = fmtUptime(s.startedAt, s.endedAt);\n html += '<div class=\"row\">'\n + '<div class=\"icon\">🌐</div>'\n + '<div class=\"body\">'\n + '<div class=\"r1\"><span class=\"adapter\">' + esc(adapter) + '</span>'\n + '<span class=\"badge ' + bc + '\">' + esc(s.status || '') + '</span></div>'\n + (url ? '<div class=\"url\">' + esc(url) + '</div>' : '')\n + '<div class=\"meta\">'\n + (s.browserPort ? '<span>port <span class=\"port\">' + esc(s.browserPort) + '</span></span><span class=\"dot\"></span>' : '')\n + '<span>' + (up ? 'uptime ' + esc(up) : '') + '</span>'\n + '</div>'\n + '</div>'\n + '</div>';\n }\n list.innerHTML = html;\n}\n\nfunction loadAndRender(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n var all = data.sessions || [];\n var browsers = all.filter(function(s){ return s.kind === 'browser'; });\n render(browsers);\n setStatus(browsers.length + ' session' + (browsers.length===1?'':'s') + ' · ' + new Date().toLocaleTimeString('fr-FR'));\n }).catch(function(e){ setStatus('Erreur : ' + e.message); });\n}\n\nfunction doPoll(){\n if (polling) return;\n polling = true;\n loadAndRender().then(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n }).catch(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n });\n}\nfunction doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }\n\ninitBridge().then(loadAndRender).then(function(){\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n}).catch(function(e){\n setStatus('Bridge : ' + e.message);\n document.getElementById('list').innerHTML = '<div id=\"empty\"><div class=\"ico\">⚠</div><div>Échec connexion bridge : ' + esc(e.message) + '</div></div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"bureau — sessions navigateur\" panel.\n *\n * Shows the browser sessions (kind === \"browser\"): adapter, base URL, port,\n * status and uptime. One row per live/recent browser service so you can see\n * what's bound where at a glance.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_bureau_sessions` is called by the host.\n * 2. execute() returns the browser-session snapshot.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list {kind:'all'}` every ~5 s, filtering to kind===\"browser\".\n *\n * Data only — no server LLM. The whole bundle is inline (zero CDN).\n *\n * This module also exports `bureauSessionsApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeBureauSessionsApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { BUREAU_SESSIONS_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const bureauSessionsInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\"`running` = only alive browser sessions; `all` = running + recent (default).\"),\n})\n\nexport type BureauSessionsInput = z.infer<typeof bureauSessionsInputSchema>\nexport type BureauSessionsOutput<TSession = unknown> = { sessions: TSession[] }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface BureauSessionsOps<TSession extends { kind: string }> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeBureauSessionsApp<TSession extends { kind: string }>(\n ops: BureauSessionsOps<TSession>,\n): AgnoMcpApp<BureauSessionsInput, BureauSessionsOutput<TSession>> {\n return {\n id: \"agentproto_bureau_sessions\",\n title: \"Bureau — sessions navigateur\",\n description:\n \"Open the browser-sessions panel — one row per browser service \" +\n \"(adapter, base URL, port, status, uptime). Polls live every ~5 s.\",\n inputSchema: bureauSessionsInputSchema,\n execute: async input => ({\n sessions: ops.listSessions(input.filter).filter(s => s.kind === \"browser\"),\n }),\n html: BUREAU_SESSIONS_HTML,\n }\n}\n\nexport const bureauSessionsApp: AppHandle = defineApp({\n id: \"@agentproto/bureau-sessions\",\n name: \"Bureau — sessions navigateur\",\n description:\n \"Open the browser-sessions panel — one row per browser service (adapter, base URL, port, \" +\n \"status, uptime).\",\n agents: [],\n ui: {\n html: BUREAU_SESSIONS_HTML,\n title: \"Bureau — sessions navigateur\",\n tools: [\"session_list\"],\n },\n})\n","/**\n * HTML bundle for the agentproto session story panel — the\n * `SESSION_STORY_PANEL_HTML` served as the `ui://agentproto_session_story/view`\n * resource. Mounted via the `agentproto_session_story` tool defined in\n * ./index.ts. Also consumed directly by packages/vscode's story webview\n * (`storyPanel.ts`), which relays this same HTML into a VS Code editor tab.\n *\n * Ported 1:1 from the validated mockup at docs/session-story-mockup.html —\n * same CSS, same DOM structure/classes, same interaction model (anchored\n * side panel, sticky collapsible chapter headers, Simple/Tech toggle,\n * composer with local chapter-routing classification). Fake data is\n * replaced with live data pulled through the MCP Apps postMessage bridge.\n *\n * The HTML must stay fully self-contained (CSP: inline CSS/JS only, no\n * CDN, no external fonts) — see runtime's mcp-apps-adapter.ts and the other\n * panels in this app family (sessions-panel, agents-overview) for the same\n * constraint. Because of that, this file cannot `import` the pure\n * `buildStory` module from @agentproto/runtime's session-story.ts (a\n * separate, CLI-facing heuristics module that stays in runtime — it isn't\n * a dependency of this panel) — the folding/chaptering heuristics are\n * ported to vanilla JS below (`buildStoryJs`), mirroring session-story.ts\n * function-for-function so the two stay easy to diff.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → session_list (status/picker) + agent_export\n * (timeline, re-fetched on turn boundaries) + agent_prompt (composer)\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport const SESSION_STORY_PANEL_HTML = `<!doctype html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>session story</title>\n<style>\n :root {\n color-scheme: light;\n --bg:#faf8f5; --panel:#fffdfa; --line:#ece5da; --line-soft:#f2ede3;\n --ink:#241f1a; --ink-mute:#7d7060; --ink-faint:#a9997f; --ink-ghost:#b3a893;\n --accent:#0d7a4f; --accent-soft:#e8f4ec;\n --gold:#a6701b; --gold-soft:#fff2dc;\n --blue:#1d4e80; --blue-soft:#e9f1fb;\n --violet:#5b3fa6; --violet-soft:#ede9ff;\n --sel:#fbeccd; --red:#b3261e; --red-soft:#fbeae8;\n }\n * { box-sizing:border-box; }\n html,body { height:100%; }\n body { margin:0; font-family:ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }\n .app { height:100vh; display:flex; flex-direction:column; }\n .hidden { display:none !important; }\n\n /* ── picker screen ── */\n #pickerScreen { height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }\n #pickerScreen h1 { font-size:15px; font-weight:700; }\n #pickerList { width:min(520px,90vw); max-height:60vh; overflow-y:auto; border:1px solid var(--line); border-radius:12px; background:var(--panel); }\n .pk-item { padding:10px 14px; border-bottom:1px solid var(--line-soft); cursor:pointer; display:flex; align-items:center; gap:10px; }\n .pk-item:last-child { border-bottom:none; }\n .pk-item:hover { background:var(--line-soft); }\n .pk-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .pk-meta { flex:none; font-size:10.5px; color:var(--ink-faint); font-weight:600; }\n .pk-empty { padding:24px; text-align:center; color:var(--ink-mute); font-size:12.5px; }\n .badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:10px; font-weight:700; }\n .badge.running { background:var(--accent-soft); color:var(--accent); }\n .badge.starting { background:var(--blue-soft); color:var(--blue); }\n .badge.exited { background:var(--line-soft); color:var(--ink-faint); }\n .badge.killed, .badge.error { background:var(--red-soft); color:var(--red); }\n\n /* ── big picture : mission + plan de sous-tâches ── */\n .hero { flex:none; padding:13px 20px 0; border-bottom:1px solid var(--line); background:var(--panel); }\n .hero-top { display:flex; align-items:center; gap:13px; }\n .pulse { width:10px; height:10px; border-radius:50%; background:var(--accent); flex:none;\n box-shadow:0 0 0 0 rgba(13,122,79,.35); animation:pulse 2.4s infinite; }\n .pulse.off { background:var(--ink-ghost); animation:none; }\n @keyframes pulse { 70% { box-shadow:0 0 0 9px rgba(13,122,79,0); } 100% { box-shadow:0 0 0 0 rgba(13,122,79,0); } }\n .who { min-width:0; flex:1; }\n .who .h1 { font-size:14.5px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .who .h2 { font-size:12px; color:var(--ink-mute); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .modewrap { display:flex; border:1px solid var(--line); border-radius:9px; overflow:hidden; flex:none; }\n .modewrap button { border:none; background:var(--panel); color:var(--ink-mute); font-size:11px; font-weight:700; padding:6px 11px; cursor:pointer; }\n .modewrap button.on { background:var(--ink); color:#fdf9f2; }\n button.sim, a.sim { border:1px solid var(--line); background:var(--panel); color:var(--ink-mute); font-weight:700;\n font-size:11.5px; border-radius:8px; padding:6px 12px; cursor:pointer; flex:none; }\n button.sim.on { background:var(--ink); border-color:var(--ink); color:#fdf9f2; }\n a.sim { display:inline-flex; align-items:center; text-decoration:none; }\n a.sim:hover { border-color:var(--ink-ghost); color:var(--ink); }\n\n /* plan strip : les sous-tâches, l'avancement d'un coup d'œil */\n .plan { display:flex; gap:6px; overflow-x:auto; padding:11px 0 12px; scrollbar-width:none; }\n .plan::-webkit-scrollbar { display:none; }\n .pt { flex:none; display:flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; padding:5px 11px;\n border-radius:999px; border:1px solid var(--line); background:var(--bg); color:var(--ink-mute); cursor:pointer; white-space:nowrap; }\n .pt:hover { border-color:var(--ink-ghost); }\n .pt .st { font-size:10px; }\n .pt.done { color:var(--accent); background:var(--accent-soft); border-color:transparent; }\n .pt.cur { color:var(--gold); background:var(--gold-soft); border-color:transparent; }\n .pt.cur .st { animation:blink 1.6s infinite; }\n @keyframes blink { 50% { opacity:.35; } }\n\n /* ── corps ── */\n .body { flex:1; display:flex; min-height:0; }\n .feedcol { flex:1; min-width:320px; display:flex; flex-direction:column; }\n .feed { flex:1; overflow-y:auto; padding:4px 14px 10px; display:flex; flex-direction:column; scroll-behavior:smooth; }\n .fspacer { flex:1; }\n\n /* chapitres */\n .chap { flex:none; position:sticky; top:0; z-index:5; margin:8px -4px 2px; padding:7px 12px; display:flex; align-items:center; gap:9px;\n background:color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter:blur(6px);\n border-radius:9px; cursor:pointer; font-size:11.5px; font-weight:800; letter-spacing:.03em; color:var(--ink-mute); }\n .chap:hover { color:var(--ink); }\n .chap .cst { flex:none; width:17px; height:17px; border-radius:50%; display:grid; place-items:center; font-size:9.5px; font-weight:900; }\n .chap.done .cst { background:var(--accent-soft); color:var(--accent); }\n .chap.cur .cst { background:var(--gold-soft); color:var(--gold); }\n .chap .cnum { color:var(--ink-ghost); font-weight:700; }\n .chap .csum { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .chap .cmeta { flex:none; font-size:10.5px; color:var(--ink-ghost); font-weight:600; }\n .chap .cchev { flex:none; color:var(--ink-ghost); transition:transform .15s; }\n .chap.open .cchev { transform:rotate(90deg); }\n\n .row { flex:none; min-height:44px; margin:1px 0 1px 10px; padding:5px 12px 5px 10px; display:flex; align-items:center; gap:11px;\n cursor:pointer; border-radius:10px; border-left:3px solid transparent; transition:background .12s; }\n .row:hover { background:var(--line-soft); }\n .row[aria-selected=\"true\"] { background:var(--sel); border-left-color:var(--gold); }\n .row .ico { width:24px; height:24px; border-radius:8px; flex:none; display:grid; place-items:center; font-size:11.5px; font-weight:800; }\n .ico.k-text { background:var(--blue-soft); color:var(--blue); }\n .ico.k-edit { background:var(--gold-soft); color:var(--gold); }\n .ico.k-bash { background:var(--accent-soft); color:var(--accent); }\n .ico.k-read { background:var(--violet-soft); color:var(--violet); }\n .ico.k-user { background:var(--ink); color:#fdf9f2; }\n .row .mid { flex:1; min-width:0; }\n .row .sum { display:block; font-size:13.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .row .raw1 { display:block; font-size:10.5px; color:var(--ink-faint); font-family:ui-monospace,Menlo,monospace;\n white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }\n body:not(.tech) .row .raw1 { display:none; }\n .row .route { display:inline-block; font-size:10px; font-weight:800; padding:1px 8px; border-radius:999px; margin-top:2px; }\n .route.cont { background:var(--blue-soft); color:var(--blue); }\n .route.newt { background:var(--gold-soft); color:var(--gold); }\n .row .cnt { flex:none; font-size:10px; font-weight:800; color:var(--gold); background:var(--gold-soft); padding:2px 7px; border-radius:999px; }\n .row .ts { flex:none; font-size:10.5px; color:var(--ink-ghost); font-variant-numeric:tabular-nums; }\n @keyframes slidein { from { opacity:0; transform:translateY(6px); } }\n .row.new { animation:slidein .25s ease-out; }\n\n /* ── panneau ancré ── */\n .panel { flex:none; width:0; overflow:hidden; border-left:1px solid transparent; background:var(--panel);\n display:flex; flex-direction:column; transition:width .22s ease, border-color .22s; }\n .panel.open { width:min(430px,46vw); border-left-color:var(--line); }\n .panel-inner { width:min(430px,46vw); flex:1; display:flex; flex-direction:column; min-height:0; }\n .phead { flex:none; padding:14px 16px 12px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; gap:11px; }\n .phead .ico { width:28px; height:28px; font-size:13px; border-radius:9px; }\n .phead .tt { min-width:0; flex:1; }\n .phead .t { font-size:14px; font-weight:700; line-height:1.4; }\n .phead .s { font-size:11px; color:var(--ink-faint); margin-top:3px; font-variant-numeric:tabular-nums; }\n .pnav { display:flex; gap:4px; flex:none; }\n .pnav button { width:26px; height:26px; border:1px solid var(--line); background:var(--panel); border-radius:8px;\n color:var(--ink-mute); font-size:12px; cursor:pointer; display:grid; place-items:center; }\n .pnav button:disabled { opacity:.3; cursor:default; }\n .pbody { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }\n .plain { font-size:14px; line-height:1.7; }\n .plain .why { margin-top:8px; font-size:12.5px; color:var(--ink-mute); line-height:1.6; }\n .facts { display:flex; flex-wrap:wrap; gap:6px; }\n .fact { font-size:11px; font-weight:700; background:var(--bg); border:1px solid var(--line); color:var(--ink-mute); padding:4px 10px; border-radius:999px; }\n .fact.ok { background:var(--accent-soft); border-color:transparent; color:var(--accent); }\n details.techbox { border:1px solid var(--line); border-radius:12px; background:var(--bg); overflow:hidden; }\n details.techbox summary { list-style:none; cursor:pointer; padding:10px 14px; font-size:11.5px; font-weight:800;\n letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint); display:flex; align-items:center; gap:8px; }\n details.techbox summary::-webkit-details-marker { display:none; }\n details.techbox summary::after { content:\"▸\"; margin-left:auto; transition:transform .15s; }\n details.techbox[open] summary::after { transform:rotate(90deg); }\n .techlist { padding:0 12px 12px; display:flex; flex-direction:column; gap:8px; }\n .titem { border:1px solid var(--line); border-radius:10px; background:var(--panel); overflow:hidden; }\n .titem .th { padding:8px 12px; font-size:11.5px; font-weight:700; color:var(--ink-mute); display:flex; align-items:center; gap:8px; }\n .titem .th .copy { margin-left:auto; border:none; background:none; color:var(--ink-ghost); font-size:11px; cursor:pointer; padding:2px 4px; border-radius:5px; }\n .titem .th .copy:hover { color:var(--ink); background:var(--line-soft); }\n .titem pre { margin:0; border-top:1px solid var(--line-soft); font-family:ui-monospace,Menlo,monospace; font-size:11.5px;\n line-height:1.55; color:#4a4236; padding:9px 12px; overflow:auto; max-height:240px; white-space:pre-wrap; word-break:break-word; }\n .d-text { font-size:13.5px; line-height:1.7; }\n .d-text p { margin:0 0 8px; }\n .d-text p:last-child { margin-bottom:0; }\n .d-text h1, .d-text h2, .d-text h3, .d-text h4, .d-text h5, .d-text h6 { margin:12px 0 6px; line-height:1.3; }\n .d-text h1:first-child, .d-text h2:first-child, .d-text h3:first-child { margin-top:0; }\n .d-text ul, .d-text ol { margin:0 0 8px; padding-left:20px; }\n .d-text code { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; background:var(--bg); border-radius:4px; padding:1px 5px; }\n .d-text pre { margin:0 0 8px; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:9px 12px; overflow:auto; }\n .d-text pre code { background:none; border-radius:0; padding:0; }\n .d-text table { border-collapse:collapse; margin:0 0 8px; font-size:12.5px; }\n .d-text th, .d-text td { border:1px solid var(--line); padding:4px 8px; text-align:left; }\n .d-text a { color:var(--blue); }\n .pfoot { flex:none; border-top:1px solid var(--line); padding:9px 16px; font-size:11px; color:var(--ink-ghost); display:flex; gap:10px; }\n .kbd { font-family:ui-monospace,Menlo,monospace; font-size:10px; border:1px solid var(--line); border-bottom-width:2px;\n border-radius:5px; padding:1px 5px; background:var(--panel); color:var(--ink-mute); }\n\n /* ── boîte d'envoi + routage IA ── */\n .composer { flex:none; border-top:1px solid var(--line); background:var(--panel); padding:10px 14px; }\n .composer .cbar { display:flex; gap:8px; }\n .composer textarea { flex:1; resize:none; border:1px solid var(--line); border-radius:10px; padding:9px 12px; font:inherit; font-size:13px; max-height:110px; background:var(--bg); }\n .composer textarea:focus { outline:2px solid #241f1a22; }\n .composer textarea:disabled { opacity:.5; cursor:not-allowed; }\n .composer button { border:none; border-radius:10px; padding:0 16px; background:var(--ink); color:#fdf9f2; font-weight:700; font-size:13px; cursor:pointer; }\n .composer button:disabled { opacity:.4; cursor:not-allowed; }\n .composer .routing { font-size:11px; color:var(--ink-faint); padding:6px 2px 0; min-height:22px; }\n .composer .routing .r-cont { color:var(--blue); font-weight:700; }\n .composer .routing .r-newt { color:var(--gold); font-weight:700; }\n #statusbar { flex:none; padding:4px 20px; font-size:10.5px; color:var(--ink-ghost); border-top:1px solid var(--line-soft); }\n</style>\n</head>\n<body>\n<div id=\"pickerScreen\">\n <h1>Choisis une session</h1>\n <div id=\"pickerList\"><div class=\"pk-empty\">Connexion…</div></div>\n</div>\n\n<div class=\"app hidden\" id=\"storyScreen\">\n <div class=\"hero\">\n <div class=\"hero-top\">\n <span class=\"pulse\" id=\"pulse\"></span>\n <div class=\"who\">\n <div class=\"h1\" id=\"heroTitle\"></div>\n <div class=\"h2\" id=\"heroSub\"></div>\n </div>\n <div class=\"modewrap\"><button id=\"modeSimple\" class=\"on\" type=\"button\">Simple</button><button id=\"modeTech\" type=\"button\">Tech</button></div>\n <a class=\"sim hidden\" id=\"fullPanelLink\" href=\"#\" target=\"_blank\" rel=\"noopener\" title=\"Ouvrir la session en direct (live-session du daemon)\">↗ panneau complet</a>\n <button class=\"sim\" id=\"switchBtn\" type=\"button\">⇆ changer</button>\n </div>\n <div class=\"plan\" id=\"plan\"></div>\n </div>\n\n <div class=\"body\">\n <div class=\"feedcol\">\n <div class=\"feed\" id=\"feed\"><div class=\"fspacer\"></div><div id=\"rows\"></div></div>\n <div class=\"composer\">\n <div class=\"cbar\">\n <textarea id=\"msgBox\" rows=\"1\" placeholder=\"Écris à l'agent… (la surcouche classe ton message : suite de la sous-tâche ou nouvelle sous-tâche)\"></textarea>\n <button id=\"sendBtn\" type=\"button\">Envoyer</button>\n </div>\n <div class=\"routing\" id=\"routing\"></div>\n </div>\n </div>\n\n <aside class=\"panel\" id=\"panel\" aria-label=\"Détail de l'étape\">\n <div class=\"panel-inner\">\n <div class=\"phead\">\n <span class=\"ico\" id=\"pIco\"></span>\n <div class=\"tt\"><div class=\"t\" id=\"pTitle\"></div><div class=\"s\" id=\"pSub\"></div></div>\n <div class=\"pnav\"><button id=\"pPrev\" type=\"button\">↑</button><button id=\"pNext\" type=\"button\">↓</button><button id=\"pClose\" type=\"button\">✕</button></div>\n </div>\n <div class=\"pbody\" id=\"pBody\"></div>\n <div class=\"pfoot\"><span class=\"kbd\">↑</span><span class=\"kbd\">↓</span> naviguer · <span class=\"kbd\">Esc</span> fermer</div>\n </div>\n </aside>\n </div>\n <div id=\"statusbar\"></div>\n</div>\n\n<script>\nvar $=function(id){ return document.getElementById(id); };\nvar esc=function(s){ return String(s==null?\"\":s).replace(/[&<>]/g,function(c){ return {\"&\":\"&\",\"<\":\"<\",\">\":\">\"}[c]; }); };\n\n// ============================================================\n// renderMd — vanilla-JS port of markdown-lite.ts. Kept\n// function-for-function identical so the two are easy to diff (same\n// self-contained-panel constraint as buildStoryJs below): headers,\n// bold/italic, inline/fenced code, bullet/numbered lists, pipe tables and\n// links, with every raw text run HTML-escaped before any generated tag\n// wraps it.\n// ============================================================\nfunction escHtmlMd(s){ return String(s).replace(/[&<>\"]/g,function(c){ if(c==='&') return '&'; if(c==='<') return '<'; if(c==='>') return '>'; return '"'; }); }\nfunction renderInlineMd(text){\n var out=escHtmlMd(text);\n out=out.replace(/\\`([^\\`]+)\\`/g,function(_m,code){ return '<code>'+code+'</code>'; });\n out=out.replace(/\\\\[([^\\\\]]+)\\\\]\\\\((https?:\\\\/\\\\/[^\\\\s)]+)\\\\)/g,function(_m,label,url){ return '<a href=\"'+url+'\" target=\"_blank\" rel=\"noopener noreferrer\">'+label+'</a>'; });\n out=out.replace(/\\\\*\\\\*([^*]+)\\\\*\\\\*/g,'<strong>$1</strong>');\n out=out.replace(/(^|[^*])\\\\*([^*]+)\\\\*(?!\\\\*)/g,'$1<em>$2</em>');\n return out;\n}\nfunction isTableSepMd(line){ return /^\\\\s*\\\\|?\\\\s*:?-+:?\\\\s*(\\\\|\\\\s*:?-+:?\\\\s*)+\\\\|?\\\\s*$/.test(line); }\nfunction splitRowMd(line){ return line.trim().replace(/^\\\\|/,'').replace(/\\\\|$/,'').split('|').map(function(c){ return c.trim(); }); }\nfunction renderMd(md){\n var lines=String(md==null?'':md).replace(/\\\\r\\\\n?/g,'\\\\n').split('\\\\n');\n var out=[], para=[], list=null;\n function flushPara(){ if(para.length){ out.push('<p>'+para.map(renderInlineMd).join('<br>')+'</p>'); para=[]; } }\n function flushList(){ if(list){ var tag=list.ordered?'ol':'ul'; out.push('<'+tag+'>'+list.items.map(function(i){ return '<li>'+renderInlineMd(i)+'</li>'; }).join('')+'</'+tag+'>'); list=null; } }\n function flushAll(){ flushPara(); flushList(); }\n var i=0;\n while(i<lines.length){\n var line=lines[i];\n if(/^\\\\s*\\`\\`\\`/.test(line)){\n flushAll();\n var code=[]; i+=1;\n while(i<lines.length && !/^\\\\s*\\`\\`\\`/.test(lines[i])){ code.push(lines[i]); i+=1; }\n i+=1;\n out.push('<pre><code>'+escHtmlMd(code.join('\\\\n'))+'</code></pre>');\n continue;\n }\n var header=line.match(/^(#{1,6})\\\\s+(.*)$/);\n if(header){\n flushAll();\n var level=header[1].length;\n out.push('<h'+level+'>'+renderInlineMd(header[2].trim())+'</h'+level+'>');\n i+=1;\n continue;\n }\n if(/^\\\\s*\\\\|/.test(line) && i+1<lines.length && isTableSepMd(lines[i+1])){\n flushAll();\n var headCells=splitRowMd(line);\n i+=2;\n var bodyRows=[];\n while(i<lines.length && /^\\\\s*\\\\|/.test(lines[i])){ bodyRows.push(splitRowMd(lines[i])); i+=1; }\n out.push('<table><thead><tr>'+headCells.map(function(c){ return '<th>'+renderInlineMd(c)+'</th>'; }).join('')+'</tr></thead><tbody>'\n +bodyRows.map(function(r){ return '<tr>'+r.map(function(c){ return '<td>'+renderInlineMd(c)+'</td>'; }).join('')+'</tr>'; }).join('')+'</tbody></table>');\n continue;\n }\n var bullet=line.match(/^\\\\s*[-*+]\\\\s+(.*)$/);\n var numbered=line.match(/^\\\\s*\\\\d+\\\\.\\\\s+(.*)$/);\n if(bullet || numbered){\n flushPara();\n var ordered=!!numbered;\n var item=(bullet||numbered)[1];\n if(!list || list.ordered!==ordered){ flushList(); list={ordered:ordered,items:[]}; }\n list.items.push(item);\n i+=1;\n continue;\n }\n if(line.trim()===''){ flushAll(); i+=1; continue; }\n flushList();\n para.push(line);\n i+=1;\n }\n flushAll();\n return out.join('');\n}\n\n${panelBridgeScript(\"agentproto-session-story-panel\")}\n// Best-effort: some hosts forward the triggering tool call's arguments as\n// a notification so the panel can auto-open the right session. Purely\n// additive — the session picker is the reliable path when this never\n// arrives.\nvar pendingSessionId=null;\nonHostNotification(function(method, params){\n if(/tool-input|tool-call/.test(method)){\n var args=(params && (params.arguments || params.input)) || {};\n if(args && args.sessionId) pendingSessionId=args.sessionId;\n }\n});\n\n// ============================================================\n// buildStory — vanilla-JS port of session-story.ts. Kept\n// function-for-function identical so the two are easy to diff; the panel\n// resource must be fully self-contained (no bundler/dynamic import), so it\n// cannot import the TS module directly.\n// ============================================================\n\nvar SALIENT_KEYS=[\"file_path\",\"path\",\"filePath\",\"file\",\"command\",\"pattern\",\"query\",\"q\",\"url\",\"todos\",\"description\",\"prompt\"];\nfunction truncateStr(v,max){ var o=String(v).replace(/\\\\s+/g,' ').trim(); return o.length>max? o.slice(0,max-1)+'…':o; }\nfunction formatArgValue(v){\n if(typeof v==='string') return v;\n if(Array.isArray(v)) return v.length+' item'+(v.length===1?'':'s');\n if(v && typeof v==='object') return JSON.stringify(v);\n return String(v);\n}\nfunction pickSalient(args){\n for(var i=0;i<SALIENT_KEYS.length;i++){\n var k=SALIENT_KEYS[i], v=args[k];\n if(v!==undefined && v!==null && v!=='') return formatArgValue(v);\n }\n return null;\n}\nfunction formatToolCall(name,args){\n name=name||'tool';\n args=(args && typeof args==='object' && !Array.isArray(args)) ? args : {};\n var salient=pickSalient(args);\n if(salient!==null){\n if(name.toLowerCase().indexOf(salient.toLowerCase())>=0) return truncateStr(name,120);\n return truncateStr(name+' '+salient,120);\n }\n if(Object.keys(args).length===0) return name;\n return truncateStr(name+' '+JSON.stringify(args),120);\n}\nfunction extractText(v){\n if(v==null) return null;\n if(typeof v==='string') return v;\n if(Array.isArray(v)){\n var parts=v.map(extractText).filter(function(x){ return x!=null; });\n return parts.length? parts.join('\\\\n') : null;\n }\n if(typeof v==='object'){\n if(typeof v.text==='string') return v.text;\n if(typeof v.message==='string') return v.message;\n if(Array.isArray(v.content)) return extractText(v.content);\n if(typeof v.error==='string') return v.error;\n if(v.error && typeof v.error==='object' && typeof v.error.message==='string') return v.error.message;\n return null;\n }\n return null;\n}\nfunction formatToolResult(toolName,result,isError){\n var text=extractText(result);\n if(isError){\n var message=text!=null? text : (result!=null? JSON.stringify(result) : 'failed');\n var firstLine=String(message).split(/\\\\r?\\\\n/)[0] || message;\n return truncateStr(firstLine,160);\n }\n if(text==null) return null;\n var trimmed=text.trim();\n if(!trimmed) return null;\n var lines=trimmed.split(/\\\\r?\\\\n/);\n if(lines.length>1){\n var bytes=new TextEncoder().encode(trimmed).length;\n return lines.length+' lines, '+bytes+'B';\n }\n return truncateStr(lines[0],160);\n}\n\nfunction classifyKind(toolCalls){\n if(!toolCalls || toolCalls.length===0) return 'text';\n var names=toolCalls.map(function(t){ return t.name.toLowerCase(); });\n if(names.some(function(n){ return /edit|write/.test(n); })) return 'edit';\n if(names.some(function(n){ return /bash|terminal|command/.test(n); })) return 'bash';\n if(names.some(function(n){ return /read|grep|glob/.test(n); })) return 'read';\n return 'text';\n}\nvar NEW_CHAPTER_RE=/\\\\b(aussi|autre|ensuite|nouveau|nouvelle|plut[oô]t|maintenant|apr[eè]s ça|il faudrait|peux[- ]tu|on pourrait|ajoute|g[eè]re)\\\\b/iu;\nfunction classifyRoute(text){\n var newt=NEW_CHAPTER_RE.test(text);\n if(!newt) return {route:'cont'};\n var title=text.replace(/[.?!].*$/,'').slice(0,42);\n return {route:'newt', title:title};\n}\nfunction formatTsJs(ts){\n if(ts===undefined || ts===null || isNaN(ts)) return '';\n return new Date(ts).toISOString().slice(11,19);\n}\nfunction firstMeaningfulLine(text){\n if(!text) return undefined;\n var lines=text.split('\\\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });\n return lines[0];\n}\nfunction lineCountOf(text){\n var n=(text||'').split('\\\\n').filter(function(l){ return l.trim().length>0; }).length;\n return n||1;\n}\nfunction parseArgsJson(s){ try{ return JSON.parse(s); }catch(e){ return {}; } }\n\nfunction foldToolStep(assistant,toolResults){\n var toolCalls=assistant.toolCalls||[];\n var kind=classifyKind(toolCalls);\n var count=toolCalls.length||1;\n var items=[], facts=[];\n if(assistant.text && assistant.text.trim()) items.push({text:assistant.text.trim()});\n toolCalls.forEach(function(tc,i){\n var args=parseArgsJson(tc.args);\n var h=formatToolCall(tc.name,args);\n var resultMsg=toolResults[i];\n var resultText=(resultMsg && resultMsg.text) || '';\n var isError=resultText.indexOf('[error]')===0;\n var r=isError? resultText.slice(7).trim() : resultText;\n items.push({h:h,r:r});\n var fact=formatToolResult(tc.name,r,isError);\n if(fact) facts.push(fact);\n });\n var firstLine=firstMeaningfulLine(assistant.text);\n var firstToolCall=toolCalls[0];\n var sum=firstLine!==undefined? firstLine : (firstToolCall? formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args)) : '…');\n var raw1;\n if(toolCalls.length===0) raw1='assistant · '+lineCountOf(assistant.text)+' ligne(s)';\n else if(toolCalls.length===1) raw1=formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args));\n else raw1=(firstToolCall? firstToolCall.name : 'tool')+' ×'+toolCalls.length;\n return {kind:kind, ts:formatTsJs(assistant.ts), sum:sum, raw1:raw1, count:count, facts:facts, items:items};\n}\nfunction foldUserStep(msg){\n var text=msg.text||'';\n return {kind:'user', ts:formatTsJs(msg.ts), sum:'« '+truncateStr(text,80)+' »', raw1:'user · '+lineCountOf(text)+' ligne(s)', count:1, facts:[], items:[{text:text}], userText:text};\n}\nfunction foldOrphanToolStep(msg){\n var text=msg.text||'';\n var isError=text.indexOf('[error]')===0;\n var r=isError? text.slice(7).trim() : text;\n var name=msg.toolName||'tool';\n var fact=formatToolResult(name,r,isError);\n return {kind:classifyKind([{name:name}]), ts:formatTsJs(msg.ts), sum: msg.toolName? (msg.toolName+' · résultat') : \"Résultat d'outil\", raw1: msg.toolName||'tool', count:1, facts: fact?[fact]:[], items:[{h:name,r:r}]};\n}\nfunction foldSystemStep(msg){\n var text=msg.text||'';\n var line=firstMeaningfulLine(text);\n return {kind:'text', ts:formatTsJs(msg.ts), sum: line!==undefined? line : text, raw1:'system', count:1, facts:[], items: text?[{text:text}]:[]};\n}\nfunction foldMessages(messages){\n var steps=[], i=0;\n while(i<messages.length){\n var msg=messages[i];\n if(msg.role==='user'){ steps.push(foldUserStep(msg)); i+=1; continue; }\n if(msg.role==='assistant'){\n var j=i+1, toolResults=[];\n while(j<messages.length && messages[j].role==='tool'){ toolResults.push(messages[j]); j+=1; }\n steps.push(foldToolStep(msg,toolResults)); i=j; continue;\n }\n if(msg.role==='tool'){ steps.push(foldOrphanToolStep(msg)); i+=1; continue; }\n steps.push(foldSystemStep(msg)); i+=1;\n }\n return steps;\n}\nfunction buildStoryJs(messages){\n var folded=foldMessages(messages||[]);\n var chapters=[], steps=[];\n var currentChapterId, sawFirstUser=false;\n function closeCurrent(){ var cur=chapters.filter(function(c){ return c.id===currentChapterId; })[0]; if(cur) cur.status='done'; }\n function openChapter(title){ var id='c'+(chapters.length+1); chapters.push({id:id,title:title,status:'cur'}); return id; }\n folded.forEach(function(step){\n var route;\n if(step.kind==='user' && step.userText!==undefined){\n if(!sawFirstUser){ sawFirstUser=true; currentChapterId=openChapter('Cadrage'); }\n else {\n var verdict=classifyRoute(step.userText);\n route=verdict.route;\n if(verdict.route==='newt'){ closeCurrent(); currentChapterId=openChapter(verdict.title||'Nouvelle sous-tâche'); }\n }\n } else if(currentChapterId===undefined){ currentChapterId=openChapter('Cadrage'); }\n var out={chap:currentChapterId, kind:step.kind, ts:step.ts, sum:step.sum, raw1:step.raw1, count:step.count, facts:step.facts, items:step.items};\n if(route) out.route=route;\n steps.push(out);\n });\n return {chapters:chapters, steps:steps};\n}\n\n// ============================================================\n// App state\n// ============================================================\nvar sessions=[];\nvar activeSessionId=null;\nvar story={chapters:[], steps:[]};\nvar open={};\nvar selected=-1;\nvar lastSeenOutputAt=null;\nvar pollTimer=null, polling=false;\nvar ICONS={text:[\"k-text\",\"A\"],edit:[\"k-edit\",\"✎\"],bash:[\"k-bash\",\"▸\"],read:[\"k-read\",\"⌕\"],user:[\"k-user\",\"T\"]};\nvar icoSpec=function(k){ return ICONS[k]||ICONS.text; };\nvar chapOf=function(id){ return story.chapters.filter(function(c){ return c.id===id; })[0]; };\nvar curChap=function(){ return story.chapters.filter(function(c){ return c.status==='cur'; })[0] || story.chapters[story.chapters.length-1]; };\n\nfunction setStatus(msg){ $('statusbar').textContent=msg; }\nfunction nowTs(){ return new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); }\n\nfunction titleOf(s){\n return s.label || s.name || (s.command? s.command.split(/\\\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);\n}\n\n// ============================================================\n// Picker screen\n// ============================================================\nfunction renderPicker(){\n var el=$('pickerList');\n if(!sessions.length){ el.innerHTML='<div class=\"pk-empty\">Aucune session</div>'; return; }\n var html='';\n sessions.forEach(function(s){\n html+='<div class=\"pk-item\" data-id=\"'+esc(s.id)+'\">'\n + '<span class=\"pk-name\">'+esc(titleOf(s))+'</span>'\n + '<span class=\"badge '+esc(s.status)+'\">'+esc(s.status)+'</span>'\n + '<span class=\"pk-meta\">'+esc(s.kind||'')+'</span>'\n + '</div>';\n });\n el.innerHTML=html;\n Array.prototype.forEach.call(el.querySelectorAll('.pk-item'), function(row){\n row.onclick=function(){ openSession(row.getAttribute('data-id')); };\n });\n}\n\nfunction showPicker(){\n activeSessionId=null;\n $('pickerScreen').classList.remove('hidden');\n $('storyScreen').classList.add('hidden');\n renderPicker();\n}\n\nfunction openSession(id){\n activeSessionId=id;\n story={chapters:[], steps:[]};\n open={};\n selected=-1;\n lastSeenOutputAt=null;\n $('pickerScreen').classList.add('hidden');\n $('storyScreen').classList.remove('hidden');\n // Deep-link to the daemon's own live-session panel (GET /apps/:appId/ui\n // pins it via ?sessionId=). Only resolvable when this panel is itself\n // served standalone by the daemon; inside an MCP host iframe the origin\n // isn't the daemon's, so the link stays hidden rather than dead.\n var daemonOrigin=(location.pathname.indexOf('/apps/')===0&&location.origin&&location.origin!=='null')?location.origin:'';\n $('fullPanelLink').href=daemonOrigin?daemonOrigin+'/apps/@agentproto/live-session/ui?sessionId='+encodeURIComponent(id):'#';\n $('fullPanelLink').classList.toggle('hidden',!daemonOrigin);\n closePanel();\n loadStory().then(renderAll);\n}\n\n// ============================================================\n// Story loading\n// ============================================================\nfunction activeSession(){ return sessions.filter(function(s){ return s.id===activeSessionId; })[0]; }\n\nfunction loadStory(){\n return callTool('agent_export', {sessionId:activeSessionId, format:'json'}).then(function(data){\n var messages=(data && data.messages) || [];\n story=buildStoryJs(messages);\n // Default open state: only the last (current) chapter is expanded.\n var last=story.chapters[story.chapters.length-1];\n if(last && !(last.id in open)) open[last.id]=true;\n }).catch(function(e){\n setStatus('Erreur export : '+e.message);\n });\n}\n\nfunction canSend(){\n var s=activeSession();\n return !!s && s.kind==='agent-cli' && s.status==='running';\n}\n\nfunction renderComposer(){\n var s=activeSession();\n var box=$('msgBox'), btn=$('sendBtn');\n var enabled=canSend();\n box.disabled=!enabled;\n btn.disabled=!enabled;\n if(!s){ box.placeholder='Session introuvable.'; }\n else if(!enabled) box.placeholder='Lecture seule — session '+esc(s.status)+'.';\n else box.placeholder=\"Écris à l'agent… (la surcouche classe ton message : suite de la sous-tâche ou nouvelle sous-tâche)\";\n}\n\nfunction renderHero(){\n var s=activeSession();\n $('heroTitle').textContent=s? titleOf(s) : (activeSessionId||'');\n var firstUser=story.steps.filter(function(st){ return st.kind==='user'; })[0];\n var mission=firstUser? firstUser.userText || (firstUser.items[0] && firstUser.items[0].text) : null;\n $('heroSub').textContent=mission? truncateStr(mission,200) : 'Aucun message pour le moment.';\n var p=$('pulse');\n p.classList.toggle('off', !(s && (s.status==='running' || s.status==='starting')));\n renderComposer();\n}\n\nfunction renderAll(){\n renderHero();\n renderPlan();\n renderRows('bottom');\n}\n\n// ============================================================\n// big picture strip\n// ============================================================\nfunction renderPlan(){\n var done=story.chapters.filter(function(c){ return c.status==='done'; }).length;\n $('plan').innerHTML=story.chapters.map(function(c,i){\n return '<span class=\"pt '+c.status+'\" data-c=\"'+esc(c.id)+'\"><span class=\"st\">'+(c.status==='done'?'✓':'●')+'</span>'+(i+1)+'. '+esc(c.title)+'</span>';\n }).join('') + '<span class=\"pt\" style=\"cursor:default\"><b>'+done+'/'+story.chapters.length+'</b> faites</span>';\n Array.prototype.forEach.call($('plan').querySelectorAll('.pt[data-c]'), function(el){\n el.onclick=function(){ open[el.getAttribute('data-c')]=true; renderRows(); jumpToChap(el.getAttribute('data-c')); };\n });\n}\nfunction jumpToChap(cid){\n var el=document.querySelector('.chap[data-c=\"'+cid+'\"]');\n if(el) el.scrollIntoView({block:'start',behavior:'smooth'});\n}\n\n// ============================================================\n// feed segmenté par chapitres\n// ============================================================\nfunction rowHtml(s,i,isNew){\n var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];\n var route=s.route? '<span class=\"route '+(s.route==='newt'?'newt':'cont')+'\">'+(s.route==='newt'?'★ nouvelle sous-tâche':'↳ suite')+'</span>' : '';\n return '<div class=\"row '+(isNew?'new':'')+'\" aria-selected=\"'+(i===selected)+'\" data-i=\"'+i+'\">'\n + '<span class=\"ico '+cls+'\">'+ch+'</span>'\n + '<span class=\"mid\"><span class=\"sum\">'+esc(s.sum)+'</span><span class=\"raw1\">'+esc(s.raw1||'')+'</span>'+route+'</span>'\n + (s.count>1? '<span class=\"cnt\">×'+s.count+'</span>':'') + '<span class=\"ts\">'+esc(s.ts||'')+'</span>'\n + '</div>';\n}\nfunction renderRows(keepScroll,newIdx){\n var feed=$('feed');\n var prevH=feed.scrollHeight, prevTop=feed.scrollTop;\n var html='';\n story.chapters.forEach(function(c,ci){\n var chapSteps=[];\n story.steps.forEach(function(s,i){ if(s.chap===c.id) chapSteps.push({s:s,i:i}); });\n if(!chapSteps.length) return;\n var isOpen=!!open[c.id];\n html+='<div class=\"chap '+c.status+' '+(isOpen?'open':'')+'\" data-c=\"'+esc(c.id)+'\">'\n + '<span class=\"cst\">'+(c.status==='done'?'✓':'●')+'</span><span class=\"cnum\">'+(ci+1)+'.</span>'\n + '<span class=\"csum\">'+esc(c.title)+'</span>'\n + '<span class=\"cmeta\">'+chapSteps.length+' étape'+(chapSteps.length>1?'s':'')+'</span><span class=\"cchev\">▸</span>'\n + '</div>';\n if(isOpen) html += chapSteps.map(function(x){ return rowHtml(x.s,x.i,x.i===newIdx); }).join('');\n });\n $('rows').innerHTML=html;\n Array.prototype.forEach.call(document.querySelectorAll('.row'), function(el){\n el.onclick=function(){ selectStep(Number(el.getAttribute('data-i'))); };\n });\n Array.prototype.forEach.call(document.querySelectorAll('.chap'), function(el){\n el.onclick=function(){ var c=el.getAttribute('data-c'); open[c]=!open[c]; renderRows(); };\n });\n if(keepScroll==='bottom') feed.scrollTop=feed.scrollHeight;\n else if(keepScroll==='preserve') feed.scrollTop=feed.scrollHeight-prevH+prevTop;\n}\n\n// ============================================================\n// panneau ancré\n// ============================================================\nfunction selectStep(i){\n selected=i;\n var s=story.steps[i]; if(!s) return;\n open[s.chap]=true;\n $('panel').classList.add('open');\n var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];\n var ico=$('pIco'); ico.className='ico '+cls; ico.textContent=ch;\n $('pTitle').textContent=s.sum;\n var c=chapOf(s.chap);\n $('pSub').textContent=(s.ts? s.ts+' · ':'')+(c? ('sous-tâche : '+c.title) : '');\n var facts=(s.facts||[]).map(function(f){\n return '<span class=\"fact '+(/✓|exit 0|passed|0 match/.test(f)?'ok':'')+'\">'+esc(f)+'</span>';\n }).join('');\n var tech=(s.items||[]).map(function(it,k){\n return it.text!==undefined\n ? '<div class=\"d-text\">'+renderMd(it.text)+'</div>'\n : '<div class=\"titem\"><div class=\"th\">'+esc(it.h)+'<button class=\"copy\" data-k=\"'+k+'\" type=\"button\">⧉</button></div><pre>'+esc(it.r)+'</pre></div>';\n }).join('');\n $('pBody').innerHTML=''\n + '<div class=\"plain\"><div>'+esc(s.sum)+'.</div><div class=\"why\">'+esc(s.why||'')+'</div></div>'\n + (facts? '<div class=\"facts\">'+facts+'</div>':'')\n + '<details class=\"techbox\" '+(document.body.classList.contains('tech')?'open':'')+'>'\n + '<summary>Détail technique · '+(s.items||[]).length+'</summary><div class=\"techlist\">'+tech+'</div>'\n + '</details>';\n Array.prototype.forEach.call($('pBody').querySelectorAll('.copy'), function(btn){\n btn.onclick=function(e){\n e.stopPropagation();\n var it=(s.items||[])[Number(btn.getAttribute('data-k'))];\n var payload=(it.h||'')+'\\\\n'+(it.r||it.text||'');\n if(navigator.clipboard) navigator.clipboard.writeText(payload).catch(function(){});\n btn.textContent='✓'; setTimeout(function(){ btn.textContent='⧉'; },900);\n };\n });\n $('pPrev').disabled=i<=0; $('pNext').disabled=i>=story.steps.length-1;\n renderRows();\n var el=document.querySelector('.row[data-i=\"'+i+'\"]');\n if(el) el.scrollIntoView({block:'nearest',behavior:'smooth'});\n}\nfunction closePanel(){ selected=-1; var p=$('panel'); if(p) p.classList.remove('open'); renderRows(); }\n$('pClose').addEventListener('click',closePanel);\n$('pPrev').addEventListener('click',function(){ if(selected>0) selectStep(selected-1); });\n$('pNext').addEventListener('click',function(){ if(selected<story.steps.length-1) selectStep(selected+1); });\ndocument.addEventListener('keydown',function(e){\n if(e.key==='Escape'){ closePanel(); return; }\n if($('storyScreen').classList.contains('hidden')) return;\n if(selected<0 || e.target.tagName==='TEXTAREA') return;\n if(e.key==='ArrowUp'){ e.preventDefault(); if(selected>0) selectStep(selected-1); }\n if(e.key==='ArrowDown'){ e.preventDefault(); if(selected<story.steps.length-1) selectStep(selected+1); }\n});\n\n// ============================================================\n// Simple / Tech modes\n// ============================================================\nfunction setMode(tech){\n document.body.classList.toggle('tech',tech);\n $('modeTech').classList.toggle('on',tech);\n $('modeSimple').classList.toggle('on',!tech);\n if(selected>=0) selectStep(selected);\n}\n$('modeSimple').addEventListener('click',function(){ setMode(false); });\n$('modeTech').addEventListener('click',function(){ setMode(true); });\n$('switchBtn').addEventListener('click', function(){ if(pollTimer) clearTimeout(pollTimer); showPicker(); doPoll(); });\n\n// ============================================================\n// composer — local chapter-routing classification + agent_prompt\n// ============================================================\n$('sendBtn').addEventListener('click', sendMsg);\n$('msgBox').addEventListener('keydown', function(e){\n if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }\n});\nfunction sendMsg(){\n if(!canSend()) return;\n var box=$('msgBox'), text=box.value.trim();\n if(!text) return;\n box.value='';\n var cur=curChap();\n var r=cur? classifyRoute(text) : {route:'cont'};\n var chapId=cur? cur.id : null;\n if(r.route==='newt' && cur){\n cur.status='done';\n var id='c'+(story.chapters.length+1);\n story.chapters.push({id:id, title:r.title||'Nouvelle sous-tâche', status:'cur'});\n chapId=id; open[id]=true;\n $('routing').innerHTML='✦ classé : <span class=\"r-newt\">★ nouvelle sous-tâche « '+esc(r.title||'')+' »</span>';\n } else {\n $('routing').innerHTML='✦ classé : <span class=\"r-cont\">↳ suite de « '+esc(cur? cur.title : '')+' »</span>';\n }\n setTimeout(function(){ $('routing').textContent=''; },5000);\n story.steps.push({\n chap:chapId, kind:'user', ts:nowTs(),\n sum:'« '+text.slice(0,80)+(text.length>80?'…':'')+' »',\n raw1:'user · '+text.split('\\\\n').length+' ligne(s)',\n route:r.route, facts:[], items:[{text:text}], userText:text,\n });\n renderPlan(); renderRows('bottom', story.steps.length-1);\n callTool('agent_prompt', {sessionId:activeSessionId, prompt:text}).catch(function(e){\n setStatus('Envoi échoué : '+e.message);\n });\n}\n\n// ============================================================\n// Poll — session_list every ~5s; re-fetch agent_export only on turn\n// boundaries (lastOutputAt changed for the active session).\n// ============================================================\nvar POLL_MS=5000;\nfunction loadSessions(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n sessions=data.sessions||[];\n if($('pickerScreen') && !$('pickerScreen').classList.contains('hidden')) renderPicker();\n }).catch(function(e){ setStatus('Erreur : '+e.message); });\n}\nfunction doPoll(){\n if(polling) return;\n polling=true;\n loadSessions().then(function(){\n if(!activeSessionId){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }\n var s=activeSession();\n if(!s){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }\n renderHero();\n var changed=s.lastOutputAt && s.lastOutputAt!==lastSeenOutputAt;\n if(changed){\n lastSeenOutputAt=s.lastOutputAt;\n loadStory().then(function(){ renderPlan(); renderRows('bottom'); polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });\n } else {\n polling=false; pollTimer=setTimeout(doPoll,POLL_MS);\n }\n }).catch(function(){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });\n}\n\n// ============================================================\n// Boot\n// ============================================================\ninitBridge().then(loadSessions).then(function(){\n setTimeout(function(){\n var target=pendingSessionId && sessions.some(function(s){ return s.id===pendingSessionId; })\n ? pendingSessionId\n : null;\n if(!target){\n var agentSessions=sessions.filter(function(s){ return s.kind==='agent-cli'; });\n if(agentSessions.length===1) target=agentSessions[0].id;\n }\n if(target) openSession(target); else showPicker();\n pollTimer=setTimeout(doPoll,POLL_MS);\n }, 50);\n}).catch(function(e){\n setStatus('Bridge : '+e.message);\n $('pickerList').innerHTML='<div class=\"pk-empty\">Échec connexion bridge : '+esc(e.message)+'</div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"session story\" panel.\n *\n * A per-session readable timeline: a one-sentence mission + a plan strip of\n * inferred sub-task \"chapters\", a feed of steps segmented by chapter, and an\n * anchored side panel with plain-language + technical detail. Ported 1:1\n * from the validated mockup at docs/session-story-mockup.html.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_session_story` is called by the host, optionally\n * with a `sessionId`.\n * 2. execute() returns just enough for the panel to boot: when a\n * `sessionId` is given, it echoes that back and skips the sessions\n * list entirely — the panel's own JS never reads the initial snapshot,\n * it always re-fetches via `session_list` over the postMessage bridge\n * on boot (see panel.ts's `loadSessions()`/boot sequence), so shipping\n * the full list here would just be dead payload. When no `sessionId`\n * is given (the picker case) it still returns the full sessions\n * snapshot, since the model/host may surface it before the panel's\n * own bridge call lands.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and drives\n * everything else live: `session_list` for status polling,\n * `agent_export` for the transcript (folded into chapters/steps with a\n * JS port of @agentproto/runtime's session-story.ts heuristics — the\n * panel HTML is fully self-contained, so it cannot import that TS\n * module directly), and `agent_prompt` for the composer.\n *\n * This module also exports `sessionStoryApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeSessionStoryPanelApp` above, which the daemon\n * mounts directly at boot: that factory closes over LIVE `listSessions`,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { SESSION_STORY_PANEL_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\n// Re-exported so packages/vscode's storyPanel.ts (which reuses this HTML\n// byte-for-byte in a VS Code webview) can import it from this app's\n// subpath instead of reaching into ./panel.js directly.\nexport { SESSION_STORY_PANEL_HTML }\n\nexport const sessionStoryInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Session id to open directly. When omitted, the panel shows a session \" +\n \"picker (built from session_list) and lets the user choose.\",\n ),\n})\n\nexport type SessionStoryInput = z.infer<typeof sessionStoryInputSchema>\nexport type SessionStoryOutput<TSession = unknown> =\n | { sessions: TSession[]; sessionId?: string }\n | { sessionId: string }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface SessionStoryOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the sessions registry so execute() doesn't need\n * an AppContext (agentproto has no userId/guildId concept).\n */\nexport function makeSessionStoryPanelApp<TSession = unknown>(\n ops: SessionStoryOps<TSession>,\n): AgnoMcpApp<SessionStoryInput, SessionStoryOutput<TSession>> {\n return {\n id: \"agentproto_session_story\",\n title: \"Session Story\",\n description:\n \"Open the session story panel — a readable, per-session timeline for \" +\n \"two audiences at once: a plain-language summary of every step for \" +\n \"beginners, expandable to raw tool-call detail for technical users. \" +\n \"Shows a one-sentence mission, a plan strip of inferred sub-task \" +\n \"chapters, a chapter-segmented feed, and a composer to keep driving \" +\n \"the session. Polls live data and lets you jump to any step.\",\n inputSchema: sessionStoryInputSchema,\n execute: async input =>\n input.sessionId\n ? { sessionId: input.sessionId }\n : { sessions: ops.listSessions(\"all\") },\n html: SESSION_STORY_PANEL_HTML,\n }\n}\n\nexport const sessionStoryApp: AppHandle = defineApp({\n id: \"@agentproto/session-story\",\n name: \"Session Story\",\n description:\n \"Open the session story panel — a readable, per-session timeline for two audiences at once: \" +\n \"a plain-language summary of every step for beginners, expandable to raw tool-call detail for \" +\n \"technical users.\",\n agents: [],\n ui: {\n html: SESSION_STORY_PANEL_HTML,\n title: \"Session Story\",\n tools: [\"session_list\", \"agent_export\", \"agent_prompt\"],\n },\n})\n","/**\n * HTML bundle for the agentproto live-session widget — `LIVE_SESSION_HTML`,\n * a two-pane panel (left: live session tree, right: streaming timeline)\n * that lets a host attach to any running agent session and watch it work in\n * real time. Mounted via the `live_session` tool defined in ./index.ts.\n *\n * Kept in its own module, importing nothing beyond plain TS, so a consumer\n * that only needs the HTML (e.g. packages/vscode's story-panel-style\n * webviews, or a future live-session webview) can import it via the\n * `@agentproto/apps/live-session/panel` subpath without pulling in\n * `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from `./index.js` would.\n *\n * Forked structurally from terminal-panel-app.ts: same \"static ui://\n * resource renders with empty initData, widget self-bootstraps via\n * tools/call\" pattern. Unlike the terminal (one WebSocket, one PTY), this\n * widget has two independent data needs — a polled tree snapshot and a\n * per-session event timeline — so it layers on top of the bridge rather\n * than replacing it with a single socket.\n *\n * Data paths (see `.plans/CONTRACT.md` §WP-B):\n * LEFT (tree): poll `app_session_tree` via `callTool` every ~2s.\n * RIGHT (timeline): PRIMARY = `EventSource` on `/sessions/:id/events/stream`\n * (opportunistic — falls back if it doesn't open/emit\n * within ~2.5s); FALLBACK = poll `app_session_events`\n * via `callTool` every ~1.5s, advancing `since`.\n * Both app_* tools are registered by WP-A (`app-pull-tools.ts`) and are\n * `visibility: [\"app\"]`-only, i.e. reachable solely over this bridge.\n *\n * The timeline reducer lives in `live-session/logic.ts` as a pure,\n * dependency-free module so it's unit-testable outside the browser. This\n * file inlines a hand-kept COPY of that same logic into the HTML bundle\n * (see \"INLINED REDUCER COPY\" below) since the widget ships as one\n * self-contained script with no bundler step. Keep the two in sync. Its MCP\n * Apps handshake comes from the shared spec-correct panel bridge.\n *\n * Rendering (WP1): the timeline body is patched INCREMENTALLY — a\n * text-delta that merges into the last row patches only that row's text\n * node (found via `data-row-id`); every other new row is appended with one\n * `insertAdjacentHTML`. A full `innerHTML` reset happens only on session\n * start/`setFocus()` and on display-mode switches. Auto-scroll decision is\n * `isNearBottom` (inlined copy, SPEC §2) captured BEFORE any DOM mutation:\n * stick to bottom when the user was at the bottom, otherwise preserve the\n * read position exactly and surface a \"new messages\" pill.\n *\n * Compact mode (WP3/WP4): when `hostContext.displayMode === 'inline'` (or\n * the viewport is under ~640px) the tree pane collapses into a header\n * `<select>` (WP4) and the timeline renders through `groupAdjacentToolCalls`\n * (inlined copy, SPEC §3) with a colour rail and collapsible tool-call\n * groups. `setFocus()` remains the only session-switch entry point.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport type LiveSessionOutput = {\n /** Echoes the input id, or undefined to let the widget self-discover a\n * focus session client-side from `app_session_tree`. */\n sessionId?: string\n /** The daemon's own HTTP origin — connection recipe for both the SSE\n * stream and the bridge's `tools/call` fallback. */\n httpBaseUrl: string\n}\n\n// ── HTML bundle (zero CDN, self-contained) ──────────────────────────────────\n\nexport function LIVE_SESSION_HTML(initData: LiveSessionOutput): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto live session</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}\n#tree-head{padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}\n#tree-body{flex:1;overflow-y:auto;padding:6px}\n#timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}\n#timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}\n#timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}\n#head-summary{margin-left:auto;font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;white-space:nowrap;overflow:hidden;min-width:0}\n#head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}\n#head-summary .sdot.running{background:var(--green)}\n#head-summary .sdot.grey{background:var(--text3)}\n#head-summary .sdot.error{background:var(--red)}\n#usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}\n#status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}\n#timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}\n#head-selector{display:none;max-width:42%;font:inherit;font-size:12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:2px 4px}\n#new-pill{display:none;position:absolute;right:14px;bottom:12px;z-index:6;background:var(--blue);color:#0d1117;border:none;font-size:11px;font-weight:700;padding:5px 11px;border-radius:12px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4)}\n#new-pill.show{display:block}\n\n/* WP3 compact mode — colour rail + tighter rows; only under body.compact-mode\n (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */\nbody.compact-mode #tree-pane{display:none}\nbody.compact-mode #head-selector{display:inline-block}\nbody.compact-mode #focus-id-label{display:none}\nbody.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}\nbody.compact-mode .row.text{border-left-color:var(--blue)}\nbody.compact-mode .row.tool-call{border-left-color:var(--yellow)}\nbody.compact-mode .row.turn-end{border-left-color:var(--purple)}\nbody.compact-mode .row .body{font-size:12px;margin-top:3px}\nbody.compact-mode .row .rhead{font-size:10px}\ndetails.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}\ndetails.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}\ndetails.tool-group[open]>summary{margin-bottom:6px}\ndetails.tool-group .row{margin-top:5px}\n\n.tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}\n.tnode:hover{background:var(--bg3)}\n.tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}\n.tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.dot.running{background:var(--green)}\n.dot.grey{background:var(--text3)}\n.dot.error{background:var(--red)}\n.tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}\n.tnode .badge{font-size:9px;font-weight:700;color:var(--purple);border:1px solid var(--purple);border-radius:3px;padding:0 4px;flex-shrink:0}\n.tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}\n#tree-empty{padding:12px;color:var(--text3);font-size:12px}\n\n.row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}\n.row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}\n.row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}\n.row.text .kind{color:var(--blue)}\n.row.tool-call .kind{color:var(--yellow)}\n.row.turn-end .kind{color:var(--purple)}\n.row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}\n.row.text .body{font-family:inherit}\n.row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}\n.status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}\n.status-badge.pending{background:var(--bg3);color:var(--text2)}\n.status-badge.ok{background:#0f3d20;color:var(--green)}\n.status-badge.error{background:#3d1418;color:var(--red)}\n.row details{margin-top:6px}\n.row summary{cursor:pointer;font-size:11px;color:var(--text2)}\n.row pre{margin-top:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-word;color:var(--text2);background:var(--bg);border-radius:5px;padding:6px 8px;max-height:200px;overflow:auto}\n.chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}\n#timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"tree-pane\">\n <div id=\"tree-head\">Sessions</div>\n <div id=\"tree-body\"><div id=\"tree-empty\">Loading…</div></div>\n </div>\n <div id=\"timeline-pane\">\n <div id=\"timeline-head\">\n <span class=\"focus-id\" id=\"focus-id-label\">—</span>\n <select id=\"head-selector\" title=\"Sessions\"></select>\n <span id=\"head-summary\"></span>\n <span id=\"status-line\">connecting…</span>\n </div>\n <div id=\"timeline-body\"><div id=\"timeline-empty\">No session focused yet.</div></div>\n <button id=\"new-pill\" type=\"button\">↓ New messages</button>\n </div>\n</div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-live-session\")}\n\n// ============================================================\n// INLINED REDUCER COPY — hand-kept mirror of live-session/logic.ts.\n// Plain JS, same semantics: coalesce consecutive text-delta of the same\n// session (and rejoin an unterminated mid-line fragment split by an\n// interleaved record — see the TS module's text-delta arm), pair\n// tool-call/tool-result by toolCallId, pass through turn-end, keep usage\n// as STATE (SPEC §1: usage leaves the timeline — a usage_update record\n// never produces a row), ignore unknown kinds. Keep in sync with the TS\n// module; the TS module is the one the test suite imports.\n// ============================================================\n\nfunction initialTimelineState() {\n return { rows: [], usage: null };\n}\n\nfunction rowId(record, rows) {\n return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);\n}\n\n// Fold a text-delta record into an existing row (fresh object), keeping the\n// row's \"partial\" hint in step with the latest record — see mergeTextDelta in\n// the TS module.\nfunction mergeTextDelta(row, record) {\n var merged = Object.assign({}, row, {\n text: row.text + (record.text || ''),\n seq: record.seq,\n ts: record.ts,\n });\n if (record.partial === true) merged.partial = true;\n else delete merged.partial;\n return merged;\n}\n\nfunction reduceEvent(state, record) {\n switch (record.kind) {\n case 'text-delta': {\n var last = state.rows[state.rows.length - 1];\n if (last && last.kind === 'text' && last.sessionId === record.sessionId) {\n return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };\n }\n // Debounce can flush an unterminated mid-word fragment (flagged\n // partial), let a tool-call land, then flush the continuation — look\n // back within the same turn (bounded by this session's last turn-end)\n // for that session's most recent text row and continue it in place.\n // Only the explicit partial flag glues: a non-partial record with no\n // trailing newline is the writer's normal end-of-text-block shape.\n for (var i = state.rows.length - 1; i >= 0; i--) {\n var prior = state.rows[i];\n if (prior.sessionId !== record.sessionId) continue;\n if (prior.kind === 'turn-end') break;\n if (prior.kind !== 'text') continue;\n if (prior.partial === true) {\n var patched = state.rows.slice();\n patched[i] = mergeTextDelta(prior, record);\n return { rows: patched, usage: state.usage };\n }\n break;\n }\n var row = {\n kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, text: record.text || '',\n };\n if (record.partial === true) row.partial = true;\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-call': {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '',\n toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-result': {\n var idx = -1;\n for (var i = 0; i < state.rows.length; i++) {\n if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;\n }\n if (idx === -1) {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',\n status: record.isError ? 'error' : 'ok', result: record.result,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n var updated = Object.assign({}, state.rows[idx], {\n status: record.isError ? 'error' : 'ok', result: record.result,\n });\n var rows = state.rows.slice();\n rows[idx] = updated;\n return { rows: rows, usage: state.usage };\n }\n case 'turn-end': {\n var row = {\n kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, reason: record.reason,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'usage_update': {\n // Usage is state, not a row (SPEC §1) — last-write-wins, no merge with\n // the prior snapshot. state.rows is reused as-is (it didn't change).\n // Except the context window: a cost-bearing frame's size sticks (see\n // the TS module's usage_update arm).\n var sizeReported = typeof record.size === 'number' && record.size > 0;\n var authoritative = record.cost !== undefined && sizeReported;\n var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);\n var keepSize = !authoritative && (prevAuth || !sizeReported);\n var usage = {\n size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,\n used: record.used, cost: record.cost,\n tokensIn: record.tokensIn, tokensOut: record.tokensOut,\n seq: record.seq, ts: record.ts,\n };\n if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;\n return { rows: state.rows, usage: usage };\n }\n default:\n return state;\n }\n}\n\n// ============================================================\n// INLINED PURE HELPERS — exact copies of live-session/logic.ts's\n// isNearBottom (SPEC §2) and groupAdjacentToolCalls (SPEC §3). Same names,\n// same signatures, same default thresholds; plain JS because the widget\n// has no import step.\n// ============================================================\n\nvar SCROLL_STICK_THRESHOLD_PX = 24;\n\nfunction isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {\n if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;\n return scrollHeight - scrollTop - clientHeight <= threshold;\n}\n\nvar TOOL_CALL_GROUP_THRESHOLD = 2;\n\n// Collapse runs of \\`threshold\\`+ adjacent tool-call rows into one\n// {kind:'tool-group', rows:[...]} entry; everything else passes through as\n// individual {kind:'row', row} entries, same order as the input.\nfunction groupAdjacentToolCalls(rows, threshold) {\n if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;\n var out = [];\n var run = [];\n function flushRun() {\n if (!run.length) return;\n if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });\n else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });\n run = [];\n }\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }\n flushRun();\n out.push({ kind: 'row', row: rows[i] });\n }\n flushRun();\n return out;\n}\n\n// ============================================================\n// Rendering helpers\n// ============================================================\n\nfunction escHtml(s) {\n return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n}\n\nfunction safeJson(v) {\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n}\n\nfunction statusDotClass(status) {\n if (status === 'running' || status === 'starting') return 'running';\n if (status === 'error' || status === 'killed') return 'error';\n return 'grey';\n}\n\n// SPEC §4 (non-contract guidance): 52524 → \"52.5k\", >=1e6 → \"M\" suffix,\n// one decimal, trailing .0 vanishes because the value stays a number.\n// cost → \"$\" + toFixed(2). Read from timelineState.usage.\nfunction fmtCompactNum(n) {\n if (typeof n !== 'number' || !isFinite(n)) return null;\n if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';\n if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';\n return String(n);\n}\n\nfunction usageChipText(usage) {\n if (!usage) return '';\n var bits = [];\n if (usage.used != null && usage.size != null) {\n bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));\n } else if (usage.used != null) {\n bits.push(fmtCompactNum(usage.used));\n }\n if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));\n return bits.join(' · ');\n}\n\n// WP5: elapsed seconds since the timeline's first row (running only).\nfunction elapsedText(rows, status) {\n if (status !== 'running' && status !== 'starting') return null;\n if (!rows.length || rows[0].ts == null) return null;\n var ms = new Date(rows[0].ts).getTime();\n if (!isFinite(ms)) return null;\n return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';\n}\n\n// ============================================================\n// LEFT pane — live tree\n// ============================================================\n\nvar currentTree = [];\nvar focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;\n// Where the current focus came from: 'pinned' (the tool result that mounted\n// this widget named a session — never auto-switched away), 'auto' (self-\n// discovered newest running session, a guess the host may still correct),\n// 'user' (explicit click/selector choice — outranks everything).\nvar focusSource = focusId ? 'pinned' : null;\nvar bootDone = false;\nvar treeTimer = null;\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId —\n// keep in sync (same convention as the reducer copy below).\nfunction extractToolResultSessionId(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try {\n var body = JSON.parse(item.text);\n if (body && typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (body && typeof body.id === 'string' && body.id) return body.id;\n } catch (_) {}\n return null;\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session instead of leaving\n// pollTree() to self-discover the newest running one, which made every\n// agent_start card show the latest started session rather than its own.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var id = extractToolResultSessionId(params);\n if (!id || focusSource === 'user' || id === focusId) return;\n focusSource = 'pinned';\n if (bootDone) setFocus(id);\n else focusId = id; // boot's own startTimeline(focusId) picks it up\n});\n\nfunction findNode(nodes, id) {\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].id === id) return nodes[i];\n var found = findNode(nodes[i].children || [], id);\n if (found) return found;\n }\n return null;\n}\n\nfunction flattenDfs(nodes, out) {\n out = out || [];\n for (var i = 0; i < nodes.length; i++) {\n out.push(nodes[i]);\n flattenDfs(nodes[i].children || [], out);\n }\n return out;\n}\n\nfunction pickInitialFocus(tree) {\n var flat = flattenDfs(tree);\n var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });\n if (alive.length) return alive[alive.length - 1].id;\n return tree.length ? tree[0].id : null;\n}\n\nfunction renderTreeNode(node, depth) {\n var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');\n var badge = node.isOrchestrator ? '<span class=\"badge\">orch</span>' : '';\n var cls = 'tnode' + (node.id === focusId ? ' focus' : '');\n return '<div class=\"' + cls + '\" data-id=\"' + escHtml(node.id) + '\">' +\n '<span class=\"dot ' + statusDotClass(node.status) + '\"></span>' +\n '<span class=\"label\">' + escHtml(node.label || node.id) + '</span>' + badge +\n '</div>' +\n (childrenHtml ? '<div class=\"tchildren\">' + childrenHtml + '</div>' : '');\n}\n\n// WP4: compact header <select> mirrors the tree; still routes through\n// setFocus() — the only session-switch entry point.\nfunction renderHeadSelector() {\n var sel = document.getElementById('head-selector');\n var flat = flattenDfs(currentTree);\n while (sel.firstChild) sel.removeChild(sel.firstChild);\n if (!flat.length) return;\n for (var i = 0; i < flat.length; i++) {\n var n = flat[i];\n var o = document.createElement('option');\n o.value = n.id;\n o.textContent = (n.id === focusId ? '● ' : '') + (n.label || n.id);\n sel.appendChild(o);\n }\n sel.value = focusId || '';\n}\n\nfunction renderTree() {\n var body = document.getElementById('tree-body');\n var root = focusId ? findNode(currentTree, focusId) : null;\n var renderNodes = root ? [root] : currentTree;\n if (!renderNodes.length) {\n body.innerHTML = '<div id=\"tree-empty\">No sessions.</div>';\n renderHeadSelector();\n return;\n }\n body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');\n var els = body.querySelectorAll('.tnode');\n els.forEach(function(el) {\n el.addEventListener('click', function() {\n var id = el.getAttribute('data-id');\n if (id === focusId) return;\n focusSource = 'user';\n setFocus(id);\n });\n });\n renderHeadSelector();\n}\n\nfunction pollTree() {\n callTool('app_session_tree', {}).then(function(res) {\n currentTree = res.tree || [];\n if (!focusId) {\n focusId = pickInitialFocus(currentTree);\n if (focusId) { focusSource = 'auto'; startTimeline(focusId); }\n }\n renderTree();\n updateHeader();\n }).catch(function() {\n // Leave the last-known tree rendered; the next poll may recover.\n });\n}\n\n// ============================================================\n// RIGHT pane — timeline for focusId\n// ============================================================\n\nvar timelineState = initialTimelineState();\nvar sincePtr = 0;\nvar activeSource = null; // {type:'sse', es} | {type:'poll', timer}\nvar compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)\n\nfunction setStatus(msg) {\n document.getElementById('status-line').textContent = msg;\n}\n\nfunction isCompact() {\n return (getHostContext() && getHostContext().displayMode === 'inline') ||\n (typeof window !== 'undefined' && window.innerWidth < 640);\n}\n\n// WP3/WP4: apply the compact/expanded split. Only re-renders the timeline\n// when the mode actually flips (scroll + <details> state survive otherwise).\nfunction applyDisplayMode() {\n var c = isCompact();\n document.body.classList.toggle('compact-mode', c);\n if (c !== compactMode) {\n compactMode = c;\n renderTimelineFull();\n renderTree();\n }\n}\n\n// WP5 + WP2: one header line — \\`● running · 3 tools · 52.5k/200k · $0.04 · 12s\\`\n// (status dot from the focus node, tool count from the rows, usage from\n// timelineState.usage — no usage_update row anymore) + transport status.\nfunction updateHeader() {\n document.getElementById('focus-id-label').textContent = focusId || '—';\n var rows = timelineState.rows || [];\n var usage = timelineState.usage;\n var tools = 0;\n for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;\n var node = focusId ? findNode(currentTree, focusId) : null;\n var st = node ? node.status : null;\n var parts = [];\n if (st) parts.push('<span class=\"sdot ' + statusDotClass(st) + '\"></span>' + escHtml(st));\n if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));\n var chip = usageChipText(usage);\n if (chip) parts.push('<span id=\"usage-chip\" class=\"chip\">' + escHtml(chip) + '</span>');\n var el = elapsedText(rows, st);\n if (el) parts.push(escHtml(el));\n document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' · ') : '';\n}\n\nfunction captureDetailsOpenFlags(body) {\n var dets = body.querySelectorAll('details');\n var flags = [];\n for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);\n return flags;\n}\n\nfunction restoreDetailsOpenFlags(body, flags) {\n var dets = body.querySelectorAll('details');\n for (var i = 0; i < dets.length && i < flags.length; i++) {\n if (flags[i]) dets[i].open = true;\n }\n}\n\n// Full rebuild — used ONLY for the first paint of a session, a setFocus()\n// session switch (SPEC §2: full reset is correct there), a display-mode\n// flip, and as the fallback for shapes the incremental patcher can't patch.\n// A display-mode flip re-renders the SAME content (WP3: \"réactif... sans\n// perdre le scroll ni l'état des <details>\"), so open <details> and the\n// exact scroll offset (not just the at-bottom decision) survive the rebuild.\nfunction renderTimelineFull() {\n var body = document.getElementById('timeline-body');\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var savedScrollTop = body.scrollTop;\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;\n updateHeader();\n}\n\nfunction buildTimelineHtml() {\n var rows = timelineState.rows;\n if (!rows.length) return '<div id=\"timeline-empty\">No events yet.</div>';\n if (compactMode) {\n var entries = groupAdjacentToolCalls(rows);\n var html = '';\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i];\n if (e.kind === 'tool-group') {\n html += '<details class=\"tool-group\"><summary>▸ ' + e.rows.length + ' tool calls</summary>' +\n e.rows.map(renderRow).join('') + '</details>';\n } else {\n html += renderRow(e.row);\n }\n }\n return html;\n }\n return rows.map(renderRow).join('');\n}\n\n// ── WP1 incremental patch (dense mode) ─────────────────────────────────\n// One record, one minimal DOM mutation. The common case (text-delta merged\n// into the LAST rendered row) patches just that row's text node. Anything\n// else appends via insertAdjacentHTML. Only rare in-place replacements\n// (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.\nfunction applyRecordToDom(prevRows) {\n var body = document.getElementById('timeline-body');\n // Capture \"stuck to bottom?\" BEFORE any mutation (SPEC §2).\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var rows = timelineState.rows;\n\n if (compactMode) {\n // Inline mode: smaller rows — full grouped rebuild, preserving\n // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM\n // (and the scroll/pill decision) when the rows actually changed — a\n // usage_update leaves rows === prevRows, so it must fall through to the\n // header-only path below, same as the dense-mode branch.\n if (rows !== prevRows) {\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n }\n updateHeader();\n return;\n }\n\n var appended = rows.length > prevRows.length;\n if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {\n // First real paint of this session (empty placeholder present) or an\n // unexpected shape — full reset is correct/cheapest.\n body.innerHTML = buildTimelineHtml();\n body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n\n if (appended) {\n body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));\n } else if (rows.length === prevRows.length && rows.length > 0) {\n // Exactly one row object replaced (tool-result merge, partial-text\n // re-merge via the reducer's lookback). Patch that one element only.\n // Scan from the tail: the hot path (a text-delta merged into the LAST\n // row, the dominant streaming case) is found in O(1) this way instead\n // of walking the whole array — the rare mid-array lookback re-merge\n // still resolves correctly, just slower.\n var idx = -1;\n for (var i = rows.length - 1; i >= 0; i--) {\n if (rows[i] !== prevRows[i]) { idx = i; break; }\n }\n var patched = false;\n if (idx >= 0) {\n var row = rows[idx];\n var el = body.querySelector('[data-row-id=\"' + escHtml(row.id) + '\"]');\n if (el) {\n if (row.kind === 'text' && idx === rows.length - 1) {\n // High-frequency case: patch the existing text node in place.\n var b = el.querySelector('.body');\n if (b) { b.textContent = row.text; patched = true; }\n }\n if (!patched) { el.outerHTML = renderRow(row); patched = true; }\n }\n }\n if (!patched) {\n // Multiple rows changed at once (shouldn't happen per-record) — rebuild.\n body.innerHTML = buildTimelineHtml();\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n } else {\n // rows unchanged (usage_update) — header only.\n updateHeader();\n return;\n }\n\n // Scroll decision AFTER the mutation: stick or leave the position alone.\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n updateHeader();\n}\n\nfunction samePrefix(prevRows, rows) {\n for (var i = 0; i < prevRows.length; i++) {\n if (prevRows[i] !== rows[i]) return false;\n }\n return true;\n}\n\nfunction showNewPill() {\n document.getElementById('new-pill').classList.add('show');\n}\n\nfunction hideNewPill() {\n document.getElementById('new-pill').classList.remove('show');\n}\n\nfunction renderRow(row) {\n if (row.kind === 'text') {\n return '<div class=\"row text\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">text</span></div>' +\n '<div class=\"body\">' + escHtml(row.text) + '</div></div>';\n }\n if (row.kind === 'tool-call') {\n var badgeCls = row.status === 'pending' ? 'pending' : row.status;\n var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');\n var resultHtml = row.status !== 'pending'\n ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'\n : '';\n return '<div class=\"row tool-call\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">tool</span>' +\n '<span class=\"toolname\">' + escHtml(row.toolName) + '</span>' +\n '<span class=\"status-badge ' + badgeCls + '\">' + badgeText + '</span></div>' +\n '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +\n resultHtml + '</div>';\n }\n if (row.kind === 'turn-end') {\n return '<div class=\"row turn-end\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">turn end</span>' +\n '<span class=\"chip\">' + escHtml(row.reason || '—') + '</span></div></div>';\n }\n return '';\n}\n\nfunction teardownTimeline() {\n if (activeSource) {\n if (activeSource.type === 'sse' && activeSource.es) {\n try { activeSource.es.close(); } catch (e) {}\n }\n // Both the poll re-arm timer AND the SSE open/first-message fallback\n // timer must be cleared, or a focus switch within the 2.5s window leaves\n // a stale timer that fires startPolling for the abandoned session.\n if (activeSource.timer) clearTimeout(activeSource.timer);\n if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);\n }\n activeSource = null;\n}\n\nfunction attemptSSE(id) {\n var settled = false;\n var es;\n try {\n es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');\n } catch (e) {\n startPolling(id);\n return;\n }\n var src = { type: 'sse', es: es, fallbackTimer: null };\n activeSource = src;\n // Every callback below re-checks focusId===id AND that src is still the\n // live source: a focus switch tears down src and starts a new one, so a\n // late open/message/error from this abandoned EventSource must be a no-op\n // rather than mutating the new focus's timeline or clobbering activeSource.\n function stale() { return activeSource !== src || focusId !== id; }\n src.fallbackTimer = setTimeout(function() {\n if (settled || stale()) return;\n settled = true;\n try { es.close(); } catch (e) {}\n startPolling(id);\n }, 2500);\n es.addEventListener('open', function() {\n if (settled || stale()) return;\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n });\n es.onmessage = function(evt) {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n }\n var rec;\n try { rec = JSON.parse(evt.data); } catch (e) { return; }\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, rec);\n if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n };\n es.onerror = function() {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n try { es.close(); } catch (e) {}\n startPolling(id);\n } else {\n // Do not leave EventSource to perform its native reconnect: this\n // endpoint replays from since=0 when no cursor is provided, so a\n // transparent reconnect would duplicate every row already reduced.\n // The bridge poll resumes from sincePtr instead and preserves the\n // exactly-once cursor contract.\n try { es.close(); } catch (e) {}\n startPolling(id);\n }\n };\n}\n\nfunction startPolling(id) {\n activeSource = { type: 'poll', timer: null };\n setStatus('polling');\n function tick() {\n if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;\n callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {\n var events = res.events || [];\n for (var i = 0; i < events.length; i++) {\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, events[i]);\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n }\n if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;\n setStatus('polling');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n }).catch(function() {\n setStatus('disconnected');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n });\n }\n tick();\n}\n\nfunction startTimeline(id) {\n teardownTimeline();\n timelineState = initialTimelineState();\n sincePtr = 0;\n hideNewPill();\n renderTimelineFull();\n setStatus('connecting…');\n attemptSSE(id);\n}\n\nfunction setFocus(id) {\n focusId = id;\n renderTree();\n startTimeline(id);\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n var init = window.__APP_INIT__ || {};\n if (init.httpBaseUrl) return init;\n // Static ui:// resources render once at server-registration time with\n // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) — fall back to\n // calling the tool ourselves over the bridge, same as the other panels.\n return callTool('live_session', {}).then(function(result) {\n window.__APP_INIT__ = result;\n return result;\n });\n}).then(function(init) {\n // Only override the focus when live_session itself named a session AND a\n // tool-result notification didn't already pin one while the fallback\n // tools/call above was in flight — the host's push names the exact\n // session this widget was mounted for, so it outranks the fallback's\n // (argument-less, hence session-less in practice) self-call.\n if (init.sessionId && focusSource !== 'pinned') {\n focusId = init.sessionId;\n focusSource = 'pinned';\n }\n onHostContext(function() {\n applyDisplayMode();\n updateHeader();\n });\n document.getElementById('head-selector').addEventListener('change', function(evt) {\n var id = evt.target.value;\n if (id && id !== focusId) { focusSource = 'user'; setFocus(id); }\n });\n document.getElementById('new-pill').addEventListener('click', function() {\n var body = document.getElementById('timeline-body');\n body.scrollTop = body.scrollHeight;\n hideNewPill();\n });\n document.getElementById('timeline-body').addEventListener('scroll', function() {\n // User scrolled back near the bottom — the pill is stale, hide it.\n var el = document.getElementById('timeline-body');\n if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();\n });\n window.addEventListener('resize', function() {\n var c = isCompact();\n if (c !== compactMode) { applyDisplayMode(); }\n });\n applyDisplayMode();\n bootDone = true;\n pollTree();\n treeTimer = setInterval(pollTree, 2000);\n setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh\n if (focusId) startTimeline(focusId);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n});\n</script>\n</body>\n</html>`\n}\n","/**\n * McpApp definition for the live-session widget — a two-pane panel (left:\n * live session tree, right: streaming timeline) that lets a host attach to\n * any running agent session and watch it work in real time.\n *\n * The HTML bundle itself (`LIVE_SESSION_HTML`, its wire protocol, the\n * inlined timeline-reducer copy, compact-mode rendering, …) lives in\n * ./panel.ts, which imports nothing beyond plain TS — see that file for the\n * full protocol/rendering doc. Kept separate so a consumer that only needs\n * the HTML can import `@agentproto/apps/live-session/panel` without pulling\n * in `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from this file would.\n *\n * This module also exports `liveSessionApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * Its `ui.html` is a static snapshot (`LIVE_SESSION_HTML` called with the\n * default `httpBaseUrl`) since `AppUiDefinition.ui.html` must be a plain\n * string; it's separate from `makeLiveSessionApp` above, which the daemon\n * mounts directly at boot with the real daemon origin baked in per-call.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\nimport { LIVE_SESSION_HTML, type LiveSessionOutput } from \"./panel.js\"\n\nexport { LIVE_SESSION_HTML }\nexport type { LiveSessionOutput }\n\nexport const liveSessionInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Attach the widget to an existing session by id or name. Omit to \" +\n \"self-discover the newest running session from the tree.\",\n ),\n})\n\nexport type LiveSessionInput = z.infer<typeof liveSessionInputSchema>\n\nexport interface LiveSessionOps {\n /** The daemon's own HTTP origin, e.g. \"http://127.0.0.1:18790\". Defaults\n * to the daemon's documented default port (`index.ts:776`) so the app\n * works out of the box in the common single-daemon case. */\n httpBaseUrl?: string\n}\n\n/**\n * Factory: close over the daemon's own HTTP origin so execute() needs\n * nothing beyond the tool input (no registry access — mirrors\n * sessions-panel/index.ts and runtime's terminal-panel-app.ts; the app_*\n * tools this widget calls over the bridge are what actually touch the\n * registry).\n */\nexport function makeLiveSessionApp(\n ops?: LiveSessionOps,\n): AgnoMcpApp<LiveSessionInput, LiveSessionOutput> {\n const httpBaseUrl = ops?.httpBaseUrl ?? \"http://127.0.0.1:18790\"\n // `new URL(...).origin` normalises the base URL to the exact connectDomains\n // entry the CSP allowlist needs (same derivation as terminal-panel-app.ts's\n // wsOrigin), covering both the SSE EventSource and any bridge fallback.\n const httpOrigin = new URL(httpBaseUrl).origin\n\n return {\n id: \"live_session\",\n title: \"Live Session\",\n description:\n \"Open the live session widget — a two-pane view of a running agent \" +\n \"session: a live tree on the left, a streaming timeline (text, tool \" +\n \"calls/results, turn-end) on the right. Omit `sessionId` to \" +\n \"attach to the newest running session; pass one to attach directly.\",\n inputSchema: liveSessionInputSchema,\n execute: async input => ({\n sessionId: input.sessionId,\n httpBaseUrl,\n }),\n html: (initData: LiveSessionOutput) => LIVE_SESSION_HTML(initData),\n csp: { connectDomains: [httpOrigin] },\n }\n}\n\nconst DEFAULT_HTTP_BASE_URL = \"http://127.0.0.1:18790\"\n\nexport const liveSessionApp: AppHandle = defineApp({\n id: \"@agentproto/live-session\",\n name: \"Live Session\",\n description:\n \"Open the live session widget — a two-pane view of a running agent session: a live tree on \" +\n \"the left, a streaming timeline (text, tool calls/results, turn-end) on the right.\",\n agents: [],\n ui: {\n html: LIVE_SESSION_HTML({ httpBaseUrl: DEFAULT_HTTP_BASE_URL }),\n title: \"Live Session\",\n tools: [\"app_session_tree\", \"app_session_events\", \"live_session\"],\n },\n})\n","/**\n * Blob pass-through for the session-chat widget (see ./panel.ts `mountBlob`).\n *\n * Claude Desktop and Codex apply their own CSP to an MCP-Apps widget frame:\n * `frame-src 'self' blob: data:`. Neither merges the resource's declared\n * `_meta.ui.csp.frameDomains` into it (measured 2026-09-18), so a direct\n * `<iframe src=\"http://127.0.0.1:18790/apps/@agentik/session-chat/ui?…\">` is\n * refused host-side before the request ever reaches the daemon. `blob:` IS\n * on that list — so the panel fetches the chat html itself (a fetch, not a\n * frame navigation; the per-boot `?et=` embed token gets it through the\n * daemon's origin gates and PNA preflight), rewrites it with the function\n * below, and mounts the result as a `blob:` document.\n *\n * What the rewrite has to fix, given that the document no longer lives on\n * the daemon origin:\n *\n * 1. `<base href=\"<deep link>\">` right after `<head>`, so every relative\n * url in the app (the injected standalone bridge's `./tool-call`, any\n * relative asset) resolves exactly as it would have on the daemon.\n * NOTE: the base is the document url itself (`…/session-chat/ui?…`),\n * not `…/ui/` — a trailing slash would add a path segment and make\n * `./tool-call` resolve to `…/ui/tool-call`, which routes nowhere.\n * 2. The blob document's origin is opaque (Claude's widget context is a\n * sandboxed frame; the blob inherits that) — every daemon request it\n * makes carries `Origin: null`, which the daemon's browser gates\n * (`guardBrowserOrigin`, `authorizeMcp`, `checkSessionsToken`) refuse\n * unless the request ALSO carries the embed token. The chat UI talks to\n * the daemon with absolute urls (`${DAEMON_URL}/mcp`, `/sessions/*`,\n * its SSE stream via `fetch`, `/sessions/:id/chat`) that we don't\n * control, so a small shim is injected ahead of every app script: it\n * wraps `window.fetch` / `window.EventSource` to append `et=` to any\n * request aimed at the daemon origin, and points\n * `window.__AGENTPROTO_BASEURL__` at the daemon origin the deep link\n * names (so a non-default port works too).\n * 3. The standalone bridge's `fetch(\"./tool-call\"` (runtime\n * app-ui-apps.ts `STANDALONE_REST_BRIDGE_SCRIPT`) is additionally\n * rewritten to the absolute tokened url — belt and braces on top of\n * the shim, and the one rewrite that is exact-string-testable.\n * 4. The shim's FIRST statement posts `{ type: \"agentproto-blob-boot\" }`\n * to its parent. A blob document inherits its creator's CSP, so if the\n * host's widget CSP ever kills inline scripts, that ack never arrives\n * and the panel tears the blob down and falls back to the direct frame\n * + launcher card (panel.ts `armBlobBootProbe`) instead of leaving a\n * silent blank. No CSP `<meta>` is added to the blob html: the\n * creator's CSP wins regardless, it would only be noise.\n *\n * Shipped as a JS string (same shape as ../panel-bridge.ts) so the panel\n * page embeds the exact code the test evaluates — no TS twin to drift from.\n */\n\n/** `postMessage` type the injected shim sends its parent on boot. */\nexport const BLOB_BOOT_MESSAGE_TYPE = \"agentproto-blob-boot\"\n\nexport function blobEmbedScript(): string {\n return `// ── Blob pass-through transform (shared: session-chat/blob-embed.ts) ──\nfunction _blobEscapeAttr(s){ return String(s).replace(/&/g, '&').replace(/\"/g, '"'); }\n// Rewrite the fetched chat html so it works as a blob: document: <base> on\n// the deep link, a fetch/EventSource shim that carries the embed token to\n// the daemon origin, and the standalone bridge's ./tool-call made absolute.\nfunction transformChatHtmlForBlob(html, url, token){\n var origin = new URL(url).origin;\n var tc = new URL('./tool-call', url);\n tc.searchParams.set('et', token);\n // The app-visible query for the blob doc: the deep link's search minus the\n // embed token (the token must not ride the app's visible params). The\n // blob document's own location has no params, so this is the ONLY channel\n // for ?session= / ?embed=1 — the app reads it via bootSearchParams().\n var bootUrl = new URL(url);\n bootUrl.searchParams.delete('et');\n var bootQuery = bootUrl.search;\n var shim = '<script>(function(){'\n + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, \"*\"); } catch (_) {}'\n + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'\n + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'\n + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'\n + 'function _sameOrLoc(u){'\n + 'try {'\n + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'\n + 'return x.origin === window.location.origin ? u : window.location.href;'\n + '} catch (_) { return window.location.href; }'\n + '}'\n + '_h.replaceState = function(s, t, u){'\n + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + '_h.pushState = function(s, t, u){'\n + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'\n + 'function withEt(u){'\n + 'try {'\n + 'var x = new URL(String(u), document.baseURI);'\n + 'if (x.origin !== ORIGIN) return null;'\n + 'if (!x.searchParams.has(\"et\")) x.searchParams.set(\"et\", TOKEN);'\n + 'return x.href;'\n + '} catch (_) { return null; }'\n + '}'\n + 'var _fetch = window.fetch;'\n + 'if (typeof _fetch === \"function\") {'\n + 'window.fetch = function(input, init){'\n + 'var r;'\n + 'if (typeof Request !== \"undefined\" && input instanceof Request) {'\n + 'r = withEt(input.url);'\n + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'\n + '} else {'\n + 'r = withEt(input);'\n + 'if (r) input = r;'\n + '}'\n + 'return _fetch.call(window, input, init);'\n + '};'\n + '}'\n + 'var _ES = window.EventSource;'\n + 'if (typeof _ES === \"function\") {'\n + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'\n + 'window.EventSource.prototype = _ES.prototype;'\n + '}'\n + '})();<\\\\/script>';\n var head = '<base href=\"' + _blobEscapeAttr(url) + '\">' + shim;\n var m = html.match(/<head[^>]*>/i);\n var out = m\n ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)\n : head + html;\n return out.split('fetch(\"./tool-call\"').join('fetch(' + JSON.stringify(tc.href));\n}`\n}\n","/**\n * HTML bundles for the session-chat builtin widget — the thin bridge page\n * (`sessionChatEmbedHtml`) and the not-installed notice\n * (`SESSION_CHAT_FALLBACK_HTML`) served as the\n * `ui://agentproto_session_chat/view` resource by `registerMcpApps` (see\n * ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via the\n * `agentproto_session_chat` tool defined in ./index.ts, AND bound to\n * `agent_start` (runtime agent-tools.ts `_meta.ui.resourceUri`) so a launch\n * card opens the chat of the session it just spawned.\n *\n * Deliberately NOT a chat UI reimplementation: the real chat lives in the\n * installed `@agentik/session-chat` app, served by the daemon's standalone\n * app host at `GET /apps/@agentik/session-chat/ui`. When the app is\n * installed, this page is a full-viewport iframe of that URL deep-linked\n * with `?session=<id>&embed=1` — the exact spelling the daemon's standalone\n * route relaxes its `frame-ancestors 'none'` for (see runtime\n * http-server.ts handleAppUiPage) and the VS Code chat-panel webview\n * already uses. No second chat bundle exists anywhere in agentproto/ts.\n *\n * Self-bootstrapping (same pattern as live-session/panel.ts): the static\n * ui:// resource is rendered ONCE at server-registration time with EMPTY\n * initData (mcp-apps-adapter.ts registerMcpApps), so the deep link can't\n * be baked in. The page resolves it at runtime, in priority order:\n * 1. `initData.url` when the html was rendered with a real tool output\n * (tests, and any host that re-renders per call) — iframe is inline.\n * 2. The host's `ui/notifications/tool-result` push of the tool call that\n * mounted the widget. An `agent_start` result IS the spawned session's\n * descriptor (`{ id: \"sess_…\", … }`) → the widget calls\n * `agentproto_session_chat({ sessionId })` over the bridge to turn that\n * id into the deep link; an `agentproto_session_chat` result already\n * carries `{ installed, url }` and mounts directly.\n * 3. Otherwise (opened bare from the catalog) `agentproto_session_chat({})`\n * → the app's session picker.\n * Both orders of (2) vs. bridge init are handled: a result that lands\n * before `initBridge()` resolves is held and applied once it does.\n *\n * Mount order (see `mount()` below):\n * 1. Blob pass-through, when a per-boot embed token is baked in (i.e. an\n * MCP-Apps host rendered this resource): fetch the chat html with the\n * token, rewrite it (./blob-embed.ts `transformChatHtmlForBlob`) and\n * mount it as a `blob:` document. Claude Desktop / Codex widget frames\n * run `frame-src 'self' blob: data:` and do not merge our declared\n * `csp.frameDomains`, so a direct daemon iframe is refused host-side —\n * `blob:` is the pass-through. The blob document must post a boot ack\n * within a settle window (`armBlobBootProbe`); a missing ack (host CSP\n * killed its inline scripts, or refused the blob frame) tears it down\n * and falls through to 2.\n * 2. Direct-src iframe of the deep link (VS Code HTTP-iframe panels, a\n * standalone tab — no token, and their origin already passes the\n * daemon's checks; or the blob path's fallback). If the host refuses\n * the frame, `armBlockProbe` removes it and the launcher card becomes\n * the interactive surface.\n *\n * A persistent header with the deep link stays visible above the iframe, so\n * if the frame is refused (older daemon without the embed relaxation, or a\n * host whose own CSP blocks daemon frames) the user still has a one-click\n * path to the chat in a tab. When the app is NOT installed the page is a\n * small readable notice pointing at `agentproto app install` — the\n * fallback branch, never a vendored UI.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\nimport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\nimport type { SessionChatOutput } from \"./index.js\"\n\n/** Only `&` and `\"` are significant inside a double-quoted attribute value. */\nfunction escapeAttr(text: string): string {\n return text.replace(/&/g, \"&\").replace(/\"/g, \""\")\n}\n\nexport function sessionChatEmbedHtml(initData: Partial<SessionChatOutput>): string {\n const url = typeof initData.url === \"string\" && initData.url ? initData.url : null\n const notInstalled = initData.installed === false\n const storeUrl =\n typeof initData.storeUrl === \"string\" && initData.storeUrl\n ? initData.storeUrl\n : \"/store?install=%40agentik%2Fsession-chat\"\n const link =\n url != null\n ? `<a href=\"${escapeAttr(url)}\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>`\n : notInstalled\n ? \"app not installed\"\n : \"connecting…\"\n const iframe =\n url != null\n // No inline src: the url mounts through the bridge script below, via\n // withEmbedToken(), so the frame never loads without the per-boot\n // embed proof — whichever render path (static resource or a host\n // re-render with a real initData) produced it.\n ? `<iframe id=\"chat\" title=\"Session Chat\"></iframe>`\n : \"\"\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}\n#bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}\n#bar a{color:#58a6ff}\n#mode{color:#6e7681}\n#stage{position:relative;height:calc(100% - 29px)}\n#chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}\n#card{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:24px}\n#card h2{font-size:15px;font-weight:600}\n#card .sid{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#8b949e;background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 8px}\n#card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}\n#card a{display:inline-block;margin-top:2px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:13px;padding:7px 14px;border-radius:6px;text-decoration:none}\n#card a:hover{border-color:#58a6ff}\n#notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\n#notice.show{display:block}\n#notice h1{font-size:18px;margin-bottom:12px}\n#notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\n#notice p{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"bar\">Session Chat · <span id=\"link\">${link}</span><span id=\"mode\"></span></div>\n<div id=\"stage\">${iframe}<div id=\"card\">\n <h2>Session Chat</h2>\n <div id=\"card-session\" class=\"sid\">—</div>\n <p>Full embed isn't supported by this host — the chat opens in a browser tab.</p>\n <a id=\"card-open\" href=\"#\" target=\"_blank\" rel=\"noreferrer\">Open chat</a>\n</div><div id=\"notice\"${notInstalled ? ' class=\"show\"' : \"\"}>\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"${escapeAttr(storeUrl)}\" target=\"_blank\" rel=\"noreferrer\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n</div></div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-session-chat\")}\n\n${blobEmbedScript()}\n\n// ── Session-chat launcher logic ──────────────────────────────────────────\nvar mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;\nvar pinnedSessionId = null; // session named by the host's tool-result push\nvar pendingUrl = null; // url pushed by the host before the bridge was up\nvar pendingNotInstalled = false;\nvar bridged = false;\nvar cardUrl = null; // deep link once resolved (drives the card button)\nvar cardSessionEl = document.getElementById('card-session');\nvar cardOpenEl = document.getElementById('card-open');\nvar mountGen = 0; // bumps per mount(); async work checks it before touching the DOM\nvar blobUrl = null; // live object url of the blob-mounted chat, if any\nvar blobBootTimer = null;\nvar blobBootOk = false;\n\nfunction escapeHtml(text) {\n return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/\"/g, '"');\n}\n\nfunction setMode(text) {\n document.getElementById('mode').textContent = text ? ' \\\\u00b7 ' + text : '';\n // The chat app is live inside the frame — the launcher bar (title, deep\n // link, mount mode) is noise on top of the real surface. Hide it and let\n // the frame own the full height. Only a failed mount (launcher card /\n // not-installed notice) shows the bar again.\n var bar = document.getElementById('bar');\n var stage = document.getElementById('stage');\n if (bar && stage) {\n bar.style.display = text ? 'none' : '';\n stage.style.height = text ? '100%' : 'calc(100% - 29px)';\n }\n}\n\n// One fresh #chat frame on the stage (over the card), the previous one gone.\nfunction newFrame() {\n var stage = document.getElementById('stage');\n var old = document.getElementById('chat');\n if (old) old.remove();\n var frame = document.createElement('iframe');\n frame.id = 'chat';\n frame.title = 'Session Chat';\n stage.insertBefore(frame, stage.firstChild);\n return frame;\n}\n\nfunction revokeBlob() {\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n if (blobUrl) {\n try { URL.revokeObjectURL(blobUrl); } catch (_) {}\n blobUrl = null;\n }\n}\n\nfunction mount(url) {\n if (!url || url === mountedUrl) return;\n mountedUrl = url;\n var gen = ++mountGen;\n revokeBlob();\n document.getElementById('notice').className = '';\n setCardUrl(url);\n document.getElementById('link').innerHTML =\n '<a href=\"' + escapeHtml(url) + '\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>';\n var tokened = withEmbedToken(url);\n if (tokened === url) {\n // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):\n // the direct frame already passes the daemon's origin checks, and the\n // blob path's daemon calls would have nothing to carry — skip it.\n mountDirect(url);\n return;\n }\n mountBlob(url, tokened, gen).catch(function (err) {\n if (gen !== mountGen) return;\n // The fetch itself was refused (host connect-src not merging our\n // connectDomains, PNA, daemon down, …) — probe answered: no blob path.\n console.warn('[session-chat] blob mount failed, falling back to a direct frame:',\n err && err.message ? err.message : String(err));\n mountDirect(url);\n });\n}\n\n// Direct-src iframe of the deep link, with the block probe → launcher card\n// fallback. The terminal mount path when the blob one is unavailable.\nfunction mountDirect(url) {\n var frame = newFrame();\n frame.src = withEmbedToken(url);\n setMode('direct frame');\n armBlockProbe(frame);\n}\n\n// Blob pass-through: fetch the chat html through the embed token, rewrite it\n// for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and\n// mount it — blob: passes a host frame-src that refuses the daemon origin.\nfunction mountBlob(url, tokened, gen) {\n return fetch(tokened, { credentials: 'omit' }).then(function (res) {\n if (!res.ok) throw new Error('HTTP ' + res.status);\n return res.text();\n }).then(function (html) {\n if (gen !== mountGen) return;\n var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);\n blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));\n var frame = newFrame();\n armBlobBootProbe(frame, url, gen);\n frame.src = blobUrl;\n setMode('embedded');\n });\n}\n\n// The blob document's shim posts a boot ack as its first statement. No ack\n// within the settle window means the host refused the blob frame or its\n// inherited CSP killed the document's inline scripts — surface that through\n// the direct-frame path (whose own probe ends on the launcher card) rather\n// than leaving a silent blank.\nwindow.addEventListener('message', function (evt) {\n var d = evt.data;\n if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;\n var frame = document.getElementById('chat');\n if (!frame || evt.source !== frame.contentWindow) return;\n blobBootOk = true;\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n});\n\nfunction armBlobBootProbe(frame, url, gen) {\n blobBootOk = false;\n blobBootTimer = setTimeout(function () {\n blobBootTimer = null;\n if (gen !== mountGen || blobBootOk) return;\n console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');\n revokeBlob();\n frame.remove();\n mountDirect(url);\n }, 5000);\n}\n\nfunction showNotInstalled() {\n var old = document.getElementById('chat');\n if (old) old.remove();\n revokeBlob();\n setMode('');\n mountedUrl = null;\n document.getElementById('card').style.display = 'none';\n document.getElementById('notice').className = 'show';\n document.getElementById('link').textContent = 'app not installed';\n}\n\n// The body of an ext-apps tool-result notification, whichever wrapping\n// shape the host uses (params IS the CallToolResult, or nests it under\n// params.result). null for error results / non-JSON text.\nfunction parseToolResultBody(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try { return JSON.parse(item.text); } catch (_) { return null; }\n}\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId — keep\n// in sync. \\`sessionId\\` (an agentproto_session_chat / live_session result)\n// wins over \\`id\\` (an agent_start result: the spawned session descriptor).\nfunction extractToolResultSessionId(params) {\n var body = parseToolResultBody(params);\n if (!body) return null;\n if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (typeof body.id === 'string' && body.id) return body.id;\n return null;\n}\n\n// ── Embed-token refresh ──────────────────────────────────────────────────\n// The token baked into this page by registerMcpApps dies with the daemon\n// boot that minted it (runtime embed-tokens.ts), but hosts cache the\n// rendered widget: Claude Desktop keeps a conversation's srcdoc, so after\n// any daemon restart the cached page replays a dead token and every daemon\n// call 403s — the blob fetch fails, the direct frame is refused, and the\n// user is left on the launcher card forever. Every tool result carries a\n// live token (SessionChatOutput.embedToken), so adopt it before mounting.\n//\n// Only a render that WAS baked refreshes. A standalone tab or VS Code\n// HTTP-iframe panel still holds the literal placeholder and passes the\n// daemon's origin checks on its own; handing it a token would switch it\n// onto the blob path for no reason.\nfunction adoptEmbedToken(body) {\n var cur = window.__AGENPROTO_EMBED_TOKEN__;\n if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;\n if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;\n window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;\n}\n\n// Turn a session id into the installed app's deep link over the bridge.\nfunction resolveSession(sessionId) {\n noteSession(sessionId);\n return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})\n .then(function(out) {\n adoptEmbedToken(out);\n if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);\n else showNotInstalled();\n })\n .catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n });\n}\n\n// ── Launcher card (the frame-blocked fallback UI) ────────────────────────\n// The iframe mounts OVER the card. When the host's own CSP refuses the\n// frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:\n// data: and do not yet merge csp.frameDomains), the frame stays on a\n// same-origin about:blank — readable, unlike the loaded cross-origin chat —\n// and is removed after a settle window so the card becomes the interactive\n// surface.\n//\n// The anchor carries target=\"_blank\" rel=\"noreferrer\" as the fallback path,\n// but that is a plain no-op in hosts that sandbox the widget iframe without\n// allow-popups (observed in Claude Desktop's side chat) — a normal click\n// on such a host does nothing. Where the host advertises the capability\n// (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,\n// captured by the bridge's getHostCapabilities()), route through the\n// host-mediated ui/open-link request instead, which is not subject to the\n// iframe's popup sandboxing.\ncardOpenEl.addEventListener('click', function (evt) {\n if (!cardUrl) { evt.preventDefault(); return; }\n if (getHostCapabilities() && getHostCapabilities().openLinks) {\n evt.preventDefault();\n openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });\n }\n});\n\nfunction noteSession(id) {\n if (id && cardSessionEl) cardSessionEl.textContent = id;\n}\n\nfunction setCardUrl(u) {\n if (!u) return;\n cardUrl = u;\n cardOpenEl.setAttribute('href', u);\n}\n\n// A blocked frame never leaves about:blank (same-origin with this page —\n// readable); the loaded chat is cross-origin (the probe throws). A readable\n// frame that is STILL blank when the settle window closes = blocked.\nfunction isBlank(frame) {\n try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }\n}\n\nfunction armBlockProbe(frame) {\n var settle = null;\n function probe() {\n if (settle) { clearTimeout(settle); settle = null; }\n if (!isBlank(frame)) return;\n // Still on about:blank right now — arm the settle window, but\n // re-check when it closes: a slow (not blocked) load may have landed\n // in the meantime, and only a frame STILL blank after the window gets\n // removed. load firing in between clears this timeout above.\n settle = setTimeout(function () {\n settle = null;\n if (isBlank(frame)) frame.remove();\n }, 2000);\n }\n setTimeout(function () {\n frame.addEventListener('load', probe);\n probe();\n }, 100);\n}\n\n// A url baked at render time (a host that re-renders per call with a real\n// initData) mounts on boot — through the same blob-first path as every\n// other mount, since the same opaque widget context frames it. The\n// pre-rendered #chat frame is replaced by mount()'s own.\nif (mountedUrl) {\n var bootUrl = mountedUrl;\n mountedUrl = null;\n adoptEmbedToken(window.__APP_INIT__);\n mount(bootUrl);\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var body = parseToolResultBody(params);\n if (!body) return;\n if (typeof body.url === 'string' && body.url && body.installed !== false) {\n // Adopt now, not at mount time: the token doesn't depend on the bridge.\n adoptEmbedToken(body);\n if (bridged) mount(body.url); else pendingUrl = body.url;\n return;\n }\n if (body.installed === false) {\n if (bridged) showNotInstalled(); else pendingNotInstalled = true;\n return;\n }\n var id = extractToolResultSessionId(params);\n if (!id || id === pinnedSessionId) return;\n pinnedSessionId = id;\n noteSession(id);\n if (bridged) resolveSession(id);\n});\n\ninitBridge().then(function() {\n bridged = true;\n if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }\n if (pendingNotInstalled) { showNotInstalled(); return; }\n if (mountedUrl) return; // rendered with a real url\n var init = window.__APP_INIT__ || {};\n if (init.installed === false) { showNotInstalled(); return; }\n // No url baked in (static ui:// render): resolve over the bridge — the\n // pinned session when the host named one, else the app's picker.\n return resolveSession(pinnedSessionId);\n}).catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n});\n</script>\n</body>\n</html>`\n}\n\nexport const SESSION_CHAT_FALLBACK_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat — not installed</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3}\n#wrap{max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\nh1{font-size:18px;margin-bottom:12px}\ncode{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\np{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"/store?install=%40agentik%2Fsession-chat\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n <p>Once installed, call <code>agentproto_session_chat</code> again (optionally with a\n <code>sessionId</code>) and the panel will embed the app's chat UI deep-linked to that\n session.</p>\n</div>\n</body>\n</html>`\n","/**\n * McpApp definition for the session-chat builtin widget — a thin launcher\n * for the installed `@agentik/session-chat` app's UI, exposed as a BUILTIN\n * daemon panel so MCP-Apps clients (and the VS Code appPanel surface) can\n * open the chat from `app_catalog` without installing anything into the\n * MCP surface itself.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * live-session — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Serving approach (decided; see the PR description for the tradeoffs):\n * (a) App installed → the tool returns the app's standalone deep-link\n * (`GET /apps/@agentik/session-chat/ui?session=<id>&embed=1`) and the\n * panel HTML is a thin full-viewport iframe of it. No duplicate chat\n * bundle: the UI is the studio-built app, served by the daemon's\n * standalone app host. The daemon relaxes that route's\n * `frame-ancestors 'none'` only when `?embed=1` is present.\n * (b) App NOT installed → the panel HTML is a small readable notice\n * pointing at `agentproto app install`. Never a vendored/reimplemented\n * chat UI.\n *\n * This module also exports `sessionChatApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install`\n * path. Its `ui.html` is the fallback notice, since `AppUiDefinition.ui.html`\n * is a plain static string and can't know at emit time whether the sibling\n * app is installed; the live-mounted `makeSessionChatApp` below is the one\n * that resolves installed-ness per boot.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\nimport { SESSION_CHAT_FALLBACK_HTML, sessionChatEmbedHtml } from \"./panel.js\"\n\nexport { SESSION_CHAT_FALLBACK_HTML, sessionChatEmbedHtml }\nexport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\n\n/** The installed studio app this builtin widget is a launcher for. */\nexport const SESSION_CHAT_APP_ID = \"@agentik/session-chat\"\n\nexport const sessionChatInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Deep-link the chat to an existing session by id (e.g. `sess_xxx`). \" +\n \"Omit to open the app with its session picker.\",\n ),\n})\n\nexport type SessionChatInput = z.infer<typeof sessionChatInputSchema>\n\nexport interface SessionChatOutput {\n /** Whether the `@agentik/session-chat` app is installed (with a UI) on\n * the daemon this widget is mounted on. */\n installed: boolean\n /** The app's standalone deep-link url when installed, else null. */\n url: string | null\n /** A live per-boot embed token, when the host daemon supplies one (see\n * `SessionChatOps.mintEmbedToken`). The panel adopts it over the token\n * baked into its resource HTML, which dies with the daemon boot that\n * rendered it — hosts that cache the widget per conversation (Claude\n * Desktop) otherwise replay a dead token and 403 forever after a daemon\n * restart. Absent when the deep link is unavailable anyway. */\n embedToken?: string\n /** When NOT installed: the daemon's App Store deep link with the entry\n * pre-selected (absolute, so it works from an MCP host iframe whose own\n * origin is not the daemon's). */\n storeUrl?: string\n}\n\nexport interface SessionChatOps {\n /** The daemon's own HTTP origin, e.g. \"http://127.0.0.1:18790\" — the\n * standalone app host the chat UI is served from. */\n httpBaseUrl: string\n /** Whether the `@agentik/session-chat` app is installed with a `ui`\n * block. The runtime supplies this from its AppRegistry; optional so\n * tests/consumers without a registry can omit it (treated as false). */\n isSessionChatInstalled?: () => boolean\n /** A live per-boot embed token for the panel to adopt (runtime\n * embed-tokens.ts `stableAppEmbedToken`). Optional: consumers without a\n * token registry (tests, docs) omit it and the panel keeps whatever was\n * baked into its HTML. */\n mintEmbedToken?: () => string\n}\n\n/**\n * Deep-link into the installed app's standalone UI — the same url shape the\n * VS Code chat-panel webview iframes (`chatPanelUrl` in\n * packages/vscode/src/commands/sessionView.logic.ts).\n */\nexport function sessionChatAppUrl(httpBaseUrl: string, sessionId?: string): string {\n const base = `${httpBaseUrl.replace(/\\/+$/, \"\")}/apps/${SESSION_CHAT_APP_ID}/ui`\n const params = new URLSearchParams()\n if (sessionId) params.set(\"session\", sessionId)\n params.set(\"embed\", \"1\")\n return `${base}?${params.toString()}`\n}\n\n/** The daemon's App Store (`GET /store`, store plan S6) with this app\n * pre-selected for install. */\nexport function sessionChatStoreUrl(httpBaseUrl: string): string {\n return `${httpBaseUrl.replace(/\\/+$/, \"\")}/store?install=${encodeURIComponent(SESSION_CHAT_APP_ID)}`\n}\n\n/** The daemon origin the widget's host-iframe CSP must allow both as a\n * frame target (the direct-src mount) AND as a connect target (the blob\n * pass-through fetches the chat html, and the blob document's own daemon\n * calls inherit the widget CSP — see ./blob-embed.ts). Same derivation as\n * live-session's connectDomains entry. */\nfunction daemonOrigin(httpBaseUrl: string): string {\n return new URL(httpBaseUrl).origin\n}\n\n/**\n * Factory: close over the daemon origin + the installed-ness check so\n * execute() needs nothing beyond the tool input (no registry access inside\n * the app itself — mirrors live-session/index.ts).\n */\nexport function makeSessionChatApp(\n ops: SessionChatOps,\n): AgnoMcpApp<SessionChatInput, SessionChatOutput> {\n const installed = ops.isSessionChatInstalled?.() ?? false\n return {\n id: \"agentproto_session_chat\",\n title: \"Session Chat\",\n description:\n \"Open the Session Chat app — a chat-thread view of any agentproto \" +\n \"daemon session: full transcript replay, live follow-over SSE, and a \" +\n \"composer that sends prompts into the same session. Requires the \" +\n \"`@agentik/session-chat` app to be installed; without it the panel \" +\n \"shows install instructions. Pass `sessionId` to deep-link straight \" +\n \"into a known session.\",\n inputSchema: sessionChatInputSchema,\n execute: async input => {\n if (!installed) return { installed, url: null, storeUrl: sessionChatStoreUrl(ops.httpBaseUrl) }\n const token = ops.mintEmbedToken?.()\n return {\n installed,\n url: sessionChatAppUrl(ops.httpBaseUrl, input.sessionId),\n // Re-arms a cached widget whose baked token died with an earlier\n // daemon boot (see SessionChatOutput.embedToken).\n ...(token ? { embedToken: token } : {}),\n }\n },\n html: (initData: SessionChatOutput) => sessionChatEmbedHtml(initData),\n // Spec-correct either way (ext-apps McpUiResourceMeta). Measured\n // 2026-09-18: Claude Desktop / Codex do NOT merge frameDomains into\n // their widget frame-src; whether they merge connectDomains is unknown,\n // so the panel probes it at runtime (blob fetch → fallback) rather than\n // assuming either way.\n csp: {\n frameDomains: [daemonOrigin(ops.httpBaseUrl)],\n connectDomains: [daemonOrigin(ops.httpBaseUrl)],\n },\n }\n}\n\nexport const sessionChatApp: AppHandle = defineApp({\n id: \"@agentproto/session-chat-widget\",\n name: \"Session Chat\",\n description:\n \"Open the Session Chat app — a chat-thread view of any agentproto daemon session, deep-linked \" +\n \"by sessionId. A thin builtin launcher for the installed @agentik/session-chat app.\",\n agents: [],\n ui: {\n html: SESSION_CHAT_FALLBACK_HTML,\n title: \"Session Chat\",\n // `session_restart` is what makes an ENDED session (completed / killed /\n // exited / crashed) recoverable from the chat instead of a dead end: the\n // UI offers \"Restart session\" in place of the composer, and the tool\n // resumes the same conversation (ACP-level resume via the adapter session\n // id, fresh spawn as fallback) and returns a NEW session id the chat\n // navigates to. Kept in step with the installed app's own allowlist\n // (`@agentik/session-chat`'s APP.md `ui.tools`) — a tool missing from\n // EITHER list is refused for the surface that reads it.\n tools: [\n \"session_list\",\n \"agent_start\",\n \"adapter_list\",\n \"conversation_read\",\n \"session_restart\",\n // SANDBOX-VISIBILITY-JOIN: lets a \"Devices\" view group sessions by\n // paired host (e.g. a CI reviewer box) instead of only this daemon's\n // own. Read-only. NOTE: the installed `@agentik/session-chat` app's\n // own APP.md `ui.tools` allowlist needs the SAME two names added in\n // lockstep (see the comment above) — that app's source lives outside\n // this repo, so its UI won't actually call these until that's done too.\n \"device_list\",\n \"device_sessions\",\n ],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/work-board/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-work-board-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui\n *\n * `pnpm --filter @agentproto/apps run build:ui:check` (wired into CI) fails\n * if this file drifts from what the Vite sources actually produce.\n */\n\nexport const WORK_BOARD_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto work board</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-work-board-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n const COLUMN_STATUSES = [\\\"pending\\\", \\\"in_progress\\\", \\\"done\\\", \\\"failed\\\"];\\n const COLUMN_LABELS = {\\n pending: \\\"Pending\\\",\\n in_progress: \\\"In Progress\\\",\\n done: \\\"Done\\\",\\n failed: \\\"Failed\\\"\\n };\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function columnOf(status) {\\n return status === \\\"cancelled\\\" ? \\\"failed\\\" : status;\\n }\\n function verificationTag(task) {\\n const v = task.verification;\\n if (v) {\\n if (v.kind === \\\"gate\\\") return '<span class=\\\"tag t-gate\\\">✓ gate</span>';\\n if (v.kind === \\\"self-report\\\") return '<span class=\\\"tag t-self\\\">self-report</span>';\\n if (v.kind === \\\"human\\\") return '<span class=\\\"tag t-human\\\">human</span>';\\n }\\n if (task.status === \\\"in_progress\\\" && task.verify) {\\n return '<span class=\\\"tag t-gated\\\">gated</span>';\\n }\\n return \\\"\\\";\\n }\\n function ownerTag(task) {\\n if (!task.owner) return '<span class=\\\"tag t-unclaimed\\\">Unclaimed</span>';\\n return `<span class=\\\"tag t-owner\\\">${esc(task.owner)}</span>`;\\n }\\n function actionsFor(task) {\\n if (task.status === \\\"pending\\\") {\\n return [\\n task.owner ? { label: \\\"Start\\\", action: \\\"start\\\", cls: \\\"primary\\\" } : { label: \\\"Claim\\\", action: \\\"claim\\\", cls: \\\"primary\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"in_progress\\\") {\\n return [\\n { label: \\\"Done\\\", action: \\\"done\\\", cls: \\\"primary\\\" },\\n { label: \\\"Fail\\\", action: \\\"fail\\\", cls: \\\"danger\\\" },\\n { label: \\\"Release\\\", action: \\\"release\\\", cls: \\\"\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"done\\\") {\\n return [{ label: \\\"Reopen\\\", action: \\\"reopen\\\", cls: \\\"\\\" }];\\n }\\n return [];\\n }\\n function renderCard(task) {\\n let html = `<div class=\\\"card\\\" data-taskid=\\\"${esc(task.taskId)}\\\">`;\\n html += `<div class=\\\"c-title\\\">${esc(task.title)}</div>`;\\n html += `<div class=\\\"c-id\\\">#${esc(task.taskId)}</div>`;\\n html += `<div class=\\\"c-row\\\">${ownerTag(task)}${verificationTag(task)}`;\\n if (task.status === \\\"cancelled\\\") html += '<span class=\\\"tag t-cancelled\\\">cancelled</span>';\\n html += \\\"</div>\\\";\\n if (task.lastVerifyError) {\\n html += `<div class=\\\"c-err\\\">verify failed: ${esc(task.lastVerifyError)}</div>`;\\n }\\n const actions = actionsFor(task);\\n if (actions.length > 0) {\\n html += '<div class=\\\"c-actions\\\">';\\n for (const a of actions) {\\n html += `<button class=\\\"abtn ${a.cls}\\\" data-taskid=\\\"${esc(task.taskId)}\\\" data-action=\\\"${a.action}\\\">${a.label}</button>`;\\n }\\n html += \\\"</div>\\\";\\n }\\n html += \\\"</div>\\\";\\n return html;\\n }\\n function renderColumns(tasks2) {\\n let html = \\\"\\\";\\n for (const status of COLUMN_STATUSES) {\\n const rows = tasks2.filter((t) => columnOf(t.status) === status);\\n html += `<div class=\\\"col\\\"><div class=\\\"col-hdr\\\"><span>${COLUMN_LABELS[status]}</span><span>${rows.length}</span></div><div class=\\\"col-body\\\">`;\\n html += rows.length === 0 ? '<div class=\\\"empty\\\">No tasks</div>' : rows.map(renderCard).join(\\\"\\\");\\n html += \\\"</div></div>\\\";\\n }\\n return html;\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`work board: missing #${id}`);\\n return el;\\n }\\n function getInput(id) {\\n const el = document.getElementById(id);\\n if (!(el instanceof HTMLInputElement)) throw new Error(`work board: missing input #${id}`);\\n return el;\\n }\\n function isCardAction(value) {\\n switch (value) {\\n case \\\"claim\\\":\\n case \\\"start\\\":\\n case \\\"done\\\":\\n case \\\"fail\\\":\\n case \\\"release\\\":\\n case \\\"cancel\\\":\\n case \\\"reopen\\\":\\n return true;\\n default:\\n return false;\\n }\\n }\\n let boardId = null;\\n let tasks = [];\\n let pollActive = false;\\n function boot() {\\n const columnsEl = getEl(\\\"columns\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const boardIdEl = getEl(\\\"board-id\\\");\\n const boardInputEl = getInput(\\\"board-input\\\");\\n const newTitleEl = getInput(\\\"new-title\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function render() {\\n boardIdEl.textContent = boardId ?? \\\"—\\\";\\n if (!boardInputEl.value) boardInputEl.value = boardId ?? \\\"\\\";\\n columnsEl.innerHTML = renderColumns(tasks);\\n }\\n function findTask(taskId) {\\n return tasks.find((t) => t.taskId === taskId);\\n }\\n function loadBoard() {\\n const args = {\\n includeClosed: true,\\n full: true,\\n ...boardId ? { boardId } : {}\\n };\\n return callTool(\\\"task_list\\\", args).then((data) => {\\n boardId = data.boardId ?? boardId;\\n tasks = data.tasks ?? [];\\n render();\\n setStatus(`${tasks.length} task${tasks.length === 1 ? \\\"\\\" : \\\"s\\\"} · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n }).catch((e) => {\\n setStatus(`Error: ${e.message}`);\\n });\\n }\\n function applyAction(taskId, action) {\\n const task = findTask(taskId);\\n if (!task) return;\\n const base = { taskId: task.taskId, rev: task.rev };\\n let promise;\\n if (action === \\\"claim\\\") {\\n promise = callTool(\\\"task_claim\\\", base);\\n } else if (action === \\\"start\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"in_progress\\\" });\\n } else if (action === \\\"done\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"done\\\" });\\n } else if (action === \\\"fail\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"failed\\\" });\\n } else if (action === \\\"release\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, owner: null });\\n } else if (action === \\\"cancel\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"cancelled\\\" });\\n } else {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"pending\\\" });\\n }\\n promise.then((result) => {\\n if (\\\"conflict\\\" in result) {\\n setStatus(`Someone else moved \\\"${task.title}\\\" first — refreshed.`);\\n } else if (\\\"error\\\" in result) {\\n setStatus(`Action failed: ${result.error}`);\\n } else {\\n setStatus(`Updated \\\"${task.title}\\\".`);\\n }\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Action failed: ${e.message}`);\\n });\\n }\\n function doPoll() {\\n if (pollActive) return;\\n pollActive = true;\\n loadBoard().finally(() => {\\n pollActive = false;\\n setTimeout(doPoll, 4e3);\\n });\\n }\\n columnsEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-action]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const taskId = btn.getAttribute(\\\"data-taskid\\\");\\n const action = btn.getAttribute(\\\"data-action\\\");\\n if (!taskId || !action || !isCardAction(action)) return;\\n applyAction(taskId, action);\\n });\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadBoard();\\n });\\n getEl(\\\"go-btn\\\").addEventListener(\\\"click\\\", () => {\\n const value = boardInputEl.value.trim();\\n if (!value) return;\\n boardId = value;\\n void loadBoard();\\n });\\n getEl(\\\"add-btn\\\").addEventListener(\\\"click\\\", () => {\\n const title = newTitleEl.value.trim();\\n if (!title) return;\\n const args = boardId ? { title, boardId } : { title };\\n callTool(\\\"task_create\\\", args).then((result) => {\\n if (result.error) {\\n setStatus(`Create failed: ${result.error}`);\\n return void 0;\\n }\\n newTitleEl.value = \\\"\\\";\\n setStatus(`Created \\\"${title}\\\".`);\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Create failed: ${e.message}`);\\n });\\n });\\n initBridge().then(() => loadBoard()).then(() => {\\n setTimeout(doPoll, 4e3);\\n }).catch((e) => {\\n setStatus(`Bridge error: ${e.message}`);\\n });\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets.\\n\\n Every rule below the next section reads ONLY the --wb-* custom\\n properties. Each one resolves through TWO mapping sheets, chosen at\\n PAINT time via plain CSS var() fallback — no build step, no JS, so a\\n host that swaps its theme live (VS Code) is never frozen at whatever\\n theme happened to be active when this bundle was built:\\n\\n 1. embedded — VS Code injects a live --vscode-* custom-property set\\n into the webview and swaps it on theme change. Whenever the\\n relevant --vscode-* variable exists, it wins — in a VS Code\\n `srcdoc` relay panel AND (structurally identically) any other host\\n that happened to expose the same names, though none is known to.\\n 2. standalone — the agentproto brand tokens (see\\n projects/agentproto/site/src/app/brand.css), used whenever no\\n --vscode-* variable is set — a plain MCP-Apps host or the daemon's\\n own `/apps/.../ui` tab. `--phos`/`--amber`/`--crimson` are\\n THEME-INVARIANT in brand.css (the phosphor terminal palette), so\\n they're identical in both blocks below; the rest follow brand.css's\\n light/dark split via `prefers-color-scheme` — a standalone tab has\\n no host to pick a theme for it, unlike VS Code's own.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --wb-bg: var(--vscode-editor-background, #f5f6f3);\\n --wb-surface: var(--vscode-sideBar-background, #ffffff);\\n --wb-ink: var(--vscode-editor-foreground, #171a1e);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --wb-border: var(--vscode-panel-border, #dde0da);\\n --wb-accent: var(--vscode-button-background, #0f62fe);\\n\\n /* Status colors — phos/amber/crimson are the theme-invariant terminal\\n palette in brand.css, so light and dark standalone fallbacks match. */\\n --wb-status-pending: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --wb-bg: var(--vscode-editor-background, #101214);\\n --wb-surface: var(--vscode-sideBar-background, #16191d);\\n --wb-ink: var(--vscode-editor-foreground, #e8eaed);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-border: var(--vscode-panel-border, #272c33);\\n --wb-accent: var(--vscode-button-background, #4589ff);\\n\\n --wb-status-pending: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--wb-bg);\\n color: var(--wb-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--wb-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n#board-label { font-size: 11px; color: var(--wb-ink-muted); }\\n#board-id { font-weight: 600; color: var(--wb-ink); font-size: 12px; font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace; }\\n#board-input {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n width: 220px;\\n}\\n.tbtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--wb-border); }\\n#new-title {\\n flex: 1;\\n min-width: 120px;\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#columns { flex: 1; display: flex; overflow-x: auto; overflow-y: hidden; }\\n.col {\\n width: 260px;\\n min-width: 220px;\\n flex-shrink: 0;\\n display: flex;\\n flex-direction: column;\\n border-right: 1px solid var(--wb-border);\\n}\\n.col-hdr {\\n padding: 8px 10px;\\n border-bottom: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--wb-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n display: flex;\\n justify-content: space-between;\\n}\\n.col-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }\\n\\n.card {\\n background: var(--wb-surface);\\n border: 1px solid var(--wb-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n}\\n.c-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.c-id {\\n font-size: 10px;\\n color: var(--wb-ink-muted);\\n margin-bottom: 6px;\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.c-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-unclaimed { background: color-mix(in srgb, var(--wb-ink-muted) 15%, transparent); color: var(--wb-ink-muted); border: 1px dashed var(--wb-ink-muted); }\\n.t-owner { background: color-mix(in srgb, var(--wb-accent) 15%, transparent); color: var(--wb-accent); }\\n\\n/* Verification tell: gate solid green, self-report amber outline, human\\n blue, a declared verify with no verification yet grey \\\"gated\\\" — never\\n render an unverified done as gate-passed (see render.ts's verificationTag). */\\n.t-gate { background: color-mix(in srgb, var(--wb-status-done) 18%, transparent); color: var(--wb-status-done); }\\n.t-self { background: transparent; color: var(--wb-status-in-progress); border: 1px solid var(--wb-status-in-progress); }\\n.t-human { background: color-mix(in srgb, var(--wb-accent) 18%, transparent); color: var(--wb-accent); }\\n.t-gated { background: color-mix(in srgb, var(--wb-ink-muted) 12%, transparent); color: var(--wb-ink-muted); }\\n.t-cancelled {\\n background: color-mix(in srgb, var(--wb-status-cancelled) 10%, transparent);\\n color: var(--wb-status-cancelled-weak);\\n border: 1px dashed color-mix(in srgb, var(--wb-status-cancelled) 40%, transparent);\\n}\\n\\n.c-err { font-size: 10px; color: var(--wb-status-failed); margin-bottom: 6px; word-break: break-word; }\\n.c-actions { display: flex; flex-wrap: wrap; gap: 4px; }\\n.abtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.abtn:hover { background: var(--wb-border); }\\n.abtn.danger { border-color: var(--wb-status-failed); color: var(--wb-status-failed); }\\n.abtn.primary { border-color: var(--wb-status-done); color: var(--wb-status-done); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--wb-ink-muted);\\n border-top: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n.empty { padding: 16px 8px; color: var(--wb-ink-muted); font-size: 11px; text-align: center; }</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"board-label\\\">Board:</span>\\n <span id=\\\"board-id\\\">—</span>\\n <input id=\\\"board-input\\\" type=\\\"text\\\" placeholder=\\\"tree:<sessionId> or ws:<slug>\\\">\\n <button class=\\\"tbtn\\\" id=\\\"go-btn\\\">Go</button>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n <input id=\\\"new-title\\\" type=\\\"text\\\" placeholder=\\\"New task title…\\\">\\n <button class=\\\"tbtn\\\" id=\\\"add-btn\\\">+ Add</button>\\n </div>\\n <div id=\\\"columns\\\"></div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto work-board panel — the `WORK_BOARD_HTML`\n * served as the `ui://agentproto_work_board/view` resource by\n * `registerMcpApps` (see ../mcp-app-types.ts and runtime's\n * mcp-apps-adapter.ts). Mounted via the `agentproto_work_board` tool defined\n * in ./index.ts.\n *\n * A kanban over the daemon's Task ledger (`packages/runtime/src/\n * task-ledger.ts`) — the one writable multi-party entity the daemon exposes\n * (Activity is a read-only projection; a session has a lifecycle, not an\n * intention). Columns are the ledger's own status enum, folding `cancelled`\n * into the `failed` column (tagged distinctly — see `columnOf` in\n * ./ui/render.ts) rather than adding a fifth column for a status v1 treats\n * as terminal scrap. The board/swimlane selector is the ledger's own board\n * id (`tree:<rootSessionId>` or `ws:<slug>`) — always shown verbatim, never\n * hidden behind a friendly name, per the ledger's scoping model.\n *\n * `WORK_BOARD_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui`\n * (scripts/build-work-board-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:check` (wired into CI) fails if that generated file drifts from\n * what the Vite sources actually produce. See ./ui/vite.config.ts for how\n * the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined as a\n * classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface). Standalone (no host at all, e.g. a top-level\n * `GET /apps/:appId/ui` tab) does NOT connect yet: panel-bridge.ts's\n * handshake has no reply to wait for in that case and hangs on\n * \"Connecting to bridge…\" — a known gap being fixed in panel-bridge.ts\n * itself by a separate change. This app will pick that fix up automatically\n * (rebuild + rebase) once it lands; nothing here needs to special-case it.\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → task_list (`full: true` — the compact projection\n * drops `verification`, which the verification tell needs) on a ~4 s poll\n * – Actions: tools/call → task_claim / task_update / task_create\n *\n * Read-only-safe first: no drag-and-drop. Each card gets the explicit status\n * actions valid from its current state (Claim/Start/Done/Fail/Release/\n * Cancel/Reopen) — a correct read-only-plus-explicit-actions board beats a\n * half-wired drag, per the brief.\n *\n * The verification tell (never render an unverified done as gate-passed):\n * - `verification.kind === \"gate\"` → solid green \"✓ gate\"\n * - `verification.kind === \"self-report\"` → amber outline \"self-report\"\n * - `verification.kind === \"human\"` → blue \"human\"\n * - a declared `verify` with no verification yet → grey \"gated\" tag\n * An owner-less task always reads \"Unclaimed\", never \"pending\" (that word is\n * reserved for the Activity vocabulary's \"blocked\" meaning elsewhere).\n */\n\nimport { WORK_BOARD_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const WORK_BOARD_APP_ID = \"@agentproto/work-board\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeWorkBoardApp`),\n * hence its resource uri `ui://agentproto_work_board/view`. */\nexport const WORK_BOARD_TOOL_ID = \"agentproto_work_board\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host that\n * dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const WORK_BOARD_UI_TOOLS = [\n \"task_list\",\n \"task_claim\",\n \"task_update\",\n \"task_create\",\n] as const\n\nexport const WORK_BOARD_HTML = WORK_BOARD_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto work-board panel — a kanban over the\n * daemon's Task ledger (`packages/runtime/src/task-ledger.ts`), the ONLY\n * writable multi-party entity the daemon exposes (Activity is a read-only\n * projection recomputed on every `list()`; a session has a lifecycle, not an\n * intention).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * session-chat — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_work_board` is called by the host with an optional\n * `boardId`.\n * 2. execute() answers an initial `{boardId, tasks}` snapshot off the\n * ledger — resolved to the caller's default board when `boardId` is\n * omitted (a session's `tree:<root>`, the operator's `ws:<slug>`).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `task_list` (`full: true` — the compact projection drops\n * `verification`) every ~4 s; writes go through `task_claim` /\n * `task_update` / `task_create`, the same verbs any other caller uses —\n * no second write path.\n *\n * This module also exports `workBoardApp`, a real `defineApp()` `AppHandle`\n * (`agents: []`, UI-only) — the catalog/emit/`app_install` path. It's\n * separate from `makeWorkBoardApp` above, which the daemon mounts directly\n * at boot: that factory closes over the LIVE ledger read, something a\n * static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n WORK_BOARD_APP_ID,\n WORK_BOARD_HTML,\n WORK_BOARD_TOOL_ID,\n WORK_BOARD_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const workBoardInputSchema = z.object({\n boardId: z\n .string()\n .optional()\n .describe(\n \"Board to open — `tree:<rootSessionId>` (a supervisor and its \" +\n \"executors) or `ws:<slug>` (an operator workspace). Omit → the \" +\n \"caller's default board.\",\n ),\n})\n\nexport type WorkBoardInput = z.infer<typeof workBoardInputSchema>\n\nexport interface WorkBoardOutput<TTask = unknown> {\n /** Always echoed back — the ledger's board ids are the swimlane\n * selector, and must be visible rather than hidden behind a friendly\n * name (it's how the operator knows which mission they're looking at). */\n boardId: string\n tasks: TTask[]\n}\n\n/**\n * Generic over the host's own task-record shape — this package doesn't own\n * that type (it lives in @agentproto/runtime's task ledger), so the factory\n * stays agnostic to it and the host supplies the concrete type as a type\n * argument when it wires the app up (mirrors SessionsPanelOps<TSession>).\n */\nexport interface WorkBoardOps<TTask = unknown> {\n /** Full (unprojected) records for a board — NOT `task_list`'s default\n * compact projection, since the verification tell needs `verification`\n * (self-report vs gate vs human), which compact mode omits. Omit\n * `boardId` to resolve the caller's default board. */\n listTasks(boardId?: string): WorkBoardOutput<TTask>\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeSessionsPanelApp.\n */\nexport function makeWorkBoardApp<TTask = unknown>(\n ops: WorkBoardOps<TTask>,\n): AgnoMcpApp<WorkBoardInput, WorkBoardOutput<TTask>> {\n return {\n id: WORK_BOARD_TOOL_ID,\n title: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task \" +\n \"ledger (columns: pending/in_progress/done/failed, cancelled folded \" +\n \"into failed). Swimlane is the ledger's own board id (`tree:<root>` \" +\n \"or `ws:<slug>`). Each card shows its owner (or \\\"Unclaimed\\\") and the \" +\n \"verification tell — a gate-passed done reads visually distinct from \" +\n \"a self-reported one. Pass `boardId` to open a specific board.\",\n inputSchema: workBoardInputSchema,\n execute: async input => ops.listTasks(input.boardId),\n html: WORK_BOARD_HTML,\n }\n}\n\nexport const workBoardApp: AppHandle = defineApp({\n id: WORK_BOARD_APP_ID,\n name: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task ledger, scoped by board id \" +\n \"(`tree:<rootSessionId>` or `ws:<slug>`), with an explicit status action per card.\",\n agents: [],\n ui: {\n html: WORK_BOARD_HTML,\n title: \"Work Board\",\n tools: [...WORK_BOARD_UI_TOOLS],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/store/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-store-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:store\n *\n * `pnpm --filter @agentproto/apps run build:ui:store:check` (wired into CI)\n * fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const STORE_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto app store</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-store-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\").replace(/'/g, \\\"'\\\");\\n }\\n function text(value, fallback) {\\n return value && value.trim() !== \\\"\\\" ? esc(value) : esc(fallback);\\n }\\n function chips(row) {\\n const parts = [];\\n if (row.tier) parts.push(`<span class=\\\"chip\\\">${esc(row.tier)}</span>`);\\n if (row.placement) parts.push(`<span class=\\\"chip\\\">placement: ${esc(row.placement)}</span>`);\\n if (row.requires?.agentprotoVersion) {\\n parts.push(`<span class=\\\"chip\\\">requires ${esc(row.requires.agentprotoVersion)}</span>`);\\n }\\n if (row.requires?.apps && row.requires.apps.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires apps: ${esc(row.requires.apps.join(\\\", \\\"))}</span>`);\\n }\\n if (row.requires?.agents && row.requires.agents.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires agents: ${esc(row.requires.agents.join(\\\", \\\"))}</span>`);\\n }\\n return parts.join(\\\"\\\");\\n }\\n function installButton(catalogRow) {\\n return `<button class=\\\"sbtn store-install-btn\\\" data-appid=\\\"${esc(catalogRow.appId)}\\\" data-decision=\\\"install\\\">Install</button>`;\\n }\\n function renderCatalogFeatured(rows) {\\n const featured = rows.filter((r) => r.featured === true);\\n if (featured.length === 0) return \\\"\\\";\\n return featured.map(\\n (r) => `<div class=\\\"card store-card-featured\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`\\n ).join(\\\"\\\");\\n }\\n function renderCatalogAvailable(rows) {\\n const byCategory = /* @__PURE__ */ new Map();\\n for (const r of rows) {\\n if (r.installed) continue;\\n if (r.featured === true) continue;\\n const key = r.category && r.category !== \\\"\\\" ? r.category : \\\"Other\\\";\\n const list = byCategory.get(key);\\n if (list) list.push(r);\\n else byCategory.set(key, [r]);\\n }\\n let out = \\\"\\\";\\n for (const [category, list] of byCategory) {\\n out = out + `<h3 class=\\\"store-category-title\\\">${esc(category)}</h3>`;\\n for (const r of list) {\\n out = out + `<div class=\\\"card store-card-available\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`;\\n }\\n }\\n return out;\\n }\\n function renderInstalled(installed, catalogRows, updates) {\\n const updateIds = new Set(updates.map((u) => u.appId));\\n const byId = new Map(catalogRows.map((c) => [c.appId, c]));\\n if (installed.length === 0) return \\\"\\\";\\n return installed.map((app) => {\\n const version = app.version ?? byId.get(app.appId)?.version ?? \\\"\\\";\\n const source = app.source?.kind ?? \\\"local\\\";\\n const hasUi = byId.get(app.appId)?.hasUi === true || app.source?.kind === \\\"local\\\" && app.dir !== void 0;\\n const actions = [\\n `<button class=\\\"sbtn store-resync-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"resync\\\">Update</button>`,\\n `<button class=\\\"sbtn store-uninstall-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"uninstall\\\">Uninstall</button>`,\\n ...hasUi ? [\\n `<button class=\\\"sbtn store-open-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"open\\\">Open</button>`\\n ] : []\\n ];\\n return `<div class=\\\"card store-card-installed\\\" id=\\\"store-entry-${esc(app.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(app.name, app.appId)}</div>\\n <div class=\\\"card-actions-meta\\\"><span class=\\\"card-version\\\">${text(version, \\\"\\\")}</span><span class=\\\"card-source\\\">${esc(source)}</span>${updateIds.has(app.appId) ? `<span class=\\\"badge store-update-badge\\\">Update available</span>` : \\\"\\\"}</div>\\n <div class=\\\"card-actions\\\">${actions.join(\\\"\\\")}</div>\\n </div>`;\\n }).join(\\\"\\\");\\n }\\n function renderSourcesHeader(snapshot2) {\\n if (!snapshot2.warnings || snapshot2.warnings.length === 0) return \\\"\\\";\\n return `<div class=\\\"store-warnings\\\">${snapshot2.warnings.map((w) => `<p class=\\\"store-warning\\\">${esc(w)}</p>`).join(\\\"\\\")}</div>`;\\n }\\n function renderBuiltins(builtinNames) {\\n if (builtinNames.length === 0) return \\\"\\\";\\n const details = `<details class=\\\"store-builtins\\\"><summary>Builtin panels</summary><ul class=\\\"store-builtin-list\\\">${builtinNames.map((n) => `<li class=\\\"store-builtin-item\\\">${esc(n)}</li>`).join(\\\"\\\")}</ul></details>`;\\n return details;\\n }\\n function isAgentappUrl(url) {\\n return url.replace(/\\\\?.*$/, \\\"\\\").replace(/#.*$/, \\\"\\\").toLowerCase().endsWith(\\\".agentapp\\\");\\n }\\n function storeEmpty(catalogRows, installed) {\\n if (catalogRows.length > 0 || installed.length > 0) return \\\"\\\";\\n return `<div class=\\\"store-empty\\\">\\n <p class=\\\"store-empty-title\\\">No apps installed, and the app catalog is empty.</p>\\n <p class=\\\"store-empty-hint\\\">Add a <code>catalog.sources</code> entry to your daemon config, drop an <code>app-catalog.json</code> next to it, or install an app directly from a URL in the form below.</p>\\n </div>`;\\n }\\n function render(snapshot2) {\\n const installed = renderInstalled(snapshot2.installed, snapshot2.catalog, snapshot2.updates);\\n const featured = renderCatalogFeatured(snapshot2.catalog);\\n const available = renderCatalogAvailable(snapshot2.catalog);\\n const builtins = renderBuiltins([\\\"sessions-panel\\\", \\\"agents-overview\\\", \\\"bureau-sessions\\\", \\\"session-story\\\", \\\"live-session\\\", \\\"work-board\\\"]);\\n const sections = [];\\n sections.push(renderSourcesHeader(snapshot2));\\n if (installed !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Installed</h2>`, `<div class=\\\"grill\\\">${installed}</div>`);\\n }\\n if (featured !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Featured</h2>`, featured);\\n }\\n if (available !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Available</h2>`, `<div class=\\\"grill\\\">${available}</div>`);\\n }\\n if (installed === \\\"\\\" && featured === \\\"\\\" && available === \\\"\\\") {\\n sections.push(storeEmpty(snapshot2.catalog, snapshot2.installed));\\n }\\n sections.push(builtins);\\n return sections.join(\\\"\\\\n\\\");\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`app store: missing #${id}`);\\n return el;\\n }\\n let snapshot = { catalog: [], installed: [], updates: [] };\\n function setStatus(msg) {\\n getEl(\\\"statusbar\\\").textContent = msg;\\n }\\n function sendInstall(payload) {\\n return callTool(\\n \\\"app_install\\\",\\n payload\\n );\\n }\\n function runInstallConfirm(payload, ask) {\\n sendInstall(payload).then((result) => {\\n if (\\\"needsConfirmation\\\" in result && result.needsConfirmation === true) {\\n if (!ask(result)) return;\\n callTool(\\\"app_install\\\", {\\n ...payload,\\n confirm: result.confirm\\n }).then(() => {\\n setStatus(`Installed ${String(payload.url)}`);\\n return refresh();\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n } else {\\n setStatus(`Installed ${result.appId}`);\\n return refresh();\\n }\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n }\\n function buildInstallPayload(entry) {\\n const source = entry.source;\\n if (!source || typeof source !== \\\"object\\\") return void 0;\\n const s = source;\\n const payload = { url: s.url };\\n if (s.kind === \\\"agentapp\\\") {\\n payload.sha256 = s.sha256;\\n } else {\\n if (s.ref) payload.ref = s.ref;\\n if (s.subdir) payload.subdir = s.subdir;\\n if (s.sha) payload.sha = s.sha;\\n payload.allowBuild = false;\\n }\\n if (entry.catalogUrl) payload.catalogUrl = entry.catalogUrl;\\n return payload;\\n }\\n function describePreview(preview) {\\n const sha = preview.kind === \\\"agentapp\\\" ? preview.sha256 ? `sha256 ${preview.sha256}` : \\\"no pinned digest\\\" : preview.sha ? `commit ${preview.sha}` : \\\"no pinned commit\\\";\\n const buildLine = preview.runsBuildCommand ? \\\"It WILL run the app's ui.build command.\\\" : \\\"It will not run any build command.\\\";\\n return `Install ${preview.url}? (${preview.kind}; ${sha})\\n\\n${buildLine}\\n\\nConfirm to proceed.`;\\n }\\n function armInstall(entry) {\\n const payload = buildInstallPayload(entry);\\n if (!payload) {\\n setStatus(\\\"Catalog entry has no remote source — cannot install.\\\");\\n return;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function installFromUrl(url, sha256, ref, subdir) {\\n const payload = { url };\\n if (isAgentappUrl(url)) {\\n if (sha256) payload.sha256 = sha256;\\n } else {\\n if (ref) payload.ref = ref;\\n if (subdir) payload.subdir = subdir;\\n payload.allowBuild = false;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function resync(appId) {\\n callTool(\\\"app_resync\\\", { appId }).then((result) => {\\n setStatus(result.changed ? `${appId} updated` : `${appId} already up to date`);\\n return refresh();\\n }).catch((e) => setStatus(`Update failed: ${e.message}`));\\n }\\n function uninstall(appId) {\\n if (!window.confirm(`Uninstall ${appId}? Its data directory is kept.`)) return;\\n callTool(\\\"app_uninstall\\\", { appId }).then(() => {\\n setStatus(`Uninstalled ${appId}`);\\n return refresh();\\n }).catch((e) => setStatus(`Uninstall failed: ${e.message}`));\\n }\\n function openUi(appId) {\\n const encoded = appId.split(\\\"/\\\").map(encodeURIComponent).join(\\\"/\\\").replace(/%40/g, \\\"@\\\");\\n window.open(`/apps/${encoded}/ui`, \\\"_blank\\\", \\\"noopener\\\");\\n }\\n let deepLinked = false;\\n function deepLinkInstall() {\\n if (deepLinked) return;\\n const target = new URLSearchParams(window.location.search).get(\\\"install\\\");\\n if (!target) return;\\n deepLinked = true;\\n const entry = snapshot.catalog.find((c) => c.appId === target && !c.installed);\\n if (!entry) return;\\n const el = document.getElementById(`store-entry-${CSS.escape(target)}`);\\n if (el && typeof el.scrollIntoView === \\\"function\\\") el.scrollIntoView({ block: \\\"center\\\" });\\n armInstall(entry);\\n }\\n function refresh() {\\n return callTool(\\\"app_catalog\\\", {}).then((catalogRaw) => {\\n const snapshotRaw = catalogRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.catalog) ? snapshotRaw.catalog : Array.isArray(catalogRaw) ? catalogRaw : [];\\n snapshot.catalog = rows.filter((r) => r && r.appId);\\n return callTool(\\\"app_list\\\", {});\\n }).then((appsRaw) => {\\n const snapshotRaw = appsRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.installed) ? snapshotRaw.installed : Array.isArray(appsRaw) ? appsRaw : [];\\n snapshot.installed = rows.filter((a) => a && a.appId);\\n return callTool(\\\"app_updates\\\", {});\\n }).then((updatesRaw) => {\\n const sr = updatesRaw ?? {};\\n const rows = Array.isArray(sr.updates) ? sr.updates : Array.isArray(updatesRaw) ? updatesRaw : [];\\n snapshot.updates = rows.filter((u) => u && u.appId);\\n renderBody();\\n deepLinkInstall();\\n }).catch((e) => setStatus(`Error: ${e.message}`));\\n }\\n function renderBody() {\\n const content = getEl(\\\"content\\\");\\n content.innerHTML = render(snapshot);\\n }\\n function boot() {\\n const content = getEl(\\\"content\\\");\\n const urlBtn = document.getElementById(\\\"store-install-from-url-btn\\\");\\n const urlInput = document.getElementById(\\\"store-url\\\");\\n const shaInput = document.getElementById(\\\"store-sha256\\\");\\n const refInput = document.getElementById(\\\"store-ref\\\");\\n const subdirInput = document.getElementById(\\\"store-subdir\\\");\\n if (urlBtn && urlInput) {\\n urlBtn.addEventListener(\\\"click\\\", () => {\\n const url = urlInput.value.trim();\\n if (url === \\\"\\\") return;\\n installFromUrl(\\n url,\\n shaInput && shaInput.value.trim() !== \\\"\\\" ? shaInput.value.trim() : void 0,\\n refInput && refInput.value.trim() !== \\\"\\\" ? refInput.value.trim() : void 0,\\n subdirInput && subdirInput.value.trim() !== \\\"\\\" ? subdirInput.value.trim() : void 0\\n );\\n });\\n }\\n content.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-decision]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const appId = btn.getAttribute(\\\"data-appid\\\");\\n if (!appId) return;\\n const decision = btn.getAttribute(\\\"data-decision\\\");\\n if (decision === \\\"install\\\") {\\n const entry = snapshot.catalog.find((c) => c.appId === appId);\\n if (entry) armInstall(entry);\\n } else if (decision === \\\"resync\\\") {\\n resync(appId);\\n } else if (decision === \\\"uninstall\\\") {\\n uninstall(appId);\\n } else if (decision === \\\"open\\\") {\\n openUi(appId);\\n }\\n });\\n void initBridge().then(() => refresh()).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n The store panel's stylesheet — same two-sheet token mapping as\\n work-board's (--vscode-* wins when a host injects them, the agentproto\\n brand fallbacks otherwise; light/dark split via prefers-color-scheme\\n for a standalone tab). See that file's header for the full rationale\\n — the --st-* names here are this panel's own namespace.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --st-bg: var(--vscode-editor-background, #f5f6f3);\\n --st-surface: var(--vscode-sideBar-background, #ffffff);\\n --st-ink: var(--vscode-editor-foreground, #171a1e);\\n --st-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --st-border: var(--vscode-panel-border, #dde0da);\\n --st-accent: var(--vscode-button-background, #0f62fe);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --st-bg: var(--vscode-editor-background, #101214);\\n --st-surface: var(--vscode-sideBar-background, #16191d);\\n --st-ink: var(--vscode-editor-foreground, #e8eaed);\\n --st-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --st-border: var(--vscode-panel-border, #272c33);\\n --st-accent: var(--vscode-button-background, #4589ff);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n }\\n}\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--st-bg);\\n color: var(--st-ink);\\n overflow: hidden;\\n}\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#store-head {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--st-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--st-surface);\\n flex-shrink: 0;\\n font-weight: 600;\\n}\\n\\n#content { flex: 1; overflow-y: auto; padding: 12px; }\\n\\n.store-section-title {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n margin: 14px 0 8px;\\n}\\n.store-section-title:first-child { margin-top: 0; }\\n\\n.store-category-title { font-size: 12px; font-weight: 600; margin: 12px 0 6px; }\\n\\n.card {\\n background: var(--st-surface);\\n border: 1px solid var(--st-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n margin-bottom: 8px;\\n}\\n.card-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.card-desc { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 6px; }\\n.card-version, .card-source {\\n font-size: 10px;\\n color: var(--st-ink-muted);\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n margin-right: 6px;\\n}\\n.card-chips { margin-bottom: 6px; }\\n.card-actions { display: flex; gap: 4px; flex-wrap: wrap; }\\n\\n.chip {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 600;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-ink-muted) 15%, transparent);\\n color: var(--st-ink-muted);\\n margin-right: 4px;\\n}\\n\\n.badge {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 700;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-warn) 18%, transparent);\\n color: var(--st-warn);\\n margin-right: 4px;\\n}\\n\\n.sbtn {\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.sbtn:hover { background: var(--st-border); }\\n.sbtn.primary { background: var(--st-accent); color: var(--st-ink-on-accent); border-color: var(--st-accent); }\\n.sbtn.danger { border-color: var(--st-warn); color: var(--st-warn); }\\n\\n.store-url-install { display: flex; gap: 8px; margin: 6px 0 12px; }\\n.store-url-install input {\\n flex: 1;\\n min-width: 120px;\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#confirm-panel {\\n border: 1px solid var(--st-accent);\\n border-radius: 6px;\\n padding: 10px;\\n margin-bottom: 12px;\\n background: var(--st-surface);\\n}\\n#confirm-panel h2 { font-size: 13px; margin-bottom: 8px; }\\n#confirm-meta { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 8px; }\\n#confirm-meta .mono, .mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.build-flag { color: var(--st-warn); font-weight: 600; }\\n\\n.store-warnings { margin-bottom: 10px; }\\n.store-warning {\\n font-size: 11px;\\n color: var(--st-warn);\\n padding: 4px 8px;\\n border: 1px solid color-mix(in srgb, var(--st-warn) 40%, transparent);\\n border-radius: 4px;\\n margin-bottom: 4px;\\n word-break: break-word;\\n}\\n\\n.store-empty {\\n border: 1px dashed var(--st-border);\\n border-radius: 6px;\\n padding: 16px;\\n margin-bottom: 12px;\\n}\\n.store-empty-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; }\\n.store-empty-hint { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 10px; line-height: 1.5; }\\n.store-empty code {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n background: color-mix(in srgb, var(--st-ink-muted) 12%, transparent);\\n padding: 0 3px;\\n border-radius: 3px;\\n}\\n\\n.store-builtins { border-top: 1px solid var(--st-border); padding-top: 8px; margin-top: 12px; }\\n.store-builtins summary {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n cursor: pointer;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n}\\n.store-builtin-list { margin: 8px 0 0 16px; font-size: 11px; color: var(--st-ink-muted); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--st-ink-muted);\\n border-top: 1px solid var(--st-border);\\n background: var(--st-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"store-head\\\"><span>App Store</span></div>\\n <div id=\\\"confirm-panel\\\" hidden></div>\\n <div id=\\\"content\\\"></div>\\n <div id=\\\"url-install-static\\\">\\n <div class=\\\"store-url-install\\\">\\n <input id=\\\"store-url\\\" type=\\\"text\\\" placeholder=\\\"https://example.com/my-app.agentapp or https://github.com/user/repo\\\" />\\n <input id=\\\"store-sha256\\\" type=\\\"text\\\" placeholder=\\\"sha256 (.agentapp, optional)\\\" />\\n <input id=\\\"store-ref\\\" type=\\\"text\\\" placeholder=\\\"git ref (optional)\\\" />\\n <input id=\\\"store-subdir\\\" type=\\\"text\\\" placeholder=\\\"git subdir (optional)\\\" />\\n <button class=\\\"sbtn\\\" id=\\\"store-install-from-url-btn\\\">Install from URL</button>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto App Store panel — the `STORE_HTML`\n * served as the `ui://agentproto_store/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_store` tool defined in ./index.ts.\n *\n * The store over the daemon's app-catalog/install plane (packages/\n * runtime/src/app-tools.ts): Installed (version/source/Update-available\n * badge, resync + uninstall actions), Featured + Available (grouped by\n * category, tier/placement/requires chips), Install-from-URL (with the\n * `.agentapp`-vs-git detection), Sources (warnings + stale flags),\n * Builtin panels (collapsed) — and a never-blank empty state pointing at\n * how to add a catalog source.\n *\n * `STORE_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run\n * build:ui:store` (scripts/build-store-ui.mjs) — same committed-generated\n * contract as work-board (panel.generated.ts lives in git,\n * `build:ui:store:check` fails the CI drift gate). See ./ui/vite.config.ts\n * for how the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined\n * as a classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface).\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Read: tools/call → app_catalog / app_list / app_updates\n * – Apply (always through the SAME verbs every other caller uses — no\n * second install path): app_install (CONFIRMED, two-call handshake),\n * app_resync, app_uninstall\n */\n\nimport { STORE_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const STORE_APP_ID = \"@agentproto/store\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeStoreApp`),\n * hence its resource uri `ui://agentproto_store/view`. Follows the\n * convention of every other builtin panel (`agentproto_work_board`,\n * `agentproto_sessions`, …): `agentproto_<slug>`. */\nexport const STORE_TOOL_ID = \"agentproto_store\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const STORE_UI_TOOLS = [\n \"app_catalog\",\n \"app_list\",\n \"app_install\",\n \"app_resync\",\n \"app_updates\",\n \"app_uninstall\",\n \"app_status\",\n] as const\n\nexport const STORE_HTML = STORE_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto App Store panel — the\n * browse/install surface over `app_catalog`, `app_list`, `app_updates`\n * (+ apply verbs `app_install`/`app_resync`/`app_uninstall`), mounted by\n * the daemon in builtin-apps.ts (`makeBuiltinPanelApps`) exactly like\n * work-board.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). Host wiring: runtime's\n * builtin-apps.ts assembles the mounted list, with this factory closing\n * over the store's LIVE read paths (`listCatalog` is `dispatchTool\n * (\"app_catalog\")`-equivalent, `listInstalled` reads the live\n * AppRegistry) — so the store's execute() answer and what the same verbs\n * return can't drift.\n *\n * Install CONFIRMATION: the panel NEVER calls `app_install` silently.\n * Through the ui.tools bridge the FIRST call (no `confirm`) comes back a\n * preview (`{needsConfirmation: true, url, sha256|sha, runsBuildCommand}`);\n * the SECOND call repeats the same payload PLUS the returned `confirm`\n * token. Contained in the panel's dialog (ui/main.ts). Direct MCP/CLI\n * callers skip this entirely: the guard's preview costs them an extra\n * call only when they pass it, never a behavioural change (the guard\n * triggers on the panel's ui.tools tool-call path only — see\n * app-tools.ts's `appInstallConfirmation`).\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n STORE_APP_ID,\n STORE_HTML,\n STORE_TOOL_ID,\n STORE_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const storeInputSchema = z.object({\n catalogUrl: z\n .string()\n .optional()\n .describe(\n \"Restrict the snapshot to ONE catalog source (`catalog.sources[i].url`). \" +\n \"Omit for the merged listing app_catalog itself answers.\",\n ),\n})\n\nexport type StoreInput = z.infer<typeof storeInputSchema>\n\n/** The store's own read paths — mirrored off what the SAME MCP verbs\n * (`app_catalog`, `app_list`, `app_updates`) answer, so the panel's\n * snapshot can never disagree with the returned rows. Generic over the\n * row shape (this package doesn't own those record types — they live in\n * @agentproto/runtime's app-registry / app-catalog) so the host supplies\n * the concrete type argument when wiring, mirroring\n * `WorkBoardOps<TTask>`/`SessionsPanelOps<TSession>`. */\nexport interface StoreOps<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n listCatalog(catalogUrl?: string): Promise<ReadonlyArray<TCatalogRow>>\n listInstalled(): ReadonlyArray<TInstalledRow>\n listUpdates(): ReadonlyArray<TUpdateRow>\n}\n\nexport interface StoreOutput<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n /** `app_catalog`'s entries — featured/uninstalled ones grouped into the\n * Available shelf by `category`; `updateAvailable` rows read the\n * Update-available badge. */\n catalog: ReadonlyArray<TCatalogRow>\n /** `app_list`'s installed records — the Installed shelf. */\n installed: ReadonlyArray<TInstalledRow>\n /** `app_updates`' update entries — empty when nothing tracked is stale. */\n updates: ReadonlyArray<TUpdateRow>\n /** Warnings from the catalog sources (a failing source becomes a\n * warning, never an error — app-tools.ts's app_catalog contract). */\n warnings?: readonly string[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeStoreApp<\n TCatalogRow = unknown,\n TInstalledRow = unknown,\n TUpdateRow = unknown,\n>(ops: StoreOps<TCatalogRow, TInstalledRow, TUpdateRow>): AgnoMcpApp<StoreInput, StoreOutput<TCatalogRow, TInstalledRow, TUpdateRow>> {\n return {\n id: STORE_TOOL_ID,\n title: \"App Store\",\n description:\n \"Open the agentproto App Store — browse/install apps from the daemon's \" +\n \"app_catalog, resync and uninstall installed ones. Sections: Installed \" +\n \"(with an Update-available badge) / Featured+Available from the catalog / \" +\n \"Install-from-URL / Sources / Builtin panels (the `?install=<appId>` \" +\n \"deep link pre-opens an entry's install confirmation).\",\n inputSchema: storeInputSchema,\n execute: async input => {\n const rows = await ops.listCatalog(input.catalogUrl)\n const warnings: string[] = []\n const catalog = rows.filter(row => {\n const w = (row as { warnings?: readonly string[] }).warnings\n if (w) warnings.push(...(w as readonly string[]))\n return true\n })\n return {\n catalog,\n installed: ops.listInstalled(),\n updates: ops.listUpdates(),\n ...(warnings.length > 0 ? { warnings } : {}),\n }\n },\n html: STORE_HTML,\n }\n}\n\nexport const storeApp: AppHandle = defineApp({\n id: STORE_APP_ID,\n name: \"App Store\",\n description:\n \"Open the agentproto App Store — installed apps, catalog shelves (featured/available), \" +\n \"install-from-URL with an explicit confirmation, sources and builtin panels.\",\n agents: [],\n ui: {\n html: STORE_HTML,\n title: \"App Store\",\n tools: [...STORE_UI_TOOLS],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/review-panel/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-review-panel-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:reviews\n *\n * `pnpm --filter @agentproto/apps run build:ui:reviews:check` (wired into\n * CI) fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const REVIEW_PANEL_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto reviews</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-review-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function liveSessionUrl(origin, sessionId) {\\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\\n }\\n function shortRange(baseSha, headSha) {\\n if (!baseSha || !headSha) return \\\"—\\\";\\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\\n }\\n function ageOf(createdAt, now = Date.now()) {\\n const ms = now - new Date(createdAt).getTime();\\n if (!Number.isFinite(ms) || ms < 0) return \\\"just now\\\";\\n const mins = Math.floor(ms / 6e4);\\n if (mins < 1) return \\\"just now\\\";\\n if (mins < 60) return `${mins}m`;\\n const hours = Math.floor(mins / 60);\\n if (hours < 24) return `${hours}h`;\\n return `${Math.floor(hours / 24)}d`;\\n }\\n function verdictChip(status) {\\n return `<span class=\\\"chip chip-verdict v-${esc(status)}\\\">${esc(status)}</span>`;\\n }\\n const LANE_STATUS_LABEL = {\\n pass: \\\"pass\\\",\\n fail: \\\"fail\\\",\\n skipped: \\\"skipped\\\",\\n timeout: \\\"timeout\\\"\\n };\\n function laneStatusChip(status) {\\n return `<span class=\\\"chip chip-lane l-${esc(status)}\\\">${esc(LANE_STATUS_LABEL[status])}</span>`;\\n }\\n function severityTag(severity) {\\n return `<span class=\\\"tag sev-${esc(severity)}\\\">${esc(severity)}</span>`;\\n }\\n function requesterLabel(row) {\\n const r = row.requester;\\n if (!r) return \\\"—\\\";\\n const id = r.sessionId ? esc(r.sessionId) : \\\"\\\";\\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \\\"\\\";\\n if (id && author) return `${id} <span class=\\\"muted\\\">(${author})</span>`;\\n return id || author || \\\"—\\\";\\n }\\n function prLabel(row) {\\n if (!row.pr) return \\\"—\\\";\\n const state = row.prState ? ` <span class=\\\"tag pr-${esc(row.prState)}\\\">${esc(row.prState)}</span>` : \\\"\\\";\\n return `<a href=\\\"${esc(row.pr.url)}\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">#${row.pr.number}</a>${state}`;\\n }\\n function renderRow(row) {\\n const status = row.status ?? row.verdict ?? \\\"incomplete\\\";\\n const flags = [];\\n if (row.dirty) flags.push('<span class=\\\"tag t-dirty\\\">dirty</span>');\\n if (row.cached) flags.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (row.status !== \\\"running\\\") {\\n flags.push(row.signed ? '<span class=\\\"tag t-signed\\\">signed</span>' : '<span class=\\\"tag t-unsigned\\\">unsigned</span>');\\n }\\n return `<tr class=\\\"row\\\" data-runid=\\\"${esc(row.runId)}\\\" tabindex=\\\"0\\\"><td>${verdictChip(status)}</td><td class=\\\"mono\\\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \\\"—\\\")}</td><td class=\\\"mono\\\">${esc(row.repoRemote ?? \\\"—\\\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\\\" \\\") || \\\"—\\\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\\n }\\n function renderList(rows2) {\\n if (rows2.length === 0) return '<div class=\\\"empty\\\">No reviews recorded yet.</div>';\\n const header = \\\"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\\\";\\n return `<table class=\\\"list\\\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\\\"\\\")}</tbody></table>`;\\n }\\n function findingLine(f) {\\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \\\"\\\"}` : \\\"\\\";\\n return `<details class=\\\"finding\\\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\\\"mono muted\\\">${loc}</span>` : \\\"\\\"}</summary><div class=\\\"finding-detail\\\">${esc(f.detail)}</div></details>`;\\n }\\n function renderLaneDetail(lane, rubrics = []) {\\n const parts = [];\\n parts.push(`<div class=\\\"lane-hdr\\\">`);\\n parts.push(`<span class=\\\"lane-id\\\">${esc(lane.id)}</span>`);\\n parts.push(laneStatusChip(lane.status));\\n parts.push(`<span class=\\\"tag ${lane.blocking ? \\\"t-blocking\\\" : \\\"t-advisory\\\"}\\\">${lane.blocking ? \\\"blocking\\\" : \\\"advisory\\\"}</span>`);\\n if (lane.durationMs !== void 0) parts.push(`<span class=\\\"muted\\\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\\n parts.push(`</div>`);\\n if (lane.error) parts.push(`<div class=\\\"lane-error\\\">${esc(lane.error)}</div>`);\\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\\n const rubric = rubrics.find((r) => r.check === lane.id);\\n const bits = [];\\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\\n if (lane.fallbacks?.length) {\\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\\\", \\\")}`);\\n }\\n if (rubric) bits.push(`rubric: <span class=\\\"mono\\\">${esc(rubric.sha256.slice(0, 12))}</span>`);\\n if (lane.sessionId) {\\n bits.push(\\n `reviewer: <button class=\\\"sess-link\\\" data-session-id=\\\"${esc(lane.sessionId)}\\\">${esc(lane.sessionId)}</button>`\\n );\\n }\\n parts.push(`<div class=\\\"lane-agent-meta\\\">${bits.join(\\\" · \\\")}</div>`);\\n }\\n if (lane.findings.length > 0) parts.push(`<div class=\\\"findings\\\">${lane.findings.map(findingLine).join(\\\"\\\")}</div>`);\\n return `<div class=\\\"lane\\\">${parts.join(\\\"\\\")}</div>`;\\n }\\n function renderDetail(detail2) {\\n const status = detail2.status === \\\"done\\\" ? detail2.verdict ?? \\\"incomplete\\\" : detail2.status;\\n const header = [`<div class=\\\"detail-hdr\\\">`, verdictChip(status), `<span class=\\\"mono\\\">${esc(detail2.runId)}</span>`];\\n if (detail2.binding) header.push(`<span class=\\\"tag\\\">${esc(detail2.binding)}</span>`);\\n if (detail2.cached) header.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (detail2.status === \\\"done\\\" && detail2.attestation) {\\n const sig = detail2.attestation.attestor?.signature;\\n header.push(\\n sig ? `<span class=\\\"tag t-signed\\\" title=\\\"${esc(sig.keyFingerprint)}\\\">signed: ${esc(sig.principal)}</span>` : '<span class=\\\"tag t-unsigned\\\">unsigned</span>'\\n );\\n }\\n header.push(`</div>`);\\n if (detail2.error) header.push(`<div class=\\\"detail-error\\\">${esc(detail2.error)}</div>`);\\n if (detail2.supersededBy) header.push(`<div class=\\\"muted\\\">superseded by ${esc(detail2.supersededBy)}</div>`);\\n const fullLanes = detail2.attestation?.lanes;\\n const rubrics = detail2.attestation?.rubrics ?? [];\\n let body;\\n if (fullLanes) {\\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\\\"\\\");\\n } else if (detail2.lanes) {\\n body = detail2.lanes.map((l) => `<div class=\\\"lane\\\"><div class=\\\"lane-hdr\\\"><span class=\\\"lane-id\\\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\\\"\\\");\\n } else {\\n body = \\\"\\\";\\n }\\n return `<div class=\\\"detail\\\">${header.join(\\\"\\\")}${body}</div>`;\\n }\\n const POLL_RUNNING_MS = 4e3;\\n const POLL_IDLE_MS = 15e3;\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`review panel: missing #${id}`);\\n return el;\\n }\\n let rows = [];\\n let selectedRunId = null;\\n let detail = null;\\n let pollTimer = null;\\n let loadInFlight = false;\\n function boot() {\\n const listPaneEl = getEl(\\\"list-pane\\\");\\n const detailPaneEl = getEl(\\\"detail-pane\\\");\\n const detailBodyEl = getEl(\\\"detail-body\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const cancelBtn = getEl(\\\"cancel-btn\\\");\\n const rerunBtn = getEl(\\\"rerun-btn\\\");\\n const prBtn = getEl(\\\"pr-btn\\\");\\n const exportBtn = getEl(\\\"export-btn\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function findRow(runId) {\\n return rows.find((r) => r.runId === runId);\\n }\\n function renderListPane() {\\n listPaneEl.innerHTML = renderList(rows);\\n if (!selectedRunId) return;\\n listPaneEl.querySelectorAll(\\\"tr[data-runid]\\\").forEach((tr) => {\\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\\\"selected\\\");\\n });\\n }\\n function renderDetailPane() {\\n if (!selectedRunId || !detail) {\\n detailPaneEl.classList.remove(\\\"open\\\");\\n return;\\n }\\n detailPaneEl.classList.add(\\\"open\\\");\\n detailBodyEl.innerHTML = renderDetail(detail);\\n const running = detail.status === \\\"running\\\";\\n cancelBtn.disabled = !running;\\n cancelBtn.style.display = running ? \\\"\\\" : \\\"none\\\";\\n detailBodyEl.querySelectorAll(\\\".sess-link\\\").forEach((btn) => {\\n btn.addEventListener(\\\"click\\\", () => openSession(btn.dataset.sessionId ?? \\\"\\\"));\\n });\\n }\\n function openSession(sessionId) {\\n if (!sessionId) return;\\n const origin = window.location.origin && window.location.origin !== \\\"null\\\" ? window.location.origin : \\\"\\\";\\n const url = liveSessionUrl(origin, sessionId);\\n const caps = getHostCapabilities();\\n if (caps?.openLinks) {\\n openLink(url).catch(() => window.open(url, \\\"_blank\\\"));\\n } else {\\n window.open(url, \\\"_blank\\\");\\n }\\n }\\n function loadList() {\\n if (loadInFlight) return Promise.resolve();\\n loadInFlight = true;\\n return callTool(\\\"review_ledger\\\", { includeRunning: true }).then((result) => {\\n rows = result.attestations;\\n renderListPane();\\n const runningCount = rows.filter((r) => r.status === \\\"running\\\").length;\\n setStatus(`${result.total} review${result.total === 1 ? \\\"\\\" : \\\"s\\\"} · ${runningCount} running · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\\n return void 0;\\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\\n loadInFlight = false;\\n });\\n }\\n function loadDetail(runId) {\\n return callTool(\\\"review_status\\\", { runId }).then((result) => {\\n detail = result;\\n renderDetailPane();\\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\\n }\\n function openDetail(runId) {\\n selectedRunId = runId;\\n renderListPane();\\n void loadDetail(runId);\\n }\\n function closeDetail() {\\n selectedRunId = null;\\n detail = null;\\n renderListPane();\\n renderDetailPane();\\n }\\n listPaneEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n listPaneEl.addEventListener(\\\"keydown\\\", (evt) => {\\n if (evt.key !== \\\"Enter\\\" && evt.key !== \\\" \\\") return;\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n evt.preventDefault();\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n getEl(\\\"detail-close\\\").addEventListener(\\\"click\\\", closeDetail);\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadList();\\n });\\n cancelBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_cancel\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Cancelled.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\\n });\\n rerunBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n const row = findRow(selectedRunId);\\n const cwd = row?.cwd;\\n if (!cwd) {\\n setStatus(\\\"Re-run failed: this row has no known checkout path.\\\");\\n return;\\n }\\n callTool(\\\"review_run\\\", {\\n cwd,\\n ...row?.binding ? { binding: row.binding } : {},\\n nocache: true,\\n wait: false,\\n supersede: true\\n }).then(() => {\\n setStatus(\\\"Re-run started.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\\n });\\n prBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_pr\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Fetched PR status.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\\n });\\n exportBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_export\\\", { runId: selectedRunId }).then((result) => {\\n setStatus(result.path ? `Exported to ${result.path}` : \\\"Exported.\\\");\\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\\n });\\n function doPoll() {\\n if (pollTimer) clearTimeout(pollTimer);\\n loadList().finally(() => {\\n const anyRunning = rows.some((r) => r.status === \\\"running\\\");\\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\\n });\\n }\\n initBridge().then(() => loadList()).then(() => {\\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets — same scheme as work-board's\\n style.css (see that file's header doc for the two-mapping-sheet\\n rationale): every rule below reads ONLY the --rp-* custom properties,\\n each resolving through a live --vscode-* var() first, then the\\n agentproto brand fallback.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --rp-border: var(--vscode-panel-border, #dde0da);\\n --rp-accent: var(--vscode-button-background, #0f62fe);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --rp-bg: var(--vscode-editor-background, #101214);\\n --rp-surface: var(--vscode-sideBar-background, #16191d);\\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-border: var(--vscode-panel-border, #272c33);\\n --rp-accent: var(--vscode-button-background, #4589ff);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--rp-bg);\\n color: var(--rp-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.muted { color: var(--rp-ink-muted); }\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--rp-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}\\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\\n.tbtn {\\n background: var(--rp-bg);\\n border: 1px solid var(--rp-border);\\n color: var(--rp-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--rp-border); }\\n.tbtn:disabled { opacity: 0.5; cursor: default; }\\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\\n\\n#main { flex: 1; overflow: hidden; display: flex; }\\n#list-pane { flex: 1; overflow-y: auto; }\\n#detail-pane {\\n width: 420px;\\n min-width: 320px;\\n border-left: 1px solid var(--rp-border);\\n overflow-y: auto;\\n display: none;\\n flex-direction: column;\\n}\\n#detail-pane.open { display: flex; }\\n#detail-close { align-self: flex-end; margin: 6px; }\\n\\ntable.list { width: 100%; border-collapse: collapse; }\\ntable.list th {\\n text-align: left;\\n font-size: 10px;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n color: var(--rp-ink-muted);\\n padding: 6px 8px;\\n border-bottom: 1px solid var(--rp-border);\\n position: sticky;\\n top: 0;\\n background: var(--rp-surface);\\n}\\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\\ntr.row { cursor: pointer; }\\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\\n\\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\\n\\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\\n\\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n\\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\\n\\n.lane {\\n border: 1px solid var(--rp-border);\\n border-radius: 6px;\\n padding: 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\\n.lane-id { font-weight: 600; font-size: 12px; }\\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\\n.sess-link {\\n background: none;\\n border: none;\\n color: var(--rp-accent);\\n cursor: pointer;\\n font: inherit;\\n padding: 0;\\n text-decoration: underline;\\n}\\n\\n.findings { display: flex; flex-direction: column; gap: 4px; }\\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\\n.finding summary::-webkit-details-marker { display: none; }\\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--rp-ink-muted);\\n border-top: 1px solid var(--rp-border);\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"toolbar-label\\\">Reviews</span>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n </div>\\n <div id=\\\"main\\\">\\n <div id=\\\"list-pane\\\"></div>\\n <div id=\\\"detail-pane\\\">\\n <button class=\\\"tbtn\\\" id=\\\"detail-close\\\">× Close</button>\\n <div id=\\\"detail-body\\\"></div>\\n <div class=\\\"detail-actions\\\">\\n <button class=\\\"tbtn danger\\\" id=\\\"cancel-btn\\\">Cancel</button>\\n <button class=\\\"tbtn\\\" id=\\\"rerun-btn\\\">Re-run fresh</button>\\n <button class=\\\"tbtn\\\" id=\\\"pr-btn\\\">Fetch PR status</button>\\n <button class=\\\"tbtn\\\" id=\\\"export-btn\\\">Export</button>\\n </div>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto review-panel — the `REVIEW_PANEL_HTML`\n * served as the `ui://agentproto_reviews/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_reviews` tool defined in ./index.ts.\n *\n * A verdict panel over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts` + `review-runner.ts`'s in-flight\n * runs) — list view (running first, then newest) and a per-run detail view\n * (lanes, findings, agent-lane provenance).\n *\n * `REVIEW_PANEL_HTML` is a real single-file Vite build, not a hand-authored\n * template string — same technique as work-board (see that package's\n * ./panel.ts docblock): the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui:reviews`\n * (scripts/build-review-panel-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:reviews:check` (wired into CI) fails if that generated file\n * drifts from what the Vite sources actually produce.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts)\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → review_ledger (`includeRunning: true`) on a poll\n * that speeds up to ~4s while any row is running, ~15s otherwise; a\n * selected row's detail comes from review_status (runId)\n * – Actions: tools/call → review_cancel / review_run (nocache + wait:false\n * + supersede, \"Re-run fresh\") / review_pr / review_export — real tool\n * names only, no second write path\n * – Deep link: an agent lane's reviewer-session id opens (openLink, or\n * window.open as a fallback) `/apps/@agentproto/live-session/ui\n * ?sessionId=<id>` — runtime's `handleAppUiPage` validates and bakes\n * that query param into the live-session widget's initData\n * (builtin-apps.ts's `isValidDeepLinkSessionId`), so it boots already\n * pinned to the exact reviewer session instead of self-discovering the\n * newest running one. See ./ui/render.ts's `liveSessionUrl`.\n *\n * The verdict/status chip (never render a running/cancelled run as if it\n * had a verdict): pass/block/incomplete/running/cancelled/failed, one\n * visual language everywhere it appears — see ./ui/render.ts's `verdictChip`.\n */\n\nimport { REVIEW_PANEL_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph can still name the panel from source (mirrors\n * work-board/panel.ts's `WORK_BOARD_APP_ID`). */\nexport const REVIEW_PANEL_APP_ID = \"@agentproto/review-panel\"\n\n/** The MCP tool the panel is registered under (./index.ts\n * `makeReviewPanelApp`), hence its resource uri `ui://agentproto_reviews/view`. */\nexport const REVIEW_PANEL_TOOL_ID = \"agentproto_reviews\"\n\n/** The daemon tools this panel's html calls, in order of first use. Consumed\n * as `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly. The reviewer-session\n * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/\n * `window.open` navigation, not a tools/call, so it needs no entry here. */\nexport const REVIEW_PANEL_UI_TOOLS = [\n \"review_ledger\",\n \"review_status\",\n \"review_cancel\",\n \"review_run\",\n \"review_pr\",\n \"review_export\",\n] as const\n\nexport const REVIEW_PANEL_HTML = REVIEW_PANEL_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto review verdict panel — a list +\n * detail view over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts`) and its in-flight runs\n * (`review-runner.ts`'s `ReviewRunner.list()`).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape, no\n * @agstudio/mcp-apps / Mastra dependency), same as sessions-panel /\n * work-board — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_reviews` is called by the host with the optional\n * `{cwd, repoRemote, runId, requesterSessionId}` scoping input.\n * 2. execute() answers an initial `review_ledger`-shaped snapshot\n * (`includeRunning: true`) via `ops.listReviews` — the SAME body\n * `review_ledger` itself runs (`reviewLedgerView`, review-tools.ts),\n * so a non-interactive caller sees exactly what the tool would return.\n * 3. The HTML panel — a static work-board-style Vite build, so it does\n * NOT thread this execute() input through — opens its own bridge and\n * polls `review_ledger({includeRunning: true})` unscoped (mirrors\n * work-board's own boardId-from-execute() omission), then\n * `review_status(runId)` for whichever row is selected. Actions go\n * through `review_cancel`/`review_run`/`review_pr`/`review_export` —\n * real tool names only, no second write path.\n *\n * This module also exports `reviewPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeReviewPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over the LIVE ledger/runner read,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { REVIEW_PANEL_APP_ID, REVIEW_PANEL_HTML, REVIEW_PANEL_TOOL_ID, REVIEW_PANEL_UI_TOOLS } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const reviewPanelInputSchema = z.object({\n cwd: z.string().optional().describe(\"A directory inside a git repo — scopes the initial snapshot to its remote.\"),\n repoRemote: z.string().optional().describe(\"Normalized repo remote (e.g. github.com/agentproto/ts).\"),\n runId: z.string().optional().describe(\"A specific run id known at open time (informational on the initial snapshot).\"),\n requesterSessionId: z\n .string()\n .optional()\n .describe(\"Scope the initial snapshot to reviews requested by this session.\"),\n})\n\nexport type ReviewPanelInput = z.infer<typeof reviewPanelInputSchema>\n\nexport interface ReviewPanelOutput {\n total: number\n attestations: Record<string, unknown>[]\n /** Echoed back from the input — the row this snapshot's caller already\n * had in mind, when known. Not consumed by the panel's own client JS\n * (see this file's docblock on why the static build doesn't thread it). */\n focusRunId?: string\n}\n\n/**\n * Generic host wiring for `makeReviewPanelApp` — the daemon supplies\n * `listReviews` bound to `reviewLedgerView(reviewRunner, ...)`\n * (review-tools.ts), the exact function `review_ledger` itself calls, so\n * the panel's initial snapshot and the tool's own output can never drift\n * apart into two implementations.\n */\nexport interface ReviewPanelOps {\n listReviews(input: ReviewPanelInput): Promise<ReviewPanelOutput> | ReviewPanelOutput\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger/runner read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeWorkBoardApp.\n */\nexport function makeReviewPanelApp(ops: ReviewPanelOps): AgnoMcpApp<ReviewPanelInput, ReviewPanelOutput> {\n return {\n id: REVIEW_PANEL_TOOL_ID,\n title: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list (running reviews first, then \" +\n \"newest) over the daemon's review ledger, with a per-run detail view (lanes, \" +\n \"findings, agent-lane provenance) and actions (cancel, re-run fresh, fetch PR \" +\n \"status, export). Pass `cwd`/`repoRemote`/`requesterSessionId` to scope the initial \" +\n \"snapshot; the panel's own live view re-polls unscoped.\",\n inputSchema: reviewPanelInputSchema,\n execute: async input => {\n const snapshot = await ops.listReviews(input)\n return { ...snapshot, ...(input.runId ? { focusRunId: input.runId } : {}) }\n },\n html: REVIEW_PANEL_HTML,\n }\n}\n\nexport const reviewPanelApp: AppHandle = defineApp({\n id: REVIEW_PANEL_APP_ID,\n name: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list + detail view over the daemon's \" +\n \"review ledger, with cancel / re-run / PR-status / export actions.\",\n agents: [],\n ui: {\n html: REVIEW_PANEL_HTML,\n title: \"Reviews\",\n tools: [...REVIEW_PANEL_UI_TOOLS],\n },\n})\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/code-team/agents/implementer.ts","../src/code-team/agents/reviewer.ts","../src/code-team/agents/fixer.ts","../src/code-team/workflows/deliver-change.ts","../src/code-team/index.ts","../src/content-team/agents/researcher.ts","../src/content-team/agents/writer.ts","../src/content-team/agents/editor.ts","../src/content-team/agents/illustrator.ts","../src/content-team/workflows/produce-content.ts","../src/content-team/workflows/produce-cover.ts","../src/content-team/index.ts","../src/mail-triage/agents/triager.ts","../src/mail-triage/workflows/triage-inbox.ts","../src/mail-triage/ui.ts","../src/mail-triage/index.ts","../src/media-viewer/agents/cataloger.ts","../src/media-viewer/workflows/scan-media.ts","../src/media-viewer/ui.ts","../src/media-viewer/index.ts","../src/ops-panel/agents/manager.ts","../src/ops-panel/agents/watchdog.ts","../src/ops-panel/ui.ts","../src/ops-panel/index.ts","../src/config/fragment.ts","../src/config/ui.ts","../src/config/index.ts","../src/session-viewer/agents/narrator.ts","../src/session-viewer/workflows/narrate-session.ts","../src/session-viewer/ui.ts","../src/session-viewer/index.ts","../src/panel-bridge.ts","../src/sessions-panel/panel.ts","../src/sessions-panel/index.ts","../src/agents-overview/panel.ts","../src/agents-overview/index.ts","../src/bureau-sessions/panel.ts","../src/bureau-sessions/index.ts","../src/session-story/panel.ts","../src/session-story/index.ts","../src/live-session/panel.ts","../src/live-session/index.ts","../src/session-chat/blob-embed.ts","../src/session-chat/panel.ts","../src/session-chat/index.ts","../src/work-board/panel.generated.ts","../src/work-board/panel.ts","../src/work-board/index.ts","../src/store/panel.generated.ts","../src/store/panel.ts","../src/store/index.ts","../src/review-panel/panel.generated.ts","../src/review-panel/panel.ts","../src/review-panel/index.ts"],"names":["defineAgent","defineWorkflow","defineApp","APP_ID","SECTIONS","z"],"mappings":"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AA,CAAK,SAAA,CAAU,uBAAuB,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AA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AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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AAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;ACxG9B,IAAM,gBAA2BD,SAAAA,CAAU;AAAA,EAChD,EAAA,EAAI,4BAAA;AAAA,EACJ,IAAA,EAAM,gBAAA;AAAA,EACN,OAAA,EAAS,OAAA;AAAA,EACT,WAAA,EAAa,qFAAA;AAAA,EACb,MAAA,EAAQ,CAAC,QAAQ,CAAA;AAAA,EACjB,SAAA,EAAW,CAAC,cAAc,CAAA;AAAA,EAC1B,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,mBAAA;AAAA,IACN,KAAA,EAAO,gBAAA;AAAA,IACP,KAAA,EAAO,CAAC,GAAG,oBAAoB;AAAA;AAEnC,CAAC;AC8DM,SAAS,kBAAkB,OAAA,EAAyB;AACzD,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA,EAkFa,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwC3C,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,GAAA,CAAA;AAM1B;;;ACtMO,IAAM,UAAA,GAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2ExB,iBAAA,CAAkB,2BAA2B,CAAC;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;ACvEzC,IAAM,wBAAA,GAA2B,EAAE,MAAA,CAAO;AAAA,EAC/C,MAAA,EAAQ,EACL,IAAA,CAAK,CAAC,WAAW,KAAK,CAAC,CAAA,CACvB,QAAA,EAAS,CACT,QAAA;AAAA,IACC;AAAA;AAEN,CAAC,CAAA;AAqBM,SAAS,qBACd,GAAA,EACqD;AACrD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,qBAAA;AAAA,IACJ,KAAA,EAAO,gBAAA;AAAA,IACP,WAAA,EACE,wTAAA;AAAA,IAKF,WAAA,EAAa,wBAAA;AAAA,IACb,OAAA,EAAS,OAAO,KAAA,MAAW;AAAA,MACzB,QAAA,EAAU,GAAA,CAAI,YAAA,CAAa,KAAA,CAAM,MAAM;AAAA,KACzC,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,mBAA8BA,SAAAA,CAAU;AAAA,EACnD,EAAA,EAAI,4BAAA;AAAA,EACJ,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EACE,oIAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,gBAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAA,EAAgB,cAAc;AAAA;AAE1C,CAAC;;;ACtEM,IAAM,oBAAA,GAAuB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuDlC,iBAAA,CAAkB,4BAA4B,CAAC;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;AClC1C,IAAM,yBAAA,GAA4BG,EAAE,MAAA,CAAO;AAAA,EAChD,MAAA,EAAQA,CAAAA,CACL,IAAA,CAAK,CAAC,SAAA,EAAW,KAAK,CAAC,CAAA,CACvB,QAAA,EAAS,CACT,QAAA,CAAS,4EAA4E;AAC1F,CAAC,CAAA;AAaM,SAAS,sBACd,GAAA,EACiE;AACjE,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,4BAAA;AAAA,IACJ,KAAA,EAAO,0BAAA;AAAA,IACP,WAAA,EACE,mQAAA;AAAA,IAIF,WAAA,EAAa,yBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,MAAU,EAAE,UAAU,GAAA,CAAI,YAAA,CAAa,KAAA,CAAM,MAAM,CAAA,EAAE,CAAA;AAAA,IACpE,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,oBAA+BH,SAAAA,CAAU;AAAA,EACpD,EAAA,EAAI,6BAAA;AAAA,EACJ,IAAA,EAAM,0BAAA;AAAA,EACN,WAAA,EACE,2KAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,oBAAA;AAAA,IACN,KAAA,EAAO,0BAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAA,EAAgB,mBAAmB;AAAA;AAE/C,CAAC;;;ACxEM,IAAM,oBAAA,GAAuB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuDlC,iBAAA,CAAkB,4BAA4B,CAAC;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;ACpC1C,IAAM,yBAAA,GAA4BG,EAAE,MAAA,CAAO;AAAA,EAChD,MAAA,EAAQA,CAAAA,CACL,IAAA,CAAK,CAAC,SAAA,EAAW,KAAK,CAAC,CAAA,CACvB,QAAA,EAAS,CACT,QAAA,CAAS,8EAA8E;AAC5F,CAAC,CAAA;AAaM,SAAS,sBACd,GAAA,EACiE;AACjE,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,4BAAA;AAAA,IACJ,KAAA,EAAO,mCAAA;AAAA,IACP,WAAA,EACE,sIAAA;AAAA,IAEF,WAAA,EAAa,yBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,MAAU;AAAA,MACvB,QAAA,EAAU,GAAA,CAAI,YAAA,CAAa,KAAA,CAAM,MAAM,EAAE,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,KAAS,SAAS;AAAA,KAC3E,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,oBAA+BH,SAAAA,CAAU;AAAA,EACpD,EAAA,EAAI,6BAAA;AAAA,EACJ,IAAA,EAAM,mCAAA;AAAA,EACN,WAAA,EACE,+GAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,oBAAA;AAAA,IACN,KAAA,EAAO,mCAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAc;AAAA;AAE1B,CAAC;;;AC/CM,IAAM,wBAAA,GAA2B,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA,EA8StC,iBAAA,CAAkB,gCAAgC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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AA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;A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A;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;A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","file":"index.mjs","sourcesContent":["import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Writes the requested change. */\nexport const implementer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/implementer\",\n description: \"Implements the requested code change.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Change only what the task asks\", \"Match the surrounding style\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You implement the requested change. Read the task and the surrounding code \" +\n \"first, make the smallest correct change that satisfies it, then run the \" +\n \"tests. Do not touch anything the task didn't ask for.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Reviews the diff; reports only. */\nexport const reviewer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/reviewer\",\n description: \"Reviews the diff and reports findings.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Report findings only\", \"Never edit files\", \"Never run gh pr merge\"],\n // Read-only + run_command (to inspect the diff / run tests). No write/edit\n // tools — the \"never edit files\" boundary is enforced by capability too.\n tools: [\"list_dir\", \"read_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You are a rigorous reviewer. Read the diff, and report each real issue as \" +\n \"`file:line — why`. Change nothing; you only report.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Applies the reviewer's findings, precisely and minimally. */\nexport const fixer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/fixer\",\n description: \"Applies the reviewer's findings, precisely and minimally.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Apply only the listed findings\", \"No unrelated refactors\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\", \"run_command\"],\n workflows: [{ ref: \"deliver-change\" }],\n }),\n body:\n \"You apply the reviewer's findings. Address each listed finding exactly, add \" +\n \"nothing extra, and re-run the tests when done.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the code team delivers a change through. Every team agent\n * references it; the app-kit attachment invariant keeps that honest.\n *\n * Three declarative `kind:\"agent\"` steps — implementer → reviewer → fixer —\n * one per team agent, each `agent.ref` resolving (at compile time, against\n * this app's own install record) to that agent's emitted AGENT.md. This\n * replaces an earlier `ToolStep`-based draft (`apply_patch`/`read_diff`/\n * `run_tests`) that dispatched daemon-side, where those tool ids don't\n * exist — the daemon has no `apply_patch` tool, only live agent sessions\n * that can edit files. Agent steps are the adoption fix (WP-B4): each step\n * spawns the named team agent as a real session and lets it act.\n */\nexport const deliverChange = defineWorkflow({\n id: \"deliver-change\",\n name: \"Deliver a code change\",\n description: \"Implement the change, review the diff, apply fixes, verify.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"implement\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/implementer\" },\n prompt: \"Implement the requested change.\",\n },\n {\n id: \"review\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/reviewer\" },\n prompt:\n \"Review the diff the implementer just produced. Report every real issue as `file:line — why`.\",\n },\n {\n id: \"fix\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/fixer\" },\n prompt: \"Apply the reviewer's findings, then re-run the tests.\",\n },\n ],\n})\n","/**\n * `code-team` — a ready-made agentproto app: a small team that ships a code\n * change. Three agents (implementer → reviewer → fixer) bound to one delivery\n * workflow, declared with `@agentproto/app-kit`.\n *\n * Generic on purpose — `@agentproto/…` ids, no product dependency, no home\n * workspace. A host imports it and uses any subset of its agents/workflows:\n * in-process via `codeTeam.toMastraAgents(...)`, or on disk via\n * `codeTeam.emit(dir)`.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { implementer } from \"./agents/implementer.js\"\nimport { reviewer } from \"./agents/reviewer.js\"\nimport { fixer } from \"./agents/fixer.js\"\nimport { deliverChange } from \"./workflows/deliver-change.js\"\n\nexport const codeTeam: AppHandle = defineApp({\n id: \"@agentproto/code-team\",\n name: \"Code Team\",\n version: \"0.1.0\",\n description: \"Implement, review, and fix code changes.\",\n agents: [implementer, reviewer, fixer],\n workflows: [deliverChange],\n})\n\nexport { implementer, reviewer, fixer, deliverChange }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Gathers sources for the piece. */\nexport const researcher: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/researcher\",\n description: \"Reads the provided sources and summarizes them for the writer.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Cite every claim\", \"Prefer primary sources\"],\n tools: [\"list_dir\", \"read_file\", \"run_command\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You read the source material already in the workspace (list_dir, then \" +\n \"read_file the sources), extract the key facts, and hand the writer a tight \" +\n \"brief with a citation for every claim. Use run_command only to fetch a URL \" +\n \"already named in the task; do not invent sources.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Drafts the piece from the brief. */\nexport const writer: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/writer\",\n description: \"Drafts the piece from the researcher's brief.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Stay on the brief\", \"One idea per paragraph\"],\n tools: [\"list_dir\", \"read_file\", \"write_file\", \"edit_file\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You draft the piece from the brief and its sources. Lead with the point, \" +\n \"keep one idea per paragraph, and don't invent facts the brief doesn't have.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Tightens the draft. */\nexport const editor: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/editor\",\n description: \"Edits the draft for voice, clarity, and length.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"Preserve the writer's meaning\", \"Cut, don't pad\"],\n tools: [\"read_file\", \"edit_file\"],\n workflows: [{ ref: \"produce-content\" }],\n }),\n body:\n \"You edit the draft. Tighten the voice, cut fluff, fix clarity — without \" +\n \"changing what the writer meant. Leave it shorter than you found it.\",\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Art-directs the cover illustration for each article. */\nexport const illustrator: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/illustrator\",\n description: \"Art-directs the cover illustration for each article.\",\n model: \"claude-sonnet-5\",\n boundaries: [\"One cover per article\", \"Text-free prompts only\"],\n tools: [\"read_file\", \"write_file\", \"run_command\"],\n workflows: [{ ref: \"produce-cover\" }],\n }),\n body:\n \"You art-direct a single cover illustration for the article. \" +\n \"Read the article (read_file) to extract the one visual idea that best \" +\n \"represents the piece, then write the art direction (write_file) as a \" +\n \"clean, text-free image prompt. Keep the visual discipline tight: bold \" +\n \"flat shapes, limited risograph-style palettes, strong negative space, and \" +\n \"a single focal subject. Never include text, lettering, logos, watermarks, \" +\n \"or UI chrome in the prompt. Produce exactly one cover per article; if the \" +\n \"article calls for a literal diagram or chart, fall back to a precise \" +\n \"gpt-image-1 style description and treat the diagram itself as the image \" +\n \"subject. Use run_command only to inspect generated assets or move files \" +\n \"during the upload step.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the content team produces a piece through. Every team\n * agent references it (app-kit attachment invariant).\n *\n * Three declarative `kind:\"agent\"` steps — researcher → writer → editor —\n * one per team agent, each `agent.ref` resolving (at compile time, against\n * this app's own install record) to that agent's emitted AGENT.md. This\n * replaces an earlier `ToolStep`-based draft (`read_file`/`write_file`/\n * `edit_file`/`run_command`) that dispatched daemon-side, where those tool\n * ids don't exist — they're agent workspace tools (adapter-side), never\n * daemon tool ids, so `app_install` rejected them. Agent steps are the\n * adoption fix: each step spawns the named team agent as a real session,\n * which uses its own workspace tools to read, draft, and edit.\n */\nexport const produceContent = defineWorkflow({\n id: \"produce-content\",\n name: \"Produce a piece of content\",\n description: \"Research the sources, draft the piece, then edit it.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"research\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/researcher\" },\n prompt:\n \"Read the source material already in the workspace and hand off a tight \" +\n \"brief with a citation for every claim.\",\n },\n {\n id: \"draft\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/writer\" },\n prompt: \"Draft the piece from the researcher's brief and its sources.\",\n },\n {\n id: \"edit\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/editor\" },\n prompt: \"Edit the draft for voice, clarity, and length, then finalize it.\",\n },\n ],\n})\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/**\n * The one workflow the illustrator uses to produce a cover.\n *\n * A single declarative `kind:\"agent\"` step — `agent.ref` resolving (at\n * compile time, against this app's own install record) to the illustrator's\n * emitted AGENT.md. This replaces an earlier `ToolStep`-based draft\n * (`content.cover-concept`/`content.cover-base-gen`/`content.cover-cleanup`/\n * `content.cover-upload`) naming tools that don't exist anywhere — no driver\n * implements them. The illustrator agent folds all four phases (concept,\n * base generation, cleanup, upload) into one prompt for a single session\n * turn, matching the tools it already has (`read_file`, `write_file`,\n * `run_command`).\n */\nexport const produceCover = defineWorkflow({\n id: \"produce-cover\",\n name: \"Produce a cover illustration\",\n description:\n \"Read the article, derive the cover concept, generate the base image, \" +\n \"clean it up, and upload the final cover.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"illustrate\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/illustrator\" },\n prompt:\n \"Produce the cover for this article in one pass: (1) read the article \" +\n \"and derive the single visual concept that best represents it, (2) \" +\n \"write the art direction as a clean, text-free image prompt and \" +\n \"generate the base image from it, (3) clean up the generated image \" +\n \"(crop, color, artifacts), then (4) upload the final cover to the \" +\n \"workspace's output location.\",\n },\n ],\n})\n","/**\n * `content-team` — a ready-made agentproto app: a small team that produces a\n * piece of content. Three agents (researcher → writer → editor) bound to one\n * production workflow, declared with `@agentproto/app-kit`.\n *\n * Generic on purpose — `@agentproto/…` ids, no product dependency. A host\n * imports it and uses any subset of its agents/workflows.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { researcher } from \"./agents/researcher.js\"\nimport { writer } from \"./agents/writer.js\"\nimport { editor } from \"./agents/editor.js\"\nimport { illustrator } from \"./agents/illustrator.js\"\nimport { produceContent } from \"./workflows/produce-content.js\"\nimport { produceCover } from \"./workflows/produce-cover.js\"\n\nexport const contentTeam: AppHandle = defineApp({\n id: \"@agentproto/content-team\",\n name: \"Content Team\",\n version: \"0.1.0\",\n description: \"Research, draft, edit, and illustrate content.\",\n agents: [researcher, writer, editor, illustrator],\n workflows: [produceContent, produceCover],\n})\n\nexport { researcher, writer, editor, illustrator, produceContent, produceCover }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/** Triages your inbox — finds unread mail, categorizes, labels, archives. */\nexport const triager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/triager\",\n description: \"Triages your inbox — finds unread mail, categorizes, labels, archives.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Always show the triage plan before applying\",\n \"Never delete — only archive, label, or mark read\",\n \"Ask before applying bulk actions on more than 20 messages\",\n ],\n tools: [\n \"mailbox_list\",\n \"mailbox_search\",\n \"mailbox_list_threads\",\n \"mailbox_get_thread\",\n \"mailbox_labels_list\",\n \"mailbox_label_create\",\n \"mailbox_triage_plan\",\n \"mailbox_triage_apply\",\n ],\n workflows: [{ ref: \"triage-inbox\" }],\n }),\n body:\n \"You start by calling mailbox_list to discover connected mailboxes. Pick one \" +\n \"with `capabilities.triage: true` (triage requires the gmail.modify scope). \" +\n \"Every subsequent mailbox tool requires the `mailbox` parameter (UUID or alias \" +\n \"from this list).\\n\" +\n \"\\n\" +\n \"Scan the mailbox for unread messages using mailbox_search with \" +\n 'query=\"is:unread\" and the mailbox from step 1. Categorize them ' +\n \"(urgent, needs-reply, newsletter, notification, spam). Create one triage plan \" +\n \"per bulk action with mailbox_triage_plan — its exact contract is \" +\n '`{ mailbox, criteria: { query } | { messageIds }, action: { type: \"markRead\"|\"archive\"|\"trash\"|\"label\", addLabelIds?, removeLabelIds? } }` ' +\n \"— e.g. label each category, archive newsletters+notifications, keep \" +\n \"urgent+needs-reply in inbox. Each plan returns a `plan_id`, a `count`, and a \" +\n \"`sample`; it expires after 15 minutes. Show the plan before applying. Execute \" +\n \"only after user confirmation, with mailbox_triage_apply \" +\n \"`{ plan_id, confirm: true }` — that is the sole mutation path.\",\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\n/** Single-agent workflow: triage the inbox. */\nexport const triageInbox = defineWorkflow({\n id: \"triage-inbox\",\n name: \"Triage Inbox\",\n description: \"Scan inbox, categorize unread messages, create and apply triage plan.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"triage\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/triager\" },\n prompt:\n \"Call mailbox_list to discover connected mailboxes, then scan the \" +\n \"selected mailbox for unread messages using mailbox_search with \" +\n 'query=\"is:unread\". Categorize them (urgent, needs-reply, newsletter, ' +\n \"notification, spam). Create a triage plan: label each category, archive \" +\n \"newsletters+notifications, keep urgent+needs-reply in inbox. \" +\n \"Show me the plan before applying.\",\n },\n ],\n})\n","/**\n * Mail Triage — a self-contained MCP App dashboard for `mail-triage`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }`. Mailbox access goes\n * through an imported agentpush MCP server (`imported:<alias>/<tool>` ids);\n * running the triager agent itself goes through the daemon's own\n * `app_run` / `app_status` / `agent_output` — a second path alongside the\n * deterministic plan/apply tools, so the panel can either drive the mailbox\n * tools directly or hand the whole job to the agent and watch it work.\n *\n * The agentpush alias is NOT hardcoded to one server: `app_tool_call`\n * enforces an exact-match allowlist (`ui.tools`), so every candidate alias\n * must be baked into `MAIL_TRIAGE_TOOLS` at emit time. The candidate list\n * comes from `MAIL_TRIAGE_MCP_ALIASES` (overridable via the\n * `MAIL_TRIAGE_MCP_ALIASES` env var, comma-separated, read when this module\n * is evaluated — i.e. at emit). At runtime the panel probes each candidate's\n * `mailbox_list` and activates the first alias that answers with mailboxes;\n * a selector appears in the toolbar when several respond.\n */\n\nconst ALIAS_TOOL_NAMES = [\n \"mailbox_list\",\n \"mailbox_search\",\n \"mailbox_triage_plan\",\n \"mailbox_triage_apply\",\n] as const\n\nconst DEFAULT_MCP_ALIASES = [\"agentpush-prod\", \"agentpush\"] as const\n\nfunction envAliases(): string[] | null {\n if (typeof process === \"undefined\" || !process.env) return null\n const raw = process.env[\"MAIL_TRIAGE_MCP_ALIASES\"]\n if (!raw) return null\n const list = raw\n .split(\",\")\n .map(s => s.trim())\n .filter(Boolean)\n return list.length > 0 ? list : null\n}\n\n/** Candidate imported-MCP aliases the dashboard may reach mailboxes through. */\nexport const MAIL_TRIAGE_MCP_ALIASES: readonly string[] = envAliases() ?? [...DEFAULT_MCP_ALIASES]\n\nexport const MAIL_TRIAGE_TOOLS: readonly string[] = [\n ...MAIL_TRIAGE_MCP_ALIASES.flatMap(alias => ALIAS_TOOL_NAMES.map(t => `imported:${alias}/${t}`)),\n \"app_run\",\n \"app_status\",\n \"agent_output\",\n \"app_list\",\n]\n\nconst APP_ID = \"@agentproto/mail-triage\"\nconst AGENT_ID = \"@agentproto/triager\"\nconst CATEGORIES = [\"urgent\", \"needs-reply\", \"newsletter\", \"notification\"]\n\nexport const MAIL_TRIAGE_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Mail Triage</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow-y:auto}\n#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0;flex-wrap:wrap}\n#toolbar .title{font-size:13px;font-weight:600;flex:1}\nselect,input[type=text]{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:4px;font-size:12px;font-family:inherit}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn:disabled{opacity:.4;cursor:default}\n.abtn.primary{background:var(--blue);border-color:var(--blue);color:#04101c}\n.abtn.primary:hover{opacity:.9}\n#caps{padding:6px 14px;display:flex;gap:6px;align-items:center;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n.badge{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--text2)}\n.badge.on{color:var(--green);border-color:var(--green)}\n.badge.off{opacity:.5;text-decoration:line-through}\n#content{padding:14px;display:flex;flex-direction:column;gap:16px}\nsection{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px}\nsection h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}\n#summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}\n.tile{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}\n.tile .count{font-size:22px;font-weight:700}\n.tile .label{font-size:11px;color:var(--text2);text-transform:capitalize}\n.tile.unread .count{color:var(--green)}\n.tile.urgent .count{color:var(--red)}\n.tile.needs-reply .count{color:var(--yellow)}\n.tile.newsletter .count{color:var(--blue)}\n.tile.notification .count{color:var(--purple)}\n#plan-list,#search-results,#log-events,#runs-list,#connect-hint-list{max-height:260px;overflow-y:auto;font-size:12px}\n.plan-item{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}\n.plan-item .subj{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.plan-item .tag{color:var(--text2);font-size:11px;white-space:nowrap}\n.runs-item{display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);cursor:pointer}\n.runs-item:hover{background:var(--bg3)}\n.runs-item .rid{font-family:Menlo,Monaco,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}\n.runs-item .st{font-size:11px;white-space:nowrap}\n.runs-item .st.running{color:var(--yellow)}\n.runs-item .st.ended{color:var(--green)}\n.runs-item .meta{color:var(--text2);font-size:11px;white-space:nowrap}\n#log-tail{font-family:Menlo,Monaco,monospace;white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px;max-height:260px;overflow-y:auto}\n.searchbar{display:flex;gap:8px;margin-bottom:8px}\n.searchbar input{flex:1}\n.actions{display:flex;gap:8px;margin-top:10px}\n#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.empty{color:var(--text2);padding:8px 0}\n.mini{font-size:10px;text-transform:none;letter-spacing:0;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit}\n.caps-hint{font-size:11px;color:var(--yellow)}\n.snippet{color:var(--text2);font-size:11px;margin-top:2px}\n.mail-checkbox{accent-color:var(--blue);cursor:pointer;flex-shrink:0;margin-top:1px}\n.mail-open{font-size:10px;cursor:pointer;color:var(--blue);background:none;border:none;font-family:inherit;white-space:nowrap;padding:0;flex-shrink:0}\n.mail-open:hover{text-decoration:underline}\n#selection-bar{display:none;margin-bottom:8px;gap:8px;align-items:center}\n#selection-bar.show{display:flex}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <span class=\"title\">Mail Triage</span>\n <select id=\"alias-select\" style=\"display:none\" title=\"MCP server\"></select>\n <select id=\"mailbox-select\"></select>\n <button class=\"abtn\" id=\"refresh-btn\">Refresh</button>\n </div>\n <div id=\"caps\" style=\"display:none\"></div>\n <div id=\"content\">\n <section id=\"connect-hint\" style=\"display:none\">\n <h3>Connect a mailbox</h3>\n <div id=\"connect-hint-list\"></div>\n <div class=\"empty\">Import/start an agentpush MCP server and connect a Gmail account in agentpush, then retry.</div>\n <div class=\"actions\">\n <button class=\"abtn primary\" id=\"connect-retry-btn\">Retry</button>\n </div>\n </section>\n\n <section>\n <h3>Inbox summary</h3>\n <div id=\"summary\"></div>\n </section>\n\n <section>\n <h3>Search</h3>\n <div class=\"searchbar\">\n <input type=\"text\" id=\"search-input\" placeholder=\"e.g. is:unread\">\n <button class=\"abtn\" id=\"search-btn\">Search</button>\n </div>\n <div id=\"selection-bar\">\n <span id=\"sel-count\" style=\"font-size:12px;color:var(--text2)\">0 selected</span>\n <button class=\"abtn primary\" data-action=\"send-selection\">Send selection to Claude</button>\n </div>\n <div id=\"search-results\"><div class=\"empty\">No search run yet.</div></div>\n </section>\n\n <section>\n <h3>Triage plan</h3>\n <div class=\"searchbar\">\n <input type=\"text\" id=\"plan-query\" placeholder=\"criteria query\" value=\"is:unread category:promotions older_than:7d\">\n <select id=\"plan-action\">\n <option value=\"markRead\">mark read</option>\n <option value=\"archive\">archive</option>\n <option value=\"label\">label…</option>\n <option value=\"trash\">trash</option>\n </select>\n <input type=\"text\" id=\"plan-label\" placeholder=\"label name\" style=\"display:none\">\n </div>\n <div class=\"actions\">\n <button class=\"abtn primary\" id=\"triage-btn\">Build plan</button>\n <button class=\"abtn\" id=\"apply-btn\" disabled>Apply</button>\n </div>\n <div id=\"plan-list\" style=\"margin-top:10px\"><div class=\"empty\">No plan yet — click Build plan.</div></div>\n </section>\n\n <section>\n <h3>Triager agent</h3>\n <div class=\"actions\">\n <button class=\"abtn\" id=\"run-agent-btn\">Run Triager Agent</button>\n </div>\n <div id=\"log-events\" style=\"margin-top:10px\"><div class=\"empty\">Agent has not run yet.</div></div>\n <div id=\"log-tail\" style=\"margin-top:6px\"></div>\n </section>\n\n <section>\n <h3><span>Past runs</span><button class=\"mini\" id=\"runs-refresh-btn\">refresh</button></h3>\n <div id=\"runs-list\"><div class=\"empty\">No runs yet.</div></div>\n </section>\n </div>\n <div id=\"status\">Connecting…</div>\n</div>\n<script>\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar AGENT_ID = ${JSON.stringify(AGENT_ID)};\nvar CATEGORIES = ${JSON.stringify(CATEGORIES)};\nvar ALIASES = ${JSON.stringify(MAIL_TRIAGE_MCP_ALIASES)};\nvar POLL_TIMEOUT_MS = 10 * 60 * 1000;\nvar callTool = null;\nvar updateModelContext = null;\nvar openLink = null;\nvar onTeardown = null;\nvar currentPlan = null;\nvar pollHandle = null;\nvar planExpiryHandle = null;\nvar activeAlias = ALIASES[0];\nvar currentMailboxes = [];\nvar categoryCounts = null;\nvar unreadCount = null;\nvar aliasStates = {};\nvar selectedMessages = {};\n\nfunction setStatus(msg) {\n document.getElementById(\"status\").textContent = msg;\n}\n\n// See media-viewer/ui.ts for why this loop exists: app_tool_call wraps its\n// dispatch result in at least one more {content:[{type:\"text\",text:...}]}\n// shell than the underlying tool returned.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try {\n cur = JSON.parse(cur);\n continue;\n } catch (e) {\n return cur;\n }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n if (!callTool) {\n return Promise.reject(new Error(\"Not connected to host bridge (standalone mode).\"));\n }\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(function (result) {\n var value = unwrapText(result);\n if (value && typeof value === \"object\" && !Array.isArray(value) && typeof value.error === \"string\") {\n throw new Error(value.error);\n }\n return value;\n });\n}\n\nfunction aliasTool(name) {\n return \"imported:\" + activeAlias + \"/\" + name;\n}\n\n// Disables a button and swaps its label to an ellipsis while a call is in\n// flight, so double-submit is impossible; restores the original label after.\nfunction setBusy(btn, busy) {\n if (busy) {\n if (btn.getAttribute(\"data-label\") === null) btn.setAttribute(\"data-label\", btn.textContent);\n btn.disabled = true;\n btn.textContent = \"\\\\u2026\";\n } else {\n btn.disabled = false;\n var label = btn.getAttribute(\"data-label\");\n if (label !== null) btn.textContent = label;\n }\n}\n\nfunction isSelected(id) {\n return selectedMessages[id] ? true : false;\n}\n\nfunction toggleSelection(id, item) {\n if (selectedMessages[id]) {\n delete selectedMessages[id];\n } else {\n selectedMessages[id] = item;\n }\n updateSelectionBar();\n}\n\nfunction updateSelectionBar() {\n var bar = document.getElementById(\"selection-bar\");\n var countEl = document.getElementById(\"sel-count\");\n var count = 0;\n for (var k in selectedMessages) { if (selectedMessages.hasOwnProperty(k)) count++; }\n countEl.textContent = count + \" selected\";\n bar.className = count > 0 ? \"show\" : \"\";\n // Sync checkboxes\n var cbs = document.querySelectorAll(\"#search-results .mail-checkbox\");\n for (var i = 0; i < cbs.length; i++) {\n var mid = cbs[i].getAttribute(\"data-message-id\");\n cbs[i].checked = mid ? isSelected(mid) : false;\n }\n}\n\nfunction sendSelectionToClaude() {\n if (!updateModelContext) {\n setStatus(\"Host does not support updateModelContext.\");\n return;\n }\n var items = [];\n var names = [];\n for (var k in selectedMessages) {\n if (!selectedMessages.hasOwnProperty(k)) continue;\n var msg = selectedMessages[k];\n items.push({\n id: msg.id || k,\n threadId: msg.threadId || \"\",\n from: msg.from || \"\",\n subject: msg.subject || \"\",\n timestamp: msg.internalDate || msg.date || \"\"\n });\n if (msg.subject) names.push(msg.subject);\n }\n if (items.length === 0) {\n setStatus(\"No messages selected.\");\n return;\n }\n var summary = items.length + \" emails selected for triage: \" + names.join(\", \");\n setStatus(\"Sending selection to Claude\\\\u2026\");\n updateModelContext({\n content: [{ type: \"text\", text: summary }],\n structuredContent: { selection: items }\n }).then(function () {\n setStatus(\"Selection sent to Claude (\" + items.length + \" emails).\");\n }).catch(function (err) {\n setStatus(\"Failed to send selection: \" + (err && err.message ? err.message : String(err)));\n });\n}\n\nfunction clearPlanExpiry() {\n if (planExpiryHandle) {\n clearTimeout(planExpiryHandle);\n planExpiryHandle = null;\n }\n}\n\nfunction onPlanExpired() {\n planExpiryHandle = null;\n currentPlan = null;\n document.getElementById(\"apply-btn\").disabled = true;\n setStatus(\"Plan expired \\\\u2014 rebuild it.\");\n}\n\n// Arms a timeout that invalidates the plan at its server-declared expires_at\n// (plans expire after 15 minutes) so Apply never fires against a stale plan.\nfunction armPlanExpiry(expiresAt) {\n clearPlanExpiry();\n if (!expiresAt) return;\n var ms = new Date(expiresAt).getTime() - Date.now();\n if (isNaN(ms)) return;\n if (ms <= 0) {\n onPlanExpired();\n return;\n }\n planExpiryHandle = setTimeout(onPlanExpired, ms);\n}\n\nfunction asList(value, keys) {\n if (Array.isArray(value)) return value;\n if (value && typeof value === \"object\") {\n for (var i = 0; i < keys.length; i++) {\n if (Array.isArray(value[keys[i]])) return value[keys[i]];\n }\n }\n return [];\n}\n\nfunction renderSummary() {\n var el = document.getElementById(\"summary\");\n el.innerHTML = \"\";\n var tiles = [{ key: \"unread\", count: unreadCount }].concat(\n CATEGORIES.map(function (cat) {\n return { key: cat, count: categoryCounts ? categoryCounts[cat] : undefined };\n })\n );\n tiles.forEach(function (t) {\n var tile = document.createElement(\"div\");\n tile.className = \"tile \" + t.key;\n tile.innerHTML = '<div class=\"count\"></div><div class=\"label\"></div>';\n tile.querySelector(\".count\").textContent = t.count === undefined || t.count === null ? \"\\\\u2013\" : String(t.count);\n tile.querySelector(\".label\").textContent = t.key.replace(\"-\", \" \");\n el.appendChild(tile);\n });\n}\n\nfunction mailboxId(mb) {\n if (typeof mb === \"string\") return mb;\n return mb.id || mb.name || JSON.stringify(mb);\n}\n\nfunction mailboxText(mb) {\n if (typeof mb === \"string\") return mb;\n var name = mb.address || mb.label || mb.name || mb.id || JSON.stringify(mb);\n var caps = mb.capabilities;\n if (caps && typeof caps === \"object\") {\n return name + (caps.triage ? \" \\\\u2014 triage \\\\u2713\" : \" \\\\u2014 read-only\");\n }\n return name;\n}\n\nfunction selectedMailbox() {\n var id = document.getElementById(\"mailbox-select\").value;\n for (var i = 0; i < currentMailboxes.length; i++) {\n if (mailboxId(currentMailboxes[i]) === id) return currentMailboxes[i];\n }\n return null;\n}\n\nfunction renderCaps(mb) {\n var el = document.getElementById(\"caps\");\n var caps = mb && typeof mb === \"object\" ? mb.capabilities : null;\n if (!caps || typeof caps !== \"object\") {\n el.style.display = \"none\";\n return;\n }\n el.style.display = \"flex\";\n el.innerHTML = \"\";\n [\"read\", \"send\", \"triage\", \"compose\"].forEach(function (cap) {\n var b = document.createElement(\"span\");\n b.className = \"badge \" + (caps[cap] ? \"on\" : \"off\");\n b.textContent = cap;\n el.appendChild(b);\n });\n if (!caps.triage) {\n var hint = document.createElement(\"span\");\n hint.className = \"caps-hint\";\n hint.textContent = \"triage scope missing \\\\u2014 grant it via agentpush (mailbox_request_elevation)\";\n el.appendChild(hint);\n }\n}\n\n// Gmail category labels -> the dashboard's triage buckets. IMPORTANT wins,\n// so an important promo counts as urgent, not newsletter.\nfunction categoryOf(labelIds) {\n var l = labelIds || [];\n if (l.indexOf(\"IMPORTANT\") !== -1) return \"urgent\";\n if (l.indexOf(\"CATEGORY_PROMOTIONS\") !== -1) return \"newsletter\";\n if (l.indexOf(\"CATEGORY_UPDATES\") !== -1 || l.indexOf(\"CATEGORY_SOCIAL\") !== -1 || l.indexOf(\"CATEGORY_FORUMS\") !== -1) return \"notification\";\n return \"needs-reply\";\n}\n\nfunction onMailboxChange() {\n clearPlanExpiry();\n var mb = selectedMailbox();\n renderCaps(mb);\n var caps = mb && typeof mb === \"object\" ? mb.capabilities : null;\n var noTriage = !!(caps && typeof caps === \"object\" && !caps.triage);\n var scopeHint = noTriage ? \"Mailbox lacks triage capability \\\\u2014 grant it via agentpush (mailbox_request_elevation)\" : \"\";\n document.getElementById(\"triage-btn\").disabled = noTriage;\n document.getElementById(\"triage-btn\").title = scopeHint;\n document.getElementById(\"apply-btn\").disabled = true;\n document.getElementById(\"apply-btn\").title = scopeHint;\n currentPlan = null;\n categoryCounts = null;\n unreadCount = null;\n renderSummary();\n if (!mb) return;\n callApp(aliasTool(\"mailbox_search\"), { mailbox: mailboxId(mb), query: \"is:unread\", limit: 50 })\n .then(function (result) {\n var items = asList(result, [\"items\", \"messages\", \"results\"]);\n if (result && typeof result === \"object\" && typeof result.resultSizeEstimate === \"number\") {\n unreadCount = result.resultSizeEstimate;\n } else {\n unreadCount = items.length;\n }\n // Category breakdown over the newest page of unread — a sample when\n // the mailbox has more unread than one page.\n var counts = {};\n items.forEach(function (item) {\n var cat = categoryOf(item && item.labelIds);\n counts[cat] = (counts[cat] || 0) + 1;\n });\n categoryCounts = items.length > 0 ? counts : null;\n renderSummary();\n })\n .catch(function (err) {\n // KPI is best-effort: keep the dashes, but say why instead of silence.\n setStatus(\"Inbox summary unavailable: \" + err.message);\n });\n}\n\nfunction renderMailboxes(mailboxes) {\n currentMailboxes = mailboxes;\n var select = document.getElementById(\"mailbox-select\");\n select.innerHTML = \"\";\n if (mailboxes.length === 0) {\n var opt = document.createElement(\"option\");\n opt.textContent = \"no mailboxes\";\n opt.value = \"\";\n select.appendChild(opt);\n renderCaps(null);\n return;\n }\n mailboxes.forEach(function (mb) {\n var opt = document.createElement(\"option\");\n opt.value = mailboxId(mb);\n opt.textContent = mailboxText(mb);\n select.appendChild(opt);\n });\n onMailboxChange();\n}\n\nfunction renderAliasSelect(liveAliases) {\n var select = document.getElementById(\"alias-select\");\n select.innerHTML = \"\";\n var shown = liveAliases.length > 0 ? liveAliases : ALIASES;\n shown.forEach(function (alias) {\n var opt = document.createElement(\"option\");\n opt.value = alias;\n opt.textContent = alias;\n select.appendChild(opt);\n });\n select.value = activeAlias;\n select.style.display = shown.length > 1 ? \"\" : \"none\";\n}\n\nfunction loadMailboxes() {\n setStatus(\"Loading mailboxes from \" + activeAlias + \"\\\\u2026\");\n return callApp(aliasTool(\"mailbox_list\"), {})\n .then(function (result) {\n renderMailboxes(asList(result, [\"mailboxes\", \"items\"]));\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n })\n .catch(function (err) {\n setStatus(\"Error loading mailboxes: \" + err.message);\n });\n}\n\nfunction renderConnectHint() {\n var el = document.getElementById(\"connect-hint\");\n var anyLive = false;\n for (var i = 0; i < ALIASES.length; i++) {\n if (aliasStates[ALIASES[i]] && aliasStates[ALIASES[i]].state === \"live\") anyLive = true;\n }\n if (anyLive) {\n el.style.display = \"none\";\n return;\n }\n el.style.display = \"\";\n var list = document.getElementById(\"connect-hint-list\");\n list.innerHTML = \"\";\n ALIASES.forEach(function (alias) {\n var st = aliasStates[alias];\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n var text;\n if (!st) text = alias + \" \\\\u2014 not probed yet\";\n else if (st.state === \"unreachable\") text = alias + \" \\\\u2014 unreachable: \" + st.message;\n else if (st.state === \"empty\") text = alias + \" \\\\u2014 connected, no mailboxes\";\n else text = alias + \" \\\\u2014 live\";\n row.textContent = text;\n list.appendChild(row);\n });\n}\n\nfunction probeAliases() {\n setStatus(\"Detecting mail servers\\\\u2026\");\n return Promise.all(\n ALIASES.map(function (alias) {\n return callApp(\"imported:\" + alias + \"/mailbox_list\", {})\n .then(function (result) {\n var mailboxes = asList(result, [\"mailboxes\", \"items\"]);\n aliasStates[alias] = { state: mailboxes.length > 0 ? \"live\" : \"empty\" };\n return { alias: alias, mailboxes: mailboxes };\n })\n .catch(function (err) {\n aliasStates[alias] = { state: \"unreachable\", message: err && err.message ? err.message : String(err) };\n return { alias: alias, mailboxes: [] };\n });\n })\n ).then(function (results) {\n var live = results.filter(function (r) { return r.mailboxes.length > 0; });\n if (live.length > 0) {\n activeAlias = live[0].alias;\n renderAliasSelect(live.map(function (r) { return r.alias; }));\n renderMailboxes(live[0].mailboxes);\n renderConnectHint();\n setStatus(\"Ready \\\\u2014 \" + activeAlias + (live.length > 1 ? \" (\" + live.length + \" servers available)\" : \"\"));\n } else {\n renderAliasSelect([]);\n renderMailboxes([]);\n renderConnectHint();\n setStatus(\"No mailboxes reachable \\\\u2014 see connection panel below.\");\n }\n });\n}\n\nfunction renderSearchResults(items, raw) {\n var el = document.getElementById(\"search-results\");\n el.innerHTML = \"\";\n if (raw && typeof raw === \"object\" && typeof raw.resultSizeEstimate === \"number\") {\n var count = document.createElement(\"div\");\n count.className = \"empty\";\n count.textContent = items.length + \" shown \\\\u00b7 ~\" + raw.resultSizeEstimate + \" total\";\n el.appendChild(count);\n }\n if (items.length === 0) {\n var none = document.createElement(\"div\");\n none.className = \"empty\";\n none.textContent = \"No results.\";\n el.appendChild(none);\n return;\n }\n items.forEach(function (item) {\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n row.style.flexDirection = \"column\";\n row.style.alignItems = \"stretch\";\n var msgId = item && (item.id || item.messageId || \"\");\n var threadId = item && (item.threadId || \"\");\n var subj = typeof item === \"string\" ? item : item.subject || item.title || JSON.stringify(item);\n var from = item && item.from ? item.from : \"\";\n var snippet = item && item.snippet ? item.snippet : \"\";\n var top = document.createElement(\"div\");\n top.style.display = \"flex\";\n top.style.alignItems = \"flex-start\";\n top.style.gap = \"6px\";\n var cb = document.createElement(\"input\");\n cb.type = \"checkbox\";\n cb.className = \"mail-checkbox\";\n cb.setAttribute(\"data-action\", \"toggle-select\");\n if (msgId) cb.setAttribute(\"data-message-id\", msgId);\n if (isSelected(msgId)) cb.checked = true;\n top.appendChild(cb);\n var subjSpan = document.createElement(\"span\");\n subjSpan.className = \"subj\";\n subjSpan.style.flex = \"1\";\n subjSpan.textContent = subj;\n top.appendChild(subjSpan);\n var tagSpan = document.createElement(\"span\");\n tagSpan.className = \"tag\";\n tagSpan.textContent = from;\n top.appendChild(tagSpan);\n if (threadId) {\n var openBtn = document.createElement(\"button\");\n openBtn.className = \"mail-open\";\n openBtn.textContent = \"Open\";\n openBtn.setAttribute(\"data-action\", \"open-gmail\");\n openBtn.setAttribute(\"data-thread-id\", threadId);\n top.appendChild(openBtn);\n }\n row.appendChild(top);\n if (snippet) {\n var sn = document.createElement(\"div\");\n sn.className = \"snippet\";\n sn.textContent = snippet;\n row.appendChild(sn);\n }\n el.appendChild(row);\n });\n}\n\n// Renders a mailbox_triage_plan result: { plan_id, action, count, sample,\n// truncated, expires_at }. The sample rows are search-shaped messages.\nfunction renderPlan(plan) {\n var el = document.getElementById(\"plan-list\");\n el.innerHTML = \"\";\n if (!plan || typeof plan !== \"object\" || !plan.plan_id) {\n el.innerHTML = '<div class=\"empty\">Plan failed \\\\u2014 unexpected response.</div>';\n return;\n }\n var head = document.createElement(\"div\");\n head.className = \"plan-item\";\n head.innerHTML = '<span class=\"subj\"></span><span class=\"tag\"></span>';\n head.querySelector(\".subj\").textContent =\n plan.count + \" message\" + (plan.count === 1 ? \"\" : \"s\") + \" \\\\u2192 \" + (plan.action && plan.action.type);\n head.querySelector(\".tag\").textContent = plan.expires_at ? \"expires \" + fmtDate(plan.expires_at) : \"\";\n el.appendChild(head);\n var sample = asList(plan, [\"sample\"]);\n sample.forEach(function (item) {\n var row = document.createElement(\"div\");\n row.className = \"plan-item\";\n row.innerHTML = '<span class=\"subj\"></span><span class=\"tag\"></span>';\n row.querySelector(\".subj\").textContent = (item && (item.subject || item.title)) || JSON.stringify(item);\n row.querySelector(\".tag\").textContent = (item && item.from) || \"\";\n el.appendChild(row);\n });\n if (plan.truncated) {\n var more = document.createElement(\"div\");\n more.className = \"empty\";\n more.textContent = \"\\\\u2026sample truncated \\\\u2014 \" + plan.count + \" total.\";\n el.appendChild(more);\n }\n}\n\nfunction fmtDate(iso) {\n if (!iso) return \"\";\n var d = new Date(iso);\n return isNaN(d.getTime()) ? String(iso) : d.toLocaleString();\n}\n\nfunction renderRuns(runs) {\n var el = document.getElementById(\"runs-list\");\n el.innerHTML = \"\";\n if (runs.length === 0) {\n el.innerHTML = '<div class=\"empty\">No runs yet.</div>';\n return;\n }\n var shown = runs.slice(0, 20);\n shown.forEach(function (run) {\n var row = document.createElement(\"div\");\n row.className = \"runs-item\";\n row.title = \"Show sessions + output for \" + run.appRunId;\n row.innerHTML = '<span class=\"st\"></span><span class=\"rid\"></span><span class=\"meta\"></span>';\n var st = row.querySelector(\".st\");\n st.textContent = run.status || \"?\";\n st.className = \"st \" + (run.status || \"\");\n row.querySelector(\".rid\").textContent = run.appRunId;\n row.querySelector(\".meta\").textContent =\n fmtDate(run.startedAt) + (run.endedAt ? \" \\\\u2192 \" + fmtDate(run.endedAt) : \"\") + \" \\\\u00b7 \" + (run.sessions || 0) + \" session\" + (run.sessions === 1 ? \"\" : \"s\");\n row.addEventListener(\"click\", function () { viewRun(run.appRunId); });\n el.appendChild(row);\n });\n if (runs.length > 20) {\n var more = document.createElement(\"div\");\n more.className = \"empty\";\n more.textContent = \"showing 20 of \" + runs.length;\n el.appendChild(more);\n }\n}\n\nfunction loadRuns() {\n return callApp(\"app_list\", {})\n .then(function (result) {\n var apps = Array.isArray(result) ? result : [];\n var mine = null;\n for (var i = 0; i < apps.length; i++) {\n if (apps[i] && apps[i].appId === APP_ID) { mine = apps[i]; break; }\n }\n var runs = (mine && Array.isArray(mine.runs) ? mine.runs.slice() : []).sort(function (a, b) {\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n renderRuns(runs);\n })\n .catch(function (err) {\n setStatus(\"Could not load runs: \" + err.message);\n });\n}\n\nfunction viewRun(appRunId) {\n setStatus(\"Loading run \" + appRunId + \"\\\\u2026\");\n callApp(\"app_status\", { appRunId: appRunId })\n .then(function (status) {\n var sessions = (status && status.sessions) || [];\n appendLog(\"\\\\u2500\\\\u2500 run \" + appRunId + \" (\" + (status && status.status) + \") \\\\u2500\\\\u2500\");\n if (sessions.length === 0) {\n appendLog(\"(no sessions)\");\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n return;\n }\n sessions.forEach(function (s) {\n appendLog(\"session \" + s.sessionId + \" (\" + (s.agentId || \"?\") + \")\");\n });\n return callApp(\"agent_output\", { sessionId: sessions[0].sessionId, lastN: 200, clean: true })\n .then(function (output) {\n if (output) appendLog(typeof output === \"string\" ? output : JSON.stringify(output, null, 2));\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n });\n })\n .catch(function (err) {\n setStatus(\"Run status failed: \" + err.message);\n });\n}\n\n// Event delegation: search-results (checkboxes + open buttons)\ndocument.getElementById(\"search-results\").addEventListener(\"change\", function (e) {\n var target = e.target;\n if (target.getAttribute(\"data-action\") === \"toggle-select\") {\n var mid = target.getAttribute(\"data-message-id\");\n if (mid) {\n var item = null;\n var el = target.parentNode;\n while (el && el.parentNode && el !== document) {\n if (el.tagName === \"DIV\" && el.className.indexOf(\"plan-item\") !== -1) {\n var subjEl = el.querySelector(\".subj\");\n var tagEl = el.querySelector(\".tag\");\n var subjText = subjEl ? subjEl.textContent : \"\";\n var tagText = tagEl ? tagEl.textContent : \"\";\n // Reconstruct a minimal item from the DOM\n item = { id: mid, threadId: \"\", subject: subjText, from: tagText };\n break;\n }\n el = el.parentNode;\n }\n toggleSelection(mid, item || { id: mid });\n }\n }\n});\ndocument.getElementById(\"search-results\").addEventListener(\"click\", function (e) {\n var target = e.target;\n if (target.getAttribute(\"data-action\") === \"open-gmail\") {\n var tid = target.getAttribute(\"data-thread-id\");\n if (tid && openLink) {\n openLink(\"https://mail.google.com/mail/u/0/#inbox/\" + tid).catch(function (err) {\n setStatus(\"Failed to open link: \" + (err && err.message ? err.message : String(err)));\n });\n } else if (tid) {\n setStatus(\"Host does not support openLink.\");\n }\n }\n});\n// Event delegation: selection-bar (send-selection)\ndocument.getElementById(\"selection-bar\").addEventListener(\"click\", function (e) {\n if (e.target.getAttribute(\"data-action\") === \"send-selection\") {\n sendSelectionToClaude();\n }\n});\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n probeAliases();\n loadRuns();\n});\n\ndocument.getElementById(\"connect-retry-btn\").addEventListener(\"click\", function () {\n probeAliases();\n loadRuns();\n});\n\ndocument.getElementById(\"alias-select\").addEventListener(\"change\", function () {\n activeAlias = this.value;\n clearPlanExpiry();\n currentPlan = null;\n document.getElementById(\"apply-btn\").disabled = true;\n loadMailboxes();\n});\n\ndocument.getElementById(\"mailbox-select\").addEventListener(\"change\", onMailboxChange);\n\ndocument.getElementById(\"runs-refresh-btn\").addEventListener(\"click\", loadRuns);\n\ndocument.getElementById(\"search-btn\").addEventListener(\"click\", function () {\n var btn = this;\n var query = document.getElementById(\"search-input\").value.trim() || \"is:unread\";\n var mailbox = document.getElementById(\"mailbox-select\").value;\n setBusy(btn, true);\n setStatus(\"Searching\\\\u2026\");\n callApp(aliasTool(\"mailbox_search\"), { mailbox: mailbox, query: query })\n .then(function (result) {\n renderSearchResults(asList(result, [\"items\", \"messages\", \"results\"]), result);\n setBusy(btn, false);\n setStatus(\"Ready \\\\u2014 \" + activeAlias);\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Search failed: \" + err.message);\n });\n});\n\ndocument.getElementById(\"plan-action\").addEventListener(\"change\", function () {\n document.getElementById(\"plan-label\").style.display = this.value === \"label\" ? \"\" : \"none\";\n});\n\ndocument.getElementById(\"triage-btn\").addEventListener(\"click\", function () {\n var btn = this;\n var mailbox = document.getElementById(\"mailbox-select\").value;\n var query = document.getElementById(\"plan-query\").value.trim() || \"is:unread\";\n var actionType = document.getElementById(\"plan-action\").value;\n var action = { type: actionType };\n if (actionType === \"label\") {\n var label = document.getElementById(\"plan-label\").value.trim();\n if (!label) {\n setStatus(\"Enter a label name for the label action.\");\n return;\n }\n action.addLabelIds = [label];\n }\n setBusy(btn, true);\n setStatus(\"Building triage plan\\\\u2026\");\n document.getElementById(\"apply-btn\").disabled = true;\n clearPlanExpiry();\n currentPlan = null;\n callApp(aliasTool(\"mailbox_triage_plan\"), { mailbox: mailbox, criteria: { query: query }, action: action })\n .then(function (plan) {\n setBusy(btn, false);\n currentPlan = plan && plan.plan_id ? plan : null;\n renderPlan(plan);\n if (currentPlan && plan.count > 0) {\n document.getElementById(\"apply-btn\").disabled = false;\n armPlanExpiry(plan.expires_at);\n setStatus(\"Plan ready (\" + plan.count + \" messages) \\\\u2014 review, then Apply.\");\n } else if (currentPlan) {\n setStatus(\"Plan matches no messages \\\\u2014 nothing to apply.\");\n } else {\n setStatus(\"Triage plan failed \\\\u2014 see plan panel.\");\n }\n })\n .catch(function (err) {\n setBusy(btn, false);\n var msg = err && err.message ? err.message : String(err);\n if (msg.indexOf(\"mailbox_scope_missing\") !== -1) {\n setStatus(\"Triage scope missing \\\\u2014 grant it via agentpush (mailbox_request_elevation).\");\n } else {\n setStatus(\"Triage plan failed: \" + msg);\n }\n });\n});\n\ndocument.getElementById(\"apply-btn\").addEventListener(\"click\", function () {\n var btn = this;\n if (!currentPlan || !currentPlan.plan_id) return;\n setBusy(btn, true);\n setStatus(\"Applying triage plan\\\\u2026\");\n callApp(aliasTool(\"mailbox_triage_apply\"), { plan_id: currentPlan.plan_id, confirm: true })\n .then(function (result) {\n setBusy(btn, false);\n var count = currentPlan.count;\n var extra = [];\n if (result && typeof result === \"object\") {\n [\"applied\", \"modified\", \"count\"].forEach(function (key) {\n if (typeof result[key] === \"number\") extra.push(key + \": \" + result[key]);\n });\n }\n setStatus(\n extra.length > 0\n ? \"Plan applied \\\\u2014 \" + extra.join(\", \") + \".\"\n : \"Plan applied (\" + count + \" message\" + (count === 1 ? \"\" : \"s\") + \").\"\n );\n document.getElementById(\"apply-btn\").disabled = true;\n clearPlanExpiry();\n currentPlan = null;\n onMailboxChange();\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Apply failed: \" + err.message);\n });\n});\n\nfunction appendLog(text) {\n var el = document.getElementById(\"log-events\");\n if (el.querySelector(\".empty\")) el.innerHTML = \"\";\n var line = document.createElement(\"div\");\n line.textContent = text;\n el.appendChild(line);\n el.scrollTop = el.scrollHeight;\n}\n\n// Replaces the whole tail on every poll tick (the daemon returns the last\n// N lines, not a delta) — appending it instead would duplicate the log.\nfunction renderLogTail(text) {\n var el = document.getElementById(\"log-tail\");\n el.textContent = text || \"\";\n el.scrollTop = el.scrollHeight;\n}\n\nfunction pollAgentRun(appRunId, startedAt) {\n if (pollHandle) {\n clearTimeout(pollHandle);\n pollHandle = null;\n }\n if (Date.now() - startedAt > POLL_TIMEOUT_MS) {\n setStatus(\"Still running \\\\u2014 check Past runs later.\");\n return;\n }\n callApp(\"app_status\", { appRunId: appRunId })\n .then(function (status) {\n var sessions = (status && status.sessions) || [];\n var sessionId = sessions[0] && sessions[0].sessionId;\n var tail = sessionId\n ? callApp(\"agent_output\", { sessionId: sessionId, lastN: 200, clean: true })\n : Promise.resolve(\"\");\n return tail.then(function (output) {\n renderLogTail(typeof output === \"string\" ? output : output ? JSON.stringify(output, null, 2) : \"\");\n var done = status && (status.status === \"ended\" || status.status === \"done\" || status.endedAt);\n if (!done) {\n pollHandle = setTimeout(function () { pollAgentRun(appRunId, startedAt); }, 2000);\n } else {\n pollHandle = null;\n setStatus(\"Agent run finished.\");\n loadRuns();\n }\n });\n })\n .catch(function (err) {\n setStatus(\"Agent status failed: \" + err.message);\n });\n}\n\ndocument.getElementById(\"run-agent-btn\").addEventListener(\"click\", function () {\n var btn = this;\n if (pollHandle) {\n clearTimeout(pollHandle);\n pollHandle = null;\n }\n document.getElementById(\"log-events\").innerHTML = \"\";\n document.getElementById(\"log-tail\").textContent = \"\";\n setBusy(btn, true);\n setStatus(\"Starting triager agent\\\\u2026\");\n callApp(\"app_run\", { appId: APP_ID, agents: [AGENT_ID], prompt: \"Triage my inbox.\" })\n .then(function (run) {\n setBusy(btn, false);\n var sessions = (run && run.sessions) || [];\n var errors = (run && run.errors) || [];\n if (sessions.length === 0 && errors.length > 0) {\n appendLog(\n \"Run failed to start: \" +\n errors.map(function (e) { return (e.agentId || \"?\") + \": \" + e.error; }).join(\"; \")\n );\n setStatus(\"Run failed to start.\");\n return;\n }\n appendLog(\"Started app run \" + (run && run.appRunId));\n loadRuns();\n pollAgentRun(run.appRunId, Date.now());\n })\n .catch(function (err) {\n setBusy(btn, false);\n setStatus(\"Run failed: \" + err.message);\n });\n});\n\n// window.McpApp is normally injected by the serving layer (postMessage bridge\n// on the MCP resource path, REST bridge on GET /apps/:id/ui). Guard anyway:\n// without it a missing bridge throws here and the panel dies silently on\n// \"Connecting…\" instead of reporting itself.\n(window.McpApp ? window.McpApp.connect() : Promise.reject(new Error(\"window.McpApp bridge missing\")))\n .then(function (bridge) {\n callTool = bridge.callTool;\n updateModelContext = bridge.updateModelContext;\n openLink = bridge.openLink;\n onTeardown = bridge.onTeardown;\n if (onTeardown) {\n onTeardown(function () {\n if (pollHandle) { clearTimeout(pollHandle); pollHandle = null; }\n if (planExpiryHandle) { clearTimeout(planExpiryHandle); planExpiryHandle = null; }\n });\n }\n return Promise.all([probeAliases(), loadRuns()]);\n })\n .catch(function (err) {\n setStatus(\"Standalone mode \\\\u2014 no host bridge (\" + err.message + \")\");\n renderSummary();\n });\n\nrenderSummary();\n</script>\n</body>\n</html>`\n","/**\n * `mail-triage` — a single-agent agentproto app for email triage.\n *\n * One agent (triager) scans inbox, categorizes unread mail, and applies\n * triage actions (label, archive). Proves the APP primitive works end-to-end\n * with a simple, useful real-world task.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { triager } from \"./agents/triager.js\"\nimport { triageInbox } from \"./workflows/triage-inbox.js\"\nimport { MAIL_TRIAGE_HTML, MAIL_TRIAGE_TOOLS } from \"./ui.js\"\n\nexport const mailTriage: AppHandle = defineApp({\n id: \"@agentproto/mail-triage\",\n name: \"Mail Triage\",\n version: \"0.1.0\",\n description: \"Triage your inbox — categorize, label, archive.\",\n agents: [triager],\n workflows: [triageInbox],\n ui: {\n html: MAIL_TRIAGE_HTML,\n title: \"Mail Triage\",\n tools: [...MAIL_TRIAGE_TOOLS],\n },\n})\n\nexport { triager, triageInbox }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\nexport const cataloger: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/media-cataloger\",\n description:\n \"Scans a folder for media files (images, video, audio), collects metadata, and produces a structured catalog.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Read-only — never modify, rename, move, or delete files\",\n \"Only catalog files with recognized media extensions\",\n \"Report exact file sizes and paths, never estimate\",\n ],\n tools: [\"list_dir\", \"read_file\", \"file_info\"],\n workflows: [{ ref: \"scan-media\" }],\n }),\n body: [\n \"You scan a workspace folder for media files and produce a structured JSON catalog.\",\n \"\",\n \"Walk the target directory recursively using list_dir. For each file, check the\",\n \"extension against known media types:\",\n \"- Images: .jpg, .jpeg, .png, .gif, .webp, .svg, .bmp, .ico, .avif\",\n \"- Video: .mp4, .webm, .mov, .avi, .mkv\",\n \"- Audio: .mp3, .wav, .ogg, .flac, .aac, .m4a\",\n \"\",\n \"For each media file found, use file_info to get size and modification date.\",\n \"\",\n \"Output a JSON object with this shape:\",\n \"```json\",\n '{',\n ' \"folder\": \"<scanned path>\",',\n ' \"scannedAt\": \"<ISO timestamp>\",',\n ' \"summary\": { \"total\": N, \"images\": N, \"video\": N, \"audio\": N, \"totalSizeBytes\": N },',\n ' \"files\": [',\n ' {',\n ' \"path\": \"relative/path.jpg\",',\n ' \"name\": \"file.jpg\",',\n ' \"type\": \"image\",',\n ' \"extension\": \".jpg\",',\n ' \"sizeBytes\": 123456,',\n ' \"modifiedAt\": \"2026-08-01T12:00:00Z\"',\n \" }\",\n \" ]\",\n \"}\",\n \"```\",\n \"\",\n \"Sort files by type (images first, then video, then audio), then by name.\",\n ].join(\"\\n\"),\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\nexport const scanMedia = defineWorkflow({\n id: \"scan-media\",\n name: \"Scan Media\",\n description:\n \"Scan a folder for media files, collect metadata, and produce a structured catalog.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"catalog\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/media-cataloger\" },\n prompt:\n \"Scan the workspace root folder for media files (images, video, audio). \" +\n \"Walk all subdirectories recursively. For each media file, collect its path, \" +\n \"name, type, extension, size, and modification date. Output the full catalog \" +\n \"as a single JSON object.\",\n },\n ],\n})\n","/**\n * Media Viewer — a self-contained MCP App gallery widget for `media-viewer`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }`. `app_tool_call`\n * double-wraps its dispatch result (it returns a text-result whose `text` is\n * the JSON-stringified inner tool result, itself an MCP content array) — see\n * `unwrapText` below, which peels that apart regardless of how many layers\n * the host's bridge already stripped.\n */\n\nexport const MEDIA_VIEWER_TOOLS = [\"directory_list\", \"file_info\", \"file_read\"] as const\n\nconst APP_ID = \"@agentproto/media-viewer\"\n\nexport const MEDIA_VIEWER_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Media Viewer</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%}\n#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;background:var(--bg2);flex-shrink:0}\n#toolbar .title{font-size:13px;font-weight:600;flex:1}\n#breadcrumb{font-size:12px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n#body{flex:1;display:flex;overflow:hidden}\n#grid-wrap{flex:1;overflow-y:auto;padding:14px}\n#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}\n.cell{display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);cursor:pointer;text-align:center}\n.cell:hover{background:var(--bg3);border-color:var(--blue)}\n.cell.selected{border-color:var(--blue);background:var(--bg3)}\n.icon{font-size:28px;line-height:1}\n.name{font-size:11px;color:var(--text);word-break:break-word;max-width:100%;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}\n#empty{color:var(--text2);padding:24px;text-align:center}\n#details{width:280px;border-left:1px solid var(--border);background:var(--bg2);padding:14px;overflow-y:auto;flex-shrink:0}\n#details h3{font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}\n#details .row{display:flex;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--border);font-size:12px}\n#details .row span:first-child{color:var(--text2)}\n#preview{margin-top:10px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow-y:auto}\n#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"toolbar\">\n <button class=\"abtn\" id=\"up-btn\" title=\"Up one level\">↑</button>\n <span class=\"title\">Media Viewer</span>\n <span id=\"breadcrumb\">.</span>\n <button class=\"abtn\" id=\"refresh-btn\">Refresh</button>\n </div>\n <div id=\"body\">\n <div id=\"grid-wrap\">\n <div id=\"grid\"></div>\n <div id=\"empty\" style=\"display:none\">No files in this folder.</div>\n </div>\n <div id=\"details\" style=\"display:none\"></div>\n </div>\n <div id=\"status\">Connecting…</div>\n</div>\n<script>\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar currentPath = \".\";\nvar entries = [];\nvar callTool = null;\n\nfunction setStatus(msg) {\n document.getElementById(\"status\").textContent = msg;\n}\n\n// app_tool_call wraps its dispatch result at least once (its own\n// text-result body is the JSON-stringified inner MCP tool result); peel\n// back through nested {content:[{type:\"text\",text:...}]} shells until the\n// payload stops looking like one, so this works whichever layer the host's\n// own bridge already unwrapped.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try {\n cur = JSON.parse(cur);\n continue;\n } catch (e) {\n return cur;\n }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction parseDirectoryListing(raw) {\n var text = typeof raw === \"string\" ? raw : \"\";\n return text\n .split(\"\\\\n\")\n .map(function (line) { return line.trim(); })\n .filter(function (line) { return line.length > 0; })\n .map(function (line) {\n var isDir = line.indexOf(\"[DIR]\") === 0;\n var name = line.replace(/^\\\\[(FILE|DIR)\\\\]\\\\s*/, \"\");\n return { name: name, isDir: isDir };\n });\n}\n\nfunction iconFor(entry) {\n if (entry.isDir) return \"\\\\uD83D\\\\uDCC1\";\n var ext = (entry.name.split(\".\").pop() || \"\").toLowerCase();\n if ([\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"svg\", \"bmp\", \"ico\", \"avif\"].indexOf(ext) !== -1) return \"\\\\uD83D\\\\uDDBC\\\\uFE0F\";\n if ([\"mp4\", \"webm\", \"mov\", \"avi\", \"mkv\"].indexOf(ext) !== -1) return \"\\\\uD83C\\\\uDFA5\";\n if ([\"mp3\", \"wav\", \"ogg\", \"flac\", \"aac\", \"m4a\"].indexOf(ext) !== -1) return \"\\\\uD83C\\\\uDFB5\";\n return \"\\\\uD83D\\\\uDCC4\";\n}\n\nfunction joinPath(base, name) {\n if (base === \".\" || base === \"\") return name;\n return base.replace(/\\\\/+$/, \"\") + \"/\" + name;\n}\n\nfunction parentPath(path) {\n if (path === \".\" || path.indexOf(\"/\") === -1) return \".\";\n return path.slice(0, path.lastIndexOf(\"/\")) || \".\";\n}\n\nfunction renderGrid() {\n document.getElementById(\"breadcrumb\").textContent = currentPath;\n var grid = document.getElementById(\"grid\");\n var empty = document.getElementById(\"empty\");\n grid.innerHTML = \"\";\n if (entries.length === 0) {\n empty.style.display = \"block\";\n return;\n }\n empty.style.display = \"none\";\n entries.forEach(function (entry, idx) {\n var cell = document.createElement(\"div\");\n cell.className = \"cell\";\n cell.dataset.idx = String(idx);\n cell.innerHTML = '<div class=\"icon\">' + iconFor(entry) + '</div><div class=\"name\"></div>';\n cell.querySelector(\".name\").textContent = entry.name;\n cell.addEventListener(\"click\", function () { onSelect(entry, cell); });\n grid.appendChild(cell);\n });\n}\n\nfunction onSelect(entry, cell) {\n Array.prototype.forEach.call(document.querySelectorAll(\".cell\"), function (c) {\n c.classList.remove(\"selected\");\n });\n cell.classList.add(\"selected\");\n if (entry.isDir) {\n loadDirectory(joinPath(currentPath, entry.name));\n return;\n }\n showFileDetails(joinPath(currentPath, entry.name));\n}\n\nfunction showFileDetails(path) {\n var details = document.getElementById(\"details\");\n details.style.display = \"block\";\n details.innerHTML = \"<h3>Loading…</h3>\";\n setStatus(\"Reading \" + path + \"\\\\u2026\");\n callApp(\"file_info\", { path: path })\n .then(function (info) {\n var rows = \"\";\n if (info && typeof info === \"object\") {\n [\"name\", \"type\", \"size\", \"modified\"].forEach(function (key) {\n if (info[key] !== undefined) {\n rows += '<div class=\"row\"><span>' + key + '</span><span></span></div>';\n }\n });\n }\n details.innerHTML = \"<h3>Details</h3>\" + rows + '<div id=\"preview\">Loading preview…</div>';\n var rowEls = details.querySelectorAll(\".row span:last-child\");\n var keys = [\"name\", \"type\", \"size\", \"modified\"].filter(function (key) {\n return info && info[key] !== undefined;\n });\n keys.forEach(function (key, i) {\n rowEls[i].textContent = String(info[key]);\n });\n return callApp(\"file_read\", { path: path });\n })\n .then(function (content) {\n var pre = document.getElementById(\"preview\");\n if (!pre) return;\n var text = typeof content === \"string\" ? content : JSON.stringify(content);\n pre.textContent = text.length > 2000 ? text.slice(0, 2000) + \"\\\\u2026 (truncated)\" : (text || \"(empty file)\");\n setStatus(\"Ready \\\\u00b7 \" + currentPath);\n })\n .catch(function (err) {\n var pre = document.getElementById(\"preview\");\n if (pre) pre.textContent = \"Preview unavailable (\" + err.message + \")\";\n setStatus(\"Ready \\\\u00b7 \" + currentPath);\n });\n}\n\nfunction loadDirectory(path) {\n document.getElementById(\"details\").style.display = \"none\";\n setStatus(\"Loading \" + path + \"\\\\u2026\");\n return callApp(\"directory_list\", { path: path })\n .then(function (raw) {\n currentPath = path;\n entries = parseDirectoryListing(raw).sort(function (a, b) {\n if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n renderGrid();\n setStatus(\"Ready \\\\u00b7 \" + entries.length + \" entries \\\\u00b7 \" + path);\n })\n .catch(function (err) {\n entries = [];\n renderGrid();\n setStatus(\"Error: \" + err.message);\n });\n}\n\ndocument.getElementById(\"up-btn\").addEventListener(\"click\", function () {\n loadDirectory(parentPath(currentPath));\n});\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadDirectory(currentPath);\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n return loadDirectory(\".\");\n })\n .catch(function (err) {\n setStatus(\"Standalone mode \\\\u2014 no host bridge (\" + err.message + \")\");\n entries = [];\n renderGrid();\n });\n</script>\n</body>\n</html>`\n","/**\n * `media-viewer` — a single-agent agentproto app for media cataloging.\n *\n * One agent (cataloger) scans a workspace folder for images, video, and audio\n * files, collects metadata, and produces a structured JSON catalog. Paired\n * with a Cowork McpApp gallery widget for visual browsing.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { cataloger } from \"./agents/cataloger.js\"\nimport { scanMedia } from \"./workflows/scan-media.js\"\nimport { MEDIA_VIEWER_HTML, MEDIA_VIEWER_TOOLS } from \"./ui.js\"\n\nexport const mediaViewer: AppHandle = defineApp({\n id: \"@agentproto/media-viewer\",\n name: \"Media Viewer\",\n version: \"0.1.0\",\n description: \"Scan a folder for images, video, and audio — gallery view.\",\n agents: [cataloger],\n workflows: [scanMedia],\n ui: {\n html: MEDIA_VIEWER_HTML,\n title: \"Media Viewer\",\n tools: [...MEDIA_VIEWER_TOOLS],\n },\n})\n\nexport { cataloger, scanMedia }\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Sessions Manager — the durable \"master of the sessions\" coordinator:\n * the human talks to IT, it talks to the other sessions. Distilled from the\n * hand-run plateau-coordinator session this app productizes.\n *\n * Where the watchdog only OBSERVES (and never touches a live session), the\n * manager DRIVES: it re-prompts idle sessions onto their task, restarts dead\n * ones, and answers \"where is X at?\" by reading transcripts. The Ops Panel\n * spawns it via `agent_start` with {@link MANAGER_LABEL} and `keepAlive`, so\n * the idle-reaper never retires a legitimately-parked coordinator.\n */\n\n/** Session label the panel spawns under — how a live manager is recognized. */\nexport const MANAGER_LABEL = \"Sessions Manager\"\n\nexport const manager: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/sessions-manager\",\n description:\n \"Durable coordinator over the daemon's sessions — routes questions to the right session, re-prompts stalled work, restarts dead sessions, reports status.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Never kill a session unless the human explicitly asks for that session by id/label\",\n \"Never merge PRs or bypass a repo's declared review flow from a supervised session\",\n \"Prefer re-prompting an existing session over spawning a duplicate for the same task\",\n ],\n tools: [\n \"session_list\",\n \"conversation_read\",\n \"agent_prompt\",\n \"session_restart\",\n \"agent_start\",\n \"session_events_poll\",\n ],\n }),\n body: [\n \"You are the manager of this agentproto daemon's sessions: the human talks to you, and you talk\",\n \"to the other sessions. You are durable — the same session across many turns — so keep a working\",\n \"memory of who is doing what and don't re-derive it from scratch every turn.\",\n \"\",\n \"What you do:\",\n \"- Locate: given a task or question, find the relevant session (`session_list`, labels, cwd,\",\n \" recency) and read its transcript (`conversation_read`) before answering — never guess status.\",\n \"- Drive: re-prompt an idle/parked session onto its task with `agent_prompt`; restart a\",\n \" killed/exited session with `session_restart` when its work is clearly unfinished.\",\n \"- Spawn: only when no existing session owns the task — check for duplicates first.\",\n \"- Report: answer with short, concrete status (who, doing what, since when, blocked on what).\",\n \"\",\n \"Boundaries:\",\n \"- Never kill a session unless the human explicitly asks for that session.\",\n \"- A session's own repo conventions (review flow, merge policy) always win — you coordinate,\",\n \" you don't override.\",\n \"- If a session awaits a human decision, surface the question to the human instead of answering\",\n \" in their place.\",\n ].join(\"\\n\"),\n}\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\n/**\n * The Session Watchdog — a durable daemon-health session, re-prompted on a\n * cron tick (~every 5 minutes). Distilled from the hand-run watchdog session\n * this app productizes: same classification rules, same hard boundaries\n * (never kills a live session, never spawns).\n *\n * The Ops Panel spawns it via `agent_start` with {@link WATCHDOG_LABEL} and\n * wires a `prompt-session` cron ({@link WATCHDOG_TICK_LABEL} /\n * {@link WATCHDOG_TICK_SCHEDULE}) pointing at the spawned session — see\n * `ui.ts`. The agent is also usable standalone via `app_run` for a one-shot\n * health pass.\n */\n\n/** Session label the panel spawns under — how a live watchdog is recognized. */\nexport const WATCHDOG_LABEL = \"Session Watchdog\"\n\n/** Label of the cron job that re-prompts the watchdog session. */\nexport const WATCHDOG_TICK_LABEL = \"watchdog-tick\"\n\n/** Every 5 minutes. */\nexport const WATCHDOG_TICK_SCHEDULE = \"*/5 * * * *\"\n\n/** The prompt each cron tick sends to the durable session. */\nexport const WATCHDOG_TICK_PROMPT = \"Run your maintenance check now.\"\n\nexport const watchdog: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-watchdog\",\n description:\n \"Durable daemon-health watchdog — classifies sessions (needs-you, stalled, duplicated, abandonable), corrects flags, archives dead rows. Never kills or spawns.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"NEVER kill a live session, message/interrupt another session's conversation, or spawn new sessions\",\n \"Housekeeping only on terminal-status sessions (exited/killed/error) — never touches a live one\",\n \"Report only what's NEW since the last tick — no padding, no repeats unless something got worse\",\n ],\n tools: [\n \"session_list\",\n \"session_events_poll\",\n \"session_flag_status\",\n \"session_archive\",\n \"session_gc\",\n ],\n }),\n body: [\n \"You are the Session Watchdog for this agentproto daemon. You run on a recurring cron tick\",\n \"(~every 5 minutes) as the SAME durable session, re-prompted each time — you retain memory of\",\n \"what you already flagged, so don't repeat an alert unless something got meaningfully worse or new.\",\n \"\",\n \"Each tick:\",\n \"1. Call `session_list` to see current session state (status, awaitingInput/awaitingQuestion,\",\n \" stalledSinceMs, busy, blockedOn, childrenBusy).\",\n \"2. Call `session_events_poll` for events since your last tick (stalled, exited, awaiting-input)\",\n \" to catch anything between ticks.\",\n \"3. Classify:\",\n \" - Needs you: `awaitingInput: true` with a real unanswered question, older than ~10 minutes.\",\n \" - Stalled: busy but silent well past normal turn latency, no legitimate blockedOn excuse.\",\n \" - Possibly duplicated: two or more live sessions share workspace/cwd and a highly similar\",\n \" recent prompt/label — use judgment, not exact string matching.\",\n \" - Looks done/abandonable: idle/parked with no plausible remaining purpose.\",\n \"4. Correct misclassifications when confident: if `session_flag_status` is available and you can\",\n \" tell a session is actually waiting on a human (or isn't, despite being flagged), call it with\",\n \" a clear `reason`. If that tool is unknown on this daemon build, skip silently.\",\n \"5. Autonomous housekeeping (reversible, so no need to ask): `session_archive` or `session_gc` on\",\n \" sessions already terminal-status (exited/killed/error) and old. Never touches a live session.\",\n \"6. NEVER: kill a live session, message/interrupt another session's conversation, or spawn new\",\n \" sessions. You only have these five tools on purpose — if you'd want another one, report the\",\n \" suggestion instead of working around it.\",\n \"7. Report only what's NEW since last tick — one line per finding. If nothing new, reply with a\",\n \" short \\\"nothing new\\\" — don't pad it.\",\n \"\",\n \"You are not a general coding assistant across ticks — stay scoped to this watchdog role.\",\n ].join(\"\\n\"),\n}\n","/**\n * Ops Panel — a self-contained MCP App panel for `ops-panel`.\n *\n * The daemon operations cockpit: special durable sessions (Session Watchdog,\n * Sessions Manager) as launchable cards, the session list filterable by\n * workspace with lifecycle actions, housekeeping (session GC, worktree GC)\n * and the cron ledger. Talks to the daemon only through the\n * `window.McpApp.connect()` bridge — every action round-trips through\n * `app_tool_call { appId, tool, args }`, gated by {@link OPS_PANEL_TOOLS}\n * (same convention as `session-viewer`).\n *\n * Special-session recognition is by spawn label (`Session Watchdog` /\n * `Sessions Manager` — see `agents/`): the panel finds the newest session\n * wearing the label, shows its status, and offers Launch / Restart / Kill.\n * Launching the watchdog also (re)wires its `watchdog-tick` cron — the\n * `prompt-session` job that re-prompts the durable session every 5 minutes —\n * deleting any stale tick first so a dead session id never keeps a failing\n * cron behind.\n */\n\nimport { manager, MANAGER_LABEL } from \"./agents/manager.js\"\nimport {\n watchdog,\n WATCHDOG_LABEL,\n WATCHDOG_TICK_LABEL,\n WATCHDOG_TICK_PROMPT,\n WATCHDOG_TICK_SCHEDULE,\n} from \"./agents/watchdog.js\"\n\nexport const OPS_PANEL_TOOLS = [\n \"daemon_health\",\n \"session_list\",\n \"agent_start\",\n \"agent_prompt\",\n \"agent_kill\",\n \"session_restart\",\n \"session_archive\",\n \"session_gc\",\n \"cron_list\",\n \"cron_create\",\n \"cron_delete\",\n \"cron_run\",\n \"worktree_gc\",\n] as const\n\nconst APP_ID = \"@agentproto/ops-panel\"\n\n/** What the panel needs to launch each special session, injected below. */\nconst SPECIALS = [\n {\n key: \"watchdog\",\n label: WATCHDOG_LABEL,\n tagline: \"Health ticks every 5 min — flags, archives, never kills.\",\n prompt: watchdog.body ?? \"\",\n cron: {\n label: WATCHDOG_TICK_LABEL,\n schedule: WATCHDOG_TICK_SCHEDULE,\n prompt: WATCHDOG_TICK_PROMPT,\n },\n },\n {\n key: \"manager\",\n label: MANAGER_LABEL,\n tagline: \"You talk to it, it drives the other sessions.\",\n prompt: manager.body ?? \"\",\n cron: null,\n },\n] as const\n\nexport const OPS_PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Ops Panel</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--text3);flex:none}\n.chip.ok .d{background:var(--green)}\n.chip.bad .d{background:var(--red)}\n#ws-select{margin-left:auto;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11.5px;font-family:inherit}\n#refresh-btn{background:none;border:1px solid var(--border);cursor:pointer;color:var(--text2);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:16px}\n.sect h2{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;display:flex;align-items:center;gap:8px}\n.sect h2 .count{font-weight:600;color:var(--text3);text-transform:none;letter-spacing:0}\n\n#specials{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px}\n.card .ct{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700}\n.card .tagline{font-size:11px;color:var(--text2);margin-top:3px}\n.card .meta{font-size:10.5px;color:var(--text3);margin-top:7px;display:flex;flex-wrap:wrap;gap:5px 10px}\n.card .cronline{font-size:10.5px;margin-top:7px;display:flex;align-items:center;gap:6px;color:var(--text2)}\n.card .actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}\n\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-busy{background:rgba(63,185,80,.25);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n.b-warn{background:rgba(210,153,34,.15);color:var(--yellow)}\n\nbutton.act{background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;cursor:pointer;font-family:inherit}\nbutton.act:hover{border-color:var(--text3)}\nbutton.act.primary{background:rgba(88,166,255,.15);border-color:rgba(88,166,255,.4);color:var(--blue)}\nbutton.act.danger{color:var(--red)}\nbutton.act.danger:hover{border-color:var(--red)}\nbutton.act.confirm{background:rgba(248,81,73,.2);border-color:var(--red);color:var(--red)}\nbutton.act:disabled{opacity:.45;cursor:default}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--border)}\ntd{font-size:11.5px;padding:6px 8px;border-bottom:1px solid var(--bg3);vertical-align:middle}\ntr:hover td{background:var(--bg2)}\ntd .sn{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;display:block}\ntd .sid{font-size:10px;color:var(--text3);font-family:Menlo,Monaco,monospace}\n.ws-tag{display:inline-block;font-size:10px;font-weight:600;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:1px 6px;color:var(--text2)}\ntd.actions{white-space:nowrap;text-align:right}\ntd.actions button{margin-left:4px}\n\n.hk{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}\n.hk .card .nums{display:flex;gap:14px;margin-top:8px}\n.hk .num{display:flex;flex-direction:column}\n.hk .num b{font-size:17px;font-weight:700}\n.hk .num span{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.04em}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--red);color:var(--red)}\n.empty{color:var(--text3);font-size:11.5px;padding:8px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>Agentproto Ops</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon…</span></span>\n <select id=\"ws-select\"><option value=\"\">all workspaces</option></select>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻ refresh</button>\n </div>\n <div id=\"body\">\n <div class=\"sect\">\n <h2>Special sessions</h2>\n <div id=\"specials\"></div>\n </div>\n <div class=\"sect\">\n <h2>Sessions <span class=\"count\" id=\"sess-count\"></span></h2>\n <div id=\"sessions\"></div>\n </div>\n <div class=\"sect\">\n <h2>Housekeeping</h2>\n <div class=\"hk\" id=\"housekeeping\"></div>\n </div>\n <div class=\"sect\">\n <h2>Crons <span class=\"count\" id=\"cron-count\"></span></h2>\n <div id=\"crons\"></div>\n </div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SPECIALS = ${JSON.stringify(SPECIALS)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm: first click arms the button, second within 4s fires.\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar crons = [];\nvar wsFilter = \"\";\nvar pollTimer = null;\nvar busyActions = false;\n\nfunction isLive(s) { return !!s && (s.status === \"running\" || s.status === \"starting\"); }\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-busy\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction labelOf(s) {\n return s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id;\n}\n\nfunction specialSessionFor(sp) {\n var best = null;\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if ((s.label || s.title) !== sp.label) continue;\n if (!best) { best = s; continue; }\n // Prefer a live one; among equals prefer the newest.\n if (isLive(s) && !isLive(best)) { best = s; continue; }\n if (isLive(s) === isLive(best) && String(s.startedAt) > String(best.startedAt)) best = s;\n }\n return best;\n}\n\nfunction cronByLabel(label) {\n var hits = [];\n for (var i = 0; i < crons.length; i++) if (crons[i].label === label) hits.push(crons[i]);\n return hits;\n}\n\n// ============================================================\n// data loading\n// ============================================================\n\nfunction loadAll() {\n return Promise.all([\n callApp(\"session_list\", { kind: \"all\" }).then(function (d) {\n sessions = (d && d.sessions) || (Array.isArray(d) ? d : []);\n }),\n callApp(\"cron_list\", {}).then(function (d) {\n crons = Array.isArray(d) ? d : (d && d.jobs) || [];\n }).catch(function () { crons = []; }),\n ]).then(function () {\n renderWorkspaces();\n renderSpecials();\n renderSessions();\n renderCrons();\n updateHousekeepingNums();\n });\n}\n\nfunction loadHealth() {\n return callApp(\"daemon_health\", {}).then(function (d) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip ok\";\n var v = (d && (d.version || (d.build && d.build.sha) || d.status)) || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n }).catch(function (e) {\n var chip = document.getElementById(\"daemon-chip\");\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n });\n}\n\n// Housekeeping card is built ONCE (so the worktree plan text and armed\n// confirm buttons survive the 5s poll); only the counters re-render — see\n// updateHousekeepingNums. The worktree plan is a separate dry-run call, on\n// demand only (it walks git state — not something to hammer on every poll).\nfunction buildHousekeeping() {\n var el = document.getElementById(\"housekeeping\");\n el.innerHTML =\n '<div class=\"card\"><div class=\"ct\">Sessions GC</div>'\n + '<div class=\"nums\" id=\"gc-nums\"></div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"gc-archive\">Archive >7d</button>'\n + '<button class=\"act danger\" id=\"gc-forget\">Forget >30d</button>'\n + '</div></div>'\n + '<div class=\"card\"><div class=\"ct\">Worktrees</div>'\n + '<div class=\"meta\" id=\"wt-plan\">No plan yet — run a dry run.</div>'\n + '<div class=\"actions\">'\n + '<button class=\"act\" id=\"wt-dry\">Dry run</button>'\n + '<button class=\"act danger\" id=\"wt-apply\" disabled>Reclaim now</button>'\n + '</div></div>';\n\n document.getElementById(\"gc-archive\").addEventListener(\"click\", function () {\n runAction(\"session_gc\", { olderThanDays: 7 }, \"GC: archived old terminal sessions\");\n });\n var forgetBtn = document.getElementById(\"gc-forget\");\n forgetBtn.addEventListener(\"click\", function () {\n confirmClick(\"gc-forget\", forgetBtn, function () {\n runAction(\"session_gc\", { olderThanDays: 30, forget: true }, \"GC: forgot >30d terminal sessions\");\n });\n });\n document.getElementById(\"wt-dry\").addEventListener(\"click\", function () { worktreePlan(false); });\n var applyBtn = document.getElementById(\"wt-apply\");\n applyBtn.addEventListener(\"click\", function () {\n confirmClick(\"wt-apply\", applyBtn, function () { worktreePlan(true); });\n });\n updateHousekeepingNums();\n}\n\nfunction updateHousekeepingNums() {\n var el = document.getElementById(\"gc-nums\");\n if (!el) return;\n var terminal = 0, live = 0;\n for (var i = 0; i < sessions.length; i++) {\n if (isLive(sessions[i])) live++;\n else terminal++;\n }\n el.innerHTML =\n '<div class=\"num\"><b>' + live + '</b><span>live</span></div>'\n + '<div class=\"num\"><b>' + terminal + '</b><span>terminal</span></div>';\n}\n\nfunction worktreePlan(apply) {\n var el = document.getElementById(\"wt-plan\");\n el.textContent = apply ? \"reclaiming\\\\u2026\" : \"planning\\\\u2026\";\n callApp(\"worktree_gc\", apply ? { apply: true } : {}).then(function (d) {\n var entries = (d && (d.plan || d.entries || d.worktrees)) || [];\n var counts = { reclaim: 0, salvage: 0, hold: 0 };\n for (var i = 0; i < entries.length; i++) {\n var c = entries[i].classification || entries[i].class || entries[i].action;\n if (counts[c] != null) counts[c]++;\n }\n el.textContent = (apply ? \"applied — \" : \"dry run — \")\n + counts.reclaim + \" reclaim / \" + counts.salvage + \" salvage / \" + counts.hold + \" hold\"\n + (entries.length === 0 ? \" (no linked worktrees)\" : \"\");\n document.getElementById(\"wt-apply\").disabled = counts.reclaim === 0 || apply;\n if (apply) toast(\"Worktree GC applied\");\n }).catch(function (e) {\n el.textContent = \"worktree_gc: \" + e.message;\n });\n}\n\n// ============================================================\n// rendering\n// ============================================================\n\nfunction renderWorkspaces() {\n var sel = document.getElementById(\"ws-select\");\n var seen = {};\n var slugs = [];\n for (var i = 0; i < sessions.length; i++) {\n var w = sessions[i].workspaceSlug;\n if (w && !seen[w]) { seen[w] = true; slugs.push(w); }\n }\n slugs.sort();\n var cur = wsFilter;\n var html = '<option value=\"\">all workspaces</option>';\n for (var j = 0; j < slugs.length; j++) {\n html += '<option value=\"' + escHtml(slugs[j]) + '\"' + (slugs[j] === cur ? \" selected\" : \"\") + '>' + escHtml(slugs[j]) + \"</option>\";\n }\n sel.innerHTML = html;\n}\n\nfunction renderSpecials() {\n var el = document.getElementById(\"specials\");\n var html = \"\";\n for (var i = 0; i < SPECIALS.length; i++) {\n var sp = SPECIALS[i];\n var s = specialSessionFor(sp);\n var badge = s ? displayBadge(s) : { label: \"not running\", cls: \"b-end\" };\n var meta = \"\";\n if (s) {\n meta += \"<span>\" + escHtml(s.id) + \"</span>\";\n if (s.startedAt) meta += \"<span>started \" + escHtml(fmtRelative(s.startedAt)) + \"</span>\";\n if (s.costUsd != null) meta += \"<span>$\" + Number(s.costUsd).toFixed(2) + \"</span>\";\n }\n var cronline = \"\";\n if (sp.cron) {\n var jobs = cronByLabel(sp.cron.label);\n if (jobs.length === 0) {\n cronline = '<span class=\"badge b-end\">no cron</span> tick not scheduled';\n } else {\n var job = jobs[0];\n var ok = !job.lastResult || job.lastResult.ok;\n var pointsAt = job.action && job.action.sessionId;\n var stale = s && isLive(s) && pointsAt && pointsAt !== s.id;\n cronline = '<span class=\"badge ' + (job.active ? (ok && !stale ? \"b-run\" : \"b-warn\") : \"b-end\") + '\">'\n + (job.active ? (ok && !stale ? \"tick ok\" : (stale ? \"tick stale\" : \"tick failing\")) : \"tick off\") + \"</span> \"\n + escHtml(job.schedule) + (pointsAt ? \" → \" + escHtml(pointsAt) : \"\")\n + (!ok && job.lastResult && job.lastResult.summary ? \" · \" + escHtml(String(job.lastResult.summary).slice(0, 60)) : \"\");\n }\n }\n var actions = \"\";\n if (!s || !isLive(s)) {\n actions += '<button class=\"act primary\" data-sp=\"' + sp.key + '\" data-do=\"launch\">Launch</button>';\n if (s) actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"restart\">Restart last</button>';\n } else {\n if (sp.cron) {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Tick now</button>';\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"fixcron\">Re-wire cron</button>';\n } else {\n actions += '<button class=\"act\" data-sp=\"' + sp.key + '\" data-do=\"tick\">Ping</button>';\n }\n actions += '<button class=\"act danger\" data-sp=\"' + sp.key + '\" data-do=\"kill\">Kill</button>';\n }\n html += '<div class=\"card\">'\n + '<div class=\"ct\">' + escHtml(sp.label) + ' <span class=\"badge ' + badge.cls + '\">' + escHtml(badge.label) + \"</span></div>\"\n + '<div class=\"tagline\">' + escHtml(sp.tagline) + \"</div>\"\n + (meta ? '<div class=\"meta\">' + meta + \"</div>\" : \"\")\n + (cronline ? '<div class=\"cronline\">' + cronline + \"</div>\" : \"\")\n + '<div class=\"actions\">' + actions + \"</div>\"\n + \"</div>\";\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-sp]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { specialAction(btn.getAttribute(\"data-sp\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderSessions() {\n var el = document.getElementById(\"sessions\");\n var rows = [];\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n if (wsFilter && s.workspaceSlug !== wsFilter) continue;\n rows.push(s);\n }\n rows.sort(function (a, b) {\n var la = isLive(a) ? 1 : 0, lb = isLive(b) ? 1 : 0;\n if (la !== lb) return lb - la;\n return String(b.startedAt || \"\").localeCompare(String(a.startedAt || \"\"));\n });\n var liveCount = 0;\n for (var c = 0; c < rows.length; c++) if (isLive(rows[c])) liveCount++;\n document.getElementById(\"sess-count\").textContent = liveCount + \" live / \" + rows.length + \" shown\";\n if (rows.length === 0) {\n el.innerHTML = '<div class=\"empty\">No sessions' + (wsFilter ? \" in \" + escHtml(wsFilter) : \"\") + \".</div>\";\n return;\n }\n var html = \"<table><thead><tr><th>Session</th><th>Workspace</th><th>Status</th><th>Started</th><th>Cost</th><th></th></tr></thead><tbody>\";\n var shown = rows.slice(0, 80);\n for (var j = 0; j < shown.length; j++) {\n var s2 = shown[j];\n var db = displayBadge(s2);\n var acts = \"\";\n if (isLive(s2)) {\n acts += '<button class=\"act danger\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"kill\">Kill</button>';\n } else {\n acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"restart\">Restart</button>';\n if (!s2.archived) acts += '<button class=\"act\" data-id=\"' + escHtml(s2.id) + '\" data-do=\"archive\">Archive</button>';\n }\n html += \"<tr>\"\n + '<td><span class=\"sn\" title=\"' + escHtml(labelOf(s2)) + '\">' + escHtml(labelOf(s2)) + '</span><span class=\"sid\">' + escHtml(s2.id) + \"</span></td>\"\n + \"<td>\" + (s2.workspaceSlug ? '<span class=\"ws-tag\">' + escHtml(s2.workspaceSlug) + \"</span>\" : \"\") + \"</td>\"\n + '<td><span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span></td>\"\n + \"<td>\" + escHtml(fmtRelative(s2.startedAt)) + \"</td>\"\n + \"<td>\" + (s2.costUsd != null ? \"$\" + Number(s2.costUsd).toFixed(2) : \"\") + \"</td>\"\n + '<td class=\"actions\">' + acts + \"</td>\"\n + \"</tr>\";\n }\n html += \"</tbody></table>\";\n if (rows.length > shown.length) html += '<div class=\"empty\">… ' + (rows.length - shown.length) + \" more (narrow with the workspace filter)</div>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-id]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { sessionAction(btn.getAttribute(\"data-id\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\nfunction renderCrons() {\n var el = document.getElementById(\"crons\");\n document.getElementById(\"cron-count\").textContent = crons.length ? crons.length + \" jobs\" : \"\";\n if (crons.length === 0) {\n el.innerHTML = '<div class=\"empty\">No cron jobs.</div>';\n return;\n }\n var html = \"<table><thead><tr><th>Label</th><th>Schedule</th><th>State</th><th>Last</th><th>Next</th><th></th></tr></thead><tbody>\";\n for (var i = 0; i < crons.length; i++) {\n var c = crons[i];\n var last = c.lastResult == null ? \"\" : (c.lastResult.ok ? \"ok\" : \"failed\");\n var lastCls = c.lastResult == null ? \"b-end\" : (c.lastResult.ok ? \"b-run\" : \"b-err\");\n html += \"<tr>\"\n + '<td><span class=\"sn\">' + escHtml(c.label || c.id) + \"</span></td>\"\n + \"<td>\" + escHtml(c.schedule) + \"</td>\"\n + '<td><span class=\"badge ' + (c.active ? \"b-run\" : \"b-end\") + '\">' + (c.active ? \"active\" : \"off\") + \"</span></td>\"\n + \"<td>\" + (last ? '<span class=\"badge ' + lastCls + '\" title=\"' + escHtml((c.lastResult && c.lastResult.summary) || \"\") + '\">' + last + \"</span>\" : \"\") + \"</td>\"\n + \"<td>\" + escHtml(c.nextRunAt ? fmtRelative(c.nextRunAt).replace(\" ago\", \"\") : \"\") + \"</td>\"\n + '<td class=\"actions\">'\n + '<button class=\"act\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"run\">Run</button>'\n + '<button class=\"act danger\" data-cron=\"' + escHtml(c.id) + '\" data-do=\"delete\">Delete</button>'\n + \"</td></tr>\";\n }\n html += \"</tbody></table>\";\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-cron]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { cronAction(btn.getAttribute(\"data-cron\"), btn.getAttribute(\"data-do\"), btn); });\n })(btns[k]);\n }\n}\n\n// ============================================================\n// actions\n// ============================================================\n\nfunction runAction(tool, args, okMsg) {\n if (busyActions) return Promise.resolve();\n busyActions = true;\n return callApp(tool, args).then(function (d) {\n busyActions = false;\n if (okMsg) toast(okMsg);\n return loadAll().then(function () { return d; });\n }).catch(function (e) {\n busyActions = false;\n toast(tool + \": \" + e.message, true);\n throw e;\n });\n}\n\nfunction findSpecial(key) {\n for (var i = 0; i < SPECIALS.length; i++) if (SPECIALS[i].key === key) return SPECIALS[i];\n return null;\n}\n\nfunction rewireTickCron(sp, sessionId) {\n if (!sp.cron) return Promise.resolve();\n var stale = cronByLabel(sp.cron.label);\n var chain = Promise.resolve();\n for (var i = 0; i < stale.length; i++) {\n (function (jobId) {\n chain = chain.then(function () { return callApp(\"cron_delete\", { jobId: jobId }); });\n })(stale[i].id);\n }\n return chain.then(function () {\n return callApp(\"cron_create\", {\n schedule: sp.cron.schedule,\n recurring: true,\n label: sp.cron.label,\n action: { kind: \"prompt-session\", sessionId: sessionId, prompt: sp.cron.prompt },\n });\n });\n}\n\nfunction specialAction(key, what, btn) {\n var sp = findSpecial(key);\n if (!sp) return;\n var s = specialSessionFor(sp);\n if (what === \"launch\") {\n btn.disabled = true;\n callApp(\"agent_start\", {\n adapter: \"claude-code\",\n label: sp.label,\n prompt: sp.prompt,\n keepAlive: true,\n }).then(function (d) {\n var id = d && d.id;\n toast(sp.label + \" launched\" + (id ? \" (\" + id + \")\" : \"\"));\n return id && sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"launch: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"restart\" && s) {\n btn.disabled = true;\n callApp(\"session_restart\", { idOrName: s.id }).then(function (d) {\n var id = (d && d.id) || s.id;\n toast(sp.label + \" restarted\");\n return sp.cron ? rewireTickCron(sp, id) : null;\n }).then(function () { return loadAll(); })\n .catch(function (e) { toast(\"restart: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"tick\" && s) {\n var prompt = sp.cron ? sp.cron.prompt : \"Status check: report where things stand, briefly.\";\n runAction(\"agent_prompt\", { sessionId: s.id, prompt: prompt }, sp.label + \" prompted\");\n return;\n }\n if (what === \"fixcron\" && s) {\n btn.disabled = true;\n rewireTickCron(sp, s.id).then(function () {\n toast(\"Cron re-wired to \" + s.id);\n return loadAll();\n }).catch(function (e) { toast(\"cron: \" + e.message, true); btn.disabled = false; });\n return;\n }\n if (what === \"kill\" && s) {\n confirmClick(\"kill-\" + s.id, btn, function () {\n runAction(\"agent_kill\", { sessionId: s.id }, sp.label + \" killed\");\n });\n }\n}\n\nfunction sessionAction(id, what, btn) {\n if (what === \"kill\") {\n confirmClick(\"kill-\" + id, btn, function () {\n runAction(\"agent_kill\", { sessionId: id }, \"Killed \" + id);\n });\n return;\n }\n if (what === \"restart\") { runAction(\"session_restart\", { idOrName: id }, \"Restarted \" + id); return; }\n if (what === \"archive\") { runAction(\"session_archive\", { idOrName: id }, \"Archived \" + id); }\n}\n\nfunction cronAction(jobId, what, btn) {\n if (what === \"run\") { runAction(\"cron_run\", { jobId: jobId }, \"Cron fired\"); return; }\n if (what === \"delete\") {\n confirmClick(\"cron-\" + jobId, btn, function () {\n runAction(\"cron_delete\", { jobId: jobId }, \"Cron deleted\");\n });\n }\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () { loadHealth(); loadAll(); });\ndocument.getElementById(\"ws-select\").addEventListener(\"change\", function (e) {\n wsFilter = e.target.value;\n renderSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n buildHousekeeping();\n loadHealth();\n return loadAll();\n })\n .then(function () {\n pollTimer = setInterval(loadAll, 5000);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `ops-panel` — the agentproto daemon operations cockpit as an app.\n *\n * Two durable \"special sessions\" ship as agents — the Session Watchdog\n * (cron-ticked health checks: classify, flag, archive; never kills) and the\n * Sessions Manager (the coordinator the human talks to, which drives the\n * other sessions). The panel (`ui.ts`) launches/restarts them by label,\n * wires the watchdog's `watchdog-tick` cron, lists every session filterable\n * by workspace with lifecycle actions, and fronts housekeeping: session GC,\n * worktree GC (dry-run first), and the cron ledger.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { manager } from \"./agents/manager.js\"\nimport { watchdog } from \"./agents/watchdog.js\"\nimport { OPS_PANEL_HTML, OPS_PANEL_TOOLS } from \"./ui.js\"\n\nexport const opsPanel: AppHandle = defineApp({\n id: \"@agentproto/ops-panel\",\n name: \"Ops Panel\",\n version: \"0.1.0\",\n description: \"Daemon ops cockpit — watchdog & manager sessions, workspace-filtered session list, GC and cron housekeeping.\",\n agents: [watchdog, manager],\n ui: {\n html: OPS_PANEL_HTML,\n title: \"Ops Panel\",\n tools: [...OPS_PANEL_TOOLS],\n },\n})\n\nexport { watchdog, manager }\nexport { WATCHDOG_LABEL, WATCHDOG_TICK_LABEL, WATCHDOG_TICK_PROMPT, WATCHDOG_TICK_SCHEDULE } from \"./agents/watchdog.js\"\nexport { MANAGER_LABEL } from \"./agents/manager.js\"\n","/**\n * The `@agentproto/config` deep-link fragment grammar (plan §3.4):\n *\n * #<section>[/<id>[/<sub>]]\n * section := wallets | harnesses | models | capabilities | defaults | remote | advanced\n * id := encodeURIComponent(profileId | adapterSlug | modelRef | configKey | \"pairing\")\n *\n * Written in plain ES5-safe syntax (`var`/`function`, no arrows, no\n * destructuring) and inlined verbatim into the panel's `<script>` via\n * `parseConfigFragment.toString()` / `buildConfigFragment.toString()`\n * (see `ui.ts`) — this is the ONE copy of the parser, tested directly here\n * and run unmodified in the browser. Every free variable it needs (the\n * section list) is declared INSIDE the function body so the embedded\n * source has no dependency on this module's closure.\n */\n\nexport const CONFIG_SECTIONS = [\"wallets\", \"harnesses\", \"models\", \"capabilities\", \"defaults\", \"remote\", \"advanced\"] as const\n\nexport type ConfigSection = (typeof CONFIG_SECTIONS)[number]\n\nexport interface ParsedConfigFragment {\n section: ConfigSection\n id?: string\n sub?: string\n}\n\n/** Unknown section -> first section (`wallets`). Unknown/absent id or sub is\n * simply omitted — never guessed, never a blank crash. A malformed\n * percent-encoding in `id`/`sub` falls back to the raw segment rather than\n * throwing. */\nexport function parseConfigFragment(hash: string): ParsedConfigFragment {\n var SECTIONS = [\"wallets\", \"harnesses\", \"models\", \"capabilities\", \"defaults\", \"remote\", \"advanced\"]\n var raw = String(hash || \"\").replace(/^#/, \"\")\n var parts = raw.split(\"/\").filter(function (p) {\n return p.length > 0\n })\n var sectionRaw = parts[0] || \"\"\n var section = SECTIONS.indexOf(sectionRaw) !== -1 ? sectionRaw : SECTIONS[0]!\n var result: { section: string; id?: string; sub?: string } = { section: section }\n var idPart = parts[1]\n if (idPart !== undefined) {\n try {\n result.id = decodeURIComponent(idPart)\n } catch (e) {\n result.id = idPart\n }\n }\n var subPart = parts[2]\n if (subPart !== undefined) {\n try {\n result.sub = decodeURIComponent(subPart)\n } catch (e) {\n result.sub = subPart\n }\n }\n return result as ParsedConfigFragment\n}\n\n/** Inverse of {@link parseConfigFragment} — builds the hash string (with the\n * leading `#`) the panel writes back to `location.hash` on navigation. */\nexport function buildConfigFragment(section: string, id?: string, sub?: string): string {\n var out = \"#\" + section\n if (id !== undefined && id !== null && id !== \"\") out += \"/\" + encodeURIComponent(id)\n if (sub !== undefined && sub !== null && sub !== \"\") out += \"/\" + encodeURIComponent(sub)\n return out\n}\n\n/**\n * MCP-hosted deep links (plan §3.4, PR-5): a browser tab resolves\n * `location.hash` directly, but an MCP host has no URL bar for this panel —\n * it opens `app_ui_config { view }` instead, and the bridge surfaces that as\n * a `ui/notifications/tool-input` (or `-result`) arrival (see `ui.ts`'s\n * `handleToolInput`).\n *\n * Precedence: `location.hash` wins on the panel's very first route — an\n * actual browser deep link must not be overridden by a stale/default tool\n * call. Every arrival after the first is a live \"go to this view\" command\n * from the host and always applies, regardless of the current hash.\n *\n * `arrivalIndex` is 1-based (the 1st, 2nd, ... tool-input notification this\n * boot has seen); `hashPresentAtLoad` is whether `location.hash` was\n * non-empty when the panel's script started running, captured once before\n * any routing happens. Pure — no DOM — so it is unit-testable without a\n * browser or jsdom, same as {@link parseConfigFragment}.\n */\nexport function shouldApplyIncomingView(arrivalIndex: number, hashPresentAtLoad: boolean): boolean {\n return !(arrivalIndex === 1 && hashPresentAtLoad)\n}\n","/**\n * `@agentproto/config` — a self-contained MCP App panel for daemon-wide\n * configuration. Its own app, not a section of `ops-panel` — that app is\n * referenced only for the mechanics this panel shares (\n * `window.McpApp.connect()` -> `app_tool_call { appId, tool, args }`, the\n * nested-envelope `unwrapText`, one card per entity, two-click confirm). See\n * its `ui.ts` for the pattern this file follows.\n *\n * Six sections, each independently loaded/rendered so one failing card never\n * blanks the others: Wallets (auth profiles + spend), Harnesses (adapters +\n * capabilities + presets + roles), Models (the vendor/product/route\n * catalog), Defaults & messaging (daemon config knobs), Remote & pairing,\n * and Advanced (raw config dump). This PR adds the write flows: wallet\n * lifecycle + curation, harness preset CRUD, a generic config_get/config_set\n * editor for Defaults/per-harness defaults/titler.model, and remote/pairing\n * lifecycle.\n *\n * Three daemon tools this panel wants (`config_get`, `config_set`, and\n * every write tool below) may not exist yet on the daemon it talks to\n * (parallel PRs) — `loadTool` turns an \"unknown daemon tool\" failure into a\n * muted, section-local notice instead of an error, and the Defaults section\n * falls back to `daemon_health`'s five effective knobs when `config_get` is\n * absent.\n *\n * Secret discipline: a credential (auth_profile_create's `credential`, a\n * remote bearer, a pairing offer URL) is read from a form field, used ONCE\n * in the same handler, and the DOM field is cleared before the async call\n * even starts. Nothing secret is ever stored in section state, written to\n * `updateModelContext`, or logged.\n *\n * Deep-link contract (plan section 3.4): `#<section>[/<id>[/<sub>]]`, parsed\n * by `parseConfigFragment` (`fragment.ts`) — embedded here via\n * `.toString()` so the exact function tested in `fragment.test.ts` is the\n * one that runs in the browser, not a second hand-copied version. A browser\n * tab resolves this from `location.hash`; an MCP host has no URL bar, so it\n * opens `app_ui_config { view }` instead — `handleToolInput` below turns\n * that into the same fragment via the bridge's `onToolInput` (app-ui-apps.ts).\n * `location.hash` wins when the panel first loads with one already set;\n * every tool-input notification after that re-routes unconditionally.\n */\n\nimport { parseConfigFragment, buildConfigFragment, shouldApplyIncomingView } from \"./fragment.js\"\n\nexport const CONFIG_TOOLS = [\n \"auth_profile_list\",\n \"auth_profile_create\",\n \"auth_profile_delete\",\n \"auth_profile_set_enabled\",\n \"auth_profile_set_models\",\n \"auth_profile_refresh_models\",\n \"auth_profile_import\",\n \"auth_discover_credentials\",\n \"auth_profile_update\",\n \"adapter_list\",\n \"harness_capabilities\",\n \"capabilities_inventory\",\n \"mcp_imported_tool_list\",\n \"mcp_imported_status\",\n \"mcp_import\",\n \"mcp_imported_remove\",\n \"harness_preset_list\",\n \"harness_preset_create\",\n \"harness_preset_delete\",\n \"harness_preset_set_default\",\n \"catalog_models\",\n \"catalog_provider_models\",\n \"role_list\",\n \"usage_rollup\",\n \"config_get\",\n \"config_set\",\n \"daemon_health\",\n \"remote_status\",\n \"remote_enable\",\n \"remote_disable\",\n \"pair_list\",\n \"pair_offer\",\n \"pair_revoke\",\n \"tunnel_list\",\n \"tunnel_status\",\n] as const\n\nconst APP_ID = \"@agentproto/config\"\n\nconst SECTIONS: ReadonlyArray<{ id: string; label: string }> = [\n { id: \"wallets\", label: \"Wallets\" },\n { id: \"harnesses\", label: \"Harnesses\" },\n { id: \"models\", label: \"Models\" },\n { id: \"capabilities\", label: \"Capabilities\" },\n { id: \"defaults\", label: \"Defaults\" },\n { id: \"remote\", label: \"Remote\" },\n { id: \"advanced\", label: \"Advanced\" },\n]\n\nexport const CONFIG_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto config</title>\n<style>\n/* Theme tokens: prefer a live --vscode-* var (VS Code webview host), else\n the brand-neutral standalone default, split light/dark by\n prefers-color-scheme — same mapping convention as work-board/ui/style.css.\n No JS theme detection: a host that swaps its theme live is never frozen. */\n:root{\n --cfg-bg: var(--vscode-editor-background, #f5f6f3);\n --cfg-surface: var(--vscode-sideBar-background, #ffffff);\n --cfg-surface-2: var(--vscode-editorWidget-background, #eef0ec);\n --cfg-ink: var(--vscode-editor-foreground, #171a1e);\n --cfg-ink-faint: var(--vscode-descriptionForeground, #6b7280);\n --cfg-border: var(--vscode-panel-border, #dde0da);\n --cfg-accent: var(--vscode-button-background, #0f62fe);\n --cfg-warning: var(--vscode-charts-yellow, #b45309);\n --cfg-danger: var(--vscode-charts-red, #b91c1c);\n --cfg-success: var(--vscode-charts-green, #15803d);\n --cfg-muted-bg: var(--vscode-input-background, #e5e7eb);\n}\n@media (prefers-color-scheme: dark){\n :root{\n --cfg-bg: var(--vscode-editor-background, #0f1115);\n --cfg-surface: var(--vscode-sideBar-background, #161a20);\n --cfg-surface-2: var(--vscode-editorWidget-background, #1c2129);\n --cfg-ink: var(--vscode-editor-foreground, #e6e9ee);\n --cfg-ink-faint: var(--vscode-descriptionForeground, #8b93a1);\n --cfg-border: var(--vscode-panel-border, #2a2f38);\n --cfg-accent: var(--vscode-button-background, #4589ff);\n --cfg-warning: var(--vscode-charts-yellow, #d29922);\n --cfg-danger: var(--vscode-charts-red, #f85149);\n --cfg-success: var(--vscode-charts-green, #3fb950);\n --cfg-muted-bg: var(--vscode-input-background, #262b33);\n }\n}\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--cfg-bg);color:var(--cfg-ink)}\n#app{display:flex;flex-direction:column;height:100%;overflow:hidden}\n\n#topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}\n#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}\n.chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);padding:3px 9px;border-radius:999px}\n.chip .d{width:7px;height:7px;border-radius:50%;background:var(--cfg-ink-faint);flex:none}\n.chip.ok .d{background:var(--cfg-success)}\n.chip.bad .d{background:var(--cfg-danger)}\n#refresh-btn{margin-left:auto;background:none;border:1px solid var(--cfg-border);cursor:pointer;color:var(--cfg-ink-faint);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}\n#refresh-btn:hover{color:var(--cfg-ink);background:var(--cfg-surface-2)}\n\n#nav{display:flex;gap:4px;padding:8px 14px 0;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}\n#nav button{background:none;border:none;border-bottom:2px solid transparent;color:var(--cfg-ink-faint);font-size:12px;font-weight:600;font-family:inherit;padding:7px 10px;cursor:pointer}\n#nav button:hover{color:var(--cfg-ink)}\n#nav button.active{color:var(--cfg-ink);border-bottom-color:var(--cfg-accent)}\n\n#pending-banner{margin:8px 14px 0;padding:6px 10px;background:var(--cfg-surface-2);border:1px solid var(--cfg-warning);color:var(--cfg-warning);border-radius:6px;font-size:11px;flex-shrink:0}\n\n#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px}\n.section{display:none;flex-direction:column;gap:12px}\n.section.active{display:flex}\n\n.notice{font-size:11.5px;color:var(--cfg-warning);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:6px;padding:6px 10px}\n.muted{color:var(--cfg-ink-faint)}\n.muted-note{color:var(--cfg-ink-faint);font-size:11px;padding:6px 10px}\n.empty{color:var(--cfg-ink-faint);font-size:11.5px;padding:8px}\n\n.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px}\n.card{background:var(--cfg-surface);border:1px solid var(--cfg-border);border-radius:8px;overflow:hidden;scroll-margin-top:12px}\n.card.hl{outline:2px solid var(--cfg-accent);outline-offset:-1px}\n.card .title{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-weight:700;background:var(--cfg-surface-2);border-bottom:1px solid var(--cfg-border)}\n.card .title .aside{font-weight:400;color:var(--cfg-ink-faint);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}\n.card .body{padding:8px 10px}\n.card dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;align-items:baseline}\n.card dt{color:var(--cfg-ink-faint);white-space:nowrap;font-size:11.5px}\n.card dd{text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11.5px}\n.bar{grid-column:1/3;height:4px;background:var(--cfg-muted-bg);border-radius:2px;overflow:hidden;margin:2px 0 4px}\n.bar>span{display:block;height:100%;background:var(--cfg-accent)}\n.bar.warn>span{background:var(--cfg-warning)}\n\n.sect-h{font-size:11px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.pillbar{display:flex;gap:4px}\n.pillbar button{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:999px;cursor:pointer;font-family:inherit}\n.pillbar button.active{color:var(--cfg-ink);border-color:var(--cfg-accent)}\nlabel.inline{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--cfg-ink-faint)}\nselect,input[type=text],input[type=password],input[type=number],textarea{background:var(--cfg-surface-2);color:var(--cfg-ink);border:1px solid var(--cfg-border);border-radius:5px;padding:3px 7px;font-size:11px;font-family:inherit}\ninput:disabled,select:disabled,textarea:disabled{opacity:.5;cursor:not-allowed}\n\n.tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:5px;padding:1px 6px;color:var(--cfg-ink-faint)}\n.tag.ok{color:var(--cfg-success);border-color:var(--cfg-success)}\n.tag.bad{color:var(--cfg-danger);border-color:var(--cfg-danger)}\n.tag.warn-tag{color:var(--cfg-warning);border-color:var(--cfg-warning)}\na.link{color:var(--cfg-accent);text-decoration:none;cursor:pointer}\na.link:hover{text-decoration:underline}\n\ntable{width:100%;border-collapse:collapse}\nth{font-size:10px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--cfg-border)}\ntd{font-size:11.5px;padding:5px 8px;border-bottom:1px solid var(--cfg-surface-2);vertical-align:middle}\ntr.hl td{background:var(--cfg-surface-2)}\ntd.matrix-cell{text-align:center;cursor:pointer}\ntd.matrix-cell.disabled{opacity:.35;cursor:default}\n\n.btn-sm{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink);font-size:10.5px;font-weight:600;padding:3px 8px;border-radius:5px;cursor:pointer;font-family:inherit}\n.btn-sm:hover{border-color:var(--cfg-accent)}\n.btn-sm:disabled{opacity:.5;cursor:not-allowed}\n.btn-sm.danger{color:var(--cfg-danger);border-color:var(--cfg-danger)}\n.btn-sm.confirm{background:var(--cfg-danger);color:#fff;border-color:var(--cfg-danger)}\n.btn-sm.primary{color:var(--cfg-accent);border-color:var(--cfg-accent)}\n\n.cfg-row{display:contents}\n.cfg-help{font-size:10.5px}\n.cfg-err{color:var(--cfg-danger);font-size:10.5px}\n\n.form-panel{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:10px;margin-top:6px;display:flex;flex-direction:column;gap:6px}\n.form-panel .field{display:flex;flex-direction:column;gap:2px;font-size:11px}\n.form-panel .field label{color:var(--cfg-ink-faint);font-size:10.5px}\n.form-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}\n\n.reveal-box{background:var(--cfg-surface-2);border:1px dashed var(--cfg-warning);border-radius:8px;padding:10px;font-size:11px;display:flex;flex-direction:column;gap:6px}\n.reveal-box code{display:block;word-break:break-all;background:var(--cfg-muted-bg);padding:4px 6px;border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:10.5px}\n\n.mm-list{max-height:160px;overflow-y:auto;margin-top:4px;padding:4px;background:var(--cfg-muted-bg);border-radius:5px}\n.wallet-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}\n\n#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}\n#toast.err{border-color:var(--cfg-danger);color:var(--cfg-danger)}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"topbar\">\n <h1>agentproto config</h1>\n <span class=\"chip\" id=\"daemon-chip\"><span class=\"d\"></span><span id=\"daemon-txt\">daemon…</span></span>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻ refresh</button>\n </div>\n <div id=\"nav\"></div>\n <div class=\"notice\" id=\"pending-banner\" style=\"display:none\"></div>\n <div id=\"body\">\n <div class=\"notice\" id=\"notfound\" style=\"display:none\"></div>\n <div class=\"section\" data-section=\"wallets\" id=\"sec-wallets\"></div>\n <div class=\"section\" data-section=\"harnesses\" id=\"sec-harnesses\"></div>\n <div class=\"section\" data-section=\"models\" id=\"sec-models\"></div>\n <div class=\"section\" data-section=\"capabilities\" id=\"sec-capabilities\"></div>\n <div class=\"section\" data-section=\"defaults\" id=\"sec-defaults\"></div>\n <div class=\"section\" data-section=\"remote\" id=\"sec-remote\"></div>\n <div class=\"section\" data-section=\"advanced\" id=\"sec-advanced\"></div>\n </div>\n</div>\n<div id=\"toast\"></div>\n<script>\nvar callTool = null;\nvar updateModelContext = null;\nvar onTeardownFn = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\nvar SECTIONS = ${JSON.stringify(SECTIONS)};\n\n// ── deep-link fragment parser (fragment.ts, embedded verbatim so the\n// browser runs the exact function fragment.test.ts exercises) ──\n${parseConfigFragment.toString()}\n${buildConfigFragment.toString()}\n${shouldApplyIncomingView.toString()}\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — same convention as ops-panel/session-viewer.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\n// Never throws: every card loads independently, so one failing tool must\n// never crash another card's render. An \"unknown daemon tool\" failure (a\n// write tool, or config_get/config_set, on a daemon that predates them) is\n// distinguished from any other failure so the caller can render a muted\n// feature-detect note instead of an error. Used for BOTH reads and writes:\n// every write tool here answers either a JSON object (success) or a plain\n// string (its own error text), the same two shapes a read tool answers.\nfunction loadTool(tool, args) {\n return callApp(tool, args).then(function (d) {\n if (typeof d === \"string\") {\n return { ok: false, message: d, unknown: /unknown daemon tool/.test(d) };\n }\n if (d && typeof d === \"object\" && typeof d.error === \"string\") {\n return { ok: false, message: d.error, unknown: /unknown daemon tool/.test(d.error) };\n }\n return { ok: true, data: d };\n }).catch(function (e) {\n var msg = (e && e.message) || String(e);\n return { ok: false, message: msg, unknown: false };\n });\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction humanize(s) { return String(s || \"\").replace(/[-_]+/g, \" \"); }\n\nfunction formatTokens(n) {\n if (n == null || !isFinite(n)) return \"\";\n var trim = function (t) { return t.replace(/\\\\.0$/, \"\"); };\n if (n >= 1000000) return trim((n / 1000000).toFixed(1)) + \"M\";\n if (n >= 1000) return trim((n / 1000).toFixed(1)) + \"k\";\n return String(n);\n}\n\nfunction formatCost(usd) {\n if (usd == null || !isFinite(usd)) return \"\";\n if (usd > 0 && usd < 0.01) return \"<$0.01\";\n return \"$\" + usd.toFixed(2);\n}\n\nfunction formatDate(iso) {\n if (!iso) return \"\";\n var d = new Date(iso);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleString();\n}\n\nfunction formatMs(ms) {\n if (ms == null || !isFinite(ms)) return \"\";\n if (ms >= 60000) return Math.round(ms / 60000) + \"m\";\n if (ms >= 1000) return Math.round(ms / 1000) + \"s\";\n return ms + \"ms\";\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nvar BILLING_LABELS = { \"oauth-bearer\": \"Subscription\", \"api-key\": \"API key\" };\n\n// ── card builders (session-header-cards.tsx visual language: card surface,\n// title strip w/ muted aside, two-col dl grid, 4px progress bar) ──\nfunction cardOpen(id, title, aside) {\n return '<div class=\"card\" data-card-id=\"' + escHtml(id) + '\">'\n + '<div class=\"title\"><span>' + escHtml(title) + '</span>'\n + (aside ? '<span class=\"aside\">' + aside + '</span>' : '')\n + '</div><div class=\"body\"><dl>';\n}\nfunction cardClose() { return '</dl></div></div>'; }\nfunction row(label, value, title) {\n return '<dt>' + escHtml(label) + '</dt><dd' + (title ? ' title=\"' + escHtml(title) + '\"' : '') + '>' + value + '</dd>';\n}\nfunction bar(pct) {\n var p = Math.max(0, Math.min(100, pct));\n return '<div class=\"bar' + (p >= 80 ? ' warn' : '') + '\"><span style=\"width:' + p + '%\"></span></div>';\n}\n\nvar toastTimer = null;\nfunction toast(msg, isErr) {\n var el = document.getElementById(\"toast\");\n el.textContent = msg;\n el.className = isErr ? \"err\" : \"\";\n el.style.display = \"block\";\n if (toastTimer) clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { el.style.display = \"none\"; }, isErr ? 6000 : 3000);\n}\n\n// Two-click confirm (ops-panel's pattern): first click arms the button\n// (\"sure?\"), a second click within 4s fires the callback; anything else\n// disarms it. Shared by every destructive action in this panel (delete\n// wallet, delete preset, disable remote, revoke pairing).\nvar armed = {};\nfunction confirmClick(key, btn, fn) {\n if (armed[key]) {\n clearTimeout(armed[key].t);\n delete armed[key];\n fn();\n return;\n }\n var prev = btn.textContent;\n btn.classList.add(\"confirm\");\n btn.textContent = \"sure?\";\n armed[key] = { t: setTimeout(function () {\n delete armed[key];\n btn.classList.remove(\"confirm\");\n btn.textContent = prev;\n }, 4000) };\n}\n\nfunction copyToClipboard(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { toast(\"Copied.\", false); }, function () { toast(\"Copy failed.\", true); });\n return;\n }\n try {\n var ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.opacity = \"0\";\n document.body.appendChild(ta);\n ta.focus();\n ta.select();\n document.execCommand(\"copy\");\n document.body.removeChild(ta);\n toast(\"Copied.\", false);\n } catch (e) {\n toast(\"Copy failed.\", true);\n }\n}\nfunction wireCopyButtons(container, valuesByKey) {\n var btns = container.querySelectorAll(\"[data-copy]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var key = btn.getAttribute(\"data-copy\");\n var value = valuesByKey[key];\n if (value) copyToClipboard(value);\n });\n })(btns[i]);\n }\n}\n\n// ============================================================\n// generic config_get / config_set field editor — shared by Defaults,\n// per-harness adapter defaults, and titler.model (Models section).\n// ============================================================\n\n// Last revision seen from ANY config_get/config_set response, reused on\n// every write (the spec: \"always pass the last revision\"). A daemon that\n// predates config_get/config_set never sets this; config_set is simply\n// called without a revision then (server treats that as unconditional).\nvar configRevision = null;\n\n// path -> true for every currently-known pending-restart key, merged from\n// every config_get response seen anywhere in the app. Drives the persistent\n// top banner — never restarts anything itself, only tells the user to.\nvar pendingRestartMap = {};\n\nfunction notePendingRestart(rows) {\n var changed = false;\n for (var i = 0; i < (rows || []).length; i++) {\n var r = rows[i];\n if (r.pendingRestart) {\n if (!pendingRestartMap[r.path]) changed = true;\n pendingRestartMap[r.path] = true;\n } else if (pendingRestartMap[r.path]) {\n delete pendingRestartMap[r.path];\n changed = true;\n }\n }\n if (changed) renderPendingBanner();\n}\n\nfunction renderPendingBanner() {\n var el = document.getElementById(\"pending-banner\");\n if (!el) return;\n var keys = [];\n for (var k in pendingRestartMap) if (Object.prototype.hasOwnProperty.call(pendingRestartMap, k)) keys.push(k);\n if (keys.length === 0) {\n el.style.display = \"none\";\n el.textContent = \"\";\n return;\n }\n keys.sort();\n el.style.display = \"block\";\n el.textContent = \"Restart the daemon yourself to apply: \" + keys.join(\", \") + \". This app never restarts it for you.\";\n}\n\n// Type inference for a config_get row: config-tools.ts's describeKey\n// spreads deriveValueType(entry.schema) onto every row, so row.valueType\n// (\"boolean\" | \"number\" | \"integer\" | \"string\" | \"enum\" | \"string[]\" |\n// \"object\" | \"unknown\") plus row.enum / row.min / row.max are the registry's\n// OWN answer and win outright. The JS-type inference below is only a\n// fallback for a daemon old enough to predate that field entirely (absent,\n// not merely \"unknown\" — \"unknown\" is itself a real, present answer for a\n// schema deriveValueType can't type, e.g. a mixed union, and must NOT be\n// re-guessed from the current value).\nfunction inferValueType(row) {\n if (row.valueType) return row.valueType;\n var v = row.effective !== undefined ? row.effective : row.value;\n if (v === undefined) v = row.default;\n if (typeof v === \"boolean\") return \"boolean\";\n if (typeof v === \"number\") return \"number\";\n if (typeof v === \"string\") return \"string\";\n if (Array.isArray(v)) {\n var allStr = true;\n for (var i = 0; i < v.length; i++) if (typeof v[i] !== \"string\") allStr = false;\n return allStr ? \"string[]\" : \"object\";\n }\n if (v && typeof v === \"object\") return \"object\";\n return \"unknown\";\n}\n\n// defaults.spawn.browser is a union of the literals \"headless\" and false\n// (config-schema.ts) - deriveValueType's enum-from-union heuristic requires\n// EVERY literal to be a string, and false isn't one, so this one specific\n// key comes back \"unknown\" rather than \"enum\" (verified against\n// packages/runtime/src/config-tools.ts's deriveValueType). A checkbox whose\n// two states are not plain true/false is still the right control for it, so\n// it keeps its own tiny special case rather than a general boolean-like-enum\n// mechanism nothing else needs.\nvar BOOL_LIKE_FIELDS = {\n \"defaults.spawn.browser\": { on: \"headless\", off: false, onLabel: \"headless\" },\n};\n\nfunction pathId(path) { return \"cfg-\" + String(path).replace(/[^a-zA-Z0-9]/g, \"-\"); }\n\nfunction writableReasonText(row) {\n if (row.secret) return \"secret, set via wallets or the CLI\";\n return \"read-only, edit the config file by hand\";\n}\n\nfunction configDisplayValue(row) {\n if (row.secret) {\n if (row.secret.set === false) return \"not set\";\n if (typeof row.secret.set === \"boolean\") {\n var bits = [];\n if (row.secret.fingerprint) bits.push(row.secret.fingerprint);\n if (row.secret.last4) bits.push(row.secret.last4);\n return bits.length ? bits.join(\" / \") : \"set\";\n }\n var parts = [];\n for (var k in row.secret) {\n if (Object.prototype.hasOwnProperty.call(row.secret, k)) {\n parts.push(k + \": \" + (row.secret[k] && row.secret[k].set ? \"set\" : \"not set\"));\n }\n }\n return parts.length ? parts.join(\", \") : \"no keys\";\n }\n return row.effective !== undefined ? row.effective : row.value;\n}\n\nfunction numberBoundAttrs(row) {\n var attrs = \"\";\n if (typeof row.min === \"number\") attrs += ' min=\"' + row.min + '\"';\n if (typeof row.max === \"number\") attrs += ' max=\"' + row.max + '\"';\n return attrs;\n}\n\nfunction configControlHtml(row) {\n var vt = inferValueType(row);\n var val = row.effective !== undefined ? row.effective : (row.value !== undefined ? row.value : row.default);\n var boolLike = BOOL_LIKE_FIELDS[row.path];\n var disabledAttr = row.writable ? \"\" : \" disabled\";\n var titleAttr = row.writable ? \"\" : ' title=\"' + escHtml(writableReasonText(row)) + '\"';\n if (boolLike) {\n var checked = val === boolLike.on;\n return '<label class=\"inline\"><input type=\"checkbox\" data-cfg-bool-like=\"' + escHtml(row.path) + '\"' + (checked ? \" checked\" : \"\") + disabledAttr + titleAttr + '> ' + escHtml(boolLike.onLabel || \"on\") + '</label>';\n }\n if (vt === \"boolean\") {\n return '<input type=\"checkbox\" data-cfg-auto=\"' + escHtml(row.path) + '\"' + (val ? \" checked\" : \"\") + disabledAttr + titleAttr + '>';\n }\n if (vt === \"enum\") {\n var enumOpts = row.enum || [];\n var out = '<select data-cfg-auto=\"' + escHtml(row.path) + '\"' + disabledAttr + titleAttr + '>';\n for (var i = 0; i < enumOpts.length; i++) {\n out += '<option value=\"' + escHtml(enumOpts[i]) + '\"' + (enumOpts[i] === val ? \" selected\" : \"\") + '>' + escHtml(enumOpts[i]) + '</option>';\n }\n return out + '</select>';\n }\n if (vt === \"number\" || vt === \"integer\") {\n return '<input type=\"number\" data-cfg-auto=\"' + escHtml(row.path) + '\" value=\"' + escHtml(val == null ? \"\" : String(val)) + '\"'\n + (vt === \"integer\" ? ' step=\"1\"' : \"\") + numberBoundAttrs(row) + disabledAttr + titleAttr + '>';\n }\n if (vt === \"string[]\") {\n return '<input type=\"text\" data-cfg-input=\"' + escHtml(row.path) + '\" value=\"' + escHtml((val || []).join(\", \")) + '\" placeholder=\"comma, separated, list\"' + disabledAttr + titleAttr + '>'\n + ' <button type=\"button\" class=\"btn-sm\" data-cfg-save=\"' + escHtml(row.path) + '\"' + disabledAttr + '>save</button>';\n }\n if (vt === \"object\" || vt === \"unknown\") {\n // \"unknown\" is a real, present answer (a schema deriveValueType can't\n // type, e.g. a mixed boolean/object union) — a raw JSON editor is the\n // one control that can represent any of those shapes without guessing.\n return '<div><textarea data-cfg-input=\"' + escHtml(row.path) + '\" rows=\"4\" style=\"width:100%;font-family:Menlo,Monaco,monospace;font-size:11px\"' + disabledAttr + titleAttr + '>' + escHtml(JSON.stringify(val === undefined ? null : val, null, 2)) + '</textarea>'\n + ' <button type=\"button\" class=\"btn-sm\" data-cfg-save=\"' + escHtml(row.path) + '\"' + disabledAttr + '>save</button></div>';\n }\n return '<input type=\"text\" data-cfg-auto=\"' + escHtml(row.path) + '\" value=\"' + escHtml(val == null ? \"\" : String(val)) + '\"' + disabledAttr + titleAttr + '>';\n}\n\n// One dl \"row\" for a config key. Wrapped in a display:contents div so the\n// dt/dd pairs stay direct grid children (the 2-col dl grid) while still\n// being addressable as one group for wiring/lookup. A writable:false row\n// still renders a real control (checkbox/select/input), just disabled,\n// with the reason in its title, never a second, ad hoc read-only widget.\nfunction configFieldHtml(row) {\n var label = row.label || humanize(String(row.path).split(\".\").slice(-1)[0]);\n var badge = '<span class=\"tag\">' + escHtml(row.apply) + '</span>';\n var extra = \"\";\n if (row.source === \"env\" && row.envOverride) extra += ' <span class=\"tag bad\">env ' + escHtml(row.envOverride) + '</span>';\n if (row.pendingRestart) extra += ' <span class=\"tag warn-tag\">restart pending</span>';\n\n var dd = \"\";\n if (row.secret) {\n dd += escHtml(String(configDisplayValue(row)));\n } else {\n dd += configControlHtml(row);\n }\n dd += \" \" + badge + extra;\n if (row.writable && !row.secret && row.value !== undefined) {\n dd += ' <button type=\"button\" class=\"btn-sm\" data-cfg-reset=\"' + escHtml(row.path) + '\" title=\"Reset to default\">reset</button>';\n }\n\n var html = '<div class=\"cfg-row\" data-cfg-path=\"' + escHtml(row.path) + '\">'\n + '<dt title=\"' + escHtml(row.path) + '\">' + escHtml(label) + '</dt><dd>' + dd + '</dd>';\n if (row.help) html += '<dt></dt><dd class=\"cfg-help muted\">' + escHtml(row.help) + '</dd>';\n html += '<dt></dt><dd class=\"cfg-err\" data-cfg-err=\"' + escHtml(row.path) + '\" style=\"display:none\"></dd>';\n html += '</div>';\n return html;\n}\n\nfunction buildConfigCardHtml(cardId, title, rows) {\n var body = \"\";\n for (var i = 0; i < rows.length; i++) body += configFieldHtml(rows[i]);\n return '<div class=\"card\" data-card-id=\"' + escHtml(cardId) + '\"><div class=\"body\"><dl>' + body + '</dl></div></div>';\n}\n\nfunction extractConfigSetErrorCode(message) {\n var s = String(message || \"\");\n var start = s.indexOf(\"[\");\n var end = start === -1 ? -1 : s.indexOf(\"]\", start);\n if (start === -1 || end === -1) return \"\";\n return s.slice(start + 1, end);\n}\n\nfunction describeConfigSetError(message) {\n var code = extractConfigSetErrorCode(message);\n var HUMAN = {\n invalid_input: \"That request was not valid.\",\n unknown_key: \"This daemon does not know that setting.\",\n not_writable: \"This setting cannot be changed here.\",\n invalid_value: \"That value is not valid for this setting.\",\n invalid_config: \"That change would leave the config file invalid.\",\n stale_revision: \"This section changed elsewhere. Reloading.\",\n };\n return HUMAN[code] || message;\n}\n\n// Attach listeners for every config control inside \"container\". rowsByPath\n// supplies the row metadata a save/reset needs (valueType for parsing).\n// onSaved(path) fires after a successful write (path just written);\n// onStale() fires on a stale_revision rejection (reload the section).\nfunction wireConfigRows(container, rowsByPath, onSaved, onStale) {\n function submit(path, value, isUnset) {\n var args = isUnset ? { key: path, unset: true, revision: configRevision } : { key: path, value: value, revision: configRevision };\n return loadTool(\"config_set\", args).then(function (r) {\n if (!r.ok) {\n var stale = extractConfigSetErrorCode(r.message) === \"stale_revision\";\n toast(describeConfigSetError(r.message), true);\n if (stale && onStale) onStale();\n return;\n }\n var data = r.data;\n if (data && data.revision) configRevision = data.revision;\n var note = \"Saved.\";\n if (data && data.applied === \"restart-required\") note = \"Saved. Restart required to apply.\";\n if (data && data.shadowedByEnv) note += \" Overridden by \" + data.shadowedByEnv + \".\";\n toast(note, false);\n if (onSaved) onSaved(path);\n });\n }\n\n var autos = container.querySelectorAll(\"[data-cfg-auto]\");\n for (var i = 0; i < autos.length; i++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var path = el.getAttribute(\"data-cfg-auto\");\n var value;\n if (el.type === \"checkbox\") value = el.checked;\n else if (el.type === \"number\") value = el.value === \"\" ? null : Number(el.value);\n else value = el.value;\n submit(path, value, false);\n });\n })(autos[i]);\n }\n\n var boolLikes = container.querySelectorAll(\"[data-cfg-bool-like]\");\n for (var b = 0; b < boolLikes.length; b++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var path = el.getAttribute(\"data-cfg-bool-like\");\n var spec = BOOL_LIKE_FIELDS[path];\n submit(path, el.checked ? spec.on : spec.off, false);\n });\n })(boolLikes[b]);\n }\n\n var saves = container.querySelectorAll(\"[data-cfg-save]\");\n for (var s = 0; s < saves.length; s++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var path = btn.getAttribute(\"data-cfg-save\");\n var row = rowsByPath[path];\n var input = container.querySelector('[data-cfg-input=\"' + cssEscape(path) + '\"]');\n var errEl = container.querySelector('[data-cfg-err=\"' + cssEscape(path) + '\"]');\n if (errEl) { errEl.style.display = \"none\"; errEl.textContent = \"\"; }\n var vt = row ? inferValueType(row) : \"string\";\n var value;\n if (vt === \"string[]\") {\n value = input.value.split(\",\").map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });\n } else if (vt === \"object\" || vt === \"unknown\") {\n try {\n value = JSON.parse(input.value);\n } catch (e) {\n if (errEl) { errEl.style.display = \"block\"; errEl.textContent = \"Invalid JSON: \" + e.message; }\n return;\n }\n } else {\n value = input.value;\n }\n submit(path, value, false);\n });\n })(saves[s]);\n }\n\n var resets = container.querySelectorAll(\"[data-cfg-reset]\");\n for (var r = 0; r < resets.length; r++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n submit(btn.getAttribute(\"data-cfg-reset\"), undefined, true);\n });\n })(resets[r]);\n }\n}\n\n// ============================================================\n// nav + deep-link routing\n// ============================================================\n\nvar current = { section: SECTIONS[0].id, id: undefined, sub: undefined };\nvar loaded = {}; // section -> true once its data has been fetched at least once\n\nfunction renderNav() {\n var el = document.getElementById(\"nav\");\n var html = \"\";\n for (var i = 0; i < SECTIONS.length; i++) {\n html += '<button data-section=\"' + SECTIONS[i].id + '\" class=\"' + (SECTIONS[i].id === current.section ? \"active\" : \"\") + '\">' + escHtml(SECTIONS[i].label) + '</button>';\n }\n el.innerHTML = html;\n var btns = el.querySelectorAll(\"button[data-section]\");\n for (var k = 0; k < btns.length; k++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n location.hash = buildConfigFragment(btn.getAttribute(\"data-section\"));\n });\n })(btns[k]);\n }\n}\n\nfunction showSection(sectionId) {\n var els = document.querySelectorAll(\".section\");\n for (var i = 0; i < els.length; i++) {\n els[i].classList.toggle(\"active\", els[i].getAttribute(\"data-section\") === sectionId);\n }\n var btns = document.querySelectorAll(\"#nav button[data-section]\");\n for (var j = 0; j < btns.length; j++) {\n btns[j].classList.toggle(\"active\", btns[j].getAttribute(\"data-section\") === sectionId);\n }\n}\n\n// Scroll to + highlight the card matching 'id' within the active section's\n// container. Unknown id means the \"not found\" banner (never a blank page).\nfunction applyDeepLink(sectionId, id, sub) {\n var notfound = document.getElementById(\"notfound\");\n notfound.style.display = \"none\";\n var prevHl = document.querySelectorAll(\".card.hl, tr.hl, th.hl\");\n for (var p = 0; p < prevHl.length; p++) prevHl[p].classList.remove(\"hl\");\n if (id === undefined) return;\n var container = document.getElementById(\"sec-\" + sectionId);\n if (!container) return;\n // #capabilities/<mcp|skills>/<sub>: 'id' selects the TAB, not a card —\n // switch tab (re-rendering the section) before looking for 'sub's card.\n if (sectionId === \"capabilities\") {\n var capTab = id === \"skills\" ? \"skills\" : \"mcp\";\n if (capabilitiesState.tab !== capTab) {\n capabilitiesState.tab = capTab;\n renderCapabilities();\n container = document.getElementById(\"sec-\" + sectionId);\n }\n if (sub === undefined) return;\n var capTarget = container.querySelector('[data-card-id=\"' + cssEscape(sub) + '\"]');\n if (!capTarget) {\n notfound.textContent = \"not found: \" + id + \"/\" + sub;\n notfound.style.display = \"block\";\n return;\n }\n capTarget.classList.add(\"hl\");\n scrollIntoViewSafe(capTarget);\n return;\n }\n var targetId = sectionId === \"remote\" && id === \"pairing\" ? \"pairing\" : id;\n var target = container.querySelector('[data-card-id=\"' + cssEscape(targetId) + '\"]');\n if (!target) {\n notfound.textContent = \"not found: \" + id + (sub ? \"/\" + sub : \"\");\n notfound.style.display = \"block\";\n return;\n }\n target.classList.add(\"hl\");\n scrollIntoViewSafe(target);\n if (sub) {\n var row = target.querySelector('[data-row-id=\"' + cssEscape(sub) + '\"]');\n if (row) {\n row.classList.add(\"hl\");\n scrollIntoViewSafe(row);\n }\n }\n}\n\n// Not every embedding host implements scrollIntoView on a sandboxed/minimal\n// DOM — highlighting must still work even when scrolling can't.\nfunction scrollIntoViewSafe(el) {\n if (el && typeof el.scrollIntoView === \"function\") el.scrollIntoView({ block: \"nearest\" });\n}\n\n// CSS.escape isn't guaranteed in every sandboxed webview — a minimal,\n// dependency-free fallback for the characters our own ids ever contain.\nfunction cssEscape(s) {\n return String(s).replace(/[^a-zA-Z0-9_-]/g, function (c) {\n return \"\\\\\\\\\" + c.charCodeAt(0).toString(16) + \" \";\n });\n}\n\n// Clears every secret this panel ever holds only in memory (a remote-enable\n// reveal, a pairing offer) — called on navigating away from #remote and on\n// host teardown. Never persisted, never in updateModelContext.\nfunction clearRemoteSecrets() {\n remoteReveal = null;\n pairReveal = null;\n clearPairCountdown();\n // The #sec-remote DOM may still show a rendered reveal box from before\n // this call — clearing the variable alone does not touch it. Force the\n // next visit to re-fetch and re-render from scratch (remoteReveal is\n // null by then, so no reveal box comes back) instead of leaving the\n // stale secret sitting in a hidden .section until some other event\n // happens to re-render it.\n loaded.remote = false;\n}\n\nfunction route(pushBack) {\n var parsed = parseConfigFragment(location.hash);\n if (current.section === \"remote\" && parsed.section !== \"remote\") {\n clearRemoteSecrets();\n }\n current = parsed;\n showSection(parsed.section);\n renderNav();\n ensureLoaded(parsed.section).then(function () {\n applyDeepLink(parsed.section, parsed.id, parsed.sub);\n });\n if (pushBack) location.hash = buildConfigFragment(parsed.section, parsed.id, parsed.sub);\n}\n\nwindow.addEventListener(\"hashchange\", function () { route(false); });\n\n// ── MCP-hosted deep links ──\n// A browser tab navigates via location.hash directly; an MCP host (Claude\n// Desktop, Codex, VS Code) has no URL bar for this panel, so it opens\n// app_ui_config with a view argument instead (plan section 3.4). The bridge\n// (app-ui-apps.ts) surfaces that as tool input; this turns it into the same\n// #<section>/<id>/<sub> fragment a browser deep link would use.\n//\n// location.hash still wins on the very first route — an actual browser deep\n// link must not be overridden by a stale/default tool call. Every\n// subsequent tool-input notification (a fresh app_ui_config invocation while\n// the panel is already open) is a live \"go to this view\" command and always\n// re-routes.\nvar toolInputCount = 0;\nvar hashPresentAtLoad = !!(location.hash && location.hash !== \"#\");\n\nfunction routeToView(view) {\n if (typeof view !== \"string\" || !view) return;\n var parsed = parseConfigFragment(view);\n var frag = buildConfigFragment(parsed.section, parsed.id, parsed.sub);\n if (location.hash === frag) { route(false); } else { location.hash = frag; }\n}\n\nfunction handleToolInput(input) {\n toolInputCount += 1;\n if (!shouldApplyIncomingView(toolInputCount, hashPresentAtLoad)) return;\n routeToView(input && input.view);\n}\n\n// ============================================================\n// section data + render dispatch\n// ============================================================\n\nvar SECTION_LOADERS = {\n wallets: loadWallets,\n harnesses: loadHarnesses,\n models: loadModels,\n capabilities: loadCapabilities,\n defaults: loadDefaults,\n remote: loadRemote,\n advanced: loadAdvanced,\n};\n\nfunction ensureLoaded(sectionId) {\n if (loaded[sectionId]) return Promise.resolve();\n var fn = SECTION_LOADERS[sectionId];\n if (!fn) return Promise.resolve();\n return fn().then(function () { loaded[sectionId] = true; });\n}\n\nfunction reloadCurrent() {\n loaded[current.section] = false;\n return ensureLoaded(current.section).then(function () {\n applyDeepLink(current.section, current.id, current.sub);\n });\n}\n\n// ============================================================\n// Wallets\n// ============================================================\n\nvar walletsState = {\n window: \"7d\",\n profilesResult: null,\n rollupResult: null,\n presetsResult: null,\n discoveredResult: null,\n showAddForm: false,\n showDiscovered: false,\n expanded: {}, // profileId -> { edit: bool, curate: bool }\n providerModelsByEndpoint: {}, // endpoint -> catalog_provider_models rows\n};\n\nfunction loadWallets() {\n return Promise.all([\n loadTool(\"auth_profile_list\", { full: true }),\n loadTool(\"usage_rollup\", { window: walletsState.window, groupBy: [\"profile\"] }),\n loadTool(\"harness_preset_list\", {}),\n ]).then(function (results) {\n walletsState.profilesResult = results[0];\n walletsState.rollupResult = results[1];\n walletsState.presetsResult = results[2];\n renderWallets();\n });\n}\n\nfunction reloadWalletsWindow(win) {\n walletsState.window = win;\n fetchRollup(false);\n}\n\nfunction fetchRollup(probe) {\n return loadTool(\"usage_rollup\", { window: walletsState.window, groupBy: [\"profile\"], probe: !!probe }).then(function (r) {\n walletsState.rollupResult = r;\n renderWallets();\n });\n}\n\nfunction byProfileMap(rollupResult) {\n var map = {};\n if (rollupResult.ok && rollupResult.data && Array.isArray(rollupResult.data.byProfile)) {\n for (var i = 0; i < rollupResult.data.byProfile.length; i++) {\n map[rollupResult.data.byProfile[i].profileRef] = rollupResult.data.byProfile[i];\n }\n }\n return map;\n}\n\nfunction presetsReferencing(profileId) {\n var out = [];\n var pr = walletsState.presetsResult;\n if (!pr || !pr.ok) return out;\n var presets = (pr.data && pr.data.presets) || [];\n for (var i = 0; i < presets.length; i++) {\n if (presets[i].profileRef === profileId) out.push(presets[i]);\n }\n return out;\n}\n\nfunction getWalletExpanded(id) {\n if (!walletsState.expanded[id]) walletsState.expanded[id] = { edit: false, curate: false, curateMode: undefined };\n return walletsState.expanded[id];\n}\n\nfunction replaceProfileInState(profile) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;\n var list = pr.data.profiles;\n for (var i = 0; i < list.length; i++) {\n if (list[i].id === profile.id) { list[i] = profile; return; }\n }\n list.push(profile);\n}\n\nfunction removeProfileFromState(id) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;\n pr.data.profiles = pr.data.profiles.filter(function (p) { return p.id !== id; });\n}\n\nfunction renderWallets() {\n var el = document.getElementById(\"sec-wallets\");\n var rr = walletsState.rollupResult;\n var html = '<div class=\"sect-h\">Spend<span class=\"pillbar\" id=\"wallet-window\"></span>'\n + '<button type=\"button\" class=\"btn-sm\" id=\"spend-refresh-live\">refresh live</button></div>'\n + '<div class=\"muted-note\" style=\"padding:0 0 4px\">Refreshing live consumes a small sliver of the rate budget for that wallet.</div>';\n\n if (rr && rr.ok) {\n var t = rr.data.total || {};\n html += '<div class=\"card\"><div class=\"body\"><dl>'\n + row(\"Window\", escHtml(rr.data.window))\n + (typeof t.spentUsd === \"number\" ? row(\"Spend\", formatCost(t.spentUsd)) : \"\")\n + (typeof t.tokensIn === \"number\" || typeof t.tokensOut === \"number\"\n ? row(\"Tokens\", (typeof t.tokensIn === \"number\" ? formatTokens(t.tokensIn) + \" in\" : \"\") + (typeof t.tokensIn === \"number\" && typeof t.tokensOut === \"number\" ? \" · \" : \"\") + (typeof t.tokensOut === \"number\" ? formatTokens(t.tokensOut) + \" out\" : \"\"))\n : \"\")\n + (typeof t.unpricedTokens === \"number\" && t.unpricedTokens > 0 ? row(\"Unpriced tokens\", formatTokens(t.unpricedTokens)) : \"\")\n + '</dl></div></div>';\n } else if (rr) {\n html += '<div class=\"muted-note\">usage_rollup: ' + escHtml(rr.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Wallets'\n + '<button type=\"button\" class=\"btn-sm\" id=\"wallet-add-toggle\">' + (walletsState.showAddForm ? \"close\" : \"+ add wallet\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm\" id=\"wallet-scan-toggle\">' + (walletsState.showDiscovered ? \"close\" : \"find credentials\") + '</button>'\n + '</div>';\n\n if (walletsState.showAddForm) html += addWalletFormHtml();\n if (walletsState.showDiscovered) html += discoveredCredentialsHtml();\n\n html += '<div class=\"grid\" id=\"wallet-cards\"></div>';\n\n el.innerHTML = html;\n\n var pillEl = document.getElementById(\"wallet-window\");\n var windows = [\"5h\", \"7d\", \"30d\"];\n var pillHtml = \"\";\n for (var w = 0; w < windows.length; w++) {\n pillHtml += '<button data-win=\"' + windows[w] + '\" class=\"' + (windows[w] === walletsState.window ? \"active\" : \"\") + '\">' + windows[w] + '</button>';\n }\n pillEl.innerHTML = pillHtml;\n var pillBtns = pillEl.querySelectorAll(\"button\");\n for (var pb = 0; pb < pillBtns.length; pb++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () { reloadWalletsWindow(btn.getAttribute(\"data-win\")); });\n })(pillBtns[pb]);\n }\n document.getElementById(\"spend-refresh-live\").addEventListener(\"click\", function () { fetchRollup(true); });\n\n document.getElementById(\"wallet-add-toggle\").addEventListener(\"click\", function () {\n walletsState.showAddForm = !walletsState.showAddForm;\n renderWallets();\n });\n document.getElementById(\"wallet-scan-toggle\").addEventListener(\"click\", function () {\n walletsState.showDiscovered = !walletsState.showDiscovered;\n if (walletsState.showDiscovered && !walletsState.discoveredResult) {\n loadTool(\"auth_discover_credentials\", {}).then(function (r) {\n walletsState.discoveredResult = r;\n renderWallets();\n });\n return;\n }\n renderWallets();\n });\n\n wireAddWalletForm(el);\n wireDiscoveredCredentials(el);\n renderWalletCards();\n}\n\nfunction addWalletFormHtml() {\n return '<div class=\"form-panel\" id=\"wallet-add-form\">'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Id</label><input type=\"text\" id=\"wf-id\" placeholder=\"my-anthropic-key\"></div>'\n + '<div class=\"field\"><label>Endpoint</label><input type=\"text\" id=\"wf-endpoint\" list=\"wf-endpoint-list\" placeholder=\"anthropic\"><datalist id=\"wf-endpoint-list\">'\n + '<option value=\"anthropic\"><option value=\"openrouter\"><option value=\"moonshot\"><option value=\"openai\"></datalist></div>'\n + '<div class=\"field\"><label>Method</label><select id=\"wf-method\"><option value=\"api-key\">API key</option><option value=\"oauth-bearer\">Subscription</option></select></div>'\n + '<div class=\"field\"><label>Label (optional)</label><input type=\"text\" id=\"wf-label\"></div>'\n + '</div>'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Credential</label><input type=\"password\" id=\"wf-credential\" placeholder=\"paste secret\"></div>'\n + '<div class=\"field\" id=\"wf-source-field\" style=\"display:none\"><label>Self-refreshing source (alternative)</label><input type=\"text\" id=\"wf-source\" placeholder=\"claude-code-oauth\"></div>'\n + '<button type=\"button\" class=\"btn-sm primary\" id=\"wf-submit\">Create wallet</button>'\n + '</div>'\n + '<div class=\"cfg-err\" id=\"wf-err\" style=\"display:none\"></div>'\n + '</div>';\n}\n\nfunction wireAddWalletForm(container) {\n var methodEl = container.querySelector(\"#wf-method\");\n var sourceField = container.querySelector(\"#wf-source-field\");\n if (!methodEl) return;\n methodEl.addEventListener(\"change\", function () {\n sourceField.style.display = methodEl.value === \"oauth-bearer\" ? \"\" : \"none\";\n });\n container.querySelector(\"#wf-submit\").addEventListener(\"click\", function () {\n var errEl = container.querySelector(\"#wf-err\");\n errEl.style.display = \"none\";\n var id = container.querySelector(\"#wf-id\").value.trim();\n var endpoint = container.querySelector(\"#wf-endpoint\").value.trim();\n var method = methodEl.value;\n var label = container.querySelector(\"#wf-label\").value.trim();\n var credentialInput = container.querySelector(\"#wf-credential\");\n var sourceInput = container.querySelector(\"#wf-source\");\n var credential = credentialInput.value;\n var source = method === \"oauth-bearer\" ? sourceInput.value.trim() : \"\";\n // Clear the secret from the DOM and this local scope's reference to it\n // immediately, before the async call even starts.\n credentialInput.value = \"\";\n if (!id || !endpoint) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Id and endpoint are required.\";\n credential = \"\";\n return;\n }\n if (!credential && !source) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Give either a credential or a self-refreshing source.\";\n credential = \"\";\n return;\n }\n var args = { id: id, endpoint: endpoint, method: method };\n if (label) args.label = label;\n if (credential) args.credential = credential;\n else if (source) args.source = source;\n credential = \"\";\n loadTool(\"auth_profile_create\", args).then(function (r) {\n if (!r.ok) {\n errEl.style.display = \"block\";\n errEl.textContent = r.message;\n return;\n }\n toast(\"Wallet created.\", false);\n walletsState.showAddForm = false;\n loadWallets();\n });\n });\n}\n\nfunction discoveredCredentialsHtml() {\n var dr = walletsState.discoveredResult;\n var html = '<div class=\"card\"><div class=\"body\">';\n if (!dr) {\n html += '<div class=\"muted-note\">scanning…</div>';\n } else if (!dr.ok) {\n html += '<div class=\"muted-note\">auth_discover_credentials: ' + escHtml(dr.message) + '</div>';\n } else {\n var creds = (dr.data && dr.data.credentials) || [];\n if (creds.length === 0) {\n html += '<div class=\"empty\">No importable credentials found on this host.</div>';\n } else {\n html += '<table><thead><tr><th>Endpoint</th><th>Method</th><th>Origin</th><th>Hint</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < creds.length; i++) {\n var c = creds[i];\n var defaultId = c.origin + \"-\" + c.endpoint;\n html += '<tr><td>' + escHtml(c.endpoint) + '</td><td>' + escHtml(BILLING_LABELS[c.method] || c.method) + '</td><td>' + escHtml(c.origin) + '</td><td>' + escHtml(c.hint) + '</td>'\n + '<td><input type=\"text\" class=\"dc-id\" data-dc-idx=\"' + i + '\" value=\"' + escHtml(defaultId) + '\" style=\"width:120px\">'\n + ' <button type=\"button\" class=\"btn-sm\" data-dc-import=\"' + i + '\">import</button></td></tr>';\n }\n html += '</tbody></table>';\n }\n }\n html += '</div></div>';\n return html;\n}\n\nfunction wireDiscoveredCredentials(container) {\n var btns = container.querySelectorAll(\"[data-dc-import]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var idx = Number(btn.getAttribute(\"data-dc-import\"));\n var dr = walletsState.discoveredResult;\n var c = dr && dr.ok && dr.data.credentials[idx];\n if (!c) return;\n var idInput = container.querySelector('.dc-id[data-dc-idx=\"' + idx + '\"]');\n var id = (idInput && idInput.value.trim()) || (c.origin + \"-\" + c.endpoint);\n loadTool(\"auth_profile_import\", { origin: c.origin, endpoint: c.endpoint, id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Imported.\", false);\n loadWallets();\n });\n });\n })(btns[i]);\n }\n}\n\nfunction renderWalletCards() {\n var cardsEl = document.getElementById(\"wallet-cards\");\n if (!cardsEl) return;\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok) {\n cardsEl.innerHTML = '<div class=\"empty\">' + (pr ? escHtml(pr.message) : \"loading…\") + '</div>';\n return;\n }\n var profiles = (pr.data && pr.data.profiles) || [];\n if (profiles.length === 0) {\n cardsEl.innerHTML = '<div class=\"empty\">No auth profiles configured on this host.</div>';\n return;\n }\n var rr = walletsState.rollupResult;\n var spend = rr && rr.ok ? byProfileMap(rr) : {};\n var out = \"\";\n for (var i = 0; i < profiles.length; i++) {\n out += walletCardHtml(profiles[i], spend[profiles[i].id]);\n }\n cardsEl.innerHTML = out;\n wireWalletCards(cardsEl);\n}\n\nfunction walletCardHtml(p, spend) {\n var title = p.label || p.id;\n var exp = getWalletExpanded(p.id);\n var out = cardOpen(p.id, title, escHtml(p.endpoint));\n out += row(\"Method\", escHtml(BILLING_LABELS[p.method] || humanize(p.method)));\n var keyLine = p.keyStatus === \"stored\"\n ? [p.fingerprint, p.last4].filter(Boolean).join(\" · \") || \"stored\"\n : p.keyStatus === \"self-refreshing\" ? \"self-refreshing\" : \"unavailable\";\n out += row(\"Key\", escHtml(keyLine));\n if (p.origin) out += row(\"Imported from\", escHtml(p.origin));\n out += '<dt>Enabled</dt><dd><label class=\"inline\"><input type=\"checkbox\" data-wallet-enabled=\"' + escHtml(p.id) + '\"' + (p.disabled ? \"\" : \" checked\") + '></label></dd>';\n var curated = !p.models || p.models.mode === \"all\" ? \"All eligible\" : (p.models.ids || []).length + \" curated\";\n out += row(\"Models\", escHtml(curated));\n if (spend) {\n if (typeof spend.spentUsd === \"number\") out += row(\"Spend (window)\", formatCost(spend.spentUsd));\n if (p.costBudget && typeof p.costBudget.maxCostUsd === \"number\") {\n var pct = p.costBudget.maxCostUsd > 0 ? ((spend.spentUsd || 0) / p.costBudget.maxCostUsd) * 100 : 0;\n out += row(\"Budget\", formatCost(p.costBudget.maxCostUsd) + \" / \" + escHtml(p.costBudget.window));\n out += bar(pct);\n }\n if (spend.remaining) {\n out += row(\"Remaining quota\", String(spend.remaining.remaining), \"resets \" + formatDate(spend.remaining.resetsAt));\n }\n if (spend.credits && typeof spend.credits.balanceUsd === \"number\") {\n out += row(\"Credit balance\", formatCost(spend.credits.balanceUsd), spend.credits.asOf ? \"as of \" + formatDate(spend.credits.asOf) : undefined);\n }\n } else if (p.costBudget && typeof p.costBudget.maxCostUsd === \"number\") {\n out += row(\"Budget\", formatCost(p.costBudget.maxCostUsd) + \" / \" + escHtml(p.costBudget.window));\n }\n out += '<div class=\"wallet-card-actions\">'\n + '<button type=\"button\" class=\"btn-sm\" data-wallet-edit-toggle=\"' + escHtml(p.id) + '\">' + (exp.edit ? \"close\" : \"rename / budget\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm\" data-wallet-curate-toggle=\"' + escHtml(p.id) + '\">' + (exp.curate ? \"close\" : \"manage models\") + '</button>'\n + '<button type=\"button\" class=\"btn-sm danger\" data-wallet-delete=\"' + escHtml(p.id) + '\">delete</button>'\n + '</div>';\n if (exp.edit) out += walletEditPanelHtml(p);\n if (exp.curate) out += walletModelsEditorHtml(p, walletsState.providerModelsByEndpoint[p.endpoint], exp.curateMode);\n out += cardClose();\n return out;\n}\n\nfunction walletEditPanelHtml(p) {\n var hasBudget = !!(p.costBudget && typeof p.costBudget.maxCostUsd === \"number\");\n return '<div class=\"form-panel\" data-wallet-edit-panel=\"' + escHtml(p.id) + '\">'\n + '<div class=\"field\"><label>Label</label><input type=\"text\" class=\"we-label\" value=\"' + escHtml(p.label || \"\") + '\"></div>'\n + '<label class=\"inline\"><input type=\"checkbox\" class=\"we-no-budget\"' + (hasBudget ? \"\" : \" checked\") + '> no spend cap</label>'\n + '<div class=\"form-row we-budget-fields\" style=\"' + (hasBudget ? \"\" : \"display:none\") + '\">'\n + '<div class=\"field\"><label>Max cost (USD)</label><input type=\"number\" class=\"we-max-cost\" value=\"' + escHtml(hasBudget ? String(p.costBudget.maxCostUsd) : \"\") + '\"></div>'\n + '<div class=\"field\"><label>Window</label><input type=\"text\" class=\"we-window\" value=\"' + escHtml(hasBudget ? p.costBudget.window : \"7d\") + '\"></div>'\n + '<div class=\"field\"><label>Scope</label><select class=\"we-scope\"><option value=\"profile\"' + (hasBudget && p.costBudget.scope === \"profile\" ? \" selected\" : \"\") + '>profile</option><option value=\"session\"' + (hasBudget && p.costBudget.scope === \"session\" ? \" selected\" : \"\") + '>session</option></select></div>'\n + '</div>'\n + '<button type=\"button\" class=\"btn-sm primary\" data-wallet-edit-save=\"' + escHtml(p.id) + '\">save</button>'\n + '</div>';\n}\n\n// pendingMode is the panel's OWN pending selection (getWalletExpanded's\n// curateMode), not necessarily what the server has stored yet: switching to\n// \"curate\" must reveal the checklist right away so there is something to\n// check, without writing a destructive \"curate to nothing\" the instant the\n// radio moves. The first checked model is what actually persists the\n// mode=allow switch (see the .mm-id change handler).\nfunction walletModelsEditorHtml(p, catalogRows, pendingMode) {\n var mode = pendingMode || (p.models && p.models.mode) || \"all\";\n var ids = (p.models && p.models.ids) || [];\n var html = '<div class=\"form-panel\" data-wallet-models-panel=\"' + escHtml(p.id) + '\">';\n html += '<label class=\"inline\"><input type=\"radio\" name=\"mm-mode-' + escHtml(p.id) + '\" value=\"all\" class=\"mm-mode\" data-mm-profile=\"' + escHtml(p.id) + '\"' + (mode === \"all\" ? \" checked\" : \"\") + '> service all eligible</label> ';\n html += '<label class=\"inline\"><input type=\"radio\" name=\"mm-mode-' + escHtml(p.id) + '\" value=\"allow\" class=\"mm-mode\" data-mm-profile=\"' + escHtml(p.id) + '\"' + (mode === \"allow\" ? \" checked\" : \"\") + '> curate</label>';\n if (mode === \"allow\") html += ' <button type=\"button\" class=\"btn-sm\" data-mm-refresh=\"' + escHtml(p.id) + '\">refresh models</button>';\n html += '<div class=\"mm-list\" data-mm-list=\"' + escHtml(p.id) + '\" style=\"' + (mode === \"allow\" ? \"\" : \"display:none\") + '\">';\n if (mode === \"allow\") {\n if (!catalogRows) {\n html += '<div class=\"muted-note\">loading models…</div>';\n } else if (catalogRows.length === 0) {\n html += '<div class=\"empty\">No models known for this endpoint.</div>';\n } else {\n for (var i = 0; i < catalogRows.length; i++) {\n var m = catalogRows[i];\n var checked = ids.indexOf(m.id) !== -1;\n html += '<label class=\"inline\" style=\"display:block;margin:2px 0\"><input type=\"checkbox\" class=\"mm-id\" data-mm-profile=\"' + escHtml(p.id) + '\" value=\"' + escHtml(m.id) + '\"' + (checked ? \" checked\" : \"\") + '> ' + escHtml(m.label || m.id) + '</label>';\n }\n }\n }\n html += '</div></div>';\n return html;\n}\n\nfunction wireWalletCards(container) {\n var enabledToggles = container.querySelectorAll(\"[data-wallet-enabled]\");\n for (var i = 0; i < enabledToggles.length; i++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-wallet-enabled\");\n var wantEnabled = el.checked;\n el.disabled = true;\n loadTool(\"auth_profile_set_enabled\", { id: id, enabled: wantEnabled }).then(function (r) {\n el.disabled = false;\n if (!r.ok) { toast(r.message, true); el.checked = !wantEnabled; return; }\n replaceProfileInState(r.data.profile);\n toast(wantEnabled ? \"Wallet enabled.\" : \"Wallet disabled.\", false);\n renderWalletCards();\n });\n });\n })(enabledToggles[i]);\n }\n\n var editToggles = container.querySelectorAll(\"[data-wallet-edit-toggle]\");\n for (var e = 0; e < editToggles.length; e++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-edit-toggle\");\n var exp = getWalletExpanded(id);\n exp.edit = !exp.edit;\n renderWalletCards();\n });\n })(editToggles[e]);\n }\n\n var editSaves = container.querySelectorAll(\"[data-wallet-edit-save]\");\n for (var s = 0; s < editSaves.length; s++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-edit-save\");\n var panel = container.querySelector('[data-wallet-edit-panel=\"' + cssEscape(id) + '\"]');\n var label = panel.querySelector(\".we-label\").value.trim();\n var noBudget = panel.querySelector(\".we-no-budget\").checked;\n var patch = { id: id, label: label === \"\" ? null : label };\n if (noBudget) {\n patch.costBudget = null;\n } else {\n patch.costBudget = {\n maxCostUsd: Number(panel.querySelector(\".we-max-cost\").value) || 0,\n window: panel.querySelector(\".we-window\").value.trim() || \"7d\",\n scope: panel.querySelector(\".we-scope\").value,\n };\n }\n loadTool(\"auth_profile_update\", patch).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n toast(\"Wallet updated.\", false);\n getWalletExpanded(id).edit = false;\n renderWalletCards();\n });\n });\n })(editSaves[s]);\n }\n\n var noBudgetToggles = container.querySelectorAll(\".we-no-budget\");\n for (var nb = 0; nb < noBudgetToggles.length; nb++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var fields = el.parentElement.querySelector(\".we-budget-fields\");\n if (fields) fields.style.display = el.checked ? \"none\" : \"\";\n });\n })(noBudgetToggles[nb]);\n }\n\n var curateToggles = container.querySelectorAll(\"[data-wallet-curate-toggle]\");\n for (var c = 0; c < curateToggles.length; c++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-wallet-curate-toggle\");\n var exp = getWalletExpanded(id);\n exp.curate = !exp.curate;\n if (exp.curate) {\n var profile = findProfile(id);\n if (profile && !walletsState.providerModelsByEndpoint[profile.endpoint]) {\n loadTool(\"catalog_provider_models\", { endpoint: profile.endpoint, limit: 200 }).then(function (r) {\n walletsState.providerModelsByEndpoint[profile.endpoint] = r.ok ? (r.data.models || []) : [];\n renderWalletCards();\n });\n return;\n }\n }\n renderWalletCards();\n });\n })(curateToggles[c]);\n }\n\n var mmModes = container.querySelectorAll(\".mm-mode\");\n for (var mm = 0; mm < mmModes.length; mm++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-mm-profile\");\n if (el.value === \"all\") {\n getWalletExpanded(id).curateMode = undefined;\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"all\" }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n renderWalletCards();\n });\n } else {\n // Reveal the checklist locally (a pending selection, independent\n // of what the server has stored); nothing is written until a\n // model is actually checked, so switching this radio alone can\n // never silently curate the wallet down to nothing.\n getWalletExpanded(id).curateMode = \"allow\";\n renderWalletCards();\n }\n });\n })(mmModes[mm]);\n }\n\n var mmIds = container.querySelectorAll(\".mm-id\");\n for (var mi = 0; mi < mmIds.length; mi++) {\n (function (el) {\n el.addEventListener(\"change\", function () {\n var id = el.getAttribute(\"data-mm-profile\");\n var list = container.querySelector('[data-mm-list=\"' + cssEscape(id) + '\"]');\n var checked = list.querySelectorAll(\".mm-id:checked\");\n var ids = [];\n for (var i2 = 0; i2 < checked.length; i2++) ids.push(checked[i2].value);\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"allow\", ids: ids }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n });\n });\n })(mmIds[mi]);\n }\n\n var mmRefresh = container.querySelectorAll(\"[data-mm-refresh]\");\n for (var mr = 0; mr < mmRefresh.length; mr++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-mm-refresh\");\n loadTool(\"auth_profile_refresh_models\", { id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n replaceProfileInState(r.data.profile);\n var added = (r.data.added || []).length;\n var removed = (r.data.removed || []).length;\n toast(\"Refreshed models (+\" + added + \" / -\" + removed + \").\", false);\n renderWalletCards();\n });\n });\n })(mmRefresh[mr]);\n }\n\n var deleteBtns = container.querySelectorAll(\"[data-wallet-delete]\");\n for (var d = 0; d < deleteBtns.length; d++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-wallet-delete\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"wallet-delete-\" + id, btn, function () {\n var blockedBy = presetsReferencing(id);\n if (blockedBy.length > 0) {\n var names = blockedBy.map(function (p) { return p.name || p.id; }).join(\", \");\n toast(\"Cannot delete: still used by harness preset(s) \" + names + \".\", true);\n return;\n }\n loadTool(\"auth_profile_delete\", { id: id }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n removeProfileFromState(id);\n toast(\"Wallet deleted.\", false);\n renderWalletCards();\n });\n });\n });\n })(deleteBtns[d]);\n }\n}\n\nfunction findProfile(id) {\n var pr = walletsState.profilesResult;\n if (!pr || !pr.ok) return null;\n var list = (pr.data && pr.data.profiles) || [];\n for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];\n return null;\n}\n\n// ============================================================\n// Harnesses\n// ============================================================\n\nvar harnessesState = {\n adaptersResult: null,\n capsResult: null,\n presetsResult: null,\n rolesResult: null,\n profilesResult: null,\n expandedDefaults: {}, // slug -> bool\n defaultsCache: {}, // slug -> row[]\n showPresetForm: false,\n};\n\nfunction loadHarnesses() {\n return Promise.all([\n loadTool(\"adapter_list\", {}),\n loadTool(\"harness_capabilities\", {}),\n loadTool(\"harness_preset_list\", {}),\n loadTool(\"role_list\", {}),\n loadTool(\"auth_profile_list\", {}),\n ]).then(function (results) {\n harnessesState.adaptersResult = results[0];\n harnessesState.capsResult = results[1];\n harnessesState.presetsResult = results[2];\n harnessesState.rolesResult = results[3];\n harnessesState.profilesResult = results[4];\n renderHarnesses();\n });\n}\n\nfunction adapterDefaultsPaths(slug) {\n return [\n \"defaults.adapters.\" + slug + \".skills\",\n \"defaults.adapters.\" + slug + \".options\",\n \"defaults.adapters.\" + slug + \".contextContinuity\",\n \"defaults.adapters.\" + slug + \".auth.mode\",\n \"defaults.adapters.\" + slug + \".auth.source\",\n \"defaults.adapters.\" + slug + \".auth.provider\",\n \"defaults.adapters.\" + slug + \".auth.token\",\n \"defaults.adapters.\" + slug + \".auth.apiKey\",\n ];\n}\n\nfunction renderHarnesses() {\n var el = document.getElementById(\"sec-harnesses\");\n var adaptersR = harnessesState.adaptersResult;\n var capsR = harnessesState.capsResult;\n var presetsR = harnessesState.presetsResult;\n var rolesR = harnessesState.rolesResult;\n if (!adaptersR.ok) {\n el.innerHTML = '<div class=\"empty\">adapter_list: ' + escHtml(adaptersR.message) + '</div>';\n return;\n }\n var adapters = (adaptersR.data && adaptersR.data.adapters) || [];\n var capsBySlug = {};\n if (capsR.ok) {\n var caps = (capsR.data && capsR.data.capabilities) || [];\n for (var i = 0; i < caps.length; i++) capsBySlug[caps[i].adapter] = caps[i];\n }\n var presetsBySlug = {};\n var allPresets = [];\n if (presetsR.ok) {\n allPresets = (presetsR.data && presetsR.data.presets) || [];\n for (var j = 0; j < allPresets.length; j++) {\n var slug = allPresets[j].harnessSlug;\n if (!presetsBySlug[slug]) presetsBySlug[slug] = [];\n presetsBySlug[slug].push(allPresets[j]);\n }\n }\n\n var html = '<div class=\"sect-h\">Adapters</div>';\n if (!capsR.ok && capsR.unknown) html += '<div class=\"muted-note\">Capability detail needs a newer agentproto daemon.</div>';\n else if (!capsR.ok) html += '<div class=\"muted-note\">harness_capabilities: ' + escHtml(capsR.message) + '</div>';\n if (!presetsR.ok) html += '<div class=\"muted-note\">harness_preset_list: ' + escHtml(presetsR.message) + '</div>';\n\n html += '<div class=\"grid\">';\n if (adapters.length === 0) {\n html += '<div class=\"empty\">No adapters installed on this host.</div>';\n }\n for (var k = 0; k < adapters.length; k++) {\n var a = adapters[k];\n var cap = capsBySlug[a.slug];\n var slugPresets = presetsBySlug[a.slug] || [];\n var def = null;\n for (var d = 0; d < slugPresets.length; d++) if (slugPresets[d].isDefault) def = slugPresets[d];\n html += cardOpen(a.slug, a.name || a.slug, escHtml(a.slug));\n if (a.version) html += row(\"Version\", escHtml(a.version));\n if (a.protocol) html += row(\"Protocol\", escHtml(a.protocol));\n html += row(\"Models\", String((a.models || []).length));\n if (cap && Array.isArray(cap.providers) && cap.providers.length > 0) {\n var chips = \"\";\n for (var pi = 0; pi < cap.providers.length; pi++) {\n var prov = cap.providers[pi];\n chips += '<span class=\"tag ' + (prov.cred && prov.cred.present ? \"ok\" : \"\") + '\">' + escHtml(prov.billingEndpoint) + '</span> ';\n }\n html += '<dt>Billing</dt><dd>' + chips + '</dd>';\n }\n if (def) {\n html += row(\"Default preset\", escHtml(def.name) + ' → <a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(def.profileRef) + '\">' + escHtml(def.profileRef) + '</a> / ' + escHtml(def.defaultModel));\n } else if (slugPresets.length > 0) {\n html += row(\"Presets\", String(slugPresets.length) + \" (no default)\");\n }\n html += '<dt></dt><dd><button type=\"button\" class=\"btn-sm\" data-adapter-defaults-toggle=\"' + escHtml(a.slug) + '\">' + (harnessesState.expandedDefaults[a.slug] ? \"hide defaults\" : \"spawn defaults\") + '</button></dd>';\n html += '<div id=\"adapter-defaults-' + escHtml(a.slug) + '\"></div>';\n html += cardClose();\n }\n html += '</div>';\n\n html += '<div class=\"sect-h\">Presets<button type=\"button\" class=\"btn-sm\" id=\"preset-add-toggle\">' + (harnessesState.showPresetForm ? \"close\" : \"+ new preset\") + '</button></div>';\n if (harnessesState.showPresetForm) html += presetFormHtml(adapters);\n html += presetsTableHtml(allPresets);\n\n html += '<div class=\"sect-h\">Roles</div>';\n if (rolesR.ok) {\n var roles = (rolesR.data && rolesR.data.roles) || [];\n if (roles.length === 0) {\n html += '<div class=\"empty\">No roles registered.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Role</th><th>Level</th><th>Delegation</th><th>Can spawn</th></tr></thead><tbody>';\n for (var r = 0; r < roles.length; r++) {\n var role = roles[r];\n html += '<tr><td>' + escHtml(role.name) + '</td><td>' + escHtml(String(role.level)) + '</td><td>' + escHtml(role.delegation) + '</td><td>' + escHtml((role.spawnable || []).join(\", \")) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n } else {\n html += '<div class=\"muted-note\">role_list: ' + escHtml(rolesR.message) + '</div>';\n }\n\n el.innerHTML = html;\n wireGotoLinks(el);\n wireAdapterDefaultsToggles(el);\n wirePresetForm(el, adapters);\n wirePresetsTable(el);\n\n for (var slug2 in harnessesState.expandedDefaults) {\n if (harnessesState.expandedDefaults[slug2] && harnessesState.defaultsCache[slug2]) {\n renderAdapterDefaultsCard(slug2);\n }\n }\n}\n\nfunction presetsTableHtml(presets) {\n if (presets.length === 0) return '<div class=\"empty\">No harness presets configured.</div>';\n var html = '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Id</th><th>Harness</th><th>Name</th><th>Wallet</th><th>Model</th><th>Default</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < presets.length; i++) {\n var p = presets[i];\n html += '<tr data-row-id=\"' + escHtml(p.id) + '\"><td>' + escHtml(p.id) + '</td><td>' + escHtml(p.harnessSlug) + '</td><td>' + escHtml(p.name) + '</td>'\n + '<td><a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(p.profileRef) + '\">' + escHtml(p.profileRef) + '</a>' + (p.profileDisabled ? ' <span class=\"tag bad\">disabled/missing</span>' : '') + '</td>'\n + '<td>' + escHtml(p.defaultModel) + '</td>'\n + '<td>' + (p.isDefault ? '<span class=\"tag ok\">default</span>' : '<button type=\"button\" class=\"btn-sm\" data-preset-set-default=\"' + escHtml(p.id) + '\" data-preset-harness=\"' + escHtml(p.harnessSlug) + '\">set default</button>') + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-preset-delete=\"' + escHtml(p.id) + '\">delete</button></td></tr>';\n }\n html += '</tbody></table></div></div>';\n return html;\n}\n\nfunction presetFormHtml(adapters) {\n var profiles = (harnessesState.profilesResult && harnessesState.profilesResult.ok && harnessesState.profilesResult.data.profiles) || [];\n var html = '<div class=\"form-panel\">'\n + '<div class=\"form-row\">'\n + '<div class=\"field\"><label>Id</label><input type=\"text\" id=\"pf-id\"></div>'\n + '<div class=\"field\"><label>Harness</label><select id=\"pf-harness\">';\n for (var i = 0; i < adapters.length; i++) html += '<option value=\"' + escHtml(adapters[i].slug) + '\">' + escHtml(adapters[i].slug) + '</option>';\n html += '</select></div>'\n + '<div class=\"field\"><label>Name</label><input type=\"text\" id=\"pf-name\"></div>'\n + '</div><div class=\"form-row\">'\n + '<div class=\"field\"><label>Wallet</label><select id=\"pf-profile\">';\n for (var j = 0; j < profiles.length; j++) html += '<option value=\"' + escHtml(profiles[j].id) + '\">' + escHtml(profiles[j].label || profiles[j].id) + '</option>';\n html += '</select></div>'\n + '<div class=\"field\"><label>Default model</label><input type=\"text\" id=\"pf-model\" placeholder=\"vendor/model\"></div>'\n + '<label class=\"inline\"><input type=\"checkbox\" id=\"pf-default\"> make default for this harness</label>'\n + '<button type=\"button\" class=\"btn-sm primary\" id=\"pf-submit\">Create preset</button>'\n + '</div><div class=\"cfg-err\" id=\"pf-err\" style=\"display:none\"></div></div>';\n return html;\n}\n\nfunction wirePresetForm(container, adapters) {\n var toggle = container.querySelector(\"#preset-add-toggle\");\n if (toggle) toggle.addEventListener(\"click\", function () {\n harnessesState.showPresetForm = !harnessesState.showPresetForm;\n renderHarnesses();\n });\n var submit = container.querySelector(\"#pf-submit\");\n if (!submit) return;\n submit.addEventListener(\"click\", function () {\n var errEl = container.querySelector(\"#pf-err\");\n errEl.style.display = \"none\";\n var id = container.querySelector(\"#pf-id\").value.trim();\n var harnessSlug = container.querySelector(\"#pf-harness\").value;\n var name = container.querySelector(\"#pf-name\").value.trim();\n var profileRef = container.querySelector(\"#pf-profile\").value;\n var defaultModel = container.querySelector(\"#pf-model\").value.trim();\n var isDefault = container.querySelector(\"#pf-default\").checked;\n if (!id || !name || !defaultModel || !profileRef) {\n errEl.style.display = \"block\";\n errEl.textContent = \"Id, name, wallet, and default model are required.\";\n return;\n }\n loadTool(\"harness_preset_create\", { id: id, harnessSlug: harnessSlug, name: name, profileRef: profileRef, defaultModel: defaultModel, isDefault: isDefault }).then(function (r) {\n if (!r.ok) { errEl.style.display = \"block\"; errEl.textContent = r.message; return; }\n toast(\"Preset saved.\", false);\n harnessesState.showPresetForm = false;\n loadHarnesses();\n });\n });\n}\n\nfunction wirePresetsTable(container) {\n var setDefaults = container.querySelectorAll(\"[data-preset-set-default]\");\n for (var i = 0; i < setDefaults.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var presetId = btn.getAttribute(\"data-preset-set-default\");\n var harnessSlug = btn.getAttribute(\"data-preset-harness\");\n loadTool(\"harness_preset_set_default\", { harnessSlug: harnessSlug, presetId: presetId }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Default preset updated.\", false);\n loadHarnesses();\n });\n });\n })(setDefaults[i]);\n }\n var deletes = container.querySelectorAll(\"[data-preset-delete]\");\n for (var d = 0; d < deletes.length; d++) {\n (function (btn) {\n var presetId = btn.getAttribute(\"data-preset-delete\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"preset-delete-\" + presetId, btn, function () {\n loadTool(\"harness_preset_delete\", { id: presetId }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Preset deleted.\", false);\n loadHarnesses();\n });\n });\n });\n })(deletes[d]);\n }\n}\n\nfunction wireAdapterDefaultsToggles(container) {\n var toggles = container.querySelectorAll(\"[data-adapter-defaults-toggle]\");\n for (var i = 0; i < toggles.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var slug = btn.getAttribute(\"data-adapter-defaults-toggle\");\n harnessesState.expandedDefaults[slug] = !harnessesState.expandedDefaults[slug];\n if (harnessesState.expandedDefaults[slug] && !harnessesState.defaultsCache[slug]) {\n loadTool(\"config_get\", { keys: adapterDefaultsPaths(slug) }).then(function (r) {\n if (r.ok && r.data.revision) configRevision = r.data.revision;\n harnessesState.defaultsCache[slug] = r.ok ? (r.data.keys || []) : [];\n if (r.ok) notePendingRestart(r.data.keys);\n if (!r.ok && r.unknown) {\n var host = document.getElementById(\"adapter-defaults-\" + slug);\n if (host) host.innerHTML = '<div class=\"muted-note\">Per-harness defaults need a newer agentproto daemon.</div>';\n return;\n }\n if (!r.ok) {\n var host2 = document.getElementById(\"adapter-defaults-\" + slug);\n if (host2) host2.innerHTML = '<div class=\"muted-note\">config_get: ' + escHtml(r.message) + '</div>';\n return;\n }\n renderAdapterDefaultsCard(slug);\n });\n }\n btn.textContent = harnessesState.expandedDefaults[slug] ? \"hide defaults\" : \"spawn defaults\";\n var host3 = document.getElementById(\"adapter-defaults-\" + slug);\n if (host3 && !harnessesState.expandedDefaults[slug]) host3.innerHTML = \"\";\n else if (host3 && harnessesState.defaultsCache[slug]) renderAdapterDefaultsCard(slug);\n });\n })(toggles[i]);\n }\n}\n\nfunction renderAdapterDefaultsCard(slug) {\n var host = document.getElementById(\"adapter-defaults-\" + slug);\n if (!host) return;\n var rows = harnessesState.defaultsCache[slug] || [];\n var rowsByPath = {};\n for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];\n host.innerHTML = '<dl>' + rows.map(configFieldHtml).join(\"\") + '</dl>';\n wireConfigRows(host, rowsByPath, function (path) {\n loadTool(\"config_get\", { keys: adapterDefaultsPaths(slug) }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n harnessesState.defaultsCache[slug] = r.data.keys || [];\n notePendingRestart(r.data.keys);\n renderAdapterDefaultsCard(slug);\n });\n }, function () {\n loadHarnesses();\n });\n}\n\nfunction wireGotoLinks(container) {\n var links = container.querySelectorAll(\"a[data-goto]\");\n for (var i = 0; i < links.length; i++) {\n (function (a) {\n a.addEventListener(\"click\", function (e) {\n e.preventDefault();\n location.hash = \"#\" + a.getAttribute(\"data-goto\");\n });\n })(links[i]);\n }\n}\n\n// ============================================================\n// Models\n// ============================================================\n\nvar modelsState = { routes: [], runnableOnly: false, walletFilter: \"\", titlerRow: null };\n\nfunction loadModels() {\n return Promise.all([\n loadTool(\"catalog_models\", { full: true }),\n loadTool(\"auth_profile_list\", { full: true }),\n loadTool(\"config_get\", { keys: [\"titler.model\"] }),\n ]).then(function (results) {\n modelsState.routesResult = results[0];\n modelsState.profilesResult = results[1];\n modelsState.titlerResult = results[2];\n if (results[2].ok && results[2].data.revision) configRevision = results[2].data.revision;\n if (results[2].ok) notePendingRestart(results[2].data.keys);\n renderModels();\n });\n}\n\nfunction renderModels() {\n var el = document.getElementById(\"sec-models\");\n var rr = modelsState.routesResult;\n if (!rr.ok) {\n el.innerHTML = '<div class=\"empty\">catalog_models: ' + escHtml(rr.message) + '</div>';\n return;\n }\n var routes = (rr.data && rr.data.routes) || [];\n var profiles = (modelsState.profilesResult && modelsState.profilesResult.ok && modelsState.profilesResult.data && modelsState.profilesResult.data.profiles) || [];\n\n var html = '<div class=\"sect-h\">Titler model</div><div id=\"titler-model-card\"></div>';\n\n html += '<div class=\"sect-h\">Catalog<label class=\"inline\"><input type=\"checkbox\" id=\"runnable-only\"' + (modelsState.runnableOnly ? \" checked\" : \"\") + '> runnable only</label>'\n + '<label class=\"inline\">wallet <select id=\"wallet-filter\"><option value=\"\">any</option>';\n for (var i = 0; i < profiles.length; i++) {\n html += '<option value=\"' + escHtml(profiles[i].id) + '\"' + (profiles[i].id === modelsState.walletFilter ? \" selected\" : \"\") + '>' + escHtml(profiles[i].label || profiles[i].id) + '</option>';\n }\n html += '</select></label></div>';\n\n var filtered = routes.filter(function (r) {\n if (modelsState.runnableOnly && !r.runnable) return false;\n if (modelsState.walletFilter && (!r.eligibleProfiles || r.eligibleProfiles.indexOf(modelsState.walletFilter) === -1)) return false;\n return true;\n });\n\n var byVendor = {};\n var vendorOrder = [];\n for (var f = 0; f < filtered.length; f++) {\n var v = filtered[f].vendor;\n if (!byVendor[v]) { byVendor[v] = []; vendorOrder.push(v); }\n byVendor[v].push(filtered[f]);\n }\n vendorOrder.sort();\n\n if (vendorOrder.length === 0) {\n html += '<div class=\"empty\">No models match the current filters.</div>';\n }\n for (var vi = 0; vi < vendorOrder.length; vi++) {\n var vendor = vendorOrder[vi];\n var vendorRows = byVendor[vendor];\n var byProduct = {};\n var productOrder = [];\n for (var pr2 = 0; pr2 < vendorRows.length; pr2++) {\n var prod = vendorRows[pr2].product;\n if (!byProduct[prod]) { byProduct[prod] = []; productOrder.push(prod); }\n byProduct[prod].push(vendorRows[pr2]);\n }\n productOrder.sort();\n html += '<div class=\"sect-h\">' + escHtml(vendor) + '</div><div class=\"grid\">';\n for (var pi2 = 0; pi2 < productOrder.length; pi2++) {\n var product = productOrder[pi2];\n var productRoutes = byProduct[product];\n var modelRef = vendor + \"/\" + product;\n html += cardOpen(modelRef, product, \"\");\n for (var ri = 0; ri < productRoutes.length; ri++) {\n var route = productRoutes[ri];\n var priceStr = route.pricing ? \"$\" + route.pricing.inPer1M + \"/$\" + route.pricing.outPer1M + \" per 1M\" : \"\";\n var elig = (route.eligibleProfiles || []).map(function (id) {\n return '<a class=\"link\" data-goto=\"wallets/' + encodeURIComponent(id) + '\">' + escHtml(id) + '</a>';\n }).join(\" \");\n html += row(route.route, (route.runnable ? '<span class=\"tag ok\">runnable</span>' : '<span class=\"tag\">not runnable</span>') + (route.curated ? ' <span class=\"tag\">curated</span>' : \"\"));\n if (priceStr) html += row(\"Price\", escHtml(priceStr));\n if (route.contextWindow) html += row(\"Context\", escHtml(route.contextWindow) + (route.maxOutput ? \" / \" + escHtml(route.maxOutput) + \" out\" : \"\"));\n if (elig) html += '<dt>Wallets</dt><dd>' + elig + '</dd>';\n }\n html += cardClose();\n }\n html += '</div>';\n }\n\n html += '<div class=\"sect-h\">Wallet × model matrix</div>' + walletModelMatrixHtml(routes, profiles);\n\n el.innerHTML = html;\n wireGotoLinks(el);\n wireModelMatrix(el, routes, profiles);\n renderTitlerModelCard();\n\n var runnableEl = document.getElementById(\"runnable-only\");\n if (runnableEl) runnableEl.addEventListener(\"change\", function (e) { modelsState.runnableOnly = e.target.checked; renderModels(); });\n var walletEl = document.getElementById(\"wallet-filter\");\n if (walletEl) walletEl.addEventListener(\"change\", function (e) { modelsState.walletFilter = e.target.value; renderModels(); });\n}\n\nfunction walletModelMatrixHtml(routes, profiles) {\n if (profiles.length === 0) return '<div class=\"empty\">No wallets configured.</div>';\n var eligibleRoutes = routes.filter(function (r) { return r.eligibleProfiles && r.eligibleProfiles.length > 0; }).slice(0, 40);\n if (eligibleRoutes.length === 0) return '<div class=\"empty\">No wallet-reachable models yet.</div>';\n var html = '<div class=\"card\"><div class=\"body\" style=\"overflow-x:auto\"><table><thead><tr><th>Wallet</th>';\n for (var c = 0; c < eligibleRoutes.length; c++) html += '<th title=\"' + escHtml(eligibleRoutes[c].route) + '\">' + escHtml(eligibleRoutes[c].product) + '</th>';\n html += '</tr></thead><tbody>';\n for (var i = 0; i < profiles.length; i++) {\n var p = profiles[i];\n html += '<tr><td>' + escHtml(p.label || p.id) + '</td>';\n for (var j = 0; j < eligibleRoutes.length; j++) {\n var route = eligibleRoutes[j];\n var modelId = route.ref || (route.vendor + \"/\" + route.product);\n var reachable = route.eligibleProfiles.indexOf(p.id) !== -1;\n if (!reachable) {\n html += '<td class=\"matrix-cell disabled\">–</td>';\n continue;\n }\n var mode = (p.models && p.models.mode) || \"all\";\n var checked = mode === \"all\" || (p.models.ids || []).indexOf(modelId) !== -1;\n html += '<td class=\"matrix-cell\" data-matrix-profile=\"' + escHtml(p.id) + '\" data-matrix-model=\"' + escHtml(modelId) + '\" data-matrix-mode=\"' + escHtml(mode) + '\">' + (checked ? \"✓\" : \"–\") + '</td>';\n }\n html += '</tr>';\n }\n html += '</tbody></table></div></div><div class=\"muted-note\">Click a checked cell in an \"all eligible\" wallet to start curating it; click a cell to toggle curation once curating.</div>';\n return html;\n}\n\nfunction wireModelMatrix(container, routes, profiles) {\n var cells = container.querySelectorAll(\"[data-matrix-profile]\");\n for (var i = 0; i < cells.length; i++) {\n (function (cell) {\n cell.addEventListener(\"click\", function () {\n var id = cell.getAttribute(\"data-matrix-profile\");\n var modelId = cell.getAttribute(\"data-matrix-model\");\n var p = null;\n for (var k = 0; k < profiles.length; k++) if (profiles[k].id === id) p = profiles[k];\n if (!p) return;\n var mode = (p.models && p.models.mode) || \"all\";\n var currentIds = (p.models && p.models.ids) || [];\n var nextIds;\n if (mode === \"all\") {\n // Start curating from just this one model.\n nextIds = [modelId];\n } else if (currentIds.indexOf(modelId) !== -1) {\n nextIds = currentIds.filter(function (x) { return x !== modelId; });\n } else {\n nextIds = currentIds.concat([modelId]);\n }\n loadTool(\"auth_profile_set_models\", { id: id, mode: \"allow\", ids: nextIds }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n for (var k2 = 0; k2 < profiles.length; k2++) if (profiles[k2].id === id) profiles[k2] = r.data.profile;\n if (modelsState.profilesResult && modelsState.profilesResult.ok) {\n for (var k3 = 0; k3 < modelsState.profilesResult.data.profiles.length; k3++) {\n if (modelsState.profilesResult.data.profiles[k3].id === id) modelsState.profilesResult.data.profiles[k3] = r.data.profile;\n }\n }\n renderModels();\n });\n });\n })(cells[i]);\n }\n}\n\nfunction renderTitlerModelCard() {\n var host = document.getElementById(\"titler-model-card\");\n if (!host) return;\n var tr = modelsState.titlerResult;\n if (!tr.ok && tr.unknown) {\n host.innerHTML = '<div class=\"muted-note\">Editing titler.model needs a newer agentproto daemon (config_get).</div>';\n return;\n }\n if (!tr.ok) {\n host.innerHTML = '<div class=\"muted-note\">config_get: ' + escHtml(tr.message) + '</div>';\n return;\n }\n var rows = tr.data.keys || [];\n var rowsByPath = {};\n for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];\n host.innerHTML = '<div class=\"card\"><div class=\"body\"><dl>' + rows.map(configFieldHtml).join(\"\") + '</dl></div></div>';\n wireConfigRows(host, rowsByPath, function () {\n loadTool(\"config_get\", { keys: [\"titler.model\"] }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n modelsState.titlerResult = r;\n notePendingRestart(r.data.keys);\n renderTitlerModelCard();\n });\n }, function () {\n loadModels();\n });\n}\n\n// ============================================================\n// Capabilities (MCP & Skills)\n// ============================================================\n\nvar capabilitiesState = {\n tab: \"mcp\", // \"mcp\" | \"skills\"\n invResult: null,\n expandedImports: {}, // importId -> bool\n};\n\nfunction loadCapabilities() {\n return loadTool(\"capabilities_inventory\", {}).then(function (r) {\n capabilitiesState.invResult = r;\n renderCapabilities();\n });\n}\n\nfunction capTabsHtml() {\n return '<span class=\"pillbar\">'\n + '<button type=\"button\" class=\"' + (capabilitiesState.tab === \"mcp\" ? \"active\" : \"\") + '\" data-cap-tab=\"mcp\">MCP</button>'\n + '<button type=\"button\" class=\"' + (capabilitiesState.tab === \"skills\" ? \"active\" : \"\") + '\" data-cap-tab=\"skills\">Skills</button>'\n + '</span>';\n}\n\nfunction capStatusTagHtml(status) {\n var cls = status === \"connected\" ? \"ok\" : status === \"error\" ? \"bad\" : status === \"idle\" ? \"\" : \"warn-tag\";\n return '<span class=\"tag ' + cls + '\">' + escHtml(status) + '</span>';\n}\n\nfunction renderCapabilities() {\n var el = document.getElementById(\"sec-capabilities\");\n var r = capabilitiesState.invResult;\n var html = '<div class=\"sect-h\">Capabilities' + capTabsHtml() + '</div>';\n if (!r) { el.innerHTML = html; return; }\n if (!r.ok) {\n html += r.unknown\n ? '<div class=\"muted-note\">Capabilities inventory needs a newer agentproto daemon (capabilities_inventory).</div>'\n : '<div class=\"muted-note\">capabilities_inventory: ' + escHtml(r.message) + '</div>';\n el.innerHTML = html;\n return;\n }\n var inv = r.data;\n html += capabilitiesState.tab === \"skills\" ? renderSkillsTabHtml(inv.skills || {}) : renderMcpTabHtml(inv.mcp || {});\n el.innerHTML = html;\n wireCapabilitiesTabs(el);\n if (capabilitiesState.tab === \"mcp\") wireMcpTab(el);\n}\n\nfunction wireCapabilitiesTabs(container) {\n var btns = container.querySelectorAll(\"[data-cap-tab]\");\n for (var i = 0; i < btns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n capabilitiesState.tab = btn.getAttribute(\"data-cap-tab\");\n renderCapabilities();\n });\n })(btns[i]);\n }\n}\n\nfunction renderMcpTabHtml(mcp) {\n var imported = mcp.imported || [];\n var discovered = mcp.discovered || [];\n var reach = mcp.daemonMountByHarness || {};\n var html = \"\";\n if (mcp.error) html += '<div class=\"muted-note\">mcp: ' + escHtml(mcp.error) + '</div>';\n\n html += '<div class=\"sect-h\">Imported MCP servers</div>';\n if (imported.length === 0) {\n html += '<div class=\"empty\">No MCP servers imported yet.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Source</th><th>Status</th><th>Tools</th><th>Used by</th><th></th><th></th></tr></thead><tbody>';\n for (var i = 0; i < imported.length; i++) {\n var m = imported[i];\n var expanded = !!capabilitiesState.expandedImports[m.id];\n var usedBy = (m.usedBySessions || []).length;\n html += '<tr data-card-id=\"' + escHtml(m.id) + '\" data-row-id=\"' + escHtml(m.id) + '\">'\n + '<td>' + escHtml(m.alias || m.name) + '</td>'\n + '<td>' + escHtml(m.source) + '</td>'\n + '<td>' + capStatusTagHtml(m.status) + (m.error ? ' <span class=\"tag bad\" title=\"' + escHtml(m.error) + '\">!</span>' : '') + '</td>'\n + '<td>' + (m.toolCount != null ? String(m.toolCount) : \"–\") + '</td>'\n + '<td>' + (usedBy > 0 ? \"used by \" + usedBy + \" session\" + (usedBy === 1 ? \"\" : \"s\") : \"unused\") + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm\" data-cap-import-tools=\"' + escHtml(m.id) + '\" data-cap-import-alias=\"' + escHtml(m.alias || m.name) + '\">' + (expanded ? \"hide tools\" : \"show tools\") + '</button></td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-cap-import-remove=\"' + escHtml(m.id) + '\">remove</button></td>'\n + '</tr>'\n + '<tr><td colspan=\"7\"><div id=\"cap-import-tools-' + escHtml(pathId(m.id)) + '\"></div></td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n\n html += '<div class=\"sect-h\">Reach</div>';\n var slugs = Object.keys(reach);\n if (slugs.length === 0) {\n html += '<div class=\"empty\">No adapters installed.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\">';\n for (var s = 0; s < slugs.length; s++) {\n var r2 = reach[slugs[s]];\n var cls = r2 === \"default\" ? \"ok\" : r2 === \"on-request\" ? \"\" : \"bad\";\n html += '<span class=\"tag ' + cls + '\" style=\"margin:2px 4px 2px 0\">' + escHtml(slugs[s]) + \": \" + escHtml(r2) + '</span>';\n }\n html += '<div class=\"muted-note\">\"default\" harnesses get the imported pool automatically; \"on-request\" harnesses can mount it explicitly via <code>mcpServers</code>; \"none\" harnesses cannot reach it at all. Either way, reach is via <code>mcp_imported_tool_list</code> / <code>mcp_imported_call</code>, not a direct per-server mount.</div>';\n html += '</div></div>';\n }\n\n html += '<div class=\"sect-h\">Discovered, not imported</div>';\n var notImported = discovered.filter(function (d) { return !d.imported; });\n if (notImported.length === 0) {\n html += '<div class=\"empty\">Nothing new discovered.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Source</th><th>Type</th><th></th></tr></thead><tbody>';\n for (var d = 0; d < notImported.length; d++) {\n var disc = notImported[d];\n html += '<tr data-card-id=\"' + escHtml(disc.id) + '\" data-row-id=\"' + escHtml(disc.id) + '\"><td>' + escHtml(disc.name) + '</td><td>' + escHtml(disc.source) + '</td><td>' + escHtml(disc.type) + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm primary\" data-cap-import=\"' + escHtml(disc.id) + '\">import</button></td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n return html;\n}\n\nfunction wireMcpTab(container) {\n var toolBtns = container.querySelectorAll(\"[data-cap-import-tools]\");\n for (var i = 0; i < toolBtns.length; i++) {\n (function (btn) {\n btn.addEventListener(\"click\", function () {\n var id = btn.getAttribute(\"data-cap-import-tools\");\n var alias = btn.getAttribute(\"data-cap-import-alias\");\n var expanded = !capabilitiesState.expandedImports[id];\n capabilitiesState.expandedImports[id] = expanded;\n btn.textContent = expanded ? \"hide tools\" : \"show tools\";\n var host = document.getElementById(\"cap-import-tools-\" + pathId(id));\n if (!host) return;\n if (!expanded) { host.innerHTML = \"\"; return; }\n host.innerHTML = '<div class=\"muted-note\">Loading tools…</div>';\n loadTool(\"mcp_imported_tool_list\", { alias: alias }).then(function (r) {\n if (!r.ok) { host.innerHTML = '<div class=\"muted-note\">mcp_imported_tool_list: ' + escHtml(r.message) + '</div>'; return; }\n var tools = (r.data && r.data.tools) || [];\n if (tools.length === 0) { host.innerHTML = '<div class=\"empty\">No tools (server may not be connected yet).</div>'; return; }\n var h = '<div class=\"muted-note\">' + tools.length + ' tool(s):</div><ul>';\n for (var t = 0; t < tools.length; t++) {\n h += '<li><code>' + escHtml(tools[t].name) + '</code>' + (tools[t].description ? \" — \" + escHtml(tools[t].description) : \"\") + '</li>';\n }\n h += '</ul>';\n host.innerHTML = h;\n });\n });\n })(toolBtns[i]);\n }\n\n var removeBtns = container.querySelectorAll(\"[data-cap-import-remove]\");\n for (var r3 = 0; r3 < removeBtns.length; r3++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-cap-import-remove\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"cap-import-remove-\" + id, btn, function () {\n loadTool(\"mcp_imported_remove\", { id: id }).then(function (res) {\n if (!res.ok) { toast(res.message, true); return; }\n toast(\"Removed.\", false);\n loadCapabilities();\n });\n });\n });\n })(removeBtns[r3]);\n }\n\n var importBtns = container.querySelectorAll(\"[data-cap-import]\");\n for (var im = 0; im < importBtns.length; im++) {\n (function (btn) {\n var id = btn.getAttribute(\"data-cap-import\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"cap-import-\" + id, btn, function () {\n loadTool(\"mcp_import\", { sourceMcpId: id }).then(function (res) {\n if (!res.ok) { toast(res.message, true); return; }\n toast(\"Imported.\", false);\n loadCapabilities();\n });\n });\n });\n })(importBtns[im]);\n }\n}\n\nfunction capPackHasSkill(packs, name) {\n for (var i = 0; i < packs.length; i++) {\n if ((packs[i].skills || []).indexOf(name) !== -1) return true;\n }\n return false;\n}\n\nfunction renderSkillsTabHtml(skills) {\n var harnesses = skills.byHarness || [];\n var packs = skills.packs || [];\n var defaults = skills.defaults || { global: [], byAdapter: {} };\n var html = \"\";\n if (skills.error) html += '<div class=\"muted-note\">skills: ' + escHtml(skills.error) + '</div>';\n\n html += '<div class=\"sect-h\">Skills by harness</div>';\n if (harnesses.length === 0) {\n html += '<div class=\"empty\">No adapters installed.</div>';\n return html;\n }\n\n var skillNames = {};\n for (var p = 0; p < packs.length; p++) {\n for (var s = 0; s < (packs[p].skills || []).length; s++) skillNames[packs[p].skills[s]] = true;\n }\n for (var h = 0; h < harnesses.length; h++) {\n for (var i2 = 0; i2 < (harnesses[h].installed || []).length; i2++) skillNames[harnesses[h].installed[i2]] = true;\n for (var n = 0; n < (harnesses[h].native || []).length; n++) skillNames[harnesses[h].native[n]] = true;\n }\n var names = Object.keys(skillNames).sort();\n\n html += '<div class=\"card\"><div class=\"body\" style=\"overflow-x:auto\"><table><thead><tr><th>Skill</th>';\n for (var c = 0; c < harnesses.length; c++) {\n var target = harnesses[c].target;\n html += '<th data-card-id=\"' + escHtml(harnesses[c].adapter) + '\">' + escHtml(harnesses[c].adapter)\n + '<div class=\"cfg-help\">' + (target ? escHtml(target.dir || \"\") : \"unsupported\") + '</div>'\n + (harnesses[c].spawnOption ? '<div class=\"cfg-help\">skills spawn option</div>' : '')\n + '</th>';\n }\n html += '</tr></thead><tbody>';\n\n html += '<tr><td><em>defaults</em></td>';\n for (var d = 0; d < harnesses.length; d++) {\n var adapterDefaults = defaults.byAdapter[harnesses[d].adapter] || [];\n var all = defaults.global.concat(adapterDefaults);\n html += '<td>' + (all.length > 0 ? escHtml(all.join(\", \")) : \"–\") + '</td>';\n }\n html += '</tr>';\n\n if (names.length === 0) {\n html += '<tr><td colspan=\"' + (harnesses.length + 1) + '\">No skills found in any installed pack.</td></tr>';\n }\n for (var nm = 0; nm < names.length; nm++) {\n html += '<tr><td>' + escHtml(names[nm]) + '</td>';\n for (var hc = 0; hc < harnesses.length; hc++) {\n var row = harnesses[hc];\n var cell;\n if (!row.target) {\n cell = '<span class=\"tag\">unsupported</span>';\n } else if ((row.installed || []).indexOf(names[nm]) !== -1) {\n cell = '<span class=\"tag ok\">installed</span>';\n } else if ((row.native || []).indexOf(names[nm]) !== -1) {\n cell = '<span class=\"tag ok\">native</span>';\n } else if (capPackHasSkill(packs, names[nm])) {\n cell = '<span class=\"tag warn-tag\">available</span><div class=\"cfg-help\"><code>agentproto install skill/' + escHtml(names[nm]) + '</code></div>';\n } else {\n cell = '<span class=\"tag\">–</span>';\n }\n html += '<td>' + cell + '</td>';\n }\n html += '</tr>';\n }\n html += '</tbody></table></div></div>';\n\n html += '<div class=\"sect-h\">Packs</div>';\n if (packs.length === 0) {\n html += '<div class=\"empty\">No skill packs found under ~/.agentproto/packs.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Pack</th><th>Version</th><th>Skills</th></tr></thead><tbody>';\n for (var pk = 0; pk < packs.length; pk++) {\n html += '<tr data-card-id=\"' + escHtml(packs[pk].name) + '\"><td>' + escHtml(packs[pk].name) + '</td><td>' + escHtml(packs[pk].version || \"\") + '</td><td>' + escHtml((packs[pk].skills || []).join(\", \")) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n return html;\n}\n\n// ============================================================\n// Defaults & messaging\n// ============================================================\n\nvar DEFAULTS_GROUPS = [\n {\n id: \"messaging\", title: \"Messaging and prompts\",\n match: function (p) {\n return p.indexOf(\"defaults.messaging.\") === 0 || p === \"defaults.agentPromptInterrupt\"\n || p === \"defaults.backgroundTaskWake.enabled\" || p === \"defaults.backgroundTaskWake.graceMs\";\n },\n },\n {\n id: \"spawn\", title: \"Spawn\",\n match: function (p) {\n return p.indexOf(\"spawn.\") === 0 || p.indexOf(\"worktrees.\") === 0 || p.indexOf(\"defaults.spawn.\") === 0\n || p === \"defaults.skills\" || p === \"defaults.options\" || p === \"defaults.defaultRoleDepthCutoff\"\n || p === \"defaults.maxGrantableDelegation\" || p === \"provenance.wrapGh\" || p === \"agentsMd.inlineMaxKb\";\n },\n },\n { id: \"context-continuity\", title: \"Context continuity\", match: function (p) { return p === \"defaults.contextContinuity\"; } },\n {\n id: \"observability\", title: \"Observability\",\n match: function (p) {\n return p.indexOf(\"titler.\") === 0 || p === \"defaults.langfuseTracing\" || p === \"defaults.traceRedactor\"\n || p === \"sessions.attentionDelaySec\" || p === \"defaults.mcp.deferredTools\";\n },\n },\n {\n id: \"daemon\", title: \"Daemon\",\n match: function (p) {\n return p.indexOf(\"daemon.\") === 0 || p === \"sessions.eventsDir\" || p === \"features.pty\" || p === \"features.llmEndpoint\"\n || p === \"profiles\" || p === \"activeProfile\";\n },\n },\n];\n\nfunction classifyDefaultsGroup(path) {\n for (var i = 0; i < DEFAULTS_GROUPS.length; i++) if (DEFAULTS_GROUPS[i].match(path)) return DEFAULTS_GROUPS[i];\n return { id: \"other\", title: \"Other\" };\n}\n\nvar defaultsState = { rowsByPath: {}, healthFallback: null };\n\nfunction loadDefaults() {\n return Promise.all([\n loadTool(\"config_get\", { section: \"defaults\" }),\n loadTool(\"config_get\", { section: \"daemon\" }),\n loadTool(\"daemon_health\", {}),\n ]).then(function (results) {\n renderDefaults(results[0], results[1], results[2]);\n });\n}\n\nfunction renderDefaults(defR, daemonR, healthR) {\n var el = document.getElementById(\"sec-defaults\");\n\n if (!defR.ok && defR.unknown) {\n // Feature-detect fallback for a daemon that predates config_get: the\n // five effective knobs daemon_health already reports, read-only.\n var html = '<div class=\"notice\">Full defaults editor needs a newer agentproto daemon (config_get). Showing the knobs daemon_health reports.</div>';\n if (healthR.ok) {\n var hd = healthR.data;\n var restartBadge = ' <span class=\"tag\">restart</span>';\n html += '<div class=\"card\" data-card-id=\"daemon\"><div class=\"title\"><span>Daemon</span>'\n + (hd.version ? '<span class=\"aside\">' + escHtml(hd.version) + '</span>' : '') + '</div><div class=\"body\"><dl>'\n + row(\"Resume sessions on boot\", (hd.resumeSessionsOnBoot ? \"Yes\" : \"No\") + restartBadge)\n + row(\"Idle reap after\", (hd.idleReapAfterMs ? formatMs(hd.idleReapAfterMs) : \"off\") + restartBadge)\n + row(\"Crash detect interval\", (hd.crashDetectIntervalMs ? formatMs(hd.crashDetectIntervalMs) : \"off\") + restartBadge)\n + row(\"Restart sweep interval\", (hd.restartSweepIntervalMs ? formatMs(hd.restartSweepIntervalMs) : \"off\") + restartBadge)\n + row(\"Turn stall after\", (hd.turnStallAfterMs ? formatMs(hd.turnStallAfterMs) : \"off\") + restartBadge)\n + (hd.build && hd.build.sha ? row(\"Build\", escHtml(hd.build.sha)) : \"\")\n + '</dl></div></div>';\n } else {\n html += '<div class=\"muted-note\">daemon_health: ' + escHtml(healthR.message) + '</div>';\n }\n el.innerHTML = html;\n return;\n }\n if (!defR.ok) {\n el.innerHTML = '<div class=\"empty\">config_get: ' + escHtml(defR.message) + '</div>';\n return;\n }\n\n if (defR.data.revision) configRevision = defR.data.revision;\n var rows = (defR.data.keys || []).concat(daemonR.ok ? (daemonR.data.keys || []) : []);\n notePendingRestart(rows);\n defaultsState.rowsByPath = {};\n for (var i = 0; i < rows.length; i++) defaultsState.rowsByPath[rows[i].path] = rows[i];\n\n el.innerHTML = renderDefaultsGroupsHtml();\n wireConfigRows(el, defaultsState.rowsByPath, function (path) {\n refreshDefaultsGroupFor(path);\n }, function () {\n loadDefaults();\n });\n}\n\nfunction objectValues(obj) {\n var out = [];\n for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) out.push(obj[k]);\n return out;\n}\n\nfunction renderDefaultsGroupsHtml() {\n var byId = {};\n var order = [];\n var rows = objectValues(defaultsState.rowsByPath);\n for (var i = 0; i < rows.length; i++) {\n var g = classifyDefaultsGroup(rows[i].path);\n if (!byId[g.id]) { byId[g.id] = { id: g.id, title: g.title, rows: [] }; order.push(g.id); }\n byId[g.id].rows.push(rows[i]);\n }\n var html = \"\";\n for (var j = 0; j < order.length; j++) {\n var group = byId[order[j]];\n html += '<div class=\"sect-h\">' + escHtml(group.title) + '</div>' + buildConfigCardHtml(group.id, group.title, group.rows);\n }\n return html;\n}\n\nfunction refreshDefaultsGroupFor(path) {\n var group = classifyDefaultsGroup(path);\n var groupPaths = [];\n for (var k in defaultsState.rowsByPath) {\n if (Object.prototype.hasOwnProperty.call(defaultsState.rowsByPath, k) && classifyDefaultsGroup(k).id === group.id) groupPaths.push(k);\n }\n loadTool(\"config_get\", { keys: groupPaths }).then(function (r) {\n if (!r.ok) { toast(\"config_get: \" + r.message, true); return; }\n if (r.data.revision) configRevision = r.data.revision;\n var newRows = r.data.keys || [];\n for (var i = 0; i < newRows.length; i++) defaultsState.rowsByPath[newRows[i].path] = newRows[i];\n notePendingRestart(newRows);\n var el = document.getElementById(\"sec-defaults\");\n var cardEl = el.querySelector('[data-card-id=\"' + cssEscape(group.id) + '\"]');\n if (!cardEl) return;\n cardEl.outerHTML = buildConfigCardHtml(group.id, group.title, newRows);\n var freshEl = el.querySelector('[data-card-id=\"' + cssEscape(group.id) + '\"]');\n wireConfigRows(freshEl, defaultsState.rowsByPath, function (p2) { refreshDefaultsGroupFor(p2); }, function () { loadDefaults(); });\n });\n}\n\n// ============================================================\n// Remote & pairing\n// ============================================================\n\n// Held ONLY in memory, never in section state that survives a re-render\n// across sections, never in updateModelContext. Cleared on navigating away\n// from #remote (route()) and on host teardown (see boot, below).\nvar remoteReveal = null;\nvar pairReveal = null;\nvar pairCountdownTimer = null;\n\nfunction clearPairCountdown() {\n if (pairCountdownTimer) { clearInterval(pairCountdownTimer); pairCountdownTimer = null; }\n}\n\nfunction startPairCountdown(expiresAtIso) {\n clearPairCountdown();\n pairCountdownTimer = setInterval(function () {\n var el = document.getElementById(\"pair-offer-countdown\");\n if (!el) { clearPairCountdown(); return; }\n var ms = new Date(expiresAtIso).getTime() - Date.now();\n if (ms <= 0) { el.textContent = \"expired\"; clearPairCountdown(); return; }\n el.textContent = Math.ceil(ms / 1000) + \"s remaining\";\n }, 1000);\n}\n\nfunction loadRemote() {\n return Promise.all([\n loadTool(\"remote_status\", {}),\n loadTool(\"pair_list\", {}),\n loadTool(\"tunnel_list\", {}),\n ]).then(function (results) {\n renderRemote(results[0], results[1], results[2]);\n });\n}\n\nfunction renderRemote(remoteR, pairR, tunnelR) {\n var el = document.getElementById(\"sec-remote\");\n var html = '<div class=\"sect-h\">Remote gateway</div>';\n if (remoteR.ok) {\n var rs = remoteR.data;\n html += '<div class=\"card\" data-card-id=\"remote\"><div class=\"body\"><dl>'\n + row(\"Enabled\", rs.enabled ? \"Yes\" : \"No\")\n + (rs.provider ? row(\"Provider\", escHtml(rs.provider)) : \"\")\n + (rs.publicUrl ? row(\"Public URL\", escHtml(rs.publicUrl)) : \"\")\n + (rs.pid != null ? row(\"PID\", String(rs.pid)) : \"\")\n + (rs.createdAt ? row(\"Created\", fmtRelative(rs.createdAt)) : \"\")\n + (rs.lastError ? row(\"Last error\", escHtml(rs.lastError)) : \"\")\n + '</dl>'\n + '<div class=\"wallet-card-actions\">'\n + (rs.enabled\n ? '<button type=\"button\" class=\"btn-sm danger\" id=\"remote-disable-btn\">disable</button>'\n : '<button type=\"button\" class=\"btn-sm primary\" id=\"remote-enable-btn\">enable</button>')\n + '</div>';\n if (remoteReveal) {\n html += '<div class=\"reveal-box\">'\n + '<div>Shown once. Store it now, it will not be shown again.</div>'\n + (remoteReveal.bearerToken ? '<div>Bearer token<code>' + escHtml(remoteReveal.bearerToken) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"bearer\">copy token</button></div>' : '')\n + (remoteReveal.phoneUrl ? '<div>Phone link<code>' + escHtml(remoteReveal.phoneUrl) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"phoneUrl\">copy link</button></div>' : '')\n + (remoteReveal.mcpConfigSnippet ? '<div>.mcp.json<code>' + escHtml(remoteReveal.mcpConfigSnippet) + '</code><button type=\"button\" class=\"btn-sm\" data-copy=\"snippet\">copy snippet</button></div>' : '')\n + '</div>';\n }\n html += '</div></div>';\n } else {\n html += '<div class=\"muted-note\">remote_status: ' + escHtml(remoteR.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Pairing<button type=\"button\" class=\"btn-sm\" id=\"pair-offer-btn\">+ new pairing</button></div>';\n if (pairReveal) {\n html += '<div class=\"reveal-box\">'\n + '<div>Offer URL. Shown once, expires <span id=\"pair-offer-countdown\"></span>.</div>'\n + '<code>' + escHtml(pairReveal.url) + '</code>'\n + '<button type=\"button\" class=\"btn-sm\" data-copy=\"pairOffer\">copy URL</button>'\n + '<div class=\"muted\">fingerprint ' + escHtml(pairReveal.fingerprint) + '</div>'\n + '</div>';\n }\n if (pairR.ok) {\n var pairings = (pairR.data && pairR.data.pairings) || [];\n html += '<div class=\"card\" data-card-id=\"pairing\"><div class=\"body\">';\n if (pairings.length === 0) {\n html += '<div class=\"empty\">No paired clients.</div>';\n } else {\n html += '<table><thead><tr><th>Name</th><th>Fingerprint</th><th>Created</th><th>Last seen</th><th>Rendezvous</th><th></th></tr></thead><tbody>';\n for (var i = 0; i < pairings.length; i++) {\n var p = pairings[i];\n html += '<tr data-row-id=\"' + escHtml(p.fingerprint) + '\"><td>' + escHtml(p.name) + '</td><td>' + escHtml(p.fingerprint) + '</td>'\n + '<td>' + escHtml(fmtRelative(p.createdAt)) + '</td><td>' + escHtml(fmtRelative(p.lastSeen)) + '</td><td>' + escHtml(p.rendezvous || \"\") + '</td>'\n + '<td><button type=\"button\" class=\"btn-sm danger\" data-pair-revoke=\"' + escHtml(p.fingerprint) + '\">revoke</button></td></tr>';\n }\n html += '</tbody></table>';\n }\n html += '</div></div>';\n } else {\n html += '<div class=\"muted-note\">pair_list: ' + escHtml(pairR.message) + '</div>';\n }\n\n html += '<div class=\"sect-h\">Tunnels</div>';\n if (tunnelR.ok) {\n var tunnels = (tunnelR.data && tunnelR.data.tunnels) || [];\n if (tunnels.length === 0) {\n html += '<div class=\"empty\">No tunnels tracked by this daemon.</div>';\n } else {\n html += '<div class=\"card\"><div class=\"body\"><table><thead><tr><th>Name</th><th>Provider</th><th>Port</th><th>URL</th><th>Status</th></tr></thead><tbody>';\n for (var t = 0; t < tunnels.length; t++) {\n var tn = tunnels[t];\n html += '<tr><td>' + escHtml(tn.label || tn.name) + '</td><td>' + escHtml(tn.provider) + '</td><td>' + escHtml(String(tn.targetPort)) + '</td>'\n + '<td>' + escHtml(tn.publicUrl || \"\") + '</td><td>' + escHtml(tn.status) + '</td></tr>';\n }\n html += '</tbody></table></div></div>';\n }\n } else {\n html += '<div class=\"muted-note\">tunnel_list: ' + escHtml(tunnelR.message) + '</div>';\n }\n\n el.innerHTML = html;\n wireRemote(el);\n if (remoteReveal) {\n wireCopyButtons(el, { bearer: remoteReveal.bearerToken, phoneUrl: remoteReveal.phoneUrl, snippet: remoteReveal.mcpConfigSnippet });\n }\n if (pairReveal) {\n wireCopyButtons(el, { pairOffer: pairReveal.url });\n startPairCountdown(pairReveal.expiresAt);\n }\n}\n\nfunction wireRemote(container) {\n var enableBtn = container.querySelector(\"#remote-enable-btn\");\n if (enableBtn) {\n enableBtn.addEventListener(\"click\", function () {\n confirmClick(\"remote-enable\", enableBtn, function () {\n loadTool(\"remote_enable\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n remoteReveal = { bearerToken: r.data.bearerToken, phoneUrl: r.data.phoneUrl, mcpConfigSnippet: r.data.mcpConfigSnippet };\n toast(\"Remote enabled.\", false);\n loadRemote();\n });\n });\n });\n }\n var disableBtn = container.querySelector(\"#remote-disable-btn\");\n if (disableBtn) {\n disableBtn.addEventListener(\"click\", function () {\n confirmClick(\"remote-disable\", disableBtn, function () {\n loadTool(\"remote_disable\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n remoteReveal = null;\n toast(\"Remote disabled.\", false);\n loadRemote();\n });\n });\n });\n }\n var offerBtn = container.querySelector(\"#pair-offer-btn\");\n if (offerBtn) {\n offerBtn.addEventListener(\"click\", function () {\n loadTool(\"pair_offer\", {}).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n pairReveal = { url: r.data.url, fingerprint: r.data.fingerprint, expiresAt: r.data.expiresAt };\n toast(\"Pairing offer created.\", false);\n loadRemote();\n });\n });\n }\n var revokeBtns = container.querySelectorAll(\"[data-pair-revoke]\");\n for (var i = 0; i < revokeBtns.length; i++) {\n (function (btn) {\n var fp = btn.getAttribute(\"data-pair-revoke\");\n btn.addEventListener(\"click\", function () {\n confirmClick(\"pair-revoke-\" + fp, btn, function () {\n loadTool(\"pair_revoke\", { target: fp }).then(function (r) {\n if (!r.ok) { toast(r.message, true); return; }\n toast(\"Pairing revoked.\", false);\n pairReveal = null;\n loadRemote();\n });\n });\n });\n })(revokeBtns[i]);\n }\n}\n\n// ============================================================\n// Advanced\n// ============================================================\n\nfunction loadAdvanced() {\n return Promise.all([\n loadTool(\"config_get\", {}),\n loadTool(\"daemon_health\", {}),\n ]).then(function (results) {\n renderAdvanced(results[0], results[1]);\n });\n}\n\nfunction renderAdvanced(configR, healthR) {\n var el = document.getElementById(\"sec-advanced\");\n var html = '<div class=\"sect-h\">Raw config dump</div>';\n if (configR.ok) {\n html += '<div class=\"card\"><div class=\"body\"><pre style=\"white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace\">' + escHtml(JSON.stringify(configR.data, null, 2)) + '</pre></div></div>';\n } else if (configR.unknown) {\n html += '<div class=\"notice\">Full config dump needs a newer agentproto daemon (config_get). See Defaults for the knobs daemon_health reports.</div>';\n } else {\n html += '<div class=\"muted-note\">config_get: ' + escHtml(configR.message) + '</div>';\n }\n html += '<div class=\"sect-h\">Daemon health</div>';\n if (healthR.ok) {\n html += '<div class=\"card\"><div class=\"body\"><pre style=\"white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace\">' + escHtml(JSON.stringify(healthR.data, null, 2)) + '</pre></div></div>';\n } else {\n html += '<div class=\"muted-note\">daemon_health: ' + escHtml(healthR.message) + '</div>';\n }\n el.innerHTML = html;\n}\n\n// ============================================================\n// boot\n// ============================================================\n\nfunction loadHealthChip() {\n return loadTool(\"daemon_health\", {}).then(function (r) {\n var chip = document.getElementById(\"daemon-chip\");\n if (r.ok) {\n chip.className = \"chip ok\";\n var v = r.data.version || (r.data.build && r.data.build.sha) || r.data.status || \"ok\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon \" + String(v).slice(0, 16);\n } else {\n chip.className = \"chip bad\";\n document.getElementById(\"daemon-txt\").textContent = \"daemon unreachable\";\n }\n });\n}\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadHealthChip();\n reloadCurrent().catch(function (e) { toast(String(e && e.message || e), true); });\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n updateModelContext = bridge.updateModelContext;\n if (bridge.onTeardown) {\n onTeardownFn = bridge.onTeardown;\n onTeardownFn(function () { clearRemoteSecrets(); });\n }\n renderNav();\n loadHealthChip();\n if (bridge.onToolInput) bridge.onToolInput(handleToolInput);\n route(true);\n })\n .catch(function (err) {\n document.getElementById(\"body\").innerHTML =\n '<div class=\"empty\">Standalone mode: no host bridge (' + escHtml(err.message) + ')</div>';\n });\n</script>\n</body>\n</html>`\n","/**\n * `@agentproto/config` — daemon-wide configuration as its own standalone app.\n *\n * A separate app from `ops-panel` (not a section of it): ops-panel is the\n * daemon operations cockpit (sessions, crons, housekeeping); this app is the\n * visual twin of `agentproto config` — wallets (auth profiles + spend),\n * harnesses, the model catalog, defaults/messaging knobs, remote/pairing,\n * and a raw config escape hatch. `ops-panel` is referenced only for its app\n * mechanics (`defineApp`, self-contained panel HTML, `app_tool_call` gated\n * by a `ui.tools` allowlist) — see `ui.ts`.\n *\n * `agents: []`: UI only, no durable sessions to launch. Edit flows live\n * behind explicit confirm/save actions in `ui.ts`; every mutating tool is\n * feature-detected the same way the read tools already are, so this app\n * works against a daemon that predates a given write tool (it just shows a\n * muted \"needs a newer agentproto daemon\" note for that one control).\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { CONFIG_HTML, CONFIG_TOOLS } from \"./ui.js\"\n\nexport const configApp: AppHandle = defineApp({\n id: \"@agentproto/config\",\n name: \"agentproto config\",\n version: \"0.1.0\",\n description:\n \"Daemon-wide configuration: wallets (auth profiles + spend), harnesses, \" +\n \"the model catalog, defaults & messaging knobs, remote & pairing, and a raw config dump.\",\n agents: [],\n ui: {\n html: CONFIG_HTML,\n title: \"agentproto config\",\n tools: [...CONFIG_TOOLS],\n },\n})\n\nexport { CONFIG_TOOLS } from \"./ui.js\"\nexport { parseConfigFragment, buildConfigFragment, CONFIG_SECTIONS } from \"./fragment.js\"\nexport type { ConfigSection, ParsedConfigFragment } from \"./fragment.js\"\n","import { defineAgent } from \"@agentproto/agent\"\nimport type { AgentEntry } from \"@agentproto/app-kit\"\n\nexport const narrator: AgentEntry = {\n agent: defineAgent({\n schema: \"agent/v1\",\n id: \"@agentproto/session-narrator\",\n description:\n \"Reads a daemon session's transcript and narrates it in plain English — what happened, what was decided, what's left.\",\n model: \"claude-sonnet-5\",\n boundaries: [\n \"Read-only — never spawn, kill, resume, or write to any session\",\n \"Narrate only what the transcript actually shows, never speculate about intent\",\n ],\n tools: [\"conversation_read\", \"session_list\"],\n workflows: [{ ref: \"narrate-session\" }],\n }),\n body: [\n \"You narrate a daemon session's conversation for a human who doesn't want to read the raw transcript.\",\n \"\",\n \"Given a session id or name, call `conversation_read` with `format: \\\"json\\\"` to get the\",\n \"structured turn list (`meta` + `messages`, each `{ role, text, toolCalls?, ts }`). If the\",\n \"session id is ambiguous or unknown, call `session_list` first to resolve it by label.\",\n \"\",\n \"Write a short plain-English narration:\",\n \"- What the user asked for.\",\n \"- What the assistant did, grouped into a few natural beats (not a line-by-line tool log).\",\n \"- Any open question, decision point, or unfinished step at the end of the transcript.\",\n \"\",\n \"Skip tool-call plumbing details unless they're the point (e.g. a failing command, a real\",\n \"decision). If `conversation.conversation` comes back null, say plainly that this session has\",\n \"no readable conversation (e.g. a bare shell PTY) instead of guessing.\",\n ].join(\"\\n\"),\n}\n","import { defineWorkflow } from \"@agentproto/workflow\"\n\nexport const narrateSession = defineWorkflow({\n id: \"narrate-session\",\n name: \"Narrate Session\",\n description: \"Read a daemon session's transcript and narrate it in plain English.\",\n version: \"0.1.0\",\n inputs: {},\n outputs: {},\n steps: [\n {\n id: \"narrate\",\n kind: \"agent\",\n agent: { ref: \"@agentproto/session-narrator\" },\n prompt:\n \"Narrate the session named in the run input (or the most recently started session if \" +\n \"none was given): what the user asked for, what the assistant did, and what — if \" +\n \"anything — is still open at the end of the transcript.\",\n },\n ],\n})\n","/**\n * Session Viewer — a self-contained MCP App panel for `session-viewer`.\n *\n * Talks to the daemon only through the `window.McpApp.connect()` bridge\n * (never a direct `fetch`): every action round-trips through the app's own\n * `app_tool_call` gateway tool, `{ appId, tool, args }` — same convention as\n * `media-viewer`/`mail-triage`. Read-only by design: `session_list` picks a\n * session (live or finished), `conversation_read` renders it as a clean\n * conversation — no spawn/kill/resume action lives here.\n *\n * Live sessions are polled (`conversation_read` has no push channel over\n * this bridge — see the PR's feasibility note on a real event/PTY stream);\n * a finished session is fetched once and left alone.\n */\n\nexport const SESSION_VIEWER_TOOLS = [\"session_list\", \"conversation_read\"] as const\n\nconst APP_ID = \"@agentproto/session-viewer\"\n\nexport const SESSION_VIEWER_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Viewer</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}\n#app{display:flex;height:100%;overflow:hidden}\n\n/* sidebar */\n#sidebar{width:250px;min-width:190px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}\n#sidebar-hdr h1{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;flex:1}\n#archived-toggle{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--text2);padding:6px 12px;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}\n#archived-toggle input{cursor:pointer}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:14px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.si .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.si .sm{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:5px;align-items:center;flex-wrap:wrap}\n.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.02em}\n.b-run{background:rgba(63,185,80,.15);color:var(--green)}\n.b-wait{background:rgba(88,166,255,.15);color:var(--blue)}\n.b-idle{background:rgba(139,148,158,.12);color:var(--text2)}\n.b-end{background:rgba(139,148,158,.1);color:var(--text3)}\n.b-err{background:rgba(248,81,73,.15);color:var(--red)}\n#empty-sidebar{padding:14px 12px;color:var(--text2);font-size:11.5px}\n\n/* main */\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#header{padding:12px 18px;border-bottom:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n#header .htitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}\n.pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex:none;box-shadow:0 0 0 0 rgba(63,185,80,.4);animation:pulse 2s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 6px rgba(63,185,80,0)}100%{box-shadow:0 0 0 0 rgba(63,185,80,0)}}\n#header .hfacts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}\n.fact{font-size:10.5px;font-weight:600;background:var(--bg3);border:1px solid var(--border);color:var(--text2);padding:3px 9px;border-radius:999px}\n#empty-main,#loading-main,#error-main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;text-align:center;padding:24px}\n#empty-main .ico,#loading-main .ico{font-size:30px}\n#error-main{color:var(--red)}\n\n/* transcript */\n#transcript{flex:1;overflow-y:auto;padding:14px 18px;display:flex;flex-direction:column;gap:10px}\n.msg{display:flex;flex-direction:column;max-width:78%}\n.msg.role-user{align-self:flex-end;align-items:flex-end}\n.msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}\n.msg.role-system{align-self:center;max-width:90%}\n.msg.role-message{align-self:flex-start;align-items:flex-start}\n.bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}\n.role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}\n.role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}\n.role-message .bubble{background:var(--bg2);border:1px dashed var(--blue);border-bottom-left-radius:3px}\n.role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}\n.mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}\n.mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}\n.mtime{font-size:10px;color:var(--text3);font-variant-numeric:tabular-nums}\n\n.chip{margin-top:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg3);overflow:hidden;cursor:pointer;width:100%}\n.chip .ch{padding:6px 10px;font-size:11.5px;display:flex;align-items:center;gap:7px;color:var(--text2)}\n.chip .ch .ci{color:var(--yellow);flex:none}\n.chip.tool-result .ch .ci{color:var(--purple)}\n.chip .ch .cname{font-weight:600;color:var(--text)}\n.chip .ch .cprev{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text3)}\n.chip .ch .cchev{flex:none;transition:transform .12s;color:var(--text3);font-size:10px}\n.chip.open .ch .cchev{transform:rotate(90deg)}\n.chip pre{display:none;margin:0;border-top:1px solid var(--border);padding:9px 11px;font-family:Menlo,Monaco,'Courier New',monospace;font-size:11px;line-height:1.5;color:var(--text2);white-space:pre-wrap;word-break:break-word;max-height:320px;overflow:auto}\n.chip.open pre{display:block}\n.reasoning{margin-bottom:6px;font-size:11.5px;font-style:italic;color:var(--text2);border-left:2px solid var(--border);padding-left:8px}\n\n#statusbar{padding:5px 18px;font-size:10.5px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0;display:flex;justify-content:space-between}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" type=\"button\">↻</button>\n </div>\n <label id=\"archived-toggle\"><input type=\"checkbox\" id=\"archived-check\"> show archived</label>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"header\" style=\"display:none\"></div>\n <div id=\"empty-main\"><div class=\"ico\">💬</div><div>Pick a session to read its conversation</div></div>\n <div id=\"transcript\" style=\"display:none\"></div>\n <div id=\"statusbar\" style=\"display:none\">\n <span id=\"status-left\"></span>\n <span id=\"status-right\"></span>\n </div>\n </div>\n</div>\n<script>\nvar callTool = null;\nvar APP_ID = ${JSON.stringify(APP_ID)};\n\n// app_tool_call wraps its dispatch result at least once (its own text-result\n// body is the JSON-stringified inner MCP tool result); peel back through\n// nested {content:[{type:\"text\",text:...}]} shells until the payload stops\n// looking like one — works whichever layer the host's bridge already\n// unwrapped. With conversation_read's format:\"json\" this resolves straight\n// to the { meta, messages } object; with session_list, straight to\n// { sessions: [...] }.\nfunction unwrapText(result) {\n var cur = result;\n for (var i = 0; i < 4; i++) {\n if (typeof cur === \"string\") {\n try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }\n }\n if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === \"string\") {\n cur = cur.content[0].text;\n continue;\n }\n break;\n }\n return cur;\n}\n\nfunction callApp(tool, args) {\n return callTool(\"app_tool_call\", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);\n}\n\nfunction escHtml(s) {\n return String(s == null ? \"\" : s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nfunction fmtTime(ts) {\n if (!ts) return \"\";\n try { return new Date(ts).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }); }\n catch (e) { return \"\"; }\n}\n\nfunction fmtClock(iso) {\n if (!iso) return \"\";\n try { return new Date(iso).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" }); }\n catch (e) { return \"\"; }\n}\n\nfunction fmtRelative(iso) {\n if (!iso) return \"\";\n var ms = Date.now() - new Date(iso).getTime();\n if (!isFinite(ms)) return \"\";\n var s = Math.round(ms / 1000);\n if (s < 5) return \"just now\";\n if (s < 60) return s + \"s ago\";\n var m = Math.round(s / 60);\n if (m < 60) return m + \"m ago\";\n var h = Math.round(m / 60);\n if (h < 24) return h + \"h ago\";\n return Math.round(h / 24) + \"d ago\";\n}\n\nfunction fmtDuration(startIso, endIso) {\n var end = endIso ? new Date(endIso).getTime() : Date.now();\n var start = new Date(startIso).getTime();\n if (!isFinite(start) || !isFinite(end)) return \"\";\n var s = Math.max(0, Math.round((end - start) / 1000));\n if (s < 60) return s + \"s\";\n var m = Math.floor(s / 60);\n if (m < 60) return m + \"m \" + (s % 60) + \"s\";\n var h = Math.floor(m / 60);\n return h + \"h \" + (m % 60) + \"m\";\n}\n\nfunction prettyArgs(raw) {\n if (raw == null) return \"\";\n if (typeof raw !== \"string\") return JSON.stringify(raw, null, 2);\n try { return JSON.stringify(JSON.parse(raw), null, 2); } catch (e) { return raw; }\n}\n\nfunction firstLine(s, max) {\n var t = String(s == null ? \"\" : s).split(\"\\\\n\")[0];\n return t.length > max ? t.slice(0, max) + \"\\\\u2026\" : t;\n}\n\n// ============================================================\n// state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar includeArchived = false;\nvar currentMeta = null;\nvar lastMessageCount = -1;\nvar sidebarTimer = null;\nvar convoTimer = null;\n\nfunction findSession(id) {\n for (var i = 0; i < sessions.length; i++) if (sessions[i].id === id) return sessions[i];\n return null;\n}\n\nfunction isLive(s) {\n return !!s && (s.status === \"running\" || s.status === \"starting\");\n}\n\n// ============================================================\n// sidebar\n// ============================================================\n\nfunction displayBadge(s) {\n if (s.status === \"running\") {\n if (s.busy) return { label: \"working\", cls: \"b-run\" };\n if (s.awaitingInput) return { label: \"waiting\", cls: \"b-wait\" };\n return { label: \"idle\", cls: \"b-idle\" };\n }\n if (s.status === \"starting\") return { label: \"starting\", cls: \"b-wait\" };\n if (s.status === \"error\" || s.status === \"killed\") return { label: s.status, cls: \"b-err\" };\n return { label: s.status || \"exited\", cls: \"b-end\" };\n}\n\nfunction renderSidebar() {\n var el = document.getElementById(\"session-list\");\n if (sessions.length === 0) {\n el.innerHTML = '<div id=\"empty-sidebar\">No sessions yet.</div>';\n return;\n }\n var html = \"\";\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.title || s.name || (s.command ? String(s.command).split(\"/\").pop() : null) || s.id.slice(0, 10);\n var db = displayBadge(s);\n var active = s.id === activeId ? \" active\" : \"\";\n html += '<div class=\"si' + active + '\" data-id=\"' + escHtml(s.id) + '\">'\n + '<div class=\"sn\">' + escHtml(label) + \"</div>\"\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + escHtml(db.label) + \"</span>\"\n + \"<span>\" + escHtml(s.kind || \"\") + \"</span>\"\n + \"<span>\" + escHtml(fmtRelative(s.startedAt)) + \"</span>\"\n + \"</div></div>\";\n }\n el.innerHTML = html;\n var rows = el.querySelectorAll(\".si\");\n for (var j = 0; j < rows.length; j++) {\n rows[j].addEventListener(\"click\", (function (id) { return function () { selectSession(id); }; })(rows[j].getAttribute(\"data-id\")));\n }\n}\n\nfunction loadSessions() {\n return callApp(\"session_list\", { kind: \"all\", includeArchived: includeArchived }).then(function (data) {\n sessions = (data && data.sessions) || [];\n renderSidebar();\n if (activeId) {\n var hdr = document.getElementById(\"header\");\n if (hdr.style.display !== \"none\") renderHeader();\n }\n }).catch(function (e) {\n document.getElementById(\"status-left\").textContent = \"sessions: \" + e.message;\n });\n}\n\n// ============================================================\n// header\n// ============================================================\n\nfunction renderHeader() {\n var s = findSession(activeId);\n var meta = currentMeta || {};\n var el = document.getElementById(\"header\");\n el.style.display = \"block\";\n\n var title = meta.title || (s && (s.label || s.title)) || activeId;\n var live = s && isLive(s);\n var pulse = live ? '<span class=\"pulse\"></span>' : \"\";\n\n var facts = [];\n if (meta.model || (s && s.model)) facts.push(escHtml(meta.model || (s && s.model)));\n if (s && s.adapterSlug) facts.push(escHtml(s.adapterSlug));\n if (s && s.kind) facts.push(escHtml(s.kind));\n if (s) { var db = displayBadge(s); facts.push(escHtml(db.label)); }\n if (s && s.cwd) facts.push('<span title=\"' + escHtml(s.cwd) + '\">' + escHtml(s.cwd.length > 46 ? \"\\\\u2026\" + s.cwd.slice(-44) : s.cwd) + \"</span>\");\n var started = (meta.startedAt || (s && s.startedAt));\n if (started) facts.push(\"started \" + escHtml(fmtClock(started)) + \" (\" + escHtml(fmtRelative(started)) + \")\");\n var ended = meta.endedAt || (s && s.endedAt);\n if (ended) facts.push(\"ran \" + escHtml(fmtDuration(started, ended)));\n else if (started) facts.push(\"running \" + escHtml(fmtDuration(started)));\n if (meta.messageCount != null) facts.push(meta.messageCount + \" messages\");\n if (meta.toolCallCount != null) facts.push(meta.toolCallCount + \" tool calls\");\n if (meta.tokens && (meta.tokens.input != null || meta.tokens.output != null)) {\n facts.push(\"tokens \" + (meta.tokens.input || 0) + \"\\\\u2192\" + (meta.tokens.output || 0));\n }\n if (meta.costUsd != null) facts.push(\"$\" + Number(meta.costUsd).toFixed(4));\n\n el.innerHTML = '<div class=\"htitle\">' + pulse + escHtml(title) + \"</div>\"\n + '<div class=\"hfacts\">' + facts.map(function (f) { return '<span class=\"fact\">' + f + \"</span>\"; }).join(\"\") + \"</div>\";\n}\n\n// ============================================================\n// transcript\n// ============================================================\n\nfunction renderToolCallChip(tc) {\n var args = prettyArgs(tc.args);\n return '<div class=\"chip tool-call\"><div class=\"ch\"><span class=\"ci\">▸</span>'\n + '<span class=\"cname\">' + escHtml(tc.name || \"tool\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(args, 64)) + \"</span>\"\n + '<span class=\"cchev\">▸</span></div><pre>' + escHtml(args) + \"</pre></div>\";\n}\n\nfunction renderToolResultChip(m) {\n var text = m.text || \"\";\n return '<div class=\"chip tool-result\"><div class=\"ch\"><span class=\"ci\">🔧</span>'\n + '<span class=\"cname\">' + escHtml(m.toolName || \"result\") + \"</span>\"\n + '<span class=\"cprev\">' + escHtml(firstLine(text, 64)) + \"</span>\"\n + '<span class=\"cchev\">▸</span></div><pre>' + escHtml(text) + \"</pre></div>\";\n}\n\nfunction renderMessage(m) {\n var time = m.ts ? '<span class=\"mtime\">' + fmtTime(m.ts) + \"</span>\" : \"\";\n if (m.role === \"system\") {\n return '<div class=\"msg role-system\"><div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n if (m.role === \"tool\") {\n return '<div class=\"msg role-tool\">' + renderToolResultChip(m) + \"</div>\";\n }\n if (m.role === \"user\" && m.from) {\n // A typed message from another session (session-message record) — never\n // the human: name the daemon-attested sender instead of \"You\".\n var who = m.from.label || (m.from.sessionId ? m.from.sessionId.slice(-6) : m.from.relation);\n return '<div class=\"msg role-message\"><div class=\"mrow\"><span class=\"mrole\">from ' + escHtml(m.from.relation)\n + \" \" + escHtml(who) + \" · \" + escHtml(m.from.kind) + \"</span>\" + time + \"</div>\"\n + '<div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n if (m.role === \"user\") {\n return '<div class=\"msg role-user\"><div class=\"mrow\"><span class=\"mrole\">You</span>' + time + \"</div>\"\n + '<div class=\"bubble\">' + escHtml(m.text || \"\") + \"</div></div>\";\n }\n // assistant\n var inner = \"\";\n if (m.reasoning) inner += '<div class=\"reasoning\">' + escHtml(m.reasoning) + \"</div>\";\n if (m.text) inner += escHtml(m.text);\n var body = inner ? '<div class=\"bubble\">' + inner + \"</div>\" : \"\";\n var chips = (m.toolCalls || []).map(renderToolCallChip).join(\"\");\n return '<div class=\"msg role-assistant\"><div class=\"mrow\"><span class=\"mrole\">Assistant</span>' + time + \"</div>\"\n + body + chips + \"</div>\";\n}\n\nfunction renderTranscript(messages) {\n var el = document.getElementById(\"transcript\");\n var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;\n el.style.display = \"flex\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n if (errEl) errEl.style.display = \"none\";\n if (messages.length === 0) {\n el.innerHTML = '<div style=\"color:var(--text2);text-align:center;padding:20px\">(empty conversation)</div>';\n return;\n }\n el.innerHTML = messages.map(renderMessage).join(\"\");\n if (atBottom) el.scrollTop = el.scrollHeight;\n\n var chips = el.querySelectorAll(\".chip\");\n for (var i = 0; i < chips.length; i++) {\n chips[i].querySelector(\".ch\").addEventListener(\"click\", (function (chip) {\n return function () { chip.classList.toggle(\"open\"); };\n })(chips[i]));\n }\n}\n\nfunction renderNoConversation(reason) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var el = document.getElementById(\"error-main\");\n el.style.display = \"flex\";\n el.innerHTML = '<div class=\"ico\">🚫</div><div>No readable conversation for this session'\n + (reason ? '<br><span style=\"font-size:11px;color:var(--text3)\">' + escHtml(reason) + \"</span>\" : \"\")\n + \"</div>\";\n}\n\n// ============================================================\n// selection + polling\n// ============================================================\n\nfunction clearConvoPoll() {\n if (convoTimer) { clearTimeout(convoTimer); convoTimer = null; }\n}\n\nfunction scheduleConvoPoll() {\n clearConvoPoll();\n var s = findSession(activeId);\n if (!isLive(s)) return;\n convoTimer = setTimeout(function () {\n loadConversation(activeId, { silent: true });\n }, 3500);\n}\n\nfunction loadConversation(id, opts) {\n opts = opts || {};\n var requestedId = id;\n if (!opts.silent) {\n document.getElementById(\"transcript\").style.display = \"none\";\n document.getElementById(\"empty-main\").style.display = \"none\";\n var errEl = document.getElementById(\"error-main\");\n errEl.style.display = \"flex\";\n errEl.innerHTML = '<div class=\"ico\">⌛</div><div>Loading\\\\u2026</div>';\n }\n document.getElementById(\"status-left\").textContent = \"loading\\\\u2026\";\n return callApp(\"conversation_read\", { idOrName: id, format: \"json\" }).then(function (data) {\n if (activeId !== requestedId) return; // switched away meanwhile\n if (!data || !Array.isArray(data.messages)) {\n currentMeta = null;\n renderHeader();\n renderNoConversation(data && data.reason);\n clearConvoPoll();\n document.getElementById(\"status-left\").textContent = \"no conversation\";\n return;\n }\n currentMeta = data.meta || {};\n lastMessageCount = data.messages.length;\n renderHeader();\n renderTranscript(data.messages);\n document.getElementById(\"status-left\").textContent = data.messages.length + \" messages\";\n document.getElementById(\"status-right\").textContent = \"updated \" + fmtTime(Date.now());\n scheduleConvoPoll();\n }).catch(function (e) {\n if (activeId !== requestedId) return;\n document.getElementById(\"status-left\").textContent = \"error: \" + e.message;\n if (!opts.silent) renderNoConversation(e.message);\n });\n}\n\nfunction selectSession(id) {\n if (activeId === id) return;\n activeId = id;\n currentMeta = null;\n lastMessageCount = -1;\n clearConvoPoll();\n renderSidebar();\n document.getElementById(\"statusbar\").style.display = \"flex\";\n renderHeader();\n loadConversation(id, {});\n}\n\n// ============================================================\n// boot\n// ============================================================\n\ndocument.getElementById(\"refresh-btn\").addEventListener(\"click\", function () {\n loadSessions();\n if (activeId) loadConversation(activeId, {});\n});\ndocument.getElementById(\"archived-check\").addEventListener(\"change\", function (e) {\n includeArchived = !!e.target.checked;\n loadSessions();\n});\n\nwindow.McpApp.connect()\n .then(function (bridge) {\n callTool = bridge.callTool;\n return loadSessions();\n })\n .then(function () {\n sidebarTimer = setInterval(loadSessions, 5000);\n if (!activeId && sessions.length > 0) selectSession(sessions[0].id);\n })\n .catch(function (err) {\n document.getElementById(\"empty-main\").innerHTML =\n '<div class=\"ico\">⚠</div><div>Standalone mode \\\\u2014 no host bridge (' + escHtml(err.message) + \")</div>\";\n });\n</script>\n</body>\n</html>`\n","/**\n * `session-viewer` — a single-agent agentproto app for reading daemon\n * sessions as a clean conversation.\n *\n * The panel (`ui.ts`) picks a session — live or finished — from\n * `session_list` and renders its transcript via `conversation_read`: user/\n * assistant turns, collapsible tool calls, timestamps, a metadata header.\n * Read-only, no spawn/kill/resume action. The bundled `session-narrator`\n * agent is a complementary plain-English narrator over the same\n * `conversation_read` data — usable standalone via `app_run`, independent\n * of the panel.\n */\n\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { narrator } from \"./agents/narrator.js\"\nimport { narrateSession } from \"./workflows/narrate-session.js\"\nimport { SESSION_VIEWER_HTML, SESSION_VIEWER_TOOLS } from \"./ui.js\"\n\nexport const sessionViewer: AppHandle = defineApp({\n id: \"@agentproto/session-viewer\",\n name: \"Session Viewer\",\n version: \"0.1.0\",\n description: \"Read a daemon session as a clean conversation — turns, tool calls, timestamps.\",\n agents: [narrator],\n workflows: [narrateSession],\n ui: {\n html: SESSION_VIEWER_HTML,\n title: \"Session Viewer\",\n tools: [...SESSION_VIEWER_TOOLS],\n },\n})\n\nexport { narrator, narrateSession }\n","/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also mounts two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes.\n * No auto-request: panels stay inline until the user clicks. The buttons\n * themselves are NOT implemented here — `@agentproto/app-client/display-mode`\n * is the one implementation, shared with the `window.McpApp` bridge injected\n * into installed apps (`packages/runtime/src/app-ui-apps.ts`); this module\n * only hands it `getHostContext`/`onHostContext`/`requestDisplayMode`. It\n * also leaves `window.AgentprotoUI.installDisplayMode` defined in the panel,\n * which is what a panel with its own header calls to place the toggle\n * inline instead (see that module's `mountToggle`).\n *\n * Standalone HTTP mode: `GET /apps/:appId/ui` (packages/runtime\n * http-server.ts) serves this exact script with NO postMessage host on the\n * other end of `window.parent` — it's a plain browser tab (or a VS Code\n * *HTTP-iframe* panel), so `window.parent === window` and nothing ever\n * answers `initBridge()`'s `ui/initialize` request, leaving every panel\n * stuck on \"Connecting to bridge…\" forever. `injectStandaloneAppBridge`\n * (app-ui-apps.ts) papers over exactly this for installed apps by injecting\n * `window.McpApp.connect() -> Promise<{callTool, ...}>`, a REST-backed\n * stand-in whose `callTool` POSTs to the sibling `./tool-call` route — but\n * this bridge never consumed `window.McpApp`, so builtin panels (the only\n * consumers of this module) were left hanging regardless.\n *\n * Fixed here by detecting that same standalone shape and short-circuiting\n * both `initBridge()` (resolves locally, no round trip) and `callTool()`\n * (routes through `window.McpApp.connect()` instead of `tools/call` over\n * postMessage). The detection requires BOTH `window.parent === window` (no\n * host to talk to — this is the actual condition standalone mode means) AND\n * `window.McpApp` being present with a `connect` function (the capability\n * this bridge needs from it): `window.McpApp` presence alone was considered\n * and rejected as the sole signal, even though it happens to also hold today\n * — the VS Code webview's `srcdoc` relay (packages/vscode\n * appPanel.ts/appPanelController.ts) answers this bridge's raw postMessage\n * JSON-RPC directly and never defines `window.McpApp` for a builtin panel\n * (only `createUiHtmlCache`'s `injectMcpAppBridge`, used for INSTALLED\n * apps' `ui.path` html, defines it) — but that's an accident of today's\n * wiring, not a guarantee; `window.parent === window` is the direct,\n * load-bearing signal (\"is there anyone to postMessage?\") and doesn't\n * depend on which injector happened to run. There is no host in standalone\n * mode, so `initBridge()` seeds a default `hostContext` of `{displayMode:\n * 'inline', availableDisplayModes: []}` — an empty `availableDisplayModes`\n * is what keeps the display-mode toggle buttons hidden (they only show for\n * modes `hostContext.availableDisplayModes` lists), rather than rendering\n * two buttons with nothing to switch to.\n *\n * The real postMessage-host path (a compliant MCP-Apps host, or the VS Code\n * srcdoc relay) is untouched byte-for-byte: same `rpcRequest`/`rpcNotify`\n * over `window.parent.postMessage`, same handshake, same display-mode\n * plumbing.\n *\n * Asymmetry not fixed here (flagged, not papered over): `window.McpApp`'s\n * `sendMessage`/`updateModelContext` (STANDALONE_REST_BRIDGE_SCRIPT,\n * app-ui-apps.ts) reject with \"no host (standalone mode)\", while\n * `appPanelController.ts`'s `ui/message`/`ui/update-model-context` handlers\n * accept-and-drop (`return {}`). This bridge doesn't currently expose either\n * call, so the difference is latent, but it's the last behavioural delta\n * between the two panel paths and worth a follow-up if this bridge ever\n * grows those methods.\n */\n\nimport { DISPLAY_MODE_SCRIPT_BODY } from \"@agentproto/app-client/display-mode\"\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nvar _hostCaps = null;\nvar _standaloneApp = null;\nfunction _isStandalone(){\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\n}\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\n// Panels that iframe the daemon's standalone app host carry this proof so\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\n// registerMcpApps replaces the placeholder with a real per-boot token when\n// serving the panel as a resource; every other render (tests, standalone\n// tab, VS Code HTTP-iframe panel — the latter two already pass\n// iframeEmbedAllowed's origin checks) keeps the literal, and\n// withEmbedToken() then degrades to an identity function.\nwindow.__AGENPROTO_EMBED_TOKEN__ = \"__AGENPROTO_EMBED_TOKEN__\";\nfunction withEmbedToken(url){\n var t = window.__AGENPROTO_EMBED_TOKEN__;\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\n}\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction getHostCapabilities(){ return _hostCaps; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n if (_isStandalone()){\n return window.McpApp.connect().then(function(conn){\n _standaloneApp = conn;\n // No host to advertise a hostContext — default to inline with no\n // other modes available, which keeps the display-mode toggle buttons\n // hidden (they only show for modes hostContext.availableDisplayModes\n // lists) instead of rendering two buttons with nothing to switch to.\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\n });\n }\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\n // \"Host supports opening external URLs\". Captured once, read via\n // getHostCapabilities() by panels that need to branch on it.\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction openLink(url){\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\n return rpcRequest('ui/open-link', {url: url});\n}\nfunction callTool(name, args){\n var raw = _standaloneApp\n ? _standaloneApp.callTool(name, args || {})\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\n return raw.then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\n// The installer is inlined verbatim (it is self-contained and idempotent),\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\n// panel stays inline until the user clicks, and the buttons only appear for\n// the modes the host advertises in hostContext.availableDisplayModes.\n${DISPLAY_MODE_SCRIPT_BODY}\nvar displayMode = window.AgentprotoUI.installDisplayMode({\n getHostContext: getHostContext,\n onHostContext: onHostContext,\n requestDisplayMode: requestDisplayMode\n});`\n}\n","/**\n * HTML bundle for the agentproto sessions panel — the `PANEL_HTML` served\n * as the `ui://agentproto_sessions/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_sessions` tool defined in ./index.ts.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → session_list + agent_output (3 s poll)\n * – Kill action: tools/call → agent_kill | terminal_kill\n * (goes through Claude's normal tool approval flow)\n *\n * The HTML bundle is fully autonomous (zero CDN, zero external deps).\n * ANSI-to-HTML renderer handles SGR codes (colors 0-15, bold, dim,\n * italic, underline, reset). CR-overwrite sequences are stripped so\n * progress bars don't corrupt the output.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle ─────────────────────────────────────────────────────────────\n\nexport const PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto sessions</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}\n#sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:15px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}\n.badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}\n.br{background:rgba(63,185,80,.15);color:var(--green)}\n.bs{background:rgba(88,166,255,.15);color:var(--blue)}\n.be{background:rgba(139,148,158,.1);color:var(--text2)}\n.bk{background:rgba(248,81,73,.1);color:var(--red)}\n.berr{background:rgba(248,81,73,.2);color:var(--red)}\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}\n#session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn:disabled{opacity:.4;cursor:default}\n.abtn.danger{border-color:var(--red);color:var(--red)}\n.abtn.danger:hover{background:rgba(248,81,73,.1)}\n#output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}\n.line{white-space:pre-wrap;word-break:break-all}\n#empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}\n#empty .ico{font-size:32px}\n#statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.errmsg{color:var(--red);padding:12px}\n/* ANSI SGR */\n.ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}\n.f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}\n.f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}\n.f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}\n.f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"toolbar\" style=\"display:none\">\n <span id=\"session-title\"></span>\n <button class=\"abtn danger\" id=\"kill-btn\" onclick=\"doKill()\">Kill</button>\n </div>\n <div id=\"output\">\n <div id=\"empty\"><div class=\"ico\">⚡</div><div>Select a session</div></div>\n </div>\n <div id=\"statusbar\">Connecting to bridge…</div>\n </div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-sessions-panel\")}\n\n// ============================================================\n// ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)\n// ============================================================\n\nfunction escHtml(s) {\n return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n}\n\nfunction ansiToHtml(raw) {\n // Strip CR overwrite sequences (progress bars: CR without LF)\n var text = raw.replace(/[^\\\\n]*\\\\r([^\\\\n])/g, '$1').replace(/\\\\r/g, '');\n var bold = false, dim = false, italic = false, underline = false, fg = -1;\n var out = '';\n // Split on ESC [ ... m (SGR) sequences; keep delimiters\n var ESC = '\u001b';\n var parts = text.split(/([\u001b]\\\\[[0-9;]*m)/);\n for (var pi = 0; pi < parts.length; pi++) {\n var part = parts[pi];\n if (part.length === 0) continue;\n if (part.charCodeAt(0) === 0x1b && part[1] === '[') {\n // Parse SGR parameters\n var inner = part.slice(2, part.length - 1); // strip ESC[ and m\n var codes = inner === '' ? [0] : inner.split(';').map(Number);\n for (var ci = 0; ci < codes.length; ci++) {\n var c = codes[ci];\n if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }\n else if (c === 1) bold = true;\n else if (c === 2) dim = true;\n else if (c === 3) italic = true;\n else if (c === 4) underline = true;\n else if (c === 22) { bold = false; dim = false; }\n else if (c === 23) italic = false;\n else if (c === 24) underline = false;\n else if (c >= 30 && c <= 37) fg = c - 30;\n else if (c === 39) fg = -1;\n else if (c >= 90 && c <= 97) fg = c - 90 + 8;\n }\n } else {\n // Text segment — strip any remaining non-printable ESC sequences\n var safe = escHtml(part).replace(/\u001b\\\\[[^m]*[A-Za-z]/g, '');\n if (safe === '') continue;\n var cls = '';\n if (bold) cls += ' ab';\n if (dim) cls += ' ad';\n if (italic) cls += ' ai';\n if (underline) cls += ' au';\n if (fg >= 0 && fg <= 15) cls += ' f' + fg;\n cls = cls.trim();\n out += cls ? '<span class=\"' + cls + '\">' + safe + '</span>' : safe;\n }\n }\n return out;\n}\n\n// ============================================================\n// App state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar outputLines = [];\nvar nextCursor = 0;\nvar pollTimer = null;\nvar pollActive = false;\n\nfunction setStatus(msg) {\n document.getElementById('statusbar').textContent = msg;\n}\n\n// ============================================================\n// Session list\n// ============================================================\n\nfunction badgeClass(status) {\n if (status === 'running') return 'br';\n if (status === 'starting') return 'bs';\n if (status === 'killed') return 'bk';\n if (status === 'error') return 'berr';\n return 'be'; // exited\n}\n\n// Derive a turn-aware display badge. A session's 'status' only tracks process\n// liveness, so an agent-cli process stays \"running\" while idle between turns.\n// Read 'busy' / 'awaitingInput' to show real activity instead:\n// working — a turn is in flight (busy)\n// waiting — turn ended on awaiting-input (needs a reply)\n// idle — process alive, no turn running (last turn done)\nfunction displayBadge(s) {\n if (s.status === 'running' && s.kind === 'agent-cli') {\n if (s.busy) return { label: 'working', cls: 'br' };\n if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };\n return { label: 'idle', cls: 'be' };\n }\n return { label: s.status, cls: badgeClass(s.status) };\n}\n\nfunction renderSidebar() {\n var el = document.getElementById('session-list');\n if (sessions.length === 0) {\n el.innerHTML = '<div style=\"padding:12px;color:var(--text2);font-size:11px\">No sessions</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);\n var db = displayBadge(s);\n var active = s.id === activeId ? ' active' : '';\n // blockedOn (set while the turn waits on a spawned sub-agent or a\n // shell command) rides next to the status badge; waiting-on-user is\n // NOT here — that's awaitingInput, a different signal.\n var blocked = '';\n if (s.blockedOn === 'subagent') blocked = '<span class=\"badge bs\">🧩 sous-agent</span>';\n else if (s.blockedOn === 'command') blocked = '<span class=\"badge bs\">⏳ commande</span>';\n else if (s.blockedOn === 'inbox') blocked = '<span class=\"badge bs\">📬 attend messages</span>';\n html += '<div class=\"si' + active + '\" onclick=\"selectSession(\\\\'' + s.id + '\\\\')\">'\n + '<div class=\"sn\">' + escHtml(label) + '</div>'\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + db.label + '</span>'\n + blocked\n + '<span>' + escHtml(s.kind || '') + '</span>'\n + '</div></div>';\n }\n el.innerHTML = html;\n}\n\nfunction loadSessions() {\n // {kind:'all'} is session_list's live-able default — agent-CLI + terminal/\n // PTY only. kind:\"command\" rows (a shell-execution log, not a resumable\n // session) are excluded unless includeCommands is explicitly passed.\n return callTool('session_list', {kind: 'all'}).then(function(data) {\n sessions = data.sessions || [];\n renderSidebar();\n setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' · ' + new Date().toLocaleTimeString());\n }).catch(function(e) {\n setStatus('Error: ' + e.message);\n });\n}\n\n// ============================================================\n// Output panel\n// ============================================================\n\nfunction renderOutputFull() {\n var el = document.getElementById('output');\n if (outputLines.length === 0) {\n el.innerHTML = '<div id=\"empty\"><div class=\"ico\">⚡</div><div>No output yet</div></div>';\n return;\n }\n var html = '';\n for (var i = 0; i < outputLines.length; i++) {\n html += '<div class=\"line\">' + ansiToHtml(outputLines[i]) + '</div>';\n }\n el.innerHTML = html;\n el.scrollTop = el.scrollHeight;\n}\n\nfunction appendLines(lines) {\n var el = document.getElementById('output');\n // Remove empty-state placeholder if present\n var empty = el.querySelector('#empty');\n if (empty) empty.parentNode.removeChild(empty);\n var frag = document.createDocumentFragment();\n for (var i = 0; i < lines.length; i++) {\n var div = document.createElement('div');\n div.className = 'line';\n div.innerHTML = ansiToHtml(lines[i]);\n frag.appendChild(div);\n }\n el.appendChild(frag);\n el.scrollTop = el.scrollHeight;\n}\n\nfunction selectSession(id) {\n activeId = id;\n outputLines = [];\n nextCursor = 0;\n renderSidebar();\n\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === id) { s = sessions[i]; break; }\n }\n\n var toolbar = document.getElementById('toolbar');\n toolbar.style.display = 'flex';\n document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);\n\n var killBtn = document.getElementById('kill-btn');\n killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');\n\n document.getElementById('output').innerHTML = '<div style=\"padding:12px;color:var(--text2)\">Loading…</div>';\n\n // Initial load\n callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n }).catch(function(e) {\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Error: ' + escHtml(e.message) + '</div>';\n });\n}\n\n// ============================================================\n// Kill\n// ============================================================\n\nfunction doKill() {\n if (!activeId) return;\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === activeId) { s = sessions[i]; break; }\n }\n var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';\n callTool(toolName, {sessionId: activeId}).then(function() {\n return doRefresh();\n }).catch(function(e) {\n setStatus('Kill failed: ' + e.message);\n });\n}\n\n// ============================================================\n// Poll\n// ============================================================\n\nfunction doPoll() {\n if (pollActive) return;\n pollActive = true;\n var p = loadSessions();\n if (activeId) {\n var capturedId = activeId;\n var capturedCursor = nextCursor;\n p = p.then(function() {\n return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});\n }).then(function(data) {\n if (capturedId !== activeId) return; // user switched sessions\n var newLines = data.lines || [];\n if (newLines.length > 0) {\n outputLines = outputLines.concat(newLines);\n if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);\n nextCursor = data.nextCursor || nextCursor;\n appendLines(newLines);\n }\n }).catch(function() {});\n }\n p.then(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n }).catch(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n });\n}\n\nfunction doRefresh() {\n return loadSessions().then(function() {\n if (activeId) {\n return callTool('agent_output', {sessionId: activeId, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n });\n }\n });\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n return loadSessions();\n}).then(function() {\n pollTimer = setTimeout(doPoll, 3000);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto sessions panel.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). The host (@agentproto/runtime)\n * wires this factory's output via `registerMcpApps` from its\n * mcp-apps-adapter.ts at boot time — see runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_sessions` is called by the host.\n * 2. execute() returns a sessions snapshot → injected into the HTML as\n * window.__APP_INIT__ (static initial render, no bridge needed).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list` + `agent_output` every 3 s for live data.\n *\n * This module also exports `sessionsPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeSessionsPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { PANEL_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const sessionsPanelInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\n \"Which sessions to return. `running` = only alive; `all` = running + recent (default).\",\n ),\n})\n\nexport type SessionsInput = z.infer<typeof sessionsPanelInputSchema>\nexport type SessionsOutput<TSession = unknown> = { sessions: TSession[] }\n\n/**\n * Generic over the host's own session-descriptor shape — this package\n * doesn't own that type (it lives in @agentproto/runtime's session\n * registry), so the factory stays agnostic to it and the host supplies the\n * concrete type as a type argument when it wires the app up.\n */\nexport interface SessionsPanelOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the sessions registry so execute() doesn't need\n * an AppContext (agentproto has no userId/guildId concept).\n */\nexport function makeSessionsPanelApp<TSession = unknown>(\n ops: SessionsPanelOps<TSession>,\n): AgnoMcpApp<SessionsInput, SessionsOutput<TSession>> {\n return {\n id: \"agentproto_sessions\",\n title: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all \" +\n \"running and recent agent-CLI and terminal/PTY sessions. Raw shell-\" +\n \"command runs are a log, not a resumable session, and don't appear \" +\n \"here — see `command_list`. The panel polls live data and lets you \" +\n \"inspect output or kill sessions.\",\n inputSchema: sessionsPanelInputSchema,\n execute: async (input) => ({\n sessions: ops.listSessions(input.filter),\n }),\n html: PANEL_HTML,\n }\n}\n\nexport const sessionsPanelApp: AppHandle = defineApp({\n id: \"@agentproto/sessions-panel\",\n name: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all running and recent \" +\n \"agent-CLI and terminal/PTY sessions.\",\n agents: [],\n ui: {\n html: PANEL_HTML,\n title: \"Agent Sessions\",\n tools: [\"session_list\", \"agent_output\"],\n },\n})\n","/**\n * HTML bundle for the agentproto \"agents — vue claire\" panel — the\n * `AGENTS_OVERVIEW_HTML` served as the `ui://agentproto_agents_overview/view`\n * resource. Mounted via the `agentproto_agents_overview` tool defined in\n * ./index.ts.\n *\n * The panel polls `session_list` every ~12s, then calls the host's\n * `summarize_session` tool (registered server-side by\n * @agentproto/runtime's summarize-session-tool.ts, NOT part of this\n * package — that tool is tied to the runtime's session ring-buffer/hot\n * path) for each visible session to fetch the plain sentence + state.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle (zero CDN, CSP-safe inline) ──────────────────────────────────\n\nexport const AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agents — vue claire</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}\n#hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}\n#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}\n.card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}\n.card.s-todo{border-left-color:var(--purple)}\n.card.s-work{border-left-color:var(--green)}\n.card.s-wait{border-left-color:var(--yellow)}\n.card.s-done{border-left-color:var(--text2)}\n.card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}\n.state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}\n.state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}\n.state.s-work{background:rgba(63,185,80,.16);color:var(--green)}\n.state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}\n.state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}\n.summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}\n.summary.muted{color:var(--text2);font-style:italic}\n.meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}\n.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}\n#empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}\n#empty .ico{font-size:30px}\n#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"hdr\">\n <div>\n <h1>Agents — vue claire</h1>\n <div class=\"sub\">une carte par agent · résumé + état</div>\n </div>\n <button id=\"refresh-btn\" title=\"Rafraîchir\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"grid\"><div id=\"empty\"><div class=\"ico\">⚡</div><div>Connexion au bridge…</div></div></div>\n <div id=\"statusbar\">Connexion…</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-agents-overview\")}\n\n// ── State ──\nvar REFRESH_MS = 12000;\nvar pollTimer = null, polling = false;\nfunction esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }\nfunction setStatus(m){ document.getElementById('statusbar').textContent = m; }\n\nvar STATE_CLASS = {\n 'à traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'terminé':'s-done'\n};\nfunction stateClass(st){ return STATE_CLASS[st] || 's-wait'; }\n\nfunction fmtAgo(iso){\n if (!iso) return '';\n var t = Date.parse(iso);\n if (isNaN(t)) return '';\n var s = Math.max(0, Math.round((Date.now() - t) / 1000));\n if (s < 60) return 'il y a ' + s + 's';\n var m = Math.round(s/60);\n if (m < 60) return 'il y a ' + m + 'min';\n var h = Math.round(m/60);\n return 'il y a ' + h + 'h';\n}\n\nfunction titleOf(s){\n return s.label || s.name || (s.command ? s.command.split(/\\\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);\n}\n\nfunction render(sessions, summaries){\n var grid = document.getElementById('grid');\n if (!sessions.length){\n grid.innerHTML = '<div id=\"empty\"><div class=\"ico\">😴</div><div>Aucune session d\\\\'agent</div></div>';\n return;\n }\n var html = '';\n for (var i=0;i<sessions.length;i++){\n var s = sessions[i];\n var sum = summaries[s.id] || {};\n var st = sum.state || 'en attente';\n var cls = stateClass(st);\n var summary = sum.summary || 'Résumé indisponible.';\n var muted = !sum.summary ? ' muted' : '';\n html += '<div class=\"card ' + cls + '\">'\n + '<div class=\"card-top\">'\n + '<span class=\"title\">' + esc(titleOf(s)) + '</span>'\n + '<span class=\"state ' + cls + '\">' + esc(st) + '</span>'\n + '</div>'\n + '<div class=\"summary' + muted + '\">' + esc(summary) + '</div>'\n + '<div class=\"meta\">'\n + '<span>' + esc(s.kind || '') + '</span><span class=\"dot\"></span>'\n + '<span>' + esc(s.status || '') + '</span>'\n + (s.lastOutputAt ? '<span class=\"dot\"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')\n + '</div>'\n + '</div>';\n }\n grid.innerHTML = html;\n}\n\nfunction loadAndRender(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n var all = data.sessions || [];\n var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });\n // Render shells immediately, then fill summaries as they arrive.\n var summaries = {};\n render(agents, summaries);\n setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' · ' + new Date().toLocaleTimeString('fr-FR'));\n return Promise.all(agents.map(function(s){\n return callTool('summarize_session', {sessionId: s.id}).then(function(sum){\n summaries[s.id] = sum;\n }).catch(function(){ /* leave shell */ });\n })).then(function(){ render(agents, summaries); });\n }).catch(function(e){ setStatus('Erreur : ' + e.message); });\n}\n\nfunction doPoll(){\n if (polling) return;\n polling = true;\n loadAndRender().then(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n }).catch(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n });\n}\nfunction doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }\n\ninitBridge().then(loadAndRender).then(function(){\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n}).catch(function(e){\n setStatus('Bridge : ' + e.message);\n document.getElementById('grid').innerHTML = '<div id=\"empty\"><div class=\"ico\">⚠</div><div>Échec connexion bridge : ' + esc(e.message) + '</div></div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"agents — vue claire\" panel.\n *\n * A plain-language overview of agent-cli sessions: one card per session\n * with a single human sentence (what the agent is doing/last said) plus a\n * coarse state — `à traiter` / `au travail` / `en attente` / `terminé`.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_agents_overview` is called by the host.\n * 2. execute() returns the agent-session snapshot → injected into the\n * HTML as the initial render via the tool result JSON.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list` every ~12 s, then calls `summarize_session` for\n * each visible session to fetch the plain sentence + state.\n *\n * The per-session summary is generated SERVER-SIDE by the\n * `summarize_session` tool, which stays in @agentproto/runtime\n * (summarize-session-tool.ts) — it's tied to the runtime's session\n * ring-buffer/hot path, not to this panel's UI. Only the panel factory +\n * HTML live here.\n *\n * This module also exports `agentsOverviewApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeAgentsOverviewApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { AGENTS_OVERVIEW_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const agentsOverviewInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\"`running` = only alive agent sessions; `all` = running + recent (default).\"),\n})\n\nexport type AgentsOverviewInput = z.infer<typeof agentsOverviewInputSchema>\nexport type AgentsOverviewOutput<TSession = unknown> = { sessions: TSession[] }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface AgentsOverviewOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeAgentsOverviewApp<TSession = unknown>(\n ops: AgentsOverviewOps<TSession>,\n): AgnoMcpApp<AgentsOverviewInput, AgentsOverviewOutput<TSession>> {\n return {\n id: \"agentproto_agents_overview\",\n title: \"Agents — vue claire\",\n description:\n \"Open the agents overview — a plain-language card per agent session \" +\n \"with one human sentence (what it's doing / last said) and a coarse \" +\n \"state (à traiter / au travail / en attente / terminé). Polls live and \" +\n \"asks the server to summarise each session.\",\n inputSchema: agentsOverviewInputSchema,\n execute: async input => ({ sessions: ops.listSessions(input.filter) }),\n html: AGENTS_OVERVIEW_HTML,\n }\n}\n\nexport const agentsOverviewApp: AppHandle = defineApp({\n id: \"@agentproto/agents-overview\",\n name: \"Agents — vue claire\",\n description:\n \"Open the agents overview — a plain-language card per agent session with one human sentence \" +\n \"and a coarse state (à traiter / au travail / en attente / terminé).\",\n agents: [],\n ui: {\n html: AGENTS_OVERVIEW_HTML,\n title: \"Agents — vue claire\",\n tools: [\"session_list\", \"summarize_session\"],\n },\n})\n","/**\n * HTML bundle for the agentproto \"bureau — sessions navigateur\" panel — the\n * `BUREAU_SESSIONS_HTML` served as the `ui://agentproto_bureau_sessions/view`\n * resource. Mounted via the `agentproto_bureau_sessions` tool defined in\n * ./index.ts.\n *\n * Data only — no server LLM. The whole bundle is inline (zero CDN).\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle (zero CDN, CSP-safe inline) ──────────────────────────────────\n\nexport const BUREAU_SESSIONS_HTML = `<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>bureau — sessions navigateur</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\n*{box-sizing:border-box}\nhtml,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;flex-direction:column;height:100%}\n#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}\n#hdr h1{font-size:13px;font-weight:600}\n#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#list{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}\n.row{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:11px 14px;display:flex;align-items:center;gap:14px}\n.icon{font-size:20px;flex-shrink:0}\n.body{flex:1;min-width:0}\n.r1{display:flex;align-items:center;gap:8px}\n.adapter{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.url{font-family:Menlo,Monaco,'Courier New',monospace;font-size:11.5px;color:var(--blue);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.meta{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}\n.badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap}\n.br{background:rgba(63,185,80,.16);color:var(--green)}\n.bs{background:rgba(88,166,255,.16);color:var(--blue)}\n.bk{background:rgba(248,81,73,.14);color:var(--red)}\n.be{background:rgba(139,148,158,.14);color:var(--text2)}\n.berr{background:rgba(248,81,73,.22);color:var(--red)}\n.port{font-family:Menlo,Monaco,'Courier New',monospace}\n#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}\n#empty .ico{font-size:30px}\n#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"hdr\">\n <div>\n <h1>Bureau — sessions navigateur</h1>\n <div class=\"sub\">services navigateur actifs · URL / port / statut</div>\n </div>\n <button id=\"refresh-btn\" title=\"Rafraîchir\" onclick=\"doRefresh()\">↻</button>\n </div>\n <div id=\"list\"><div id=\"empty\"><div class=\"ico\">🌐</div><div>Connexion au bridge…</div></div></div>\n <div id=\"statusbar\">Connexion…</div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-bureau-sessions\")}\n\n// ── State ──\nvar REFRESH_MS = 5000;\nvar pollTimer = null, polling = false;\nfunction esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }\nfunction setStatus(m){ document.getElementById('statusbar').textContent = m; }\n\nfunction badgeClass(st){\n if (st === 'running') return 'br';\n if (st === 'starting') return 'bs';\n if (st === 'killed') return 'bk';\n if (st === 'error') return 'berr';\n return 'be';\n}\nfunction fmtUptime(iso, end){\n if (!iso) return '';\n var start = Date.parse(iso);\n if (isNaN(start)) return '';\n var ref = end ? Date.parse(end) : Date.now();\n var s = Math.max(0, Math.round((ref - start) / 1000));\n if (s < 60) return s + 's';\n var m = Math.floor(s/60), rs = s%60;\n if (m < 60) return m + 'm ' + rs + 's';\n var h = Math.floor(m/60), rm = m%60;\n return h + 'h ' + rm + 'm';\n}\n\nfunction render(rows){\n var list = document.getElementById('list');\n if (!rows.length){\n list.innerHTML = '<div id=\"empty\"><div class=\"ico\">🌐</div><div>Aucune session navigateur</div></div>';\n return;\n }\n var html = '';\n for (var i=0;i<rows.length;i++){\n var s = rows[i];\n var bc = badgeClass(s.status);\n var url = s.browserBaseUrl || (s.browserPort ? ('http://127.0.0.1:' + s.browserPort) : '');\n var adapter = s.browserAdapterId || s.label || s.name || 'navigateur';\n var up = fmtUptime(s.startedAt, s.endedAt);\n html += '<div class=\"row\">'\n + '<div class=\"icon\">🌐</div>'\n + '<div class=\"body\">'\n + '<div class=\"r1\"><span class=\"adapter\">' + esc(adapter) + '</span>'\n + '<span class=\"badge ' + bc + '\">' + esc(s.status || '') + '</span></div>'\n + (url ? '<div class=\"url\">' + esc(url) + '</div>' : '')\n + '<div class=\"meta\">'\n + (s.browserPort ? '<span>port <span class=\"port\">' + esc(s.browserPort) + '</span></span><span class=\"dot\"></span>' : '')\n + '<span>' + (up ? 'uptime ' + esc(up) : '') + '</span>'\n + '</div>'\n + '</div>'\n + '</div>';\n }\n list.innerHTML = html;\n}\n\nfunction loadAndRender(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n var all = data.sessions || [];\n var browsers = all.filter(function(s){ return s.kind === 'browser'; });\n render(browsers);\n setStatus(browsers.length + ' session' + (browsers.length===1?'':'s') + ' · ' + new Date().toLocaleTimeString('fr-FR'));\n }).catch(function(e){ setStatus('Erreur : ' + e.message); });\n}\n\nfunction doPoll(){\n if (polling) return;\n polling = true;\n loadAndRender().then(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n }).catch(function(){\n polling = false;\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n });\n}\nfunction doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }\n\ninitBridge().then(loadAndRender).then(function(){\n pollTimer = setTimeout(doPoll, REFRESH_MS);\n}).catch(function(e){\n setStatus('Bridge : ' + e.message);\n document.getElementById('list').innerHTML = '<div id=\"empty\"><div class=\"ico\">⚠</div><div>Échec connexion bridge : ' + esc(e.message) + '</div></div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"bureau — sessions navigateur\" panel.\n *\n * Shows the browser sessions (kind === \"browser\"): adapter, base URL, port,\n * status and uptime. One row per live/recent browser service so you can see\n * what's bound where at a glance.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_bureau_sessions` is called by the host.\n * 2. execute() returns the browser-session snapshot.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list {kind:'all'}` every ~5 s, filtering to kind===\"browser\".\n *\n * Data only — no server LLM. The whole bundle is inline (zero CDN).\n *\n * This module also exports `bureauSessionsApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeBureauSessionsApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { BUREAU_SESSIONS_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const bureauSessionsInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\"`running` = only alive browser sessions; `all` = running + recent (default).\"),\n})\n\nexport type BureauSessionsInput = z.infer<typeof bureauSessionsInputSchema>\nexport type BureauSessionsOutput<TSession = unknown> = { sessions: TSession[] }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface BureauSessionsOps<TSession extends { kind: string }> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeBureauSessionsApp<TSession extends { kind: string }>(\n ops: BureauSessionsOps<TSession>,\n): AgnoMcpApp<BureauSessionsInput, BureauSessionsOutput<TSession>> {\n return {\n id: \"agentproto_bureau_sessions\",\n title: \"Bureau — sessions navigateur\",\n description:\n \"Open the browser-sessions panel — one row per browser service \" +\n \"(adapter, base URL, port, status, uptime). Polls live every ~5 s.\",\n inputSchema: bureauSessionsInputSchema,\n execute: async input => ({\n sessions: ops.listSessions(input.filter).filter(s => s.kind === \"browser\"),\n }),\n html: BUREAU_SESSIONS_HTML,\n }\n}\n\nexport const bureauSessionsApp: AppHandle = defineApp({\n id: \"@agentproto/bureau-sessions\",\n name: \"Bureau — sessions navigateur\",\n description:\n \"Open the browser-sessions panel — one row per browser service (adapter, base URL, port, \" +\n \"status, uptime).\",\n agents: [],\n ui: {\n html: BUREAU_SESSIONS_HTML,\n title: \"Bureau — sessions navigateur\",\n tools: [\"session_list\"],\n },\n})\n","/**\n * HTML bundle for the agentproto session story panel — the\n * `SESSION_STORY_PANEL_HTML` served as the `ui://agentproto_session_story/view`\n * resource. Mounted via the `agentproto_session_story` tool defined in\n * ./index.ts. Also consumed directly by packages/vscode's story webview\n * (`storyPanel.ts`), which relays this same HTML into a VS Code editor tab.\n *\n * Ported 1:1 from the validated mockup at docs/session-story-mockup.html —\n * same CSS, same DOM structure/classes, same interaction model (anchored\n * side panel, sticky collapsible chapter headers, Simple/Tech toggle,\n * composer with local chapter-routing classification). Fake data is\n * replaced with live data pulled through the MCP Apps postMessage bridge.\n *\n * The HTML must stay fully self-contained (CSP: inline CSS/JS only, no\n * CDN, no external fonts) — see runtime's mcp-apps-adapter.ts and the other\n * panels in this app family (sessions-panel, agents-overview) for the same\n * constraint. Because of that, this file cannot `import` the pure\n * `buildStory` module from @agentproto/runtime's session-story.ts (a\n * separate, CLI-facing heuristics module that stays in runtime — it isn't\n * a dependency of this panel) — the folding/chaptering heuristics are\n * ported to vanilla JS below (`buildStoryJs`), mirroring session-story.ts\n * function-for-function so the two stay easy to diff.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → session_list (status/picker) + agent_export\n * (timeline, re-fetched on turn boundaries) + agent_prompt (composer)\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport const SESSION_STORY_PANEL_HTML = `<!doctype html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>session story</title>\n<style>\n :root {\n color-scheme: light;\n --bg:#faf8f5; --panel:#fffdfa; --line:#ece5da; --line-soft:#f2ede3;\n --ink:#241f1a; --ink-mute:#7d7060; --ink-faint:#a9997f; --ink-ghost:#b3a893;\n --accent:#0d7a4f; --accent-soft:#e8f4ec;\n --gold:#a6701b; --gold-soft:#fff2dc;\n --blue:#1d4e80; --blue-soft:#e9f1fb;\n --violet:#5b3fa6; --violet-soft:#ede9ff;\n --sel:#fbeccd; --red:#b3261e; --red-soft:#fbeae8;\n }\n * { box-sizing:border-box; }\n html,body { height:100%; }\n body { margin:0; font-family:ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }\n .app { height:100vh; display:flex; flex-direction:column; }\n .hidden { display:none !important; }\n\n /* ── picker screen ── */\n #pickerScreen { height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }\n #pickerScreen h1 { font-size:15px; font-weight:700; }\n #pickerList { width:min(520px,90vw); max-height:60vh; overflow-y:auto; border:1px solid var(--line); border-radius:12px; background:var(--panel); }\n .pk-item { padding:10px 14px; border-bottom:1px solid var(--line-soft); cursor:pointer; display:flex; align-items:center; gap:10px; }\n .pk-item:last-child { border-bottom:none; }\n .pk-item:hover { background:var(--line-soft); }\n .pk-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .pk-meta { flex:none; font-size:10.5px; color:var(--ink-faint); font-weight:600; }\n .pk-empty { padding:24px; text-align:center; color:var(--ink-mute); font-size:12.5px; }\n .badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:10px; font-weight:700; }\n .badge.running { background:var(--accent-soft); color:var(--accent); }\n .badge.starting { background:var(--blue-soft); color:var(--blue); }\n .badge.exited { background:var(--line-soft); color:var(--ink-faint); }\n .badge.killed, .badge.error { background:var(--red-soft); color:var(--red); }\n\n /* ── big picture : mission + plan de sous-tâches ── */\n .hero { flex:none; padding:13px 20px 0; border-bottom:1px solid var(--line); background:var(--panel); }\n .hero-top { display:flex; align-items:center; gap:13px; }\n .pulse { width:10px; height:10px; border-radius:50%; background:var(--accent); flex:none;\n box-shadow:0 0 0 0 rgba(13,122,79,.35); animation:pulse 2.4s infinite; }\n .pulse.off { background:var(--ink-ghost); animation:none; }\n @keyframes pulse { 70% { box-shadow:0 0 0 9px rgba(13,122,79,0); } 100% { box-shadow:0 0 0 0 rgba(13,122,79,0); } }\n .who { min-width:0; flex:1; }\n .who .h1 { font-size:14.5px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .who .h2 { font-size:12px; color:var(--ink-mute); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .modewrap { display:flex; border:1px solid var(--line); border-radius:9px; overflow:hidden; flex:none; }\n .modewrap button { border:none; background:var(--panel); color:var(--ink-mute); font-size:11px; font-weight:700; padding:6px 11px; cursor:pointer; }\n .modewrap button.on { background:var(--ink); color:#fdf9f2; }\n button.sim, a.sim { border:1px solid var(--line); background:var(--panel); color:var(--ink-mute); font-weight:700;\n font-size:11.5px; border-radius:8px; padding:6px 12px; cursor:pointer; flex:none; }\n button.sim.on { background:var(--ink); border-color:var(--ink); color:#fdf9f2; }\n a.sim { display:inline-flex; align-items:center; text-decoration:none; }\n a.sim:hover { border-color:var(--ink-ghost); color:var(--ink); }\n\n /* plan strip : les sous-tâches, l'avancement d'un coup d'œil */\n .plan { display:flex; gap:6px; overflow-x:auto; padding:11px 0 12px; scrollbar-width:none; }\n .plan::-webkit-scrollbar { display:none; }\n .pt { flex:none; display:flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; padding:5px 11px;\n border-radius:999px; border:1px solid var(--line); background:var(--bg); color:var(--ink-mute); cursor:pointer; white-space:nowrap; }\n .pt:hover { border-color:var(--ink-ghost); }\n .pt .st { font-size:10px; }\n .pt.done { color:var(--accent); background:var(--accent-soft); border-color:transparent; }\n .pt.cur { color:var(--gold); background:var(--gold-soft); border-color:transparent; }\n .pt.cur .st { animation:blink 1.6s infinite; }\n @keyframes blink { 50% { opacity:.35; } }\n\n /* ── corps ── */\n .body { flex:1; display:flex; min-height:0; }\n .feedcol { flex:1; min-width:320px; display:flex; flex-direction:column; }\n .feed { flex:1; overflow-y:auto; padding:4px 14px 10px; display:flex; flex-direction:column; scroll-behavior:smooth; }\n .fspacer { flex:1; }\n\n /* chapitres */\n .chap { flex:none; position:sticky; top:0; z-index:5; margin:8px -4px 2px; padding:7px 12px; display:flex; align-items:center; gap:9px;\n background:color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter:blur(6px);\n border-radius:9px; cursor:pointer; font-size:11.5px; font-weight:800; letter-spacing:.03em; color:var(--ink-mute); }\n .chap:hover { color:var(--ink); }\n .chap .cst { flex:none; width:17px; height:17px; border-radius:50%; display:grid; place-items:center; font-size:9.5px; font-weight:900; }\n .chap.done .cst { background:var(--accent-soft); color:var(--accent); }\n .chap.cur .cst { background:var(--gold-soft); color:var(--gold); }\n .chap .cnum { color:var(--ink-ghost); font-weight:700; }\n .chap .csum { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .chap .cmeta { flex:none; font-size:10.5px; color:var(--ink-ghost); font-weight:600; }\n .chap .cchev { flex:none; color:var(--ink-ghost); transition:transform .15s; }\n .chap.open .cchev { transform:rotate(90deg); }\n\n .row { flex:none; min-height:44px; margin:1px 0 1px 10px; padding:5px 12px 5px 10px; display:flex; align-items:center; gap:11px;\n cursor:pointer; border-radius:10px; border-left:3px solid transparent; transition:background .12s; }\n .row:hover { background:var(--line-soft); }\n .row[aria-selected=\"true\"] { background:var(--sel); border-left-color:var(--gold); }\n .row .ico { width:24px; height:24px; border-radius:8px; flex:none; display:grid; place-items:center; font-size:11.5px; font-weight:800; }\n .ico.k-text { background:var(--blue-soft); color:var(--blue); }\n .ico.k-edit { background:var(--gold-soft); color:var(--gold); }\n .ico.k-bash { background:var(--accent-soft); color:var(--accent); }\n .ico.k-read { background:var(--violet-soft); color:var(--violet); }\n .ico.k-user { background:var(--ink); color:#fdf9f2; }\n .row .mid { flex:1; min-width:0; }\n .row .sum { display:block; font-size:13.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n .row .raw1 { display:block; font-size:10.5px; color:var(--ink-faint); font-family:ui-monospace,Menlo,monospace;\n white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }\n body:not(.tech) .row .raw1 { display:none; }\n .row .route { display:inline-block; font-size:10px; font-weight:800; padding:1px 8px; border-radius:999px; margin-top:2px; }\n .route.cont { background:var(--blue-soft); color:var(--blue); }\n .route.newt { background:var(--gold-soft); color:var(--gold); }\n .row .cnt { flex:none; font-size:10px; font-weight:800; color:var(--gold); background:var(--gold-soft); padding:2px 7px; border-radius:999px; }\n .row .ts { flex:none; font-size:10.5px; color:var(--ink-ghost); font-variant-numeric:tabular-nums; }\n @keyframes slidein { from { opacity:0; transform:translateY(6px); } }\n .row.new { animation:slidein .25s ease-out; }\n\n /* ── panneau ancré ── */\n .panel { flex:none; width:0; overflow:hidden; border-left:1px solid transparent; background:var(--panel);\n display:flex; flex-direction:column; transition:width .22s ease, border-color .22s; }\n .panel.open { width:min(430px,46vw); border-left-color:var(--line); }\n .panel-inner { width:min(430px,46vw); flex:1; display:flex; flex-direction:column; min-height:0; }\n .phead { flex:none; padding:14px 16px 12px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; gap:11px; }\n .phead .ico { width:28px; height:28px; font-size:13px; border-radius:9px; }\n .phead .tt { min-width:0; flex:1; }\n .phead .t { font-size:14px; font-weight:700; line-height:1.4; }\n .phead .s { font-size:11px; color:var(--ink-faint); margin-top:3px; font-variant-numeric:tabular-nums; }\n .pnav { display:flex; gap:4px; flex:none; }\n .pnav button { width:26px; height:26px; border:1px solid var(--line); background:var(--panel); border-radius:8px;\n color:var(--ink-mute); font-size:12px; cursor:pointer; display:grid; place-items:center; }\n .pnav button:disabled { opacity:.3; cursor:default; }\n .pbody { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }\n .plain { font-size:14px; line-height:1.7; }\n .plain .why { margin-top:8px; font-size:12.5px; color:var(--ink-mute); line-height:1.6; }\n .facts { display:flex; flex-wrap:wrap; gap:6px; }\n .fact { font-size:11px; font-weight:700; background:var(--bg); border:1px solid var(--line); color:var(--ink-mute); padding:4px 10px; border-radius:999px; }\n .fact.ok { background:var(--accent-soft); border-color:transparent; color:var(--accent); }\n details.techbox { border:1px solid var(--line); border-radius:12px; background:var(--bg); overflow:hidden; }\n details.techbox summary { list-style:none; cursor:pointer; padding:10px 14px; font-size:11.5px; font-weight:800;\n letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint); display:flex; align-items:center; gap:8px; }\n details.techbox summary::-webkit-details-marker { display:none; }\n details.techbox summary::after { content:\"▸\"; margin-left:auto; transition:transform .15s; }\n details.techbox[open] summary::after { transform:rotate(90deg); }\n .techlist { padding:0 12px 12px; display:flex; flex-direction:column; gap:8px; }\n .titem { border:1px solid var(--line); border-radius:10px; background:var(--panel); overflow:hidden; }\n .titem .th { padding:8px 12px; font-size:11.5px; font-weight:700; color:var(--ink-mute); display:flex; align-items:center; gap:8px; }\n .titem .th .copy { margin-left:auto; border:none; background:none; color:var(--ink-ghost); font-size:11px; cursor:pointer; padding:2px 4px; border-radius:5px; }\n .titem .th .copy:hover { color:var(--ink); background:var(--line-soft); }\n .titem pre { margin:0; border-top:1px solid var(--line-soft); font-family:ui-monospace,Menlo,monospace; font-size:11.5px;\n line-height:1.55; color:#4a4236; padding:9px 12px; overflow:auto; max-height:240px; white-space:pre-wrap; word-break:break-word; }\n .d-text { font-size:13.5px; line-height:1.7; }\n .d-text p { margin:0 0 8px; }\n .d-text p:last-child { margin-bottom:0; }\n .d-text h1, .d-text h2, .d-text h3, .d-text h4, .d-text h5, .d-text h6 { margin:12px 0 6px; line-height:1.3; }\n .d-text h1:first-child, .d-text h2:first-child, .d-text h3:first-child { margin-top:0; }\n .d-text ul, .d-text ol { margin:0 0 8px; padding-left:20px; }\n .d-text code { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; background:var(--bg); border-radius:4px; padding:1px 5px; }\n .d-text pre { margin:0 0 8px; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:9px 12px; overflow:auto; }\n .d-text pre code { background:none; border-radius:0; padding:0; }\n .d-text table { border-collapse:collapse; margin:0 0 8px; font-size:12.5px; }\n .d-text th, .d-text td { border:1px solid var(--line); padding:4px 8px; text-align:left; }\n .d-text a { color:var(--blue); }\n .pfoot { flex:none; border-top:1px solid var(--line); padding:9px 16px; font-size:11px; color:var(--ink-ghost); display:flex; gap:10px; }\n .kbd { font-family:ui-monospace,Menlo,monospace; font-size:10px; border:1px solid var(--line); border-bottom-width:2px;\n border-radius:5px; padding:1px 5px; background:var(--panel); color:var(--ink-mute); }\n\n /* ── boîte d'envoi + routage IA ── */\n .composer { flex:none; border-top:1px solid var(--line); background:var(--panel); padding:10px 14px; }\n .composer .cbar { display:flex; gap:8px; }\n .composer textarea { flex:1; resize:none; border:1px solid var(--line); border-radius:10px; padding:9px 12px; font:inherit; font-size:13px; max-height:110px; background:var(--bg); }\n .composer textarea:focus { outline:2px solid #241f1a22; }\n .composer textarea:disabled { opacity:.5; cursor:not-allowed; }\n .composer button { border:none; border-radius:10px; padding:0 16px; background:var(--ink); color:#fdf9f2; font-weight:700; font-size:13px; cursor:pointer; }\n .composer button:disabled { opacity:.4; cursor:not-allowed; }\n .composer .routing { font-size:11px; color:var(--ink-faint); padding:6px 2px 0; min-height:22px; }\n .composer .routing .r-cont { color:var(--blue); font-weight:700; }\n .composer .routing .r-newt { color:var(--gold); font-weight:700; }\n #statusbar { flex:none; padding:4px 20px; font-size:10.5px; color:var(--ink-ghost); border-top:1px solid var(--line-soft); }\n</style>\n</head>\n<body>\n<div id=\"pickerScreen\">\n <h1>Choisis une session</h1>\n <div id=\"pickerList\"><div class=\"pk-empty\">Connexion…</div></div>\n</div>\n\n<div class=\"app hidden\" id=\"storyScreen\">\n <div class=\"hero\">\n <div class=\"hero-top\">\n <span class=\"pulse\" id=\"pulse\"></span>\n <div class=\"who\">\n <div class=\"h1\" id=\"heroTitle\"></div>\n <div class=\"h2\" id=\"heroSub\"></div>\n </div>\n <div class=\"modewrap\"><button id=\"modeSimple\" class=\"on\" type=\"button\">Simple</button><button id=\"modeTech\" type=\"button\">Tech</button></div>\n <a class=\"sim hidden\" id=\"fullPanelLink\" href=\"#\" target=\"_blank\" rel=\"noopener\" title=\"Ouvrir la session en direct (live-session du daemon)\">↗ panneau complet</a>\n <button class=\"sim\" id=\"switchBtn\" type=\"button\">⇆ changer</button>\n </div>\n <div class=\"plan\" id=\"plan\"></div>\n </div>\n\n <div class=\"body\">\n <div class=\"feedcol\">\n <div class=\"feed\" id=\"feed\"><div class=\"fspacer\"></div><div id=\"rows\"></div></div>\n <div class=\"composer\">\n <div class=\"cbar\">\n <textarea id=\"msgBox\" rows=\"1\" placeholder=\"Écris à l'agent… (la surcouche classe ton message : suite de la sous-tâche ou nouvelle sous-tâche)\"></textarea>\n <button id=\"sendBtn\" type=\"button\">Envoyer</button>\n </div>\n <div class=\"routing\" id=\"routing\"></div>\n </div>\n </div>\n\n <aside class=\"panel\" id=\"panel\" aria-label=\"Détail de l'étape\">\n <div class=\"panel-inner\">\n <div class=\"phead\">\n <span class=\"ico\" id=\"pIco\"></span>\n <div class=\"tt\"><div class=\"t\" id=\"pTitle\"></div><div class=\"s\" id=\"pSub\"></div></div>\n <div class=\"pnav\"><button id=\"pPrev\" type=\"button\">↑</button><button id=\"pNext\" type=\"button\">↓</button><button id=\"pClose\" type=\"button\">✕</button></div>\n </div>\n <div class=\"pbody\" id=\"pBody\"></div>\n <div class=\"pfoot\"><span class=\"kbd\">↑</span><span class=\"kbd\">↓</span> naviguer · <span class=\"kbd\">Esc</span> fermer</div>\n </div>\n </aside>\n </div>\n <div id=\"statusbar\"></div>\n</div>\n\n<script>\nvar $=function(id){ return document.getElementById(id); };\nvar esc=function(s){ return String(s==null?\"\":s).replace(/[&<>]/g,function(c){ return {\"&\":\"&\",\"<\":\"<\",\">\":\">\"}[c]; }); };\n\n// ============================================================\n// renderMd — vanilla-JS port of markdown-lite.ts. Kept\n// function-for-function identical so the two are easy to diff (same\n// self-contained-panel constraint as buildStoryJs below): headers,\n// bold/italic, inline/fenced code, bullet/numbered lists, pipe tables and\n// links, with every raw text run HTML-escaped before any generated tag\n// wraps it.\n// ============================================================\nfunction escHtmlMd(s){ return String(s).replace(/[&<>\"]/g,function(c){ if(c==='&') return '&'; if(c==='<') return '<'; if(c==='>') return '>'; return '"'; }); }\nfunction renderInlineMd(text){\n var out=escHtmlMd(text);\n out=out.replace(/\\`([^\\`]+)\\`/g,function(_m,code){ return '<code>'+code+'</code>'; });\n out=out.replace(/\\\\[([^\\\\]]+)\\\\]\\\\((https?:\\\\/\\\\/[^\\\\s)]+)\\\\)/g,function(_m,label,url){ return '<a href=\"'+url+'\" target=\"_blank\" rel=\"noopener noreferrer\">'+label+'</a>'; });\n out=out.replace(/\\\\*\\\\*([^*]+)\\\\*\\\\*/g,'<strong>$1</strong>');\n out=out.replace(/(^|[^*])\\\\*([^*]+)\\\\*(?!\\\\*)/g,'$1<em>$2</em>');\n return out;\n}\nfunction isTableSepMd(line){ return /^\\\\s*\\\\|?\\\\s*:?-+:?\\\\s*(\\\\|\\\\s*:?-+:?\\\\s*)+\\\\|?\\\\s*$/.test(line); }\nfunction splitRowMd(line){ return line.trim().replace(/^\\\\|/,'').replace(/\\\\|$/,'').split('|').map(function(c){ return c.trim(); }); }\nfunction renderMd(md){\n var lines=String(md==null?'':md).replace(/\\\\r\\\\n?/g,'\\\\n').split('\\\\n');\n var out=[], para=[], list=null;\n function flushPara(){ if(para.length){ out.push('<p>'+para.map(renderInlineMd).join('<br>')+'</p>'); para=[]; } }\n function flushList(){ if(list){ var tag=list.ordered?'ol':'ul'; out.push('<'+tag+'>'+list.items.map(function(i){ return '<li>'+renderInlineMd(i)+'</li>'; }).join('')+'</'+tag+'>'); list=null; } }\n function flushAll(){ flushPara(); flushList(); }\n var i=0;\n while(i<lines.length){\n var line=lines[i];\n if(/^\\\\s*\\`\\`\\`/.test(line)){\n flushAll();\n var code=[]; i+=1;\n while(i<lines.length && !/^\\\\s*\\`\\`\\`/.test(lines[i])){ code.push(lines[i]); i+=1; }\n i+=1;\n out.push('<pre><code>'+escHtmlMd(code.join('\\\\n'))+'</code></pre>');\n continue;\n }\n var header=line.match(/^(#{1,6})\\\\s+(.*)$/);\n if(header){\n flushAll();\n var level=header[1].length;\n out.push('<h'+level+'>'+renderInlineMd(header[2].trim())+'</h'+level+'>');\n i+=1;\n continue;\n }\n if(/^\\\\s*\\\\|/.test(line) && i+1<lines.length && isTableSepMd(lines[i+1])){\n flushAll();\n var headCells=splitRowMd(line);\n i+=2;\n var bodyRows=[];\n while(i<lines.length && /^\\\\s*\\\\|/.test(lines[i])){ bodyRows.push(splitRowMd(lines[i])); i+=1; }\n out.push('<table><thead><tr>'+headCells.map(function(c){ return '<th>'+renderInlineMd(c)+'</th>'; }).join('')+'</tr></thead><tbody>'\n +bodyRows.map(function(r){ return '<tr>'+r.map(function(c){ return '<td>'+renderInlineMd(c)+'</td>'; }).join('')+'</tr>'; }).join('')+'</tbody></table>');\n continue;\n }\n var bullet=line.match(/^\\\\s*[-*+]\\\\s+(.*)$/);\n var numbered=line.match(/^\\\\s*\\\\d+\\\\.\\\\s+(.*)$/);\n if(bullet || numbered){\n flushPara();\n var ordered=!!numbered;\n var item=(bullet||numbered)[1];\n if(!list || list.ordered!==ordered){ flushList(); list={ordered:ordered,items:[]}; }\n list.items.push(item);\n i+=1;\n continue;\n }\n if(line.trim()===''){ flushAll(); i+=1; continue; }\n flushList();\n para.push(line);\n i+=1;\n }\n flushAll();\n return out.join('');\n}\n\n${panelBridgeScript(\"agentproto-session-story-panel\")}\n// Best-effort: some hosts forward the triggering tool call's arguments as\n// a notification so the panel can auto-open the right session. Purely\n// additive — the session picker is the reliable path when this never\n// arrives.\nvar pendingSessionId=null;\nonHostNotification(function(method, params){\n if(/tool-input|tool-call/.test(method)){\n var args=(params && (params.arguments || params.input)) || {};\n if(args && args.sessionId) pendingSessionId=args.sessionId;\n }\n});\n\n// ============================================================\n// buildStory — vanilla-JS port of session-story.ts. Kept\n// function-for-function identical so the two are easy to diff; the panel\n// resource must be fully self-contained (no bundler/dynamic import), so it\n// cannot import the TS module directly.\n// ============================================================\n\nvar SALIENT_KEYS=[\"file_path\",\"path\",\"filePath\",\"file\",\"command\",\"pattern\",\"query\",\"q\",\"url\",\"todos\",\"description\",\"prompt\"];\nfunction truncateStr(v,max){ var o=String(v).replace(/\\\\s+/g,' ').trim(); return o.length>max? o.slice(0,max-1)+'…':o; }\nfunction formatArgValue(v){\n if(typeof v==='string') return v;\n if(Array.isArray(v)) return v.length+' item'+(v.length===1?'':'s');\n if(v && typeof v==='object') return JSON.stringify(v);\n return String(v);\n}\nfunction pickSalient(args){\n for(var i=0;i<SALIENT_KEYS.length;i++){\n var k=SALIENT_KEYS[i], v=args[k];\n if(v!==undefined && v!==null && v!=='') return formatArgValue(v);\n }\n return null;\n}\nfunction formatToolCall(name,args){\n name=name||'tool';\n args=(args && typeof args==='object' && !Array.isArray(args)) ? args : {};\n var salient=pickSalient(args);\n if(salient!==null){\n if(name.toLowerCase().indexOf(salient.toLowerCase())>=0) return truncateStr(name,120);\n return truncateStr(name+' '+salient,120);\n }\n if(Object.keys(args).length===0) return name;\n return truncateStr(name+' '+JSON.stringify(args),120);\n}\nfunction extractText(v){\n if(v==null) return null;\n if(typeof v==='string') return v;\n if(Array.isArray(v)){\n var parts=v.map(extractText).filter(function(x){ return x!=null; });\n return parts.length? parts.join('\\\\n') : null;\n }\n if(typeof v==='object'){\n if(typeof v.text==='string') return v.text;\n if(typeof v.message==='string') return v.message;\n if(Array.isArray(v.content)) return extractText(v.content);\n if(typeof v.error==='string') return v.error;\n if(v.error && typeof v.error==='object' && typeof v.error.message==='string') return v.error.message;\n return null;\n }\n return null;\n}\nfunction formatToolResult(toolName,result,isError){\n var text=extractText(result);\n if(isError){\n var message=text!=null? text : (result!=null? JSON.stringify(result) : 'failed');\n var firstLine=String(message).split(/\\\\r?\\\\n/)[0] || message;\n return truncateStr(firstLine,160);\n }\n if(text==null) return null;\n var trimmed=text.trim();\n if(!trimmed) return null;\n var lines=trimmed.split(/\\\\r?\\\\n/);\n if(lines.length>1){\n var bytes=new TextEncoder().encode(trimmed).length;\n return lines.length+' lines, '+bytes+'B';\n }\n return truncateStr(lines[0],160);\n}\n\nfunction classifyKind(toolCalls){\n if(!toolCalls || toolCalls.length===0) return 'text';\n var names=toolCalls.map(function(t){ return t.name.toLowerCase(); });\n if(names.some(function(n){ return /edit|write/.test(n); })) return 'edit';\n if(names.some(function(n){ return /bash|terminal|command/.test(n); })) return 'bash';\n if(names.some(function(n){ return /read|grep|glob/.test(n); })) return 'read';\n return 'text';\n}\nvar NEW_CHAPTER_RE=/\\\\b(aussi|autre|ensuite|nouveau|nouvelle|plut[oô]t|maintenant|apr[eè]s ça|il faudrait|peux[- ]tu|on pourrait|ajoute|g[eè]re)\\\\b/iu;\nfunction classifyRoute(text){\n var newt=NEW_CHAPTER_RE.test(text);\n if(!newt) return {route:'cont'};\n var title=text.replace(/[.?!].*$/,'').slice(0,42);\n return {route:'newt', title:title};\n}\nfunction formatTsJs(ts){\n if(ts===undefined || ts===null || isNaN(ts)) return '';\n return new Date(ts).toISOString().slice(11,19);\n}\nfunction firstMeaningfulLine(text){\n if(!text) return undefined;\n var lines=text.split('\\\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });\n return lines[0];\n}\nfunction lineCountOf(text){\n var n=(text||'').split('\\\\n').filter(function(l){ return l.trim().length>0; }).length;\n return n||1;\n}\nfunction parseArgsJson(s){ try{ return JSON.parse(s); }catch(e){ return {}; } }\n\nfunction foldToolStep(assistant,toolResults){\n var toolCalls=assistant.toolCalls||[];\n var kind=classifyKind(toolCalls);\n var count=toolCalls.length||1;\n var items=[], facts=[];\n if(assistant.text && assistant.text.trim()) items.push({text:assistant.text.trim()});\n toolCalls.forEach(function(tc,i){\n var args=parseArgsJson(tc.args);\n var h=formatToolCall(tc.name,args);\n var resultMsg=toolResults[i];\n var resultText=(resultMsg && resultMsg.text) || '';\n var isError=resultText.indexOf('[error]')===0;\n var r=isError? resultText.slice(7).trim() : resultText;\n items.push({h:h,r:r});\n var fact=formatToolResult(tc.name,r,isError);\n if(fact) facts.push(fact);\n });\n var firstLine=firstMeaningfulLine(assistant.text);\n var firstToolCall=toolCalls[0];\n var sum=firstLine!==undefined? firstLine : (firstToolCall? formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args)) : '…');\n var raw1;\n if(toolCalls.length===0) raw1='assistant · '+lineCountOf(assistant.text)+' ligne(s)';\n else if(toolCalls.length===1) raw1=formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args));\n else raw1=(firstToolCall? firstToolCall.name : 'tool')+' ×'+toolCalls.length;\n return {kind:kind, ts:formatTsJs(assistant.ts), sum:sum, raw1:raw1, count:count, facts:facts, items:items};\n}\nfunction foldUserStep(msg){\n var text=msg.text||'';\n return {kind:'user', ts:formatTsJs(msg.ts), sum:'« '+truncateStr(text,80)+' »', raw1:'user · '+lineCountOf(text)+' ligne(s)', count:1, facts:[], items:[{text:text}], userText:text};\n}\nfunction foldOrphanToolStep(msg){\n var text=msg.text||'';\n var isError=text.indexOf('[error]')===0;\n var r=isError? text.slice(7).trim() : text;\n var name=msg.toolName||'tool';\n var fact=formatToolResult(name,r,isError);\n return {kind:classifyKind([{name:name}]), ts:formatTsJs(msg.ts), sum: msg.toolName? (msg.toolName+' · résultat') : \"Résultat d'outil\", raw1: msg.toolName||'tool', count:1, facts: fact?[fact]:[], items:[{h:name,r:r}]};\n}\nfunction foldSystemStep(msg){\n var text=msg.text||'';\n var line=firstMeaningfulLine(text);\n return {kind:'text', ts:formatTsJs(msg.ts), sum: line!==undefined? line : text, raw1:'system', count:1, facts:[], items: text?[{text:text}]:[]};\n}\nfunction foldMessages(messages){\n var steps=[], i=0;\n while(i<messages.length){\n var msg=messages[i];\n if(msg.role==='user'){ steps.push(foldUserStep(msg)); i+=1; continue; }\n if(msg.role==='assistant'){\n var j=i+1, toolResults=[];\n while(j<messages.length && messages[j].role==='tool'){ toolResults.push(messages[j]); j+=1; }\n steps.push(foldToolStep(msg,toolResults)); i=j; continue;\n }\n if(msg.role==='tool'){ steps.push(foldOrphanToolStep(msg)); i+=1; continue; }\n steps.push(foldSystemStep(msg)); i+=1;\n }\n return steps;\n}\nfunction buildStoryJs(messages){\n var folded=foldMessages(messages||[]);\n var chapters=[], steps=[];\n var currentChapterId, sawFirstUser=false;\n function closeCurrent(){ var cur=chapters.filter(function(c){ return c.id===currentChapterId; })[0]; if(cur) cur.status='done'; }\n function openChapter(title){ var id='c'+(chapters.length+1); chapters.push({id:id,title:title,status:'cur'}); return id; }\n folded.forEach(function(step){\n var route;\n if(step.kind==='user' && step.userText!==undefined){\n if(!sawFirstUser){ sawFirstUser=true; currentChapterId=openChapter('Cadrage'); }\n else {\n var verdict=classifyRoute(step.userText);\n route=verdict.route;\n if(verdict.route==='newt'){ closeCurrent(); currentChapterId=openChapter(verdict.title||'Nouvelle sous-tâche'); }\n }\n } else if(currentChapterId===undefined){ currentChapterId=openChapter('Cadrage'); }\n var out={chap:currentChapterId, kind:step.kind, ts:step.ts, sum:step.sum, raw1:step.raw1, count:step.count, facts:step.facts, items:step.items};\n if(route) out.route=route;\n steps.push(out);\n });\n return {chapters:chapters, steps:steps};\n}\n\n// ============================================================\n// App state\n// ============================================================\nvar sessions=[];\nvar activeSessionId=null;\nvar story={chapters:[], steps:[]};\nvar open={};\nvar selected=-1;\nvar lastSeenOutputAt=null;\nvar pollTimer=null, polling=false;\nvar ICONS={text:[\"k-text\",\"A\"],edit:[\"k-edit\",\"✎\"],bash:[\"k-bash\",\"▸\"],read:[\"k-read\",\"⌕\"],user:[\"k-user\",\"T\"]};\nvar icoSpec=function(k){ return ICONS[k]||ICONS.text; };\nvar chapOf=function(id){ return story.chapters.filter(function(c){ return c.id===id; })[0]; };\nvar curChap=function(){ return story.chapters.filter(function(c){ return c.status==='cur'; })[0] || story.chapters[story.chapters.length-1]; };\n\nfunction setStatus(msg){ $('statusbar').textContent=msg; }\nfunction nowTs(){ return new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); }\n\nfunction titleOf(s){\n return s.label || s.name || (s.command? s.command.split(/\\\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);\n}\n\n// ============================================================\n// Picker screen\n// ============================================================\nfunction renderPicker(){\n var el=$('pickerList');\n if(!sessions.length){ el.innerHTML='<div class=\"pk-empty\">Aucune session</div>'; return; }\n var html='';\n sessions.forEach(function(s){\n html+='<div class=\"pk-item\" data-id=\"'+esc(s.id)+'\">'\n + '<span class=\"pk-name\">'+esc(titleOf(s))+'</span>'\n + '<span class=\"badge '+esc(s.status)+'\">'+esc(s.status)+'</span>'\n + '<span class=\"pk-meta\">'+esc(s.kind||'')+'</span>'\n + '</div>';\n });\n el.innerHTML=html;\n Array.prototype.forEach.call(el.querySelectorAll('.pk-item'), function(row){\n row.onclick=function(){ openSession(row.getAttribute('data-id')); };\n });\n}\n\nfunction showPicker(){\n activeSessionId=null;\n $('pickerScreen').classList.remove('hidden');\n $('storyScreen').classList.add('hidden');\n renderPicker();\n}\n\nfunction openSession(id){\n activeSessionId=id;\n story={chapters:[], steps:[]};\n open={};\n selected=-1;\n lastSeenOutputAt=null;\n $('pickerScreen').classList.add('hidden');\n $('storyScreen').classList.remove('hidden');\n // Deep-link to the daemon's own live-session panel (GET /apps/:appId/ui\n // pins it via ?sessionId=). Only resolvable when this panel is itself\n // served standalone by the daemon; inside an MCP host iframe the origin\n // isn't the daemon's, so the link stays hidden rather than dead.\n var daemonOrigin=(location.pathname.indexOf('/apps/')===0&&location.origin&&location.origin!=='null')?location.origin:'';\n $('fullPanelLink').href=daemonOrigin?daemonOrigin+'/apps/@agentproto/live-session/ui?sessionId='+encodeURIComponent(id):'#';\n $('fullPanelLink').classList.toggle('hidden',!daemonOrigin);\n closePanel();\n loadStory().then(renderAll);\n}\n\n// ============================================================\n// Story loading\n// ============================================================\nfunction activeSession(){ return sessions.filter(function(s){ return s.id===activeSessionId; })[0]; }\n\nfunction loadStory(){\n return callTool('agent_export', {sessionId:activeSessionId, format:'json'}).then(function(data){\n var messages=(data && data.messages) || [];\n story=buildStoryJs(messages);\n // Default open state: only the last (current) chapter is expanded.\n var last=story.chapters[story.chapters.length-1];\n if(last && !(last.id in open)) open[last.id]=true;\n }).catch(function(e){\n setStatus('Erreur export : '+e.message);\n });\n}\n\nfunction canSend(){\n var s=activeSession();\n return !!s && s.kind==='agent-cli' && s.status==='running';\n}\n\nfunction renderComposer(){\n var s=activeSession();\n var box=$('msgBox'), btn=$('sendBtn');\n var enabled=canSend();\n box.disabled=!enabled;\n btn.disabled=!enabled;\n if(!s){ box.placeholder='Session introuvable.'; }\n else if(!enabled) box.placeholder='Lecture seule — session '+esc(s.status)+'.';\n else box.placeholder=\"Écris à l'agent… (la surcouche classe ton message : suite de la sous-tâche ou nouvelle sous-tâche)\";\n}\n\nfunction renderHero(){\n var s=activeSession();\n $('heroTitle').textContent=s? titleOf(s) : (activeSessionId||'');\n var firstUser=story.steps.filter(function(st){ return st.kind==='user'; })[0];\n var mission=firstUser? firstUser.userText || (firstUser.items[0] && firstUser.items[0].text) : null;\n $('heroSub').textContent=mission? truncateStr(mission,200) : 'Aucun message pour le moment.';\n var p=$('pulse');\n p.classList.toggle('off', !(s && (s.status==='running' || s.status==='starting')));\n renderComposer();\n}\n\nfunction renderAll(){\n renderHero();\n renderPlan();\n renderRows('bottom');\n}\n\n// ============================================================\n// big picture strip\n// ============================================================\nfunction renderPlan(){\n var done=story.chapters.filter(function(c){ return c.status==='done'; }).length;\n $('plan').innerHTML=story.chapters.map(function(c,i){\n return '<span class=\"pt '+c.status+'\" data-c=\"'+esc(c.id)+'\"><span class=\"st\">'+(c.status==='done'?'✓':'●')+'</span>'+(i+1)+'. '+esc(c.title)+'</span>';\n }).join('') + '<span class=\"pt\" style=\"cursor:default\"><b>'+done+'/'+story.chapters.length+'</b> faites</span>';\n Array.prototype.forEach.call($('plan').querySelectorAll('.pt[data-c]'), function(el){\n el.onclick=function(){ open[el.getAttribute('data-c')]=true; renderRows(); jumpToChap(el.getAttribute('data-c')); };\n });\n}\nfunction jumpToChap(cid){\n var el=document.querySelector('.chap[data-c=\"'+cid+'\"]');\n if(el) el.scrollIntoView({block:'start',behavior:'smooth'});\n}\n\n// ============================================================\n// feed segmenté par chapitres\n// ============================================================\nfunction rowHtml(s,i,isNew){\n var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];\n var route=s.route? '<span class=\"route '+(s.route==='newt'?'newt':'cont')+'\">'+(s.route==='newt'?'★ nouvelle sous-tâche':'↳ suite')+'</span>' : '';\n return '<div class=\"row '+(isNew?'new':'')+'\" aria-selected=\"'+(i===selected)+'\" data-i=\"'+i+'\">'\n + '<span class=\"ico '+cls+'\">'+ch+'</span>'\n + '<span class=\"mid\"><span class=\"sum\">'+esc(s.sum)+'</span><span class=\"raw1\">'+esc(s.raw1||'')+'</span>'+route+'</span>'\n + (s.count>1? '<span class=\"cnt\">×'+s.count+'</span>':'') + '<span class=\"ts\">'+esc(s.ts||'')+'</span>'\n + '</div>';\n}\nfunction renderRows(keepScroll,newIdx){\n var feed=$('feed');\n var prevH=feed.scrollHeight, prevTop=feed.scrollTop;\n var html='';\n story.chapters.forEach(function(c,ci){\n var chapSteps=[];\n story.steps.forEach(function(s,i){ if(s.chap===c.id) chapSteps.push({s:s,i:i}); });\n if(!chapSteps.length) return;\n var isOpen=!!open[c.id];\n html+='<div class=\"chap '+c.status+' '+(isOpen?'open':'')+'\" data-c=\"'+esc(c.id)+'\">'\n + '<span class=\"cst\">'+(c.status==='done'?'✓':'●')+'</span><span class=\"cnum\">'+(ci+1)+'.</span>'\n + '<span class=\"csum\">'+esc(c.title)+'</span>'\n + '<span class=\"cmeta\">'+chapSteps.length+' étape'+(chapSteps.length>1?'s':'')+'</span><span class=\"cchev\">▸</span>'\n + '</div>';\n if(isOpen) html += chapSteps.map(function(x){ return rowHtml(x.s,x.i,x.i===newIdx); }).join('');\n });\n $('rows').innerHTML=html;\n Array.prototype.forEach.call(document.querySelectorAll('.row'), function(el){\n el.onclick=function(){ selectStep(Number(el.getAttribute('data-i'))); };\n });\n Array.prototype.forEach.call(document.querySelectorAll('.chap'), function(el){\n el.onclick=function(){ var c=el.getAttribute('data-c'); open[c]=!open[c]; renderRows(); };\n });\n if(keepScroll==='bottom') feed.scrollTop=feed.scrollHeight;\n else if(keepScroll==='preserve') feed.scrollTop=feed.scrollHeight-prevH+prevTop;\n}\n\n// ============================================================\n// panneau ancré\n// ============================================================\nfunction selectStep(i){\n selected=i;\n var s=story.steps[i]; if(!s) return;\n open[s.chap]=true;\n $('panel').classList.add('open');\n var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];\n var ico=$('pIco'); ico.className='ico '+cls; ico.textContent=ch;\n $('pTitle').textContent=s.sum;\n var c=chapOf(s.chap);\n $('pSub').textContent=(s.ts? s.ts+' · ':'')+(c? ('sous-tâche : '+c.title) : '');\n var facts=(s.facts||[]).map(function(f){\n return '<span class=\"fact '+(/✓|exit 0|passed|0 match/.test(f)?'ok':'')+'\">'+esc(f)+'</span>';\n }).join('');\n var tech=(s.items||[]).map(function(it,k){\n return it.text!==undefined\n ? '<div class=\"d-text\">'+renderMd(it.text)+'</div>'\n : '<div class=\"titem\"><div class=\"th\">'+esc(it.h)+'<button class=\"copy\" data-k=\"'+k+'\" type=\"button\">⧉</button></div><pre>'+esc(it.r)+'</pre></div>';\n }).join('');\n $('pBody').innerHTML=''\n + '<div class=\"plain\"><div>'+esc(s.sum)+'.</div><div class=\"why\">'+esc(s.why||'')+'</div></div>'\n + (facts? '<div class=\"facts\">'+facts+'</div>':'')\n + '<details class=\"techbox\" '+(document.body.classList.contains('tech')?'open':'')+'>'\n + '<summary>Détail technique · '+(s.items||[]).length+'</summary><div class=\"techlist\">'+tech+'</div>'\n + '</details>';\n Array.prototype.forEach.call($('pBody').querySelectorAll('.copy'), function(btn){\n btn.onclick=function(e){\n e.stopPropagation();\n var it=(s.items||[])[Number(btn.getAttribute('data-k'))];\n var payload=(it.h||'')+'\\\\n'+(it.r||it.text||'');\n if(navigator.clipboard) navigator.clipboard.writeText(payload).catch(function(){});\n btn.textContent='✓'; setTimeout(function(){ btn.textContent='⧉'; },900);\n };\n });\n $('pPrev').disabled=i<=0; $('pNext').disabled=i>=story.steps.length-1;\n renderRows();\n var el=document.querySelector('.row[data-i=\"'+i+'\"]');\n if(el) el.scrollIntoView({block:'nearest',behavior:'smooth'});\n}\nfunction closePanel(){ selected=-1; var p=$('panel'); if(p) p.classList.remove('open'); renderRows(); }\n$('pClose').addEventListener('click',closePanel);\n$('pPrev').addEventListener('click',function(){ if(selected>0) selectStep(selected-1); });\n$('pNext').addEventListener('click',function(){ if(selected<story.steps.length-1) selectStep(selected+1); });\ndocument.addEventListener('keydown',function(e){\n if(e.key==='Escape'){ closePanel(); return; }\n if($('storyScreen').classList.contains('hidden')) return;\n if(selected<0 || e.target.tagName==='TEXTAREA') return;\n if(e.key==='ArrowUp'){ e.preventDefault(); if(selected>0) selectStep(selected-1); }\n if(e.key==='ArrowDown'){ e.preventDefault(); if(selected<story.steps.length-1) selectStep(selected+1); }\n});\n\n// ============================================================\n// Simple / Tech modes\n// ============================================================\nfunction setMode(tech){\n document.body.classList.toggle('tech',tech);\n $('modeTech').classList.toggle('on',tech);\n $('modeSimple').classList.toggle('on',!tech);\n if(selected>=0) selectStep(selected);\n}\n$('modeSimple').addEventListener('click',function(){ setMode(false); });\n$('modeTech').addEventListener('click',function(){ setMode(true); });\n$('switchBtn').addEventListener('click', function(){ if(pollTimer) clearTimeout(pollTimer); showPicker(); doPoll(); });\n\n// ============================================================\n// composer — local chapter-routing classification + agent_prompt\n// ============================================================\n$('sendBtn').addEventListener('click', sendMsg);\n$('msgBox').addEventListener('keydown', function(e){\n if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }\n});\nfunction sendMsg(){\n if(!canSend()) return;\n var box=$('msgBox'), text=box.value.trim();\n if(!text) return;\n box.value='';\n var cur=curChap();\n var r=cur? classifyRoute(text) : {route:'cont'};\n var chapId=cur? cur.id : null;\n if(r.route==='newt' && cur){\n cur.status='done';\n var id='c'+(story.chapters.length+1);\n story.chapters.push({id:id, title:r.title||'Nouvelle sous-tâche', status:'cur'});\n chapId=id; open[id]=true;\n $('routing').innerHTML='✦ classé : <span class=\"r-newt\">★ nouvelle sous-tâche « '+esc(r.title||'')+' »</span>';\n } else {\n $('routing').innerHTML='✦ classé : <span class=\"r-cont\">↳ suite de « '+esc(cur? cur.title : '')+' »</span>';\n }\n setTimeout(function(){ $('routing').textContent=''; },5000);\n story.steps.push({\n chap:chapId, kind:'user', ts:nowTs(),\n sum:'« '+text.slice(0,80)+(text.length>80?'…':'')+' »',\n raw1:'user · '+text.split('\\\\n').length+' ligne(s)',\n route:r.route, facts:[], items:[{text:text}], userText:text,\n });\n renderPlan(); renderRows('bottom', story.steps.length-1);\n callTool('agent_prompt', {sessionId:activeSessionId, prompt:text}).catch(function(e){\n setStatus('Envoi échoué : '+e.message);\n });\n}\n\n// ============================================================\n// Poll — session_list every ~5s; re-fetch agent_export only on turn\n// boundaries (lastOutputAt changed for the active session).\n// ============================================================\nvar POLL_MS=5000;\nfunction loadSessions(){\n return callTool('session_list', {kind:'all'}).then(function(data){\n sessions=data.sessions||[];\n if($('pickerScreen') && !$('pickerScreen').classList.contains('hidden')) renderPicker();\n }).catch(function(e){ setStatus('Erreur : '+e.message); });\n}\nfunction doPoll(){\n if(polling) return;\n polling=true;\n loadSessions().then(function(){\n if(!activeSessionId){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }\n var s=activeSession();\n if(!s){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }\n renderHero();\n var changed=s.lastOutputAt && s.lastOutputAt!==lastSeenOutputAt;\n if(changed){\n lastSeenOutputAt=s.lastOutputAt;\n loadStory().then(function(){ renderPlan(); renderRows('bottom'); polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });\n } else {\n polling=false; pollTimer=setTimeout(doPoll,POLL_MS);\n }\n }).catch(function(){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });\n}\n\n// ============================================================\n// Boot\n// ============================================================\ninitBridge().then(loadSessions).then(function(){\n setTimeout(function(){\n var target=pendingSessionId && sessions.some(function(s){ return s.id===pendingSessionId; })\n ? pendingSessionId\n : null;\n if(!target){\n var agentSessions=sessions.filter(function(s){ return s.kind==='agent-cli'; });\n if(agentSessions.length===1) target=agentSessions[0].id;\n }\n if(target) openSession(target); else showPicker();\n pollTimer=setTimeout(doPoll,POLL_MS);\n }, 50);\n}).catch(function(e){\n setStatus('Bridge : '+e.message);\n $('pickerList').innerHTML='<div class=\"pk-empty\">Échec connexion bridge : '+esc(e.message)+'</div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto \"session story\" panel.\n *\n * A per-session readable timeline: a one-sentence mission + a plan strip of\n * inferred sub-task \"chapters\", a feed of steps segmented by chapter, and an\n * anchored side panel with plain-language + technical detail. Ported 1:1\n * from the validated mockup at docs/session-story-mockup.html.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps). The host (@agentproto/runtime) wires\n * this factory's output via `registerMcpApps` at boot time — see runtime's\n * builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_session_story` is called by the host, optionally\n * with a `sessionId`.\n * 2. execute() returns just enough for the panel to boot: when a\n * `sessionId` is given, it echoes that back and skips the sessions\n * list entirely — the panel's own JS never reads the initial snapshot,\n * it always re-fetches via `session_list` over the postMessage bridge\n * on boot (see panel.ts's `loadSessions()`/boot sequence), so shipping\n * the full list here would just be dead payload. When no `sessionId`\n * is given (the picker case) it still returns the full sessions\n * snapshot, since the model/host may surface it before the panel's\n * own bridge call lands.\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and drives\n * everything else live: `session_list` for status polling,\n * `agent_export` for the transcript (folded into chapters/steps with a\n * JS port of @agentproto/runtime's session-story.ts heuristics — the\n * panel HTML is fully self-contained, so it cannot import that TS\n * module directly), and `agent_prompt` for the composer.\n *\n * This module also exports `sessionStoryApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeSessionStoryPanelApp` above, which the daemon\n * mounts directly at boot: that factory closes over LIVE `listSessions`,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { SESSION_STORY_PANEL_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\n// Re-exported so packages/vscode's storyPanel.ts (which reuses this HTML\n// byte-for-byte in a VS Code webview) can import it from this app's\n// subpath instead of reaching into ./panel.js directly.\nexport { SESSION_STORY_PANEL_HTML }\n\nexport const sessionStoryInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Session id to open directly. When omitted, the panel shows a session \" +\n \"picker (built from session_list) and lets the user choose.\",\n ),\n})\n\nexport type SessionStoryInput = z.infer<typeof sessionStoryInputSchema>\nexport type SessionStoryOutput<TSession = unknown> =\n | { sessions: TSession[]; sessionId?: string }\n | { sessionId: string }\n\n/** Generic over the host's own session-descriptor shape — see\n * sessions-panel/index.ts's SessionsPanelOps for why. */\nexport interface SessionStoryOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the sessions registry so execute() doesn't need\n * an AppContext (agentproto has no userId/guildId concept).\n */\nexport function makeSessionStoryPanelApp<TSession = unknown>(\n ops: SessionStoryOps<TSession>,\n): AgnoMcpApp<SessionStoryInput, SessionStoryOutput<TSession>> {\n return {\n id: \"agentproto_session_story\",\n title: \"Session Story\",\n description:\n \"Open the session story panel — a readable, per-session timeline for \" +\n \"two audiences at once: a plain-language summary of every step for \" +\n \"beginners, expandable to raw tool-call detail for technical users. \" +\n \"Shows a one-sentence mission, a plan strip of inferred sub-task \" +\n \"chapters, a chapter-segmented feed, and a composer to keep driving \" +\n \"the session. Polls live data and lets you jump to any step.\",\n inputSchema: sessionStoryInputSchema,\n execute: async input =>\n input.sessionId\n ? { sessionId: input.sessionId }\n : { sessions: ops.listSessions(\"all\") },\n html: SESSION_STORY_PANEL_HTML,\n }\n}\n\nexport const sessionStoryApp: AppHandle = defineApp({\n id: \"@agentproto/session-story\",\n name: \"Session Story\",\n description:\n \"Open the session story panel — a readable, per-session timeline for two audiences at once: \" +\n \"a plain-language summary of every step for beginners, expandable to raw tool-call detail for \" +\n \"technical users.\",\n agents: [],\n ui: {\n html: SESSION_STORY_PANEL_HTML,\n title: \"Session Story\",\n tools: [\"session_list\", \"agent_export\", \"agent_prompt\"],\n },\n})\n","/**\n * HTML bundle for the agentproto live-session widget — `LIVE_SESSION_HTML`,\n * a two-pane panel (left: live session tree, right: streaming timeline)\n * that lets a host attach to any running agent session and watch it work in\n * real time. Mounted via the `live_session` tool defined in ./index.ts.\n *\n * Kept in its own module, importing nothing beyond plain TS, so a consumer\n * that only needs the HTML (e.g. packages/vscode's story-panel-style\n * webviews, or a future live-session webview) can import it via the\n * `@agentproto/apps/live-session/panel` subpath without pulling in\n * `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from `./index.js` would.\n *\n * Forked structurally from terminal-panel-app.ts: same \"static ui://\n * resource renders with empty initData, widget self-bootstraps via\n * tools/call\" pattern. Unlike the terminal (one WebSocket, one PTY), this\n * widget has two independent data needs — a polled tree snapshot and a\n * per-session event timeline — so it layers on top of the bridge rather\n * than replacing it with a single socket.\n *\n * Data paths (see `.plans/CONTRACT.md` §WP-B):\n * LEFT (tree): poll `app_session_tree` via `callTool` every ~2s.\n * RIGHT (timeline): PRIMARY = `EventSource` on `/sessions/:id/events/stream`\n * (opportunistic — falls back if it doesn't open/emit\n * within ~2.5s); FALLBACK = poll `app_session_events`\n * via `callTool` every ~1.5s, advancing `since`.\n * Both app_* tools are registered by WP-A (`app-pull-tools.ts`) and are\n * `visibility: [\"app\"]`-only, i.e. reachable solely over this bridge.\n *\n * The timeline reducer lives in `live-session/logic.ts` as a pure,\n * dependency-free module so it's unit-testable outside the browser. This\n * file inlines a hand-kept COPY of that same logic into the HTML bundle\n * (see \"INLINED REDUCER COPY\" below) since the widget ships as one\n * self-contained script with no bundler step. Keep the two in sync. Its MCP\n * Apps handshake comes from the shared spec-correct panel bridge.\n *\n * Rendering (WP1): the timeline body is patched INCREMENTALLY — a\n * text-delta that merges into the last row patches only that row's text\n * node (found via `data-row-id`); every other new row is appended with one\n * `insertAdjacentHTML`. A full `innerHTML` reset happens only on session\n * start/`setFocus()` and on display-mode switches. Auto-scroll decision is\n * `isNearBottom` (inlined copy, SPEC §2) captured BEFORE any DOM mutation:\n * stick to bottom when the user was at the bottom, otherwise preserve the\n * read position exactly and surface a \"new messages\" pill.\n *\n * Compact mode (WP3/WP4): when `hostContext.displayMode === 'inline'` (or\n * the viewport is under ~640px) the tree pane collapses into a header\n * `<select>` (WP4) and the timeline renders through `groupAdjacentToolCalls`\n * (inlined copy, SPEC §3) with a colour rail and collapsible tool-call\n * groups. `setFocus()` remains the only session-switch entry point.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport type LiveSessionOutput = {\n /** Echoes the input id, or undefined to let the widget self-discover a\n * focus session client-side from `app_session_tree`. */\n sessionId?: string\n /** The daemon's own HTTP origin — connection recipe for both the SSE\n * stream and the bridge's `tools/call` fallback. */\n httpBaseUrl: string\n}\n\n// ── HTML bundle (zero CDN, self-contained) ──────────────────────────────────\n\nexport function LIVE_SESSION_HTML(initData: LiveSessionOutput): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto live session</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}\n#tree-head{padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}\n#tree-body{flex:1;overflow-y:auto;padding:6px}\n#timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}\n#timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}\n#timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}\n#head-summary{margin-left:auto;font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;white-space:nowrap;overflow:hidden;min-width:0}\n#head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}\n#head-summary .sdot.running{background:var(--green)}\n#head-summary .sdot.grey{background:var(--text3)}\n#head-summary .sdot.error{background:var(--red)}\n#usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}\n#status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}\n#timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}\n#head-selector{display:none;max-width:42%;font:inherit;font-size:12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:2px 4px}\n#new-pill{display:none;position:absolute;right:14px;bottom:12px;z-index:6;background:var(--blue);color:#0d1117;border:none;font-size:11px;font-weight:700;padding:5px 11px;border-radius:12px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4)}\n#new-pill.show{display:block}\n\n/* WP3 compact mode — colour rail + tighter rows; only under body.compact-mode\n (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */\nbody.compact-mode #tree-pane{display:none}\nbody.compact-mode #head-selector{display:inline-block}\nbody.compact-mode #focus-id-label{display:none}\nbody.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}\nbody.compact-mode .row.text{border-left-color:var(--blue)}\nbody.compact-mode .row.tool-call{border-left-color:var(--yellow)}\nbody.compact-mode .row.turn-end{border-left-color:var(--purple)}\nbody.compact-mode .row .body{font-size:12px;margin-top:3px}\nbody.compact-mode .row .rhead{font-size:10px}\ndetails.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}\ndetails.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}\ndetails.tool-group[open]>summary{margin-bottom:6px}\ndetails.tool-group .row{margin-top:5px}\n\n.tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}\n.tnode:hover{background:var(--bg3)}\n.tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}\n.tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.dot.running{background:var(--green)}\n.dot.grey{background:var(--text3)}\n.dot.error{background:var(--red)}\n.tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}\n.tnode .badge{font-size:9px;font-weight:700;color:var(--purple);border:1px solid var(--purple);border-radius:3px;padding:0 4px;flex-shrink:0}\n.tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}\n#tree-empty{padding:12px;color:var(--text3);font-size:12px}\n\n.row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}\n.row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}\n.row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}\n.row.text .kind{color:var(--blue)}\n.row.tool-call .kind{color:var(--yellow)}\n.row.turn-end .kind{color:var(--purple)}\n.row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}\n.row.text .body{font-family:inherit}\n.row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}\n.status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}\n.status-badge.pending{background:var(--bg3);color:var(--text2)}\n.status-badge.ok{background:#0f3d20;color:var(--green)}\n.status-badge.error{background:#3d1418;color:var(--red)}\n.row details{margin-top:6px}\n.row summary{cursor:pointer;font-size:11px;color:var(--text2)}\n.row pre{margin-top:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-word;color:var(--text2);background:var(--bg);border-radius:5px;padding:6px 8px;max-height:200px;overflow:auto}\n.chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}\n#timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"tree-pane\">\n <div id=\"tree-head\">Sessions</div>\n <div id=\"tree-body\"><div id=\"tree-empty\">Loading…</div></div>\n </div>\n <div id=\"timeline-pane\">\n <div id=\"timeline-head\">\n <span class=\"focus-id\" id=\"focus-id-label\">—</span>\n <select id=\"head-selector\" title=\"Sessions\"></select>\n <span id=\"head-summary\"></span>\n <span id=\"status-line\">connecting…</span>\n </div>\n <div id=\"timeline-body\"><div id=\"timeline-empty\">No session focused yet.</div></div>\n <button id=\"new-pill\" type=\"button\">↓ New messages</button>\n </div>\n</div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-live-session\")}\n\n// ============================================================\n// INLINED REDUCER COPY — hand-kept mirror of live-session/logic.ts.\n// Plain JS, same semantics: coalesce consecutive text-delta of the same\n// session (and rejoin an unterminated mid-line fragment split by an\n// interleaved record — see the TS module's text-delta arm), pair\n// tool-call/tool-result by toolCallId, pass through turn-end, keep usage\n// as STATE (SPEC §1: usage leaves the timeline — a usage_update record\n// never produces a row), ignore unknown kinds. Keep in sync with the TS\n// module; the TS module is the one the test suite imports.\n// ============================================================\n\nfunction initialTimelineState() {\n return { rows: [], usage: null };\n}\n\nfunction rowId(record, rows) {\n return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);\n}\n\n// Fold a text-delta record into an existing row (fresh object), keeping the\n// row's \"partial\" hint in step with the latest record — see mergeTextDelta in\n// the TS module.\nfunction mergeTextDelta(row, record) {\n var merged = Object.assign({}, row, {\n text: row.text + (record.text || ''),\n seq: record.seq,\n ts: record.ts,\n });\n if (record.partial === true) merged.partial = true;\n else delete merged.partial;\n return merged;\n}\n\nfunction reduceEvent(state, record) {\n switch (record.kind) {\n case 'text-delta': {\n var last = state.rows[state.rows.length - 1];\n if (last && last.kind === 'text' && last.sessionId === record.sessionId) {\n return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };\n }\n // Debounce can flush an unterminated mid-word fragment (flagged\n // partial), let a tool-call land, then flush the continuation — look\n // back within the same turn (bounded by this session's last turn-end)\n // for that session's most recent text row and continue it in place.\n // Only the explicit partial flag glues: a non-partial record with no\n // trailing newline is the writer's normal end-of-text-block shape.\n for (var i = state.rows.length - 1; i >= 0; i--) {\n var prior = state.rows[i];\n if (prior.sessionId !== record.sessionId) continue;\n if (prior.kind === 'turn-end') break;\n if (prior.kind !== 'text') continue;\n if (prior.partial === true) {\n var patched = state.rows.slice();\n patched[i] = mergeTextDelta(prior, record);\n return { rows: patched, usage: state.usage };\n }\n break;\n }\n var row = {\n kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, text: record.text || '',\n };\n if (record.partial === true) row.partial = true;\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-call': {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '',\n toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-result': {\n var idx = -1;\n for (var i = 0; i < state.rows.length; i++) {\n if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;\n }\n if (idx === -1) {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',\n status: record.isError ? 'error' : 'ok', result: record.result,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n var updated = Object.assign({}, state.rows[idx], {\n status: record.isError ? 'error' : 'ok', result: record.result,\n });\n var rows = state.rows.slice();\n rows[idx] = updated;\n return { rows: rows, usage: state.usage };\n }\n case 'turn-end': {\n var row = {\n kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, reason: record.reason,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'usage_update': {\n // Usage is state, not a row (SPEC §1) — last-write-wins, no merge with\n // the prior snapshot. state.rows is reused as-is (it didn't change).\n // Except the context window: a cost-bearing frame's size sticks (see\n // the TS module's usage_update arm).\n var sizeReported = typeof record.size === 'number' && record.size > 0;\n var authoritative = record.cost !== undefined && sizeReported;\n var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);\n var keepSize = !authoritative && (prevAuth || !sizeReported);\n var usage = {\n size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,\n used: record.used, cost: record.cost,\n tokensIn: record.tokensIn, tokensOut: record.tokensOut,\n seq: record.seq, ts: record.ts,\n };\n if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;\n return { rows: state.rows, usage: usage };\n }\n default:\n return state;\n }\n}\n\n// ============================================================\n// INLINED PURE HELPERS — exact copies of live-session/logic.ts's\n// isNearBottom (SPEC §2) and groupAdjacentToolCalls (SPEC §3). Same names,\n// same signatures, same default thresholds; plain JS because the widget\n// has no import step.\n// ============================================================\n\nvar SCROLL_STICK_THRESHOLD_PX = 24;\n\nfunction isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {\n if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;\n return scrollHeight - scrollTop - clientHeight <= threshold;\n}\n\nvar TOOL_CALL_GROUP_THRESHOLD = 2;\n\n// Collapse runs of \\`threshold\\`+ adjacent tool-call rows into one\n// {kind:'tool-group', rows:[...]} entry; everything else passes through as\n// individual {kind:'row', row} entries, same order as the input.\nfunction groupAdjacentToolCalls(rows, threshold) {\n if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;\n var out = [];\n var run = [];\n function flushRun() {\n if (!run.length) return;\n if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });\n else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });\n run = [];\n }\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }\n flushRun();\n out.push({ kind: 'row', row: rows[i] });\n }\n flushRun();\n return out;\n}\n\n// ============================================================\n// Rendering helpers\n// ============================================================\n\nfunction escHtml(s) {\n return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n}\n\nfunction safeJson(v) {\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n}\n\nfunction statusDotClass(status) {\n if (status === 'running' || status === 'starting') return 'running';\n if (status === 'error' || status === 'killed') return 'error';\n return 'grey';\n}\n\n// SPEC §4 (non-contract guidance): 52524 → \"52.5k\", >=1e6 → \"M\" suffix,\n// one decimal, trailing .0 vanishes because the value stays a number.\n// cost → \"$\" + toFixed(2). Read from timelineState.usage.\nfunction fmtCompactNum(n) {\n if (typeof n !== 'number' || !isFinite(n)) return null;\n if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';\n if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';\n return String(n);\n}\n\nfunction usageChipText(usage) {\n if (!usage) return '';\n var bits = [];\n if (usage.used != null && usage.size != null) {\n bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));\n } else if (usage.used != null) {\n bits.push(fmtCompactNum(usage.used));\n }\n if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));\n return bits.join(' · ');\n}\n\n// WP5: elapsed seconds since the timeline's first row (running only).\nfunction elapsedText(rows, status) {\n if (status !== 'running' && status !== 'starting') return null;\n if (!rows.length || rows[0].ts == null) return null;\n var ms = new Date(rows[0].ts).getTime();\n if (!isFinite(ms)) return null;\n return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';\n}\n\n// ============================================================\n// LEFT pane — live tree\n// ============================================================\n\nvar currentTree = [];\nvar focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;\n// Where the current focus came from: 'pinned' (the tool result that mounted\n// this widget named a session — never auto-switched away), 'auto' (self-\n// discovered newest running session, a guess the host may still correct),\n// 'user' (explicit click/selector choice — outranks everything).\nvar focusSource = focusId ? 'pinned' : null;\nvar bootDone = false;\nvar treeTimer = null;\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId —\n// keep in sync (same convention as the reducer copy below).\nfunction extractToolResultSessionId(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try {\n var body = JSON.parse(item.text);\n if (body && typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (body && typeof body.id === 'string' && body.id) return body.id;\n } catch (_) {}\n return null;\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session instead of leaving\n// pollTree() to self-discover the newest running one, which made every\n// agent_start card show the latest started session rather than its own.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var id = extractToolResultSessionId(params);\n if (!id || focusSource === 'user' || id === focusId) return;\n focusSource = 'pinned';\n if (bootDone) setFocus(id);\n else focusId = id; // boot's own startTimeline(focusId) picks it up\n});\n\nfunction findNode(nodes, id) {\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].id === id) return nodes[i];\n var found = findNode(nodes[i].children || [], id);\n if (found) return found;\n }\n return null;\n}\n\nfunction flattenDfs(nodes, out) {\n out = out || [];\n for (var i = 0; i < nodes.length; i++) {\n out.push(nodes[i]);\n flattenDfs(nodes[i].children || [], out);\n }\n return out;\n}\n\nfunction pickInitialFocus(tree) {\n var flat = flattenDfs(tree);\n var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });\n if (alive.length) return alive[alive.length - 1].id;\n return tree.length ? tree[0].id : null;\n}\n\nfunction renderTreeNode(node, depth) {\n var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');\n var badge = node.isOrchestrator ? '<span class=\"badge\">orch</span>' : '';\n var cls = 'tnode' + (node.id === focusId ? ' focus' : '');\n return '<div class=\"' + cls + '\" data-id=\"' + escHtml(node.id) + '\">' +\n '<span class=\"dot ' + statusDotClass(node.status) + '\"></span>' +\n '<span class=\"label\">' + escHtml(node.label || node.id) + '</span>' + badge +\n '</div>' +\n (childrenHtml ? '<div class=\"tchildren\">' + childrenHtml + '</div>' : '');\n}\n\n// WP4: compact header <select> mirrors the tree; still routes through\n// setFocus() — the only session-switch entry point.\nfunction renderHeadSelector() {\n var sel = document.getElementById('head-selector');\n var flat = flattenDfs(currentTree);\n while (sel.firstChild) sel.removeChild(sel.firstChild);\n if (!flat.length) return;\n for (var i = 0; i < flat.length; i++) {\n var n = flat[i];\n var o = document.createElement('option');\n o.value = n.id;\n o.textContent = (n.id === focusId ? '● ' : '') + (n.label || n.id);\n sel.appendChild(o);\n }\n sel.value = focusId || '';\n}\n\nfunction renderTree() {\n var body = document.getElementById('tree-body');\n var root = focusId ? findNode(currentTree, focusId) : null;\n var renderNodes = root ? [root] : currentTree;\n if (!renderNodes.length) {\n body.innerHTML = '<div id=\"tree-empty\">No sessions.</div>';\n renderHeadSelector();\n return;\n }\n body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');\n var els = body.querySelectorAll('.tnode');\n els.forEach(function(el) {\n el.addEventListener('click', function() {\n var id = el.getAttribute('data-id');\n if (id === focusId) return;\n focusSource = 'user';\n setFocus(id);\n });\n });\n renderHeadSelector();\n}\n\nfunction pollTree() {\n callTool('app_session_tree', {}).then(function(res) {\n currentTree = res.tree || [];\n if (!focusId) {\n focusId = pickInitialFocus(currentTree);\n if (focusId) { focusSource = 'auto'; startTimeline(focusId); }\n }\n renderTree();\n updateHeader();\n }).catch(function() {\n // Leave the last-known tree rendered; the next poll may recover.\n });\n}\n\n// ============================================================\n// RIGHT pane — timeline for focusId\n// ============================================================\n\nvar timelineState = initialTimelineState();\nvar sincePtr = 0;\nvar activeSource = null; // {type:'sse', es} | {type:'poll', timer}\nvar compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)\n\nfunction setStatus(msg) {\n document.getElementById('status-line').textContent = msg;\n}\n\nfunction isCompact() {\n return (getHostContext() && getHostContext().displayMode === 'inline') ||\n (typeof window !== 'undefined' && window.innerWidth < 640);\n}\n\n// WP3/WP4: apply the compact/expanded split. Only re-renders the timeline\n// when the mode actually flips (scroll + <details> state survive otherwise).\nfunction applyDisplayMode() {\n var c = isCompact();\n document.body.classList.toggle('compact-mode', c);\n if (c !== compactMode) {\n compactMode = c;\n renderTimelineFull();\n renderTree();\n }\n}\n\n// WP5 + WP2: one header line — \\`● running · 3 tools · 52.5k/200k · $0.04 · 12s\\`\n// (status dot from the focus node, tool count from the rows, usage from\n// timelineState.usage — no usage_update row anymore) + transport status.\nfunction updateHeader() {\n document.getElementById('focus-id-label').textContent = focusId || '—';\n var rows = timelineState.rows || [];\n var usage = timelineState.usage;\n var tools = 0;\n for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;\n var node = focusId ? findNode(currentTree, focusId) : null;\n var st = node ? node.status : null;\n var parts = [];\n if (st) parts.push('<span class=\"sdot ' + statusDotClass(st) + '\"></span>' + escHtml(st));\n if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));\n var chip = usageChipText(usage);\n if (chip) parts.push('<span id=\"usage-chip\" class=\"chip\">' + escHtml(chip) + '</span>');\n var el = elapsedText(rows, st);\n if (el) parts.push(escHtml(el));\n document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' · ') : '';\n}\n\nfunction captureDetailsOpenFlags(body) {\n var dets = body.querySelectorAll('details');\n var flags = [];\n for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);\n return flags;\n}\n\nfunction restoreDetailsOpenFlags(body, flags) {\n var dets = body.querySelectorAll('details');\n for (var i = 0; i < dets.length && i < flags.length; i++) {\n if (flags[i]) dets[i].open = true;\n }\n}\n\n// Full rebuild — used ONLY for the first paint of a session, a setFocus()\n// session switch (SPEC §2: full reset is correct there), a display-mode\n// flip, and as the fallback for shapes the incremental patcher can't patch.\n// A display-mode flip re-renders the SAME content (WP3: \"réactif... sans\n// perdre le scroll ni l'état des <details>\"), so open <details> and the\n// exact scroll offset (not just the at-bottom decision) survive the rebuild.\nfunction renderTimelineFull() {\n var body = document.getElementById('timeline-body');\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var savedScrollTop = body.scrollTop;\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;\n updateHeader();\n}\n\nfunction buildTimelineHtml() {\n var rows = timelineState.rows;\n if (!rows.length) return '<div id=\"timeline-empty\">No events yet.</div>';\n if (compactMode) {\n var entries = groupAdjacentToolCalls(rows);\n var html = '';\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i];\n if (e.kind === 'tool-group') {\n html += '<details class=\"tool-group\"><summary>▸ ' + e.rows.length + ' tool calls</summary>' +\n e.rows.map(renderRow).join('') + '</details>';\n } else {\n html += renderRow(e.row);\n }\n }\n return html;\n }\n return rows.map(renderRow).join('');\n}\n\n// ── WP1 incremental patch (dense mode) ─────────────────────────────────\n// One record, one minimal DOM mutation. The common case (text-delta merged\n// into the LAST rendered row) patches just that row's text node. Anything\n// else appends via insertAdjacentHTML. Only rare in-place replacements\n// (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.\nfunction applyRecordToDom(prevRows) {\n var body = document.getElementById('timeline-body');\n // Capture \"stuck to bottom?\" BEFORE any mutation (SPEC §2).\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var rows = timelineState.rows;\n\n if (compactMode) {\n // Inline mode: smaller rows — full grouped rebuild, preserving\n // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM\n // (and the scroll/pill decision) when the rows actually changed — a\n // usage_update leaves rows === prevRows, so it must fall through to the\n // header-only path below, same as the dense-mode branch.\n if (rows !== prevRows) {\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n }\n updateHeader();\n return;\n }\n\n var appended = rows.length > prevRows.length;\n if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {\n // First real paint of this session (empty placeholder present) or an\n // unexpected shape — full reset is correct/cheapest.\n body.innerHTML = buildTimelineHtml();\n body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n\n if (appended) {\n body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));\n } else if (rows.length === prevRows.length && rows.length > 0) {\n // Exactly one row object replaced (tool-result merge, partial-text\n // re-merge via the reducer's lookback). Patch that one element only.\n // Scan from the tail: the hot path (a text-delta merged into the LAST\n // row, the dominant streaming case) is found in O(1) this way instead\n // of walking the whole array — the rare mid-array lookback re-merge\n // still resolves correctly, just slower.\n var idx = -1;\n for (var i = rows.length - 1; i >= 0; i--) {\n if (rows[i] !== prevRows[i]) { idx = i; break; }\n }\n var patched = false;\n if (idx >= 0) {\n var row = rows[idx];\n var el = body.querySelector('[data-row-id=\"' + escHtml(row.id) + '\"]');\n if (el) {\n if (row.kind === 'text' && idx === rows.length - 1) {\n // High-frequency case: patch the existing text node in place.\n var b = el.querySelector('.body');\n if (b) { b.textContent = row.text; patched = true; }\n }\n if (!patched) { el.outerHTML = renderRow(row); patched = true; }\n }\n }\n if (!patched) {\n // Multiple rows changed at once (shouldn't happen per-record) — rebuild.\n body.innerHTML = buildTimelineHtml();\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n } else {\n // rows unchanged (usage_update) — header only.\n updateHeader();\n return;\n }\n\n // Scroll decision AFTER the mutation: stick or leave the position alone.\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n updateHeader();\n}\n\nfunction samePrefix(prevRows, rows) {\n for (var i = 0; i < prevRows.length; i++) {\n if (prevRows[i] !== rows[i]) return false;\n }\n return true;\n}\n\nfunction showNewPill() {\n document.getElementById('new-pill').classList.add('show');\n}\n\nfunction hideNewPill() {\n document.getElementById('new-pill').classList.remove('show');\n}\n\nfunction renderRow(row) {\n if (row.kind === 'text') {\n return '<div class=\"row text\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">text</span></div>' +\n '<div class=\"body\">' + escHtml(row.text) + '</div></div>';\n }\n if (row.kind === 'tool-call') {\n var badgeCls = row.status === 'pending' ? 'pending' : row.status;\n var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');\n var resultHtml = row.status !== 'pending'\n ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'\n : '';\n return '<div class=\"row tool-call\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">tool</span>' +\n '<span class=\"toolname\">' + escHtml(row.toolName) + '</span>' +\n '<span class=\"status-badge ' + badgeCls + '\">' + badgeText + '</span></div>' +\n '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +\n resultHtml + '</div>';\n }\n if (row.kind === 'turn-end') {\n return '<div class=\"row turn-end\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">turn end</span>' +\n '<span class=\"chip\">' + escHtml(row.reason || '—') + '</span></div></div>';\n }\n return '';\n}\n\nfunction teardownTimeline() {\n if (activeSource) {\n if (activeSource.type === 'sse' && activeSource.es) {\n try { activeSource.es.close(); } catch (e) {}\n }\n // Both the poll re-arm timer AND the SSE open/first-message fallback\n // timer must be cleared, or a focus switch within the 2.5s window leaves\n // a stale timer that fires startPolling for the abandoned session.\n if (activeSource.timer) clearTimeout(activeSource.timer);\n if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);\n }\n activeSource = null;\n}\n\nfunction attemptSSE(id) {\n var settled = false;\n var es;\n try {\n es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');\n } catch (e) {\n startPolling(id);\n return;\n }\n var src = { type: 'sse', es: es, fallbackTimer: null };\n activeSource = src;\n // Every callback below re-checks focusId===id AND that src is still the\n // live source: a focus switch tears down src and starts a new one, so a\n // late open/message/error from this abandoned EventSource must be a no-op\n // rather than mutating the new focus's timeline or clobbering activeSource.\n function stale() { return activeSource !== src || focusId !== id; }\n src.fallbackTimer = setTimeout(function() {\n if (settled || stale()) return;\n settled = true;\n try { es.close(); } catch (e) {}\n startPolling(id);\n }, 2500);\n es.addEventListener('open', function() {\n if (settled || stale()) return;\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n });\n es.onmessage = function(evt) {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n }\n var rec;\n try { rec = JSON.parse(evt.data); } catch (e) { return; }\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, rec);\n if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n };\n es.onerror = function() {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n try { es.close(); } catch (e) {}\n startPolling(id);\n } else {\n // Do not leave EventSource to perform its native reconnect: this\n // endpoint replays from since=0 when no cursor is provided, so a\n // transparent reconnect would duplicate every row already reduced.\n // The bridge poll resumes from sincePtr instead and preserves the\n // exactly-once cursor contract.\n try { es.close(); } catch (e) {}\n startPolling(id);\n }\n };\n}\n\nfunction startPolling(id) {\n activeSource = { type: 'poll', timer: null };\n setStatus('polling');\n function tick() {\n if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;\n callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {\n var events = res.events || [];\n for (var i = 0; i < events.length; i++) {\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, events[i]);\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n }\n if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;\n setStatus('polling');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n }).catch(function() {\n setStatus('disconnected');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n });\n }\n tick();\n}\n\nfunction startTimeline(id) {\n teardownTimeline();\n timelineState = initialTimelineState();\n sincePtr = 0;\n hideNewPill();\n renderTimelineFull();\n setStatus('connecting…');\n attemptSSE(id);\n}\n\nfunction setFocus(id) {\n focusId = id;\n renderTree();\n startTimeline(id);\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n var init = window.__APP_INIT__ || {};\n if (init.httpBaseUrl) return init;\n // Static ui:// resources render once at server-registration time with\n // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) — fall back to\n // calling the tool ourselves over the bridge, same as the other panels.\n return callTool('live_session', {}).then(function(result) {\n window.__APP_INIT__ = result;\n return result;\n });\n}).then(function(init) {\n // Only override the focus when live_session itself named a session AND a\n // tool-result notification didn't already pin one while the fallback\n // tools/call above was in flight — the host's push names the exact\n // session this widget was mounted for, so it outranks the fallback's\n // (argument-less, hence session-less in practice) self-call.\n if (init.sessionId && focusSource !== 'pinned') {\n focusId = init.sessionId;\n focusSource = 'pinned';\n }\n onHostContext(function() {\n applyDisplayMode();\n updateHeader();\n });\n document.getElementById('head-selector').addEventListener('change', function(evt) {\n var id = evt.target.value;\n if (id && id !== focusId) { focusSource = 'user'; setFocus(id); }\n });\n document.getElementById('new-pill').addEventListener('click', function() {\n var body = document.getElementById('timeline-body');\n body.scrollTop = body.scrollHeight;\n hideNewPill();\n });\n document.getElementById('timeline-body').addEventListener('scroll', function() {\n // User scrolled back near the bottom — the pill is stale, hide it.\n var el = document.getElementById('timeline-body');\n if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();\n });\n window.addEventListener('resize', function() {\n var c = isCompact();\n if (c !== compactMode) { applyDisplayMode(); }\n });\n applyDisplayMode();\n bootDone = true;\n pollTree();\n treeTimer = setInterval(pollTree, 2000);\n setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh\n if (focusId) startTimeline(focusId);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n});\n</script>\n</body>\n</html>`\n}\n","/**\n * McpApp definition for the live-session widget — a two-pane panel (left:\n * live session tree, right: streaming timeline) that lets a host attach to\n * any running agent session and watch it work in real time.\n *\n * The HTML bundle itself (`LIVE_SESSION_HTML`, its wire protocol, the\n * inlined timeline-reducer copy, compact-mode rendering, …) lives in\n * ./panel.ts, which imports nothing beyond plain TS — see that file for the\n * full protocol/rendering doc. Kept separate so a consumer that only needs\n * the HTML can import `@agentproto/apps/live-session/panel` without pulling\n * in `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from this file would.\n *\n * This module also exports `liveSessionApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * Its `ui.html` is a static snapshot (`LIVE_SESSION_HTML` called with the\n * default `httpBaseUrl`) since `AppUiDefinition.ui.html` must be a plain\n * string; it's separate from `makeLiveSessionApp` above, which the daemon\n * mounts directly at boot with the real daemon origin baked in per-call.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\nimport { LIVE_SESSION_HTML, type LiveSessionOutput } from \"./panel.js\"\n\nexport { LIVE_SESSION_HTML }\nexport type { LiveSessionOutput }\n\nexport const liveSessionInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Attach the widget to an existing session by id or name. Omit to \" +\n \"self-discover the newest running session from the tree.\",\n ),\n})\n\nexport type LiveSessionInput = z.infer<typeof liveSessionInputSchema>\n\nexport interface LiveSessionOps {\n /** The daemon's own HTTP origin, e.g. \"http://127.0.0.1:18790\". Defaults\n * to the daemon's documented default port (`index.ts:776`) so the app\n * works out of the box in the common single-daemon case. */\n httpBaseUrl?: string\n}\n\n/**\n * Factory: close over the daemon's own HTTP origin so execute() needs\n * nothing beyond the tool input (no registry access — mirrors\n * sessions-panel/index.ts and runtime's terminal-panel-app.ts; the app_*\n * tools this widget calls over the bridge are what actually touch the\n * registry).\n */\nexport function makeLiveSessionApp(\n ops?: LiveSessionOps,\n): AgnoMcpApp<LiveSessionInput, LiveSessionOutput> {\n const httpBaseUrl = ops?.httpBaseUrl ?? \"http://127.0.0.1:18790\"\n // `new URL(...).origin` normalises the base URL to the exact connectDomains\n // entry the CSP allowlist needs (same derivation as terminal-panel-app.ts's\n // wsOrigin), covering both the SSE EventSource and any bridge fallback.\n const httpOrigin = new URL(httpBaseUrl).origin\n\n return {\n id: \"live_session\",\n title: \"Live Session\",\n description:\n \"Open the live session widget — a two-pane view of a running agent \" +\n \"session: a live tree on the left, a streaming timeline (text, tool \" +\n \"calls/results, turn-end) on the right. Omit `sessionId` to \" +\n \"attach to the newest running session; pass one to attach directly.\",\n inputSchema: liveSessionInputSchema,\n execute: async input => ({\n sessionId: input.sessionId,\n httpBaseUrl,\n }),\n html: (initData: LiveSessionOutput) => LIVE_SESSION_HTML(initData),\n csp: { connectDomains: [httpOrigin] },\n }\n}\n\nconst DEFAULT_HTTP_BASE_URL = \"http://127.0.0.1:18790\"\n\nexport const liveSessionApp: AppHandle = defineApp({\n id: \"@agentproto/live-session\",\n name: \"Live Session\",\n description:\n \"Open the live session widget — a two-pane view of a running agent session: a live tree on \" +\n \"the left, a streaming timeline (text, tool calls/results, turn-end) on the right.\",\n agents: [],\n ui: {\n html: LIVE_SESSION_HTML({ httpBaseUrl: DEFAULT_HTTP_BASE_URL }),\n title: \"Live Session\",\n tools: [\"app_session_tree\", \"app_session_events\", \"live_session\"],\n },\n})\n","/**\n * Blob pass-through for the session-chat widget (see ./panel.ts `mountBlob`).\n *\n * Claude Desktop and Codex apply their own CSP to an MCP-Apps widget frame:\n * `frame-src 'self' blob: data:`. Neither merges the resource's declared\n * `_meta.ui.csp.frameDomains` into it (measured 2026-09-18), so a direct\n * `<iframe src=\"http://127.0.0.1:18790/apps/@agentik/session-chat/ui?…\">` is\n * refused host-side before the request ever reaches the daemon. `blob:` IS\n * on that list — so the panel fetches the chat html itself (a fetch, not a\n * frame navigation; the per-boot `?et=` embed token gets it through the\n * daemon's origin gates and PNA preflight), rewrites it with the function\n * below, and mounts the result as a `blob:` document.\n *\n * What the rewrite has to fix, given that the document no longer lives on\n * the daemon origin:\n *\n * 1. `<base href=\"<deep link>\">` right after `<head>`, so every relative\n * url in the app (the injected standalone bridge's `./tool-call`, any\n * relative asset) resolves exactly as it would have on the daemon.\n * NOTE: the base is the document url itself (`…/session-chat/ui?…`),\n * not `…/ui/` — a trailing slash would add a path segment and make\n * `./tool-call` resolve to `…/ui/tool-call`, which routes nowhere.\n * 2. The blob document's origin is opaque (Claude's widget context is a\n * sandboxed frame; the blob inherits that) — every daemon request it\n * makes carries `Origin: null`, which the daemon's browser gates\n * (`guardBrowserOrigin`, `authorizeMcp`, `checkSessionsToken`) refuse\n * unless the request ALSO carries the embed token. The chat UI talks to\n * the daemon with absolute urls (`${DAEMON_URL}/mcp`, `/sessions/*`,\n * its SSE stream via `fetch`, `/sessions/:id/chat`) that we don't\n * control, so a small shim is injected ahead of every app script: it\n * wraps `window.fetch` / `window.EventSource` to append `et=` to any\n * request aimed at the daemon origin, and points\n * `window.__AGENTPROTO_BASEURL__` at the daemon origin the deep link\n * names (so a non-default port works too).\n * 3. The standalone bridge's `fetch(\"./tool-call\"` (runtime\n * app-ui-apps.ts `STANDALONE_REST_BRIDGE_SCRIPT`) is additionally\n * rewritten to the absolute tokened url — belt and braces on top of\n * the shim, and the one rewrite that is exact-string-testable.\n * 4. The shim's FIRST statement posts `{ type: \"agentproto-blob-boot\" }`\n * to its parent. A blob document inherits its creator's CSP, so if the\n * host's widget CSP ever kills inline scripts, that ack never arrives\n * and the panel tears the blob down and falls back to the direct frame\n * + launcher card (panel.ts `armBlobBootProbe`) instead of leaving a\n * silent blank. No CSP `<meta>` is added to the blob html: the\n * creator's CSP wins regardless, it would only be noise.\n *\n * Shipped as a JS string (same shape as ../panel-bridge.ts) so the panel\n * page embeds the exact code the test evaluates — no TS twin to drift from.\n */\n\n/** `postMessage` type the injected shim sends its parent on boot. */\nexport const BLOB_BOOT_MESSAGE_TYPE = \"agentproto-blob-boot\"\n\nexport function blobEmbedScript(): string {\n return `// ── Blob pass-through transform (shared: session-chat/blob-embed.ts) ──\nfunction _blobEscapeAttr(s){ return String(s).replace(/&/g, '&').replace(/\"/g, '"'); }\n// Rewrite the fetched chat html so it works as a blob: document: <base> on\n// the deep link, a fetch/EventSource shim that carries the embed token to\n// the daemon origin, and the standalone bridge's ./tool-call made absolute.\nfunction transformChatHtmlForBlob(html, url, token){\n var origin = new URL(url).origin;\n var tc = new URL('./tool-call', url);\n tc.searchParams.set('et', token);\n // The app-visible query for the blob doc: the deep link's search minus the\n // embed token (the token must not ride the app's visible params). The\n // blob document's own location has no params, so this is the ONLY channel\n // for ?session= / ?embed=1 — the app reads it via bootSearchParams().\n var bootUrl = new URL(url);\n bootUrl.searchParams.delete('et');\n var bootQuery = bootUrl.search;\n var shim = '<script>(function(){'\n + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, \"*\"); } catch (_) {}'\n + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'\n + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'\n + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'\n + 'function _sameOrLoc(u){'\n + 'try {'\n + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'\n + 'return x.origin === window.location.origin ? u : window.location.href;'\n + '} catch (_) { return window.location.href; }'\n + '}'\n + '_h.replaceState = function(s, t, u){'\n + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + '_h.pushState = function(s, t, u){'\n + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'\n + '};'\n + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'\n + 'function withEt(u){'\n + 'try {'\n + 'var x = new URL(String(u), document.baseURI);'\n + 'if (x.origin !== ORIGIN) return null;'\n + 'if (!x.searchParams.has(\"et\")) x.searchParams.set(\"et\", TOKEN);'\n + 'return x.href;'\n + '} catch (_) { return null; }'\n + '}'\n + 'var _fetch = window.fetch;'\n + 'if (typeof _fetch === \"function\") {'\n + 'window.fetch = function(input, init){'\n + 'var r;'\n + 'if (typeof Request !== \"undefined\" && input instanceof Request) {'\n + 'r = withEt(input.url);'\n + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'\n + '} else {'\n + 'r = withEt(input);'\n + 'if (r) input = r;'\n + '}'\n + 'return _fetch.call(window, input, init);'\n + '};'\n + '}'\n + 'var _ES = window.EventSource;'\n + 'if (typeof _ES === \"function\") {'\n + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'\n + 'window.EventSource.prototype = _ES.prototype;'\n + '}'\n + '})();<\\\\/script>';\n var head = '<base href=\"' + _blobEscapeAttr(url) + '\">' + shim;\n var m = html.match(/<head[^>]*>/i);\n var out = m\n ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)\n : head + html;\n return out.split('fetch(\"./tool-call\"').join('fetch(' + JSON.stringify(tc.href));\n}`\n}\n","/**\n * HTML bundles for the session-chat builtin widget — the thin bridge page\n * (`sessionChatEmbedHtml`) and the not-installed notice\n * (`SESSION_CHAT_FALLBACK_HTML`) served as the\n * `ui://agentproto_session_chat/view` resource by `registerMcpApps` (see\n * ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via the\n * `agentproto_session_chat` tool defined in ./index.ts, AND bound to\n * `agent_start` (runtime agent-tools.ts `_meta.ui.resourceUri`) so a launch\n * card opens the chat of the session it just spawned.\n *\n * Deliberately NOT a chat UI reimplementation: the real chat lives in the\n * installed `@agentik/session-chat` app, served by the daemon's standalone\n * app host at `GET /apps/@agentik/session-chat/ui`. When the app is\n * installed, this page is a full-viewport iframe of that URL deep-linked\n * with `?session=<id>&embed=1` — the exact spelling the daemon's standalone\n * route relaxes its `frame-ancestors 'none'` for (see runtime\n * http-server.ts handleAppUiPage) and the VS Code chat-panel webview\n * already uses. No second chat bundle exists anywhere in agentproto/ts.\n *\n * Self-bootstrapping (same pattern as live-session/panel.ts): the static\n * ui:// resource is rendered ONCE at server-registration time with EMPTY\n * initData (mcp-apps-adapter.ts registerMcpApps), so the deep link can't\n * be baked in. The page resolves it at runtime, in priority order:\n * 1. `initData.url` when the html was rendered with a real tool output\n * (tests, and any host that re-renders per call) — iframe is inline.\n * 2. The host's `ui/notifications/tool-result` push of the tool call that\n * mounted the widget. An `agent_start` result IS the spawned session's\n * descriptor (`{ id: \"sess_…\", … }`) → the widget calls\n * `agentproto_session_chat({ sessionId })` over the bridge to turn that\n * id into the deep link; an `agentproto_session_chat` result already\n * carries `{ installed, url }` and mounts directly.\n * 3. Otherwise (opened bare from the catalog) `agentproto_session_chat({})`\n * → the app's session picker.\n * Both orders of (2) vs. bridge init are handled: a result that lands\n * before `initBridge()` resolves is held and applied once it does.\n *\n * Mount order (see `mount()` below):\n * 1. Blob pass-through, when a per-boot embed token is baked in (i.e. an\n * MCP-Apps host rendered this resource): fetch the chat html with the\n * token, rewrite it (./blob-embed.ts `transformChatHtmlForBlob`) and\n * mount it as a `blob:` document. Claude Desktop / Codex widget frames\n * run `frame-src 'self' blob: data:` and do not merge our declared\n * `csp.frameDomains`, so a direct daemon iframe is refused host-side —\n * `blob:` is the pass-through. The blob document must post a boot ack\n * within a settle window (`armBlobBootProbe`); a missing ack (host CSP\n * killed its inline scripts, or refused the blob frame) tears it down\n * and falls through to 2.\n * 2. Direct-src iframe of the deep link (VS Code HTTP-iframe panels, a\n * standalone tab — no token, and their origin already passes the\n * daemon's checks; or the blob path's fallback). If the host refuses\n * the frame, `armBlockProbe` removes it and the launcher card becomes\n * the interactive surface.\n *\n * A persistent header with the deep link stays visible above the iframe, so\n * if the frame is refused (older daemon without the embed relaxation, or a\n * host whose own CSP blocks daemon frames) the user still has a one-click\n * path to the chat in a tab. When the app is NOT installed the page is a\n * small readable notice pointing at `agentproto app install` — the\n * fallback branch, never a vendored UI.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\nimport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\nimport type { SessionChatOutput } from \"./index.js\"\n\n/** Only `&` and `\"` are significant inside a double-quoted attribute value. */\nfunction escapeAttr(text: string): string {\n return text.replace(/&/g, \"&\").replace(/\"/g, \""\")\n}\n\nexport function sessionChatEmbedHtml(initData: Partial<SessionChatOutput>): string {\n const url = typeof initData.url === \"string\" && initData.url ? initData.url : null\n const notInstalled = initData.installed === false\n const storeUrl =\n typeof initData.storeUrl === \"string\" && initData.storeUrl\n ? initData.storeUrl\n : \"/store?install=%40agentik%2Fsession-chat\"\n const link =\n url != null\n ? `<a href=\"${escapeAttr(url)}\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>`\n : notInstalled\n ? \"app not installed\"\n : \"connecting…\"\n const iframe =\n url != null\n // No inline src: the url mounts through the bridge script below, via\n // withEmbedToken(), so the frame never loads without the per-boot\n // embed proof — whichever render path (static resource or a host\n // re-render with a real initData) produced it.\n ? `<iframe id=\"chat\" title=\"Session Chat\"></iframe>`\n : \"\"\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}\n#bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}\n#bar a{color:#58a6ff}\n#mode{color:#6e7681}\n#stage{position:relative;height:calc(100% - 29px)}\n#chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}\n#card{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:24px}\n#card h2{font-size:15px;font-weight:600}\n#card .sid{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#8b949e;background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 8px}\n#card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}\n#card a{display:inline-block;margin-top:2px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:13px;padding:7px 14px;border-radius:6px;text-decoration:none}\n#card a:hover{border-color:#58a6ff}\n#notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\n#notice.show{display:block}\n#notice h1{font-size:18px;margin-bottom:12px}\n#notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\n#notice p{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"bar\">Session Chat · <span id=\"link\">${link}</span><span id=\"mode\"></span></div>\n<div id=\"stage\">${iframe}<div id=\"card\">\n <h2>Session Chat</h2>\n <div id=\"card-session\" class=\"sid\">—</div>\n <p>Full embed isn't supported by this host — the chat opens in a browser tab.</p>\n <a id=\"card-open\" href=\"#\" target=\"_blank\" rel=\"noreferrer\">Open chat</a>\n</div><div id=\"notice\"${notInstalled ? ' class=\"show\"' : \"\"}>\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"${escapeAttr(storeUrl)}\" target=\"_blank\" rel=\"noreferrer\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n</div></div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-session-chat\")}\n\n${blobEmbedScript()}\n\n// ── Session-chat launcher logic ──────────────────────────────────────────\nvar mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;\nvar pinnedSessionId = null; // session named by the host's tool-result push\nvar pendingUrl = null; // url pushed by the host before the bridge was up\nvar pendingNotInstalled = false;\nvar bridged = false;\nvar cardUrl = null; // deep link once resolved (drives the card button)\nvar cardSessionEl = document.getElementById('card-session');\nvar cardOpenEl = document.getElementById('card-open');\nvar mountGen = 0; // bumps per mount(); async work checks it before touching the DOM\nvar blobUrl = null; // live object url of the blob-mounted chat, if any\nvar blobBootTimer = null;\nvar blobBootOk = false;\n\nfunction escapeHtml(text) {\n return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/\"/g, '"');\n}\n\nfunction setMode(text) {\n document.getElementById('mode').textContent = text ? ' \\\\u00b7 ' + text : '';\n // The chat app is live inside the frame — the launcher bar (title, deep\n // link, mount mode) is noise on top of the real surface. Hide it and let\n // the frame own the full height. Only a failed mount (launcher card /\n // not-installed notice) shows the bar again.\n var bar = document.getElementById('bar');\n var stage = document.getElementById('stage');\n if (bar && stage) {\n bar.style.display = text ? 'none' : '';\n stage.style.height = text ? '100%' : 'calc(100% - 29px)';\n }\n}\n\n// One fresh #chat frame on the stage (over the card), the previous one gone.\nfunction newFrame() {\n var stage = document.getElementById('stage');\n var old = document.getElementById('chat');\n if (old) old.remove();\n var frame = document.createElement('iframe');\n frame.id = 'chat';\n frame.title = 'Session Chat';\n stage.insertBefore(frame, stage.firstChild);\n return frame;\n}\n\nfunction revokeBlob() {\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n if (blobUrl) {\n try { URL.revokeObjectURL(blobUrl); } catch (_) {}\n blobUrl = null;\n }\n}\n\nfunction mount(url) {\n if (!url || url === mountedUrl) return;\n mountedUrl = url;\n var gen = ++mountGen;\n revokeBlob();\n document.getElementById('notice').className = '';\n setCardUrl(url);\n document.getElementById('link').innerHTML =\n '<a href=\"' + escapeHtml(url) + '\" target=\"_blank\" rel=\"noreferrer\">open in a tab</a>';\n var tokened = withEmbedToken(url);\n if (tokened === url) {\n // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):\n // the direct frame already passes the daemon's origin checks, and the\n // blob path's daemon calls would have nothing to carry — skip it.\n mountDirect(url);\n return;\n }\n mountBlob(url, tokened, gen).catch(function (err) {\n if (gen !== mountGen) return;\n // The fetch itself was refused (host connect-src not merging our\n // connectDomains, PNA, daemon down, …) — probe answered: no blob path.\n console.warn('[session-chat] blob mount failed, falling back to a direct frame:',\n err && err.message ? err.message : String(err));\n mountDirect(url);\n });\n}\n\n// Direct-src iframe of the deep link, with the block probe → launcher card\n// fallback. The terminal mount path when the blob one is unavailable.\nfunction mountDirect(url) {\n var frame = newFrame();\n frame.src = withEmbedToken(url);\n setMode('direct frame');\n armBlockProbe(frame);\n}\n\n// Blob pass-through: fetch the chat html through the embed token, rewrite it\n// for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and\n// mount it — blob: passes a host frame-src that refuses the daemon origin.\nfunction mountBlob(url, tokened, gen) {\n return fetch(tokened, { credentials: 'omit' }).then(function (res) {\n if (!res.ok) throw new Error('HTTP ' + res.status);\n return res.text();\n }).then(function (html) {\n if (gen !== mountGen) return;\n var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);\n blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));\n var frame = newFrame();\n armBlobBootProbe(frame, url, gen);\n frame.src = blobUrl;\n setMode('embedded');\n });\n}\n\n// The blob document's shim posts a boot ack as its first statement. No ack\n// within the settle window means the host refused the blob frame or its\n// inherited CSP killed the document's inline scripts — surface that through\n// the direct-frame path (whose own probe ends on the launcher card) rather\n// than leaving a silent blank.\nwindow.addEventListener('message', function (evt) {\n var d = evt.data;\n if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;\n var frame = document.getElementById('chat');\n if (!frame || evt.source !== frame.contentWindow) return;\n blobBootOk = true;\n if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }\n});\n\nfunction armBlobBootProbe(frame, url, gen) {\n blobBootOk = false;\n blobBootTimer = setTimeout(function () {\n blobBootTimer = null;\n if (gen !== mountGen || blobBootOk) return;\n console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');\n revokeBlob();\n frame.remove();\n mountDirect(url);\n }, 5000);\n}\n\nfunction showNotInstalled() {\n var old = document.getElementById('chat');\n if (old) old.remove();\n revokeBlob();\n setMode('');\n mountedUrl = null;\n document.getElementById('card').style.display = 'none';\n document.getElementById('notice').className = 'show';\n document.getElementById('link').textContent = 'app not installed';\n}\n\n// The body of an ext-apps tool-result notification, whichever wrapping\n// shape the host uses (params IS the CallToolResult, or nests it under\n// params.result). null for error results / non-JSON text.\nfunction parseToolResultBody(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try { return JSON.parse(item.text); } catch (_) { return null; }\n}\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId — keep\n// in sync. \\`sessionId\\` (an agentproto_session_chat / live_session result)\n// wins over \\`id\\` (an agent_start result: the spawned session descriptor).\nfunction extractToolResultSessionId(params) {\n var body = parseToolResultBody(params);\n if (!body) return null;\n if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (typeof body.id === 'string' && body.id) return body.id;\n return null;\n}\n\n// ── Embed-token refresh ──────────────────────────────────────────────────\n// The token baked into this page by registerMcpApps dies with the daemon\n// boot that minted it (runtime embed-tokens.ts), but hosts cache the\n// rendered widget: Claude Desktop keeps a conversation's srcdoc, so after\n// any daemon restart the cached page replays a dead token and every daemon\n// call 403s — the blob fetch fails, the direct frame is refused, and the\n// user is left on the launcher card forever. Every tool result carries a\n// live token (SessionChatOutput.embedToken), so adopt it before mounting.\n//\n// Only a render that WAS baked refreshes. A standalone tab or VS Code\n// HTTP-iframe panel still holds the literal placeholder and passes the\n// daemon's origin checks on its own; handing it a token would switch it\n// onto the blob path for no reason.\nfunction adoptEmbedToken(body) {\n var cur = window.__AGENPROTO_EMBED_TOKEN__;\n if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;\n if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;\n window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;\n}\n\n// Turn a session id into the installed app's deep link over the bridge.\nfunction resolveSession(sessionId) {\n noteSession(sessionId);\n return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})\n .then(function(out) {\n adoptEmbedToken(out);\n if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);\n else showNotInstalled();\n })\n .catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n });\n}\n\n// ── Launcher card (the frame-blocked fallback UI) ────────────────────────\n// The iframe mounts OVER the card. When the host's own CSP refuses the\n// frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:\n// data: and do not yet merge csp.frameDomains), the frame stays on a\n// same-origin about:blank — readable, unlike the loaded cross-origin chat —\n// and is removed after a settle window so the card becomes the interactive\n// surface.\n//\n// The anchor carries target=\"_blank\" rel=\"noreferrer\" as the fallback path,\n// but that is a plain no-op in hosts that sandbox the widget iframe without\n// allow-popups (observed in Claude Desktop's side chat) — a normal click\n// on such a host does nothing. Where the host advertises the capability\n// (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,\n// captured by the bridge's getHostCapabilities()), route through the\n// host-mediated ui/open-link request instead, which is not subject to the\n// iframe's popup sandboxing.\ncardOpenEl.addEventListener('click', function (evt) {\n if (!cardUrl) { evt.preventDefault(); return; }\n if (getHostCapabilities() && getHostCapabilities().openLinks) {\n evt.preventDefault();\n openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });\n }\n});\n\nfunction noteSession(id) {\n if (id && cardSessionEl) cardSessionEl.textContent = id;\n}\n\nfunction setCardUrl(u) {\n if (!u) return;\n cardUrl = u;\n cardOpenEl.setAttribute('href', u);\n}\n\n// A blocked frame never leaves about:blank (same-origin with this page —\n// readable); the loaded chat is cross-origin (the probe throws). A readable\n// frame that is STILL blank when the settle window closes = blocked.\nfunction isBlank(frame) {\n try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }\n}\n\nfunction armBlockProbe(frame) {\n var settle = null;\n function probe() {\n if (settle) { clearTimeout(settle); settle = null; }\n if (!isBlank(frame)) return;\n // Still on about:blank right now — arm the settle window, but\n // re-check when it closes: a slow (not blocked) load may have landed\n // in the meantime, and only a frame STILL blank after the window gets\n // removed. load firing in between clears this timeout above.\n settle = setTimeout(function () {\n settle = null;\n if (isBlank(frame)) frame.remove();\n }, 2000);\n }\n setTimeout(function () {\n frame.addEventListener('load', probe);\n probe();\n }, 100);\n}\n\n// A url baked at render time (a host that re-renders per call with a real\n// initData) mounts on boot — through the same blob-first path as every\n// other mount, since the same opaque widget context frames it. The\n// pre-rendered #chat frame is replaced by mount()'s own.\nif (mountedUrl) {\n var bootUrl = mountedUrl;\n mountedUrl = null;\n adoptEmbedToken(window.__APP_INIT__);\n mount(bootUrl);\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var body = parseToolResultBody(params);\n if (!body) return;\n if (typeof body.url === 'string' && body.url && body.installed !== false) {\n // Adopt now, not at mount time: the token doesn't depend on the bridge.\n adoptEmbedToken(body);\n if (bridged) mount(body.url); else pendingUrl = body.url;\n return;\n }\n if (body.installed === false) {\n if (bridged) showNotInstalled(); else pendingNotInstalled = true;\n return;\n }\n var id = extractToolResultSessionId(params);\n if (!id || id === pinnedSessionId) return;\n pinnedSessionId = id;\n noteSession(id);\n if (bridged) resolveSession(id);\n});\n\ninitBridge().then(function() {\n bridged = true;\n if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }\n if (pendingNotInstalled) { showNotInstalled(); return; }\n if (mountedUrl) return; // rendered with a real url\n var init = window.__APP_INIT__ || {};\n if (init.installed === false) { showNotInstalled(); return; }\n // No url baked in (static ui:// render): resolve over the bridge — the\n // pinned session when the host named one, else the app's picker.\n return resolveSession(pinnedSessionId);\n}).catch(function(err) {\n document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));\n});\n</script>\n</body>\n</html>`\n}\n\nexport const SESSION_CHAT_FALLBACK_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Session Chat — not installed</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3}\n#wrap{max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}\nh1{font-size:18px;margin-bottom:12px}\ncode{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}\np{margin:8px 0;color:#8b949e;font-size:13px}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n <h1>Session Chat is not installed</h1>\n <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app —\n it does not bundle the chat UI itself.</p>\n <p>To use it, install the app into this daemon:</p>\n <p><code>agentproto app install @agentik/session-chat</code></p>\n <p>or open <a href=\"/store?install=%40agentik%2Fsession-chat\">the App Store</a>\n (the daemon's <code>/store</code> panel, with the entry pre-selected).</p>\n <p>Once installed, call <code>agentproto_session_chat</code> again (optionally with a\n <code>sessionId</code>) and the panel will embed the app's chat UI deep-linked to that\n session.</p>\n</div>\n</body>\n</html>`\n","/**\n * McpApp definition for the session-chat builtin widget — a thin launcher\n * for the installed `@agentik/session-chat` app's UI, exposed as a BUILTIN\n * daemon panel so MCP-Apps clients (and the VS Code appPanel surface) can\n * open the chat from `app_catalog` without installing anything into the\n * MCP surface itself.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * live-session — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Serving approach (decided; see the PR description for the tradeoffs):\n * (a) App installed → the tool returns the app's standalone deep-link\n * (`GET /apps/@agentik/session-chat/ui?session=<id>&embed=1`) and the\n * panel HTML is a thin full-viewport iframe of it. No duplicate chat\n * bundle: the UI is the studio-built app, served by the daemon's\n * standalone app host. The daemon relaxes that route's\n * `frame-ancestors 'none'` only when `?embed=1` is present.\n * (b) App NOT installed → the panel HTML is a small readable notice\n * pointing at `agentproto app install`. Never a vendored/reimplemented\n * chat UI.\n *\n * This module also exports `sessionChatApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install`\n * path. Its `ui.html` is the fallback notice, since `AppUiDefinition.ui.html`\n * is a plain static string and can't know at emit time whether the sibling\n * app is installed; the live-mounted `makeSessionChatApp` below is the one\n * that resolves installed-ness per boot.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\nimport { SESSION_CHAT_FALLBACK_HTML, sessionChatEmbedHtml } from \"./panel.js\"\n\nexport { SESSION_CHAT_FALLBACK_HTML, sessionChatEmbedHtml }\nexport { BLOB_BOOT_MESSAGE_TYPE, blobEmbedScript } from \"./blob-embed.js\"\n\n/** The installed studio app this builtin widget is a launcher for. */\nexport const SESSION_CHAT_APP_ID = \"@agentik/session-chat\"\n\nexport const sessionChatInputSchema = z.object({\n sessionId: z\n .string()\n .optional()\n .describe(\n \"Deep-link the chat to an existing session by id (e.g. `sess_xxx`). \" +\n \"Omit to open the app with its session picker.\",\n ),\n})\n\nexport type SessionChatInput = z.infer<typeof sessionChatInputSchema>\n\nexport interface SessionChatOutput {\n /** Whether the `@agentik/session-chat` app is installed (with a UI) on\n * the daemon this widget is mounted on. */\n installed: boolean\n /** The app's standalone deep-link url when installed, else null. */\n url: string | null\n /** A live per-boot embed token, when the host daemon supplies one (see\n * `SessionChatOps.mintEmbedToken`). The panel adopts it over the token\n * baked into its resource HTML, which dies with the daemon boot that\n * rendered it — hosts that cache the widget per conversation (Claude\n * Desktop) otherwise replay a dead token and 403 forever after a daemon\n * restart. Absent when the deep link is unavailable anyway. */\n embedToken?: string\n /** When NOT installed: the daemon's App Store deep link with the entry\n * pre-selected (absolute, so it works from an MCP host iframe whose own\n * origin is not the daemon's). */\n storeUrl?: string\n}\n\nexport interface SessionChatOps {\n /** The daemon's own HTTP origin, e.g. \"http://127.0.0.1:18790\" — the\n * standalone app host the chat UI is served from. */\n httpBaseUrl: string\n /** Whether the `@agentik/session-chat` app is installed with a `ui`\n * block. The runtime supplies this from its AppRegistry; optional so\n * tests/consumers without a registry can omit it (treated as false). */\n isSessionChatInstalled?: () => boolean\n /** A live per-boot embed token for the panel to adopt (runtime\n * embed-tokens.ts `stableAppEmbedToken`). Optional: consumers without a\n * token registry (tests, docs) omit it and the panel keeps whatever was\n * baked into its HTML. */\n mintEmbedToken?: () => string\n}\n\n/**\n * Deep-link into the installed app's standalone UI — the same url shape the\n * VS Code chat-panel webview iframes (`chatPanelUrl` in\n * packages/vscode/src/commands/sessionView.logic.ts).\n */\nexport function sessionChatAppUrl(httpBaseUrl: string, sessionId?: string): string {\n const base = `${httpBaseUrl.replace(/\\/+$/, \"\")}/apps/${SESSION_CHAT_APP_ID}/ui`\n const params = new URLSearchParams()\n if (sessionId) params.set(\"session\", sessionId)\n params.set(\"embed\", \"1\")\n return `${base}?${params.toString()}`\n}\n\n/** The daemon's App Store (`GET /store`, store plan S6) with this app\n * pre-selected for install. */\nexport function sessionChatStoreUrl(httpBaseUrl: string): string {\n return `${httpBaseUrl.replace(/\\/+$/, \"\")}/store?install=${encodeURIComponent(SESSION_CHAT_APP_ID)}`\n}\n\n/** The daemon origin the widget's host-iframe CSP must allow both as a\n * frame target (the direct-src mount) AND as a connect target (the blob\n * pass-through fetches the chat html, and the blob document's own daemon\n * calls inherit the widget CSP — see ./blob-embed.ts). Same derivation as\n * live-session's connectDomains entry. */\nfunction daemonOrigin(httpBaseUrl: string): string {\n return new URL(httpBaseUrl).origin\n}\n\n/**\n * Factory: close over the daemon origin + the installed-ness check so\n * execute() needs nothing beyond the tool input (no registry access inside\n * the app itself — mirrors live-session/index.ts).\n */\nexport function makeSessionChatApp(\n ops: SessionChatOps,\n): AgnoMcpApp<SessionChatInput, SessionChatOutput> {\n const installed = ops.isSessionChatInstalled?.() ?? false\n return {\n id: \"agentproto_session_chat\",\n title: \"Session Chat\",\n description:\n \"Open the Session Chat app — a chat-thread view of any agentproto \" +\n \"daemon session: full transcript replay, live follow-over SSE, and a \" +\n \"composer that sends prompts into the same session. Requires the \" +\n \"`@agentik/session-chat` app to be installed; without it the panel \" +\n \"shows install instructions. Pass `sessionId` to deep-link straight \" +\n \"into a known session.\",\n inputSchema: sessionChatInputSchema,\n execute: async input => {\n if (!installed) return { installed, url: null, storeUrl: sessionChatStoreUrl(ops.httpBaseUrl) }\n const token = ops.mintEmbedToken?.()\n return {\n installed,\n url: sessionChatAppUrl(ops.httpBaseUrl, input.sessionId),\n // Re-arms a cached widget whose baked token died with an earlier\n // daemon boot (see SessionChatOutput.embedToken).\n ...(token ? { embedToken: token } : {}),\n }\n },\n html: (initData: SessionChatOutput) => sessionChatEmbedHtml(initData),\n // Spec-correct either way (ext-apps McpUiResourceMeta). Measured\n // 2026-09-18: Claude Desktop / Codex do NOT merge frameDomains into\n // their widget frame-src; whether they merge connectDomains is unknown,\n // so the panel probes it at runtime (blob fetch → fallback) rather than\n // assuming either way.\n csp: {\n frameDomains: [daemonOrigin(ops.httpBaseUrl)],\n connectDomains: [daemonOrigin(ops.httpBaseUrl)],\n },\n }\n}\n\nexport const sessionChatApp: AppHandle = defineApp({\n id: \"@agentproto/session-chat-widget\",\n name: \"Session Chat\",\n description:\n \"Open the Session Chat app — a chat-thread view of any agentproto daemon session, deep-linked \" +\n \"by sessionId. A thin builtin launcher for the installed @agentik/session-chat app.\",\n agents: [],\n ui: {\n html: SESSION_CHAT_FALLBACK_HTML,\n title: \"Session Chat\",\n // `session_restart` is what makes an ENDED session (completed / killed /\n // exited / crashed) recoverable from the chat instead of a dead end: the\n // UI offers \"Restart session\" in place of the composer, and the tool\n // resumes the same conversation (ACP-level resume via the adapter session\n // id, fresh spawn as fallback) and returns a NEW session id the chat\n // navigates to. Kept in step with the installed app's own allowlist\n // (`@agentik/session-chat`'s APP.md `ui.tools`) — a tool missing from\n // EITHER list is refused for the surface that reads it.\n tools: [\n \"session_list\",\n \"agent_start\",\n \"adapter_list\",\n \"conversation_read\",\n \"session_restart\",\n // SANDBOX-VISIBILITY-JOIN: lets a \"Devices\" view group sessions by\n // paired host (e.g. a CI reviewer box) instead of only this daemon's\n // own. Read-only. NOTE: the installed `@agentik/session-chat` app's\n // own APP.md `ui.tools` allowlist needs the SAME two names added in\n // lockstep (see the comment above) — that app's source lives outside\n // this repo, so its UI won't actually call these until that's done too.\n \"device_list\",\n \"device_sessions\",\n // Session artifacts (documents, images, ...): the chat's artifact\n // panel lists and pins them, add/get create and open one. Same\n // lockstep rule: the installed app's APP.md `ui.tools` must carry\n // the ones its UI calls (today `session_artifact_list` and\n // `session_artifact_pin`).\n \"session_artifact_add\",\n \"session_artifact_list\",\n \"session_artifact_get\",\n \"session_artifact_pin\",\n ],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/work-board/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-work-board-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui\n *\n * `pnpm --filter @agentproto/apps run build:ui:check` (wired into CI) fails\n * if this file drifts from what the Vite sources actually produce.\n */\n\nexport const WORK_BOARD_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto work board</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-work-board-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n const COLUMN_STATUSES = [\\\"pending\\\", \\\"in_progress\\\", \\\"done\\\", \\\"failed\\\"];\\n const COLUMN_LABELS = {\\n pending: \\\"Pending\\\",\\n in_progress: \\\"In Progress\\\",\\n done: \\\"Done\\\",\\n failed: \\\"Failed\\\"\\n };\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function columnOf(status) {\\n return status === \\\"cancelled\\\" ? \\\"failed\\\" : status;\\n }\\n function verificationTag(task) {\\n const v = task.verification;\\n if (v) {\\n if (v.kind === \\\"gate\\\") return '<span class=\\\"tag t-gate\\\">✓ gate</span>';\\n if (v.kind === \\\"self-report\\\") return '<span class=\\\"tag t-self\\\">self-report</span>';\\n if (v.kind === \\\"human\\\") return '<span class=\\\"tag t-human\\\">human</span>';\\n }\\n if (task.status === \\\"in_progress\\\" && task.verify) {\\n return '<span class=\\\"tag t-gated\\\">gated</span>';\\n }\\n return \\\"\\\";\\n }\\n function ownerTag(task) {\\n if (!task.owner) return '<span class=\\\"tag t-unclaimed\\\">Unclaimed</span>';\\n return `<span class=\\\"tag t-owner\\\">${esc(task.owner)}</span>`;\\n }\\n function actionsFor(task) {\\n if (task.status === \\\"pending\\\") {\\n return [\\n task.owner ? { label: \\\"Start\\\", action: \\\"start\\\", cls: \\\"primary\\\" } : { label: \\\"Claim\\\", action: \\\"claim\\\", cls: \\\"primary\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"in_progress\\\") {\\n return [\\n { label: \\\"Done\\\", action: \\\"done\\\", cls: \\\"primary\\\" },\\n { label: \\\"Fail\\\", action: \\\"fail\\\", cls: \\\"danger\\\" },\\n { label: \\\"Release\\\", action: \\\"release\\\", cls: \\\"\\\" },\\n { label: \\\"Cancel\\\", action: \\\"cancel\\\", cls: \\\"danger\\\" }\\n ];\\n }\\n if (task.status === \\\"done\\\") {\\n return [{ label: \\\"Reopen\\\", action: \\\"reopen\\\", cls: \\\"\\\" }];\\n }\\n return [];\\n }\\n function renderCard(task) {\\n let html = `<div class=\\\"card\\\" data-taskid=\\\"${esc(task.taskId)}\\\">`;\\n html += `<div class=\\\"c-title\\\">${esc(task.title)}</div>`;\\n html += `<div class=\\\"c-id\\\">#${esc(task.taskId)}</div>`;\\n html += `<div class=\\\"c-row\\\">${ownerTag(task)}${verificationTag(task)}`;\\n if (task.status === \\\"cancelled\\\") html += '<span class=\\\"tag t-cancelled\\\">cancelled</span>';\\n html += \\\"</div>\\\";\\n if (task.lastVerifyError) {\\n html += `<div class=\\\"c-err\\\">verify failed: ${esc(task.lastVerifyError)}</div>`;\\n }\\n const actions = actionsFor(task);\\n if (actions.length > 0) {\\n html += '<div class=\\\"c-actions\\\">';\\n for (const a of actions) {\\n html += `<button class=\\\"abtn ${a.cls}\\\" data-taskid=\\\"${esc(task.taskId)}\\\" data-action=\\\"${a.action}\\\">${a.label}</button>`;\\n }\\n html += \\\"</div>\\\";\\n }\\n html += \\\"</div>\\\";\\n return html;\\n }\\n function renderColumns(tasks2) {\\n let html = \\\"\\\";\\n for (const status of COLUMN_STATUSES) {\\n const rows = tasks2.filter((t) => columnOf(t.status) === status);\\n html += `<div class=\\\"col\\\"><div class=\\\"col-hdr\\\"><span>${COLUMN_LABELS[status]}</span><span>${rows.length}</span></div><div class=\\\"col-body\\\">`;\\n html += rows.length === 0 ? '<div class=\\\"empty\\\">No tasks</div>' : rows.map(renderCard).join(\\\"\\\");\\n html += \\\"</div></div>\\\";\\n }\\n return html;\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`work board: missing #${id}`);\\n return el;\\n }\\n function getInput(id) {\\n const el = document.getElementById(id);\\n if (!(el instanceof HTMLInputElement)) throw new Error(`work board: missing input #${id}`);\\n return el;\\n }\\n function isCardAction(value) {\\n switch (value) {\\n case \\\"claim\\\":\\n case \\\"start\\\":\\n case \\\"done\\\":\\n case \\\"fail\\\":\\n case \\\"release\\\":\\n case \\\"cancel\\\":\\n case \\\"reopen\\\":\\n return true;\\n default:\\n return false;\\n }\\n }\\n let boardId = null;\\n let tasks = [];\\n let pollActive = false;\\n function boot() {\\n const columnsEl = getEl(\\\"columns\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const boardIdEl = getEl(\\\"board-id\\\");\\n const boardInputEl = getInput(\\\"board-input\\\");\\n const newTitleEl = getInput(\\\"new-title\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function render() {\\n boardIdEl.textContent = boardId ?? \\\"—\\\";\\n if (!boardInputEl.value) boardInputEl.value = boardId ?? \\\"\\\";\\n columnsEl.innerHTML = renderColumns(tasks);\\n }\\n function findTask(taskId) {\\n return tasks.find((t) => t.taskId === taskId);\\n }\\n function loadBoard() {\\n const args = {\\n includeClosed: true,\\n full: true,\\n ...boardId ? { boardId } : {}\\n };\\n return callTool(\\\"task_list\\\", args).then((data) => {\\n boardId = data.boardId ?? boardId;\\n tasks = data.tasks ?? [];\\n render();\\n setStatus(`${tasks.length} task${tasks.length === 1 ? \\\"\\\" : \\\"s\\\"} · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n }).catch((e) => {\\n setStatus(`Error: ${e.message}`);\\n });\\n }\\n function applyAction(taskId, action) {\\n const task = findTask(taskId);\\n if (!task) return;\\n const base = { taskId: task.taskId, rev: task.rev };\\n let promise;\\n if (action === \\\"claim\\\") {\\n promise = callTool(\\\"task_claim\\\", base);\\n } else if (action === \\\"start\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"in_progress\\\" });\\n } else if (action === \\\"done\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"done\\\" });\\n } else if (action === \\\"fail\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"failed\\\" });\\n } else if (action === \\\"release\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, owner: null });\\n } else if (action === \\\"cancel\\\") {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"cancelled\\\" });\\n } else {\\n promise = callTool(\\\"task_update\\\", { ...base, status: \\\"pending\\\" });\\n }\\n promise.then((result) => {\\n if (\\\"conflict\\\" in result) {\\n setStatus(`Someone else moved \\\"${task.title}\\\" first — refreshed.`);\\n } else if (\\\"error\\\" in result) {\\n setStatus(`Action failed: ${result.error}`);\\n } else {\\n setStatus(`Updated \\\"${task.title}\\\".`);\\n }\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Action failed: ${e.message}`);\\n });\\n }\\n function doPoll() {\\n if (pollActive) return;\\n pollActive = true;\\n loadBoard().finally(() => {\\n pollActive = false;\\n setTimeout(doPoll, 4e3);\\n });\\n }\\n columnsEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-action]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const taskId = btn.getAttribute(\\\"data-taskid\\\");\\n const action = btn.getAttribute(\\\"data-action\\\");\\n if (!taskId || !action || !isCardAction(action)) return;\\n applyAction(taskId, action);\\n });\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadBoard();\\n });\\n getEl(\\\"go-btn\\\").addEventListener(\\\"click\\\", () => {\\n const value = boardInputEl.value.trim();\\n if (!value) return;\\n boardId = value;\\n void loadBoard();\\n });\\n getEl(\\\"add-btn\\\").addEventListener(\\\"click\\\", () => {\\n const title = newTitleEl.value.trim();\\n if (!title) return;\\n const args = boardId ? { title, boardId } : { title };\\n callTool(\\\"task_create\\\", args).then((result) => {\\n if (result.error) {\\n setStatus(`Create failed: ${result.error}`);\\n return void 0;\\n }\\n newTitleEl.value = \\\"\\\";\\n setStatus(`Created \\\"${title}\\\".`);\\n return loadBoard();\\n }).catch((e) => {\\n setStatus(`Create failed: ${e.message}`);\\n });\\n });\\n initBridge().then(() => loadBoard()).then(() => {\\n setTimeout(doPoll, 4e3);\\n }).catch((e) => {\\n setStatus(`Bridge error: ${e.message}`);\\n });\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets.\\n\\n Every rule below the next section reads ONLY the --wb-* custom\\n properties. Each one resolves through TWO mapping sheets, chosen at\\n PAINT time via plain CSS var() fallback — no build step, no JS, so a\\n host that swaps its theme live (VS Code) is never frozen at whatever\\n theme happened to be active when this bundle was built:\\n\\n 1. embedded — VS Code injects a live --vscode-* custom-property set\\n into the webview and swaps it on theme change. Whenever the\\n relevant --vscode-* variable exists, it wins — in a VS Code\\n `srcdoc` relay panel AND (structurally identically) any other host\\n that happened to expose the same names, though none is known to.\\n 2. standalone — the agentproto brand tokens (see\\n projects/agentproto/site/src/app/brand.css), used whenever no\\n --vscode-* variable is set — a plain MCP-Apps host or the daemon's\\n own `/apps/.../ui` tab. `--phos`/`--amber`/`--crimson` are\\n THEME-INVARIANT in brand.css (the phosphor terminal palette), so\\n they're identical in both blocks below; the rest follow brand.css's\\n light/dark split via `prefers-color-scheme` — a standalone tab has\\n no host to pick a theme for it, unlike VS Code's own.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --wb-bg: var(--vscode-editor-background, #f5f6f3);\\n --wb-surface: var(--vscode-sideBar-background, #ffffff);\\n --wb-ink: var(--vscode-editor-foreground, #171a1e);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --wb-border: var(--vscode-panel-border, #dde0da);\\n --wb-accent: var(--vscode-button-background, #0f62fe);\\n\\n /* Status colors — phos/amber/crimson are the theme-invariant terminal\\n palette in brand.css, so light and dark standalone fallbacks match. */\\n --wb-status-pending: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --wb-bg: var(--vscode-editor-background, #101214);\\n --wb-surface: var(--vscode-sideBar-background, #16191d);\\n --wb-ink: var(--vscode-editor-foreground, #e8eaed);\\n --wb-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-border: var(--vscode-panel-border, #272c33);\\n --wb-accent: var(--vscode-button-background, #4589ff);\\n\\n --wb-status-pending: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-in-progress: var(--vscode-charts-yellow, #f0b429);\\n --wb-status-done: var(--vscode-charts-green, #3edc81);\\n --wb-status-failed: var(--vscode-charts-red, #fa4d56);\\n --wb-status-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --wb-status-cancelled-weak: color-mix(in srgb, var(--wb-status-cancelled) 55%, transparent);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--wb-bg);\\n color: var(--wb-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--wb-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n#board-label { font-size: 11px; color: var(--wb-ink-muted); }\\n#board-id { font-weight: 600; color: var(--wb-ink); font-size: 12px; font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace; }\\n#board-input {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n width: 220px;\\n}\\n.tbtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--wb-border); }\\n#new-title {\\n flex: 1;\\n min-width: 120px;\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#columns { flex: 1; display: flex; overflow-x: auto; overflow-y: hidden; }\\n.col {\\n width: 260px;\\n min-width: 220px;\\n flex-shrink: 0;\\n display: flex;\\n flex-direction: column;\\n border-right: 1px solid var(--wb-border);\\n}\\n.col-hdr {\\n padding: 8px 10px;\\n border-bottom: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--wb-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n display: flex;\\n justify-content: space-between;\\n}\\n.col-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }\\n\\n.card {\\n background: var(--wb-surface);\\n border: 1px solid var(--wb-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n}\\n.c-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.c-id {\\n font-size: 10px;\\n color: var(--wb-ink-muted);\\n margin-bottom: 6px;\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.c-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-bottom: 6px; }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-unclaimed { background: color-mix(in srgb, var(--wb-ink-muted) 15%, transparent); color: var(--wb-ink-muted); border: 1px dashed var(--wb-ink-muted); }\\n.t-owner { background: color-mix(in srgb, var(--wb-accent) 15%, transparent); color: var(--wb-accent); }\\n\\n/* Verification tell: gate solid green, self-report amber outline, human\\n blue, a declared verify with no verification yet grey \\\"gated\\\" — never\\n render an unverified done as gate-passed (see render.ts's verificationTag). */\\n.t-gate { background: color-mix(in srgb, var(--wb-status-done) 18%, transparent); color: var(--wb-status-done); }\\n.t-self { background: transparent; color: var(--wb-status-in-progress); border: 1px solid var(--wb-status-in-progress); }\\n.t-human { background: color-mix(in srgb, var(--wb-accent) 18%, transparent); color: var(--wb-accent); }\\n.t-gated { background: color-mix(in srgb, var(--wb-ink-muted) 12%, transparent); color: var(--wb-ink-muted); }\\n.t-cancelled {\\n background: color-mix(in srgb, var(--wb-status-cancelled) 10%, transparent);\\n color: var(--wb-status-cancelled-weak);\\n border: 1px dashed color-mix(in srgb, var(--wb-status-cancelled) 40%, transparent);\\n}\\n\\n.c-err { font-size: 10px; color: var(--wb-status-failed); margin-bottom: 6px; word-break: break-word; }\\n.c-actions { display: flex; flex-wrap: wrap; gap: 4px; }\\n.abtn {\\n background: var(--wb-bg);\\n border: 1px solid var(--wb-border);\\n color: var(--wb-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.abtn:hover { background: var(--wb-border); }\\n.abtn.danger { border-color: var(--wb-status-failed); color: var(--wb-status-failed); }\\n.abtn.primary { border-color: var(--wb-status-done); color: var(--wb-status-done); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--wb-ink-muted);\\n border-top: 1px solid var(--wb-border);\\n background: var(--wb-surface);\\n flex-shrink: 0;\\n}\\n.empty { padding: 16px 8px; color: var(--wb-ink-muted); font-size: 11px; text-align: center; }</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"board-label\\\">Board:</span>\\n <span id=\\\"board-id\\\">—</span>\\n <input id=\\\"board-input\\\" type=\\\"text\\\" placeholder=\\\"tree:<sessionId> or ws:<slug>\\\">\\n <button class=\\\"tbtn\\\" id=\\\"go-btn\\\">Go</button>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n <input id=\\\"new-title\\\" type=\\\"text\\\" placeholder=\\\"New task title…\\\">\\n <button class=\\\"tbtn\\\" id=\\\"add-btn\\\">+ Add</button>\\n </div>\\n <div id=\\\"columns\\\"></div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto work-board panel — the `WORK_BOARD_HTML`\n * served as the `ui://agentproto_work_board/view` resource by\n * `registerMcpApps` (see ../mcp-app-types.ts and runtime's\n * mcp-apps-adapter.ts). Mounted via the `agentproto_work_board` tool defined\n * in ./index.ts.\n *\n * A kanban over the daemon's Task ledger (`packages/runtime/src/\n * task-ledger.ts`) — the one writable multi-party entity the daemon exposes\n * (Activity is a read-only projection; a session has a lifecycle, not an\n * intention). Columns are the ledger's own status enum, folding `cancelled`\n * into the `failed` column (tagged distinctly — see `columnOf` in\n * ./ui/render.ts) rather than adding a fifth column for a status v1 treats\n * as terminal scrap. The board/swimlane selector is the ledger's own board\n * id (`tree:<rootSessionId>` or `ws:<slug>`) — always shown verbatim, never\n * hidden behind a friendly name, per the ledger's scoping model.\n *\n * `WORK_BOARD_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui`\n * (scripts/build-work-board-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:check` (wired into CI) fails if that generated file drifts from\n * what the Vite sources actually produce. See ./ui/vite.config.ts for how\n * the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined as a\n * classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface). Standalone (no host at all, e.g. a top-level\n * `GET /apps/:appId/ui` tab) does NOT connect yet: panel-bridge.ts's\n * handshake has no reply to wait for in that case and hangs on\n * \"Connecting to bridge…\" — a known gap being fixed in panel-bridge.ts\n * itself by a separate change. This app will pick that fix up automatically\n * (rebuild + rebase) once it lands; nothing here needs to special-case it.\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → task_list (`full: true` — the compact projection\n * drops `verification`, which the verification tell needs) on a ~4 s poll\n * – Actions: tools/call → task_claim / task_update / task_create\n *\n * Read-only-safe first: no drag-and-drop. Each card gets the explicit status\n * actions valid from its current state (Claim/Start/Done/Fail/Release/\n * Cancel/Reopen) — a correct read-only-plus-explicit-actions board beats a\n * half-wired drag, per the brief.\n *\n * The verification tell (never render an unverified done as gate-passed):\n * - `verification.kind === \"gate\"` → solid green \"✓ gate\"\n * - `verification.kind === \"self-report\"` → amber outline \"self-report\"\n * - `verification.kind === \"human\"` → blue \"human\"\n * - a declared `verify` with no verification yet → grey \"gated\" tag\n * An owner-less task always reads \"Unclaimed\", never \"pending\" (that word is\n * reserved for the Activity vocabulary's \"blocked\" meaning elsewhere).\n */\n\nimport { WORK_BOARD_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const WORK_BOARD_APP_ID = \"@agentproto/work-board\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeWorkBoardApp`),\n * hence its resource uri `ui://agentproto_work_board/view`. */\nexport const WORK_BOARD_TOOL_ID = \"agentproto_work_board\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host that\n * dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const WORK_BOARD_UI_TOOLS = [\n \"task_list\",\n \"task_claim\",\n \"task_update\",\n \"task_create\",\n] as const\n\nexport const WORK_BOARD_HTML = WORK_BOARD_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto work-board panel — a kanban over the\n * daemon's Task ledger (`packages/runtime/src/task-ledger.ts`), the ONLY\n * writable multi-party entity the daemon exposes (Activity is a read-only\n * projection recomputed on every `list()`; a session has a lifecycle, not an\n * intention).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra), same as sessions-panel /\n * session-chat — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_work_board` is called by the host with an optional\n * `boardId`.\n * 2. execute() answers an initial `{boardId, tasks}` snapshot off the\n * ledger — resolved to the caller's default board when `boardId` is\n * omitted (a session's `tree:<root>`, the operator's `ws:<slug>`).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `task_list` (`full: true` — the compact projection drops\n * `verification`) every ~4 s; writes go through `task_claim` /\n * `task_update` / `task_create`, the same verbs any other caller uses —\n * no second write path.\n *\n * This module also exports `workBoardApp`, a real `defineApp()` `AppHandle`\n * (`agents: []`, UI-only) — the catalog/emit/`app_install` path. It's\n * separate from `makeWorkBoardApp` above, which the daemon mounts directly\n * at boot: that factory closes over the LIVE ledger read, something a\n * static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n WORK_BOARD_APP_ID,\n WORK_BOARD_HTML,\n WORK_BOARD_TOOL_ID,\n WORK_BOARD_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const workBoardInputSchema = z.object({\n boardId: z\n .string()\n .optional()\n .describe(\n \"Board to open — `tree:<rootSessionId>` (a supervisor and its \" +\n \"executors) or `ws:<slug>` (an operator workspace). Omit → the \" +\n \"caller's default board.\",\n ),\n})\n\nexport type WorkBoardInput = z.infer<typeof workBoardInputSchema>\n\nexport interface WorkBoardOutput<TTask = unknown> {\n /** Always echoed back — the ledger's board ids are the swimlane\n * selector, and must be visible rather than hidden behind a friendly\n * name (it's how the operator knows which mission they're looking at). */\n boardId: string\n tasks: TTask[]\n}\n\n/**\n * Generic over the host's own task-record shape — this package doesn't own\n * that type (it lives in @agentproto/runtime's task ledger), so the factory\n * stays agnostic to it and the host supplies the concrete type as a type\n * argument when it wires the app up (mirrors SessionsPanelOps<TSession>).\n */\nexport interface WorkBoardOps<TTask = unknown> {\n /** Full (unprojected) records for a board — NOT `task_list`'s default\n * compact projection, since the verification tell needs `verification`\n * (self-report vs gate vs human), which compact mode omits. Omit\n * `boardId` to resolve the caller's default board. */\n listTasks(boardId?: string): WorkBoardOutput<TTask>\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeSessionsPanelApp.\n */\nexport function makeWorkBoardApp<TTask = unknown>(\n ops: WorkBoardOps<TTask>,\n): AgnoMcpApp<WorkBoardInput, WorkBoardOutput<TTask>> {\n return {\n id: WORK_BOARD_TOOL_ID,\n title: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task \" +\n \"ledger (columns: pending/in_progress/done/failed, cancelled folded \" +\n \"into failed). Swimlane is the ledger's own board id (`tree:<root>` \" +\n \"or `ws:<slug>`). Each card shows its owner (or \\\"Unclaimed\\\") and the \" +\n \"verification tell — a gate-passed done reads visually distinct from \" +\n \"a self-reported one. Pass `boardId` to open a specific board.\",\n inputSchema: workBoardInputSchema,\n execute: async input => ops.listTasks(input.boardId),\n html: WORK_BOARD_HTML,\n }\n}\n\nexport const workBoardApp: AppHandle = defineApp({\n id: WORK_BOARD_APP_ID,\n name: \"Work Board\",\n description:\n \"Open the agentproto work board — a kanban over the daemon's Task ledger, scoped by board id \" +\n \"(`tree:<rootSessionId>` or `ws:<slug>`), with an explicit status action per card.\",\n agents: [],\n ui: {\n html: WORK_BOARD_HTML,\n title: \"Work Board\",\n tools: [...WORK_BOARD_UI_TOOLS],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/store/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-store-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:store\n *\n * `pnpm --filter @agentproto/apps run build:ui:store:check` (wired into CI)\n * fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const STORE_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto app store</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-store-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\").replace(/'/g, \\\"'\\\");\\n }\\n function text(value, fallback) {\\n return value && value.trim() !== \\\"\\\" ? esc(value) : esc(fallback);\\n }\\n function chips(row) {\\n const parts = [];\\n if (row.tier) parts.push(`<span class=\\\"chip\\\">${esc(row.tier)}</span>`);\\n if (row.placement) parts.push(`<span class=\\\"chip\\\">placement: ${esc(row.placement)}</span>`);\\n if (row.requires?.agentprotoVersion) {\\n parts.push(`<span class=\\\"chip\\\">requires ${esc(row.requires.agentprotoVersion)}</span>`);\\n }\\n if (row.requires?.apps && row.requires.apps.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires apps: ${esc(row.requires.apps.join(\\\", \\\"))}</span>`);\\n }\\n if (row.requires?.agents && row.requires.agents.length > 0) {\\n parts.push(`<span class=\\\"chip\\\">requires agents: ${esc(row.requires.agents.join(\\\", \\\"))}</span>`);\\n }\\n return parts.join(\\\"\\\");\\n }\\n function installButton(catalogRow) {\\n return `<button class=\\\"sbtn store-install-btn\\\" data-appid=\\\"${esc(catalogRow.appId)}\\\" data-decision=\\\"install\\\">Install</button>`;\\n }\\n function renderCatalogFeatured(rows) {\\n const featured = rows.filter((r) => r.featured === true);\\n if (featured.length === 0) return \\\"\\\";\\n return featured.map(\\n (r) => `<div class=\\\"card store-card-featured\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`\\n ).join(\\\"\\\");\\n }\\n function renderCatalogAvailable(rows) {\\n const byCategory = /* @__PURE__ */ new Map();\\n for (const r of rows) {\\n if (r.installed) continue;\\n if (r.featured === true) continue;\\n const key = r.category && r.category !== \\\"\\\" ? r.category : \\\"Other\\\";\\n const list = byCategory.get(key);\\n if (list) list.push(r);\\n else byCategory.set(key, [r]);\\n }\\n let out = \\\"\\\";\\n for (const [category, list] of byCategory) {\\n out = out + `<h3 class=\\\"store-category-title\\\">${esc(category)}</h3>`;\\n for (const r of list) {\\n out = out + `<div class=\\\"card store-card-available\\\" id=\\\"store-entry-${esc(r.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(r.name, r.appId)}</div>\\n <div class=\\\"card-desc\\\">${text(r.description, \\\"\\\")}</div>\\n <div class=\\\"card-chips\\\">${chips(r)}</div>\\n <div class=\\\"card-actions\\\">${installButton(r)}</div>\\n </div>`;\\n }\\n }\\n return out;\\n }\\n function renderInstalled(installed, catalogRows, updates) {\\n const updateIds = new Set(updates.map((u) => u.appId));\\n const byId = new Map(catalogRows.map((c) => [c.appId, c]));\\n if (installed.length === 0) return \\\"\\\";\\n return installed.map((app) => {\\n const version = app.version ?? byId.get(app.appId)?.version ?? \\\"\\\";\\n const source = app.source?.kind ?? \\\"local\\\";\\n const hasUi = byId.get(app.appId)?.hasUi === true || app.source?.kind === \\\"local\\\" && app.dir !== void 0;\\n const actions = [\\n `<button class=\\\"sbtn store-resync-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"resync\\\">Update</button>`,\\n `<button class=\\\"sbtn store-uninstall-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"uninstall\\\">Uninstall</button>`,\\n ...hasUi ? [\\n `<button class=\\\"sbtn store-open-btn\\\" data-appid=\\\"${esc(app.appId)}\\\" data-decision=\\\"open\\\">Open</button>`\\n ] : []\\n ];\\n return `<div class=\\\"card store-card-installed\\\" id=\\\"store-entry-${esc(app.appId)}\\\">\\n <div class=\\\"card-title\\\">${text(app.name, app.appId)}</div>\\n <div class=\\\"card-actions-meta\\\"><span class=\\\"card-version\\\">${text(version, \\\"\\\")}</span><span class=\\\"card-source\\\">${esc(source)}</span>${updateIds.has(app.appId) ? `<span class=\\\"badge store-update-badge\\\">Update available</span>` : \\\"\\\"}</div>\\n <div class=\\\"card-actions\\\">${actions.join(\\\"\\\")}</div>\\n </div>`;\\n }).join(\\\"\\\");\\n }\\n function renderSourcesHeader(snapshot2) {\\n if (!snapshot2.warnings || snapshot2.warnings.length === 0) return \\\"\\\";\\n return `<div class=\\\"store-warnings\\\">${snapshot2.warnings.map((w) => `<p class=\\\"store-warning\\\">${esc(w)}</p>`).join(\\\"\\\")}</div>`;\\n }\\n function renderBuiltins(builtinNames) {\\n if (builtinNames.length === 0) return \\\"\\\";\\n const details = `<details class=\\\"store-builtins\\\"><summary>Builtin panels</summary><ul class=\\\"store-builtin-list\\\">${builtinNames.map((n) => `<li class=\\\"store-builtin-item\\\">${esc(n)}</li>`).join(\\\"\\\")}</ul></details>`;\\n return details;\\n }\\n function isAgentappUrl(url) {\\n return url.replace(/\\\\?.*$/, \\\"\\\").replace(/#.*$/, \\\"\\\").toLowerCase().endsWith(\\\".agentapp\\\");\\n }\\n function storeEmpty(catalogRows, installed) {\\n if (catalogRows.length > 0 || installed.length > 0) return \\\"\\\";\\n return `<div class=\\\"store-empty\\\">\\n <p class=\\\"store-empty-title\\\">No apps installed, and the app catalog is empty.</p>\\n <p class=\\\"store-empty-hint\\\">Add a <code>catalog.sources</code> entry to your daemon config, drop an <code>app-catalog.json</code> next to it, or install an app directly from a URL in the form below.</p>\\n </div>`;\\n }\\n function render(snapshot2) {\\n const installed = renderInstalled(snapshot2.installed, snapshot2.catalog, snapshot2.updates);\\n const featured = renderCatalogFeatured(snapshot2.catalog);\\n const available = renderCatalogAvailable(snapshot2.catalog);\\n const builtins = renderBuiltins([\\\"sessions-panel\\\", \\\"agents-overview\\\", \\\"bureau-sessions\\\", \\\"session-story\\\", \\\"live-session\\\", \\\"work-board\\\"]);\\n const sections = [];\\n sections.push(renderSourcesHeader(snapshot2));\\n if (installed !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Installed</h2>`, `<div class=\\\"grill\\\">${installed}</div>`);\\n }\\n if (featured !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Featured</h2>`, featured);\\n }\\n if (available !== \\\"\\\") {\\n sections.push(`<h2 class=\\\"store-section-title\\\">Available</h2>`, `<div class=\\\"grill\\\">${available}</div>`);\\n }\\n if (installed === \\\"\\\" && featured === \\\"\\\" && available === \\\"\\\") {\\n sections.push(storeEmpty(snapshot2.catalog, snapshot2.installed));\\n }\\n sections.push(builtins);\\n return sections.join(\\\"\\\\n\\\");\\n }\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`app store: missing #${id}`);\\n return el;\\n }\\n let snapshot = { catalog: [], installed: [], updates: [] };\\n function setStatus(msg) {\\n getEl(\\\"statusbar\\\").textContent = msg;\\n }\\n function sendInstall(payload) {\\n return callTool(\\n \\\"app_install\\\",\\n payload\\n );\\n }\\n function runInstallConfirm(payload, ask) {\\n sendInstall(payload).then((result) => {\\n if (\\\"needsConfirmation\\\" in result && result.needsConfirmation === true) {\\n if (!ask(result)) return;\\n callTool(\\\"app_install\\\", {\\n ...payload,\\n confirm: result.confirm\\n }).then(() => {\\n setStatus(`Installed ${String(payload.url)}`);\\n return refresh();\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n } else {\\n setStatus(`Installed ${result.appId}`);\\n return refresh();\\n }\\n }).catch((e) => setStatus(`Install failed: ${e.message}`));\\n }\\n function buildInstallPayload(entry) {\\n const source = entry.source;\\n if (!source || typeof source !== \\\"object\\\") return void 0;\\n const s = source;\\n const payload = { url: s.url };\\n if (s.kind === \\\"agentapp\\\") {\\n payload.sha256 = s.sha256;\\n } else {\\n if (s.ref) payload.ref = s.ref;\\n if (s.subdir) payload.subdir = s.subdir;\\n if (s.sha) payload.sha = s.sha;\\n payload.allowBuild = false;\\n }\\n if (entry.catalogUrl) payload.catalogUrl = entry.catalogUrl;\\n return payload;\\n }\\n function describePreview(preview) {\\n const sha = preview.kind === \\\"agentapp\\\" ? preview.sha256 ? `sha256 ${preview.sha256}` : \\\"no pinned digest\\\" : preview.sha ? `commit ${preview.sha}` : \\\"no pinned commit\\\";\\n const buildLine = preview.runsBuildCommand ? \\\"It WILL run the app's ui.build command.\\\" : \\\"It will not run any build command.\\\";\\n return `Install ${preview.url}? (${preview.kind}; ${sha})\\n\\n${buildLine}\\n\\nConfirm to proceed.`;\\n }\\n function armInstall(entry) {\\n const payload = buildInstallPayload(entry);\\n if (!payload) {\\n setStatus(\\\"Catalog entry has no remote source — cannot install.\\\");\\n return;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function installFromUrl(url, sha256, ref, subdir) {\\n const payload = { url };\\n if (isAgentappUrl(url)) {\\n if (sha256) payload.sha256 = sha256;\\n } else {\\n if (ref) payload.ref = ref;\\n if (subdir) payload.subdir = subdir;\\n payload.allowBuild = false;\\n }\\n runInstallConfirm(payload, (preview) => window.confirm(describePreview(preview)));\\n }\\n function resync(appId) {\\n callTool(\\\"app_resync\\\", { appId }).then((result) => {\\n setStatus(result.changed ? `${appId} updated` : `${appId} already up to date`);\\n return refresh();\\n }).catch((e) => setStatus(`Update failed: ${e.message}`));\\n }\\n function uninstall(appId) {\\n if (!window.confirm(`Uninstall ${appId}? Its data directory is kept.`)) return;\\n callTool(\\\"app_uninstall\\\", { appId }).then(() => {\\n setStatus(`Uninstalled ${appId}`);\\n return refresh();\\n }).catch((e) => setStatus(`Uninstall failed: ${e.message}`));\\n }\\n function openUi(appId) {\\n const encoded = appId.split(\\\"/\\\").map(encodeURIComponent).join(\\\"/\\\").replace(/%40/g, \\\"@\\\");\\n window.open(`/apps/${encoded}/ui`, \\\"_blank\\\", \\\"noopener\\\");\\n }\\n let deepLinked = false;\\n function deepLinkInstall() {\\n if (deepLinked) return;\\n const target = new URLSearchParams(window.location.search).get(\\\"install\\\");\\n if (!target) return;\\n deepLinked = true;\\n const entry = snapshot.catalog.find((c) => c.appId === target && !c.installed);\\n if (!entry) return;\\n const el = document.getElementById(`store-entry-${CSS.escape(target)}`);\\n if (el && typeof el.scrollIntoView === \\\"function\\\") el.scrollIntoView({ block: \\\"center\\\" });\\n armInstall(entry);\\n }\\n function refresh() {\\n return callTool(\\\"app_catalog\\\", {}).then((catalogRaw) => {\\n const snapshotRaw = catalogRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.catalog) ? snapshotRaw.catalog : Array.isArray(catalogRaw) ? catalogRaw : [];\\n snapshot.catalog = rows.filter((r) => r && r.appId);\\n return callTool(\\\"app_list\\\", {});\\n }).then((appsRaw) => {\\n const snapshotRaw = appsRaw ?? {};\\n const rows = Array.isArray(snapshotRaw.installed) ? snapshotRaw.installed : Array.isArray(appsRaw) ? appsRaw : [];\\n snapshot.installed = rows.filter((a) => a && a.appId);\\n return callTool(\\\"app_updates\\\", {});\\n }).then((updatesRaw) => {\\n const sr = updatesRaw ?? {};\\n const rows = Array.isArray(sr.updates) ? sr.updates : Array.isArray(updatesRaw) ? updatesRaw : [];\\n snapshot.updates = rows.filter((u) => u && u.appId);\\n renderBody();\\n deepLinkInstall();\\n }).catch((e) => setStatus(`Error: ${e.message}`));\\n }\\n function renderBody() {\\n const content = getEl(\\\"content\\\");\\n content.innerHTML = render(snapshot);\\n }\\n function boot() {\\n const content = getEl(\\\"content\\\");\\n const urlBtn = document.getElementById(\\\"store-install-from-url-btn\\\");\\n const urlInput = document.getElementById(\\\"store-url\\\");\\n const shaInput = document.getElementById(\\\"store-sha256\\\");\\n const refInput = document.getElementById(\\\"store-ref\\\");\\n const subdirInput = document.getElementById(\\\"store-subdir\\\");\\n if (urlBtn && urlInput) {\\n urlBtn.addEventListener(\\\"click\\\", () => {\\n const url = urlInput.value.trim();\\n if (url === \\\"\\\") return;\\n installFromUrl(\\n url,\\n shaInput && shaInput.value.trim() !== \\\"\\\" ? shaInput.value.trim() : void 0,\\n refInput && refInput.value.trim() !== \\\"\\\" ? refInput.value.trim() : void 0,\\n subdirInput && subdirInput.value.trim() !== \\\"\\\" ? subdirInput.value.trim() : void 0\\n );\\n });\\n }\\n content.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const btn = evt.target.closest(\\\"button[data-decision]\\\");\\n if (!(btn instanceof HTMLButtonElement)) return;\\n const appId = btn.getAttribute(\\\"data-appid\\\");\\n if (!appId) return;\\n const decision = btn.getAttribute(\\\"data-decision\\\");\\n if (decision === \\\"install\\\") {\\n const entry = snapshot.catalog.find((c) => c.appId === appId);\\n if (entry) armInstall(entry);\\n } else if (decision === \\\"resync\\\") {\\n resync(appId);\\n } else if (decision === \\\"uninstall\\\") {\\n uninstall(appId);\\n } else if (decision === \\\"open\\\") {\\n openUi(appId);\\n }\\n });\\n void initBridge().then(() => refresh()).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n The store panel's stylesheet — same two-sheet token mapping as\\n work-board's (--vscode-* wins when a host injects them, the agentproto\\n brand fallbacks otherwise; light/dark split via prefers-color-scheme\\n for a standalone tab). See that file's header for the full rationale\\n — the --st-* names here are this panel's own namespace.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --st-bg: var(--vscode-editor-background, #f5f6f3);\\n --st-surface: var(--vscode-sideBar-background, #ffffff);\\n --st-ink: var(--vscode-editor-foreground, #171a1e);\\n --st-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --st-border: var(--vscode-panel-border, #dde0da);\\n --st-accent: var(--vscode-button-background, #0f62fe);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --st-bg: var(--vscode-editor-background, #101214);\\n --st-surface: var(--vscode-sideBar-background, #16191d);\\n --st-ink: var(--vscode-editor-foreground, #e8eaed);\\n --st-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --st-border: var(--vscode-panel-border, #272c33);\\n --st-accent: var(--vscode-button-background, #4589ff);\\n --st-ink-on-accent: var(--vscode-button-foreground, #ffffff);\\n --st-warn: var(--vscode-charts-yellow, #f0b429);\\n }\\n}\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--st-bg);\\n color: var(--st-ink);\\n overflow: hidden;\\n}\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#store-head {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--st-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--st-surface);\\n flex-shrink: 0;\\n font-weight: 600;\\n}\\n\\n#content { flex: 1; overflow-y: auto; padding: 12px; }\\n\\n.store-section-title {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n margin: 14px 0 8px;\\n}\\n.store-section-title:first-child { margin-top: 0; }\\n\\n.store-category-title { font-size: 12px; font-weight: 600; margin: 12px 0 6px; }\\n\\n.card {\\n background: var(--st-surface);\\n border: 1px solid var(--st-border);\\n border-radius: 6px;\\n padding: 8px 10px;\\n margin-bottom: 8px;\\n}\\n.card-title { font-size: 12px; font-weight: 500; margin-bottom: 4px; word-break: break-word; }\\n.card-desc { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 6px; }\\n.card-version, .card-source {\\n font-size: 10px;\\n color: var(--st-ink-muted);\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n margin-right: 6px;\\n}\\n.card-chips { margin-bottom: 6px; }\\n.card-actions { display: flex; gap: 4px; flex-wrap: wrap; }\\n\\n.chip {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 600;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-ink-muted) 15%, transparent);\\n color: var(--st-ink-muted);\\n margin-right: 4px;\\n}\\n\\n.badge {\\n display: inline-block;\\n padding: 1px 6px;\\n border-radius: 10px;\\n font-size: 10px;\\n font-weight: 700;\\n white-space: nowrap;\\n background: color-mix(in srgb, var(--st-warn) 18%, transparent);\\n color: var(--st-warn);\\n margin-right: 4px;\\n}\\n\\n.sbtn {\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 3px 8px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 11px;\\n font-family: inherit;\\n}\\n.sbtn:hover { background: var(--st-border); }\\n.sbtn.primary { background: var(--st-accent); color: var(--st-ink-on-accent); border-color: var(--st-accent); }\\n.sbtn.danger { border-color: var(--st-warn); color: var(--st-warn); }\\n\\n.store-url-install { display: flex; gap: 8px; margin: 6px 0 12px; }\\n.store-url-install input {\\n flex: 1;\\n min-width: 120px;\\n background: var(--st-bg);\\n border: 1px solid var(--st-border);\\n color: var(--st-ink);\\n padding: 4px 8px;\\n border-radius: 4px;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n\\n#confirm-panel {\\n border: 1px solid var(--st-accent);\\n border-radius: 6px;\\n padding: 10px;\\n margin-bottom: 12px;\\n background: var(--st-surface);\\n}\\n#confirm-panel h2 { font-size: 13px; margin-bottom: 8px; }\\n#confirm-meta { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 8px; }\\n#confirm-meta .mono, .mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.build-flag { color: var(--st-warn); font-weight: 600; }\\n\\n.store-warnings { margin-bottom: 10px; }\\n.store-warning {\\n font-size: 11px;\\n color: var(--st-warn);\\n padding: 4px 8px;\\n border: 1px solid color-mix(in srgb, var(--st-warn) 40%, transparent);\\n border-radius: 4px;\\n margin-bottom: 4px;\\n word-break: break-word;\\n}\\n\\n.store-empty {\\n border: 1px dashed var(--st-border);\\n border-radius: 6px;\\n padding: 16px;\\n margin-bottom: 12px;\\n}\\n.store-empty-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; }\\n.store-empty-hint { font-size: 11px; color: var(--st-ink-muted); margin-bottom: 10px; line-height: 1.5; }\\n.store-empty code {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n background: color-mix(in srgb, var(--st-ink-muted) 12%, transparent);\\n padding: 0 3px;\\n border-radius: 3px;\\n}\\n\\n.store-builtins { border-top: 1px solid var(--st-border); padding-top: 8px; margin-top: 12px; }\\n.store-builtins summary {\\n font-size: 11px;\\n font-weight: 600;\\n color: var(--st-ink-muted);\\n cursor: pointer;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n}\\n.store-builtin-list { margin: 8px 0 0 16px; font-size: 11px; color: var(--st-ink-muted); }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--st-ink-muted);\\n border-top: 1px solid var(--st-border);\\n background: var(--st-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"store-head\\\"><span>App Store</span></div>\\n <div id=\\\"confirm-panel\\\" hidden></div>\\n <div id=\\\"content\\\"></div>\\n <div id=\\\"url-install-static\\\">\\n <div class=\\\"store-url-install\\\">\\n <input id=\\\"store-url\\\" type=\\\"text\\\" placeholder=\\\"https://example.com/my-app.agentapp or https://github.com/user/repo\\\" />\\n <input id=\\\"store-sha256\\\" type=\\\"text\\\" placeholder=\\\"sha256 (.agentapp, optional)\\\" />\\n <input id=\\\"store-ref\\\" type=\\\"text\\\" placeholder=\\\"git ref (optional)\\\" />\\n <input id=\\\"store-subdir\\\" type=\\\"text\\\" placeholder=\\\"git subdir (optional)\\\" />\\n <button class=\\\"sbtn\\\" id=\\\"store-install-from-url-btn\\\">Install from URL</button>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto App Store panel — the `STORE_HTML`\n * served as the `ui://agentproto_store/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_store` tool defined in ./index.ts.\n *\n * The store over the daemon's app-catalog/install plane (packages/\n * runtime/src/app-tools.ts): Installed (version/source/Update-available\n * badge, resync + uninstall actions), Featured + Available (grouped by\n * category, tier/placement/requires chips), Install-from-URL (with the\n * `.agentapp`-vs-git detection), Sources (warnings + stale flags),\n * Builtin panels (collapsed) — and a never-blank empty state pointing at\n * how to add a catalog source.\n *\n * `STORE_HTML` is a real single-file Vite build, not a hand-authored\n * template string: the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run\n * build:ui:store` (scripts/build-store-ui.mjs) — same committed-generated\n * contract as work-board (panel.generated.ts lives in git,\n * `build:ui:store:check` fails the CI drift gate). See ./ui/vite.config.ts\n * for how the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined\n * as a classic `<script>` ahead of the bundled module script.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,\n * unmodified by this change — this app only calls its `initBridge()`/\n * `callTool()` surface).\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Read: tools/call → app_catalog / app_list / app_updates\n * – Apply (always through the SAME verbs every other caller uses — no\n * second install path): app_install (CONFIRMED, two-call handshake),\n * app_resync, app_uninstall\n */\n\nimport { STORE_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph — the VS Code extension bundles with esbuild and\n * chokes on its transitive native `.node` deps — can still name the panel\n * from source. */\nexport const STORE_APP_ID = \"@agentproto/store\"\n\n/** The MCP tool the panel is registered under (./index.ts `makeStoreApp`),\n * hence its resource uri `ui://agentproto_store/view`. Follows the\n * convention of every other builtin panel (`agentproto_work_board`,\n * `agentproto_sessions`, …): `agentproto_<slug>`. */\nexport const STORE_TOOL_ID = \"agentproto_store\"\n\n/** The daemon tools this panel's html calls, in order of use. Consumed as\n * `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly — a builtin is not an\n * installed app, so `app_tool_call` cannot route for it. */\nexport const STORE_UI_TOOLS = [\n \"app_catalog\",\n \"app_list\",\n \"app_install\",\n \"app_resync\",\n \"app_updates\",\n \"app_uninstall\",\n \"app_status\",\n] as const\n\nexport const STORE_HTML = STORE_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto App Store panel — the\n * browse/install surface over `app_catalog`, `app_list`, `app_updates`\n * (+ apply verbs `app_install`/`app_resync`/`app_uninstall`), mounted by\n * the daemon in builtin-apps.ts (`makeBuiltinPanelApps`) exactly like\n * work-board.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). Host wiring: runtime's\n * builtin-apps.ts assembles the mounted list, with this factory closing\n * over the store's LIVE read paths (`listCatalog` is `dispatchTool\n * (\"app_catalog\")`-equivalent, `listInstalled` reads the live\n * AppRegistry) — so the store's execute() answer and what the same verbs\n * return can't drift.\n *\n * Install CONFIRMATION: the panel NEVER calls `app_install` silently.\n * Through the ui.tools bridge the FIRST call (no `confirm`) comes back a\n * preview (`{needsConfirmation: true, url, sha256|sha, runsBuildCommand}`);\n * the SECOND call repeats the same payload PLUS the returned `confirm`\n * token. Contained in the panel's dialog (ui/main.ts). Direct MCP/CLI\n * callers skip this entirely: the guard's preview costs them an extra\n * call only when they pass it, never a behavioural change (the guard\n * triggers on the panel's ui.tools tool-call path only — see\n * app-tools.ts's `appInstallConfirmation`).\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport {\n STORE_APP_ID,\n STORE_HTML,\n STORE_TOOL_ID,\n STORE_UI_TOOLS,\n} from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const storeInputSchema = z.object({\n catalogUrl: z\n .string()\n .optional()\n .describe(\n \"Restrict the snapshot to ONE catalog source (`catalog.sources[i].url`). \" +\n \"Omit for the merged listing app_catalog itself answers.\",\n ),\n})\n\nexport type StoreInput = z.infer<typeof storeInputSchema>\n\n/** The store's own read paths — mirrored off what the SAME MCP verbs\n * (`app_catalog`, `app_list`, `app_updates`) answer, so the panel's\n * snapshot can never disagree with the returned rows. Generic over the\n * row shape (this package doesn't own those record types — they live in\n * @agentproto/runtime's app-registry / app-catalog) so the host supplies\n * the concrete type argument when wiring, mirroring\n * `WorkBoardOps<TTask>`/`SessionsPanelOps<TSession>`. */\nexport interface StoreOps<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n listCatalog(catalogUrl?: string): Promise<ReadonlyArray<TCatalogRow>>\n listInstalled(): ReadonlyArray<TInstalledRow>\n listUpdates(): ReadonlyArray<TUpdateRow>\n}\n\nexport interface StoreOutput<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {\n /** `app_catalog`'s entries — featured/uninstalled ones grouped into the\n * Available shelf by `category`; `updateAvailable` rows read the\n * Update-available badge. */\n catalog: ReadonlyArray<TCatalogRow>\n /** `app_list`'s installed records — the Installed shelf. */\n installed: ReadonlyArray<TInstalledRow>\n /** `app_updates`' update entries — empty when nothing tracked is stale. */\n updates: ReadonlyArray<TUpdateRow>\n /** Warnings from the catalog sources (a failing source becomes a\n * warning, never an error — app-tools.ts's app_catalog contract). */\n warnings?: readonly string[]\n}\n\nexport type { AgnoMcpApp }\n\nexport function makeStoreApp<\n TCatalogRow = unknown,\n TInstalledRow = unknown,\n TUpdateRow = unknown,\n>(ops: StoreOps<TCatalogRow, TInstalledRow, TUpdateRow>): AgnoMcpApp<StoreInput, StoreOutput<TCatalogRow, TInstalledRow, TUpdateRow>> {\n return {\n id: STORE_TOOL_ID,\n title: \"App Store\",\n description:\n \"Open the agentproto App Store — browse/install apps from the daemon's \" +\n \"app_catalog, resync and uninstall installed ones. Sections: Installed \" +\n \"(with an Update-available badge) / Featured+Available from the catalog / \" +\n \"Install-from-URL / Sources / Builtin panels (the `?install=<appId>` \" +\n \"deep link pre-opens an entry's install confirmation).\",\n inputSchema: storeInputSchema,\n execute: async input => {\n const rows = await ops.listCatalog(input.catalogUrl)\n const warnings: string[] = []\n const catalog = rows.filter(row => {\n const w = (row as { warnings?: readonly string[] }).warnings\n if (w) warnings.push(...(w as readonly string[]))\n return true\n })\n return {\n catalog,\n installed: ops.listInstalled(),\n updates: ops.listUpdates(),\n ...(warnings.length > 0 ? { warnings } : {}),\n }\n },\n html: STORE_HTML,\n }\n}\n\nexport const storeApp: AppHandle = defineApp({\n id: STORE_APP_ID,\n name: \"App Store\",\n description:\n \"Open the agentproto App Store — installed apps, catalog shelves (featured/available), \" +\n \"install-from-URL with an explicit confirmation, sources and builtin panels.\",\n agents: [],\n ui: {\n html: STORE_HTML,\n title: \"App Store\",\n tools: [...STORE_UI_TOOLS],\n },\n})\n","/**\n * @generated — DO NOT EDIT BY HAND.\n *\n * Built from src/review-panel/ui/** (Vite + vite-plugin-singlefile) by\n * scripts/build-review-panel-ui.mjs. Edit the Vite sources and regenerate:\n *\n * pnpm --filter @agentproto/apps run build:ui:reviews\n *\n * `pnpm --filter @agentproto/apps run build:ui:reviews:check` (wired into\n * CI) fails if this file drifts from what the Vite sources actually produce.\n */\n\nexport const REVIEW_PANEL_HTML_GENERATED = \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"UTF-8\\\" />\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\" />\\n<title>agentproto reviews</title>\\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\\n vite.config.ts's injectPanelBridge plugin — a classic (non-module)\\n script so its `initBridge`/`callTool` land as globals before the\\n deferred module script below runs. -->\\n<script>// ── MCP Apps bridge (shared: panel-bridge.ts) ──\\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\\nvar _hostContext = null, _hostContextHandlers = [];\\nvar _hostCaps = null;\\nvar _standaloneApp = null;\\nfunction _isStandalone(){\\n return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';\\n}\\n// ── Per-boot embed token (runtime embed-tokens.ts) ────────────────────────\\n// Panels that iframe the daemon's standalone app host carry this proof so\\n// GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for\\n// MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).\\n// registerMcpApps replaces the placeholder with a real per-boot token when\\n// serving the panel as a resource; every other render (tests, standalone\\n// tab, VS Code HTTP-iframe panel — the latter two already pass\\n// iframeEmbedAllowed's origin checks) keeps the literal, and\\n// withEmbedToken() then degrades to an identity function.\\nwindow.__AGENPROTO_EMBED_TOKEN__ = \\\"__AGENPROTO_EMBED_TOKEN__\\\";\\nfunction withEmbedToken(url){\\n var t = window.__AGENPROTO_EMBED_TOKEN__;\\n if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;\\n return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);\\n}\\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\\nfunction getHostContext(){ return _hostContext; }\\nfunction getHostCapabilities(){ return _hostCaps; }\\nfunction onHostContext(cb){\\n _hostContextHandlers.push(cb);\\n // Replay the last context so a late subscriber isn't stuck blind.\\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\\n}\\nfunction _setHostContext(ctx){\\n if (!ctx || typeof ctx !== 'object') return;\\n // ui/notifications/host-context-changed carries only the changed keys —\\n // merge, matching the official ext-apps App behaviour.\\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\\n for (var i = 0; i < _hostContextHandlers.length; i++){\\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\\n }\\n}\\nfunction rpcRequest(method, params){\\n return new Promise(function(resolve, reject){\\n var id = _nextId++;\\n _pending[id] = {resolve: resolve, reject: reject};\\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\\n });\\n}\\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\\nwindow.addEventListener('message', function(evt){\\n var msg = evt.data;\\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\\n if (msg.id != null && msg.method == null){\\n var p = _pending[msg.id];\\n if (!p) return;\\n delete _pending[msg.id];\\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\\n else p.resolve(msg.result);\\n return;\\n }\\n if (msg.method){\\n if (msg.method === 'ui/notifications/host-context-changed'){\\n _setHostContext(msg.params || {});\\n }\\n for (var i = 0; i < _notifyHandlers.length; i++){\\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\\n }\\n }\\n});\\nfunction initBridge(){\\n if (_isStandalone()){\\n return window.McpApp.connect().then(function(conn){\\n _standaloneApp = conn;\\n // No host to advertise a hostContext — default to inline with no\\n // other modes available, which keeps the display-mode toggle buttons\\n // hidden (they only show for modes hostContext.availableDisplayModes\\n // lists) instead of rendering two buttons with nothing to switch to.\\n _setHostContext({displayMode: 'inline', availableDisplayModes: []});\\n });\\n }\\n return rpcRequest('ui/initialize', {\\n appInfo: {name: \\\"agentproto-review-panel\\\", version: '0.1.0'},\\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\\n protocolVersion: '2026-01-26'\\n }).then(function(result){\\n // The initialize result carries the initial hostContext (displayMode +\\n // availableDisplayModes) — capture it before notifying the host.\\n if (result && result.hostContext) _setHostContext(result.hostContext);\\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) — e.g. openLinks,\\n // \\\"Host supports opening external URLs\\\". Captured once, read via\\n // getHostCapabilities() by panels that need to branch on it.\\n if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;\\n rpcNotify('ui/notifications/initialized', {});\\n });\\n}\\nfunction requestDisplayMode(mode){\\n return rpcRequest('ui/request-display-mode', {mode: mode});\\n}\\nfunction openLink(url){\\n if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);\\n return rpcRequest('ui/open-link', {url: url});\\n}\\nfunction callTool(name, args){\\n var raw = _standaloneApp\\n ? _standaloneApp.callTool(name, args || {})\\n : rpcRequest('tools/call', {name: name, arguments: args || {}});\\n return raw.then(function(result){\\n if (result.isError){\\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\\n throw new Error(e);\\n }\\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\\n return JSON.parse(text);\\n });\\n}\\n\\n// ── Display-mode toggle (shared: @agentproto/app-client/display-mode) ──\\n// The installer is inlined verbatim (it is self-contained and idempotent),\\n// then handed this panel's own JSON-RPC plumbing. NO auto-request: the\\n// panel stays inline until the user clicks, and the buttons only appear for\\n// the modes the host advertises in hostContext.availableDisplayModes.\\n(function () {\\n if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;\\n window.AgentprotoUI = window.AgentprotoUI || {};\\n\\n var STYLE_ID = \\\"agentproto-display-mode-style\\\";\\n var BAR_ID = \\\"agentproto-display-mode-bar\\\";\\n // Gap between the host's safe area (or the viewport edge) and the button.\\n var EDGE_GAP = 8;\\n\\n // Glyph + accessible label per mode. The button is an icon; the words go\\n // to title/aria-label so the control stays compact without going mute to\\n // a screen reader.\\n var LABELS = {\\n fullscreen: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\u2922\\\",\\n onLabel: \\\"R\\\\u00e9duire\\\", offLabel: \\\"Agrandir\\\"\\n },\\n pip: {\\n on: \\\"\\\\u2921\\\", off: \\\"\\\\uD83D\\\\uDCCC\\\",\\n onLabel: \\\"D\\\\u00e9tacher\\\", offLabel: \\\"\\\\u00c9pingler sur le c\\\\u00f4t\\\\u00e9 (pip)\\\"\\n }\\n };\\n\\n // Every colour and both offsets resolve through an app-overridable custom\\n // property first, so an app can restyle/reposition the button with a\\n // stylesheet and never touch this script. The safe-area vars are what the\\n // host's own chrome feeds (see applyInsets), sitting UNDER the app's.\\n function ensureStyle() {\\n if (!document.head || document.getElementById(STYLE_ID)) return;\\n var style = document.createElement(\\\"style\\\");\\n style.id = STYLE_ID;\\n style.textContent =\\n \\\"#\\\" + BAR_ID + \\\"{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;\\\" +\\n \\\"top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));\\\" +\\n \\\"right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}\\\" +\\n \\\".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;\\\" +\\n \\\"box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;\\\" +\\n \\\"cursor:pointer;-webkit-appearance:none;appearance:none;\\\" +\\n \\\"font:600 14px/1 system-ui,-apple-system,sans-serif;\\\" +\\n \\\"border:1px solid var(--agentproto-dm-border,#d0d0d0);\\\" +\\n \\\"background:var(--agentproto-dm-bg,#fff);\\\" +\\n \\\"color:var(--agentproto-dm-fg,#1a1a1a);\\\" +\\n \\\"box-shadow:0 1px 4px rgba(0,0,0,.18)}\\\" +\\n \\\".agentproto-dm-btn:hover{filter:brightness(.95)}\\\" +\\n \\\".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}\\\" +\\n \\\".agentproto-dm-btn[data-theme=\\\\\\\"dark\\\\\\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}\\\" +\\n \\\"@media (prefers-color-scheme:dark){\\\" +\\n \\\".agentproto-dm-btn:not([data-theme=\\\\\\\"light\\\\\\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}\\\";\\n document.head.appendChild(style);\\n }\\n\\n function ready(fn) {\\n if (document.body) fn();\\n else document.addEventListener(\\\"DOMContentLoaded\\\", fn);\\n }\\n\\n function metaToggle() {\\n var el = document.querySelector('meta[name=\\\"agentproto-display-toggle\\\"]');\\n if (!el) return \\\"\\\";\\n return (el.getAttribute(\\\"content\\\") || \\\"\\\").toLowerCase().replace(/^\\\\s+|\\\\s+$/g, \\\"\\\");\\n }\\n\\n function ensureBar() {\\n var bar = document.getElementById(BAR_ID);\\n if (bar) return bar;\\n bar = document.createElement(\\\"div\\\");\\n bar.id = BAR_ID;\\n document.body.appendChild(bar);\\n return bar;\\n }\\n\\n function px(v) {\\n return typeof v === \\\"number\\\" && isFinite(v) && v > 0 ? v : 0;\\n }\\n\\n window.AgentprotoUI.installDisplayMode = function (api, opts) {\\n api = api || {};\\n opts = opts || {};\\n\\n var ctx = {};\\n var listeners = [];\\n var buttons = [];\\n // Modes the host has refused once. Checked before availability, so a\\n // later host-context-changed can never resurrect a dead button.\\n var refused = {};\\n var loggedKeys = false;\\n\\n var setting = opts.toggle || metaToggle() || \\\"auto\\\";\\n var optimistic = setting === \\\"optimistic\\\";\\n var autoMount = setting !== \\\"none\\\";\\n\\n function get() {\\n return ctx.displayMode || \\\"inline\\\";\\n }\\n\\n function available() {\\n var a = ctx.availableDisplayModes;\\n return a && a.slice ? a.slice() : [];\\n }\\n\\n function supports(mode) {\\n if (refused[mode]) return false;\\n var a = ctx.availableDisplayModes;\\n if (a && a.indexOf && a.indexOf(mode) >= 0) return true;\\n // Optimistic: the host advertises nothing (Claude Desktop, some\\n // embedders) but may still honour the request. Fullscreen only — pip\\n // is niche enough that guessing at it is just noise.\\n return optimistic && mode === \\\"fullscreen\\\";\\n }\\n\\n // Host chrome to stay clear of. Published as custom properties rather\\n // than inline styles so an app's own --agentproto-dm-top/right still\\n // wins (an inline style would beat any stylesheet it could write).\\n function applyInsets() {\\n var insets = ctx.safeAreaInsets || {};\\n var root = document.documentElement;\\n if (!root || !root.style || !root.style.setProperty) return;\\n root.style.setProperty(\\\"--agentproto-dm-safe-top\\\", (px(insets.top) + EDGE_GAP) + \\\"px\\\");\\n root.style.setProperty(\\\"--agentproto-dm-safe-right\\\", (px(insets.right) + EDGE_GAP) + \\\"px\\\");\\n }\\n\\n function syncButton(btn) {\\n var mode = btn.getAttribute(\\\"data-agentproto-dm\\\") || \\\"fullscreen\\\";\\n var labels = LABELS[mode] || LABELS.fullscreen;\\n var active = get() === mode;\\n btn.style.display = supports(mode) ? \\\"\\\" : \\\"none\\\";\\n btn.textContent = active ? labels.on : labels.off;\\n var label = active ? labels.onLabel : labels.offLabel;\\n btn.title = label;\\n btn.setAttribute(\\\"aria-label\\\", label);\\n btn.setAttribute(\\\"aria-pressed\\\", active ? \\\"true\\\" : \\\"false\\\");\\n if (ctx.theme === \\\"dark\\\" || ctx.theme === \\\"light\\\") btn.setAttribute(\\\"data-theme\\\", ctx.theme);\\n else btn.removeAttribute(\\\"data-theme\\\");\\n }\\n\\n function sync() {\\n applyInsets();\\n for (var i = buttons.length - 1; i >= 0; i--) {\\n // An app that removed its inline toggle stops being our problem.\\n if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }\\n syncButton(buttons[i]);\\n }\\n }\\n\\n function emit() {\\n var mode = get();\\n for (var i = 0; i < listeners.length; i++) {\\n try { listeners[i](mode, ctx); } catch (_) {}\\n }\\n }\\n\\n // host-context-changed carries only the changed keys — merge, matching\\n // the official ext-apps App behaviour.\\n function setContext(next) {\\n if (!next || typeof next !== \\\"object\\\") return;\\n for (var k in next) {\\n if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];\\n }\\n sync();\\n emit();\\n }\\n\\n function retire(mode) {\\n refused[mode] = true;\\n sync();\\n }\\n\\n function request(mode) {\\n var pending;\\n try {\\n pending = api.requestDisplayMode\\n ? api.requestDisplayMode(mode)\\n : Promise.reject(new Error(\\\"display mode: no host\\\"));\\n } catch (err) {\\n pending = Promise.reject(err);\\n }\\n return Promise.resolve(pending).then(function (result) {\\n // McpUiRequestDisplayModeResult.mode is the mode actually set, and\\n // the spec lets it differ from the one asked for — so a host that\\n // answers \\\"still inline\\\" is a refusal, not a success.\\n var settled = (result && result.mode) || mode;\\n if (optimistic && mode !== \\\"inline\\\" && settled !== mode) {\\n retire(mode);\\n throw new Error(\\\"host refused display mode: \\\" + mode);\\n }\\n // Hosts are not required to follow up with host-context-changed —\\n // trust the result so the glyph flips either way.\\n setContext({ displayMode: settled });\\n return settled;\\n }, function (err) {\\n if (optimistic && mode !== \\\"inline\\\") retire(mode);\\n throw err;\\n });\\n }\\n\\n function makeButton(mode) {\\n var btn = document.createElement(\\\"button\\\");\\n btn.type = \\\"button\\\";\\n btn.className = \\\"agentproto-dm-btn\\\";\\n // Stable id for the floating pair (styling hooks, tests). An app that\\n // mounts extra inline toggles gets unnamed ones rather than duplicates.\\n var id = mode === \\\"fullscreen\\\"\\n ? \\\"agentproto-display-mode\\\"\\n : \\\"agentproto-display-mode-\\\" + mode;\\n if (!document.getElementById(id)) btn.id = id;\\n btn.setAttribute(\\\"data-agentproto-dm\\\", mode);\\n btn.style.display = \\\"none\\\";\\n btn.addEventListener(\\\"click\\\", function () {\\n var cur = get();\\n // Same rule both copies had: anything that isn't inline collapses\\n // back to inline, so the button is always an escape hatch.\\n var inPanel = cur === \\\"fullscreen\\\" || cur === \\\"pip\\\";\\n request(inPanel ? \\\"inline\\\" : mode)[\\\"catch\\\"](function () {});\\n });\\n return btn;\\n }\\n\\n function mountToggle(el, mode) {\\n ensureStyle();\\n mode = mode || \\\"fullscreen\\\";\\n var btn = makeButton(mode);\\n if (el) el.appendChild(btn);\\n else ensureBar().appendChild(btn);\\n buttons.push(btn);\\n syncButton(btn);\\n return btn;\\n }\\n\\n function onChange(cb) {\\n if (typeof cb !== \\\"function\\\") return function () {};\\n listeners.push(cb);\\n try { cb(get(), ctx); } catch (_) {}\\n return function () {\\n var i = listeners.indexOf(cb);\\n if (i >= 0) listeners.splice(i, 1);\\n };\\n }\\n\\n ready(function () {\\n ensureStyle();\\n // Only the first installer in a document owns the floating bar: a\\n // built-in panel served standalone gets the injected window.McpApp\\n // bridge's copy AND its own, and two stacked buttons is a bug.\\n if (autoMount && !document.getElementById(BAR_ID)) {\\n ensureBar();\\n mountToggle(null, \\\"pip\\\");\\n mountToggle(null, \\\"fullscreen\\\");\\n }\\n sync();\\n });\\n\\n // The diagnostic both copies had, now on BOTH paths: it used to exist\\n // only in the built-in panel bridge, so an installed app debugged in\\n // Claude Desktop (which advertises no fullscreen, hence no button)\\n // printed nothing at all and left the user with no way to tell \\\"the\\n // host refuses\\\" from \\\"the toggle is broken\\\".\\n function observe(next) {\\n console.log('[mcp-app] displayMode=', next && next.displayMode,\\n 'availableDisplayModes=', next && next.availableDisplayModes);\\n setContext(next);\\n // What ELSE does this host send? Keys only, once — a host context can\\n // carry toolInfo (the instantiating tool call's arguments) and other\\n // host-private fields, so the names are the diagnostic and the values\\n // stay out of the console.\\n if (!loggedKeys && next && typeof next === \\\"object\\\") {\\n loggedKeys = true;\\n var keys = [];\\n for (var k in ctx) {\\n if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);\\n }\\n console.log('[mcp-app] hostContext keys=', keys.sort().join(','));\\n }\\n }\\n\\n if (typeof api.onHostContext === \\\"function\\\") api.onHostContext(observe);\\n else if (typeof api.getHostContext === \\\"function\\\") observe(api.getHostContext());\\n\\n return {\\n get: get,\\n available: available,\\n request: request,\\n onChange: onChange,\\n mountToggle: mountToggle\\n };\\n };\\n})();\\nvar displayMode = window.AgentprotoUI.installDisplayMode({\\n getHostContext: getHostContext,\\n onHostContext: onHostContext,\\n requestDisplayMode: requestDisplayMode\\n});</script>\\n <script>(function() {\\n \\\"use strict\\\";\\n function esc(value) {\\n return value.replace(/&/g, \\\"&\\\").replace(/</g, \\\"<\\\").replace(/>/g, \\\">\\\").replace(/\\\"/g, \\\""\\\");\\n }\\n function liveSessionUrl(origin, sessionId) {\\n return `${origin}/apps/@agentproto/live-session/ui?sessionId=${encodeURIComponent(sessionId)}`;\\n }\\n function shortRange(baseSha, headSha) {\\n if (!baseSha || !headSha) return \\\"—\\\";\\n return `${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`;\\n }\\n function ageOf(createdAt, now = Date.now()) {\\n const ms = now - new Date(createdAt).getTime();\\n if (!Number.isFinite(ms) || ms < 0) return \\\"just now\\\";\\n const mins = Math.floor(ms / 6e4);\\n if (mins < 1) return \\\"just now\\\";\\n if (mins < 60) return `${mins}m`;\\n const hours = Math.floor(mins / 60);\\n if (hours < 24) return `${hours}h`;\\n return `${Math.floor(hours / 24)}d`;\\n }\\n function verdictChip(status) {\\n return `<span class=\\\"chip chip-verdict v-${esc(status)}\\\">${esc(status)}</span>`;\\n }\\n const LANE_STATUS_LABEL = {\\n pass: \\\"pass\\\",\\n fail: \\\"fail\\\",\\n skipped: \\\"skipped\\\",\\n timeout: \\\"timeout\\\"\\n };\\n function laneStatusChip(status) {\\n return `<span class=\\\"chip chip-lane l-${esc(status)}\\\">${esc(LANE_STATUS_LABEL[status])}</span>`;\\n }\\n function severityTag(severity) {\\n return `<span class=\\\"tag sev-${esc(severity)}\\\">${esc(severity)}</span>`;\\n }\\n function requesterLabel(row) {\\n const r = row.requester;\\n if (!r) return \\\"—\\\";\\n const id = r.sessionId ? esc(r.sessionId) : \\\"\\\";\\n const author = r.gitAuthor ? esc(r.gitAuthor.name) : \\\"\\\";\\n if (id && author) return `${id} <span class=\\\"muted\\\">(${author})</span>`;\\n return id || author || \\\"—\\\";\\n }\\n function prLabel(row) {\\n if (!row.pr) return \\\"—\\\";\\n const state = row.prState ? ` <span class=\\\"tag pr-${esc(row.prState)}\\\">${esc(row.prState)}</span>` : \\\"\\\";\\n return `<a href=\\\"${esc(row.pr.url)}\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">#${row.pr.number}</a>${state}`;\\n }\\n function renderRow(row) {\\n const status = row.status ?? row.verdict ?? \\\"incomplete\\\";\\n const flags = [];\\n if (row.dirty) flags.push('<span class=\\\"tag t-dirty\\\">dirty</span>');\\n if (row.cached) flags.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (row.status !== \\\"running\\\") {\\n flags.push(row.signed ? '<span class=\\\"tag t-signed\\\">signed</span>' : '<span class=\\\"tag t-unsigned\\\">unsigned</span>');\\n }\\n return `<tr class=\\\"row\\\" data-runid=\\\"${esc(row.runId)}\\\" tabindex=\\\"0\\\"><td>${verdictChip(status)}</td><td class=\\\"mono\\\">${esc(shortRange(row.baseSha, row.headSha))}</td><td>${esc(row.binding ?? \\\"—\\\")}</td><td class=\\\"mono\\\">${esc(row.repoRemote ?? \\\"—\\\")}</td><td>${requesterLabel(row)}</td><td>${prLabel(row)}</td><td>${flags.join(\\\" \\\") || \\\"—\\\"}</td><td>${esc(ageOf(row.createdAt))}</td></tr>`;\\n }\\n function renderList(rows2) {\\n if (rows2.length === 0) return '<div class=\\\"empty\\\">No reviews recorded yet.</div>';\\n const header = \\\"<tr><th>Verdict</th><th>Range</th><th>Binding</th><th>Repo</th><th>Requester</th><th>PR</th><th>Flags</th><th>Age</th></tr>\\\";\\n return `<table class=\\\"list\\\"><thead>${header}</thead><tbody>${rows2.map(renderRow).join(\\\"\\\")}</tbody></table>`;\\n }\\n function findingLine(f) {\\n const loc = f.file ? `${esc(f.file)}${f.line ? `:${f.line}` : \\\"\\\"}` : \\\"\\\";\\n return `<details class=\\\"finding\\\"><summary>${severityTag(f.severity)} ${esc(f.title)}${loc ? ` <span class=\\\"mono muted\\\">${loc}</span>` : \\\"\\\"}</summary><div class=\\\"finding-detail\\\">${esc(f.detail)}</div></details>`;\\n }\\n function renderLaneDetail(lane, rubrics = []) {\\n const parts = [];\\n parts.push(`<div class=\\\"lane-hdr\\\">`);\\n parts.push(`<span class=\\\"lane-id\\\">${esc(lane.id)}</span>`);\\n parts.push(laneStatusChip(lane.status));\\n parts.push(`<span class=\\\"tag ${lane.blocking ? \\\"t-blocking\\\" : \\\"t-advisory\\\"}\\\">${lane.blocking ? \\\"blocking\\\" : \\\"advisory\\\"}</span>`);\\n if (lane.durationMs !== void 0) parts.push(`<span class=\\\"muted\\\">${(lane.durationMs / 1e3).toFixed(1)}s</span>`);\\n parts.push(`</div>`);\\n if (lane.error) parts.push(`<div class=\\\"lane-error\\\">${esc(lane.error)}</div>`);\\n if (lane.sessionId || lane.preset || lane.model || lane.fallbacks?.length) {\\n const rubric = rubrics.find((r) => r.check === lane.id);\\n const bits = [];\\n if (lane.model) bits.push(`model: ${esc(lane.model)}`);\\n if (lane.preset) bits.push(`preset: ${esc(lane.preset)}`);\\n if (lane.fallbacks?.length) {\\n bits.push(`after unavailable: ${lane.fallbacks.map((f) => `${esc(f.preset)} (${esc(f.error)})`).join(\\\", \\\")}`);\\n }\\n if (rubric) bits.push(`rubric: <span class=\\\"mono\\\">${esc(rubric.sha256.slice(0, 12))}</span>`);\\n if (lane.sessionId) {\\n bits.push(\\n `reviewer: <button class=\\\"sess-link\\\" data-session-id=\\\"${esc(lane.sessionId)}\\\">${esc(lane.sessionId)}</button>`\\n );\\n }\\n parts.push(`<div class=\\\"lane-agent-meta\\\">${bits.join(\\\" · \\\")}</div>`);\\n }\\n if (lane.findings.length > 0) parts.push(`<div class=\\\"findings\\\">${lane.findings.map(findingLine).join(\\\"\\\")}</div>`);\\n return `<div class=\\\"lane\\\">${parts.join(\\\"\\\")}</div>`;\\n }\\n function renderDetail(detail2) {\\n const status = detail2.status === \\\"done\\\" ? detail2.verdict ?? \\\"incomplete\\\" : detail2.status;\\n const header = [`<div class=\\\"detail-hdr\\\">`, verdictChip(status), `<span class=\\\"mono\\\">${esc(detail2.runId)}</span>`];\\n if (detail2.binding) header.push(`<span class=\\\"tag\\\">${esc(detail2.binding)}</span>`);\\n if (detail2.cached) header.push('<span class=\\\"tag t-cached\\\">cached</span>');\\n if (detail2.status === \\\"done\\\" && detail2.attestation) {\\n const sig = detail2.attestation.attestor?.signature;\\n header.push(\\n sig ? `<span class=\\\"tag t-signed\\\" title=\\\"${esc(sig.keyFingerprint)}\\\">signed: ${esc(sig.principal)}</span>` : '<span class=\\\"tag t-unsigned\\\">unsigned</span>'\\n );\\n }\\n header.push(`</div>`);\\n if (detail2.error) header.push(`<div class=\\\"detail-error\\\">${esc(detail2.error)}</div>`);\\n if (detail2.supersededBy) header.push(`<div class=\\\"muted\\\">superseded by ${esc(detail2.supersededBy)}</div>`);\\n const fullLanes = detail2.attestation?.lanes;\\n const rubrics = detail2.attestation?.rubrics ?? [];\\n let body;\\n if (fullLanes) {\\n body = fullLanes.map((l) => renderLaneDetail(l, rubrics)).join(\\\"\\\");\\n } else if (detail2.lanes) {\\n body = detail2.lanes.map((l) => `<div class=\\\"lane\\\"><div class=\\\"lane-hdr\\\"><span class=\\\"lane-id\\\">${esc(l.id)}</span>${laneStatusChip(l.status)}</div></div>`).join(\\\"\\\");\\n } else {\\n body = \\\"\\\";\\n }\\n return `<div class=\\\"detail\\\">${header.join(\\\"\\\")}${body}</div>`;\\n }\\n const POLL_RUNNING_MS = 4e3;\\n const POLL_IDLE_MS = 15e3;\\n function getEl(id) {\\n const el = document.getElementById(id);\\n if (!el) throw new Error(`review panel: missing #${id}`);\\n return el;\\n }\\n let rows = [];\\n let selectedRunId = null;\\n let detail = null;\\n let pollTimer = null;\\n let loadInFlight = false;\\n function boot() {\\n const listPaneEl = getEl(\\\"list-pane\\\");\\n const detailPaneEl = getEl(\\\"detail-pane\\\");\\n const detailBodyEl = getEl(\\\"detail-body\\\");\\n const statusbarEl = getEl(\\\"statusbar\\\");\\n const cancelBtn = getEl(\\\"cancel-btn\\\");\\n const rerunBtn = getEl(\\\"rerun-btn\\\");\\n const prBtn = getEl(\\\"pr-btn\\\");\\n const exportBtn = getEl(\\\"export-btn\\\");\\n function setStatus(msg) {\\n statusbarEl.textContent = msg;\\n }\\n function findRow(runId) {\\n return rows.find((r) => r.runId === runId);\\n }\\n function renderListPane() {\\n listPaneEl.innerHTML = renderList(rows);\\n if (!selectedRunId) return;\\n listPaneEl.querySelectorAll(\\\"tr[data-runid]\\\").forEach((tr) => {\\n if (tr.dataset.runid === selectedRunId) tr.classList.add(\\\"selected\\\");\\n });\\n }\\n function renderDetailPane() {\\n if (!selectedRunId || !detail) {\\n detailPaneEl.classList.remove(\\\"open\\\");\\n return;\\n }\\n detailPaneEl.classList.add(\\\"open\\\");\\n detailBodyEl.innerHTML = renderDetail(detail);\\n const running = detail.status === \\\"running\\\";\\n cancelBtn.disabled = !running;\\n cancelBtn.style.display = running ? \\\"\\\" : \\\"none\\\";\\n detailBodyEl.querySelectorAll(\\\".sess-link\\\").forEach((btn) => {\\n btn.addEventListener(\\\"click\\\", () => openSession(btn.dataset.sessionId ?? \\\"\\\"));\\n });\\n }\\n function openSession(sessionId) {\\n if (!sessionId) return;\\n const origin = window.location.origin && window.location.origin !== \\\"null\\\" ? window.location.origin : \\\"\\\";\\n const url = liveSessionUrl(origin, sessionId);\\n const caps = getHostCapabilities();\\n if (caps?.openLinks) {\\n openLink(url).catch(() => window.open(url, \\\"_blank\\\"));\\n } else {\\n window.open(url, \\\"_blank\\\");\\n }\\n }\\n function loadList() {\\n if (loadInFlight) return Promise.resolve();\\n loadInFlight = true;\\n return callTool(\\\"review_ledger\\\", { includeRunning: true }).then((result) => {\\n rows = result.attestations;\\n renderListPane();\\n const runningCount = rows.filter((r) => r.status === \\\"running\\\").length;\\n setStatus(`${result.total} review${result.total === 1 ? \\\"\\\" : \\\"s\\\"} · ${runningCount} running · ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`);\\n if (selectedRunId && findRow(selectedRunId)) return loadDetail(selectedRunId);\\n return void 0;\\n }).catch((e) => setStatus(`Error: ${e.message}`)).finally(() => {\\n loadInFlight = false;\\n });\\n }\\n function loadDetail(runId) {\\n return callTool(\\\"review_status\\\", { runId }).then((result) => {\\n detail = result;\\n renderDetailPane();\\n }).catch((e) => setStatus(`Error loading detail: ${e.message}`));\\n }\\n function openDetail(runId) {\\n selectedRunId = runId;\\n renderListPane();\\n void loadDetail(runId);\\n }\\n function closeDetail() {\\n selectedRunId = null;\\n detail = null;\\n renderListPane();\\n renderDetailPane();\\n }\\n listPaneEl.addEventListener(\\\"click\\\", (evt) => {\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n listPaneEl.addEventListener(\\\"keydown\\\", (evt) => {\\n if (evt.key !== \\\"Enter\\\" && evt.key !== \\\" \\\") return;\\n if (!(evt.target instanceof Element)) return;\\n const tr = evt.target.closest(\\\"tr[data-runid]\\\");\\n if (!(tr instanceof HTMLElement)) return;\\n evt.preventDefault();\\n const runId = tr.dataset.runid;\\n if (runId) openDetail(runId);\\n });\\n getEl(\\\"detail-close\\\").addEventListener(\\\"click\\\", closeDetail);\\n getEl(\\\"refresh-btn\\\").addEventListener(\\\"click\\\", () => {\\n void loadList();\\n });\\n cancelBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_cancel\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Cancelled.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Cancel failed: ${e.message}`));\\n });\\n rerunBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n const row = findRow(selectedRunId);\\n const cwd = row?.cwd;\\n if (!cwd) {\\n setStatus(\\\"Re-run failed: this row has no known checkout path.\\\");\\n return;\\n }\\n callTool(\\\"review_run\\\", {\\n cwd,\\n ...row?.binding ? { binding: row.binding } : {},\\n nocache: true,\\n wait: false,\\n supersede: true\\n }).then(() => {\\n setStatus(\\\"Re-run started.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Re-run failed: ${e.message}`));\\n });\\n prBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_pr\\\", { runId: selectedRunId }).then(() => {\\n setStatus(\\\"Fetched PR status.\\\");\\n return loadList();\\n }).catch((e) => setStatus(`Fetch PR status failed: ${e.message}`));\\n });\\n exportBtn.addEventListener(\\\"click\\\", () => {\\n if (!selectedRunId) return;\\n callTool(\\\"review_export\\\", { runId: selectedRunId }).then((result) => {\\n setStatus(result.path ? `Exported to ${result.path}` : \\\"Exported.\\\");\\n }).catch((e) => setStatus(`Export failed: ${e.message}`));\\n });\\n function doPoll() {\\n if (pollTimer) clearTimeout(pollTimer);\\n loadList().finally(() => {\\n const anyRunning = rows.some((r) => r.status === \\\"running\\\");\\n pollTimer = setTimeout(doPoll, anyRunning ? POLL_RUNNING_MS : POLL_IDLE_MS);\\n });\\n }\\n initBridge().then(() => loadList()).then(() => {\\n pollTimer = setTimeout(doPoll, POLL_RUNNING_MS);\\n }).catch((e) => setStatus(`Bridge error: ${e.message}`));\\n }\\n if (document.readyState === \\\"loading\\\") {\\n document.addEventListener(\\\"DOMContentLoaded\\\", boot);\\n } else {\\n boot();\\n }\\n})();</script>\\n <style rel=\\\"stylesheet\\\" crossorigin>/* ═══════════════════════════════════════════════════════════════════\\n Semantic tokens + runtime mapping sheets — same scheme as work-board's\\n style.css (see that file's header doc for the two-mapping-sheet\\n rationale): every rule below reads ONLY the --rp-* custom properties,\\n each resolving through a live --vscode-* var() first, then the\\n agentproto brand fallback.\\n ═══════════════════════════════════════════════════════════════════ */\\n\\n:root {\\n --rp-bg: var(--vscode-editor-background, #f5f6f3);\\n --rp-surface: var(--vscode-sideBar-background, #ffffff);\\n --rp-ink: var(--vscode-editor-foreground, #171a1e);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #5c6470);\\n --rp-border: var(--vscode-panel-border, #dde0da);\\n --rp-accent: var(--vscode-button-background, #0f62fe);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #5c6470);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --rp-bg: var(--vscode-editor-background, #101214);\\n --rp-surface: var(--vscode-sideBar-background, #16191d);\\n --rp-ink: var(--vscode-editor-foreground, #e8eaed);\\n --rp-ink-muted: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-border: var(--vscode-panel-border, #272c33);\\n --rp-accent: var(--vscode-button-background, #4589ff);\\n\\n --rp-pass: var(--vscode-charts-green, #3edc81);\\n --rp-block: var(--vscode-charts-red, #fa4d56);\\n --rp-incomplete: var(--vscode-charts-yellow, #f0b429);\\n --rp-running: var(--vscode-charts-blue, #4589ff);\\n --rp-cancelled: var(--vscode-descriptionForeground, #99a2ad);\\n --rp-failed: var(--vscode-charts-red, #fa4d56);\\n }\\n}\\n\\n/* ── Base ─────────────────────────────────────────────────────────── */\\n\\n* { box-sizing: border-box; margin: 0; padding: 0; }\\n\\nhtml, body {\\n height: 100%;\\n font-family: \\\"IBM Plex Sans\\\", -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", Roboto, Helvetica, Arial, sans-serif;\\n font-size: 13px;\\n background: var(--rp-bg);\\n color: var(--rp-ink);\\n overflow: hidden;\\n}\\n\\n.mono {\\n font-family: \\\"IBM Plex Mono\\\", ui-monospace, SFMono-Regular, Menlo, Monaco, \\\"Courier New\\\", monospace;\\n}\\n.muted { color: var(--rp-ink-muted); }\\n\\n#app { display: flex; flex-direction: column; height: 100%; }\\n\\n#toolbar {\\n padding: 8px 12px;\\n border-bottom: 1px solid var(--rp-border);\\n display: flex;\\n align-items: center;\\n gap: 8px;\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}\\n#toolbar-label { font-size: 11px; color: var(--rp-ink-muted); }\\n.tbtn {\\n background: var(--rp-bg);\\n border: 1px solid var(--rp-border);\\n color: var(--rp-ink);\\n padding: 4px 10px;\\n border-radius: 4px;\\n cursor: pointer;\\n font-size: 12px;\\n font-family: inherit;\\n}\\n.tbtn:hover { background: var(--rp-border); }\\n.tbtn:disabled { opacity: 0.5; cursor: default; }\\n.tbtn.danger { border-color: var(--rp-block); color: var(--rp-block); }\\n\\n#main { flex: 1; overflow: hidden; display: flex; }\\n#list-pane { flex: 1; overflow-y: auto; }\\n#detail-pane {\\n width: 420px;\\n min-width: 320px;\\n border-left: 1px solid var(--rp-border);\\n overflow-y: auto;\\n display: none;\\n flex-direction: column;\\n}\\n#detail-pane.open { display: flex; }\\n#detail-close { align-self: flex-end; margin: 6px; }\\n\\ntable.list { width: 100%; border-collapse: collapse; }\\ntable.list th {\\n text-align: left;\\n font-size: 10px;\\n text-transform: uppercase;\\n letter-spacing: 0.05em;\\n color: var(--rp-ink-muted);\\n padding: 6px 8px;\\n border-bottom: 1px solid var(--rp-border);\\n position: sticky;\\n top: 0;\\n background: var(--rp-surface);\\n}\\ntable.list td { padding: 6px 8px; border-bottom: 1px solid var(--rp-border); font-size: 12px; vertical-align: middle; }\\ntr.row { cursor: pointer; }\\ntr.row:hover, tr.row:focus { background: color-mix(in srgb, var(--rp-accent) 8%, transparent); outline: none; }\\ntr.row.selected { background: color-mix(in srgb, var(--rp-accent) 14%, transparent); }\\n\\n.empty { padding: 24px 12px; color: var(--rp-ink-muted); font-size: 12px; text-align: center; }\\n\\n.chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }\\n.chip-verdict.v-pass { background: color-mix(in srgb, var(--rp-pass) 18%, transparent); color: var(--rp-pass); }\\n.chip-verdict.v-block { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.chip-verdict.v-incomplete { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.chip-verdict.v-running { background: color-mix(in srgb, var(--rp-running) 18%, transparent); color: var(--rp-running); }\\n.chip-verdict.v-cancelled { background: color-mix(in srgb, var(--rp-cancelled) 15%, transparent); color: var(--rp-cancelled); }\\n.chip-verdict.v-failed { background: color-mix(in srgb, var(--rp-failed) 18%, transparent); color: var(--rp-failed); }\\n\\n.chip-lane.l-pass { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.chip-lane.l-fail { background: color-mix(in srgb, var(--rp-block) 15%, transparent); color: var(--rp-block); }\\n.chip-lane.l-skipped { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.chip-lane.l-timeout { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n\\n.tag { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; }\\n.t-dirty { background: color-mix(in srgb, var(--rp-incomplete) 15%, transparent); color: var(--rp-incomplete); }\\n.t-cached { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.t-signed { background: color-mix(in srgb, var(--rp-pass) 12%, transparent); color: var(--rp-pass); }\\n.t-unsigned { background: color-mix(in srgb, var(--rp-ink-muted) 10%, transparent); color: var(--rp-ink-muted); }\\n.t-blocking { background: color-mix(in srgb, var(--rp-block) 12%, transparent); color: var(--rp-block); }\\n.t-advisory { background: color-mix(in srgb, var(--rp-ink-muted) 12%, transparent); color: var(--rp-ink-muted); }\\n.pr-open { background: color-mix(in srgb, var(--rp-pass) 15%, transparent); color: var(--rp-pass); }\\n.pr-merged { background: color-mix(in srgb, var(--rp-accent) 15%, transparent); color: var(--rp-accent); }\\n.pr-closed { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n.sev-high { background: color-mix(in srgb, var(--rp-block) 18%, transparent); color: var(--rp-block); }\\n.sev-medium { background: color-mix(in srgb, var(--rp-incomplete) 18%, transparent); color: var(--rp-incomplete); }\\n.sev-low { background: color-mix(in srgb, var(--rp-ink-muted) 15%, transparent); color: var(--rp-ink-muted); }\\n\\n.detail { padding: 10px; display: flex; flex-direction: column; gap: 10px; }\\n.detail-hdr { display: flex; align-items: center; gap: 8px; }\\n.detail-error { color: var(--rp-block); font-size: 12px; word-break: break-word; }\\n.detail-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 10px; }\\n\\n.lane {\\n border: 1px solid var(--rp-border);\\n border-radius: 6px;\\n padding: 8px;\\n display: flex;\\n flex-direction: column;\\n gap: 6px;\\n}\\n.lane-hdr { display: flex; align-items: center; gap: 6px; }\\n.lane-id { font-weight: 600; font-size: 12px; }\\n.lane-error { color: var(--rp-block); font-size: 11px; word-break: break-word; }\\n.lane-agent-meta { font-size: 11px; color: var(--rp-ink-muted); }\\n.sess-link {\\n background: none;\\n border: none;\\n color: var(--rp-accent);\\n cursor: pointer;\\n font: inherit;\\n padding: 0;\\n text-decoration: underline;\\n}\\n\\n.findings { display: flex; flex-direction: column; gap: 4px; }\\n.finding summary { cursor: pointer; font-size: 11px; list-style: none; }\\n.finding summary::-webkit-details-marker { display: none; }\\n.finding-detail { font-size: 11px; color: var(--rp-ink-muted); padding: 4px 0 0 4px; white-space: pre-wrap; word-break: break-word; }\\n\\n#statusbar {\\n padding: 4px 12px;\\n font-size: 11px;\\n color: var(--rp-ink-muted);\\n border-top: 1px solid var(--rp-border);\\n background: var(--rp-surface);\\n flex-shrink: 0;\\n}</style>\\n</head>\\n<body>\\n<div id=\\\"app\\\">\\n <div id=\\\"toolbar\\\">\\n <span id=\\\"toolbar-label\\\">Reviews</span>\\n <button class=\\\"tbtn\\\" id=\\\"refresh-btn\\\" title=\\\"Refresh\\\">↻</button>\\n </div>\\n <div id=\\\"main\\\">\\n <div id=\\\"list-pane\\\"></div>\\n <div id=\\\"detail-pane\\\">\\n <button class=\\\"tbtn\\\" id=\\\"detail-close\\\">× Close</button>\\n <div id=\\\"detail-body\\\"></div>\\n <div class=\\\"detail-actions\\\">\\n <button class=\\\"tbtn danger\\\" id=\\\"cancel-btn\\\">Cancel</button>\\n <button class=\\\"tbtn\\\" id=\\\"rerun-btn\\\">Re-run fresh</button>\\n <button class=\\\"tbtn\\\" id=\\\"pr-btn\\\">Fetch PR status</button>\\n <button class=\\\"tbtn\\\" id=\\\"export-btn\\\">Export</button>\\n </div>\\n </div>\\n </div>\\n <div id=\\\"statusbar\\\">Connecting to bridge…</div>\\n</div>\\n</body>\\n</html>\\n\"\n","/**\n * HTML bundle for the agentproto review-panel — the `REVIEW_PANEL_HTML`\n * served as the `ui://agentproto_reviews/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_reviews` tool defined in ./index.ts.\n *\n * A verdict panel over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts` + `review-runner.ts`'s in-flight\n * runs) — list view (running first, then newest) and a per-run detail view\n * (lanes, findings, agent-lane provenance).\n *\n * `REVIEW_PANEL_HTML` is a real single-file Vite build, not a hand-authored\n * template string — same technique as work-board (see that package's\n * ./panel.ts docblock): the source lives in ./ui/** (index.html/main.ts/\n * types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with\n * vite-plugin-singlefile) and is committed, pre-built, into\n * ./panel.generated.ts by `pnpm --filter @agentproto/apps run build:ui:reviews`\n * (scripts/build-review-panel-ui.mjs) — so this package still builds with\n * plain `pnpm build` on a fresh clone, no Vite step in the critical path.\n * `build:ui:reviews:check` (wired into CI) fails if that generated file\n * drifts from what the Vite sources actually produce.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts)\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → review_ledger (`includeRunning: true`) on a poll\n * that speeds up to ~4s while any row is running, ~15s otherwise; a\n * selected row's detail comes from review_status (runId)\n * – Actions: tools/call → review_cancel / review_run (nocache + wait:false\n * + supersede, \"Re-run fresh\") / review_pr / review_export — real tool\n * names only, no second write path\n * – Deep link: an agent lane's reviewer-session id opens (openLink, or\n * window.open as a fallback) `/apps/@agentproto/live-session/ui\n * ?sessionId=<id>` — runtime's `handleAppUiPage` validates and bakes\n * that query param into the live-session widget's initData\n * (builtin-apps.ts's `isValidDeepLinkSessionId`), so it boots already\n * pinned to the exact reviewer session instead of self-discovering the\n * newest running one. See ./ui/render.ts's `liveSessionUrl`.\n *\n * The verdict/status chip (never render a running/cancelled run as if it\n * had a verdict): pass/block/incomplete/running/cancelled/failed, one\n * visual language everywhere it appears — see ./ui/render.ts's `verdictChip`.\n */\n\nimport { REVIEW_PANEL_HTML_GENERATED } from \"./panel.generated.js\"\n\n/** The builtin's app id — how `app_catalog` reports the panel. Lives in this\n * html-only entry (not ./index.ts) so a consumer that must NOT pull the\n * app-kit dependency graph can still name the panel from source (mirrors\n * work-board/panel.ts's `WORK_BOARD_APP_ID`). */\nexport const REVIEW_PANEL_APP_ID = \"@agentproto/review-panel\"\n\n/** The MCP tool the panel is registered under (./index.ts\n * `makeReviewPanelApp`), hence its resource uri `ui://agentproto_reviews/view`. */\nexport const REVIEW_PANEL_TOOL_ID = \"agentproto_reviews\"\n\n/** The daemon tools this panel's html calls, in order of first use. Consumed\n * as `ui.tools` by ./index.ts and as the client-side allowlist by any host\n * that dispatches the panel's `tools/call` directly. The reviewer-session\n * deep link (./ui/render.ts's `liveSessionUrl`) is a plain `openLink`/\n * `window.open` navigation, not a tools/call, so it needs no entry here. */\nexport const REVIEW_PANEL_UI_TOOLS = [\n \"review_ledger\",\n \"review_status\",\n \"review_cancel\",\n \"review_run\",\n \"review_pr\",\n \"review_export\",\n] as const\n\nexport const REVIEW_PANEL_HTML = REVIEW_PANEL_HTML_GENERATED\n","/**\n * McpApp definition for the agentproto review verdict panel — a list +\n * detail view over the daemon's review ledger\n * (`packages/runtime/src/review-ledger.ts`) and its in-flight runs\n * (`review-runner.ts`'s `ReviewRunner.list()`).\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape, no\n * @agstudio/mcp-apps / Mastra dependency), same as sessions-panel /\n * work-board — the host (@agentproto/runtime) wires this factory's output\n * via `registerMcpApps` from its mcp-apps-adapter.ts at boot time, with the\n * mounted list assembled in runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_reviews` is called by the host with the optional\n * `{cwd, repoRemote, runId, requesterSessionId}` scoping input.\n * 2. execute() answers an initial `review_ledger`-shaped snapshot\n * (`includeRunning: true`) via `ops.listReviews` — the SAME body\n * `review_ledger` itself runs (`reviewLedgerView`, review-tools.ts),\n * so a non-interactive caller sees exactly what the tool would return.\n * 3. The HTML panel — a static work-board-style Vite build, so it does\n * NOT thread this execute() input through — opens its own bridge and\n * polls `review_ledger({includeRunning: true})` unscoped (mirrors\n * work-board's own boardId-from-execute() omission), then\n * `review_status(runId)` for whichever row is selected. Actions go\n * through `review_cancel`/`review_run`/`review_pr`/`review_export` —\n * real tool names only, no second write path.\n *\n * This module also exports `reviewPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeReviewPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over the LIVE ledger/runner read,\n * something a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { REVIEW_PANEL_APP_ID, REVIEW_PANEL_HTML, REVIEW_PANEL_TOOL_ID, REVIEW_PANEL_UI_TOOLS } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const reviewPanelInputSchema = z.object({\n cwd: z.string().optional().describe(\"A directory inside a git repo — scopes the initial snapshot to its remote.\"),\n repoRemote: z.string().optional().describe(\"Normalized repo remote (e.g. github.com/agentproto/ts).\"),\n runId: z.string().optional().describe(\"A specific run id known at open time (informational on the initial snapshot).\"),\n requesterSessionId: z\n .string()\n .optional()\n .describe(\"Scope the initial snapshot to reviews requested by this session.\"),\n})\n\nexport type ReviewPanelInput = z.infer<typeof reviewPanelInputSchema>\n\nexport interface ReviewPanelOutput {\n total: number\n attestations: Record<string, unknown>[]\n /** Echoed back from the input — the row this snapshot's caller already\n * had in mind, when known. Not consumed by the panel's own client JS\n * (see this file's docblock on why the static build doesn't thread it). */\n focusRunId?: string\n}\n\n/**\n * Generic host wiring for `makeReviewPanelApp` — the daemon supplies\n * `listReviews` bound to `reviewLedgerView(reviewRunner, ...)`\n * (review-tools.ts), the exact function `review_ledger` itself calls, so\n * the panel's initial snapshot and the tool's own output can never drift\n * apart into two implementations.\n */\nexport interface ReviewPanelOps {\n listReviews(input: ReviewPanelInput): Promise<ReviewPanelOutput> | ReviewPanelOutput\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the ledger/runner read so execute() doesn't need an\n * AppContext (agentproto has no userId/guildId concept) — mirrors\n * makeWorkBoardApp.\n */\nexport function makeReviewPanelApp(ops: ReviewPanelOps): AgnoMcpApp<ReviewPanelInput, ReviewPanelOutput> {\n return {\n id: REVIEW_PANEL_TOOL_ID,\n title: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list (running reviews first, then \" +\n \"newest) over the daemon's review ledger, with a per-run detail view (lanes, \" +\n \"findings, agent-lane provenance) and actions (cancel, re-run fresh, fetch PR \" +\n \"status, export). Pass `cwd`/`repoRemote`/`requesterSessionId` to scope the initial \" +\n \"snapshot; the panel's own live view re-polls unscoped.\",\n inputSchema: reviewPanelInputSchema,\n execute: async input => {\n const snapshot = await ops.listReviews(input)\n return { ...snapshot, ...(input.runId ? { focusRunId: input.runId } : {}) }\n },\n html: REVIEW_PANEL_HTML,\n }\n}\n\nexport const reviewPanelApp: AppHandle = defineApp({\n id: REVIEW_PANEL_APP_ID,\n name: \"Reviews\",\n description:\n \"Open the agentproto review panel — a verdict list + detail view over the daemon's \" +\n \"review ledger, with cancel / re-run / PR-status / export actions.\",\n agents: [],\n ui: {\n html: REVIEW_PANEL_HTML,\n title: \"Reviews\",\n tools: [...REVIEW_PANEL_UI_TOOLS],\n },\n})\n"]}
|