@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,545 @@
1
+ import { z } from 'zod';
2
+ import { defineApp } from '@agentproto/app-kit';
3
+
4
+ /**
5
+ * @agentproto/apps v0.1.0
6
+ * Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
7
+ */
8
+
9
+
10
+ // src/panel-bridge.ts
11
+ function panelBridgeScript(appName) {
12
+ return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
13
+ // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
14
+ var _nextId = 1, _pending = {}, _notifyHandlers = [];
15
+ var _hostContext = null, _hostContextHandlers = [];
16
+ function post(msg){ window.parent.postMessage(msg, '*'); }
17
+ function getHostContext(){ return _hostContext; }
18
+ function onHostContext(cb){
19
+ _hostContextHandlers.push(cb);
20
+ // Replay the last context so a late subscriber isn't stuck blind.
21
+ if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
22
+ }
23
+ function _setHostContext(ctx){
24
+ if (!ctx || typeof ctx !== 'object') return;
25
+ // ui/notifications/host-context-changed carries only the changed keys \u2014
26
+ // merge, matching the official ext-apps App behaviour.
27
+ _hostContext = Object.assign({}, _hostContext || {}, ctx);
28
+ for (var i = 0; i < _hostContextHandlers.length; i++){
29
+ try { _hostContextHandlers[i](_hostContext); } catch(_) {}
30
+ }
31
+ }
32
+ function rpcRequest(method, params){
33
+ return new Promise(function(resolve, reject){
34
+ var id = _nextId++;
35
+ _pending[id] = {resolve: resolve, reject: reject};
36
+ post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
37
+ });
38
+ }
39
+ function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
40
+ function onHostNotification(cb){ _notifyHandlers.push(cb); }
41
+ window.addEventListener('message', function(evt){
42
+ var msg = evt.data;
43
+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
44
+ if (msg.id != null && msg.method == null){
45
+ var p = _pending[msg.id];
46
+ if (!p) return;
47
+ delete _pending[msg.id];
48
+ if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
49
+ else p.resolve(msg.result);
50
+ return;
51
+ }
52
+ if (msg.method){
53
+ if (msg.method === 'ui/notifications/host-context-changed'){
54
+ _setHostContext(msg.params || {});
55
+ }
56
+ for (var i = 0; i < _notifyHandlers.length; i++){
57
+ try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
58
+ }
59
+ }
60
+ });
61
+ function initBridge(){
62
+ return rpcRequest('ui/initialize', {
63
+ appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
64
+ appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
65
+ protocolVersion: '2026-01-26'
66
+ }).then(function(result){
67
+ // The initialize result carries the initial hostContext (displayMode +
68
+ // availableDisplayModes) \u2014 capture it before notifying the host.
69
+ if (result && result.hostContext) _setHostContext(result.hostContext);
70
+ rpcNotify('ui/notifications/initialized', {});
71
+ });
72
+ }
73
+ function requestDisplayMode(mode){
74
+ return rpcRequest('ui/request-display-mode', {mode: mode});
75
+ }
76
+ function callTool(name, args){
77
+ return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
78
+ if (result.isError){
79
+ var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
80
+ throw new Error(e);
81
+ }
82
+ var text = (result.content && result.content[0] && result.content[0].text) || '{}';
83
+ return JSON.parse(text);
84
+ });
85
+ }
86
+
87
+ // \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
88
+ // Injected by the shared bridge so every panel gets them without touching
89
+ // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
90
+ // panel stays inline by default; the user expands on demand. Buttons only
91
+ // appear for modes the host advertises in hostContext.availableDisplayModes.
92
+ (function(){
93
+ function mount(){
94
+ var style = document.createElement('style');
95
+ style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
96
+ + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
97
+ + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
98
+ + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
99
+ + '#dm{right:8px}#pin{right:118px}'
100
+ + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
101
+ + '@media (prefers-color-scheme:dark){'
102
+ + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
103
+ + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
104
+ document.head.appendChild(style);
105
+
106
+ var btn = document.createElement('button');
107
+ btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
108
+ var pin = document.createElement('button');
109
+ pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
110
+ document.body.appendChild(pin);
111
+ document.body.appendChild(btn);
112
+
113
+ function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
114
+
115
+ // Re-sync button visibility + label from the current host context.
116
+ function syncBtn(ctx){
117
+ ctx = ctx || {};
118
+ var avail = ctx.availableDisplayModes;
119
+ // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
120
+ console.log('[mcp-app] displayMode=', ctx.displayMode,
121
+ 'availableDisplayModes=', avail);
122
+
123
+ // Fullscreen toggle button.
124
+ if (has(avail, 'fullscreen')){
125
+ btn.style.display = 'block';
126
+ btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
127
+ } else { btn.style.display = 'none'; }
128
+
129
+ // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
130
+ if (has(avail, 'pip')){
131
+ pin.style.display = 'block';
132
+ pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
133
+ } else { pin.style.display = 'none'; }
134
+ }
135
+
136
+ onHostContext(syncBtn);
137
+
138
+ btn.addEventListener('click', function(){
139
+ var ctx = getHostContext() || {};
140
+ var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
141
+ requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
142
+ });
143
+
144
+ pin.addEventListener('click', function(){
145
+ var ctx = getHostContext() || {};
146
+ requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
147
+ });
148
+ }
149
+ if (document.body) mount();
150
+ else document.addEventListener('DOMContentLoaded', mount);
151
+ })();`;
152
+ }
153
+
154
+ // src/sessions-panel/panel.ts
155
+ var PANEL_HTML = `<!DOCTYPE html>
156
+ <html lang="en">
157
+ <head>
158
+ <meta charset="UTF-8">
159
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
+ <title>agentproto sessions</title>
161
+ <style>
162
+ *{box-sizing:border-box;margin:0;padding:0}
163
+ :root{
164
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
165
+ --text:#e6edf3;--text2:#8b949e;
166
+ --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
167
+ }
168
+ html,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
169
+ #app{display:flex;height:100%}
170
+ #sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
171
+ #sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
172
+ #sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}
173
+ #refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:15px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}
174
+ #refresh-btn:hover{color:var(--text);background:var(--bg3)}
175
+ #session-list{flex:1;overflow-y:auto;padding:4px 0}
176
+ .si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
177
+ .si:hover{background:var(--bg3)}
178
+ .si.active{background:var(--bg3);border-left-color:var(--blue)}
179
+ .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
180
+ .sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}
181
+ .badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}
182
+ .br{background:rgba(63,185,80,.15);color:var(--green)}
183
+ .bs{background:rgba(88,166,255,.15);color:var(--blue)}
184
+ .be{background:rgba(139,148,158,.1);color:var(--text2)}
185
+ .bk{background:rgba(248,81,73,.1);color:var(--red)}
186
+ .berr{background:rgba(248,81,73,.2);color:var(--red)}
187
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
188
+ #toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}
189
+ #session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
190
+ .abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
191
+ .abtn:hover{background:var(--border)}
192
+ .abtn:disabled{opacity:.4;cursor:default}
193
+ .abtn.danger{border-color:var(--red);color:var(--red)}
194
+ .abtn.danger:hover{background:rgba(248,81,73,.1)}
195
+ #output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}
196
+ .line{white-space:pre-wrap;word-break:break-all}
197
+ #empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}
198
+ #empty .ico{font-size:32px}
199
+ #statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
200
+ .errmsg{color:var(--red);padding:12px}
201
+ /* ANSI SGR */
202
+ .ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}
203
+ .f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}
204
+ .f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}
205
+ .f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}
206
+ .f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}
207
+ </style>
208
+ </head>
209
+ <body>
210
+ <div id="app">
211
+ <div id="sidebar">
212
+ <div id="sidebar-hdr">
213
+ <h1>Sessions</h1>
214
+ <button id="refresh-btn" title="Refresh" onclick="doRefresh()">&#8635;</button>
215
+ </div>
216
+ <div id="session-list"></div>
217
+ </div>
218
+ <div id="main">
219
+ <div id="toolbar" style="display:none">
220
+ <span id="session-title"></span>
221
+ <button class="abtn danger" id="kill-btn" onclick="doKill()">Kill</button>
222
+ </div>
223
+ <div id="output">
224
+ <div id="empty"><div class="ico">&#9889;</div><div>Select a session</div></div>
225
+ </div>
226
+ <div id="statusbar">Connecting to bridge&#8230;</div>
227
+ </div>
228
+ </div>
229
+ <script>
230
+ ${panelBridgeScript("agentproto-sessions-panel")}
231
+
232
+ // ============================================================
233
+ // ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)
234
+ // ============================================================
235
+
236
+ function escHtml(s) {
237
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
238
+ }
239
+
240
+ function ansiToHtml(raw) {
241
+ // Strip CR overwrite sequences (progress bars: CR without LF)
242
+ var text = raw.replace(/[^\\n]*\\r([^\\n])/g, '$1').replace(/\\r/g, '');
243
+ var bold = false, dim = false, italic = false, underline = false, fg = -1;
244
+ var out = '';
245
+ // Split on ESC [ ... m (SGR) sequences; keep delimiters
246
+ var ESC = '\x1B';
247
+ var parts = text.split(/([\x1B]\\[[0-9;]*m)/);
248
+ for (var pi = 0; pi < parts.length; pi++) {
249
+ var part = parts[pi];
250
+ if (part.length === 0) continue;
251
+ if (part.charCodeAt(0) === 0x1b && part[1] === '[') {
252
+ // Parse SGR parameters
253
+ var inner = part.slice(2, part.length - 1); // strip ESC[ and m
254
+ var codes = inner === '' ? [0] : inner.split(';').map(Number);
255
+ for (var ci = 0; ci < codes.length; ci++) {
256
+ var c = codes[ci];
257
+ if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }
258
+ else if (c === 1) bold = true;
259
+ else if (c === 2) dim = true;
260
+ else if (c === 3) italic = true;
261
+ else if (c === 4) underline = true;
262
+ else if (c === 22) { bold = false; dim = false; }
263
+ else if (c === 23) italic = false;
264
+ else if (c === 24) underline = false;
265
+ else if (c >= 30 && c <= 37) fg = c - 30;
266
+ else if (c === 39) fg = -1;
267
+ else if (c >= 90 && c <= 97) fg = c - 90 + 8;
268
+ }
269
+ } else {
270
+ // Text segment \u2014 strip any remaining non-printable ESC sequences
271
+ var safe = escHtml(part).replace(/\x1B\\[[^m]*[A-Za-z]/g, '');
272
+ if (safe === '') continue;
273
+ var cls = '';
274
+ if (bold) cls += ' ab';
275
+ if (dim) cls += ' ad';
276
+ if (italic) cls += ' ai';
277
+ if (underline) cls += ' au';
278
+ if (fg >= 0 && fg <= 15) cls += ' f' + fg;
279
+ cls = cls.trim();
280
+ out += cls ? '<span class="' + cls + '">' + safe + '</span>' : safe;
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
286
+ // ============================================================
287
+ // App state
288
+ // ============================================================
289
+
290
+ var sessions = [];
291
+ var activeId = null;
292
+ var outputLines = [];
293
+ var nextCursor = 0;
294
+ var pollTimer = null;
295
+ var pollActive = false;
296
+
297
+ function setStatus(msg) {
298
+ document.getElementById('statusbar').textContent = msg;
299
+ }
300
+
301
+ // ============================================================
302
+ // Session list
303
+ // ============================================================
304
+
305
+ function badgeClass(status) {
306
+ if (status === 'running') return 'br';
307
+ if (status === 'starting') return 'bs';
308
+ if (status === 'killed') return 'bk';
309
+ if (status === 'error') return 'berr';
310
+ return 'be'; // exited
311
+ }
312
+
313
+ // Derive a turn-aware display badge. A session's 'status' only tracks process
314
+ // liveness, so an agent-cli process stays "running" while idle between turns.
315
+ // Read 'busy' / 'awaitingInput' to show real activity instead:
316
+ // working \u2014 a turn is in flight (busy)
317
+ // waiting \u2014 turn ended on awaiting-input (needs a reply)
318
+ // idle \u2014 process alive, no turn running (last turn done)
319
+ function displayBadge(s) {
320
+ if (s.status === 'running' && s.kind === 'agent-cli') {
321
+ if (s.busy) return { label: 'working', cls: 'br' };
322
+ if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };
323
+ return { label: 'idle', cls: 'be' };
324
+ }
325
+ return { label: s.status, cls: badgeClass(s.status) };
326
+ }
327
+
328
+ function renderSidebar() {
329
+ var el = document.getElementById('session-list');
330
+ if (sessions.length === 0) {
331
+ el.innerHTML = '<div style="padding:12px;color:var(--text2);font-size:11px">No sessions</div>';
332
+ return;
333
+ }
334
+ var html = '';
335
+ for (var i = 0; i < sessions.length; i++) {
336
+ var s = sessions[i];
337
+ var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);
338
+ var db = displayBadge(s);
339
+ var active = s.id === activeId ? ' active' : '';
340
+ // blockedOn (set while the turn waits on a spawned sub-agent or a
341
+ // shell command) rides next to the status badge; waiting-on-user is
342
+ // NOT here \u2014 that's awaitingInput, a different signal.
343
+ var blocked = '';
344
+ if (s.blockedOn === 'subagent') blocked = '<span class="badge bs">&#129513; sous-agent</span>';
345
+ else if (s.blockedOn === 'command') blocked = '<span class="badge bs">&#9203; commande</span>';
346
+ html += '<div class="si' + active + '" onclick="selectSession(\\'' + s.id + '\\')">'
347
+ + '<div class="sn">' + escHtml(label) + '</div>'
348
+ + '<div class="sm">'
349
+ + '<span class="badge ' + db.cls + '">' + db.label + '</span>'
350
+ + blocked
351
+ + '<span>' + escHtml(s.kind || '') + '</span>'
352
+ + '</div></div>';
353
+ }
354
+ el.innerHTML = html;
355
+ }
356
+
357
+ function loadSessions() {
358
+ // {kind:'all'} is session_list's live-able default \u2014 agent-CLI + terminal/
359
+ // PTY only. kind:"command" rows (a shell-execution log, not a resumable
360
+ // session) are excluded unless includeCommands is explicitly passed.
361
+ return callTool('session_list', {kind: 'all'}).then(function(data) {
362
+ sessions = data.sessions || [];
363
+ renderSidebar();
364
+ setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' \xB7 ' + new Date().toLocaleTimeString());
365
+ }).catch(function(e) {
366
+ setStatus('Error: ' + e.message);
367
+ });
368
+ }
369
+
370
+ // ============================================================
371
+ // Output panel
372
+ // ============================================================
373
+
374
+ function renderOutputFull() {
375
+ var el = document.getElementById('output');
376
+ if (outputLines.length === 0) {
377
+ el.innerHTML = '<div id="empty"><div class="ico">&#9889;</div><div>No output yet</div></div>';
378
+ return;
379
+ }
380
+ var html = '';
381
+ for (var i = 0; i < outputLines.length; i++) {
382
+ html += '<div class="line">' + ansiToHtml(outputLines[i]) + '</div>';
383
+ }
384
+ el.innerHTML = html;
385
+ el.scrollTop = el.scrollHeight;
386
+ }
387
+
388
+ function appendLines(lines) {
389
+ var el = document.getElementById('output');
390
+ // Remove empty-state placeholder if present
391
+ var empty = el.querySelector('#empty');
392
+ if (empty) empty.parentNode.removeChild(empty);
393
+ var frag = document.createDocumentFragment();
394
+ for (var i = 0; i < lines.length; i++) {
395
+ var div = document.createElement('div');
396
+ div.className = 'line';
397
+ div.innerHTML = ansiToHtml(lines[i]);
398
+ frag.appendChild(div);
399
+ }
400
+ el.appendChild(frag);
401
+ el.scrollTop = el.scrollHeight;
402
+ }
403
+
404
+ function selectSession(id) {
405
+ activeId = id;
406
+ outputLines = [];
407
+ nextCursor = 0;
408
+ renderSidebar();
409
+
410
+ var s = null;
411
+ for (var i = 0; i < sessions.length; i++) {
412
+ if (sessions[i].id === id) { s = sessions[i]; break; }
413
+ }
414
+
415
+ var toolbar = document.getElementById('toolbar');
416
+ toolbar.style.display = 'flex';
417
+ document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);
418
+
419
+ var killBtn = document.getElementById('kill-btn');
420
+ killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');
421
+
422
+ document.getElementById('output').innerHTML = '<div style="padding:12px;color:var(--text2)">Loading&#8230;</div>';
423
+
424
+ // Initial load
425
+ callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {
426
+ outputLines = data.lines || [];
427
+ nextCursor = data.nextCursor || 0;
428
+ renderOutputFull();
429
+ }).catch(function(e) {
430
+ document.getElementById('output').innerHTML = '<div class="errmsg">Error: ' + escHtml(e.message) + '</div>';
431
+ });
432
+ }
433
+
434
+ // ============================================================
435
+ // Kill
436
+ // ============================================================
437
+
438
+ function doKill() {
439
+ if (!activeId) return;
440
+ var s = null;
441
+ for (var i = 0; i < sessions.length; i++) {
442
+ if (sessions[i].id === activeId) { s = sessions[i]; break; }
443
+ }
444
+ var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';
445
+ callTool(toolName, {sessionId: activeId}).then(function() {
446
+ return doRefresh();
447
+ }).catch(function(e) {
448
+ setStatus('Kill failed: ' + e.message);
449
+ });
450
+ }
451
+
452
+ // ============================================================
453
+ // Poll
454
+ // ============================================================
455
+
456
+ function doPoll() {
457
+ if (pollActive) return;
458
+ pollActive = true;
459
+ var p = loadSessions();
460
+ if (activeId) {
461
+ var capturedId = activeId;
462
+ var capturedCursor = nextCursor;
463
+ p = p.then(function() {
464
+ return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});
465
+ }).then(function(data) {
466
+ if (capturedId !== activeId) return; // user switched sessions
467
+ var newLines = data.lines || [];
468
+ if (newLines.length > 0) {
469
+ outputLines = outputLines.concat(newLines);
470
+ if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);
471
+ nextCursor = data.nextCursor || nextCursor;
472
+ appendLines(newLines);
473
+ }
474
+ }).catch(function() {});
475
+ }
476
+ p.then(function() {
477
+ pollActive = false;
478
+ pollTimer = setTimeout(doPoll, 3000);
479
+ }).catch(function() {
480
+ pollActive = false;
481
+ pollTimer = setTimeout(doPoll, 3000);
482
+ });
483
+ }
484
+
485
+ function doRefresh() {
486
+ return loadSessions().then(function() {
487
+ if (activeId) {
488
+ return callTool('agent_output', {sessionId: activeId, lastN: 200}).then(function(data) {
489
+ outputLines = data.lines || [];
490
+ nextCursor = data.nextCursor || 0;
491
+ renderOutputFull();
492
+ });
493
+ }
494
+ });
495
+ }
496
+
497
+ // ============================================================
498
+ // Boot
499
+ // ============================================================
500
+
501
+ initBridge().then(function() {
502
+ return loadSessions();
503
+ }).then(function() {
504
+ pollTimer = setTimeout(doPoll, 3000);
505
+ }).catch(function(e) {
506
+ setStatus('Bridge error: ' + e.message);
507
+ document.getElementById('output').innerHTML = '<div class="errmsg">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';
508
+ });
509
+ </script>
510
+ </body>
511
+ </html>`;
512
+
513
+ // src/sessions-panel/index.ts
514
+ var sessionsPanelInputSchema = z.object({
515
+ filter: z.enum(["running", "all"]).optional().describe(
516
+ "Which sessions to return. `running` = only alive; `all` = running + recent (default)."
517
+ )
518
+ });
519
+ function makeSessionsPanelApp(ops) {
520
+ return {
521
+ id: "agentproto_sessions",
522
+ title: "Agent Sessions",
523
+ description: "Open the agentproto sessions panel \u2014 an interactive UI that shows all running and recent agent-CLI and terminal/PTY sessions. Raw shell-command runs are a log, not a resumable session, and don't appear here \u2014 see `command_list`. The panel polls live data and lets you inspect output or kill sessions.",
524
+ inputSchema: sessionsPanelInputSchema,
525
+ execute: async (input) => ({
526
+ sessions: ops.listSessions(input.filter)
527
+ }),
528
+ html: PANEL_HTML
529
+ };
530
+ }
531
+ var sessionsPanelApp = defineApp({
532
+ id: "@agentproto/sessions-panel",
533
+ name: "Agent Sessions",
534
+ description: "Open the agentproto sessions panel \u2014 an interactive UI that shows all running and recent agent-CLI and terminal/PTY sessions.",
535
+ agents: [],
536
+ ui: {
537
+ html: PANEL_HTML,
538
+ title: "Agent Sessions",
539
+ tools: ["session_list", "agent_output"]
540
+ }
541
+ });
542
+
543
+ export { makeSessionsPanelApp, sessionsPanelApp, sessionsPanelInputSchema };
544
+ //# sourceMappingURL=sessions-panel.mjs.map
545
+ //# sourceMappingURL=sessions-panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/panel-bridge.ts","../src/sessions-panel/panel.ts","../src/sessions-panel/index.ts"],"names":[],"mappings":";;;;;;;;;;AAqCO,SAAS,kBAAkB,OAAA,EAAyB;AACzD,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA,EAmDa,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAAA,CAAA;AAyF7C;;;AC3JO,IAAM,UAAA,GAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2ExB,iBAAA,CAAkB,2BAA2B,CAAC;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;;;ACvEzC,IAAM,wBAAA,GAA2B,EAAE,MAAA,CAAO;AAAA,EAC/C,MAAA,EAAQ,EACL,IAAA,CAAK,CAAC,WAAW,KAAK,CAAC,CAAA,CACvB,QAAA,EAAS,CACT,QAAA;AAAA,IACC;AAAA;AAEN,CAAC;AAqBM,SAAS,qBACd,GAAA,EACqD;AACrD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,qBAAA;AAAA,IACJ,KAAA,EAAO,gBAAA;AAAA,IACP,WAAA,EACE,wTAAA;AAAA,IAKF,WAAA,EAAa,wBAAA;AAAA,IACb,OAAA,EAAS,OAAO,KAAA,MAAW;AAAA,MACzB,QAAA,EAAU,GAAA,CAAI,YAAA,CAAa,KAAA,CAAM,MAAM;AAAA,KACzC,CAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACR;AACF;AAEO,IAAM,mBAA8B,SAAA,CAAU;AAAA,EACnD,EAAA,EAAI,4BAAA;AAAA,EACJ,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EACE,oIAAA;AAAA,EAEF,QAAQ,EAAC;AAAA,EACT,EAAA,EAAI;AAAA,IACF,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,gBAAA;AAAA,IACP,KAAA,EAAO,CAAC,cAAA,EAAgB,cAAc;AAAA;AAE1C,CAAC","file":"sessions-panel.mjs","sourcesContent":["/**\n * Shared MCP Apps client bridge for agentproto panels.\n *\n * One spec-correct copy of the JSON-RPC-over-postMessage bridge that every\n * ui:// panel embeds in its <script> block. Mirrors the behaviour of\n * @agstudio/mcp-apps' mcpAppClientScript() — which we cannot import here:\n * agentproto is a standalone pnpm workspace with no @agstudio/* dependency\n * (same isolation invariant as mcp-apps-adapter.ts).\n *\n * Handshake (MCP Apps ext spec 2026-01-26, ext-apps McpUiInitializeRequest):\n * params REQUIRE `appInfo` (Implementation: {name, version}),\n * `appCapabilities`, and `protocolVersion`. Sending `clientInfo` instead\n * of `appInfo` fails the host's schema validation — hosts either reply\n * with an error or silently drop the request, so the panel's initBridge()\n * promise never resolves and the panel renders blank while every\n * server-side surface (tools, resources) works. That exact bug shipped in\n * 4 copy-pasted bridges; this module is the single corrected source.\n *\n * The script defines, in panel scope:\n * post / rpcRequest / rpcNotify — raw JSON-RPC plumbing\n * onHostNotification(cb) — subscribe to host-pushed\n * notifications (method, params)\n * initBridge() — ui/initialize → ui/notifications/initialized\n * callTool(name, args) — tools/call + JSON text unwrap\n * getHostContext() / onHostContext(cb) — host context (displayMode, theme, …),\n * captured from the initialize result and\n * merged on ui/notifications/host-context-changed\n * requestDisplayMode(mode) — ui/request-display-mode (inline|fullscreen|pip)\n *\n * Display-mode toggle: the bridge also injects two floating buttons\n * (fullscreen + pip) in the top-right corner of every panel, shown only for\n * the modes the host actually advertises via hostContext.availableDisplayModes\n * — same pattern as guilde's canvas.app.ts canvasShellHtml() (the reference\n * implementation that renders correctly in Claude hosts). No auto-request:\n * panels stay inline until the user clicks.\n */\n\nexport function panelBridgeScript(appName: string): string {\n return `// ── MCP Apps bridge (shared: panel-bridge.ts) ──\n// JSON-RPC 2.0 over window.parent.postMessage · spec 2026-01-26\nvar _nextId = 1, _pending = {}, _notifyHandlers = [];\nvar _hostContext = null, _hostContextHandlers = [];\nfunction post(msg){ window.parent.postMessage(msg, '*'); }\nfunction getHostContext(){ return _hostContext; }\nfunction onHostContext(cb){\n _hostContextHandlers.push(cb);\n // Replay the last context so a late subscriber isn't stuck blind.\n if (_hostContext){ try { cb(_hostContext); } catch(_) {} }\n}\nfunction _setHostContext(ctx){\n if (!ctx || typeof ctx !== 'object') return;\n // ui/notifications/host-context-changed carries only the changed keys —\n // merge, matching the official ext-apps App behaviour.\n _hostContext = Object.assign({}, _hostContext || {}, ctx);\n for (var i = 0; i < _hostContextHandlers.length; i++){\n try { _hostContextHandlers[i](_hostContext); } catch(_) {}\n }\n}\nfunction rpcRequest(method, params){\n return new Promise(function(resolve, reject){\n var id = _nextId++;\n _pending[id] = {resolve: resolve, reject: reject};\n post({jsonrpc: '2.0', id: id, method: method, params: params || {}});\n });\n}\nfunction rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }\nfunction onHostNotification(cb){ _notifyHandlers.push(cb); }\nwindow.addEventListener('message', function(evt){\n var msg = evt.data;\n if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;\n if (msg.id != null && msg.method == null){\n var p = _pending[msg.id];\n if (!p) return;\n delete _pending[msg.id];\n if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method){\n if (msg.method === 'ui/notifications/host-context-changed'){\n _setHostContext(msg.params || {});\n }\n for (var i = 0; i < _notifyHandlers.length; i++){\n try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}\n }\n }\n});\nfunction initBridge(){\n return rpcRequest('ui/initialize', {\n appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},\n appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},\n protocolVersion: '2026-01-26'\n }).then(function(result){\n // The initialize result carries the initial hostContext (displayMode +\n // availableDisplayModes) — capture it before notifying the host.\n if (result && result.hostContext) _setHostContext(result.hostContext);\n rpcNotify('ui/notifications/initialized', {});\n });\n}\nfunction requestDisplayMode(mode){\n return rpcRequest('ui/request-display-mode', {mode: mode});\n}\nfunction callTool(name, args){\n return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){\n if (result.isError){\n var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';\n throw new Error(e);\n }\n var text = (result.content && result.content[0] && result.content[0].text) || '{}';\n return JSON.parse(text);\n });\n}\n\n// ── Display-mode toggle buttons (NO auto-request) ──────────────────────\n// Injected by the shared bridge so every panel gets them without touching\n// its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the\n// panel stays inline by default; the user expands on demand. Buttons only\n// appear for modes the host advertises in hostContext.availableDisplayModes.\n(function(){\n function mount(){\n var style = document.createElement('style');\n style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'\n + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'\n + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'\n + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'\n + '#dm{right:8px}#pin{right:118px}'\n + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'\n + '@media (prefers-color-scheme:dark){'\n + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'\n + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';\n document.head.appendChild(style);\n\n var btn = document.createElement('button');\n btn.id = 'dm'; btn.type = 'button'; btn.title = \"Basculer l'affichage\";\n var pin = document.createElement('button');\n pin.id = 'pin'; pin.type = 'button'; pin.title = 'Épingler sur le côté (pip)';\n document.body.appendChild(pin);\n document.body.appendChild(btn);\n\n function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }\n\n // Re-sync button visibility + label from the current host context.\n function syncBtn(ctx){\n ctx = ctx || {};\n var avail = ctx.availableDisplayModes;\n // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)\n console.log('[mcp-app] displayMode=', ctx.displayMode,\n 'availableDisplayModes=', avail);\n\n // Fullscreen toggle button.\n if (has(avail, 'fullscreen')){\n btn.style.display = 'block';\n btn.textContent = (ctx.displayMode === 'fullscreen') ? '⤡ Réduire' : '⤢ Agrandir';\n } else { btn.style.display = 'none'; }\n\n // Dedicated pip (\"pinned on side\") button — only if the host advertises pip.\n if (has(avail, 'pip')){\n pin.style.display = 'block';\n pin.textContent = (ctx.displayMode === 'pip') ? '⤡ Détacher' : '📌 Épingler';\n } else { pin.style.display = 'none'; }\n }\n\n onHostContext(syncBtn);\n\n btn.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');\n requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});\n });\n\n pin.addEventListener('click', function(){\n var ctx = getHostContext() || {};\n requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});\n });\n }\n if (document.body) mount();\n else document.addEventListener('DOMContentLoaded', mount);\n})();`\n}\n","/**\n * HTML bundle for the agentproto sessions panel — the `PANEL_HTML` served\n * as the `ui://agentproto_sessions/view` resource by `registerMcpApps`\n * (see ../mcp-app-types.ts and runtime's mcp-apps-adapter.ts). Mounted via\n * the `agentproto_sessions` tool defined in ./index.ts.\n *\n * Protocol: MCP Apps ext spec 2026-01-26\n * – Bridge: JSON-RPC 2.0 over window.parent.postMessage\n * – Handshake: ui/initialize → host result → ui/notifications/initialized\n * – Data: tools/call → session_list + agent_output (3 s poll)\n * – Kill action: tools/call → agent_kill | terminal_kill\n * (goes through Claude's normal tool approval flow)\n *\n * The HTML bundle is fully autonomous (zero CDN, zero external deps).\n * ANSI-to-HTML renderer handles SGR codes (colors 0-15, bold, dim,\n * italic, underline, reset). CR-overwrite sequences are stripped so\n * progress bars don't corrupt the output.\n */\n\nimport { panelBridgeScript } from \"../panel-bridge.js\"\n\n// ── HTML bundle ─────────────────────────────────────────────────────────────\n\nexport const PANEL_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>agentproto sessions</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;\n --text:#e6edf3;--text2:#8b949e;\n --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;\n}\nhtml,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}\n#app{display:flex;height:100%}\n#sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}\n#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}\n#sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}\n#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:15px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}\n#refresh-btn:hover{color:var(--text);background:var(--bg3)}\n#session-list{flex:1;overflow-y:auto;padding:4px 0}\n.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}\n.si:hover{background:var(--bg3)}\n.si.active{background:var(--bg3);border-left-color:var(--blue)}\n.sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}\n.badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}\n.br{background:rgba(63,185,80,.15);color:var(--green)}\n.bs{background:rgba(88,166,255,.15);color:var(--blue)}\n.be{background:rgba(139,148,158,.1);color:var(--text2)}\n.bk{background:rgba(248,81,73,.1);color:var(--red)}\n.berr{background:rgba(248,81,73,.2);color:var(--red)}\n#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}\n#toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}\n#session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}\n.abtn:hover{background:var(--border)}\n.abtn:disabled{opacity:.4;cursor:default}\n.abtn.danger{border-color:var(--red);color:var(--red)}\n.abtn.danger:hover{background:rgba(248,81,73,.1)}\n#output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}\n.line{white-space:pre-wrap;word-break:break-all}\n#empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}\n#empty .ico{font-size:32px}\n#statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}\n.errmsg{color:var(--red);padding:12px}\n/* ANSI SGR */\n.ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}\n.f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}\n.f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}\n.f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}\n.f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <div id=\"sidebar\">\n <div id=\"sidebar-hdr\">\n <h1>Sessions</h1>\n <button id=\"refresh-btn\" title=\"Refresh\" onclick=\"doRefresh()\">&#8635;</button>\n </div>\n <div id=\"session-list\"></div>\n </div>\n <div id=\"main\">\n <div id=\"toolbar\" style=\"display:none\">\n <span id=\"session-title\"></span>\n <button class=\"abtn danger\" id=\"kill-btn\" onclick=\"doKill()\">Kill</button>\n </div>\n <div id=\"output\">\n <div id=\"empty\"><div class=\"ico\">&#9889;</div><div>Select a session</div></div>\n </div>\n <div id=\"statusbar\">Connecting to bridge&#8230;</div>\n </div>\n</div>\n<script>\n${panelBridgeScript(\"agentproto-sessions-panel\")}\n\n// ============================================================\n// ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)\n// ============================================================\n\nfunction escHtml(s) {\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n}\n\nfunction ansiToHtml(raw) {\n // Strip CR overwrite sequences (progress bars: CR without LF)\n var text = raw.replace(/[^\\\\n]*\\\\r([^\\\\n])/g, '$1').replace(/\\\\r/g, '');\n var bold = false, dim = false, italic = false, underline = false, fg = -1;\n var out = '';\n // Split on ESC [ ... m (SGR) sequences; keep delimiters\n var ESC = '\u001b';\n var parts = text.split(/([\u001b]\\\\[[0-9;]*m)/);\n for (var pi = 0; pi < parts.length; pi++) {\n var part = parts[pi];\n if (part.length === 0) continue;\n if (part.charCodeAt(0) === 0x1b && part[1] === '[') {\n // Parse SGR parameters\n var inner = part.slice(2, part.length - 1); // strip ESC[ and m\n var codes = inner === '' ? [0] : inner.split(';').map(Number);\n for (var ci = 0; ci < codes.length; ci++) {\n var c = codes[ci];\n if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }\n else if (c === 1) bold = true;\n else if (c === 2) dim = true;\n else if (c === 3) italic = true;\n else if (c === 4) underline = true;\n else if (c === 22) { bold = false; dim = false; }\n else if (c === 23) italic = false;\n else if (c === 24) underline = false;\n else if (c >= 30 && c <= 37) fg = c - 30;\n else if (c === 39) fg = -1;\n else if (c >= 90 && c <= 97) fg = c - 90 + 8;\n }\n } else {\n // Text segment — strip any remaining non-printable ESC sequences\n var safe = escHtml(part).replace(/\u001b\\\\[[^m]*[A-Za-z]/g, '');\n if (safe === '') continue;\n var cls = '';\n if (bold) cls += ' ab';\n if (dim) cls += ' ad';\n if (italic) cls += ' ai';\n if (underline) cls += ' au';\n if (fg >= 0 && fg <= 15) cls += ' f' + fg;\n cls = cls.trim();\n out += cls ? '<span class=\"' + cls + '\">' + safe + '</span>' : safe;\n }\n }\n return out;\n}\n\n// ============================================================\n// App state\n// ============================================================\n\nvar sessions = [];\nvar activeId = null;\nvar outputLines = [];\nvar nextCursor = 0;\nvar pollTimer = null;\nvar pollActive = false;\n\nfunction setStatus(msg) {\n document.getElementById('statusbar').textContent = msg;\n}\n\n// ============================================================\n// Session list\n// ============================================================\n\nfunction badgeClass(status) {\n if (status === 'running') return 'br';\n if (status === 'starting') return 'bs';\n if (status === 'killed') return 'bk';\n if (status === 'error') return 'berr';\n return 'be'; // exited\n}\n\n// Derive a turn-aware display badge. A session's 'status' only tracks process\n// liveness, so an agent-cli process stays \"running\" while idle between turns.\n// Read 'busy' / 'awaitingInput' to show real activity instead:\n// working — a turn is in flight (busy)\n// waiting — turn ended on awaiting-input (needs a reply)\n// idle — process alive, no turn running (last turn done)\nfunction displayBadge(s) {\n if (s.status === 'running' && s.kind === 'agent-cli') {\n if (s.busy) return { label: 'working', cls: 'br' };\n if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };\n return { label: 'idle', cls: 'be' };\n }\n return { label: s.status, cls: badgeClass(s.status) };\n}\n\nfunction renderSidebar() {\n var el = document.getElementById('session-list');\n if (sessions.length === 0) {\n el.innerHTML = '<div style=\"padding:12px;color:var(--text2);font-size:11px\">No sessions</div>';\n return;\n }\n var html = '';\n for (var i = 0; i < sessions.length; i++) {\n var s = sessions[i];\n var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);\n var db = displayBadge(s);\n var active = s.id === activeId ? ' active' : '';\n // blockedOn (set while the turn waits on a spawned sub-agent or a\n // shell command) rides next to the status badge; waiting-on-user is\n // NOT here — that's awaitingInput, a different signal.\n var blocked = '';\n if (s.blockedOn === 'subagent') blocked = '<span class=\"badge bs\">&#129513; sous-agent</span>';\n else if (s.blockedOn === 'command') blocked = '<span class=\"badge bs\">&#9203; commande</span>';\n html += '<div class=\"si' + active + '\" onclick=\"selectSession(\\\\'' + s.id + '\\\\')\">'\n + '<div class=\"sn\">' + escHtml(label) + '</div>'\n + '<div class=\"sm\">'\n + '<span class=\"badge ' + db.cls + '\">' + db.label + '</span>'\n + blocked\n + '<span>' + escHtml(s.kind || '') + '</span>'\n + '</div></div>';\n }\n el.innerHTML = html;\n}\n\nfunction loadSessions() {\n // {kind:'all'} is session_list's live-able default — agent-CLI + terminal/\n // PTY only. kind:\"command\" rows (a shell-execution log, not a resumable\n // session) are excluded unless includeCommands is explicitly passed.\n return callTool('session_list', {kind: 'all'}).then(function(data) {\n sessions = data.sessions || [];\n renderSidebar();\n setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' · ' + new Date().toLocaleTimeString());\n }).catch(function(e) {\n setStatus('Error: ' + e.message);\n });\n}\n\n// ============================================================\n// Output panel\n// ============================================================\n\nfunction renderOutputFull() {\n var el = document.getElementById('output');\n if (outputLines.length === 0) {\n el.innerHTML = '<div id=\"empty\"><div class=\"ico\">&#9889;</div><div>No output yet</div></div>';\n return;\n }\n var html = '';\n for (var i = 0; i < outputLines.length; i++) {\n html += '<div class=\"line\">' + ansiToHtml(outputLines[i]) + '</div>';\n }\n el.innerHTML = html;\n el.scrollTop = el.scrollHeight;\n}\n\nfunction appendLines(lines) {\n var el = document.getElementById('output');\n // Remove empty-state placeholder if present\n var empty = el.querySelector('#empty');\n if (empty) empty.parentNode.removeChild(empty);\n var frag = document.createDocumentFragment();\n for (var i = 0; i < lines.length; i++) {\n var div = document.createElement('div');\n div.className = 'line';\n div.innerHTML = ansiToHtml(lines[i]);\n frag.appendChild(div);\n }\n el.appendChild(frag);\n el.scrollTop = el.scrollHeight;\n}\n\nfunction selectSession(id) {\n activeId = id;\n outputLines = [];\n nextCursor = 0;\n renderSidebar();\n\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === id) { s = sessions[i]; break; }\n }\n\n var toolbar = document.getElementById('toolbar');\n toolbar.style.display = 'flex';\n document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);\n\n var killBtn = document.getElementById('kill-btn');\n killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');\n\n document.getElementById('output').innerHTML = '<div style=\"padding:12px;color:var(--text2)\">Loading&#8230;</div>';\n\n // Initial load\n callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n }).catch(function(e) {\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Error: ' + escHtml(e.message) + '</div>';\n });\n}\n\n// ============================================================\n// Kill\n// ============================================================\n\nfunction doKill() {\n if (!activeId) return;\n var s = null;\n for (var i = 0; i < sessions.length; i++) {\n if (sessions[i].id === activeId) { s = sessions[i]; break; }\n }\n var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';\n callTool(toolName, {sessionId: activeId}).then(function() {\n return doRefresh();\n }).catch(function(e) {\n setStatus('Kill failed: ' + e.message);\n });\n}\n\n// ============================================================\n// Poll\n// ============================================================\n\nfunction doPoll() {\n if (pollActive) return;\n pollActive = true;\n var p = loadSessions();\n if (activeId) {\n var capturedId = activeId;\n var capturedCursor = nextCursor;\n p = p.then(function() {\n return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});\n }).then(function(data) {\n if (capturedId !== activeId) return; // user switched sessions\n var newLines = data.lines || [];\n if (newLines.length > 0) {\n outputLines = outputLines.concat(newLines);\n if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);\n nextCursor = data.nextCursor || nextCursor;\n appendLines(newLines);\n }\n }).catch(function() {});\n }\n p.then(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n }).catch(function() {\n pollActive = false;\n pollTimer = setTimeout(doPoll, 3000);\n });\n}\n\nfunction doRefresh() {\n return loadSessions().then(function() {\n if (activeId) {\n return callTool('agent_output', {sessionId: activeId, lastN: 200}).then(function(data) {\n outputLines = data.lines || [];\n nextCursor = data.nextCursor || 0;\n renderOutputFull();\n });\n }\n });\n}\n\n// ============================================================\n// Boot\n// ============================================================\n\ninitBridge().then(function() {\n return loadSessions();\n}).then(function() {\n pollTimer = setTimeout(doPoll, 3000);\n}).catch(function(e) {\n setStatus('Bridge error: ' + e.message);\n document.getElementById('output').innerHTML = '<div class=\"errmsg\">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';\n});\n</script>\n</body>\n</html>`\n","/**\n * McpApp definition for the agentproto sessions panel.\n *\n * Uses the AgnoMcpApp contract (a local McpApp-compatible shape that does\n * NOT depend on @agstudio/mcp-apps or Mastra). The host (@agentproto/runtime)\n * wires this factory's output via `registerMcpApps` from its\n * mcp-apps-adapter.ts at boot time — see runtime's builtin-apps.ts.\n *\n * Protocol flow:\n * 1. Tool `agentproto_sessions` is called by the host.\n * 2. execute() returns a sessions snapshot → injected into the HTML as\n * window.__APP_INIT__ (static initial render, no bridge needed).\n * 3. The HTML panel opens a JSON-RPC bridge (postMessage) and polls\n * `session_list` + `agent_output` every 3 s for live data.\n *\n * This module also exports `sessionsPanelApp`, a real `defineApp()`\n * `AppHandle` (`agents: []`, UI-only) — the catalog/emit/`app_install` path.\n * It's separate from `makeSessionsPanelApp` above, which the daemon mounts\n * directly at boot: that factory closes over LIVE `listSessions`, something\n * a static emitted `ui.html` snapshot can't carry.\n */\n\nimport { z } from \"zod\"\nimport { defineApp, type AppHandle } from \"@agentproto/app-kit\"\nimport { PANEL_HTML } from \"./panel.js\"\nimport type { AgnoMcpApp } from \"../mcp-app-types.js\"\n\nexport const sessionsPanelInputSchema = z.object({\n filter: z\n .enum([\"running\", \"all\"])\n .optional()\n .describe(\n \"Which sessions to return. `running` = only alive; `all` = running + recent (default).\",\n ),\n})\n\nexport type SessionsInput = z.infer<typeof sessionsPanelInputSchema>\nexport type SessionsOutput<TSession = unknown> = { sessions: TSession[] }\n\n/**\n * Generic over the host's own session-descriptor shape — this package\n * doesn't own that type (it lives in @agentproto/runtime's session\n * registry), so the factory stays agnostic to it and the host supplies the\n * concrete type as a type argument when it wires the app up.\n */\nexport interface SessionsPanelOps<TSession = unknown> {\n listSessions(filter?: \"running\" | \"all\"): TSession[]\n}\n\nexport type { AgnoMcpApp }\n\n/**\n * Factory: close over the sessions registry so execute() doesn't need\n * an AppContext (agentproto has no userId/guildId concept).\n */\nexport function makeSessionsPanelApp<TSession = unknown>(\n ops: SessionsPanelOps<TSession>,\n): AgnoMcpApp<SessionsInput, SessionsOutput<TSession>> {\n return {\n id: \"agentproto_sessions\",\n title: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all \" +\n \"running and recent agent-CLI and terminal/PTY sessions. Raw shell-\" +\n \"command runs are a log, not a resumable session, and don't appear \" +\n \"here — see `command_list`. The panel polls live data and lets you \" +\n \"inspect output or kill sessions.\",\n inputSchema: sessionsPanelInputSchema,\n execute: async (input) => ({\n sessions: ops.listSessions(input.filter),\n }),\n html: PANEL_HTML,\n }\n}\n\nexport const sessionsPanelApp: AppHandle = defineApp({\n id: \"@agentproto/sessions-panel\",\n name: \"Agent Sessions\",\n description:\n \"Open the agentproto sessions panel — an interactive UI that shows all running and recent \" +\n \"agent-CLI and terminal/PTY sessions.\",\n agents: [],\n ui: {\n html: PANEL_HTML,\n title: \"Agent Sessions\",\n tools: [\"session_list\", \"agent_output\"],\n },\n})\n"]}