@jmanuelcorral/openteam 0.11.0 → 0.12.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/cli.js CHANGED
@@ -2040,6 +2040,13 @@ var FAILURE_CAUSE_RULES = [
2040
2040
  "prompt-server"
2041
2041
  ]
2042
2042
  ];
2043
+ var TIMEOUT_NEEDLES = [
2044
+ "timed out",
2045
+ "timeout",
2046
+ "timedout",
2047
+ "etimedout",
2048
+ "deadline exceeded"
2049
+ ];
2043
2050
  function matchFailureCause(lowerReason) {
2044
2051
  for (const [needles, cls] of FAILURE_CAUSE_RULES) {
2045
2052
  if (needles.some((needle) => lowerReason.includes(needle))) {
@@ -2050,7 +2057,7 @@ function matchFailureCause(lowerReason) {
2050
2057
  }
2051
2058
  function classifyFailureForTelemetry(failureReason, failureStage) {
2052
2059
  const lower = failureReason.toLowerCase();
2053
- if (lower.includes("timed out") || lower.includes("timeout")) {
2060
+ if (TIMEOUT_NEEDLES.some((needle) => lower.includes(needle))) {
2054
2061
  return "prompt-timeout";
2055
2062
  }
2056
2063
  if (lower.includes("did not include a session id") || lower.includes("missing session")) {
@@ -3517,91 +3524,80 @@ var RESTART_REQUIRED_KEYS = [
3517
3524
  "console.port"
3518
3525
  ];
3519
3526
 
3527
+ // src/web/graphRoute.ts
3528
+ var GRAPH_DISABLED_REMEDIATION = 'Agent tree is off. In .opencode/openteam.json set graph.mode to a value other than "off" and console.graphView.enabled to true, then restart the console.';
3529
+ var GRAPH_DISABLED_ENVELOPE = {
3530
+ disabled: true,
3531
+ reason: "graph-disabled",
3532
+ remediation: GRAPH_DISABLED_REMEDIATION
3533
+ };
3534
+ var JSON_HEADERS = { "content-type": "application/json" };
3535
+ var GRAPH_SSE_EVENT = "graph";
3536
+ function graphSseMessage(json) {
3537
+ return `event: ${GRAPH_SSE_EVENT}
3538
+ data: ${json}
3539
+
3540
+ `;
3541
+ }
3542
+ function bearerFromHeader(request) {
3543
+ const header = request.headers.authorization;
3544
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
3545
+ return;
3546
+ }
3547
+ const value = header.slice("Bearer ".length).trim();
3548
+ return value.length === 0 ? undefined : value;
3549
+ }
3550
+ function authorized(request, query, token) {
3551
+ const provided = tokenFromQuery(query) ?? bearerFromHeader(request);
3552
+ return provided !== undefined && safeEqualToken(provided, token);
3553
+ }
3554
+ async function handleGraphRoute(request, response, url, deps) {
3555
+ if (url.pathname !== "/api/graph") {
3556
+ return false;
3557
+ }
3558
+ if (request.method !== "GET") {
3559
+ response.writeHead(405, JSON_HEADERS).end(JSON.stringify({ error: "method not allowed" }));
3560
+ return true;
3561
+ }
3562
+ if (deps.remoteToken !== undefined && !authorized(request, url.searchParams, deps.remoteToken)) {
3563
+ response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
3564
+ return true;
3565
+ }
3566
+ const view = await deps.readView();
3567
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify(view));
3568
+ return true;
3569
+ }
3570
+
3520
3571
  // src/console/assets.ts
3521
3572
  var CONSOLE_STYLE = `
3522
- :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
3523
- *{box-sizing:border-box}
3524
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
3525
- header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
3526
- header h1{font-size:18px;margin:0}
3527
- header .meta{color:var(--muted);font-size:12px}
3528
- main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
3529
- .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
3530
- .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
3531
- .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
3532
- .stat{display:flex;flex-direction:column;gap:2px}
3533
- .stat .k{color:var(--muted);font-size:12px}
3534
- .stat .v{font-size:18px;font-weight:600}
3535
- .good{color:var(--good)}
3536
- .local{color:var(--local)}
3537
- .frontier{color:var(--frontier)}
3538
- .muted{color:var(--muted)}
3539
- table{width:100%;border-collapse:collapse;font-size:13px}
3540
- th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
3541
- th{color:var(--muted);font-weight:600}
3542
- .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
3543
- .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
3544
- .progress .bar{height:100%;background:var(--good)}
3545
- ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
3546
- ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
3547
- ul.items li.done{color:var(--muted)}
3548
- ul.items .box{font-family:monospace}
3549
- .who{color:var(--local)}
3550
- .tag{color:var(--frontier);font-size:12px}
3551
- .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
3552
- code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
3553
- .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
3554
- .tabs{display:flex;gap:6px;padding:12px 24px 0;flex-wrap:wrap;border-bottom:1px solid var(--line)}
3555
- .tabs .tab{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-bottom:none;border-radius:8px 8px 0 0;padding:8px 14px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:6px}
3556
- .tabs .tab.active{background:var(--bg);border-color:var(--local);color:var(--fg)}
3557
- .dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--muted)}
3558
- .badge{font-size:11px;padding:1px 8px;border-radius:10px;border:1px solid var(--line);color:var(--muted);vertical-align:middle}
3559
- .status-running{background:var(--good)}
3560
- .badge.status-running{color:var(--good);border-color:var(--good)}
3561
- .status-idle{background:var(--muted)}
3562
- .badge.status-idle{color:var(--muted)}
3563
- .status-waiting-input{background:var(--frontier)}
3564
- .badge.status-waiting-input{color:var(--frontier);border-color:var(--frontier)}
3565
- .status-ended{background:#6e7681}
3566
- .badge.status-ended{color:#6e7681}
3567
- .term .termlog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
3568
- .term .tl{padding:1px 0}
3569
- .term .tl.input{color:var(--local)}
3570
- .term .tl.tool{color:var(--frontier)}
3571
- .term .tl.status{color:var(--muted)}
3572
- .term .tl.error{color:#f85149}
3573
- .term .tl.permission{color:var(--frontier);font-weight:600}
3574
- .term form.terminput{display:flex;gap:8px;margin-top:10px}
3575
- .term form.terminput input{flex:1;background:#0b0d11;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
3576
- .term form.terminput button{background:var(--local);color:#04122b;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600}
3577
- .pty .ptylog{background:#05070a;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap;color:#c8d1dc}
3578
- .pty form.ptyinput{display:flex;gap:8px;margin-top:10px}
3579
- .pty form.ptyinput input{flex:1;background:#05070a;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font:inherit}
3580
- .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
3581
- .pty .pty-start:disabled{opacity:.5;cursor:default}
3582
- .config-panel{grid-column:1/-1}
3583
- .config-form{display:flex;flex-direction:column;gap:12px}
3584
- .config-note{margin:0;font-size:12px;color:var(--muted)}
3585
- .config-note.config-overlay{color:var(--frontier)}
3586
- .reload-legend h3,.config-summary h4{font-size:12px;margin:0 0 6px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
3587
- .reload-table{width:100%}
3588
- .reload-table th{white-space:nowrap;vertical-align:top;width:1%}
3589
- .reload-table th.reload-hot{color:var(--good)}
3590
- .reload-table th.reload-restart{color:var(--frontier)}
3591
- .config-label{font-size:12px;color:var(--muted)}
3592
- .config-json{width:100%;min-height:280px;resize:vertical;background:#0b0d11;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:10px;font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.5}
3593
- .config-actions{display:flex;gap:8px}
3594
- .config-save{background:var(--good);color:#08110a;border:0;border-radius:8px;padding:8px 16px;font-weight:600;cursor:pointer}
3595
- .config-save:disabled{opacity:.5;cursor:default}
3596
- .config-result:empty{display:none}
3597
- .config-summary{border:1px solid var(--line);border-radius:8px;padding:12px;margin-top:4px}
3598
- .config-ok{margin:0 0 8px;color:var(--good);font-weight:600}
3599
- .config-err{margin:0;color:var(--frontier);font-weight:600}
3600
- .config-applied h4{color:var(--good)}
3601
- .config-restart h4{color:var(--frontier)}
3602
- .config-warn{margin:0 0 6px;color:var(--frontier);font-size:13px}
3603
- .config-summary ul{margin:0;padding-left:18px}
3604
- .config-summary li{padding:2px 0;border:0}
3573
+ :root{color-scheme:dark;--bg:#0f0f0f;--surface:#17181b;--surface-2:#1e2024;--panel:var(--surface);--fg:#eceef1;--muted:#a2a8b2;--faint:#6b7079;--border:rgba(255,255,255,.08);--border-strong:rgba(255,255,255,.16);--line:var(--border);--accent:#35c07a;--accent-bg:rgba(53,192,122,.12);--accent-fg:#57d492;--good:var(--accent-fg);--local:#6aa6f5;--frontier:#e0a53a;--warn:#e0a53a;--warn-bg:rgba(224,165,58,.12);--danger:#f0687f;--danger-bg:rgba(240,104,127,.12);--info:#6aa6f5;--info-bg:rgba(106,166,245,.12);--font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;--font-mono:"JetBrains Mono","IBM Plex Mono",ui-monospace,"SFMono-Regular",Consolas,monospace;--shadow-pop:0 8px 28px rgba(0,0,0,.5);--sidebar-w:248px}
3574
+ *{box-sizing:border-box;margin:0;padding:0}
3575
+ html,body{height:100%}
3576
+ body{background:var(--bg);color:var(--fg);font:14px/1.55 var(--font-sans);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-variant-numeric:tabular-nums}
3577
+ .mono,code,.hash{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
3578
+ button{font:inherit;cursor:pointer}
3579
+ button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
3580
+ ::selection{background:var(--accent-bg)}
3581
+ .app{display:grid;grid-template-columns:var(--sidebar-w) 1fr;grid-template-rows:100vh;min-height:100vh}
3582
+ .sidebar{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}
3583
+ .brand{display:flex;align-items:center;gap:10px;padding:18px 18px 16px;border-bottom:1px solid var(--border)}
3584
+ .logo{width:26px;height:26px;border-radius:6px;background:var(--accent);color:#fff;display:grid;place-items:center;flex:none}
3585
+ .logo svg{width:15px;height:15px}.brand .name{font-weight:600;letter-spacing:-.01em}.brand .tag{font:10px/1 var(--font-mono);letter-spacing:.08em;color:var(--accent-fg);background:var(--accent-bg);padding:3px 6px;border-radius:4px;margin-left:auto;text-transform:uppercase}
3586
+ .nav{padding:10px;display:flex;flex-direction:column;gap:2px}.nav-label{font:10px/1 var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding:14px 8px 6px}.nav-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:7px;color:var(--muted);background:transparent;border:0;width:100%;text-align:left;letter-spacing:-.005em;transition:background .12s,color .12s}.nav-item svg{width:17px;height:17px;flex:none}.nav-item .count{margin-left:auto;font:11px/1 var(--font-mono);color:var(--faint)}.nav-item .pill{margin-left:auto}.nav-item:hover{background:var(--surface-2);color:var(--fg)}.nav-item[aria-current="page"]{background:var(--accent-bg);color:var(--accent-fg);font-weight:550}.nav-item[aria-current="page"] .count{color:var(--accent-fg)}
3587
+ .sidebar-foot{margin-top:auto;padding:14px 16px;border-top:1px solid var(--border);font-size:12px;color:var(--muted);display:flex;flex-direction:column;gap:8px}.host-row{display:flex;align-items:center;gap:8px}.dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none;background:var(--muted)}.dot.live,.dot.status-running{background:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}.dot.warn,.dot.status-waiting-input{background:var(--warn)}.dot.down{background:var(--danger)}.dot.status-idle{background:var(--muted)}.dot.status-ended{background:#6e7681}
3588
+ .main{min-width:0;min-height:0;display:flex;flex-direction:column}.topbar{height:56px;flex:none;border-bottom:1px solid var(--border);background:color-mix(in srgb,var(--surface) 82%,transparent);backdrop-filter:blur(8px);display:flex;align-items:center;gap:14px;padding:0 20px}.topbar h1{font-size:15px;font-weight:600;letter-spacing:-.01em}.refresh,.token-chip{display:inline-flex;align-items:center;gap:7px;font:11px/1 var(--font-mono);color:var(--muted);padding:5px 9px;border:1px solid var(--border);border-radius:6px;background:var(--surface)}.refresh .beat{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:beat 2s ease-in-out infinite}@keyframes beat{0%,100%{opacity:1}50%{opacity:.25}}.spacer{margin-left:auto}.icon-btn{width:34px;height:34px;border-radius:7px;border:1px solid var(--border);background:var(--surface);color:var(--muted);display:grid;place-items:center;transition:background .12s,color .12s,border-color .12s}.icon-btn:hover{background:var(--surface-2);color:var(--fg);border-color:var(--border-strong)}.icon-btn svg{width:17px;height:17px}.token-chip b{color:var(--fg);font-weight:600}
3589
+ .scroll{flex:1;overflow-y:auto;min-height:0}.screen{display:none;padding:22px 24px 40px;max-width:1320px}.screen.active{display:block}.screen[hidden]{display:none!important}.screen-head{margin-bottom:18px}.screen-head h2{font-size:19px;font-weight:600;letter-spacing:-.02em;display:flex;align-items:center;gap:8px}.screen-head p{color:var(--muted);font-size:13px;margin-top:3px;max-width:74ch;text-wrap:pretty}.screen-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}.screen-grid .wide,.panel.wide{grid-column:1/-1}
3590
+ .panel,.card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px}.panel h2,.panel h3,.card-head h3{font-size:13px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.card-head{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--border);margin:-16px -16px 14px}.card-head .sub{font:11px/1 var(--font-mono);color:var(--faint);margin-left:auto}.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.stat{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:15px 16px;display:flex;flex-direction:column;gap:2px}.stat .k{font:11px/1 var(--font-mono);letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}.stat .v{font-size:20px;font-weight:600;letter-spacing:-.02em;margin-top:6px}.stat .d{font-size:12px;color:var(--muted);margin-top:4px;display:flex;align-items:center;gap:6px}.good{color:var(--good)}.local{color:var(--local)}.frontier{color:var(--frontier)}.muted{color:var(--muted)}.faint{color:var(--faint)}
3591
+ table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border)}th{font:11px/1 var(--font-mono);letter-spacing:.05em;text-transform:uppercase;color:var(--faint);font-weight:500}tbody tr:last-child td{border-bottom:0}tbody tr:hover{background:var(--surface-2)}td.num{text-align:right;font-family:var(--font-mono)}.hash{color:var(--faint)}
3592
+ .pill,.badge{display:inline-flex;align-items:center;gap:5px;font:11px/1 var(--font-mono);letter-spacing:.02em;padding:3px 8px;border-radius:999px;border:1px solid transparent}.pill.local,.badge.status-running{color:var(--accent-fg);background:var(--accent-bg);border-color:var(--accent-bg)}.pill.frontier{color:var(--info);background:var(--info-bg)}.pill.warn,.badge.status-waiting-input{color:var(--warn);background:var(--warn-bg);border-color:var(--warn-bg)}.pill.danger{color:var(--danger);background:var(--danger-bg)}.pill.idle,.badge.status-idle,.badge.status-ended{color:var(--muted);background:var(--surface-2)}
3593
+ .progress{height:10px;background:var(--surface-2);border-radius:6px;overflow:hidden;border:1px solid var(--border)}.progress .bar{height:100%;background:var(--accent)}ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}ul.items li,ul.timeline li{padding:6px 0;border-bottom:1px solid var(--border)}ul.items li.done{color:var(--muted)}ul.items .box{font-family:var(--font-mono)}.who{color:var(--local)}.tag{color:var(--frontier);font-size:12px}.kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}code{background:var(--bg);padding:1px 5px;border-radius:4px}
3594
+ .logs-layout{display:grid;grid-template-columns:220px 1fr;gap:16px;align-items:start;margin-bottom:16px}.session-list{padding:6px;max-height:70vh;overflow-y:auto}.session-btn{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border-radius:7px;border:0;background:transparent;color:var(--muted);text-align:left;font:12px/1.3 var(--font-mono)}.session-btn:hover{background:var(--surface-2);color:var(--fg)}.session-btn.sel,.session-btn[aria-selected="true"]{background:var(--accent-bg);color:var(--accent-fg)}.session-btn .sid{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-btn .n{margin-left:auto;color:var(--faint)}.session-detail{display:flex;flex-direction:column;gap:16px}.session-detail[hidden]{display:none}.empty-state{padding:28px;text-align:center;color:var(--muted)}
3595
+ .term .termlog,.pty .ptylog{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px;max-height:340px;overflow:auto;font-family:var(--font-mono);font-size:12px;white-space:pre-wrap}.term .tl{padding:1px 0}.term .tl.input{color:var(--local)}.term .tl.tool{color:var(--frontier)}.term .tl.status{color:var(--muted)}.term .tl.error{color:var(--danger)}.term .tl.permission{color:var(--frontier);font-weight:600}.term form.terminput,.pty form.ptyinput{display:flex;gap:8px;margin-top:10px}.term form.terminput input,.pty form.ptyinput input{flex:1;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:8px 10px;font:inherit}.term form.terminput button{background:var(--local);color:#04122b;border:none;border-radius:8px;padding:8px 16px;font:inherit;font-weight:600}.pty .ptylog{background:#05070a;color:#c8d1dc}.pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;font:inherit;font-weight:600;margin-top:8px}.pty .pty-start:disabled{opacity:.5;cursor:default}
3596
+ .config-panel{grid-column:1/-1}.config-form{display:flex;flex-direction:column;gap:12px}.config-note{margin:0;font-size:12px;color:var(--muted)}.config-note.config-overlay{color:var(--frontier)}.reload-legend h3,.config-summary h4{font-size:12px;margin:0 0 6px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.reload-table{width:100%}.reload-table th{white-space:nowrap;vertical-align:top;width:1%}.reload-table th.reload-hot{color:var(--good)}.reload-table th.reload-restart{color:var(--frontier)}.config-label{font-size:12px;color:var(--muted)}.config-json{width:100%;min-height:280px;resize:vertical;background:var(--bg);color:var(--fg);border:1px solid var(--border-strong);border-radius:8px;padding:10px;font-family:var(--font-mono);font-size:12px;line-height:1.5}.config-actions{display:flex;gap:8px}.config-save{background:var(--accent);color:#fff;border:0;border-radius:8px;padding:8px 16px;font-weight:600;cursor:pointer}.config-save:disabled{opacity:.5;cursor:default}.config-result:empty{display:none}.config-summary{border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:4px}.config-ok{margin:0 0 8px;color:var(--good);font-weight:600}.config-err{margin:0;color:var(--frontier);font-weight:600}.config-applied h4{color:var(--good)}.config-restart h4{color:var(--frontier)}.config-warn{margin:0 0 6px;color:var(--frontier);font-size:13px}.config-summary ul{margin:0;padding-left:18px}.config-summary li{padding:2px 0;border:0}
3597
+ .diag-banner{border-bottom:1px solid color-mix(in srgb,var(--warn) 40%,transparent);background:var(--warn-bg);color:var(--warn);padding:10px 20px;font-size:13px}.graph-panel{min-height:240px}.graph-disabled .hint{margin-top:6px}.scrim{display:none}.menu-btn{display:none}
3598
+ @media (max-width:900px){.app{grid-template-columns:1fr}.sidebar{position:fixed;inset:0 auto 0 0;width:280px;z-index:50;transform:translateX(-100%);transition:transform .22s ease}body.nav-open .sidebar{transform:translateX(0);box-shadow:var(--shadow-pop)}body.nav-open .scrim{display:block;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:40}.menu-btn{display:grid}.logs-layout{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.token-chip .lbl{display:none}}
3599
+ @media (max-width:560px){.screen{padding:16px 14px 32px}.screen-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.refresh{display:none}}
3600
+ @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
3605
3601
  `;
3606
3602
  var GRAPH_STYLE = `
3607
3603
  .graph-wrap{overflow:auto;background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:8px}
@@ -3671,24 +3667,69 @@ var CONSOLE_CLIENT_JS = `
3671
3667
  var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
3672
3668
  setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
3673
3669
 
3674
- function selectTab(name){
3675
- var tabs=document.querySelectorAll('#tabbar .tab');
3676
- for(var i=0;i<tabs.length;i++){tabs[i].classList.toggle('active',tabs[i].getAttribute('data-tab')===name)}
3677
- var views=document.querySelectorAll('main .view');
3670
+ function configActive(){
3671
+ var s=document.getElementById('screen-config');
3672
+ return !!(s&&!s.hidden);
3673
+ }
3674
+ function wireLogsScreen(screen){
3675
+ if(screen.__logsWired){return}
3676
+ screen.__logsWired=true;
3677
+ var buttons=screen.querySelectorAll('[data-session-target]');
3678
+ var cards=screen.querySelectorAll('[data-session-view]');
3679
+ function showSession(id){
3680
+ var matched=false;
3681
+ for(var i=0;i<buttons.length;i++){
3682
+ var on=buttons[i].getAttribute('data-session-target')===id;
3683
+ buttons[i].classList.toggle('sel',on);
3684
+ buttons[i].setAttribute('aria-selected',on?'true':'false');
3685
+ }
3686
+ for(var j=0;j<cards.length;j++){
3687
+ var show=cards[j].getAttribute('data-session-view')===id;
3688
+ cards[j].hidden=!show;
3689
+ if(show){matched=true;connectTerminal(cards[j],id)}
3690
+ }
3691
+ return matched;
3692
+ }
3693
+ for(var k=0;k<buttons.length;k++){
3694
+ buttons[k].addEventListener('click',function(e){
3695
+ var id=e.currentTarget.getAttribute('data-session-target');
3696
+ if(id){showSession(id)}
3697
+ });
3698
+ }
3699
+ if(buttons.length>0){
3700
+ showSession(buttons[0].getAttribute('data-session-target'));
3701
+ }
3702
+ }
3703
+ function selectScreen(name){
3704
+ var screens=document.querySelectorAll('.screen');
3705
+ var items=document.querySelectorAll('[data-screen]');
3678
3706
  var matched=false;
3679
- for(var j=0;j<views.length;j++){
3680
- var show=views[j].getAttribute('data-view')===name;
3681
- views[j].hidden=!show;
3682
- if(show){matched=true;if(name==='config'){loadConfig(views[j])}else if(name!=='all'){connectTerminal(views[j],name)}}
3707
+ var titleText='';
3708
+ var activeScreen=null;
3709
+ for(var i=0;i<screens.length;i++){
3710
+ var show=screens[i].id==='screen-'+name;
3711
+ screens[i].classList.toggle('active',show);
3712
+ screens[i].hidden=!show;
3713
+ if(show){matched=true;activeScreen=screens[i]}
3683
3714
  }
3684
3715
  if(!matched){return false}
3685
- try{sessionStorage.setItem('openteam.tab',name)}catch(e){}
3716
+ for(var j=0;j<items.length;j++){
3717
+ var on=items[j].getAttribute('data-screen')===name;
3718
+ if(on){
3719
+ items[j].setAttribute('aria-current','page');
3720
+ titleText=items[j].getAttribute('data-title')||'';
3721
+ }else{
3722
+ items[j].removeAttribute('aria-current');
3723
+ }
3724
+ }
3725
+ var title=document.getElementById('screen-title');
3726
+ if(title){title.textContent=titleText}
3727
+ if(name==='config'&&activeScreen){loadConfig(activeScreen)}
3728
+ if(name==='logs'&&activeScreen){wireLogsScreen(activeScreen)}
3729
+ try{localStorage.setItem('openteam.screen',name)}catch(e){}
3730
+ document.body.classList.remove('nav-open');
3686
3731
  return true;
3687
3732
  }
3688
- function configActive(){
3689
- var v=document.querySelector('main .view[data-view="config"]');
3690
- return !!(v&&!v.hidden);
3691
- }
3692
3733
  function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
3693
3734
  // Config editor (W7-S09 / #193). The config values travel only over the
3694
3735
  // token-gated /api/config, never in this open page, so the browser fetches
@@ -3827,7 +3868,7 @@ var CONSOLE_CLIENT_JS = `
3827
3868
  if(!token){return}
3828
3869
  var drill=document.querySelector('.graph-drill');
3829
3870
  if(!drill){return}
3830
- var gview=drill.closest('.view');
3871
+ var gview=drill.closest('.screen');
3831
3872
  if(!gview){return}
3832
3873
  var log=drill.querySelector('.drilllog');
3833
3874
  var title=drill.querySelector('.drill-title');
@@ -3861,17 +3902,21 @@ var CONSOLE_CLIENT_JS = `
3861
3902
  if(t&&t.getAttribute&&t.getAttribute('data-session')){open(t.getAttribute('data-session'))}
3862
3903
  });
3863
3904
  }
3864
- var bar=document.getElementById('tabbar');
3865
- if(bar){
3866
- bar.addEventListener('click',function(e){
3905
+ var nav=document.querySelector('.nav');
3906
+ if(nav){
3907
+ nav.addEventListener('click',function(e){
3867
3908
  var t=e.target;
3868
- while(t&&t!==bar&&!t.getAttribute('data-tab')){t=t.parentNode}
3869
- if(t&&t.getAttribute('data-tab')){selectTab(t.getAttribute('data-tab'))}
3909
+ while(t&&t!==nav&&!(t.getAttribute&&t.getAttribute('data-screen'))){t=t.parentNode}
3910
+ if(t&&t.getAttribute&&t.getAttribute('data-screen')){selectScreen(t.getAttribute('data-screen'))}
3870
3911
  });
3871
3912
  }
3872
- var saved='all';
3873
- try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
3874
- if(!selectTab(saved)){selectTab('all')}
3913
+ var toggle=document.querySelector('[data-toggle-nav]');
3914
+ if(toggle){toggle.addEventListener('click',function(){document.body.classList.toggle('nav-open')})}
3915
+ var close=document.querySelector('[data-close-nav]');
3916
+ if(close){close.addEventListener('click',function(){document.body.classList.remove('nav-open')})}
3917
+ var saved='tree';
3918
+ try{saved=localStorage.getItem('openteam.screen')||'tree'}catch(e){}
3919
+ if(!selectScreen(saved)){selectScreen('tree')}
3875
3920
  wireGraphDrill();
3876
3921
  })();
3877
3922
  `;
@@ -3901,6 +3946,13 @@ function avgMs(stats) {
3901
3946
  function agentModel(agent) {
3902
3947
  return agent.model === undefined ? "inherits default" : `${agent.model.providerID}/${agent.model.modelID}`;
3903
3948
  }
3949
+ function domIdPart(value) {
3950
+ const cleaned = value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3951
+ return cleaned.length === 0 ? "session" : cleaned;
3952
+ }
3953
+ function panelId(base, idSuffix) {
3954
+ return idSuffix === undefined ? base : `${base}-${idSuffix}`;
3955
+ }
3904
3956
  function summaryPanel(cost) {
3905
3957
  return [
3906
3958
  '<section class="panel" id="panel-summary">',
@@ -3979,10 +4031,11 @@ function teamPanel(team) {
3979
4031
  function routeRow(route) {
3980
4032
  return `<tr><td>${route.tier}</td><td class="${route.routeKind}">${route.routeKind}</td><td>${escapeHtml(route.model)}</td><td class="hash">${escapeHtml(route.promptHash)}</td><td>${usd(route.estimatedCostUSD)}</td><td class="good">${usd(route.estimatedSavingsUSD)}</td></tr>`;
3981
4033
  }
3982
- function routesPanel(routes) {
4034
+ function routesPanel(routes, idSuffix) {
4035
+ const id = panelId("panel-routes", idSuffix);
3983
4036
  if (routes.length === 0) {
3984
4037
  return [
3985
- '<section class="panel" id="panel-routes">',
4038
+ `<section class="panel" id="${id}">`,
3986
4039
  "<h2>Recent decisions</h2>",
3987
4040
  '<p class="muted">No routing decisions yet.</p>',
3988
4041
  "</section>"
@@ -3990,7 +4043,7 @@ function routesPanel(routes) {
3990
4043
  }
3991
4044
  const rows = routes.map(routeRow).join("");
3992
4045
  return [
3993
- '<section class="panel" id="panel-routes">',
4046
+ `<section class="panel" id="${id}">`,
3994
4047
  "<h2>Recent decisions</h2>",
3995
4048
  "<table><thead><tr><th>Tier</th><th>Route</th><th>Model</th><th>Prompt (hash)</th><th>Cost</th><th>Savings</th></tr></thead>",
3996
4049
  `<tbody>${rows}</tbody></table>`,
@@ -4001,10 +4054,11 @@ function activityRow(entry) {
4001
4054
  const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
4002
4055
  return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
4003
4056
  }
4004
- function activityPanel(activity) {
4057
+ function activityPanel(activity, idSuffix) {
4058
+ const id = panelId("panel-activity", idSuffix);
4005
4059
  if (activity.length === 0) {
4006
4060
  return [
4007
- '<section class="panel" id="panel-activity">',
4061
+ `<section class="panel" id="${id}">`,
4008
4062
  "<h2>Activity</h2>",
4009
4063
  '<p class="muted">No activity recorded.</p>',
4010
4064
  "</section>"
@@ -4012,7 +4066,7 @@ function activityPanel(activity) {
4012
4066
  }
4013
4067
  const rows = activity.map(activityRow).join("");
4014
4068
  return [
4015
- '<section class="panel" id="panel-activity">',
4069
+ `<section class="panel" id="${id}">`,
4016
4070
  "<h2>Activity</h2>",
4017
4071
  `<ul class="timeline">${rows}</ul>`,
4018
4072
  "</section>"
@@ -4074,10 +4128,11 @@ function decisionRow(decision2) {
4074
4128
  const tags = decision2.tags === undefined || decision2.tags.length === 0 ? "" : ` ${decision2.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
4075
4129
  return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision2.summary)}${tags}</li>`;
4076
4130
  }
4077
- function decisionsPanel(decisions) {
4131
+ function decisionsPanel(decisions, idSuffix) {
4132
+ const id = panelId("panel-decisions", idSuffix);
4078
4133
  if (decisions.length === 0) {
4079
4134
  return [
4080
- '<section class="panel" id="panel-decisions">',
4135
+ `<section class="panel" id="${id}">`,
4081
4136
  "<h2>Decisions</h2>",
4082
4137
  '<p class="muted">No decisions recorded (the scribe writes to <code>openteam-decisions.md</code>).</p>',
4083
4138
  "</section>"
@@ -4085,7 +4140,7 @@ function decisionsPanel(decisions) {
4085
4140
  }
4086
4141
  const rows = decisions.map(decisionRow).join("");
4087
4142
  return [
4088
- '<section class="panel" id="panel-decisions">',
4143
+ `<section class="panel" id="${id}">`,
4089
4144
  "<h2>Decisions</h2>",
4090
4145
  `<ul class="timeline">${rows}</ul>`,
4091
4146
  "</section>"
@@ -4096,10 +4151,11 @@ function meetingRow(meeting) {
4096
4151
  const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
4097
4152
  return `<li class="agent"><span class="kind">meeting</span> <span class="who">${roles}</span>${purpose}</li>`;
4098
4153
  }
4099
- function meetingsPanel(meetings) {
4154
+ function meetingsPanel(meetings, idSuffix) {
4155
+ const id = panelId("panel-meetings", idSuffix);
4100
4156
  if (meetings.length === 0) {
4101
4157
  return [
4102
- '<section class="panel" id="panel-meetings">',
4158
+ `<section class="panel" id="${id}">`,
4103
4159
  "<h2>Meetings</h2>",
4104
4160
  '<p class="muted">No multi-agent meetings yet.</p>',
4105
4161
  "</section>"
@@ -4107,7 +4163,7 @@ function meetingsPanel(meetings) {
4107
4163
  }
4108
4164
  const rows = meetings.map(meetingRow).join("");
4109
4165
  return [
4110
- '<section class="panel" id="panel-meetings">',
4166
+ `<section class="panel" id="${id}">`,
4111
4167
  "<h2>Meetings</h2>",
4112
4168
  `<ul class="timeline">${rows}</ul>`,
4113
4169
  "</section>"
@@ -4122,20 +4178,54 @@ var STATUS_LABELS = {
4122
4178
  function statusBadge(status) {
4123
4179
  return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
4124
4180
  }
4125
- function tabBar(sessions, graphEnabled, configEnabled) {
4126
- const allTab = '<button type="button" class="tab" data-tab="all">All</button>';
4127
- const graphTab = graphEnabled ? '<button type="button" class="tab" data-tab="graph">Graph</button>' : "";
4128
- const configTab = configEnabled ? '<button type="button" class="tab" data-tab="config">Configuration</button>' : "";
4129
- const tabs = sessions.map((tab) => {
4130
- const id = escapeHtml(tab.sessionID);
4131
- const short = escapeHtml(shortSessionID(tab.sessionID));
4132
- return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
4133
- }).join("");
4134
- return `<nav class="tabs" id="tabbar">${allTab}${graphTab}${configTab}${tabs}</nav>`;
4135
- }
4136
4181
  function shortSessionID(sessionID) {
4137
4182
  return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
4138
4183
  }
4184
+ var ICON_TREE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="6" height="5" rx="1"></rect><rect x="3" y="16" width="6" height="5" rx="1"></rect><rect x="15" y="16" width="6" height="5" rx="1"></rect><path d="M12 8v4M6 16v-2h12v2"></path></svg>';
4185
+ var ICON_ROUTING = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"></path><path d="M7 15l3-4 3 2 4-6"></path></svg>';
4186
+ var ICON_LOGS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16M4 10h16M4 15h10M4 20h13"></path></svg>';
4187
+ var ICON_CONFIG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"></path><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 6 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H0"></path></svg>';
4188
+ function navButton(screen, label, icon, options = {}) {
4189
+ const active = options.active === true ? ' aria-current="page"' : "";
4190
+ const count = options.count === undefined ? "" : `<span class="count mono"${screen === "tree" ? " data-live-count" : ""}>${options.count}</span>`;
4191
+ const pill = options.pill === undefined ? "" : `<span class="pill warn">${escapeHtml(options.pill)}</span>`;
4192
+ return `<button type="button" class="nav-item" role="tab" data-screen="${screen}" data-title="${escapeHtml(label)}" aria-controls="screen-${screen}"${active}>${icon} ${escapeHtml(label)}${count}${pill}</button>`;
4193
+ }
4194
+ function sidebarNav(liveCount, logCount) {
4195
+ return [
4196
+ '<aside class="sidebar" id="sidebar">',
4197
+ '<div class="brand">',
4198
+ '<span class="logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="2.4"></circle><circle cx="5.5" cy="18" r="2.4"></circle><circle cx="18.5" cy="18" r="2.4"></circle><path d="M12 7.4v4.2M12 11.6 6.6 16M12 11.6 17.4 16"></path></svg></span>',
4199
+ '<span class="name">openteam</span>',
4200
+ '<span class="tag">console</span>',
4201
+ "</div>",
4202
+ '<nav class="nav" aria-label="Console surfaces">',
4203
+ '<div class="nav-label">Operations</div>',
4204
+ navButton("tree", "Agent tree", ICON_TREE, {
4205
+ active: true,
4206
+ count: liveCount
4207
+ }),
4208
+ navButton("routing", "Routing + telemetry", ICON_ROUTING),
4209
+ navButton("logs", "Logs / activity", ICON_LOGS, { count: logCount }),
4210
+ navButton("config", "Configuration", ICON_CONFIG, { pill: "editable" }),
4211
+ "</nav>",
4212
+ '<div class="sidebar-foot">',
4213
+ '<div class="host-row"><span class="dot live"></span><span>loopback console</span></div>',
4214
+ '<div class="host-row faint"><span>prompts stay redacted by hash</span></div>',
4215
+ "</div>",
4216
+ "</aside>"
4217
+ ].join("");
4218
+ }
4219
+ function screenSection(screen, label, heading, description, body, active = false) {
4220
+ const className = active ? "screen active" : "screen";
4221
+ const hidden = active ? "" : " hidden";
4222
+ return [
4223
+ `<section class="${className}" id="screen-${screen}" role="tabpanel" aria-label="${escapeHtml(label)}"${hidden}>`,
4224
+ `<div class="screen-head"><h2>${heading}</h2><p>${description}</p></div>`,
4225
+ body,
4226
+ "</section>"
4227
+ ].join("");
4228
+ }
4139
4229
  function sessionHeaderPanel(tab) {
4140
4230
  const endpoint = tab.endpoint === undefined ? '<span class="muted">endpoint not announced</span>' : `<code>${escapeHtml(tab.endpoint.url)}</code>`;
4141
4231
  const dir = tab.endpoint?.directory === undefined ? "" : `<div class="stat"><span class="k">Directory</span><span class="v mono">${escapeHtml(tab.endpoint.directory)}</span></div>`;
@@ -4181,17 +4271,18 @@ function ptyPanel(tab) {
4181
4271
  "</section>"
4182
4272
  ].join("");
4183
4273
  }
4184
- function sessionView(tab, terminalEnabled, ptyEnabled) {
4274
+ function sessionDetailCard(tab, terminalEnabled, ptyEnabled, visible, index) {
4275
+ const idSuffix = `${index}-${domIdPart(tab.sessionID)}`;
4185
4276
  const body = [
4186
4277
  sessionHeaderPanel(tab),
4187
4278
  terminalEnabled ? terminalPanel(tab) : "",
4188
4279
  terminalEnabled && ptyEnabled ? ptyPanel(tab) : "",
4189
- routesPanel(tab.recentRoutes),
4190
- decisionsPanel(tab.decisions),
4191
- meetingsPanel(tab.meetings),
4192
- activityPanel(tab.activity)
4280
+ routesPanel(tab.recentRoutes, idSuffix),
4281
+ decisionsPanel(tab.decisions, idSuffix),
4282
+ meetingsPanel(tab.meetings, idSuffix),
4283
+ activityPanel(tab.activity, idSuffix)
4193
4284
  ].join("");
4194
- return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
4285
+ return `<article class="session-detail" id="session-${idSuffix}" data-session-view="${escapeHtml(tab.sessionID)}"${visible ? "" : " hidden"}>${body}</article>`;
4195
4286
  }
4196
4287
  var GRAPH_COL_W = 220;
4197
4288
  var GRAPH_ROW_H = 96;
@@ -4356,51 +4447,110 @@ function drillPanel() {
4356
4447
  "</aside>"
4357
4448
  ].join("");
4358
4449
  }
4359
- function graphViewPanel(projection, drilldownEnabled, disabled) {
4450
+ function treeGraphPanel(projection, drilldownEnabled, disabled) {
4360
4451
  return [
4361
- '<div class="view" data-view="graph" hidden>',
4362
- '<section class="panel" id="panel-graph">',
4363
- "<h2>Agent graph</h2>",
4452
+ '<section class="panel graph-panel" id="panel-graph">',
4453
+ "<h3>Live graph</h3>",
4364
4454
  disabled || projection === null ? "" : healthBanner(projection.health),
4455
+ '<div id="tree-content" data-graph-mount>',
4365
4456
  renderGraphView(projection, { disabled }),
4457
+ "</div>",
4366
4458
  !disabled && drilldownEnabled ? drillPanel() : "",
4367
- "</section>",
4368
- "</div>"
4459
+ "</section>"
4369
4460
  ].join("");
4370
4461
  }
4371
4462
  function graphDisabledHint() {
4372
4463
  return [
4373
4464
  '<div class="graph-disabled" role="status">',
4374
- '<p class="muted">The live agent tree is disabled.</p>',
4375
- '<p class="hint">Set <code>graph.mode</code> to <code>shadow</code> in ',
4376
- "<code>.opencode/openteam.json</code> to enable it, then restart the session.</p>",
4465
+ `<p class="muted">${escapeHtml(GRAPH_DISABLED_REMEDIATION)}</p>`,
4377
4466
  "</div>"
4378
4467
  ].join("");
4379
4468
  }
4380
- function renderConsoleHtml(state, options = {}) {
4381
- const refreshMs = options.refreshMs ?? 2000;
4382
- const terminalEnabled = options.terminalEnabled === true;
4383
- const ptyEnabled = options.ptyEnabled === true;
4384
- const graphEnabled = options.graphView !== undefined;
4385
- const allBody = [
4469
+ function routingScreenBody(state) {
4470
+ return [
4471
+ '<div class="screen-grid">',
4386
4472
  summaryPanel(state.cost),
4387
4473
  totalsPanel(state.totals),
4388
4474
  sessionsPanel(state.sessions),
4389
- loopPanel(state.loop),
4390
- teamPanel(state.team),
4391
4475
  tierPanel(state.cost),
4392
4476
  routesPanel(state.recentRoutes),
4477
+ "</div>"
4478
+ ].join("");
4479
+ }
4480
+ function emptySessionList() {
4481
+ return [
4482
+ '<div class="card">',
4483
+ '<div class="card-head"><h3>Sessions</h3><span class="sub">0</span></div>',
4484
+ '<div class="empty-state">No sessions recorded yet.</div>',
4485
+ "</div>"
4486
+ ].join("");
4487
+ }
4488
+ function sessionList(sessions) {
4489
+ if (sessions.length === 0) {
4490
+ return emptySessionList();
4491
+ }
4492
+ const buttons = sessions.map((tab, index) => {
4493
+ const selected = index === 0;
4494
+ return [
4495
+ `<button type="button" class="session-btn${selected ? " sel" : ""}" role="tab" aria-selected="${selected ? "true" : "false"}" data-session-target="${escapeHtml(tab.sessionID)}">`,
4496
+ `<span class="dot status-${tab.status}"></span>`,
4497
+ `<span class="sid">${escapeHtml(shortSessionID(tab.sessionID))}</span>`,
4498
+ `<span class="n">${tab.summary.messageCount}</span>`,
4499
+ "</button>"
4500
+ ].join("");
4501
+ }).join("");
4502
+ return [
4503
+ '<div class="card">',
4504
+ `<div class="card-head"><h3>Sessions</h3><span class="sub">${sessions.length}</span></div>`,
4505
+ '<div class="session-list" role="tablist" aria-label="Sessions">',
4506
+ buttons,
4507
+ "</div>",
4508
+ "</div>"
4509
+ ].join("");
4510
+ }
4511
+ function sessionDetailList(sessions, terminalEnabled, ptyEnabled) {
4512
+ if (sessions.length === 0) {
4513
+ return '<div class="card empty-state">No session activity recorded yet.</div>';
4514
+ }
4515
+ return sessions.map((tab, index) => sessionDetailCard(tab, terminalEnabled, ptyEnabled, index === 0, index)).join("");
4516
+ }
4517
+ function logsScreenBody(state, terminalEnabled, ptyEnabled) {
4518
+ return [
4519
+ '<div class="logs-layout">',
4520
+ sessionList(state.sessionsDetail),
4521
+ "<div>",
4522
+ sessionDetailList(state.sessionsDetail, terminalEnabled, ptyEnabled),
4523
+ "</div>",
4524
+ "</div>",
4525
+ '<div class="screen-grid">',
4526
+ loopPanel(state.loop),
4527
+ teamPanel(state.team),
4393
4528
  decisionsPanel(state.decisions),
4394
4529
  meetingsPanel(state.meetings),
4395
- activityPanel(state.activity)
4530
+ activityPanel(state.activity),
4531
+ "</div>"
4396
4532
  ].join("");
4397
- const allView = `<div class="view" data-view="all">${allBody}</div>`;
4398
- const graphDrilldown = graphEnabled && options.drilldownEnabled === true;
4399
- const graphView = graphEnabled ? graphViewPanel(options.graphView?.projection ?? null, graphDrilldown, options.graphView?.disabled === true) : "";
4400
- const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
4533
+ }
4534
+ function configScreenBody(hasToken) {
4535
+ return renderConfigEditorPanel({ hasToken });
4536
+ }
4537
+ function renderConsoleHtml(state, options = {}) {
4538
+ const refreshMs = options.refreshMs ?? 2000;
4539
+ const terminalEnabled = options.terminalEnabled === true;
4540
+ const ptyEnabled = options.ptyEnabled === true;
4541
+ const graphOptionProvided = options.graphView !== undefined;
4542
+ const graphDrilldown = graphOptionProvided && options.drilldownEnabled === true;
4543
+ const graphProjection = options.graphView?.projection ?? null;
4544
+ const graphDisabled = options.graphView === undefined || options.graphView.disabled === true;
4545
+ const liveCount = graphOptionProvided && graphProjection !== null && !graphDisabled ? graphProjection.nodes.length : state.sessionsDetail.length;
4401
4546
  const configEnabled = options.configEditor !== undefined;
4402
- const configView = configEnabled ? renderConfigView({ hasToken: options.configEditor?.hasToken === true }) : "";
4403
- const body = allView + graphView + configView + sessionViews;
4547
+ const treeBody = treeGraphPanel(graphProjection, graphDrilldown, graphDisabled);
4548
+ const body = [
4549
+ screenSection("tree", "Live agent tree", "Agent tree · live", "Live agent topology. Graph updates arrive over the <code>graph</code> event on <code>/events</code>.", treeBody, true),
4550
+ screenSection("routing", "Routing and telemetry", "Routing + telemetry", "Aggregate routing and cost telemetry from <code>GET /api/state</code> across all sessions.", routingScreenBody(state)),
4551
+ screenSection("logs", "Logs and session activity", "Logs / activity", "Per-session activity. Prompts are referenced by hash only and never rendered in clear text.", logsScreenBody(state, terminalEnabled, ptyEnabled)),
4552
+ screenSection("config", "Editable configuration", 'Configuration <span class="pill warn">editable</span>', "Edit the whole openteam JSON configuration. Roles are editable inside the JSON when present; unsupported surfaces are not shown.", configScreenBody(options.configEditor?.hasToken === true))
4553
+ ].join("");
4404
4554
  const session = state.session.id === undefined ? "" : ` · session ${escapeHtml(state.session.id)}`;
4405
4555
  const tokenAttr = (terminalEnabled || graphDrilldown || configEnabled) && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}"${terminalEnabled ? ' data-terminal="1"' : ""}${ptyEnabled ? ' data-pty="1"' : ""}${graphDrilldown ? ' data-drilldown="1"' : ""}${configEnabled ? ' data-config="1"' : ""}` : "";
4406
4556
  return [
@@ -4410,16 +4560,24 @@ function renderConsoleHtml(state, options = {}) {
4410
4560
  '<meta charset="utf-8">',
4411
4561
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
4412
4562
  "<title>openteam console</title>",
4413
- `<style>${CONSOLE_STYLE}${graphEnabled ? GRAPH_STYLE : ""}</style>`,
4563
+ `<style>${CONSOLE_STYLE}${graphOptionProvided ? GRAPH_STYLE : ""}</style>`,
4414
4564
  "</head>",
4415
4565
  "<body>",
4416
- "<header>",
4417
- "<h1>openteam console</h1>",
4418
- `<span class="meta">updated ${escapeHtml(state.generatedAt)}${session}</span>`,
4566
+ '<div class="app">',
4567
+ sidebarNav(liveCount, state.sessionsDetail.length),
4568
+ '<div class="scrim" data-close-nav></div>',
4569
+ '<main class="main">',
4570
+ '<header class="topbar">',
4571
+ '<button type="button" class="icon-btn menu-btn" data-toggle-nav aria-label="Open navigation"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h16M4 18h16"></path></svg></button>',
4572
+ '<h1 id="screen-title">Agent tree</h1>',
4573
+ `<span class="refresh" title="SSE + polling"><span class="beat"></span> ${refreshMs} ms · SSE</span>`,
4574
+ '<div class="spacer"></div>',
4575
+ `<span class="token-chip" title="Page status"><span class="lbl">updated</span> <b class="mono">${escapeHtml(state.generatedAt)}${session}</b></span>`,
4419
4576
  "</header>",
4420
4577
  options.diagnosticActive === true ? '<div class="diag-banner" role="alert">Diagnostic mode enabled — this loopback console may show raw prompt and model text locally.</div>' : "",
4421
- tabBar(state.sessionsDetail, graphEnabled, configEnabled),
4422
- `<main>${body}</main>`,
4578
+ `<div class="scroll">${body}</div>`,
4579
+ "</main>",
4580
+ "</div>",
4423
4581
  `<script>${CONSOLE_CLIENT_JS}</script>`,
4424
4582
  "</body>",
4425
4583
  "</html>"
@@ -4482,23 +4640,21 @@ function renderConfigSaveSummary(write) {
4482
4640
  }
4483
4641
  return `<div class="config-summary">${parts.join("")}</div>`;
4484
4642
  }
4485
- function renderConfigView(opts) {
4643
+ function renderConfigEditorPanel(opts) {
4486
4644
  const inner = opts.hasToken ? [
4487
4645
  '<p class="muted">Edit the openteam configuration. Portable keys are committed to <code>.opencode/openteam.json</code>; machine-specific keys stay in your local overlay and are never committed; secrets are read from environment variables and are never shown here.</p>',
4488
4646
  '<div class="config-mount" data-config-mount><p class="muted">Loading configuration…</p></div>'
4489
4647
  ].join("") : '<p class="muted">Configuration editing requires the console session token, which is not present in this page. This view is read-only.</p>';
4490
4648
  return [
4491
- '<div class="view" data-view="config" hidden>',
4492
4649
  '<section class="panel config-panel">',
4493
- "<h2>Configuration</h2>",
4650
+ "<h3>Configuration JSON</h3>",
4494
4651
  inner,
4495
- "</section>",
4496
- "</div>"
4652
+ "</section>"
4497
4653
  ].join("");
4498
4654
  }
4499
4655
 
4500
4656
  // src/web/configRoute.ts
4501
- var JSON_HEADERS = { "content-type": "application/json" };
4657
+ var JSON_HEADERS2 = { "content-type": "application/json" };
4502
4658
  var LOOPBACK_HOSTS = new Set([
4503
4659
  "127.0.0.1",
4504
4660
  "localhost",
@@ -4506,7 +4662,7 @@ var LOOPBACK_HOSTS = new Set([
4506
4662
  "::1"
4507
4663
  ]);
4508
4664
  var MAX_CONFIG_BODY_CHARS = 256 * 1024;
4509
- function bearerFromHeader(request) {
4665
+ function bearerFromHeader2(request) {
4510
4666
  const header = request.headers.authorization;
4511
4667
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
4512
4668
  return;
@@ -4519,7 +4675,7 @@ function queryToken(query) {
4519
4675
  return value === null || value.length === 0 ? undefined : value;
4520
4676
  }
4521
4677
  function readAuthorized(request, query, token) {
4522
- const provided = bearerFromHeader(request) ?? queryToken(query);
4678
+ const provided = bearerFromHeader2(request) ?? queryToken(query);
4523
4679
  return provided !== undefined && safeEqualToken(provided, token);
4524
4680
  }
4525
4681
  function isCrossOriginMutation(request) {
@@ -4570,34 +4726,34 @@ async function handleConfigRoute(request, response, url, deps) {
4570
4726
  }
4571
4727
  if (request.method === "GET") {
4572
4728
  if (!readAuthorized(request, url.searchParams, deps.token)) {
4573
- response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
4729
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
4574
4730
  return true;
4575
4731
  }
4576
4732
  const result = await deps.editor.read();
4577
- response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ...result, render: renderConfigEditorForm(result) }));
4733
+ response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ ...result, render: renderConfigEditorForm(result) }));
4578
4734
  return true;
4579
4735
  }
4580
4736
  if (request.method === "POST") {
4581
4737
  if (isCrossOriginMutation(request)) {
4582
- response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "cross-origin forbidden" }));
4738
+ response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "cross-origin forbidden" }));
4583
4739
  return true;
