@agentproto/apps 0.8.2 → 0.9.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 (60) hide show
  1. package/README.md +28 -1
  2. package/dist/agents-overview/index.d.ts +54 -0
  3. package/dist/agents-overview/index.d.ts.map +1 -0
  4. package/dist/agents-overview/panel.d.ts +14 -0
  5. package/dist/agents-overview/panel.d.ts.map +1 -0
  6. package/dist/agents-overview/panel.mjs +306 -0
  7. package/dist/agents-overview/panel.mjs.map +1 -0
  8. package/dist/agents-overview.mjs +336 -0
  9. package/dist/agents-overview.mjs.map +1 -0
  10. package/dist/bin/sync.mjs +25 -11
  11. package/dist/bin/sync.mjs.map +1 -1
  12. package/dist/bureau-sessions/index.d.ts +52 -0
  13. package/dist/bureau-sessions/index.d.ts.map +1 -0
  14. package/dist/bureau-sessions/panel.d.ts +10 -0
  15. package/dist/bureau-sessions/panel.d.ts.map +1 -0
  16. package/dist/bureau-sessions/panel.mjs +297 -0
  17. package/dist/bureau-sessions/panel.mjs.map +1 -0
  18. package/dist/bureau-sessions.mjs +329 -0
  19. package/dist/bureau-sessions.mjs.map +1 -0
  20. package/dist/content-team/workflows/produce-content.d.ts +14 -2
  21. package/dist/content-team/workflows/produce-content.d.ts.map +1 -1
  22. package/dist/content-team/workflows/produce-cover.d.ts +13 -1
  23. package/dist/content-team/workflows/produce-cover.d.ts.map +1 -1
  24. package/dist/content-team.mjs +25 -11
  25. package/dist/content-team.mjs.map +1 -1
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.mjs +2635 -12
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/live-session/index.d.ts +46 -0
  31. package/dist/live-session/index.d.ts.map +1 -0
  32. package/dist/live-session/logic.d.ts +125 -0
  33. package/dist/live-session/logic.d.ts.map +1 -0
  34. package/dist/live-session/panel.d.ts +61 -0
  35. package/dist/live-session/panel.d.ts.map +1 -0
  36. package/dist/live-session/panel.mjs +993 -0
  37. package/dist/live-session/panel.mjs.map +1 -0
  38. package/dist/live-session.mjs +1032 -0
  39. package/dist/live-session.mjs.map +1 -0
  40. package/dist/mcp-app-types.d.ts +33 -0
  41. package/dist/mcp-app-types.d.ts.map +1 -0
  42. package/dist/panel-bridge.d.ts +38 -0
  43. package/dist/panel-bridge.d.ts.map +1 -0
  44. package/dist/session-story/index.d.ts +66 -0
  45. package/dist/session-story/index.d.ts.map +1 -0
  46. package/dist/session-story/panel.d.ts +31 -0
  47. package/dist/session-story/panel.d.ts.map +1 -0
  48. package/dist/session-story/panel.mjs +970 -0
  49. package/dist/session-story/panel.mjs.map +1 -0
  50. package/dist/session-story.mjs +1002 -0
  51. package/dist/session-story.mjs.map +1 -0
  52. package/dist/sessions-panel/index.d.ts +51 -0
  53. package/dist/sessions-panel/index.d.ts.map +1 -0
  54. package/dist/sessions-panel/panel.d.ts +20 -0
  55. package/dist/sessions-panel/panel.d.ts.map +1 -0
  56. package/dist/sessions-panel/panel.mjs +511 -0
  57. package/dist/sessions-panel/panel.mjs.map +1 -0
  58. package/dist/sessions-panel.mjs +545 -0
  59. package/dist/sessions-panel.mjs.map +1 -0
  60. package/package.json +56 -5
