@agentproto/apps 0.19.0 → 0.20.1
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.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +77 -4
- package/dist/index.mjs.map +1 -1
- package/dist/session-chat/index.d.ts +7 -0
- package/dist/session-chat/index.d.ts.map +1 -1
- package/dist/session-chat/panel.d.ts +1 -1
- package/dist/session-chat/panel.d.ts.map +1 -1
- package/dist/session-chat/panel.mjs +5 -0
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +10 -2
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.d.ts.map +1 -1
- package/dist/session-story/panel.mjs +8 -2
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +8 -2
- package/dist/session-story.mjs.map +1 -1
- package/dist/store/index.d.ts +61 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/panel.d.ts +53 -0
- package/dist/store/panel.d.ts.map +1 -0
- package/dist/store/panel.generated.d.ts +13 -0
- package/dist/store/panel.generated.d.ts.map +1 -0
- package/dist/store/panel.mjs +25 -0
- package/dist/store/panel.mjs.map +1 -0
- package/dist/store.mjs +71 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +13 -1
- package/session-steward/.agentproto/workflows/session-steward/WORKFLOW.md +22 -2
- package/session-steward/.agentproto/workflows/session-steward/cron-rules.mjs +22 -4
- package/session-steward/.agentproto/workflows/session-steward/entry.mjs +64 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/panel-bridge.ts","../src/session-story/panel.ts","../src/session-story/index.ts"],"names":[],"mappings":";;;;;;;;;AA4FO,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;;;AC7LO,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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;;;AC7R9C,IAAM,uBAAA,GAA0B,EAAE,MAAA,CAAO;AAAA,EAC9C,SAAA,EAAW,CAAA,CACR,MAAA,EAAO,CACP,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAGN,CAAC;AAmBM,SAAS,yBACd,GAAA,EAC6D;AAC7D,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,0BAAA;AAAA,IACJ,KAAA,EAAO,eAAA;AAAA,IACP,WAAA,EACE,8YAAA;AAAA,IAMF,WAAA,EAAa,uBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,KACb,KAAA,CAAM,YACF,EAAE,SAAA,EAAW,KAAA,CAAM,SAAA,KACnB,EAAE,QAAA,EAAU,GAAA,CAAI,YAAA,CAAa,KAAK,CAAA,EAAE;AAAA,IAC1C,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,kBAA6B,SAAA,CAAU;AAAA,EAClD,EAAA,EAAI,2BAAA;AAAA,EACJ,IAAA,EAAM,eAAA;AAAA,EACN,WAAA,EACE,+MAAA;AAAA,EAGF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,wBAAA;AAAA,IACN,KAAA,EAAO,eAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAA,EAAgB,cAAA,EAAgB,cAAc;AAAA;AAE1D,CAAC","file":"session-story.mjs","sourcesContent":["/**\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 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\" id=\"fullPanelLink\" href=\"#\" target=\"_blank\" rel=\"noopener\" title=\"Ouvrir le panneau complet (Terminal/Chat/JSON/TTY)\">↗ 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 $('fullPanelLink').href='https://cli.agentproto.sh/panel?session='+encodeURIComponent(id);\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/panel-bridge.ts","../src/session-story/panel.ts","../src/session-story/index.ts"],"names":[],"mappings":";;;;;;;;;AA4FO,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;;;AC7LO,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;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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;;;AC7R9C,IAAM,uBAAA,GAA0B,EAAE,MAAA,CAAO;AAAA,EAC9C,SAAA,EAAW,CAAA,CACR,MAAA,EAAO,CACP,UAAS,CACT,QAAA;AAAA,IACC;AAAA;AAGN,CAAC;AAmBM,SAAS,yBACd,GAAA,EAC6D;AAC7D,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,0BAAA;AAAA,IACJ,KAAA,EAAO,eAAA;AAAA,IACP,WAAA,EACE,8YAAA;AAAA,IAMF,WAAA,EAAa,uBAAA;AAAA,IACb,OAAA,EAAS,OAAM,KAAA,KACb,KAAA,CAAM,YACF,EAAE,SAAA,EAAW,KAAA,CAAM,SAAA,KACnB,EAAE,QAAA,EAAU,GAAA,CAAI,YAAA,CAAa,KAAK,CAAA,EAAE;AAAA,IAC1C,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,kBAA6B,SAAA,CAAU;AAAA,EAClD,EAAA,EAAI,2BAAA;AAAA,EACJ,IAAA,EAAM,eAAA;AAAA,EACN,WAAA,EACE,+MAAA;AAAA,EAGF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,wBAAA;AAAA,IACN,KAAA,EAAO,eAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAA,EAAgB,cAAA,EAAgB,cAAc;AAAA;AAE1D,CAAC","file":"session-story.mjs","sourcesContent":["/**\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 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"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpApp definition for the agentproto App Store panel — the
|
|
3
|
+
* browse/install surface over `app_catalog`, `app_list`, `app_updates`
|
|
4
|
+
* (+ apply verbs `app_install`/`app_resync`/`app_uninstall`), mounted by
|
|
5
|
+
* the daemon in builtin-apps.ts (`makeBuiltinPanelApps`) exactly like
|
|
6
|
+
* work-board.
|
|
7
|
+
*
|
|
8
|
+
* Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does
|
|
9
|
+
* NOT depend on @agstudio/mcp-apps or Mastra). Host wiring: runtime's
|
|
10
|
+
* builtin-apps.ts assembles the mounted list, with this factory closing
|
|
11
|
+
* over the store's LIVE read paths (`listCatalog` is `dispatchTool
|
|
12
|
+
* ("app_catalog")`-equivalent, `listInstalled` reads the live
|
|
13
|
+
* AppRegistry) — so the store's execute() answer and what the same verbs
|
|
14
|
+
* return can't drift.
|
|
15
|
+
*
|
|
16
|
+
* Install CONFIRMATION: the panel NEVER calls `app_install` silently.
|
|
17
|
+
* Through the ui.tools bridge the FIRST call (no `confirm`) comes back a
|
|
18
|
+
* preview (`{needsConfirmation: true, url, sha256|sha, runsBuildCommand}`);
|
|
19
|
+
* the SECOND call repeats the same payload PLUS the returned `confirm`
|
|
20
|
+
* token. Contained in the panel's dialog (ui/main.ts). Direct MCP/CLI
|
|
21
|
+
* callers skip this entirely: the guard's preview costs them an extra
|
|
22
|
+
* call only when they pass it, never a behavioural change (the guard
|
|
23
|
+
* triggers on the panel's ui.tools tool-call path only — see
|
|
24
|
+
* app-tools.ts's `appInstallConfirmation`).
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { type AppHandle } from "@agentproto/app-kit";
|
|
28
|
+
import type { AgnoMcpApp } from "../mcp-app-types.js";
|
|
29
|
+
export declare const storeInputSchema: z.ZodObject<{
|
|
30
|
+
catalogUrl: z.ZodOptional<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
export type StoreInput = z.infer<typeof storeInputSchema>;
|
|
33
|
+
/** The store's own read paths — mirrored off what the SAME MCP verbs
|
|
34
|
+
* (`app_catalog`, `app_list`, `app_updates`) answer, so the panel's
|
|
35
|
+
* snapshot can never disagree with the returned rows. Generic over the
|
|
36
|
+
* row shape (this package doesn't own those record types — they live in
|
|
37
|
+
* @agentproto/runtime's app-registry / app-catalog) so the host supplies
|
|
38
|
+
* the concrete type argument when wiring, mirroring
|
|
39
|
+
* `WorkBoardOps<TTask>`/`SessionsPanelOps<TSession>`. */
|
|
40
|
+
export interface StoreOps<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {
|
|
41
|
+
listCatalog(catalogUrl?: string): Promise<ReadonlyArray<TCatalogRow>>;
|
|
42
|
+
listInstalled(): ReadonlyArray<TInstalledRow>;
|
|
43
|
+
listUpdates(): ReadonlyArray<TUpdateRow>;
|
|
44
|
+
}
|
|
45
|
+
export interface StoreOutput<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown> {
|
|
46
|
+
/** `app_catalog`'s entries — featured/uninstalled ones grouped into the
|
|
47
|
+
* Available shelf by `category`; `updateAvailable` rows read the
|
|
48
|
+
* Update-available badge. */
|
|
49
|
+
catalog: ReadonlyArray<TCatalogRow>;
|
|
50
|
+
/** `app_list`'s installed records — the Installed shelf. */
|
|
51
|
+
installed: ReadonlyArray<TInstalledRow>;
|
|
52
|
+
/** `app_updates`' update entries — empty when nothing tracked is stale. */
|
|
53
|
+
updates: ReadonlyArray<TUpdateRow>;
|
|
54
|
+
/** Warnings from the catalog sources (a failing source becomes a
|
|
55
|
+
* warning, never an error — app-tools.ts's app_catalog contract). */
|
|
56
|
+
warnings?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
export type { AgnoMcpApp };
|
|
59
|
+
export declare function makeStoreApp<TCatalogRow = unknown, TInstalledRow = unknown, TUpdateRow = unknown>(ops: StoreOps<TCatalogRow, TInstalledRow, TUpdateRow>): AgnoMcpApp<StoreInput, StoreOutput<TCatalogRow, TInstalledRow, TUpdateRow>>;
|
|
60
|
+
export declare const storeApp: AppHandle;
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAO/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,eAAO,MAAM,gBAAgB;;iBAQ3B,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAEzD;;;;;;0DAM0D;AAC1D,MAAM,WAAW,QAAQ,CAAC,WAAW,GAAG,OAAO,EAAE,aAAa,GAAG,OAAO,EAAE,UAAU,GAAG,OAAO;IAC5F,WAAW,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;IACrE,aAAa,IAAI,aAAa,CAAC,aAAa,CAAC,CAAA;IAC7C,WAAW,IAAI,aAAa,CAAC,UAAU,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,WAAW,CAAC,WAAW,GAAG,OAAO,EAAE,aAAa,GAAG,OAAO,EAAE,UAAU,GAAG,OAAO;IAC/F;;kCAE8B;IAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACnC,4DAA4D;IAC5D,SAAS,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IACvC,2EAA2E;IAC3E,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;IAClC;0EACsE;IACtE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B,wBAAgB,YAAY,CAC1B,WAAW,GAAG,OAAO,EACrB,aAAa,GAAG,OAAO,EACvB,UAAU,GAAG,OAAO,EACpB,GAAG,EAAE,QAAQ,CAAC,WAAW,EAAE,aAAa,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC,UAAU,EAAE,WAAW,CAAC,WAAW,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CA4BpI;AAED,eAAO,MAAM,QAAQ,EAAE,SAYrB,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML bundle for the agentproto App Store panel — the `STORE_HTML`
|
|
3
|
+
* served as the `ui://agentproto_store/view` resource by `registerMcpApps`
|
|
4
|
+
* (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via
|
|
5
|
+
* the `agentproto_store` tool defined in ./index.ts.
|
|
6
|
+
*
|
|
7
|
+
* The store over the daemon's app-catalog/install plane (packages/
|
|
8
|
+
* runtime/src/app-tools.ts): Installed (version/source/Update-available
|
|
9
|
+
* badge, resync + uninstall actions), Featured + Available (grouped by
|
|
10
|
+
* category, tier/placement/requires chips), Install-from-URL (with the
|
|
11
|
+
* `.agentapp`-vs-git detection), Sources (warnings + stale flags),
|
|
12
|
+
* Builtin panels (collapsed) — and a never-blank empty state pointing at
|
|
13
|
+
* how to add a catalog source.
|
|
14
|
+
*
|
|
15
|
+
* `STORE_HTML` is a real single-file Vite build, not a hand-authored
|
|
16
|
+
* template string: the source lives in ./ui/** (index.html/main.ts/
|
|
17
|
+
* types.ts/render.ts/style.css, bundled by ./ui/vite.config.ts with
|
|
18
|
+
* vite-plugin-singlefile) and is committed, pre-built, into
|
|
19
|
+
* ./panel.generated.ts by `pnpm --filter @agentproto/apps run
|
|
20
|
+
* build:ui:store` (scripts/build-store-ui.mjs) — same committed-generated
|
|
21
|
+
* contract as work-board (panel.generated.ts lives in git,
|
|
22
|
+
* `build:ui:store:check` fails the CI drift gate). See ./ui/vite.config.ts
|
|
23
|
+
* for how the shared `panelBridgeScript` (../panel-bridge.ts) gets inlined
|
|
24
|
+
* as a classic `<script>` ahead of the bundled module script.
|
|
25
|
+
*
|
|
26
|
+
* Protocol: MCP Apps ext spec 2026-01-26
|
|
27
|
+
* – Bridge: JSON-RPC 2.0 over window.parent.postMessage (../panel-bridge.ts,
|
|
28
|
+
* unmodified by this change — this app only calls its `initBridge()`/
|
|
29
|
+
* `callTool()` surface).
|
|
30
|
+
* – Handshake: ui/initialize → host result → ui/notifications/initialized
|
|
31
|
+
* – Read: tools/call → app_catalog / app_list / app_updates
|
|
32
|
+
* – Apply (always through the SAME verbs every other caller uses — no
|
|
33
|
+
* second install path): app_install (CONFIRMED, two-call handshake),
|
|
34
|
+
* app_resync, app_uninstall
|
|
35
|
+
*/
|
|
36
|
+
/** The builtin's app id — how `app_catalog` reports the panel. Lives in this
|
|
37
|
+
* html-only entry (not ./index.ts) so a consumer that must NOT pull the
|
|
38
|
+
* app-kit dependency graph — the VS Code extension bundles with esbuild and
|
|
39
|
+
* chokes on its transitive native `.node` deps — can still name the panel
|
|
40
|
+
* from source. */
|
|
41
|
+
export declare const STORE_APP_ID = "@agentproto/store";
|
|
42
|
+
/** The MCP tool the panel is registered under (./index.ts `makeStoreApp`),
|
|
43
|
+
* hence its resource uri `ui://agentproto_store/view`. Follows the
|
|
44
|
+
* convention of every other builtin panel (`agentproto_work_board`,
|
|
45
|
+
* `agentproto_sessions`, …): `agentproto_<slug>`. */
|
|
46
|
+
export declare const STORE_TOOL_ID = "agentproto_store";
|
|
47
|
+
/** The daemon tools this panel's html calls, in order of use. Consumed as
|
|
48
|
+
* `ui.tools` by ./index.ts and as the client-side allowlist by any host
|
|
49
|
+
* that dispatches the panel's `tools/call` directly — a builtin is not an
|
|
50
|
+
* installed app, so `app_tool_call` cannot route for it. */
|
|
51
|
+
export declare const STORE_UI_TOOLS: readonly ["app_catalog", "app_list", "app_install", "app_resync", "app_updates", "app_uninstall", "app_status"];
|
|
52
|
+
export declare const STORE_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 app store</title>\n<!-- PANEL_BRIDGE: replaced with the inline panelBridgeScript() output by\n vite.config.ts's injectPanelBridge plugin \u2014 a classic (non-module)\n script so its `initBridge`/`callTool` land as globals before the\n deferred module script below runs. -->\n<script>// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500\n// JSON-RPC 2.0 over window.parent.postMessage \u00B7 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// \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \u2014 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 \u2014\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 \u2014 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) \u2014 capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 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// \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500\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 \u2014 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 \u2014 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 \u2014 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 \u2014\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 \u2014 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 \u2014 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>/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n The store panel's stylesheet \u2014 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 \u2014 the --st-* names here are this panel's own namespace.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\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";
|
|
53
|
+
//# sourceMappingURL=panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/store/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAIH;;;;mBAImB;AACnB,eAAO,MAAM,YAAY,sBAAsB,CAAA;AAE/C;;;sDAGsD;AACtD,eAAO,MAAM,aAAa,qBAAqB,CAAA;AAE/C;;;6DAG6D;AAC7D,eAAO,MAAM,cAAc,iHAQjB,CAAA;AAEV,eAAO,MAAM,UAAU,2huCAAuB,CAAA"}
|