4584
4740
  }
4585
- const bearer = bearerFromHeader(request);
4741
+ const bearer = bearerFromHeader2(request);
4586
4742
  if (bearer === undefined || !safeEqualToken(bearer, deps.token)) {
4587
- response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
4743
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
4588
4744
  return true;
4589
4745
  }
4590
4746
  const body = await readJsonBody(request, MAX_CONFIG_BODY_CHARS);
4591
4747
  if (!body.ok) {
4592
- response.writeHead(400, JSON_HEADERS).end(JSON.stringify({ ok: false, error: "invalid JSON body" }));
4748
+ response.writeHead(400, JSON_HEADERS2).end(JSON.stringify({ ok: false, error: "invalid JSON body" }));
4593
4749
  return true;
4594
4750
  }
4595
4751
  const result = await deps.editor.write(body.value);
4596
4752
  const responseBody = result.status === 200 ? { ...result.body, render: renderConfigSaveSummary(result.body) } : result.body;
4597
- response.writeHead(result.status, JSON_HEADERS).end(JSON.stringify(responseBody));
4753
+ response.writeHead(result.status, JSON_HEADERS2).end(JSON.stringify(responseBody));
4598
4754
  return true;
4599
4755
  }
4600
- response.writeHead(405, JSON_HEADERS).end(JSON.stringify({ error: "method not allowed" }));
4756
+ response.writeHead(405, JSON_HEADERS2).end(JSON.stringify({ error: "method not allowed" }));
4601
4757
  return true;
