@jmanuelcorral/openteam 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/cli.js CHANGED
@@ -299,7 +299,7 @@ var GraphEngineSchema = z2.enum(["builtin", "langgraph"]);
299
299
  var DEFAULT_GRAPH_JOURNAL_ROOT = ".opencode/openteam-local/graph/runs";
300
300
  var DEFAULT_GRAPH_WORKTREES_DIR = ".opencode/openteam-local/graph/worktrees";
301
301
  var GraphConfigSchema = z2.object({
302
- mode: GraphModeSchema.default("off"),
302
+ mode: GraphModeSchema.default("shadow"),
303
303
  engine: GraphEngineSchema.optional(),
304
304
  killSwitch: z2.boolean().default(false),
305
305
  journalRoot: z2.string().min(1).default(DEFAULT_GRAPH_JOURNAL_ROOT),
@@ -322,8 +322,8 @@ var ConsoleConfigSchema = z2.object({
322
322
  pty: z2.boolean().optional()
323
323
  }).strict().default({ enabled: true }),
324
324
  graphView: z2.object({
325
- enabled: z2.boolean().default(false)
326
- }).strict().optional()
325
+ enabled: z2.boolean().default(true)
326
+ }).strict().default({ enabled: true })
327
327
  }).strict().default({
328
328
  host: "127.0.0.1",
329
329
  port: 4599,
@@ -332,7 +332,8 @@ var ConsoleConfigSchema = z2.object({
332
332
  recentRoutes: 50,
333
333
  openBrowser: false,
334
334
  remoteStorage: false,
335
- terminal: { enabled: true }
335
+ terminal: { enabled: true },
336
+ graphView: { enabled: true }
336
337
  });
337
338
  var DEFAULT_STORAGE_SQLITE_INDEX_PATH = ".opencode/openteam-local/index.sqlite";
338
339
  var StorageConfigSchema = z2.object({
@@ -511,7 +512,13 @@ var OpenTeamConfigObjectSchema = z2.object({
511
512
  console: ConsoleConfigSchema,
512
513
  storage: StorageConfigSchema,
513
514
  memory: MemoryConfigSchema.optional(),
514
- graph: GraphConfigSchema.optional(),
515
+ graph: GraphConfigSchema.default({
516
+ mode: "shadow",
517
+ killSwitch: false,
518
+ journalRoot: DEFAULT_GRAPH_JOURNAL_ROOT,
519
+ operatorApproval: false,
520
+ worktrees: { enabled: false }
521
+ }),
515
522
  orchestrator: OrchestratorConfigSchema.optional()
516
523
  }).strict();
517
524
  var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
@@ -2040,6 +2047,13 @@ var FAILURE_CAUSE_RULES = [
2040
2047
  "prompt-server"
2041
2048
  ]
2042
2049
  ];
2050
+ var TIMEOUT_NEEDLES = [
2051
+ "timed out",
2052
+ "timeout",
2053
+ "timedout",
2054
+ "etimedout",
2055
+ "deadline exceeded"
2056
+ ];
2043
2057
  function matchFailureCause(lowerReason) {
2044
2058
  for (const [needles, cls] of FAILURE_CAUSE_RULES) {
2045
2059
  if (needles.some((needle) => lowerReason.includes(needle))) {
@@ -2050,7 +2064,7 @@ function matchFailureCause(lowerReason) {
2050
2064
  }
2051
2065
  function classifyFailureForTelemetry(failureReason, failureStage) {
2052
2066
  const lower = failureReason.toLowerCase();
2053
- if (lower.includes("timed out") || lower.includes("timeout")) {
2067
+ if (TIMEOUT_NEEDLES.some((needle) => lower.includes(needle))) {
2054
2068
  return "prompt-timeout";
2055
2069
  }
2056
2070
  if (lower.includes("did not include a session id") || lower.includes("missing session")) {
@@ -3517,91 +3531,80 @@ var RESTART_REQUIRED_KEYS = [
3517
3531
  "console.port"
3518
3532
  ];
3519
3533
 
3534
+ // src/web/graphRoute.ts
3535
+ 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.';
3536
+ var GRAPH_DISABLED_ENVELOPE = {
3537
+ disabled: true,
3538
+ reason: "graph-disabled",
3539
+ remediation: GRAPH_DISABLED_REMEDIATION
3540
+ };
3541
+ var JSON_HEADERS = { "content-type": "application/json" };
3542
+ var GRAPH_SSE_EVENT = "graph";
3543
+ function graphSseMessage(json) {
3544
+ return `event: ${GRAPH_SSE_EVENT}
3545
+ data: ${json}
3546
+
3547
+ `;
3548
+ }
3549
+ function bearerFromHeader(request) {
3550
+ const header = request.headers.authorization;
3551
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
3552
+ return;
3553
+ }
3554
+ const value = header.slice("Bearer ".length).trim();
3555
+ return value.length === 0 ? undefined : value;
3556
+ }
3557
+ function authorized(request, query, token) {
3558
+ const provided = tokenFromQuery(query) ?? bearerFromHeader(request);
3559
+ return provided !== undefined && safeEqualToken(provided, token);
3560
+ }
3561
+ async function handleGraphRoute(request, response, url, deps) {
3562
+ if (url.pathname !== "/api/graph") {
3563
+ return false;
3564
+ }
3565
+ if (request.method !== "GET") {
3566
+ response.writeHead(405, JSON_HEADERS).end(JSON.stringify({ error: "method not allowed" }));
3567
+ return true;
3568
+ }
3569
+ if (deps.remoteToken !== undefined && !authorized(request, url.searchParams, deps.remoteToken)) {
3570
+ response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
3571
+ return true;
3572
+ }
3573
+ const view = await deps.readView();
3574
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify(view));
3575
+ return true;
3576
+ }
3577
+
3520
3578
  // src/console/assets.ts
3521
3579
  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}
3580
+ :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}
3581
+ *{box-sizing:border-box;margin:0;padding:0}
3582
+ html,body{height:100%}
3583
+ 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}
3584
+ .mono,code,.hash{font-family:var(--font-mono);font-variant-numeric:tabular-nums}
3585
+ button{font:inherit;cursor:pointer}
3586
+ button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
3587
+ ::selection{background:var(--accent-bg)}
3588
+ .app{display:grid;grid-template-columns:var(--sidebar-w) 1fr;grid-template-rows:100vh;min-height:100vh}
3589
+ .sidebar{background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}
3590
+ .brand{display:flex;align-items:center;gap:10px;padding:18px 18px 16px;border-bottom:1px solid var(--border)}
3591
+ .logo{width:26px;height:26px;border-radius:6px;background:var(--accent);color:#fff;display:grid;place-items:center;flex:none}
3592
+ .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}
3593
+ .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)}
3594
+ .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}
3595
+ .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}
3596
+ .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}
3597
+ .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)}
3598
+ 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)}
3599
+ .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)}
3600
+ .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}
3601
+ .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)}
3602
+ .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}
3603
+ .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}
3604
+ .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,.graph-empty{border:1px solid var(--border);border-radius:8px;padding:14px;background:var(--surface-2)}.graph-empty{border-color:color-mix(in srgb,var(--info) 35%,var(--border));background:var(--info-bg)}.graph-disabled .hint{margin-top:6px}.scrim{display:none}.menu-btn{display:none}
3605
+ @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}}
3606
+ @media (max-width:560px){.screen{padding:16px 14px 32px}.screen-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.refresh{display:none}}
3607
+ @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
3605
3608
  `;
3606
3609
  var GRAPH_STYLE = `
