@agentproto/runtime 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -20,11 +20,13 @@ import { inferLegacyModeKind, parseModelSwitchCommand, isModelSwitchAcknowledgem
20
20
  import { loadSandboxConfig, resolveCommandSandbox, COMMAND_SANDBOX_MODE_ENV } from '@agentproto/command-sandbox';
21
21
  import { CatalogProviderSchema, getModelsByProvider, getStaticModelProvider } from '@agentproto/model-catalog';
22
22
  import { createBrainManager, parseKnowledgeConfig } from '@agentproto/workspace-brain';
23
+ import { makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, liveSessionApp } from '@agentproto/apps';
23
24
  import matter2 from 'gray-matter';
24
25
  import { createServer } from 'http';
25
26
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
26
27
  import { WebSocketServer } from 'ws';
27
28
  import { loadAppHandle } from '@agentproto/app-kit';
29
+ import { loadAgent } from '@agentproto/agent';
28
30
  import { normalizeToolId } from '@agentproto/driver';
29
31
  import { createIngestionClient } from '@agentproto/telemetry-langfuse';
30
32
  import { resolveRedactor } from '@agentproto/redaction';
@@ -239,6 +241,14 @@ function appendFooterOnce(body, footer) {
239
241
  function hasProvenanceFooter(body) {
240
242
  return new RegExp(`<sub>[^\\n]*${MARKER}`).test(body);
241
243
  }
244
+ function footerHasCost(body) {
245
+ const m = new RegExp(`<sub>[^\\n]*${MARKER}[^\\n]*</sub>`).exec(body);
246
+ return m !== null && /\$\d/.test(m[0]);
247
+ }
248
+ function replaceProvenanceFooter(body, footer) {
249
+ if (!hasProvenanceFooter(body)) return appendFooterOnce(body, footer);
250
+ return body.replace(FOOTER_BLOCK_RE, footer);
251
+ }
242
252
  function parseGhPrCreate(command, args, stdout) {
243
253
  if (basename(command) !== "gh") return null;
244
254
  const positionals = args.filter((a) => !a.startsWith("-"));
@@ -279,7 +289,7 @@ function pickExecutorSession(sessions, cwd) {
279
289
  if (live.length > 0) return live[0];
280
290
  return [...candidates].sort(byRecency)[0];
281
291
  }
282
- var MARKER, fmtTokens, buildFooter;
292
+ var MARKER, fmtTokens, buildFooter, FOOTER_BLOCK_RE;
283
293
  var init_pr_provenance = __esm({
284
294
  "src/pr-provenance.ts"() {
285
295
  MARKER = "@agentproto-bot";
@@ -324,6 +334,7 @@ var init_pr_provenance = __esm({
324
334
  ---
325
335
  <sub>${parts.join(" \xB7 ")}</sub>`;
326
336
  };
337
+ FOOTER_BLOCK_RE = new RegExp(`(?:\\n+---)?\\n*<sub>[^\\n]*${MARKER}[^\\n]*</sub>`);
327
338
  }
328
339
  });
329
340
  function sessionTranscriptDir(sessionId, baseDir) {
@@ -6321,7 +6332,13 @@ async function spawnAgentSession(deps2, input) {
6321
6332
  const existing = claims.get(key);
6322
6333
  if (existing) {
6323
6334
  const result = await existing.result;
6324
- return result.ok ? { ...result, deduped: true, dedupeSource } : result;
6335
+ if (result.ok) {
6336
+ console.warn(
6337
+ `[agent_start] dedupe hit (${dedupeSource}): returning existing session ${result.descriptor.id}${input.label ? ` (label "${input.label}")` : ""} for a repeated spawn (adapter ${input.adapter}, cwd ${cwd})`
6338
+ );
6339
+ return { ...result, deduped: true, dedupeSource };
6340
+ }
6341
+ return result;
6325
6342
  }
6326
6343
  let resolveClaim;
6327
6344
  claims.set(key, {
@@ -6598,7 +6615,11 @@ ${asyncPrompt}`;
6598
6615
  // the descriptor's `parentSessionId` so the child can discover
6599
6616
  // who spawned it without a registry round-trip. Absent on a
6600
6617
  // parentless root spawn.
6601
- ...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {}
6618
+ ...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {},
6619
+ // App identity (APP_ID_ENV's doc, sessions.ts) — set only for an
6620
+ // `app_run` spawn, so a daemon-tool proxy the child builds can
6621
+ // auto-fill `appId` into an `app_*` call that omits one.
6622
+ ...input.appId ? { [APP_ID_ENV]: input.appId } : {}
6602
6623
  },
6603
6624
  onActivity: () => {
6604
6625
  if (liveSessionId) registry.pulseActivity(liveSessionId);
@@ -6884,7 +6905,25 @@ async function bootSandboxAgentSession(opts) {
6884
6905
  agentSession: createSandboxAgentSessionProxy({ host, remoteSessionId, lifecyclePolicy }),
6885
6906
  commandPreview: `sandbox:${providerSlug} \u2192 ${opts.adapter}`,
6886
6907
  sandboxId: host.sandboxId,
6887
- sandboxTeardown: lifecyclePolicy.teardown
6908
+ sandboxTeardown: lifecyclePolicy.teardown,
6909
+ // The proxy flattens the box's stream to text (documented limitation),
6910
+ // so cost/tokens/model never ride the event stream out of the box. Read
6911
+ // them back from the box daemon's own `session_usage` at each turn-end —
6912
+ // the same `readUsage` hook hermes uses for its state.db — so the HOST
6913
+ // descriptor (and every footer / session_usage built from it) carries
6914
+ // the amount the sandboxed session actually spent.
6915
+ ...typeof host.usage === "function" ? {
6916
+ readUsage: async () => {
6917
+ const snap = await host.usage(remoteSessionId);
6918
+ const usage = {
6919
+ ...typeof snap.model === "string" && snap.model.length > 0 ? { model: snap.model } : {},
6920
+ ...typeof snap.costUsd === "number" ? { costUsd: snap.costUsd } : {},
6921
+ ...typeof snap.tokensIn === "number" ? { tokensIn: snap.tokensIn } : {},
6922
+ ...typeof snap.tokensOut === "number" ? { tokensOut: snap.tokensOut } : {}
6923
+ };
6924
+ return Object.keys(usage).length > 0 ? usage : null;
6925
+ }
6926
+ } : {}
6888
6927
  };
6889
6928
  }
6890
6929
  function sandboxAuthFromResolved(auth) {
@@ -8450,6 +8489,7 @@ function adapterConfigDirFor(sessionId) {
8450
8489
  var SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
8451
8490
  var WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
8452
8491
  var PARENT_SESSION_ID_ENV = "AGENTPROTO_PARENT_SESSION_ID";
8492
+ var APP_ID_ENV = "AGENTPROTO_APP_ID";
8453
8493
  var SessionNotAliveError = class extends Error {
8454
8494
  sessionId;
8455
8495
  status;
@@ -9887,6 +9927,9 @@ ${message}`;
9887
9927
  try {
9888
9928
  const usage2 = await rt.readUsage();
9889
9929
  if (usage2) {
9930
+ if (typeof usage2.model === "string" && usage2.model.length > 0 && rt.desc.model === void 0) {
9931
+ rt.desc.model = usage2.model;
9932
+ }
9890
9933
  if (usage2.costUsd !== void 0) {
9891
9934
  rt.desc.costUsd = usage2.costUsd;
9892
9935
  rt.adapterReportedCost = true;
@@ -11588,6 +11631,15 @@ async function stampFooterOnPr(input) {
11588
11631
  if (view.exitCode !== 0) return { stamped: false, reason: `gh pr view exit ${view.exitCode}` };
11589
11632
  const body = view.stdout.replace(/\n+$/, "");
11590
11633
  const alreadyStamped = hasProvenanceFooter(body);
11634
+ if (alreadyStamped && input.refresh === true) {
11635
+ if (footerHasCost(body) || !footerHasCost(footer)) {
11636
+ return { stamped: true, url: input.prUrl, number: input.prNumber, sessionId: input.session.id, alreadyStamped, refreshed: false };
11637
+ }
11638
+ const refreshedBody = replaceProvenanceFooter(body, footer);
11639
+ const edit = await run(["pr", "edit", input.prUrl, "--body", refreshedBody], input.cwd);
11640
+ if (edit.exitCode !== 0) return { stamped: false, reason: `gh pr edit exit ${edit.exitCode}` };
11641
+ return { stamped: true, url: input.prUrl, number: input.prNumber, sessionId: input.session.id, alreadyStamped, refreshed: true };
11642
+ }
11591
11643
  if (!alreadyStamped) {
11592
11644
  const newBody = appendFooterOnce(body, footer);
11593
11645
  const edit = await run(["pr", "edit", input.prUrl, "--body", newBody], input.cwd);
@@ -18148,528 +18200,48 @@ function registerMcpApps(server, apps) {
18148
18200
  );
18149
18201
  }
18150
18202
  }
18151
-
18152
- // src/panel-bridge.ts
18153
- function panelBridgeScript(appName) {
18154
- return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
18155
- // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
18156
- var _nextId = 1, _pending = {}, _notifyHandlers = [];
18157
- var _hostContext = null, _hostContextHandlers = [];
18158
- function post(msg){ window.parent.postMessage(msg, '*'); }
18159
- function getHostContext(){ return _hostContext; }
18160
- function onHostContext(cb){
18161
- _hostContextHandlers.push(cb);
18162
- // Replay the last context so a late subscriber isn't stuck blind.
18163
- if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
18164
- }
18165
- function _setHostContext(ctx){
18166
- if (!ctx || typeof ctx !== 'object') return;
18167
- // ui/notifications/host-context-changed carries only the changed keys \u2014
18168
- // merge, matching the official ext-apps App behaviour.
18169
- _hostContext = Object.assign({}, _hostContext || {}, ctx);
18170
- for (var i = 0; i < _hostContextHandlers.length; i++){
18171
- try { _hostContextHandlers[i](_hostContext); } catch(_) {}
18172
- }
18173
- }
18174
- function rpcRequest(method, params){
18175
- return new Promise(function(resolve, reject){
18176
- var id = _nextId++;
18177
- _pending[id] = {resolve: resolve, reject: reject};
18178
- post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
18179
- });
18180
- }
18181
- function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
18182
- function onHostNotification(cb){ _notifyHandlers.push(cb); }
18183
- window.addEventListener('message', function(evt){
18184
- var msg = evt.data;
18185
- if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
18186
- if (msg.id != null && msg.method == null){
18187
- var p = _pending[msg.id];
18188
- if (!p) return;
18189
- delete _pending[msg.id];
18190
- if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
18191
- else p.resolve(msg.result);
18192
- return;
18193
- }
18194
- if (msg.method){
18195
- if (msg.method === 'ui/notifications/host-context-changed'){
18196
- _setHostContext(msg.params || {});
18197
- }
18198
- for (var i = 0; i < _notifyHandlers.length; i++){
18199
- try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
18200
- }
18201
- }
18202
- });
18203
- function initBridge(){
18204
- return rpcRequest('ui/initialize', {
18205
- appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
18206
- appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
18207
- protocolVersion: '2026-01-26'
18208
- }).then(function(result){
18209
- // The initialize result carries the initial hostContext (displayMode +
18210
- // availableDisplayModes) \u2014 capture it before notifying the host.
18211
- if (result && result.hostContext) _setHostContext(result.hostContext);
18212
- rpcNotify('ui/notifications/initialized', {});
18213
- });
18214
- }
18215
- function requestDisplayMode(mode){
18216
- return rpcRequest('ui/request-display-mode', {mode: mode});
18217
- }
18218
- function callTool(name, args){
18219
- return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
18220
- if (result.isError){
18221
- var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
18222
- throw new Error(e);
18223
- }
18224
- var text = (result.content && result.content[0] && result.content[0].text) || '{}';
18225
- return JSON.parse(text);
18226
- });
18227
- }
18228
-
18229
- // \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
18230
- // Injected by the shared bridge so every panel gets them without touching
18231
- // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
18232
- // panel stays inline by default; the user expands on demand. Buttons only
18233
- // appear for modes the host advertises in hostContext.availableDisplayModes.
18234
- (function(){
18235
- function mount(){
18236
- var style = document.createElement('style');
18237
- style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
18238
- + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
18239
- + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
18240
- + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
18241
- + '#dm{right:8px}#pin{right:118px}'
18242
- + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
18243
- + '@media (prefers-color-scheme:dark){'
18244
- + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
18245
- + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
18246
- document.head.appendChild(style);
18247
-
18248
- var btn = document.createElement('button');
18249
- btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
18250
- var pin = document.createElement('button');
18251
- pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
18252
- document.body.appendChild(pin);
18253
- document.body.appendChild(btn);
18254
-
18255
- function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
18256
-
18257
- // Re-sync button visibility + label from the current host context.
18258
- function syncBtn(ctx){
18259
- ctx = ctx || {};
18260
- var avail = ctx.availableDisplayModes;
18261
- // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
18262
- console.log('[mcp-app] displayMode=', ctx.displayMode,
18263
- 'availableDisplayModes=', avail);
18264
-
18265
- // Fullscreen toggle button.
18266
- if (has(avail, 'fullscreen')){
18267
- btn.style.display = 'block';
18268
- btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
18269
- } else { btn.style.display = 'none'; }
18270
-
18271
- // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
18272
- if (has(avail, 'pip')){
18273
- pin.style.display = 'block';
18274
- pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
18275
- } else { pin.style.display = 'none'; }
18276
- }
18277
-
18278
- onHostContext(syncBtn);
18279
-
18280
- btn.addEventListener('click', function(){
18281
- var ctx = getHostContext() || {};
18282
- var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
18283
- requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
18284
- });
18285
-
18286
- pin.addEventListener('click', function(){
18287
- var ctx = getHostContext() || {};
18288
- requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
18289
- });
18290
- }
18291
- if (document.body) mount();
18292
- else document.addEventListener('DOMContentLoaded', mount);
18293
- })();`;
18294
- }
18295
-
18296
- // src/sessions-panel.ts
18297
- var PANEL_HTML = `<!DOCTYPE html>
18298
- <html lang="en">
18299
- <head>
18300
- <meta charset="UTF-8">
18301
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
18302
- <title>agentproto sessions</title>
18303
- <style>
18304
- *{box-sizing:border-box;margin:0;padding:0}
18305
- :root{
18306
- --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
18307
- --text:#e6edf3;--text2:#8b949e;
18308
- --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
18309
- }
18310
- html,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
18311
- #app{display:flex;height:100%}
18312
- #sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
18313
- #sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
18314
- #sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}
18315
- #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}
18316
- #refresh-btn:hover{color:var(--text);background:var(--bg3)}
18317
- #session-list{flex:1;overflow-y:auto;padding:4px 0}
18318
- .si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
18319
- .si:hover{background:var(--bg3)}
18320
- .si.active{background:var(--bg3);border-left-color:var(--blue)}
18321
- .sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
18322
- .sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}
18323
- .badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}
18324
- .br{background:rgba(63,185,80,.15);color:var(--green)}
18325
- .bs{background:rgba(88,166,255,.15);color:var(--blue)}
18326
- .be{background:rgba(139,148,158,.1);color:var(--text2)}
18327
- .bk{background:rgba(248,81,73,.1);color:var(--red)}
18328
- .berr{background:rgba(248,81,73,.2);color:var(--red)}
18329
- #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
18330
- #toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}
18331
- #session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
18332
- .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}
18333
- .abtn:hover{background:var(--border)}
18334
- .abtn:disabled{opacity:.4;cursor:default}
18335
- .abtn.danger{border-color:var(--red);color:var(--red)}
18336
- .abtn.danger:hover{background:rgba(248,81,73,.1)}
18337
- #output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}
18338
- .line{white-space:pre-wrap;word-break:break-all}
18339
- #empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}
18340
- #empty .ico{font-size:32px}
18341
- #statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
18342
- .errmsg{color:var(--red);padding:12px}
18343
- /* ANSI SGR */
18344
- .ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}
18345
- .f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}
18346
- .f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}
18347
- .f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}
18348
- .f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}
18349
- </style>
18350
- </head>
18351
- <body>
18352
- <div id="app">
18353
- <div id="sidebar">
18354
- <div id="sidebar-hdr">
18355
- <h1>Sessions</h1>
18356
- <button id="refresh-btn" title="Refresh" onclick="doRefresh()">&#8635;</button>
18357
- </div>
18358
- <div id="session-list"></div>
18359
- </div>
18360
- <div id="main">
18361
- <div id="toolbar" style="display:none">
18362
- <span id="session-title"></span>
18363
- <button class="abtn danger" id="kill-btn" onclick="doKill()">Kill</button>
18364
- </div>
18365
- <div id="output">
18366
- <div id="empty"><div class="ico">&#9889;</div><div>Select a session</div></div>
18367
- </div>
18368
- <div id="statusbar">Connecting to bridge&#8230;</div>
18369
- </div>
18370
- </div>
18371
- <script>
18372
- ${panelBridgeScript("agentproto-sessions-panel")}
18373
-
18374
- // ============================================================
18375
- // ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)
18376
- // ============================================================
18377
-
18378
- function escHtml(s) {
18379
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
18380
- }
18381
-
18382
- function ansiToHtml(raw) {
18383
- // Strip CR overwrite sequences (progress bars: CR without LF)
18384
- var text = raw.replace(/[^\\n]*\\r([^\\n])/g, '$1').replace(/\\r/g, '');
18385
- var bold = false, dim = false, italic = false, underline = false, fg = -1;
18386
- var out = '';
18387
- // Split on ESC [ ... m (SGR) sequences; keep delimiters
18388
- var ESC = '\x1B';
18389
- var parts = text.split(/([\x1B]\\[[0-9;]*m)/);
18390
- for (var pi = 0; pi < parts.length; pi++) {
18391
- var part = parts[pi];
18392
- if (part.length === 0) continue;
18393
- if (part.charCodeAt(0) === 0x1b && part[1] === '[') {
18394
- // Parse SGR parameters
18395
- var inner = part.slice(2, part.length - 1); // strip ESC[ and m
18396
- var codes = inner === '' ? [0] : inner.split(';').map(Number);
18397
- for (var ci = 0; ci < codes.length; ci++) {
18398
- var c = codes[ci];
18399
- if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }
18400
- else if (c === 1) bold = true;
18401
- else if (c === 2) dim = true;
18402
- else if (c === 3) italic = true;
18403
- else if (c === 4) underline = true;
18404
- else if (c === 22) { bold = false; dim = false; }
18405
- else if (c === 23) italic = false;
18406
- else if (c === 24) underline = false;
18407
- else if (c >= 30 && c <= 37) fg = c - 30;
18408
- else if (c === 39) fg = -1;
18409
- else if (c >= 90 && c <= 97) fg = c - 90 + 8;
18410
- }
18411
- } else {
18412
- // Text segment \u2014 strip any remaining non-printable ESC sequences
18413
- var safe = escHtml(part).replace(/\x1B\\[[^m]*[A-Za-z]/g, '');
18414
- if (safe === '') continue;
18415
- var cls = '';
18416
- if (bold) cls += ' ab';
18417
- if (dim) cls += ' ad';
18418
- if (italic) cls += ' ai';
18419
- if (underline) cls += ' au';
18420
- if (fg >= 0 && fg <= 15) cls += ' f' + fg;
18421
- cls = cls.trim();
18422
- out += cls ? '<span class="' + cls + '">' + safe + '</span>' : safe;
18423
- }
18424
- }
18425
- return out;
18426
- }
18427
-
18428
- // ============================================================
18429
- // App state
18430
- // ============================================================
18431
-
18432
- var sessions = [];
18433
- var activeId = null;
18434
- var outputLines = [];
18435
- var nextCursor = 0;
18436
- var pollTimer = null;
18437
- var pollActive = false;
18438
-
18439
- function setStatus(msg) {
18440
- document.getElementById('statusbar').textContent = msg;
18441
- }
18442
-
18443
- // ============================================================
18444
- // Session list
18445
- // ============================================================
18446
-
18447
- function badgeClass(status) {
18448
- if (status === 'running') return 'br';
18449
- if (status === 'starting') return 'bs';
18450
- if (status === 'killed') return 'bk';
18451
- if (status === 'error') return 'berr';
18452
- return 'be'; // exited
18453
- }
18454
-
18455
- // Derive a turn-aware display badge. A session's 'status' only tracks process
18456
- // liveness, so an agent-cli process stays "running" while idle between turns.
18457
- // Read 'busy' / 'awaitingInput' to show real activity instead:
18458
- // working \u2014 a turn is in flight (busy)
18459
- // waiting \u2014 turn ended on awaiting-input (needs a reply)
18460
- // idle \u2014 process alive, no turn running (last turn done)
18461
- function displayBadge(s) {
18462
- if (s.status === 'running' && s.kind === 'agent-cli') {
18463
- if (s.busy) return { label: 'working', cls: 'br' };
18464
- if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };
18465
- return { label: 'idle', cls: 'be' };
18466
- }
18467
- return { label: s.status, cls: badgeClass(s.status) };
18468
- }
18469
-
18470
- function renderSidebar() {
18471
- var el = document.getElementById('session-list');
18472
- if (sessions.length === 0) {
18473
- el.innerHTML = '<div style="padding:12px;color:var(--text2);font-size:11px">No sessions</div>';
18474
- return;
18475
- }
18476
- var html = '';
18477
- for (var i = 0; i < sessions.length; i++) {
18478
- var s = sessions[i];
18479
- var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);
18480
- var db = displayBadge(s);
18481
- var active = s.id === activeId ? ' active' : '';
18482
- // blockedOn (set while the turn waits on a spawned sub-agent or a
18483
- // shell command) rides next to the status badge; waiting-on-user is
18484
- // NOT here \u2014 that's awaitingInput, a different signal.
18485
- var blocked = '';
18486
- if (s.blockedOn === 'subagent') blocked = '<span class="badge bs">&#129513; sous-agent</span>';
18487
- else if (s.blockedOn === 'command') blocked = '<span class="badge bs">&#9203; commande</span>';
18488
- html += '<div class="si' + active + '" onclick="selectSession(\\'' + s.id + '\\')">'
18489
- + '<div class="sn">' + escHtml(label) + '</div>'
18490
- + '<div class="sm">'
18491
- + '<span class="badge ' + db.cls + '">' + db.label + '</span>'
18492
- + blocked
18493
- + '<span>' + escHtml(s.kind || '') + '</span>'
18494
- + '</div></div>';
18495
- }
18496
- el.innerHTML = html;
18497
- }
18498
-
18499
- function loadSessions() {
18500
- // {kind:'all'} is session_list's live-able default \u2014 agent-CLI + terminal/
18501
- // PTY only. kind:"command" rows (a shell-execution log, not a resumable
18502
- // session) are excluded unless includeCommands is explicitly passed.
18503
- return callTool('session_list', {kind: 'all'}).then(function(data) {
18504
- sessions = data.sessions || [];
18505
- renderSidebar();
18506
- setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' \xB7 ' + new Date().toLocaleTimeString());
18507
- }).catch(function(e) {
18508
- setStatus('Error: ' + e.message);
18509
- });
18510
- }
18511
-
18512
- // ============================================================
18513
- // Output panel
18514
- // ============================================================
18515
-
18516
- function renderOutputFull() {
18517
- var el = document.getElementById('output');
18518
- if (outputLines.length === 0) {
18519
- el.innerHTML = '<div id="empty"><div class="ico">&#9889;</div><div>No output yet</div></div>';
18520
- return;
18521
- }
18522
- var html = '';
18523
- for (var i = 0; i < outputLines.length; i++) {
18524
- html += '<div class="line">' + ansiToHtml(outputLines[i]) + '</div>';
18525
- }
18526
- el.innerHTML = html;
18527
- el.scrollTop = el.scrollHeight;
18528
- }
18529
-
18530
- function appendLines(lines) {
18531
- var el = document.getElementById('output');
18532
- // Remove empty-state placeholder if present
18533
- var empty = el.querySelector('#empty');
18534
- if (empty) empty.parentNode.removeChild(empty);
18535
- var frag = document.createDocumentFragment();
18536
- for (var i = 0; i < lines.length; i++) {
18537
- var div = document.createElement('div');
18538
- div.className = 'line';
18539
- div.innerHTML = ansiToHtml(lines[i]);
18540
- frag.appendChild(div);
18541
- }
18542
- el.appendChild(frag);
18543
- el.scrollTop = el.scrollHeight;
18544
- }
18545
-
18546
- function selectSession(id) {
18547
- activeId = id;
18548
- outputLines = [];
18549
- nextCursor = 0;
18550
- renderSidebar();
18551
-
18552
- var s = null;
18553
- for (var i = 0; i < sessions.length; i++) {
18554
- if (sessions[i].id === id) { s = sessions[i]; break; }
18555
- }
18556
-
18557
- var toolbar = document.getElementById('toolbar');
18558
- toolbar.style.display = 'flex';
18559
- document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);
18560
-
18561
- var killBtn = document.getElementById('kill-btn');
18562
- killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');
18563
-
18564
- document.getElementById('output').innerHTML = '<div style="padding:12px;color:var(--text2)">Loading&#8230;</div>';
18565
-
18566
- // Initial load
18567
- callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {
18568
- outputLines = data.lines || [];
18569
- nextCursor = data.nextCursor || 0;
18570
- renderOutputFull();
18571
- }).catch(function(e) {
18572
- document.getElementById('output').innerHTML = '<div class="errmsg">Error: ' + escHtml(e.message) + '</div>';
18573
- });
18574
- }
18575
-
18576
- // ============================================================
18577
- // Kill
18578
- // ============================================================
18579
-
18580
- function doKill() {
18581
- if (!activeId) return;
18582
- var s = null;
18583
- for (var i = 0; i < sessions.length; i++) {
18584
- if (sessions[i].id === activeId) { s = sessions[i]; break; }
18585
- }
18586
- var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';
18587
- callTool(toolName, {sessionId: activeId}).then(function() {
18588
- return doRefresh();
18589
- }).catch(function(e) {
18590
- setStatus('Kill failed: ' + e.message);
18591
- });
18203
+ function makeBuiltinPanelApps(ops) {
18204
+ return [
18205
+ makeSessionsPanelApp({ listSessions: ops.listSessions }),
18206
+ makeAgentsOverviewApp({ listSessions: ops.listSessions }),
18207
+ makeBureauSessionsApp({ listSessions: ops.listSessions }),
18208
+ makeSessionStoryPanelApp({ listSessions: ops.listSessions }),
18209
+ // Live-session widget — resource ui://live_session/view, also bound to
18210
+ // `agent_start` via _meta.ui.resourceUri (agent-tools.ts) so a launch
18211
+ // auto-renders it.
18212
+ makeLiveSessionApp({ httpBaseUrl: ops.httpBaseUrl })
18213
+ ];
18592
18214
  }
18593
-
18594
- // ============================================================
18595
- // Poll
18596
- // ============================================================
18597
-
18598
- function doPoll() {
18599
- if (pollActive) return;
18600
- pollActive = true;
18601
- var p = loadSessions();
18602
- if (activeId) {
18603
- var capturedId = activeId;
18604
- var capturedCursor = nextCursor;
18605
- p = p.then(function() {
18606
- return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});
18607
- }).then(function(data) {
18608
- if (capturedId !== activeId) return; // user switched sessions
18609
- var newLines = data.lines || [];
18610
- if (newLines.length > 0) {
18611
- outputLines = outputLines.concat(newLines);
18612
- if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);
18613
- nextCursor = data.nextCursor || nextCursor;
18614
- appendLines(newLines);
18615
- }
18616
- }).catch(function() {});
18617
- }
18618
- p.then(function() {
18619
- pollActive = false;
18620
- pollTimer = setTimeout(doPoll, 3000);
18621
- }).catch(function() {
18622
- pollActive = false;
18623
- pollTimer = setTimeout(doPoll, 3000);
18215
+ var PANEL_APP_HANDLES = [
18216
+ sessionsPanelApp,
18217
+ agentsOverviewApp,
18218
+ bureauSessionsApp,
18219
+ sessionStoryApp,
18220
+ liveSessionApp
18221
+ ];
18222
+ function builtinPanelCatalogEntries() {
18223
+ const apps = makeBuiltinPanelApps({
18224
+ listSessions: () => [],
18225
+ httpBaseUrl: "http://127.0.0.1:0"
18624
18226
  });
18625
- }
18626
-
18627
- function doRefresh() {
18628
- return loadSessions().then(function() {
18629
- if (activeId) {
18630
- return callTool('agent_output', {sessionId: activeId, lastN: 200}).then(function(data) {
18631
- outputLines = data.lines || [];
18632
- nextCursor = data.nextCursor || 0;
18633
- renderOutputFull();
18634
- });
18635
- }
18227
+ return apps.map((app, i) => {
18228
+ const handle = PANEL_APP_HANDLES[i];
18229
+ const slug = (handle.id ?? app.id).replace(/^@[^/]+\//, "");
18230
+ return {
18231
+ appId: handle.id ?? `@agentproto/${slug}`,
18232
+ name: handle.name ?? app.title,
18233
+ description: handle.description ?? app.description ?? app.title,
18234
+ dir: `packages/apps/src/${slug}`,
18235
+ category: "builtin",
18236
+ installed: true,
18237
+ hasUi: true,
18238
+ hasArtifact: false,
18239
+ hasSkill: false,
18240
+ toolId: app.id,
18241
+ resourceUri: `ui://${app.id}/view`
18242
+ };
18636
18243
  });
18637
18244
  }
18638
-
18639
- // ============================================================
18640
- // Boot
18641
- // ============================================================
18642
-
18643
- initBridge().then(function() {
18644
- return loadSessions();
18645
- }).then(function() {
18646
- pollTimer = setTimeout(doPoll, 3000);
18647
- }).catch(function(e) {
18648
- setStatus('Bridge error: ' + e.message);
18649
- document.getElementById('output').innerHTML = '<div class="errmsg">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';
18650
- });
18651
- </script>
18652
- </body>
18653
- </html>`;
18654
-
18655
- // src/sessions-panel-app.ts
18656
- var sessionsPanelInputSchema = z.object({
18657
- filter: z.enum(["running", "all"]).optional().describe(
18658
- "Which sessions to return. `running` = only alive; `all` = running + recent (default)."
18659
- )
18660
- });
18661
- function makeSessionsPanelApp(ops) {
18662
- return {
18663
- id: "agentproto_sessions",
18664
- title: "Agent Sessions",
18665
- 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.",
18666
- inputSchema: sessionsPanelInputSchema,
18667
- execute: async (input) => ({
18668
- sessions: ops.listSessions(input.filter)
18669
- }),
18670
- html: PANEL_HTML
18671
- };
18672
- }
18673
18245
  function summarizeSession(desc, lines, nowMs) {
18674
18246
  return {
18675
18247
  sessionId: desc.id,
@@ -18708,1164 +18280,6 @@ function registerSummarizeSessionTool(server, ops) {
18708
18280
  }
18709
18281
  );
18710
18282
  }
18711
- var agentsOverviewInputSchema = z.object({
18712
- filter: z.enum(["running", "all"]).optional().describe("`running` = only alive agent sessions; `all` = running + recent (default).")
18713
- });
18714
- function makeAgentsOverviewApp(ops) {
18715
- return {
18716
- id: "agentproto_agents_overview",
18717
- title: "Agents \u2014 vue claire",
18718
- description: "Open the agents overview \u2014 a plain-language card per agent session with one human sentence (what it's doing / last said) and a coarse state (\xE0 traiter / au travail / en attente / termin\xE9). Polls live and asks the server to summarise each session.",
18719
- inputSchema: agentsOverviewInputSchema,
18720
- execute: async (input) => ({ sessions: ops.listSessions(input.filter) }),
18721
- html: AGENTS_OVERVIEW_HTML
18722
- };
18723
- }
18724
- var AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>
18725
- <html lang="fr">
18726
- <head>
18727
- <meta charset="UTF-8">
18728
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
18729
- <title>agents \u2014 vue claire</title>
18730
- <style>
18731
- *{box-sizing:border-box;margin:0;padding:0}
18732
- :root{
18733
- --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
18734
- --text:#e6edf3;--text2:#8b949e;
18735
- --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
18736
- }
18737
- html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
18738
- #app{display:flex;flex-direction:column;height:100%}
18739
- #hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}
18740
- #hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}
18741
- #hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}
18742
- #refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}
18743
- #refresh-btn:hover{color:var(--text);background:var(--bg3)}
18744
- #grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}
18745
- .card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}
18746
- .card.s-todo{border-left-color:var(--purple)}
18747
- .card.s-work{border-left-color:var(--green)}
18748
- .card.s-wait{border-left-color:var(--yellow)}
18749
- .card.s-done{border-left-color:var(--text2)}
18750
- .card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
18751
- .title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
18752
- .state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}
18753
- .state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}
18754
- .state.s-work{background:rgba(63,185,80,.16);color:var(--green)}
18755
- .state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}
18756
- .state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}
18757
- .summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}
18758
- .summary.muted{color:var(--text2);font-style:italic}
18759
- .meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}
18760
- .dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}
18761
- #empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}
18762
- #empty .ico{font-size:30px}
18763
- #statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
18764
- </style>
18765
- </head>
18766
- <body>
18767
- <div id="app">
18768
- <div id="hdr">
18769
- <div>
18770
- <h1>Agents \u2014 vue claire</h1>
18771
- <div class="sub">une carte par agent \xB7 r\xE9sum\xE9 + \xE9tat</div>
18772
- </div>
18773
- <button id="refresh-btn" title="Rafra\xEEchir" onclick="doRefresh()">&#8635;</button>
18774
- </div>
18775
- <div id="grid"><div id="empty"><div class="ico">&#9889;</div><div>Connexion au bridge&#8230;</div></div></div>
18776
- <div id="statusbar">Connexion&#8230;</div>
18777
- </div>
18778
- <script>
18779
- ${panelBridgeScript("agentproto-agents-overview")}
18780
-
18781
- // \u2500\u2500 State \u2500\u2500
18782
- var REFRESH_MS = 12000;
18783
- var pollTimer = null, polling = false;
18784
- function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
18785
- function setStatus(m){ document.getElementById('statusbar').textContent = m; }
18786
-
18787
- var STATE_CLASS = {
18788
- '\xE0 traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'termin\xE9':'s-done'
18789
- };
18790
- function stateClass(st){ return STATE_CLASS[st] || 's-wait'; }
18791
-
18792
- function fmtAgo(iso){
18793
- if (!iso) return '';
18794
- var t = Date.parse(iso);
18795
- if (isNaN(t)) return '';
18796
- var s = Math.max(0, Math.round((Date.now() - t) / 1000));
18797
- if (s < 60) return 'il y a ' + s + 's';
18798
- var m = Math.round(s/60);
18799
- if (m < 60) return 'il y a ' + m + 'min';
18800
- var h = Math.round(m/60);
18801
- return 'il y a ' + h + 'h';
18802
- }
18803
-
18804
- function titleOf(s){
18805
- return s.label || s.name || (s.command ? s.command.split(/\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);
18806
- }
18807
-
18808
- function render(sessions, summaries){
18809
- var grid = document.getElementById('grid');
18810
- if (!sessions.length){
18811
- grid.innerHTML = '<div id="empty"><div class="ico">&#128564;</div><div>Aucune session d\\'agent</div></div>';
18812
- return;
18813
- }
18814
- var html = '';
18815
- for (var i=0;i<sessions.length;i++){
18816
- var s = sessions[i];
18817
- var sum = summaries[s.id] || {};
18818
- var st = sum.state || 'en attente';
18819
- var cls = stateClass(st);
18820
- var summary = sum.summary || 'R\xE9sum\xE9 indisponible.';
18821
- var muted = !sum.summary ? ' muted' : '';
18822
- html += '<div class="card ' + cls + '">'
18823
- + '<div class="card-top">'
18824
- + '<span class="title">' + esc(titleOf(s)) + '</span>'
18825
- + '<span class="state ' + cls + '">' + esc(st) + '</span>'
18826
- + '</div>'
18827
- + '<div class="summary' + muted + '">' + esc(summary) + '</div>'
18828
- + '<div class="meta">'
18829
- + '<span>' + esc(s.kind || '') + '</span><span class="dot"></span>'
18830
- + '<span>' + esc(s.status || '') + '</span>'
18831
- + (s.lastOutputAt ? '<span class="dot"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')
18832
- + '</div>'
18833
- + '</div>';
18834
- }
18835
- grid.innerHTML = html;
18836
- }
18837
-
18838
- function loadAndRender(){
18839
- return callTool('session_list', {kind:'all'}).then(function(data){
18840
- var all = data.sessions || [];
18841
- var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });
18842
- // Render shells immediately, then fill summaries as they arrive.
18843
- var summaries = {};
18844
- render(agents, summaries);
18845
- setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' \xB7 ' + new Date().toLocaleTimeString('fr-FR'));
18846
- return Promise.all(agents.map(function(s){
18847
- return callTool('summarize_session', {sessionId: s.id}).then(function(sum){
18848
- summaries[s.id] = sum;
18849
- }).catch(function(){ /* leave shell */ });
18850
- })).then(function(){ render(agents, summaries); });
18851
- }).catch(function(e){ setStatus('Erreur : ' + e.message); });
18852
- }
18853
-
18854
- function doPoll(){
18855
- if (polling) return;
18856
- polling = true;
18857
- loadAndRender().then(function(){
18858
- polling = false;
18859
- pollTimer = setTimeout(doPoll, REFRESH_MS);
18860
- }).catch(function(){
18861
- polling = false;
18862
- pollTimer = setTimeout(doPoll, REFRESH_MS);
18863
- });
18864
- }
18865
- function doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }
18866
-
18867
- initBridge().then(loadAndRender).then(function(){
18868
- pollTimer = setTimeout(doPoll, REFRESH_MS);
18869
- }).catch(function(e){
18870
- setStatus('Bridge : ' + e.message);
18871
- document.getElementById('grid').innerHTML = '<div id="empty"><div class="ico">&#9888;</div><div>\xC9chec connexion bridge : ' + esc(e.message) + '</div></div>';
18872
- });
18873
- </script>
18874
- </body>
18875
- </html>`;
18876
- var bureauSessionsInputSchema = z.object({
18877
- filter: z.enum(["running", "all"]).optional().describe("`running` = only alive browser sessions; `all` = running + recent (default).")
18878
- });
18879
- function makeBureauSessionsApp(ops) {
18880
- return {
18881
- id: "agentproto_bureau_sessions",
18882
- title: "Bureau \u2014 sessions navigateur",
18883
- description: "Open the browser-sessions panel \u2014 one row per browser service (adapter, base URL, port, status, uptime). Polls live every ~5 s.",
18884
- inputSchema: bureauSessionsInputSchema,
18885
- execute: async (input) => ({
18886
- sessions: ops.listSessions(input.filter).filter((s) => s.kind === "browser")
18887
- }),
18888
- html: BUREAU_SESSIONS_HTML
18889
- };
18890
- }
18891
- var BUREAU_SESSIONS_HTML = `<!DOCTYPE html>
18892
- <html lang="fr">
18893
- <head>
18894
- <meta charset="UTF-8">
18895
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
18896
- <title>bureau \u2014 sessions navigateur</title>
18897
- <style>
18898
- *{box-sizing:border-box;margin:0;padding:0}
18899
- :root{
18900
- --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
18901
- --text:#e6edf3;--text2:#8b949e;
18902
- --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
18903
- }
18904
- *{box-sizing:border-box}
18905
- html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
18906
- #app{display:flex;flex-direction:column;height:100%}
18907
- #hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}
18908
- #hdr h1{font-size:13px;font-weight:600}
18909
- #hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}
18910
- #refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}
18911
- #refresh-btn:hover{color:var(--text);background:var(--bg3)}
18912
- #list{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
18913
- .row{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:11px 14px;display:flex;align-items:center;gap:14px}
18914
- .icon{font-size:20px;flex-shrink:0}
18915
- .body{flex:1;min-width:0}
18916
- .r1{display:flex;align-items:center;gap:8px}
18917
- .adapter{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
18918
- .url{font-family:Menlo,Monaco,'Courier New',monospace;font-size:11.5px;color:var(--blue);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
18919
- .meta{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
18920
- .dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}
18921
- .badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap}
18922
- .br{background:rgba(63,185,80,.16);color:var(--green)}
18923
- .bs{background:rgba(88,166,255,.16);color:var(--blue)}
18924
- .bk{background:rgba(248,81,73,.14);color:var(--red)}
18925
- .be{background:rgba(139,148,158,.14);color:var(--text2)}
18926
- .berr{background:rgba(248,81,73,.22);color:var(--red)}
18927
- .port{font-family:Menlo,Monaco,'Courier New',monospace}
18928
- #empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}
18929
- #empty .ico{font-size:30px}
18930
- #statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
18931
- </style>
18932
- </head>
18933
- <body>
18934
- <div id="app">
18935
- <div id="hdr">
18936
- <div>
18937
- <h1>Bureau \u2014 sessions navigateur</h1>
18938
- <div class="sub">services navigateur actifs \xB7 URL / port / statut</div>
18939
- </div>
18940
- <button id="refresh-btn" title="Rafra\xEEchir" onclick="doRefresh()">&#8635;</button>
18941
- </div>
18942
- <div id="list"><div id="empty"><div class="ico">&#127760;</div><div>Connexion au bridge&#8230;</div></div></div>
18943
- <div id="statusbar">Connexion&#8230;</div>
18944
- </div>
18945
- <script>
18946
- ${panelBridgeScript("agentproto-bureau-sessions")}
18947
-
18948
- // \u2500\u2500 State \u2500\u2500
18949
- var REFRESH_MS = 5000;
18950
- var pollTimer = null, polling = false;
18951
- function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
18952
- function setStatus(m){ document.getElementById('statusbar').textContent = m; }
18953
-
18954
- function badgeClass(st){
18955
- if (st === 'running') return 'br';
18956
- if (st === 'starting') return 'bs';
18957
- if (st === 'killed') return 'bk';
18958
- if (st === 'error') return 'berr';
18959
- return 'be';
18960
- }
18961
- function fmtUptime(iso, end){
18962
- if (!iso) return '';
18963
- var start = Date.parse(iso);
18964
- if (isNaN(start)) return '';
18965
- var ref = end ? Date.parse(end) : Date.now();
18966
- var s = Math.max(0, Math.round((ref - start) / 1000));
18967
- if (s < 60) return s + 's';
18968
- var m = Math.floor(s/60), rs = s%60;
18969
- if (m < 60) return m + 'm ' + rs + 's';
18970
- var h = Math.floor(m/60), rm = m%60;
18971
- return h + 'h ' + rm + 'm';
18972
- }
18973
-
18974
- function render(rows){
18975
- var list = document.getElementById('list');
18976
- if (!rows.length){
18977
- list.innerHTML = '<div id="empty"><div class="ico">&#127760;</div><div>Aucune session navigateur</div></div>';
18978
- return;
18979
- }
18980
- var html = '';
18981
- for (var i=0;i<rows.length;i++){
18982
- var s = rows[i];
18983
- var bc = badgeClass(s.status);
18984
- var url = s.browserBaseUrl || (s.browserPort ? ('http://127.0.0.1:' + s.browserPort) : '');
18985
- var adapter = s.browserAdapterId || s.label || s.name || 'navigateur';
18986
- var up = fmtUptime(s.startedAt, s.endedAt);
18987
- html += '<div class="row">'
18988
- + '<div class="icon">&#127760;</div>'
18989
- + '<div class="body">'
18990
- + '<div class="r1"><span class="adapter">' + esc(adapter) + '</span>'
18991
- + '<span class="badge ' + bc + '">' + esc(s.status || '') + '</span></div>'
18992
- + (url ? '<div class="url">' + esc(url) + '</div>' : '')
18993
- + '<div class="meta">'
18994
- + (s.browserPort ? '<span>port <span class="port">' + esc(s.browserPort) + '</span></span><span class="dot"></span>' : '')
18995
- + '<span>' + (up ? 'uptime ' + esc(up) : '') + '</span>'
18996
- + '</div>'
18997
- + '</div>'
18998
- + '</div>';
18999
- }
19000
- list.innerHTML = html;
19001
- }
19002
-
19003
- function loadAndRender(){
19004
- return callTool('session_list', {kind:'all'}).then(function(data){
19005
- var all = data.sessions || [];
19006
- var browsers = all.filter(function(s){ return s.kind === 'browser'; });
19007
- render(browsers);
19008
- setStatus(browsers.length + ' session' + (browsers.length===1?'':'s') + ' \xB7 ' + new Date().toLocaleTimeString('fr-FR'));
19009
- }).catch(function(e){ setStatus('Erreur : ' + e.message); });
19010
- }
19011
-
19012
- function doPoll(){
19013
- if (polling) return;
19014
- polling = true;
19015
- loadAndRender().then(function(){
19016
- polling = false;
19017
- pollTimer = setTimeout(doPoll, REFRESH_MS);
19018
- }).catch(function(){
19019
- polling = false;
19020
- pollTimer = setTimeout(doPoll, REFRESH_MS);
19021
- });
19022
- }
19023
- function doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }
19024
-
19025
- initBridge().then(loadAndRender).then(function(){
19026
- pollTimer = setTimeout(doPoll, REFRESH_MS);
19027
- }).catch(function(e){
19028
- setStatus('Bridge : ' + e.message);
19029
- document.getElementById('list').innerHTML = '<div id="empty"><div class="ico">&#9888;</div><div>\xC9chec connexion bridge : ' + esc(e.message) + '</div></div>';
19030
- });
19031
- </script>
19032
- </body>
19033
- </html>`;
19034
-
19035
- // src/session-story-panel.ts
19036
- var SESSION_STORY_PANEL_HTML = `<!doctype html>
19037
- <html lang="fr">
19038
- <head>
19039
- <meta charset="utf-8" />
19040
- <meta name="viewport" content="width=device-width, initial-scale=1" />
19041
- <title>session story</title>
19042
- <style>
19043
- :root {
19044
- color-scheme: light;
19045
- --bg:#faf8f5; --panel:#fffdfa; --line:#ece5da; --line-soft:#f2ede3;
19046
- --ink:#241f1a; --ink-mute:#7d7060; --ink-faint:#a9997f; --ink-ghost:#b3a893;
19047
- --accent:#0d7a4f; --accent-soft:#e8f4ec;
19048
- --gold:#a6701b; --gold-soft:#fff2dc;
19049
- --blue:#1d4e80; --blue-soft:#e9f1fb;
19050
- --violet:#5b3fa6; --violet-soft:#ede9ff;
19051
- --sel:#fbeccd; --red:#b3261e; --red-soft:#fbeae8;
19052
- }
19053
- * { box-sizing:border-box; }
19054
- html,body { height:100%; }
19055
- body { margin:0; font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
19056
- .app { height:100vh; display:flex; flex-direction:column; }
19057
- .hidden { display:none !important; }
19058
-
19059
- /* \u2500\u2500 picker screen \u2500\u2500 */
19060
- #pickerScreen { height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }
19061
- #pickerScreen h1 { font-size:15px; font-weight:700; }
19062
- #pickerList { width:min(520px,90vw); max-height:60vh; overflow-y:auto; border:1px solid var(--line); border-radius:12px; background:var(--panel); }
19063
- .pk-item { padding:10px 14px; border-bottom:1px solid var(--line-soft); cursor:pointer; display:flex; align-items:center; gap:10px; }
19064
- .pk-item:last-child { border-bottom:none; }
19065
- .pk-item:hover { background:var(--line-soft); }
19066
- .pk-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19067
- .pk-meta { flex:none; font-size:10.5px; color:var(--ink-faint); font-weight:600; }
19068
- .pk-empty { padding:24px; text-align:center; color:var(--ink-mute); font-size:12.5px; }
19069
- .badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:10px; font-weight:700; }
19070
- .badge.running { background:var(--accent-soft); color:var(--accent); }
19071
- .badge.starting { background:var(--blue-soft); color:var(--blue); }
19072
- .badge.exited { background:var(--line-soft); color:var(--ink-faint); }
19073
- .badge.killed, .badge.error { background:var(--red-soft); color:var(--red); }
19074
-
19075
- /* \u2500\u2500 big picture : mission + plan de sous-t\xE2ches \u2500\u2500 */
19076
- .hero { flex:none; padding:13px 20px 0; border-bottom:1px solid var(--line); background:var(--panel); }
19077
- .hero-top { display:flex; align-items:center; gap:13px; }
19078
- .pulse { width:10px; height:10px; border-radius:50%; background:var(--accent); flex:none;
19079
- box-shadow:0 0 0 0 rgba(13,122,79,.35); animation:pulse 2.4s infinite; }
19080
- .pulse.off { background:var(--ink-ghost); animation:none; }
19081
- @keyframes pulse { 70% { box-shadow:0 0 0 9px rgba(13,122,79,0); } 100% { box-shadow:0 0 0 0 rgba(13,122,79,0); } }
19082
- .who { min-width:0; flex:1; }
19083
- .who .h1 { font-size:14.5px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19084
- .who .h2 { font-size:12px; color:var(--ink-mute); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19085
- .modewrap { display:flex; border:1px solid var(--line); border-radius:9px; overflow:hidden; flex:none; }
19086
- .modewrap button { border:none; background:var(--panel); color:var(--ink-mute); font-size:11px; font-weight:700; padding:6px 11px; cursor:pointer; }
19087
- .modewrap button.on { background:var(--ink); color:#fdf9f2; }
19088
- button.sim, a.sim { border:1px solid var(--line); background:var(--panel); color:var(--ink-mute); font-weight:700;
19089
- font-size:11.5px; border-radius:8px; padding:6px 12px; cursor:pointer; flex:none; }
19090
- button.sim.on { background:var(--ink); border-color:var(--ink); color:#fdf9f2; }
19091
- a.sim { display:inline-flex; align-items:center; text-decoration:none; }
19092
- a.sim:hover { border-color:var(--ink-ghost); color:var(--ink); }
19093
-
19094
- /* plan strip : les sous-t\xE2ches, l'avancement d'un coup d'\u0153il */
19095
- .plan { display:flex; gap:6px; overflow-x:auto; padding:11px 0 12px; scrollbar-width:none; }
19096
- .plan::-webkit-scrollbar { display:none; }
19097
- .pt { flex:none; display:flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; padding:5px 11px;
19098
- border-radius:999px; border:1px solid var(--line); background:var(--bg); color:var(--ink-mute); cursor:pointer; white-space:nowrap; }
19099
- .pt:hover { border-color:var(--ink-ghost); }
19100
- .pt .st { font-size:10px; }
19101
- .pt.done { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
19102
- .pt.cur { color:var(--gold); background:var(--gold-soft); border-color:transparent; }
19103
- .pt.cur .st { animation:blink 1.6s infinite; }
19104
- @keyframes blink { 50% { opacity:.35; } }
19105
-
19106
- /* \u2500\u2500 corps \u2500\u2500 */
19107
- .body { flex:1; display:flex; min-height:0; }
19108
- .feedcol { flex:1; min-width:320px; display:flex; flex-direction:column; }
19109
- .feed { flex:1; overflow-y:auto; padding:4px 14px 10px; display:flex; flex-direction:column; scroll-behavior:smooth; }
19110
- .fspacer { flex:1; }
19111
-
19112
- /* chapitres */
19113
- .chap { flex:none; position:sticky; top:0; z-index:5; margin:8px -4px 2px; padding:7px 12px; display:flex; align-items:center; gap:9px;
19114
- background:color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter:blur(6px);
19115
- border-radius:9px; cursor:pointer; font-size:11.5px; font-weight:800; letter-spacing:.03em; color:var(--ink-mute); }
19116
- .chap:hover { color:var(--ink); }
19117
- .chap .cst { flex:none; width:17px; height:17px; border-radius:50%; display:grid; place-items:center; font-size:9.5px; font-weight:900; }
19118
- .chap.done .cst { background:var(--accent-soft); color:var(--accent); }
19119
- .chap.cur .cst { background:var(--gold-soft); color:var(--gold); }
19120
- .chap .cnum { color:var(--ink-ghost); font-weight:700; }
19121
- .chap .csum { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19122
- .chap .cmeta { flex:none; font-size:10.5px; color:var(--ink-ghost); font-weight:600; }
19123
- .chap .cchev { flex:none; color:var(--ink-ghost); transition:transform .15s; }
19124
- .chap.open .cchev { transform:rotate(90deg); }
19125
-
19126
- .row { flex:none; min-height:44px; margin:1px 0 1px 10px; padding:5px 12px 5px 10px; display:flex; align-items:center; gap:11px;
19127
- cursor:pointer; border-radius:10px; border-left:3px solid transparent; transition:background .12s; }
19128
- .row:hover { background:var(--line-soft); }
19129
- .row[aria-selected="true"] { background:var(--sel); border-left-color:var(--gold); }
19130
- .row .ico { width:24px; height:24px; border-radius:8px; flex:none; display:grid; place-items:center; font-size:11.5px; font-weight:800; }
19131
- .ico.k-text { background:var(--blue-soft); color:var(--blue); }
19132
- .ico.k-edit { background:var(--gold-soft); color:var(--gold); }
19133
- .ico.k-bash { background:var(--accent-soft); color:var(--accent); }
19134
- .ico.k-read { background:var(--violet-soft); color:var(--violet); }
19135
- .ico.k-user { background:var(--ink); color:#fdf9f2; }
19136
- .row .mid { flex:1; min-width:0; }
19137
- .row .sum { display:block; font-size:13.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19138
- .row .raw1 { display:block; font-size:10.5px; color:var(--ink-faint); font-family:ui-monospace,Menlo,monospace;
19139
- white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }
19140
- body:not(.tech) .row .raw1 { display:none; }
19141
- .row .route { display:inline-block; font-size:10px; font-weight:800; padding:1px 8px; border-radius:999px; margin-top:2px; }
19142
- .route.cont { background:var(--blue-soft); color:var(--blue); }
19143
- .route.newt { background:var(--gold-soft); color:var(--gold); }
19144
- .row .cnt { flex:none; font-size:10px; font-weight:800; color:var(--gold); background:var(--gold-soft); padding:2px 7px; border-radius:999px; }
19145
- .row .ts { flex:none; font-size:10.5px; color:var(--ink-ghost); font-variant-numeric:tabular-nums; }
19146
- @keyframes slidein { from { opacity:0; transform:translateY(6px); } }
19147
- .row.new { animation:slidein .25s ease-out; }
19148
-
19149
- /* \u2500\u2500 panneau ancr\xE9 \u2500\u2500 */
19150
- .panel { flex:none; width:0; overflow:hidden; border-left:1px solid transparent; background:var(--panel);
19151
- display:flex; flex-direction:column; transition:width .22s ease, border-color .22s; }
19152
- .panel.open { width:min(430px,46vw); border-left-color:var(--line); }
19153
- .panel-inner { width:min(430px,46vw); flex:1; display:flex; flex-direction:column; min-height:0; }
19154
- .phead { flex:none; padding:14px 16px 12px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; gap:11px; }
19155
- .phead .ico { width:28px; height:28px; font-size:13px; border-radius:9px; }
19156
- .phead .tt { min-width:0; flex:1; }
19157
- .phead .t { font-size:14px; font-weight:700; line-height:1.4; }
19158
- .phead .s { font-size:11px; color:var(--ink-faint); margin-top:3px; font-variant-numeric:tabular-nums; }
19159
- .pnav { display:flex; gap:4px; flex:none; }
19160
- .pnav button { width:26px; height:26px; border:1px solid var(--line); background:var(--panel); border-radius:8px;
19161
- color:var(--ink-mute); font-size:12px; cursor:pointer; display:grid; place-items:center; }
19162
- .pnav button:disabled { opacity:.3; cursor:default; }
19163
- .pbody { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }
19164
- .plain { font-size:14px; line-height:1.7; }
19165
- .plain .why { margin-top:8px; font-size:12.5px; color:var(--ink-mute); line-height:1.6; }
19166
- .facts { display:flex; flex-wrap:wrap; gap:6px; }
19167
- .fact { font-size:11px; font-weight:700; background:var(--bg); border:1px solid var(--line); color:var(--ink-mute); padding:4px 10px; border-radius:999px; }
19168
- .fact.ok { background:var(--accent-soft); border-color:transparent; color:var(--accent); }
19169
- details.techbox { border:1px solid var(--line); border-radius:12px; background:var(--bg); overflow:hidden; }
19170
- details.techbox summary { list-style:none; cursor:pointer; padding:10px 14px; font-size:11.5px; font-weight:800;
19171
- letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint); display:flex; align-items:center; gap:8px; }
19172
- details.techbox summary::-webkit-details-marker { display:none; }
19173
- details.techbox summary::after { content:"\u25B8"; margin-left:auto; transition:transform .15s; }
19174
- details.techbox[open] summary::after { transform:rotate(90deg); }
19175
- .techlist { padding:0 12px 12px; display:flex; flex-direction:column; gap:8px; }
19176
- .titem { border:1px solid var(--line); border-radius:10px; background:var(--panel); overflow:hidden; }
19177
- .titem .th { padding:8px 12px; font-size:11.5px; font-weight:700; color:var(--ink-mute); display:flex; align-items:center; gap:8px; }
19178
- .titem .th .copy { margin-left:auto; border:none; background:none; color:var(--ink-ghost); font-size:11px; cursor:pointer; padding:2px 4px; border-radius:5px; }
19179
- .titem .th .copy:hover { color:var(--ink); background:var(--line-soft); }
19180
- .titem pre { margin:0; border-top:1px solid var(--line-soft); font-family:ui-monospace,Menlo,monospace; font-size:11.5px;
19181
- line-height:1.55; color:#4a4236; padding:9px 12px; overflow:auto; max-height:240px; white-space:pre-wrap; word-break:break-word; }
19182
- .d-text { font-size:13.5px; line-height:1.7; }
19183
- .d-text p { margin:0 0 8px; }
19184
- .d-text p:last-child { margin-bottom:0; }
19185
- .d-text h1, .d-text h2, .d-text h3, .d-text h4, .d-text h5, .d-text h6 { margin:12px 0 6px; line-height:1.3; }
19186
- .d-text h1:first-child, .d-text h2:first-child, .d-text h3:first-child { margin-top:0; }
19187
- .d-text ul, .d-text ol { margin:0 0 8px; padding-left:20px; }
19188
- .d-text code { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; background:var(--bg); border-radius:4px; padding:1px 5px; }
19189
- .d-text pre { margin:0 0 8px; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:9px 12px; overflow:auto; }
19190
- .d-text pre code { background:none; border-radius:0; padding:0; }
19191
- .d-text table { border-collapse:collapse; margin:0 0 8px; font-size:12.5px; }
19192
- .d-text th, .d-text td { border:1px solid var(--line); padding:4px 8px; text-align:left; }
19193
- .d-text a { color:var(--blue); }
19194
- .pfoot { flex:none; border-top:1px solid var(--line); padding:9px 16px; font-size:11px; color:var(--ink-ghost); display:flex; gap:10px; }
19195
- .kbd { font-family:ui-monospace,Menlo,monospace; font-size:10px; border:1px solid var(--line); border-bottom-width:2px;
19196
- border-radius:5px; padding:1px 5px; background:var(--panel); color:var(--ink-mute); }
19197
-
19198
- /* \u2500\u2500 bo\xEEte d'envoi + routage IA \u2500\u2500 */
19199
- .composer { flex:none; border-top:1px solid var(--line); background:var(--panel); padding:10px 14px; }
19200
- .composer .cbar { display:flex; gap:8px; }
19201
- .composer textarea { flex:1; resize:none; border:1px solid var(--line); border-radius:10px; padding:9px 12px; font:inherit; font-size:13px; max-height:110px; background:var(--bg); }
19202
- .composer textarea:focus { outline:2px solid #241f1a22; }
19203
- .composer textarea:disabled { opacity:.5; cursor:not-allowed; }
19204
- .composer button { border:none; border-radius:10px; padding:0 16px; background:var(--ink); color:#fdf9f2; font-weight:700; font-size:13px; cursor:pointer; }
19205
- .composer button:disabled { opacity:.4; cursor:not-allowed; }
19206
- .composer .routing { font-size:11px; color:var(--ink-faint); padding:6px 2px 0; min-height:22px; }
19207
- .composer .routing .r-cont { color:var(--blue); font-weight:700; }
19208
- .composer .routing .r-newt { color:var(--gold); font-weight:700; }
19209
- #statusbar { flex:none; padding:4px 20px; font-size:10.5px; color:var(--ink-ghost); border-top:1px solid var(--line-soft); }
19210
- </style>
19211
- </head>
19212
- <body>
19213
- <div id="pickerScreen">
19214
- <h1>Choisis une session</h1>
19215
- <div id="pickerList"><div class="pk-empty">Connexion&#8230;</div></div>
19216
- </div>
19217
-
19218
- <div class="app hidden" id="storyScreen">
19219
- <div class="hero">
19220
- <div class="hero-top">
19221
- <span class="pulse" id="pulse"></span>
19222
- <div class="who">
19223
- <div class="h1" id="heroTitle"></div>
19224
- <div class="h2" id="heroSub"></div>
19225
- </div>
19226
- <div class="modewrap"><button id="modeSimple" class="on" type="button">Simple</button><button id="modeTech" type="button">Tech</button></div>
19227
- <a class="sim" id="fullPanelLink" href="#" target="_blank" rel="noopener" title="Ouvrir le panneau complet (Terminal/Chat/JSON/TTY)">&#8599; panneau complet</a>
19228
- <button class="sim" id="switchBtn" type="button">&#8646; changer</button>
19229
- </div>
19230
- <div class="plan" id="plan"></div>
19231
- </div>
19232
-
19233
- <div class="body">
19234
- <div class="feedcol">
19235
- <div class="feed" id="feed"><div class="fspacer"></div><div id="rows"></div></div>
19236
- <div class="composer">
19237
- <div class="cbar">
19238
- <textarea id="msgBox" rows="1" placeholder="\xC9cris \xE0 l'agent\u2026 (la surcouche classe ton message : suite de la sous-t\xE2che ou nouvelle sous-t\xE2che)"></textarea>
19239
- <button id="sendBtn" type="button">Envoyer</button>
19240
- </div>
19241
- <div class="routing" id="routing"></div>
19242
- </div>
19243
- </div>
19244
-
19245
- <aside class="panel" id="panel" aria-label="D\xE9tail de l'\xE9tape">
19246
- <div class="panel-inner">
19247
- <div class="phead">
19248
- <span class="ico" id="pIco"></span>
19249
- <div class="tt"><div class="t" id="pTitle"></div><div class="s" id="pSub"></div></div>
19250
- <div class="pnav"><button id="pPrev" type="button">\u2191</button><button id="pNext" type="button">\u2193</button><button id="pClose" type="button">\u2715</button></div>
19251
- </div>
19252
- <div class="pbody" id="pBody"></div>
19253
- <div class="pfoot"><span class="kbd">\u2191</span><span class="kbd">\u2193</span> naviguer \xB7 <span class="kbd">Esc</span> fermer</div>
19254
- </div>
19255
- </aside>
19256
- </div>
19257
- <div id="statusbar"></div>
19258
- </div>
19259
-
19260
- <script>
19261
- var $=function(id){ return document.getElementById(id); };
19262
- var esc=function(s){ return String(s==null?"":s).replace(/[&<>]/g,function(c){ return {"&":"&amp;","<":"&lt;",">":"&gt;"}[c]; }); };
19263
-
19264
- // ============================================================
19265
- // renderMd \u2014 vanilla-JS port of markdown-lite.ts. Kept
19266
- // function-for-function identical so the two are easy to diff (same
19267
- // self-contained-panel constraint as buildStoryJs below): headers,
19268
- // bold/italic, inline/fenced code, bullet/numbered lists, pipe tables and
19269
- // links, with every raw text run HTML-escaped before any generated tag
19270
- // wraps it.
19271
- // ============================================================
19272
- function escHtmlMd(s){ return String(s).replace(/[&<>"]/g,function(c){ if(c==='&') return '&amp;'; if(c==='<') return '&lt;'; if(c==='>') return '&gt;'; return '&quot;'; }); }
19273
- function renderInlineMd(text){
19274
- var out=escHtmlMd(text);
19275
- out=out.replace(/\`([^\`]+)\`/g,function(_m,code){ return '<code>'+code+'</code>'; });
19276
- out=out.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g,function(_m,label,url){ return '<a href="'+url+'" target="_blank" rel="noopener noreferrer">'+label+'</a>'; });
19277
- out=out.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
19278
- out=out.replace(/(^|[^*])\\*([^*]+)\\*(?!\\*)/g,'$1<em>$2</em>');
19279
- return out;
19280
- }
19281
- function isTableSepMd(line){ return /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)+\\|?\\s*$/.test(line); }
19282
- function splitRowMd(line){ return line.trim().replace(/^\\|/,'').replace(/\\|$/,'').split('|').map(function(c){ return c.trim(); }); }
19283
- function renderMd(md){
19284
- var lines=String(md==null?'':md).replace(/\\r\\n?/g,'\\n').split('\\n');
19285
- var out=[], para=[], list=null;
19286
- function flushPara(){ if(para.length){ out.push('<p>'+para.map(renderInlineMd).join('<br>')+'</p>'); para=[]; } }
19287
- function flushList(){ if(list){ var tag=list.ordered?'ol':'ul'; out.push('<'+tag+'>'+list.items.map(function(i){ return '<li>'+renderInlineMd(i)+'</li>'; }).join('')+'</'+tag+'>'); list=null; } }
19288
- function flushAll(){ flushPara(); flushList(); }
19289
- var i=0;
19290
- while(i<lines.length){
19291
- var line=lines[i];
19292
- if(/^\\s*\`\`\`/.test(line)){
19293
- flushAll();
19294
- var code=[]; i+=1;
19295
- while(i<lines.length && !/^\\s*\`\`\`/.test(lines[i])){ code.push(lines[i]); i+=1; }
19296
- i+=1;
19297
- out.push('<pre><code>'+escHtmlMd(code.join('\\n'))+'</code></pre>');
19298
- continue;
19299
- }
19300
- var header=line.match(/^(#{1,6})\\s+(.*)$/);
19301
- if(header){
19302
- flushAll();
19303
- var level=header[1].length;
19304
- out.push('<h'+level+'>'+renderInlineMd(header[2].trim())+'</h'+level+'>');
19305
- i+=1;
19306
- continue;
19307
- }
19308
- if(/^\\s*\\|/.test(line) && i+1<lines.length && isTableSepMd(lines[i+1])){
19309
- flushAll();
19310
- var headCells=splitRowMd(line);
19311
- i+=2;
19312
- var bodyRows=[];
19313
- while(i<lines.length && /^\\s*\\|/.test(lines[i])){ bodyRows.push(splitRowMd(lines[i])); i+=1; }
19314
- out.push('<table><thead><tr>'+headCells.map(function(c){ return '<th>'+renderInlineMd(c)+'</th>'; }).join('')+'</tr></thead><tbody>'
19315
- +bodyRows.map(function(r){ return '<tr>'+r.map(function(c){ return '<td>'+renderInlineMd(c)+'</td>'; }).join('')+'</tr>'; }).join('')+'</tbody></table>');
19316
- continue;
19317
- }
19318
- var bullet=line.match(/^\\s*[-*+]\\s+(.*)$/);
19319
- var numbered=line.match(/^\\s*\\d+\\.\\s+(.*)$/);
19320
- if(bullet || numbered){
19321
- flushPara();
19322
- var ordered=!!numbered;
19323
- var item=(bullet||numbered)[1];
19324
- if(!list || list.ordered!==ordered){ flushList(); list={ordered:ordered,items:[]}; }
19325
- list.items.push(item);
19326
- i+=1;
19327
- continue;
19328
- }
19329
- if(line.trim()===''){ flushAll(); i+=1; continue; }
19330
- flushList();
19331
- para.push(line);
19332
- i+=1;
19333
- }
19334
- flushAll();
19335
- return out.join('');
19336
- }
19337
-
19338
- ${panelBridgeScript("agentproto-session-story-panel")}
19339
- // Best-effort: some hosts forward the triggering tool call's arguments as
19340
- // a notification so the panel can auto-open the right session. Purely
19341
- // additive \u2014 the session picker is the reliable path when this never
19342
- // arrives.
19343
- var pendingSessionId=null;
19344
- onHostNotification(function(method, params){
19345
- if(/tool-input|tool-call/.test(method)){
19346
- var args=(params && (params.arguments || params.input)) || {};
19347
- if(args && args.sessionId) pendingSessionId=args.sessionId;
19348
- }
19349
- });
19350
-
19351
- // ============================================================
19352
- // buildStory \u2014 vanilla-JS port of session-story.ts. Kept
19353
- // function-for-function identical so the two are easy to diff; the panel
19354
- // resource must be fully self-contained (no bundler/dynamic import), so it
19355
- // cannot import the TS module directly.
19356
- // ============================================================
19357
-
19358
- var SALIENT_KEYS=["file_path","path","filePath","file","command","pattern","query","q","url","todos","description","prompt"];
19359
- function truncateStr(v,max){ var o=String(v).replace(/\\s+/g,' ').trim(); return o.length>max? o.slice(0,max-1)+'\u2026':o; }
19360
- function formatArgValue(v){
19361
- if(typeof v==='string') return v;
19362
- if(Array.isArray(v)) return v.length+' item'+(v.length===1?'':'s');
19363
- if(v && typeof v==='object') return JSON.stringify(v);
19364
- return String(v);
19365
- }
19366
- function pickSalient(args){
19367
- for(var i=0;i<SALIENT_KEYS.length;i++){
19368
- var k=SALIENT_KEYS[i], v=args[k];
19369
- if(v!==undefined && v!==null && v!=='') return formatArgValue(v);
19370
- }
19371
- return null;
19372
- }
19373
- function formatToolCall(name,args){
19374
- name=name||'tool';
19375
- args=(args && typeof args==='object' && !Array.isArray(args)) ? args : {};
19376
- var salient=pickSalient(args);
19377
- if(salient!==null){
19378
- if(name.toLowerCase().indexOf(salient.toLowerCase())>=0) return truncateStr(name,120);
19379
- return truncateStr(name+' '+salient,120);
19380
- }
19381
- if(Object.keys(args).length===0) return name;
19382
- return truncateStr(name+' '+JSON.stringify(args),120);
19383
- }
19384
- function extractText(v){
19385
- if(v==null) return null;
19386
- if(typeof v==='string') return v;
19387
- if(Array.isArray(v)){
19388
- var parts=v.map(extractText).filter(function(x){ return x!=null; });
19389
- return parts.length? parts.join('\\n') : null;
19390
- }
19391
- if(typeof v==='object'){
19392
- if(typeof v.text==='string') return v.text;
19393
- if(typeof v.message==='string') return v.message;
19394
- if(Array.isArray(v.content)) return extractText(v.content);
19395
- if(typeof v.error==='string') return v.error;
19396
- if(v.error && typeof v.error==='object' && typeof v.error.message==='string') return v.error.message;
19397
- return null;
19398
- }
19399
- return null;
19400
- }
19401
- function formatToolResult(toolName,result,isError){
19402
- var text=extractText(result);
19403
- if(isError){
19404
- var message=text!=null? text : (result!=null? JSON.stringify(result) : 'failed');
19405
- var firstLine=String(message).split(/\\r?\\n/)[0] || message;
19406
- return truncateStr(firstLine,160);
19407
- }
19408
- if(text==null) return null;
19409
- var trimmed=text.trim();
19410
- if(!trimmed) return null;
19411
- var lines=trimmed.split(/\\r?\\n/);
19412
- if(lines.length>1){
19413
- var bytes=new TextEncoder().encode(trimmed).length;
19414
- return lines.length+' lines, '+bytes+'B';
19415
- }
19416
- return truncateStr(lines[0],160);
19417
- }
19418
-
19419
- function classifyKind(toolCalls){
19420
- if(!toolCalls || toolCalls.length===0) return 'text';
19421
- var names=toolCalls.map(function(t){ return t.name.toLowerCase(); });
19422
- if(names.some(function(n){ return /edit|write/.test(n); })) return 'edit';
19423
- if(names.some(function(n){ return /bash|terminal|command/.test(n); })) return 'bash';
19424
- if(names.some(function(n){ return /read|grep|glob/.test(n); })) return 'read';
19425
- return 'text';
19426
- }
19427
- var NEW_CHAPTER_RE=/\\b(aussi|autre|ensuite|nouveau|nouvelle|plut[o\xF4]t|maintenant|apr[e\xE8]s \xE7a|il faudrait|peux[- ]tu|on pourrait|ajoute|g[e\xE8]re)\\b/iu;
19428
- function classifyRoute(text){
19429
- var newt=NEW_CHAPTER_RE.test(text);
19430
- if(!newt) return {route:'cont'};
19431
- var title=text.replace(/[.?!].*$/,'').slice(0,42);
19432
- return {route:'newt', title:title};
19433
- }
19434
- function formatTsJs(ts){
19435
- if(ts===undefined || ts===null || isNaN(ts)) return '';
19436
- return new Date(ts).toISOString().slice(11,19);
19437
- }
19438
- function firstMeaningfulLine(text){
19439
- if(!text) return undefined;
19440
- var lines=text.split('\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });
19441
- return lines[0];
19442
- }
19443
- function lineCountOf(text){
19444
- var n=(text||'').split('\\n').filter(function(l){ return l.trim().length>0; }).length;
19445
- return n||1;
19446
- }
19447
- function parseArgsJson(s){ try{ return JSON.parse(s); }catch(e){ return {}; } }
19448
-
19449
- function foldToolStep(assistant,toolResults){
19450
- var toolCalls=assistant.toolCalls||[];
19451
- var kind=classifyKind(toolCalls);
19452
- var count=toolCalls.length||1;
19453
- var items=[], facts=[];
19454
- if(assistant.text && assistant.text.trim()) items.push({text:assistant.text.trim()});
19455
- toolCalls.forEach(function(tc,i){
19456
- var args=parseArgsJson(tc.args);
19457
- var h=formatToolCall(tc.name,args);
19458
- var resultMsg=toolResults[i];
19459
- var resultText=(resultMsg && resultMsg.text) || '';
19460
- var isError=resultText.indexOf('[error]')===0;
19461
- var r=isError? resultText.slice(7).trim() : resultText;
19462
- items.push({h:h,r:r});
19463
- var fact=formatToolResult(tc.name,r,isError);
19464
- if(fact) facts.push(fact);
19465
- });
19466
- var firstLine=firstMeaningfulLine(assistant.text);
19467
- var firstToolCall=toolCalls[0];
19468
- var sum=firstLine!==undefined? firstLine : (firstToolCall? formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args)) : '\u2026');
19469
- var raw1;
19470
- if(toolCalls.length===0) raw1='assistant \xB7 '+lineCountOf(assistant.text)+' ligne(s)';
19471
- else if(toolCalls.length===1) raw1=formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args));
19472
- else raw1=(firstToolCall? firstToolCall.name : 'tool')+' \xD7'+toolCalls.length;
19473
- return {kind:kind, ts:formatTsJs(assistant.ts), sum:sum, raw1:raw1, count:count, facts:facts, items:items};
19474
- }
19475
- function foldUserStep(msg){
19476
- var text=msg.text||'';
19477
- return {kind:'user', ts:formatTsJs(msg.ts), sum:'\xAB '+truncateStr(text,80)+' \xBB', raw1:'user \xB7 '+lineCountOf(text)+' ligne(s)', count:1, facts:[], items:[{text:text}], userText:text};
19478
- }
19479
- function foldOrphanToolStep(msg){
19480
- var text=msg.text||'';
19481
- var isError=text.indexOf('[error]')===0;
19482
- var r=isError? text.slice(7).trim() : text;
19483
- var name=msg.toolName||'tool';
19484
- var fact=formatToolResult(name,r,isError);
19485
- return {kind:classifyKind([{name:name}]), ts:formatTsJs(msg.ts), sum: msg.toolName? (msg.toolName+' \xB7 r\xE9sultat') : "R\xE9sultat d'outil", raw1: msg.toolName||'tool', count:1, facts: fact?[fact]:[], items:[{h:name,r:r}]};
19486
- }
19487
- function foldSystemStep(msg){
19488
- var text=msg.text||'';
19489
- var line=firstMeaningfulLine(text);
19490
- return {kind:'text', ts:formatTsJs(msg.ts), sum: line!==undefined? line : text, raw1:'system', count:1, facts:[], items: text?[{text:text}]:[]};
19491
- }
19492
- function foldMessages(messages){
19493
- var steps=[], i=0;
19494
- while(i<messages.length){
19495
- var msg=messages[i];
19496
- if(msg.role==='user'){ steps.push(foldUserStep(msg)); i+=1; continue; }
19497
- if(msg.role==='assistant'){
19498
- var j=i+1, toolResults=[];
19499
- while(j<messages.length && messages[j].role==='tool'){ toolResults.push(messages[j]); j+=1; }
19500
- steps.push(foldToolStep(msg,toolResults)); i=j; continue;
19501
- }
19502
- if(msg.role==='tool'){ steps.push(foldOrphanToolStep(msg)); i+=1; continue; }
19503
- steps.push(foldSystemStep(msg)); i+=1;
19504
- }
19505
- return steps;
19506
- }
19507
- function buildStoryJs(messages){
19508
- var folded=foldMessages(messages||[]);
19509
- var chapters=[], steps=[];
19510
- var currentChapterId, sawFirstUser=false;
19511
- function closeCurrent(){ var cur=chapters.filter(function(c){ return c.id===currentChapterId; })[0]; if(cur) cur.status='done'; }
19512
- function openChapter(title){ var id='c'+(chapters.length+1); chapters.push({id:id,title:title,status:'cur'}); return id; }
19513
- folded.forEach(function(step){
19514
- var route;
19515
- if(step.kind==='user' && step.userText!==undefined){
19516
- if(!sawFirstUser){ sawFirstUser=true; currentChapterId=openChapter('Cadrage'); }
19517
- else {
19518
- var verdict=classifyRoute(step.userText);
19519
- route=verdict.route;
19520
- if(verdict.route==='newt'){ closeCurrent(); currentChapterId=openChapter(verdict.title||'Nouvelle sous-t\xE2che'); }
19521
- }
19522
- } else if(currentChapterId===undefined){ currentChapterId=openChapter('Cadrage'); }
19523
- var out={chap:currentChapterId, kind:step.kind, ts:step.ts, sum:step.sum, raw1:step.raw1, count:step.count, facts:step.facts, items:step.items};
19524
- if(route) out.route=route;
19525
- steps.push(out);
19526
- });
19527
- return {chapters:chapters, steps:steps};
19528
- }
19529
-
19530
- // ============================================================
19531
- // App state
19532
- // ============================================================
19533
- var sessions=[];
19534
- var activeSessionId=null;
19535
- var story={chapters:[], steps:[]};
19536
- var open={};
19537
- var selected=-1;
19538
- var lastSeenOutputAt=null;
19539
- var pollTimer=null, polling=false;
19540
- var ICONS={text:["k-text","A"],edit:["k-edit","\u270E"],bash:["k-bash","\u25B8"],read:["k-read","\u2315"],user:["k-user","T"]};
19541
- var icoSpec=function(k){ return ICONS[k]||ICONS.text; };
19542
- var chapOf=function(id){ return story.chapters.filter(function(c){ return c.id===id; })[0]; };
19543
- var curChap=function(){ return story.chapters.filter(function(c){ return c.status==='cur'; })[0] || story.chapters[story.chapters.length-1]; };
19544
-
19545
- function setStatus(msg){ $('statusbar').textContent=msg; }
19546
- function nowTs(){ return new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); }
19547
-
19548
- function titleOf(s){
19549
- return s.label || s.name || (s.command? s.command.split(/\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);
19550
- }
19551
-
19552
- // ============================================================
19553
- // Picker screen
19554
- // ============================================================
19555
- function renderPicker(){
19556
- var el=$('pickerList');
19557
- if(!sessions.length){ el.innerHTML='<div class="pk-empty">Aucune session</div>'; return; }
19558
- var html='';
19559
- sessions.forEach(function(s){
19560
- html+='<div class="pk-item" data-id="'+esc(s.id)+'">'
19561
- + '<span class="pk-name">'+esc(titleOf(s))+'</span>'
19562
- + '<span class="badge '+esc(s.status)+'">'+esc(s.status)+'</span>'
19563
- + '<span class="pk-meta">'+esc(s.kind||'')+'</span>'
19564
- + '</div>';
19565
- });
19566
- el.innerHTML=html;
19567
- Array.prototype.forEach.call(el.querySelectorAll('.pk-item'), function(row){
19568
- row.onclick=function(){ openSession(row.getAttribute('data-id')); };
19569
- });
19570
- }
19571
-
19572
- function showPicker(){
19573
- activeSessionId=null;
19574
- $('pickerScreen').classList.remove('hidden');
19575
- $('storyScreen').classList.add('hidden');
19576
- renderPicker();
19577
- }
19578
-
19579
- function openSession(id){
19580
- activeSessionId=id;
19581
- story={chapters:[], steps:[]};
19582
- open={};
19583
- selected=-1;
19584
- lastSeenOutputAt=null;
19585
- $('pickerScreen').classList.add('hidden');
19586
- $('storyScreen').classList.remove('hidden');
19587
- $('fullPanelLink').href='https://cli.agentproto.sh/panel?session='+encodeURIComponent(id);
19588
- closePanel();
19589
- loadStory().then(renderAll);
19590
- }
19591
-
19592
- // ============================================================
19593
- // Story loading
19594
- // ============================================================
19595
- function activeSession(){ return sessions.filter(function(s){ return s.id===activeSessionId; })[0]; }
19596
-
19597
- function loadStory(){
19598
- return callTool('agent_export', {sessionId:activeSessionId, format:'json'}).then(function(data){
19599
- var messages=(data && data.messages) || [];
19600
- story=buildStoryJs(messages);
19601
- // Default open state: only the last (current) chapter is expanded.
19602
- var last=story.chapters[story.chapters.length-1];
19603
- if(last && !(last.id in open)) open[last.id]=true;
19604
- }).catch(function(e){
19605
- setStatus('Erreur export : '+e.message);
19606
- });
19607
- }
19608
-
19609
- function canSend(){
19610
- var s=activeSession();
19611
- return !!s && s.kind==='agent-cli' && s.status==='running';
19612
- }
19613
-
19614
- function renderComposer(){
19615
- var s=activeSession();
19616
- var box=$('msgBox'), btn=$('sendBtn');
19617
- var enabled=canSend();
19618
- box.disabled=!enabled;
19619
- btn.disabled=!enabled;
19620
- if(!s){ box.placeholder='Session introuvable.'; }
19621
- else if(!enabled) box.placeholder='Lecture seule \u2014 session '+esc(s.status)+'.';
19622
- else box.placeholder="\xC9cris \xE0 l'agent\u2026 (la surcouche classe ton message : suite de la sous-t\xE2che ou nouvelle sous-t\xE2che)";
19623
- }
19624
-
19625
- function renderHero(){
19626
- var s=activeSession();
19627
- $('heroTitle').textContent=s? titleOf(s) : (activeSessionId||'');
19628
- var firstUser=story.steps.filter(function(st){ return st.kind==='user'; })[0];
19629
- var mission=firstUser? firstUser.userText || (firstUser.items[0] && firstUser.items[0].text) : null;
19630
- $('heroSub').textContent=mission? truncateStr(mission,200) : 'Aucun message pour le moment.';
19631
- var p=$('pulse');
19632
- p.classList.toggle('off', !(s && (s.status==='running' || s.status==='starting')));
19633
- renderComposer();
19634
- }
19635
-
19636
- function renderAll(){
19637
- renderHero();
19638
- renderPlan();
19639
- renderRows('bottom');
19640
- }
19641
-
19642
- // ============================================================
19643
- // big picture strip
19644
- // ============================================================
19645
- function renderPlan(){
19646
- var done=story.chapters.filter(function(c){ return c.status==='done'; }).length;
19647
- $('plan').innerHTML=story.chapters.map(function(c,i){
19648
- return '<span class="pt '+c.status+'" data-c="'+esc(c.id)+'"><span class="st">'+(c.status==='done'?'\u2713':'\u25CF')+'</span>'+(i+1)+'. '+esc(c.title)+'</span>';
19649
- }).join('') + '<span class="pt" style="cursor:default"><b>'+done+'/'+story.chapters.length+'</b>&nbsp;faites</span>';
19650
- Array.prototype.forEach.call($('plan').querySelectorAll('.pt[data-c]'), function(el){
19651
- el.onclick=function(){ open[el.getAttribute('data-c')]=true; renderRows(); jumpToChap(el.getAttribute('data-c')); };
19652
- });
19653
- }
19654
- function jumpToChap(cid){
19655
- var el=document.querySelector('.chap[data-c="'+cid+'"]');
19656
- if(el) el.scrollIntoView({block:'start',behavior:'smooth'});
19657
- }
19658
-
19659
- // ============================================================
19660
- // feed segment\xE9 par chapitres
19661
- // ============================================================
19662
- function rowHtml(s,i,isNew){
19663
- var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];
19664
- var route=s.route? '<span class="route '+(s.route==='newt'?'newt':'cont')+'">'+(s.route==='newt'?'\u2605 nouvelle sous-t\xE2che':'\u21B3 suite')+'</span>' : '';
19665
- return '<div class="row '+(isNew?'new':'')+'" aria-selected="'+(i===selected)+'" data-i="'+i+'">'
19666
- + '<span class="ico '+cls+'">'+ch+'</span>'
19667
- + '<span class="mid"><span class="sum">'+esc(s.sum)+'</span><span class="raw1">'+esc(s.raw1||'')+'</span>'+route+'</span>'
19668
- + (s.count>1? '<span class="cnt">\xD7'+s.count+'</span>':'') + '<span class="ts">'+esc(s.ts||'')+'</span>'
19669
- + '</div>';
19670
- }
19671
- function renderRows(keepScroll,newIdx){
19672
- var feed=$('feed');
19673
- var prevH=feed.scrollHeight, prevTop=feed.scrollTop;
19674
- var html='';
19675
- story.chapters.forEach(function(c,ci){
19676
- var chapSteps=[];
19677
- story.steps.forEach(function(s,i){ if(s.chap===c.id) chapSteps.push({s:s,i:i}); });
19678
- if(!chapSteps.length) return;
19679
- var isOpen=!!open[c.id];
19680
- html+='<div class="chap '+c.status+' '+(isOpen?'open':'')+'" data-c="'+esc(c.id)+'">'
19681
- + '<span class="cst">'+(c.status==='done'?'\u2713':'\u25CF')+'</span><span class="cnum">'+(ci+1)+'.</span>'
19682
- + '<span class="csum">'+esc(c.title)+'</span>'
19683
- + '<span class="cmeta">'+chapSteps.length+' \xE9tape'+(chapSteps.length>1?'s':'')+'</span><span class="cchev">\u25B8</span>'
19684
- + '</div>';
19685
- if(isOpen) html += chapSteps.map(function(x){ return rowHtml(x.s,x.i,x.i===newIdx); }).join('');
19686
- });
19687
- $('rows').innerHTML=html;
19688
- Array.prototype.forEach.call(document.querySelectorAll('.row'), function(el){
19689
- el.onclick=function(){ selectStep(Number(el.getAttribute('data-i'))); };
19690
- });
19691
- Array.prototype.forEach.call(document.querySelectorAll('.chap'), function(el){
19692
- el.onclick=function(){ var c=el.getAttribute('data-c'); open[c]=!open[c]; renderRows(); };
19693
- });
19694
- if(keepScroll==='bottom') feed.scrollTop=feed.scrollHeight;
19695
- else if(keepScroll==='preserve') feed.scrollTop=feed.scrollHeight-prevH+prevTop;
19696
- }
19697
-
19698
- // ============================================================
19699
- // panneau ancr\xE9
19700
- // ============================================================
19701
- function selectStep(i){
19702
- selected=i;
19703
- var s=story.steps[i]; if(!s) return;
19704
- open[s.chap]=true;
19705
- $('panel').classList.add('open');
19706
- var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];
19707
- var ico=$('pIco'); ico.className='ico '+cls; ico.textContent=ch;
19708
- $('pTitle').textContent=s.sum;
19709
- var c=chapOf(s.chap);
19710
- $('pSub').textContent=(s.ts? s.ts+' \xB7 ':'')+(c? ('sous-t\xE2che : '+c.title) : '');
19711
- var facts=(s.facts||[]).map(function(f){
19712
- return '<span class="fact '+(/\u2713|exit 0|passed|0 match/.test(f)?'ok':'')+'">'+esc(f)+'</span>';
19713
- }).join('');
19714
- var tech=(s.items||[]).map(function(it,k){
19715
- return it.text!==undefined
19716
- ? '<div class="d-text">'+renderMd(it.text)+'</div>'
19717
- : '<div class="titem"><div class="th">'+esc(it.h)+'<button class="copy" data-k="'+k+'" type="button">\u29C9</button></div><pre>'+esc(it.r)+'</pre></div>';
19718
- }).join('');
19719
- $('pBody').innerHTML=''
19720
- + '<div class="plain"><div>'+esc(s.sum)+'.</div><div class="why">'+esc(s.why||'')+'</div></div>'
19721
- + (facts? '<div class="facts">'+facts+'</div>':'')
19722
- + '<details class="techbox" '+(document.body.classList.contains('tech')?'open':'')+'>'
19723
- + '<summary>D\xE9tail technique \xB7 '+(s.items||[]).length+'</summary><div class="techlist">'+tech+'</div>'
19724
- + '</details>';
19725
- Array.prototype.forEach.call($('pBody').querySelectorAll('.copy'), function(btn){
19726
- btn.onclick=function(e){
19727
- e.stopPropagation();
19728
- var it=(s.items||[])[Number(btn.getAttribute('data-k'))];
19729
- var payload=(it.h||'')+'\\n'+(it.r||it.text||'');
19730
- if(navigator.clipboard) navigator.clipboard.writeText(payload).catch(function(){});
19731
- btn.textContent='\u2713'; setTimeout(function(){ btn.textContent='\u29C9'; },900);
19732
- };
19733
- });
19734
- $('pPrev').disabled=i<=0; $('pNext').disabled=i>=story.steps.length-1;
19735
- renderRows();
19736
- var el=document.querySelector('.row[data-i="'+i+'"]');
19737
- if(el) el.scrollIntoView({block:'nearest',behavior:'smooth'});
19738
- }
19739
- function closePanel(){ selected=-1; var p=$('panel'); if(p) p.classList.remove('open'); renderRows(); }
19740
- $('pClose').addEventListener('click',closePanel);
19741
- $('pPrev').addEventListener('click',function(){ if(selected>0) selectStep(selected-1); });
19742
- $('pNext').addEventListener('click',function(){ if(selected<story.steps.length-1) selectStep(selected+1); });
19743
- document.addEventListener('keydown',function(e){
19744
- if(e.key==='Escape'){ closePanel(); return; }
19745
- if($('storyScreen').classList.contains('hidden')) return;
19746
- if(selected<0 || e.target.tagName==='TEXTAREA') return;
19747
- if(e.key==='ArrowUp'){ e.preventDefault(); if(selected>0) selectStep(selected-1); }
19748
- if(e.key==='ArrowDown'){ e.preventDefault(); if(selected<story.steps.length-1) selectStep(selected+1); }
19749
- });
19750
-
19751
- // ============================================================
19752
- // Simple / Tech modes
19753
- // ============================================================
19754
- function setMode(tech){
19755
- document.body.classList.toggle('tech',tech);
19756
- $('modeTech').classList.toggle('on',tech);
19757
- $('modeSimple').classList.toggle('on',!tech);
19758
- if(selected>=0) selectStep(selected);
19759
- }
19760
- $('modeSimple').addEventListener('click',function(){ setMode(false); });
19761
- $('modeTech').addEventListener('click',function(){ setMode(true); });
19762
- $('switchBtn').addEventListener('click', function(){ if(pollTimer) clearTimeout(pollTimer); showPicker(); doPoll(); });
19763
-
19764
- // ============================================================
19765
- // composer \u2014 local chapter-routing classification + agent_prompt
19766
- // ============================================================
19767
- $('sendBtn').addEventListener('click', sendMsg);
19768
- $('msgBox').addEventListener('keydown', function(e){
19769
- if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }
19770
- });
19771
- function sendMsg(){
19772
- if(!canSend()) return;
19773
- var box=$('msgBox'), text=box.value.trim();
19774
- if(!text) return;
19775
- box.value='';
19776
- var cur=curChap();
19777
- var r=cur? classifyRoute(text) : {route:'cont'};
19778
- var chapId=cur? cur.id : null;
19779
- if(r.route==='newt' && cur){
19780
- cur.status='done';
19781
- var id='c'+(story.chapters.length+1);
19782
- story.chapters.push({id:id, title:r.title||'Nouvelle sous-t\xE2che', status:'cur'});
19783
- chapId=id; open[id]=true;
19784
- $('routing').innerHTML='\u2726 class\xE9 : <span class="r-newt">\u2605 nouvelle sous-t\xE2che \xAB '+esc(r.title||'')+' \xBB</span>';
19785
- } else {
19786
- $('routing').innerHTML='\u2726 class\xE9 : <span class="r-cont">\u21B3 suite de \xAB '+esc(cur? cur.title : '')+' \xBB</span>';
19787
- }
19788
- setTimeout(function(){ $('routing').textContent=''; },5000);
19789
- story.steps.push({
19790
- chap:chapId, kind:'user', ts:nowTs(),
19791
- sum:'\xAB '+text.slice(0,80)+(text.length>80?'\u2026':'')+' \xBB',
19792
- raw1:'user \xB7 '+text.split('\\n').length+' ligne(s)',
19793
- route:r.route, facts:[], items:[{text:text}], userText:text,
19794
- });
19795
- renderPlan(); renderRows('bottom', story.steps.length-1);
19796
- callTool('agent_prompt', {sessionId:activeSessionId, prompt:text}).catch(function(e){
19797
- setStatus('Envoi \xE9chou\xE9 : '+e.message);
19798
- });
19799
- }
19800
-
19801
- // ============================================================
19802
- // Poll \u2014 session_list every ~5s; re-fetch agent_export only on turn
19803
- // boundaries (lastOutputAt changed for the active session).
19804
- // ============================================================
19805
- var POLL_MS=5000;
19806
- function loadSessions(){
19807
- return callTool('session_list', {kind:'all'}).then(function(data){
19808
- sessions=data.sessions||[];
19809
- if($('pickerScreen') && !$('pickerScreen').classList.contains('hidden')) renderPicker();
19810
- }).catch(function(e){ setStatus('Erreur : '+e.message); });
19811
- }
19812
- function doPoll(){
19813
- if(polling) return;
19814
- polling=true;
19815
- loadSessions().then(function(){
19816
- if(!activeSessionId){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }
19817
- var s=activeSession();
19818
- if(!s){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }
19819
- renderHero();
19820
- var changed=s.lastOutputAt && s.lastOutputAt!==lastSeenOutputAt;
19821
- if(changed){
19822
- lastSeenOutputAt=s.lastOutputAt;
19823
- loadStory().then(function(){ renderPlan(); renderRows('bottom'); polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });
19824
- } else {
19825
- polling=false; pollTimer=setTimeout(doPoll,POLL_MS);
19826
- }
19827
- }).catch(function(){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });
19828
- }
19829
-
19830
- // ============================================================
19831
- // Boot
19832
- // ============================================================
19833
- initBridge().then(loadSessions).then(function(){
19834
- setTimeout(function(){
19835
- var target=pendingSessionId && sessions.some(function(s){ return s.id===pendingSessionId; })
19836
- ? pendingSessionId
19837
- : null;
19838
- if(!target){
19839
- var agentSessions=sessions.filter(function(s){ return s.kind==='agent-cli'; });
19840
- if(agentSessions.length===1) target=agentSessions[0].id;
19841
- }
19842
- if(target) openSession(target); else showPicker();
19843
- pollTimer=setTimeout(doPoll,POLL_MS);
19844
- }, 50);
19845
- }).catch(function(e){
19846
- setStatus('Bridge : '+e.message);
19847
- $('pickerList').innerHTML='<div class="pk-empty">\xC9chec connexion bridge : '+esc(e.message)+'</div>';
19848
- });
19849
- </script>
19850
- </body>
19851
- </html>`;
19852
-
19853
- // src/session-story-panel-app.ts
19854
- var sessionStoryInputSchema = z.object({
19855
- sessionId: z.string().optional().describe(
19856
- "Session id to open directly. When omitted, the panel shows a session picker (built from session_list) and lets the user choose."
19857
- )
19858
- });
19859
- function makeSessionStoryPanelApp(ops) {
19860
- return {
19861
- id: "agentproto_session_story",
19862
- title: "Session Story",
19863
- description: "Open the session story panel \u2014 a readable, per-session timeline for two audiences at once: a plain-language summary of every step for beginners, expandable to raw tool-call detail for technical users. Shows a one-sentence mission, a plan strip of inferred sub-task chapters, a chapter-segmented feed, and a composer to keep driving the session. Polls live data and lets you jump to any step.",
19864
- inputSchema: sessionStoryInputSchema,
19865
- execute: async (input) => input.sessionId ? { sessionId: input.sessionId } : { sessions: ops.listSessions("all") },
19866
- html: SESSION_STORY_PANEL_HTML
19867
- };
19868
- }
19869
18283
  var terminalPanelInputSchema = z.object({
19870
18284
  sessionId: z.string().optional().describe(
19871
18285
  "Attach to an existing PTY session (id or name from terminal_start). Omit to spawn a new one from `argv`."
@@ -20198,819 +18612,6 @@ initBridge().then(function() {
20198
18612
  </body>
20199
18613
  </html>`;
20200
18614
  }
20201
- var liveSessionInputSchema = z.object({
20202
- sessionId: z.string().optional().describe(
20203
- "Attach the widget to an existing session by id or name. Omit to self-discover the newest running session from the tree."
20204
- )
20205
- });
20206
- function makeLiveSessionApp(ops) {
20207
- const httpBaseUrl = ops?.httpBaseUrl ?? "http://127.0.0.1:18790";
20208
- const httpOrigin = new URL(httpBaseUrl).origin;
20209
- return {
20210
- id: "live_session",
20211
- title: "Live Session",
20212
- description: "Open the live session widget \u2014 a two-pane view of a running agent session: a live tree on the left, a streaming timeline (text, tool calls/results, turn-end) on the right. Omit `sessionId` to attach to the newest running session; pass one to attach directly.",
20213
- inputSchema: liveSessionInputSchema,
20214
- execute: async (input) => ({
20215
- sessionId: input.sessionId,
20216
- httpBaseUrl
20217
- }),
20218
- html: (initData) => LIVE_SESSION_HTML(initData),
20219
- csp: { connectDomains: [httpOrigin] }
20220
- };
20221
- }
20222
- function LIVE_SESSION_HTML(initData) {
20223
- return `<!DOCTYPE html>
20224
- <html lang="en">
20225
- <head>
20226
- <meta charset="UTF-8">
20227
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
20228
- <title>agentproto live session</title>
20229
- <style>
20230
- *{box-sizing:border-box;margin:0;padding:0}
20231
- :root{
20232
- --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
20233
- --text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
20234
- --green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
20235
- }
20236
- 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}
20237
- #app{display:flex;height:100%}
20238
- #tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}
20239
- #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}
20240
- #tree-body{flex:1;overflow-y:auto;padding:6px}
20241
- #timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}
20242
- #timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}
20243
- #timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}
20244
- #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}
20245
- #head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}
20246
- #head-summary .sdot.running{background:var(--green)}
20247
- #head-summary .sdot.grey{background:var(--text3)}
20248
- #head-summary .sdot.error{background:var(--red)}
20249
- #usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}
20250
- #status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}
20251
- #timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}
20252
- #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}
20253
- #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)}
20254
- #new-pill.show{display:block}
20255
-
20256
- /* WP3 compact mode \u2014 colour rail + tighter rows; only under body.compact-mode
20257
- (displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */
20258
- body.compact-mode #tree-pane{display:none}
20259
- body.compact-mode #head-selector{display:inline-block}
20260
- body.compact-mode #focus-id-label{display:none}
20261
- body.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}
20262
- body.compact-mode .row.text{border-left-color:var(--blue)}
20263
- body.compact-mode .row.tool-call{border-left-color:var(--yellow)}
20264
- body.compact-mode .row.turn-end{border-left-color:var(--purple)}
20265
- body.compact-mode .row .body{font-size:12px;margin-top:3px}
20266
- body.compact-mode .row .rhead{font-size:10px}
20267
- details.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}
20268
- details.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}
20269
- details.tool-group[open]>summary{margin-bottom:6px}
20270
- details.tool-group .row{margin-top:5px}
20271
-
20272
- .tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}
20273
- .tnode:hover{background:var(--bg3)}
20274
- .tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}
20275
- .tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
20276
- .dot.running{background:var(--green)}
20277
- .dot.grey{background:var(--text3)}
20278
- .dot.error{background:var(--red)}
20279
- .tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}
20280
- .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}
20281
- .tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}
20282
- #tree-empty{padding:12px;color:var(--text3);font-size:12px}
20283
-
20284
- .row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}
20285
- .row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}
20286
- .row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}
20287
- .row.text .kind{color:var(--blue)}
20288
- .row.tool-call .kind{color:var(--yellow)}
20289
- .row.turn-end .kind{color:var(--purple)}
20290
- .row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
20291
- .row.text .body{font-family:inherit}
20292
- .row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}
20293
- .status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}
20294
- .status-badge.pending{background:var(--bg3);color:var(--text2)}
20295
- .status-badge.ok{background:#0f3d20;color:var(--green)}
20296
- .status-badge.error{background:#3d1418;color:var(--red)}
20297
- .row details{margin-top:6px}
20298
- .row summary{cursor:pointer;font-size:11px;color:var(--text2)}
20299
- .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}
20300
- .chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}
20301
- #timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}
20302
- </style>
20303
- </head>
20304
- <body>
20305
- <div id="app">
20306
- <div id="tree-pane">
20307
- <div id="tree-head">Sessions</div>
20308
- <div id="tree-body"><div id="tree-empty">Loading\u2026</div></div>
20309
- </div>
20310
- <div id="timeline-pane">
20311
- <div id="timeline-head">
20312
- <span class="focus-id" id="focus-id-label">\u2014</span>
20313
- <select id="head-selector" title="Sessions"></select>
20314
- <span id="head-summary"></span>
20315
- <span id="status-line">connecting\u2026</span>
20316
- </div>
20317
- <div id="timeline-body"><div id="timeline-empty">No session focused yet.</div></div>
20318
- <button id="new-pill" type="button">\u2193 New messages</button>
20319
- </div>
20320
- </div>
20321
- <script>
20322
- window.__APP_INIT__ = ${JSON.stringify(initData)};
20323
-
20324
- ${panelBridgeScript("agentproto-live-session")}
20325
-
20326
- // ============================================================
20327
- // INLINED REDUCER COPY \u2014 hand-kept mirror of live-session-app.logic.ts.
20328
- // Plain JS, same semantics: coalesce consecutive text-delta of the same
20329
- // session (and rejoin an unterminated mid-line fragment split by an
20330
- // interleaved record \u2014 see the TS module's text-delta arm), pair
20331
- // tool-call/tool-result by toolCallId, pass through turn-end, keep usage
20332
- // as STATE (SPEC \xA71: usage leaves the timeline \u2014 a usage_update record
20333
- // never produces a row), ignore unknown kinds. Keep in sync with the TS
20334
- // module; the TS module is the one the test suite imports.
20335
- // ============================================================
20336
-
20337
- function initialTimelineState() {
20338
- return { rows: [], usage: null };
20339
- }
20340
-
20341
- function rowId(record, rows) {
20342
- return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);
20343
- }
20344
-
20345
- // Fold a text-delta record into an existing row (fresh object), keeping the
20346
- // row's "partial" hint in step with the latest record \u2014 see mergeTextDelta in
20347
- // the TS module.
20348
- function mergeTextDelta(row, record) {
20349
- var merged = Object.assign({}, row, {
20350
- text: row.text + (record.text || ''),
20351
- seq: record.seq,
20352
- ts: record.ts,
20353
- });
20354
- if (record.partial === true) merged.partial = true;
20355
- else delete merged.partial;
20356
- return merged;
20357
- }
20358
-
20359
- function reduceEvent(state, record) {
20360
- switch (record.kind) {
20361
- case 'text-delta': {
20362
- var last = state.rows[state.rows.length - 1];
20363
- if (last && last.kind === 'text' && last.sessionId === record.sessionId) {
20364
- return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };
20365
- }
20366
- // Debounce can flush an unterminated mid-word fragment (flagged
20367
- // partial), let a tool-call land, then flush the continuation \u2014 look
20368
- // back within the same turn (bounded by this session's last turn-end)
20369
- // for that session's most recent text row and continue it in place.
20370
- // Only the explicit partial flag glues: a non-partial record with no
20371
- // trailing newline is the writer's normal end-of-text-block shape.
20372
- for (var i = state.rows.length - 1; i >= 0; i--) {
20373
- var prior = state.rows[i];
20374
- if (prior.sessionId !== record.sessionId) continue;
20375
- if (prior.kind === 'turn-end') break;
20376
- if (prior.kind !== 'text') continue;
20377
- if (prior.partial === true) {
20378
- var patched = state.rows.slice();
20379
- patched[i] = mergeTextDelta(prior, record);
20380
- return { rows: patched, usage: state.usage };
20381
- }
20382
- break;
20383
- }
20384
- var row = {
20385
- kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
20386
- sessionId: record.sessionId, text: record.text || '',
20387
- };
20388
- if (record.partial === true) row.partial = true;
20389
- return { rows: state.rows.concat([row]), usage: state.usage };
20390
- }
20391
- case 'tool-call': {
20392
- var row = {
20393
- kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
20394
- sessionId: record.sessionId, toolCallId: record.toolCallId || '',
20395
- toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',
20396
- };
20397
- return { rows: state.rows.concat([row]), usage: state.usage };
20398
- }
20399
- case 'tool-result': {
20400
- var idx = -1;
20401
- for (var i = 0; i < state.rows.length; i++) {
20402
- if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;
20403
- }
20404
- if (idx === -1) {
20405
- var row = {
20406
- kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
20407
- sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',
20408
- status: record.isError ? 'error' : 'ok', result: record.result,
20409
- };
20410
- return { rows: state.rows.concat([row]), usage: state.usage };
20411
- }
20412
- var updated = Object.assign({}, state.rows[idx], {
20413
- status: record.isError ? 'error' : 'ok', result: record.result,
20414
- });
20415
- var rows = state.rows.slice();
20416
- rows[idx] = updated;
20417
- return { rows: rows, usage: state.usage };
20418
- }
20419
- case 'turn-end': {
20420
- var row = {
20421
- kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
20422
- sessionId: record.sessionId, reason: record.reason,
20423
- };
20424
- return { rows: state.rows.concat([row]), usage: state.usage };
20425
- }
20426
- case 'usage_update': {
20427
- // Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
20428
- // the prior snapshot. state.rows is reused as-is (it didn't change).
20429
- return {
20430
- rows: state.rows,
20431
- usage: {
20432
- size: record.size, used: record.used, cost: record.cost,
20433
- tokensIn: record.tokensIn, tokensOut: record.tokensOut,
20434
- seq: record.seq, ts: record.ts,
20435
- },
20436
- };
20437
- }
20438
- default:
20439
- return state;
20440
- }
20441
- }
20442
-
20443
- // ============================================================
20444
- // INLINED PURE HELPERS \u2014 exact copies of live-session-app.logic.ts's
20445
- // isNearBottom (SPEC \xA72) and groupAdjacentToolCalls (SPEC \xA73). Same names,
20446
- // same signatures, same default thresholds; plain JS because the widget
20447
- // has no import step.
20448
- // ============================================================
20449
-
20450
- var SCROLL_STICK_THRESHOLD_PX = 24;
20451
-
20452
- function isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {
20453
- if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;
20454
- return scrollHeight - scrollTop - clientHeight <= threshold;
20455
- }
20456
-
20457
- var TOOL_CALL_GROUP_THRESHOLD = 2;
20458
-
20459
- // Collapse runs of \`threshold\`+ adjacent tool-call rows into one
20460
- // {kind:'tool-group', rows:[...]} entry; everything else passes through as
20461
- // individual {kind:'row', row} entries, same order as the input.
20462
- function groupAdjacentToolCalls(rows, threshold) {
20463
- if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;
20464
- var out = [];
20465
- var run = [];
20466
- function flushRun() {
20467
- if (!run.length) return;
20468
- if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });
20469
- else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });
20470
- run = [];
20471
- }
20472
- for (var i = 0; i < rows.length; i++) {
20473
- if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }
20474
- flushRun();
20475
- out.push({ kind: 'row', row: rows[i] });
20476
- }
20477
- flushRun();
20478
- return out;
20479
- }
20480
-
20481
- // ============================================================
20482
- // Rendering helpers
20483
- // ============================================================
20484
-
20485
- function escHtml(s) {
20486
- return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
20487
- }
20488
-
20489
- function safeJson(v) {
20490
- try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
20491
- }
20492
-
20493
- function statusDotClass(status) {
20494
- if (status === 'running' || status === 'starting') return 'running';
20495
- if (status === 'error' || status === 'killed') return 'error';
20496
- return 'grey';
20497
- }
20498
-
20499
- // SPEC \xA74 (non-contract guidance): 52524 \u2192 "52.5k", >=1e6 \u2192 "M" suffix,
20500
- // one decimal, trailing .0 vanishes because the value stays a number.
20501
- // cost \u2192 "$" + toFixed(2). Read from timelineState.usage.
20502
- function fmtCompactNum(n) {
20503
- if (typeof n !== 'number' || !isFinite(n)) return null;
20504
- if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';
20505
- if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';
20506
- return String(n);
20507
- }
20508
-
20509
- function usageChipText(usage) {
20510
- if (!usage) return '';
20511
- var bits = [];
20512
- if (usage.used != null && usage.size != null) {
20513
- bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));
20514
- } else if (usage.used != null) {
20515
- bits.push(fmtCompactNum(usage.used));
20516
- }
20517
- if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));
20518
- return bits.join(' \xB7 ');
20519
- }
20520
-
20521
- // WP5: elapsed seconds since the timeline's first row (running only).
20522
- function elapsedText(rows, status) {
20523
- if (status !== 'running' && status !== 'starting') return null;
20524
- if (!rows.length || rows[0].ts == null) return null;
20525
- var ms = new Date(rows[0].ts).getTime();
20526
- if (!isFinite(ms)) return null;
20527
- return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';
20528
- }
20529
-
20530
- // ============================================================
20531
- // LEFT pane \u2014 live tree
20532
- // ============================================================
20533
-
20534
- var currentTree = [];
20535
- var focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;
20536
- var treeTimer = null;
20537
-
20538
- function findNode(nodes, id) {
20539
- for (var i = 0; i < nodes.length; i++) {
20540
- if (nodes[i].id === id) return nodes[i];
20541
- var found = findNode(nodes[i].children || [], id);
20542
- if (found) return found;
20543
- }
20544
- return null;
20545
- }
20546
-
20547
- function flattenDfs(nodes, out) {
20548
- out = out || [];
20549
- for (var i = 0; i < nodes.length; i++) {
20550
- out.push(nodes[i]);
20551
- flattenDfs(nodes[i].children || [], out);
20552
- }
20553
- return out;
20554
- }
20555
-
20556
- function pickInitialFocus(tree) {
20557
- var flat = flattenDfs(tree);
20558
- var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });
20559
- if (alive.length) return alive[alive.length - 1].id;
20560
- return tree.length ? tree[0].id : null;
20561
- }
20562
-
20563
- function renderTreeNode(node, depth) {
20564
- var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');
20565
- var badge = node.isOrchestrator ? '<span class="badge">orch</span>' : '';
20566
- var cls = 'tnode' + (node.id === focusId ? ' focus' : '');
20567
- return '<div class="' + cls + '" data-id="' + escHtml(node.id) + '">' +
20568
- '<span class="dot ' + statusDotClass(node.status) + '"></span>' +
20569
- '<span class="label">' + escHtml(node.label || node.id) + '</span>' + badge +
20570
- '</div>' +
20571
- (childrenHtml ? '<div class="tchildren">' + childrenHtml + '</div>' : '');
20572
- }
20573
-
20574
- // WP4: compact header <select> mirrors the tree; still routes through
20575
- // setFocus() \u2014 the only session-switch entry point.
20576
- function renderHeadSelector() {
20577
- var sel = document.getElementById('head-selector');
20578
- var flat = flattenDfs(currentTree);
20579
- while (sel.firstChild) sel.removeChild(sel.firstChild);
20580
- if (!flat.length) return;
20581
- for (var i = 0; i < flat.length; i++) {
20582
- var n = flat[i];
20583
- var o = document.createElement('option');
20584
- o.value = n.id;
20585
- o.textContent = (n.id === focusId ? '\u25CF ' : '') + (n.label || n.id);
20586
- sel.appendChild(o);
20587
- }
20588
- sel.value = focusId || '';
20589
- }
20590
-
20591
- function renderTree() {
20592
- var body = document.getElementById('tree-body');
20593
- var root = focusId ? findNode(currentTree, focusId) : null;
20594
- var renderNodes = root ? [root] : currentTree;
20595
- if (!renderNodes.length) {
20596
- body.innerHTML = '<div id="tree-empty">No sessions.</div>';
20597
- renderHeadSelector();
20598
- return;
20599
- }
20600
- body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');
20601
- var els = body.querySelectorAll('.tnode');
20602
- els.forEach(function(el) {
20603
- el.addEventListener('click', function() {
20604
- var id = el.getAttribute('data-id');
20605
- if (id === focusId) return;
20606
- setFocus(id);
20607
- });
20608
- });
20609
- renderHeadSelector();
20610
- }
20611
-
20612
- function pollTree() {
20613
- callTool('app_session_tree', {}).then(function(res) {
20614
- currentTree = res.tree || [];
20615
- if (!focusId) {
20616
- focusId = pickInitialFocus(currentTree);
20617
- if (focusId) startTimeline(focusId);
20618
- }
20619
- renderTree();
20620
- updateHeader();
20621
- }).catch(function() {
20622
- // Leave the last-known tree rendered; the next poll may recover.
20623
- });
20624
- }
20625
-
20626
- // ============================================================
20627
- // RIGHT pane \u2014 timeline for focusId
20628
- // ============================================================
20629
-
20630
- var timelineState = initialTimelineState();
20631
- var sincePtr = 0;
20632
- var activeSource = null; // {type:'sse', es} | {type:'poll', timer}
20633
- var compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)
20634
-
20635
- function setStatus(msg) {
20636
- document.getElementById('status-line').textContent = msg;
20637
- }
20638
-
20639
- function isCompact() {
20640
- return (getHostContext() && getHostContext().displayMode === 'inline') ||
20641
- (typeof window !== 'undefined' && window.innerWidth < 640);
20642
- }
20643
-
20644
- // WP3/WP4: apply the compact/expanded split. Only re-renders the timeline
20645
- // when the mode actually flips (scroll + <details> state survive otherwise).
20646
- function applyDisplayMode() {
20647
- var c = isCompact();
20648
- document.body.classList.toggle('compact-mode', c);
20649
- if (c !== compactMode) {
20650
- compactMode = c;
20651
- renderTimelineFull();
20652
- renderTree();
20653
- }
20654
- }
20655
-
20656
- // WP5 + WP2: one header line \u2014 \`\u25CF running \xB7 3 tools \xB7 52.5k/200k \xB7 $0.04 \xB7 12s\`
20657
- // (status dot from the focus node, tool count from the rows, usage from
20658
- // timelineState.usage \u2014 no usage_update row anymore) + transport status.
20659
- function updateHeader() {
20660
- document.getElementById('focus-id-label').textContent = focusId || '\u2014';
20661
- var rows = timelineState.rows || [];
20662
- var usage = timelineState.usage;
20663
- var tools = 0;
20664
- for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;
20665
- var node = focusId ? findNode(currentTree, focusId) : null;
20666
- var st = node ? node.status : null;
20667
- var parts = [];
20668
- if (st) parts.push('<span class="sdot ' + statusDotClass(st) + '"></span>' + escHtml(st));
20669
- if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));
20670
- var chip = usageChipText(usage);
20671
- if (chip) parts.push('<span id="usage-chip" class="chip">' + escHtml(chip) + '</span>');
20672
- var el = elapsedText(rows, st);
20673
- if (el) parts.push(escHtml(el));
20674
- document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' \xB7 ') : '';
20675
- }
20676
-
20677
- function captureDetailsOpenFlags(body) {
20678
- var dets = body.querySelectorAll('details');
20679
- var flags = [];
20680
- for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);
20681
- return flags;
20682
- }
20683
-
20684
- function restoreDetailsOpenFlags(body, flags) {
20685
- var dets = body.querySelectorAll('details');
20686
- for (var i = 0; i < dets.length && i < flags.length; i++) {
20687
- if (flags[i]) dets[i].open = true;
20688
- }
20689
- }
20690
-
20691
- // Full rebuild \u2014 used ONLY for the first paint of a session, a setFocus()
20692
- // session switch (SPEC \xA72: full reset is correct there), a display-mode
20693
- // flip, and as the fallback for shapes the incremental patcher can't patch.
20694
- // A display-mode flip re-renders the SAME content (WP3: "r\xE9actif... sans
20695
- // perdre le scroll ni l'\xE9tat des <details>"), so open <details> and the
20696
- // exact scroll offset (not just the at-bottom decision) survive the rebuild.
20697
- function renderTimelineFull() {
20698
- var body = document.getElementById('timeline-body');
20699
- var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
20700
- var savedScrollTop = body.scrollTop;
20701
- var flags = captureDetailsOpenFlags(body);
20702
- body.innerHTML = buildTimelineHtml();
20703
- restoreDetailsOpenFlags(body, flags);
20704
- body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;
20705
- updateHeader();
20706
- }
20707
-
20708
- function buildTimelineHtml() {
20709
- var rows = timelineState.rows;
20710
- if (!rows.length) return '<div id="timeline-empty">No events yet.</div>';
20711
- if (compactMode) {
20712
- var entries = groupAdjacentToolCalls(rows);
20713
- var html = '';
20714
- for (var i = 0; i < entries.length; i++) {
20715
- var e = entries[i];
20716
- if (e.kind === 'tool-group') {
20717
- html += '<details class="tool-group"><summary>\u25B8 ' + e.rows.length + ' tool calls</summary>' +
20718
- e.rows.map(renderRow).join('') + '</details>';
20719
- } else {
20720
- html += renderRow(e.row);
20721
- }
20722
- }
20723
- return html;
20724
- }
20725
- return rows.map(renderRow).join('');
20726
- }
20727
-
20728
- // \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
20729
- // One record, one minimal DOM mutation. The common case (text-delta merged
20730
- // into the LAST rendered row) patches just that row's text node. Anything
20731
- // else appends via insertAdjacentHTML. Only rare in-place replacements
20732
- // (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.
20733
- function applyRecordToDom(prevRows) {
20734
- var body = document.getElementById('timeline-body');
20735
- // Capture "stuck to bottom?" BEFORE any mutation (SPEC \xA72).
20736
- var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
20737
- var rows = timelineState.rows;
20738
-
20739
- if (compactMode) {
20740
- // Inline mode: smaller rows \u2014 full grouped rebuild, preserving
20741
- // wasAtBottom/scrollTop and <details> open-state. Only touch the DOM
20742
- // (and the scroll/pill decision) when the rows actually changed \u2014 a
20743
- // usage_update leaves rows === prevRows, so it must fall through to the
20744
- // header-only path below, same as the dense-mode branch.
20745
- if (rows !== prevRows) {
20746
- var flags = captureDetailsOpenFlags(body);
20747
- body.innerHTML = buildTimelineHtml();
20748
- restoreDetailsOpenFlags(body, flags);
20749
- if (wasAtBottom) body.scrollTop = body.scrollHeight;
20750
- else showNewPill();
20751
- }
20752
- updateHeader();
20753
- return;
20754
- }
20755
-
20756
- var appended = rows.length > prevRows.length;
20757
- if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {
20758
- // First real paint of this session (empty placeholder present) or an
20759
- // unexpected shape \u2014 full reset is correct/cheapest.
20760
- body.innerHTML = buildTimelineHtml();
20761
- body.scrollTop = body.scrollHeight;
20762
- updateHeader();
20763
- return;
20764
- }
20765
-
20766
- if (appended) {
20767
- body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));
20768
- } else if (rows.length === prevRows.length && rows.length > 0) {
20769
- // Exactly one row object replaced (tool-result merge, partial-text
20770
- // re-merge via the reducer's lookback). Patch that one element only.
20771
- // Scan from the tail: the hot path (a text-delta merged into the LAST
20772
- // row, the dominant streaming case) is found in O(1) this way instead
20773
- // of walking the whole array \u2014 the rare mid-array lookback re-merge
20774
- // still resolves correctly, just slower.
20775
- var idx = -1;
20776
- for (var i = rows.length - 1; i >= 0; i--) {
20777
- if (rows[i] !== prevRows[i]) { idx = i; break; }
20778
- }
20779
- var patched = false;
20780
- if (idx >= 0) {
20781
- var row = rows[idx];
20782
- var el = body.querySelector('[data-row-id="' + escHtml(row.id) + '"]');
20783
- if (el) {
20784
- if (row.kind === 'text' && idx === rows.length - 1) {
20785
- // High-frequency case: patch the existing text node in place.
20786
- var b = el.querySelector('.body');
20787
- if (b) { b.textContent = row.text; patched = true; }
20788
- }
20789
- if (!patched) { el.outerHTML = renderRow(row); patched = true; }
20790
- }
20791
- }
20792
- if (!patched) {
20793
- // Multiple rows changed at once (shouldn't happen per-record) \u2014 rebuild.
20794
- body.innerHTML = buildTimelineHtml();
20795
- if (wasAtBottom) body.scrollTop = body.scrollHeight;
20796
- updateHeader();
20797
- return;
20798
- }
20799
- } else {
20800
- // rows unchanged (usage_update) \u2014 header only.
20801
- updateHeader();
20802
- return;
20803
- }
20804
-
20805
- // Scroll decision AFTER the mutation: stick or leave the position alone.
20806
- if (wasAtBottom) body.scrollTop = body.scrollHeight;
20807
- else showNewPill();
20808
- updateHeader();
20809
- }
20810
-
20811
- function samePrefix(prevRows, rows) {
20812
- for (var i = 0; i < prevRows.length; i++) {
20813
- if (prevRows[i] !== rows[i]) return false;
20814
- }
20815
- return true;
20816
- }
20817
-
20818
- function showNewPill() {
20819
- document.getElementById('new-pill').classList.add('show');
20820
- }
20821
-
20822
- function hideNewPill() {
20823
- document.getElementById('new-pill').classList.remove('show');
20824
- }
20825
-
20826
- function renderRow(row) {
20827
- if (row.kind === 'text') {
20828
- return '<div class="row text" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">text</span></div>' +
20829
- '<div class="body">' + escHtml(row.text) + '</div></div>';
20830
- }
20831
- if (row.kind === 'tool-call') {
20832
- var badgeCls = row.status === 'pending' ? 'pending' : row.status;
20833
- var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');
20834
- var resultHtml = row.status !== 'pending'
20835
- ? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'
20836
- : '';
20837
- return '<div class="row tool-call" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">tool</span>' +
20838
- '<span class="toolname">' + escHtml(row.toolName) + '</span>' +
20839
- '<span class="status-badge ' + badgeCls + '">' + badgeText + '</span></div>' +
20840
- '<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +
20841
- resultHtml + '</div>';
20842
- }
20843
- if (row.kind === 'turn-end') {
20844
- return '<div class="row turn-end" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">turn end</span>' +
20845
- '<span class="chip">' + escHtml(row.reason || '\u2014') + '</span></div></div>';
20846
- }
20847
- return '';
20848
- }
20849
-
20850
- function teardownTimeline() {
20851
- if (activeSource) {
20852
- if (activeSource.type === 'sse' && activeSource.es) {
20853
- try { activeSource.es.close(); } catch (e) {}
20854
- }
20855
- // Both the poll re-arm timer AND the SSE open/first-message fallback
20856
- // timer must be cleared, or a focus switch within the 2.5s window leaves
20857
- // a stale timer that fires startPolling for the abandoned session.
20858
- if (activeSource.timer) clearTimeout(activeSource.timer);
20859
- if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);
20860
- }
20861
- activeSource = null;
20862
- }
20863
-
20864
- function attemptSSE(id) {
20865
- var settled = false;
20866
- var es;
20867
- try {
20868
- es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');
20869
- } catch (e) {
20870
- startPolling(id);
20871
- return;
20872
- }
20873
- var src = { type: 'sse', es: es, fallbackTimer: null };
20874
- activeSource = src;
20875
- // Every callback below re-checks focusId===id AND that src is still the
20876
- // live source: a focus switch tears down src and starts a new one, so a
20877
- // late open/message/error from this abandoned EventSource must be a no-op
20878
- // rather than mutating the new focus's timeline or clobbering activeSource.
20879
- function stale() { return activeSource !== src || focusId !== id; }
20880
- src.fallbackTimer = setTimeout(function() {
20881
- if (settled || stale()) return;
20882
- settled = true;
20883
- try { es.close(); } catch (e) {}
20884
- startPolling(id);
20885
- }, 2500);
20886
- es.addEventListener('open', function() {
20887
- if (settled || stale()) return;
20888
- settled = true;
20889
- clearTimeout(src.fallbackTimer);
20890
- setStatus('streaming via SSE');
20891
- });
20892
- es.onmessage = function(evt) {
20893
- if (stale()) return;
20894
- if (!settled) {
20895
- settled = true;
20896
- clearTimeout(src.fallbackTimer);
20897
- setStatus('streaming via SSE');
20898
- }
20899
- var rec;
20900
- try { rec = JSON.parse(evt.data); } catch (e) { return; }
20901
- var prev = timelineState;
20902
- timelineState = reduceEvent(timelineState, rec);
20903
- if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;
20904
- if (timelineState !== prev) applyRecordToDom(prev.rows);
20905
- };
20906
- es.onerror = function() {
20907
- if (stale()) return;
20908
- if (!settled) {
20909
- settled = true;
20910
- clearTimeout(src.fallbackTimer);
20911
- try { es.close(); } catch (e) {}
20912
- startPolling(id);
20913
- } else {
20914
- // Do not leave EventSource to perform its native reconnect: this
20915
- // endpoint replays from since=0 when no cursor is provided, so a
20916
- // transparent reconnect would duplicate every row already reduced.
20917
- // The bridge poll resumes from sincePtr instead and preserves the
20918
- // exactly-once cursor contract.
20919
- try { es.close(); } catch (e) {}
20920
- startPolling(id);
20921
- }
20922
- };
20923
- }
20924
-
20925
- function startPolling(id) {
20926
- activeSource = { type: 'poll', timer: null };
20927
- setStatus('polling');
20928
- function tick() {
20929
- if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;
20930
- callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {
20931
- var events = res.events || [];
20932
- for (var i = 0; i < events.length; i++) {
20933
- var prev = timelineState;
20934
- timelineState = reduceEvent(timelineState, events[i]);
20935
- if (timelineState !== prev) applyRecordToDom(prev.rows);
20936
- }
20937
- if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;
20938
- setStatus('polling');
20939
- if (activeSource) activeSource.timer = setTimeout(tick, 1500);
20940
- }).catch(function() {
20941
- setStatus('disconnected');
20942
- if (activeSource) activeSource.timer = setTimeout(tick, 1500);
20943
- });
20944
- }
20945
- tick();
20946
- }
20947
-
20948
- function startTimeline(id) {
20949
- teardownTimeline();
20950
- timelineState = initialTimelineState();
20951
- sincePtr = 0;
20952
- hideNewPill();
20953
- renderTimelineFull();
20954
- setStatus('connecting\u2026');
20955
- attemptSSE(id);
20956
- }
20957
-
20958
- function setFocus(id) {
20959
- focusId = id;
20960
- renderTree();
20961
- startTimeline(id);
20962
- }
20963
-
20964
- // ============================================================
20965
- // Boot
20966
- // ============================================================
20967
-
20968
- initBridge().then(function() {
20969
- var init = window.__APP_INIT__ || {};
20970
- if (init.httpBaseUrl) return init;
20971
- // Static ui:// resources render once at server-registration time with
20972
- // EMPTY initData (mcp-apps-adapter.ts registerMcpApps) \u2014 fall back to
20973
- // calling the tool ourselves over the bridge, same as the other panels.
20974
- return callTool('live_session', {}).then(function(result) {
20975
- window.__APP_INIT__ = result;
20976
- return result;
20977
- });
20978
- }).then(function(init) {
20979
- focusId = init.sessionId || null;
20980
- onHostContext(function() {
20981
- applyDisplayMode();
20982
- updateHeader();
20983
- });
20984
- document.getElementById('head-selector').addEventListener('change', function(evt) {
20985
- var id = evt.target.value;
20986
- if (id && id !== focusId) setFocus(id);
20987
- });
20988
- document.getElementById('new-pill').addEventListener('click', function() {
20989
- var body = document.getElementById('timeline-body');
20990
- body.scrollTop = body.scrollHeight;
20991
- hideNewPill();
20992
- });
20993
- document.getElementById('timeline-body').addEventListener('scroll', function() {
20994
- // User scrolled back near the bottom \u2014 the pill is stale, hide it.
20995
- var el = document.getElementById('timeline-body');
20996
- if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();
20997
- });
20998
- window.addEventListener('resize', function() {
20999
- var c = isCompact();
21000
- if (c !== compactMode) { applyDisplayMode(); }
21001
- });
21002
- applyDisplayMode();
21003
- pollTree();
21004
- treeTimer = setInterval(pollTree, 2000);
21005
- setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh
21006
- if (focusId) startTimeline(focusId);
21007
- }).catch(function(e) {
21008
- setStatus('Bridge error: ' + e.message);
21009
- });
21010
- </script>
21011
- </body>
21012
- </html>`;
21013
- }
21014
18615
  init_transcript_writer();
21015
18616
  function readSessionEventsSince(filePath, since, limit) {
21016
18617
  let raw;
@@ -21496,6 +19097,350 @@ function createAppRegistry(opts) {
21496
19097
  }
21497
19098
  };
21498
19099
  }
19100
+ var AppPathTraversalError = class extends Error {
19101
+ code = "APP_PATH_TRAVERSAL";
19102
+ constructor(relPath) {
19103
+ super(`app-data: path traversal rejected for "${relPath}" \u2014 must resolve inside the app dir.`);
19104
+ this.name = "AppPathTraversalError";
19105
+ }
19106
+ };
19107
+ var DEFAULT_APP_DATA_SUBDIR = "data";
19108
+ function appDataDir(app) {
19109
+ return app.dataDir ?? join(app.dir, DEFAULT_APP_DATA_SUBDIR);
19110
+ }
19111
+ function isDefaultAppDataLayout(app) {
19112
+ return resolve(appDataDir(app)) === resolve(app.dir, DEFAULT_APP_DATA_SUBDIR);
19113
+ }
19114
+ function collapseLegacyDataPrefix(relPath) {
19115
+ const n = normalize(relPath);
19116
+ if (n === DEFAULT_APP_DATA_SUBDIR) return ".";
19117
+ const prefix = DEFAULT_APP_DATA_SUBDIR + sep;
19118
+ if (n.startsWith(prefix)) {
19119
+ const rest = n.slice(prefix.length);
19120
+ return rest === "" ? "." : rest;
19121
+ }
19122
+ return relPath;
19123
+ }
19124
+ function resolveAppDataPath(appDir, relPath) {
19125
+ if (isAbsolute(relPath)) throw new AppPathTraversalError(relPath);
19126
+ if (/^[A-Za-z]:/.test(relPath)) throw new AppPathTraversalError(relPath);
19127
+ const root = resolve(appDir);
19128
+ const target = resolve(appDir, relPath);
19129
+ const rootWithSep = root.endsWith(sep) ? root : root + sep;
19130
+ if (target !== root && !target.startsWith(rootWithSep)) {
19131
+ throw new AppPathTraversalError(relPath);
19132
+ }
19133
+ return target;
19134
+ }
19135
+ async function pathExists(p) {
19136
+ try {
19137
+ await stat(p);
19138
+ return true;
19139
+ } catch {
19140
+ return false;
19141
+ }
19142
+ }
19143
+ async function realpathMaybe(p) {
19144
+ try {
19145
+ return await realpath(p);
19146
+ } catch {
19147
+ return void 0;
19148
+ }
19149
+ }
19150
+ async function resolveAppDataRoots(app, opts) {
19151
+ const dataDir = resolve(appDataDir(app));
19152
+ if (opts?.ensureDataDir) await mkdir(dataDir, { recursive: true });
19153
+ const realData = await realpathMaybe(dataDir);
19154
+ const legacyRoot = await realpathMaybe(app.dir);
19155
+ return {
19156
+ dataRoot: realData ?? dataDir,
19157
+ dataRootExists: realData !== void 0,
19158
+ legacyRoot,
19159
+ defaultLayout: isDefaultAppDataLayout(app)
19160
+ };
19161
+ }
19162
+ async function assertRealInside(root, target) {
19163
+ let real;
19164
+ try {
19165
+ real = await realpath(target);
19166
+ } catch {
19167
+ return;
19168
+ }
19169
+ const rootWithSep = root.endsWith(sep) ? root : root + sep;
19170
+ if (real !== root && !real.startsWith(rootWithSep)) {
19171
+ throw new AppPathTraversalError(target);
19172
+ }
19173
+ }
19174
+ function firstSegment(rel) {
19175
+ const n = normalize(rel);
19176
+ const seg = n.split(sep).find((s) => s !== "" && s !== ".");
19177
+ return seg === void 0 || seg === ".." ? void 0 : seg;
19178
+ }
19179
+ async function locateAppDataPath(roots, relPath) {
19180
+ const rel = roots.defaultLayout ? collapseLegacyDataPrefix(relPath) : relPath;
19181
+ const primary = resolveAppDataPath(roots.dataRoot, rel);
19182
+ await assertRealInside(roots.dataRoot, primary);
19183
+ const primaryExists = await pathExists(primary);
19184
+ let legacyTarget;
19185
+ if (roots.legacyRoot !== void 0) {
19186
+ try {
19187
+ legacyTarget = resolveAppDataPath(roots.legacyRoot, relPath);
19188
+ } catch {
19189
+ legacyTarget = void 0;
19190
+ }
19191
+ if (legacyTarget === primary) legacyTarget = void 0;
19192
+ }
19193
+ let legacyExists = false;
19194
+ if (legacyTarget !== void 0) {
19195
+ await assertRealInside(roots.legacyRoot, legacyTarget);
19196
+ legacyExists = await pathExists(legacyTarget);
19197
+ }
19198
+ if (primaryExists) {
19199
+ return {
19200
+ target: primary,
19201
+ root: roots.dataRoot,
19202
+ legacy: false,
19203
+ ...legacyExists && legacyTarget !== void 0 ? { sibling: legacyTarget } : {}
19204
+ };
19205
+ }
19206
+ if (legacyExists && legacyTarget !== void 0) {
19207
+ return { target: legacyTarget, root: roots.legacyRoot, legacy: true };
19208
+ }
19209
+ const top = firstSegment(relPath);
19210
+ if (top !== void 0 && roots.legacyRoot !== void 0 && legacyTarget !== void 0) {
19211
+ const primaryTop = resolveAppDataPath(roots.dataRoot, roots.defaultLayout ? collapseLegacyDataPrefix(top) : top);
19212
+ const legacyTop = resolveAppDataPath(roots.legacyRoot, top);
19213
+ if (legacyTop !== primaryTop && !await pathExists(primaryTop) && await pathExists(legacyTop)) {
19214
+ return { target: legacyTarget, root: roots.legacyRoot, legacy: true };
19215
+ }
19216
+ }
19217
+ return { target: primary, root: roots.dataRoot, legacy: false };
19218
+ }
19219
+ function textResult(body) {
19220
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
19221
+ }
19222
+ function errorResult(text10) {
19223
+ return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
19224
+ }
19225
+ async function atomicWrite(filePath, data) {
19226
+ await mkdir(dirname(filePath), { recursive: true });
19227
+ const tmp = `${filePath}.tmp.${process.pid}`;
19228
+ await writeFile(tmp, data, "utf8");
19229
+ await rename(tmp, filePath);
19230
+ }
19231
+ async function writeRaw(roots, rel, data) {
19232
+ await atomicWrite((await locateAppDataPath(roots, rel)).target, data);
19233
+ }
19234
+ async function writeJson(roots, rel, value) {
19235
+ await writeRaw(roots, rel, JSON.stringify(value, null, 2) + "\n");
19236
+ }
19237
+ async function readTextMaybe(path) {
19238
+ try {
19239
+ return await readFile(path, "utf8");
19240
+ } catch {
19241
+ return void 0;
19242
+ }
19243
+ }
19244
+ async function readJsonMaybe(path) {
19245
+ const raw = await readTextMaybe(path);
19246
+ if (raw === void 0) return void 0;
19247
+ try {
19248
+ return JSON.parse(raw);
19249
+ } catch {
19250
+ return void 0;
19251
+ }
19252
+ }
19253
+ function normalizeJob(raw) {
19254
+ const jobId = raw.jobId ?? raw.id;
19255
+ const out = { ...raw, id: jobId, jobId };
19256
+ if (out.applyUrl === void 0 || out.applyUrl === null) out.applyUrl = raw.url;
19257
+ return out;
19258
+ }
19259
+ async function readDossierJobId(dossierDir) {
19260
+ const parsed = await readJsonMaybe(join(dossierDir, "job.json"));
19261
+ if (!parsed || typeof parsed !== "object") return void 0;
19262
+ const jobId = parsed.jobId ?? parsed.id;
19263
+ return typeof jobId === "string" && jobId.length > 0 ? jobId : void 0;
19264
+ }
19265
+ function registerAppDataTools(server, opts) {
19266
+ const { appRegistry } = opts;
19267
+ server.tool(
19268
+ "app_data_read",
19269
+ "Read an app-scoped data file (app-relative path). JSON paths return the parsed value in `content`; everything else returns the raw text. Paths resolve under the app's data dir (`dataDir`, default `<dir>/data`); under the default layout a leading `data/` is accepted as the legacy spelling, and a file that only exists under the app's source dir (a pre-dataDir install) is still found there. Path traversal outside either root is rejected.",
19270
+ { appId: z.string(), path: z.string().describe("App-relative path under the app's data dir.") },
19271
+ async (input) => {
19272
+ const installed = appRegistry.getApp(input.appId);
19273
+ if (!installed) return errorResult(`app_data_read: no installed app "${input.appId}".`);
19274
+ let target;
19275
+ try {
19276
+ const roots = await resolveAppDataRoots(installed);
19277
+ target = (await locateAppDataPath(roots, input.path)).target;
19278
+ } catch (err) {
19279
+ return errorResult(`app_data_read: ${err instanceof Error ? err.message : String(err)}`);
19280
+ }
19281
+ const raw = await readTextMaybe(target);
19282
+ if (raw === void 0) return textResult({ appId: input.appId, path: input.path, exists: false });
19283
+ if (input.path.endsWith(".json")) {
19284
+ try {
19285
+ return textResult({ appId: input.appId, path: input.path, exists: true, content: JSON.parse(raw) });
19286
+ } catch {
19287
+ return textResult({ appId: input.appId, path: input.path, exists: true, content: raw });
19288
+ }
19289
+ }
19290
+ return textResult({ appId: input.appId, path: input.path, exists: true, content: raw });
19291
+ }
19292
+ );
19293
+ server.tool(
19294
+ "app_data_write",
19295
+ "Write an app-scoped data file (app-relative path), creating parent directories as needed. `.json` paths are JSON-stringified (pretty); other paths write the raw string passed as `content.text` (or a plain string `content`). Atomic write (tmp + rename). New files land under the app's data dir (`dataDir`, default `<dir>/data`); a file (or top-level folder) that already exists under the app's source dir from a pre-dataDir install is updated in place. Path traversal outside either root is rejected.",
19296
+ {
19297
+ appId: z.string(),
19298
+ path: z.string().describe("App-relative path under the app's data dir."),
19299
+ content: z.unknown().describe("JSON value for `.json` paths, or `{ text }` / string for others.")
19300
+ },
19301
+ async (input) => {
19302
+ const installed = appRegistry.getApp(input.appId);
19303
+ if (!installed) return errorResult(`app_data_write: no installed app "${input.appId}".`);
19304
+ let target;
19305
+ try {
19306
+ const roots = await resolveAppDataRoots(installed, { ensureDataDir: true });
19307
+ target = (await locateAppDataPath(roots, input.path)).target;
19308
+ } catch (err) {
19309
+ return errorResult(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
19310
+ }
19311
+ let payload;
19312
+ if (input.path.endsWith(".json")) {
19313
+ payload = JSON.stringify(input.content, null, 2);
19314
+ } else {
19315
+ const raw = input.content;
19316
+ if (typeof raw === "string") payload = raw;
19317
+ else if (raw !== null && typeof raw === "object" && typeof raw.text === "string") {
19318
+ payload = raw.text;
19319
+ } else {
19320
+ payload = JSON.stringify(raw);
19321
+ }
19322
+ }
19323
+ try {
19324
+ await atomicWrite(target, payload);
19325
+ return textResult({ appId: input.appId, path: input.path, size: Buffer.byteLength(payload, "utf8") });
19326
+ } catch (err) {
19327
+ return errorResult(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
19328
+ }
19329
+ }
19330
+ );
19331
+ server.tool(
19332
+ "app_data_list",
19333
+ "List entries (name + type + size) under an app-relative directory (default `.`, the app's data dir). A missing directory returns empty entries, not an error. When the same directory also exists under the app's source dir (a pre-dataDir install) both views are merged, data dir entries winning on name clashes; `.` lists the data dir only (or the source dir while no data dir exists yet). Path traversal outside either root is rejected.",
19334
+ {
19335
+ appId: z.string(),
19336
+ dir: z.string().optional().describe("App-relative directory to list. Defaults to `.`.")
19337
+ },
19338
+ async (input) => {
19339
+ const installed = appRegistry.getApp(input.appId);
19340
+ if (!installed) return errorResult(`app_data_list: no installed app "${input.appId}".`);
19341
+ const relDir = input.dir ?? ".";
19342
+ const dirs = [];
19343
+ try {
19344
+ const roots = await resolveAppDataRoots(installed);
19345
+ const located = await locateAppDataPath(roots, relDir);
19346
+ const isRoot = located.target === roots.dataRoot || located.target === roots.legacyRoot;
19347
+ dirs.push(located.target);
19348
+ if (!isRoot && located.sibling !== void 0) dirs.push(located.sibling);
19349
+ } catch (err) {
19350
+ return errorResult(`app_data_list: ${err instanceof Error ? err.message : String(err)}`);
19351
+ }
19352
+ const seen = /* @__PURE__ */ new Map();
19353
+ for (const target of dirs) {
19354
+ let dirents;
19355
+ try {
19356
+ dirents = await readdir(target, { withFileTypes: true });
19357
+ } catch {
19358
+ continue;
19359
+ }
19360
+ for (const d of dirents) {
19361
+ if (seen.has(d.name)) continue;
19362
+ const isDirectory = d.isDirectory();
19363
+ let size = 0;
19364
+ if (!isDirectory) {
19365
+ try {
19366
+ size = (await stat(join(target, d.name))).size;
19367
+ } catch {
19368
+ size = 0;
19369
+ }
19370
+ }
19371
+ seen.set(d.name, { name: d.name, type: isDirectory ? "directory" : "file", size });
19372
+ }
19373
+ }
19374
+ const entries = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
19375
+ return textResult({ appId: input.appId, dir: relDir, entries });
19376
+ }
19377
+ );
19378
+ server.tool(
19379
+ "app_data_migrate",
19380
+ "One-time import of legacy job-app data into the durable shape under the app's data dir: `jobs/<jobId>.json` (normalized id/jobId/applyUrl), `rankings/latest.json` (full ranked list) + per-job ranking artifacts, `applications/<jobId>/{job.json,cv.json,cover.md}` from matching `dossiers/*` folders, and `state.json`. The legacy inputs (`ranked-jobs.json`, `dossiers/`) are read from wherever they resolve \u2014 the app's source dir for pre-dataDir installs. Idempotent \u2014 re-running after migration returns `alreadyMigrated` unless `force`.",
19381
+ {
19382
+ appId: z.string(),
19383
+ force: z.boolean().optional().describe("Re-run even if already migrated.")
19384
+ },
19385
+ async (input) => {
19386
+ const installed = appRegistry.getApp(input.appId);
19387
+ if (!installed) return errorResult(`app_data_migrate: no installed app "${input.appId}".`);
19388
+ let roots;
19389
+ try {
19390
+ roots = await resolveAppDataRoots(installed, { ensureDataDir: true });
19391
+ } catch (err) {
19392
+ return errorResult(`app_data_migrate: ${err instanceof Error ? err.message : String(err)}`);
19393
+ }
19394
+ const at = async (rel) => (await locateAppDataPath(roots, rel)).target;
19395
+ const stateRel = "state.json";
19396
+ if (!input.force && await readJsonMaybe(await at(stateRel)) !== void 0) {
19397
+ return textResult({ appId: input.appId, migrated: false, alreadyMigrated: true });
19398
+ }
19399
+ let jobs = [];
19400
+ const rankedRaw = await readJsonMaybe(await at("ranked-jobs.json"));
19401
+ if (Array.isArray(rankedRaw)) jobs = rankedRaw;
19402
+ const normalized = jobs.map(normalizeJob).filter((j) => typeof j.jobId === "string" && j.jobId.length > 0);
19403
+ for (const job of normalized) {
19404
+ const id = job.jobId;
19405
+ await writeJson(roots, `jobs/${id}.json`, job);
19406
+ await writeJson(roots, `rankings/${id}.json`, job);
19407
+ }
19408
+ await writeJson(roots, "rankings/latest.json", normalized);
19409
+ let folderNames = [];
19410
+ try {
19411
+ folderNames = (await readdir(await at("dossiers"), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
19412
+ } catch {
19413
+ folderNames = [];
19414
+ }
19415
+ const matched = /* @__PURE__ */ new Set();
19416
+ const skippedFolders = [];
19417
+ for (const name of folderNames) {
19418
+ const dossierDir = await at(join("dossiers", name));
19419
+ const jobId = await readDossierJobId(dossierDir);
19420
+ const targetJob = normalized.find((j) => j.jobId === jobId);
19421
+ if (!jobId || !targetJob) {
19422
+ skippedFolders.push(name);
19423
+ continue;
19424
+ }
19425
+ matched.add(jobId);
19426
+ await writeJson(roots, `applications/${jobId}/job.json`, targetJob);
19427
+ const cv = await readJsonMaybe(join(dossierDir, "cv.json"));
19428
+ if (cv !== void 0) await writeJson(roots, `applications/${jobId}/cv.json`, cv);
19429
+ const cover = await readTextMaybe(join(dossierDir, "cover.md"));
19430
+ if (cover !== void 0) await writeRaw(roots, `applications/${jobId}/cover.md`, cover);
19431
+ }
19432
+ const jobCount = normalized.length;
19433
+ const dossierCount = matched.size;
19434
+ await writeJson(roots, stateRel, {
19435
+ migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
19436
+ jobCount,
19437
+ dossierCount,
19438
+ skippedFolders
19439
+ });
19440
+ return textResult({ appId: input.appId, migrated: true, jobCount, dossierCount, skippedFolders });
19441
+ }
19442
+ );
19443
+ }
21499
19444
  var EMPTY_CATALOG = { apps: [] };