4602
4758
  }
4603
4759
  function createConfigReloader(deps) {
@@ -4921,8 +5077,8 @@ var SSE_HEADERS = {
4921
5077
  "cache-control": "no-cache",
4922
5078
  connection: "keep-alive"
4923
5079
  };
4924
- var JSON_HEADERS2 = { "content-type": "application/json" };
4925
- function bearerFromHeader2(request) {
5080
+ var JSON_HEADERS3 = { "content-type": "application/json" };
5081
+ function bearerFromHeader3(request) {
4926
5082
  const header = request.headers.authorization;
4927
5083
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
4928
5084
  return;
@@ -4930,8 +5086,8 @@ function bearerFromHeader2(request) {
4930
5086
  const value = header.slice("Bearer ".length).trim();
4931
5087
  return value.length === 0 ? undefined : value;
4932
5088
  }
4933
- function authorized(request, query, token) {
4934
- const provided = tokenFromQuery(query) ?? bearerFromHeader2(request);
5089
+ function authorized2(request, query, token) {
5090
+ const provided = tokenFromQuery(query) ?? bearerFromHeader3(request);
4935
5091
  return provided !== undefined && safeEqualToken(provided, token);
4936
5092
  }
4937
5093
  async function readJsonBody2(request, limit) {
@@ -4974,7 +5130,7 @@ function parseConsolePath(path) {
4974
5130
  async function handleStream(request, response, sessionID, deps, diagnostic) {
4975
5131
  const endpoint = await deps.resolveEndpoint(sessionID);
4976
5132
  if (endpoint === undefined) {
4977
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5133
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
4978
5134
  return;
4979
5135
  }
4980
5136
  response.writeHead(200, SSE_HEADERS);
@@ -4995,31 +5151,31 @@ data: {}
4995
5151
  async function handleMessages(_request, response, sessionID, deps, diagnostic) {
4996
5152
  const endpoint = await deps.resolveEndpoint(sessionID);
4997
5153
  if (endpoint === undefined) {
4998
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5154
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
4999
5155
  return;
5000
5156
  }
5001
5157
  const frames = await deps.createClient(endpoint).listMessages(sessionID, { diagnostic });
5002
- response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ frames }));
5158
+ response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({ frames }));
5003
5159
  }
5004
5160
  async function handleInput(request, response, sessionID, deps) {
5005
5161
  const endpoint = await deps.resolveEndpoint(sessionID);
5006
5162
  if (endpoint === undefined) {
5007
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5163
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5008
5164
  return;
5009
5165
  }
5010
5166
  const body = await readJsonBody2(request, MAX_INPUT_CHARS + 1024);
5011
5167
  const input = parseSessionInput(body);
5012
5168
  if (input === undefined) {
5013
- response.writeHead(400, JSON_HEADERS2).end(JSON.stringify({ error: "invalid input" }));
5169
+ response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid input" }));
5014
5170
  return;
5015
5171
  }
5016
5172
  await deps.createClient(endpoint).sendPrompt(sessionID, input.text);
5017
- response.writeHead(202, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
5173
+ response.writeHead(202, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
5018
5174
  }
5019
5175
  async function handlePermission(request, response, sessionID, deps) {
5020
5176
  const endpoint = await deps.resolveEndpoint(sessionID);
5021
5177
  if (endpoint === undefined) {
5022
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5178
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5023
5179
  return;
5024
5180
  }
5025
5181
  const body = await readJsonBody2(request, 4096);
@@ -5027,34 +5183,34 @@ async function handlePermission(request, response, sessionID, deps) {
5027
5183
  const permissionID = record.permissionID;
5028
5184
  const responseValue = record.response;
5029
5185
  if (typeof permissionID !== "string" || permissionID.length === 0 || responseValue !== "once" && responseValue !== "always" && responseValue !== "reject") {
5030
- response.writeHead(400, JSON_HEADERS2).end(JSON.stringify({ error: "invalid permission" }));
5186
+ response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid permission" }));
5031
5187
  return;
5032
5188
  }
5033
5189
  await deps.createClient(endpoint).respondPermission(sessionID, permissionID, responseValue);
5034
- response.writeHead(202, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
5190
+ response.writeHead(202, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
5035
5191
  }
5036
5192
  async function handlePty(_request, response, sessionID, deps) {
5037
5193
  if (deps.ptyEnabled !== true || deps.createPtyClient === undefined) {
5038
- response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "pty disabled" }));
5194
+ response.writeHead(403, JSON_HEADERS3).end(JSON.stringify({ error: "pty disabled" }));
5039
5195
  return;
5040
5196
  }
5041
5197
  const endpoint = await deps.resolveEndpoint(sessionID);
5042
5198
  if (endpoint === undefined) {
5043
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5199
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5044
5200
  return;
5045
5201
  }
5046
5202
  const client = deps.createPtyClient(endpoint);
5047
5203
  const pty = await client.createPty({ command: "", title: "openteam" });
5048
5204
  if (pty === undefined) {
5049
- response.writeHead(502, JSON_HEADERS2).end(JSON.stringify({ error: "pty create failed" }));
5205
+ response.writeHead(502, JSON_HEADERS3).end(JSON.stringify({ error: "pty create failed" }));
5050
5206
  return;
5051
5207
  }
5052
5208
  const token = await client.connectToken(pty.id);
5053
5209
  if (token === undefined) {
5054
- response.writeHead(502, JSON_HEADERS2).end(JSON.stringify({ error: "connect token failed" }));
5210
+ response.writeHead(502, JSON_HEADERS3).end(JSON.stringify({ error: "connect token failed" }));
5055
5211
  return;
5056
5212
  }
5057
- response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({
5213
+ response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({
5058
5214
  ptyID: pty.id,
5059
5215
  wsUrl: client.wsUrl(pty.id, token.ticket)
5060
5216
  }));
@@ -5065,17 +5221,17 @@ async function handleConsoleRoute(request, response, url, deps) {
5065
5221
  }
5066
5222
  const route = parseConsolePath(url.pathname);
5067
5223
  if (route === undefined) {
5068
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "not found" }));
5224
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "not found" }));
5069
5225
  return true;
5070
5226
  }
5071
5227
  const readOnlyAction = route.action === "stream" || route.action === "messages";
5072
5228
  const channelEnabled = deps.terminalEnabled || readOnlyAction && deps.drilldownEnabled === true;
5073
5229
  if (!channelEnabled) {
5074
- response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "terminal disabled" }));
5230
+ response.writeHead(403, JSON_HEADERS3).end(JSON.stringify({ error: "terminal disabled" }));
5075
5231
  return true;
