@agentproto/apps 0.11.0 → 0.13.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.
Files changed (70) hide show
  1. package/dist/agents-overview/panel.mjs +39 -67
  2. package/dist/agents-overview/panel.mjs.map +1 -1
  3. package/dist/agents-overview.mjs +38 -68
  4. package/dist/agents-overview.mjs.map +1 -1
  5. package/dist/bin/sync.d.ts.map +1 -1
  6. package/dist/bin/sync.mjs +2374 -2
  7. package/dist/bin/sync.mjs.map +1 -1
  8. package/dist/bureau-sessions/panel.mjs +39 -67
  9. package/dist/bureau-sessions/panel.mjs.map +1 -1
  10. package/dist/bureau-sessions.mjs +38 -68
  11. package/dist/bureau-sessions.mjs.map +1 -1
  12. package/dist/config/fragment.d.ts +50 -0
  13. package/dist/config/fragment.d.ts.map +1 -0
  14. package/dist/config/index.d.ts +23 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/ui.d.ts +43 -0
  17. package/dist/config/ui.d.ts.map +1 -0
  18. package/dist/config.mjs +2373 -0
  19. package/dist/config.mjs.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.mjs +3200 -150
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/live-session/logic.d.ts +2 -0
  25. package/dist/live-session/logic.d.ts.map +1 -1
  26. package/dist/live-session/panel.d.ts.map +1 -1
  27. package/dist/live-session/panel.mjs +52 -74
  28. package/dist/live-session/panel.mjs.map +1 -1
  29. package/dist/live-session.mjs +51 -75
  30. package/dist/live-session.mjs.map +1 -1
  31. package/dist/panel-bridge.d.ts +11 -6
  32. package/dist/panel-bridge.d.ts.map +1 -1
  33. package/dist/session-chat/blob-embed.d.ts +53 -0
  34. package/dist/session-chat/blob-embed.d.ts.map +1 -0
  35. package/dist/session-chat/index.d.ts +13 -0
  36. package/dist/session-chat/index.d.ts.map +1 -1
  37. package/dist/session-chat/panel.d.ts +38 -2
  38. package/dist/session-chat/panel.d.ts.map +1 -1
  39. package/dist/session-chat/panel.mjs +556 -6
  40. package/dist/session-chat/panel.mjs.map +1 -1
  41. package/dist/session-chat.mjs +591 -14
  42. package/dist/session-chat.mjs.map +1 -1
  43. package/dist/session-story/panel.mjs +39 -67
  44. package/dist/session-story/panel.mjs.map +1 -1
  45. package/dist/session-story.mjs +38 -68
  46. package/dist/session-story.mjs.map +1 -1
  47. package/dist/session-viewer/ui.d.ts.map +1 -1
  48. package/dist/session-viewer.mjs +10 -0
  49. package/dist/session-viewer.mjs.map +1 -1
  50. package/dist/sessions-panel/panel.d.ts.map +1 -1
  51. package/dist/sessions-panel/panel.mjs +40 -67
  52. package/dist/sessions-panel/panel.mjs.map +1 -1
  53. package/dist/sessions-panel.mjs +39 -68
  54. package/dist/sessions-panel.mjs.map +1 -1
  55. package/dist/work-board/panel.d.ts +1 -1
  56. package/dist/work-board/panel.d.ts.map +1 -1
  57. package/dist/work-board/panel.generated.d.ts +1 -1
  58. package/dist/work-board/panel.generated.d.ts.map +1 -1
  59. package/dist/work-board/panel.mjs +318 -62
  60. package/dist/work-board/panel.mjs.map +1 -1
  61. package/dist/work-board.mjs +318 -62
  62. package/dist/work-board.mjs.map +1 -1
  63. package/package.json +12 -4
  64. package/repo-maintenance/.agentproto/APP.md +22 -0
  65. package/repo-maintenance/.agentproto/agents/repo-maintenance-reviewer/AGENT.md +142 -0
  66. package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +262 -0
  67. package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +672 -0
  68. package/repo-maintenance/README.md +79 -0
  69. package/repo-maintenance/routines/repo-maintenance-daily/ROUTINE.md +77 -0
  70. package/repo-maintenance/routines/repo-maintenance-weekly/ROUTINE.md +82 -0
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineApp } from '@agentproto/app-kit';
2
2
  import { defineAgent } from '@agentproto/agent';
3
3
  import { defineWorkflow } from '@agentproto/workflow';
4
+ import { DISPLAY_MODE_SCRIPT_BODY } from '@agentproto/app-client/display-mode';
4
5
  import { z } from 'zod';
5
6
 
