@agentproto/apps 0.12.0 → 0.13.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.
Files changed (65) hide show
  1. package/dist/agents-overview/panel.mjs +14 -67
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +13 -68
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.d.ts.map +1 -1
  6. package/dist/bin/sync.mjs +2374 -2
  7. package/dist/bin/sync.mjs.map +1 -1
  8. package/dist/bureau-sessions/panel.mjs +14 -67
  9. package/dist/bureau-sessions/panel.mjs.map +1 -1
  10. package/dist/bureau-sessions.mjs +13 -68
  11. package/dist/bureau-sessions.mjs.map +1 -1
  12. package/dist/config/fragment.d.ts +50 -0
  13. package/dist/config/fragment.d.ts.map +1 -0
  14. package/dist/config/index.d.ts +23 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/ui.d.ts +43 -0
  17. package/dist/config/ui.d.ts.map +1 -0
  18. package/dist/config.mjs +2373 -0
  19. package/dist/config.mjs.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.mjs +2706 -138
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/live-session/logic.d.ts +2 -0
  25. package/dist/live-session/logic.d.ts.map +1 -1
  26. package/dist/live-session/panel.d.ts.map +1 -1
  27. package/dist/live-session/panel.mjs +27 -74
  28. package/dist/live-session/panel.mjs.map +1 -1
  29. package/dist/live-session.mjs +26 -75
  30. package/dist/live-session.mjs.map +1 -1
  31. package/dist/panel-bridge.d.ts +11 -6
  32. package/dist/panel-bridge.d.ts.map +1 -1
  33. package/dist/session-chat/index.d.ts.map +1 -1
  34. package/dist/session-chat/panel.mjs +14 -67
  35. package/dist/session-chat/panel.mjs.map +1 -1
  36. package/dist/session-chat.mjs +28 -69
  37. package/dist/session-chat.mjs.map +1 -1
  38. package/dist/session-story/panel.mjs +14 -67
  39. package/dist/session-story/panel.mjs.map +1 -1
  40. package/dist/session-story.mjs +13 -68
  41. package/dist/session-story.mjs.map +1 -1
  42. package/dist/session-viewer/ui.d.ts.map +1 -1
  43. package/dist/session-viewer.mjs +10 -0
  44. package/dist/session-viewer.mjs.map +1 -1
  45. package/dist/sessions-panel/panel.d.ts.map +1 -1
  46. package/dist/sessions-panel/panel.mjs +15 -67
  47. package/dist/sessions-panel/panel.mjs.map +1 -1
  48. package/dist/sessions-panel.mjs +14 -68
  49. package/dist/sessions-panel.mjs.map +1 -1
  50. package/dist/work-board/panel.d.ts +1 -1
  51. package/dist/work-board/panel.d.ts.map +1 -1
  52. package/dist/work-board/panel.generated.d.ts +1 -1
  53. package/dist/work-board/panel.generated.d.ts.map +1 -1
  54. package/dist/work-board/panel.mjs +293 -62
  55. package/dist/work-board/panel.mjs.map +1 -1
  56. package/dist/work-board.mjs +293 -62
  57. package/dist/work-board.mjs.map +1 -1
  58. package/package.json +12 -4
  59. package/repo-maintenance/.agentproto/APP.md +22 -0
  60. package/repo-maintenance/.agentproto/agents/repo-maintenance-reviewer/AGENT.md +142 -0
  61. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +262 -0
  62. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +672 -0
  63. package/repo-maintenance/README.md +80 -0
  64. package/repo-maintenance/routines/repo-maintenance-daily/ROUTINE.md +77 -0
  65. package/repo-maintenance/routines/repo-maintenance-weekly/ROUTINE.md +82 -0
@@ -65,6 +65,8 @@ export interface TurnEndRow extends RowBase {
65
65
  }