5076
5232
  }
5077
- if (!authorized(request, url.searchParams, deps.token)) {
5078
- response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
5233
+ if (!authorized2(request, url.searchParams, deps.token)) {
5234
+ response.writeHead(401, JSON_HEADERS3).end(JSON.stringify({ error: "unauthorized" }));
5079
5235
  return true;
5080
5236
  }
5081
5237
  const diagnostic = deps.diagnosticEnabled === true && url.searchParams.get("diagnostic") === "1";
@@ -5099,45 +5255,7 @@ async function handleConsoleRoute(request, response, url, deps) {
5099
5255
  await handlePty(request, response, route.sessionID, deps);
5100
5256
  return true;
5101
5257
  }
5102
- response.writeHead(405, JSON_HEADERS2).end(JSON.stringify({ error: "method not allowed" }));
5103
- return true;
5104
- }
5105
-
5106
- // src/web/graphRoute.ts
5107
- var JSON_HEADERS3 = { "content-type": "application/json" };
5108
- var GRAPH_SSE_EVENT = "graph";
5109
- function graphSseMessage(json) {
5110
- return `event: ${GRAPH_SSE_EVENT}
5111
- data: ${json}
5112
-
5113
- `;
5114
- }
5115
- function bearerFromHeader3(request) {
5116
- const header = request.headers.authorization;
5117
- if (typeof header !== "string" || !header.startsWith("Bearer ")) {
5118
- return;
5119
- }
5120
- const value = header.slice("Bearer ".length).trim();
5121
- return value.length === 0 ? undefined : value;
5122
- }
5123
- function authorized2(request, query, token) {
5124
- const provided = tokenFromQuery(query) ?? bearerFromHeader3(request);
5125
- return provided !== undefined && safeEqualToken(provided, token);
5126
- }
5127
- async function handleGraphRoute(request, response, url, deps) {
5128
- if (url.pathname !== "/api/graph") {
5129
- return false;
5130
- }
5131
- if (request.method !== "GET") {
5132
- response.writeHead(405, JSON_HEADERS3).end(JSON.stringify({ error: "method not allowed" }));
5133
- return true;
5134
- }
5135
- if (deps.remoteToken !== undefined && !authorized2(request, url.searchParams, deps.remoteToken)) {
5136
- response.writeHead(401, JSON_HEADERS3).end(JSON.stringify({ error: "unauthorized" }));
5137
- return true;
5138
- }
5139
- const view = await deps.readView();
5140
- response.writeHead(200, JSON_HEADERS3).end(JSON.stringify(view));
5258
+ response.writeHead(405, JSON_HEADERS3).end(JSON.stringify({ error: "method not allowed" }));
5141
5259
  return true;
5142
5260
  }