6
7
  /**
@@ -2355,6 +2356,2367 @@ var opsPanel = defineApp({
2355
2356
  tools: [...OPS_PANEL_TOOLS]
2356
2357
  }
2357
2358
  });
2359
+
2360
+ // src/config/fragment.ts
2361
+ function parseConfigFragment(hash) {
2362
+ var SECTIONS2 = ["wallets", "harnesses", "models", "defaults", "remote", "advanced"];
2363
+ var raw = String(hash || "").replace(/^#/, "");
2364
+ var parts = raw.split("/").filter(function(p) {
2365
+ return p.length > 0;
2366
+ });
2367
+ var sectionRaw = parts[0] || "";
2368
+ var section = SECTIONS2.indexOf(sectionRaw) !== -1 ? sectionRaw : SECTIONS2[0];
2369
+ var result = { section };
2370
+ var idPart = parts[1];
2371
+ if (idPart !== void 0) {
2372
+ try {
2373
+ result.id = decodeURIComponent(idPart);
2374
+ } catch (e) {
2375
+ result.id = idPart;
2376
+ }
2377
+ }
2378
+ var subPart = parts[2];
2379
+ if (subPart !== void 0) {
2380
+ try {
2381
+ result.sub = decodeURIComponent(subPart);
2382
+ } catch (e) {
2383
+ result.sub = subPart;
2384
+ }
2385
+ }
2386
+ return result;
2387
+ }
2388
+ function buildConfigFragment(section, id, sub) {
2389
+ var out = "#" + section;
2390
+ if (id !== void 0 && id !== null && id !== "") out += "/" + encodeURIComponent(id);
2391
+ if (sub !== void 0 && sub !== null && sub !== "") out += "/" + encodeURIComponent(sub);
2392
+ return out;
2393
+ }
2394
+ function shouldApplyIncomingView(arrivalIndex, hashPresentAtLoad) {
2395
+ return !(arrivalIndex === 1 && hashPresentAtLoad);
2396
+ }
2397
+
2398
+ // src/config/ui.ts
2399
+ var CONFIG_TOOLS = [
2400
+ "auth_profile_list",
2401
+ "auth_profile_create",
2402
+ "auth_profile_delete",
2403
+ "auth_profile_set_enabled",
2404
+ "auth_profile_set_models",
2405
+ "auth_profile_refresh_models",
2406
+ "auth_profile_import",
2407
+ "auth_discover_credentials",
2408
+ "auth_profile_update",
2409
+ "adapter_list",
2410
+ "harness_capabilities",
2411
+ "harness_preset_list",
2412
+ "harness_preset_create",
2413
+ "harness_preset_delete",
2414
+ "harness_preset_set_default",
2415
+ "catalog_models",
2416
+ "catalog_provider_models",
2417
+ "role_list",
2418
+ "usage_rollup",
2419
+ "config_get",
2420
+ "config_set",
2421
+ "daemon_health",
2422
+ "remote_status",
2423
+ "remote_enable",
2424
+ "remote_disable",
2425
+ "pair_list",
2426
+ "pair_offer",
2427
+ "pair_revoke",
2428
+ "tunnel_list",
2429
+ "tunnel_status"
2430
+ ];
2431
+ var APP_ID4 = "@agentproto/config";
2432
+ var SECTIONS = [
2433
+ { id: "wallets", label: "Wallets" },
2434
+ { id: "harnesses", label: "Harnesses" },
2435
+ { id: "models", label: "Models" },
2436
+ { id: "defaults", label: "Defaults" },
2437
+ { id: "remote", label: "Remote" },
2438
+ { id: "advanced", label: "Advanced" }
2439
+ ];
2440
+ var CONFIG_HTML = `<!DOCTYPE html>
2441
+ <html lang="en">
2442
+ <head>
2443
+ <meta charset="UTF-8">
2444
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2445
+ <title>agentproto config</title>
2446
+ <style>
2447
+ /* Theme tokens: prefer a live --vscode-* var (VS Code webview host), else
2448
+ the brand-neutral standalone default, split light/dark by
2449
+ prefers-color-scheme \u2014 same mapping convention as work-board/ui/style.css.
2450
+ No JS theme detection: a host that swaps its theme live is never frozen. */
2451
+ :root{
2452
+ --cfg-bg: var(--vscode-editor-background, #f5f6f3);
2453
+ --cfg-surface: var(--vscode-sideBar-background, #ffffff);
2454
+ --cfg-surface-2: var(--vscode-editorWidget-background, #eef0ec);
2455
+ --cfg-ink: var(--vscode-editor-foreground, #171a1e);
2456
+ --cfg-ink-faint: var(--vscode-descriptionForeground, #6b7280);
2457
+ --cfg-border: var(--vscode-panel-border, #dde0da);
2458
+ --cfg-accent: var(--vscode-button-background, #0f62fe);
2459
+ --cfg-warning: var(--vscode-charts-yellow, #b45309);
2460
+ --cfg-danger: var(--vscode-charts-red, #b91c1c);
2461
+ --cfg-success: var(--vscode-charts-green, #15803d);
2462
+ --cfg-muted-bg: var(--vscode-input-background, #e5e7eb);
2463
+ }
2464
+ @media (prefers-color-scheme: dark){
2465
+ :root{
2466
+ --cfg-bg: var(--vscode-editor-background, #0f1115);
2467
+ --cfg-surface: var(--vscode-sideBar-background, #161a20);
2468
+ --cfg-surface-2: var(--vscode-editorWidget-background, #1c2129);
2469
+ --cfg-ink: var(--vscode-editor-foreground, #e6e9ee);
2470
+ --cfg-ink-faint: var(--vscode-descriptionForeground, #8b93a1);
2471
+ --cfg-border: var(--vscode-panel-border, #2a2f38);
2472
+ --cfg-accent: var(--vscode-button-background, #4589ff);
2473
+ --cfg-warning: var(--vscode-charts-yellow, #d29922);
2474
+ --cfg-danger: var(--vscode-charts-red, #f85149);
2475
+ --cfg-success: var(--vscode-charts-green, #3fb950);
2476
+ --cfg-muted-bg: var(--vscode-input-background, #262b33);
2477
+ }
2478
+ }
2479
+ *{box-sizing:border-box;margin:0;padding:0}
2480
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--cfg-bg);color:var(--cfg-ink)}
2481
+ #app{display:flex;flex-direction:column;height:100%;overflow:hidden}
2482
+
2483
+ #topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}
2484
+ #topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}
2485
+ .chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);padding:3px 9px;border-radius:999px}
2486
+ .chip .d{width:7px;height:7px;border-radius:50%;background:var(--cfg-ink-faint);flex:none}
2487
+ .chip.ok .d{background:var(--cfg-success)}
2488
+ .chip.bad .d{background:var(--cfg-danger)}
2489
+ #refresh-btn{margin-left:auto;background:none;border:1px solid var(--cfg-border);cursor:pointer;color:var(--cfg-ink-faint);font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;font-family:inherit}
2490
+ #refresh-btn:hover{color:var(--cfg-ink);background:var(--cfg-surface-2)}
2491
+
2492
+ #nav{display:flex;gap:4px;padding:8px 14px 0;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}
2493
+ #nav button{background:none;border:none;border-bottom:2px solid transparent;color:var(--cfg-ink-faint);font-size:12px;font-weight:600;font-family:inherit;padding:7px 10px;cursor:pointer}
2494
+ #nav button:hover{color:var(--cfg-ink)}
2495
+ #nav button.active{color:var(--cfg-ink);border-bottom-color:var(--cfg-accent)}
2496
+
2497
+ #pending-banner{margin:8px 14px 0;padding:6px 10px;background:var(--cfg-surface-2);border:1px solid var(--cfg-warning);color:var(--cfg-warning);border-radius:6px;font-size:11px;flex-shrink:0}
2498
+
2499
+ #body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px}
2500
+ .section{display:none;flex-direction:column;gap:12px}
2501
+ .section.active{display:flex}
2502
+
2503
+ .notice{font-size:11.5px;color:var(--cfg-warning);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:6px;padding:6px 10px}
2504
+ .muted{color:var(--cfg-ink-faint)}
2505
+ .muted-note{color:var(--cfg-ink-faint);font-size:11px;padding:6px 10px}
2506
+ .empty{color:var(--cfg-ink-faint);font-size:11.5px;padding:8px}
2507
+
2508
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px}
2509
+ .card{background:var(--cfg-surface);border:1px solid var(--cfg-border);border-radius:8px;overflow:hidden;scroll-margin-top:12px}
2510
+ .card.hl{outline:2px solid var(--cfg-accent);outline-offset:-1px}
2511
+ .card .title{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-weight:700;background:var(--cfg-surface-2);border-bottom:1px solid var(--cfg-border)}
2512
+ .card .title .aside{font-weight:400;color:var(--cfg-ink-faint);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}
2513
+ .card .body{padding:8px 10px}
2514
+ .card dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;align-items:baseline}
2515
+ .card dt{color:var(--cfg-ink-faint);white-space:nowrap;font-size:11.5px}
2516
+ .card dd{text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11.5px}
2517
+ .bar{grid-column:1/3;height:4px;background:var(--cfg-muted-bg);border-radius:2px;overflow:hidden;margin:2px 0 4px}
2518
+ .bar>span{display:block;height:100%;background:var(--cfg-accent)}
2519
+ .bar.warn>span{background:var(--cfg-warning)}
2520
+
2521
+ .sect-h{font-size:11px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
2522
+ .pillbar{display:flex;gap:4px}
2523
+ .pillbar button{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink-faint);font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:999px;cursor:pointer;font-family:inherit}
2524
+ .pillbar button.active{color:var(--cfg-ink);border-color:var(--cfg-accent)}
2525
+ label.inline{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--cfg-ink-faint)}
2526
+ select,input[type=text],input[type=password],input[type=number],textarea{background:var(--cfg-surface-2);color:var(--cfg-ink);border:1px solid var(--cfg-border);border-radius:5px;padding:3px 7px;font-size:11px;font-family:inherit}
2527
+ input:disabled,select:disabled,textarea:disabled{opacity:.5;cursor:not-allowed}
2528
+
2529
+ .tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:5px;padding:1px 6px;color:var(--cfg-ink-faint)}
2530
+ .tag.ok{color:var(--cfg-success);border-color:var(--cfg-success)}
2531
+ .tag.bad{color:var(--cfg-danger);border-color:var(--cfg-danger)}
2532
+ .tag.warn-tag{color:var(--cfg-warning);border-color:var(--cfg-warning)}
2533
+ a.link{color:var(--cfg-accent);text-decoration:none;cursor:pointer}
2534
+ a.link:hover{text-decoration:underline}
2535
+
2536
+ table{width:100%;border-collapse:collapse}
2537
+ th{font-size:10px;font-weight:700;color:var(--cfg-ink-faint);text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:4px 8px;border-bottom:1px solid var(--cfg-border)}
2538
+ td{font-size:11.5px;padding:5px 8px;border-bottom:1px solid var(--cfg-surface-2);vertical-align:middle}
2539
+ tr.hl td{background:var(--cfg-surface-2)}
2540
+ td.matrix-cell{text-align:center;cursor:pointer}
2541
+ td.matrix-cell.disabled{opacity:.35;cursor:default}
2542
+
2543
+ .btn-sm{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);color:var(--cfg-ink);font-size:10.5px;font-weight:600;padding:3px 8px;border-radius:5px;cursor:pointer;font-family:inherit}
2544
+ .btn-sm:hover{border-color:var(--cfg-accent)}
2545
+ .btn-sm:disabled{opacity:.5;cursor:not-allowed}
2546
+ .btn-sm.danger{color:var(--cfg-danger);border-color:var(--cfg-danger)}
2547
+ .btn-sm.confirm{background:var(--cfg-danger);color:#fff;border-color:var(--cfg-danger)}
2548
+ .btn-sm.primary{color:var(--cfg-accent);border-color:var(--cfg-accent)}
2549
+
2550
+ .cfg-row{display:contents}
2551
+ .cfg-help{font-size:10.5px}
2552
+ .cfg-err{color:var(--cfg-danger);font-size:10.5px}
2553
+
2554
+ .form-panel{background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:10px;margin-top:6px;display:flex;flex-direction:column;gap:6px}
2555
+ .form-panel .field{display:flex;flex-direction:column;gap:2px;font-size:11px}
2556
+ .form-panel .field label{color:var(--cfg-ink-faint);font-size:10.5px}
2557
+ .form-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
2558
+
2559
+ .reveal-box{background:var(--cfg-surface-2);border:1px dashed var(--cfg-warning);border-radius:8px;padding:10px;font-size:11px;display:flex;flex-direction:column;gap:6px}
2560
+ .reveal-box code{display:block;word-break:break-all;background:var(--cfg-muted-bg);padding:4px 6px;border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:10.5px}
2561
+
2562
+ .mm-list{max-height:160px;overflow-y:auto;margin-top:4px;padding:4px;background:var(--cfg-muted-bg);border-radius:5px}
2563
+ .wallet-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
2564
+
2565
+ #toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--cfg-surface-2);border:1px solid var(--cfg-border);border-radius:8px;padding:8px 14px;font-size:11.5px;display:none;max-width:80%;z-index:10}
2566
+ #toast.err{border-color:var(--cfg-danger);color:var(--cfg-danger)}
2567
+ </style>
2568
+ </head>
2569
+ <body>
2570
+ <div id="app">
2571
+ <div id="topbar">
2572
+ <h1>agentproto config</h1>
2573
+ <span class="chip" id="daemon-chip"><span class="d"></span><span id="daemon-txt">daemon&hellip;</span></span>
2574
+ <button id="refresh-btn" title="Refresh" type="button">&#8635; refresh</button>
2575
+ </div>
2576
+ <div id="nav"></div>
2577
+ <div class="notice" id="pending-banner" style="display:none"></div>
2578
+ <div id="body">
2579
+ <div class="notice" id="notfound" style="display:none"></div>
2580
+ <div class="section" data-section="wallets" id="sec-wallets"></div>
2581
+ <div class="section" data-section="harnesses" id="sec-harnesses"></div>
2582
+ <div class="section" data-section="models" id="sec-models"></div>
2583
+ <div class="section" data-section="defaults" id="sec-defaults"></div>
2584
+ <div class="section" data-section="remote" id="sec-remote"></div>
2585
+ <div class="section" data-section="advanced" id="sec-advanced"></div>
2586
+ </div>
2587
+ </div>
2588
+ <div id="toast"></div>
2589
+ <script>
2590
+ var callTool = null;
2591
+ var updateModelContext = null;
2592
+ var onTeardownFn = null;
2593
+ var APP_ID = ${JSON.stringify(APP_ID4)};
2594
+ var SECTIONS = ${JSON.stringify(SECTIONS)};
2595
+
2596
+ // \u2500\u2500 deep-link fragment parser (fragment.ts, embedded verbatim so the
2597
+ // browser runs the exact function fragment.test.ts exercises) \u2500\u2500
2598
+ ${parseConfigFragment.toString()}
2599
+ ${buildConfigFragment.toString()}
2600
+ ${shouldApplyIncomingView.toString()}
2601
+
2602
+ // app_tool_call wraps its dispatch result at least once (its own text-result
2603
+ // body is the JSON-stringified inner MCP tool result); peel back through
2604
+ // nested {content:[{type:"text",text:...}]} shells until the payload stops
2605
+ // looking like one \u2014 same convention as ops-panel/session-viewer.
2606
+ function unwrapText(result) {
2607
+ var cur = result;
2608
+ for (var i = 0; i < 4; i++) {
2609
+ if (typeof cur === "string") {
2610
+ try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
2611
+ }
2612
+ if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
2613
+ cur = cur.content[0].text;
2614
+ continue;
2615
+ }
2616
+ break;
2617
+ }
2618
+ return cur;
2619
+ }
2620
+
2621
+ function callApp(tool, args) {
2622
+ return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
2623
+ }
2624
+
2625
+ // Never throws: every card loads independently, so one failing tool must
2626
+ // never crash another card's render. An "unknown daemon tool" failure (a
2627
+ // write tool, or config_get/config_set, on a daemon that predates them) is
2628
+ // distinguished from any other failure so the caller can render a muted
2629
+ // feature-detect note instead of an error. Used for BOTH reads and writes:
2630
+ // every write tool here answers either a JSON object (success) or a plain
2631
+ // string (its own error text), the same two shapes a read tool answers.
2632
+ function loadTool(tool, args) {
2633
+ return callApp(tool, args).then(function (d) {
2634
+ if (typeof d === "string") {
2635
+ return { ok: false, message: d, unknown: /unknown daemon tool/.test(d) };
2636
+ }
2637
+ if (d && typeof d === "object" && typeof d.error === "string") {
2638
+ return { ok: false, message: d.error, unknown: /unknown daemon tool/.test(d.error) };
2639
+ }
2640
+ return { ok: true, data: d };
2641
+ }).catch(function (e) {
2642
+ var msg = (e && e.message) || String(e);
2643
+ return { ok: false, message: msg, unknown: false };
2644
+ });
2645
+ }
2646
+
2647
+ function escHtml(s) {
2648
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2649
+ }
2650
+
2651
+ function humanize(s) { return String(s || "").replace(/[-_]+/g, " "); }
2652
+
2653
+ function formatTokens(n) {
2654
+ if (n == null || !isFinite(n)) return "";
2655
+ var trim = function (t) { return t.replace(/\\.0$/, ""); };
2656
+ if (n >= 1000000) return trim((n / 1000000).toFixed(1)) + "M";
2657
+ if (n >= 1000) return trim((n / 1000).toFixed(1)) + "k";
2658
+ return String(n);
2659
+ }
2660
+
2661
+ function formatCost(usd) {
2662
+ if (usd == null || !isFinite(usd)) return "";
2663
+ if (usd > 0 && usd < 0.01) return "<$0.01";
2664
+ return "$" + usd.toFixed(2);
2665
+ }
2666
+
2667
+ function formatDate(iso) {
2668
+ if (!iso) return "";
2669
+ var d = new Date(iso);
2670
+ if (isNaN(d.getTime())) return "";
2671
+ return d.toLocaleString();
2672
+ }
2673
+
2674
+ function formatMs(ms) {
2675
+ if (ms == null || !isFinite(ms)) return "";
2676
+ if (ms >= 60000) return Math.round(ms / 60000) + "m";
2677
+ if (ms >= 1000) return Math.round(ms / 1000) + "s";
2678
+ return ms + "ms";
2679
+ }
2680
+
2681
+ function fmtRelative(iso) {
2682
+ if (!iso) return "";
2683
+ var ms = Date.now() - new Date(iso).getTime();
2684
+ if (!isFinite(ms)) return "";
2685
+ var s = Math.round(ms / 1000);
2686
+ if (s < 5) return "just now";
2687
+ if (s < 60) return s + "s ago";
2688
+ var m = Math.round(s / 60);
2689
+ if (m < 60) return m + "m ago";
2690
+ var h = Math.round(m / 60);
2691
+ if (h < 24) return h + "h ago";
2692
+ return Math.round(h / 24) + "d ago";
2693
+ }
2694
+
2695
+ var BILLING_LABELS = { "oauth-bearer": "Subscription", "api-key": "API key" };
2696
+
2697
+ // \u2500\u2500 card builders (session-header-cards.tsx visual language: card surface,
2698
+ // title strip w/ muted aside, two-col dl grid, 4px progress bar) \u2500\u2500
2699
+ function cardOpen(id, title, aside) {
2700
+ return '<div class="card" data-card-id="' + escHtml(id) + '">'
2701
+ + '<div class="title"><span>' + escHtml(title) + '</span>'
2702
+ + (aside ? '<span class="aside">' + aside + '</span>' : '')
2703
+ + '</div><div class="body"><dl>';
2704
+ }
2705
+ function cardClose() { return '</dl></div></div>'; }
2706
+ function row(label, value, title) {
2707
+ return '<dt>' + escHtml(label) + '</dt><dd' + (title ? ' title="' + escHtml(title) + '"' : '') + '>' + value + '</dd>';
2708
+ }
2709
+ function bar(pct) {
2710
+ var p = Math.max(0, Math.min(100, pct));
2711
+ return '<div class="bar' + (p >= 80 ? ' warn' : '') + '"><span style="width:' + p + '%"></span></div>';
2712
+ }
2713
+
2714
+ var toastTimer = null;
2715
+ function toast(msg, isErr) {
2716
+ var el = document.getElementById("toast");
2717
+ el.textContent = msg;
2718
+ el.className = isErr ? "err" : "";
2719
+ el.style.display = "block";
2720
+ if (toastTimer) clearTimeout(toastTimer);
2721
+ toastTimer = setTimeout(function () { el.style.display = "none"; }, isErr ? 6000 : 3000);
2722
+ }
2723
+
2724
+ // Two-click confirm (ops-panel's pattern): first click arms the button
2725
+ // ("sure?"), a second click within 4s fires the callback; anything else
2726
+ // disarms it. Shared by every destructive action in this panel (delete
2727
+ // wallet, delete preset, disable remote, revoke pairing).
2728
+ var armed = {};
2729
+ function confirmClick(key, btn, fn) {
2730
+ if (armed[key]) {
2731
+ clearTimeout(armed[key].t);
2732
+ delete armed[key];
2733
+ fn();
2734
+ return;
2735
+ }
2736
+ var prev = btn.textContent;
2737
+ btn.classList.add("confirm");
2738
+ btn.textContent = "sure?";
2739
+ armed[key] = { t: setTimeout(function () {
2740
+ delete armed[key];
2741
+ btn.classList.remove("confirm");
2742
+ btn.textContent = prev;
2743
+ }, 4000) };
2744
+ }
2745
+
2746
+ function copyToClipboard(text) {
2747
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2748
+ navigator.clipboard.writeText(text).then(function () { toast("Copied.", false); }, function () { toast("Copy failed.", true); });
2749
+ return;
2750
+ }
2751
+ try {
2752
+ var ta = document.createElement("textarea");
2753
+ ta.value = text;
2754
+ ta.style.position = "fixed";
2755
+ ta.style.opacity = "0";
2756
+ document.body.appendChild(ta);
2757
+ ta.focus();
2758
+ ta.select();
2759
+ document.execCommand("copy");
2760
+ document.body.removeChild(ta);
2761
+ toast("Copied.", false);
2762
+ } catch (e) {
2763
+ toast("Copy failed.", true);
2764
+ }
2765
+ }
2766
+ function wireCopyButtons(container, valuesByKey) {
2767
+ var btns = container.querySelectorAll("[data-copy]");
2768
+ for (var i = 0; i < btns.length; i++) {
2769
+ (function (btn) {
2770
+ btn.addEventListener("click", function () {
2771
+ var key = btn.getAttribute("data-copy");
2772
+ var value = valuesByKey[key];
2773
+ if (value) copyToClipboard(value);
2774
+ });
2775
+ })(btns[i]);
2776
+ }
2777
+ }
2778
+
2779
+ // ============================================================
2780
+ // generic config_get / config_set field editor \u2014 shared by Defaults,
2781
+ // per-harness adapter defaults, and titler.model (Models section).
2782
+ // ============================================================
2783
+
2784
+ // Last revision seen from ANY config_get/config_set response, reused on
2785
+ // every write (the spec: "always pass the last revision"). A daemon that
2786
+ // predates config_get/config_set never sets this; config_set is simply
2787
+ // called without a revision then (server treats that as unconditional).
2788
+ var configRevision = null;
2789
+
2790
+ // path -> true for every currently-known pending-restart key, merged from
2791
+ // every config_get response seen anywhere in the app. Drives the persistent
2792
+ // top banner \u2014 never restarts anything itself, only tells the user to.
2793
+ var pendingRestartMap = {};
2794
+
2795
+ function notePendingRestart(rows) {
2796
+ var changed = false;
2797
+ for (var i = 0; i < (rows || []).length; i++) {
2798
+ var r = rows[i];
2799
+ if (r.pendingRestart) {
2800
+ if (!pendingRestartMap[r.path]) changed = true;
2801
+ pendingRestartMap[r.path] = true;
2802
+ } else if (pendingRestartMap[r.path]) {
2803
+ delete pendingRestartMap[r.path];
2804
+ changed = true;
2805
+ }
2806
+ }
2807
+ if (changed) renderPendingBanner();
2808
+ }
2809
+
2810
+ function renderPendingBanner() {
2811
+ var el = document.getElementById("pending-banner");
2812
+ if (!el) return;
2813
+ var keys = [];
2814
+ for (var k in pendingRestartMap) if (Object.prototype.hasOwnProperty.call(pendingRestartMap, k)) keys.push(k);
2815
+ if (keys.length === 0) {
2816
+ el.style.display = "none";
2817
+ el.textContent = "";
2818
+ return;
2819
+ }
2820
+ keys.sort();
2821
+ el.style.display = "block";
2822
+ el.textContent = "Restart the daemon yourself to apply: " + keys.join(", ") + ". This app never restarts it for you.";
2823
+ }
2824
+
2825
+ // Type inference for a config_get row: config-tools.ts's describeKey
2826
+ // spreads deriveValueType(entry.schema) onto every row, so row.valueType
2827
+ // ("boolean" | "number" | "integer" | "string" | "enum" | "string[]" |
2828
+ // "object" | "unknown") plus row.enum / row.min / row.max are the registry's
2829
+ // OWN answer and win outright. The JS-type inference below is only a
2830
+ // fallback for a daemon old enough to predate that field entirely (absent,
2831
+ // not merely "unknown" \u2014 "unknown" is itself a real, present answer for a
2832
+ // schema deriveValueType can't type, e.g. a mixed union, and must NOT be
2833
+ // re-guessed from the current value).
2834
+ function inferValueType(row) {
2835
+ if (row.valueType) return row.valueType;
2836
+ var v = row.effective !== undefined ? row.effective : row.value;
2837
+ if (v === undefined) v = row.default;
2838
+ if (typeof v === "boolean") return "boolean";
2839
+ if (typeof v === "number") return "number";
2840
+ if (typeof v === "string") return "string";
2841
+ if (Array.isArray(v)) {
2842
+ var allStr = true;
2843
+ for (var i = 0; i < v.length; i++) if (typeof v[i] !== "string") allStr = false;
2844
+ return allStr ? "string[]" : "object";
2845
+ }
2846
+ if (v && typeof v === "object") return "object";
2847
+ return "unknown";
2848
+ }
2849
+
2850
+ // defaults.spawn.browser is a union of the literals "headless" and false
2851
+ // (config-schema.ts) - deriveValueType's enum-from-union heuristic requires
2852
+ // EVERY literal to be a string, and false isn't one, so this one specific
2853
+ // key comes back "unknown" rather than "enum" (verified against
2854
+ // packages/runtime/src/config-tools.ts's deriveValueType). A checkbox whose
2855
+ // two states are not plain true/false is still the right control for it, so
2856
+ // it keeps its own tiny special case rather than a general boolean-like-enum
2857
+ // mechanism nothing else needs.
2858
+ var BOOL_LIKE_FIELDS = {
2859
+ "defaults.spawn.browser": { on: "headless", off: false, onLabel: "headless" },
2860
+ };
2861
+
2862
+ function pathId(path) { return "cfg-" + String(path).replace(/[^a-zA-Z0-9]/g, "-"); }
2863
+
2864
+ function writableReasonText(row) {
2865
+ if (row.secret) return "secret, set via wallets or the CLI";
2866
+ return "read-only, edit the config file by hand";
2867
+ }
2868
+
2869
+ function configDisplayValue(row) {
2870
+ if (row.secret) {
2871
+ if (row.secret.set === false) return "not set";
2872
+ if (typeof row.secret.set === "boolean") {
2873
+ var bits = [];
2874
+ if (row.secret.fingerprint) bits.push(row.secret.fingerprint);
2875
+ if (row.secret.last4) bits.push(row.secret.last4);
2876
+ return bits.length ? bits.join(" / ") : "set";
2877
+ }
2878
+ var parts = [];
2879
+ for (var k in row.secret) {
2880
+ if (Object.prototype.hasOwnProperty.call(row.secret, k)) {
2881
+ parts.push(k + ": " + (row.secret[k] && row.secret[k].set ? "set" : "not set"));
2882
+ }
2883
+ }
2884
+ return parts.length ? parts.join(", ") : "no keys";
2885
+ }
2886
+ return row.effective !== undefined ? row.effective : row.value;
2887
+ }
2888
+
2889
+ function numberBoundAttrs(row) {
2890
+ var attrs = "";
2891
+ if (typeof row.min === "number") attrs += ' min="' + row.min + '"';
2892
+ if (typeof row.max === "number") attrs += ' max="' + row.max + '"';
2893
+ return attrs;
2894
+ }
2895
+
2896
+ function configControlHtml(row) {
2897
+ var vt = inferValueType(row);
2898
+ var val = row.effective !== undefined ? row.effective : (row.value !== undefined ? row.value : row.default);
2899
+ var boolLike = BOOL_LIKE_FIELDS[row.path];
2900
+ var disabledAttr = row.writable ? "" : " disabled";
2901
+ var titleAttr = row.writable ? "" : ' title="' + escHtml(writableReasonText(row)) + '"';
2902
+ if (boolLike) {
2903
+ var checked = val === boolLike.on;
2904
+ return '<label class="inline"><input type="checkbox" data-cfg-bool-like="' + escHtml(row.path) + '"' + (checked ? " checked" : "") + disabledAttr + titleAttr + '> ' + escHtml(boolLike.onLabel || "on") + '</label>';
2905
+ }
2906
+ if (vt === "boolean") {
2907
+ return '<input type="checkbox" data-cfg-auto="' + escHtml(row.path) + '"' + (val ? " checked" : "") + disabledAttr + titleAttr + '>';
2908
+ }
2909
+ if (vt === "enum") {
2910
+ var enumOpts = row.enum || [];
2911
+ var out = '<select data-cfg-auto="' + escHtml(row.path) + '"' + disabledAttr + titleAttr + '>';
2912
+ for (var i = 0; i < enumOpts.length; i++) {
2913
+ out += '<option value="' + escHtml(enumOpts[i]) + '"' + (enumOpts[i] === val ? " selected" : "") + '>' + escHtml(enumOpts[i]) + '</option>';
2914
+ }
2915
+ return out + '</select>';
2916
+ }
2917
+ if (vt === "number" || vt === "integer") {
2918
+ return '<input type="number" data-cfg-auto="' + escHtml(row.path) + '" value="' + escHtml(val == null ? "" : String(val)) + '"'
2919
+ + (vt === "integer" ? ' step="1"' : "") + numberBoundAttrs(row) + disabledAttr + titleAttr + '>';
2920
+ }
2921
+ if (vt === "string[]") {
2922
+ return '<input type="text" data-cfg-input="' + escHtml(row.path) + '" value="' + escHtml((val || []).join(", ")) + '" placeholder="comma, separated, list"' + disabledAttr + titleAttr + '>'
2923
+ + ' <button type="button" class="btn-sm" data-cfg-save="' + escHtml(row.path) + '"' + disabledAttr + '>save</button>';
2924
+ }
2925
+ if (vt === "object" || vt === "unknown") {
2926
+ // "unknown" is a real, present answer (a schema deriveValueType can't
2927
+ // type, e.g. a mixed boolean/object union) \u2014 a raw JSON editor is the
2928
+ // one control that can represent any of those shapes without guessing.
2929
+ return '<div><textarea data-cfg-input="' + escHtml(row.path) + '" rows="4" style="width:100%;font-family:Menlo,Monaco,monospace;font-size:11px"' + disabledAttr + titleAttr + '>' + escHtml(JSON.stringify(val === undefined ? null : val, null, 2)) + '</textarea>'
2930
+ + ' <button type="button" class="btn-sm" data-cfg-save="' + escHtml(row.path) + '"' + disabledAttr + '>save</button></div>';
2931
+ }
2932
+ return '<input type="text" data-cfg-auto="' + escHtml(row.path) + '" value="' + escHtml(val == null ? "" : String(val)) + '"' + disabledAttr + titleAttr + '>';
2933
+ }
2934
+
2935
+ // One dl "row" for a config key. Wrapped in a display:contents div so the
2936
+ // dt/dd pairs stay direct grid children (the 2-col dl grid) while still
2937
+ // being addressable as one group for wiring/lookup. A writable:false row
2938
+ // still renders a real control (checkbox/select/input), just disabled,
2939
+ // with the reason in its title, never a second, ad hoc read-only widget.
2940
+ function configFieldHtml(row) {
2941
+ var label = row.label || humanize(String(row.path).split(".").slice(-1)[0]);
2942
+ var badge = '<span class="tag">' + escHtml(row.apply) + '</span>';
2943
+ var extra = "";
2944
+ if (row.source === "env" && row.envOverride) extra += ' <span class="tag bad">env ' + escHtml(row.envOverride) + '</span>';
2945
+ if (row.pendingRestart) extra += ' <span class="tag warn-tag">restart pending</span>';
2946
+
2947
+ var dd = "";
2948
+ if (row.secret) {
2949
+ dd += escHtml(String(configDisplayValue(row)));
2950
+ } else {
2951
+ dd += configControlHtml(row);
2952
+ }
2953
+ dd += " " + badge + extra;
2954
+ if (row.writable && !row.secret && row.value !== undefined) {
2955
+ dd += ' <button type="button" class="btn-sm" data-cfg-reset="' + escHtml(row.path) + '" title="Reset to default">reset</button>';
2956
+ }
2957
+
2958
+ var html = '<div class="cfg-row" data-cfg-path="' + escHtml(row.path) + '">'
2959
+ + '<dt title="' + escHtml(row.path) + '">' + escHtml(label) + '</dt><dd>' + dd + '</dd>';
2960
+ if (row.help) html += '<dt></dt><dd class="cfg-help muted">' + escHtml(row.help) + '</dd>';
2961
+ html += '<dt></dt><dd class="cfg-err" data-cfg-err="' + escHtml(row.path) + '" style="display:none"></dd>';
2962
+ html += '</div>';
2963
+ return html;
2964
+ }
2965
+
2966
+ function buildConfigCardHtml(cardId, title, rows) {
2967
+ var body = "";
2968
+ for (var i = 0; i < rows.length; i++) body += configFieldHtml(rows[i]);
2969
+ return '<div class="card" data-card-id="' + escHtml(cardId) + '"><div class="body"><dl>' + body + '</dl></div></div>';
2970
+ }
2971
+
2972
+ function extractConfigSetErrorCode(message) {
2973
+ var s = String(message || "");
2974
+ var start = s.indexOf("[");
2975
+ var end = start === -1 ? -1 : s.indexOf("]", start);
2976
+ if (start === -1 || end === -1) return "";
2977
+ return s.slice(start + 1, end);
2978
+ }
2979
+
2980
+ function describeConfigSetError(message) {
2981
+ var code = extractConfigSetErrorCode(message);
2982
+ var HUMAN = {
2983
+ invalid_input: "That request was not valid.",
2984
+ unknown_key: "This daemon does not know that setting.",
2985
+ not_writable: "This setting cannot be changed here.",
2986
+ invalid_value: "That value is not valid for this setting.",
2987
+ invalid_config: "That change would leave the config file invalid.",
2988
+ stale_revision: "This section changed elsewhere. Reloading.",
2989
+ };
2990
+ return HUMAN[code] || message;
2991
+ }
2992
+
2993
+ // Attach listeners for every config control inside "container". rowsByPath
2994
+ // supplies the row metadata a save/reset needs (valueType for parsing).
2995
+ // onSaved(path) fires after a successful write (path just written);
2996
+ // onStale() fires on a stale_revision rejection (reload the section).
2997
+ function wireConfigRows(container, rowsByPath, onSaved, onStale) {
2998
+ function submit(path, value, isUnset) {
2999
+ var args = isUnset ? { key: path, unset: true, revision: configRevision } : { key: path, value: value, revision: configRevision };
3000
+ return loadTool("config_set", args).then(function (r) {
3001
+ if (!r.ok) {
3002
+ var stale = extractConfigSetErrorCode(r.message) === "stale_revision";
3003
+ toast(describeConfigSetError(r.message), true);
3004
+ if (stale && onStale) onStale();
3005
+ return;
3006
+ }
3007
+ var data = r.data;
3008
+ if (data && data.revision) configRevision = data.revision;
3009
+ var note = "Saved.";
3010
+ if (data && data.applied === "restart-required") note = "Saved. Restart required to apply.";
3011
+ if (data && data.shadowedByEnv) note += " Overridden by " + data.shadowedByEnv + ".";
3012
+ toast(note, false);
3013
+ if (onSaved) onSaved(path);
3014
+ });
3015
+ }
3016
+
3017
+ var autos = container.querySelectorAll("[data-cfg-auto]");
3018
+ for (var i = 0; i < autos.length; i++) {
3019
+ (function (el) {
3020
+ el.addEventListener("change", function () {
3021
+ var path = el.getAttribute("data-cfg-auto");
3022
+ var value;
3023
+ if (el.type === "checkbox") value = el.checked;
3024
+ else if (el.type === "number") value = el.value === "" ? null : Number(el.value);
3025
+ else value = el.value;
3026
+ submit(path, value, false);
3027
+ });
3028
+ })(autos[i]);
3029
+ }
3030
+
3031
+ var boolLikes = container.querySelectorAll("[data-cfg-bool-like]");
3032
+ for (var b = 0; b < boolLikes.length; b++) {
3033
+ (function (el) {
3034
+ el.addEventListener("change", function () {
3035
+ var path = el.getAttribute("data-cfg-bool-like");
3036
+ var spec = BOOL_LIKE_FIELDS[path];
3037
+ submit(path, el.checked ? spec.on : spec.off, false);
3038
+ });
3039
+ })(boolLikes[b]);
3040
+ }
3041
+
3042
+ var saves = container.querySelectorAll("[data-cfg-save]");
3043
+ for (var s = 0; s < saves.length; s++) {
3044
+ (function (btn) {
3045
+ btn.addEventListener("click", function () {
3046
+ var path = btn.getAttribute("data-cfg-save");
3047
+ var row = rowsByPath[path];
3048
+ var input = container.querySelector('[data-cfg-input="' + cssEscape(path) + '"]');
3049
+ var errEl = container.querySelector('[data-cfg-err="' + cssEscape(path) + '"]');
3050
+ if (errEl) { errEl.style.display = "none"; errEl.textContent = ""; }
3051
+ var vt = row ? inferValueType(row) : "string";
3052
+ var value;
3053
+ if (vt === "string[]") {
3054
+ value = input.value.split(",").map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });
3055
+ } else if (vt === "object" || vt === "unknown") {
3056
+ try {
3057
+ value = JSON.parse(input.value);
3058
+ } catch (e) {
3059
+ if (errEl) { errEl.style.display = "block"; errEl.textContent = "Invalid JSON: " + e.message; }
3060
+ return;
3061
+ }
3062
+ } else {
3063
+ value = input.value;
3064
+ }
3065
+ submit(path, value, false);
3066
+ });
3067
+ })(saves[s]);
3068
+ }
3069
+
3070
+ var resets = container.querySelectorAll("[data-cfg-reset]");
3071
+ for (var r = 0; r < resets.length; r++) {
3072
+ (function (btn) {
3073
+ btn.addEventListener("click", function () {
3074
+ submit(btn.getAttribute("data-cfg-reset"), undefined, true);
3075
+ });
3076
+ })(resets[r]);
3077
+ }
3078
+ }
3079
+
3080
+ // ============================================================
3081
+ // nav + deep-link routing
3082
+ // ============================================================
3083
+
3084
+ var current = { section: SECTIONS[0].id, id: undefined, sub: undefined };
3085
+ var loaded = {}; // section -> true once its data has been fetched at least once
3086
+
3087
+ function renderNav() {
3088
+ var el = document.getElementById("nav");
3089
+ var html = "";
3090
+ for (var i = 0; i < SECTIONS.length; i++) {
3091
+ html += '<button data-section="' + SECTIONS[i].id + '" class="' + (SECTIONS[i].id === current.section ? "active" : "") + '">' + escHtml(SECTIONS[i].label) + '</button>';
3092
+ }
3093
+ el.innerHTML = html;
3094
+ var btns = el.querySelectorAll("button[data-section]");
3095
+ for (var k = 0; k < btns.length; k++) {
3096
+ (function (btn) {
3097
+ btn.addEventListener("click", function () {
3098
+ location.hash = buildConfigFragment(btn.getAttribute("data-section"));
3099
+ });
3100
+ })(btns[k]);
3101
+ }
3102
+ }
3103
+
3104
+ function showSection(sectionId) {
3105
+ var els = document.querySelectorAll(".section");
3106
+ for (var i = 0; i < els.length; i++) {
3107
+ els[i].classList.toggle("active", els[i].getAttribute("data-section") === sectionId);
3108
+ }
3109
+ var btns = document.querySelectorAll("#nav button[data-section]");
3110
+ for (var j = 0; j < btns.length; j++) {
3111
+ btns[j].classList.toggle("active", btns[j].getAttribute("data-section") === sectionId);
3112
+ }
3113
+ }
3114
+
3115
+ // Scroll to + highlight the card matching 'id' within the active section's
3116
+ // container. Unknown id means the "not found" banner (never a blank page).
3117
+ function applyDeepLink(sectionId, id, sub) {
3118
+ var notfound = document.getElementById("notfound");
3119
+ notfound.style.display = "none";
3120
+ var prevHl = document.querySelectorAll(".card.hl, tr.hl");
3121
+ for (var p = 0; p < prevHl.length; p++) prevHl[p].classList.remove("hl");
3122
+ if (id === undefined) return;
3123
+ var container = document.getElementById("sec-" + sectionId);
3124
+ if (!container) return;
3125
+ var targetId = sectionId === "remote" && id === "pairing" ? "pairing" : id;
3126
+ var target = container.querySelector('[data-card-id="' + cssEscape(targetId) + '"]');
3127
+ if (!target) {
3128
+ notfound.textContent = "not found: " + id + (sub ? "/" + sub : "");
3129
+ notfound.style.display = "block";
3130
+ return;
3131
+ }
3132
+ target.classList.add("hl");
3133
+ scrollIntoViewSafe(target);
3134
+ if (sub) {
3135
+ var row = target.querySelector('[data-row-id="' + cssEscape(sub) + '"]');
3136
+ if (row) {
3137
+ row.classList.add("hl");
3138
+ scrollIntoViewSafe(row);
3139
+ }
3140
+ }
3141
+ }
3142
+
3143
+ // Not every embedding host implements scrollIntoView on a sandboxed/minimal
3144
+ // DOM \u2014 highlighting must still work even when scrolling can't.
3145
+ function scrollIntoViewSafe(el) {
3146
+ if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "nearest" });
3147
+ }
3148
+
3149
+ // CSS.escape isn't guaranteed in every sandboxed webview \u2014 a minimal,
3150
+ // dependency-free fallback for the characters our own ids ever contain.
3151
+ function cssEscape(s) {
3152
+ return String(s).replace(/[^a-zA-Z0-9_-]/g, function (c) {
3153
+ return "\\\\" + c.charCodeAt(0).toString(16) + " ";
3154
+ });
3155
+ }
3156
+
3157
+ // Clears every secret this panel ever holds only in memory (a remote-enable
3158
+ // reveal, a pairing offer) \u2014 called on navigating away from #remote and on
3159
+ // host teardown. Never persisted, never in updateModelContext.
3160
+ function clearRemoteSecrets() {
3161
+ remoteReveal = null;
3162
+ pairReveal = null;
3163
+ clearPairCountdown();
3164
+ // The #sec-remote DOM may still show a rendered reveal box from before
3165
+ // this call \u2014 clearing the variable alone does not touch it. Force the
3166
+ // next visit to re-fetch and re-render from scratch (remoteReveal is
3167
+ // null by then, so no reveal box comes back) instead of leaving the
3168
+ // stale secret sitting in a hidden .section until some other event
3169
+ // happens to re-render it.
3170
+ loaded.remote = false;
3171
+ }
3172
+
3173
+ function route(pushBack) {
3174
+ var parsed = parseConfigFragment(location.hash);
3175
+ if (current.section === "remote" && parsed.section !== "remote") {
3176
+ clearRemoteSecrets();
3177
+ }
3178
+ current = parsed;
3179
+ showSection(parsed.section);
3180
+ renderNav();
3181
+ ensureLoaded(parsed.section).then(function () {
3182
+ applyDeepLink(parsed.section, parsed.id, parsed.sub);
3183
+ });
3184
+ if (pushBack) location.hash = buildConfigFragment(parsed.section, parsed.id, parsed.sub);
3185
+ }
3186
+
3187
+ window.addEventListener("hashchange", function () { route(false); });
3188
+
3189
+ // \u2500\u2500 MCP-hosted deep links \u2500\u2500
3190
+ // A browser tab navigates via location.hash directly; an MCP host (Claude
3191
+ // Desktop, Codex, VS Code) has no URL bar for this panel, so it opens
3192
+ // app_ui_config with a view argument instead (plan section 3.4). The bridge
3193
+ // (app-ui-apps.ts) surfaces that as tool input; this turns it into the same
3194
+ // #<section>/<id>/<sub> fragment a browser deep link would use.
3195
+ //
3196
+ // location.hash still wins on the very first route \u2014 an actual browser deep
3197
+ // link must not be overridden by a stale/default tool call. Every
3198
+ // subsequent tool-input notification (a fresh app_ui_config invocation while
3199
+ // the panel is already open) is a live "go to this view" command and always
3200
+ // re-routes.
3201
+ var toolInputCount = 0;
3202
+ var hashPresentAtLoad = !!(location.hash && location.hash !== "#");
3203
+
3204
+ function routeToView(view) {
3205
+ if (typeof view !== "string" || !view) return;
3206
+ var parsed = parseConfigFragment(view);
3207
+ var frag = buildConfigFragment(parsed.section, parsed.id, parsed.sub);
3208
+ if (location.hash === frag) { route(false); } else { location.hash = frag; }
3209
+ }
3210
+
3211
+ function handleToolInput(input) {
3212
+ toolInputCount += 1;
3213
+ if (!shouldApplyIncomingView(toolInputCount, hashPresentAtLoad)) return;
3214
+ routeToView(input && input.view);
3215
+ }
3216
+
3217
+ // ============================================================
3218
+ // section data + render dispatch
3219
+ // ============================================================
3220
+
3221
+ var SECTION_LOADERS = {
3222
+ wallets: loadWallets,
3223
+ harnesses: loadHarnesses,
3224
+ models: loadModels,
3225
+ defaults: loadDefaults,
3226
+ remote: loadRemote,
3227
+ advanced: loadAdvanced,
3228
+ };
3229
+
3230
+ function ensureLoaded(sectionId) {
3231
+ if (loaded[sectionId]) return Promise.resolve();
3232
+ var fn = SECTION_LOADERS[sectionId];
3233
+ if (!fn) return Promise.resolve();
3234
+ return fn().then(function () { loaded[sectionId] = true; });
3235
+ }
3236
+
3237
+ function reloadCurrent() {
3238
+ loaded[current.section] = false;
3239
+ return ensureLoaded(current.section).then(function () {
3240
+ applyDeepLink(current.section, current.id, current.sub);
3241
+ });
3242
+ }
3243
+
3244
+ // ============================================================
3245
+ // Wallets
3246
+ // ============================================================
3247
+
3248
+ var walletsState = {
3249
+ window: "7d",
3250
+ profilesResult: null,
3251
+ rollupResult: null,
3252
+ presetsResult: null,
3253
+ discoveredResult: null,
3254
+ showAddForm: false,
3255
+ showDiscovered: false,
3256
+ expanded: {}, // profileId -> { edit: bool, curate: bool }
3257
+ providerModelsByEndpoint: {}, // endpoint -> catalog_provider_models rows
3258
+ };
3259
+
3260
+ function loadWallets() {
3261
+ return Promise.all([
3262
+ loadTool("auth_profile_list", { full: true }),
3263
+ loadTool("usage_rollup", { window: walletsState.window, groupBy: ["profile"] }),
3264
+ loadTool("harness_preset_list", {}),
3265
+ ]).then(function (results) {
3266
+ walletsState.profilesResult = results[0];
3267
+ walletsState.rollupResult = results[1];
3268
+ walletsState.presetsResult = results[2];
3269
+ renderWallets();
3270
+ });
3271
+ }
3272
+
3273
+ function reloadWalletsWindow(win) {
3274
+ walletsState.window = win;
3275
+ fetchRollup(false);
3276
+ }
3277
+
3278
+ function fetchRollup(probe) {
3279
+ return loadTool("usage_rollup", { window: walletsState.window, groupBy: ["profile"], probe: !!probe }).then(function (r) {
3280
+ walletsState.rollupResult = r;
3281
+ renderWallets();
3282
+ });
3283
+ }
3284
+
3285
+ function byProfileMap(rollupResult) {
3286
+ var map = {};
3287
+ if (rollupResult.ok && rollupResult.data && Array.isArray(rollupResult.data.byProfile)) {
3288
+ for (var i = 0; i < rollupResult.data.byProfile.length; i++) {
3289
+ map[rollupResult.data.byProfile[i].profileRef] = rollupResult.data.byProfile[i];
3290
+ }
3291
+ }
3292
+ return map;
3293
+ }
3294
+
3295
+ function presetsReferencing(profileId) {
3296
+ var out = [];
3297
+ var pr = walletsState.presetsResult;
3298
+ if (!pr || !pr.ok) return out;
3299
+ var presets = (pr.data && pr.data.presets) || [];
3300
+ for (var i = 0; i < presets.length; i++) {
3301
+ if (presets[i].profileRef === profileId) out.push(presets[i]);
3302
+ }
3303
+ return out;
3304
+ }
3305
+
3306
+ function getWalletExpanded(id) {
3307
+ if (!walletsState.expanded[id]) walletsState.expanded[id] = { edit: false, curate: false, curateMode: undefined };
3308
+ return walletsState.expanded[id];
3309
+ }
3310
+
3311
+ function replaceProfileInState(profile) {
3312
+ var pr = walletsState.profilesResult;
3313
+ if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;
3314
+ var list = pr.data.profiles;
3315
+ for (var i = 0; i < list.length; i++) {
3316
+ if (list[i].id === profile.id) { list[i] = profile; return; }
3317
+ }
3318
+ list.push(profile);
3319
+ }
3320
+
3321
+ function removeProfileFromState(id) {
3322
+ var pr = walletsState.profilesResult;
3323
+ if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;
3324
+ pr.data.profiles = pr.data.profiles.filter(function (p) { return p.id !== id; });
3325
+ }
3326
+
3327
+ function renderWallets() {
3328
+ var el = document.getElementById("sec-wallets");
3329
+ var rr = walletsState.rollupResult;
3330
+ var html = '<div class="sect-h">Spend<span class="pillbar" id="wallet-window"></span>'
3331
+ + '<button type="button" class="btn-sm" id="spend-refresh-live">refresh live</button></div>'
3332
+ + '<div class="muted-note" style="padding:0 0 4px">Refreshing live consumes a small sliver of the rate budget for that wallet.</div>';
3333
+
3334
+ if (rr && rr.ok) {
3335
+ var t = rr.data.total || {};
3336
+ html += '<div class="card"><div class="body"><dl>'
3337
+ + row("Window", escHtml(rr.data.window))
3338
+ + (typeof t.spentUsd === "number" ? row("Spend", formatCost(t.spentUsd)) : "")
3339
+ + (typeof t.tokensIn === "number" || typeof t.tokensOut === "number"
3340
+ ? row("Tokens", (typeof t.tokensIn === "number" ? formatTokens(t.tokensIn) + " in" : "") + (typeof t.tokensIn === "number" && typeof t.tokensOut === "number" ? " &middot; " : "") + (typeof t.tokensOut === "number" ? formatTokens(t.tokensOut) + " out" : ""))
3341
+ : "")
3342
+ + (typeof t.unpricedTokens === "number" && t.unpricedTokens > 0 ? row("Unpriced tokens", formatTokens(t.unpricedTokens)) : "")
3343
+ + '</dl></div></div>';
3344
+ } else if (rr) {
3345
+ html += '<div class="muted-note">usage_rollup: ' + escHtml(rr.message) + '</div>';
3346
+ }
3347
+
3348
+ html += '<div class="sect-h">Wallets'
3349
+ + '<button type="button" class="btn-sm" id="wallet-add-toggle">' + (walletsState.showAddForm ? "close" : "+ add wallet") + '</button>'
3350
+ + '<button type="button" class="btn-sm" id="wallet-scan-toggle">' + (walletsState.showDiscovered ? "close" : "find credentials") + '</button>'
3351
+ + '</div>';
3352
+
3353
+ if (walletsState.showAddForm) html += addWalletFormHtml();
3354
+ if (walletsState.showDiscovered) html += discoveredCredentialsHtml();
3355
+
3356
+ html += '<div class="grid" id="wallet-cards"></div>';
3357
+
3358
+ el.innerHTML = html;
3359
+
3360
+ var pillEl = document.getElementById("wallet-window");
3361
+ var windows = ["5h", "7d", "30d"];
3362
+ var pillHtml = "";
3363
+ for (var w = 0; w < windows.length; w++) {
3364
+ pillHtml += '<button data-win="' + windows[w] + '" class="' + (windows[w] === walletsState.window ? "active" : "") + '">' + windows[w] + '</button>';
3365
+ }
3366
+ pillEl.innerHTML = pillHtml;
3367
+ var pillBtns = pillEl.querySelectorAll("button");
3368
+ for (var pb = 0; pb < pillBtns.length; pb++) {
3369
+ (function (btn) {
3370
+ btn.addEventListener("click", function () { reloadWalletsWindow(btn.getAttribute("data-win")); });
3371
+ })(pillBtns[pb]);
3372
+ }
3373
+ document.getElementById("spend-refresh-live").addEventListener("click", function () { fetchRollup(true); });
3374
+
3375
+ document.getElementById("wallet-add-toggle").addEventListener("click", function () {
3376
+ walletsState.showAddForm = !walletsState.showAddForm;
3377
+ renderWallets();
3378
+ });
3379
+ document.getElementById("wallet-scan-toggle").addEventListener("click", function () {
3380
+ walletsState.showDiscovered = !walletsState.showDiscovered;
3381
+ if (walletsState.showDiscovered && !walletsState.discoveredResult) {
3382
+ loadTool("auth_discover_credentials", {}).then(function (r) {
3383
+ walletsState.discoveredResult = r;
3384
+ renderWallets();
3385
+ });
3386
+ return;
3387
+ }
3388
+ renderWallets();
3389
+ });
3390
+
3391
+ wireAddWalletForm(el);
3392
+ wireDiscoveredCredentials(el);
3393
+ renderWalletCards();
3394
+ }
3395
+
3396
+ function addWalletFormHtml() {
3397
+ return '<div class="form-panel" id="wallet-add-form">'
3398
+ + '<div class="form-row">'
3399
+ + '<div class="field"><label>Id</label><input type="text" id="wf-id" placeholder="my-anthropic-key"></div>'
3400
+ + '<div class="field"><label>Endpoint</label><input type="text" id="wf-endpoint" list="wf-endpoint-list" placeholder="anthropic"><datalist id="wf-endpoint-list">'
3401
+ + '<option value="anthropic"><option value="openrouter"><option value="moonshot"><option value="openai"></datalist></div>'
3402
+ + '<div class="field"><label>Method</label><select id="wf-method"><option value="api-key">API key</option><option value="oauth-bearer">Subscription</option></select></div>'
3403
+ + '<div class="field"><label>Label (optional)</label><input type="text" id="wf-label"></div>'
3404
+ + '</div>'
3405
+ + '<div class="form-row">'
3406
+ + '<div class="field"><label>Credential</label><input type="password" id="wf-credential" placeholder="paste secret"></div>'
3407
+ + '<div class="field" id="wf-source-field" style="display:none"><label>Self-refreshing source (alternative)</label><input type="text" id="wf-source" placeholder="claude-code-oauth"></div>'
3408
+ + '<button type="button" class="btn-sm primary" id="wf-submit">Create wallet</button>'
3409
+ + '</div>'
3410
+ + '<div class="cfg-err" id="wf-err" style="display:none"></div>'
3411
+ + '</div>';
3412
+ }
3413
+
3414
+ function wireAddWalletForm(container) {
3415
+ var methodEl = container.querySelector("#wf-method");
3416
+ var sourceField = container.querySelector("#wf-source-field");
3417
+ if (!methodEl) return;
3418
+ methodEl.addEventListener("change", function () {
3419
+ sourceField.style.display = methodEl.value === "oauth-bearer" ? "" : "none";
3420
+ });
3421
+ container.querySelector("#wf-submit").addEventListener("click", function () {
3422
+ var errEl = container.querySelector("#wf-err");
3423
+ errEl.style.display = "none";
3424
+ var id = container.querySelector("#wf-id").value.trim();
3425
+ var endpoint = container.querySelector("#wf-endpoint").value.trim();
3426
+ var method = methodEl.value;
3427
+ var label = container.querySelector("#wf-label").value.trim();
3428
+ var credentialInput = container.querySelector("#wf-credential");
3429
+ var sourceInput = container.querySelector("#wf-source");
3430
+ var credential = credentialInput.value;
3431
+ var source = method === "oauth-bearer" ? sourceInput.value.trim() : "";
3432
+ // Clear the secret from the DOM and this local scope's reference to it
3433
+ // immediately, before the async call even starts.
3434
+ credentialInput.value = "";
3435
+ if (!id || !endpoint) {
3436
+ errEl.style.display = "block";
3437
+ errEl.textContent = "Id and endpoint are required.";
3438
+ credential = "";
3439
+ return;
3440
+ }
3441
+ if (!credential && !source) {
3442
+ errEl.style.display = "block";
3443
+ errEl.textContent = "Give either a credential or a self-refreshing source.";
3444
+ credential = "";
3445
+ return;
3446
+ }
3447
+ var args = { id: id, endpoint: endpoint, method: method };
3448
+ if (label) args.label = label;
3449
+ if (credential) args.credential = credential;
3450
+ else if (source) args.source = source;
3451
+ credential = "";
3452
+ loadTool("auth_profile_create", args).then(function (r) {
3453
+ if (!r.ok) {
3454
+ errEl.style.display = "block";
3455
+ errEl.textContent = r.message;
3456
+ return;
3457
+ }
3458
+ toast("Wallet created.", false);
3459
+ walletsState.showAddForm = false;
3460
+ loadWallets();
3461
+ });
3462
+ });
3463
+ }
3464
+
3465
+ function discoveredCredentialsHtml() {
3466
+ var dr = walletsState.discoveredResult;
3467
+ var html = '<div class="card"><div class="body">';
3468
+ if (!dr) {
3469
+ html += '<div class="muted-note">scanning&hellip;</div>';
3470
+ } else if (!dr.ok) {
3471
+ html += '<div class="muted-note">auth_discover_credentials: ' + escHtml(dr.message) + '</div>';
3472
+ } else {
3473
+ var creds = (dr.data && dr.data.credentials) || [];
3474
+ if (creds.length === 0) {
3475
+ html += '<div class="empty">No importable credentials found on this host.</div>';
3476
+ } else {
3477
+ html += '<table><thead><tr><th>Endpoint</th><th>Method</th><th>Origin</th><th>Hint</th><th></th></tr></thead><tbody>';
3478
+ for (var i = 0; i < creds.length; i++) {
3479
+ var c = creds[i];
3480
+ var defaultId = c.origin + "-" + c.endpoint;
3481
+ html += '<tr><td>' + escHtml(c.endpoint) + '</td><td>' + escHtml(BILLING_LABELS[c.method] || c.method) + '</td><td>' + escHtml(c.origin) + '</td><td>' + escHtml(c.hint) + '</td>'
3482
+ + '<td><input type="text" class="dc-id" data-dc-idx="' + i + '" value="' + escHtml(defaultId) + '" style="width:120px">'
3483
+ + ' <button type="button" class="btn-sm" data-dc-import="' + i + '">import</button></td></tr>';
3484
+ }
3485
+ html += '</tbody></table>';
3486
+ }
3487
+ }
3488
+ html += '</div></div>';
3489
+ return html;
3490
+ }
3491
+
3492
+ function wireDiscoveredCredentials(container) {
3493
+ var btns = container.querySelectorAll("[data-dc-import]");
3494
+ for (var i = 0; i < btns.length; i++) {
3495
+ (function (btn) {
3496
+ btn.addEventListener("click", function () {
3497
+ var idx = Number(btn.getAttribute("data-dc-import"));
3498
+ var dr = walletsState.discoveredResult;
3499
+ var c = dr && dr.ok && dr.data.credentials[idx];
3500
+ if (!c) return;
3501
+ var idInput = container.querySelector('.dc-id[data-dc-idx="' + idx + '"]');
3502
+ var id = (idInput && idInput.value.trim()) || (c.origin + "-" + c.endpoint);
3503
+ loadTool("auth_profile_import", { origin: c.origin, endpoint: c.endpoint, id: id }).then(function (r) {
3504
+ if (!r.ok) { toast(r.message, true); return; }
3505
+ toast("Imported.", false);
3506
+ loadWallets();
3507
+ });
3508
+ });
3509
+ })(btns[i]);
3510
+ }
3511
+ }
3512
+
3513
+ function renderWalletCards() {
3514
+ var cardsEl = document.getElementById("wallet-cards");
3515
+ if (!cardsEl) return;
3516
+ var pr = walletsState.profilesResult;
3517
+ if (!pr || !pr.ok) {
3518
+ cardsEl.innerHTML = '<div class="empty">' + (pr ? escHtml(pr.message) : "loading&hellip;") + '</div>';
3519
+ return;
3520
+ }
3521
+ var profiles = (pr.data && pr.data.profiles) || [];
3522
+ if (profiles.length === 0) {
3523
+ cardsEl.innerHTML = '<div class="empty">No auth profiles configured on this host.</div>';
3524
+ return;
3525
+ }
3526
+ var rr = walletsState.rollupResult;
3527
+ var spend = rr && rr.ok ? byProfileMap(rr) : {};
3528
+ var out = "";
3529
+ for (var i = 0; i < profiles.length; i++) {
3530
+ out += walletCardHtml(profiles[i], spend[profiles[i].id]);
3531
+ }
3532
+ cardsEl.innerHTML = out;
3533
+ wireWalletCards(cardsEl);
3534
+ }
3535
+
3536
+ function walletCardHtml(p, spend) {
3537
+ var title = p.label || p.id;
3538
+ var exp = getWalletExpanded(p.id);
3539
+ var out = cardOpen(p.id, title, escHtml(p.endpoint));
3540
+ out += row("Method", escHtml(BILLING_LABELS[p.method] || humanize(p.method)));
3541
+ var keyLine = p.keyStatus === "stored"
3542
+ ? [p.fingerprint, p.last4].filter(Boolean).join(" &middot; ") || "stored"
3543
+ : p.keyStatus === "self-refreshing" ? "self-refreshing" : "unavailable";
3544
+ out += row("Key", escHtml(keyLine));
3545
+ if (p.origin) out += row("Imported from", escHtml(p.origin));
3546
+ out += '<dt>Enabled</dt><dd><label class="inline"><input type="checkbox" data-wallet-enabled="' + escHtml(p.id) + '"' + (p.disabled ? "" : " checked") + '></label></dd>';
3547
+ var curated = !p.models || p.models.mode === "all" ? "All eligible" : (p.models.ids || []).length + " curated";
3548
+ out += row("Models", escHtml(curated));
3549
+ if (spend) {
3550
+ if (typeof spend.spentUsd === "number") out += row("Spend (window)", formatCost(spend.spentUsd));
3551
+ if (p.costBudget && typeof p.costBudget.maxCostUsd === "number") {
3552
+ var pct = p.costBudget.maxCostUsd > 0 ? ((spend.spentUsd || 0) / p.costBudget.maxCostUsd) * 100 : 0;
3553
+ out += row("Budget", formatCost(p.costBudget.maxCostUsd) + " / " + escHtml(p.costBudget.window));
3554
+ out += bar(pct);
3555
+ }
3556
+ if (spend.remaining) {
3557
+ out += row("Remaining quota", String(spend.remaining.remaining), "resets " + formatDate(spend.remaining.resetsAt));
3558
+ }
3559
+ if (spend.credits && typeof spend.credits.balanceUsd === "number") {
3560
+ out += row("Credit balance", formatCost(spend.credits.balanceUsd), spend.credits.asOf ? "as of " + formatDate(spend.credits.asOf) : undefined);
3561
+ }
3562
+ } else if (p.costBudget && typeof p.costBudget.maxCostUsd === "number") {
3563
+ out += row("Budget", formatCost(p.costBudget.maxCostUsd) + " / " + escHtml(p.costBudget.window));
3564
+ }
3565
+ out += '<div class="wallet-card-actions">'
3566
+ + '<button type="button" class="btn-sm" data-wallet-edit-toggle="' + escHtml(p.id) + '">' + (exp.edit ? "close" : "rename / budget") + '</button>'
3567
+ + '<button type="button" class="btn-sm" data-wallet-curate-toggle="' + escHtml(p.id) + '">' + (exp.curate ? "close" : "manage models") + '</button>'
3568
+ + '<button type="button" class="btn-sm danger" data-wallet-delete="' + escHtml(p.id) + '">delete</button>'
3569
+ + '</div>';
3570
+ if (exp.edit) out += walletEditPanelHtml(p);
3571
+ if (exp.curate) out += walletModelsEditorHtml(p, walletsState.providerModelsByEndpoint[p.endpoint], exp.curateMode);
3572
+ out += cardClose();
3573
+ return out;
3574
+ }
3575
+
3576
+ function walletEditPanelHtml(p) {
3577
+ var hasBudget = !!(p.costBudget && typeof p.costBudget.maxCostUsd === "number");
3578
+ return '<div class="form-panel" data-wallet-edit-panel="' + escHtml(p.id) + '">'
3579
+ + '<div class="field"><label>Label</label><input type="text" class="we-label" value="' + escHtml(p.label || "") + '"></div>'
3580
+ + '<label class="inline"><input type="checkbox" class="we-no-budget"' + (hasBudget ? "" : " checked") + '> no spend cap</label>'
3581
+ + '<div class="form-row we-budget-fields" style="' + (hasBudget ? "" : "display:none") + '">'
3582
+ + '<div class="field"><label>Max cost (USD)</label><input type="number" class="we-max-cost" value="' + escHtml(hasBudget ? String(p.costBudget.maxCostUsd) : "") + '"></div>'
3583
+ + '<div class="field"><label>Window</label><input type="text" class="we-window" value="' + escHtml(hasBudget ? p.costBudget.window : "7d") + '"></div>'
3584
+ + '<div class="field"><label>Scope</label><select class="we-scope"><option value="profile"' + (hasBudget && p.costBudget.scope === "profile" ? " selected" : "") + '>profile</option><option value="session"' + (hasBudget && p.costBudget.scope === "session" ? " selected" : "") + '>session</option></select></div>'
3585
+ + '</div>'
3586
+ + '<button type="button" class="btn-sm primary" data-wallet-edit-save="' + escHtml(p.id) + '">save</button>'
3587
+ + '</div>';
3588
+ }
3589
+
3590
+ // pendingMode is the panel's OWN pending selection (getWalletExpanded's
3591
+ // curateMode), not necessarily what the server has stored yet: switching to
3592
+ // "curate" must reveal the checklist right away so there is something to
3593
+ // check, without writing a destructive "curate to nothing" the instant the
3594
+ // radio moves. The first checked model is what actually persists the
3595
+ // mode=allow switch (see the .mm-id change handler).
3596
+ function walletModelsEditorHtml(p, catalogRows, pendingMode) {
3597
+ var mode = pendingMode || (p.models && p.models.mode) || "all";
3598
+ var ids = (p.models && p.models.ids) || [];
3599
+ var html = '<div class="form-panel" data-wallet-models-panel="' + escHtml(p.id) + '">';
3600
+ html += '<label class="inline"><input type="radio" name="mm-mode-' + escHtml(p.id) + '" value="all" class="mm-mode" data-mm-profile="' + escHtml(p.id) + '"' + (mode === "all" ? " checked" : "") + '> service all eligible</label> ';
3601
+ html += '<label class="inline"><input type="radio" name="mm-mode-' + escHtml(p.id) + '" value="allow" class="mm-mode" data-mm-profile="' + escHtml(p.id) + '"' + (mode === "allow" ? " checked" : "") + '> curate</label>';
3602
+ if (mode === "allow") html += ' <button type="button" class="btn-sm" data-mm-refresh="' + escHtml(p.id) + '">refresh models</button>';
3603
+ html += '<div class="mm-list" data-mm-list="' + escHtml(p.id) + '" style="' + (mode === "allow" ? "" : "display:none") + '">';
3604
+ if (mode === "allow") {
3605
+ if (!catalogRows) {
3606
+ html += '<div class="muted-note">loading models&hellip;</div>';
3607
+ } else if (catalogRows.length === 0) {
3608
+ html += '<div class="empty">No models known for this endpoint.</div>';
3609
+ } else {
3610
+ for (var i = 0; i < catalogRows.length; i++) {
3611
+ var m = catalogRows[i];
3612
+ var checked = ids.indexOf(m.id) !== -1;
3613
+ html += '<label class="inline" style="display:block;margin:2px 0"><input type="checkbox" class="mm-id" data-mm-profile="' + escHtml(p.id) + '" value="' + escHtml(m.id) + '"' + (checked ? " checked" : "") + '> ' + escHtml(m.label || m.id) + '</label>';
3614
+ }
3615
+ }
3616
+ }
3617
+ html += '</div></div>';
3618
+ return html;
3619
+ }
3620
+
3621
+ function wireWalletCards(container) {
3622
+ var enabledToggles = container.querySelectorAll("[data-wallet-enabled]");
3623
+ for (var i = 0; i < enabledToggles.length; i++) {
3624
+ (function (el) {
3625
+ el.addEventListener("change", function () {
3626
+ var id = el.getAttribute("data-wallet-enabled");
3627
+ var wantEnabled = el.checked;
3628
+ el.disabled = true;
3629
+ loadTool("auth_profile_set_enabled", { id: id, enabled: wantEnabled }).then(function (r) {
3630
+ el.disabled = false;
3631
+ if (!r.ok) { toast(r.message, true); el.checked = !wantEnabled; return; }
3632
+ replaceProfileInState(r.data.profile);
3633
+ toast(wantEnabled ? "Wallet enabled." : "Wallet disabled.", false);
3634
+ renderWalletCards();
3635
+ });
3636
+ });
3637
+ })(enabledToggles[i]);
3638
+ }
3639
+
3640
+ var editToggles = container.querySelectorAll("[data-wallet-edit-toggle]");
3641
+ for (var e = 0; e < editToggles.length; e++) {
3642
+ (function (btn) {
3643
+ btn.addEventListener("click", function () {
3644
+ var id = btn.getAttribute("data-wallet-edit-toggle");
3645
+ var exp = getWalletExpanded(id);
3646
+ exp.edit = !exp.edit;
3647
+ renderWalletCards();
3648
+ });
3649
+ })(editToggles[e]);
3650
+ }
3651
+
3652
+ var editSaves = container.querySelectorAll("[data-wallet-edit-save]");
3653
+ for (var s = 0; s < editSaves.length; s++) {
3654
+ (function (btn) {
3655
+ btn.addEventListener("click", function () {
3656
+ var id = btn.getAttribute("data-wallet-edit-save");
3657
+ var panel = container.querySelector('[data-wallet-edit-panel="' + cssEscape(id) + '"]');
3658
+ var label = panel.querySelector(".we-label").value.trim();
3659
+ var noBudget = panel.querySelector(".we-no-budget").checked;
3660
+ var patch = { id: id, label: label === "" ? null : label };
3661
+ if (noBudget) {
3662
+ patch.costBudget = null;
3663
+ } else {
3664
+ patch.costBudget = {
3665
+ maxCostUsd: Number(panel.querySelector(".we-max-cost").value) || 0,
3666
+ window: panel.querySelector(".we-window").value.trim() || "7d",
3667
+ scope: panel.querySelector(".we-scope").value,
3668
+ };
3669
+ }
3670
+ loadTool("auth_profile_update", patch).then(function (r) {
3671
+ if (!r.ok) { toast(r.message, true); return; }
3672
+ replaceProfileInState(r.data.profile);
3673
+ toast("Wallet updated.", false);
3674
+ getWalletExpanded(id).edit = false;
3675
+ renderWalletCards();
3676
+ });
3677
+ });
3678
+ })(editSaves[s]);
3679
+ }
3680
+
3681
+ var noBudgetToggles = container.querySelectorAll(".we-no-budget");
3682
+ for (var nb = 0; nb < noBudgetToggles.length; nb++) {
3683
+ (function (el) {
3684
+ el.addEventListener("change", function () {
3685
+ var fields = el.parentElement.querySelector(".we-budget-fields");
3686
+ if (fields) fields.style.display = el.checked ? "none" : "";
3687
+ });
3688
+ })(noBudgetToggles[nb]);
3689
+ }
3690
+
3691
+ var curateToggles = container.querySelectorAll("[data-wallet-curate-toggle]");
3692
+ for (var c = 0; c < curateToggles.length; c++) {
3693
+ (function (btn) {
3694
+ btn.addEventListener("click", function () {
3695
+ var id = btn.getAttribute("data-wallet-curate-toggle");
3696
+ var exp = getWalletExpanded(id);
3697
+ exp.curate = !exp.curate;
3698
+ if (exp.curate) {
3699
+ var profile = findProfile(id);
3700
+ if (profile && !walletsState.providerModelsByEndpoint[profile.endpoint]) {
3701
+ loadTool("catalog_provider_models", { endpoint: profile.endpoint, limit: 200 }).then(function (r) {
3702
+ walletsState.providerModelsByEndpoint[profile.endpoint] = r.ok ? (r.data.models || []) : [];
3703
+ renderWalletCards();
3704
+ });
3705
+ return;
3706
+ }
3707
+ }
3708
+ renderWalletCards();
3709
+ });
3710
+ })(curateToggles[c]);
3711
+ }
3712
+
3713
+ var mmModes = container.querySelectorAll(".mm-mode");
3714
+ for (var mm = 0; mm < mmModes.length; mm++) {
3715
+ (function (el) {
3716
+ el.addEventListener("change", function () {
3717
+ var id = el.getAttribute("data-mm-profile");
3718
+ if (el.value === "all") {
3719
+ getWalletExpanded(id).curateMode = undefined;
3720
+ loadTool("auth_profile_set_models", { id: id, mode: "all" }).then(function (r) {
3721
+ if (!r.ok) { toast(r.message, true); return; }
3722
+ replaceProfileInState(r.data.profile);
3723
+ renderWalletCards();
3724
+ });
3725
+ } else {
3726
+ // Reveal the checklist locally (a pending selection, independent
3727
+ // of what the server has stored); nothing is written until a
3728
+ // model is actually checked, so switching this radio alone can
3729
+ // never silently curate the wallet down to nothing.
3730
+ getWalletExpanded(id).curateMode = "allow";
3731
+ renderWalletCards();
3732
+ }
3733
+ });
3734
+ })(mmModes[mm]);
3735
+ }
3736
+
3737
+ var mmIds = container.querySelectorAll(".mm-id");
3738
+ for (var mi = 0; mi < mmIds.length; mi++) {
3739
+ (function (el) {
3740
+ el.addEventListener("change", function () {
3741
+ var id = el.getAttribute("data-mm-profile");
3742
+ var list = container.querySelector('[data-mm-list="' + cssEscape(id) + '"]');
3743
+ var checked = list.querySelectorAll(".mm-id:checked");
3744
+ var ids = [];
3745
+ for (var i2 = 0; i2 < checked.length; i2++) ids.push(checked[i2].value);
3746
+ loadTool("auth_profile_set_models", { id: id, mode: "allow", ids: ids }).then(function (r) {
3747
+ if (!r.ok) { toast(r.message, true); return; }
3748
+ replaceProfileInState(r.data.profile);
3749
+ });
3750
+ });
3751
+ })(mmIds[mi]);
3752
+ }
3753
+
3754
+ var mmRefresh = container.querySelectorAll("[data-mm-refresh]");
3755
+ for (var mr = 0; mr < mmRefresh.length; mr++) {
3756
+ (function (btn) {
3757
+ btn.addEventListener("click", function () {
3758
+ var id = btn.getAttribute("data-mm-refresh");
3759
+ loadTool("auth_profile_refresh_models", { id: id }).then(function (r) {
3760
+ if (!r.ok) { toast(r.message, true); return; }
3761
+ replaceProfileInState(r.data.profile);
3762
+ var added = (r.data.added || []).length;
3763
+ var removed = (r.data.removed || []).length;
3764
+ toast("Refreshed models (+" + added + " / -" + removed + ").", false);
3765
+ renderWalletCards();
3766
+ });
3767
+ });
3768
+ })(mmRefresh[mr]);
3769
+ }
3770
+
3771
+ var deleteBtns = container.querySelectorAll("[data-wallet-delete]");
3772
+ for (var d = 0; d < deleteBtns.length; d++) {
3773
+ (function (btn) {
3774
+ var id = btn.getAttribute("data-wallet-delete");
3775
+ btn.addEventListener("click", function () {
3776
+ confirmClick("wallet-delete-" + id, btn, function () {
3777
+ var blockedBy = presetsReferencing(id);
3778
+ if (blockedBy.length > 0) {
3779
+ var names = blockedBy.map(function (p) { return p.name || p.id; }).join(", ");
3780
+ toast("Cannot delete: still used by harness preset(s) " + names + ".", true);
3781
+ return;
3782
+ }
3783
+ loadTool("auth_profile_delete", { id: id }).then(function (r) {
3784
+ if (!r.ok) { toast(r.message, true); return; }
3785
+ removeProfileFromState(id);
3786
+ toast("Wallet deleted.", false);
3787
+ renderWalletCards();
3788
+ });
3789
+ });
3790
+ });
3791
+ })(deleteBtns[d]);
3792
+ }
3793
+ }
3794
+
3795
+ function findProfile(id) {
3796
+ var pr = walletsState.profilesResult;
3797
+ if (!pr || !pr.ok) return null;
3798
+ var list = (pr.data && pr.data.profiles) || [];
3799
+ for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
3800
+ return null;
3801
+ }
3802
+
3803
+ // ============================================================
3804
+ // Harnesses
3805
+ // ============================================================
3806
+
3807
+ var harnessesState = {
3808
+ adaptersResult: null,
3809
+ capsResult: null,
3810
+ presetsResult: null,
3811
+ rolesResult: null,
3812
+ profilesResult: null,
3813
+ expandedDefaults: {}, // slug -> bool
3814
+ defaultsCache: {}, // slug -> row[]
3815
+ showPresetForm: false,
3816
+ };
3817
+
3818
+ function loadHarnesses() {
3819
+ return Promise.all([
3820
+ loadTool("adapter_list", {}),
3821
+ loadTool("harness_capabilities", {}),
3822
+ loadTool("harness_preset_list", {}),
3823
+ loadTool("role_list", {}),
3824
+ loadTool("auth_profile_list", {}),
3825
+ ]).then(function (results) {
3826
+ harnessesState.adaptersResult = results[0];
3827
+ harnessesState.capsResult = results[1];
3828
+ harnessesState.presetsResult = results[2];
3829
+ harnessesState.rolesResult = results[3];
3830
+ harnessesState.profilesResult = results[4];
3831
+ renderHarnesses();
3832
+ });
3833
+ }
3834
+
3835
+ function adapterDefaultsPaths(slug) {
3836
+ return [
3837
+ "defaults.adapters." + slug + ".skills",
3838
+ "defaults.adapters." + slug + ".options",
3839
+ "defaults.adapters." + slug + ".contextContinuity",
3840
+ "defaults.adapters." + slug + ".auth.mode",
3841
+ "defaults.adapters." + slug + ".auth.source",
3842
+ "defaults.adapters." + slug + ".auth.provider",
3843
+ "defaults.adapters." + slug + ".auth.token",
3844
+ "defaults.adapters." + slug + ".auth.apiKey",
3845
+ ];
3846
+ }
3847
+
3848
+ function renderHarnesses() {
3849
+ var el = document.getElementById("sec-harnesses");
3850
+ var adaptersR = harnessesState.adaptersResult;
3851
+ var capsR = harnessesState.capsResult;
3852
+ var presetsR = harnessesState.presetsResult;
3853
+ var rolesR = harnessesState.rolesResult;
3854
+ if (!adaptersR.ok) {
3855
+ el.innerHTML = '<div class="empty">adapter_list: ' + escHtml(adaptersR.message) + '</div>';
3856
+ return;
3857
+ }
3858
+ var adapters = (adaptersR.data && adaptersR.data.adapters) || [];
3859
+ var capsBySlug = {};
3860
+ if (capsR.ok) {
3861
+ var caps = (capsR.data && capsR.data.capabilities) || [];
3862
+ for (var i = 0; i < caps.length; i++) capsBySlug[caps[i].adapter] = caps[i];
3863
+ }
3864
+ var presetsBySlug = {};
3865
+ var allPresets = [];
3866
+ if (presetsR.ok) {
3867
+ allPresets = (presetsR.data && presetsR.data.presets) || [];
3868
+ for (var j = 0; j < allPresets.length; j++) {
3869
+ var slug = allPresets[j].harnessSlug;
3870
+ if (!presetsBySlug[slug]) presetsBySlug[slug] = [];
3871
+ presetsBySlug[slug].push(allPresets[j]);
3872
+ }
3873
+ }
3874
+
3875
+ var html = '<div class="sect-h">Adapters</div>';
3876
+ if (!capsR.ok && capsR.unknown) html += '<div class="muted-note">Capability detail needs a newer agentproto daemon.</div>';
3877
+ else if (!capsR.ok) html += '<div class="muted-note">harness_capabilities: ' + escHtml(capsR.message) + '</div>';
3878
+ if (!presetsR.ok) html += '<div class="muted-note">harness_preset_list: ' + escHtml(presetsR.message) + '</div>';
3879
+
3880
+ html += '<div class="grid">';
3881
+ if (adapters.length === 0) {
3882
+ html += '<div class="empty">No adapters installed on this host.</div>';
3883
+ }
3884
+ for (var k = 0; k < adapters.length; k++) {
3885
+ var a = adapters[k];
3886
+ var cap = capsBySlug[a.slug];
3887
+ var slugPresets = presetsBySlug[a.slug] || [];
3888
+ var def = null;
3889
+ for (var d = 0; d < slugPresets.length; d++) if (slugPresets[d].isDefault) def = slugPresets[d];
3890
+ html += cardOpen(a.slug, a.name || a.slug, escHtml(a.slug));
3891
+ if (a.version) html += row("Version", escHtml(a.version));
3892
+ if (a.protocol) html += row("Protocol", escHtml(a.protocol));
3893
+ html += row("Models", String((a.models || []).length));
3894
+ if (cap && Array.isArray(cap.providers) && cap.providers.length > 0) {
3895
+ var chips = "";
3896
+ for (var pi = 0; pi < cap.providers.length; pi++) {
3897
+ var prov = cap.providers[pi];
3898
+ chips += '<span class="tag ' + (prov.cred && prov.cred.present ? "ok" : "") + '">' + escHtml(prov.billingEndpoint) + '</span> ';
3899
+ }
3900
+ html += '<dt>Billing</dt><dd>' + chips + '</dd>';
3901
+ }
3902
+ if (def) {
3903
+ html += row("Default preset", escHtml(def.name) + ' &rarr; <a class="link" data-goto="wallets/' + encodeURIComponent(def.profileRef) + '">' + escHtml(def.profileRef) + '</a> / ' + escHtml(def.defaultModel));
3904
+ } else if (slugPresets.length > 0) {
3905
+ html += row("Presets", String(slugPresets.length) + " (no default)");
3906
+ }
3907
+ html += '<dt></dt><dd><button type="button" class="btn-sm" data-adapter-defaults-toggle="' + escHtml(a.slug) + '">' + (harnessesState.expandedDefaults[a.slug] ? "hide defaults" : "spawn defaults") + '</button></dd>';
3908
+ html += '<div id="adapter-defaults-' + escHtml(a.slug) + '"></div>';
3909
+ html += cardClose();
3910
+ }
3911
+ html += '</div>';
3912
+
3913
+ html += '<div class="sect-h">Presets<button type="button" class="btn-sm" id="preset-add-toggle">' + (harnessesState.showPresetForm ? "close" : "+ new preset") + '</button></div>';
3914
+ if (harnessesState.showPresetForm) html += presetFormHtml(adapters);
3915
+ html += presetsTableHtml(allPresets);
3916
+
3917
+ html += '<div class="sect-h">Roles</div>';
3918
+ if (rolesR.ok) {
3919
+ var roles = (rolesR.data && rolesR.data.roles) || [];
3920
+ if (roles.length === 0) {
3921
+ html += '<div class="empty">No roles registered.</div>';
3922
+ } else {
3923
+ html += '<div class="card"><div class="body"><table><thead><tr><th>Role</th><th>Level</th><th>Delegation</th><th>Can spawn</th></tr></thead><tbody>';
3924
+ for (var r = 0; r < roles.length; r++) {
3925
+ var role = roles[r];
3926
+ html += '<tr><td>' + escHtml(role.name) + '</td><td>' + escHtml(String(role.level)) + '</td><td>' + escHtml(role.delegation) + '</td><td>' + escHtml((role.spawnable || []).join(", ")) + '</td></tr>';
3927
+ }
3928
+ html += '</tbody></table></div></div>';
3929
+ }
3930
+ } else {
3931
+ html += '<div class="muted-note">role_list: ' + escHtml(rolesR.message) + '</div>';
3932
+ }
3933
+
3934
+ el.innerHTML = html;
3935
+ wireGotoLinks(el);
3936
+ wireAdapterDefaultsToggles(el);
3937
+ wirePresetForm(el, adapters);
3938
+ wirePresetsTable(el);
3939
+
3940
+ for (var slug2 in harnessesState.expandedDefaults) {
3941
+ if (harnessesState.expandedDefaults[slug2] && harnessesState.defaultsCache[slug2]) {
3942
+ renderAdapterDefaultsCard(slug2);
3943
+ }
3944
+ }
3945
+ }
3946
+
3947
+ function presetsTableHtml(presets) {
3948
+ if (presets.length === 0) return '<div class="empty">No harness presets configured.</div>';
3949
+ var html = '<div class="card"><div class="body"><table><thead><tr><th>Id</th><th>Harness</th><th>Name</th><th>Wallet</th><th>Model</th><th>Default</th><th></th></tr></thead><tbody>';
3950
+ for (var i = 0; i < presets.length; i++) {
3951
+ var p = presets[i];
3952
+ html += '<tr data-row-id="' + escHtml(p.id) + '"><td>' + escHtml(p.id) + '</td><td>' + escHtml(p.harnessSlug) + '</td><td>' + escHtml(p.name) + '</td>'
3953
+ + '<td><a class="link" data-goto="wallets/' + encodeURIComponent(p.profileRef) + '">' + escHtml(p.profileRef) + '</a>' + (p.profileDisabled ? ' <span class="tag bad">disabled/missing</span>' : '') + '</td>'
3954
+ + '<td>' + escHtml(p.defaultModel) + '</td>'
3955
+ + '<td>' + (p.isDefault ? '<span class="tag ok">default</span>' : '<button type="button" class="btn-sm" data-preset-set-default="' + escHtml(p.id) + '" data-preset-harness="' + escHtml(p.harnessSlug) + '">set default</button>') + '</td>'
3956
+ + '<td><button type="button" class="btn-sm danger" data-preset-delete="' + escHtml(p.id) + '">delete</button></td></tr>';
3957
+ }
3958
+ html += '</tbody></table></div></div>';
3959
+ return html;
3960
+ }
3961
+
3962
+ function presetFormHtml(adapters) {
3963
+ var profiles = (harnessesState.profilesResult && harnessesState.profilesResult.ok && harnessesState.profilesResult.data.profiles) || [];
3964
+ var html = '<div class="form-panel">'
3965
+ + '<div class="form-row">'
3966
+ + '<div class="field"><label>Id</label><input type="text" id="pf-id"></div>'
3967
+ + '<div class="field"><label>Harness</label><select id="pf-harness">';
3968
+ for (var i = 0; i < adapters.length; i++) html += '<option value="' + escHtml(adapters[i].slug) + '">' + escHtml(adapters[i].slug) + '</option>';
3969
+ html += '</select></div>'
3970
+ + '<div class="field"><label>Name</label><input type="text" id="pf-name"></div>'
3971
+ + '</div><div class="form-row">'
3972
+ + '<div class="field"><label>Wallet</label><select id="pf-profile">';
3973
+ for (var j = 0; j < profiles.length; j++) html += '<option value="' + escHtml(profiles[j].id) + '">' + escHtml(profiles[j].label || profiles[j].id) + '</option>';
3974
+ html += '</select></div>'
3975
+ + '<div class="field"><label>Default model</label><input type="text" id="pf-model" placeholder="vendor/model"></div>'
3976
+ + '<label class="inline"><input type="checkbox" id="pf-default"> make default for this harness</label>'
3977
+ + '<button type="button" class="btn-sm primary" id="pf-submit">Create preset</button>'
3978
+ + '</div><div class="cfg-err" id="pf-err" style="display:none"></div></div>';
3979
+ return html;
3980
+ }
3981
+
3982
+ function wirePresetForm(container, adapters) {
3983
+ var toggle = container.querySelector("#preset-add-toggle");
3984
+ if (toggle) toggle.addEventListener("click", function () {
3985
+ harnessesState.showPresetForm = !harnessesState.showPresetForm;
3986
+ renderHarnesses();
3987
+ });
3988
+ var submit = container.querySelector("#pf-submit");
3989
+ if (!submit) return;
3990
+ submit.addEventListener("click", function () {
3991
+ var errEl = container.querySelector("#pf-err");
3992
+ errEl.style.display = "none";
3993
+ var id = container.querySelector("#pf-id").value.trim();
3994
+ var harnessSlug = container.querySelector("#pf-harness").value;
3995
+ var name = container.querySelector("#pf-name").value.trim();
3996
+ var profileRef = container.querySelector("#pf-profile").value;
3997
+ var defaultModel = container.querySelector("#pf-model").value.trim();
3998
+ var isDefault = container.querySelector("#pf-default").checked;
3999
+ if (!id || !name || !defaultModel || !profileRef) {
4000
+ errEl.style.display = "block";
4001
+ errEl.textContent = "Id, name, wallet, and default model are required.";
4002
+ return;
4003
+ }
4004
+ loadTool("harness_preset_create", { id: id, harnessSlug: harnessSlug, name: name, profileRef: profileRef, defaultModel: defaultModel, isDefault: isDefault }).then(function (r) {
4005
+ if (!r.ok) { errEl.style.display = "block"; errEl.textContent = r.message; return; }
4006
+ toast("Preset saved.", false);
4007
+ harnessesState.showPresetForm = false;
4008
+ loadHarnesses();
4009
+ });
4010
+ });
4011
+ }
4012
+
4013
+ function wirePresetsTable(container) {
4014
+ var setDefaults = container.querySelectorAll("[data-preset-set-default]");
4015
+ for (var i = 0; i < setDefaults.length; i++) {
4016
+ (function (btn) {
4017
+ btn.addEventListener("click", function () {
4018
+ var presetId = btn.getAttribute("data-preset-set-default");
4019
+ var harnessSlug = btn.getAttribute("data-preset-harness");
4020
+ loadTool("harness_preset_set_default", { harnessSlug: harnessSlug, presetId: presetId }).then(function (r) {
4021
+ if (!r.ok) { toast(r.message, true); return; }
4022
+ toast("Default preset updated.", false);
4023
+ loadHarnesses();
4024
+ });
4025
+ });
4026
+ })(setDefaults[i]);
4027
+ }
4028
+ var deletes = container.querySelectorAll("[data-preset-delete]");
4029
+ for (var d = 0; d < deletes.length; d++) {
4030
+ (function (btn) {
4031
+ var presetId = btn.getAttribute("data-preset-delete");
4032
+ btn.addEventListener("click", function () {
4033
+ confirmClick("preset-delete-" + presetId, btn, function () {
4034
+ loadTool("harness_preset_delete", { id: presetId }).then(function (r) {
4035
+ if (!r.ok) { toast(r.message, true); return; }
4036
+ toast("Preset deleted.", false);
4037
+ loadHarnesses();
4038
+ });
4039
+ });
4040
+ });
4041
+ })(deletes[d]);
4042
+ }
4043
+ }
4044
+
4045
+ function wireAdapterDefaultsToggles(container) {
4046
+ var toggles = container.querySelectorAll("[data-adapter-defaults-toggle]");
4047
+ for (var i = 0; i < toggles.length; i++) {
4048
+ (function (btn) {
4049
+ btn.addEventListener("click", function () {
4050
+ var slug = btn.getAttribute("data-adapter-defaults-toggle");
4051
+ harnessesState.expandedDefaults[slug] = !harnessesState.expandedDefaults[slug];
4052
+ if (harnessesState.expandedDefaults[slug] && !harnessesState.defaultsCache[slug]) {
4053
+ loadTool("config_get", { keys: adapterDefaultsPaths(slug) }).then(function (r) {
4054
+ if (r.ok && r.data.revision) configRevision = r.data.revision;
4055
+ harnessesState.defaultsCache[slug] = r.ok ? (r.data.keys || []) : [];
4056
+ if (r.ok) notePendingRestart(r.data.keys);
4057
+ if (!r.ok && r.unknown) {
4058
+ var host = document.getElementById("adapter-defaults-" + slug);
4059
+ if (host) host.innerHTML = '<div class="muted-note">Per-harness defaults need a newer agentproto daemon.</div>';
4060
+ return;
4061
+ }
4062
+ if (!r.ok) {
4063
+ var host2 = document.getElementById("adapter-defaults-" + slug);
4064
+ if (host2) host2.innerHTML = '<div class="muted-note">config_get: ' + escHtml(r.message) + '</div>';
4065
+ return;
4066
+ }
4067
+ renderAdapterDefaultsCard(slug);
4068
+ });
4069
+ }
4070
+ btn.textContent = harnessesState.expandedDefaults[slug] ? "hide defaults" : "spawn defaults";
4071
+ var host3 = document.getElementById("adapter-defaults-" + slug);
4072
+ if (host3 && !harnessesState.expandedDefaults[slug]) host3.innerHTML = "";
4073
+ else if (host3 && harnessesState.defaultsCache[slug]) renderAdapterDefaultsCard(slug);
4074
+ });
4075
+ })(toggles[i]);
4076
+ }
4077
+ }
4078
+
4079
+ function renderAdapterDefaultsCard(slug) {
4080
+ var host = document.getElementById("adapter-defaults-" + slug);
4081
+ if (!host) return;
4082
+ var rows = harnessesState.defaultsCache[slug] || [];
4083
+ var rowsByPath = {};
4084
+ for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];
4085
+ host.innerHTML = '<dl>' + rows.map(configFieldHtml).join("") + '</dl>';
4086
+ wireConfigRows(host, rowsByPath, function (path) {
4087
+ loadTool("config_get", { keys: adapterDefaultsPaths(slug) }).then(function (r) {
4088
+ if (!r.ok) { toast("config_get: " + r.message, true); return; }
4089
+ if (r.data.revision) configRevision = r.data.revision;
4090
+ harnessesState.defaultsCache[slug] = r.data.keys || [];
4091
+ notePendingRestart(r.data.keys);
4092
+ renderAdapterDefaultsCard(slug);
4093
+ });
4094
+ }, function () {
4095
+ loadHarnesses();
4096
+ });
4097
+ }
4098
+
4099
+ function wireGotoLinks(container) {
4100
+ var links = container.querySelectorAll("a[data-goto]");
4101
+ for (var i = 0; i < links.length; i++) {
4102
+ (function (a) {
4103
+ a.addEventListener("click", function (e) {
4104
+ e.preventDefault();
4105
+ location.hash = "#" + a.getAttribute("data-goto");
4106
+ });
4107
+ })(links[i]);
4108
+ }
4109
+ }
4110
+
4111
+ // ============================================================
4112
+ // Models
4113
+ // ============================================================
4114
+
4115
+ var modelsState = { routes: [], runnableOnly: false, walletFilter: "", titlerRow: null };
4116
+
4117
+ function loadModels() {
4118
+ return Promise.all([
4119
+ loadTool("catalog_models", { full: true }),
4120
+ loadTool("auth_profile_list", { full: true }),
4121
+ loadTool("config_get", { keys: ["titler.model"] }),
4122
+ ]).then(function (results) {
4123
+ modelsState.routesResult = results[0];
4124
+ modelsState.profilesResult = results[1];
4125
+ modelsState.titlerResult = results[2];
4126
+ if (results[2].ok && results[2].data.revision) configRevision = results[2].data.revision;
4127
+ if (results[2].ok) notePendingRestart(results[2].data.keys);
4128
+ renderModels();
4129
+ });
4130
+ }
4131
+
4132
+ function renderModels() {
4133
+ var el = document.getElementById("sec-models");
4134
+ var rr = modelsState.routesResult;
4135
+ if (!rr.ok) {
4136
+ el.innerHTML = '<div class="empty">catalog_models: ' + escHtml(rr.message) + '</div>';
4137
+ return;
4138
+ }
4139
+ var routes = (rr.data && rr.data.routes) || [];
4140
+ var profiles = (modelsState.profilesResult && modelsState.profilesResult.ok && modelsState.profilesResult.data && modelsState.profilesResult.data.profiles) || [];
4141
+
4142
+ var html = '<div class="sect-h">Titler model</div><div id="titler-model-card"></div>';
4143
+
4144
+ html += '<div class="sect-h">Catalog<label class="inline"><input type="checkbox" id="runnable-only"' + (modelsState.runnableOnly ? " checked" : "") + '> runnable only</label>'
4145
+ + '<label class="inline">wallet <select id="wallet-filter"><option value="">any</option>';
4146
+ for (var i = 0; i < profiles.length; i++) {
4147
+ html += '<option value="' + escHtml(profiles[i].id) + '"' + (profiles[i].id === modelsState.walletFilter ? " selected" : "") + '>' + escHtml(profiles[i].label || profiles[i].id) + '</option>';
4148
+ }
4149
+ html += '</select></label></div>';
4150
+
4151
+ var filtered = routes.filter(function (r) {
4152
+ if (modelsState.runnableOnly && !r.runnable) return false;
4153
+ if (modelsState.walletFilter && (!r.eligibleProfiles || r.eligibleProfiles.indexOf(modelsState.walletFilter) === -1)) return false;
4154
+ return true;
4155
+ });
4156
+
4157
+ var byVendor = {};
4158
+ var vendorOrder = [];
4159
+ for (var f = 0; f < filtered.length; f++) {
4160
+ var v = filtered[f].vendor;
4161
+ if (!byVendor[v]) { byVendor[v] = []; vendorOrder.push(v); }
4162
+ byVendor[v].push(filtered[f]);
4163
+ }
4164
+ vendorOrder.sort();
4165
+
4166
+ if (vendorOrder.length === 0) {
4167
+ html += '<div class="empty">No models match the current filters.</div>';
4168
+ }
4169
+ for (var vi = 0; vi < vendorOrder.length; vi++) {
4170
+ var vendor = vendorOrder[vi];
4171
+ var vendorRows = byVendor[vendor];
4172
+ var byProduct = {};
4173
+ var productOrder = [];
4174
+ for (var pr2 = 0; pr2 < vendorRows.length; pr2++) {
4175
+ var prod = vendorRows[pr2].product;
4176
+ if (!byProduct[prod]) { byProduct[prod] = []; productOrder.push(prod); }
4177
+ byProduct[prod].push(vendorRows[pr2]);
4178
+ }
4179
+ productOrder.sort();
4180
+ html += '<div class="sect-h">' + escHtml(vendor) + '</div><div class="grid">';
4181
+ for (var pi2 = 0; pi2 < productOrder.length; pi2++) {
4182
+ var product = productOrder[pi2];
4183
+ var productRoutes = byProduct[product];
4184
+ var modelRef = vendor + "/" + product;
4185
+ html += cardOpen(modelRef, product, "");
4186
+ for (var ri = 0; ri < productRoutes.length; ri++) {
4187
+ var route = productRoutes[ri];
4188
+ var priceStr = route.pricing ? "$" + route.pricing.inPer1M + "/$" + route.pricing.outPer1M + " per 1M" : "";
4189
+ var elig = (route.eligibleProfiles || []).map(function (id) {
4190
+ return '<a class="link" data-goto="wallets/' + encodeURIComponent(id) + '">' + escHtml(id) + '</a>';
4191
+ }).join(" ");
4192
+ html += row(route.route, (route.runnable ? '<span class="tag ok">runnable</span>' : '<span class="tag">not runnable</span>') + (route.curated ? ' <span class="tag">curated</span>' : ""));
4193
+ if (priceStr) html += row("Price", escHtml(priceStr));
4194
+ if (route.contextWindow) html += row("Context", escHtml(route.contextWindow) + (route.maxOutput ? " / " + escHtml(route.maxOutput) + " out" : ""));
4195
+ if (elig) html += '<dt>Wallets</dt><dd>' + elig + '</dd>';
4196
+ }
4197
+ html += cardClose();
4198
+ }
4199
+ html += '</div>';
4200
+ }
4201
+
4202
+ html += '<div class="sect-h">Wallet &times; model matrix</div>' + walletModelMatrixHtml(routes, profiles);
4203
+
4204
+ el.innerHTML = html;
4205
+ wireGotoLinks(el);
4206
+ wireModelMatrix(el, routes, profiles);
4207
+ renderTitlerModelCard();
4208
+
4209
+ var runnableEl = document.getElementById("runnable-only");
4210
+ if (runnableEl) runnableEl.addEventListener("change", function (e) { modelsState.runnableOnly = e.target.checked; renderModels(); });
4211
+ var walletEl = document.getElementById("wallet-filter");
4212
+ if (walletEl) walletEl.addEventListener("change", function (e) { modelsState.walletFilter = e.target.value; renderModels(); });
4213
+ }
4214
+
4215
+ function walletModelMatrixHtml(routes, profiles) {
4216
+ if (profiles.length === 0) return '<div class="empty">No wallets configured.</div>';
4217
+ var eligibleRoutes = routes.filter(function (r) { return r.eligibleProfiles && r.eligibleProfiles.length > 0; }).slice(0, 40);
4218
+ if (eligibleRoutes.length === 0) return '<div class="empty">No wallet-reachable models yet.</div>';
4219
+ var html = '<div class="card"><div class="body" style="overflow-x:auto"><table><thead><tr><th>Wallet</th>';
4220
+ for (var c = 0; c < eligibleRoutes.length; c++) html += '<th title="' + escHtml(eligibleRoutes[c].route) + '">' + escHtml(eligibleRoutes[c].product) + '</th>';
4221
+ html += '</tr></thead><tbody>';
4222
+ for (var i = 0; i < profiles.length; i++) {
4223
+ var p = profiles[i];
4224
+ html += '<tr><td>' + escHtml(p.label || p.id) + '</td>';
4225
+ for (var j = 0; j < eligibleRoutes.length; j++) {
4226
+ var route = eligibleRoutes[j];
4227
+ var modelId = route.ref || (route.vendor + "/" + route.product);
4228
+ var reachable = route.eligibleProfiles.indexOf(p.id) !== -1;
4229
+ if (!reachable) {
4230
+ html += '<td class="matrix-cell disabled">&ndash;</td>';
4231
+ continue;
4232
+ }
4233
+ var mode = (p.models && p.models.mode) || "all";
4234
+ var checked = mode === "all" || (p.models.ids || []).indexOf(modelId) !== -1;
4235
+ html += '<td class="matrix-cell" data-matrix-profile="' + escHtml(p.id) + '" data-matrix-model="' + escHtml(modelId) + '" data-matrix-mode="' + escHtml(mode) + '">' + (checked ? "&#10003;" : "&#8211;") + '</td>';
4236
+ }
4237
+ html += '</tr>';
4238
+ }
4239
+ html += '</tbody></table></div></div><div class="muted-note">Click a checked cell in an "all eligible" wallet to start curating it; click a cell to toggle curation once curating.</div>';
4240
+ return html;
4241
+ }
4242
+
4243
+ function wireModelMatrix(container, routes, profiles) {
4244
+ var cells = container.querySelectorAll("[data-matrix-profile]");
4245
+ for (var i = 0; i < cells.length; i++) {
4246
+ (function (cell) {
4247
+ cell.addEventListener("click", function () {
4248
+ var id = cell.getAttribute("data-matrix-profile");
4249
+ var modelId = cell.getAttribute("data-matrix-model");
4250
+ var p = null;
4251
+ for (var k = 0; k < profiles.length; k++) if (profiles[k].id === id) p = profiles[k];
4252
+ if (!p) return;
4253
+ var mode = (p.models && p.models.mode) || "all";
4254
+ var currentIds = (p.models && p.models.ids) || [];
4255
+ var nextIds;
4256
+ if (mode === "all") {
4257
+ // Start curating from just this one model.
4258
+ nextIds = [modelId];
4259
+ } else if (currentIds.indexOf(modelId) !== -1) {
4260
+ nextIds = currentIds.filter(function (x) { return x !== modelId; });
4261
+ } else {
4262
+ nextIds = currentIds.concat([modelId]);
4263
+ }
4264
+ loadTool("auth_profile_set_models", { id: id, mode: "allow", ids: nextIds }).then(function (r) {
4265
+ if (!r.ok) { toast(r.message, true); return; }
4266
+ for (var k2 = 0; k2 < profiles.length; k2++) if (profiles[k2].id === id) profiles[k2] = r.data.profile;
4267
+ if (modelsState.profilesResult && modelsState.profilesResult.ok) {
4268
+ for (var k3 = 0; k3 < modelsState.profilesResult.data.profiles.length; k3++) {
4269
+ if (modelsState.profilesResult.data.profiles[k3].id === id) modelsState.profilesResult.data.profiles[k3] = r.data.profile;
4270
+ }
4271
+ }
4272
+ renderModels();
4273
+ });
4274
+ });
4275
+ })(cells[i]);
4276
+ }
4277
+ }
4278
+
4279
+ function renderTitlerModelCard() {
4280
+ var host = document.getElementById("titler-model-card");
4281
+ if (!host) return;
4282
+ var tr = modelsState.titlerResult;
4283
+ if (!tr.ok && tr.unknown) {
4284
+ host.innerHTML = '<div class="muted-note">Editing titler.model needs a newer agentproto daemon (config_get).</div>';
4285
+ return;
4286
+ }
4287
+ if (!tr.ok) {
4288
+ host.innerHTML = '<div class="muted-note">config_get: ' + escHtml(tr.message) + '</div>';
4289
+ return;
4290
+ }
4291
+ var rows = tr.data.keys || [];
4292
+ var rowsByPath = {};
4293
+ for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];
4294
+ host.innerHTML = '<div class="card"><div class="body"><dl>' + rows.map(configFieldHtml).join("") + '</dl></div></div>';
4295
+ wireConfigRows(host, rowsByPath, function () {
4296
+ loadTool("config_get", { keys: ["titler.model"] }).then(function (r) {
4297
+ if (!r.ok) { toast("config_get: " + r.message, true); return; }
4298
+ if (r.data.revision) configRevision = r.data.revision;
4299
+ modelsState.titlerResult = r;
4300
+ notePendingRestart(r.data.keys);
4301
+ renderTitlerModelCard();
4302
+ });
4303
+ }, function () {
4304
+ loadModels();
4305
+ });
4306
+ }
4307
+
4308
+ // ============================================================
4309
+ // Defaults & messaging
4310
+ // ============================================================
4311
+
4312
+ var DEFAULTS_GROUPS = [
4313
+ {
4314
+ id: "messaging", title: "Messaging and prompts",
4315
+ match: function (p) {
4316
+ return p.indexOf("defaults.messaging.") === 0 || p === "defaults.agentPromptInterrupt"
4317
+ || p === "defaults.backgroundTaskWake.enabled" || p === "defaults.backgroundTaskWake.graceMs";
4318
+ },
4319
+ },
4320
+ {
4321
+ id: "spawn", title: "Spawn",
4322
+ match: function (p) {
4323
+ return p.indexOf("spawn.") === 0 || p.indexOf("worktrees.") === 0 || p.indexOf("defaults.spawn.") === 0
4324
+ || p === "defaults.skills" || p === "defaults.options" || p === "defaults.defaultRoleDepthCutoff"
4325
+ || p === "defaults.maxGrantableDelegation" || p === "provenance.wrapGh" || p === "agentsMd.inlineMaxKb";
4326
+ },
4327
+ },
4328
+ { id: "context-continuity", title: "Context continuity", match: function (p) { return p === "defaults.contextContinuity"; } },
4329
+ {
4330
+ id: "observability", title: "Observability",
4331
+ match: function (p) {
4332
+ return p.indexOf("titler.") === 0 || p === "defaults.langfuseTracing" || p === "defaults.traceRedactor"
4333
+ || p === "sessions.attentionDelaySec" || p === "defaults.mcp.deferredTools";
4334
+ },
4335
+ },
4336
+ {
4337
+ id: "daemon", title: "Daemon",
4338
+ match: function (p) {
4339
+ return p.indexOf("daemon.") === 0 || p === "sessions.eventsDir" || p === "features.pty" || p === "features.llmEndpoint"
4340
+ || p === "profiles" || p === "activeProfile";
4341
+ },
4342
+ },
4343
+ ];
4344
+
4345
+ function classifyDefaultsGroup(path) {
4346
+ for (var i = 0; i < DEFAULTS_GROUPS.length; i++) if (DEFAULTS_GROUPS[i].match(path)) return DEFAULTS_GROUPS[i];
4347
+ return { id: "other", title: "Other" };
4348
+ }
4349
+
4350
+ var defaultsState = { rowsByPath: {}, healthFallback: null };
4351
+
4352
+ function loadDefaults() {
4353
+ return Promise.all([
4354
+ loadTool("config_get", { section: "defaults" }),
4355
+ loadTool("config_get", { section: "daemon" }),
4356
+ loadTool("daemon_health", {}),
4357
+ ]).then(function (results) {
4358
+ renderDefaults(results[0], results[1], results[2]);
4359
+ });
4360
+ }
4361
+
4362
+ function renderDefaults(defR, daemonR, healthR) {
4363
+ var el = document.getElementById("sec-defaults");
4364
+
4365
+ if (!defR.ok && defR.unknown) {
4366
+ // Feature-detect fallback for a daemon that predates config_get: the
4367
+ // five effective knobs daemon_health already reports, read-only.
4368
+ var html = '<div class="notice">Full defaults editor needs a newer agentproto daemon (config_get). Showing the knobs daemon_health reports.</div>';
4369
+ if (healthR.ok) {
4370
+ var hd = healthR.data;
4371
+ var restartBadge = ' <span class="tag">restart</span>';
4372
+ html += '<div class="card" data-card-id="daemon"><div class="title"><span>Daemon</span>'
4373
+ + (hd.version ? '<span class="aside">' + escHtml(hd.version) + '</span>' : '') + '</div><div class="body"><dl>'
4374
+ + row("Resume sessions on boot", (hd.resumeSessionsOnBoot ? "Yes" : "No") + restartBadge)
4375
+ + row("Idle reap after", (hd.idleReapAfterMs ? formatMs(hd.idleReapAfterMs) : "off") + restartBadge)
4376
+ + row("Crash detect interval", (hd.crashDetectIntervalMs ? formatMs(hd.crashDetectIntervalMs) : "off") + restartBadge)
4377
+ + row("Restart sweep interval", (hd.restartSweepIntervalMs ? formatMs(hd.restartSweepIntervalMs) : "off") + restartBadge)
4378
+ + row("Turn stall after", (hd.turnStallAfterMs ? formatMs(hd.turnStallAfterMs) : "off") + restartBadge)
4379
+ + (hd.build && hd.build.sha ? row("Build", escHtml(hd.build.sha)) : "")
4380
+ + '</dl></div></div>';
4381
+ } else {
4382
+ html += '<div class="muted-note">daemon_health: ' + escHtml(healthR.message) + '</div>';
4383
+ }
4384
+ el.innerHTML = html;
4385
+ return;
4386
+ }
4387
+ if (!defR.ok) {
4388
+ el.innerHTML = '<div class="empty">config_get: ' + escHtml(defR.message) + '</div>';
4389
+ return;
4390
+ }
4391
+
4392
+ if (defR.data.revision) configRevision = defR.data.revision;
4393
+ var rows = (defR.data.keys || []).concat(daemonR.ok ? (daemonR.data.keys || []) : []);
4394
+ notePendingRestart(rows);
4395
+ defaultsState.rowsByPath = {};
4396
+ for (var i = 0; i < rows.length; i++) defaultsState.rowsByPath[rows[i].path] = rows[i];
4397
+
4398
+ el.innerHTML = renderDefaultsGroupsHtml();
4399
+ wireConfigRows(el, defaultsState.rowsByPath, function (path) {
4400
+ refreshDefaultsGroupFor(path);
4401
+ }, function () {
4402
+ loadDefaults();
4403
+ });
4404
+ }
4405
+
4406
+ function objectValues(obj) {
4407
+ var out = [];
4408
+ for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) out.push(obj[k]);
4409
+ return out;
4410
+ }
4411
+
4412
+ function renderDefaultsGroupsHtml() {
4413
+ var byId = {};
4414
+ var order = [];
4415
+ var rows = objectValues(defaultsState.rowsByPath);
4416
+ for (var i = 0; i < rows.length; i++) {
4417
+ var g = classifyDefaultsGroup(rows[i].path);
4418
+ if (!byId[g.id]) { byId[g.id] = { id: g.id, title: g.title, rows: [] }; order.push(g.id); }
4419
+ byId[g.id].rows.push(rows[i]);
4420
+ }
4421
+ var html = "";
4422
+ for (var j = 0; j < order.length; j++) {
4423
+ var group = byId[order[j]];
4424
+ html += '<div class="sect-h">' + escHtml(group.title) + '</div>' + buildConfigCardHtml(group.id, group.title, group.rows);
4425
+ }
4426
+ return html;
4427
+ }
4428
+
4429
+ function refreshDefaultsGroupFor(path) {
4430
+ var group = classifyDefaultsGroup(path);
4431
+ var groupPaths = [];
4432
+ for (var k in defaultsState.rowsByPath) {
4433
+ if (Object.prototype.hasOwnProperty.call(defaultsState.rowsByPath, k) && classifyDefaultsGroup(k).id === group.id) groupPaths.push(k);
4434
+ }
4435
+ loadTool("config_get", { keys: groupPaths }).then(function (r) {
4436
+ if (!r.ok) { toast("config_get: " + r.message, true); return; }
4437
+ if (r.data.revision) configRevision = r.data.revision;
4438
+ var newRows = r.data.keys || [];
4439
+ for (var i = 0; i < newRows.length; i++) defaultsState.rowsByPath[newRows[i].path] = newRows[i];
4440
+ notePendingRestart(newRows);
4441
+ var el = document.getElementById("sec-defaults");
4442
+ var cardEl = el.querySelector('[data-card-id="' + cssEscape(group.id) + '"]');
4443
+ if (!cardEl) return;
4444
+ cardEl.outerHTML = buildConfigCardHtml(group.id, group.title, newRows);
4445
+ var freshEl = el.querySelector('[data-card-id="' + cssEscape(group.id) + '"]');
4446
+ wireConfigRows(freshEl, defaultsState.rowsByPath, function (p2) { refreshDefaultsGroupFor(p2); }, function () { loadDefaults(); });
4447
+ });
4448
+ }
4449
+
4450
+ // ============================================================
4451
+ // Remote & pairing
4452
+ // ============================================================
4453
+
4454
+ // Held ONLY in memory, never in section state that survives a re-render
4455
+ // across sections, never in updateModelContext. Cleared on navigating away
4456
+ // from #remote (route()) and on host teardown (see boot, below).
4457
+ var remoteReveal = null;
4458
+ var pairReveal = null;
4459
+ var pairCountdownTimer = null;
4460
+
4461
+ function clearPairCountdown() {
4462
+ if (pairCountdownTimer) { clearInterval(pairCountdownTimer); pairCountdownTimer = null; }
4463
+ }
4464
+
4465
+ function startPairCountdown(expiresAtIso) {
4466
+ clearPairCountdown();
4467
+ pairCountdownTimer = setInterval(function () {
4468
+ var el = document.getElementById("pair-offer-countdown");
4469
+ if (!el) { clearPairCountdown(); return; }
4470
+ var ms = new Date(expiresAtIso).getTime() - Date.now();
4471
+ if (ms <= 0) { el.textContent = "expired"; clearPairCountdown(); return; }
4472
+ el.textContent = Math.ceil(ms / 1000) + "s remaining";
4473
+ }, 1000);
4474
+ }
4475
+
4476
+ function loadRemote() {
4477
+ return Promise.all([
4478
+ loadTool("remote_status", {}),
4479
+ loadTool("pair_list", {}),
4480
+ loadTool("tunnel_list", {}),
4481
+ ]).then(function (results) {
4482
+ renderRemote(results[0], results[1], results[2]);
4483
+ });
4484
+ }
4485
+
4486
+ function renderRemote(remoteR, pairR, tunnelR) {
4487
+ var el = document.getElementById("sec-remote");
4488
+ var html = '<div class="sect-h">Remote gateway</div>';
4489
+ if (remoteR.ok) {
4490
+ var rs = remoteR.data;
4491
+ html += '<div class="card" data-card-id="remote"><div class="body"><dl>'
4492
+ + row("Enabled", rs.enabled ? "Yes" : "No")
4493
+ + (rs.provider ? row("Provider", escHtml(rs.provider)) : "")
4494
+ + (rs.publicUrl ? row("Public URL", escHtml(rs.publicUrl)) : "")
4495
+ + (rs.pid != null ? row("PID", String(rs.pid)) : "")
4496
+ + (rs.createdAt ? row("Created", fmtRelative(rs.createdAt)) : "")
4497
+ + (rs.lastError ? row("Last error", escHtml(rs.lastError)) : "")
4498
+ + '</dl>'
4499
+ + '<div class="wallet-card-actions">'
4500
+ + (rs.enabled
4501
+ ? '<button type="button" class="btn-sm danger" id="remote-disable-btn">disable</button>'
4502
+ : '<button type="button" class="btn-sm primary" id="remote-enable-btn">enable</button>')
4503
+ + '</div>';
4504
+ if (remoteReveal) {
4505
+ html += '<div class="reveal-box">'
4506
+ + '<div>Shown once. Store it now, it will not be shown again.</div>'
4507
+ + (remoteReveal.bearerToken ? '<div>Bearer token<code>' + escHtml(remoteReveal.bearerToken) + '</code><button type="button" class="btn-sm" data-copy="bearer">copy token</button></div>' : '')
4508
+ + (remoteReveal.phoneUrl ? '<div>Phone link<code>' + escHtml(remoteReveal.phoneUrl) + '</code><button type="button" class="btn-sm" data-copy="phoneUrl">copy link</button></div>' : '')
4509
+ + (remoteReveal.mcpConfigSnippet ? '<div>.mcp.json<code>' + escHtml(remoteReveal.mcpConfigSnippet) + '</code><button type="button" class="btn-sm" data-copy="snippet">copy snippet</button></div>' : '')
4510
+ + '</div>';
4511
+ }
4512
+ html += '</div></div>';
4513
+ } else {
4514
+ html += '<div class="muted-note">remote_status: ' + escHtml(remoteR.message) + '</div>';
4515
+ }
4516
+
4517
+ html += '<div class="sect-h">Pairing<button type="button" class="btn-sm" id="pair-offer-btn">+ new pairing</button></div>';
4518
+ if (pairReveal) {
4519
+ html += '<div class="reveal-box">'
4520
+ + '<div>Offer URL. Shown once, expires <span id="pair-offer-countdown"></span>.</div>'
4521
+ + '<code>' + escHtml(pairReveal.url) + '</code>'
4522
+ + '<button type="button" class="btn-sm" data-copy="pairOffer">copy URL</button>'
4523
+ + '<div class="muted">fingerprint ' + escHtml(pairReveal.fingerprint) + '</div>'
4524
+ + '</div>';
4525
+ }
4526
+ if (pairR.ok) {
4527
+ var pairings = (pairR.data && pairR.data.pairings) || [];
4528
+ html += '<div class="card" data-card-id="pairing"><div class="body">';
4529
+ if (pairings.length === 0) {
4530
+ html += '<div class="empty">No paired clients.</div>';
4531
+ } else {
4532
+ html += '<table><thead><tr><th>Name</th><th>Fingerprint</th><th>Created</th><th>Last seen</th><th>Rendezvous</th><th></th></tr></thead><tbody>';
4533
+ for (var i = 0; i < pairings.length; i++) {
4534
+ var p = pairings[i];
4535
+ html += '<tr data-row-id="' + escHtml(p.fingerprint) + '"><td>' + escHtml(p.name) + '</td><td>' + escHtml(p.fingerprint) + '</td>'
4536
+ + '<td>' + escHtml(fmtRelative(p.createdAt)) + '</td><td>' + escHtml(fmtRelative(p.lastSeen)) + '</td><td>' + escHtml(p.rendezvous || "") + '</td>'
4537
+ + '<td><button type="button" class="btn-sm danger" data-pair-revoke="' + escHtml(p.fingerprint) + '">revoke</button></td></tr>';
4538
+ }
4539
+ html += '</tbody></table>';
4540
+ }
4541
+ html += '</div></div>';
4542
+ } else {
4543
+ html += '<div class="muted-note">pair_list: ' + escHtml(pairR.message) + '</div>';
4544
+ }
4545
+
4546
+ html += '<div class="sect-h">Tunnels</div>';
4547
+ if (tunnelR.ok) {
4548
+ var tunnels = (tunnelR.data && tunnelR.data.tunnels) || [];
4549
+ if (tunnels.length === 0) {
4550
+ html += '<div class="empty">No tunnels tracked by this daemon.</div>';
4551
+ } else {
4552
+ html += '<div class="card"><div class="body"><table><thead><tr><th>Name</th><th>Provider</th><th>Port</th><th>URL</th><th>Status</th></tr></thead><tbody>';
4553
+ for (var t = 0; t < tunnels.length; t++) {
4554
+ var tn = tunnels[t];
4555
+ html += '<tr><td>' + escHtml(tn.label || tn.name) + '</td><td>' + escHtml(tn.provider) + '</td><td>' + escHtml(String(tn.targetPort)) + '</td>'
4556
+ + '<td>' + escHtml(tn.publicUrl || "") + '</td><td>' + escHtml(tn.status) + '</td></tr>';
4557
+ }
4558
+ html += '</tbody></table></div></div>';
4559
+ }
4560
+ } else {
4561
+ html += '<div class="muted-note">tunnel_list: ' + escHtml(tunnelR.message) + '</div>';
4562
+ }
4563
+
4564
+ el.innerHTML = html;
4565
+ wireRemote(el);
4566
+ if (remoteReveal) {
4567
+ wireCopyButtons(el, { bearer: remoteReveal.bearerToken, phoneUrl: remoteReveal.phoneUrl, snippet: remoteReveal.mcpConfigSnippet });
4568
+ }
4569
+ if (pairReveal) {
4570
+ wireCopyButtons(el, { pairOffer: pairReveal.url });
4571
+ startPairCountdown(pairReveal.expiresAt);
4572
+ }
4573
+ }
4574
+
4575
+ function wireRemote(container) {
4576
+ var enableBtn = container.querySelector("#remote-enable-btn");
4577
+ if (enableBtn) {
4578
+ enableBtn.addEventListener("click", function () {
4579
+ confirmClick("remote-enable", enableBtn, function () {
4580
+ loadTool("remote_enable", {}).then(function (r) {
4581
+ if (!r.ok) { toast(r.message, true); return; }
4582
+ remoteReveal = { bearerToken: r.data.bearerToken, phoneUrl: r.data.phoneUrl, mcpConfigSnippet: r.data.mcpConfigSnippet };
4583
+ toast("Remote enabled.", false);
4584
+ loadRemote();
4585
+ });
4586
+ });
4587
+ });
4588
+ }
4589
+ var disableBtn = container.querySelector("#remote-disable-btn");
4590
+ if (disableBtn) {
4591
+ disableBtn.addEventListener("click", function () {
4592
+ confirmClick("remote-disable", disableBtn, function () {
4593
+ loadTool("remote_disable", {}).then(function (r) {
4594
+ if (!r.ok) { toast(r.message, true); return; }
4595
+ remoteReveal = null;
4596
+ toast("Remote disabled.", false);
4597
+ loadRemote();
4598
+ });
4599
+ });
4600
+ });
4601
+ }
4602
+ var offerBtn = container.querySelector("#pair-offer-btn");
4603
+ if (offerBtn) {
4604
+ offerBtn.addEventListener("click", function () {
4605
+ loadTool("pair_offer", {}).then(function (r) {
4606
+ if (!r.ok) { toast(r.message, true); return; }
4607
+ pairReveal = { url: r.data.url, fingerprint: r.data.fingerprint, expiresAt: r.data.expiresAt };
4608
+ toast("Pairing offer created.", false);
4609
+ loadRemote();
4610
+ });
4611
+ });
4612
+ }
4613
+ var revokeBtns = container.querySelectorAll("[data-pair-revoke]");
4614
+ for (var i = 0; i < revokeBtns.length; i++) {
4615
+ (function (btn) {
4616
+ var fp = btn.getAttribute("data-pair-revoke");
4617
+ btn.addEventListener("click", function () {
4618
+ confirmClick("pair-revoke-" + fp, btn, function () {
4619
+ loadTool("pair_revoke", { target: fp }).then(function (r) {
4620
+ if (!r.ok) { toast(r.message, true); return; }
4621
+ toast("Pairing revoked.", false);
4622
+ pairReveal = null;
4623
+ loadRemote();
4624
+ });
4625
+ });
4626
+ });
4627
+ })(revokeBtns[i]);
4628
+ }
4629
+ }
4630
+
4631
+ // ============================================================
4632
+ // Advanced
4633
+ // ============================================================
4634
+
4635
+ function loadAdvanced() {
4636
+ return Promise.all([
4637
+ loadTool("config_get", {}),
4638
+ loadTool("daemon_health", {}),
4639
+ ]).then(function (results) {
4640
+ renderAdvanced(results[0], results[1]);
4641
+ });
4642
+ }
4643
+
4644
+ function renderAdvanced(configR, healthR) {
4645
+ var el = document.getElementById("sec-advanced");
4646
+ var html = '<div class="sect-h">Raw config dump</div>';
4647
+ if (configR.ok) {
4648
+ html += '<div class="card"><div class="body"><pre style="white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace">' + escHtml(JSON.stringify(configR.data, null, 2)) + '</pre></div></div>';
4649
+ } else if (configR.unknown) {
4650
+ html += '<div class="notice">Full config dump needs a newer agentproto daemon (config_get). See Defaults for the knobs daemon_health reports.</div>';
4651
+ } else {
4652
+ html += '<div class="muted-note">config_get: ' + escHtml(configR.message) + '</div>';
4653
+ }
4654
+ html += '<div class="sect-h">Daemon health</div>';
4655
+ if (healthR.ok) {
4656
+ html += '<div class="card"><div class="body"><pre style="white-space:pre-wrap;font-size:11px;font-family:Menlo,Monaco,monospace">' + escHtml(JSON.stringify(healthR.data, null, 2)) + '</pre></div></div>';
4657
+ } else {
4658
+ html += '<div class="muted-note">daemon_health: ' + escHtml(healthR.message) + '</div>';
4659
+ }
4660
+ el.innerHTML = html;
4661
+ }
4662
+
4663
+ // ============================================================
4664
+ // boot
4665
+ // ============================================================
4666
+
4667
+ function loadHealthChip() {
4668
+ return loadTool("daemon_health", {}).then(function (r) {
4669
+ var chip = document.getElementById("daemon-chip");
4670
+ if (r.ok) {
4671
+ chip.className = "chip ok";
4672
+ var v = r.data.version || (r.data.build && r.data.build.sha) || r.data.status || "ok";
4673
+ document.getElementById("daemon-txt").textContent = "daemon " + String(v).slice(0, 16);
4674
+ } else {
4675
+ chip.className = "chip bad";
4676
+ document.getElementById("daemon-txt").textContent = "daemon unreachable";
4677
+ }
4678
+ });
4679
+ }
4680
+
4681
+ document.getElementById("refresh-btn").addEventListener("click", function () {
4682
+ loadHealthChip();
4683
+ reloadCurrent().catch(function (e) { toast(String(e && e.message || e), true); });
4684
+ });
4685
+
4686
+ window.McpApp.connect()
4687
+ .then(function (bridge) {
4688
+ callTool = bridge.callTool;
4689
+ updateModelContext = bridge.updateModelContext;
4690
+ if (bridge.onTeardown) {
4691
+ onTeardownFn = bridge.onTeardown;
4692
+ onTeardownFn(function () { clearRemoteSecrets(); });
4693
+ }
4694
+ renderNav();
4695
+ loadHealthChip();
4696
+ if (bridge.onToolInput) bridge.onToolInput(handleToolInput);
4697
+ route(true);
4698
+ })
4699
+ .catch(function (err) {
4700
+ document.getElementById("body").innerHTML =
4701
+ '<div class="empty">Standalone mode: no host bridge (' + escHtml(err.message) + ')</div>';
4702
+ });
4703
+ </script>
4704
+ </body>
4705
+ </html>`;
4706
+
4707
+ // src/config/index.ts
4708
+ var configApp = defineApp({
4709
+ id: "@agentproto/config",
4710
+ name: "agentproto config",
4711
+ version: "0.1.0",
4712
+ description: "Daemon-wide configuration: wallets (auth profiles + spend), harnesses, the model catalog, defaults & messaging knobs, remote & pairing, and a raw config dump.",
4713
+ agents: [],
4714
+ ui: {
4715
+ html: CONFIG_HTML,
4716
+ title: "agentproto config",
4717
+ tools: [...CONFIG_TOOLS]
4718
+ }
4719
+ });
2358
4720
  var narrator = {
2359
4721
  agent: defineAgent({
2360
4722
  schema: "agent/v1",
@@ -2404,7 +4766,7 @@ var narrateSession = defineWorkflow({
2404
4766
 
2405
4767
  // src/session-viewer/ui.ts
2406
4768
  var SESSION_VIEWER_TOOLS = ["session_list", "conversation_read"];
2407
- var APP_ID4 = "@agentproto/session-viewer";
4769
+ var APP_ID5 = "@agentproto/session-viewer";
2408
4770
  var SESSION_VIEWER_HTML = `<!DOCTYPE html>
2409
4771
  <html lang="en">
2410
4772
  <head>
@@ -2461,9 +4823,11 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sa
2461
4823
  .msg.role-user{align-self:flex-end;align-items:flex-end}
2462
4824
  .msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}
2463
4825
  .msg.role-system{align-self:center;max-width:90%}
4826
+ .msg.role-message{align-self:flex-start;align-items:flex-start}
2464
4827
  .bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
2465
4828
  .role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}
2466
4829
  .role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}
4830
+ .role-message .bubble{background:var(--bg2);border:1px dashed var(--blue);border-bottom-left-radius:3px}
2467
4831
  .role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}
2468
4832
  .mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}
