@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.
- package/dist/agents-overview/panel.mjs +39 -67
- package/dist/agents-overview/panel.mjs.map +1 -1
- package/dist/agents-overview.mjs +38 -68
- package/dist/agents-overview.mjs.map +1 -1
- package/dist/bin/sync.d.ts.map +1 -1
- package/dist/bin/sync.mjs +2374 -2
- package/dist/bin/sync.mjs.map +1 -1
- package/dist/bureau-sessions/panel.mjs +39 -67
- package/dist/bureau-sessions/panel.mjs.map +1 -1
- package/dist/bureau-sessions.mjs +38 -68
- package/dist/bureau-sessions.mjs.map +1 -1
- package/dist/config/fragment.d.ts +50 -0
- package/dist/config/fragment.d.ts.map +1 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/ui.d.ts +43 -0
- package/dist/config/ui.d.ts.map +1 -0
- package/dist/config.mjs +2373 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3200 -150
- package/dist/index.mjs.map +1 -1
- package/dist/live-session/logic.d.ts +2 -0
- package/dist/live-session/logic.d.ts.map +1 -1
- package/dist/live-session/panel.d.ts.map +1 -1
- package/dist/live-session/panel.mjs +52 -74
- package/dist/live-session/panel.mjs.map +1 -1
- package/dist/live-session.mjs +51 -75
- package/dist/live-session.mjs.map +1 -1
- package/dist/panel-bridge.d.ts +11 -6
- package/dist/panel-bridge.d.ts.map +1 -1
- package/dist/session-chat/blob-embed.d.ts +53 -0
- package/dist/session-chat/blob-embed.d.ts.map +1 -0
- package/dist/session-chat/index.d.ts +13 -0
- package/dist/session-chat/index.d.ts.map +1 -1
- package/dist/session-chat/panel.d.ts +38 -2
- package/dist/session-chat/panel.d.ts.map +1 -1
- package/dist/session-chat/panel.mjs +556 -6
- package/dist/session-chat/panel.mjs.map +1 -1
- package/dist/session-chat.mjs +591 -14
- package/dist/session-chat.mjs.map +1 -1
- package/dist/session-story/panel.mjs +39 -67
- package/dist/session-story/panel.mjs.map +1 -1
- package/dist/session-story.mjs +38 -68
- package/dist/session-story.mjs.map +1 -1
- package/dist/session-viewer/ui.d.ts.map +1 -1
- package/dist/session-viewer.mjs +10 -0
- package/dist/session-viewer.mjs.map +1 -1
- package/dist/sessions-panel/panel.d.ts.map +1 -1
- package/dist/sessions-panel/panel.mjs +40 -67
- package/dist/sessions-panel/panel.mjs.map +1 -1
- package/dist/sessions-panel.mjs +39 -68
- package/dist/sessions-panel.mjs.map +1 -1
- package/dist/work-board/panel.d.ts +1 -1
- package/dist/work-board/panel.d.ts.map +1 -1
- package/dist/work-board/panel.generated.d.ts +1 -1
- package/dist/work-board/panel.generated.d.ts.map +1 -1
- package/dist/work-board/panel.mjs +318 -62
- package/dist/work-board/panel.mjs.map +1 -1
- package/dist/work-board.mjs +318 -62
- package/dist/work-board.mjs.map +1 -1
- package/package.json +12 -4
- package/repo-maintenance/.agentproto/APP.md +22 -0
- package/repo-maintenance/.agentproto/agents/repo-maintenance-reviewer/AGENT.md +142 -0
- package/repo-maintenance/.agentproto/workflows/maintain/WORKFLOW.md +262 -0
- package/repo-maintenance/.agentproto/workflows/maintain/entry.mjs +672 -0
- package/repo-maintenance/README.md +79 -0
- package/repo-maintenance/routines/repo-maintenance-daily/ROUTINE.md +77 -0
- package/repo-maintenance/routines/repo-maintenance-weekly/ROUTINE.md +82 -0
package/dist/bin/sync.mjs
CHANGED
|
@@ -2359,6 +2359,2367 @@ var opsPanel = defineApp({
|
|
|
2359
2359
|
tools: [...OPS_PANEL_TOOLS]
|
|
2360
2360
|
}
|
|
2361
2361
|
});
|
|
2362
|
+
|
|
2363
|
+
// src/config/fragment.ts
|
|
2364
|
+
function parseConfigFragment(hash) {
|
|
2365
|
+
var SECTIONS2 = ["wallets", "harnesses", "models", "defaults", "remote", "advanced"];
|
|
2366
|
+
var raw = String(hash || "").replace(/^#/, "");
|
|
2367
|
+
var parts = raw.split("/").filter(function(p) {
|
|
2368
|
+
return p.length > 0;
|
|
2369
|
+
});
|
|
2370
|
+
var sectionRaw = parts[0] || "";
|
|
2371
|
+
var section = SECTIONS2.indexOf(sectionRaw) !== -1 ? sectionRaw : SECTIONS2[0];
|
|
2372
|
+
var result = { section };
|
|
2373
|
+
var idPart = parts[1];
|
|
2374
|
+
if (idPart !== void 0) {
|
|
2375
|
+
try {
|
|
2376
|
+
result.id = decodeURIComponent(idPart);
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
result.id = idPart;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
var subPart = parts[2];
|
|
2382
|
+
if (subPart !== void 0) {
|
|
2383
|
+
try {
|
|
2384
|
+
result.sub = decodeURIComponent(subPart);
|
|
2385
|
+
} catch (e) {
|
|
2386
|
+
result.sub = subPart;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
return result;
|
|
2390
|
+
}
|
|
2391
|
+
function buildConfigFragment(section, id, sub) {
|
|
2392
|
+
var out = "#" + section;
|
|
2393
|
+
if (id !== void 0 && id !== null && id !== "") out += "/" + encodeURIComponent(id);
|
|
2394
|
+
if (sub !== void 0 && sub !== null && sub !== "") out += "/" + encodeURIComponent(sub);
|
|
2395
|
+
return out;
|
|
2396
|
+
}
|
|
2397
|
+
function shouldApplyIncomingView(arrivalIndex, hashPresentAtLoad) {
|
|
2398
|
+
return !(arrivalIndex === 1 && hashPresentAtLoad);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/config/ui.ts
|
|
2402
|
+
var CONFIG_TOOLS = [
|
|
2403
|
+
"auth_profile_list",
|
|
2404
|
+
"auth_profile_create",
|
|
2405
|
+
"auth_profile_delete",
|
|
2406
|
+
"auth_profile_set_enabled",
|
|
2407
|
+
"auth_profile_set_models",
|
|
2408
|
+
"auth_profile_refresh_models",
|
|
2409
|
+
"auth_profile_import",
|
|
2410
|
+
"auth_discover_credentials",
|
|
2411
|
+
"auth_profile_update",
|
|
2412
|
+
"adapter_list",
|
|
2413
|
+
"harness_capabilities",
|
|
2414
|
+
"harness_preset_list",
|
|
2415
|
+
"harness_preset_create",
|
|
2416
|
+
"harness_preset_delete",
|
|
2417
|
+
"harness_preset_set_default",
|
|
2418
|
+
"catalog_models",
|
|
2419
|
+
"catalog_provider_models",
|
|
2420
|
+
"role_list",
|
|
2421
|
+
"usage_rollup",
|
|
2422
|
+
"config_get",
|
|
2423
|
+
"config_set",
|
|
2424
|
+
"daemon_health",
|
|
2425
|
+
"remote_status",
|
|
2426
|
+
"remote_enable",
|
|
2427
|
+
"remote_disable",
|
|
2428
|
+
"pair_list",
|
|
2429
|
+
"pair_offer",
|
|
2430
|
+
"pair_revoke",
|
|
2431
|
+
"tunnel_list",
|
|
2432
|
+
"tunnel_status"
|
|
2433
|
+
];
|
|
2434
|
+
var APP_ID4 = "@agentproto/config";
|
|
2435
|
+
var SECTIONS = [
|
|
2436
|
+
{ id: "wallets", label: "Wallets" },
|
|
2437
|
+
{ id: "harnesses", label: "Harnesses" },
|
|
2438
|
+
{ id: "models", label: "Models" },
|
|
2439
|
+
{ id: "defaults", label: "Defaults" },
|
|
2440
|
+
{ id: "remote", label: "Remote" },
|
|
2441
|
+
{ id: "advanced", label: "Advanced" }
|
|
2442
|
+
];
|
|
2443
|
+
var CONFIG_HTML = `<!DOCTYPE html>
|
|
2444
|
+
<html lang="en">
|
|
2445
|
+
<head>
|
|
2446
|
+
<meta charset="UTF-8">
|
|
2447
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2448
|
+
<title>agentproto config</title>
|
|
2449
|
+
<style>
|
|
2450
|
+
/* Theme tokens: prefer a live --vscode-* var (VS Code webview host), else
|
|
2451
|
+
the brand-neutral standalone default, split light/dark by
|
|
2452
|
+
prefers-color-scheme \u2014 same mapping convention as work-board/ui/style.css.
|
|
2453
|
+
No JS theme detection: a host that swaps its theme live is never frozen. */
|
|
2454
|
+
:root{
|
|
2455
|
+
--cfg-bg: var(--vscode-editor-background, #f5f6f3);
|
|
2456
|
+
--cfg-surface: var(--vscode-sideBar-background, #ffffff);
|
|
2457
|
+
--cfg-surface-2: var(--vscode-editorWidget-background, #eef0ec);
|
|
2458
|
+
--cfg-ink: var(--vscode-editor-foreground, #171a1e);
|
|
2459
|
+
--cfg-ink-faint: var(--vscode-descriptionForeground, #6b7280);
|
|
2460
|
+
--cfg-border: var(--vscode-panel-border, #dde0da);
|
|
2461
|
+
--cfg-accent: var(--vscode-button-background, #0f62fe);
|
|
2462
|
+
--cfg-warning: var(--vscode-charts-yellow, #b45309);
|
|
2463
|
+
--cfg-danger: var(--vscode-charts-red, #b91c1c);
|
|
2464
|
+
--cfg-success: var(--vscode-charts-green, #15803d);
|
|
2465
|
+
--cfg-muted-bg: var(--vscode-input-background, #e5e7eb);
|
|
2466
|
+
}
|
|
2467
|
+
@media (prefers-color-scheme: dark){
|
|
2468
|
+
:root{
|
|
2469
|
+
--cfg-bg: var(--vscode-editor-background, #0f1115);
|
|
2470
|
+
--cfg-surface: var(--vscode-sideBar-background, #161a20);
|
|
2471
|
+
--cfg-surface-2: var(--vscode-editorWidget-background, #1c2129);
|
|
2472
|
+
--cfg-ink: var(--vscode-editor-foreground, #e6e9ee);
|
|
2473
|
+
--cfg-ink-faint: var(--vscode-descriptionForeground, #8b93a1);
|
|
2474
|
+
--cfg-border: var(--vscode-panel-border, #2a2f38);
|
|
2475
|
+
--cfg-accent: var(--vscode-button-background, #4589ff);
|
|
2476
|
+
--cfg-warning: var(--vscode-charts-yellow, #d29922);
|
|
2477
|
+
--cfg-danger: var(--vscode-charts-red, #f85149);
|
|
2478
|
+
--cfg-success: var(--vscode-charts-green, #3fb950);
|
|
2479
|
+
--cfg-muted-bg: var(--vscode-input-background, #262b33);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2483
|
+
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--cfg-bg);color:var(--cfg-ink)}
|
|
2484
|
+
#app{display:flex;flex-direction:column;height:100%;overflow:hidden}
|
|
2485
|
+
|
|
2486
|
+
#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}
|
|
2487
|
+
#topbar h1{font-size:13px;font-weight:700;letter-spacing:.02em}
|
|
2488
|
+
.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}
|
|
2489
|
+
.chip .d{width:7px;height:7px;border-radius:50%;background:var(--cfg-ink-faint);flex:none}
|
|
2490
|
+
.chip.ok .d{background:var(--cfg-success)}
|
|
2491
|
+
.chip.bad .d{background:var(--cfg-danger)}
|
|
2492
|
+
#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}
|
|
2493
|
+
#refresh-btn:hover{color:var(--cfg-ink);background:var(--cfg-surface-2)}
|
|
2494
|
+
|
|
2495
|
+
#nav{display:flex;gap:4px;padding:8px 14px 0;background:var(--cfg-surface);border-bottom:1px solid var(--cfg-border);flex-shrink:0}
|
|
2496
|
+
#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}
|
|
2497
|
+
#nav button:hover{color:var(--cfg-ink)}
|
|
2498
|
+
#nav button.active{color:var(--cfg-ink);border-bottom-color:var(--cfg-accent)}
|
|
2499
|
+
|
|
2500
|
+
#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}
|
|
2501
|
+
|
|
2502
|
+
#body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px}
|
|
2503
|
+
.section{display:none;flex-direction:column;gap:12px}
|
|
2504
|
+
.section.active{display:flex}
|
|
2505
|
+
|
|
2506
|
+
.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}
|
|
2507
|
+
.muted{color:var(--cfg-ink-faint)}
|
|
2508
|
+
.muted-note{color:var(--cfg-ink-faint);font-size:11px;padding:6px 10px}
|
|
2509
|
+
.empty{color:var(--cfg-ink-faint);font-size:11.5px;padding:8px}
|
|
2510
|
+
|
|
2511
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px}
|
|
2512
|
+
.card{background:var(--cfg-surface);border:1px solid var(--cfg-border);border-radius:8px;overflow:hidden;scroll-margin-top:12px}
|
|
2513
|
+
.card.hl{outline:2px solid var(--cfg-accent);outline-offset:-1px}
|
|
2514
|
+
.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)}
|
|
2515
|
+
.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%}
|
|
2516
|
+
.card .body{padding:8px 10px}
|
|
2517
|
+
.card dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px;align-items:baseline}
|
|
2518
|
+
.card dt{color:var(--cfg-ink-faint);white-space:nowrap;font-size:11.5px}
|
|
2519
|
+
.card dd{text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:11.5px}
|
|
2520
|
+
.bar{grid-column:1/3;height:4px;background:var(--cfg-muted-bg);border-radius:2px;overflow:hidden;margin:2px 0 4px}
|
|
2521
|
+
.bar>span{display:block;height:100%;background:var(--cfg-accent)}
|
|
2522
|
+
.bar.warn>span{background:var(--cfg-warning)}
|
|
2523
|
+
|
|
2524
|
+
.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}
|
|
2525
|
+
.pillbar{display:flex;gap:4px}
|
|
2526
|
+
.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}
|
|
2527
|
+
.pillbar button.active{color:var(--cfg-ink);border-color:var(--cfg-accent)}
|
|
2528
|
+
label.inline{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--cfg-ink-faint)}
|
|
2529
|
+
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}
|
|
2530
|
+
input:disabled,select:disabled,textarea:disabled{opacity:.5;cursor:not-allowed}
|
|
2531
|
+
|
|
2532
|
+
.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)}
|
|
2533
|
+
.tag.ok{color:var(--cfg-success);border-color:var(--cfg-success)}
|
|
2534
|
+
.tag.bad{color:var(--cfg-danger);border-color:var(--cfg-danger)}
|
|
2535
|
+
.tag.warn-tag{color:var(--cfg-warning);border-color:var(--cfg-warning)}
|
|
2536
|
+
a.link{color:var(--cfg-accent);text-decoration:none;cursor:pointer}
|
|
2537
|
+
a.link:hover{text-decoration:underline}
|
|
2538
|
+
|
|
2539
|
+
table{width:100%;border-collapse:collapse}
|
|
2540
|
+
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)}
|
|
2541
|
+
td{font-size:11.5px;padding:5px 8px;border-bottom:1px solid var(--cfg-surface-2);vertical-align:middle}
|
|
2542
|
+
tr.hl td{background:var(--cfg-surface-2)}
|
|
2543
|
+
td.matrix-cell{text-align:center;cursor:pointer}
|
|
2544
|
+
td.matrix-cell.disabled{opacity:.35;cursor:default}
|
|
2545
|
+
|
|
2546
|
+
.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}
|
|
2547
|
+
.btn-sm:hover{border-color:var(--cfg-accent)}
|
|
2548
|
+
.btn-sm:disabled{opacity:.5;cursor:not-allowed}
|
|
2549
|
+
.btn-sm.danger{color:var(--cfg-danger);border-color:var(--cfg-danger)}
|
|
2550
|
+
.btn-sm.confirm{background:var(--cfg-danger);color:#fff;border-color:var(--cfg-danger)}
|
|
2551
|
+
.btn-sm.primary{color:var(--cfg-accent);border-color:var(--cfg-accent)}
|
|
2552
|
+
|
|
2553
|
+
.cfg-row{display:contents}
|
|
2554
|
+
.cfg-help{font-size:10.5px}
|
|
2555
|
+
.cfg-err{color:var(--cfg-danger);font-size:10.5px}
|
|
2556
|
+
|
|
2557
|
+
.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}
|
|
2558
|
+
.form-panel .field{display:flex;flex-direction:column;gap:2px;font-size:11px}
|
|
2559
|
+
.form-panel .field label{color:var(--cfg-ink-faint);font-size:10.5px}
|
|
2560
|
+
.form-row{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
|
|
2561
|
+
|
|
2562
|
+
.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}
|
|
2563
|
+
.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}
|
|
2564
|
+
|
|
2565
|
+
.mm-list{max-height:160px;overflow-y:auto;margin-top:4px;padding:4px;background:var(--cfg-muted-bg);border-radius:5px}
|
|
2566
|
+
.wallet-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
|
|
2567
|
+
|
|
2568
|
+
#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}
|
|
2569
|
+
#toast.err{border-color:var(--cfg-danger);color:var(--cfg-danger)}
|
|
2570
|
+
</style>
|
|
2571
|
+
</head>
|
|
2572
|
+
<body>
|
|
2573
|
+
<div id="app">
|
|
2574
|
+
<div id="topbar">
|
|
2575
|
+
<h1>agentproto config</h1>
|
|
2576
|
+
<span class="chip" id="daemon-chip"><span class="d"></span><span id="daemon-txt">daemon…</span></span>
|
|
2577
|
+
<button id="refresh-btn" title="Refresh" type="button">↻ refresh</button>
|
|
2578
|
+
</div>
|
|
2579
|
+
<div id="nav"></div>
|
|
2580
|
+
<div class="notice" id="pending-banner" style="display:none"></div>
|
|
2581
|
+
<div id="body">
|
|
2582
|
+
<div class="notice" id="notfound" style="display:none"></div>
|
|
2583
|
+
<div class="section" data-section="wallets" id="sec-wallets"></div>
|
|
2584
|
+
<div class="section" data-section="harnesses" id="sec-harnesses"></div>
|
|
2585
|
+
<div class="section" data-section="models" id="sec-models"></div>
|
|
2586
|
+
<div class="section" data-section="defaults" id="sec-defaults"></div>
|
|
2587
|
+
<div class="section" data-section="remote" id="sec-remote"></div>
|
|
2588
|
+
<div class="section" data-section="advanced" id="sec-advanced"></div>
|
|
2589
|
+
</div>
|
|
2590
|
+
</div>
|
|
2591
|
+
<div id="toast"></div>
|
|
2592
|
+
<script>
|
|
2593
|
+
var callTool = null;
|
|
2594
|
+
var updateModelContext = null;
|
|
2595
|
+
var onTeardownFn = null;
|
|
2596
|
+
var APP_ID = ${JSON.stringify(APP_ID4)};
|
|
2597
|
+
var SECTIONS = ${JSON.stringify(SECTIONS)};
|
|
2598
|
+
|
|
2599
|
+
// \u2500\u2500 deep-link fragment parser (fragment.ts, embedded verbatim so the
|
|
2600
|
+
// browser runs the exact function fragment.test.ts exercises) \u2500\u2500
|
|
2601
|
+
${parseConfigFragment.toString()}
|
|
2602
|
+
${buildConfigFragment.toString()}
|
|
2603
|
+
${shouldApplyIncomingView.toString()}
|
|
2604
|
+
|
|
2605
|
+
// app_tool_call wraps its dispatch result at least once (its own text-result
|
|
2606
|
+
// body is the JSON-stringified inner MCP tool result); peel back through
|
|
2607
|
+
// nested {content:[{type:"text",text:...}]} shells until the payload stops
|
|
2608
|
+
// looking like one \u2014 same convention as ops-panel/session-viewer.
|
|
2609
|
+
function unwrapText(result) {
|
|
2610
|
+
var cur = result;
|
|
2611
|
+
for (var i = 0; i < 4; i++) {
|
|
2612
|
+
if (typeof cur === "string") {
|
|
2613
|
+
try { cur = JSON.parse(cur); continue; } catch (e) { return cur; }
|
|
2614
|
+
}
|
|
2615
|
+
if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
|
|
2616
|
+
cur = cur.content[0].text;
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
break;
|
|
2620
|
+
}
|
|
2621
|
+
return cur;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
function callApp(tool, args) {
|
|
2625
|
+
return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// Never throws: every card loads independently, so one failing tool must
|
|
2629
|
+
// never crash another card's render. An "unknown daemon tool" failure (a
|
|
2630
|
+
// write tool, or config_get/config_set, on a daemon that predates them) is
|
|
2631
|
+
// distinguished from any other failure so the caller can render a muted
|
|
2632
|
+
// feature-detect note instead of an error. Used for BOTH reads and writes:
|
|
2633
|
+
// every write tool here answers either a JSON object (success) or a plain
|
|
2634
|
+
// string (its own error text), the same two shapes a read tool answers.
|
|
2635
|
+
function loadTool(tool, args) {
|
|
2636
|
+
return callApp(tool, args).then(function (d) {
|
|
2637
|
+
if (typeof d === "string") {
|
|
2638
|
+
return { ok: false, message: d, unknown: /unknown daemon tool/.test(d) };
|
|
2639
|
+
}
|
|
2640
|
+
if (d && typeof d === "object" && typeof d.error === "string") {
|
|
2641
|
+
return { ok: false, message: d.error, unknown: /unknown daemon tool/.test(d.error) };
|
|
2642
|
+
}
|
|
2643
|
+
return { ok: true, data: d };
|
|
2644
|
+
}).catch(function (e) {
|
|
2645
|
+
var msg = (e && e.message) || String(e);
|
|
2646
|
+
return { ok: false, message: msg, unknown: false };
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
function escHtml(s) {
|
|
2651
|
+
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
function humanize(s) { return String(s || "").replace(/[-_]+/g, " "); }
|
|
2655
|
+
|
|
2656
|
+
function formatTokens(n) {
|
|
2657
|
+
if (n == null || !isFinite(n)) return "";
|
|
2658
|
+
var trim = function (t) { return t.replace(/\\.0$/, ""); };
|
|
2659
|
+
if (n >= 1000000) return trim((n / 1000000).toFixed(1)) + "M";
|
|
2660
|
+
if (n >= 1000) return trim((n / 1000).toFixed(1)) + "k";
|
|
2661
|
+
return String(n);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
function formatCost(usd) {
|
|
2665
|
+
if (usd == null || !isFinite(usd)) return "";
|
|
2666
|
+
if (usd > 0 && usd < 0.01) return "<$0.01";
|
|
2667
|
+
return "$" + usd.toFixed(2);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function formatDate(iso) {
|
|
2671
|
+
if (!iso) return "";
|
|
2672
|
+
var d = new Date(iso);
|
|
2673
|
+
if (isNaN(d.getTime())) return "";
|
|
2674
|
+
return d.toLocaleString();
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function formatMs(ms) {
|
|
2678
|
+
if (ms == null || !isFinite(ms)) return "";
|
|
2679
|
+
if (ms >= 60000) return Math.round(ms / 60000) + "m";
|
|
2680
|
+
if (ms >= 1000) return Math.round(ms / 1000) + "s";
|
|
2681
|
+
return ms + "ms";
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function fmtRelative(iso) {
|
|
2685
|
+
if (!iso) return "";
|
|
2686
|
+
var ms = Date.now() - new Date(iso).getTime();
|
|
2687
|
+
if (!isFinite(ms)) return "";
|
|
2688
|
+
var s = Math.round(ms / 1000);
|
|
2689
|
+
if (s < 5) return "just now";
|
|
2690
|
+
if (s < 60) return s + "s ago";
|
|
2691
|
+
var m = Math.round(s / 60);
|
|
2692
|
+
if (m < 60) return m + "m ago";
|
|
2693
|
+
var h = Math.round(m / 60);
|
|
2694
|
+
if (h < 24) return h + "h ago";
|
|
2695
|
+
return Math.round(h / 24) + "d ago";
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
var BILLING_LABELS = { "oauth-bearer": "Subscription", "api-key": "API key" };
|
|
2699
|
+
|
|
2700
|
+
// \u2500\u2500 card builders (session-header-cards.tsx visual language: card surface,
|
|
2701
|
+
// title strip w/ muted aside, two-col dl grid, 4px progress bar) \u2500\u2500
|
|
2702
|
+
function cardOpen(id, title, aside) {
|
|
2703
|
+
return '<div class="card" data-card-id="' + escHtml(id) + '">'
|
|
2704
|
+
+ '<div class="title"><span>' + escHtml(title) + '</span>'
|
|
2705
|
+
+ (aside ? '<span class="aside">' + aside + '</span>' : '')
|
|
2706
|
+
+ '</div><div class="body"><dl>';
|
|
2707
|
+
}
|
|
2708
|
+
function cardClose() { return '</dl></div></div>'; }
|
|
2709
|
+
function row(label, value, title) {
|
|
2710
|
+
return '<dt>' + escHtml(label) + '</dt><dd' + (title ? ' title="' + escHtml(title) + '"' : '') + '>' + value + '</dd>';
|
|
2711
|
+
}
|
|
2712
|
+
function bar(pct) {
|
|
2713
|
+
var p = Math.max(0, Math.min(100, pct));
|
|
2714
|
+
return '<div class="bar' + (p >= 80 ? ' warn' : '') + '"><span style="width:' + p + '%"></span></div>';
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
var toastTimer = null;
|
|
2718
|
+
function toast(msg, isErr) {
|
|
2719
|
+
var el = document.getElementById("toast");
|
|
2720
|
+
el.textContent = msg;
|
|
2721
|
+
el.className = isErr ? "err" : "";
|
|
2722
|
+
el.style.display = "block";
|
|
2723
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
2724
|
+
toastTimer = setTimeout(function () { el.style.display = "none"; }, isErr ? 6000 : 3000);
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// Two-click confirm (ops-panel's pattern): first click arms the button
|
|
2728
|
+
// ("sure?"), a second click within 4s fires the callback; anything else
|
|
2729
|
+
// disarms it. Shared by every destructive action in this panel (delete
|
|
2730
|
+
// wallet, delete preset, disable remote, revoke pairing).
|
|
2731
|
+
var armed = {};
|
|
2732
|
+
function confirmClick(key, btn, fn) {
|
|
2733
|
+
if (armed[key]) {
|
|
2734
|
+
clearTimeout(armed[key].t);
|
|
2735
|
+
delete armed[key];
|
|
2736
|
+
fn();
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
var prev = btn.textContent;
|
|
2740
|
+
btn.classList.add("confirm");
|
|
2741
|
+
btn.textContent = "sure?";
|
|
2742
|
+
armed[key] = { t: setTimeout(function () {
|
|
2743
|
+
delete armed[key];
|
|
2744
|
+
btn.classList.remove("confirm");
|
|
2745
|
+
btn.textContent = prev;
|
|
2746
|
+
}, 4000) };
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
function copyToClipboard(text) {
|
|
2750
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2751
|
+
navigator.clipboard.writeText(text).then(function () { toast("Copied.", false); }, function () { toast("Copy failed.", true); });
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
try {
|
|
2755
|
+
var ta = document.createElement("textarea");
|
|
2756
|
+
ta.value = text;
|
|
2757
|
+
ta.style.position = "fixed";
|
|
2758
|
+
ta.style.opacity = "0";
|
|
2759
|
+
document.body.appendChild(ta);
|
|
2760
|
+
ta.focus();
|
|
2761
|
+
ta.select();
|
|
2762
|
+
document.execCommand("copy");
|
|
2763
|
+
document.body.removeChild(ta);
|
|
2764
|
+
toast("Copied.", false);
|
|
2765
|
+
} catch (e) {
|
|
2766
|
+
toast("Copy failed.", true);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
function wireCopyButtons(container, valuesByKey) {
|
|
2770
|
+
var btns = container.querySelectorAll("[data-copy]");
|
|
2771
|
+
for (var i = 0; i < btns.length; i++) {
|
|
2772
|
+
(function (btn) {
|
|
2773
|
+
btn.addEventListener("click", function () {
|
|
2774
|
+
var key = btn.getAttribute("data-copy");
|
|
2775
|
+
var value = valuesByKey[key];
|
|
2776
|
+
if (value) copyToClipboard(value);
|
|
2777
|
+
});
|
|
2778
|
+
})(btns[i]);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// ============================================================
|
|
2783
|
+
// generic config_get / config_set field editor \u2014 shared by Defaults,
|
|
2784
|
+
// per-harness adapter defaults, and titler.model (Models section).
|
|
2785
|
+
// ============================================================
|
|
2786
|
+
|
|
2787
|
+
// Last revision seen from ANY config_get/config_set response, reused on
|
|
2788
|
+
// every write (the spec: "always pass the last revision"). A daemon that
|
|
2789
|
+
// predates config_get/config_set never sets this; config_set is simply
|
|
2790
|
+
// called without a revision then (server treats that as unconditional).
|
|
2791
|
+
var configRevision = null;
|
|
2792
|
+
|
|
2793
|
+
// path -> true for every currently-known pending-restart key, merged from
|
|
2794
|
+
// every config_get response seen anywhere in the app. Drives the persistent
|
|
2795
|
+
// top banner \u2014 never restarts anything itself, only tells the user to.
|
|
2796
|
+
var pendingRestartMap = {};
|
|
2797
|
+
|
|
2798
|
+
function notePendingRestart(rows) {
|
|
2799
|
+
var changed = false;
|
|
2800
|
+
for (var i = 0; i < (rows || []).length; i++) {
|
|
2801
|
+
var r = rows[i];
|
|
2802
|
+
if (r.pendingRestart) {
|
|
2803
|
+
if (!pendingRestartMap[r.path]) changed = true;
|
|
2804
|
+
pendingRestartMap[r.path] = true;
|
|
2805
|
+
} else if (pendingRestartMap[r.path]) {
|
|
2806
|
+
delete pendingRestartMap[r.path];
|
|
2807
|
+
changed = true;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (changed) renderPendingBanner();
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function renderPendingBanner() {
|
|
2814
|
+
var el = document.getElementById("pending-banner");
|
|
2815
|
+
if (!el) return;
|
|
2816
|
+
var keys = [];
|
|
2817
|
+
for (var k in pendingRestartMap) if (Object.prototype.hasOwnProperty.call(pendingRestartMap, k)) keys.push(k);
|
|
2818
|
+
if (keys.length === 0) {
|
|
2819
|
+
el.style.display = "none";
|
|
2820
|
+
el.textContent = "";
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
keys.sort();
|
|
2824
|
+
el.style.display = "block";
|
|
2825
|
+
el.textContent = "Restart the daemon yourself to apply: " + keys.join(", ") + ". This app never restarts it for you.";
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// Type inference for a config_get row: config-tools.ts's describeKey
|
|
2829
|
+
// spreads deriveValueType(entry.schema) onto every row, so row.valueType
|
|
2830
|
+
// ("boolean" | "number" | "integer" | "string" | "enum" | "string[]" |
|
|
2831
|
+
// "object" | "unknown") plus row.enum / row.min / row.max are the registry's
|
|
2832
|
+
// OWN answer and win outright. The JS-type inference below is only a
|
|
2833
|
+
// fallback for a daemon old enough to predate that field entirely (absent,
|
|
2834
|
+
// not merely "unknown" \u2014 "unknown" is itself a real, present answer for a
|
|
2835
|
+
// schema deriveValueType can't type, e.g. a mixed union, and must NOT be
|
|
2836
|
+
// re-guessed from the current value).
|
|
2837
|
+
function inferValueType(row) {
|
|
2838
|
+
if (row.valueType) return row.valueType;
|
|
2839
|
+
var v = row.effective !== undefined ? row.effective : row.value;
|
|
2840
|
+
if (v === undefined) v = row.default;
|
|
2841
|
+
if (typeof v === "boolean") return "boolean";
|
|
2842
|
+
if (typeof v === "number") return "number";
|
|
2843
|
+
if (typeof v === "string") return "string";
|
|
2844
|
+
if (Array.isArray(v)) {
|
|
2845
|
+
var allStr = true;
|
|
2846
|
+
for (var i = 0; i < v.length; i++) if (typeof v[i] !== "string") allStr = false;
|
|
2847
|
+
return allStr ? "string[]" : "object";
|
|
2848
|
+
}
|
|
2849
|
+
if (v && typeof v === "object") return "object";
|
|
2850
|
+
return "unknown";
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// defaults.spawn.browser is a union of the literals "headless" and false
|
|
2854
|
+
// (config-schema.ts) - deriveValueType's enum-from-union heuristic requires
|
|
2855
|
+
// EVERY literal to be a string, and false isn't one, so this one specific
|
|
2856
|
+
// key comes back "unknown" rather than "enum" (verified against
|
|
2857
|
+
// packages/runtime/src/config-tools.ts's deriveValueType). A checkbox whose
|
|
2858
|
+
// two states are not plain true/false is still the right control for it, so
|
|
2859
|
+
// it keeps its own tiny special case rather than a general boolean-like-enum
|
|
2860
|
+
// mechanism nothing else needs.
|
|
2861
|
+
var BOOL_LIKE_FIELDS = {
|
|
2862
|
+
"defaults.spawn.browser": { on: "headless", off: false, onLabel: "headless" },
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2865
|
+
function pathId(path) { return "cfg-" + String(path).replace(/[^a-zA-Z0-9]/g, "-"); }
|
|
2866
|
+
|
|
2867
|
+
function writableReasonText(row) {
|
|
2868
|
+
if (row.secret) return "secret, set via wallets or the CLI";
|
|
2869
|
+
return "read-only, edit the config file by hand";
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function configDisplayValue(row) {
|
|
2873
|
+
if (row.secret) {
|
|
2874
|
+
if (row.secret.set === false) return "not set";
|
|
2875
|
+
if (typeof row.secret.set === "boolean") {
|
|
2876
|
+
var bits = [];
|
|
2877
|
+
if (row.secret.fingerprint) bits.push(row.secret.fingerprint);
|
|
2878
|
+
if (row.secret.last4) bits.push(row.secret.last4);
|
|
2879
|
+
return bits.length ? bits.join(" / ") : "set";
|
|
2880
|
+
}
|
|
2881
|
+
var parts = [];
|
|
2882
|
+
for (var k in row.secret) {
|
|
2883
|
+
if (Object.prototype.hasOwnProperty.call(row.secret, k)) {
|
|
2884
|
+
parts.push(k + ": " + (row.secret[k] && row.secret[k].set ? "set" : "not set"));
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
return parts.length ? parts.join(", ") : "no keys";
|
|
2888
|
+
}
|
|
2889
|
+
return row.effective !== undefined ? row.effective : row.value;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function numberBoundAttrs(row) {
|
|
2893
|
+
var attrs = "";
|
|
2894
|
+
if (typeof row.min === "number") attrs += ' min="' + row.min + '"';
|
|
2895
|
+
if (typeof row.max === "number") attrs += ' max="' + row.max + '"';
|
|
2896
|
+
return attrs;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function configControlHtml(row) {
|
|
2900
|
+
var vt = inferValueType(row);
|
|
2901
|
+
var val = row.effective !== undefined ? row.effective : (row.value !== undefined ? row.value : row.default);
|
|
2902
|
+
var boolLike = BOOL_LIKE_FIELDS[row.path];
|
|
2903
|
+
var disabledAttr = row.writable ? "" : " disabled";
|
|
2904
|
+
var titleAttr = row.writable ? "" : ' title="' + escHtml(writableReasonText(row)) + '"';
|
|
2905
|
+
if (boolLike) {
|
|
2906
|
+
var checked = val === boolLike.on;
|
|
2907
|
+
return '<label class="inline"><input type="checkbox" data-cfg-bool-like="' + escHtml(row.path) + '"' + (checked ? " checked" : "") + disabledAttr + titleAttr + '> ' + escHtml(boolLike.onLabel || "on") + '</label>';
|
|
2908
|
+
}
|
|
2909
|
+
if (vt === "boolean") {
|
|
2910
|
+
return '<input type="checkbox" data-cfg-auto="' + escHtml(row.path) + '"' + (val ? " checked" : "") + disabledAttr + titleAttr + '>';
|
|
2911
|
+
}
|
|
2912
|
+
if (vt === "enum") {
|
|
2913
|
+
var enumOpts = row.enum || [];
|
|
2914
|
+
var out = '<select data-cfg-auto="' + escHtml(row.path) + '"' + disabledAttr + titleAttr + '>';
|
|
2915
|
+
for (var i = 0; i < enumOpts.length; i++) {
|
|
2916
|
+
out += '<option value="' + escHtml(enumOpts[i]) + '"' + (enumOpts[i] === val ? " selected" : "") + '>' + escHtml(enumOpts[i]) + '</option>';
|
|
2917
|
+
}
|
|
2918
|
+
return out + '</select>';
|
|
2919
|
+
}
|
|
2920
|
+
if (vt === "number" || vt === "integer") {
|
|
2921
|
+
return '<input type="number" data-cfg-auto="' + escHtml(row.path) + '" value="' + escHtml(val == null ? "" : String(val)) + '"'
|
|
2922
|
+
+ (vt === "integer" ? ' step="1"' : "") + numberBoundAttrs(row) + disabledAttr + titleAttr + '>';
|
|
2923
|
+
}
|
|
2924
|
+
if (vt === "string[]") {
|
|
2925
|
+
return '<input type="text" data-cfg-input="' + escHtml(row.path) + '" value="' + escHtml((val || []).join(", ")) + '" placeholder="comma, separated, list"' + disabledAttr + titleAttr + '>'
|
|
2926
|
+
+ ' <button type="button" class="btn-sm" data-cfg-save="' + escHtml(row.path) + '"' + disabledAttr + '>save</button>';
|
|
2927
|
+
}
|
|
2928
|
+
if (vt === "object" || vt === "unknown") {
|
|
2929
|
+
// "unknown" is a real, present answer (a schema deriveValueType can't
|
|
2930
|
+
// type, e.g. a mixed boolean/object union) \u2014 a raw JSON editor is the
|
|
2931
|
+
// one control that can represent any of those shapes without guessing.
|
|
2932
|
+
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>'
|
|
2933
|
+
+ ' <button type="button" class="btn-sm" data-cfg-save="' + escHtml(row.path) + '"' + disabledAttr + '>save</button></div>';
|
|
2934
|
+
}
|
|
2935
|
+
return '<input type="text" data-cfg-auto="' + escHtml(row.path) + '" value="' + escHtml(val == null ? "" : String(val)) + '"' + disabledAttr + titleAttr + '>';
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// One dl "row" for a config key. Wrapped in a display:contents div so the
|
|
2939
|
+
// dt/dd pairs stay direct grid children (the 2-col dl grid) while still
|
|
2940
|
+
// being addressable as one group for wiring/lookup. A writable:false row
|
|
2941
|
+
// still renders a real control (checkbox/select/input), just disabled,
|
|
2942
|
+
// with the reason in its title, never a second, ad hoc read-only widget.
|
|
2943
|
+
function configFieldHtml(row) {
|
|
2944
|
+
var label = row.label || humanize(String(row.path).split(".").slice(-1)[0]);
|
|
2945
|
+
var badge = '<span class="tag">' + escHtml(row.apply) + '</span>';
|
|
2946
|
+
var extra = "";
|
|
2947
|
+
if (row.source === "env" && row.envOverride) extra += ' <span class="tag bad">env ' + escHtml(row.envOverride) + '</span>';
|
|
2948
|
+
if (row.pendingRestart) extra += ' <span class="tag warn-tag">restart pending</span>';
|
|
2949
|
+
|
|
2950
|
+
var dd = "";
|
|
2951
|
+
if (row.secret) {
|
|
2952
|
+
dd += escHtml(String(configDisplayValue(row)));
|
|
2953
|
+
} else {
|
|
2954
|
+
dd += configControlHtml(row);
|
|
2955
|
+
}
|
|
2956
|
+
dd += " " + badge + extra;
|
|
2957
|
+
if (row.writable && !row.secret && row.value !== undefined) {
|
|
2958
|
+
dd += ' <button type="button" class="btn-sm" data-cfg-reset="' + escHtml(row.path) + '" title="Reset to default">reset</button>';
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
var html = '<div class="cfg-row" data-cfg-path="' + escHtml(row.path) + '">'
|
|
2962
|
+
+ '<dt title="' + escHtml(row.path) + '">' + escHtml(label) + '</dt><dd>' + dd + '</dd>';
|
|
2963
|
+
if (row.help) html += '<dt></dt><dd class="cfg-help muted">' + escHtml(row.help) + '</dd>';
|
|
2964
|
+
html += '<dt></dt><dd class="cfg-err" data-cfg-err="' + escHtml(row.path) + '" style="display:none"></dd>';
|
|
2965
|
+
html += '</div>';
|
|
2966
|
+
return html;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function buildConfigCardHtml(cardId, title, rows) {
|
|
2970
|
+
var body = "";
|
|
2971
|
+
for (var i = 0; i < rows.length; i++) body += configFieldHtml(rows[i]);
|
|
2972
|
+
return '<div class="card" data-card-id="' + escHtml(cardId) + '"><div class="body"><dl>' + body + '</dl></div></div>';
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
function extractConfigSetErrorCode(message) {
|
|
2976
|
+
var s = String(message || "");
|
|
2977
|
+
var start = s.indexOf("[");
|
|
2978
|
+
var end = start === -1 ? -1 : s.indexOf("]", start);
|
|
2979
|
+
if (start === -1 || end === -1) return "";
|
|
2980
|
+
return s.slice(start + 1, end);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function describeConfigSetError(message) {
|
|
2984
|
+
var code = extractConfigSetErrorCode(message);
|
|
2985
|
+
var HUMAN = {
|
|
2986
|
+
invalid_input: "That request was not valid.",
|
|
2987
|
+
unknown_key: "This daemon does not know that setting.",
|
|
2988
|
+
not_writable: "This setting cannot be changed here.",
|
|
2989
|
+
invalid_value: "That value is not valid for this setting.",
|
|
2990
|
+
invalid_config: "That change would leave the config file invalid.",
|
|
2991
|
+
stale_revision: "This section changed elsewhere. Reloading.",
|
|
2992
|
+
};
|
|
2993
|
+
return HUMAN[code] || message;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// Attach listeners for every config control inside "container". rowsByPath
|
|
2997
|
+
// supplies the row metadata a save/reset needs (valueType for parsing).
|
|
2998
|
+
// onSaved(path) fires after a successful write (path just written);
|
|
2999
|
+
// onStale() fires on a stale_revision rejection (reload the section).
|
|
3000
|
+
function wireConfigRows(container, rowsByPath, onSaved, onStale) {
|
|
3001
|
+
function submit(path, value, isUnset) {
|
|
3002
|
+
var args = isUnset ? { key: path, unset: true, revision: configRevision } : { key: path, value: value, revision: configRevision };
|
|
3003
|
+
return loadTool("config_set", args).then(function (r) {
|
|
3004
|
+
if (!r.ok) {
|
|
3005
|
+
var stale = extractConfigSetErrorCode(r.message) === "stale_revision";
|
|
3006
|
+
toast(describeConfigSetError(r.message), true);
|
|
3007
|
+
if (stale && onStale) onStale();
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
var data = r.data;
|
|
3011
|
+
if (data && data.revision) configRevision = data.revision;
|
|
3012
|
+
var note = "Saved.";
|
|
3013
|
+
if (data && data.applied === "restart-required") note = "Saved. Restart required to apply.";
|
|
3014
|
+
if (data && data.shadowedByEnv) note += " Overridden by " + data.shadowedByEnv + ".";
|
|
3015
|
+
toast(note, false);
|
|
3016
|
+
if (onSaved) onSaved(path);
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
var autos = container.querySelectorAll("[data-cfg-auto]");
|
|
3021
|
+
for (var i = 0; i < autos.length; i++) {
|
|
3022
|
+
(function (el) {
|
|
3023
|
+
el.addEventListener("change", function () {
|
|
3024
|
+
var path = el.getAttribute("data-cfg-auto");
|
|
3025
|
+
var value;
|
|
3026
|
+
if (el.type === "checkbox") value = el.checked;
|
|
3027
|
+
else if (el.type === "number") value = el.value === "" ? null : Number(el.value);
|
|
3028
|
+
else value = el.value;
|
|
3029
|
+
submit(path, value, false);
|
|
3030
|
+
});
|
|
3031
|
+
})(autos[i]);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
var boolLikes = container.querySelectorAll("[data-cfg-bool-like]");
|
|
3035
|
+
for (var b = 0; b < boolLikes.length; b++) {
|
|
3036
|
+
(function (el) {
|
|
3037
|
+
el.addEventListener("change", function () {
|
|
3038
|
+
var path = el.getAttribute("data-cfg-bool-like");
|
|
3039
|
+
var spec = BOOL_LIKE_FIELDS[path];
|
|
3040
|
+
submit(path, el.checked ? spec.on : spec.off, false);
|
|
3041
|
+
});
|
|
3042
|
+
})(boolLikes[b]);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
var saves = container.querySelectorAll("[data-cfg-save]");
|
|
3046
|
+
for (var s = 0; s < saves.length; s++) {
|
|
3047
|
+
(function (btn) {
|
|
3048
|
+
btn.addEventListener("click", function () {
|
|
3049
|
+
var path = btn.getAttribute("data-cfg-save");
|
|
3050
|
+
var row = rowsByPath[path];
|
|
3051
|
+
var input = container.querySelector('[data-cfg-input="' + cssEscape(path) + '"]');
|
|
3052
|
+
var errEl = container.querySelector('[data-cfg-err="' + cssEscape(path) + '"]');
|
|
3053
|
+
if (errEl) { errEl.style.display = "none"; errEl.textContent = ""; }
|
|
3054
|
+
var vt = row ? inferValueType(row) : "string";
|
|
3055
|
+
var value;
|
|
3056
|
+
if (vt === "string[]") {
|
|
3057
|
+
value = input.value.split(",").map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });
|
|
3058
|
+
} else if (vt === "object" || vt === "unknown") {
|
|
3059
|
+
try {
|
|
3060
|
+
value = JSON.parse(input.value);
|
|
3061
|
+
} catch (e) {
|
|
3062
|
+
if (errEl) { errEl.style.display = "block"; errEl.textContent = "Invalid JSON: " + e.message; }
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
} else {
|
|
3066
|
+
value = input.value;
|
|
3067
|
+
}
|
|
3068
|
+
submit(path, value, false);
|
|
3069
|
+
});
|
|
3070
|
+
})(saves[s]);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
var resets = container.querySelectorAll("[data-cfg-reset]");
|
|
3074
|
+
for (var r = 0; r < resets.length; r++) {
|
|
3075
|
+
(function (btn) {
|
|
3076
|
+
btn.addEventListener("click", function () {
|
|
3077
|
+
submit(btn.getAttribute("data-cfg-reset"), undefined, true);
|
|
3078
|
+
});
|
|
3079
|
+
})(resets[r]);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// ============================================================
|
|
3084
|
+
// nav + deep-link routing
|
|
3085
|
+
// ============================================================
|
|
3086
|
+
|
|
3087
|
+
var current = { section: SECTIONS[0].id, id: undefined, sub: undefined };
|
|
3088
|
+
var loaded = {}; // section -> true once its data has been fetched at least once
|
|
3089
|
+
|
|
3090
|
+
function renderNav() {
|
|
3091
|
+
var el = document.getElementById("nav");
|
|
3092
|
+
var html = "";
|
|
3093
|
+
for (var i = 0; i < SECTIONS.length; i++) {
|
|
3094
|
+
html += '<button data-section="' + SECTIONS[i].id + '" class="' + (SECTIONS[i].id === current.section ? "active" : "") + '">' + escHtml(SECTIONS[i].label) + '</button>';
|
|
3095
|
+
}
|
|
3096
|
+
el.innerHTML = html;
|
|
3097
|
+
var btns = el.querySelectorAll("button[data-section]");
|
|
3098
|
+
for (var k = 0; k < btns.length; k++) {
|
|
3099
|
+
(function (btn) {
|
|
3100
|
+
btn.addEventListener("click", function () {
|
|
3101
|
+
location.hash = buildConfigFragment(btn.getAttribute("data-section"));
|
|
3102
|
+
});
|
|
3103
|
+
})(btns[k]);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
function showSection(sectionId) {
|
|
3108
|
+
var els = document.querySelectorAll(".section");
|
|
3109
|
+
for (var i = 0; i < els.length; i++) {
|
|
3110
|
+
els[i].classList.toggle("active", els[i].getAttribute("data-section") === sectionId);
|
|
3111
|
+
}
|
|
3112
|
+
var btns = document.querySelectorAll("#nav button[data-section]");
|
|
3113
|
+
for (var j = 0; j < btns.length; j++) {
|
|
3114
|
+
btns[j].classList.toggle("active", btns[j].getAttribute("data-section") === sectionId);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// Scroll to + highlight the card matching 'id' within the active section's
|
|
3119
|
+
// container. Unknown id means the "not found" banner (never a blank page).
|
|
3120
|
+
function applyDeepLink(sectionId, id, sub) {
|
|
3121
|
+
var notfound = document.getElementById("notfound");
|
|
3122
|
+
notfound.style.display = "none";
|
|
3123
|
+
var prevHl = document.querySelectorAll(".card.hl, tr.hl");
|
|
3124
|
+
for (var p = 0; p < prevHl.length; p++) prevHl[p].classList.remove("hl");
|
|
3125
|
+
if (id === undefined) return;
|
|
3126
|
+
var container = document.getElementById("sec-" + sectionId);
|
|
3127
|
+
if (!container) return;
|
|
3128
|
+
var targetId = sectionId === "remote" && id === "pairing" ? "pairing" : id;
|
|
3129
|
+
var target = container.querySelector('[data-card-id="' + cssEscape(targetId) + '"]');
|
|
3130
|
+
if (!target) {
|
|
3131
|
+
notfound.textContent = "not found: " + id + (sub ? "/" + sub : "");
|
|
3132
|
+
notfound.style.display = "block";
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
target.classList.add("hl");
|
|
3136
|
+
scrollIntoViewSafe(target);
|
|
3137
|
+
if (sub) {
|
|
3138
|
+
var row = target.querySelector('[data-row-id="' + cssEscape(sub) + '"]');
|
|
3139
|
+
if (row) {
|
|
3140
|
+
row.classList.add("hl");
|
|
3141
|
+
scrollIntoViewSafe(row);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// Not every embedding host implements scrollIntoView on a sandboxed/minimal
|
|
3147
|
+
// DOM \u2014 highlighting must still work even when scrolling can't.
|
|
3148
|
+
function scrollIntoViewSafe(el) {
|
|
3149
|
+
if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "nearest" });
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// CSS.escape isn't guaranteed in every sandboxed webview \u2014 a minimal,
|
|
3153
|
+
// dependency-free fallback for the characters our own ids ever contain.
|
|
3154
|
+
function cssEscape(s) {
|
|
3155
|
+
return String(s).replace(/[^a-zA-Z0-9_-]/g, function (c) {
|
|
3156
|
+
return "\\\\" + c.charCodeAt(0).toString(16) + " ";
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// Clears every secret this panel ever holds only in memory (a remote-enable
|
|
3161
|
+
// reveal, a pairing offer) \u2014 called on navigating away from #remote and on
|
|
3162
|
+
// host teardown. Never persisted, never in updateModelContext.
|
|
3163
|
+
function clearRemoteSecrets() {
|
|
3164
|
+
remoteReveal = null;
|
|
3165
|
+
pairReveal = null;
|
|
3166
|
+
clearPairCountdown();
|
|
3167
|
+
// The #sec-remote DOM may still show a rendered reveal box from before
|
|
3168
|
+
// this call \u2014 clearing the variable alone does not touch it. Force the
|
|
3169
|
+
// next visit to re-fetch and re-render from scratch (remoteReveal is
|
|
3170
|
+
// null by then, so no reveal box comes back) instead of leaving the
|
|
3171
|
+
// stale secret sitting in a hidden .section until some other event
|
|
3172
|
+
// happens to re-render it.
|
|
3173
|
+
loaded.remote = false;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function route(pushBack) {
|
|
3177
|
+
var parsed = parseConfigFragment(location.hash);
|
|
3178
|
+
if (current.section === "remote" && parsed.section !== "remote") {
|
|
3179
|
+
clearRemoteSecrets();
|
|
3180
|
+
}
|
|
3181
|
+
current = parsed;
|
|
3182
|
+
showSection(parsed.section);
|
|
3183
|
+
renderNav();
|
|
3184
|
+
ensureLoaded(parsed.section).then(function () {
|
|
3185
|
+
applyDeepLink(parsed.section, parsed.id, parsed.sub);
|
|
3186
|
+
});
|
|
3187
|
+
if (pushBack) location.hash = buildConfigFragment(parsed.section, parsed.id, parsed.sub);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
window.addEventListener("hashchange", function () { route(false); });
|
|
3191
|
+
|
|
3192
|
+
// \u2500\u2500 MCP-hosted deep links \u2500\u2500
|
|
3193
|
+
// A browser tab navigates via location.hash directly; an MCP host (Claude
|
|
3194
|
+
// Desktop, Codex, VS Code) has no URL bar for this panel, so it opens
|
|
3195
|
+
// app_ui_config with a view argument instead (plan section 3.4). The bridge
|
|
3196
|
+
// (app-ui-apps.ts) surfaces that as tool input; this turns it into the same
|
|
3197
|
+
// #<section>/<id>/<sub> fragment a browser deep link would use.
|
|
3198
|
+
//
|
|
3199
|
+
// location.hash still wins on the very first route \u2014 an actual browser deep
|
|
3200
|
+
// link must not be overridden by a stale/default tool call. Every
|
|
3201
|
+
// subsequent tool-input notification (a fresh app_ui_config invocation while
|
|
3202
|
+
// the panel is already open) is a live "go to this view" command and always
|
|
3203
|
+
// re-routes.
|
|
3204
|
+
var toolInputCount = 0;
|
|
3205
|
+
var hashPresentAtLoad = !!(location.hash && location.hash !== "#");
|
|
3206
|
+
|
|
3207
|
+
function routeToView(view) {
|
|
3208
|
+
if (typeof view !== "string" || !view) return;
|
|
3209
|
+
var parsed = parseConfigFragment(view);
|
|
3210
|
+
var frag = buildConfigFragment(parsed.section, parsed.id, parsed.sub);
|
|
3211
|
+
if (location.hash === frag) { route(false); } else { location.hash = frag; }
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
function handleToolInput(input) {
|
|
3215
|
+
toolInputCount += 1;
|
|
3216
|
+
if (!shouldApplyIncomingView(toolInputCount, hashPresentAtLoad)) return;
|
|
3217
|
+
routeToView(input && input.view);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// ============================================================
|
|
3221
|
+
// section data + render dispatch
|
|
3222
|
+
// ============================================================
|
|
3223
|
+
|
|
3224
|
+
var SECTION_LOADERS = {
|
|
3225
|
+
wallets: loadWallets,
|
|
3226
|
+
harnesses: loadHarnesses,
|
|
3227
|
+
models: loadModels,
|
|
3228
|
+
defaults: loadDefaults,
|
|
3229
|
+
remote: loadRemote,
|
|
3230
|
+
advanced: loadAdvanced,
|
|
3231
|
+
};
|
|
3232
|
+
|
|
3233
|
+
function ensureLoaded(sectionId) {
|
|
3234
|
+
if (loaded[sectionId]) return Promise.resolve();
|
|
3235
|
+
var fn = SECTION_LOADERS[sectionId];
|
|
3236
|
+
if (!fn) return Promise.resolve();
|
|
3237
|
+
return fn().then(function () { loaded[sectionId] = true; });
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function reloadCurrent() {
|
|
3241
|
+
loaded[current.section] = false;
|
|
3242
|
+
return ensureLoaded(current.section).then(function () {
|
|
3243
|
+
applyDeepLink(current.section, current.id, current.sub);
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
// ============================================================
|
|
3248
|
+
// Wallets
|
|
3249
|
+
// ============================================================
|
|
3250
|
+
|
|
3251
|
+
var walletsState = {
|
|
3252
|
+
window: "7d",
|
|
3253
|
+
profilesResult: null,
|
|
3254
|
+
rollupResult: null,
|
|
3255
|
+
presetsResult: null,
|
|
3256
|
+
discoveredResult: null,
|
|
3257
|
+
showAddForm: false,
|
|
3258
|
+
showDiscovered: false,
|
|
3259
|
+
expanded: {}, // profileId -> { edit: bool, curate: bool }
|
|
3260
|
+
providerModelsByEndpoint: {}, // endpoint -> catalog_provider_models rows
|
|
3261
|
+
};
|
|
3262
|
+
|
|
3263
|
+
function loadWallets() {
|
|
3264
|
+
return Promise.all([
|
|
3265
|
+
loadTool("auth_profile_list", { full: true }),
|
|
3266
|
+
loadTool("usage_rollup", { window: walletsState.window, groupBy: ["profile"] }),
|
|
3267
|
+
loadTool("harness_preset_list", {}),
|
|
3268
|
+
]).then(function (results) {
|
|
3269
|
+
walletsState.profilesResult = results[0];
|
|
3270
|
+
walletsState.rollupResult = results[1];
|
|
3271
|
+
walletsState.presetsResult = results[2];
|
|
3272
|
+
renderWallets();
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
function reloadWalletsWindow(win) {
|
|
3277
|
+
walletsState.window = win;
|
|
3278
|
+
fetchRollup(false);
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function fetchRollup(probe) {
|
|
3282
|
+
return loadTool("usage_rollup", { window: walletsState.window, groupBy: ["profile"], probe: !!probe }).then(function (r) {
|
|
3283
|
+
walletsState.rollupResult = r;
|
|
3284
|
+
renderWallets();
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function byProfileMap(rollupResult) {
|
|
3289
|
+
var map = {};
|
|
3290
|
+
if (rollupResult.ok && rollupResult.data && Array.isArray(rollupResult.data.byProfile)) {
|
|
3291
|
+
for (var i = 0; i < rollupResult.data.byProfile.length; i++) {
|
|
3292
|
+
map[rollupResult.data.byProfile[i].profileRef] = rollupResult.data.byProfile[i];
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return map;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
function presetsReferencing(profileId) {
|
|
3299
|
+
var out = [];
|
|
3300
|
+
var pr = walletsState.presetsResult;
|
|
3301
|
+
if (!pr || !pr.ok) return out;
|
|
3302
|
+
var presets = (pr.data && pr.data.presets) || [];
|
|
3303
|
+
for (var i = 0; i < presets.length; i++) {
|
|
3304
|
+
if (presets[i].profileRef === profileId) out.push(presets[i]);
|
|
3305
|
+
}
|
|
3306
|
+
return out;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function getWalletExpanded(id) {
|
|
3310
|
+
if (!walletsState.expanded[id]) walletsState.expanded[id] = { edit: false, curate: false, curateMode: undefined };
|
|
3311
|
+
return walletsState.expanded[id];
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function replaceProfileInState(profile) {
|
|
3315
|
+
var pr = walletsState.profilesResult;
|
|
3316
|
+
if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;
|
|
3317
|
+
var list = pr.data.profiles;
|
|
3318
|
+
for (var i = 0; i < list.length; i++) {
|
|
3319
|
+
if (list[i].id === profile.id) { list[i] = profile; return; }
|
|
3320
|
+
}
|
|
3321
|
+
list.push(profile);
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function removeProfileFromState(id) {
|
|
3325
|
+
var pr = walletsState.profilesResult;
|
|
3326
|
+
if (!pr || !pr.ok || !pr.data || !Array.isArray(pr.data.profiles)) return;
|
|
3327
|
+
pr.data.profiles = pr.data.profiles.filter(function (p) { return p.id !== id; });
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
function renderWallets() {
|
|
3331
|
+
var el = document.getElementById("sec-wallets");
|
|
3332
|
+
var rr = walletsState.rollupResult;
|
|
3333
|
+
var html = '<div class="sect-h">Spend<span class="pillbar" id="wallet-window"></span>'
|
|
3334
|
+
+ '<button type="button" class="btn-sm" id="spend-refresh-live">refresh live</button></div>'
|
|
3335
|
+
+ '<div class="muted-note" style="padding:0 0 4px">Refreshing live consumes a small sliver of the rate budget for that wallet.</div>';
|
|
3336
|
+
|
|
3337
|
+
if (rr && rr.ok) {
|
|
3338
|
+
var t = rr.data.total || {};
|
|
3339
|
+
html += '<div class="card"><div class="body"><dl>'
|
|
3340
|
+
+ row("Window", escHtml(rr.data.window))
|
|
3341
|
+
+ (typeof t.spentUsd === "number" ? row("Spend", formatCost(t.spentUsd)) : "")
|
|
3342
|
+
+ (typeof t.tokensIn === "number" || typeof t.tokensOut === "number"
|
|
3343
|
+
? row("Tokens", (typeof t.tokensIn === "number" ? formatTokens(t.tokensIn) + " in" : "") + (typeof t.tokensIn === "number" && typeof t.tokensOut === "number" ? " · " : "") + (typeof t.tokensOut === "number" ? formatTokens(t.tokensOut) + " out" : ""))
|
|
3344
|
+
: "")
|
|
3345
|
+
+ (typeof t.unpricedTokens === "number" && t.unpricedTokens > 0 ? row("Unpriced tokens", formatTokens(t.unpricedTokens)) : "")
|
|
3346
|
+
+ '</dl></div></div>';
|
|
3347
|
+
} else if (rr) {
|
|
3348
|
+
html += '<div class="muted-note">usage_rollup: ' + escHtml(rr.message) + '</div>';
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
html += '<div class="sect-h">Wallets'
|
|
3352
|
+
+ '<button type="button" class="btn-sm" id="wallet-add-toggle">' + (walletsState.showAddForm ? "close" : "+ add wallet") + '</button>'
|
|
3353
|
+
+ '<button type="button" class="btn-sm" id="wallet-scan-toggle">' + (walletsState.showDiscovered ? "close" : "find credentials") + '</button>'
|
|
3354
|
+
+ '</div>';
|
|
3355
|
+
|
|
3356
|
+
if (walletsState.showAddForm) html += addWalletFormHtml();
|
|
3357
|
+
if (walletsState.showDiscovered) html += discoveredCredentialsHtml();
|
|
3358
|
+
|
|
3359
|
+
html += '<div class="grid" id="wallet-cards"></div>';
|
|
3360
|
+
|
|
3361
|
+
el.innerHTML = html;
|
|
3362
|
+
|
|
3363
|
+
var pillEl = document.getElementById("wallet-window");
|
|
3364
|
+
var windows = ["5h", "7d", "30d"];
|
|
3365
|
+
var pillHtml = "";
|
|
3366
|
+
for (var w = 0; w < windows.length; w++) {
|
|
3367
|
+
pillHtml += '<button data-win="' + windows[w] + '" class="' + (windows[w] === walletsState.window ? "active" : "") + '">' + windows[w] + '</button>';
|
|
3368
|
+
}
|
|
3369
|
+
pillEl.innerHTML = pillHtml;
|
|
3370
|
+
var pillBtns = pillEl.querySelectorAll("button");
|
|
3371
|
+
for (var pb = 0; pb < pillBtns.length; pb++) {
|
|
3372
|
+
(function (btn) {
|
|
3373
|
+
btn.addEventListener("click", function () { reloadWalletsWindow(btn.getAttribute("data-win")); });
|
|
3374
|
+
})(pillBtns[pb]);
|
|
3375
|
+
}
|
|
3376
|
+
document.getElementById("spend-refresh-live").addEventListener("click", function () { fetchRollup(true); });
|
|
3377
|
+
|
|
3378
|
+
document.getElementById("wallet-add-toggle").addEventListener("click", function () {
|
|
3379
|
+
walletsState.showAddForm = !walletsState.showAddForm;
|
|
3380
|
+
renderWallets();
|
|
3381
|
+
});
|
|
3382
|
+
document.getElementById("wallet-scan-toggle").addEventListener("click", function () {
|
|
3383
|
+
walletsState.showDiscovered = !walletsState.showDiscovered;
|
|
3384
|
+
if (walletsState.showDiscovered && !walletsState.discoveredResult) {
|
|
3385
|
+
loadTool("auth_discover_credentials", {}).then(function (r) {
|
|
3386
|
+
walletsState.discoveredResult = r;
|
|
3387
|
+
renderWallets();
|
|
3388
|
+
});
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
renderWallets();
|
|
3392
|
+
});
|
|
3393
|
+
|
|
3394
|
+
wireAddWalletForm(el);
|
|
3395
|
+
wireDiscoveredCredentials(el);
|
|
3396
|
+
renderWalletCards();
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function addWalletFormHtml() {
|
|
3400
|
+
return '<div class="form-panel" id="wallet-add-form">'
|
|
3401
|
+
+ '<div class="form-row">'
|
|
3402
|
+
+ '<div class="field"><label>Id</label><input type="text" id="wf-id" placeholder="my-anthropic-key"></div>'
|
|
3403
|
+
+ '<div class="field"><label>Endpoint</label><input type="text" id="wf-endpoint" list="wf-endpoint-list" placeholder="anthropic"><datalist id="wf-endpoint-list">'
|
|
3404
|
+
+ '<option value="anthropic"><option value="openrouter"><option value="moonshot"><option value="openai"></datalist></div>'
|
|
3405
|
+
+ '<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>'
|
|
3406
|
+
+ '<div class="field"><label>Label (optional)</label><input type="text" id="wf-label"></div>'
|
|
3407
|
+
+ '</div>'
|
|
3408
|
+
+ '<div class="form-row">'
|
|
3409
|
+
+ '<div class="field"><label>Credential</label><input type="password" id="wf-credential" placeholder="paste secret"></div>'
|
|
3410
|
+
+ '<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>'
|
|
3411
|
+
+ '<button type="button" class="btn-sm primary" id="wf-submit">Create wallet</button>'
|
|
3412
|
+
+ '</div>'
|
|
3413
|
+
+ '<div class="cfg-err" id="wf-err" style="display:none"></div>'
|
|
3414
|
+
+ '</div>';
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
function wireAddWalletForm(container) {
|
|
3418
|
+
var methodEl = container.querySelector("#wf-method");
|
|
3419
|
+
var sourceField = container.querySelector("#wf-source-field");
|
|
3420
|
+
if (!methodEl) return;
|
|
3421
|
+
methodEl.addEventListener("change", function () {
|
|
3422
|
+
sourceField.style.display = methodEl.value === "oauth-bearer" ? "" : "none";
|
|
3423
|
+
});
|
|
3424
|
+
container.querySelector("#wf-submit").addEventListener("click", function () {
|
|
3425
|
+
var errEl = container.querySelector("#wf-err");
|
|
3426
|
+
errEl.style.display = "none";
|
|
3427
|
+
var id = container.querySelector("#wf-id").value.trim();
|
|
3428
|
+
var endpoint = container.querySelector("#wf-endpoint").value.trim();
|
|
3429
|
+
var method = methodEl.value;
|
|
3430
|
+
var label = container.querySelector("#wf-label").value.trim();
|
|
3431
|
+
var credentialInput = container.querySelector("#wf-credential");
|
|
3432
|
+
var sourceInput = container.querySelector("#wf-source");
|
|
3433
|
+
var credential = credentialInput.value;
|
|
3434
|
+
var source = method === "oauth-bearer" ? sourceInput.value.trim() : "";
|
|
3435
|
+
// Clear the secret from the DOM and this local scope's reference to it
|
|
3436
|
+
// immediately, before the async call even starts.
|
|
3437
|
+
credentialInput.value = "";
|
|
3438
|
+
if (!id || !endpoint) {
|
|
3439
|
+
errEl.style.display = "block";
|
|
3440
|
+
errEl.textContent = "Id and endpoint are required.";
|
|
3441
|
+
credential = "";
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
if (!credential && !source) {
|
|
3445
|
+
errEl.style.display = "block";
|
|
3446
|
+
errEl.textContent = "Give either a credential or a self-refreshing source.";
|
|
3447
|
+
credential = "";
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
var args = { id: id, endpoint: endpoint, method: method };
|
|
3451
|
+
if (label) args.label = label;
|
|
3452
|
+
if (credential) args.credential = credential;
|
|
3453
|
+
else if (source) args.source = source;
|
|
3454
|
+
credential = "";
|
|
3455
|
+
loadTool("auth_profile_create", args).then(function (r) {
|
|
3456
|
+
if (!r.ok) {
|
|
3457
|
+
errEl.style.display = "block";
|
|
3458
|
+
errEl.textContent = r.message;
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
toast("Wallet created.", false);
|
|
3462
|
+
walletsState.showAddForm = false;
|
|
3463
|
+
loadWallets();
|
|
3464
|
+
});
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function discoveredCredentialsHtml() {
|
|
3469
|
+
var dr = walletsState.discoveredResult;
|
|
3470
|
+
var html = '<div class="card"><div class="body">';
|
|
3471
|
+
if (!dr) {
|
|
3472
|
+
html += '<div class="muted-note">scanning…</div>';
|
|
3473
|
+
} else if (!dr.ok) {
|
|
3474
|
+
html += '<div class="muted-note">auth_discover_credentials: ' + escHtml(dr.message) + '</div>';
|
|
3475
|
+
} else {
|
|
3476
|
+
var creds = (dr.data && dr.data.credentials) || [];
|
|
3477
|
+
if (creds.length === 0) {
|
|
3478
|
+
html += '<div class="empty">No importable credentials found on this host.</div>';
|
|
3479
|
+
} else {
|
|
3480
|
+
html += '<table><thead><tr><th>Endpoint</th><th>Method</th><th>Origin</th><th>Hint</th><th></th></tr></thead><tbody>';
|
|
3481
|
+
for (var i = 0; i < creds.length; i++) {
|
|
3482
|
+
var c = creds[i];
|
|
3483
|
+
var defaultId = c.origin + "-" + c.endpoint;
|
|
3484
|
+
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>'
|
|
3485
|
+
+ '<td><input type="text" class="dc-id" data-dc-idx="' + i + '" value="' + escHtml(defaultId) + '" style="width:120px">'
|
|
3486
|
+
+ ' <button type="button" class="btn-sm" data-dc-import="' + i + '">import</button></td></tr>';
|
|
3487
|
+
}
|
|
3488
|
+
html += '</tbody></table>';
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
html += '</div></div>';
|
|
3492
|
+
return html;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
function wireDiscoveredCredentials(container) {
|
|
3496
|
+
var btns = container.querySelectorAll("[data-dc-import]");
|
|
3497
|
+
for (var i = 0; i < btns.length; i++) {
|
|
3498
|
+
(function (btn) {
|
|
3499
|
+
btn.addEventListener("click", function () {
|
|
3500
|
+
var idx = Number(btn.getAttribute("data-dc-import"));
|
|
3501
|
+
var dr = walletsState.discoveredResult;
|
|
3502
|
+
var c = dr && dr.ok && dr.data.credentials[idx];
|
|
3503
|
+
if (!c) return;
|
|
3504
|
+
var idInput = container.querySelector('.dc-id[data-dc-idx="' + idx + '"]');
|
|
3505
|
+
var id = (idInput && idInput.value.trim()) || (c.origin + "-" + c.endpoint);
|
|
3506
|
+
loadTool("auth_profile_import", { origin: c.origin, endpoint: c.endpoint, id: id }).then(function (r) {
|
|
3507
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3508
|
+
toast("Imported.", false);
|
|
3509
|
+
loadWallets();
|
|
3510
|
+
});
|
|
3511
|
+
});
|
|
3512
|
+
})(btns[i]);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function renderWalletCards() {
|
|
3517
|
+
var cardsEl = document.getElementById("wallet-cards");
|
|
3518
|
+
if (!cardsEl) return;
|
|
3519
|
+
var pr = walletsState.profilesResult;
|
|
3520
|
+
if (!pr || !pr.ok) {
|
|
3521
|
+
cardsEl.innerHTML = '<div class="empty">' + (pr ? escHtml(pr.message) : "loading…") + '</div>';
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
var profiles = (pr.data && pr.data.profiles) || [];
|
|
3525
|
+
if (profiles.length === 0) {
|
|
3526
|
+
cardsEl.innerHTML = '<div class="empty">No auth profiles configured on this host.</div>';
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
var rr = walletsState.rollupResult;
|
|
3530
|
+
var spend = rr && rr.ok ? byProfileMap(rr) : {};
|
|
3531
|
+
var out = "";
|
|
3532
|
+
for (var i = 0; i < profiles.length; i++) {
|
|
3533
|
+
out += walletCardHtml(profiles[i], spend[profiles[i].id]);
|
|
3534
|
+
}
|
|
3535
|
+
cardsEl.innerHTML = out;
|
|
3536
|
+
wireWalletCards(cardsEl);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function walletCardHtml(p, spend) {
|
|
3540
|
+
var title = p.label || p.id;
|
|
3541
|
+
var exp = getWalletExpanded(p.id);
|
|
3542
|
+
var out = cardOpen(p.id, title, escHtml(p.endpoint));
|
|
3543
|
+
out += row("Method", escHtml(BILLING_LABELS[p.method] || humanize(p.method)));
|
|
3544
|
+
var keyLine = p.keyStatus === "stored"
|
|
3545
|
+
? [p.fingerprint, p.last4].filter(Boolean).join(" · ") || "stored"
|
|
3546
|
+
: p.keyStatus === "self-refreshing" ? "self-refreshing" : "unavailable";
|
|
3547
|
+
out += row("Key", escHtml(keyLine));
|
|
3548
|
+
if (p.origin) out += row("Imported from", escHtml(p.origin));
|
|
3549
|
+
out += '<dt>Enabled</dt><dd><label class="inline"><input type="checkbox" data-wallet-enabled="' + escHtml(p.id) + '"' + (p.disabled ? "" : " checked") + '></label></dd>';
|
|
3550
|
+
var curated = !p.models || p.models.mode === "all" ? "All eligible" : (p.models.ids || []).length + " curated";
|
|
3551
|
+
out += row("Models", escHtml(curated));
|
|
3552
|
+
if (spend) {
|
|
3553
|
+
if (typeof spend.spentUsd === "number") out += row("Spend (window)", formatCost(spend.spentUsd));
|
|
3554
|
+
if (p.costBudget && typeof p.costBudget.maxCostUsd === "number") {
|
|
3555
|
+
var pct = p.costBudget.maxCostUsd > 0 ? ((spend.spentUsd || 0) / p.costBudget.maxCostUsd) * 100 : 0;
|
|
3556
|
+
out += row("Budget", formatCost(p.costBudget.maxCostUsd) + " / " + escHtml(p.costBudget.window));
|
|
3557
|
+
out += bar(pct);
|
|
3558
|
+
}
|
|
3559
|
+
if (spend.remaining) {
|
|
3560
|
+
out += row("Remaining quota", String(spend.remaining.remaining), "resets " + formatDate(spend.remaining.resetsAt));
|
|
3561
|
+
}
|
|
3562
|
+
if (spend.credits && typeof spend.credits.balanceUsd === "number") {
|
|
3563
|
+
out += row("Credit balance", formatCost(spend.credits.balanceUsd), spend.credits.asOf ? "as of " + formatDate(spend.credits.asOf) : undefined);
|
|
3564
|
+
}
|
|
3565
|
+
} else if (p.costBudget && typeof p.costBudget.maxCostUsd === "number") {
|
|
3566
|
+
out += row("Budget", formatCost(p.costBudget.maxCostUsd) + " / " + escHtml(p.costBudget.window));
|
|
3567
|
+
}
|
|
3568
|
+
out += '<div class="wallet-card-actions">'
|
|
3569
|
+
+ '<button type="button" class="btn-sm" data-wallet-edit-toggle="' + escHtml(p.id) + '">' + (exp.edit ? "close" : "rename / budget") + '</button>'
|
|
3570
|
+
+ '<button type="button" class="btn-sm" data-wallet-curate-toggle="' + escHtml(p.id) + '">' + (exp.curate ? "close" : "manage models") + '</button>'
|
|
3571
|
+
+ '<button type="button" class="btn-sm danger" data-wallet-delete="' + escHtml(p.id) + '">delete</button>'
|
|
3572
|
+
+ '</div>';
|
|
3573
|
+
if (exp.edit) out += walletEditPanelHtml(p);
|
|
3574
|
+
if (exp.curate) out += walletModelsEditorHtml(p, walletsState.providerModelsByEndpoint[p.endpoint], exp.curateMode);
|
|
3575
|
+
out += cardClose();
|
|
3576
|
+
return out;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function walletEditPanelHtml(p) {
|
|
3580
|
+
var hasBudget = !!(p.costBudget && typeof p.costBudget.maxCostUsd === "number");
|
|
3581
|
+
return '<div class="form-panel" data-wallet-edit-panel="' + escHtml(p.id) + '">'
|
|
3582
|
+
+ '<div class="field"><label>Label</label><input type="text" class="we-label" value="' + escHtml(p.label || "") + '"></div>'
|
|
3583
|
+
+ '<label class="inline"><input type="checkbox" class="we-no-budget"' + (hasBudget ? "" : " checked") + '> no spend cap</label>'
|
|
3584
|
+
+ '<div class="form-row we-budget-fields" style="' + (hasBudget ? "" : "display:none") + '">'
|
|
3585
|
+
+ '<div class="field"><label>Max cost (USD)</label><input type="number" class="we-max-cost" value="' + escHtml(hasBudget ? String(p.costBudget.maxCostUsd) : "") + '"></div>'
|
|
3586
|
+
+ '<div class="field"><label>Window</label><input type="text" class="we-window" value="' + escHtml(hasBudget ? p.costBudget.window : "7d") + '"></div>'
|
|
3587
|
+
+ '<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>'
|
|
3588
|
+
+ '</div>'
|
|
3589
|
+
+ '<button type="button" class="btn-sm primary" data-wallet-edit-save="' + escHtml(p.id) + '">save</button>'
|
|
3590
|
+
+ '</div>';
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
// pendingMode is the panel's OWN pending selection (getWalletExpanded's
|
|
3594
|
+
// curateMode), not necessarily what the server has stored yet: switching to
|
|
3595
|
+
// "curate" must reveal the checklist right away so there is something to
|
|
3596
|
+
// check, without writing a destructive "curate to nothing" the instant the
|
|
3597
|
+
// radio moves. The first checked model is what actually persists the
|
|
3598
|
+
// mode=allow switch (see the .mm-id change handler).
|
|
3599
|
+
function walletModelsEditorHtml(p, catalogRows, pendingMode) {
|
|
3600
|
+
var mode = pendingMode || (p.models && p.models.mode) || "all";
|
|
3601
|
+
var ids = (p.models && p.models.ids) || [];
|
|
3602
|
+
var html = '<div class="form-panel" data-wallet-models-panel="' + escHtml(p.id) + '">';
|
|
3603
|
+
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> ';
|
|
3604
|
+
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>';
|
|
3605
|
+
if (mode === "allow") html += ' <button type="button" class="btn-sm" data-mm-refresh="' + escHtml(p.id) + '">refresh models</button>';
|
|
3606
|
+
html += '<div class="mm-list" data-mm-list="' + escHtml(p.id) + '" style="' + (mode === "allow" ? "" : "display:none") + '">';
|
|
3607
|
+
if (mode === "allow") {
|
|
3608
|
+
if (!catalogRows) {
|
|
3609
|
+
html += '<div class="muted-note">loading models…</div>';
|
|
3610
|
+
} else if (catalogRows.length === 0) {
|
|
3611
|
+
html += '<div class="empty">No models known for this endpoint.</div>';
|
|
3612
|
+
} else {
|
|
3613
|
+
for (var i = 0; i < catalogRows.length; i++) {
|
|
3614
|
+
var m = catalogRows[i];
|
|
3615
|
+
var checked = ids.indexOf(m.id) !== -1;
|
|
3616
|
+
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>';
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
html += '</div></div>';
|
|
3621
|
+
return html;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
function wireWalletCards(container) {
|
|
3625
|
+
var enabledToggles = container.querySelectorAll("[data-wallet-enabled]");
|
|
3626
|
+
for (var i = 0; i < enabledToggles.length; i++) {
|
|
3627
|
+
(function (el) {
|
|
3628
|
+
el.addEventListener("change", function () {
|
|
3629
|
+
var id = el.getAttribute("data-wallet-enabled");
|
|
3630
|
+
var wantEnabled = el.checked;
|
|
3631
|
+
el.disabled = true;
|
|
3632
|
+
loadTool("auth_profile_set_enabled", { id: id, enabled: wantEnabled }).then(function (r) {
|
|
3633
|
+
el.disabled = false;
|
|
3634
|
+
if (!r.ok) { toast(r.message, true); el.checked = !wantEnabled; return; }
|
|
3635
|
+
replaceProfileInState(r.data.profile);
|
|
3636
|
+
toast(wantEnabled ? "Wallet enabled." : "Wallet disabled.", false);
|
|
3637
|
+
renderWalletCards();
|
|
3638
|
+
});
|
|
3639
|
+
});
|
|
3640
|
+
})(enabledToggles[i]);
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
var editToggles = container.querySelectorAll("[data-wallet-edit-toggle]");
|
|
3644
|
+
for (var e = 0; e < editToggles.length; e++) {
|
|
3645
|
+
(function (btn) {
|
|
3646
|
+
btn.addEventListener("click", function () {
|
|
3647
|
+
var id = btn.getAttribute("data-wallet-edit-toggle");
|
|
3648
|
+
var exp = getWalletExpanded(id);
|
|
3649
|
+
exp.edit = !exp.edit;
|
|
3650
|
+
renderWalletCards();
|
|
3651
|
+
});
|
|
3652
|
+
})(editToggles[e]);
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
var editSaves = container.querySelectorAll("[data-wallet-edit-save]");
|
|
3656
|
+
for (var s = 0; s < editSaves.length; s++) {
|
|
3657
|
+
(function (btn) {
|
|
3658
|
+
btn.addEventListener("click", function () {
|
|
3659
|
+
var id = btn.getAttribute("data-wallet-edit-save");
|
|
3660
|
+
var panel = container.querySelector('[data-wallet-edit-panel="' + cssEscape(id) + '"]');
|
|
3661
|
+
var label = panel.querySelector(".we-label").value.trim();
|
|
3662
|
+
var noBudget = panel.querySelector(".we-no-budget").checked;
|
|
3663
|
+
var patch = { id: id, label: label === "" ? null : label };
|
|
3664
|
+
if (noBudget) {
|
|
3665
|
+
patch.costBudget = null;
|
|
3666
|
+
} else {
|
|
3667
|
+
patch.costBudget = {
|
|
3668
|
+
maxCostUsd: Number(panel.querySelector(".we-max-cost").value) || 0,
|
|
3669
|
+
window: panel.querySelector(".we-window").value.trim() || "7d",
|
|
3670
|
+
scope: panel.querySelector(".we-scope").value,
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
loadTool("auth_profile_update", patch).then(function (r) {
|
|
3674
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3675
|
+
replaceProfileInState(r.data.profile);
|
|
3676
|
+
toast("Wallet updated.", false);
|
|
3677
|
+
getWalletExpanded(id).edit = false;
|
|
3678
|
+
renderWalletCards();
|
|
3679
|
+
});
|
|
3680
|
+
});
|
|
3681
|
+
})(editSaves[s]);
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
var noBudgetToggles = container.querySelectorAll(".we-no-budget");
|
|
3685
|
+
for (var nb = 0; nb < noBudgetToggles.length; nb++) {
|
|
3686
|
+
(function (el) {
|
|
3687
|
+
el.addEventListener("change", function () {
|
|
3688
|
+
var fields = el.parentElement.querySelector(".we-budget-fields");
|
|
3689
|
+
if (fields) fields.style.display = el.checked ? "none" : "";
|
|
3690
|
+
});
|
|
3691
|
+
})(noBudgetToggles[nb]);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
var curateToggles = container.querySelectorAll("[data-wallet-curate-toggle]");
|
|
3695
|
+
for (var c = 0; c < curateToggles.length; c++) {
|
|
3696
|
+
(function (btn) {
|
|
3697
|
+
btn.addEventListener("click", function () {
|
|
3698
|
+
var id = btn.getAttribute("data-wallet-curate-toggle");
|
|
3699
|
+
var exp = getWalletExpanded(id);
|
|
3700
|
+
exp.curate = !exp.curate;
|
|
3701
|
+
if (exp.curate) {
|
|
3702
|
+
var profile = findProfile(id);
|
|
3703
|
+
if (profile && !walletsState.providerModelsByEndpoint[profile.endpoint]) {
|
|
3704
|
+
loadTool("catalog_provider_models", { endpoint: profile.endpoint, limit: 200 }).then(function (r) {
|
|
3705
|
+
walletsState.providerModelsByEndpoint[profile.endpoint] = r.ok ? (r.data.models || []) : [];
|
|
3706
|
+
renderWalletCards();
|
|
3707
|
+
});
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
renderWalletCards();
|
|
3712
|
+
});
|
|
3713
|
+
})(curateToggles[c]);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
var mmModes = container.querySelectorAll(".mm-mode");
|
|
3717
|
+
for (var mm = 0; mm < mmModes.length; mm++) {
|
|
3718
|
+
(function (el) {
|
|
3719
|
+
el.addEventListener("change", function () {
|
|
3720
|
+
var id = el.getAttribute("data-mm-profile");
|
|
3721
|
+
if (el.value === "all") {
|
|
3722
|
+
getWalletExpanded(id).curateMode = undefined;
|
|
3723
|
+
loadTool("auth_profile_set_models", { id: id, mode: "all" }).then(function (r) {
|
|
3724
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3725
|
+
replaceProfileInState(r.data.profile);
|
|
3726
|
+
renderWalletCards();
|
|
3727
|
+
});
|
|
3728
|
+
} else {
|
|
3729
|
+
// Reveal the checklist locally (a pending selection, independent
|
|
3730
|
+
// of what the server has stored); nothing is written until a
|
|
3731
|
+
// model is actually checked, so switching this radio alone can
|
|
3732
|
+
// never silently curate the wallet down to nothing.
|
|
3733
|
+
getWalletExpanded(id).curateMode = "allow";
|
|
3734
|
+
renderWalletCards();
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
3737
|
+
})(mmModes[mm]);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
var mmIds = container.querySelectorAll(".mm-id");
|
|
3741
|
+
for (var mi = 0; mi < mmIds.length; mi++) {
|
|
3742
|
+
(function (el) {
|
|
3743
|
+
el.addEventListener("change", function () {
|
|
3744
|
+
var id = el.getAttribute("data-mm-profile");
|
|
3745
|
+
var list = container.querySelector('[data-mm-list="' + cssEscape(id) + '"]');
|
|
3746
|
+
var checked = list.querySelectorAll(".mm-id:checked");
|
|
3747
|
+
var ids = [];
|
|
3748
|
+
for (var i2 = 0; i2 < checked.length; i2++) ids.push(checked[i2].value);
|
|
3749
|
+
loadTool("auth_profile_set_models", { id: id, mode: "allow", ids: ids }).then(function (r) {
|
|
3750
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3751
|
+
replaceProfileInState(r.data.profile);
|
|
3752
|
+
});
|
|
3753
|
+
});
|
|
3754
|
+
})(mmIds[mi]);
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
var mmRefresh = container.querySelectorAll("[data-mm-refresh]");
|
|
3758
|
+
for (var mr = 0; mr < mmRefresh.length; mr++) {
|
|
3759
|
+
(function (btn) {
|
|
3760
|
+
btn.addEventListener("click", function () {
|
|
3761
|
+
var id = btn.getAttribute("data-mm-refresh");
|
|
3762
|
+
loadTool("auth_profile_refresh_models", { id: id }).then(function (r) {
|
|
3763
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3764
|
+
replaceProfileInState(r.data.profile);
|
|
3765
|
+
var added = (r.data.added || []).length;
|
|
3766
|
+
var removed = (r.data.removed || []).length;
|
|
3767
|
+
toast("Refreshed models (+" + added + " / -" + removed + ").", false);
|
|
3768
|
+
renderWalletCards();
|
|
3769
|
+
});
|
|
3770
|
+
});
|
|
3771
|
+
})(mmRefresh[mr]);
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
var deleteBtns = container.querySelectorAll("[data-wallet-delete]");
|
|
3775
|
+
for (var d = 0; d < deleteBtns.length; d++) {
|
|
3776
|
+
(function (btn) {
|
|
3777
|
+
var id = btn.getAttribute("data-wallet-delete");
|
|
3778
|
+
btn.addEventListener("click", function () {
|
|
3779
|
+
confirmClick("wallet-delete-" + id, btn, function () {
|
|
3780
|
+
var blockedBy = presetsReferencing(id);
|
|
3781
|
+
if (blockedBy.length > 0) {
|
|
3782
|
+
var names = blockedBy.map(function (p) { return p.name || p.id; }).join(", ");
|
|
3783
|
+
toast("Cannot delete: still used by harness preset(s) " + names + ".", true);
|
|
3784
|
+
return;
|
|
3785
|
+
}
|
|
3786
|
+
loadTool("auth_profile_delete", { id: id }).then(function (r) {
|
|
3787
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
3788
|
+
removeProfileFromState(id);
|
|
3789
|
+
toast("Wallet deleted.", false);
|
|
3790
|
+
renderWalletCards();
|
|
3791
|
+
});
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
})(deleteBtns[d]);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
function findProfile(id) {
|
|
3799
|
+
var pr = walletsState.profilesResult;
|
|
3800
|
+
if (!pr || !pr.ok) return null;
|
|
3801
|
+
var list = (pr.data && pr.data.profiles) || [];
|
|
3802
|
+
for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
|
|
3803
|
+
return null;
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
// ============================================================
|
|
3807
|
+
// Harnesses
|
|
3808
|
+
// ============================================================
|
|
3809
|
+
|
|
3810
|
+
var harnessesState = {
|
|
3811
|
+
adaptersResult: null,
|
|
3812
|
+
capsResult: null,
|
|
3813
|
+
presetsResult: null,
|
|
3814
|
+
rolesResult: null,
|
|
3815
|
+
profilesResult: null,
|
|
3816
|
+
expandedDefaults: {}, // slug -> bool
|
|
3817
|
+
defaultsCache: {}, // slug -> row[]
|
|
3818
|
+
showPresetForm: false,
|
|
3819
|
+
};
|
|
3820
|
+
|
|
3821
|
+
function loadHarnesses() {
|
|
3822
|
+
return Promise.all([
|
|
3823
|
+
loadTool("adapter_list", {}),
|
|
3824
|
+
loadTool("harness_capabilities", {}),
|
|
3825
|
+
loadTool("harness_preset_list", {}),
|
|
3826
|
+
loadTool("role_list", {}),
|
|
3827
|
+
loadTool("auth_profile_list", {}),
|
|
3828
|
+
]).then(function (results) {
|
|
3829
|
+
harnessesState.adaptersResult = results[0];
|
|
3830
|
+
harnessesState.capsResult = results[1];
|
|
3831
|
+
harnessesState.presetsResult = results[2];
|
|
3832
|
+
harnessesState.rolesResult = results[3];
|
|
3833
|
+
harnessesState.profilesResult = results[4];
|
|
3834
|
+
renderHarnesses();
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
function adapterDefaultsPaths(slug) {
|
|
3839
|
+
return [
|
|
3840
|
+
"defaults.adapters." + slug + ".skills",
|
|
3841
|
+
"defaults.adapters." + slug + ".options",
|
|
3842
|
+
"defaults.adapters." + slug + ".contextContinuity",
|
|
3843
|
+
"defaults.adapters." + slug + ".auth.mode",
|
|
3844
|
+
"defaults.adapters." + slug + ".auth.source",
|
|
3845
|
+
"defaults.adapters." + slug + ".auth.provider",
|
|
3846
|
+
"defaults.adapters." + slug + ".auth.token",
|
|
3847
|
+
"defaults.adapters." + slug + ".auth.apiKey",
|
|
3848
|
+
];
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
function renderHarnesses() {
|
|
3852
|
+
var el = document.getElementById("sec-harnesses");
|
|
3853
|
+
var adaptersR = harnessesState.adaptersResult;
|
|
3854
|
+
var capsR = harnessesState.capsResult;
|
|
3855
|
+
var presetsR = harnessesState.presetsResult;
|
|
3856
|
+
var rolesR = harnessesState.rolesResult;
|
|
3857
|
+
if (!adaptersR.ok) {
|
|
3858
|
+
el.innerHTML = '<div class="empty">adapter_list: ' + escHtml(adaptersR.message) + '</div>';
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
var adapters = (adaptersR.data && adaptersR.data.adapters) || [];
|
|
3862
|
+
var capsBySlug = {};
|
|
3863
|
+
if (capsR.ok) {
|
|
3864
|
+
var caps = (capsR.data && capsR.data.capabilities) || [];
|
|
3865
|
+
for (var i = 0; i < caps.length; i++) capsBySlug[caps[i].adapter] = caps[i];
|
|
3866
|
+
}
|
|
3867
|
+
var presetsBySlug = {};
|
|
3868
|
+
var allPresets = [];
|
|
3869
|
+
if (presetsR.ok) {
|
|
3870
|
+
allPresets = (presetsR.data && presetsR.data.presets) || [];
|
|
3871
|
+
for (var j = 0; j < allPresets.length; j++) {
|
|
3872
|
+
var slug = allPresets[j].harnessSlug;
|
|
3873
|
+
if (!presetsBySlug[slug]) presetsBySlug[slug] = [];
|
|
3874
|
+
presetsBySlug[slug].push(allPresets[j]);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
var html = '<div class="sect-h">Adapters</div>';
|
|
3879
|
+
if (!capsR.ok && capsR.unknown) html += '<div class="muted-note">Capability detail needs a newer agentproto daemon.</div>';
|
|
3880
|
+
else if (!capsR.ok) html += '<div class="muted-note">harness_capabilities: ' + escHtml(capsR.message) + '</div>';
|
|
3881
|
+
if (!presetsR.ok) html += '<div class="muted-note">harness_preset_list: ' + escHtml(presetsR.message) + '</div>';
|
|
3882
|
+
|
|
3883
|
+
html += '<div class="grid">';
|
|
3884
|
+
if (adapters.length === 0) {
|
|
3885
|
+
html += '<div class="empty">No adapters installed on this host.</div>';
|
|
3886
|
+
}
|
|
3887
|
+
for (var k = 0; k < adapters.length; k++) {
|
|
3888
|
+
var a = adapters[k];
|
|
3889
|
+
var cap = capsBySlug[a.slug];
|
|
3890
|
+
var slugPresets = presetsBySlug[a.slug] || [];
|
|
3891
|
+
var def = null;
|
|
3892
|
+
for (var d = 0; d < slugPresets.length; d++) if (slugPresets[d].isDefault) def = slugPresets[d];
|
|
3893
|
+
html += cardOpen(a.slug, a.name || a.slug, escHtml(a.slug));
|
|
3894
|
+
if (a.version) html += row("Version", escHtml(a.version));
|
|
3895
|
+
if (a.protocol) html += row("Protocol", escHtml(a.protocol));
|
|
3896
|
+
html += row("Models", String((a.models || []).length));
|
|
3897
|
+
if (cap && Array.isArray(cap.providers) && cap.providers.length > 0) {
|
|
3898
|
+
var chips = "";
|
|
3899
|
+
for (var pi = 0; pi < cap.providers.length; pi++) {
|
|
3900
|
+
var prov = cap.providers[pi];
|
|
3901
|
+
chips += '<span class="tag ' + (prov.cred && prov.cred.present ? "ok" : "") + '">' + escHtml(prov.billingEndpoint) + '</span> ';
|
|
3902
|
+
}
|
|
3903
|
+
html += '<dt>Billing</dt><dd>' + chips + '</dd>';
|
|
3904
|
+
}
|
|
3905
|
+
if (def) {
|
|
3906
|
+
html += row("Default preset", escHtml(def.name) + ' → <a class="link" data-goto="wallets/' + encodeURIComponent(def.profileRef) + '">' + escHtml(def.profileRef) + '</a> / ' + escHtml(def.defaultModel));
|
|
3907
|
+
} else if (slugPresets.length > 0) {
|
|
3908
|
+
html += row("Presets", String(slugPresets.length) + " (no default)");
|
|
3909
|
+
}
|
|
3910
|
+
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>';
|
|
3911
|
+
html += '<div id="adapter-defaults-' + escHtml(a.slug) + '"></div>';
|
|
3912
|
+
html += cardClose();
|
|
3913
|
+
}
|
|
3914
|
+
html += '</div>';
|
|
3915
|
+
|
|
3916
|
+
html += '<div class="sect-h">Presets<button type="button" class="btn-sm" id="preset-add-toggle">' + (harnessesState.showPresetForm ? "close" : "+ new preset") + '</button></div>';
|
|
3917
|
+
if (harnessesState.showPresetForm) html += presetFormHtml(adapters);
|
|
3918
|
+
html += presetsTableHtml(allPresets);
|
|
3919
|
+
|
|
3920
|
+
html += '<div class="sect-h">Roles</div>';
|
|
3921
|
+
if (rolesR.ok) {
|
|
3922
|
+
var roles = (rolesR.data && rolesR.data.roles) || [];
|
|
3923
|
+
if (roles.length === 0) {
|
|
3924
|
+
html += '<div class="empty">No roles registered.</div>';
|
|
3925
|
+
} else {
|
|
3926
|
+
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>';
|
|
3927
|
+
for (var r = 0; r < roles.length; r++) {
|
|
3928
|
+
var role = roles[r];
|
|
3929
|
+
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>';
|
|
3930
|
+
}
|
|
3931
|
+
html += '</tbody></table></div></div>';
|
|
3932
|
+
}
|
|
3933
|
+
} else {
|
|
3934
|
+
html += '<div class="muted-note">role_list: ' + escHtml(rolesR.message) + '</div>';
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
el.innerHTML = html;
|
|
3938
|
+
wireGotoLinks(el);
|
|
3939
|
+
wireAdapterDefaultsToggles(el);
|
|
3940
|
+
wirePresetForm(el, adapters);
|
|
3941
|
+
wirePresetsTable(el);
|
|
3942
|
+
|
|
3943
|
+
for (var slug2 in harnessesState.expandedDefaults) {
|
|
3944
|
+
if (harnessesState.expandedDefaults[slug2] && harnessesState.defaultsCache[slug2]) {
|
|
3945
|
+
renderAdapterDefaultsCard(slug2);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
function presetsTableHtml(presets) {
|
|
3951
|
+
if (presets.length === 0) return '<div class="empty">No harness presets configured.</div>';
|
|
3952
|
+
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>';
|
|
3953
|
+
for (var i = 0; i < presets.length; i++) {
|
|
3954
|
+
var p = presets[i];
|
|
3955
|
+
html += '<tr data-row-id="' + escHtml(p.id) + '"><td>' + escHtml(p.id) + '</td><td>' + escHtml(p.harnessSlug) + '</td><td>' + escHtml(p.name) + '</td>'
|
|
3956
|
+
+ '<td><a class="link" data-goto="wallets/' + encodeURIComponent(p.profileRef) + '">' + escHtml(p.profileRef) + '</a>' + (p.profileDisabled ? ' <span class="tag bad">disabled/missing</span>' : '') + '</td>'
|
|
3957
|
+
+ '<td>' + escHtml(p.defaultModel) + '</td>'
|
|
3958
|
+
+ '<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>'
|
|
3959
|
+
+ '<td><button type="button" class="btn-sm danger" data-preset-delete="' + escHtml(p.id) + '">delete</button></td></tr>';
|
|
3960
|
+
}
|
|
3961
|
+
html += '</tbody></table></div></div>';
|
|
3962
|
+
return html;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
function presetFormHtml(adapters) {
|
|
3966
|
+
var profiles = (harnessesState.profilesResult && harnessesState.profilesResult.ok && harnessesState.profilesResult.data.profiles) || [];
|
|
3967
|
+
var html = '<div class="form-panel">'
|
|
3968
|
+
+ '<div class="form-row">'
|
|
3969
|
+
+ '<div class="field"><label>Id</label><input type="text" id="pf-id"></div>'
|
|
3970
|
+
+ '<div class="field"><label>Harness</label><select id="pf-harness">';
|
|
3971
|
+
for (var i = 0; i < adapters.length; i++) html += '<option value="' + escHtml(adapters[i].slug) + '">' + escHtml(adapters[i].slug) + '</option>';
|
|
3972
|
+
html += '</select></div>'
|
|
3973
|
+
+ '<div class="field"><label>Name</label><input type="text" id="pf-name"></div>'
|
|
3974
|
+
+ '</div><div class="form-row">'
|
|
3975
|
+
+ '<div class="field"><label>Wallet</label><select id="pf-profile">';
|
|
3976
|
+
for (var j = 0; j < profiles.length; j++) html += '<option value="' + escHtml(profiles[j].id) + '">' + escHtml(profiles[j].label || profiles[j].id) + '</option>';
|
|
3977
|
+
html += '</select></div>'
|
|
3978
|
+
+ '<div class="field"><label>Default model</label><input type="text" id="pf-model" placeholder="vendor/model"></div>'
|
|
3979
|
+
+ '<label class="inline"><input type="checkbox" id="pf-default"> make default for this harness</label>'
|
|
3980
|
+
+ '<button type="button" class="btn-sm primary" id="pf-submit">Create preset</button>'
|
|
3981
|
+
+ '</div><div class="cfg-err" id="pf-err" style="display:none"></div></div>';
|
|
3982
|
+
return html;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
function wirePresetForm(container, adapters) {
|
|
3986
|
+
var toggle = container.querySelector("#preset-add-toggle");
|
|
3987
|
+
if (toggle) toggle.addEventListener("click", function () {
|
|
3988
|
+
harnessesState.showPresetForm = !harnessesState.showPresetForm;
|
|
3989
|
+
renderHarnesses();
|
|
3990
|
+
});
|
|
3991
|
+
var submit = container.querySelector("#pf-submit");
|
|
3992
|
+
if (!submit) return;
|
|
3993
|
+
submit.addEventListener("click", function () {
|
|
3994
|
+
var errEl = container.querySelector("#pf-err");
|
|
3995
|
+
errEl.style.display = "none";
|
|
3996
|
+
var id = container.querySelector("#pf-id").value.trim();
|
|
3997
|
+
var harnessSlug = container.querySelector("#pf-harness").value;
|
|
3998
|
+
var name = container.querySelector("#pf-name").value.trim();
|
|
3999
|
+
var profileRef = container.querySelector("#pf-profile").value;
|
|
4000
|
+
var defaultModel = container.querySelector("#pf-model").value.trim();
|
|
4001
|
+
var isDefault = container.querySelector("#pf-default").checked;
|
|
4002
|
+
if (!id || !name || !defaultModel || !profileRef) {
|
|
4003
|
+
errEl.style.display = "block";
|
|
4004
|
+
errEl.textContent = "Id, name, wallet, and default model are required.";
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
loadTool("harness_preset_create", { id: id, harnessSlug: harnessSlug, name: name, profileRef: profileRef, defaultModel: defaultModel, isDefault: isDefault }).then(function (r) {
|
|
4008
|
+
if (!r.ok) { errEl.style.display = "block"; errEl.textContent = r.message; return; }
|
|
4009
|
+
toast("Preset saved.", false);
|
|
4010
|
+
harnessesState.showPresetForm = false;
|
|
4011
|
+
loadHarnesses();
|
|
4012
|
+
});
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
function wirePresetsTable(container) {
|
|
4017
|
+
var setDefaults = container.querySelectorAll("[data-preset-set-default]");
|
|
4018
|
+
for (var i = 0; i < setDefaults.length; i++) {
|
|
4019
|
+
(function (btn) {
|
|
4020
|
+
btn.addEventListener("click", function () {
|
|
4021
|
+
var presetId = btn.getAttribute("data-preset-set-default");
|
|
4022
|
+
var harnessSlug = btn.getAttribute("data-preset-harness");
|
|
4023
|
+
loadTool("harness_preset_set_default", { harnessSlug: harnessSlug, presetId: presetId }).then(function (r) {
|
|
4024
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4025
|
+
toast("Default preset updated.", false);
|
|
4026
|
+
loadHarnesses();
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
})(setDefaults[i]);
|
|
4030
|
+
}
|
|
4031
|
+
var deletes = container.querySelectorAll("[data-preset-delete]");
|
|
4032
|
+
for (var d = 0; d < deletes.length; d++) {
|
|
4033
|
+
(function (btn) {
|
|
4034
|
+
var presetId = btn.getAttribute("data-preset-delete");
|
|
4035
|
+
btn.addEventListener("click", function () {
|
|
4036
|
+
confirmClick("preset-delete-" + presetId, btn, function () {
|
|
4037
|
+
loadTool("harness_preset_delete", { id: presetId }).then(function (r) {
|
|
4038
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4039
|
+
toast("Preset deleted.", false);
|
|
4040
|
+
loadHarnesses();
|
|
4041
|
+
});
|
|
4042
|
+
});
|
|
4043
|
+
});
|
|
4044
|
+
})(deletes[d]);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
function wireAdapterDefaultsToggles(container) {
|
|
4049
|
+
var toggles = container.querySelectorAll("[data-adapter-defaults-toggle]");
|
|
4050
|
+
for (var i = 0; i < toggles.length; i++) {
|
|
4051
|
+
(function (btn) {
|
|
4052
|
+
btn.addEventListener("click", function () {
|
|
4053
|
+
var slug = btn.getAttribute("data-adapter-defaults-toggle");
|
|
4054
|
+
harnessesState.expandedDefaults[slug] = !harnessesState.expandedDefaults[slug];
|
|
4055
|
+
if (harnessesState.expandedDefaults[slug] && !harnessesState.defaultsCache[slug]) {
|
|
4056
|
+
loadTool("config_get", { keys: adapterDefaultsPaths(slug) }).then(function (r) {
|
|
4057
|
+
if (r.ok && r.data.revision) configRevision = r.data.revision;
|
|
4058
|
+
harnessesState.defaultsCache[slug] = r.ok ? (r.data.keys || []) : [];
|
|
4059
|
+
if (r.ok) notePendingRestart(r.data.keys);
|
|
4060
|
+
if (!r.ok && r.unknown) {
|
|
4061
|
+
var host = document.getElementById("adapter-defaults-" + slug);
|
|
4062
|
+
if (host) host.innerHTML = '<div class="muted-note">Per-harness defaults need a newer agentproto daemon.</div>';
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
if (!r.ok) {
|
|
4066
|
+
var host2 = document.getElementById("adapter-defaults-" + slug);
|
|
4067
|
+
if (host2) host2.innerHTML = '<div class="muted-note">config_get: ' + escHtml(r.message) + '</div>';
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
renderAdapterDefaultsCard(slug);
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
btn.textContent = harnessesState.expandedDefaults[slug] ? "hide defaults" : "spawn defaults";
|
|
4074
|
+
var host3 = document.getElementById("adapter-defaults-" + slug);
|
|
4075
|
+
if (host3 && !harnessesState.expandedDefaults[slug]) host3.innerHTML = "";
|
|
4076
|
+
else if (host3 && harnessesState.defaultsCache[slug]) renderAdapterDefaultsCard(slug);
|
|
4077
|
+
});
|
|
4078
|
+
})(toggles[i]);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
function renderAdapterDefaultsCard(slug) {
|
|
4083
|
+
var host = document.getElementById("adapter-defaults-" + slug);
|
|
4084
|
+
if (!host) return;
|
|
4085
|
+
var rows = harnessesState.defaultsCache[slug] || [];
|
|
4086
|
+
var rowsByPath = {};
|
|
4087
|
+
for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];
|
|
4088
|
+
host.innerHTML = '<dl>' + rows.map(configFieldHtml).join("") + '</dl>';
|
|
4089
|
+
wireConfigRows(host, rowsByPath, function (path) {
|
|
4090
|
+
loadTool("config_get", { keys: adapterDefaultsPaths(slug) }).then(function (r) {
|
|
4091
|
+
if (!r.ok) { toast("config_get: " + r.message, true); return; }
|
|
4092
|
+
if (r.data.revision) configRevision = r.data.revision;
|
|
4093
|
+
harnessesState.defaultsCache[slug] = r.data.keys || [];
|
|
4094
|
+
notePendingRestart(r.data.keys);
|
|
4095
|
+
renderAdapterDefaultsCard(slug);
|
|
4096
|
+
});
|
|
4097
|
+
}, function () {
|
|
4098
|
+
loadHarnesses();
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
function wireGotoLinks(container) {
|
|
4103
|
+
var links = container.querySelectorAll("a[data-goto]");
|
|
4104
|
+
for (var i = 0; i < links.length; i++) {
|
|
4105
|
+
(function (a) {
|
|
4106
|
+
a.addEventListener("click", function (e) {
|
|
4107
|
+
e.preventDefault();
|
|
4108
|
+
location.hash = "#" + a.getAttribute("data-goto");
|
|
4109
|
+
});
|
|
4110
|
+
})(links[i]);
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// ============================================================
|
|
4115
|
+
// Models
|
|
4116
|
+
// ============================================================
|
|
4117
|
+
|
|
4118
|
+
var modelsState = { routes: [], runnableOnly: false, walletFilter: "", titlerRow: null };
|
|
4119
|
+
|
|
4120
|
+
function loadModels() {
|
|
4121
|
+
return Promise.all([
|
|
4122
|
+
loadTool("catalog_models", { full: true }),
|
|
4123
|
+
loadTool("auth_profile_list", { full: true }),
|
|
4124
|
+
loadTool("config_get", { keys: ["titler.model"] }),
|
|
4125
|
+
]).then(function (results) {
|
|
4126
|
+
modelsState.routesResult = results[0];
|
|
4127
|
+
modelsState.profilesResult = results[1];
|
|
4128
|
+
modelsState.titlerResult = results[2];
|
|
4129
|
+
if (results[2].ok && results[2].data.revision) configRevision = results[2].data.revision;
|
|
4130
|
+
if (results[2].ok) notePendingRestart(results[2].data.keys);
|
|
4131
|
+
renderModels();
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function renderModels() {
|
|
4136
|
+
var el = document.getElementById("sec-models");
|
|
4137
|
+
var rr = modelsState.routesResult;
|
|
4138
|
+
if (!rr.ok) {
|
|
4139
|
+
el.innerHTML = '<div class="empty">catalog_models: ' + escHtml(rr.message) + '</div>';
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
var routes = (rr.data && rr.data.routes) || [];
|
|
4143
|
+
var profiles = (modelsState.profilesResult && modelsState.profilesResult.ok && modelsState.profilesResult.data && modelsState.profilesResult.data.profiles) || [];
|
|
4144
|
+
|
|
4145
|
+
var html = '<div class="sect-h">Titler model</div><div id="titler-model-card"></div>';
|
|
4146
|
+
|
|
4147
|
+
html += '<div class="sect-h">Catalog<label class="inline"><input type="checkbox" id="runnable-only"' + (modelsState.runnableOnly ? " checked" : "") + '> runnable only</label>'
|
|
4148
|
+
+ '<label class="inline">wallet <select id="wallet-filter"><option value="">any</option>';
|
|
4149
|
+
for (var i = 0; i < profiles.length; i++) {
|
|
4150
|
+
html += '<option value="' + escHtml(profiles[i].id) + '"' + (profiles[i].id === modelsState.walletFilter ? " selected" : "") + '>' + escHtml(profiles[i].label || profiles[i].id) + '</option>';
|
|
4151
|
+
}
|
|
4152
|
+
html += '</select></label></div>';
|
|
4153
|
+
|
|
4154
|
+
var filtered = routes.filter(function (r) {
|
|
4155
|
+
if (modelsState.runnableOnly && !r.runnable) return false;
|
|
4156
|
+
if (modelsState.walletFilter && (!r.eligibleProfiles || r.eligibleProfiles.indexOf(modelsState.walletFilter) === -1)) return false;
|
|
4157
|
+
return true;
|
|
4158
|
+
});
|
|
4159
|
+
|
|
4160
|
+
var byVendor = {};
|
|
4161
|
+
var vendorOrder = [];
|
|
4162
|
+
for (var f = 0; f < filtered.length; f++) {
|
|
4163
|
+
var v = filtered[f].vendor;
|
|
4164
|
+
if (!byVendor[v]) { byVendor[v] = []; vendorOrder.push(v); }
|
|
4165
|
+
byVendor[v].push(filtered[f]);
|
|
4166
|
+
}
|
|
4167
|
+
vendorOrder.sort();
|
|
4168
|
+
|
|
4169
|
+
if (vendorOrder.length === 0) {
|
|
4170
|
+
html += '<div class="empty">No models match the current filters.</div>';
|
|
4171
|
+
}
|
|
4172
|
+
for (var vi = 0; vi < vendorOrder.length; vi++) {
|
|
4173
|
+
var vendor = vendorOrder[vi];
|
|
4174
|
+
var vendorRows = byVendor[vendor];
|
|
4175
|
+
var byProduct = {};
|
|
4176
|
+
var productOrder = [];
|
|
4177
|
+
for (var pr2 = 0; pr2 < vendorRows.length; pr2++) {
|
|
4178
|
+
var prod = vendorRows[pr2].product;
|
|
4179
|
+
if (!byProduct[prod]) { byProduct[prod] = []; productOrder.push(prod); }
|
|
4180
|
+
byProduct[prod].push(vendorRows[pr2]);
|
|
4181
|
+
}
|
|
4182
|
+
productOrder.sort();
|
|
4183
|
+
html += '<div class="sect-h">' + escHtml(vendor) + '</div><div class="grid">';
|
|
4184
|
+
for (var pi2 = 0; pi2 < productOrder.length; pi2++) {
|
|
4185
|
+
var product = productOrder[pi2];
|
|
4186
|
+
var productRoutes = byProduct[product];
|
|
4187
|
+
var modelRef = vendor + "/" + product;
|
|
4188
|
+
html += cardOpen(modelRef, product, "");
|
|
4189
|
+
for (var ri = 0; ri < productRoutes.length; ri++) {
|
|
4190
|
+
var route = productRoutes[ri];
|
|
4191
|
+
var priceStr = route.pricing ? "$" + route.pricing.inPer1M + "/$" + route.pricing.outPer1M + " per 1M" : "";
|
|
4192
|
+
var elig = (route.eligibleProfiles || []).map(function (id) {
|
|
4193
|
+
return '<a class="link" data-goto="wallets/' + encodeURIComponent(id) + '">' + escHtml(id) + '</a>';
|
|
4194
|
+
}).join(" ");
|
|
4195
|
+
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>' : ""));
|
|
4196
|
+
if (priceStr) html += row("Price", escHtml(priceStr));
|
|
4197
|
+
if (route.contextWindow) html += row("Context", escHtml(route.contextWindow) + (route.maxOutput ? " / " + escHtml(route.maxOutput) + " out" : ""));
|
|
4198
|
+
if (elig) html += '<dt>Wallets</dt><dd>' + elig + '</dd>';
|
|
4199
|
+
}
|
|
4200
|
+
html += cardClose();
|
|
4201
|
+
}
|
|
4202
|
+
html += '</div>';
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
html += '<div class="sect-h">Wallet × model matrix</div>' + walletModelMatrixHtml(routes, profiles);
|
|
4206
|
+
|
|
4207
|
+
el.innerHTML = html;
|
|
4208
|
+
wireGotoLinks(el);
|
|
4209
|
+
wireModelMatrix(el, routes, profiles);
|
|
4210
|
+
renderTitlerModelCard();
|
|
4211
|
+
|
|
4212
|
+
var runnableEl = document.getElementById("runnable-only");
|
|
4213
|
+
if (runnableEl) runnableEl.addEventListener("change", function (e) { modelsState.runnableOnly = e.target.checked; renderModels(); });
|
|
4214
|
+
var walletEl = document.getElementById("wallet-filter");
|
|
4215
|
+
if (walletEl) walletEl.addEventListener("change", function (e) { modelsState.walletFilter = e.target.value; renderModels(); });
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
function walletModelMatrixHtml(routes, profiles) {
|
|
4219
|
+
if (profiles.length === 0) return '<div class="empty">No wallets configured.</div>';
|
|
4220
|
+
var eligibleRoutes = routes.filter(function (r) { return r.eligibleProfiles && r.eligibleProfiles.length > 0; }).slice(0, 40);
|
|
4221
|
+
if (eligibleRoutes.length === 0) return '<div class="empty">No wallet-reachable models yet.</div>';
|
|
4222
|
+
var html = '<div class="card"><div class="body" style="overflow-x:auto"><table><thead><tr><th>Wallet</th>';
|
|
4223
|
+
for (var c = 0; c < eligibleRoutes.length; c++) html += '<th title="' + escHtml(eligibleRoutes[c].route) + '">' + escHtml(eligibleRoutes[c].product) + '</th>';
|
|
4224
|
+
html += '</tr></thead><tbody>';
|
|
4225
|
+
for (var i = 0; i < profiles.length; i++) {
|
|
4226
|
+
var p = profiles[i];
|
|
4227
|
+
html += '<tr><td>' + escHtml(p.label || p.id) + '</td>';
|
|
4228
|
+
for (var j = 0; j < eligibleRoutes.length; j++) {
|
|
4229
|
+
var route = eligibleRoutes[j];
|
|
4230
|
+
var modelId = route.ref || (route.vendor + "/" + route.product);
|
|
4231
|
+
var reachable = route.eligibleProfiles.indexOf(p.id) !== -1;
|
|
4232
|
+
if (!reachable) {
|
|
4233
|
+
html += '<td class="matrix-cell disabled">–</td>';
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
var mode = (p.models && p.models.mode) || "all";
|
|
4237
|
+
var checked = mode === "all" || (p.models.ids || []).indexOf(modelId) !== -1;
|
|
4238
|
+
html += '<td class="matrix-cell" data-matrix-profile="' + escHtml(p.id) + '" data-matrix-model="' + escHtml(modelId) + '" data-matrix-mode="' + escHtml(mode) + '">' + (checked ? "✓" : "–") + '</td>';
|
|
4239
|
+
}
|
|
4240
|
+
html += '</tr>';
|
|
4241
|
+
}
|
|
4242
|
+
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>';
|
|
4243
|
+
return html;
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
function wireModelMatrix(container, routes, profiles) {
|
|
4247
|
+
var cells = container.querySelectorAll("[data-matrix-profile]");
|
|
4248
|
+
for (var i = 0; i < cells.length; i++) {
|
|
4249
|
+
(function (cell) {
|
|
4250
|
+
cell.addEventListener("click", function () {
|
|
4251
|
+
var id = cell.getAttribute("data-matrix-profile");
|
|
4252
|
+
var modelId = cell.getAttribute("data-matrix-model");
|
|
4253
|
+
var p = null;
|
|
4254
|
+
for (var k = 0; k < profiles.length; k++) if (profiles[k].id === id) p = profiles[k];
|
|
4255
|
+
if (!p) return;
|
|
4256
|
+
var mode = (p.models && p.models.mode) || "all";
|
|
4257
|
+
var currentIds = (p.models && p.models.ids) || [];
|
|
4258
|
+
var nextIds;
|
|
4259
|
+
if (mode === "all") {
|
|
4260
|
+
// Start curating from just this one model.
|
|
4261
|
+
nextIds = [modelId];
|
|
4262
|
+
} else if (currentIds.indexOf(modelId) !== -1) {
|
|
4263
|
+
nextIds = currentIds.filter(function (x) { return x !== modelId; });
|
|
4264
|
+
} else {
|
|
4265
|
+
nextIds = currentIds.concat([modelId]);
|
|
4266
|
+
}
|
|
4267
|
+
loadTool("auth_profile_set_models", { id: id, mode: "allow", ids: nextIds }).then(function (r) {
|
|
4268
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4269
|
+
for (var k2 = 0; k2 < profiles.length; k2++) if (profiles[k2].id === id) profiles[k2] = r.data.profile;
|
|
4270
|
+
if (modelsState.profilesResult && modelsState.profilesResult.ok) {
|
|
4271
|
+
for (var k3 = 0; k3 < modelsState.profilesResult.data.profiles.length; k3++) {
|
|
4272
|
+
if (modelsState.profilesResult.data.profiles[k3].id === id) modelsState.profilesResult.data.profiles[k3] = r.data.profile;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
renderModels();
|
|
4276
|
+
});
|
|
4277
|
+
});
|
|
4278
|
+
})(cells[i]);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function renderTitlerModelCard() {
|
|
4283
|
+
var host = document.getElementById("titler-model-card");
|
|
4284
|
+
if (!host) return;
|
|
4285
|
+
var tr = modelsState.titlerResult;
|
|
4286
|
+
if (!tr.ok && tr.unknown) {
|
|
4287
|
+
host.innerHTML = '<div class="muted-note">Editing titler.model needs a newer agentproto daemon (config_get).</div>';
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
if (!tr.ok) {
|
|
4291
|
+
host.innerHTML = '<div class="muted-note">config_get: ' + escHtml(tr.message) + '</div>';
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
var rows = tr.data.keys || [];
|
|
4295
|
+
var rowsByPath = {};
|
|
4296
|
+
for (var i = 0; i < rows.length; i++) rowsByPath[rows[i].path] = rows[i];
|
|
4297
|
+
host.innerHTML = '<div class="card"><div class="body"><dl>' + rows.map(configFieldHtml).join("") + '</dl></div></div>';
|
|
4298
|
+
wireConfigRows(host, rowsByPath, function () {
|
|
4299
|
+
loadTool("config_get", { keys: ["titler.model"] }).then(function (r) {
|
|
4300
|
+
if (!r.ok) { toast("config_get: " + r.message, true); return; }
|
|
4301
|
+
if (r.data.revision) configRevision = r.data.revision;
|
|
4302
|
+
modelsState.titlerResult = r;
|
|
4303
|
+
notePendingRestart(r.data.keys);
|
|
4304
|
+
renderTitlerModelCard();
|
|
4305
|
+
});
|
|
4306
|
+
}, function () {
|
|
4307
|
+
loadModels();
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
// ============================================================
|
|
4312
|
+
// Defaults & messaging
|
|
4313
|
+
// ============================================================
|
|
4314
|
+
|
|
4315
|
+
var DEFAULTS_GROUPS = [
|
|
4316
|
+
{
|
|
4317
|
+
id: "messaging", title: "Messaging and prompts",
|
|
4318
|
+
match: function (p) {
|
|
4319
|
+
return p.indexOf("defaults.messaging.") === 0 || p === "defaults.agentPromptInterrupt"
|
|
4320
|
+
|| p === "defaults.backgroundTaskWake.enabled" || p === "defaults.backgroundTaskWake.graceMs";
|
|
4321
|
+
},
|
|
4322
|
+
},
|
|
4323
|
+
{
|
|
4324
|
+
id: "spawn", title: "Spawn",
|
|
4325
|
+
match: function (p) {
|
|
4326
|
+
return p.indexOf("spawn.") === 0 || p.indexOf("worktrees.") === 0 || p.indexOf("defaults.spawn.") === 0
|
|
4327
|
+
|| p === "defaults.skills" || p === "defaults.options" || p === "defaults.defaultRoleDepthCutoff"
|
|
4328
|
+
|| p === "defaults.maxGrantableDelegation" || p === "provenance.wrapGh" || p === "agentsMd.inlineMaxKb";
|
|
4329
|
+
},
|
|
4330
|
+
},
|
|
4331
|
+
{ id: "context-continuity", title: "Context continuity", match: function (p) { return p === "defaults.contextContinuity"; } },
|
|
4332
|
+
{
|
|
4333
|
+
id: "observability", title: "Observability",
|
|
4334
|
+
match: function (p) {
|
|
4335
|
+
return p.indexOf("titler.") === 0 || p === "defaults.langfuseTracing" || p === "defaults.traceRedactor"
|
|
4336
|
+
|| p === "sessions.attentionDelaySec" || p === "defaults.mcp.deferredTools";
|
|
4337
|
+
},
|
|
4338
|
+
},
|
|
4339
|
+
{
|
|
4340
|
+
id: "daemon", title: "Daemon",
|
|
4341
|
+
match: function (p) {
|
|
4342
|
+
return p.indexOf("daemon.") === 0 || p === "sessions.eventsDir" || p === "features.pty" || p === "features.llmEndpoint"
|
|
4343
|
+
|| p === "profiles" || p === "activeProfile";
|
|
4344
|
+
},
|
|
4345
|
+
},
|
|
4346
|
+
];
|
|
4347
|
+
|
|
4348
|
+
function classifyDefaultsGroup(path) {
|
|
4349
|
+
for (var i = 0; i < DEFAULTS_GROUPS.length; i++) if (DEFAULTS_GROUPS[i].match(path)) return DEFAULTS_GROUPS[i];
|
|
4350
|
+
return { id: "other", title: "Other" };
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
var defaultsState = { rowsByPath: {}, healthFallback: null };
|
|
4354
|
+
|
|
4355
|
+
function loadDefaults() {
|
|
4356
|
+
return Promise.all([
|
|
4357
|
+
loadTool("config_get", { section: "defaults" }),
|
|
4358
|
+
loadTool("config_get", { section: "daemon" }),
|
|
4359
|
+
loadTool("daemon_health", {}),
|
|
4360
|
+
]).then(function (results) {
|
|
4361
|
+
renderDefaults(results[0], results[1], results[2]);
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
function renderDefaults(defR, daemonR, healthR) {
|
|
4366
|
+
var el = document.getElementById("sec-defaults");
|
|
4367
|
+
|
|
4368
|
+
if (!defR.ok && defR.unknown) {
|
|
4369
|
+
// Feature-detect fallback for a daemon that predates config_get: the
|
|
4370
|
+
// five effective knobs daemon_health already reports, read-only.
|
|
4371
|
+
var html = '<div class="notice">Full defaults editor needs a newer agentproto daemon (config_get). Showing the knobs daemon_health reports.</div>';
|
|
4372
|
+
if (healthR.ok) {
|
|
4373
|
+
var hd = healthR.data;
|
|
4374
|
+
var restartBadge = ' <span class="tag">restart</span>';
|
|
4375
|
+
html += '<div class="card" data-card-id="daemon"><div class="title"><span>Daemon</span>'
|
|
4376
|
+
+ (hd.version ? '<span class="aside">' + escHtml(hd.version) + '</span>' : '') + '</div><div class="body"><dl>'
|
|
4377
|
+
+ row("Resume sessions on boot", (hd.resumeSessionsOnBoot ? "Yes" : "No") + restartBadge)
|
|
4378
|
+
+ row("Idle reap after", (hd.idleReapAfterMs ? formatMs(hd.idleReapAfterMs) : "off") + restartBadge)
|
|
4379
|
+
+ row("Crash detect interval", (hd.crashDetectIntervalMs ? formatMs(hd.crashDetectIntervalMs) : "off") + restartBadge)
|
|
4380
|
+
+ row("Restart sweep interval", (hd.restartSweepIntervalMs ? formatMs(hd.restartSweepIntervalMs) : "off") + restartBadge)
|
|
4381
|
+
+ row("Turn stall after", (hd.turnStallAfterMs ? formatMs(hd.turnStallAfterMs) : "off") + restartBadge)
|
|
4382
|
+
+ (hd.build && hd.build.sha ? row("Build", escHtml(hd.build.sha)) : "")
|
|
4383
|
+
+ '</dl></div></div>';
|
|
4384
|
+
} else {
|
|
4385
|
+
html += '<div class="muted-note">daemon_health: ' + escHtml(healthR.message) + '</div>';
|
|
4386
|
+
}
|
|
4387
|
+
el.innerHTML = html;
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
if (!defR.ok) {
|
|
4391
|
+
el.innerHTML = '<div class="empty">config_get: ' + escHtml(defR.message) + '</div>';
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
if (defR.data.revision) configRevision = defR.data.revision;
|
|
4396
|
+
var rows = (defR.data.keys || []).concat(daemonR.ok ? (daemonR.data.keys || []) : []);
|
|
4397
|
+
notePendingRestart(rows);
|
|
4398
|
+
defaultsState.rowsByPath = {};
|
|
4399
|
+
for (var i = 0; i < rows.length; i++) defaultsState.rowsByPath[rows[i].path] = rows[i];
|
|
4400
|
+
|
|
4401
|
+
el.innerHTML = renderDefaultsGroupsHtml();
|
|
4402
|
+
wireConfigRows(el, defaultsState.rowsByPath, function (path) {
|
|
4403
|
+
refreshDefaultsGroupFor(path);
|
|
4404
|
+
}, function () {
|
|
4405
|
+
loadDefaults();
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
function objectValues(obj) {
|
|
4410
|
+
var out = [];
|
|
4411
|
+
for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) out.push(obj[k]);
|
|
4412
|
+
return out;
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
function renderDefaultsGroupsHtml() {
|
|
4416
|
+
var byId = {};
|
|
4417
|
+
var order = [];
|
|
4418
|
+
var rows = objectValues(defaultsState.rowsByPath);
|
|
4419
|
+
for (var i = 0; i < rows.length; i++) {
|
|
4420
|
+
var g = classifyDefaultsGroup(rows[i].path);
|
|
4421
|
+
if (!byId[g.id]) { byId[g.id] = { id: g.id, title: g.title, rows: [] }; order.push(g.id); }
|
|
4422
|
+
byId[g.id].rows.push(rows[i]);
|
|
4423
|
+
}
|
|
4424
|
+
var html = "";
|
|
4425
|
+
for (var j = 0; j < order.length; j++) {
|
|
4426
|
+
var group = byId[order[j]];
|
|
4427
|
+
html += '<div class="sect-h">' + escHtml(group.title) + '</div>' + buildConfigCardHtml(group.id, group.title, group.rows);
|
|
4428
|
+
}
|
|
4429
|
+
return html;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
function refreshDefaultsGroupFor(path) {
|
|
4433
|
+
var group = classifyDefaultsGroup(path);
|
|
4434
|
+
var groupPaths = [];
|
|
4435
|
+
for (var k in defaultsState.rowsByPath) {
|
|
4436
|
+
if (Object.prototype.hasOwnProperty.call(defaultsState.rowsByPath, k) && classifyDefaultsGroup(k).id === group.id) groupPaths.push(k);
|
|
4437
|
+
}
|
|
4438
|
+
loadTool("config_get", { keys: groupPaths }).then(function (r) {
|
|
4439
|
+
if (!r.ok) { toast("config_get: " + r.message, true); return; }
|
|
4440
|
+
if (r.data.revision) configRevision = r.data.revision;
|
|
4441
|
+
var newRows = r.data.keys || [];
|
|
4442
|
+
for (var i = 0; i < newRows.length; i++) defaultsState.rowsByPath[newRows[i].path] = newRows[i];
|
|
4443
|
+
notePendingRestart(newRows);
|
|
4444
|
+
var el = document.getElementById("sec-defaults");
|
|
4445
|
+
var cardEl = el.querySelector('[data-card-id="' + cssEscape(group.id) + '"]');
|
|
4446
|
+
if (!cardEl) return;
|
|
4447
|
+
cardEl.outerHTML = buildConfigCardHtml(group.id, group.title, newRows);
|
|
4448
|
+
var freshEl = el.querySelector('[data-card-id="' + cssEscape(group.id) + '"]');
|
|
4449
|
+
wireConfigRows(freshEl, defaultsState.rowsByPath, function (p2) { refreshDefaultsGroupFor(p2); }, function () { loadDefaults(); });
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
// ============================================================
|
|
4454
|
+
// Remote & pairing
|
|
4455
|
+
// ============================================================
|
|
4456
|
+
|
|
4457
|
+
// Held ONLY in memory, never in section state that survives a re-render
|
|
4458
|
+
// across sections, never in updateModelContext. Cleared on navigating away
|
|
4459
|
+
// from #remote (route()) and on host teardown (see boot, below).
|
|
4460
|
+
var remoteReveal = null;
|
|
4461
|
+
var pairReveal = null;
|
|
4462
|
+
var pairCountdownTimer = null;
|
|
4463
|
+
|
|
4464
|
+
function clearPairCountdown() {
|
|
4465
|
+
if (pairCountdownTimer) { clearInterval(pairCountdownTimer); pairCountdownTimer = null; }
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
function startPairCountdown(expiresAtIso) {
|
|
4469
|
+
clearPairCountdown();
|
|
4470
|
+
pairCountdownTimer = setInterval(function () {
|
|
4471
|
+
var el = document.getElementById("pair-offer-countdown");
|
|
4472
|
+
if (!el) { clearPairCountdown(); return; }
|
|
4473
|
+
var ms = new Date(expiresAtIso).getTime() - Date.now();
|
|
4474
|
+
if (ms <= 0) { el.textContent = "expired"; clearPairCountdown(); return; }
|
|
4475
|
+
el.textContent = Math.ceil(ms / 1000) + "s remaining";
|
|
4476
|
+
}, 1000);
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
function loadRemote() {
|
|
4480
|
+
return Promise.all([
|
|
4481
|
+
loadTool("remote_status", {}),
|
|
4482
|
+
loadTool("pair_list", {}),
|
|
4483
|
+
loadTool("tunnel_list", {}),
|
|
4484
|
+
]).then(function (results) {
|
|
4485
|
+
renderRemote(results[0], results[1], results[2]);
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
function renderRemote(remoteR, pairR, tunnelR) {
|
|
4490
|
+
var el = document.getElementById("sec-remote");
|
|
4491
|
+
var html = '<div class="sect-h">Remote gateway</div>';
|
|
4492
|
+
if (remoteR.ok) {
|
|
4493
|
+
var rs = remoteR.data;
|
|
4494
|
+
html += '<div class="card" data-card-id="remote"><div class="body"><dl>'
|
|
4495
|
+
+ row("Enabled", rs.enabled ? "Yes" : "No")
|
|
4496
|
+
+ (rs.provider ? row("Provider", escHtml(rs.provider)) : "")
|
|
4497
|
+
+ (rs.publicUrl ? row("Public URL", escHtml(rs.publicUrl)) : "")
|
|
4498
|
+
+ (rs.pid != null ? row("PID", String(rs.pid)) : "")
|
|
4499
|
+
+ (rs.createdAt ? row("Created", fmtRelative(rs.createdAt)) : "")
|
|
4500
|
+
+ (rs.lastError ? row("Last error", escHtml(rs.lastError)) : "")
|
|
4501
|
+
+ '</dl>'
|
|
4502
|
+
+ '<div class="wallet-card-actions">'
|
|
4503
|
+
+ (rs.enabled
|
|
4504
|
+
? '<button type="button" class="btn-sm danger" id="remote-disable-btn">disable</button>'
|
|
4505
|
+
: '<button type="button" class="btn-sm primary" id="remote-enable-btn">enable</button>')
|
|
4506
|
+
+ '</div>';
|
|
4507
|
+
if (remoteReveal) {
|
|
4508
|
+
html += '<div class="reveal-box">'
|
|
4509
|
+
+ '<div>Shown once. Store it now, it will not be shown again.</div>'
|
|
4510
|
+
+ (remoteReveal.bearerToken ? '<div>Bearer token<code>' + escHtml(remoteReveal.bearerToken) + '</code><button type="button" class="btn-sm" data-copy="bearer">copy token</button></div>' : '')
|
|
4511
|
+
+ (remoteReveal.phoneUrl ? '<div>Phone link<code>' + escHtml(remoteReveal.phoneUrl) + '</code><button type="button" class="btn-sm" data-copy="phoneUrl">copy link</button></div>' : '')
|
|
4512
|
+
+ (remoteReveal.mcpConfigSnippet ? '<div>.mcp.json<code>' + escHtml(remoteReveal.mcpConfigSnippet) + '</code><button type="button" class="btn-sm" data-copy="snippet">copy snippet</button></div>' : '')
|
|
4513
|
+
+ '</div>';
|
|
4514
|
+
}
|
|
4515
|
+
html += '</div></div>';
|
|
4516
|
+
} else {
|
|
4517
|
+
html += '<div class="muted-note">remote_status: ' + escHtml(remoteR.message) + '</div>';
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
html += '<div class="sect-h">Pairing<button type="button" class="btn-sm" id="pair-offer-btn">+ new pairing</button></div>';
|
|
4521
|
+
if (pairReveal) {
|
|
4522
|
+
html += '<div class="reveal-box">'
|
|
4523
|
+
+ '<div>Offer URL. Shown once, expires <span id="pair-offer-countdown"></span>.</div>'
|
|
4524
|
+
+ '<code>' + escHtml(pairReveal.url) + '</code>'
|
|
4525
|
+
+ '<button type="button" class="btn-sm" data-copy="pairOffer">copy URL</button>'
|
|
4526
|
+
+ '<div class="muted">fingerprint ' + escHtml(pairReveal.fingerprint) + '</div>'
|
|
4527
|
+
+ '</div>';
|
|
4528
|
+
}
|
|
4529
|
+
if (pairR.ok) {
|
|
4530
|
+
var pairings = (pairR.data && pairR.data.pairings) || [];
|
|
4531
|
+
html += '<div class="card" data-card-id="pairing"><div class="body">';
|
|
4532
|
+
if (pairings.length === 0) {
|
|
4533
|
+
html += '<div class="empty">No paired clients.</div>';
|
|
4534
|
+
} else {
|
|
4535
|
+
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>';
|
|
4536
|
+
for (var i = 0; i < pairings.length; i++) {
|
|
4537
|
+
var p = pairings[i];
|
|
4538
|
+
html += '<tr data-row-id="' + escHtml(p.fingerprint) + '"><td>' + escHtml(p.name) + '</td><td>' + escHtml(p.fingerprint) + '</td>'
|
|
4539
|
+
+ '<td>' + escHtml(fmtRelative(p.createdAt)) + '</td><td>' + escHtml(fmtRelative(p.lastSeen)) + '</td><td>' + escHtml(p.rendezvous || "") + '</td>'
|
|
4540
|
+
+ '<td><button type="button" class="btn-sm danger" data-pair-revoke="' + escHtml(p.fingerprint) + '">revoke</button></td></tr>';
|
|
4541
|
+
}
|
|
4542
|
+
html += '</tbody></table>';
|
|
4543
|
+
}
|
|
4544
|
+
html += '</div></div>';
|
|
4545
|
+
} else {
|
|
4546
|
+
html += '<div class="muted-note">pair_list: ' + escHtml(pairR.message) + '</div>';
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
html += '<div class="sect-h">Tunnels</div>';
|
|
4550
|
+
if (tunnelR.ok) {
|
|
4551
|
+
var tunnels = (tunnelR.data && tunnelR.data.tunnels) || [];
|
|
4552
|
+
if (tunnels.length === 0) {
|
|
4553
|
+
html += '<div class="empty">No tunnels tracked by this daemon.</div>';
|
|
4554
|
+
} else {
|
|
4555
|
+
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>';
|
|
4556
|
+
for (var t = 0; t < tunnels.length; t++) {
|
|
4557
|
+
var tn = tunnels[t];
|
|
4558
|
+
html += '<tr><td>' + escHtml(tn.label || tn.name) + '</td><td>' + escHtml(tn.provider) + '</td><td>' + escHtml(String(tn.targetPort)) + '</td>'
|
|
4559
|
+
+ '<td>' + escHtml(tn.publicUrl || "") + '</td><td>' + escHtml(tn.status) + '</td></tr>';
|
|
4560
|
+
}
|
|
4561
|
+
html += '</tbody></table></div></div>';
|
|
4562
|
+
}
|
|
4563
|
+
} else {
|
|
4564
|
+
html += '<div class="muted-note">tunnel_list: ' + escHtml(tunnelR.message) + '</div>';
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
el.innerHTML = html;
|
|
4568
|
+
wireRemote(el);
|
|
4569
|
+
if (remoteReveal) {
|
|
4570
|
+
wireCopyButtons(el, { bearer: remoteReveal.bearerToken, phoneUrl: remoteReveal.phoneUrl, snippet: remoteReveal.mcpConfigSnippet });
|
|
4571
|
+
}
|
|
4572
|
+
if (pairReveal) {
|
|
4573
|
+
wireCopyButtons(el, { pairOffer: pairReveal.url });
|
|
4574
|
+
startPairCountdown(pairReveal.expiresAt);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
function wireRemote(container) {
|
|
4579
|
+
var enableBtn = container.querySelector("#remote-enable-btn");
|
|
4580
|
+
if (enableBtn) {
|
|
4581
|
+
enableBtn.addEventListener("click", function () {
|
|
4582
|
+
confirmClick("remote-enable", enableBtn, function () {
|
|
4583
|
+
loadTool("remote_enable", {}).then(function (r) {
|
|
4584
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4585
|
+
remoteReveal = { bearerToken: r.data.bearerToken, phoneUrl: r.data.phoneUrl, mcpConfigSnippet: r.data.mcpConfigSnippet };
|
|
4586
|
+
toast("Remote enabled.", false);
|
|
4587
|
+
loadRemote();
|
|
4588
|
+
});
|
|
4589
|
+
});
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
var disableBtn = container.querySelector("#remote-disable-btn");
|
|
4593
|
+
if (disableBtn) {
|
|
4594
|
+
disableBtn.addEventListener("click", function () {
|
|
4595
|
+
confirmClick("remote-disable", disableBtn, function () {
|
|
4596
|
+
loadTool("remote_disable", {}).then(function (r) {
|
|
4597
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4598
|
+
remoteReveal = null;
|
|
4599
|
+
toast("Remote disabled.", false);
|
|
4600
|
+
loadRemote();
|
|
4601
|
+
});
|
|
4602
|
+
});
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
var offerBtn = container.querySelector("#pair-offer-btn");
|
|
4606
|
+
if (offerBtn) {
|
|
4607
|
+
offerBtn.addEventListener("click", function () {
|
|
4608
|
+
loadTool("pair_offer", {}).then(function (r) {
|
|
4609
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4610
|
+
pairReveal = { url: r.data.url, fingerprint: r.data.fingerprint, expiresAt: r.data.expiresAt };
|
|
4611
|
+
toast("Pairing offer created.", false);
|
|
4612
|
+
loadRemote();
|
|
4613
|
+
});
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
var revokeBtns = container.querySelectorAll("[data-pair-revoke]");
|
|
4617
|
+
for (var i = 0; i < revokeBtns.length; i++) {
|
|
4618
|
+
(function (btn) {
|
|
4619
|
+
var fp = btn.getAttribute("data-pair-revoke");
|
|
4620
|
+
btn.addEventListener("click", function () {
|
|
4621
|
+
confirmClick("pair-revoke-" + fp, btn, function () {
|
|
4622
|
+
loadTool("pair_revoke", { target: fp }).then(function (r) {
|
|
4623
|
+
if (!r.ok) { toast(r.message, true); return; }
|
|
4624
|
+
toast("Pairing revoked.", false);
|
|
4625
|
+
pairReveal = null;
|
|
4626
|
+
loadRemote();
|
|
4627
|
+
});
|
|
4628
|
+
});
|
|
4629
|
+
});
|
|
4630
|
+
})(revokeBtns[i]);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
// ============================================================
|
|
4635
|
+
// Advanced
|
|
4636
|
+
// ============================================================
|
|
4637
|
+
|
|
4638
|
+
function loadAdvanced() {
|
|
4639
|
+
return Promise.all([
|
|
4640
|
+
loadTool("config_get", {}),
|
|
4641
|
+
loadTool("daemon_health", {}),
|
|
4642
|
+
]).then(function (results) {
|
|
4643
|
+
renderAdvanced(results[0], results[1]);
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function renderAdvanced(configR, healthR) {
|
|
4648
|
+
var el = document.getElementById("sec-advanced");
|
|
4649
|
+
var html = '<div class="sect-h">Raw config dump</div>';
|
|
4650
|
+
if (configR.ok) {
|
|
4651
|
+
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>';
|
|
4652
|
+
} else if (configR.unknown) {
|
|
4653
|
+
html += '<div class="notice">Full config dump needs a newer agentproto daemon (config_get). See Defaults for the knobs daemon_health reports.</div>';
|
|
4654
|
+
} else {
|
|
4655
|
+
html += '<div class="muted-note">config_get: ' + escHtml(configR.message) + '</div>';
|
|
4656
|
+
}
|
|
4657
|
+
html += '<div class="sect-h">Daemon health</div>';
|
|
4658
|
+
if (healthR.ok) {
|
|
4659
|
+
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>';
|
|
4660
|
+
} else {
|
|
4661
|
+
html += '<div class="muted-note">daemon_health: ' + escHtml(healthR.message) + '</div>';
|
|
4662
|
+
}
|
|
4663
|
+
el.innerHTML = html;
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4666
|
+
// ============================================================
|
|
4667
|
+
// boot
|
|
4668
|
+
// ============================================================
|
|
4669
|
+
|
|
4670
|
+
function loadHealthChip() {
|
|
4671
|
+
return loadTool("daemon_health", {}).then(function (r) {
|
|
4672
|
+
var chip = document.getElementById("daemon-chip");
|
|
4673
|
+
if (r.ok) {
|
|
4674
|
+
chip.className = "chip ok";
|
|
4675
|
+
var v = r.data.version || (r.data.build && r.data.build.sha) || r.data.status || "ok";
|
|
4676
|
+
document.getElementById("daemon-txt").textContent = "daemon " + String(v).slice(0, 16);
|
|
4677
|
+
} else {
|
|
4678
|
+
chip.className = "chip bad";
|
|
4679
|
+
document.getElementById("daemon-txt").textContent = "daemon unreachable";
|
|
4680
|
+
}
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4684
|
+
document.getElementById("refresh-btn").addEventListener("click", function () {
|
|
4685
|
+
loadHealthChip();
|
|
4686
|
+
reloadCurrent().catch(function (e) { toast(String(e && e.message || e), true); });
|
|
4687
|
+
});
|
|
4688
|
+
|
|
4689
|
+
window.McpApp.connect()
|
|
4690
|
+
.then(function (bridge) {
|
|
4691
|
+
callTool = bridge.callTool;
|
|
4692
|
+
updateModelContext = bridge.updateModelContext;
|
|
4693
|
+
if (bridge.onTeardown) {
|
|
4694
|
+
onTeardownFn = bridge.onTeardown;
|
|
4695
|
+
onTeardownFn(function () { clearRemoteSecrets(); });
|
|
4696
|
+
}
|
|
4697
|
+
renderNav();
|
|
4698
|
+
loadHealthChip();
|
|
4699
|
+
if (bridge.onToolInput) bridge.onToolInput(handleToolInput);
|
|
4700
|
+
route(true);
|
|
4701
|
+
})
|
|
4702
|
+
.catch(function (err) {
|
|
4703
|
+
document.getElementById("body").innerHTML =
|
|
4704
|
+
'<div class="empty">Standalone mode: no host bridge (' + escHtml(err.message) + ')</div>';
|
|
4705
|
+
});
|
|
4706
|
+
</script>
|
|
4707
|
+
</body>
|
|
4708
|
+
</html>`;
|
|
4709
|
+
|
|
4710
|
+
// src/config/index.ts
|
|
4711
|
+
var configApp = defineApp({
|
|
4712
|
+
id: "@agentproto/config",
|
|
4713
|
+
name: "agentproto config",
|
|
4714
|
+
version: "0.1.0",
|
|
4715
|
+
description: "Daemon-wide configuration: wallets (auth profiles + spend), harnesses, the model catalog, defaults & messaging knobs, remote & pairing, and a raw config dump.",
|
|
4716
|
+
agents: [],
|
|
4717
|
+
ui: {
|
|
4718
|
+
html: CONFIG_HTML,
|
|
4719
|
+
title: "agentproto config",
|
|
4720
|
+
tools: [...CONFIG_TOOLS]
|
|
4721
|
+
}
|
|
4722
|
+
});
|
|
2362
4723
|
var narrator = {
|
|
2363
4724
|
agent: defineAgent({
|
|
2364
4725
|
schema: "agent/v1",
|
|
@@ -2408,7 +4769,7 @@ var narrateSession = defineWorkflow({
|
|
|
2408
4769
|
|
|
2409
4770
|
// src/session-viewer/ui.ts
|
|
2410
4771
|
var SESSION_VIEWER_TOOLS = ["session_list", "conversation_read"];
|
|
2411
|
-
var
|
|
4772
|
+
var APP_ID5 = "@agentproto/session-viewer";
|
|
2412
4773
|
var SESSION_VIEWER_HTML = `<!DOCTYPE html>
|
|
2413
4774
|
<html lang="en">
|
|
2414
4775
|
<head>
|
|
@@ -2465,9 +4826,11 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sa
|
|
|
2465
4826
|
.msg.role-user{align-self:flex-end;align-items:flex-end}
|
|
2466
4827
|
.msg.role-assistant,.msg.role-tool{align-self:flex-start;align-items:flex-start}
|
|
2467
4828
|
.msg.role-system{align-self:center;max-width:90%}
|
|
4829
|
+
.msg.role-message{align-self:flex-start;align-items:flex-start}
|
|
2468
4830
|
.bubble{border-radius:10px;padding:9px 12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
|
|
2469
4831
|
.role-user .bubble{background:var(--blue);color:#04101c;border-bottom-right-radius:3px}
|
|
2470
4832
|
.role-assistant .bubble{background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:3px}
|
|
4833
|
+
.role-message .bubble{background:var(--bg2);border:1px dashed var(--blue);border-bottom-left-radius:3px}
|
|
2471
4834
|
.role-system .bubble{background:transparent;border:1px dashed var(--border);color:var(--text2);font-size:11.5px;text-align:center}
|
|
2472
4835
|
.mrow{display:flex;align-items:center;gap:6px;margin-bottom:3px}
|
|
2473
4836
|
.mrole{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text3)}
|
|
@@ -2510,7 +4873,7 @@ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sa
|
|
|
2510
4873
|
</div>
|
|
2511
4874
|
<script>
|
|
2512
4875
|
var callTool = null;
|
|
2513
|
-
var APP_ID = ${JSON.stringify(
|
|
4876
|
+
var APP_ID = ${JSON.stringify(APP_ID5)};
|
|
2514
4877
|
|
|
2515
4878
|
// app_tool_call wraps its dispatch result at least once (its own text-result
|
|
2516
4879
|
// body is the JSON-stringified inner MCP tool result); peel back through
|
|
@@ -2731,6 +5094,14 @@ function renderMessage(m) {
|
|
|
2731
5094
|
if (m.role === "tool") {
|
|
2732
5095
|
return '<div class="msg role-tool">' + renderToolResultChip(m) + "</div>";
|
|
2733
5096
|
}
|
|
5097
|
+
if (m.role === "user" && m.from) {
|
|
5098
|
+
// A typed message from another session (session-message record) \u2014 never
|
|
5099
|
+
// the human: name the daemon-attested sender instead of "You".
|
|
5100
|
+
var who = m.from.label || (m.from.sessionId ? m.from.sessionId.slice(-6) : m.from.relation);
|
|
5101
|
+
return '<div class="msg role-message"><div class="mrow"><span class="mrole">from ' + escHtml(m.from.relation)
|
|
5102
|
+
+ " " + escHtml(who) + " \xB7 " + escHtml(m.from.kind) + "</span>" + time + "</div>"
|
|
5103
|
+
+ '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
|
|
5104
|
+
}
|
|
2734
5105
|
if (m.role === "user") {
|
|
2735
5106
|
return '<div class="msg role-user"><div class="mrow"><span class="mrole">You</span>' + time + "</div>"
|
|
2736
5107
|
+ '<div class="bubble">' + escHtml(m.text || "") + "</div></div>";
|
|
@@ -2893,6 +5264,7 @@ var APPS = [
|
|
|
2893
5264
|
{ slug: "mail-triage", category: "app", handle: mailTriage },
|
|
2894
5265
|
{ slug: "media-viewer", category: "app", handle: mediaViewer },
|
|
2895
5266
|
{ slug: "ops-panel", category: "app", handle: opsPanel },
|
|
5267
|
+
{ slug: "config", category: "app", handle: configApp },
|
|
2896
5268
|
{ slug: "session-viewer", category: "app", handle: sessionViewer }
|
|
2897
5269
|
];
|
|
2898
5270
|
async function syncApps(baseDir) {
|