5143
5261
 
@@ -5408,6 +5526,14 @@ async function createConsoleServer(deps) {
5408
5526
  if (graphDeps !== undefined && await handleGraphRoute(request, response, url2, graphDeps)) {
5409
5527
  return;
5410
5528
  }
5529
+ if (graphDeps === undefined && url2.pathname === "/api/graph") {
5530
+ if (request.method !== "GET") {
5531
+ response.writeHead(405, { "content-type": "application/json" }).end(JSON.stringify({ error: "method not allowed" }));
5532
+ return;
5533
+ }
5534
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(GRAPH_DISABLED_ENVELOPE));
5535
+ return;
5536
+ }
5411
5537
  if (deps.storageApi !== undefined && await handleStorageRoute(request, response, url2, deps.storageApi)) {
5412
5538
  return;
5413
5539
  }
@@ -5982,7 +6108,7 @@ async function createConsoleRuntime(deps) {
5982
6108
  });
5983
6109
  const createServer = deps.serve ?? createConsoleServer;
5984
6110
  const graphApi = deps.config.graphView?.enabled === true && deps.graphView !== undefined ? { readView: deps.graphView.readView } : undefined;
5985
- const graphDisabled = deps.config.graphView?.enabled === true && graphApi === undefined;
6111
+ const graphDisabled = graphApi === undefined;
5986
6112
  const server = await createServer({
5987
6113
  readSnapshot,
5988
6114
  config: deps.config,
@@ -15315,7 +15441,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
15315
15441
  // package.json
15316
15442
  var package_default = {
15317
15443
  name: "@jmanuelcorral/openteam",
15318
- version: "0.11.0",
15444
+ version: "0.12.0",
15319
15445
  packageManager: "bun@1.3.14",
15320
15446
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
15321
15447
  license: "MIT",