2469
4833
  .mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}
@@ -2506,7 +4870,7 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sa
2506
4870
  </div>
2507
4871
  <script>
2508
4872
  var callTool = null;
2509
- var APP_ID = ${JSON.stringify(APP_ID4)};
4873
+ var APP_ID = ${JSON.stringify(APP_ID5)};
2510
4874
 
2511
4875
  // app_tool_call wraps its dispatch result at least once (its own text-result
2512
4876
  // body is the JSON-stringified inner MCP tool result); peel back through
@@ -2727,6 +5091,14 @@ function renderMessage(m) {
2727
5091
  if (m.role === "tool") {
2728
5092
  return '<div class="msg role-tool">' + renderToolResultChip(m) + "</div>";
2729
5093
  }
5094
+ if (m.role === "user" && m.from) {
5095
+ // A typed message from another session (session-message record) \u2014 never
5096
+ // the human: name the daemon-attested sender instead of "You".
5097
+ var who = m.from.label || (m.from.sessionId ? m.from.sessionId.slice(-6) : m.from.relation);
5098
+ return '<div class="msg role-message"><div class="mrow"><span class="mrole">from ' + escHtml(m.from.relation)
5099
+ + " " + escHtml(who) + " \xB7 " + escHtml(m.from.kind) + "</span>" + time + "</div>"
5100
+ + '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
5101
+ }
2730
5102
  if (m.role === "user") {
2731
5103
  return '<div class="msg role-user"><div class="mrow"><span class="mrole">You</span>' + time + "</div>"
2732
5104
  + '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
@@ -2881,19 +5253,34 @@ var sessionViewer = defineApp({
2881
5253
  tools: [...SESSION_VIEWER_TOOLS]
2882
5254
  }
2883
5255
  });
2884
-
2885
- // src/panel-bridge.ts
2886
5256
  function panelBridgeScript(appName) {
2887
5257
  return `// \u2500\u2500 MCP Apps bridge (shared: panel-bridge.ts) \u2500\u2500
2888
5258
  // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
2889
5259
  var _nextId = 1, _pending = {}, _notifyHandlers = [];
2890
5260
  var _hostContext = null, _hostContextHandlers = [];
5261
+ var _hostCaps = null;
2891
5262
  var _standaloneApp = null;
2892
5263
  function _isStandalone(){
2893
5264
  return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
2894
5265
  }
5266
+ // \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5267
+ // Panels that iframe the daemon's standalone app host carry this proof so
5268
+ // GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for
5269
+ // MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).
5270
+ // registerMcpApps replaces the placeholder with a real per-boot token when
5271
+ // serving the panel as a resource; every other render (tests, standalone
5272
+ // tab, VS Code HTTP-iframe panel \u2014 the latter two already pass
5273
+ // iframeEmbedAllowed's origin checks) keeps the literal, and
5274
+ // withEmbedToken() then degrades to an identity function.
5275
+ window.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";
5276
+ function withEmbedToken(url){
5277
+ var t = window.__AGENPROTO_EMBED_TOKEN__;
5278
+ if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;
5279
+ return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);
5280
+ }
2895
5281
  function post(msg){ window.parent.postMessage(msg, '*'); }
2896
5282
  function getHostContext(){ return _hostContext; }
5283
+ function getHostCapabilities(){ return _hostCaps; }
2897
5284
  function onHostContext(cb){
2898
5285
  _hostContextHandlers.push(cb);
2899
5286
  // Replay the last context so a late subscriber isn't stuck blind.
@@ -2942,7 +5329,7 @@ function initBridge(){
2942
5329
  return window.McpApp.connect().then(function(conn){
2943
5330
  _standaloneApp = conn;
2944
5331
  // No host to advertise a hostContext \u2014 default to inline with no
2945
- // other modes available, which keeps the #dm/#pin toggle buttons
5332
+ // other modes available, which keeps the display-mode toggle buttons
2946
5333
  // hidden (they only show for modes hostContext.availableDisplayModes
2947
5334
  // lists) instead of rendering two buttons with nothing to switch to.
2948
5335
  _setHostContext({displayMode: 'inline', availableDisplayModes: []});
@@ -2956,12 +5343,20 @@ function initBridge(){
2956
5343
  // The initialize result carries the initial hostContext (displayMode +
2957
5344
  // availableDisplayModes) \u2014 capture it before notifying the host.
2958
5345
  if (result && result.hostContext) _setHostContext(result.hostContext);
5346
+ // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,
5347
+ // "Host supports opening external URLs". Captured once, read via
5348
+ // getHostCapabilities() by panels that need to branch on it.
5349
+ if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;
2959
5350
  rpcNotify('ui/notifications/initialized', {});
2960
5351
  });
2961
5352
  }
2962
5353
  function requestDisplayMode(mode){
2963
5354
  return rpcRequest('ui/request-display-mode', {mode: mode});
2964
5355
  }
5356
+ function openLink(url){
5357
+ if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
5358
+ return rpcRequest('ui/open-link', {url: url});
5359
+ }
2965
5360
  function callTool(name, args){
2966
5361
  var raw = _standaloneApp
2967
5362
  ? _standaloneApp.callTool(name, args || {})
@@ -2976,71 +5371,17 @@ function callTool(name, args){
2976
5371
  });
2977
5372
  }
2978
5373
 
2979
- // \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
2980
- // Injected by the shared bridge so every panel gets them without touching
2981
- // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
2982
- // panel stays inline by default; the user expands on demand. Buttons only
2983
- // appear for modes the host advertises in hostContext.availableDisplayModes.
2984
- (function(){
2985
- function mount(){
2986
- var style = document.createElement('style');
2987
- style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
2988
- + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
2989
- + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
2990
- + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
2991
- + '#dm{right:8px}#pin{right:118px}'
2992
- + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
2993
- + '@media (prefers-color-scheme:dark){'
2994
- + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
2995
- + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
2996
- document.head.appendChild(style);
2997
-
2998
- var btn = document.createElement('button');
2999
- btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
3000
- var pin = document.createElement('button');
3001
- pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
3002
- document.body.appendChild(pin);
3003
- document.body.appendChild(btn);
3004
-
3005
- function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
3006
-
3007
- // Re-sync button visibility + label from the current host context.
3008
- function syncBtn(ctx){
3009
- ctx = ctx || {};
3010
- var avail = ctx.availableDisplayModes;
3011
- // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
3012
- console.log('[mcp-app] displayMode=', ctx.displayMode,
3013
- 'availableDisplayModes=', avail);
3014
-
3015
- // Fullscreen toggle button.
3016
- if (has(avail, 'fullscreen')){
3017
- btn.style.display = 'block';
3018
- btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
3019
- } else { btn.style.display = 'none'; }
3020
-
3021
- // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
3022
- if (has(avail, 'pip')){
3023
- pin.style.display = 'block';
3024
- pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
3025
- } else { pin.style.display = 'none'; }
3026
- }
3027
-
3028
- onHostContext(syncBtn);
3029
-
3030
- btn.addEventListener('click', function(){
3031
- var ctx = getHostContext() || {};
3032
- var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
3033
- requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
3034
- });
3035
-
3036
- pin.addEventListener('click', function(){
3037
- var ctx = getHostContext() || {};
3038
- requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
3039
- });
3040
- }
3041
- if (document.body) mount();
3042
- else document.addEventListener('DOMContentLoaded', mount);
3043
- })();`;
5374
+ // \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
5375
+ // The installer is inlined verbatim (it is self-contained and idempotent),
5376
+ // then handed this panel's own JSON-RPC plumbing. NO auto-request: the
5377
+ // panel stays inline until the user clicks, and the buttons only appear for
5378
+ // the modes the host advertises in hostContext.availableDisplayModes.
5379
+ ${DISPLAY_MODE_SCRIPT_BODY}
5380
+ var displayMode = window.AgentprotoUI.installDisplayMode({
5381
+ getHostContext: getHostContext,
5382
+ onHostContext: onHostContext,
5383
+ requestDisplayMode: requestDisplayMode
5384
+ });`;
3044
5385
  }
3045
5386
 
3046
5387
  // src/sessions-panel/panel.ts
@@ -3235,6 +5576,7 @@ function renderSidebar() {
3235
5576
  var blocked = '';
3236
5577
  if (s.blockedOn === 'subagent') blocked = '<span class="badge bs">&#129513; sous-agent</span>';
3237
5578
  else if (s.blockedOn === 'command') blocked = '<span class="badge bs">&#9203; commande</span>';
5579
+ else if (s.blockedOn === 'inbox') blocked = '<span class="badge bs">&#128236; attend messages</span>';
3238
5580
  html += '<div class="si' + active + '" onclick="selectSession(\\'' + s.id + '\\')">'
3239
5581
  + '<div class="sn">' + escHtml(label) + '</div>'
3240
5582
  + '<div class="sm">'
@@ -4839,14 +7181,20 @@ function reduceEvent(state, record) {
4839
7181
  case 'usage_update': {
4840
7182
  // Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
4841
7183
  // the prior snapshot. state.rows is reused as-is (it didn't change).
4842
- return {
4843
- rows: state.rows,
4844
- usage: {
4845
- size: record.size, used: record.used, cost: record.cost,
4846
- tokensIn: record.tokensIn, tokensOut: record.tokensOut,
4847
- seq: record.seq, ts: record.ts,
4848
- },
7184
+ // Except the context window: a cost-bearing frame's size sticks (see
7185
+ // the TS module's usage_update arm).
7186
+ var sizeReported = typeof record.size === 'number' && record.size > 0;
7187
+ var authoritative = record.cost !== undefined && sizeReported;
7188
+ var prevAuth = !!(state.usage && state.usage.sizeAuthoritative);
7189
+ var keepSize = !authoritative && (prevAuth || !sizeReported);
7190
+ var usage = {
7191
+ size: keepSize ? (state.usage ? state.usage.size : undefined) : record.size,
7192
+ used: record.used, cost: record.cost,
7193
+ tokensIn: record.tokensIn, tokensOut: record.tokensOut,
7194
+ seq: record.seq, ts: record.ts,
4849
7195
  };
7196
+ if (authoritative || (keepSize && prevAuth)) usage.sizeAuthoritative = true;
7197
+ return { rows: state.rows, usage: usage };
4850
7198
  }
4851
7199
  default:
4852
7200
  return state;
@@ -5507,14 +7855,89 @@ var liveSessionApp = defineApp({
5507
7855
  }
5508
7856
  });
5509
7857
 
7858
+ // src/session-chat/blob-embed.ts
7859
+ var BLOB_BOOT_MESSAGE_TYPE = "agentproto-blob-boot";
7860
+ function blobEmbedScript() {
7861
+ return `// \u2500\u2500 Blob pass-through transform (shared: session-chat/blob-embed.ts) \u2500\u2500
7862
+ function _blobEscapeAttr(s){ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;'); }
7863
+ // Rewrite the fetched chat html so it works as a blob: document: <base> on
7864
+ // the deep link, a fetch/EventSource shim that carries the embed token to
7865
+ // the daemon origin, and the standalone bridge's ./tool-call made absolute.
7866
+ function transformChatHtmlForBlob(html, url, token){
7867
+ var origin = new URL(url).origin;
7868
+ var tc = new URL('./tool-call', url);
7869
+ tc.searchParams.set('et', token);
7870
+ // The app-visible query for the blob doc: the deep link's search minus the
7871
+ // embed token (the token must not ride the app's visible params). The
7872
+ // blob document's own location has no params, so this is the ONLY channel
7873
+ // for ?session= / ?embed=1 \u2014 the app reads it via bootSearchParams().
7874
+ var bootUrl = new URL(url);
7875
+ bootUrl.searchParams.delete('et');
7876
+ var bootQuery = bootUrl.search;
7877
+ var shim = '<script>(function(){'
7878
+ + 'try { window.parent.postMessage({ type: ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)} }, "*"); } catch (_) {}'
7879
+ + 'var ORIGIN = ' + JSON.stringify(origin) + ', TOKEN = ' + JSON.stringify(token) + ';'
7880
+ + 'window.__AGENTPROTO_BASEURL__ = ORIGIN;'
7881
+ + 'var _h = window.history, _rs = _h.replaceState.bind(_h), _ps = _h.pushState.bind(_h);'
7882
+ + 'function _sameOrLoc(u){'
7883
+ + 'try {'
7884
+ + 'var x = new URL(u == null ? window.location.href : u, document.baseURI);'
7885
+ + 'return x.origin === window.location.origin ? u : window.location.href;'
7886
+ + '} catch (_) { return window.location.href; }'
7887
+ + '}'
7888
+ + '_h.replaceState = function(s, t, u){'
7889
+ + 'try { _rs(s, t, _sameOrLoc(u)); } catch (_) { try { _rs(s, t, window.location.href); } catch (_) {} }'
7890
+ + '};'
7891
+ + '_h.pushState = function(s, t, u){'
7892
+ + 'try { _ps(s, t, _sameOrLoc(u)); } catch (_) { try { _ps(s, t, window.location.href); } catch (_) {} }'
7893
+ + '};'
7894
+ + 'window.__AGENTPROTO_BOOT_QUERY__ = ' + JSON.stringify(bootQuery) + ';'
7895
+ + 'function withEt(u){'
7896
+ + 'try {'
7897
+ + 'var x = new URL(String(u), document.baseURI);'
7898
+ + 'if (x.origin !== ORIGIN) return null;'
7899
+ + 'if (!x.searchParams.has("et")) x.searchParams.set("et", TOKEN);'
7900
+ + 'return x.href;'
7901
+ + '} catch (_) { return null; }'
7902
+ + '}'
7903
+ + 'var _fetch = window.fetch;'
7904
+ + 'if (typeof _fetch === "function") {'
7905
+ + 'window.fetch = function(input, init){'
7906
+ + 'var r;'
7907
+ + 'if (typeof Request !== "undefined" && input instanceof Request) {'
7908
+ + 'r = withEt(input.url);'
7909
+ + 'if (r) { try { input = new Request(r, input); } catch (_) {} }'
7910
+ + '} else {'
7911
+ + 'r = withEt(input);'
7912
+ + 'if (r) input = r;'
7913
+ + '}'
7914
+ + 'return _fetch.call(window, input, init);'
7915
+ + '};'
7916
+ + '}'
7917
+ + 'var _ES = window.EventSource;'
7918
+ + 'if (typeof _ES === "function") {'
7919
+ + 'window.EventSource = function(u, cfg){ return new _ES(withEt(u) || u, cfg); };'
7920
+ + 'window.EventSource.prototype = _ES.prototype;'
7921
+ + '}'
7922
+ + '})();<\\/script>';
7923
+ var head = '<base href="' + _blobEscapeAttr(url) + '">' + shim;
7924
+ var m = html.match(/<head[^>]*>/i);
7925
+ var out = m
7926
+ ? html.slice(0, m.index + m[0].length) + head + html.slice(m.index + m[0].length)
7927
+ : head + html;
7928
+ return out.split('fetch("./tool-call"').join('fetch(' + JSON.stringify(tc.href));
7929
+ }`;
7930
+ }
7931
+
5510
7932
  // src/session-chat/panel.ts
5511
7933
  function escapeAttr(text) {
5512
7934
  return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
5513
7935
  }
5514
7936
  function sessionChatEmbedHtml(initData) {
5515
- const url = initData.url;
5516
- const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : "app not installed";
5517
- const iframe = url != null ? `<iframe id="chat" title="Session Chat" src="${escapeAttr(url)}"></iframe>` : "";
7937
+ const url = typeof initData.url === "string" && initData.url ? initData.url : null;
7938
+ const notInstalled = initData.installed === false;
7939
+ const link = url != null ? `<a href="${escapeAttr(url)}" target="_blank" rel="noreferrer">open in a tab</a>` : notInstalled ? "app not installed" : "connecting\u2026";
7940
+ const iframe = url != null ? `<iframe id="chat" title="Session Chat"></iframe>` : "";
5518
7941
  return `<!DOCTYPE html>
5519
7942
  <html lang="en">
5520
7943
  <head>
@@ -5526,12 +7949,354 @@ function sessionChatEmbedHtml(initData) {
5526
7949
  html,body{height:100%;font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#e6edf3;overflow:hidden}
5527
7950
  #bar{padding:6px 12px;font-size:12px;color:#8b949e;background:#161b22;border-bottom:1px solid #30363d}
5528
7951
  #bar a{color:#58a6ff}
5529
- #chat{border:0;display:block;width:100%;height:calc(100% - 29px)}
7952
+ #mode{color:#6e7681}
7953
+ #stage{position:relative;height:calc(100% - 29px)}
7954
+ #chat{position:absolute;inset:0;z-index:2;border:0;display:block;width:100%;height:100%}
7955
+ #card{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:24px}
7956
+ #card h2{font-size:15px;font-weight:600}
7957
+ #card .sid{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#8b949e;background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 8px}
7958
+ #card p{font-size:12px;color:#8b949e;max-width:420px;line-height:1.5}
7959
+ #card a{display:inline-block;margin-top:2px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:13px;padding:7px 14px;border-radius:6px;text-decoration:none}
7960
+ #card a:hover{border-color:#58a6ff}
7961
+ #notice{display:none;max-width:560px;margin:0 auto;padding:48px 24px;line-height:1.6}
7962
+ #notice.show{display:block}
7963
+ #notice h1{font-size:18px;margin-bottom:12px}
7964
+ #notice code{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:13px}
7965
+ #notice p{margin:8px 0;color:#8b949e;font-size:13px}
5530
7966
  </style>
5531
7967
  </head>
5532
7968
  <body>
5533
- <div id="bar">Session Chat &#183; ${link}</div>
5534
- ${iframe}
7969
+ <div id="bar">Session Chat &#183; <span id="link">${link}</span><span id="mode"></span></div>
7970
+ <div id="stage">${iframe}<div id="card">
7971
+ <h2>Session Chat</h2>
7972
+ <div id="card-session" class="sid">&#8212;</div>
7973
+ <p>Full embed isn't supported by this host &#8212; the chat opens in a browser tab.</p>
7974
+ <a id="card-open" href="#" target="_blank" rel="noreferrer">Open chat</a>
7975
+ </div><div id="notice"${notInstalled ? ' class="show"' : ""}>
7976
+ <h1>Session Chat is not installed</h1>
7977
+ <p>This panel is a thin launcher for the <code>@agentik/session-chat</code> app &#8212;
7978
+ it does not bundle the chat UI itself.</p>
7979
+ <p>To use it, install the app into this daemon:</p>
7980
+ <p><code>agentproto app install @agentik/session-chat</code></p>
7981
+ </div></div>
7982
+ <script>
7983
+ window.__APP_INIT__ = ${JSON.stringify(initData)};
7984
+
7985
+ ${panelBridgeScript("agentproto-session-chat")}
7986
+
7987
+ ${blobEmbedScript()}
7988
+
7989
+ // \u2500\u2500 Session-chat launcher logic \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7990
+ var mountedUrl = (window.__APP_INIT__ && window.__APP_INIT__.url) || null;
7991
+ var pinnedSessionId = null; // session named by the host's tool-result push
7992
+ var pendingUrl = null; // url pushed by the host before the bridge was up
7993
+ var pendingNotInstalled = false;
7994
+ var bridged = false;
7995
+ var cardUrl = null; // deep link once resolved (drives the card button)
7996
+ var cardSessionEl = document.getElementById('card-session');
7997
+ var cardOpenEl = document.getElementById('card-open');
7998
+ var mountGen = 0; // bumps per mount(); async work checks it before touching the DOM
7999
+ var blobUrl = null; // live object url of the blob-mounted chat, if any
8000
+ var blobBootTimer = null;
8001
+ var blobBootOk = false;
8002
+
8003
+ function escapeHtml(text) {
8004
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
8005
+ }
8006
+
8007
+ function setMode(text) {
8008
+ document.getElementById('mode').textContent = text ? ' \\u00b7 ' + text : '';
8009
+ // The chat app is live inside the frame \u2014 the launcher bar (title, deep
8010
+ // link, mount mode) is noise on top of the real surface. Hide it and let
8011
+ // the frame own the full height. Only a failed mount (launcher card /
8012
+ // not-installed notice) shows the bar again.
8013
+ var bar = document.getElementById('bar');
8014
+ var stage = document.getElementById('stage');
8015
+ if (bar && stage) {
8016
+ bar.style.display = text ? 'none' : '';
8017
+ stage.style.height = text ? '100%' : 'calc(100% - 29px)';
8018
+ }
8019
+ }
8020
+
8021
+ // One fresh #chat frame on the stage (over the card), the previous one gone.
8022
+ function newFrame() {
8023
+ var stage = document.getElementById('stage');
8024
+ var old = document.getElementById('chat');
8025
+ if (old) old.remove();
8026
+ var frame = document.createElement('iframe');
8027
+ frame.id = 'chat';
8028
+ frame.title = 'Session Chat';
8029
+ stage.insertBefore(frame, stage.firstChild);
8030
+ return frame;
8031
+ }
8032
+
8033
+ function revokeBlob() {
8034
+ if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
8035
+ if (blobUrl) {
8036
+ try { URL.revokeObjectURL(blobUrl); } catch (_) {}
8037
+ blobUrl = null;
8038
+ }
8039
+ }
8040
+
8041
+ function mount(url) {
8042
+ if (!url || url === mountedUrl) return;
8043
+ mountedUrl = url;
8044
+ var gen = ++mountGen;
8045
+ revokeBlob();
8046
+ document.getElementById('notice').className = '';
8047
+ setCardUrl(url);
8048
+ document.getElementById('link').innerHTML =
8049
+ '<a href="' + escapeHtml(url) + '" target="_blank" rel="noreferrer">open in a tab</a>';
8050
+ var tokened = withEmbedToken(url);
8051
+ if (tokened === url) {
8052
+ // No per-boot token baked in (standalone tab, VS Code HTTP-iframe panel):
8053
+ // the direct frame already passes the daemon's origin checks, and the
8054
+ // blob path's daemon calls would have nothing to carry \u2014 skip it.
8055
+ mountDirect(url);
8056
+ return;
8057
+ }
8058
+ mountBlob(url, tokened, gen).catch(function (err) {
8059
+ if (gen !== mountGen) return;
8060
+ // The fetch itself was refused (host connect-src not merging our
8061
+ // connectDomains, PNA, daemon down, \u2026) \u2014 probe answered: no blob path.
8062
+ console.warn('[session-chat] blob mount failed, falling back to a direct frame:',
8063
+ err && err.message ? err.message : String(err));
8064
+ mountDirect(url);
8065
+ });
8066
+ }
8067
+
8068
+ // Direct-src iframe of the deep link, with the block probe \u2192 launcher card
8069
+ // fallback. The terminal mount path when the blob one is unavailable.
8070
+ function mountDirect(url) {
8071
+ var frame = newFrame();
8072
+ frame.src = withEmbedToken(url);
8073
+ setMode('direct frame');
8074
+ armBlockProbe(frame);
8075
+ }
8076
+
8077
+ // Blob pass-through: fetch the chat html through the embed token, rewrite it
8078
+ // for life as a blob: document (transformChatHtmlForBlob, blob-embed.ts) and
8079
+ // mount it \u2014 blob: passes a host frame-src that refuses the daemon origin.
8080
+ function mountBlob(url, tokened, gen) {
8081
+ return fetch(tokened, { credentials: 'omit' }).then(function (res) {
8082
+ if (!res.ok) throw new Error('HTTP ' + res.status);
8083
+ return res.text();
8084
+ }).then(function (html) {
8085
+ if (gen !== mountGen) return;
8086
+ var doc = transformChatHtmlForBlob(html, url, window.__AGENPROTO_EMBED_TOKEN__);
8087
+ blobUrl = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));
8088
+ var frame = newFrame();
8089
+ armBlobBootProbe(frame, url, gen);
8090
+ frame.src = blobUrl;
8091
+ setMode('embedded');
8092
+ });
8093
+ }
8094
+
8095
+ // The blob document's shim posts a boot ack as its first statement. No ack
8096
+ // within the settle window means the host refused the blob frame or its
8097
+ // inherited CSP killed the document's inline scripts \u2014 surface that through
8098
+ // the direct-frame path (whose own probe ends on the launcher card) rather
8099
+ // than leaving a silent blank.
8100
+ window.addEventListener('message', function (evt) {
8101
+ var d = evt.data;
8102
+ if (!d || typeof d !== 'object' || d.type !== ${JSON.stringify(BLOB_BOOT_MESSAGE_TYPE)}) return;
8103
+ var frame = document.getElementById('chat');
8104
+ if (!frame || evt.source !== frame.contentWindow) return;
8105
+ blobBootOk = true;
8106
+ if (blobBootTimer) { clearTimeout(blobBootTimer); blobBootTimer = null; }
8107
+ });
8108
+
8109
+ function armBlobBootProbe(frame, url, gen) {
8110
+ blobBootOk = false;
8111
+ blobBootTimer = setTimeout(function () {
8112
+ blobBootTimer = null;
8113
+ if (gen !== mountGen || blobBootOk) return;
8114
+ console.warn('[session-chat] blob document never booted (host CSP?), falling back to a direct frame');
8115
+ revokeBlob();
8116
+ frame.remove();
8117
+ mountDirect(url);
8118
+ }, 5000);
8119
+ }
8120
+
8121
+ function showNotInstalled() {
8122
+ var old = document.getElementById('chat');
8123
+ if (old) old.remove();
8124
+ revokeBlob();
8125
+ setMode('');
8126
+ mountedUrl = null;
8127
+ document.getElementById('card').style.display = 'none';
8128
+ document.getElementById('notice').className = 'show';
8129
+ document.getElementById('link').textContent = 'app not installed';
8130
+ }
8131
+
8132
+ // The body of an ext-apps tool-result notification, whichever wrapping
8133
+ // shape the host uses (params IS the CallToolResult, or nests it under
8134
+ // params.result). null for error results / non-JSON text.
8135
+ function parseToolResultBody(params) {
8136
+ if (!params || typeof params !== 'object') return null;
8137
+ var res = (params.result && typeof params.result === 'object') ? params.result : params;
8138
+ if (res.isError) return null;
8139
+ var content = Array.isArray(res.content) ? res.content : [];
8140
+ var item = content[0];
8141
+ if (!item || item.type !== 'text' || typeof item.text !== 'string') return null;
8142
+ try { return JSON.parse(item.text); } catch (_) { return null; }
8143
+ }
8144
+
8145
+ // INLINED COPY of live-session/logic.ts extractToolResultSessionId \u2014 keep
8146
+ // in sync. \`sessionId\` (an agentproto_session_chat / live_session result)
8147
+ // wins over \`id\` (an agent_start result: the spawned session descriptor).
8148
+ function extractToolResultSessionId(params) {
8149
+ var body = parseToolResultBody(params);
8150
+ if (!body) return null;
8151
+ if (typeof body.sessionId === 'string' && body.sessionId) return body.sessionId;
8152
+ if (typeof body.id === 'string' && body.id) return body.id;
8153
+ return null;
8154
+ }
8155
+
8156
+ // \u2500\u2500 Embed-token refresh \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8157
+ // The token baked into this page by registerMcpApps dies with the daemon
8158
+ // boot that minted it (runtime embed-tokens.ts), but hosts cache the
8159
+ // rendered widget: Claude Desktop keeps a conversation's srcdoc, so after
8160
+ // any daemon restart the cached page replays a dead token and every daemon
8161
+ // call 403s \u2014 the blob fetch fails, the direct frame is refused, and the
8162
+ // user is left on the launcher card forever. Every tool result carries a
8163
+ // live token (SessionChatOutput.embedToken), so adopt it before mounting.
8164
+ //
8165
+ // Only a render that WAS baked refreshes. A standalone tab or VS Code
8166
+ // HTTP-iframe panel still holds the literal placeholder and passes the
8167
+ // daemon's origin checks on its own; handing it a token would switch it
8168
+ // onto the blob path for no reason.
8169
+ function adoptEmbedToken(body) {
8170
+ var cur = window.__AGENPROTO_EMBED_TOKEN__;
8171
+ if (!cur || cur === '__AGENPROTO_EMBED_TOKEN__') return;
8172
+ if (!body || typeof body.embedToken !== 'string' || !body.embedToken) return;
8173
+ window.__AGENPROTO_EMBED_TOKEN__ = body.embedToken;
8174
+ }
8175
+
8176
+ // Turn a session id into the installed app's deep link over the bridge.
8177
+ function resolveSession(sessionId) {
8178
+ noteSession(sessionId);
8179
+ return callTool('agentproto_session_chat', sessionId ? { sessionId: sessionId } : {})
8180
+ .then(function(out) {
8181
+ adoptEmbedToken(out);
8182
+ if (out && out.installed && typeof out.url === 'string' && out.url) mount(out.url);
8183
+ else showNotInstalled();
8184
+ })
8185
+ .catch(function(err) {
8186
+ document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
8187
+ });
8188
+ }
8189
+
8190
+ // \u2500\u2500 Launcher card (the frame-blocked fallback UI) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8191
+ // The iframe mounts OVER the card. When the host's own CSP refuses the
8192
+ // frame (Claude Desktop / Codex widget frames run frame-src 'self' blob:
8193
+ // data: and do not yet merge csp.frameDomains), the frame stays on a
8194
+ // same-origin about:blank \u2014 readable, unlike the loaded cross-origin chat \u2014
8195
+ // and is removed after a settle window so the card becomes the interactive
8196
+ // surface.
8197
+ //
8198
+ // The anchor carries target="_blank" rel="noreferrer" as the fallback path,
8199
+ // but that is a plain no-op in hosts that sandbox the widget iframe without
8200
+ // allow-popups (observed in Claude Desktop's side chat) \u2014 a normal click
8201
+ // on such a host does nothing. Where the host advertises the capability
8202
+ // (McpUiInitializeResult.hostCapabilities.openLinks in the ext-apps spec,
8203
+ // captured by the bridge's getHostCapabilities()), route through the
8204
+ // host-mediated ui/open-link request instead, which is not subject to the
8205
+ // iframe's popup sandboxing.
8206
+ cardOpenEl.addEventListener('click', function (evt) {
8207
+ if (!cardUrl) { evt.preventDefault(); return; }
8208
+ if (getHostCapabilities() && getHostCapabilities().openLinks) {
8209
+ evt.preventDefault();
8210
+ openLink(cardUrl).catch(function () { window.open(cardUrl, '_blank'); });
8211
+ }
8212
+ });
8213
+
8214
+ function noteSession(id) {
8215
+ if (id && cardSessionEl) cardSessionEl.textContent = id;
8216
+ }
8217
+
8218
+ function setCardUrl(u) {
8219
+ if (!u) return;
8220
+ cardUrl = u;
8221
+ cardOpenEl.setAttribute('href', u);
8222
+ }
8223
+
8224
+ // A blocked frame never leaves about:blank (same-origin with this page \u2014
8225
+ // readable); the loaded chat is cross-origin (the probe throws). A readable
8226
+ // frame that is STILL blank when the settle window closes = blocked.
8227
+ function isBlank(frame) {
8228
+ try { return frame.contentWindow.location.href === 'about:blank'; } catch (_) { return false; }
8229
+ }
8230
+
8231
+ function armBlockProbe(frame) {
8232
+ var settle = null;
8233
+ function probe() {
8234
+ if (settle) { clearTimeout(settle); settle = null; }
8235
+ if (!isBlank(frame)) return;
8236
+ // Still on about:blank right now \u2014 arm the settle window, but
8237
+ // re-check when it closes: a slow (not blocked) load may have landed
8238
+ // in the meantime, and only a frame STILL blank after the window gets
8239
+ // removed. load firing in between clears this timeout above.
8240
+ settle = setTimeout(function () {
8241
+ settle = null;
8242
+ if (isBlank(frame)) frame.remove();
8243
+ }, 2000);
8244
+ }
8245
+ setTimeout(function () {
8246
+ frame.addEventListener('load', probe);
8247
+ probe();
8248
+ }, 100);
8249
+ }
8250
+
8251
+ // A url baked at render time (a host that re-renders per call with a real
8252
+ // initData) mounts on boot \u2014 through the same blob-first path as every
8253
+ // other mount, since the same opaque widget context frames it. The
8254
+ // pre-rendered #chat frame is replaced by mount()'s own.
8255
+ if (mountedUrl) {
8256
+ var bootUrl = mountedUrl;
8257
+ mountedUrl = null;
8258
+ adoptEmbedToken(window.__APP_INIT__);
8259
+ mount(bootUrl);
8260
+ }
8261
+
8262
+ // The host pushes the triggering tool call's result (ext-apps
8263
+ // ui/notifications/tool-result). For agent_start that result IS the spawned
8264
+ // session's descriptor \u2014 pin the widget to THAT session.
8265
+ onHostNotification(function(method, params) {
8266
+ if (method !== 'ui/notifications/tool-result') return;
8267
+ var body = parseToolResultBody(params);
8268
+ if (!body) return;
8269
+ if (typeof body.url === 'string' && body.url && body.installed !== false) {
8270
+ // Adopt now, not at mount time: the token doesn't depend on the bridge.
8271
+ adoptEmbedToken(body);
8272
+ if (bridged) mount(body.url); else pendingUrl = body.url;
8273
+ return;
8274
+ }
8275
+ if (body.installed === false) {
8276
+ if (bridged) showNotInstalled(); else pendingNotInstalled = true;
8277
+ return;
8278
+ }
8279
+ var id = extractToolResultSessionId(params);
8280
+ if (!id || id === pinnedSessionId) return;
8281
+ pinnedSessionId = id;
8282
+ noteSession(id);
8283
+ if (bridged) resolveSession(id);
8284
+ });
8285
+
8286
+ initBridge().then(function() {
8287
+ bridged = true;
8288
+ if (pendingUrl) { mount(pendingUrl); pendingUrl = null; return; }
8289
+ if (pendingNotInstalled) { showNotInstalled(); return; }
8290
+ if (mountedUrl) return; // rendered with a real url
8291
+ var init = window.__APP_INIT__ || {};
8292
+ if (init.installed === false) { showNotInstalled(); return; }
8293
+ // No url baked in (static ui:// render): resolve over the bridge \u2014 the
8294
+ // pinned session when the host named one, else the app's picker.
8295
+ return resolveSession(pinnedSessionId);
8296
+ }).catch(function(err) {
8297
+ document.getElementById('link').textContent = 'bridge error: ' + (err && err.message ? err.message : String(err));
8298
+ });
8299
+ </script>
5535
8300
  </body>
5536
8301
  </html>`;
5537
8302
  }
@@ -5578,7 +8343,7 @@ function sessionChatAppUrl(httpBaseUrl, sessionId) {
5578
8343
  params.set("embed", "1");
5579
8344
  return `${base}?${params.toString()}`;
5580
8345
  }
5581
- function frameOrigin(httpBaseUrl) {
8346
+ function daemonOrigin(httpBaseUrl) {
5582
8347
  return new URL(httpBaseUrl).origin;
5583
8348
  }
5584
8349
  function makeSessionChatApp(ops) {
@@ -5588,12 +8353,27 @@ function makeSessionChatApp(ops) {
5588
8353
  title: "Session Chat",
5589
8354
  description: "Open the Session Chat app \u2014 a chat-thread view of any agentproto daemon session: full transcript replay, live follow-over SSE, and a composer that sends prompts into the same session. Requires the `@agentik/session-chat` app to be installed; without it the panel shows install instructions. Pass `sessionId` to deep-link straight into a known session.",
5590
8355
  inputSchema: sessionChatInputSchema,
5591
- execute: async (input) => ({
5592
- installed,
5593
- url: installed ? sessionChatAppUrl(ops.httpBaseUrl, input.sessionId) : null
5594
- }),
8356
+ execute: async (input) => {
8357
+ if (!installed) return { installed, url: null };
8358
+ const token = ops.mintEmbedToken?.();
8359
+ return {
8360
+ installed,
8361
+ url: sessionChatAppUrl(ops.httpBaseUrl, input.sessionId),
8362
+ // Re-arms a cached widget whose baked token died with an earlier
8363
+ // daemon boot (see SessionChatOutput.embedToken).
8364
+ ...token ? { embedToken: token } : {}
8365
+ };
8366
+ },
5595
8367
  html: (initData) => sessionChatEmbedHtml(initData),
5596
- csp: { frameDomains: [frameOrigin(ops.httpBaseUrl)] }
8368
+ // Spec-correct either way (ext-apps McpUiResourceMeta). Measured
8369
+ // 2026-09-18: Claude Desktop / Codex do NOT merge frameDomains into
8370
+ // their widget frame-src; whether they merge connectDomains is unknown,
8371
+ // so the panel probes it at runtime (blob fetch → fallback) rather than
8372
+ // assuming either way.
8373
+ csp: {
8374
+ frameDomains: [daemonOrigin(ops.httpBaseUrl)],
8375
+ connectDomains: [daemonOrigin(ops.httpBaseUrl)]
8376
+ }
5597
8377
  };
5598
8378
  }
5599
8379
  var sessionChatApp = defineApp({
@@ -5604,7 +8384,21 @@ var sessionChatApp = defineApp({
5604
8384
  ui: {
5605
8385
  html: SESSION_CHAT_FALLBACK_HTML,
5606
8386
  title: "Session Chat",
5607
- tools: ["session_list", "agent_start", "adapter_list", "conversation_read"]
8387
+ // `session_restart` is what makes an ENDED session (completed / killed /
8388
+ // exited / crashed) recoverable from the chat instead of a dead end: the
8389
+ // UI offers "Restart session" in place of the composer, and the tool
8390
+ // resumes the same conversation (ACP-level resume via the adapter session
8391
+ // id, fresh spawn as fallback) and returns a NEW session id the chat
8392
+ // navigates to. Kept in step with the installed app's own allowlist
8393
+ // (`@agentik/session-chat`'s APP.md `ui.tools`) — a tool missing from
8394
+ // EITHER list is refused for the surface that reads it.
8395
+ tools: [
8396
+ "session_list",
8397
+ "agent_start",
8398
+ "adapter_list",
8399
+ "conversation_read",
8400
+ "session_restart"
8401
+ ]
5608
8402
  }
5609
8403
  });
5610
8404
 
@@ -5623,12 +8417,29 @@ var WORK_BOARD_HTML_GENERATED = `<!DOCTYPE html>
5623
8417
  // JSON-RPC 2.0 over window.parent.postMessage \xB7 spec 2026-01-26
5624
8418
  var _nextId = 1, _pending = {}, _notifyHandlers = [];
5625
8419
  var _hostContext = null, _hostContextHandlers = [];
8420
+ var _hostCaps = null;
5626
8421
  var _standaloneApp = null;
5627
8422
  function _isStandalone(){
5628
8423
  return window.parent === window && !!window.McpApp && typeof window.McpApp.connect === 'function';
5629
8424
  }
8425
+ // \u2500\u2500 Per-boot embed token (runtime embed-tokens.ts) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8426
+ // Panels that iframe the daemon's standalone app host carry this proof so
8427
+ // GET /apps/:appId/ui?embed=1&et=... can drop its anti-framing headers for
8428
+ // MCP-Apps hosts whose widget context is an opaque origin (Claude Desktop).
8429
+ // registerMcpApps replaces the placeholder with a real per-boot token when
8430
+ // serving the panel as a resource; every other render (tests, standalone
8431
+ // tab, VS Code HTTP-iframe panel \u2014 the latter two already pass
8432
+ // iframeEmbedAllowed's origin checks) keeps the literal, and
8433
+ // withEmbedToken() then degrades to an identity function.
8434
+ window.__AGENPROTO_EMBED_TOKEN__ = "__AGENPROTO_EMBED_TOKEN__";
8435
+ function withEmbedToken(url){
8436
+ var t = window.__AGENPROTO_EMBED_TOKEN__;
8437
+ if (!t || t === '__AGENPROTO_EMBED_TOKEN__') return url;
8438
+ return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'et=' + encodeURIComponent(t);
8439
+ }
5630
8440
  function post(msg){ window.parent.postMessage(msg, '*'); }
5631
8441
  function getHostContext(){ return _hostContext; }
8442
+ function getHostCapabilities(){ return _hostCaps; }
5632
8443
  function onHostContext(cb){
5633
8444
  _hostContextHandlers.push(cb);
5634
8445
  // Replay the last context so a late subscriber isn't stuck blind.
@@ -5677,7 +8488,7 @@ function initBridge(){
5677
8488
  return window.McpApp.connect().then(function(conn){
5678
8489
  _standaloneApp = conn;
5679
8490
  // No host to advertise a hostContext \u2014 default to inline with no
5680
- // other modes available, which keeps the #dm/#pin toggle buttons
8491
+ // other modes available, which keeps the display-mode toggle buttons
5681
8492
  // hidden (they only show for modes hostContext.availableDisplayModes
5682
8493
  // lists) instead of rendering two buttons with nothing to switch to.
5683
8494
  _setHostContext({displayMode: 'inline', availableDisplayModes: []});
@@ -5691,12 +8502,20 @@ function initBridge(){
5691
8502
  // The initialize result carries the initial hostContext (displayMode +
5692
8503
  // availableDisplayModes) \u2014 capture it before notifying the host.
5693
8504
  if (result && result.hostContext) _setHostContext(result.hostContext);
8505
+ // McpUiInitializeResult.hostCapabilities (ext-apps spec) \u2014 e.g. openLinks,
8506
+ // "Host supports opening external URLs". Captured once, read via
8507
+ // getHostCapabilities() by panels that need to branch on it.
8508
+ if (result && result.hostCapabilities) _hostCaps = result.hostCapabilities;
5694
8509
  rpcNotify('ui/notifications/initialized', {});
5695
8510
  });
5696
8511
  }
5697
8512
  function requestDisplayMode(mode){
5698
8513
  return rpcRequest('ui/request-display-mode', {mode: mode});
5699
8514
  }
8515
+ function openLink(url){
8516
+ if (_standaloneApp && typeof _standaloneApp.openLink === 'function') return _standaloneApp.openLink(url);
8517
+ return rpcRequest('ui/open-link', {url: url});
8518
+ }
5700
8519
  function callTool(name, args){
5701
8520
  var raw = _standaloneApp
5702
8521
  ? _standaloneApp.callTool(name, args || {})
@@ -5711,71 +8530,302 @@ function callTool(name, args){
5711
8530
  });
5712
8531
  }
5713
8532
 
5714
- // \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
5715
- // Injected by the shared bridge so every panel gets them without touching
5716
- // its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
5717
- // panel stays inline by default; the user expands on demand. Buttons only
5718
- // appear for modes the host advertises in hostContext.availableDisplayModes.
5719
- (function(){
5720
- function mount(){
5721
- var style = document.createElement('style');
5722
- style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
5723
- + 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
5724
- + 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
5725
- + 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
5726
- + '#dm{right:8px}#pin{right:118px}'
5727
- + '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
5728
- + '@media (prefers-color-scheme:dark){'
5729
- + '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
5730
- + '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
8533
+ // \u2500\u2500 Display-mode toggle (shared: @agentproto/app-client/display-mode) \u2500\u2500
8534
+ // The installer is inlined verbatim (it is self-contained and idempotent),
8535
+ // then handed this panel's own JSON-RPC plumbing. NO auto-request: the
8536
+ // panel stays inline until the user clicks, and the buttons only appear for
8537
+ // the modes the host advertises in hostContext.availableDisplayModes.
8538
+ (function () {
8539
+ if (window.AgentprotoUI && window.AgentprotoUI.installDisplayMode) return;
8540
+ window.AgentprotoUI = window.AgentprotoUI || {};
8541
+
8542
+ var STYLE_ID = "agentproto-display-mode-style";
8543
+ var BAR_ID = "agentproto-display-mode-bar";
8544
+ // Gap between the host's safe area (or the viewport edge) and the button.
8545
+ var EDGE_GAP = 8;
8546
+
8547
+ // Glyph + accessible label per mode. The button is an icon; the words go
8548
+ // to title/aria-label so the control stays compact without going mute to
8549
+ // a screen reader.
8550
+ var LABELS = {
8551
+ fullscreen: {
8552
+ on: "\\u2921", off: "\\u2922",
8553
+ onLabel: "R\\u00e9duire", offLabel: "Agrandir"
8554
+ },
8555
+ pip: {
8556
+ on: "\\u2921", off: "\\uD83D\\uDCCC",
8557
+ onLabel: "D\\u00e9tacher", offLabel: "\\u00c9pingler sur le c\\u00f4t\\u00e9 (pip)"
8558
+ }
8559
+ };
8560
+
8561
+ // Every colour and both offsets resolve through an app-overridable custom
8562
+ // property first, so an app can restyle/reposition the button with a
8563
+ // stylesheet and never touch this script. The safe-area vars are what the
8564
+ // host's own chrome feeds (see applyInsets), sitting UNDER the app's.
8565
+ function ensureStyle() {
8566
+ if (!document.head || document.getElementById(STYLE_ID)) return;
8567
+ var style = document.createElement("style");
8568
+ style.id = STYLE_ID;
8569
+ style.textContent =
8570
+ "#" + BAR_ID + "{position:fixed;display:flex;gap:6px;align-items:center;z-index:10;" +
8571
+ "top:var(--agentproto-dm-top,var(--agentproto-dm-safe-top,8px));" +
8572
+ "right:var(--agentproto-dm-right,var(--agentproto-dm-safe-right,8px))}" +
8573
+ ".agentproto-dm-btn{display:inline-flex;align-items:center;justify-content:center;" +
8574
+ "box-sizing:border-box;width:28px;height:28px;padding:0;margin:0;border-radius:6px;" +
8575
+ "cursor:pointer;-webkit-appearance:none;appearance:none;" +
8576
+ "font:600 14px/1 system-ui,-apple-system,sans-serif;" +
8577
+ "border:1px solid var(--agentproto-dm-border,#d0d0d0);" +
8578
+ "background:var(--agentproto-dm-bg,#fff);" +
8579
+ "color:var(--agentproto-dm-fg,#1a1a1a);" +
8580
+ "box-shadow:0 1px 4px rgba(0,0,0,.18)}" +
8581
+ ".agentproto-dm-btn:hover{filter:brightness(.95)}" +
8582
+ ".agentproto-dm-btn:focus-visible{outline:2px solid var(--agentproto-dm-fg,#1a1a1a);outline-offset:2px}" +
8583
+ ".agentproto-dm-btn[data-theme=\\"dark\\"]{border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}" +
8584
+ "@media (prefers-color-scheme:dark){" +
8585
+ ".agentproto-dm-btn:not([data-theme=\\"light\\"]){border-color:var(--agentproto-dm-border,#555);background:var(--agentproto-dm-bg,#2a2a2a);color:var(--agentproto-dm-fg,#f0f0f0);box-shadow:0 1px 4px rgba(0,0,0,.5)}}";
5731
8586
  document.head.appendChild(style);
8587
+ }
8588
+
8589
+ function ready(fn) {
8590
+ if (document.body) fn();
8591
+ else document.addEventListener("DOMContentLoaded", fn);
8592
+ }
8593
+
8594
+ function metaToggle() {
8595
+ var el = document.querySelector('meta[name="agentproto-display-toggle"]');
8596
+ if (!el) return "";
8597
+ return (el.getAttribute("content") || "").toLowerCase().replace(/^\\s+|\\s+$/g, "");
8598
+ }
8599
+
8600
+ function ensureBar() {
8601
+ var bar = document.getElementById(BAR_ID);
8602
+ if (bar) return bar;
8603
+ bar = document.createElement("div");
8604
+ bar.id = BAR_ID;
8605
+ document.body.appendChild(bar);
8606
+ return bar;
8607
+ }
5732
8608
 
5733
- var btn = document.createElement('button');
5734
- btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
5735
- var pin = document.createElement('button');
5736
- pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
5737
- document.body.appendChild(pin);
5738
- document.body.appendChild(btn);
5739
-
5740
- function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
5741
-
5742
- // Re-sync button visibility + label from the current host context.
5743
- function syncBtn(ctx){
5744
- ctx = ctx || {};
5745
- var avail = ctx.availableDisplayModes;
5746
- // Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
5747
- console.log('[mcp-app] displayMode=', ctx.displayMode,
5748
- 'availableDisplayModes=', avail);
5749
-
5750
- // Fullscreen toggle button.
5751
- if (has(avail, 'fullscreen')){
5752
- btn.style.display = 'block';
5753
- btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
5754
- } else { btn.style.display = 'none'; }
5755
-
5756
- // Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
5757
- if (has(avail, 'pip')){
5758
- pin.style.display = 'block';
5759
- pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
5760
- } else { pin.style.display = 'none'; }
8609
+ function px(v) {
8610
+ return typeof v === "number" && isFinite(v) && v > 0 ? v : 0;
8611
+ }
8612
+
8613
+ window.AgentprotoUI.installDisplayMode = function (api, opts) {
8614
+ api = api || {};
8615
+ opts = opts || {};
8616
+
8617
+ var ctx = {};
8618
+ var listeners = [];
8619
+ var buttons = [];
8620
+ // Modes the host has refused once. Checked before availability, so a
8621
+ // later host-context-changed can never resurrect a dead button.
8622
+ var refused = {};
8623
+ var loggedKeys = false;
8624
+
8625
+ var setting = opts.toggle || metaToggle() || "auto";
8626
+ var optimistic = setting === "optimistic";
8627
+ var autoMount = setting !== "none";
8628
+
8629
+ function get() {
8630
+ return ctx.displayMode || "inline";
5761
8631
  }
5762
8632
 
5763
- onHostContext(syncBtn);
8633
+ function available() {
8634
+ var a = ctx.availableDisplayModes;
8635
+ return a && a.slice ? a.slice() : [];
8636
+ }
5764
8637
 
5765
- btn.addEventListener('click', function(){
5766
- var ctx = getHostContext() || {};
5767
- var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
5768
- requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
5769
- });
8638
+ function supports(mode) {
8639
+ if (refused[mode]) return false;
8640
+ var a = ctx.availableDisplayModes;
8641
+ if (a && a.indexOf && a.indexOf(mode) >= 0) return true;
8642
+ // Optimistic: the host advertises nothing (Claude Desktop, some
8643
+ // embedders) but may still honour the request. Fullscreen only \u2014 pip
8644
+ // is niche enough that guessing at it is just noise.
8645
+ return optimistic && mode === "fullscreen";
8646
+ }
8647
+
8648
+ // Host chrome to stay clear of. Published as custom properties rather
8649
+ // than inline styles so an app's own --agentproto-dm-top/right still
8650
+ // wins (an inline style would beat any stylesheet it could write).
8651
+ function applyInsets() {
8652
+ var insets = ctx.safeAreaInsets || {};
8653
+ var root = document.documentElement;
8654
+ if (!root || !root.style || !root.style.setProperty) return;
8655
+ root.style.setProperty("--agentproto-dm-safe-top", (px(insets.top) + EDGE_GAP) + "px");
8656
+ root.style.setProperty("--agentproto-dm-safe-right", (px(insets.right) + EDGE_GAP) + "px");
8657
+ }
8658
+
8659
+ function syncButton(btn) {
8660
+ var mode = btn.getAttribute("data-agentproto-dm") || "fullscreen";
8661
+ var labels = LABELS[mode] || LABELS.fullscreen;
8662
+ var active = get() === mode;
8663
+ btn.style.display = supports(mode) ? "" : "none";
8664
+ btn.textContent = active ? labels.on : labels.off;
8665
+ var label = active ? labels.onLabel : labels.offLabel;
8666
+ btn.title = label;
8667
+ btn.setAttribute("aria-label", label);
8668
+ btn.setAttribute("aria-pressed", active ? "true" : "false");
8669
+ if (ctx.theme === "dark" || ctx.theme === "light") btn.setAttribute("data-theme", ctx.theme);
8670
+ else btn.removeAttribute("data-theme");
8671
+ }
8672
+
8673
+ function sync() {
8674
+ applyInsets();
8675
+ for (var i = buttons.length - 1; i >= 0; i--) {
8676
+ // An app that removed its inline toggle stops being our problem.
8677
+ if (buttons[i].isConnected === false) { buttons.splice(i, 1); continue; }
8678
+ syncButton(buttons[i]);
8679
+ }
8680
+ }
8681
+
8682
+ function emit() {
8683
+ var mode = get();
8684
+ for (var i = 0; i < listeners.length; i++) {
8685
+ try { listeners[i](mode, ctx); } catch (_) {}
8686
+ }
8687
+ }
8688
+
8689
+ // host-context-changed carries only the changed keys \u2014 merge, matching
8690
+ // the official ext-apps App behaviour.
8691
+ function setContext(next) {
8692
+ if (!next || typeof next !== "object") return;
8693
+ for (var k in next) {
8694
+ if (Object.prototype.hasOwnProperty.call(next, k)) ctx[k] = next[k];
8695
+ }
8696
+ sync();
8697
+ emit();
8698
+ }
8699
+
8700
+ function retire(mode) {
8701
+ refused[mode] = true;
8702
+ sync();
8703
+ }
8704
+
8705
+ function request(mode) {
8706
+ var pending;
8707
+ try {
8708
+ pending = api.requestDisplayMode
8709
+ ? api.requestDisplayMode(mode)
8710
+ : Promise.reject(new Error("display mode: no host"));
8711
+ } catch (err) {
8712
+ pending = Promise.reject(err);
8713
+ }
8714
+ return Promise.resolve(pending).then(function (result) {
8715
+ // McpUiRequestDisplayModeResult.mode is the mode actually set, and
8716
+ // the spec lets it differ from the one asked for \u2014 so a host that
8717
+ // answers "still inline" is a refusal, not a success.
8718
+ var settled = (result && result.mode) || mode;
8719
+ if (optimistic && mode !== "inline" && settled !== mode) {
8720
+ retire(mode);
8721
+ throw new Error("host refused display mode: " + mode);
8722
+ }
8723
+ // Hosts are not required to follow up with host-context-changed \u2014
8724
+ // trust the result so the glyph flips either way.
8725
+ setContext({ displayMode: settled });
8726
+ return settled;
8727
+ }, function (err) {
8728
+ if (optimistic && mode !== "inline") retire(mode);
8729
+ throw err;
8730
+ });
8731
+ }
8732
+
8733
+ function makeButton(mode) {
8734
+ var btn = document.createElement("button");
8735
+ btn.type = "button";
8736
+ btn.className = "agentproto-dm-btn";
8737
+ // Stable id for the floating pair (styling hooks, tests). An app that
8738
+ // mounts extra inline toggles gets unnamed ones rather than duplicates.
8739
+ var id = mode === "fullscreen"
8740
+ ? "agentproto-display-mode"
8741
+ : "agentproto-display-mode-" + mode;
8742
+ if (!document.getElementById(id)) btn.id = id;
8743
+ btn.setAttribute("data-agentproto-dm", mode);
8744
+ btn.style.display = "none";
8745
+ btn.addEventListener("click", function () {
8746
+ var cur = get();
8747
+ // Same rule both copies had: anything that isn't inline collapses
8748
+ // back to inline, so the button is always an escape hatch.
8749
+ var inPanel = cur === "fullscreen" || cur === "pip";
8750
+ request(inPanel ? "inline" : mode)["catch"](function () {});
8751
+ });
8752
+ return btn;
8753
+ }
5770
8754
 
5771
- pin.addEventListener('click', function(){
5772
- var ctx = getHostContext() || {};
5773
- requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
8755
+ function mountToggle(el, mode) {
8756
+ ensureStyle();
8757
+ mode = mode || "fullscreen";
8758
+ var btn = makeButton(mode);
8759
+ if (el) el.appendChild(btn);
8760
+ else ensureBar().appendChild(btn);
8761
+ buttons.push(btn);
8762
+ syncButton(btn);
8763
+ return btn;
8764
+ }
8765
+
8766
+ function onChange(cb) {
8767
+ if (typeof cb !== "function") return function () {};
8768
+ listeners.push(cb);
8769
+ try { cb(get(), ctx); } catch (_) {}
8770
+ return function () {
8771
+ var i = listeners.indexOf(cb);
8772
+ if (i >= 0) listeners.splice(i, 1);
8773
+ };
8774
+ }
8775
+
8776
+ ready(function () {
8777
+ ensureStyle();
8778
+ // Only the first installer in a document owns the floating bar: a
8779
+ // built-in panel served standalone gets the injected window.McpApp
8780
+ // bridge's copy AND its own, and two stacked buttons is a bug.
8781
+ if (autoMount && !document.getElementById(BAR_ID)) {
8782
+ ensureBar();
8783
+ mountToggle(null, "pip");
8784
+ mountToggle(null, "fullscreen");
8785
+ }
8786
+ sync();
5774
8787
  });
5775
- }
5776
- if (document.body) mount();
5777
- else document.addEventListener('DOMContentLoaded', mount);
5778
- })();</script>
8788
+
8789
+ // The diagnostic both copies had, now on BOTH paths: it used to exist
8790
+ // only in the built-in panel bridge, so an installed app debugged in
8791
+ // Claude Desktop (which advertises no fullscreen, hence no button)
8792
+ // printed nothing at all and left the user with no way to tell "the
8793
+ // host refuses" from "the toggle is broken".
8794
+ function observe(next) {
8795
+ console.log('[mcp-app] displayMode=', next && next.displayMode,
8796
+ 'availableDisplayModes=', next && next.availableDisplayModes);
8797
+ setContext(next);
8798
+ // What ELSE does this host send? Keys only, once \u2014 a host context can
8799
+ // carry toolInfo (the instantiating tool call's arguments) and other
8800
+ // host-private fields, so the names are the diagnostic and the values
8801
+ // stay out of the console.
8802
+ if (!loggedKeys && next && typeof next === "object") {
8803
+ loggedKeys = true;
8804
+ var keys = [];
8805
+ for (var k in ctx) {
8806
+ if (Object.prototype.hasOwnProperty.call(ctx, k)) keys.push(k);
8807
+ }
8808
+ console.log('[mcp-app] hostContext keys=', keys.sort().join(','));
8809
+ }
8810
+ }
8811
+
8812
+ if (typeof api.onHostContext === "function") api.onHostContext(observe);
8813
+ else if (typeof api.getHostContext === "function") observe(api.getHostContext());
8814
+
8815
+ return {
8816
+ get: get,
8817
+ available: available,
8818
+ request: request,
8819
+ onChange: onChange,
8820
+ mountToggle: mountToggle
8821
+ };
8822
+ };
8823
+ })();
8824
+ var displayMode = window.AgentprotoUI.installDisplayMode({
8825
+ getHostContext: getHostContext,
8826
+ onHostContext: onHostContext,
8827
+ requestDisplayMode: requestDisplayMode
8828
+ });</script>
5779
8829
  <script>(function() {
5780
8830
  "use strict";
5781
8831
  const COLUMN_STATUSES = ["pending", "in_progress", "done", "failed"];
@@ -6265,6 +9315,6 @@ var workBoardApp = defineApp({
6265
9315
  }
6266
9316
  });
6267
9317
 
6268
- export { SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, agentsOverviewApp, bureauSessionsApp, codeTeam, contentTeam, liveSessionApp, mailTriage, makeAgentsOverviewApp, makeBureauSessionsApp, makeLiveSessionApp, makeSessionChatApp, makeSessionStoryPanelApp, makeSessionsPanelApp, makeWorkBoardApp, mediaViewer, opsPanel, panelBridgeScript, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionStoryApp, sessionViewer, sessionsPanelApp, workBoardApp };
9318
+ export { SESSION_CHAT_APP_ID, SESSION_CHAT_FALLBACK_HTML, agentsOverviewApp, bureauSessionsApp, codeTeam, configApp, contentTeam, liveSessionApp, mailTriage, makeAgentsOverviewApp, makeBureauSessionsApp, makeLiveSessionApp, makeSessionChatApp, makeSessionStoryPanelApp, makeSessionsPanelApp, makeWorkBoardApp, mediaViewer, opsPanel, panelBridgeScript, sessionChatApp, sessionChatAppUrl, sessionChatEmbedHtml, sessionStoryApp, sessionViewer, sessionsPanelApp, workBoardApp };
6269
9319
  //# sourceMappingURL=index.mjs.map
6270
9320
  //# sourceMappingURL=index.mjs.map