@@ -0,0 +1,993 @@
1
+ /**
2
+ * @agentproto/apps v0.1.0
3
+ * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
4
+ */
5
+
6
+ // src/panel-bridge.ts
7
+ function panelBridgeScript(appName) {
8
+ return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
9
+ // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
10
+ var _nextId = 1, _pending = {}, _notifyHandlers = [];
11
+ var _hostContext = null, _hostContextHandlers = [];
12
+ function post(msg){ window.parent.postMessage(msg, '*'); }
13
+ function getHostContext(){ return _hostContext; }
14
+ function onHostContext(cb){
15
+ _hostContextHandlers.push(cb);
16
+ // Replay the last context so a late subscriber isn't stuck blind.
17
+ if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
18
+ }
19
+ function _setHostContext(ctx){
20
+ if (!ctx || typeof ctx !== 'object') return;
21
+ // ui/notifications/host-context-changed carries only the changed keys \u2014
22
+ // merge, matching the official ext-apps App behaviour.
23
+ _hostContext = Object.assign({}, _hostContext || {}, ctx);
24
+ for (var i = 0; i < _hostContextHandlers.length; i++){
25
+ try { _hostContextHandlers[i](_hostContext); } catch(_) {}
26
+ }
27
+ }
28
+ function rpcRequest(method, params){
29
+ return new Promise(function(resolve, reject){
30
+ var id = _nextId++;
31
+ _pending[id] = {resolve: resolve, reject: reject};
32
+ post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
33
+ });
34
+ }
35
+ function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
36
+ function onHostNotification(cb){ _notifyHandlers.push(cb); }
37
+ window.addEventListener('message', function(evt){
38
+ var msg = evt.data;
39
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
40
+ if (msg.id != null && msg.method == null){
41
+ var p = _pending[msg.id];
42
+ if (!p) return;
43
+ delete _pending[msg.id];
44
+ if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
45
+ else p.resolve(msg.result);
46
+ return;
47
+ }
48
+ if (msg.method){
49
+ if (msg.method === 'ui/notifications/host-context-changed'){
50
+ _setHostContext(msg.params || {});
51
+ }
52
+ for (var i = 0; i < _notifyHandlers.length; i++){
53
+ try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
54
+ }
55
+ }
56
+ });
57
+ function initBridge(){
58
+ return rpcRequest('ui/initialize', {
59
+ appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
60
+ appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
61
+ protocolVersion: '2026-01-26'
62
+ }).then(function(result){
63
+ // The initialize result carries the initial hostContext (displayMode +
64
+ // availableDisplayModes) \u2014 capture it before notifying the host.
65
+ if (result && result.hostContext) _setHostContext(result.hostContext);
66
+ rpcNotify('ui/notifications/initialized', {});
67
+ });
68
+ }
69
+ function requestDisplayMode(mode){
70
+ return rpcRequest('ui/request-display-mode', {mode: mode});
71
+ }
72
+ function callTool(name, args){
73
+ return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
74
+ if (result.isError){
75
+ var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
76
+ throw new Error(e);
77
+ }
78
+ var text = (result.content && result.content[0] && result.content[0].text) || '{}';
79
+ return JSON.parse(text);
80
+ });
81
+ }
82
+
83
+ // \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
84
+ // Injected by the shared bridge so every panel gets them without touching
85
+ // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
86
+ // panel stays inline by default; the user expands on demand. Buttons only
87
+ // appear for modes the host advertises in hostContext.availableDisplayModes.
88
+ (function(){
89
+ function mount(){
90
+ var style = document.createElement('style');
91
+ style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
92
+ + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
93
+ + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
94
+ + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
95
+ + '#dm{right:8px}#pin{right:118px}'
96
+ + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
97
+ + '@media (prefers-color-scheme:dark){'
98
+ + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
99
+ + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
100
+ document.head.appendChild(style);
101
+
102
+ var btn = document.createElement('button');
103
+ btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
104
+ var pin = document.createElement('button');
105
+ pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
106
+ document.body.appendChild(pin);
107
+ document.body.appendChild(btn);
108
+
109
+ function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
110
+
111
+ // Re-sync button visibility + label from the current host context.
112
+ function syncBtn(ctx){
113
+ ctx = ctx || {};
114
+ var avail = ctx.availableDisplayModes;
115
+ // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
116
+ console.log('[mcp-app] displayMode=', ctx.displayMode,
117
+ 'availableDisplayModes=', avail);
118
+
119
+ // Fullscreen toggle button.
120
+ if (has(avail, 'fullscreen')){
121
+ btn.style.display = 'block';
122
+ btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
123
+ } else { btn.style.display = 'none'; }
124
+
125
+ // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
126
+ if (has(avail, 'pip')){
127
+ pin.style.display = 'block';
128
+ pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
129
+ } else { pin.style.display = 'none'; }
130
+ }
131
+
132
+ onHostContext(syncBtn);
133
+
134
+ btn.addEventListener('click', function(){
135
+ var ctx = getHostContext() || {};
136
+ var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
137
+ requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
138
+ });
139
+
140
+ pin.addEventListener('click', function(){
141
+ var ctx = getHostContext() || {};
142
+ requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
143
+ });
144
+ }
145
+ if (document.body) mount();
146
+ else document.addEventListener('DOMContentLoaded', mount);
147
+ })();`;
148
+ }
149
+
150
+ // src/live-session/panel.ts
151
+ function LIVE_SESSION_HTML(initData) {
152
+ return `<!DOCTYPE html>
153
+ <html lang="en">
154
+ <head>
155
+ <meta charset="UTF-8">
156
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
+ <title>agentproto live session</title>
158
+ <style>
159
+ *{box-sizing:border-box;margin:0;padding:0}
160
+ :root{
161
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
162
+ --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
163
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
164
+ }
165
+ html,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}
166
+ #app{display:flex;height:100%}
167
+ #tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}
168
+ #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}
169
+ #tree-body{flex:1;overflow-y:auto;padding:6px}
170
+ #timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}
171
+ #timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}
172
+ #timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}
173
+ #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}
174
+ #head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}
175
+ #head-summary .sdot.running{background:var(--green)}
176
+ #head-summary .sdot.grey{background:var(--text3)}
177
+ #head-summary .sdot.error{background:var(--red)}
178
+ #usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}
179
+ #status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}
180
+ #timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}
181
+ #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}
182
+ #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)}
183
+ #new-pill.show{display:block}
184
+
185
+ /* WP3 compact mode \u2014 colour rail + tighter rows; only under body.compact-mode
186
+ (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */
187
+ body.compact-mode #tree-pane{display:none}
188
+ body.compact-mode #head-selector{display:inline-block}
189
+ body.compact-mode #focus-id-label{display:none}
190
+ body.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}
191
+ body.compact-mode .row.text{border-left-color:var(--blue)}
192
+ body.compact-mode .row.tool-call{border-left-color:var(--yellow)}
193
+ body.compact-mode .row.turn-end{border-left-color:var(--purple)}
194
+ body.compact-mode .row .body{font-size:12px;margin-top:3px}
195
+ body.compact-mode .row .rhead{font-size:10px}
196
+ details.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}
197
+ details.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}
198
+ details.tool-group[open]>summary{margin-bottom:6px}
199
+ details.tool-group .row{margin-top:5px}
200
+
201
+ .tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}
202
+ .tnode:hover{background:var(--bg3)}
203
+ .tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}
204
+ .tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
205
+ .dot.running{background:var(--green)}
206
+ .dot.grey{background:var(--text3)}
207
+ .dot.error{background:var(--red)}
208
+ .tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}
209
+ .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}
210
+ .tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}
211
+ #tree-empty{padding:12px;color:var(--text3);font-size:12px}
212
+
213
+ .row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}
214
+ .row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}
215
+ .row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}
216
+ .row.text .kind{color:var(--blue)}
217
+ .row.tool-call .kind{color:var(--yellow)}
218
+ .row.turn-end .kind{color:var(--purple)}
219
+ .row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
220
+ .row.text .body{font-family:inherit}
221
+ .row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}
222
+ .status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}
223
+ .status-badge.pending{background:var(--bg3);color:var(--text2)}
224
+ .status-badge.ok{background:#0f3d20;color:var(--green)}
225
+ .status-badge.error{background:#3d1418;color:var(--red)}
226
+ .row details{margin-top:6px}
227
+ .row summary{cursor:pointer;font-size:11px;color:var(--text2)}
228
+ .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}
229
+ .chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}
230
+ #timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}
231
+ </style>
232
+ </head>
233
+ <body>
234
+ <div id="app">
235
+ <div id="tree-pane">
236
+ <div id="tree-head">Sessions</div>
237
+ <div id="tree-body"><div id="tree-empty">Loading\u2026</div></div>
238
+ </div>
239
+ <div id="timeline-pane">
240
+ <div id="timeline-head">
241
+ <span class="focus-id" id="focus-id-label">\u2014</span>
242
+ <select id="head-selector" title="Sessions"></select>
243
+ <span id="head-summary"></span>
244
+ <span id="status-line">connecting\u2026</span>
245
+ </div>
246
+ <div id="timeline-body"><div id="timeline-empty">No session focused yet.</div></div>
247
+ <button id="new-pill" type="button">\u2193 New messages</button>
248
+ </div>
249
+ </div>
250
+ <script>
251
+ window.__APP_INIT__ = ${JSON.stringify(initData)};
252
+
253
+ ${panelBridgeScript("agentproto-live-session")}
254
+
255
+ // ============================================================
256
+ // INLINED REDUCER COPY \u2014 hand-kept mirror of live-session/logic.ts.
257
+ // Plain JS, same semantics: coalesce consecutive text-delta of the same
258
+ // session (and rejoin an unterminated mid-line fragment split by an
259
+ // interleaved record \u2014 see the TS module's text-delta arm), pair
260
+ // tool-call/tool-result by toolCallId, pass through turn-end, keep usage
261
+ // as STATE (SPEC \xA71: usage leaves the timeline \u2014 a usage_update record
262
+ // never produces a row), ignore unknown kinds. Keep in sync with the TS
263
+ // module; the TS module is the one the test suite imports.
264
+ // ============================================================
265
+
266
+ function initialTimelineState() {
267
+ return { rows: [], usage: null };
268
+ }
269
+
270
+ function rowId(record, rows) {
271
+ return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);
272
+ }
273
+
274
+ // Fold a text-delta record into an existing row (fresh object), keeping the
275
+ // row's "partial" hint in step with the latest record \u2014 see mergeTextDelta in
276
+ // the TS module.
277
+ function mergeTextDelta(row, record) {
278
+ var merged = Object.assign({}, row, {
279
+ text: row.text + (record.text || ''),
280
+ seq: record.seq,
281
+ ts: record.ts,
282
+ });
283
+ if (record.partial === true) merged.partial = true;
284
+ else delete merged.partial;
285
+ return merged;
286
+ }
287
+
288
+ function reduceEvent(state, record) {
289
+ switch (record.kind) {
290
+ case 'text-delta': {
291
+ var last = state.rows[state.rows.length - 1];
292
+ if (last && last.kind === 'text' && last.sessionId === record.sessionId) {
293
+ return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };
294
+ }
295
+ // Debounce can flush an unterminated mid-word fragment (flagged
296
+ // partial), let a tool-call land, then flush the continuation \u2014 look
297
+ // back within the same turn (bounded by this session's last turn-end)
298
+ // for that session's most recent text row and continue it in place.
299
+ // Only the explicit partial flag glues: a non-partial record with no
300
+ // trailing newline is the writer's normal end-of-text-block shape.
301
+ for (var i = state.rows.length - 1; i >= 0; i--) {
302
+ var prior = state.rows[i];
303
+ if (prior.sessionId !== record.sessionId) continue;
304
+ if (prior.kind === 'turn-end') break;
305
+ if (prior.kind !== 'text') continue;
306
+ if (prior.partial === true) {
307
+ var patched = state.rows.slice();
308
+ patched[i] = mergeTextDelta(prior, record);
309
+ return { rows: patched, usage: state.usage };
310
+ }
311
+ break;
312
+ }
313
+ var row = {
314
+ kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
315
+ sessionId: record.sessionId, text: record.text || '',
316
+ };
317
+ if (record.partial === true) row.partial = true;
318
+ return { rows: state.rows.concat([row]), usage: state.usage };
319
+ }
320
+ case 'tool-call': {
321
+ var row = {
322
+ kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
323
+ sessionId: record.sessionId, toolCallId: record.toolCallId || '',
324
+ toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',
325
+ };
326
+ return { rows: state.rows.concat([row]), usage: state.usage };
327
+ }
328
+ case 'tool-result': {
329
+ var idx = -1;
330
+ for (var i = 0; i < state.rows.length; i++) {
331
+ if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;
332
+ }
333
+ if (idx === -1) {
334
+ var row = {
335
+ kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
336
+ sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',
337
+ status: record.isError ? 'error' : 'ok', result: record.result,
338
+ };
339
+ return { rows: state.rows.concat([row]), usage: state.usage };
340
+ }
341
+ var updated = Object.assign({}, state.rows[idx], {
342
+ status: record.isError ? 'error' : 'ok', result: record.result,
343
+ });
344
+ var rows = state.rows.slice();
345
+ rows[idx] = updated;
346
+ return { rows: rows, usage: state.usage };
347
+ }
348
+ case 'turn-end': {
349
+ var row = {
350
+ kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
351
+ sessionId: record.sessionId, reason: record.reason,
352
+ };
353
+ return { rows: state.rows.concat([row]), usage: state.usage };
354
+ }
355
+ case 'usage_update': {
356
+ // Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
357
+ // the prior snapshot. state.rows is reused as-is (it didn't change).
358
+ return {
359
+ rows: state.rows,
360
+ usage: {
361
+ size: record.size, used: record.used, cost: record.cost,
362
+ tokensIn: record.tokensIn, tokensOut: record.tokensOut,
363
+ seq: record.seq, ts: record.ts,
364
+ },
365
+ };
366
+ }
367
+ default:
368
+ return state;
369
+ }
370
+ }
371
+
372
+ // ============================================================
373
+ // INLINED PURE HELPERS \u2014 exact copies of live-session/logic.ts's
374
+ // isNearBottom (SPEC \xA72) and groupAdjacentToolCalls (SPEC \xA73). Same names,
375
+ // same signatures, same default thresholds; plain JS because the widget
376
+ // has no import step.
377
+ // ============================================================
378
+
379
+ var SCROLL_STICK_THRESHOLD_PX = 24;
380
+
381
+ function isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {
382
+ if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;
383
+ return scrollHeight - scrollTop - clientHeight <= threshold;
384
+ }
385
+
386
+ var TOOL_CALL_GROUP_THRESHOLD = 2;
387
+
388
+ // Collapse runs of \`threshold\`+ adjacent tool-call rows into one
389
+ // {kind:'tool-group', rows:[...]} entry; everything else passes through as
390
+ // individual {kind:'row', row} entries, same order as the input.
391
+ function groupAdjacentToolCalls(rows, threshold) {
392
+ if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;
393
+ var out = [];
394
+ var run = [];
395
+ function flushRun() {
396
+ if (!run.length) return;
397
+ if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });
398
+ else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });
399
+ run = [];
400
+ }
401
+ for (var i = 0; i < rows.length; i++) {
402
+ if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }
403
+ flushRun();
404
+ out.push({ kind: 'row', row: rows[i] });
405
+ }
406
+ flushRun();
407
+ return out;
408
+ }
409
+
410
+ // ============================================================
411
+ // Rendering helpers
412
+ // ============================================================
413
+
414
+ function escHtml(s) {
415
+ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
416
+ }
417
+
418
+ function safeJson(v) {
419
+ try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
420
+ }
421
+
422
+ function statusDotClass(status) {
423
+ if (status === 'running' || status === 'starting') return 'running';
424
+ if (status === 'error' || status === 'killed') return 'error';
425
+ return 'grey';
426
+ }
427
+
428
+ // SPEC \xA74 (non-contract guidance): 52524 \u2192 "52.5k", >=1e6 \u2192 "M" suffix,
429
+ // one decimal, trailing .0 vanishes because the value stays a number.
430
+ // cost \u2192 "$" + toFixed(2). Read from timelineState.usage.
431
+ function fmtCompactNum(n) {
432
+ if (typeof n !== 'number' || !isFinite(n)) return null;
433
+ if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';
434
+ if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';
435
+ return String(n);
436
+ }
437
+
438
+ function usageChipText(usage) {
439
+ if (!usage) return '';
440
+ var bits = [];
441
+ if (usage.used != null && usage.size != null) {
442
+ bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));
443
+ } else if (usage.used != null) {
444
+ bits.push(fmtCompactNum(usage.used));
445
+ }
446
+ if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));
447
+ return bits.join(' \xB7 ');
448
+ }
449
+
450
+ // WP5: elapsed seconds since the timeline's first row (running only).
451
+ function elapsedText(rows, status) {
452
+ if (status !== 'running' && status !== 'starting') return null;
453
+ if (!rows.length || rows[0].ts == null) return null;
454
+ var ms = new Date(rows[0].ts).getTime();
455
+ if (!isFinite(ms)) return null;
456
+ return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';
457
+ }
458
+
459
+ // ============================================================
460
+ // LEFT pane \u2014 live tree
461
+ // ============================================================
462
+
463
+ var currentTree = [];
464
+ var focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;
465
+ // Where the current focus came from: 'pinned' (the tool result that mounted
466
+ // this widget named a session \u2014 never auto-switched away), 'auto' (self-
467
+ // discovered newest running session, a guess the host may still correct),
468
+ // 'user' (explicit click/selector choice \u2014 outranks everything).
469
+ var focusSource = focusId ? 'pinned' : null;
470
+ var bootDone = false;
471
+ var treeTimer = null;
472
+
473
+ // INLINED COPY of live-session/logic.ts extractToolResultSessionId \u2014
474
+ // keep in sync (same convention as the reducer copy below).
475
+ function extractToolResultSessionId(params) {
476
+ if (!params || typeof params !== 'object') return null;
477
+ var res = (params.result && typeof params.result === 'object') ? params.result : params;
478
+ if (res.isError) return null;
479
+ var content = Array.isArray(res.content) ? res.content : [];
480
+ var item = content[0];
481
+ if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;
482
+ try {
483
+ var body = JSON.parse(item.text);
484
+ if (body && typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;
485
+ if (body && typeof body.id === 'string' && body.id) return body.id;
486
+ } catch (_) {}
487
+ return null;
488
+ }
489
+
490
+ // The host pushes the triggering tool call's result (ext-apps
491
+ // ui/notifications/tool-result). For agent_start that result IS the spawned
492
+ // session's descriptor \u2014 pin the widget to THAT session instead of leaving
493
+ // pollTree() to self-discover the newest running one, which made every
494
+ // agent_start card show the latest started session rather than its own.
495
+ onHostNotification(function(method, params) {
496
+ if (method !== 'ui/notifications/tool-result') return;
497
+ var id = extractToolResultSessionId(params);
498
+ if (!id || focusSource === 'user' || id === focusId) return;
499
+ focusSource = 'pinned';
500
+ if (bootDone) setFocus(id);
501
+ else focusId = id; // boot's own startTimeline(focusId) picks it up
502
+ });
503
+
504
+ function findNode(nodes, id) {
505
+ for (var i = 0; i < nodes.length; i++) {
506
+ if (nodes[i].id === id) return nodes[i];
507
+ var found = findNode(nodes[i].children || [], id);
508
+ if (found) return found;
509
+ }
510
+ return null;
511
+ }
512
+
513
+ function flattenDfs(nodes, out) {
514
+ out = out || [];
515
+ for (var i = 0; i < nodes.length; i++) {
516
+ out.push(nodes[i]);
517
+ flattenDfs(nodes[i].children || [], out);
518
+ }
519
+ return out;
520
+ }
521
+
522
+ function pickInitialFocus(tree) {
523
+ var flat = flattenDfs(tree);
524
+ var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });
525
+ if (alive.length) return alive[alive.length - 1].id;
526
+ return tree.length ? tree[0].id : null;
527
+ }
528
+
529
+ function renderTreeNode(node, depth) {
530
+ var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');
531
+ var badge = node.isOrchestrator ? '<span class="badge">orch</span>' : '';
532
+ var cls = 'tnode' + (node.id === focusId ? ' focus' : '');
533
+ return '<div class="' + cls + '" data-id="' + escHtml(node.id) + '">' +
534
+ '<span class="dot ' + statusDotClass(node.status) + '"></span>' +
535
+ '<span class="label">' + escHtml(node.label || node.id) + '</span>' + badge +
536
+ '</div>' +
537
+ (childrenHtml ? '<div class="tchildren">' + childrenHtml + '</div>' : '');
538
+ }
539
+
540
+ // WP4: compact header <select> mirrors the tree; still routes through
541
+ // setFocus() \u2014 the only session-switch entry point.
542
+ function renderHeadSelector() {
543
+ var sel = document.getElementById('head-selector');
544
+ var flat = flattenDfs(currentTree);
545
+ while (sel.firstChild) sel.removeChild(sel.firstChild);
546
+ if (!flat.length) return;
547
+ for (var i = 0; i < flat.length; i++) {
548
+ var n = flat[i];
549
+ var o = document.createElement('option');
550
+ o.value = n.id;
551
+ o.textContent = (n.id === focusId ? '\u25CF ' : '') + (n.label || n.id);
552
+ sel.appendChild(o);
553
+ }
554
+ sel.value = focusId || '';
555
+ }
556
+
557
+ function renderTree() {
558
+ var body = document.getElementById('tree-body');
559
+ var root = focusId ? findNode(currentTree, focusId) : null;
560
+ var renderNodes = root ? [root] : currentTree;
561
+ if (!renderNodes.length) {
562
+ body.innerHTML = '<div id="tree-empty">No sessions.</div>';
563
+ renderHeadSelector();
564
+ return;
565
+ }
566
+ body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');
567
+ var els = body.querySelectorAll('.tnode');
568
+ els.forEach(function(el) {
569
+ el.addEventListener('click', function() {
570
+ var id = el.getAttribute('data-id');
571
+ if (id === focusId) return;
572
+ focusSource = 'user';
573
+ setFocus(id);
574
+ });
575
+ });
576
+ renderHeadSelector();
577
+ }
578
+
579
+ function pollTree() {
580
+ callTool('app_session_tree', {}).then(function(res) {
581
+ currentTree = res.tree || [];
582
+ if (!focusId) {
583
+ focusId = pickInitialFocus(currentTree);
584
+ if (focusId) { focusSource = 'auto'; startTimeline(focusId); }
585
+ }
586
+ renderTree();
587
+ updateHeader();
588
+ }).catch(function() {
589
+ // Leave the last-known tree rendered; the next poll may recover.
590
+ });
591
+ }
592
+
593
+ // ============================================================
594
+ // RIGHT pane \u2014 timeline for focusId
595
+ // ============================================================
596
+
597
+ var timelineState = initialTimelineState();
598
+ var sincePtr = 0;
599
+ var activeSource = null; // {type:'sse', es} | {type:'poll', timer}
600
+ var compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)
601
+
602
+ function setStatus(msg) {
603
+ document.getElementById('status-line').textContent = msg;
604
+ }
605
+
606
+ function isCompact() {
607
+ return (getHostContext() && getHostContext().displayMode === 'inline') ||
608
+ (typeof window !== 'undefined' && window.innerWidth < 640);
609
+ }
610
+
611
+ // WP3/WP4: apply the compact/expanded split. Only re-renders the timeline
612
+ // when the mode actually flips (scroll + <details> state survive otherwise).
613
+ function applyDisplayMode() {
614
+ var c = isCompact();
615
+ document.body.classList.toggle('compact-mode', c);
616
+ if (c !== compactMode) {
617
+ compactMode = c;
618
+ renderTimelineFull();
619
+ renderTree();
620
+ }
621
+ }
622
+
623
+ // WP5 + WP2: one header line \u2014 \`\u25CF running \xB7 3 tools \xB7 52.5k/200k \xB7 $0.04 \xB7 12s\`
624
+ // (status dot from the focus node, tool count from the rows, usage from
625
+ // timelineState.usage \u2014 no usage_update row anymore) + transport status.
626
+ function updateHeader() {
627
+ document.getElementById('focus-id-label').textContent = focusId || '\u2014';
628
+ var rows = timelineState.rows || [];
629
+ var usage = timelineState.usage;
630
+ var tools = 0;
631
+ for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;
632
+ var node = focusId ? findNode(currentTree, focusId) : null;
633
+ var st = node ? node.status : null;
634
+ var parts = [];
635
+ if (st) parts.push('<span class="sdot ' + statusDotClass(st) + '"></span>' + escHtml(st));
636
+ if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));
637
+ var chip = usageChipText(usage);
638
+ if (chip) parts.push('<span id="usage-chip" class="chip">' + escHtml(chip) + '</span>');
639
+ var el = elapsedText(rows, st);
640
+ if (el) parts.push(escHtml(el));
641
+ document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' \xB7 ') : '';
642
+ }
643
+
644
+ function captureDetailsOpenFlags(body) {
645
+ var dets = body.querySelectorAll('details');
646
+ var flags = [];
647
+ for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);
648
+ return flags;
649
+ }
650
+
651
+ function restoreDetailsOpenFlags(body, flags) {
652
+ var dets = body.querySelectorAll('details');
653
+ for (var i = 0; i < dets.length && i < flags.length; i++) {
654
+ if (flags[i]) dets[i].open = true;
655
+ }
656
+ }
657
+
658
+ // Full rebuild \u2014 used ONLY for the first paint of a session, a setFocus()
659
+ // session switch (SPEC \xA72: full reset is correct there), a display-mode
660
+ // flip, and as the fallback for shapes the incremental patcher can't patch.
661
+ // A display-mode flip re-renders the SAME content (WP3: "r\xE9actif... sans
662
+ // perdre le scroll ni l'\xE9tat des <details>"), so open <details> and the
663
+ // exact scroll offset (not just the at-bottom decision) survive the rebuild.
664
+ function renderTimelineFull() {
665
+ var body = document.getElementById('timeline-body');
666
+ var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
667
+ var savedScrollTop = body.scrollTop;
668
+ var flags = captureDetailsOpenFlags(body);
669
+ body.innerHTML = buildTimelineHtml();
670
+ restoreDetailsOpenFlags(body, flags);
671
+ body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;
672
+ updateHeader();
673
+ }
674
+
675
+ function buildTimelineHtml() {
676
+ var rows = timelineState.rows;
677
+ if (!rows.length) return '<div id="timeline-empty">No events yet.</div>';
678
+ if (compactMode) {
679
+ var entries = groupAdjacentToolCalls(rows);
680
+ var html = '';
681
+ for (var i = 0; i < entries.length; i++) {
682
+ var e = entries[i];
683
+ if (e.kind === 'tool-group') {
684
+ html += '<details class="tool-group"><summary>\u25B8 ' + e.rows.length + ' tool calls</summary>' +
685
+ e.rows.map(renderRow).join('') + '</details>';
686
+ } else {
687
+ html += renderRow(e.row);
688
+ }
689
+ }
690
+ return html;
691
+ }
692
+ return rows.map(renderRow).join('');
693
+ }
694
+
695
+ // \u2500\u2500 WP1 incremental patch (dense mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
696
+ // One record, one minimal DOM mutation. The common case (text-delta merged
697
+ // into the LAST rendered row) patches just that row's text node. Anything
698
+ // else appends via insertAdjacentHTML. Only rare in-place replacements
699
+ // (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.
700
+ function applyRecordToDom(prevRows) {
701
+ var body = document.getElementById('timeline-body');
702
+ // Capture "stuck to bottom?" BEFORE any mutation (SPEC \xA72).
703
+ var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
704
+ var rows = timelineState.rows;
705
+
706
+ if (compactMode) {
707
+ // Inline mode: smaller rows \u2014 full grouped rebuild, preserving
708
+ // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM
709
+ // (and the scroll/pill decision) when the rows actually changed \u2014 a
710
+ // usage_update leaves rows === prevRows, so it must fall through to the
711
+ // header-only path below, same as the dense-mode branch.
712
+ if (rows !== prevRows) {
713
+ var flags = captureDetailsOpenFlags(body);
714
+ body.innerHTML = buildTimelineHtml();
715
+ restoreDetailsOpenFlags(body, flags);
716
+ if (wasAtBottom) body.scrollTop = body.scrollHeight;
717
+ else showNewPill();
718
+ }
719
+ updateHeader();
720
+ return;
721
+ }
722
+
723
+ var appended = rows.length > prevRows.length;
724
+ if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {
725
+ // First real paint of this session (empty placeholder present) or an
726
+ // unexpected shape \u2014 full reset is correct/cheapest.
727
+ body.innerHTML = buildTimelineHtml();
728
+ body.scrollTop = body.scrollHeight;
729
+ updateHeader();
730
+ return;
731
+ }
732
+
733
+ if (appended) {
734
+ body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));
735
+ } else if (rows.length === prevRows.length && rows.length > 0) {
736
+ // Exactly one row object replaced (tool-result merge, partial-text
737
+ // re-merge via the reducer's lookback). Patch that one element only.
738
+ // Scan from the tail: the hot path (a text-delta merged into the LAST
739
+ // row, the dominant streaming case) is found in O(1) this way instead
740
+ // of walking the whole array \u2014 the rare mid-array lookback re-merge
741
+ // still resolves correctly, just slower.
742
+ var idx = -1;
743
+ for (var i = rows.length - 1; i >= 0; i--) {
744
+ if (rows[i] !== prevRows[i]) { idx = i; break; }
745
+ }
746
+ var patched = false;
747
+ if (idx >= 0) {
748
+ var row = rows[idx];
749
+ var el = body.querySelector('[data-row-id="' + escHtml(row.id) + '"]');
750
+ if (el) {
751
+ if (row.kind === 'text' && idx === rows.length - 1) {
752
+ // High-frequency case: patch the existing text node in place.
753
+ var b = el.querySelector('.body');
754
+ if (b) { b.textContent = row.text; patched = true; }
755
+ }
756
+ if (!patched) { el.outerHTML = renderRow(row); patched = true; }
757
+ }
758
+ }
759
+ if (!patched) {
760
+ // Multiple rows changed at once (shouldn't happen per-record) \u2014 rebuild.
761
+ body.innerHTML = buildTimelineHtml();
762
+ if (wasAtBottom) body.scrollTop = body.scrollHeight;
763
+ updateHeader();
764
+ return;
765
+ }
766
+ } else {
767
+ // rows unchanged (usage_update) \u2014 header only.
768
+ updateHeader();
769
+ return;
770
+ }
771
+
772
+ // Scroll decision AFTER the mutation: stick or leave the position alone.
773
+ if (wasAtBottom) body.scrollTop = body.scrollHeight;
774
+ else showNewPill();
775
+ updateHeader();
776
+ }
777
+
778
+ function samePrefix(prevRows, rows) {
779
+ for (var i = 0; i < prevRows.length; i++) {
780
+ if (prevRows[i] !== rows[i]) return false;
781
+ }
782
+ return true;
783
+ }
784
+
785
+ function showNewPill() {
786
+ document.getElementById('new-pill').classList.add('show');
787
+ }
788
+
789
+ function hideNewPill() {
790
+ document.getElementById('new-pill').classList.remove('show');
791
+ }
792
+
793
+ function renderRow(row) {
794
+ if (row.kind === 'text') {
795
+ return '<div class="row text" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">text</span></div>' +
796
+ '<div class="body">' + escHtml(row.text) + '</div></div>';
797
+ }
798
+ if (row.kind === 'tool-call') {
799
+ var badgeCls = row.status === 'pending' ? 'pending' : row.status;
800
+ var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');
801
+ var resultHtml = row.status !== 'pending'
802
+ ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'
803
+ : '';
804
+ return '<div class="row tool-call" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">tool</span>' +
805
+ '<span class="toolname">' + escHtml(row.toolName) + '</span>' +
806
+ '<span class="status-badge ' + badgeCls + '">' + badgeText + '</span></div>' +
807
+ '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +
808
+ resultHtml + '</div>';
809
+ }
810
+ if (row.kind === 'turn-end') {
811
+ return '<div class="row turn-end" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">turn end</span>' +
812
+ '<span class="chip">' + escHtml(row.reason || '\u2014') + '</span></div></div>';
813
+ }
814
+ return '';
815
+ }
816
+
817
+ function teardownTimeline() {
818
+ if (activeSource) {
819
+ if (activeSource.type === 'sse' && activeSource.es) {
820
+ try { activeSource.es.close(); } catch (e) {}
821
+ }
822
+ // Both the poll re-arm timer AND the SSE open/first-message fallback
823
+ // timer must be cleared, or a focus switch within the 2.5s window leaves
824
+ // a stale timer that fires startPolling for the abandoned session.
825
+ if (activeSource.timer) clearTimeout(activeSource.timer);
826
+ if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);
827
+ }
828
+ activeSource = null;
829
+ }
830
+
831
+ function attemptSSE(id) {
832
+ var settled = false;
833
+ var es;
834
+ try {
835
+ es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');
836
+ } catch (e) {
837
+ startPolling(id);
838
+ return;
839
+ }
840
+ var src = { type: 'sse', es: es, fallbackTimer: null };
841
+ activeSource = src;
842
+ // Every callback below re-checks focusId===id AND that src is still the
843
+ // live source: a focus switch tears down src and starts a new one, so a
844
+ // late open/message/error from this abandoned EventSource must be a no-op
845
+ // rather than mutating the new focus's timeline or clobbering activeSource.
846
+ function stale() { return activeSource !== src || focusId !== id; }
847
+ src.fallbackTimer = setTimeout(function() {
848
+ if (settled || stale()) return;
849
+ settled = true;
850
+ try { es.close(); } catch (e) {}
851
+ startPolling(id);
852
+ }, 2500);
853
+ es.addEventListener('open', function() {
854
+ if (settled || stale()) return;
855
+ settled = true;
856
+ clearTimeout(src.fallbackTimer);
857
+ setStatus('streaming via SSE');
858
+ });
859
+ es.onmessage = function(evt) {
860
+ if (stale()) return;
861
+ if (!settled) {
862
+ settled = true;
863
+ clearTimeout(src.fallbackTimer);
864
+ setStatus('streaming via SSE');
865
+ }
866
+ var rec;
867
+ try { rec = JSON.parse(evt.data); } catch (e) { return; }
868
+ var prev = timelineState;
869
+ timelineState = reduceEvent(timelineState, rec);
870
+ if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;
871
+ if (timelineState !== prev) applyRecordToDom(prev.rows);
872
+ };
873
+ es.onerror = function() {
874
+ if (stale()) return;
875
+ if (!settled) {
876
+ settled = true;
877
+ clearTimeout(src.fallbackTimer);
878
+ try { es.close(); } catch (e) {}
879
+ startPolling(id);
880
+ } else {
881
+ // Do not leave EventSource to perform its native reconnect: this
882
+ // endpoint replays from since=0 when no cursor is provided, so a
883
+ // transparent reconnect would duplicate every row already reduced.
884
+ // The bridge poll resumes from sincePtr instead and preserves the
885
+ // exactly-once cursor contract.
886
+ try { es.close(); } catch (e) {}
887
+ startPolling(id);
888
+ }
889
+ };
890
+ }
891
+
892
+ function startPolling(id) {
893
+ activeSource = { type: 'poll', timer: null };
894
+ setStatus('polling');
895
+ function tick() {
896
+ if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;
897
+ callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {
898
+ var events = res.events || [];
899
+ for (var i = 0; i < events.length; i++) {
900
+ var prev = timelineState;
901
+ timelineState = reduceEvent(timelineState, events[i]);
902
+ if (timelineState !== prev) applyRecordToDom(prev.rows);
903
+ }
904
+ if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;
905
+ setStatus('polling');
906
+ if (activeSource) activeSource.timer = setTimeout(tick, 1500);
907
+ }).catch(function() {
908
+ setStatus('disconnected');
909
+ if (activeSource) activeSource.timer = setTimeout(tick, 1500);
910
+ });
911
+ }
912
+ tick();
913
+ }
914
+
915
+ function startTimeline(id) {
916
+ teardownTimeline();
917
+ timelineState = initialTimelineState();
918
+ sincePtr = 0;
919
+ hideNewPill();
920
+ renderTimelineFull();
921
+ setStatus('connecting\u2026');
922
+ attemptSSE(id);
923
+ }
924
+
925
+ function setFocus(id) {
926
+ focusId = id;
927
+ renderTree();
928
+ startTimeline(id);
929
+ }
930
+
931
+ // ============================================================
932
+ // Boot
933
+ // ============================================================
934
+
935
+ initBridge().then(function() {
936
+ var init = window.__APP_INIT__ || {};
937
+ if (init.httpBaseUrl) return init;
938
+ // Static ui:// resources render once at server-registration time with
939
+ // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) \u2014 fall back to
940
+ // calling the tool ourselves over the bridge, same as the other panels.
941
+ return callTool('live_session', {}).then(function(result) {
942
+ window.__APP_INIT__ = result;
943
+ return result;
944
+ });
945
+ }).then(function(init) {
946
+ // Only override the focus when live_session itself named a session AND a
947
+ // tool-result notification didn't already pin one while the fallback
948
+ // tools/call above was in flight \u2014 the host's push names the exact
949
+ // session this widget was mounted for, so it outranks the fallback's
950
+ // (argument-less, hence session-less in practice) self-call.
951
+ if (init.sessionId && focusSource !== 'pinned') {
952
+ focusId = init.sessionId;
953
+ focusSource = 'pinned';
954
+ }
955
+ onHostContext(function() {
956
+ applyDisplayMode();
957
+ updateHeader();
958
+ });
959
+ document.getElementById('head-selector').addEventListener('change', function(evt) {
960
+ var id = evt.target.value;
961
+ if (id && id !== focusId) { focusSource = 'user'; setFocus(id); }
962
+ });
963
+ document.getElementById('new-pill').addEventListener('click', function() {
964
+ var body = document.getElementById('timeline-body');
965
+ body.scrollTop = body.scrollHeight;
966
+ hideNewPill();
967
+ });
968
+ document.getElementById('timeline-body').addEventListener('scroll', function() {
969
+ // User scrolled back near the bottom \u2014 the pill is stale, hide it.
970
+ var el = document.getElementById('timeline-body');
971
+ if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();
972
+ });
973
+ window.addEventListener('resize', function() {
974
+ var c = isCompact();
975
+ if (c !== compactMode) { applyDisplayMode(); }
976
+ });
977
+ applyDisplayMode();
978
+ bootDone = true;
979
+ pollTree();
980
+ treeTimer = setInterval(pollTree, 2000);
981
+ setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh
982
+ if (focusId) startTimeline(focusId);
983
+ }).catch(function(e) {
984
+ setStatus('Bridge error: ' + e.message);
985
+ });
986
+ </script>
987
+ </body>
988
+ </html>`;
989
+ }
990
+
991
+ export { LIVE_SESSION_HTML };
992
+ //# sourceMappingURL=panel.mjs.map
993
+ //# sourceMappingURL=panel.mjs.map