3607
3610
  .graph-wrap{overflow:auto;background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:8px}
@@ -3671,24 +3674,69 @@ var CONSOLE_CLIENT_JS = `
3671
3674
  var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
3672
3675
  setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
3673
3676
 
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');
3677
+ function configActive(){
3678
+ var s=document.getElementById('screen-config');
3679
+ return !!(s&&!s.hidden);
3680
+ }
3681
+ function wireLogsScreen(screen){
3682
+ if(screen.__logsWired){return}
3683
+ screen.__logsWired=true;
3684
+ var buttons=screen.querySelectorAll('[data-session-target]');
3685
+ var cards=screen.querySelectorAll('[data-session-view]');
3686
+ function showSession(id){
3687
+ var matched=false;
3688
+ for(var i=0;i<buttons.length;i++){
3689
+ var on=buttons[i].getAttribute('data-session-target')===id;
3690
+ buttons[i].classList.toggle('sel',on);
3691
+ buttons[i].setAttribute('aria-selected',on?'true':'false');
3692
+ }
3693
+ for(var j=0;j<cards.length;j++){
3694
+ var show=cards[j].getAttribute('data-session-view')===id;
3695
+ cards[j].hidden=!show;
3696
+ if(show){matched=true;connectTerminal(cards[j],id)}
3697
+ }
3698
+ return matched;
3699
+ }
3700
+ for(var k=0;k<buttons.length;k++){
3701
+ buttons[k].addEventListener('click',function(e){
3702
+ var id=e.currentTarget.getAttribute('data-session-target');
3703
+ if(id){showSession(id)}
3704
+ });
3705
+ }
3706
+ if(buttons.length>0){
3707
+ showSession(buttons[0].getAttribute('data-session-target'));
3708
+ }
3709
+ }
3710
+ function selectScreen(name){
3711
+ var screens=document.querySelectorAll('.screen');
3712
+ var items=document.querySelectorAll('[data-screen]');
3678
3713
  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)}}
3714
+ var titleText='';
3715
+ var activeScreen=null;
3716
+ for(var i=0;i<screens.length;i++){
3717
+ var show=screens[i].id==='screen-'+name;
3718
+ screens[i].classList.toggle('active',show);
3719
+ screens[i].hidden=!show;
3720
+ if(show){matched=true;activeScreen=screens[i]}
3683
3721
  }
3684
3722
  if(!matched){return false}
3685
- try{sessionStorage.setItem('openteam.tab',name)}catch(e){}
3723
+ for(var j=0;j<items.length;j++){
3724
+ var on=items[j].getAttribute('data-screen')===name;
3725
+ if(on){
3726
+ items[j].setAttribute('aria-current','page');
3727
+ titleText=items[j].getAttribute('data-title')||'';
3728
+ }else{
3729
+ items[j].removeAttribute('aria-current');
3730
+ }
3731
+ }
3732
+ var title=document.getElementById('screen-title');
3733
+ if(title){title.textContent=titleText}
3734
+ if(name==='config'&&activeScreen){loadConfig(activeScreen)}
3735
+ if(name==='logs'&&activeScreen){wireLogsScreen(activeScreen)}
3736
+ try{localStorage.setItem('openteam.screen',name)}catch(e){}
3737
+ document.body.classList.remove('nav-open');
3686
3738
  return true;
3687
3739
  }
3688
- function configActive(){
3689
- var v=document.querySelector('main .view[data-view="config"]');
3690
- return !!(v&&!v.hidden);
3691
- }
3692
3740
  function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
3693
3741
  // Config editor (W7-S09 / #193). The config values travel only over the
3694
3742
  // token-gated /api/config, never in this open page, so the browser fetches
@@ -3827,7 +3875,7 @@ var CONSOLE_CLIENT_JS = `
3827
3875
  if(!token){return}
3828
3876
  var drill=document.querySelector('.graph-drill');
3829
3877
  if(!drill){return}
3830
- var gview=drill.closest('.view');
3878
+ var gview=drill.closest('.screen');
3831
3879
  if(!gview){return}
3832
3880
  var log=drill.querySelector('.drilllog');
3833
3881
  var title=drill.querySelector('.drill-title');