66
66
  export interface UsageInfo {
67
67
  size?: number;
68
+ /** True once `size` came from a cost-bearing (authoritative) usage_update. */
69
+ sizeAuthoritative?: boolean;
68
70
  used?: number;
69
71
  cost?: number;
70
72
  tokensIn?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"logic.d.ts","sourceRoot":"","sources":["../../src/live-session/logic.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"logic.d.ts","sourceRoot":"","sources":["../../src/live-session/logic.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/live-session/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAIH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;6DACyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;yDACqD;IACrD,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAs0BrE"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/live-session/panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAIH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;6DACyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;yDACqD;IACrD,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CA40BrE"}
@@ -1,9 +1,10 @@
1
+ import { DISPLAY_MODE_SCRIPT_BODY } from '@agentproto/app-client/display-mode';
2
+
1
3
  /**
2
4
  * @agentproto/apps v0.1.0
3
5
  * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
4
6
  */
5
7
 
6
- // src/panel-bridge.ts
7
8
  function panelBridgeScript(appName) {
8
9
  return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
9
10
  // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
@@ -80,7 +81,7 @@ function initBridge(){
80
81
  return window.McpApp.connect().then(function(conn){
81
82
  _standaloneApp = conn;
82
83
  // No host to advertise a hostContext \u2014 default to inline with no
83
- // other modes available, which keeps the #dm/#pin toggle buttons
84
+ // other modes available, which keeps the display-mode toggle buttons
84
85
  // hidden (they only show for modes hostContext.availableDisplayModes
85
86
  // lists) instead of rendering two buttons with nothing to switch to.
86
87
  _setHostContext({displayMode: 'inline', availableDisplayModes: []});
@@ -122,71 +123,17 @@ function callTool(name, args){
122
123
  });
123
124
  }
124
125
 
125
- // \u2500\u2500 Display-mode toggle buttons (NO auto-request) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
126
- // Injected by the shared bridge so every panel gets them without touching
127
- // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
128
- // panel stays inline by default; the user expands on demand. Buttons only
129
- // appear for modes the host advertises in hostContext.availableDisplayModes.
130
- (function(){
131
- function mount(){
132
- var style = document.createElement('style');
133
- style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
134
- + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
135
- + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
136
- + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
137
- + '#dm{right:8px}#pin{right:118px}'
138
- + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
139
- + '@media (prefers-color-scheme:dark){'
140
- + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
141
- + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
142
- document.head.appendChild(style);
143
-
144
- var btn = document.createElement('button');
145
- btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
146
- var pin = document.createElement('button');
147
- pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
148
- document.body.appendChild(pin);
149
- document.body.appendChild(btn);
150
-
151
- function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
152
-
153
- // Re-sync button visibility + label from the current host context.
154
- function syncBtn(ctx){
155
- ctx = ctx || {};
156
- var avail = ctx.availableDisplayModes;
157
- // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
158
- console.log('[mcp-app] displayMode=', ctx.displayMode,
159
- 'availableDisplayModes=', avail);
160
-
161
- // Fullscreen toggle button.
162
- if (has(avail, 'fullscreen')){
163
- btn.style.display = 'block';
164
- btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
165
- } else { btn.style.display = 'none'; }
166
-
167
- // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
168
- if (has(avail, 'pip')){
169
- pin.style.display = 'block';
170
- pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
171
- } else { pin.style.display = 'none'; }
172
- }
173
-
174
- onHostContext(syncBtn);
175
-
176
- btn.addEventListener('click', function(){
177
- var ctx = getHostContext() || {};
178
- var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
179
- requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
180
- });
181
-
182
- pin.addEventListener('click', function(){
183
- var ctx = getHostContext() || {};
184
- requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
185
- });
186
- }
187
- if (document.body) mount();
188
- else document.addEventListener('DOMContentLoaded', mount);
189
- })();`;
126
+ // \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
127
+ // The installer is inlined verbatim (it is self-contained and idempotent),
128
+ // then handed this panel's own JSON-RPC plumbing. NO auto-request: the
129
+ // panel stays inline until the user clicks, and the buttons only appear for
130
+ // the modes the host advertises in hostContext.availableDisplayModes.
131
+ ${DISPLAY_MODE_SCRIPT_BODY}
132
+ var displayMode = window.AgentprotoUI.installDisplayMode({
133
+ getHostContext: getHostContext,
134
+ onHostContext: onHostContext,
135
+ requestDisplayMode: requestDisplayMode
136
+ });`;
190
137
  }
191
138
 
192
139
  // src/live-session/panel.ts
@@ -397,14 +344,20 @@ function reduceEvent(state, record) {
397
344
  case 'usage_update': {
398
345
  // Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
399
346
  // the prior snapshot. state.rows is reused as-is (it didn't change).
400
- return {
401
- rows: state.rows,
402
- usage: {
403
- size: record.size, used: record.used, cost: record.cost,
404
- tokensIn: record.tokensIn, tokensOut: record.tokensOut,
405
- seq: record.seq, ts: record.ts,
406
- },
347
+ // Except the context window: a cost-bearing frame's size sticks (see
348
+ // the TS module's usage_update arm).
349
+ var sizeReported = typeof record.size === 'number' && record.size > 0;
350
+ var authoritative = record.cost !== undefined && sizeReported;
351
+ var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);
352
+ var keepSize = !authoritative && (prevAuth || !sizeReported);
353
+ var usage = {
354
+ size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,
355
+ used: record.used, cost: record.cost,
356
+ tokensIn: record.tokensIn, tokensOut: record.tokensOut,
357
+ seq: record.seq, ts: record.ts,
407
358
  };
359
+ if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;
360
+ return { rows: state.rows, usage: usage };
408
361
  }
409
362
  default:
410
363
  return state;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/panel-bridge.ts","../../src/live-session/panel.ts"],"names":[],"mappings":"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;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;AAguB9C","file":"panel.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 injects 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 * — same pattern as guilde's canvas.app.ts canvasShellHtml() (the reference\n * implementation that renders correctly in Claude hosts). No auto-request:\n * panels stay inline until the user clicks.\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 `#dm`/`#pin` 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\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 #dm/#pin 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 buttons (NO auto-request) ──────────────────────\n// Injected by the shared bridge so every panel gets them without touching\n// its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the\n// panel stays inline by default; the user expands on demand. Buttons only\n// appear for modes the host advertises in hostContext.availableDisplayModes.\n(function(){\n function mount(){\n var style = document.createElement('style');\n style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'\n + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'\n + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'\n + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'\n + '#dm{right:8px}#pin{right:118px}'\n + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'\n + '@media (prefers-color-scheme:dark){'\n + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'\n + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';\n document.head.appendChild(style);\n\n var btn = document.createElement('button');\n btn.id = 'dm'; btn.type = 'button'; btn.title = \"Basculer l'affichage\";\n var pin = document.createElement('button');\n pin.id = 'pin'; pin.type = 'button'; pin.title = 'Épingler sur le côté (pip)';\n document.body.appendChild(pin);\n document.body.appendChild(btn);\n\n function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }\n\n // Re-sync button visibility + label from the current host context.\n function syncBtn(ctx){\n ctx = ctx || {};\n var avail = ctx.availableDisplayModes;\n // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)\n console.log('[mcp-app] displayMode=', ctx.displayMode,\n 'availableDisplayModes=', avail);\n\n // Fullscreen toggle button.\n if (has(avail, 'fullscreen')){\n btn.style.display = 'block';\n btn.textContent = (ctx.displayMode === 'fullscreen') ? '⤡ Réduire' : '⤢ Agrandir';\n } else { btn.style.display = 'none'; }\n\n // Dedicated pip (\"pinned on side\") button — only if the host advertises pip.\n if (has(avail, 'pip')){\n pin.style.display = 'block';\n pin.textContent = (ctx.displayMode === 'pip') ? '⤡ Détacher' : '📌 Épingler';\n } else { pin.style.display = 'none'; }\n }\n\n onHostContext(syncBtn);\n\n btn.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');\n requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});\n });\n\n pin.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});\n });\n }\n if (document.body) mount();\n else document.addEventListener('DOMContentLoaded', mount);\n})();`\n}\n","/**\n * HTML bundle for the agentproto live-session widget — `LIVE_SESSION_HTML`,\n * a two-pane panel (left: live session tree, right: streaming timeline)\n * that lets a host attach to any running agent session and watch it work in\n * real time. Mounted via the `live_session` tool defined in ./index.ts.\n *\n * Kept in its own module, importing nothing beyond plain TS, so a consumer\n * that only needs the HTML (e.g. packages/vscode's story-panel-style\n * webviews, or a future live-session webview) can import it via the\n * `@agentproto/apps/live-session/panel` subpath without pulling in\n * `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from `./index.js` would.\n *\n * Forked structurally from terminal-panel-app.ts: same \"static ui://\n * resource renders with empty initData, widget self-bootstraps via\n * tools/call\" pattern. Unlike the terminal (one WebSocket, one PTY), this\n * widget has two independent data needs — a polled tree snapshot and a\n * per-session event timeline — so it layers on top of the bridge rather\n * than replacing it with a single socket.\n *\n * Data paths (see `.plans/CONTRACT.md` §WP-B):\n * LEFT (tree): poll `app_session_tree` via `callTool` every ~2s.\n * RIGHT (timeline): PRIMARY = `EventSource` on `/sessions/:id/events/stream`\n * (opportunistic — falls back if it doesn't open/emit\n * within ~2.5s); FALLBACK = poll `app_session_events`\n * via `callTool` every ~1.5s, advancing `since`.\n * Both app_* tools are registered by WP-A (`app-pull-tools.ts`) and are\n * `visibility: [\"app\"]`-only, i.e. reachable solely over this bridge.\n *\n * The timeline reducer lives in `live-session/logic.ts` as a pure,\n * dependency-free module so it's unit-testable outside the browser. This\n * file inlines a hand-kept COPY of that same logic into the HTML bundle\n * (see \"INLINED REDUCER COPY\" below) since the widget ships as one\n * self-contained script with no bundler step. Keep the two in sync. Its MCP\n * Apps handshake comes from the shared spec-correct panel bridge.\n *\n * Rendering (WP1): the timeline body is patched INCREMENTALLY — a\n * text-delta that merges into the last row patches only that row's text\n * node (found via `data-row-id`); every other new row is appended with one\n * `insertAdjacentHTML`. A full `innerHTML` reset happens only on session\n * start/`setFocus()` and on display-mode switches. Auto-scroll decision is\n * `isNearBottom` (inlined copy, SPEC §2) captured BEFORE any DOM mutation:\n * stick to bottom when the user was at the bottom, otherwise preserve the\n * read position exactly and surface a \"new messages\" pill.\n *\n * Compact mode (WP3/WP4): when `hostContext.displayMode === 'inline'` (or\n * the viewport is under ~640px) the tree pane collapses into a header\n * `<select>` (WP4) and the timeline renders through `groupAdjacentToolCalls`\n * (inlined copy, SPEC §3) with a colour rail and collapsible tool-call\n * groups. `setFocus()` remains the only session-switch entry point.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport type LiveSessionOutput = {\n /** Echoes the input id, or undefined to let the widget self-discover a\n * focus session client-side from `app_session_tree`. */\n sessionId?: string\n /** The daemon's own HTTP origin — connection recipe for both the SSE\n * stream and the bridge's `tools/call` fallback. */\n httpBaseUrl: string\n}\n\n// ── HTML bundle (zero CDN, self-contained) ──────────────────────────────────\n\nexport function LIVE_SESSION_HTML(initData: LiveSessionOutput): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto live session</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}\n#tree-head{padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}\n#tree-body{flex:1;overflow-y:auto;padding:6px}\n#timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}\n#timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}\n#timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}\n#head-summary{margin-left:auto;font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;white-space:nowrap;overflow:hidden;min-width:0}\n#head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}\n#head-summary .sdot.running{background:var(--green)}\n#head-summary .sdot.grey{background:var(--text3)}\n#head-summary .sdot.error{background:var(--red)}\n#usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}\n#status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}\n#timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}\n#head-selector{display:none;max-width:42%;font:inherit;font-size:12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:2px 4px}\n#new-pill{display:none;position:absolute;right:14px;bottom:12px;z-index:6;background:var(--blue);color:#0d1117;border:none;font-size:11px;font-weight:700;padding:5px 11px;border-radius:12px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4)}\n#new-pill.show{display:block}\n\n/* WP3 compact mode — colour rail + tighter rows; only under body.compact-mode\n (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */\nbody.compact-mode #tree-pane{display:none}\nbody.compact-mode #head-selector{display:inline-block}\nbody.compact-mode #focus-id-label{display:none}\nbody.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}\nbody.compact-mode .row.text{border-left-color:var(--blue)}\nbody.compact-mode .row.tool-call{border-left-color:var(--yellow)}\nbody.compact-mode .row.turn-end{border-left-color:var(--purple)}\nbody.compact-mode .row .body{font-size:12px;margin-top:3px}\nbody.compact-mode .row .rhead{font-size:10px}\ndetails.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}\ndetails.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}\ndetails.tool-group[open]>summary{margin-bottom:6px}\ndetails.tool-group .row{margin-top:5px}\n\n.tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}\n.tnode:hover{background:var(--bg3)}\n.tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}\n.tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.dot.running{background:var(--green)}\n.dot.grey{background:var(--text3)}\n.dot.error{background:var(--red)}\n.tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}\n.tnode .badge{font-size:9px;font-weight:700;color:var(--purple);border:1px solid var(--purple);border-radius:3px;padding:0 4px;flex-shrink:0}\n.tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}\n#tree-empty{padding:12px;color:var(--text3);font-size:12px}\n\n.row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}\n.row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}\n.row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}\n.row.text .kind{color:var(--blue)}\n.row.tool-call .kind{color:var(--yellow)}\n.row.turn-end .kind{color:var(--purple)}\n.row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}\n.row.text .body{font-family:inherit}\n.row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}\n.status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}\n.status-badge.pending{background:var(--bg3);color:var(--text2)}\n.status-badge.ok{background:#0f3d20;color:var(--green)}\n.status-badge.error{background:#3d1418;color:var(--red)}\n.row details{margin-top:6px}\n.row summary{cursor:pointer;font-size:11px;color:var(--text2)}\n.row pre{margin-top:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-word;color:var(--text2);background:var(--bg);border-radius:5px;padding:6px 8px;max-height:200px;overflow:auto}\n.chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}\n#timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"tree-pane\">\n <div id=\"tree-head\">Sessions</div>\n <div id=\"tree-body\"><div id=\"tree-empty\">Loading…</div></div>\n </div>\n <div id=\"timeline-pane\">\n <div id=\"timeline-head\">\n <span class=\"focus-id\" id=\"focus-id-label\">—</span>\n <select id=\"head-selector\" title=\"Sessions\"></select>\n <span id=\"head-summary\"></span>\n <span id=\"status-line\">connecting…</span>\n </div>\n <div id=\"timeline-body\"><div id=\"timeline-empty\">No session focused yet.</div></div>\n <button id=\"new-pill\" type=\"button\">↓ New messages</button>\n </div>\n</div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-live-session\")}\n\n// ============================================================\n// INLINED REDUCER COPY — hand-kept mirror of live-session/logic.ts.\n// Plain JS, same semantics: coalesce consecutive text-delta of the same\n// session (and rejoin an unterminated mid-line fragment split by an\n// interleaved record — see the TS module's text-delta arm), pair\n// tool-call/tool-result by toolCallId, pass through turn-end, keep usage\n// as STATE (SPEC §1: usage leaves the timeline — a usage_update record\n// never produces a row), ignore unknown kinds. Keep in sync with the TS\n// module; the TS module is the one the test suite imports.\n// ============================================================\n\nfunction initialTimelineState() {\n return { rows: [], usage: null };\n}\n\nfunction rowId(record, rows) {\n return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);\n}\n\n// Fold a text-delta record into an existing row (fresh object), keeping the\n// row's \"partial\" hint in step with the latest record — see mergeTextDelta in\n// the TS module.\nfunction mergeTextDelta(row, record) {\n var merged = Object.assign({}, row, {\n text: row.text + (record.text || ''),\n seq: record.seq,\n ts: record.ts,\n });\n if (record.partial === true) merged.partial = true;\n else delete merged.partial;\n return merged;\n}\n\nfunction reduceEvent(state, record) {\n switch (record.kind) {\n case 'text-delta': {\n var last = state.rows[state.rows.length - 1];\n if (last && last.kind === 'text' && last.sessionId === record.sessionId) {\n return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };\n }\n // Debounce can flush an unterminated mid-word fragment (flagged\n // partial), let a tool-call land, then flush the continuation — look\n // back within the same turn (bounded by this session's last turn-end)\n // for that session's most recent text row and continue it in place.\n // Only the explicit partial flag glues: a non-partial record with no\n // trailing newline is the writer's normal end-of-text-block shape.\n for (var i = state.rows.length - 1; i >= 0; i--) {\n var prior = state.rows[i];\n if (prior.sessionId !== record.sessionId) continue;\n if (prior.kind === 'turn-end') break;\n if (prior.kind !== 'text') continue;\n if (prior.partial === true) {\n var patched = state.rows.slice();\n patched[i] = mergeTextDelta(prior, record);\n return { rows: patched, usage: state.usage };\n }\n break;\n }\n var row = {\n kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, text: record.text || '',\n };\n if (record.partial === true) row.partial = true;\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-call': {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '',\n toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-result': {\n var idx = -1;\n for (var i = 0; i < state.rows.length; i++) {\n if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;\n }\n if (idx === -1) {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',\n status: record.isError ? 'error' : 'ok', result: record.result,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n var updated = Object.assign({}, state.rows[idx], {\n status: record.isError ? 'error' : 'ok', result: record.result,\n });\n var rows = state.rows.slice();\n rows[idx] = updated;\n return { rows: rows, usage: state.usage };\n }\n case 'turn-end': {\n var row = {\n kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, reason: record.reason,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'usage_update': {\n // Usage is state, not a row (SPEC §1) — last-write-wins, no merge with\n // the prior snapshot. state.rows is reused as-is (it didn't change).\n return {\n rows: state.rows,\n usage: {\n size: record.size, used: record.used, cost: record.cost,\n tokensIn: record.tokensIn, tokensOut: record.tokensOut,\n seq: record.seq, ts: record.ts,\n },\n };\n }\n default:\n return state;\n }\n}\n\n// ============================================================\n// INLINED PURE HELPERS — exact copies of live-session/logic.ts's\n// isNearBottom (SPEC §2) and groupAdjacentToolCalls (SPEC §3). Same names,\n// same signatures, same default thresholds; plain JS because the widget\n// has no import step.\n// ============================================================\n\nvar SCROLL_STICK_THRESHOLD_PX = 24;\n\nfunction isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {\n if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;\n return scrollHeight - scrollTop - clientHeight <= threshold;\n}\n\nvar TOOL_CALL_GROUP_THRESHOLD = 2;\n\n// Collapse runs of \\`threshold\\`+ adjacent tool-call rows into one\n// {kind:'tool-group', rows:[...]} entry; everything else passes through as\n// individual {kind:'row', row} entries, same order as the input.\nfunction groupAdjacentToolCalls(rows, threshold) {\n if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;\n var out = [];\n var run = [];\n function flushRun() {\n if (!run.length) return;\n if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });\n else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });\n run = [];\n }\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }\n flushRun();\n out.push({ kind: 'row', row: rows[i] });\n }\n flushRun();\n return out;\n}\n\n// ============================================================\n// Rendering helpers\n// ============================================================\n\nfunction escHtml(s) {\n return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n}\n\nfunction safeJson(v) {\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n}\n\nfunction statusDotClass(status) {\n if (status === 'running' || status === 'starting') return 'running';\n if (status === 'error' || status === 'killed') return 'error';\n return 'grey';\n}\n\n// SPEC §4 (non-contract guidance): 52524 → \"52.5k\", >=1e6 → \"M\" suffix,\n// one decimal, trailing .0 vanishes because the value stays a number.\n// cost → \"$\" + toFixed(2). Read from timelineState.usage.\nfunction fmtCompactNum(n) {\n if (typeof n !== 'number' || !isFinite(n)) return null;\n if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';\n if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';\n return String(n);\n}\n\nfunction usageChipText(usage) {\n if (!usage) return '';\n var bits = [];\n if (usage.used != null && usage.size != null) {\n bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));\n } else if (usage.used != null) {\n bits.push(fmtCompactNum(usage.used));\n }\n if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));\n return bits.join(' · ');\n}\n\n// WP5: elapsed seconds since the timeline's first row (running only).\nfunction elapsedText(rows, status) {\n if (status !== 'running' && status !== 'starting') return null;\n if (!rows.length || rows[0].ts == null) return null;\n var ms = new Date(rows[0].ts).getTime();\n if (!isFinite(ms)) return null;\n return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';\n}\n\n// ============================================================\n// LEFT pane — live tree\n// ============================================================\n\nvar currentTree = [];\nvar focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;\n// Where the current focus came from: 'pinned' (the tool result that mounted\n// this widget named a session — never auto-switched away), 'auto' (self-\n// discovered newest running session, a guess the host may still correct),\n// 'user' (explicit click/selector choice — outranks everything).\nvar focusSource = focusId ? 'pinned' : null;\nvar bootDone = false;\nvar treeTimer = null;\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId —\n// keep in sync (same convention as the reducer copy below).\nfunction extractToolResultSessionId(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try {\n var body = JSON.parse(item.text);\n if (body && typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (body && typeof body.id === 'string' && body.id) return body.id;\n } catch (_) {}\n return null;\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session instead of leaving\n// pollTree() to self-discover the newest running one, which made every\n// agent_start card show the latest started session rather than its own.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var id = extractToolResultSessionId(params);\n if (!id || focusSource === 'user' || id === focusId) return;\n focusSource = 'pinned';\n if (bootDone) setFocus(id);\n else focusId = id; // boot's own startTimeline(focusId) picks it up\n});\n\nfunction findNode(nodes, id) {\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].id === id) return nodes[i];\n var found = findNode(nodes[i].children || [], id);\n if (found) return found;\n }\n return null;\n}\n\nfunction flattenDfs(nodes, out) {\n out = out || [];\n for (var i = 0; i < nodes.length; i++) {\n out.push(nodes[i]);\n flattenDfs(nodes[i].children || [], out);\n }\n return out;\n}\n\nfunction pickInitialFocus(tree) {\n var flat = flattenDfs(tree);\n var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });\n if (alive.length) return alive[alive.length - 1].id;\n return tree.length ? tree[0].id : null;\n}\n\nfunction renderTreeNode(node, depth) {\n var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');\n var badge = node.isOrchestrator ? '<span class=\"badge\">orch</span>' : '';\n var cls = 'tnode' + (node.id === focusId ? ' focus' : '');\n return '<div class=\"' + cls + '\" data-id=\"' + escHtml(node.id) + '\">' +\n '<span class=\"dot ' + statusDotClass(node.status) + '\"></span>' +\n '<span class=\"label\">' + escHtml(node.label || node.id) + '</span>' + badge +\n '</div>' +\n (childrenHtml ? '<div class=\"tchildren\">' + childrenHtml + '</div>' : '');\n}\n\n// WP4: compact header <select> mirrors the tree; still routes through\n// setFocus() — the only session-switch entry point.\nfunction renderHeadSelector() {\n var sel = document.getElementById('head-selector');\n var flat = flattenDfs(currentTree);\n while (sel.firstChild) sel.removeChild(sel.firstChild);\n if (!flat.length) return;\n for (var i = 0; i < flat.length; i++) {\n var n = flat[i];\n var o = document.createElement('option');\n o.value = n.id;\n o.textContent = (n.id === focusId ? '● ' : '') + (n.label || n.id);\n sel.appendChild(o);\n }\n sel.value = focusId || '';\n}\n\nfunction renderTree() {\n var body = document.getElementById('tree-body');\n var root = focusId ? findNode(currentTree, focusId) : null;\n var renderNodes = root ? [root] : currentTree;\n if (!renderNodes.length) {\n body.innerHTML = '<div id=\"tree-empty\">No sessions.</div>';\n renderHeadSelector();\n return;\n }\n body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');\n var els = body.querySelectorAll('.tnode');\n els.forEach(function(el) {\n el.addEventListener('click', function() {\n var id = el.getAttribute('data-id');\n if (id === focusId) return;\n focusSource = 'user';\n setFocus(id);\n });\n });\n renderHeadSelector();\n}\n\nfunction pollTree() {\n callTool('app_session_tree', {}).then(function(res) {\n currentTree = res.tree || [];\n if (!focusId) {\n focusId = pickInitialFocus(currentTree);\n if (focusId) { focusSource = 'auto'; startTimeline(focusId); }\n }\n renderTree();\n updateHeader();\n }).catch(function() {\n // Leave the last-known tree rendered; the next poll may recover.\n });\n}\n\n// ============================================================\n// RIGHT pane — timeline for focusId\n// ============================================================\n\nvar timelineState = initialTimelineState();\nvar sincePtr = 0;\nvar activeSource = null; // {type:'sse', es} | {type:'poll', timer}\nvar compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)\n\nfunction setStatus(msg) {\n document.getElementById('status-line').textContent = msg;\n}\n\nfunction isCompact() {\n return (getHostContext() && getHostContext().displayMode === 'inline') ||\n (typeof window !== 'undefined' && window.innerWidth < 640);\n}\n\n// WP3/WP4: apply the compact/expanded split. Only re-renders the timeline\n// when the mode actually flips (scroll + <details> state survive otherwise).\nfunction applyDisplayMode() {\n var c = isCompact();\n document.body.classList.toggle('compact-mode', c);\n if (c !== compactMode) {\n compactMode = c;\n renderTimelineFull();\n renderTree();\n }\n}\n\n// WP5 + WP2: one header line — \\`● running · 3 tools · 52.5k/200k · $0.04 · 12s\\`\n// (status dot from the focus node, tool count from the rows, usage from\n// timelineState.usage — no usage_update row anymore) + transport status.\nfunction updateHeader() {\n document.getElementById('focus-id-label').textContent = focusId || '—';\n var rows = timelineState.rows || [];\n var usage = timelineState.usage;\n var tools = 0;\n for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;\n var node = focusId ? findNode(currentTree, focusId) : null;\n var st = node ? node.status : null;\n var parts = [];\n if (st) parts.push('<span class=\"sdot ' + statusDotClass(st) + '\"></span>' + escHtml(st));\n if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));\n var chip = usageChipText(usage);\n if (chip) parts.push('<span id=\"usage-chip\" class=\"chip\">' + escHtml(chip) + '</span>');\n var el = elapsedText(rows, st);\n if (el) parts.push(escHtml(el));\n document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' · ') : '';\n}\n\nfunction captureDetailsOpenFlags(body) {\n var dets = body.querySelectorAll('details');\n var flags = [];\n for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);\n return flags;\n}\n\nfunction restoreDetailsOpenFlags(body, flags) {\n var dets = body.querySelectorAll('details');\n for (var i = 0; i < dets.length && i < flags.length; i++) {\n if (flags[i]) dets[i].open = true;\n }\n}\n\n// Full rebuild — used ONLY for the first paint of a session, a setFocus()\n// session switch (SPEC §2: full reset is correct there), a display-mode\n// flip, and as the fallback for shapes the incremental patcher can't patch.\n// A display-mode flip re-renders the SAME content (WP3: \"réactif... sans\n// perdre le scroll ni l'état des <details>\"), so open <details> and the\n// exact scroll offset (not just the at-bottom decision) survive the rebuild.\nfunction renderTimelineFull() {\n var body = document.getElementById('timeline-body');\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var savedScrollTop = body.scrollTop;\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;\n updateHeader();\n}\n\nfunction buildTimelineHtml() {\n var rows = timelineState.rows;\n if (!rows.length) return '<div id=\"timeline-empty\">No events yet.</div>';\n if (compactMode) {\n var entries = groupAdjacentToolCalls(rows);\n var html = '';\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i];\n if (e.kind === 'tool-group') {\n html += '<details class=\"tool-group\"><summary>▸ ' + e.rows.length + ' tool calls</summary>' +\n e.rows.map(renderRow).join('') + '</details>';\n } else {\n html += renderRow(e.row);\n }\n }\n return html;\n }\n return rows.map(renderRow).join('');\n}\n\n// ── WP1 incremental patch (dense mode) ─────────────────────────────────\n// One record, one minimal DOM mutation. The common case (text-delta merged\n// into the LAST rendered row) patches just that row's text node. Anything\n// else appends via insertAdjacentHTML. Only rare in-place replacements\n// (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.\nfunction applyRecordToDom(prevRows) {\n var body = document.getElementById('timeline-body');\n // Capture \"stuck to bottom?\" BEFORE any mutation (SPEC §2).\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var rows = timelineState.rows;\n\n if (compactMode) {\n // Inline mode: smaller rows — full grouped rebuild, preserving\n // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM\n // (and the scroll/pill decision) when the rows actually changed — a\n // usage_update leaves rows === prevRows, so it must fall through to the\n // header-only path below, same as the dense-mode branch.\n if (rows !== prevRows) {\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n }\n updateHeader();\n return;\n }\n\n var appended = rows.length > prevRows.length;\n if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {\n // First real paint of this session (empty placeholder present) or an\n // unexpected shape — full reset is correct/cheapest.\n body.innerHTML = buildTimelineHtml();\n body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n\n if (appended) {\n body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));\n } else if (rows.length === prevRows.length && rows.length > 0) {\n // Exactly one row object replaced (tool-result merge, partial-text\n // re-merge via the reducer's lookback). Patch that one element only.\n // Scan from the tail: the hot path (a text-delta merged into the LAST\n // row, the dominant streaming case) is found in O(1) this way instead\n // of walking the whole array — the rare mid-array lookback re-merge\n // still resolves correctly, just slower.\n var idx = -1;\n for (var i = rows.length - 1; i >= 0; i--) {\n if (rows[i] !== prevRows[i]) { idx = i; break; }\n }\n var patched = false;\n if (idx >= 0) {\n var row = rows[idx];\n var el = body.querySelector('[data-row-id=\"' + escHtml(row.id) + '\"]');\n if (el) {\n if (row.kind === 'text' && idx === rows.length - 1) {\n // High-frequency case: patch the existing text node in place.\n var b = el.querySelector('.body');\n if (b) { b.textContent = row.text; patched = true; }\n }\n if (!patched) { el.outerHTML = renderRow(row); patched = true; }\n }\n }\n if (!patched) {\n // Multiple rows changed at once (shouldn't happen per-record) — rebuild.\n body.innerHTML = buildTimelineHtml();\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n } else {\n // rows unchanged (usage_update) — header only.\n updateHeader();\n return;\n }\n\n // Scroll decision AFTER the mutation: stick or leave the position alone.\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n updateHeader();\n}\n\nfunction samePrefix(prevRows, rows) {\n for (var i = 0; i < prevRows.length; i++) {\n if (prevRows[i] !== rows[i]) return false;\n }\n return true;\n}\n\nfunction showNewPill() {\n document.getElementById('new-pill').classList.add('show');\n}\n\nfunction hideNewPill() {\n document.getElementById('new-pill').classList.remove('show');\n}\n\nfunction renderRow(row) {\n if (row.kind === 'text') {\n return '<div class=\"row text\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">text</span></div>' +\n '<div class=\"body\">' + escHtml(row.text) + '</div></div>';\n }\n if (row.kind === 'tool-call') {\n var badgeCls = row.status === 'pending' ? 'pending' : row.status;\n var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');\n var resultHtml = row.status !== 'pending'\n ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'\n : '';\n return '<div class=\"row tool-call\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">tool</span>' +\n '<span class=\"toolname\">' + escHtml(row.toolName) + '</span>' +\n '<span class=\"status-badge ' + badgeCls + '\">' + badgeText + '</span></div>' +\n '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +\n resultHtml + '</div>';\n }\n if (row.kind === 'turn-end') {\n return '<div class=\"row turn-end\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">turn end</span>' +\n '<span class=\"chip\">' + escHtml(row.reason || '—') + '</span></div></div>';\n }\n return '';\n}\n\nfunction teardownTimeline() {\n if (activeSource) {\n if (activeSource.type === 'sse' && activeSource.es) {\n try { activeSource.es.close(); } catch (e) {}\n }\n // Both the poll re-arm timer AND the SSE open/first-message fallback\n // timer must be cleared, or a focus switch within the 2.5s window leaves\n // a stale timer that fires startPolling for the abandoned session.\n if (activeSource.timer) clearTimeout(activeSource.timer);\n if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);\n }\n activeSource = null;\n}\n\nfunction attemptSSE(id) {\n var settled = false;\n var es;\n try {\n es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');\n } catch (e) {\n startPolling(id);\n return;\n }\n var src = { type: 'sse', es: es, fallbackTimer: null };\n activeSource = src;\n // Every callback below re-checks focusId===id AND that src is still the\n // live source: a focus switch tears down src and starts a new one, so a\n // late open/message/error from this abandoned EventSource must be a no-op\n // rather than mutating the new focus's timeline or clobbering activeSource.\n function stale() { return activeSource !== src || focusId !== id; }\n src.fallbackTimer = setTimeout(function() {\n if (settled || stale()) return;\n settled = true;\n try { es.close(); } catch (e) {}\n startPolling(id);\n }, 2500);\n es.addEventListener('open', function() {\n if (settled || stale()) return;\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n });\n es.onmessage = function(evt) {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n }\n var rec;\n try { rec = JSON.parse(evt.data); } catch (e) { return; }\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, rec);\n if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n };\n es.onerror = function() {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n try { es.close(); } catch (e) {}\n startPolling(id);\n } else {\n // Do not leave EventSource to perform its native reconnect: this\n // endpoint replays from since=0 when no cursor is provided, so a\n // transparent reconnect would duplicate every row already reduced.\n // The bridge poll resumes from sincePtr instead and preserves the\n // exactly-once cursor contract.\n try { es.close(); } catch (e) {}\n startPolling(id);\n }\n };\n}\n\nfunction startPolling(id) {\n activeSource = { type: 'poll', timer: null };\n setStatus('polling');\n function tick() {\n if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;\n callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {\n var events = res.events || [];\n for (var i = 0; i < events.length; i++) {\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, events[i]);\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n }\n if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;\n setStatus('polling');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n }).catch(function() {\n setStatus('disconnected');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n });\n }\n tick();\n}\n\nfunction startTimeline(id) {\n teardownTimeline();\n timelineState = initialTimelineState();\n sincePtr = 0;\n hideNewPill();\n renderTimelineFull();\n setStatus('connecting…');\n attemptSSE(id);\n}\n\nfunction setFocus(id) {\n focusId = id;\n renderTree();\n startTimeline(id);\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n var init = window.__APP_INIT__ || {};\n if (init.httpBaseUrl) return init;\n // Static ui:// resources render once at server-registration time with\n // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) — fall back to\n // calling the tool ourselves over the bridge, same as the other panels.\n return callTool('live_session', {}).then(function(result) {\n window.__APP_INIT__ = result;\n return result;\n });\n}).then(function(init) {\n // Only override the focus when live_session itself named a session AND a\n // tool-result notification didn't already pin one while the fallback\n // tools/call above was in flight — the host's push names the exact\n // session this widget was mounted for, so it outranks the fallback's\n // (argument-less, hence session-less in practice) self-call.\n if (init.sessionId && focusSource !== 'pinned') {\n focusId = init.sessionId;\n focusSource = 'pinned';\n }\n onHostContext(function() {\n applyDisplayMode();\n updateHeader();\n });\n document.getElementById('head-selector').addEventListener('change', function(evt) {\n var id = evt.target.value;\n if (id && id !== focusId) { focusSource = 'user'; setFocus(id); }\n });\n document.getElementById('new-pill').addEventListener('click', function() {\n var body = document.getElementById('timeline-body');\n body.scrollTop = body.scrollHeight;\n hideNewPill();\n });\n document.getElementById('timeline-body').addEventListener('scroll', function() {\n // User scrolled back near the bottom — the pill is stale, hide it.\n var el = document.getElementById('timeline-body');\n if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();\n });\n window.addEventListener('resize', function() {\n var c = isCompact();\n if (c !== compactMode) { applyDisplayMode(); }\n });\n applyDisplayMode();\n bootDone = true;\n pollTree();\n treeTimer = setInterval(pollTree, 2000);\n setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh\n if (focusId) startTimeline(focusId);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n});\n</script>\n</body>\n</html>`\n}\n"]}
1
+ {"version":3,"sources":["../../src/panel-bridge.ts","../../src/live-session/panel.ts"],"names":[],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;AAsuB9C","file":"panel.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 live-session widget — `LIVE_SESSION_HTML`,\n * a two-pane panel (left: live session tree, right: streaming timeline)\n * that lets a host attach to any running agent session and watch it work in\n * real time. Mounted via the `live_session` tool defined in ./index.ts.\n *\n * Kept in its own module, importing nothing beyond plain TS, so a consumer\n * that only needs the HTML (e.g. packages/vscode's story-panel-style\n * webviews, or a future live-session webview) can import it via the\n * `@agentproto/apps/live-session/panel` subpath without pulling in\n * `@agentproto/app-kit` (and therefore `@mastra/core`) the way importing\n * from `./index.js` would.\n *\n * Forked structurally from terminal-panel-app.ts: same \"static ui://\n * resource renders with empty initData, widget self-bootstraps via\n * tools/call\" pattern. Unlike the terminal (one WebSocket, one PTY), this\n * widget has two independent data needs — a polled tree snapshot and a\n * per-session event timeline — so it layers on top of the bridge rather\n * than replacing it with a single socket.\n *\n * Data paths (see `.plans/CONTRACT.md` §WP-B):\n * LEFT (tree): poll `app_session_tree` via `callTool` every ~2s.\n * RIGHT (timeline): PRIMARY = `EventSource` on `/sessions/:id/events/stream`\n * (opportunistic — falls back if it doesn't open/emit\n * within ~2.5s); FALLBACK = poll `app_session_events`\n * via `callTool` every ~1.5s, advancing `since`.\n * Both app_* tools are registered by WP-A (`app-pull-tools.ts`) and are\n * `visibility: [\"app\"]`-only, i.e. reachable solely over this bridge.\n *\n * The timeline reducer lives in `live-session/logic.ts` as a pure,\n * dependency-free module so it's unit-testable outside the browser. This\n * file inlines a hand-kept COPY of that same logic into the HTML bundle\n * (see \"INLINED REDUCER COPY\" below) since the widget ships as one\n * self-contained script with no bundler step. Keep the two in sync. Its MCP\n * Apps handshake comes from the shared spec-correct panel bridge.\n *\n * Rendering (WP1): the timeline body is patched INCREMENTALLY — a\n * text-delta that merges into the last row patches only that row's text\n * node (found via `data-row-id`); every other new row is appended with one\n * `insertAdjacentHTML`. A full `innerHTML` reset happens only on session\n * start/`setFocus()` and on display-mode switches. Auto-scroll decision is\n * `isNearBottom` (inlined copy, SPEC §2) captured BEFORE any DOM mutation:\n * stick to bottom when the user was at the bottom, otherwise preserve the\n * read position exactly and surface a \"new messages\" pill.\n *\n * Compact mode (WP3/WP4): when `hostContext.displayMode === 'inline'` (or\n * the viewport is under ~640px) the tree pane collapses into a header\n * `<select>` (WP4) and the timeline renders through `groupAdjacentToolCalls`\n * (inlined copy, SPEC §3) with a colour rail and collapsible tool-call\n * groups. `setFocus()` remains the only session-switch entry point.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\nexport type LiveSessionOutput = {\n /** Echoes the input id, or undefined to let the widget self-discover a\n * focus session client-side from `app_session_tree`. */\n sessionId?: string\n /** The daemon's own HTTP origin — connection recipe for both the SSE\n * stream and the bridge's `tools/call` fallback. */\n httpBaseUrl: string\n}\n\n// ── HTML bundle (zero CDN, self-contained) ──────────────────────────────────\n\nexport function LIVE_SESSION_HTML(initData: LiveSessionOutput): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto live session</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}\n#tree-head{padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}\n#tree-body{flex:1;overflow-y:auto;padding:6px}\n#timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}\n#timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}\n#timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}\n#head-summary{margin-left:auto;font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;white-space:nowrap;overflow:hidden;min-width:0}\n#head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}\n#head-summary .sdot.running{background:var(--green)}\n#head-summary .sdot.grey{background:var(--text3)}\n#head-summary .sdot.error{background:var(--red)}\n#usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}\n#status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}\n#timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}\n#head-selector{display:none;max-width:42%;font:inherit;font-size:12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:2px 4px}\n#new-pill{display:none;position:absolute;right:14px;bottom:12px;z-index:6;background:var(--blue);color:#0d1117;border:none;font-size:11px;font-weight:700;padding:5px 11px;border-radius:12px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4)}\n#new-pill.show{display:block}\n\n/* WP3 compact mode — colour rail + tighter rows; only under body.compact-mode\n (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */\nbody.compact-mode #tree-pane{display:none}\nbody.compact-mode #head-selector{display:inline-block}\nbody.compact-mode #focus-id-label{display:none}\nbody.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}\nbody.compact-mode .row.text{border-left-color:var(--blue)}\nbody.compact-mode .row.tool-call{border-left-color:var(--yellow)}\nbody.compact-mode .row.turn-end{border-left-color:var(--purple)}\nbody.compact-mode .row .body{font-size:12px;margin-top:3px}\nbody.compact-mode .row .rhead{font-size:10px}\ndetails.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}\ndetails.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}\ndetails.tool-group[open]>summary{margin-bottom:6px}\ndetails.tool-group .row{margin-top:5px}\n\n.tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}\n.tnode:hover{background:var(--bg3)}\n.tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}\n.tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.dot.running{background:var(--green)}\n.dot.grey{background:var(--text3)}\n.dot.error{background:var(--red)}\n.tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}\n.tnode .badge{font-size:9px;font-weight:700;color:var(--purple);border:1px solid var(--purple);border-radius:3px;padding:0 4px;flex-shrink:0}\n.tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}\n#tree-empty{padding:12px;color:var(--text3);font-size:12px}\n\n.row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}\n.row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}\n.row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}\n.row.text .kind{color:var(--blue)}\n.row.tool-call .kind{color:var(--yellow)}\n.row.turn-end .kind{color:var(--purple)}\n.row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}\n.row.text .body{font-family:inherit}\n.row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}\n.status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}\n.status-badge.pending{background:var(--bg3);color:var(--text2)}\n.status-badge.ok{background:#0f3d20;color:var(--green)}\n.status-badge.error{background:#3d1418;color:var(--red)}\n.row details{margin-top:6px}\n.row summary{cursor:pointer;font-size:11px;color:var(--text2)}\n.row pre{margin-top:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-word;color:var(--text2);background:var(--bg);border-radius:5px;padding:6px 8px;max-height:200px;overflow:auto}\n.chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}\n#timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"tree-pane\">\n <div id=\"tree-head\">Sessions</div>\n <div id=\"tree-body\"><div id=\"tree-empty\">Loading…</div></div>\n </div>\n <div id=\"timeline-pane\">\n <div id=\"timeline-head\">\n <span class=\"focus-id\" id=\"focus-id-label\">—</span>\n <select id=\"head-selector\" title=\"Sessions\"></select>\n <span id=\"head-summary\"></span>\n <span id=\"status-line\">connecting…</span>\n </div>\n <div id=\"timeline-body\"><div id=\"timeline-empty\">No session focused yet.</div></div>\n <button id=\"new-pill\" type=\"button\">↓ New messages</button>\n </div>\n</div>\n<script>\nwindow.__APP_INIT__ = ${JSON.stringify(initData)};\n\n${panelBridgeScript(\"agentproto-live-session\")}\n\n// ============================================================\n// INLINED REDUCER COPY — hand-kept mirror of live-session/logic.ts.\n// Plain JS, same semantics: coalesce consecutive text-delta of the same\n// session (and rejoin an unterminated mid-line fragment split by an\n// interleaved record — see the TS module's text-delta arm), pair\n// tool-call/tool-result by toolCallId, pass through turn-end, keep usage\n// as STATE (SPEC §1: usage leaves the timeline — a usage_update record\n// never produces a row), ignore unknown kinds. Keep in sync with the TS\n// module; the TS module is the one the test suite imports.\n// ============================================================\n\nfunction initialTimelineState() {\n return { rows: [], usage: null };\n}\n\nfunction rowId(record, rows) {\n return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);\n}\n\n// Fold a text-delta record into an existing row (fresh object), keeping the\n// row's \"partial\" hint in step with the latest record — see mergeTextDelta in\n// the TS module.\nfunction mergeTextDelta(row, record) {\n var merged = Object.assign({}, row, {\n text: row.text + (record.text || ''),\n seq: record.seq,\n ts: record.ts,\n });\n if (record.partial === true) merged.partial = true;\n else delete merged.partial;\n return merged;\n}\n\nfunction reduceEvent(state, record) {\n switch (record.kind) {\n case 'text-delta': {\n var last = state.rows[state.rows.length - 1];\n if (last && last.kind === 'text' && last.sessionId === record.sessionId) {\n return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };\n }\n // Debounce can flush an unterminated mid-word fragment (flagged\n // partial), let a tool-call land, then flush the continuation — look\n // back within the same turn (bounded by this session's last turn-end)\n // for that session's most recent text row and continue it in place.\n // Only the explicit partial flag glues: a non-partial record with no\n // trailing newline is the writer's normal end-of-text-block shape.\n for (var i = state.rows.length - 1; i >= 0; i--) {\n var prior = state.rows[i];\n if (prior.sessionId !== record.sessionId) continue;\n if (prior.kind === 'turn-end') break;\n if (prior.kind !== 'text') continue;\n if (prior.partial === true) {\n var patched = state.rows.slice();\n patched[i] = mergeTextDelta(prior, record);\n return { rows: patched, usage: state.usage };\n }\n break;\n }\n var row = {\n kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, text: record.text || '',\n };\n if (record.partial === true) row.partial = true;\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-call': {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '',\n toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'tool-result': {\n var idx = -1;\n for (var i = 0; i < state.rows.length; i++) {\n if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;\n }\n if (idx === -1) {\n var row = {\n kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',\n status: record.isError ? 'error' : 'ok', result: record.result,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n var updated = Object.assign({}, state.rows[idx], {\n status: record.isError ? 'error' : 'ok', result: record.result,\n });\n var rows = state.rows.slice();\n rows[idx] = updated;\n return { rows: rows, usage: state.usage };\n }\n case 'turn-end': {\n var row = {\n kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,\n sessionId: record.sessionId, reason: record.reason,\n };\n return { rows: state.rows.concat([row]), usage: state.usage };\n }\n case 'usage_update': {\n // Usage is state, not a row (SPEC §1) — last-write-wins, no merge with\n // the prior snapshot. state.rows is reused as-is (it didn't change).\n // Except the context window: a cost-bearing frame's size sticks (see\n // the TS module's usage_update arm).\n var sizeReported = typeof record.size === 'number' && record.size > 0;\n var authoritative = record.cost !== undefined && sizeReported;\n var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);\n var keepSize = !authoritative && (prevAuth || !sizeReported);\n var usage = {\n size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,\n used: record.used, cost: record.cost,\n tokensIn: record.tokensIn, tokensOut: record.tokensOut,\n seq: record.seq, ts: record.ts,\n };\n if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;\n return { rows: state.rows, usage: usage };\n }\n default:\n return state;\n }\n}\n\n// ============================================================\n// INLINED PURE HELPERS — exact copies of live-session/logic.ts's\n// isNearBottom (SPEC §2) and groupAdjacentToolCalls (SPEC §3). Same names,\n// same signatures, same default thresholds; plain JS because the widget\n// has no import step.\n// ============================================================\n\nvar SCROLL_STICK_THRESHOLD_PX = 24;\n\nfunction isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {\n if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;\n return scrollHeight - scrollTop - clientHeight <= threshold;\n}\n\nvar TOOL_CALL_GROUP_THRESHOLD = 2;\n\n// Collapse runs of \\`threshold\\`+ adjacent tool-call rows into one\n// {kind:'tool-group', rows:[...]} entry; everything else passes through as\n// individual {kind:'row', row} entries, same order as the input.\nfunction groupAdjacentToolCalls(rows, threshold) {\n if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;\n var out = [];\n var run = [];\n function flushRun() {\n if (!run.length) return;\n if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });\n else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });\n run = [];\n }\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }\n flushRun();\n out.push({ kind: 'row', row: rows[i] });\n }\n flushRun();\n return out;\n}\n\n// ============================================================\n// Rendering helpers\n// ============================================================\n\nfunction escHtml(s) {\n return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n}\n\nfunction safeJson(v) {\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n}\n\nfunction statusDotClass(status) {\n if (status === 'running' || status === 'starting') return 'running';\n if (status === 'error' || status === 'killed') return 'error';\n return 'grey';\n}\n\n// SPEC §4 (non-contract guidance): 52524 → \"52.5k\", >=1e6 → \"M\" suffix,\n// one decimal, trailing .0 vanishes because the value stays a number.\n// cost → \"$\" + toFixed(2). Read from timelineState.usage.\nfunction fmtCompactNum(n) {\n if (typeof n !== 'number' || !isFinite(n)) return null;\n if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';\n if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';\n return String(n);\n}\n\nfunction usageChipText(usage) {\n if (!usage) return '';\n var bits = [];\n if (usage.used != null && usage.size != null) {\n bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));\n } else if (usage.used != null) {\n bits.push(fmtCompactNum(usage.used));\n }\n if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));\n return bits.join(' · ');\n}\n\n// WP5: elapsed seconds since the timeline's first row (running only).\nfunction elapsedText(rows, status) {\n if (status !== 'running' && status !== 'starting') return null;\n if (!rows.length || rows[0].ts == null) return null;\n var ms = new Date(rows[0].ts).getTime();\n if (!isFinite(ms)) return null;\n return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';\n}\n\n// ============================================================\n// LEFT pane — live tree\n// ============================================================\n\nvar currentTree = [];\nvar focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;\n// Where the current focus came from: 'pinned' (the tool result that mounted\n// this widget named a session — never auto-switched away), 'auto' (self-\n// discovered newest running session, a guess the host may still correct),\n// 'user' (explicit click/selector choice — outranks everything).\nvar focusSource = focusId ? 'pinned' : null;\nvar bootDone = false;\nvar treeTimer = null;\n\n// INLINED COPY of live-session/logic.ts extractToolResultSessionId —\n// keep in sync (same convention as the reducer copy below).\nfunction extractToolResultSessionId(params) {\n if (!params || typeof params !== 'object') return null;\n var res = (params.result && typeof params.result === 'object') ? params.result : params;\n if (res.isError) return null;\n var content = Array.isArray(res.content) ? res.content : [];\n var item = content[0];\n if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;\n try {\n var body = JSON.parse(item.text);\n if (body && typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;\n if (body && typeof body.id === 'string' && body.id) return body.id;\n } catch (_) {}\n return null;\n}\n\n// The host pushes the triggering tool call's result (ext-apps\n// ui/notifications/tool-result). For agent_start that result IS the spawned\n// session's descriptor — pin the widget to THAT session instead of leaving\n// pollTree() to self-discover the newest running one, which made every\n// agent_start card show the latest started session rather than its own.\nonHostNotification(function(method, params) {\n if (method !== 'ui/notifications/tool-result') return;\n var id = extractToolResultSessionId(params);\n if (!id || focusSource === 'user' || id === focusId) return;\n focusSource = 'pinned';\n if (bootDone) setFocus(id);\n else focusId = id; // boot's own startTimeline(focusId) picks it up\n});\n\nfunction findNode(nodes, id) {\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].id === id) return nodes[i];\n var found = findNode(nodes[i].children || [], id);\n if (found) return found;\n }\n return null;\n}\n\nfunction flattenDfs(nodes, out) {\n out = out || [];\n for (var i = 0; i < nodes.length; i++) {\n out.push(nodes[i]);\n flattenDfs(nodes[i].children || [], out);\n }\n return out;\n}\n\nfunction pickInitialFocus(tree) {\n var flat = flattenDfs(tree);\n var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });\n if (alive.length) return alive[alive.length - 1].id;\n return tree.length ? tree[0].id : null;\n}\n\nfunction renderTreeNode(node, depth) {\n var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');\n var badge = node.isOrchestrator ? '<span class=\"badge\">orch</span>' : '';\n var cls = 'tnode' + (node.id === focusId ? ' focus' : '');\n return '<div class=\"' + cls + '\" data-id=\"' + escHtml(node.id) + '\">' +\n '<span class=\"dot ' + statusDotClass(node.status) + '\"></span>' +\n '<span class=\"label\">' + escHtml(node.label || node.id) + '</span>' + badge +\n '</div>' +\n (childrenHtml ? '<div class=\"tchildren\">' + childrenHtml + '</div>' : '');\n}\n\n// WP4: compact header <select> mirrors the tree; still routes through\n// setFocus() — the only session-switch entry point.\nfunction renderHeadSelector() {\n var sel = document.getElementById('head-selector');\n var flat = flattenDfs(currentTree);\n while (sel.firstChild) sel.removeChild(sel.firstChild);\n if (!flat.length) return;\n for (var i = 0; i < flat.length; i++) {\n var n = flat[i];\n var o = document.createElement('option');\n o.value = n.id;\n o.textContent = (n.id === focusId ? '● ' : '') + (n.label || n.id);\n sel.appendChild(o);\n }\n sel.value = focusId || '';\n}\n\nfunction renderTree() {\n var body = document.getElementById('tree-body');\n var root = focusId ? findNode(currentTree, focusId) : null;\n var renderNodes = root ? [root] : currentTree;\n if (!renderNodes.length) {\n body.innerHTML = '<div id=\"tree-empty\">No sessions.</div>';\n renderHeadSelector();\n return;\n }\n body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');\n var els = body.querySelectorAll('.tnode');\n els.forEach(function(el) {\n el.addEventListener('click', function() {\n var id = el.getAttribute('data-id');\n if (id === focusId) return;\n focusSource = 'user';\n setFocus(id);\n });\n });\n renderHeadSelector();\n}\n\nfunction pollTree() {\n callTool('app_session_tree', {}).then(function(res) {\n currentTree = res.tree || [];\n if (!focusId) {\n focusId = pickInitialFocus(currentTree);\n if (focusId) { focusSource = 'auto'; startTimeline(focusId); }\n }\n renderTree();\n updateHeader();\n }).catch(function() {\n // Leave the last-known tree rendered; the next poll may recover.\n });\n}\n\n// ============================================================\n// RIGHT pane — timeline for focusId\n// ============================================================\n\nvar timelineState = initialTimelineState();\nvar sincePtr = 0;\nvar activeSource = null; // {type:'sse', es} | {type:'poll', timer}\nvar compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)\n\nfunction setStatus(msg) {\n document.getElementById('status-line').textContent = msg;\n}\n\nfunction isCompact() {\n return (getHostContext() && getHostContext().displayMode === 'inline') ||\n (typeof window !== 'undefined' && window.innerWidth < 640);\n}\n\n// WP3/WP4: apply the compact/expanded split. Only re-renders the timeline\n// when the mode actually flips (scroll + <details> state survive otherwise).\nfunction applyDisplayMode() {\n var c = isCompact();\n document.body.classList.toggle('compact-mode', c);\n if (c !== compactMode) {\n compactMode = c;\n renderTimelineFull();\n renderTree();\n }\n}\n\n// WP5 + WP2: one header line — \\`● running · 3 tools · 52.5k/200k · $0.04 · 12s\\`\n// (status dot from the focus node, tool count from the rows, usage from\n// timelineState.usage — no usage_update row anymore) + transport status.\nfunction updateHeader() {\n document.getElementById('focus-id-label').textContent = focusId || '—';\n var rows = timelineState.rows || [];\n var usage = timelineState.usage;\n var tools = 0;\n for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;\n var node = focusId ? findNode(currentTree, focusId) : null;\n var st = node ? node.status : null;\n var parts = [];\n if (st) parts.push('<span class=\"sdot ' + statusDotClass(st) + '\"></span>' + escHtml(st));\n if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));\n var chip = usageChipText(usage);\n if (chip) parts.push('<span id=\"usage-chip\" class=\"chip\">' + escHtml(chip) + '</span>');\n var el = elapsedText(rows, st);\n if (el) parts.push(escHtml(el));\n document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' · ') : '';\n}\n\nfunction captureDetailsOpenFlags(body) {\n var dets = body.querySelectorAll('details');\n var flags = [];\n for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);\n return flags;\n}\n\nfunction restoreDetailsOpenFlags(body, flags) {\n var dets = body.querySelectorAll('details');\n for (var i = 0; i < dets.length && i < flags.length; i++) {\n if (flags[i]) dets[i].open = true;\n }\n}\n\n// Full rebuild — used ONLY for the first paint of a session, a setFocus()\n// session switch (SPEC §2: full reset is correct there), a display-mode\n// flip, and as the fallback for shapes the incremental patcher can't patch.\n// A display-mode flip re-renders the SAME content (WP3: \"réactif... sans\n// perdre le scroll ni l'état des <details>\"), so open <details> and the\n// exact scroll offset (not just the at-bottom decision) survive the rebuild.\nfunction renderTimelineFull() {\n var body = document.getElementById('timeline-body');\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var savedScrollTop = body.scrollTop;\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;\n updateHeader();\n}\n\nfunction buildTimelineHtml() {\n var rows = timelineState.rows;\n if (!rows.length) return '<div id=\"timeline-empty\">No events yet.</div>';\n if (compactMode) {\n var entries = groupAdjacentToolCalls(rows);\n var html = '';\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i];\n if (e.kind === 'tool-group') {\n html += '<details class=\"tool-group\"><summary>▸ ' + e.rows.length + ' tool calls</summary>' +\n e.rows.map(renderRow).join('') + '</details>';\n } else {\n html += renderRow(e.row);\n }\n }\n return html;\n }\n return rows.map(renderRow).join('');\n}\n\n// ── WP1 incremental patch (dense mode) ─────────────────────────────────\n// One record, one minimal DOM mutation. The common case (text-delta merged\n// into the LAST rendered row) patches just that row's text node. Anything\n// else appends via insertAdjacentHTML. Only rare in-place replacements\n// (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.\nfunction applyRecordToDom(prevRows) {\n var body = document.getElementById('timeline-body');\n // Capture \"stuck to bottom?\" BEFORE any mutation (SPEC §2).\n var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);\n var rows = timelineState.rows;\n\n if (compactMode) {\n // Inline mode: smaller rows — full grouped rebuild, preserving\n // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM\n // (and the scroll/pill decision) when the rows actually changed — a\n // usage_update leaves rows === prevRows, so it must fall through to the\n // header-only path below, same as the dense-mode branch.\n if (rows !== prevRows) {\n var flags = captureDetailsOpenFlags(body);\n body.innerHTML = buildTimelineHtml();\n restoreDetailsOpenFlags(body, flags);\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n }\n updateHeader();\n return;\n }\n\n var appended = rows.length > prevRows.length;\n if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {\n // First real paint of this session (empty placeholder present) or an\n // unexpected shape — full reset is correct/cheapest.\n body.innerHTML = buildTimelineHtml();\n body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n\n if (appended) {\n body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));\n } else if (rows.length === prevRows.length && rows.length > 0) {\n // Exactly one row object replaced (tool-result merge, partial-text\n // re-merge via the reducer's lookback). Patch that one element only.\n // Scan from the tail: the hot path (a text-delta merged into the LAST\n // row, the dominant streaming case) is found in O(1) this way instead\n // of walking the whole array — the rare mid-array lookback re-merge\n // still resolves correctly, just slower.\n var idx = -1;\n for (var i = rows.length - 1; i >= 0; i--) {\n if (rows[i] !== prevRows[i]) { idx = i; break; }\n }\n var patched = false;\n if (idx >= 0) {\n var row = rows[idx];\n var el = body.querySelector('[data-row-id=\"' + escHtml(row.id) + '\"]');\n if (el) {\n if (row.kind === 'text' && idx === rows.length - 1) {\n // High-frequency case: patch the existing text node in place.\n var b = el.querySelector('.body');\n if (b) { b.textContent = row.text; patched = true; }\n }\n if (!patched) { el.outerHTML = renderRow(row); patched = true; }\n }\n }\n if (!patched) {\n // Multiple rows changed at once (shouldn't happen per-record) — rebuild.\n body.innerHTML = buildTimelineHtml();\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n updateHeader();\n return;\n }\n } else {\n // rows unchanged (usage_update) — header only.\n updateHeader();\n return;\n }\n\n // Scroll decision AFTER the mutation: stick or leave the position alone.\n if (wasAtBottom) body.scrollTop = body.scrollHeight;\n else showNewPill();\n updateHeader();\n}\n\nfunction samePrefix(prevRows, rows) {\n for (var i = 0; i < prevRows.length; i++) {\n if (prevRows[i] !== rows[i]) return false;\n }\n return true;\n}\n\nfunction showNewPill() {\n document.getElementById('new-pill').classList.add('show');\n}\n\nfunction hideNewPill() {\n document.getElementById('new-pill').classList.remove('show');\n}\n\nfunction renderRow(row) {\n if (row.kind === 'text') {\n return '<div class=\"row text\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">text</span></div>' +\n '<div class=\"body\">' + escHtml(row.text) + '</div></div>';\n }\n if (row.kind === 'tool-call') {\n var badgeCls = row.status === 'pending' ? 'pending' : row.status;\n var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');\n var resultHtml = row.status !== 'pending'\n ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'\n : '';\n return '<div class=\"row tool-call\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">tool</span>' +\n '<span class=\"toolname\">' + escHtml(row.toolName) + '</span>' +\n '<span class=\"status-badge ' + badgeCls + '\">' + badgeText + '</span></div>' +\n '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +\n resultHtml + '</div>';\n }\n if (row.kind === 'turn-end') {\n return '<div class=\"row turn-end\" data-row-id=\"' + escHtml(row.id) + '\"><div class=\"rhead\"><span class=\"kind\">turn end</span>' +\n '<span class=\"chip\">' + escHtml(row.reason || '—') + '</span></div></div>';\n }\n return '';\n}\n\nfunction teardownTimeline() {\n if (activeSource) {\n if (activeSource.type === 'sse' && activeSource.es) {\n try { activeSource.es.close(); } catch (e) {}\n }\n // Both the poll re-arm timer AND the SSE open/first-message fallback\n // timer must be cleared, or a focus switch within the 2.5s window leaves\n // a stale timer that fires startPolling for the abandoned session.\n if (activeSource.timer) clearTimeout(activeSource.timer);\n if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);\n }\n activeSource = null;\n}\n\nfunction attemptSSE(id) {\n var settled = false;\n var es;\n try {\n es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');\n } catch (e) {\n startPolling(id);\n return;\n }\n var src = { type: 'sse', es: es, fallbackTimer: null };\n activeSource = src;\n // Every callback below re-checks focusId===id AND that src is still the\n // live source: a focus switch tears down src and starts a new one, so a\n // late open/message/error from this abandoned EventSource must be a no-op\n // rather than mutating the new focus's timeline or clobbering activeSource.\n function stale() { return activeSource !== src || focusId !== id; }\n src.fallbackTimer = setTimeout(function() {\n if (settled || stale()) return;\n settled = true;\n try { es.close(); } catch (e) {}\n startPolling(id);\n }, 2500);\n es.addEventListener('open', function() {\n if (settled || stale()) return;\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n });\n es.onmessage = function(evt) {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n setStatus('streaming via SSE');\n }\n var rec;\n try { rec = JSON.parse(evt.data); } catch (e) { return; }\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, rec);\n if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n };\n es.onerror = function() {\n if (stale()) return;\n if (!settled) {\n settled = true;\n clearTimeout(src.fallbackTimer);\n try { es.close(); } catch (e) {}\n startPolling(id);\n } else {\n // Do not leave EventSource to perform its native reconnect: this\n // endpoint replays from since=0 when no cursor is provided, so a\n // transparent reconnect would duplicate every row already reduced.\n // The bridge poll resumes from sincePtr instead and preserves the\n // exactly-once cursor contract.\n try { es.close(); } catch (e) {}\n startPolling(id);\n }\n };\n}\n\nfunction startPolling(id) {\n activeSource = { type: 'poll', timer: null };\n setStatus('polling');\n function tick() {\n if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;\n callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {\n var events = res.events || [];\n for (var i = 0; i < events.length; i++) {\n var prev = timelineState;\n timelineState = reduceEvent(timelineState, events[i]);\n if (timelineState !== prev) applyRecordToDom(prev.rows);\n }\n if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;\n setStatus('polling');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n }).catch(function() {\n setStatus('disconnected');\n if (activeSource) activeSource.timer = setTimeout(tick, 1500);\n });\n }\n tick();\n}\n\nfunction startTimeline(id) {\n teardownTimeline();\n timelineState = initialTimelineState();\n sincePtr = 0;\n hideNewPill();\n renderTimelineFull();\n setStatus('connecting…');\n attemptSSE(id);\n}\n\nfunction setFocus(id) {\n focusId = id;\n renderTree();\n startTimeline(id);\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n var init = window.__APP_INIT__ || {};\n if (init.httpBaseUrl) return init;\n // Static ui:// resources render once at server-registration time with\n // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) — fall back to\n // calling the tool ourselves over the bridge, same as the other panels.\n return callTool('live_session', {}).then(function(result) {\n window.__APP_INIT__ = result;\n return result;\n });\n}).then(function(init) {\n // Only override the focus when live_session itself named a session AND a\n // tool-result notification didn't already pin one while the fallback\n // tools/call above was in flight — the host's push names the exact\n // session this widget was mounted for, so it outranks the fallback's\n // (argument-less, hence session-less in practice) self-call.\n if (init.sessionId && focusSource !== 'pinned') {\n focusId = init.sessionId;\n focusSource = 'pinned';\n }\n onHostContext(function() {\n applyDisplayMode();\n updateHeader();\n });\n document.getElementById('head-selector').addEventListener('change', function(evt) {\n var id = evt.target.value;\n if (id && id !== focusId) { focusSource = 'user'; setFocus(id); }\n });\n document.getElementById('new-pill').addEventListener('click', function() {\n var body = document.getElementById('timeline-body');\n body.scrollTop = body.scrollHeight;\n hideNewPill();\n });\n document.getElementById('timeline-body').addEventListener('scroll', function() {\n // User scrolled back near the bottom — the pill is stale, hide it.\n var el = document.getElementById('timeline-body');\n if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();\n });\n window.addEventListener('resize', function() {\n var c = isCompact();\n if (c !== compactMode) { applyDisplayMode(); }\n });\n applyDisplayMode();\n bootDone = true;\n pollTree();\n treeTimer = setInterval(pollTree, 2000);\n setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh\n if (focusId) startTimeline(focusId);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n});\n</script>\n</body>\n</html>`\n}\n"]}
@@ -1,13 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { defineApp } from '@agentproto/app-kit';
3
+ import { DISPLAY_MODE_SCRIPT_BODY } from '@agentproto/app-client/display-mode';
3
4
 
4
5
  /**
5
6
  * @agentproto/apps v0.1.0
6
7
  * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
7
8
  */
8
9
 
9
-
10
- // src/panel-bridge.ts
11
10
  function panelBridgeScript(appName) {
12
11
  return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
13
12
  // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
@@ -84,7 +83,7 @@ function initBridge(){
84
83
  return window.McpApp.connect().then(function(conn){
85
84
  _standaloneApp = conn;
86
85
  // No host to advertise a hostContext \u2014 default to inline with no
87
- // other modes available, which keeps the #dm/#pin toggle buttons
86
+ // other modes available, which keeps the display-mode toggle buttons
88
87
  // hidden (they only show for modes hostContext.availableDisplayModes
89
88
  // lists) instead of rendering two buttons with nothing to switch to.
90
89
  _setHostContext({displayMode: 'inline', availableDisplayModes: []});
@@ -126,71 +125,17 @@ function callTool(name, args){
126
125
  });
127
126
  }
128
127
 
129
- // \u2500\u2500 Display-mode toggle buttons (NO auto-request) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
130
- // Injected by the shared bridge so every panel gets them without touching
131
- // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
132
- // panel stays inline by default; the user expands on demand. Buttons only
133
- // appear for modes the host advertises in hostContext.availableDisplayModes.
134
- (function(){
135
- function mount(){
136
- var style = document.createElement('style');
137
- style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
138
- + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
139
- + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
140
- + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
141
- + '#dm{right:8px}#pin{right:118px}'
142
- + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
143
- + '@media (prefers-color-scheme:dark){'
144
- + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
145
- + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
146
- document.head.appendChild(style);
147
-
148
- var btn = document.createElement('button');
149
- btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
150
- var pin = document.createElement('button');
151
- pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
152
- document.body.appendChild(pin);
153
- document.body.appendChild(btn);
154
-
155
- function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
156
-
157
- // Re-sync button visibility + label from the current host context.
158
- function syncBtn(ctx){
159
- ctx = ctx || {};
160
- var avail = ctx.availableDisplayModes;
161
- // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
162
- console.log('[mcp-app] displayMode=', ctx.displayMode,
163
- 'availableDisplayModes=', avail);
164
-
165
- // Fullscreen toggle button.
166
- if (has(avail, 'fullscreen')){
167
- btn.style.display = 'block';
168
- btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
169
- } else { btn.style.display = 'none'; }
170
-
171
- // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
172
- if (has(avail, 'pip')){
173
- pin.style.display = 'block';
174
- pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
175
- } else { pin.style.display = 'none'; }
176
- }
177
-
178
- onHostContext(syncBtn);
179
-
180
- btn.addEventListener('click', function(){
181
- var ctx = getHostContext() || {};
182
- var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
183
- requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
184
- });
185
-
186
- pin.addEventListener('click', function(){
187
- var ctx = getHostContext() || {};
188
- requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
189
- });
190
- }
191
- if (document.body) mount();
192
- else document.addEventListener('DOMContentLoaded', mount);
193
- })();`;
128
+ // \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
129
+ // The installer is inlined verbatim (it is self-contained and idempotent),
130
+ // then handed this panel's own JSON-RPC plumbing. NO auto-request: the
131
+ // panel stays inline until the user clicks, and the buttons only appear for
132
+ // the modes the host advertises in hostContext.availableDisplayModes.
133
+ ${DISPLAY_MODE_SCRIPT_BODY}
134
+ var displayMode = window.AgentprotoUI.installDisplayMode({
135
+ getHostContext: getHostContext,
136
+ onHostContext: onHostContext,
137
+ requestDisplayMode: requestDisplayMode
138
+ });`;
194
139
  }
195
140
 
196
141
  // src/live-session/panel.ts
@@ -401,14 +346,20 @@ function reduceEvent(state, record) {
401
346
  case 'usage_update': {
402
347
  // Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
403
348
  // the prior snapshot. state.rows is reused as-is (it didn't change).
404
- return {
405
- rows: state.rows,
406
- usage: {
407
- size: record.size, used: record.used, cost: record.cost,
408
- tokensIn: record.tokensIn, tokensOut: record.tokensOut,
409
- seq: record.seq, ts: record.ts,
410
- },
349
+ // Except the context window: a cost-bearing frame's size sticks (see
350
+ // the TS module's usage_update arm).
351
+ var sizeReported = typeof record.size === 'number' && record.size > 0;
352
+ var authoritative = record.cost !== undefined && sizeReported;
353
+ var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);
354
+ var keepSize = !authoritative && (prevAuth || !sizeReported);
355
+ var usage = {
356
+ size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,
357
+ used: record.used, cost: record.cost,
358
+ tokensIn: record.tokensIn, tokensOut: record.tokensOut,
359
+ seq: record.seq, ts: record.ts,
411
360
  };
361
+ if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;
362
+ return { rows: state.rows, usage: usage };
412
363
  }
413
364
  default:
414
365
  return state;