21500
19445
  function defaultAppCatalogPath() {
21501
19446
  return join(homedir(), ".agentproto", "app-catalog.json");
@@ -21549,25 +19494,40 @@ function resolveAgentRefsForWorkflow(appRegistry, workflowId) {
21549
19494
  }
21550
19495
  return refs;
21551
19496
  }
21552
- function textResult(body) {
19497
+ function buildAgentRunSpawnConfig(agent, input) {
19498
+ const model = input.model ?? agent.model;
19499
+ const body = agent.body.trim();
19500
+ const prompt = body ? input.prompt ? `${body}
19501
+
19502
+ ${input.prompt}` : body : input.prompt;
19503
+ return {
19504
+ ...model !== void 0 ? { model } : {},
19505
+ ...prompt !== void 0 ? { prompt } : {}
19506
+ };
19507
+ }
19508
+ async function loadAgentPromptDefaults(agentPath) {
19509
+ const { handle, body } = await loadAgent(agentPath);
19510
+ return { ...typeof handle.model === "string" ? { model: handle.model } : {}, body };
19511
+ }
19512
+ function textResult2(body) {
21553
19513
  return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
21554
19514
  }
21555
- function errorResult(text10) {
19515
+ function errorResult2(text10) {
21556
19516
  return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
21557
19517
  }
21558
19518
  function notEnabled(tool) {
21559
- return errorResult(
19519
+ return errorResult2(
21560
19520
  `${tool} is not enabled \u2014 the daemon was started without an adapter resolver. Re-run the daemon with the \`@agentproto/cli\` shim wired (see playground/scripts/gateway.ts).`
21561
19521
  );
21562
19522
  }
21563
19523
  async function performAppToolCall(appRegistry, input, deps2) {
21564
19524
  const installed = appRegistry.getApp(input.appId);
21565
19525
  if (!installed || !installed.ui) {
21566
- return errorResult(`app_tool_call: app "${input.appId}" is not installed or has no UI.`);
19526
+ return errorResult2(`app_tool_call: app "${input.appId}" is not installed or has no UI.`);
21567
19527
  }
21568
19528
  const allowlist = installed.ui.tools ?? [];
21569
19529
  if (!allowlist.includes(input.tool)) {
21570
- return errorResult(
19530
+ return errorResult2(
21571
19531
  `app_tool_call: tool "${input.tool}" is not in app "${input.appId}"'s ui.tools allowlist: ${allowlist.length > 0 ? allowlist.join(", ") : "(empty)"}`
21572
19532
  );
21573
19533
  }
@@ -21578,18 +19538,18 @@ async function performAppToolCall(appRegistry, input, deps2) {
21578
19538
  const rest = input.tool.slice("imported:".length);
21579
19539
  const slash = rest.indexOf("/");
21580
19540
  if (slash === -1) {
21581
- return errorResult(
19541
+ return errorResult2(
21582
19542
  `app_tool_call: malformed imported tool id "${input.tool}" \u2014 expected "imported:<alias>/<toolName>".`
21583
19543
  );
21584
19544
  }
21585
19545
  const result2 = await deps2.callImportedTool(rest.slice(0, slash), rest.slice(slash + 1), args);
21586
- return textResult(result2);
19546
+ return textResult2(result2);
21587
19547
  }
21588
19548
  if (!deps2.dispatchTool) return notEnabled("app_tool_call");
21589
19549
  const result = await deps2.dispatchTool(input.tool, args);
21590
- return textResult(result);
19550
+ return textResult2(result);
21591
19551
  } catch (err) {
21592
- return errorResult(`app_tool_call: ${err instanceof Error ? err.message : String(err)}`);
19552
+ return errorResult2(`app_tool_call: ${err instanceof Error ? err.message : String(err)}`);
21593
19553
  }
21594
19554
  }
21595
19555
  function refIdOf(ref) {
@@ -21651,7 +19611,19 @@ async function normalizeExternalReadRoots(roots) {
21651
19611
  }
21652
19612
  return { ok: true, roots: normalized };
21653
19613
  }
21654
- async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter) {
19614
+ async function resolveInstallDataDir(input) {
19615
+ const raw = input.explicit ?? input.previous ?? input.hint;
19616
+ const dataDir = raw === void 0 ? resolve(input.dir, DEFAULT_APP_DATA_SUBDIR) : resolve(input.dir, expandHome2(raw));
19617
+ try {
19618
+ const st = await stat(dataDir);
19619
+ if (!st.isDirectory()) {
19620
+ return { ok: false, error: `dataDir "${dataDir}" exists but is not a directory.` };
19621
+ }
19622
+ } catch {
19623
+ }
19624
+ return { ok: true, dataDir };
19625
+ }
19626
+ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, opts) {
21655
19627
  let handle;
21656
19628
  try {
21657
19629
  handle = await loadAppHandle(dir);
@@ -21726,9 +19698,17 @@ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAg
21726
19698
  if (!result.ok) return { ok: false, error: `app_install: ${result.error}` };
21727
19699
  externalReadRoots = result.roots;
21728
19700
  }
19701
+ const dataDirResult = await resolveInstallDataDir({
19702
+ dir,
19703
+ ...opts?.dataDir !== void 0 ? { explicit: opts.dataDir } : {},
19704
+ ...appRegistry.getApp(handle.id)?.dataDir !== void 0 ? { previous: appRegistry.getApp(handle.id).dataDir } : {},
19705
+ ...handle.data?.dir !== void 0 ? { hint: handle.data.dir } : {}
19706
+ });
19707
+ if (!dataDirResult.ok) return { ok: false, error: `app_install: ${dataDirResult.error}` };
21729
19708
  const record2 = appRegistry.upsertApp({
21730
19709
  appId: handle.id,
21731
19710
  dir,
19711
+ dataDir: dataDirResult.dataDir,
21732
19712
  ...handle.version ? { version: handle.version } : {},
21733
19713
  ...handle.name ? { name: handle.name } : {},
21734
19714
  ...handle.description ? { description: handle.description } : {},
@@ -21753,12 +19733,19 @@ function registerAppTools(server, opts) {
21753
19733
  });
21754
19734
  server.tool(
21755
19735
  "app_install",
21756
- "Install an @agentproto/app-kit app from its emitted directory (`<dir>/.agentproto/APP.md` \u2014 see `defineApp().emit(dir)`). Validates every WORKFLOW.md `tool` step's id against the daemon's dispatchable tools (missing ids are reported ALL at once, instead of failing one at a time at STEP-DISPATCH time) and checks the `mastra-agent` adapter resolves. Agent-declared tool refs (workspace tools like `read_file`) are the adapter's own business and are never validated here \u2014 see `unvalidatedAgentTools` on the result. Re-installing the same appId upserts.",
21757
- { dir: z.string().describe("Absolute path to the app's directory.") },
19736
+ "Install an @agentproto/app-kit app from its emitted directory (`<dir>/.agentproto/APP.md` \u2014 see `defineApp().emit(dir)`). Validates every WORKFLOW.md `tool` step's id against the daemon's dispatchable tools (missing ids are reported ALL at once, instead of failing one at a time at STEP-DISPATCH time) and checks the `mastra-agent` adapter resolves. Agent-declared tool refs (workspace tools like `read_file`) are the adapter's own business and are never validated here \u2014 see `unvalidatedAgentTools` on the result. Re-installing the same appId upserts (and keeps its existing `dataDir` unless a new one is passed).",
19737
+ {
19738
+ dir: z.string().describe("Absolute path to the app's directory."),
19739
+ dataDir: z.string().optional().describe(
19740
+ "Where the app's durable data (`app_data_*`) lives. Absolute or `~`-relative; a relative path is taken relative to `dir`. Defaults to the previously installed dataDir, else the APP.md `data.dir` hint, else `<dir>/data`."
19741
+ )
19742
+ },
21758
19743
  async (input) => {
21759
- const result = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
21760
- if (!result.ok) return errorResult(`app_install: ${result.error}`);
21761
- return textResult(result.record);
19744
+ const result = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
19745
+ ...input.dataDir !== void 0 ? { dataDir: input.dataDir } : {}
19746
+ });
19747
+ if (!result.ok) return errorResult2(`app_install: ${result.error}`);
19748
+ return textResult2(result.record);
21762
19749
  }
21763
19750
  );
21764
19751
  server.tool(
@@ -21769,6 +19756,7 @@ function registerAppTools(server, opts) {
21769
19756
  const runs = appRegistry.listRuns();
21770
19757
  const apps = appRegistry.listApps().map((app) => ({
21771
19758
  ...app,
19759
+ dataDir: appDataDir(app),
21772
19760
  runs: runs.filter((r) => r.appId === app.appId).map((r) => ({
21773
19761
  appRunId: r.appRunId,
21774
19762
  status: r.status,
@@ -21780,12 +19768,12 @@ function registerAppTools(server, opts) {
21780
19768
  sessions: r.sessions.length
21781
19769
  }))
21782
19770
  }));
21783
- return textResult(apps);
19771
+ return textResult2(apps);
21784
19772
  }
21785
19773
  );
21786
19774
  server.tool(
21787
19775
  "app_run",
21788
- "Run an installed app's agents as live sessions \u2014 one `agent_start`-equivalent spawn per selected agent (default adapter `mastra-agent`, pointed at that agent's emitted AGENT.md via the adapter's `agent` option), grouped under a fresh appRunId. Re-reads the app's directory first, so a stale install record (paths moved, a workflow renamed) is refreshed before spawning \u2014 the same refreshed paths are what make `workflow_run_file` work against this app's WORKFLOW.md files. Poll with `app_status`, kill with `app_stop`.\n\nOrchestration: pass `sequence` to run agents ONE-AT-A-TIME in the given order (each waits for its predecessor's session to reach a terminal state, bounded ~60\xD72s, before the next spawns) \u2014 the scout\u2192tailor workflow. Without `sequence`, `agents` spawn concurrently (legacy behaviour). When `sequence` is set every agent still lives under the SAME appRunId and is awaited; the run is marked `ended` once the last completes.\n\nRunner selection: `adapter`/`harness`/`model` are passed through to every spawn and mirrored onto the run record for observability. `harness` is the canonical slug and defaults `adapter` to itself when `adapter` is absent; a bare `adapter` sets `harness` to itself; both default to `mastra-agent`. An unresolvable adapter is collected as a per-agent error rather than failing the whole run.",
19776
+ "Run an installed app's agents as live sessions \u2014 one `agent_start`-equivalent spawn per selected agent, grouped under a fresh appRunId. Re-reads the app's directory first, so a stale install record (paths moved, a workflow renamed) is refreshed before spawning \u2014 the same refreshed paths are what make `workflow_run_file` work against this app's WORKFLOW.md files. Poll with `app_status`, kill with `app_stop`.\n\nAdapter support: with the default adapter `mastra-agent` (or any other adapter whose manifest declares an `agent` option), each spawn is pointed straight at the agent's emitted AGENT.md via that option. Any OTHER adapter (`claude-code`, `hermes`, `codex`, ...) declares no such option, so its spawn is built FROM the AGENT.md instead: the frontmatter `model` becomes the spawn's default model (an explicit `model` arg here still wins) and the AGENT.md body becomes the system/prefix of the first prompt (a `prompt` arg is appended after it). `cwd` is still the app's dir, and the daemon's own MCP gateway is still mounted for adapters that get it by default (claude-code, hermes) \u2014 see `shouldInjectDaemonSelfMount` \u2014 so the spawned agent still reaches `app_data_*`/`mcp_imported_call` natively.\n\nOrchestration: pass `sequence` to run agents ONE-AT-A-TIME in the given order (each waits for its predecessor's session to reach a terminal state, bounded ~60\xD72s, before the next spawns) \u2014 the scout\u2192tailor workflow. Without `sequence`, `agents` spawn concurrently (legacy behaviour). When `sequence` is set every agent still lives under the SAME appRunId and is awaited; the run is marked `ended` once the last completes.\n\nRunner selection: `adapter`/`harness`/`model` are passed through to every spawn and mirrored onto the run record for observability. `harness` is the canonical slug and defaults `adapter` to itself when `adapter` is absent; a bare `adapter` sets `harness` to itself; both default to `mastra-agent`. `access.profileRef` pins a named auth profile (see `agent_start.access`) on every spawn this run makes \u2014 needed when an adapter's default credential profile is disabled on this host. An unresolvable adapter is collected as a per-agent error rather than failing the whole run.",
21789
19777
  {
21790
19778
  appId: z.string(),
21791
19779
  agents: z.array(z.string()).optional().describe("Agent ids to run concurrently. Omit to run every agent the app bundles. Ignored when `sequence` is set."),
@@ -21795,7 +19783,12 @@ function registerAppTools(server, opts) {
21795
19783
  scopeId: z.string().optional().describe("When passed, refuse to run if the app is not applied to this scope."),
21796
19784
  adapter: z.string().optional().describe("Agent adapter slug (default `mastra-agent`). Used for the spawn; sets `harness` when `harness` is absent."),
21797
19785
  harness: z.string().optional().describe("Canonical harness slug (defaults to `adapter`). Recorded on the run + each session; sets `adapter` when `adapter` is absent."),
21798
- model: z.string().optional().describe("Model id passed through to each spawned session.")
19786
+ model: z.string().optional().describe(
19787
+ "Model id passed through to each spawned session. For an adapter with no `agent` option, this wins over the AGENT.md frontmatter's own `model`."
19788
+ ),
19789
+ access: z.object({ profileRef: z.string().optional() }).optional().describe(
19790
+ "Named auth-profile pin threaded to every spawn's `agent_start`-equivalent (see `agent_start.access`) \u2014 e.g. `{ profileRef: \"claude-subs-agentik\" }` when the adapter's default credential profile is disabled on this host."
19791
+ )
21799
19792
  // follow-up: no sandbox support in this WP — the e2b image doesn't carry
21800
19793
  // the mastra-agent adapter yet (see output/phase-a-findings.md A3). Thread
21801
19794
  // a `sandbox` field through to `spawnAgentSession` here once an image
@@ -21806,12 +19799,12 @@ function registerAppTools(server, opts) {
21806
19799
  if (!resolveAgentAdapter) return notEnabled("app_run");
21807
19800
  const installed = appRegistry.getApp(input.appId);
21808
19801
  if (!installed) {
21809
- return errorResult(`app_run: no installed app "${input.appId}" \u2014 call app_install first.`);
19802
+ return errorResult2(`app_run: no installed app "${input.appId}" \u2014 call app_install first.`);
21810
19803
  }
21811
19804
  if (input.scopeId) {
21812
19805
  const applied = appRegistry.listApplied(input.scopeId);
21813
19806
  if (!applied.some((m) => m.appId === input.appId)) {
21814
- return errorResult(
19807
+ return errorResult2(
21815
19808
  `app_run: app "${input.appId}" is not applied to scope "${input.scopeId}". Call app_apply first.`
21816
19809
  );
21817
19810
  }
@@ -21820,18 +19813,24 @@ function registerAppTools(server, opts) {
21820
19813
  try {
21821
19814
  refs = await readAppRefs(installed.dir);
21822
19815
  } catch (err) {
21823
- return errorResult(
19816
+ return errorResult2(
21824
19817
  `app_run: could not re-read "${installed.dir}": ${err instanceof Error ? err.message : String(err)}`
21825
19818
  );
21826
19819
  }
21827
19820
  const app = appRegistry.upsertApp({ ...installed, agents: refs.agents, workflows: refs.workflows });
19821
+ if (app.agents.length === 0) {
19822
+ return errorResult2(`app_run: app "${app.appId}" declares no agents; open its UI panel instead.`);
19823
+ }
21828
19824
  const adapter = input.adapter ?? input.harness ?? DEFAULT_AGENT_ADAPTER;
21829
19825
  const harness = input.harness ?? input.adapter ?? DEFAULT_AGENT_ADAPTER;
21830
19826
  const model = input.model;
19827
+ const resolvedAdapter = await resolveAgentAdapter(adapter);
19828
+ const declaredOptionsKnown = resolvedAdapter?.declaredOptions !== void 0;
19829
+ const declaresAgentOption = !declaredOptionsKnown || resolvedAdapter.declaredOptions.some((o) => o.id === "agent");
21831
19830
  const ordered = input.sequence ?? input.agents ?? app.agents.map((a) => a.id);
21832
19831
  const unknown = ordered.filter((id) => !app.agents.some((a) => a.id === id));
21833
19832
  if (unknown.length > 0) {
21834
- return errorResult(
19833
+ return errorResult2(
21835
19834
  `app_run: unknown agent id(s) for app "${app.appId}": ${unknown.join(", ")}`
21836
19835
  );
21837
19836
  }
@@ -21839,15 +19838,36 @@ function registerAppTools(server, opts) {
21839
19838
  const errors = [];
21840
19839
  const spawnOne = async (agentId) => {
21841
19840
  const agentPath = app.agents.find((a) => a.id === agentId).path;
19841
+ let spawnModel = model;
19842
+ let spawnPrompt = input.prompt;
19843
+ let spawnOptions;
19844
+ if (declaresAgentOption) {
19845
+ spawnOptions = { agent: agentPath };
19846
+ } else {
19847
+ try {
19848
+ const defaults = await loadAgentPromptDefaults(agentPath);
19849
+ const built = buildAgentRunSpawnConfig(defaults, { model, prompt: input.prompt });
19850
+ spawnModel = built.model;
19851
+ spawnPrompt = built.prompt;
19852
+ } catch (err) {
19853
+ errors.push({
19854
+ agentId,
19855
+ error: `could not read AGENT.md "${agentPath}": ${err instanceof Error ? err.message : String(err)}`
19856
+ });
19857
+ return null;
19858
+ }
19859
+ }
21842
19860
  const result = await spawnAgentSession(
21843
19861
  { registry, resolveAgentAdapter },
21844
19862
  {
21845
19863
  adapter,
21846
19864
  ...harness !== adapter ? { harness } : {},
21847
- ...model !== void 0 ? { model } : {},
19865
+ ...spawnModel !== void 0 ? { model: spawnModel } : {},
21848
19866
  cwd: input.cwd ?? app.dir,
21849
- ...input.prompt ? { prompt: input.prompt } : {},
21850
- options: { agent: agentPath },
19867
+ ...spawnPrompt ? { prompt: spawnPrompt } : {},
19868
+ ...spawnOptions ? { options: spawnOptions } : {},
19869
+ ...input.access ? { access: input.access } : {},
19870
+ appId: app.appId,
21851
19871
  label: `app:${app.appId}:${agentId}`
21852
19872
  }
21853
19873
  );
@@ -21880,7 +19900,7 @@ function registerAppTools(server, opts) {
21880
19900
  ...model !== void 0 ? { model } : {}
21881
19901
  });
21882
19902
  appRegistry.endRun(run2.appRunId, { status: "ended" });
21883
- return textResult({
19903
+ return textResult2({
21884
19904
  appRunId: run2.appRunId,
21885
19905
  status: run2.status,
21886
19906
  ...run2.endedAt ? { endedAt: run2.endedAt } : {},
@@ -21899,7 +19919,7 @@ function registerAppTools(server, opts) {
21899
19919
  harness,
21900
19920
  ...model !== void 0 ? { model } : {}
21901
19921
  });
21902
- return textResult({
19922
+ return textResult2({
21903
19923
  appRunId: run.appRunId,
21904
19924
  adapter,
21905
19925
  harness,
@@ -21915,7 +19935,7 @@ function registerAppTools(server, opts) {
21915
19935
  { appRunId: z.string() },
21916
19936
  async (input) => {
21917
19937
  const run = appRegistry.getRun(input.appRunId);
21918
- if (!run) return errorResult(`app_status: no app run "${input.appRunId}".`);
19938
+ if (!run) return errorResult2(`app_status: no app run "${input.appRunId}".`);
21919
19939
  const app = appRegistry.getApp(run.appId);
21920
19940
  const sessions = run.sessions.map((s) => ({
21921
19941
  agentId: s.agentId,
@@ -21926,7 +19946,7 @@ function registerAppTools(server, opts) {
21926
19946
  const storedTerminal = run.status !== "running";
21927
19947
  const reconciledStatus = storedTerminal ? run.status : allSessionsTerminal ? "ended" : "running";
21928
19948
  const workflowRuns = workflowRunner && app ? workflowRunner.list().filter((r) => app.workflows.some((w) => w.id === r.workflowId)) : [];
21929
- return textResult({
19949
+ return textResult2({
21930
19950
  appRunId: run.appRunId,
21931
19951
  appId: run.appId,
21932
19952
  status: reconciledStatus,
@@ -21946,7 +19966,7 @@ function registerAppTools(server, opts) {
21946
19966
  { appRunId: z.string() },
21947
19967
  async (input) => {
21948
19968
  const run = appRegistry.getRun(input.appRunId);
21949
- if (!run) return errorResult(`app_stop: no app run "${input.appRunId}".`);
19969
+ if (!run) return errorResult2(`app_stop: no app run "${input.appRunId}".`);
21950
19970
  const killed = [];
21951
19971
  const notFound = [];
21952
19972
  for (const s of run.sessions) {
@@ -21954,7 +19974,7 @@ function registerAppTools(server, opts) {
21954
19974
  else notFound.push(s.sessionId);
21955
19975
  }
21956
19976
  const ended = appRegistry.endRun(input.appRunId);
21957
- return textResult({
19977
+ return textResult2({
21958
19978
  appRunId: input.appRunId,
21959
19979
  killed,
21960
19980
  ...notFound.length > 0 ? { notFound } : {},
@@ -21968,17 +19988,20 @@ function registerAppTools(server, opts) {
21968
19988
  {
21969
19989
  appId: z.string(),
21970
19990
  scopeId: z.string().optional().describe("Scope to apply to. Defaults to 'root'."),
21971
- dir: z.string().optional().describe("Absolute path to install from if not already installed.")
19991
+ dir: z.string().optional().describe("Absolute path to install from if not already installed."),
19992
+ dataDir: z.string().optional().describe("Data root to install with (see app_install). Only used when installing.")
21972
19993
  },
21973
19994
  async (input) => {
21974
19995
  const scopeId = input.scopeId ?? "root";
21975
19996
  let installed = appRegistry.getApp(input.appId);
21976
19997
  if (!installed && input.dir) {
21977
- const installResult = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
21978
- if (!installResult.ok) return errorResult(`app_apply: ${installResult.error}`);
19998
+ const installResult = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
19999
+ ...input.dataDir !== void 0 ? { dataDir: input.dataDir } : {}
20000
+ });
20001
+ if (!installResult.ok) return errorResult2(`app_apply: ${installResult.error}`);
21979
20002
  installed = installResult.record;
21980
20003
  } else if (!installed) {
21981
- return errorResult(
20004
+ return errorResult2(
21982
20005
  `app_apply: app "${input.appId}" is not installed. Either call app_install first or provide a 'dir' parameter.`
21983
20006
  );
21984
20007
  }
@@ -21987,19 +20010,20 @@ function registerAppTools(server, opts) {
21987
20010
  const appliedIds = new Set(applied.map((m) => m.appId));
21988
20011
  const missing = installed.requires.filter((reqId) => !appliedIds.has(reqId));
21989
20012
  if (missing.length > 0) {
21990
- return errorResult(
20013
+ return errorResult2(
21991
20014
  `app_apply: app "${input.appId}" requires the following apps to be applied to scope "${scopeId}" first: ${missing.join(", ")}`
21992
20015
  );
21993
20016
  }
21994
20017
  }
21995
20018
  const mount = appRegistry.applyApp({ scopeId, appId: input.appId });
21996
- return textResult({
20019
+ return textResult2({
21997
20020
  scopeId: mount.scopeId,
21998
20021
  appId: mount.appId,
21999
20022
  appliedAt: mount.appliedAt,
22000
20023
  agents: installed.agents,
22001
20024
  workflows: installed.workflows,
22002
- unvalidatedAgentTools: installed.unvalidatedAgentTools
20025
+ unvalidatedAgentTools: installed.unvalidatedAgentTools,
20026
+ ...installed.agents.length === 0 ? { note: "app declares no agents \u2014 nothing to activate in this scope; open its UI panel directly." } : {}
22003
20027
  });
22004
20028
  }
22005
20029
  );
@@ -22022,15 +20046,15 @@ function registerAppTools(server, opts) {
22022
20046
  }
22023
20047
  }
22024
20048
  if (dependents.length > 0) {
22025
- return errorResult(
20049
+ return errorResult2(
22026
20050
  `app_unapply: cannot unapply app "${input.appId}" from scope "${scopeId}" \u2014 the following apps in this scope require it: ${dependents.join(", ")}`
22027
20051
  );
22028
20052
  }
22029
20053
  const removed = appRegistry.unapplyApp({ scopeId, appId: input.appId });
22030
20054
  if (!removed) {
22031
- return errorResult(`app_unapply: app "${input.appId}" is not applied to scope "${scopeId}".`);
20055
+ return errorResult2(`app_unapply: app "${input.appId}" is not applied to scope "${scopeId}".`);
22032
20056
  }
22033
- return textResult({ scopeId: removed.scopeId, appId: removed.appId, appliedAt: removed.appliedAt });
20057
+ return textResult2({ scopeId: removed.scopeId, appId: removed.appId, appliedAt: removed.appliedAt });
22034
20058
  }
22035
20059
  );
22036
20060
  server.tool(
@@ -22054,7 +20078,7 @@ function registerAppTools(server, opts) {
22054
20078
  } : {}
22055
20079
  };
22056
20080
  });
22057
- return textResult(result);
20081
+ return textResult2(result);
22058
20082
  }
22059
20083
  );
22060
20084
  server.tool(
@@ -22077,26 +20101,26 @@ function registerAppTools(server, opts) {
22077
20101
  async (input) => {
22078
20102
  const applied = appRegistry.listApplied().filter((m) => m.appId === input.appId);
22079
20103
  if (applied.length > 0) {
22080
- return errorResult(
20104
+ return errorResult2(
22081
20105
  `app_uninstall: app "${input.appId}" is applied to scope(s) ${applied.map((m) => m.scopeId).join(", ")} \u2014 unapply from scopes first.`
22082
20106
  );
22083
20107
  }
22084
20108
  const runningRuns = appRegistry.listRuns().filter((r) => r.appId === input.appId && r.status === "running");
22085
20109
  if (runningRuns.length > 0) {
22086
- return errorResult(
20110
+ return errorResult2(
22087
20111
  `app_uninstall: app "${input.appId}" has running app_run(s) ${runningRuns.map((r) => r.appRunId).join(", ")} \u2014 stop app runs first.`
22088
20112
  );
22089
20113
  }
22090
20114
  const removed = appRegistry.removeApp(input.appId);
22091
20115
  if (!removed) {
22092
- return errorResult(`app_uninstall: no installed app "${input.appId}".`);
20116
+ return errorResult2(`app_uninstall: no installed app "${input.appId}".`);
22093
20117
  }
22094
- return textResult({ appId: removed.appId });
20118
+ return textResult2({ appId: removed.appId });
22095
20119
  }
22096
20120
  );
22097
20121
  server.tool(
22098
20122
  "app_catalog",
22099
- "List browsable apps from the catalog file (default `~/.agentproto/app-catalog.json`, tolerates a missing file), merged with installed-app status \u2014 every entry reports `installed`, `hasUi`, `hasArtifact`, and `hasSkill`. Installed apps absent from the catalog file are included too.",
20123
+ "List browsable apps from the catalog file (default `~/.agentproto/app-catalog.json`, tolerates a missing file), merged with installed-app status \u2014 every entry reports `installed`, `hasUi`, `hasArtifact`, and `hasSkill`. Installed apps absent from the catalog file are included too, as are the five always-on builtin panels (category `builtin`) \u2014 they need no `app_install`.",
22100
20124
  {
22101
20125
  scopeId: z.string().optional().describe("Reserved for future scope-aware filtering. Currently unused.")
22102
20126
  },
@@ -22135,7 +20159,8 @@ function registerAppTools(server, opts) {
22135
20159
  hasSkill: app.skill !== void 0
22136
20160
  });
22137
20161
  }
22138
- return textResult(entries);
20162
+ entries.push(...builtinPanelCatalogEntries());
20163
+ return textResult2(entries);
22139
20164
  }
22140
20165
  );
22141
20166
  server.tool(
@@ -22145,20 +20170,20 @@ function registerAppTools(server, opts) {
22145
20170
  async (input) => {
22146
20171
  const installed = appRegistry.getApp(input.appId);
22147
20172
  if (!installed) {
22148
- return errorResult(`app_artifact_get: no installed app "${input.appId}".`);
20173
+ return errorResult2(`app_artifact_get: no installed app "${input.appId}".`);
22149
20174
  }
22150
20175
  if (!installed.artifact) {
22151
- return errorResult(`app_artifact_get: app "${input.appId}" has no artifact.`);
20176
+ return errorResult2(`app_artifact_get: app "${input.appId}" has no artifact.`);
22152
20177
  }
22153
20178
  let html;
22154
20179
  try {
22155
20180
  html = await readFile(installed.artifact.path, "utf8");
22156
20181
  } catch (err) {
22157
- return errorResult(
20182
+ return errorResult2(
22158
20183
  `app_artifact_get: could not read artifact "${installed.artifact.path}": ${err instanceof Error ? err.message : String(err)}`
22159
20184
  );
22160
20185
  }
22161
- return textResult({
20186
+ return textResult2({
22162
20187
  appId: installed.appId,
22163
20188
  ...installed.artifact.title ? { title: installed.artifact.title } : {},
22164
20189
  ...installed.artifact.description ? { description: installed.artifact.description } : {},
@@ -22173,17 +20198,17 @@ function registerAppTools(server, opts) {
22173
20198
  async (input) => {
22174
20199
  const installed = appRegistry.getApp(input.appId);
22175
20200
  if (!installed) {
22176
- return errorResult(`app_skill_get: no installed app "${input.appId}".`);
20201
+ return errorResult2(`app_skill_get: no installed app "${input.appId}".`);
22177
20202
  }
22178
20203
  if (!installed.skill) {
22179
- return errorResult(`app_skill_get: app "${input.appId}" has no skill.`);
20204
+ return errorResult2(`app_skill_get: app "${input.appId}" has no skill.`);
22180
20205
  }
22181
20206
  const skillDir = installed.skill.path;
22182
20207
  let skillSource;
22183
20208
  try {
22184
20209
  skillSource = await readFile(join(skillDir, "SKILL.md"), "utf8");
22185
20210
  } catch (err) {
22186
- return errorResult(
20211
+ return errorResult2(
22187
20212
  `app_skill_get: could not read SKILL.md in "${skillDir}": ${err instanceof Error ? err.message : String(err)}`
22188
20213
  );
22189
20214
  }
@@ -22211,7 +20236,7 @@ function registerAppTools(server, opts) {
22211
20236
  }
22212
20237
  }
22213
20238
  } catch (err) {
22214
- return errorResult(
20239
+ return errorResult2(
22215
20240
  `app_skill_get: could not read skill directory "${skillDir}": ${err instanceof Error ? err.message : String(err)}`
22216
20241
  );
22217
20242
  }
@@ -22222,7 +20247,7 @@ function registerAppTools(server, opts) {
22222
20247
  files
22223
20248
  };
22224
20249
  if (skipped.length > 0) result.skipped = skipped;
22225
- return textResult(result);
20250
+ return textResult2(result);
22226
20251
  }
22227
20252
  );
22228
20253
  }
@@ -22442,10 +20467,10 @@ async function assertExternalPathRealInside(root, target) {
22442
20467
  throw new ExternalPathTraversalError(target);
22443
20468
  }
22444
20469
  }
22445
- function textResult2(body) {
20470
+ function textResult3(body) {
22446
20471
  return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
22447
20472
  }
22448
- function errorResult2(text10) {
20473
+ function errorResult3(text10) {
22449
20474
  return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
22450
20475
  }
22451
20476
  function registerAppExternalTools(server, opts) {
@@ -22460,27 +20485,27 @@ function registerAppExternalTools(server, opts) {
22460
20485
  },
22461
20486
  async (input) => {
22462
20487
  const installed = appRegistry.getApp(input.appId);
22463
- if (!installed) return errorResult2(`app_external_list: no installed app "${input.appId}".`);
20488
+ if (!installed) return errorResult3(`app_external_list: no installed app "${input.appId}".`);
22464
20489
  try {
22465
20490
  assertRootGranted(installed, input.root);
22466
20491
  } catch (err) {
22467
- return errorResult2(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
20492
+ return errorResult3(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
22468
20493
  }
22469
20494
  const root = await realpathExternalRoot(input.root);
22470
- if (!root) return errorResult2(`app_external_list: root "${input.root}" is not accessible.`);
20495
+ if (!root) return errorResult3(`app_external_list: root "${input.root}" is not accessible.`);
22471
20496
  const relPath = input.path ?? "";
22472
20497
  let target;
22473
20498
  try {
22474
20499
  target = resolveExternalPath(root, relPath);
22475
20500
  await assertExternalPathRealInside(root, target);
22476
20501
  } catch (err) {
22477
- return errorResult2(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
20502
+ return errorResult3(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
22478
20503
  }
22479
20504
  let dirents;
22480
20505
  try {
22481
20506
  dirents = await readdir(target, { withFileTypes: true });
22482
20507
  } catch (err) {
22483
- return errorResult2(
20508
+ return errorResult3(
22484
20509
  `app_external_list: cannot list "${relPath || "."}": ${err instanceof Error ? err.message : String(err)}`
22485
20510
  );
22486
20511
  }
@@ -22500,7 +20525,7 @@ function registerAppExternalTools(server, opts) {
22500
20525
  entries.push({ name: d.name, isDirectory, size });
22501
20526
  }
22502
20527
  entries.sort((a, b) => a.name.localeCompare(b.name));
22503
- return textResult2({ appId: input.appId, root: input.root, path: relPath, entries });
20528
+ return textResult3({ appId: input.appId, root: input.root, path: relPath, entries });
22504
20529
  }
22505
20530
  );
22506
20531
  server.tool(
@@ -22513,40 +20538,40 @@ function registerAppExternalTools(server, opts) {
22513
20538
  },
22514
20539
  async (input) => {
22515
20540
  const installed = appRegistry.getApp(input.appId);
22516
- if (!installed) return errorResult2(`app_external_read: no installed app "${input.appId}".`);
20541
+ if (!installed) return errorResult3(`app_external_read: no installed app "${input.appId}".`);
22517
20542
  try {
22518
20543
  assertRootGranted(installed, input.root);
22519
20544
  } catch (err) {
22520
- return errorResult2(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
20545
+ return errorResult3(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
22521
20546
  }
22522
20547
  const ext = extname(input.path).toLowerCase();
22523
20548
  if (!TEXT_EXTENSIONS.has(ext)) {
22524
- return errorResult2(
20549
+ return errorResult3(
22525
20550
  `app_external_read: "${input.path}" has extension "${ext || "(none)"}", which is not text-ish (allowed: ${[...TEXT_EXTENSIONS].sort().join(", ")}). Use GET /apps/${input.appId}/external-blob?root=${encodeURIComponent(input.root)}&path=${encodeURIComponent(input.path)} to fetch this file's bytes instead.`
22526
20551
  );
22527
20552
  }
22528
20553
  const root = await realpathExternalRoot(input.root);
22529
- if (!root) return errorResult2(`app_external_read: root "${input.root}" is not accessible.`);
20554
+ if (!root) return errorResult3(`app_external_read: root "${input.root}" is not accessible.`);
22530
20555
  let target;
22531
20556
  try {
22532
20557
  target = resolveExternalPath(root, input.path);
22533
20558
  await assertExternalPathRealInside(root, target);
22534
20559
  } catch (err) {
22535
- return errorResult2(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
20560
+ return errorResult3(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
22536
20561
  }
22537
20562
  let st;
22538
20563
  try {
22539
20564
  st = await stat(target);
22540
20565
  } catch (err) {
22541
- return errorResult2(
20566
+ return errorResult3(
22542
20567
  `app_external_read: cannot stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
22543
20568
  );
22544
20569
  }
22545
20570
  if (st.isDirectory()) {
22546
- return errorResult2(`app_external_read: "${input.path}" is a directory, not a file.`);
20571
+ return errorResult3(`app_external_read: "${input.path}" is a directory, not a file.`);
22547
20572
  }
22548
20573
  if (st.size > MAX_TEXT_READ_BYTES) {
22549
- return errorResult2(
20574
+ return errorResult3(
22550
20575
  `app_external_read: "${input.path}" is ${st.size} bytes, over the ${MAX_TEXT_READ_BYTES}-byte text-read cap. Use the external-blob HTTP route instead.`
22551
20576
  );
22552
20577
  }
@@ -22554,18 +20579,18 @@ function registerAppExternalTools(server, opts) {
22554
20579
  try {
22555
20580
  raw = await readFile(target, "utf8");
22556
20581
  } catch (err) {
22557
- return errorResult2(
20582
+ return errorResult3(
22558
20583
  `app_external_read: cannot read "${input.path}": ${err instanceof Error ? err.message : String(err)}`
22559
20584
  );
22560
20585
  }
22561
20586
  if (ext === ".json") {
22562
20587
  try {
22563
- return textResult2({ appId: input.appId, root: input.root, path: input.path, content: JSON.parse(raw) });
20588
+ return textResult3({ appId: input.appId, root: input.root, path: input.path, content: JSON.parse(raw) });
22564
20589
  } catch {
22565
- return textResult2({ appId: input.appId, root: input.root, path: input.path, content: raw });
20590
+ return textResult3({ appId: input.appId, root: input.root, path: input.path, content: raw });
22566
20591
  }
22567
20592
  }
22568
- return textResult2({ appId: input.appId, root: input.root, path: input.path, content: raw });
20593
+ return textResult3({ appId: input.appId, root: input.root, path: input.path, content: raw });
22569
20594
  }
22570
20595
  );
22571
20596
  }
@@ -29652,15 +27677,15 @@ async function handleAppExternalBlob(req, res, appId, appRegistry) {
29652
27677
  reply(403, { error: `root "${root}" is not granted to app "${appId}".` });
29653
27678
  return;
29654
27679
  }
29655
- const safeRoot2 = await realpathExternalRoot(root);
29656
- if (!safeRoot2) {
27680
+ const safeRoot = await realpathExternalRoot(root);
27681
+ if (!safeRoot) {
29657
27682
  reply(404, { error: `root "${root}" is not accessible.` });
29658
27683
  return;
29659
27684
  }
29660
27685
  let target;
29661
27686
  try {
29662
- target = resolveExternalPath(safeRoot2, relPath);
29663
- await assertExternalPathRealInside(safeRoot2, target);
27687
+ target = resolveExternalPath(safeRoot, relPath);
27688
+ await assertExternalPathRealInside(safeRoot, target);
29664
27689
  } catch (err) {
29665
27690
  reply(400, { error: err instanceof Error ? err.message : String(err) });
29666
27691
  return;
@@ -29726,7 +27751,9 @@ async function handleApps(req, res, path, appRegistry, performInstall2, listRegi
29726
27751
  const scopeId = body?.scopeId ?? "root";
29727
27752
  let installed = appRegistry.getApp(appId);
29728
27753
  if (!installed && body?.dir) {
29729
- const installResult = await performInstall2(body.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter);
27754
+ const installResult = await performInstall2(body.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
27755
+ ...body.dataDir !== void 0 ? { dataDir: body.dataDir } : {}
27756
+ });
29730
27757
  if (!installResult.ok) {
29731
27758
  res.writeHead(400, { "content-type": "application/json" });
29732
27759
  res.end(JSON.stringify({ error: installResult.error }));
@@ -30997,261 +29024,6 @@ stderr: ${stderrText.slice(-600)}` : msg);
30997
29024
  `import "${entry.alias}" has unsupported transport type "${snap.type}"`
30998
29025
  );
30999
29026
  }
31000
- var AppPathTraversalError = class extends Error {
31001
- code = "APP_PATH_TRAVERSAL";
31002
- constructor(relPath) {
31003
- super(`app-data: path traversal rejected for "${relPath}" \u2014 must resolve inside the app dir.`);
31004
- this.name = "AppPathTraversalError";
31005
- }
31006
- };
31007
- function resolveAppDataPath(appDir, relPath) {
31008
- if (isAbsolute(relPath)) throw new AppPathTraversalError(relPath);
31009
- if (/^[A-Za-z]:/.test(relPath)) throw new AppPathTraversalError(relPath);
31010
- const root = resolve(appDir);
31011
- const target = resolve(appDir, relPath);
31012
- const rootWithSep = root.endsWith(sep) ? root : root + sep;
31013
- if (target !== root && !target.startsWith(rootWithSep)) {
31014
- throw new AppPathTraversalError(relPath);
31015
- }
31016
- return target;
31017
- }
31018
- function textResult3(body) {
31019
- return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
31020
- }
31021
- function errorResult3(text10) {
31022
- return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
31023
- }
31024
- async function safeRoot(appDir) {
31025
- try {
31026
- return await realpath(appDir);
31027
- } catch {
31028
- return void 0;
31029
- }
31030
- }
31031
- async function assertRealInside(root, target) {
31032
- let real;
31033
- try {
31034
- real = await realpath(target);
31035
- } catch {
31036
- return;
31037
- }
31038
- const rootWithSep = root.endsWith(sep) ? root : root + sep;
31039
- if (real !== root && !real.startsWith(rootWithSep)) {
31040
- throw new AppPathTraversalError(target);
31041
- }
31042
- }
31043
- async function atomicWrite(filePath, data) {
31044
- await mkdir(dirname(filePath), { recursive: true });
31045
- const tmp = `${filePath}.tmp.${process.pid}`;
31046
- await writeFile(tmp, data, "utf8");
31047
- await rename(tmp, filePath);
31048
- }
31049
- async function writeRaw(root, rel, data) {
31050
- await atomicWrite(resolveAppDataPath(root, rel), data);
31051
- }
31052
- async function writeJson(root, rel, value) {
31053
- await writeRaw(root, rel, JSON.stringify(value, null, 2) + "\n");
31054
- }
31055
- async function readTextMaybe(path) {
31056
- try {
31057
- return await readFile(path, "utf8");
31058
- } catch {
31059
- return void 0;
31060
- }
31061
- }
31062
- async function readJsonMaybe(path) {
31063
- const raw = await readTextMaybe(path);
31064
- if (raw === void 0) return void 0;
31065
- try {
31066
- return JSON.parse(raw);
31067
- } catch {
31068
- return void 0;
31069
- }
31070
- }
31071
- function normalizeJob(raw) {
31072
- const jobId = raw.jobId ?? raw.id;
31073
- const out = { ...raw, id: jobId, jobId };
31074
- if (out.applyUrl === void 0 || out.applyUrl === null) out.applyUrl = raw.url;
31075
- return out;
31076
- }
31077
- async function readDossierJobId(dossierDir) {
31078
- const parsed = await readJsonMaybe(join(dossierDir, "job.json"));
31079
- if (!parsed || typeof parsed !== "object") return void 0;
31080
- const jobId = parsed.jobId ?? parsed.id;
31081
- return typeof jobId === "string" && jobId.length > 0 ? jobId : void 0;
31082
- }
31083
- function registerAppDataTools(server, opts) {
31084
- const { appRegistry } = opts;
31085
- server.tool(
31086
- "app_data_read",
31087
- "Read an app-scoped data file (app-relative path). JSON paths return the parsed value in `content`; everything else returns the raw text. Path traversal outside the app dir is rejected.",
31088
- { appId: z.string(), path: z.string().describe("App-relative path under the app's own dir.") },
31089
- async (input) => {
31090
- const installed = appRegistry.getApp(input.appId);
31091
- if (!installed) return errorResult3(`app_data_read: no installed app "${input.appId}".`);
31092
- const root = await safeRoot(installed.dir);
31093
- if (!root) return errorResult3(`app_data_read: app dir "${installed.dir}" is not accessible.`);
31094
- let target;
31095
- try {
31096
- target = resolveAppDataPath(root, input.path);
31097
- await assertRealInside(root, target);
31098
- } catch (err) {
31099
- return errorResult3(`app_data_read: ${err instanceof Error ? err.message : String(err)}`);
31100
- }
31101
- const raw = await readTextMaybe(target);
31102
- if (raw === void 0) return textResult3({ appId: input.appId, path: input.path, exists: false });
31103
- if (input.path.endsWith(".json")) {
31104
- try {
31105
- return textResult3({ appId: input.appId, path: input.path, exists: true, content: JSON.parse(raw) });
31106
- } catch {
31107
- return textResult3({ appId: input.appId, path: input.path, exists: true, content: raw });
31108
- }
31109
- }
31110
- return textResult3({ appId: input.appId, path: input.path, exists: true, content: raw });
31111
- }
31112
- );
31113
- server.tool(
31114
- "app_data_write",
31115
- "Write an app-scoped data file (app-relative path), creating parent directories as needed. `.json` paths are JSON-stringified (pretty); other paths write the raw string passed as `content.text` (or a plain string `content`). Atomic write (tmp + rename). Path traversal outside the app dir is rejected.",
31116
- {
31117
- appId: z.string(),
31118
- path: z.string().describe("App-relative path under the app's own dir."),
31119
- content: z.unknown().describe("JSON value for `.json` paths, or `{ text }` / string for others.")
31120
- },
31121
- async (input) => {
31122
- const installed = appRegistry.getApp(input.appId);
31123
- if (!installed) return errorResult3(`app_data_write: no installed app "${input.appId}".`);
31124
- const root = await safeRoot(installed.dir);
31125
- if (!root) return errorResult3(`app_data_write: app dir "${installed.dir}" is not accessible.`);
31126
- let target;
31127
- try {
31128
- target = resolveAppDataPath(root, input.path);
31129
- } catch (err) {
31130
- return errorResult3(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
31131
- }
31132
- let payload;
31133
- if (input.path.endsWith(".json")) {
31134
- payload = JSON.stringify(input.content, null, 2);
31135
- } else {
31136
- const raw = input.content;
31137
- if (typeof raw === "string") payload = raw;
31138
- else if (raw !== null && typeof raw === "object" && typeof raw.text === "string") {
31139
- payload = raw.text;
31140
- } else {
31141
- payload = JSON.stringify(raw);
31142
- }
31143
- }
31144
- try {
31145
- await atomicWrite(target, payload);
31146
- return textResult3({ appId: input.appId, path: input.path, size: Buffer.byteLength(payload, "utf8") });
31147
- } catch (err) {
31148
- return errorResult3(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
31149
- }
31150
- }
31151
- );
31152
- server.tool(
31153
- "app_data_list",
31154
- "List entries (name + type + size) under an app-relative directory (default `.`). A missing directory returns empty entries, not an error. Path traversal outside the app dir is rejected.",
31155
- {
31156
- appId: z.string(),
31157
- dir: z.string().optional().describe("App-relative directory to list. Defaults to `.`.")
31158
- },
31159
- async (input) => {
31160
- const installed = appRegistry.getApp(input.appId);
31161
- if (!installed) return errorResult3(`app_data_list: no installed app "${input.appId}".`);
31162
- const root = await safeRoot(installed.dir);
31163
- if (!root) return errorResult3(`app_data_list: app dir "${installed.dir}" is not accessible.`);
31164
- const relDir = input.dir ?? ".";
31165
- let target;
31166
- try {
31167
- target = resolveAppDataPath(root, relDir);
31168
- } catch (err) {
31169
- return errorResult3(`app_data_list: ${err instanceof Error ? err.message : String(err)}`);
31170
- }
31171
- let dirents;
31172
- try {
31173
- dirents = await readdir(target, { withFileTypes: true });
31174
- } catch {
31175
- return textResult3({ appId: input.appId, dir: relDir, entries: [] });
31176
- }
31177
- const entries = [];
31178
- for (const d of dirents) {
31179
- const isDirectory = d.isDirectory();
31180
- let size = 0;
31181
- if (!isDirectory) {
31182
- try {
31183
- size = (await stat(join(target, d.name))).size;
31184
- } catch {
31185
- size = 0;
31186
- }
31187
- }
31188
- entries.push({ name: d.name, type: isDirectory ? "directory" : "file", size });
31189
- }
31190
- entries.sort((a, b) => a.name.localeCompare(b.name));
31191
- return textResult3({ appId: input.appId, dir: relDir, entries });
31192
- }
31193
- );
31194
- server.tool(
31195
- "app_data_migrate",
31196
- "One-time import of legacy job-app data into the durable shape under the app dir: `data/jobs/<jobId>.json` (normalized id/jobId/applyUrl), `data/rankings/latest.json` (full ranked list) + per-job ranking artifacts, `applications/<jobId>/{job.json,cv.json,cover.md}` from matching `dossiers/*` folders, and `data/state.json`. Idempotent \u2014 re-running after migration returns `alreadyMigrated` unless `force`.",
31197
- {
31198
- appId: z.string(),
31199
- force: z.boolean().optional().describe("Re-run even if already migrated.")
31200
- },
31201
- async (input) => {
31202
- const installed = appRegistry.getApp(input.appId);
31203
- if (!installed) return errorResult3(`app_data_migrate: no installed app "${input.appId}".`);
31204
- const root = await safeRoot(installed.dir);
31205
- if (!root) return errorResult3(`app_data_migrate: app dir "${installed.dir}" is not accessible.`);
31206
- const stateRel = "data/state.json";
31207
- if (!input.force && await readJsonMaybe(resolveAppDataPath(root, stateRel)) !== void 0) {
31208
- return textResult3({ appId: input.appId, migrated: false, alreadyMigrated: true });
31209
- }
31210
- let jobs = [];
31211
- const rankedRaw = await readJsonMaybe(resolveAppDataPath(root, "ranked-jobs.json"));
31212
- if (Array.isArray(rankedRaw)) jobs = rankedRaw;
31213
- const normalized = jobs.map(normalizeJob).filter((j) => typeof j.jobId === "string" && j.jobId.length > 0);
31214
- for (const job of normalized) {
31215
- const id = job.jobId;
31216
- await writeJson(root, `data/jobs/${id}.json`, job);
31217
- await writeJson(root, `data/rankings/${id}.json`, job);
31218
- }
31219
- await writeJson(root, "data/rankings/latest.json", normalized);
31220
- let folderNames = [];
31221
- try {
31222
- folderNames = (await readdir(resolveAppDataPath(root, "dossiers"), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
31223
- } catch {
31224
- folderNames = [];
31225
- }
31226
- const matched = /* @__PURE__ */ new Set();
31227
- const skippedFolders = [];
31228
- for (const name of folderNames) {
31229
- const dossierDir = resolveAppDataPath(root, join("dossiers", name));
31230
- const jobId = await readDossierJobId(dossierDir);
31231
- const targetJob = normalized.find((j) => j.jobId === jobId);
31232
- if (!jobId || !targetJob) {
31233
- skippedFolders.push(name);
31234
- continue;
31235
- }
31236
- matched.add(jobId);
31237
- await writeJson(root, `applications/${jobId}/job.json`, targetJob);
31238
- const cv = await readJsonMaybe(join(dossierDir, "cv.json"));
31239
- if (cv !== void 0) await writeJson(root, `applications/${jobId}/cv.json`, cv);
31240
- const cover = await readTextMaybe(join(dossierDir, "cover.md"));
31241
- if (cover !== void 0) await writeRaw(root, `applications/${jobId}/cover.md`, cover);
31242
- }
31243
- const jobCount = normalized.length;
31244
- const dossierCount = matched.size;
31245
- await writeJson(root, stateRel, {
31246
- migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
31247
- jobCount,
31248
- dossierCount,
31249
- skippedFolders
31250
- });
31251
- return textResult3({ appId: input.appId, migrated: true, jobCount, dossierCount, skippedFolders });
31252
- }
31253
- );
31254
- }
31255
29027
  function createSessionEventBus() {
31256
29028
  const ee = new EventEmitter();
31257
29029
  ee.setMaxListeners(100);
@@ -32155,9 +29927,14 @@ function withDeferredTools(server, opts) {
32155
29927
  }
32156
29928
 
32157
29929
  // src/pr-provenance-reconciler.ts
29930
+ function prNumberFromUrl(url) {
29931
+ const m = /\/pull\/(\d+)(?:[/?#]|$)/.exec(url);
29932
+ return m ? Number(m[1]) : null;
29933
+ }
32158
29934
  var POLL_THROTTLE_MS = 15e3;
32159
29935
  function createPrProvenanceReconciler(opts) {
32160
29936
  const stampedPrUrls = /* @__PURE__ */ new Set();
29937
+ const costRefreshedPrUrls = /* @__PURE__ */ new Set();
32161
29938
  const lastPollAt = /* @__PURE__ */ new Map();
32162
29939
  const reconcile = async (sessionId, terminal) => {
32163
29940
  const desc = opts.registry.get(sessionId);
@@ -32198,9 +29975,31 @@ function createPrProvenanceReconciler(opts) {
32198
29975
  await stamp({ number: record2.createdPrNumber, url: record2.createdPrUrl });
32199
29976
  }
32200
29977
  const pr = await opts.resolveOpenPr(cwd);
32201
- if (!pr) return;
32202
- if (!shouldStamp(pr.url)) return;
32203
- await stamp(pr);
29978
+ if (pr && shouldStamp(pr.url)) await stamp(pr);
29979
+ await refreshCost(desc, supervisor, cwd);
29980
+ };
29981
+ const refreshCost = async (desc, supervisor, cwd) => {
29982
+ if (typeof desc.costUsd !== "number") return;
29983
+ for (const opened of desc.openedPrs ?? []) {
29984
+ if (costRefreshedPrUrls.has(opened.url)) continue;
29985
+ const number = opened.number ?? prNumberFromUrl(opened.url);
29986
+ if (number === null) {
29987
+ costRefreshedPrUrls.add(opened.url);
29988
+ continue;
29989
+ }
29990
+ const outcome = await stampFooterOnPr({
29991
+ registry: opts.registry,
29992
+ session: desc,
29993
+ supervisor,
29994
+ prNumber: number,
29995
+ prUrl: opened.url,
29996
+ cwd,
29997
+ refresh: true,
29998
+ ...opts.run ? { run: opts.run } : {},
29999
+ ...opts.host ? { host: opts.host } : {}
30000
+ });
30001
+ if (outcome.stamped) costRefreshedPrUrls.add(opened.url);
30002
+ }
32204
30003
  };
32205
30004
  const safeReconcile = (sessionId, terminal) => {
32206
30005
  void reconcile(sessionId, terminal).catch(() => {
@@ -32219,6 +30018,7 @@ function createPrProvenanceReconciler(opts) {
32219
30018
  dispose() {
32220
30019
  for (const unsubscribe of unsubscribes) unsubscribe();
32221
30020
  stampedPrUrls.clear();
30021
+ costRefreshedPrUrls.clear();
32222
30022
  lastPollAt.clear();
32223
30023
  }
32224
30024
  };
@@ -37033,14 +34833,12 @@ async function createGateway(opts) {
37033
34833
  };
37034
34834
  registerAppPullTools(server, { registry: sessions });
37035
34835
  const builtinPanelApps = [
37036
- makeSessionsPanelApp({ listSessions: listSessionsFiltered }),
37037
- makeAgentsOverviewApp({ listSessions: listSessionsFiltered }),
37038
- makeBureauSessionsApp({ listSessions: listSessionsFiltered }),
37039
- makeSessionStoryPanelApp({ listSessions: listSessionsFiltered }),
37040
- // Live-session widget — resource ui://live_session/view, also bound to
37041
- // `agent_start` via _meta.ui.resourceUri so a launch auto-renders it.
37042
- // httpBaseUrl = this daemon's own origin (SSE stream + bridge fallback).
37043
- makeLiveSessionApp({ httpBaseUrl: `http://127.0.0.1:${port}` }),
34836
+ ...makeBuiltinPanelApps({
34837
+ listSessions: listSessionsFiltered,
34838
+ // httpBaseUrl = this daemon's own origin (SSE stream + bridge
34839
+ // fallback for the live-session widget).
34840
+ httpBaseUrl: `http://127.0.0.1:${port}`
34841
+ }),
37044
34842
  // Same ptyEnabled gate as terminal_start/terminal_input/… in
37045
34843
  // session-tools.ts — the panel would be able to open the WS but
37046
34844
  // every spawn/attach would fail once node-pty isn't available, so