@@ -3861,22 +3909,27 @@ var CONSOLE_CLIENT_JS = `
3861
3909
  if(t&&t.getAttribute&&t.getAttribute('data-session')){open(t.getAttribute('data-session'))}
3862
3910
  });
3863
3911
  }
3864
- var bar=document.getElementById('tabbar');
3865
- if(bar){
3866
- bar.addEventListener('click',function(e){
3912
+ var nav=document.querySelector('.nav');
3913
+ if(nav){
3914
+ nav.addEventListener('click',function(e){
3867
3915
  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'))}
3916
+ while(t&&t!==nav&&!(t.getAttribute&&t.getAttribute('data-screen'))){t=t.parentNode}
3917
+ if(t&&t.getAttribute&&t.getAttribute('data-screen')){selectScreen(t.getAttribute('data-screen'))}
3870
3918
  });
3871
3919
  }
3872
- var saved='all';
3873
- try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
3874
- if(!selectTab(saved)){selectTab('all')}
3920
+ var toggle=document.querySelector('[data-toggle-nav]');
3921
+ if(toggle){toggle.addEventListener('click',function(){document.body.classList.toggle('nav-open')})}
3922
+ var close=document.querySelector('[data-close-nav]');
3923
+ if(close){close.addEventListener('click',function(){document.body.classList.remove('nav-open')})}
3924
+ var saved='tree';
3925
+ try{saved=localStorage.getItem('openteam.screen')||'tree'}catch(e){}
3926
+ if(!selectScreen(saved)){selectScreen('tree')}
3875
3927
  wireGraphDrill();
3876
3928
  })();
3877
3929
  `;
3878
3930
 
3879
3931
  // src/console/render.ts
