@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.
- package/README.md +28 -1
- package/dist/agents-overview/index.d.ts +54 -0
- package/dist/agents-overview/index.d.ts.map +1 -0
- package/dist/agents-overview/panel.d.ts +14 -0
- package/dist/agents-overview/panel.d.ts.map +1 -0
- package/dist/agents-overview/panel.mjs +306 -0
- package/dist/agents-overview/panel.mjs.map +1 -0
- package/dist/agents-overview.mjs +336 -0
- package/dist/agents-overview.mjs.map +1 -0
- package/dist/bin/sync.mjs +25 -11
- package/dist/bin/sync.mjs.map +1 -1
- package/dist/bureau-sessions/index.d.ts +52 -0
- package/dist/bureau-sessions/index.d.ts.map +1 -0
- package/dist/bureau-sessions/panel.d.ts +10 -0
- package/dist/bureau-sessions/panel.d.ts.map +1 -0
- package/dist/bureau-sessions/panel.mjs +297 -0
- package/dist/bureau-sessions/panel.mjs.map +1 -0
- package/dist/bureau-sessions.mjs +329 -0
- package/dist/bureau-sessions.mjs.map +1 -0
- package/dist/content-team/workflows/produce-content.d.ts +14 -2
- package/dist/content-team/workflows/produce-content.d.ts.map +1 -1
- package/dist/content-team/workflows/produce-cover.d.ts +13 -1
- package/dist/content-team/workflows/produce-cover.d.ts.map +1 -1
- package/dist/content-team.mjs +25 -11
- package/dist/content-team.mjs.map +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2635 -12
- package/dist/index.mjs.map +1 -1
- package/dist/live-session/index.d.ts +46 -0
- package/dist/live-session/index.d.ts.map +1 -0
- package/dist/live-session/logic.d.ts +125 -0
- package/dist/live-session/logic.d.ts.map +1 -0
- package/dist/live-session/panel.d.ts +61 -0
- package/dist/live-session/panel.d.ts.map +1 -0
- package/dist/live-session/panel.mjs +993 -0
- package/dist/live-session/panel.mjs.map +1 -0
- package/dist/live-session.mjs +1032 -0
- package/dist/live-session.mjs.map +1 -0
- package/dist/mcp-app-types.d.ts +33 -0
- package/dist/mcp-app-types.d.ts.map +1 -0
- package/dist/panel-bridge.d.ts +38 -0
- package/dist/panel-bridge.d.ts.map +1 -0
- package/dist/session-story/index.d.ts +66 -0
- package/dist/session-story/index.d.ts.map +1 -0
- package/dist/session-story/panel.d.ts +31 -0
- package/dist/session-story/panel.d.ts.map +1 -0
- package/dist/session-story/panel.mjs +970 -0
- package/dist/session-story/panel.mjs.map +1 -0
- package/dist/session-story.mjs +1002 -0
- package/dist/session-story.mjs.map +1 -0
- package/dist/sessions-panel/index.d.ts +51 -0
- package/dist/sessions-panel/index.d.ts.map +1 -0
- package/dist/sessions-panel/panel.d.ts +20 -0
- package/dist/sessions-panel/panel.d.ts.map +1 -0
- package/dist/sessions-panel/panel.mjs +511 -0
- package/dist/sessions-panel/panel.mjs.map +1 -0
- package/dist/sessions-panel.mjs +545 -0
- package/dist/sessions-panel.mjs.map +1 -0
- 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
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
|