3932
+ var GRAPH_EMPTY_REMEDIATION = "No graph run yet. The tree fills when openteam-orchestrate dispatches roles; ordinary chat does not create a graph run.";
3880
3933
  var TIERS = [
3881
3934
  "trivial",
3882
3935
  "simple",
@@ -3901,6 +3954,13 @@ function avgMs(stats) {
3901
3954
  function agentModel(agent) {
3902
3955
  return agent.model === undefined ? "inherits default" : `${agent.model.providerID}/${agent.model.modelID}`;
3903
3956
  }
3957
+ function domIdPart(value) {
3958
+ const cleaned = value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3959
+ return cleaned.length === 0 ? "session" : cleaned;
3960
+ }
3961
+ function panelId(base, idSuffix) {
3962
+ return idSuffix === undefined ? base : `${base}-${idSuffix}`;
3963
+ }
3904
3964
  function summaryPanel(cost) {
3905
3965
  return [
3906
3966
  '<section class="panel" id="panel-summary">',
@@ -3979,10 +4039,11 @@ function teamPanel(team) {
3979
4039
  function routeRow(route) {
3980
4040
  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
4041
  }
3982
- function routesPanel(routes) {
4042
+ function routesPanel(routes, idSuffix) {
4043
+ const id = panelId("panel-routes", idSuffix);
3983
4044
  if (routes.length === 0) {
3984
4045
  return [
3985
- '<section class="panel" id="panel-routes">',
4046
+ `<section class="panel" id="${id}">`,
3986
4047
  "<h2>Recent decisions</h2>",
3987
4048
  '<p class="muted">No routing decisions yet.</p>',
3988
4049
  "</section>"
@@ -3990,7 +4051,7 @@ function routesPanel(routes) {
3990
4051
  }
3991
4052
  const rows = routes.map(routeRow).join("");
3992
4053
  return [
3993
- '<section class="panel" id="panel-routes">',
4054
+ `<section class="panel" id="${id}">`,
3994
4055
  "<h2>Recent decisions</h2>",
3995
4056
  "<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
4057
  `<tbody>${rows}</tbody></table>`,
@@ -4001,10 +4062,11 @@ function activityRow(entry) {
4001
4062
  const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
4002
4063
  return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
4003
4064
  }
4004
- function activityPanel(activity) {
4065
+ function activityPanel(activity, idSuffix) {
4066
+ const id = panelId("panel-activity", idSuffix);
4005
4067
  if (activity.length === 0) {
4006
4068
  return [
4007
- '<section class="panel" id="panel-activity">',
4069
+ `<section class="panel" id="${id}">`,
4008
4070
  "<h2>Activity</h2>",
4009
4071
  '<p class="muted">No activity recorded.</p>',
4010
4072
  "</section>"
@@ -4012,7 +4074,7 @@ function activityPanel(activity) {
4012
4074
  }
4013
4075
  const rows = activity.map(activityRow).join("");
4014
4076
  return [
4015
- '<section class="panel" id="panel-activity">',
4077
+ `<section class="panel" id="${id}">`,
4016
4078
  "<h2>Activity</h2>",
4017
4079
  `<ul class="timeline">${rows}</ul>`,
4018
4080
  "</section>"
@@ -4074,10 +4136,11 @@ function decisionRow(decision2) {
4074
4136
  const tags = decision2.tags === undefined || decision2.tags.length === 0 ? "" : ` ${decision2.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
4075
4137
  return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision2.summary)}${tags}</li>`;
4076
4138
  }
4077
- function decisionsPanel(decisions) {
4139
+ function decisionsPanel(decisions, idSuffix) {
4140
+ const id = panelId("panel-decisions", idSuffix);
4078
4141
  if (decisions.length === 0) {
4079
4142
  return [
4080
- '<section class="panel" id="panel-decisions">',
4143
+ `<section class="panel" id="${id}">`,
4081
4144
  "<h2>Decisions</h2>",
4082
4145
  '<p class="muted">No decisions recorded (the scribe writes to <code>openteam-decisions.md</code>).</p>',
4083
4146
  "</section>"
@@ -4085,7 +4148,7 @@ function decisionsPanel(decisions) {
4085
4148
  }
4086
4149
  const rows = decisions.map(decisionRow).join("");
4087
4150
  return [
4088
- '<section class="panel" id="panel-decisions">',
4151
+ `<section class="panel" id="${id}">`,
4089
4152
  "<h2>Decisions</h2>",
4090
4153
  `<ul class="timeline">${rows}</ul>`,
4091
4154
  "</section>"
@@ -4096,10 +4159,11 @@ function meetingRow(meeting) {
4096
4159
  const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
4097
4160
  return `<li class="agent"><span class="kind">meeting</span> <span class="who">${roles}</span>${purpose}</li>`;
4098
4161
  }
4099
- function meetingsPanel(meetings) {
4162
+ function meetingsPanel(meetings, idSuffix) {
4163
+ const id = panelId("panel-meetings", idSuffix);
4100
4164
  if (meetings.length === 0) {
4101
4165
  return [
4102
- '<section class="panel" id="panel-meetings">',
4166
+ `<section class="panel" id="${id}">`,
4103
4167
  "<h2>Meetings</h2>",
4104
4168
  '<p class="muted">No multi-agent meetings yet.</p>',
4105
4169
  "</section>"
@@ -4107,7 +4171,7 @@ function meetingsPanel(meetings) {
4107
4171
  }
4108
4172
  const rows = meetings.map(meetingRow).join("");
4109
4173
  return [
4110
- '<section class="panel" id="panel-meetings">',
4174
+ `<section class="panel" id="${id}">`,
4111
4175
  "<h2>Meetings</h2>",
4112
4176
  `<ul class="timeline">${rows}</ul>`,
4113
4177
  "</section>"
@@ -4122,20 +4186,54 @@ var STATUS_LABELS = {
4122
4186
  function statusBadge(status) {
4123
4187
  return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
4124
4188
  }
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
4189
  function shortSessionID(sessionID) {
4137
4190
  return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
4138
4191
  }
4192
+ 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>';
4193
+ 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>';
4194
+ 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>';
4195
+ 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>';
4196
+ function navButton(screen, label, icon, options = {}) {
4197
+ const active = options.active === true ? ' aria-current="page"' : "";
4198
+ const count = options.count === undefined ? "" : `<span class="count mono"${screen === "tree" ? " data-live-count" : ""}>${options.count}</span>`;
4199
+ const pill = options.pill === undefined ? "" : `<span class="pill warn">${escapeHtml(options.pill)}</span>`;
4200
+ 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>`;
4201
+ }
4202
+ function sidebarNav(liveCount, logCount) {
4203
+ return [
4204
+ '<aside class="sidebar" id="sidebar">',
4205
+ '<div class="brand">',
4206
+ '<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>',
4207
+ '<span class="name">openteam</span>',
4208
+ '<span class="tag">console</span>',
4209
+ "</div>",
4210
+ '<nav class="nav" aria-label="Console surfaces">',
4211
+ '<div class="nav-label">Operations</div>',
4212
+ navButton("tree", "Agent tree", ICON_TREE, {
4213
+ active: true,
4214
+ count: liveCount
4215
+ }),
4216
+ navButton("routing", "Routing + telemetry", ICON_ROUTING),
4217
+ navButton("logs", "Logs / activity", ICON_LOGS, { count: logCount }),
4218
+ navButton("config", "Configuration", ICON_CONFIG, { pill: "editable" }),
4219
+ "</nav>",
4220
+ '<div class="sidebar-foot">',
4221
+ '<div class="host-row"><span class="dot live"></span><span>loopback console</span></div>',
4222
+ '<div class="host-row faint"><span>prompts stay redacted by hash</span></div>',
4223
+ "</div>",
4224
+ "</aside>"
4225
+ ].join("");
4226
+ }
4227
+ function screenSection(screen, label, heading, description, body, active = false) {
4228
+ const className = active ? "screen active" : "screen";
4229
+ const hidden = active ? "" : " hidden";
4230
+ return [
4231
+ `<section class="${className}" id="screen-${screen}" role="tabpanel" aria-label="${escapeHtml(label)}"${hidden}>`,
4232
+ `<div class="screen-head"><h2>${heading}</h2><p>${description}</p></div>`,
4233
+ body,
4234
+ "</section>"
4235
+ ].join("");
4236
+ }
4139
4237
  function sessionHeaderPanel(tab) {
4140
4238
  const endpoint = tab.endpoint === undefined ? '<span class="muted">endpoint not announced</span>' : `<code>${escapeHtml(tab.endpoint.url)}</code>`;
4141
4239
  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 +4279,18 @@ function ptyPanel(tab) {
4181
4279
  "</section>"
4182
4280
  ].join("");
4183
4281
  }
4184
- function sessionView(tab, terminalEnabled, ptyEnabled) {
4282
+ function sessionDetailCard(tab, terminalEnabled, ptyEnabled, visible, index) {
4283
+ const idSuffix = `${index}-${domIdPart(tab.sessionID)}`;
4185
4284
  const body = [
4186
4285
  sessionHeaderPanel(tab),
4187
4286
  terminalEnabled ? terminalPanel(tab) : "",
4188
4287
  terminalEnabled && ptyEnabled ? ptyPanel(tab) : "",
4189
- routesPanel(tab.recentRoutes),
4190
- decisionsPanel(tab.decisions),
4191
- meetingsPanel(tab.meetings),
4192
- activityPanel(tab.activity)
4288
+ routesPanel(tab.recentRoutes, idSuffix),
4289
+ decisionsPanel(tab.decisions, idSuffix),
4290
+ meetingsPanel(tab.meetings, idSuffix),
4291
+ activityPanel(tab.activity, idSuffix)
4193
4292
  ].join("");
4194
- return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
4293
+ return `<article class="session-detail" id="session-${idSuffix}" data-session-view="${escapeHtml(tab.sessionID)}"${visible ? "" : " hidden"}>${body}</article>`;
4195
4294
  }
4196
4295
  var GRAPH_COL_W = 220;
4197
4296
  var GRAPH_ROW_H = 96;
@@ -4317,7 +4416,7 @@ function renderGraphView(projection, options = {}) {
4317
4416
  return graphDisabledHint();
4318
4417
  }
4319
4418
  if (projection === null) {
4320
- return '<p class="muted">No active graph run.</p>';
4419
+ return graphEmptyHint();
4321
4420
  }
4322
4421
  if (projection.nodes.length === 0) {
4323
4422
  return '<p class="muted">The run has no nodes.</p>';
@@ -4356,51 +4455,117 @@ function drillPanel() {
4356
4455
  "</aside>"
4357
4456
  ].join("");
4358
4457
  }
4359
- function graphViewPanel(projection, drilldownEnabled, disabled) {
4458
+ function treeGraphPanel(projection, drilldownEnabled, disabled) {
4360
4459
  return [
4361
- '<div class="view" data-view="graph" hidden>',
4362
- '<section class="panel" id="panel-graph">',
4363
- "<h2>Agent graph</h2>",
4460
+ '<section class="panel graph-panel" id="panel-graph">',
4461
+ "<h3>Live graph</h3>",
4364
4462
  disabled || projection === null ? "" : healthBanner(projection.health),
4463
+ '<div id="tree-content" data-graph-mount>',
4365
4464
  renderGraphView(projection, { disabled }),
4465
+ "</div>",
4366
4466
  !disabled && drilldownEnabled ? drillPanel() : "",
4367
- "</section>",
4368
- "</div>"
4467
+ "</section>"
4369
4468
  ].join("");
4370
4469
  }
4371
4470
  function graphDisabledHint() {
4372
4471
  return [
4373
4472
  '<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>",
4473
+ `<p class="muted">${escapeHtml(GRAPH_DISABLED_REMEDIATION)}</p>`,
4377
4474
  "</div>"
4378
4475
  ].join("");
4379
4476
  }
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 = [
4477
+ function graphEmptyHint() {
4478
+ return [
4479
+ '<div class="graph-empty" role="status">',
4480
+ `<p class="muted">${escapeHtml(GRAPH_EMPTY_REMEDIATION)}</p>`,
4481
+ "</div>"
4482
+ ].join("");
4483
+ }
4484
+ function routingScreenBody(state) {
4485
+ return [
4486
+ '<div class="screen-grid">',
4386
4487
  summaryPanel(state.cost),
4387
4488
  totalsPanel(state.totals),
4388
4489
  sessionsPanel(state.sessions),
4389
- loopPanel(state.loop),
4390
- teamPanel(state.team),
4391
4490
  tierPanel(state.cost),
4392
4491
  routesPanel(state.recentRoutes),
4492
+ "</div>"
4493
+ ].join("");
4494
+ }
4495
+ function emptySessionList() {
4496
+ return [
4497
+ '<div class="card">',
4498
+ '<div class="card-head"><h3>Sessions</h3><span class="sub">0</span></div>',
4499
+ '<div class="empty-state">No sessions recorded yet.</div>',
4500
+ "</div>"
4501
+ ].join("");
4502
+ }
4503
+ function sessionList(sessions) {
4504
+ if (sessions.length === 0) {
4505
+ return emptySessionList();
4506
+ }
4507
+ const buttons = sessions.map((tab, index) => {
4508
+ const selected = index === 0;
4509
+ return [
4510
+ `<button type="button" class="session-btn${selected ? " sel" : ""}" role="tab" aria-selected="${selected ? "true" : "false"}" data-session-target="${escapeHtml(tab.sessionID)}">`,
4511
+ `<span class="dot status-${tab.status}"></span>`,
4512
+ `<span class="sid">${escapeHtml(shortSessionID(tab.sessionID))}</span>`,
4513
+ `<span class="n">${tab.summary.messageCount}</span>`,
4514
+ "</button>"
4515
+ ].join("");
4516
+ }).join("");
4517
+ return [
4518
+ '<div class="card">',
4519
+ `<div class="card-head"><h3>Sessions</h3><span class="sub">${sessions.length}</span></div>`,
4520
+ '<div class="session-list" role="tablist" aria-label="Sessions">',
4521
+ buttons,
4522
+ "</div>",
4523
+ "</div>"
4524
+ ].join("");
4525
+ }
4526
+ function sessionDetailList(sessions, terminalEnabled, ptyEnabled) {
4527
+ if (sessions.length === 0) {
4528
+ return '<div class="card empty-state">No session activity recorded yet.</div>';
4529
+ }
4530
+ return sessions.map((tab, index) => sessionDetailCard(tab, terminalEnabled, ptyEnabled, index === 0, index)).join("");
4531
+ }
4532
+ function logsScreenBody(state, terminalEnabled, ptyEnabled) {
4533
+ return [
4534
+ '<div class="logs-layout">',
4535
+ sessionList(state.sessionsDetail),
4536
+ "<div>",
4537
+ sessionDetailList(state.sessionsDetail, terminalEnabled, ptyEnabled),
4538
+ "</div>",
4539
+ "</div>",
4540
+ '<div class="screen-grid">',
4541
+ loopPanel(state.loop),
4542
+ teamPanel(state.team),
4393
4543
  decisionsPanel(state.decisions),
4394
4544
  meetingsPanel(state.meetings),
4395
- activityPanel(state.activity)
4545
+ activityPanel(state.activity),
4546
+ "</div>"
4396
4547
  ].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("");
4548
+ }
4549
+ function configScreenBody(hasToken) {
4550
+ return renderConfigEditorPanel({ hasToken });
4551
+ }
4552
+ function renderConsoleHtml(state, options = {}) {
4553
+ const refreshMs = options.refreshMs ?? 2000;
4554
+ const terminalEnabled = options.terminalEnabled === true;
4555
+ const ptyEnabled = options.ptyEnabled === true;
4556
+ const graphOptionProvided = options.graphView !== undefined;
4557
+ const graphDrilldown = graphOptionProvided && options.drilldownEnabled === true;
4558
+ const graphProjection = options.graphView?.projection ?? null;
4559
+ const graphDisabled = options.graphView === undefined || options.graphView.disabled === true;
4560
+ const liveCount = graphOptionProvided && graphProjection !== null && !graphDisabled ? graphProjection.nodes.length : state.sessionsDetail.length;
4401
4561
  const configEnabled = options.configEditor !== undefined;
4402
- const configView = configEnabled ? renderConfigView({ hasToken: options.configEditor?.hasToken === true }) : "";
4403
- const body = allView + graphView + configView + sessionViews;
4562
+ const treeBody = treeGraphPanel(graphProjection, graphDrilldown, graphDisabled);
4563
+ const body = [
4564
+ 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),
4565
+ screenSection("routing", "Routing and telemetry", "Routing + telemetry", "Aggregate routing and cost telemetry from <code>GET /api/state</code> across all sessions.", routingScreenBody(state)),
4566
+ 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)),
4567
+ 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))
4568
+ ].join("");
4404
4569
  const session = state.session.id === undefined ? "" : ` · session ${escapeHtml(state.session.id)}`;
4405
4570
  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
4571
  return [
@@ -4410,16 +4575,24 @@ function renderConsoleHtml(state, options = {}) {
4410
4575
  '<meta charset="utf-8">',
4411
4576
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
4412
4577
  "<title>openteam console</title>",
4413
- `<style>${CONSOLE_STYLE}${graphEnabled ? GRAPH_STYLE : ""}</style>`,
4578
+ `<style>${CONSOLE_STYLE}${graphOptionProvided ? GRAPH_STYLE : ""}</style>`,
4414
4579
  "</head>",
4415
4580
  "<body>",
4416
- "<header>",
4417
- "<h1>openteam console</h1>",
4418
- `<span class="meta">updated ${escapeHtml(state.generatedAt)}${session}</span>`,
4581
+ '<div class="app">',
4582
+ sidebarNav(liveCount, state.sessionsDetail.length),
4583
+ '<div class="scrim" data-close-nav></div>',
4584
+ '<main class="main">',
4585
+ '<header class="topbar">',
4586
+ '<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>',
4587
+ '<h1 id="screen-title">Agent tree</h1>',
4588
+ `<span class="refresh" title="SSE + polling"><span class="beat"></span> ${refreshMs} ms · SSE</span>`,
4589
+ '<div class="spacer"></div>',
4590
+ `<span class="token-chip" title="Page status"><span class="lbl">updated</span> <b class="mono">${escapeHtml(state.generatedAt)}${session}</b></span>`,
4419
4591
  "</header>",
4420
4592
  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>`,
4593
+ `<div class="scroll">${body}</div>`,
4594
+ "</main>",
4595
+ "</div>",
4423
4596
  `<script>${CONSOLE_CLIENT_JS}</script>`,
4424
4597
  "</body>",
4425
4598
  "</html>"
@@ -4482,23 +4655,21 @@ function renderConfigSaveSummary(write) {
4482
4655
  }
4483
4656
  return `<div class="config-summary">${parts.join("")}</div>`;
4484
4657
  }
4485
- function renderConfigView(opts) {
4658
+ function renderConfigEditorPanel(opts) {
4486
4659
  const inner = opts.hasToken ? [
4487
4660
  '<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
4661
  '<div class="config-mount" data-config-mount><p class="muted">Loading configuration…</p></div>'
4489
4662
  ].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
4663
  return [
4491
- '<div class="view" data-view="config" hidden>',
4492
4664
  '<section class="panel config-panel">',
4493
- "<h2>Configuration</h2>",
4665
+ "<h3>Configuration JSON</h3>",
4494
4666
  inner,
4495
- "</section>",
4496
- "</div>"
4667
+ "</section>"
4497
4668
  ].join("");
4498
4669
  }
4499
4670
 
4500
4671
  // src/web/configRoute.ts
4501
- var JSON_HEADERS = { "content-type": "application/json" };
4672
+ var JSON_HEADERS2 = { "content-type": "application/json" };
4502
4673
  var LOOPBACK_HOSTS = new Set([
4503
4674
  "127.0.0.1",
4504
4675
  "localhost",
@@ -4506,7 +4677,7 @@ var LOOPBACK_HOSTS = new Set([
4506
4677
  "::1"
4507
4678
  ]);
4508
4679
  var MAX_CONFIG_BODY_CHARS = 256 * 1024;
4509
- function bearerFromHeader(request) {
4680
+ function bearerFromHeader2(request) {
4510
4681
  const header = request.headers.authorization;
4511
4682
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
4512
4683
  return;
@@ -4519,7 +4690,7 @@ function queryToken(query) {
4519
4690
  return value === null || value.length === 0 ? undefined : value;
4520
4691
  }
4521
4692
  function readAuthorized(request, query, token) {
4522
- const provided = bearerFromHeader(request) ?? queryToken(query);
4693
+ const provided = bearerFromHeader2(request) ?? queryToken(query);
4523
4694
  return provided !== undefined && safeEqualToken(provided, token);
4524
4695
  }
4525
4696
  function isCrossOriginMutation(request) {
@@ -4570,34 +4741,34 @@ async function handleConfigRoute(request, response, url, deps) {
4570
4741
  }
4571
4742
  if (request.method === "GET") {
4572
4743
  if (!readAuthorized(request, url.searchParams, deps.token)) {
4573
- response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
4744
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
4574
4745
  return true;
4575
4746
  }
4576
4747
  const result = await deps.editor.read();
4577
- response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ...result, render: renderConfigEditorForm(result) }));
4748
+ response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ ...result, render: renderConfigEditorForm(result) }));
4578
4749
  return true;
4579
4750
  }
4580
4751
  if (request.method === "POST") {
4581
4752
  if (isCrossOriginMutation(request)) {
4582
- response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "cross-origin forbidden" }));
4753
+ response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "cross-origin forbidden" }));
4583
4754
  return true;
4584
4755
  }
4585
- const bearer = bearerFromHeader(request);
4756
+ const bearer = bearerFromHeader2(request);
4586
4757
  if (bearer === undefined || !safeEqualToken(bearer, deps.token)) {
4587
- response.writeHead(401, JSON_HEADERS).end(JSON.stringify({ error: "unauthorized" }));
4758
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
4588
4759
  return true;
4589
4760
  }
4590
4761
  const body = await readJsonBody(request, MAX_CONFIG_BODY_CHARS);
4591
4762
  if (!body.ok) {
4592
- response.writeHead(400, JSON_HEADERS).end(JSON.stringify({ ok: false, error: "invalid JSON body" }));
4763
+ response.writeHead(400, JSON_HEADERS2).end(JSON.stringify({ ok: false, error: "invalid JSON body" }));
4593
4764
  return true;
4594
4765
  }
4595
4766
  const result = await deps.editor.write(body.value);
4596
4767
  const responseBody = result.status === 200 ? { ...result.body, render: renderConfigSaveSummary(result.body) } : result.body;
4597
- response.writeHead(result.status, JSON_HEADERS).end(JSON.stringify(responseBody));
4768
+ response.writeHead(result.status, JSON_HEADERS2).end(JSON.stringify(responseBody));
4598
4769
  return true;
4599
4770
  }
4600
- response.writeHead(405, JSON_HEADERS).end(JSON.stringify({ error: "method not allowed" }));
4771
+ response.writeHead(405, JSON_HEADERS2).end(JSON.stringify({ error: "method not allowed" }));
4601
4772
  return true;
4602
4773
  }
4603
4774
  function createConfigReloader(deps) {
@@ -4921,8 +5092,8 @@ var SSE_HEADERS = {
4921
5092
  "cache-control": "no-cache",
4922
5093
  connection: "keep-alive"
4923
5094
  };
4924
- var JSON_HEADERS2 = { "content-type": "application/json" };
4925
- function bearerFromHeader2(request) {
5095
+ var JSON_HEADERS3 = { "content-type": "application/json" };
5096
+ function bearerFromHeader3(request) {
4926
5097
  const header = request.headers.authorization;
4927
5098
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
4928
5099
  return;
@@ -4930,8 +5101,8 @@ function bearerFromHeader2(request) {
4930
5101
  const value = header.slice("Bearer ".length).trim();
4931
5102
  return value.length === 0 ? undefined : value;
4932
5103
  }
4933
- function authorized(request, query, token) {
4934
- const provided = tokenFromQuery(query) ?? bearerFromHeader2(request);
5104
+ function authorized2(request, query, token) {
5105
+ const provided = tokenFromQuery(query) ?? bearerFromHeader3(request);
4935
5106
  return provided !== undefined && safeEqualToken(provided, token);
4936
5107
  }
4937
5108
  async function readJsonBody2(request, limit) {
@@ -4974,7 +5145,7 @@ function parseConsolePath(path) {
4974
5145
  async function handleStream(request, response, sessionID, deps, diagnostic) {
4975
5146
  const endpoint = await deps.resolveEndpoint(sessionID);
4976
5147
  if (endpoint === undefined) {
4977
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5148
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
4978
5149
  return;
4979
5150
  }
4980
5151
  response.writeHead(200, SSE_HEADERS);
@@ -4995,31 +5166,31 @@ data: {}
4995
5166
  async function handleMessages(_request, response, sessionID, deps, diagnostic) {
4996
5167
  const endpoint = await deps.resolveEndpoint(sessionID);
4997
5168
  if (endpoint === undefined) {
4998
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5169
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
4999
5170
  return;
5000
5171
  }
5001
5172
  const frames = await deps.createClient(endpoint).listMessages(sessionID, { diagnostic });
5002
- response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({ frames }));
5173
+ response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({ frames }));
5003
5174
  }
5004
5175
  async function handleInput(request, response, sessionID, deps) {
5005
5176
  const endpoint = await deps.resolveEndpoint(sessionID);
5006
5177
  if (endpoint === undefined) {
5007
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5178
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5008
5179
  return;
5009
5180
  }
5010
5181
  const body = await readJsonBody2(request, MAX_INPUT_CHARS + 1024);
5011
5182
  const input = parseSessionInput(body);
5012
5183
  if (input === undefined) {
5013
- response.writeHead(400, JSON_HEADERS2).end(JSON.stringify({ error: "invalid input" }));
5184
+ response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid input" }));
5014
5185
  return;
5015
5186
  }
5016
5187
  await deps.createClient(endpoint).sendPrompt(sessionID, input.text);
5017
- response.writeHead(202, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
5188
+ response.writeHead(202, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
5018
5189
  }
5019
5190
  async function handlePermission(request, response, sessionID, deps) {
5020
5191
  const endpoint = await deps.resolveEndpoint(sessionID);
5021
5192
  if (endpoint === undefined) {
5022
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5193
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5023
5194
  return;
5024
5195
  }
5025
5196
  const body = await readJsonBody2(request, 4096);
@@ -5027,34 +5198,34 @@ async function handlePermission(request, response, sessionID, deps) {
5027
5198
  const permissionID = record.permissionID;
5028
5199
  const responseValue = record.response;
5029
5200
  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" }));
5201
+ response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid permission" }));
5031
5202
  return;
5032
5203
  }
5033
5204
  await deps.createClient(endpoint).respondPermission(sessionID, permissionID, responseValue);
5034
- response.writeHead(202, JSON_HEADERS2).end(JSON.stringify({ ok: true }));
5205
+ response.writeHead(202, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
5035
5206
  }
5036
5207
  async function handlePty(_request, response, sessionID, deps) {
5037
5208
  if (deps.ptyEnabled !== true || deps.createPtyClient === undefined) {
5038
- response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "pty disabled" }));
5209
+ response.writeHead(403, JSON_HEADERS3).end(JSON.stringify({ error: "pty disabled" }));
5039
5210
  return;
5040
5211
  }
5041
5212
  const endpoint = await deps.resolveEndpoint(sessionID);
5042
5213
  if (endpoint === undefined) {
5043
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "no endpoint" }));
5214
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "no endpoint" }));
5044
5215
  return;
5045
5216
  }
5046
5217
  const client = deps.createPtyClient(endpoint);
5047
5218
  const pty = await client.createPty({ command: "", title: "openteam" });
5048
5219
  if (pty === undefined) {
5049
- response.writeHead(502, JSON_HEADERS2).end(JSON.stringify({ error: "pty create failed" }));
5220
+ response.writeHead(502, JSON_HEADERS3).end(JSON.stringify({ error: "pty create failed" }));
5050
5221
  return;
5051
5222
  }
5052
5223
  const token = await client.connectToken(pty.id);
5053
5224
  if (token === undefined) {
5054
- response.writeHead(502, JSON_HEADERS2).end(JSON.stringify({ error: "connect token failed" }));
5225
+ response.writeHead(502, JSON_HEADERS3).end(JSON.stringify({ error: "connect token failed" }));
5055
5226
  return;
5056
5227
  }
5057
- response.writeHead(200, JSON_HEADERS2).end(JSON.stringify({
5228
+ response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({
5058
5229
  ptyID: pty.id,
5059
5230
  wsUrl: client.wsUrl(pty.id, token.ticket)
5060
5231
  }));
@@ -5065,17 +5236,17 @@ async function handleConsoleRoute(request, response, url, deps) {
5065
5236
  }
5066
5237
  const route = parseConsolePath(url.pathname);
5067
5238
  if (route === undefined) {
5068
- response.writeHead(404, JSON_HEADERS2).end(JSON.stringify({ error: "not found" }));
5239
+ response.writeHead(404, JSON_HEADERS3).end(JSON.stringify({ error: "not found" }));
5069
5240
  return true;
5070
5241
  }
5071
5242
  const readOnlyAction = route.action === "stream" || route.action === "messages";
5072
5243
  const channelEnabled = deps.terminalEnabled || readOnlyAction && deps.drilldownEnabled === true;
5073
5244
  if (!channelEnabled) {
5074
- response.writeHead(403, JSON_HEADERS2).end(JSON.stringify({ error: "terminal disabled" }));
5245
+ response.writeHead(403, JSON_HEADERS3).end(JSON.stringify({ error: "terminal disabled" }));
5075
5246
  return true;
5076
5247
  }
5077
- if (!authorized(request, url.searchParams, deps.token)) {
5078
- response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
5248
+ if (!authorized2(request, url.searchParams, deps.token)) {
5249
+ response.writeHead(401, JSON_HEADERS3).end(JSON.stringify({ error: "unauthorized" }));
5079
5250
  return true;
5080
5251
  }
5081
5252
  const diagnostic = deps.diagnosticEnabled === true && url.searchParams.get("diagnostic") === "1";
@@ -5099,45 +5270,7 @@ async function handleConsoleRoute(request, response, url, deps) {
5099
5270
  await handlePty(request, response, route.sessionID, deps);
5100
5271
  return true;
5101
5272
  }
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));
5273
+ response.writeHead(405, JSON_HEADERS3).end(JSON.stringify({ error: "method not allowed" }));
5141
5274
  return true;
5142
5275
  }
5143
5276
 
@@ -5408,6 +5541,14 @@ async function createConsoleServer(deps) {
5408
5541
  if (graphDeps !== undefined && await handleGraphRoute(request, response, url2, graphDeps)) {
5409
5542
  return;
5410
5543
  }
5544
+ if (graphDeps === undefined && url2.pathname === "/api/graph") {
5545
+ if (request.method !== "GET") {
5546
+ response.writeHead(405, { "content-type": "application/json" }).end(JSON.stringify({ error: "method not allowed" }));
5547
+ return;
5548
+ }
5549
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(GRAPH_DISABLED_ENVELOPE));
5550
+ return;
5551
+ }
5411
5552
  if (deps.storageApi !== undefined && await handleStorageRoute(request, response, url2, deps.storageApi)) {
5412
5553
  return;
5413
5554
  }
@@ -5982,7 +6123,7 @@ async function createConsoleRuntime(deps) {
5982
6123
  });
5983
6124
  const createServer = deps.serve ?? createConsoleServer;
5984
6125
  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;
6126
+ const graphDisabled = graphApi === undefined;
5986
6127
  const server = await createServer({
5987
6128
  readSnapshot,
5988
6129
  config: deps.config,
@@ -6960,7 +7101,7 @@ async function runConsoleServe(deps, options) {
6960
7101
  endpoint,
6961
7102
  ...deps.serverPassword !== undefined ? { password: deps.serverPassword } : {}
6962
7103
  }));
6963
- const graphMode = config.graph?.mode ?? "off";
7104
+ const graphMode = config.graph?.mode ?? "shadow";
6964
7105
  const graphViewEnabled = config.console.graphView?.enabled === true;
6965
7106
  const buildGraphView2 = deps.createGraphViewProvider ?? createGraphViewProvider;
6966
7107
  const graphViewProvider = graphMode !== "off" && graphViewEnabled ? buildGraphView2({
@@ -8363,7 +8504,7 @@ import { z as z16 } from "zod";
8363
8504
 
8364
8505
  // src/config/resolve.ts
8365
8506
  function resolveGraphConfig(config, source) {
8366
- const configuredMode = config.graph?.mode ?? "off";
8507
+ const configuredMode = config.graph?.mode ?? "shadow";
8367
8508
  const killSwitch = config.graph?.killSwitch ?? false;
8368
8509
  const effectiveMode = killSwitch ? "off" : configuredMode;
8369
8510
  return {
@@ -15315,7 +15456,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
15315
15456
  // package.json
15316
15457
  var package_default = {
15317
15458
  name: "@jmanuelcorral/openteam",
15318
- version: "0.11.0",
15459
+ version: "0.13.0",
15319
15460
  packageManager: "bun@1.3.14",
15320
15461
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
15321
15462
  license: "MIT",