omnius 1.0.600 → 1.0.602

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -678140,6 +678140,8 @@ ${AGENTS_END}`;
678140
678140
  var daemon_exports = {};
678141
678141
  __export(daemon_exports, {
678142
678142
  claimDaemonEndpoint: () => claimDaemonEndpoint,
678143
+ daemonPortFromServiceEnvironment: () => daemonPortFromServiceEnvironment,
678144
+ daemonServiceMatchesPort: () => daemonServiceMatchesPort,
678143
678145
  ensureDaemon: () => ensureDaemon,
678144
678146
  ensureDaemonVersion: () => ensureDaemonVersion,
678145
678147
  forceKillDaemon: () => forceKillDaemon,
@@ -678155,7 +678157,8 @@ __export(daemon_exports, {
678155
678157
  releaseDaemonEndpointForCurrentProcess: () => releaseDaemonEndpointForCurrentProcess,
678156
678158
  restartDaemon: () => restartDaemon,
678157
678159
  startDaemon: () => startDaemon,
678158
- stopDaemon: () => stopDaemon
678160
+ stopDaemon: () => stopDaemon,
678161
+ stopDaemonAtPort: () => stopDaemonAtPort
678159
678162
  });
678160
678163
  import { spawn as spawn28 } from "node:child_process";
678161
678164
  import { existsSync as existsSync116, readFileSync as readFileSync94, writeFileSync as writeFileSync60, mkdirSync as mkdirSync69, unlinkSync as unlinkSync22, openSync as openSync4, closeSync as closeSync4, writeSync as writeSync3, statSync as statSync46, renameSync as renameSync17 } from "node:fs";
@@ -678279,7 +678282,9 @@ async function isDaemonRunning(port) {
678279
678282
  const resp = await fetch(`http://127.0.0.1:${p2}/health`, {
678280
678283
  signal: AbortSignal.timeout(2e3)
678281
678284
  });
678282
- return resp.ok;
678285
+ if (!resp.ok) return false;
678286
+ const health = await resp.json();
678287
+ return health.status === "ok" && (typeof health.boot_version === "string" || typeof health.version === "string");
678283
678288
  } catch {
678284
678289
  return false;
678285
678290
  }
@@ -678345,12 +678350,49 @@ async function runUserSystemctl(args, timeout2 = 2e4) {
678345
678350
  return { available: false, ok: false };
678346
678351
  }
678347
678352
  }
678353
+ async function readUserSystemctl(args, timeout2 = 5e3) {
678354
+ try {
678355
+ const { spawnSync: spawnSync11 } = await import("node:child_process");
678356
+ const result = spawnSync11("systemctl", ["--user", ...args], {
678357
+ encoding: "utf8",
678358
+ stdio: ["ignore", "pipe", "ignore"],
678359
+ timeout: timeout2
678360
+ });
678361
+ return result.status === 0 ? result.stdout.trim() : null;
678362
+ } catch {
678363
+ return null;
678364
+ }
678365
+ }
678348
678366
  async function hasManagedDaemonService() {
678349
678367
  const enabled2 = await runUserSystemctl(["is-enabled", "omnius-daemon.service"], 5e3);
678350
678368
  if (enabled2.ok) return true;
678351
678369
  const active = await runUserSystemctl(["is-active", "omnius-daemon.service"], 5e3);
678352
678370
  return active.ok;
678353
678371
  }
678372
+ function daemonPortFromServiceEnvironment(value2) {
678373
+ const host = value2.match(/(?:^|\s)OMNIUS_HOST=(?:"([^"]+)"|'([^']+)'|([^\s]+))/)?.slice(1).find(Boolean);
678374
+ const direct = value2.match(/(?:^|\s)OMNIUS_PORT=(?:"(\d+)"|'(\d+)'|(\d+))/)?.slice(1).find(Boolean);
678375
+ const candidate = host?.match(/:(\d+)$/)?.[1] ?? direct;
678376
+ if (!candidate) return null;
678377
+ const port = Number(candidate);
678378
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
678379
+ }
678380
+ function daemonServiceMatchesPort(requestedPort, effectiveEnvironment) {
678381
+ const effectivePort2 = daemonPortFromServiceEnvironment(effectiveEnvironment);
678382
+ return effectivePort2 === null ? requestedPort === DEFAULT_PORT2 : effectivePort2 === requestedPort;
678383
+ }
678384
+ async function managedDaemonServiceMatchesPort(port) {
678385
+ if (!await hasManagedDaemonService()) return false;
678386
+ const environment = await readUserSystemctl([
678387
+ "show",
678388
+ "omnius-daemon.service",
678389
+ "--property",
678390
+ "Environment",
678391
+ "--value"
678392
+ ]);
678393
+ if (environment === null) return false;
678394
+ return daemonServiceMatchesPort(port, environment);
678395
+ }
678354
678396
  function systemdQuote(value2) {
678355
678397
  return /[\s"\\]/.test(value2) ? `"${value2.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value2;
678356
678398
  }
@@ -678448,6 +678490,43 @@ async function waitForDaemonStopped(port, attempts = DAEMON_GRACEFUL_STOP_ATTEMP
678448
678490
  }
678449
678491
  return false;
678450
678492
  }
678493
+ async function reclaimStaleDaemonEndpointClaim(port) {
678494
+ const lockFile = daemonLockFile(port);
678495
+ const record = readDaemonLock(lockFile);
678496
+ if (!record) return true;
678497
+ if (!await daemonPortIsFree(port)) return true;
678498
+ if (!processIsAlive(record.pid)) {
678499
+ const current2 = readDaemonLock(lockFile);
678500
+ if (current2?.pid === record.pid && current2.token === record.token) {
678501
+ try {
678502
+ unlinkSync22(lockFile);
678503
+ } catch {
678504
+ }
678505
+ }
678506
+ return true;
678507
+ }
678508
+ const lease = listProcessLeases({ includeInactive: false }).find(
678509
+ (item) => item.status === "active" && item.pid === record.pid && item.ownerKind === "daemon" && item.ownerId === `daemon:${port}`
678510
+ );
678511
+ if (!lease) return false;
678512
+ const stopped = await stopProcessLease(lease.leaseId, {
678513
+ reason: `stale daemon endpoint claim on port ${port}`,
678514
+ termGraceMs: 1e3
678515
+ });
678516
+ if (stopped.action !== "killed" && stopped.action !== "dead") return false;
678517
+ for (let attempt = 0; attempt < 20 && processIsAlive(record.pid); attempt++) {
678518
+ await delay3(100);
678519
+ }
678520
+ if (processIsAlive(record.pid)) return false;
678521
+ const current = readDaemonLock(lockFile);
678522
+ if (current?.pid === record.pid && current.token === record.token) {
678523
+ try {
678524
+ unlinkSync22(lockFile);
678525
+ } catch {
678526
+ }
678527
+ }
678528
+ return daemonPortIsFree(port);
678529
+ }
678451
678530
  async function reclaimOwnedDaemonListener(port = getDaemonPort(), dependencies = DEFAULT_RECLAIM_DEPENDENCIES) {
678452
678531
  const holders = await dependencies.holderPids(port);
678453
678532
  if (holders === null) {
@@ -678523,7 +678602,7 @@ async function reclaimOwnedDaemonListener(port = getDaemonPort(), dependencies =
678523
678602
  }
678524
678603
  async function restartDaemon(port, expectedVersion) {
678525
678604
  const p2 = port ?? getDaemonPort();
678526
- const managed = await hasManagedDaemonService();
678605
+ const managed = await managedDaemonServiceMatchesPort(p2);
678527
678606
  if (managed) {
678528
678607
  const restarted = await runUserSystemctl(["restart", "omnius-daemon.service"]);
678529
678608
  if (restarted.ok && (await waitForDaemonReady(p2, expectedVersion)).ok) return true;
@@ -678544,6 +678623,7 @@ async function restartDaemon(port, expectedVersion) {
678544
678623
  const reclaimed = await reclaimOwnedDaemonListener(p2);
678545
678624
  if (!reclaimed.ok) return false;
678546
678625
  }
678626
+ if (!await reclaimStaleDaemonEndpointClaim(p2)) return false;
678547
678627
  const pid = await startDaemon(p2);
678548
678628
  if (!pid) return false;
678549
678629
  return (await waitForDaemonReady(p2, expectedVersion)).ok;
@@ -678650,7 +678730,7 @@ async function startDaemon(port = getDaemonPort()) {
678650
678730
  projectRoot: process.cwd(),
678651
678731
  lifecycle: "daemon",
678652
678732
  persistent: true,
678653
- reason: `shared API daemon on port ${getDaemonPort()}`,
678733
+ reason: `shared API daemon on port ${daemonPort}`,
678654
678734
  command: [daemonCommand.command, ...daemonCommand.args].join(" "),
678655
678735
  cwd: process.cwd()
678656
678736
  });
@@ -678695,6 +678775,17 @@ function stopDaemon() {
678695
678775
  return false;
678696
678776
  }
678697
678777
  }
678778
+ async function stopDaemonAtPort(port = getDaemonPort()) {
678779
+ if (await managedDaemonServiceMatchesPort(port)) {
678780
+ const stopped = await runUserSystemctl(["stop", "omnius-daemon.service"]);
678781
+ if (stopped.ok && await waitForDaemonStopped(port)) return true;
678782
+ }
678783
+ const wasRunning = await isDaemonRunning(port);
678784
+ const reclaimed = await reclaimOwnedDaemonListener(port);
678785
+ if (!reclaimed.ok) return false;
678786
+ const claimCleared = await reclaimStaleDaemonEndpointClaim(port);
678787
+ return claimCleared && (wasRunning || reclaimed.action === "cleared");
678788
+ }
678698
678789
  async function forceKillDaemon(port) {
678699
678790
  const p2 = port ?? getDaemonPort();
678700
678791
  let killed = 0;
@@ -678804,6 +678895,9 @@ async function ensureDaemonVersion(expectedVersion = getLocalCliVersion(), port
678804
678895
  process.platform === "linux" && reclaimed.action === "blocked"
678805
678896
  );
678806
678897
  }
678898
+ if (!await reclaimStaleDaemonEndpointClaim(port)) {
678899
+ return finish(false, "failed", null, 0, process.platform === "linux");
678900
+ }
678807
678901
  getDaemonPid();
678808
678902
  const pid = await startDaemon(port);
678809
678903
  if (!pid) {
@@ -678888,12 +678982,15 @@ __export(tray_exports, {
678888
678982
  resolveTrayEndpoint: () => resolveTrayEndpoint,
678889
678983
  resolveTrayLaunchCommand: () => resolveTrayLaunchCommand,
678890
678984
  resolveTrayPaths: () => resolveTrayPaths,
678985
+ resolveTrayRegistrationCommand: () => resolveTrayRegistrationCommand,
678891
678986
  runTrayForeground: () => runTrayForeground,
678892
678987
  startTray: () => startTray,
678893
678988
  stopTray: () => stopTray,
678989
+ trayProcessCommandLooksOwned: () => trayProcessCommandLooksOwned,
678894
678990
  trayRequiresRestart: () => trayRequiresRestart,
678895
678991
  traySupport: () => traySupport,
678896
678992
  trayUpdatePresentation: () => trayUpdatePresentation,
678993
+ trayVersionPresentation: () => trayVersionPresentation,
678897
678994
  uninstallTrayAutostart: () => uninstallTrayAutostart,
678898
678995
  verifyTrayHelper: () => verifyTrayHelper
678899
678996
  });
@@ -678906,6 +679003,7 @@ import {
678906
679003
  mkdirSync as mkdirSync70,
678907
679004
  openSync as openSync5,
678908
679005
  readFileSync as readFileSync95,
679006
+ realpathSync,
678909
679007
  renameSync as renameSync18,
678910
679008
  unlinkSync as unlinkSync23,
678911
679009
  writeFileSync as writeFileSync61,
@@ -678921,7 +679019,8 @@ function trayUpdatePresentation(currentVersion, latestVersion, state = null) {
678921
679019
  title: `Updating to v${state.target_version} — ${state.phase.replaceAll("_", " ")}`,
678922
679020
  tooltip: `Operation ${state.operation_id}; progress is shared with the dashboard and CLI`,
678923
679021
  enabled: false,
678924
- targetVersion: state.target_version
679022
+ targetVersion: state.target_version,
679023
+ action: "update-omnius"
678925
679024
  };
678926
679025
  }
678927
679026
  if (state?.status === "failed" && state.target_version === latestVersion) {
@@ -678929,7 +679028,8 @@ function trayUpdatePresentation(currentVersion, latestVersion, state = null) {
678929
679028
  title: `Update to v${state.target_version} failed — retry`,
678930
679029
  tooltip: state.remediation || state.error || "Open the tray logs for update diagnostics",
678931
679030
  enabled: true,
678932
- targetVersion: state.target_version
679031
+ targetVersion: state.target_version,
679032
+ action: "update-omnius"
678933
679033
  };
678934
679034
  }
678935
679035
  if (currentVersion && latestVersion) {
@@ -678937,12 +679037,31 @@ function trayUpdatePresentation(currentVersion, latestVersion, state = null) {
678937
679037
  title: `Update Omnius to v${latestVersion}`,
678938
679038
  tooltip: `Install and verify the global package, executable, daemon, and tray (current v${currentVersion})`,
678939
679039
  enabled: true,
678940
- targetVersion: latestVersion
679040
+ targetVersion: latestVersion,
679041
+ action: "update-omnius"
678941
679042
  };
678942
679043
  }
678943
679044
  return {
678944
- title: currentVersion ? `Omnius v${currentVersion} is up to date` : "Check for Omnius updates",
678945
- tooltip: "Automatic update checks use the same cached registry result as the CLI and dashboard",
679045
+ title: currentVersion ? `Check for Omnius updates (v${currentVersion})` : "Check for Omnius updates",
679046
+ tooltip: "Check npm now; automatic checks continue in the background",
679047
+ enabled: Boolean(currentVersion),
679048
+ action: "check-update"
679049
+ };
679050
+ }
679051
+ function trayVersionPresentation(health, update2) {
679052
+ if (health.kind === "online" && update2.targetVersion) {
679053
+ const current = health.version ? `v${health.version}` : "current version";
679054
+ const running = !update2.enabled && update2.title.startsWith("Updating ");
679055
+ return {
679056
+ title: running ? update2.title : `Omnius ${current} — update to v${update2.targetVersion} available`,
679057
+ tooltip: update2.tooltip,
679058
+ enabled: update2.enabled,
679059
+ action: "update-omnius"
679060
+ };
679061
+ }
679062
+ return {
679063
+ title: health.label,
679064
+ tooltip: health.tooltip,
678946
679065
  enabled: false
678947
679066
  };
678948
679067
  }
@@ -678994,19 +679113,6 @@ function normalizeTrayEndpoint(value2) {
678994
679113
  return null;
678995
679114
  }
678996
679115
  }
678997
- function systemdDaemonEnvironment() {
678998
- if (process.platform !== "linux") return "";
678999
- try {
679000
- const result = spawnSync9(
679001
- "systemctl",
679002
- ["--user", "show", SERVICE_LABEL, "--property", "Environment", "--value"],
679003
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }
679004
- );
679005
- return result.status === 0 ? result.stdout.trim() : "";
679006
- } catch {
679007
- return "";
679008
- }
679009
- }
679010
679116
  function endpointFromServiceEnvironment(value2) {
679011
679117
  const host = value2.match(/(?:^|\s)OMNIUS_HOST=(?:"([^"]+)"|'([^']+)'|([^\s]+))/)?.slice(1).find(Boolean);
679012
679118
  const port = value2.match(/(?:^|\s)OMNIUS_PORT=(?:"(\d+)"|'(\d+)'|(\d+))/)?.slice(1).find(Boolean);
@@ -679018,7 +679124,6 @@ function resolveTrayEndpoint(explicit) {
679018
679124
  process.env["OMNIUS_TRAY_ENDPOINT"],
679019
679125
  process.env["OMNIUS_HOST"],
679020
679126
  process.env["OMNIUS_PORT"],
679021
- endpointFromServiceEnvironment(systemdDaemonEnvironment()),
679022
679127
  DEFAULT_ENDPOINT
679023
679128
  ];
679024
679129
  for (const candidate of candidates) {
@@ -679158,6 +679263,30 @@ function readProcessState(path16) {
679158
679263
  return null;
679159
679264
  }
679160
679265
  }
679266
+ function processCommandLine(pid) {
679267
+ try {
679268
+ if (process.platform === "linux") {
679269
+ return readFileSync95(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ");
679270
+ }
679271
+ if (process.platform !== "win32") {
679272
+ return spawnSync9("ps", ["-p", String(pid), "-o", "command="], {
679273
+ encoding: "utf8",
679274
+ stdio: ["ignore", "pipe", "ignore"],
679275
+ timeout: 2e3
679276
+ }).stdout.trim();
679277
+ }
679278
+ return spawnSync9("wmic", ["process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value"], {
679279
+ encoding: "utf8",
679280
+ stdio: ["ignore", "pipe", "ignore"],
679281
+ timeout: 2e3
679282
+ }).stdout;
679283
+ } catch {
679284
+ return "";
679285
+ }
679286
+ }
679287
+ function trayProcessCommandLooksOwned(command) {
679288
+ return /(?:^|\s)tray\s+run(?:\s|$)/i.test(command) && (/(?:^|[\\/\s])omnius(?:[\\/\s]|$)/i.test(command) || /[\\/]dist[\\/]index\.js\b|launcher\.cjs\b/i.test(command));
679289
+ }
679161
679290
  function writeProcessState(paths, state) {
679162
679291
  mkdirSync70(paths.runtimeDir, { recursive: true, mode: 448 });
679163
679292
  const temporary = `${paths.stateFile}.${process.pid}.tmp`;
@@ -679232,6 +679361,34 @@ function resolveTrayLaunchCommand() {
679232
679361
  }
679233
679362
  return null;
679234
679363
  }
679364
+ function installedOmniusLaunchCommand() {
679365
+ try {
679366
+ const result = spawnSync9(process.platform === "win32" ? "where" : "which", ["omnius"], {
679367
+ encoding: "utf8",
679368
+ stdio: ["ignore", "pipe", "ignore"],
679369
+ timeout: 2e3
679370
+ });
679371
+ const first2 = result.stdout?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
679372
+ return first2 && existsSync117(first2) ? commandForEntrypoint2(realpathSync(first2)) : null;
679373
+ } catch {
679374
+ return null;
679375
+ }
679376
+ }
679377
+ function resolveTrayRegistrationCommand() {
679378
+ return installedOmniusLaunchCommand() ?? resolveTrayLaunchCommand();
679379
+ }
679380
+ function registrationIconPath(launch) {
679381
+ const extension3 = process.platform === "win32" ? "ico" : "png";
679382
+ const entrypoints = [...launch.args, launch.command].filter((candidate) => existsSync117(candidate));
679383
+ for (const entrypoint of entrypoints) {
679384
+ const real = realpathSync(entrypoint);
679385
+ for (const packageRoot of [dirname43(real), join129(dirname43(real), "..")]) {
679386
+ const candidate = join129(packageRoot, "assets", "tray", `omnius-online.${extension3}`);
679387
+ if (existsSync117(candidate)) return candidate;
679388
+ }
679389
+ }
679390
+ return assetPath("online");
679391
+ }
679235
679392
  function desktopExecQuote(value2) {
679236
679393
  return `"${value2.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
679237
679394
  }
@@ -679285,13 +679442,13 @@ start "" /b ${argv.map(windowsCmdQuote).join(" ")}\r
679285
679442
  throw new Error(`Autostart is not supported on ${platform8}`);
679286
679443
  }
679287
679444
  function writeTrayAutostart(endpoint) {
679288
- const launch = resolveTrayLaunchCommand();
679445
+ const launch = resolveTrayRegistrationCommand();
679289
679446
  if (!launch) throw new Error("Could not resolve the installed Omnius CLI entrypoint");
679290
679447
  const paths = resolveTrayPaths();
679291
679448
  mkdirSync70(dirname43(paths.autostartFile), { recursive: true });
679292
679449
  writeFileSync61(
679293
679450
  paths.autostartFile,
679294
- buildAutostartContent(process.platform, launch, endpoint, assetPath("online")),
679451
+ buildAutostartContent(process.platform, launch, endpoint, registrationIconPath(launch)),
679295
679452
  { encoding: "utf8", mode: process.platform === "win32" ? 448 : 384 }
679296
679453
  );
679297
679454
  return paths.autostartFile;
@@ -679313,44 +679470,12 @@ function uninstallTrayAutostart() {
679313
679470
  unlinkSync23(paths.autostartFile);
679314
679471
  return true;
679315
679472
  }
679316
- function runServiceControl(action) {
679317
- try {
679318
- if (process.platform === "linux") {
679319
- const result = spawnSync9("systemctl", ["--user", action, SERVICE_LABEL], {
679320
- stdio: "ignore",
679321
- timeout: 1e4
679322
- });
679323
- return result.status === 0;
679324
- }
679325
- if (process.platform === "darwin" && typeof process.getuid === "function") {
679326
- const domain = `gui/${process.getuid()}`;
679327
- const plist = join129(homedir40(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
679328
- if (action === "stop") {
679329
- return spawnSync9("launchctl", ["bootout", `${domain}/${LAUNCHD_LABEL}`], { stdio: "ignore", timeout: 1e4 }).status === 0;
679330
- }
679331
- if (action === "restart") {
679332
- return spawnSync9("launchctl", ["kickstart", "-k", `${domain}/${LAUNCHD_LABEL}`], { stdio: "ignore", timeout: 1e4 }).status === 0;
679333
- }
679334
- spawnSync9("launchctl", ["bootstrap", domain, plist], { stdio: "ignore", timeout: 1e4 });
679335
- return spawnSync9("launchctl", ["kickstart", `${domain}/${LAUNCHD_LABEL}`], { stdio: "ignore", timeout: 1e4 }).status === 0;
679336
- }
679337
- if (process.platform === "win32") {
679338
- const args = action === "stop" ? ["/End", "/TN", WINDOWS_TASK_NAME] : ["/Run", "/TN", WINDOWS_TASK_NAME];
679339
- return spawnSync9("schtasks", args, { stdio: "ignore", timeout: 1e4 }).status === 0;
679340
- }
679341
- } catch {
679342
- }
679343
- return false;
679344
- }
679345
679473
  async function controlDaemon(action, endpoint) {
679346
- if (runServiceControl(action)) return true;
679347
- if (action === "stop") return stopDaemon();
679348
- if (action === "restart") {
679349
- stopDaemon();
679350
- await new Promise((resolve87) => setTimeout(resolve87, 500));
679351
- }
679352
679474
  const port = Number(new URL(endpoint).port);
679353
- return await startDaemon(Number.isInteger(port) && port > 0 ? port : void 0) !== null;
679475
+ if (!Number.isInteger(port) || port <= 0) return false;
679476
+ if (action === "stop") return stopDaemonAtPort(port);
679477
+ if (action === "restart") return restartDaemon(port, getLocalCliVersion());
679478
+ return (await ensureDaemonVersion(getLocalCliVersion(), port)).ok;
679354
679479
  }
679355
679480
  function openExternal(target) {
679356
679481
  try {
@@ -679372,7 +679497,7 @@ async function loadSysTray() {
679372
679497
  return constructor;
679373
679498
  }
679374
679499
  function menuForHealth(health, endpoint, registered, update2) {
679375
- const healthItem = { title: health.label, tooltip: health.tooltip, enabled: false };
679500
+ const healthItem = trayVersionPresentation(health, update2);
679376
679501
  const actionItem = {
679377
679502
  title: health.kind === "online" ? "Restart daemon" : "Start daemon",
679378
679503
  tooltip: health.kind === "online" ? "Restart the Omnius API daemon" : "Start the Omnius API daemon",
@@ -679394,7 +679519,8 @@ function menuForHealth(health, endpoint, registered, update2) {
679394
679519
  title: update2.title,
679395
679520
  tooltip: update2.tooltip,
679396
679521
  enabled: update2.enabled,
679397
- action: "update-omnius"
679522
+ action: update2.action,
679523
+ hidden: Boolean(update2.targetVersion)
679398
679524
  };
679399
679525
  return {
679400
679526
  healthItem,
@@ -679462,7 +679588,9 @@ async function startTray(explicitEndpoint) {
679462
679588
  if (!trayRequiresRestart(existing, endpoint, liveHealth)) {
679463
679589
  return { ...existing, health: liveHealth };
679464
679590
  }
679465
- await stopTray();
679591
+ if (!await stopTray()) {
679592
+ throw new Error(`Could not stop the existing Omnius indicator on ${existing.endpoint}`);
679593
+ }
679466
679594
  }
679467
679595
  const support = traySupport();
679468
679596
  if (!support.supported) throw new Error(support.reason || "Tray is not supported on this host");
@@ -679513,16 +679641,49 @@ async function stopTray() {
679513
679641
  }
679514
679642
  return false;
679515
679643
  }
679644
+ const state = readProcessState(paths.stateFile);
679645
+ if (state?.pid !== pid || !trayProcessCommandLooksOwned(processCommandLine(pid))) {
679646
+ return false;
679647
+ }
679516
679648
  try {
679517
679649
  process.kill(pid, "SIGTERM");
679518
679650
  } catch {
679519
679651
  return false;
679520
679652
  }
679521
679653
  for (let i2 = 0; i2 < 30; i2++) {
679522
- if (!isProcessAlive2(pid)) return true;
679654
+ if (!isProcessAlive2(pid)) {
679655
+ try {
679656
+ unlinkSync23(paths.pidFile);
679657
+ } catch {
679658
+ }
679659
+ try {
679660
+ unlinkSync23(paths.stateFile);
679661
+ } catch {
679662
+ }
679663
+ return true;
679664
+ }
679665
+ await new Promise((resolve87) => setTimeout(resolve87, 100));
679666
+ }
679667
+ if (!trayProcessCommandLooksOwned(processCommandLine(pid))) return false;
679668
+ try {
679669
+ process.kill(pid, "SIGKILL");
679670
+ } catch {
679671
+ }
679672
+ for (let i2 = 0; i2 < 20; i2++) {
679673
+ if (!isProcessAlive2(pid)) {
679674
+ try {
679675
+ unlinkSync23(paths.pidFile);
679676
+ } catch {
679677
+ }
679678
+ try {
679679
+ unlinkSync23(paths.stateFile);
679680
+ } catch {
679681
+ }
679682
+ return true;
679683
+ }
679523
679684
  await new Promise((resolve87) => setTimeout(resolve87, 100));
679524
679685
  }
679525
- return !isProcessAlive2(pid);
679686
+ return false;
679526
679687
  }
679527
679688
  async function runTrayForeground(explicitEndpoint) {
679528
679689
  const support = traySupport();
@@ -679605,8 +679766,6 @@ async function runTrayForeground(explicitEndpoint) {
679605
679766
  health = next;
679606
679767
  state = { ...state, health };
679607
679768
  writeProcessState(paths, state);
679608
- menuState.healthItem.title = health.label;
679609
- menuState.healthItem.tooltip = health.tooltip;
679610
679769
  menuState.actionItem.title = health.kind === "online" ? "Restart daemon" : "Start daemon";
679611
679770
  menuState.actionItem.action = health.kind === "online" ? "restart-daemon" : "start-daemon";
679612
679771
  menuState.stopItem.enabled = health.kind === "online";
@@ -679619,6 +679778,14 @@ async function runTrayForeground(explicitEndpoint) {
679619
679778
  menuState.updateItem.title = updateView.title;
679620
679779
  menuState.updateItem.tooltip = updateView.tooltip;
679621
679780
  menuState.updateItem.enabled = updateView.enabled;
679781
+ menuState.updateItem.action = updateView.action;
679782
+ menuState.updateItem.hidden = Boolean(updateView.targetVersion);
679783
+ const versionView = trayVersionPresentation(health, updateView);
679784
+ menuState.healthItem.title = versionView.title;
679785
+ menuState.healthItem.tooltip = versionView.tooltip;
679786
+ menuState.healthItem.enabled = versionView.enabled;
679787
+ if (versionView.action) menuState.healthItem.action = versionView.action;
679788
+ else delete menuState.healthItem.action;
679622
679789
  const autostartRegistered = existsSync117(paths.autostartFile);
679623
679790
  const autostartChanged = menuState.autostartItem.checked !== autostartRegistered;
679624
679791
  if (autostartChanged) menuState.autostartItem.checked = autostartRegistered;
@@ -679676,14 +679843,56 @@ async function runTrayForeground(explicitEndpoint) {
679676
679843
  trayWasRunning: true
679677
679844
  });
679678
679845
  updateView = trayUpdatePresentation(currentVersion, targetVersion, readUpdateState());
679846
+ if (updateView.enabled) {
679847
+ updateView = {
679848
+ title: `Updating to v${targetVersion} — starting`,
679849
+ tooltip: "The global npm update transaction is starting",
679850
+ enabled: false,
679851
+ targetVersion,
679852
+ action: "update-omnius"
679853
+ };
679854
+ }
679855
+ const versionView = trayVersionPresentation(health, updateView);
679856
+ menuState.healthItem.title = versionView.title;
679857
+ menuState.healthItem.tooltip = versionView.tooltip;
679858
+ menuState.healthItem.enabled = versionView.enabled;
679859
+ menuState.healthItem.action = "update-omnius";
679679
679860
  menuState.updateItem.title = updateView.title;
679680
679861
  menuState.updateItem.tooltip = updateView.tooltip;
679681
679862
  menuState.updateItem.enabled = false;
679863
+ menuState.updateItem.hidden = true;
679682
679864
  } catch (error) {
679683
- menuState.updateItem.title = "Update could not start retry";
679684
- menuState.updateItem.tooltip = error instanceof Error ? error.message : String(error);
679685
- menuState.updateItem.enabled = true;
679865
+ const message2 = error instanceof Error ? error.message : String(error);
679866
+ menuState.healthItem.title = `Update to v${targetVersion} could not start — retry`;
679867
+ menuState.healthItem.tooltip = message2;
679868
+ menuState.healthItem.enabled = true;
679869
+ menuState.healthItem.action = "update-omnius";
679686
679870
  }
679871
+ await tray?.sendAction({ type: "update-item", item: menuState.healthItem });
679872
+ await tray?.sendAction({ type: "update-item", item: menuState.updateItem });
679873
+ break;
679874
+ }
679875
+ case "check-update": {
679876
+ const currentVersion = health.version;
679877
+ if (!currentVersion) break;
679878
+ availableUpdate = await checkForUpdate(currentVersion, true);
679879
+ updateView = trayUpdatePresentation(
679880
+ currentVersion,
679881
+ availableUpdate?.latestVersion,
679882
+ readUpdateState()
679883
+ );
679884
+ menuState.updateItem.title = updateView.title;
679885
+ menuState.updateItem.tooltip = updateView.tooltip;
679886
+ menuState.updateItem.enabled = updateView.enabled;
679887
+ menuState.updateItem.action = updateView.action;
679888
+ menuState.updateItem.hidden = Boolean(updateView.targetVersion);
679889
+ const versionView = trayVersionPresentation(health, updateView);
679890
+ menuState.healthItem.title = versionView.title;
679891
+ menuState.healthItem.tooltip = versionView.tooltip;
679892
+ menuState.healthItem.enabled = versionView.enabled;
679893
+ if (versionView.action) menuState.healthItem.action = versionView.action;
679894
+ else delete menuState.healthItem.action;
679895
+ await tray?.sendAction({ type: "update-item", item: menuState.healthItem });
679687
679896
  await tray?.sendAction({ type: "update-item", item: menuState.updateItem });
679688
679897
  break;
679689
679898
  }
@@ -679708,7 +679917,7 @@ async function runTrayForeground(explicitEndpoint) {
679708
679917
  releaseTrayPid(paths);
679709
679918
  }
679710
679919
  }
679711
- var DEFAULT_ENDPOINT, TRAY_HELPER_VERSION, POLL_INTERVAL_MS, START_WAIT_MS, SERVICE_LABEL, LAUNCHD_LABEL, WINDOWS_TASK_NAME, EXPECTED_HELPER_SHA256;
679920
+ var DEFAULT_ENDPOINT, TRAY_HELPER_VERSION, POLL_INTERVAL_MS, START_WAIT_MS, EXPECTED_HELPER_SHA256;
679712
679921
  var init_tray = __esm({
679713
679922
  "packages/cli/src/tray.ts"() {
679714
679923
  init_daemon();
@@ -679718,9 +679927,6 @@ var init_tray = __esm({
679718
679927
  TRAY_HELPER_VERSION = "2.1.4";
679719
679928
  POLL_INTERVAL_MS = 1e4;
679720
679929
  START_WAIT_MS = 5e3;
679721
- SERVICE_LABEL = "omnius-daemon.service";
679722
- LAUNCHD_LABEL = "ai.omnius.daemon";
679723
- WINDOWS_TASK_NAME = "OmniusDaemon";
679724
679930
  EXPECTED_HELPER_SHA256 = {
679725
679931
  aix: void 0,
679726
679932
  android: void 0,
@@ -679759,6 +679965,26 @@ function output(status, json = false) {
679759
679965
  `);
679760
679966
  else printStatus(status);
679761
679967
  }
679968
+ function endpointPort(endpoint) {
679969
+ const port = Number(new URL(endpoint).port);
679970
+ if (!Number.isInteger(port) || port <= 0) {
679971
+ throw new Error(`Invalid tray daemon endpoint: ${endpoint}`);
679972
+ }
679973
+ return port;
679974
+ }
679975
+ async function ensureTrayDaemonOnline(endpoint) {
679976
+ const port = endpointPort(endpoint);
679977
+ const daemon = await ensureDaemonVersion(getLocalCliVersion(), port);
679978
+ if (!daemon.ok) {
679979
+ throw new Error(`Could not reconcile the Omnius daemon on port ${port}`);
679980
+ }
679981
+ for (let attempt = 0; attempt < 10; attempt++) {
679982
+ const health = await pollTrayHealth(endpoint);
679983
+ if (health.kind === "online") return;
679984
+ await new Promise((resolve87) => setTimeout(resolve87, 200));
679985
+ }
679986
+ throw new Error(`Omnius daemon did not become online at ${endpoint}`);
679987
+ }
679762
679988
  async function trayCommand(options2) {
679763
679989
  const raw = options2.subCommand || "status";
679764
679990
  if (!SUBCOMMANDS.has(raw)) {
@@ -679767,10 +679993,12 @@ async function trayCommand(options2) {
679767
679993
  const subCommand = raw;
679768
679994
  const endpoint = resolveTrayEndpoint(options2.endpoint);
679769
679995
  if (subCommand === "run") {
679996
+ await ensureTrayDaemonOnline(endpoint);
679770
679997
  await runTrayForeground(endpoint);
679771
679998
  return;
679772
679999
  }
679773
680000
  if (subCommand === "install") {
680001
+ await ensureTrayDaemonOnline(endpoint);
679774
680002
  const registrationFile = installTrayAutostart(endpoint);
679775
680003
  const status = await startTray(endpoint);
679776
680004
  if (!options2.json) process.stdout.write(`Registered tray autostart: ${registrationFile}
@@ -679787,6 +680015,7 @@ async function trayCommand(options2) {
679787
680015
  return;
679788
680016
  }
679789
680017
  if (subCommand === "start") {
680018
+ await ensureTrayDaemonOnline(endpoint);
679790
680019
  output(await startTray(endpoint), options2.json);
679791
680020
  return;
679792
680021
  }
@@ -679798,7 +680027,11 @@ async function trayCommand(options2) {
679798
680027
  return;
679799
680028
  }
679800
680029
  if (subCommand === "restart") {
679801
- await stopTray();
680030
+ await ensureTrayDaemonOnline(endpoint);
680031
+ const status = await getTrayStatus(endpoint);
680032
+ if (status.running && !await stopTray()) {
680033
+ throw new Error(`Could not stop the existing Omnius indicator on ${status.endpoint}`);
680034
+ }
679802
680035
  output(await startTray(endpoint), options2.json);
679803
680036
  return;
679804
680037
  }
@@ -679808,6 +680041,7 @@ var SUBCOMMANDS;
679808
680041
  var init_tray2 = __esm({
679809
680042
  "packages/cli/src/commands/tray.ts"() {
679810
680043
  init_tray();
680044
+ init_daemon();
679811
680045
  SUBCOMMANDS = /* @__PURE__ */ new Set([
679812
680046
  "install",
679813
680047
  "uninstall",
@@ -724624,9 +724858,18 @@ async function runIndicatorCommand(rawAction, operations = DEFAULT_OPERATIONS) {
724624
724858
  daemon
724625
724859
  };
724626
724860
  }
724861
+ operations.register?.(endpoint);
724627
724862
  const started = await operations.start(endpoint);
724863
+ if (!started.running || !started.ready || started.endpoint !== endpoint || started.error) {
724864
+ return {
724865
+ level: "error",
724866
+ message: `Indicator did not bind to ${endpoint}. ` + formatIndicatorStatus(started, daemon),
724867
+ status: started,
724868
+ daemon
724869
+ };
724870
+ }
724628
724871
  const health = await waitForOnlineHealth(endpoint, operations);
724629
- const status = { ...started, endpoint, health };
724872
+ const status = { ...started, health };
724630
724873
  const level = status.error ? "error" : status.running && status.ready && health.kind === "online" ? "info" : "error";
724631
724874
  return {
724632
724875
  level,
@@ -724648,6 +724891,7 @@ var init_indicator_command = __esm({
724648
724891
  init_daemon();
724649
724892
  DEFAULT_OPERATIONS = {
724650
724893
  ensureDaemon: () => ensureDaemonVersion(),
724894
+ register: (endpoint) => installTrayAutostart(endpoint),
724651
724895
  start: (endpoint) => startTray(endpoint),
724652
724896
  status: () => getTrayStatus(),
724653
724897
  health: (endpoint) => pollTrayHealth(endpoint),
@@ -792695,6 +792939,17 @@ var init_routes_v1 = __esm({
792695
792939
  });
792696
792940
 
792697
792941
  // packages/cli/src/api/web-ui.ts
792942
+ function renderRouteHeading(options2) {
792943
+ const signals = options2.signals.map((signal) => `<span class="route-heading__signal">${signal}</span>`).join("");
792944
+ return `<header class="route-heading" data-route-heading="${options2.page}">
792945
+ <div class="route-heading__copy">
792946
+ <span class="route-heading__eyebrow">${options2.eyebrow}</span>
792947
+ <h1 class="route-heading__title">${options2.title}</h1>
792948
+ <p class="route-heading__description">${options2.description}</p>
792949
+ </div>
792950
+ <div class="route-heading__signals" aria-label="Visible operational areas">${signals}</div>
792951
+ </header>`;
792952
+ }
792698
792953
  function getWebUI() {
792699
792954
  return `<!DOCTYPE html>
792700
792955
  <html lang="en">
@@ -794792,6 +795047,303 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794792
795047
  }
794793
795048
  *::-webkit-scrollbar-thumb:hover { background: var(--nclp-yellow); }
794794
795049
 
795050
+ /* Shared route composition. This carries the useful six-track command-center
795051
+ * grammar from the Voryn persona/call-tree surfaces while keeping Omnius's
795052
+ * existing NOCLIP palette, compact density, and 3px geometry authoritative. */
795053
+ [data-route-page] {
795054
+ min-width: 0;
795055
+ background: var(--nclp-bg-main) !important;
795056
+ }
795057
+
795058
+ .omnius-route-shell {
795059
+ padding: 12px 16px !important;
795060
+ }
795061
+
795062
+ .route-heading {
795063
+ display: grid;
795064
+ grid-template-columns: minmax(0, 1fr) auto;
795065
+ align-items: end;
795066
+ gap: 12px;
795067
+ margin-bottom: 12px;
795068
+ padding-bottom: 10px;
795069
+ border-bottom: 1px solid var(--nclp-line);
795070
+ min-width: 0;
795071
+ }
795072
+
795073
+ .route-heading__copy { min-width: 0; }
795074
+ .route-heading__eyebrow {
795075
+ display: block;
795076
+ margin-bottom: 3px;
795077
+ color: var(--nclp-yellow);
795078
+ font-size: 9px;
795079
+ font-weight: 800;
795080
+ letter-spacing: 0.14em;
795081
+ text-transform: uppercase;
795082
+ }
795083
+ .route-heading__title {
795084
+ margin: 0 !important;
795085
+ color: var(--nclp-text) !important;
795086
+ font-size: 15px !important;
795087
+ letter-spacing: 0.02em !important;
795088
+ text-transform: none !important;
795089
+ }
795090
+ .route-heading__description {
795091
+ max-width: 760px;
795092
+ margin: 4px 0 0;
795093
+ color: var(--nclp-muted);
795094
+ font-size: 11px;
795095
+ line-height: 1.45;
795096
+ }
795097
+ .route-heading__signals {
795098
+ display: flex;
795099
+ flex-wrap: wrap;
795100
+ justify-content: flex-end;
795101
+ gap: 5px;
795102
+ }
795103
+ .route-heading__signal,
795104
+ .observability-chip {
795105
+ border: 1px solid var(--nclp-line);
795106
+ border-radius: var(--radius-sm);
795107
+ background: var(--nclp-bg-secondary);
795108
+ color: var(--nclp-muted);
795109
+ padding: 3px 6px;
795110
+ font-size: 9px;
795111
+ font-weight: 700;
795112
+ letter-spacing: 0.06em;
795113
+ text-transform: uppercase;
795114
+ white-space: nowrap;
795115
+ }
795116
+
795117
+ .observability-grid {
795118
+ display: grid;
795119
+ grid-template-columns: repeat(6, minmax(0, 1fr));
795120
+ gap: 12px;
795121
+ min-width: 0;
795122
+ align-items: start;
795123
+ }
795124
+ .observability-card {
795125
+ position: relative;
795126
+ min-width: 0;
795127
+ overflow: hidden;
795128
+ border: 1px solid var(--nclp-line);
795129
+ border-radius: var(--radius-lg);
795130
+ background: var(--nclp-card);
795131
+ box-shadow: var(--nclp-shadow);
795132
+ }
795133
+ .observability-card::before {
795134
+ content: "";
795135
+ position: absolute;
795136
+ z-index: 2;
795137
+ top: 0;
795138
+ left: 0;
795139
+ width: 38px;
795140
+ height: 2px;
795141
+ background: var(--nclp-yellow);
795142
+ pointer-events: none;
795143
+ }
795144
+ .observability-card__header {
795145
+ display: grid;
795146
+ grid-template-columns: minmax(0, 1fr) auto;
795147
+ align-items: center;
795148
+ gap: 10px;
795149
+ min-width: 0;
795150
+ padding: 10px 12px 8px;
795151
+ border-bottom: 1px solid var(--nclp-line);
795152
+ }
795153
+ .observability-card__title {
795154
+ min-width: 0;
795155
+ margin: 0 !important;
795156
+ color: var(--nclp-text) !important;
795157
+ font-size: 10px !important;
795158
+ letter-spacing: 0.1em !important;
795159
+ text-transform: uppercase !important;
795160
+ }
795161
+ .observability-card__meta {
795162
+ overflow: hidden;
795163
+ color: var(--nclp-quiet);
795164
+ font-size: 9px;
795165
+ text-overflow: ellipsis;
795166
+ white-space: nowrap;
795167
+ }
795168
+ .observability-card__body {
795169
+ min-width: 0;
795170
+ padding: 11px 12px 12px;
795171
+ }
795172
+ .observability-card__body--flush { padding: 0; }
795173
+ .observability-span-2 { grid-column: span 2; }
795174
+ .observability-span-3 { grid-column: span 3; }
795175
+ .observability-span-4 { grid-column: span 4; }
795176
+ .observability-span-6 { grid-column: 1 / -1; }
795177
+
795178
+ .observability-facts {
795179
+ display: grid;
795180
+ grid-template-columns: repeat(3, minmax(0, 1fr));
795181
+ gap: 7px;
795182
+ min-width: 0;
795183
+ }
795184
+ .observability-facts > * {
795185
+ min-width: 0;
795186
+ margin: 0 !important;
795187
+ }
795188
+ .observability-stack {
795189
+ display: grid;
795190
+ gap: 7px;
795191
+ min-width: 0;
795192
+ }
795193
+ .observability-actions {
795194
+ display: flex;
795195
+ flex-wrap: wrap;
795196
+ align-items: center;
795197
+ gap: 7px;
795198
+ }
795199
+ .observability-actions > :is(input, select, textarea) { min-width: 0; }
795200
+ .observability-table-scroll { overflow-x: auto; overscroll-behavior-inline: contain; }
795201
+
795202
+ .route-workspace {
795203
+ display: flex;
795204
+ flex: 1;
795205
+ min-width: 0;
795206
+ min-height: 0;
795207
+ overflow: hidden;
795208
+ }
795209
+ .route-workspace__main {
795210
+ display: flex;
795211
+ flex: 1;
795212
+ flex-direction: column;
795213
+ min-width: 0;
795214
+ min-height: 0;
795215
+ overflow: hidden;
795216
+ }
795217
+ .route-scroll-region {
795218
+ min-width: 0;
795219
+ min-height: 0;
795220
+ overflow: auto;
795221
+ }
795222
+ .agent-command-body { padding: 11px 12px 12px; }
795223
+ .agent-command-body #agent-params { margin: 0; border: 0; background: transparent; }
795224
+ .agent-task-input { min-height: 120px !important; margin: 0 !important; }
795225
+ .agent-events-surface { min-height: 180px; }
795226
+
795227
+ .workspace-switcher { position: relative; flex: 1; min-width: 0; }
795228
+ .workspace-switcher__trigger {
795229
+ display: grid !important;
795230
+ grid-template-columns: 9px minmax(0, 1fr) 12px;
795231
+ align-items: center;
795232
+ gap: 8px;
795233
+ width: 100%;
795234
+ min-width: 0;
795235
+ padding: 5px 7px !important;
795236
+ text-align: left;
795237
+ }
795238
+ #sidebar-brand {
795239
+ overflow: hidden;
795240
+ color: var(--nclp-text) !important;
795241
+ font-size: 11px !important;
795242
+ letter-spacing: 0.01em !important;
795243
+ text-overflow: ellipsis;
795244
+ text-transform: none !important;
795245
+ white-space: nowrap;
795246
+ }
795247
+ .workspace-switcher__chevron { color: var(--nclp-quiet); transition: transform 0.14s ease; }
795248
+ .workspace-switcher__trigger[aria-expanded="true"] .workspace-switcher__chevron { transform: rotate(180deg); }
795249
+ .workspace-picker {
795250
+ position: absolute;
795251
+ z-index: 80;
795252
+ top: calc(100% + 6px);
795253
+ left: 0;
795254
+ width: min(420px, calc(100vw - 24px));
795255
+ max-height: min(520px, 72vh);
795256
+ overflow: hidden;
795257
+ border: 1px solid var(--nclp-line-strong);
795258
+ border-radius: var(--radius-lg);
795259
+ background: var(--nclp-card);
795260
+ box-shadow: 0 22px 56px rgba(0, 0, 0, 0.48);
795261
+ }
795262
+ .workspace-picker__header { padding: 10px; border-bottom: 1px solid var(--nclp-line); }
795263
+ .workspace-picker__header input { width: 100%; padding: 7px 9px; }
795264
+ .workspace-picker__list {
795265
+ display: grid;
795266
+ gap: 3px;
795267
+ max-height: 42vh;
795268
+ overflow-y: auto;
795269
+ padding: 6px;
795270
+ overscroll-behavior: contain;
795271
+ }
795272
+ .workspace-picker__option {
795273
+ display: grid !important;
795274
+ grid-template-columns: minmax(0, 1fr) auto;
795275
+ align-items: center;
795276
+ gap: 10px;
795277
+ width: 100%;
795278
+ padding: 8px 9px !important;
795279
+ text-align: left;
795280
+ text-transform: none !important;
795281
+ }
795282
+ .workspace-picker__option[aria-selected="true"] {
795283
+ border-color: var(--nclp-yellow) !important;
795284
+ background: var(--nclp-amber-soft) !important;
795285
+ }
795286
+ .workspace-picker__option.is-keyboard-active {
795287
+ border-color: var(--nclp-yellow) !important;
795288
+ color: var(--nclp-yellow) !important;
795289
+ }
795290
+ .workspace-picker__name,
795291
+ .workspace-picker__path { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
795292
+ .workspace-picker__name { color: var(--nclp-text); font-size: 11px; font-weight: 700; }
795293
+ .workspace-picker__path { margin-top: 2px; color: var(--nclp-quiet); font-size: 9px; font-weight: 500; }
795294
+ .workspace-picker__check { color: var(--nclp-yellow); }
795295
+ .workspace-picker__empty { padding: 18px 10px; color: var(--nclp-quiet); font-size: 10px; text-align: center; }
795296
+ .workspace-picker__footer { display: flex; justify-content: flex-end; padding: 7px 9px; border-top: 1px solid var(--nclp-line); }
795297
+ #omnius-sidebar[data-collapsed="true"] .workspace-switcher { display: none; }
795298
+
795299
+ /* Generate keeps its purpose-built rails, but uses the same six-track span
795300
+ * proportions as the overview grids: 1/6 controls, 3/6 work, 2/6 signal. */
795301
+ .hud-workbench[data-layout="generate-workbench"] {
795302
+ grid-template-columns: repeat(6, minmax(0, 1fr));
795303
+ }
795304
+ .hud-workbench[data-layout="generate-workbench"] > #gen-mode-rail { grid-column: span 1; }
795305
+ .hud-workbench[data-layout="generate-workbench"] > main { grid-column: span 3; min-width: 0; }
795306
+ .hud-workbench[data-layout="generate-workbench"] > .hud-side-output { grid-column: span 2; min-width: 0; }
795307
+
795308
+ @media (max-width: 1180px) {
795309
+ .hud-workbench[data-layout="generate-workbench"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
795310
+ .hud-workbench[data-layout="generate-workbench"] > #gen-mode-rail,
795311
+ .hud-workbench[data-layout="generate-workbench"] > main { grid-column: span 1; }
795312
+ .hud-workbench[data-layout="generate-workbench"] > .hud-side-output { grid-column: 1 / -1; }
795313
+ }
795314
+
795315
+ @media (max-width: 820px) {
795316
+ .hud-workbench[data-layout="generate-workbench"] { grid-template-columns: minmax(0, 1fr); }
795317
+ .hud-workbench[data-layout="generate-workbench"] > #gen-mode-rail,
795318
+ .hud-workbench[data-layout="generate-workbench"] > main,
795319
+ .hud-workbench[data-layout="generate-workbench"] > .hud-side-output { grid-column: 1; }
795320
+ }
795321
+
795322
+ @media (max-width: 920px) {
795323
+ .observability-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
795324
+ .observability-span-2,
795325
+ .observability-span-3 { grid-column: span 1; }
795326
+ .observability-span-4,
795327
+ .observability-span-6 { grid-column: 1 / -1; }
795328
+ .observability-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
795329
+ }
795330
+
795331
+ @media (max-width: 760px) {
795332
+ .omnius-route-shell { padding: 10px !important; }
795333
+ .route-heading { grid-template-columns: 1fr; align-items: start; }
795334
+ .route-heading__signals { justify-content: flex-start; }
795335
+ .observability-grid { grid-template-columns: minmax(0, 1fr); gap: 10px; }
795336
+ .observability-span-2,
795337
+ .observability-span-3,
795338
+ .observability-span-4,
795339
+ .observability-span-6 { grid-column: 1; }
795340
+ .observability-facts { grid-template-columns: minmax(0, 1fr); }
795341
+ .observability-card__header { grid-template-columns: minmax(0, 1fr); }
795342
+ .observability-card__meta { white-space: normal; }
795343
+ .observability-actions > :is(button, input, select) { max-width: 100%; }
795344
+ .workspace-picker { position: fixed; top: 54px; right: 12px; left: 12px; width: auto; }
795345
+ }
795346
+
794795
795347
  @media (prefers-reduced-motion: reduce) {
794796
795348
  *, *::before, *::after {
794797
795349
  animation-duration: 0.01ms !important;
@@ -794820,8 +795372,22 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794820
795372
  <button id="sidebar-toggle" onclick="toggleSidebar()" title="Toggle sidebar (Cmd/Ctrl+B)" style="background:transparent;border:none;color:var(--color-fg-muted);padding:4px;border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:center;width:28px;height:28px">
794821
795373
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
794822
795374
  </button>
794823
- <span id="sidebar-brand-dot" title="Connection status" style="width:9px;height:9px;border-radius:999px;background:#f59e0b;box-shadow:0 0 0 2px rgba(245,158,11,.16);flex:0 0 auto"></span>
794824
- <span id="sidebar-brand" style="font-weight:600;font-size:0.92rem;color:var(--color-fg);letter-spacing:-0.01em;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">No project</span>
795375
+ <div class="workspace-switcher">
795376
+ <button id="workspace-switcher-trigger" class="workspace-switcher__trigger" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="workspace-picker" onclick="toggleWorkspacePicker(event)" title="Switch workspace">
795377
+ <span id="sidebar-brand-dot" title="Connection status" style="width:9px;height:9px;border-radius:999px;background:#f59e0b;box-shadow:0 0 0 2px rgba(245,158,11,.16);flex:0 0 auto"></span>
795378
+ <span id="sidebar-brand">No project</span>
795379
+ <svg class="workspace-switcher__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
795380
+ </button>
795381
+ <section id="workspace-picker" class="workspace-picker" role="dialog" aria-label="Select workspace" style="display:none">
795382
+ <div class="workspace-picker__header">
795383
+ <input id="workspace-picker-search" type="search" placeholder="Filter workspaces..." autocomplete="off" aria-label="Filter workspaces" oninput="filterWorkspacePicker(this.value)" onkeydown="handleWorkspacePickerKeys(event)">
795384
+ </div>
795385
+ <div id="workspace-picker-list" class="workspace-picker__list" role="listbox" aria-label="Available workspaces"></div>
795386
+ <div class="workspace-picker__footer">
795387
+ <button type="button" onclick="closeWorkspacePicker(); switchTab('projects')">Manage workspaces</button>
795388
+ </div>
795389
+ </section>
795390
+ </div>
794825
795391
  </div>
794826
795392
 
794827
795393
  <!-- New chat button -->
@@ -794857,35 +795423,35 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794857
795423
 
794858
795424
  <!-- Section nav (the legacy tabs migrated into the sidebar footer) -->
794859
795425
  <div id="sidebar-nav" class="sb-label" style="border-top:1px solid var(--color-border);padding:8px;display:flex;flex-direction:column;gap:2px">
794860
- <button class="sb-nav" data-tab="chat" onclick="switchTab('chat')" title="Chat">
795426
+ <button class="sb-nav" data-tab="chat" data-route="/chat" aria-controls="chat-container" aria-current="page" onclick="switchTab('chat')" title="Chat">
794861
795427
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
794862
795428
  <span class="sb-label">Chat</span>
794863
795429
  </button>
794864
- <button class="sb-nav" data-tab="agent" onclick="switchTab('agent')" title="Agent">
795430
+ <button class="sb-nav" data-tab="agent" data-route="/agent" aria-controls="agent-panel" onclick="switchTab('agent')" title="Agent">
794865
795431
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 4 7v10l8 5 8-5V7l-8-5z"/><path d="M12 22V12"/><path d="m4 7 8 5 8-5"/></svg>
794866
795432
  <span class="sb-label">Agent</span>
794867
795433
  </button>
794868
- <button class="sb-nav" data-tab="voice" onclick="switchTab('voice')" title="Voice">
795434
+ <button class="sb-nav" data-tab="voice" data-route="/voice" aria-controls="voice-panel" onclick="switchTab('voice')" title="Voice">
794869
795435
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
794870
795436
  <span class="sb-label">Voice</span>
794871
795437
  </button>
794872
- <button class="sb-nav" data-tab="generate" onclick="switchTab('generate')" title="Generate media">
795438
+ <button class="sb-nav" data-tab="generate" data-route="/generate" aria-controls="generate-panel" onclick="switchTab('generate')" title="Generate media">
794873
795439
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
794874
795440
  <span class="sb-label">Generate</span>
794875
795441
  </button>
794876
- <button class="sb-nav" data-tab="projects" onclick="switchTab('projects')" title="Projects">
795442
+ <button class="sb-nav" data-tab="projects" data-route="/projects" aria-controls="projects-panel" onclick="switchTab('projects')" title="Projects">
794877
795443
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
794878
795444
  <span class="sb-label">Projects</span>
794879
795445
  </button>
794880
- <button class="sb-nav" data-tab="jobs" onclick="switchTab('jobs')" title="Dashboard">
795446
+ <button class="sb-nav" data-tab="jobs" data-route="/dashboard" aria-controls="jobs-panel" onclick="switchTab('jobs')" title="Dashboard">
794881
795447
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>
794882
795448
  <span class="sb-label">Dashboard</span>
794883
795449
  </button>
794884
- <button class="sb-nav" data-tab="activity" onclick="switchTab('activity')" title="Activity">
795450
+ <button class="sb-nav" data-tab="activity" data-route="/activity" aria-controls="activity-panel" onclick="switchTab('activity')" title="Activity">
794885
795451
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
794886
795452
  <span class="sb-label">Activity</span>
794887
795453
  </button>
794888
- <button class="sb-nav" data-tab="config" onclick="switchTab('config')" title="Config">
795454
+ <button class="sb-nav" data-tab="config" data-route="/settings" aria-controls="config-panel" onclick="switchTab('config')" title="Settings">
794889
795455
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
794890
795456
  <span class="sb-label">Settings</span>
794891
795457
  </button>
@@ -794911,8 +795477,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794911
795477
  <div id="omnius-main" style="flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden;background:var(--color-bg)">
794912
795478
 
794913
795479
  <div id="header">
794914
- <span class="accent" id="header-project-name">No project</span>
794915
- <span class="status" id="status">connecting...</span>
795480
+ <span class="status" id="status" aria-live="polite" style="display:none">connecting...</span>
794916
795481
  <span id="update-btn" style="display:none;background:#0f3f3d;border:1px solid var(--color-brand);color:var(--color-brand);padding:2px 8px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer" onclick="doUpdate()">update</span>
794917
795482
  <select id="model-select"><option>loading...</option></select>
794918
795483
  <button class="key-btn" id="files-btn" onclick="toggleFilesForActiveTab()" title="Toggle workspace sidebar (chat or agent depending on active tab)">files</button>
@@ -794954,7 +795519,9 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794954
795519
  <span id="token-counter" style="font-size:0.6rem;color:var(--color-fg-faint);display:flex;flex-flow:column;justify-content:center;padding:0 6px">0 tokens</span>
794955
795520
  </div>
794956
795521
  </div>
794957
- <div id="chat-container" style="display:flex;flex:1;overflow:hidden">
795522
+ <div id="chat-container" class="omnius-route-shell" data-route-page="chat" data-route-tab="chat" style="display:flex;flex:1;overflow:hidden;flex-direction:column">
795523
+ ${renderRouteHeading({ page: "chat", eyebrow: "Conversation", title: "Chat workspace", description: "Keep the active session, execution plan, workspace context, and conversation visible as one operational surface.", signals: ["session", "plan", "live context"] })}
795524
+ <div class="route-workspace" data-layout="chat-split">
794958
795525
  <div id="workspace-sidebar" style="display:none;width:250px;background:var(--color-bg-elevated);border-right:1px solid var(--color-bg-input);overflow-y:auto;padding:8px;flex-shrink:0;font-size:0.7rem">
794959
795526
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
794960
795527
  <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Workspace</span>
@@ -794964,7 +795531,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794964
795531
  <div id="workspace-tree" style="color:var(--color-fg)"></div>
794965
795532
  </div>
794966
795533
  <!-- Vertical column: session topbar + checklist + conversation -->
794967
- <div style="display:flex;flex-direction:column;flex:1;min-width:0;overflow:hidden">
795534
+ <div class="route-workspace__main">
794968
795535
  <!-- WO-TASK-02 — Chat session topbar (separate storage from agent runs) -->
794969
795536
  <div class="session-topbar" id="chat-session-topbar">
794970
795537
  <span class="topbar-label">chat session</span>
@@ -794981,8 +795548,10 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794981
795548
  </div>
794982
795549
  <div id="conversation" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:4px;min-height:0"></div>
794983
795550
  </div>
795551
+ </div>
794984
795552
  </div>
794985
- <div id="agent-panel" style="display:none;flex:1;overflow:hidden;flex-direction:column">
795553
+ <div id="agent-panel" class="omnius-route-shell" data-route-page="agent" data-route-tab="agent" style="display:none;flex:1;overflow:hidden;flex-direction:column">
795554
+ ${renderRouteHeading({ page: "agent", eyebrow: "Execution", title: "Agent command center", description: "Define the task contract, inspect the run configuration, and follow the event stream without losing workspace context.", signals: ["task contract", "run profile", "event stream"] })}
794986
795555
  <!-- WO-TASK-02 — Agent run topbar (separate storage from chat sessions) -->
794987
795556
  <div class="session-topbar" id="agent-session-topbar">
794988
795557
  <span class="topbar-label">agent run</span>
@@ -794994,7 +795563,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794994
795563
  <button onclick="refreshAgentRunSelect()" title="Refresh the run list">⟳</button>
794995
795564
  </div>
794996
795565
  <!-- Body row: agent workspace sidebar (isolated from chat) | scrollable form -->
794997
- <div style="display:flex;flex:1;overflow:hidden;min-height:0">
795566
+ <div class="route-workspace" data-layout="agent-form">
794998
795567
  <div id="agent-workspace-sidebar" style="display:none;width:250px;background:var(--color-bg-elevated);border-right:1px solid var(--color-bg-input);overflow-y:auto;padding:8px;flex-shrink:0;font-size:0.7rem">
794999
795568
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
795000
795569
  <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Agent Workspace</span>
@@ -795003,10 +795572,12 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795003
795572
  <div id="agent-workspace-cwd" style="color:var(--color-fg-faint);font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
795004
795573
  <div id="agent-workspace-tree" style="color:var(--color-fg)"></div>
795005
795574
  </div>
795006
- <div style="flex:1;overflow-y:auto;padding:12px 16px;min-height:0;min-width:0">
795575
+ <div class="route-scroll-region" style="flex:1;padding:0 0 4px;">
795576
+ <div class="observability-grid">
795007
795577
  <!-- Parameter form: all the dials for POST /v1/run -->
795008
- <div id="agent-params" style="background:var(--color-bg);border:1px solid var(--color-bg-input);border-radius:4px;padding:10px 12px;margin-bottom:10px;font-size:0.68rem">
795009
- <div style="color:var(--color-brand);font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px">Task Parameters</div>
795578
+ <section class="observability-card observability-span-4" data-module="agent-parameters">
795579
+ <header class="observability-card__header"><h2 class="observability-card__title">Run configuration</h2><span class="observability-card__meta">POST /v1/run</span></header>
795580
+ <div id="agent-params" class="observability-card__body agent-command-body" style="font-size:0.68rem">
795010
795581
  <div style="display:grid;grid-template-columns:120px 1fr;gap:6px 12px;align-items:center">
795011
795582
  <label style="color:var(--color-fg-muted)">Working dir</label>
795012
795583
  <div style="display:flex;gap:4px;align-items:center">
@@ -795037,36 +795608,75 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795037
795608
  <label style="color:var(--color-fg-muted)">Isolate workspace</label>
795038
795609
  <div><input type="checkbox" id="agent-isolate" style="accent-color:var(--color-brand)"> <span style="color:var(--color-fg-faint);font-size:0.6rem">fresh temp dir per run</span></div>
795039
795610
  </div>
795040
- </div>
795611
+ </div>
795612
+ </section>
795041
795613
 
795042
- <textarea id="agent-task" placeholder="Describe the task for the agent..." style="width:100%;min-height:80px;background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:8px 12px;color:var(--color-fg);font-family:inherit;font-size:0.82rem;resize:vertical;outline:none;margin-bottom:8px"></textarea>
795043
- <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
795614
+ <section class="observability-card observability-span-2" data-module="agent-task-contract">
795615
+ <header class="observability-card__header"><h2 class="observability-card__title">Task contract</h2><span class="observability-card__meta">persona + controls</span></header>
795616
+ <div class="observability-card__body observability-stack">
795617
+ <textarea id="agent-task" class="agent-task-input" placeholder="Describe the task for the agent..." style="width:100%;background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:8px 12px;color:var(--color-fg);font-family:inherit;font-size:0.82rem;resize:vertical;outline:none"></textarea>
795618
+ <div class="observability-actions">
795044
795619
  <select id="agent-profile" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem"><option value="">no profile</option></select>
795045
795620
  <button onclick="submitAgentTask()" id="agent-submit" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer">run task</button>
795046
795621
  <button onclick="abortAgentTask()" id="agent-abort" style="display:none;background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer">abort</button>
795047
795622
  </div>
795048
- <div id="agent-events" style="font-size:0.78rem;line-height:1.5"></div>
795623
+ </div>
795624
+ </section>
795625
+
795626
+ <section class="observability-card observability-span-6" data-module="agent-event-stream">
795627
+ <header class="observability-card__header"><h2 class="observability-card__title">Event stream</h2><span class="observability-card__meta">tools · stdout · completion</span></header>
795628
+ <div id="agent-events" class="observability-card__body agent-events-surface" style="font-size:0.78rem;line-height:1.5"></div>
795629
+ </section>
795630
+ </div>
795049
795631
  </div><!-- /scrollable agent body -->
795050
795632
  </div><!-- /agent body row (sidebar | form) -->
795051
795633
  </div>
795052
- <div id="jobs-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795053
- <div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
795054
- <div id="dashboard-daemons" style="margin-bottom:16px"></div>
795055
- <div id="dashboard-scheduled" style="margin-bottom:16px"></div>
795056
- <div id="dashboard-services" style="margin-bottom:16px"></div>
795057
- <div id="dashboard-usage" style="margin-bottom:16px"></div>
795058
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Job History</h3>
795059
- <div id="jobs-list" style="font-size:0.78rem"></div>
795634
+ <div id="jobs-panel" class="omnius-route-shell" data-route-page="dashboard" data-route-tab="jobs" style="display:none;flex:1;overflow-y:auto">
795635
+ ${renderRouteHeading({ page: "dashboard", eyebrow: "Operations", title: "System dashboard", description: "Health, active execution, scheduled automation, services, usage, and job history share one scan-friendly command grid.", signals: ["health", "daemons", "usage", "history"] })}
795636
+ <div class="observability-grid" data-layout="dashboard-metrics">
795637
+ <section class="observability-card observability-span-6" data-module="runtime-health">
795638
+ <header class="observability-card__header"><h2 class="observability-card__title">Runtime health</h2><span class="observability-card__meta">live daemon telemetry</span></header>
795639
+ <div id="dashboard-health" class="observability-card__body observability-facts"></div>
795640
+ </section>
795641
+ <section class="observability-card observability-span-2" data-module="active-processes">
795642
+ <header class="observability-card__header"><h2 class="observability-card__title">Active processes</h2><span class="observability-card__meta">agent runs</span></header>
795643
+ <div id="dashboard-daemons" class="observability-card__body"></div>
795644
+ </section>
795645
+ <section class="observability-card observability-span-4" data-module="scheduled-tasks">
795646
+ <header class="observability-card__header"><h2 class="observability-card__title">Scheduled tasks</h2><span class="observability-card__meta">automation control</span></header>
795647
+ <div id="dashboard-scheduled" class="observability-card__body"></div>
795648
+ </section>
795649
+ <section class="observability-card observability-span-3" data-module="services">
795650
+ <header class="observability-card__header"><h2 class="observability-card__title">Services</h2><span class="observability-card__meta">systemd --user</span></header>
795651
+ <div id="dashboard-services" class="observability-card__body"></div>
795652
+ </section>
795653
+ <section class="observability-card observability-span-3" data-module="token-usage">
795654
+ <header class="observability-card__header"><h2 class="observability-card__title">Token usage</h2><span class="observability-card__meta">persistent by provider</span></header>
795655
+ <div id="dashboard-usage" class="observability-card__body"></div>
795656
+ </section>
795657
+ <section class="observability-card observability-span-6" data-module="job-history">
795658
+ <header class="observability-card__header"><h2 class="observability-card__title">Job history</h2><span class="observability-card__meta">latest 20 runs</span></header>
795659
+ <div class="observability-card__body observability-table-scroll"><div id="jobs-list" style="font-size:0.78rem"></div></div>
795660
+ </section>
795661
+ </div>
795060
795662
  </div>
795061
- <div id="config-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795062
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Server Configuration</h3>
795063
- <div id="config-content" style="font-size:0.78rem"></div>
795064
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Model</h3>
795065
- <div style="display:flex;gap:8px;align-items:center">
795663
+ <div id="config-panel" class="omnius-route-shell" data-route-page="settings" data-route-tab="config" style="display:none;flex:1;overflow-y:auto">
795664
+ ${renderRouteHeading({ page: "settings", eyebrow: "Control plane", title: "Settings", description: "Inspect daemon configuration, choose models and providers, manage profiles, and export session data from one modular surface.", signals: ["runtime", "models", "providers", "profiles"] })}
795665
+ <div class="observability-grid" data-layout="config-form">
795666
+ <section class="observability-card observability-span-2" data-module="server-configuration">
795667
+ <header class="observability-card__header"><h2 class="observability-card__title">Server configuration</h2><span class="observability-card__meta">read only</span></header>
795668
+ <div id="config-content" class="observability-card__body" style="font-size:0.78rem"></div>
795669
+ </section>
795670
+ <section class="observability-card observability-span-4" data-module="active-model">
795671
+ <header class="observability-card__header"><h2 class="observability-card__title">Model</h2><span class="observability-card__meta">active inference target</span></header>
795672
+ <div class="observability-card__body observability-actions">
795066
795673
  <select id="config-model-select" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem;flex:1"></select>
795067
795674
  <button onclick="switchModel()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">switch</button>
795068
- </div>
795069
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Inference Provider</h3>
795675
+ </div>
795676
+ </section>
795677
+ <section class="observability-card observability-span-6" data-module="inference-provider">
795678
+ <header class="observability-card__header"><h2 class="observability-card__title">Inference provider</h2><span class="observability-card__meta">connection + authentication</span></header>
795679
+ <div class="observability-card__body">
795070
795680
  <div id="config-endpoint" style="font-size:0.78rem;color:var(--color-fg-muted);margin-bottom:8px"></div>
795071
795681
  <div id="config-recent-providers-section" style="display:none;margin-bottom:12px">
795072
795682
  <div style="font-size:0.66rem;color:var(--color-fg-muted);margin-bottom:6px;font-weight:500">Previously connected</div>
@@ -795082,48 +795692,71 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795082
795692
  </select>
795083
795693
  <button type="submit" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">set</button>
795084
795694
  </form>
795085
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Profiles</h3>
795086
- <div id="config-profiles" style="font-size:0.78rem"></div>
795087
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Export Conversation</h3>
795088
- <div style="display:flex;gap:8px">
795695
+ </div>
795696
+ </section>
795697
+ <section class="observability-card observability-span-3" data-module="agent-profiles">
795698
+ <header class="observability-card__header"><h2 class="observability-card__title">Profiles</h2><span class="observability-card__meta">agent personas</span></header>
795699
+ <div id="config-profiles" class="observability-card__body" style="font-size:0.78rem"></div>
795700
+ </section>
795701
+ <section class="observability-card observability-span-3" data-module="conversation-export">
795702
+ <header class="observability-card__header"><h2 class="observability-card__title">Export conversation</h2><span class="observability-card__meta">portable session record</span></header>
795703
+ <div class="observability-card__body observability-actions">
795089
795704
  <button onclick="exportChat('md')" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">markdown</button>
795090
795705
  <button onclick="exportChat('json')" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">JSON</button>
795091
795706
  </div>
795707
+ </section>
795708
+ </div>
795092
795709
  </div>
795093
- <div id="activity-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795094
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Recent Activity (Audit Log)</h3>
795095
- <div id="activity-feed" style="font-size:0.72rem"></div>
795710
+ <div id="activity-panel" class="omnius-route-shell" data-route-page="activity" data-route-tab="activity" style="display:none;flex:1;overflow-y:auto">
795711
+ ${renderRouteHeading({ page: "activity", eyebrow: "Audit", title: "Recent activity", description: "Review request outcomes, routes, latency, and actor context as a continuous operational record.", signals: ["status", "latency", "actor"] })}
795712
+ <div class="observability-grid" data-layout="activity-ledger">
795713
+ <section class="observability-card observability-span-6" data-module="audit-log">
795714
+ <header class="observability-card__header"><h2 class="observability-card__title">Audit log</h2><span class="observability-card__meta">latest 50 records</span></header>
795715
+ <div id="activity-feed" class="observability-card__body" style="font-size:0.72rem"></div>
795716
+ </section>
795717
+ </div>
795096
795718
  </div>
795097
- <div id="projects-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795098
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
795099
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:0">Projects</h3>
795100
- <div style="display:flex;gap:6px;align-items:center">
795719
+ <div id="projects-panel" class="omnius-route-shell" data-route-page="projects" data-route-tab="projects" style="display:none;flex:1;overflow-y:auto">
795720
+ ${renderRouteHeading({ page: "projects", eyebrow: "Workspace registry", title: "Projects", description: "Find, register, and activate workspaces while keeping the current execution context unambiguous.", signals: ["active context", "registry", "discovery"] })}
795721
+ <div class="observability-grid" data-layout="project-toolbar">
795722
+ <section class="observability-card observability-span-6" data-module="workspace-tools">
795723
+ <header class="observability-card__header"><h2 class="observability-card__title">Workspace tools</h2><span class="observability-card__meta">filter · scan · register</span></header>
795724
+ <div class="observability-card__body observability-actions">
795101
795725
  <input id="project-search" type="text" placeholder="Filter projects…" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:3px 8px;border-radius:3px;font-size:0.65rem;width:140px;font-family:inherit" oninput="filterProjects()">
795102
795726
  <button id="scan-btn" onclick="scanProjects()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:3px 10px;border-radius:3px;font-size:0.65rem;cursor:pointer;font-family:inherit">🔍 Scan</button>
795103
795727
  <button id="add-project-btn" onclick="showAddProject()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:3px 10px;border-radius:3px;font-size:0.65rem;cursor:pointer;font-family:inherit">+ Add</button>
795104
795728
  </div>
795105
- </div>
795106
- <div style="font-size:0.65rem;color:var(--color-fg-subtle);margin-bottom:10px">Every folder where you have run <code style="color:var(--color-brand)">omnius</code> is registered here. Click to switch workspace.</div>
795107
- <div id="projects-current" style="background:var(--color-bg);border-left:2px solid var(--color-brand);padding:8px 12px;margin-bottom:12px;font-size:0.72rem;color:var(--color-fg)">
795729
+ </section>
795730
+ <section class="observability-card observability-span-2" data-module="current-workspace">
795731
+ <header class="observability-card__header"><h2 class="observability-card__title">Current workspace</h2><span class="observability-card__meta">active context</span></header>
795732
+ <div id="projects-current" class="observability-card__body" style="font-size:0.72rem;color:var(--color-fg)">
795108
795733
  <span style="color:var(--color-fg-subtle)">current:</span> <span id="projects-current-name" style="color:var(--color-brand)">(none)</span>
795109
795734
  <span id="projects-current-root" style="color:var(--color-fg-subtle);margin-left:8px"></span>
795110
795735
  </div>
795111
- <div id="projects-list" style="font-size:0.72rem"></div>
795112
- <div id="add-project-form" style="display:none;margin-top:10px;padding:10px;background:var(--color-bg);border:1px solid var(--color-border);border-radius:4px">
795113
- <div style="font-size:0.7rem;color:var(--color-brand);margin-bottom:6px">Add Project</div>
795736
+ </section>
795737
+ <section class="observability-card observability-span-4" data-module="workspace-registry">
795738
+ <header class="observability-card__header"><h2 class="observability-card__title">Registered workspaces</h2><span class="observability-card__meta">select to activate</span></header>
795739
+ <div id="projects-list" class="observability-card__body" style="font-size:0.72rem"></div>
795740
+ </section>
795741
+ <section id="add-project-form" class="observability-card observability-span-6" data-module="register-workspace" style="display:none">
795742
+ <header class="observability-card__header"><h2 class="observability-card__title">Add project</h2><span class="observability-card__meta">absolute filesystem path</span></header>
795743
+ <div class="observability-card__body">
795114
795744
  <input id="add-project-path" type="text" placeholder="/absolute/path/to/project" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:4px 8px;border-radius:3px;font-size:0.65rem;width:100%;margin-bottom:6px;font-family:inherit">
795115
- <div style="display:flex;gap:6px">
795745
+ <div class="observability-actions">
795116
795746
  <button onclick="addProject()" style="background:var(--color-brand);border:none;color:var(--color-accent-fg);padding:4px 12px;border-radius:3px;font-size:0.65rem;cursor:pointer;font-family:inherit">Register</button>
795117
795747
  <button onclick="hideAddProject()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:4px 12px;border-radius:3px;font-size:0.65rem;cursor:pointer;font-family:inherit">Cancel</button>
795118
795748
  </div>
795749
+ </div>
795750
+ </section>
795119
795751
  </div>
795120
795752
  </div>
795121
795753
 
795122
795754
  <!-- Generate tab — global media generation (image/video/audio/music) backed by
795123
795755
  the unified ~/.omnius model store. Loads models from /v1/media/models,
795124
795756
  generates via /v1/media/<kind>, and browses the global gallery. -->
795125
- <div id="generate-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795126
- <div class="hud-workbench">
795757
+ <div id="generate-panel" class="omnius-route-shell" data-route-page="generate" data-route-tab="generate" style="display:none;flex:1;overflow-y:auto">
795758
+ ${renderRouteHeading({ page: "generate", eyebrow: "Media operations", title: "Generate", description: "Move between synthesis, perception, model storage, evidence output, and the media archive without leaving the workbench.", signals: ["synthesis", "analysis", "signal", "archive"] })}
795759
+ <div class="hud-workbench" data-layout="generate-workbench">
795127
795760
  <aside class="hud-panel hud-panel--slant hud-rail" id="gen-mode-rail">
795128
795761
  <div class="hud-panel__chrome" aria-hidden="true"></div>
795129
795762
  <header class="hud-panel__header">
@@ -795292,9 +795925,12 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795292
795925
  </div>
795293
795926
 
795294
795927
  <!-- Voice tab — voicechat toggle + clone management. AudioWorklet drives mic capture; WebAudio handles TTS playback. -->
795295
- <div id="voice-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795296
- <div style="margin-bottom:18px">
795297
- <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">voicechat</div>
795928
+ <div id="voice-panel" class="omnius-route-shell" data-route-page="voice" data-route-tab="voice" style="display:none;flex:1;overflow-y:auto">
795929
+ ${renderRouteHeading({ page: "voice", eyebrow: "Realtime audio", title: "Voice", description: "Control the live voice session, active model, model-specific options, transcript, and speech test from one observable surface.", signals: ["session", "model", "transcript", "TTS"] })}
795930
+ <div class="observability-grid" data-layout="voice-controls">
795931
+ <section class="observability-card observability-span-4" data-module="voice-session">
795932
+ <header class="observability-card__header"><h2 class="observability-card__title">Voice session</h2><span class="observability-card__meta">mic ↔ ASR ↔ agent ↔ TTS</span></header>
795933
+ <div class="observability-card__body">
795298
795934
  <div style="font-size:0.65rem;color:var(--color-fg-subtle);margin-bottom:8px">
795299
795935
  Live mic ↔ ASR ↔ agent ↔ TTS over WebSocket. Audio routes over the same origin
795300
795936
  as this page — works on localhost or over a forwarded public IP. Mic permission
@@ -795306,26 +795942,29 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795306
795942
  <span id="voice-mic-pill" style="display:none;background:var(--color-bg);border-left:2px solid var(--color-success);padding:4px 10px;font-size:0.6rem;color:var(--color-success)">● mic active</span>
795307
795943
  </div>
795308
795944
  <div id="voice-transcript-pane" style="display:none;background:var(--color-bg);border:1px solid var(--color-bg-input);border-radius:3px;padding:10px;margin-top:6px;min-height:80px;max-height:280px;overflow-y:auto;font-size:0.7rem"></div>
795309
- </div>
795945
+ </div>
795946
+ </section>
795310
795947
 
795311
- <div style="margin-bottom:18px">
795312
- <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">voice model</div>
795313
- <div style="display:flex;gap:8px;align-items:center">
795948
+ <section class="observability-card observability-span-2" data-module="voice-model">
795949
+ <header class="observability-card__header"><h2 class="observability-card__title">Voice model</h2><span class="observability-card__meta">active engine</span></header>
795950
+ <div class="observability-card__body observability-actions">
795314
795951
  <select id="voice-model-select" onchange="renderVoiceModelOptions()" style="background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:5px 10px;border-radius:3px;font-family:inherit;font-size:0.7rem;flex:1"></select>
795315
795952
  <button onclick="switchVoiceModel()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:5px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">switch</button>
795316
795953
  </div>
795317
- </div>
795954
+ </section>
795318
795955
 
795319
- <div id="voice-model-options-panel" style="margin-bottom:18px">
795320
- <div id="voice-model-options-content" style="font-size:0.7rem;color:var(--color-fg-muted)"></div>
795321
- </div>
795956
+ <section id="voice-model-options-panel" class="observability-card observability-span-3" data-module="voice-model-options">
795957
+ <header class="observability-card__header"><h2 class="observability-card__title">Model options</h2><span class="observability-card__meta">engine capabilities</span></header>
795958
+ <div id="voice-model-options-content" class="observability-card__body" style="font-size:0.7rem;color:var(--color-fg-muted)"></div>
795959
+ </section>
795322
795960
 
795323
- <div style="margin-bottom:18px">
795324
- <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">say something (test TTS)</div>
795325
- <div style="display:flex;gap:8px">
795961
+ <section class="observability-card observability-span-3" data-module="voice-tts-test">
795962
+ <header class="observability-card__header"><h2 class="observability-card__title">Speech test</h2><span class="observability-card__meta">test active TTS</span></header>
795963
+ <div class="observability-card__body observability-actions">
795326
795964
  <input type="text" id="voice-test-text" placeholder="Type and the voice model speaks it..." style="flex:1;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px 10px;border-radius:3px;font-family:inherit;font-size:0.7rem">
795327
795965
  <button onclick="testTTS()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:5px 14px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">speak</button>
795328
795966
  </div>
795967
+ </section>
795329
795968
  </div>
795330
795969
  </div>
795331
795970
 
@@ -795398,7 +796037,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795398
796037
  the mic button on the input row. The panel hits /v1/voice/start
795399
796038
  to warm the engines on first open, /v1/voice/tts to synthesize
795400
796039
  arbitrary text, and /v1/voice/transcribe with mic blob bytes. -->
795401
- <div id="voice-panel" style="display:none;padding:8px 10px;background:var(--color-bg-elevated);border-top:1px solid var(--color-border);font-size:0.78rem">
796040
+ <div id="voice-mini-panel" style="display:none;padding:8px 10px;background:var(--color-bg-elevated);border-top:1px solid var(--color-border);font-size:0.78rem">
795402
796041
  <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
795403
796042
  <span id="voice-state-badge" style="padding:2px 8px;border-radius:10px;background:var(--color-bg-input);border:1px solid var(--color-border);font-size:0.7rem;color:var(--color-fg-muted)">idle</span>
795404
796043
  <button type="button" onclick="warmVoice()" style="font-size:0.74rem">warm</button>
@@ -795420,7 +796059,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795420
796059
  <div id="input-row">
795421
796060
  <span id="system-prompt-toggle" onclick="toggleSystemPrompt()" style="display:none">sys</span>
795422
796061
  <textarea id="input-area" placeholder="Type a message..." rows="1"></textarea>
795423
- <button id="voice-toggle-btn" type="button" onclick="toggleVoicePanel()" title="open voice panel" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg-muted);padding:10px 12px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">🎙</button>
796062
+ <button id="voice-input-toggle-btn" type="button" onclick="toggleVoicePanel()" title="open voice panel" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg-muted);padding:10px 12px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">🎙</button>
795424
796063
  <button id="send-btn" onclick="sendMessage()">send</button>
795425
796064
  <button id="stop-btn" onclick="stopChat()" style="display:none;background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:10px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">stop</button>
795426
796065
  <!-- WO-CHAT-CHECKIN — teal accent button that takes the place of stop
@@ -795559,6 +796198,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795559
796198
  </div>
795560
796199
 
795561
796200
  <script>
796201
+ const webUiRouteSpecs = ${JSON.stringify(WEB_UI_ROUTE_SPECS)};
795562
796202
  // ════════════════════════════════════════════════════════════════════
795563
796203
  // Store layer — Svelte-writable shape, vanilla JS.
795564
796204
  // ════════════════════════════════════════════════════════════════════
@@ -795819,18 +796459,8 @@ const $activeTab = writable('chat');
795819
796459
 
795820
796460
  function parseInitialGuiRoute() {
795821
796461
  const path = (location.pathname || '/').replace(/\\/+$/, '') || '/';
795822
- const pathToTab = {
795823
- '/chat': 'chat',
795824
- '/agent': 'agent',
795825
- '/voice': 'voice',
795826
- '/projects': 'projects',
795827
- '/dashboard': 'jobs',
795828
- '/jobs': 'jobs',
795829
- '/activity': 'activity',
795830
- '/settings': 'config',
795831
- '/config': 'config',
795832
- };
795833
- const tab = pathToTab[path] || null;
796462
+ const matched = webUiRouteSpecs.find(route => route.path === path || route.aliases.includes(path));
796463
+ const tab = matched?.tab || null;
795834
796464
  let chatSession = null;
795835
796465
  if (tab === 'chat') {
795836
796466
  const qs = location.search.replace(/^\\?/, '').trim();
@@ -796029,36 +796659,18 @@ function _projectDisplayName(proj) {
796029
796659
  if (!proj) return 'No project';
796030
796660
  return proj.name || _projectPathLeaf(proj.root) || proj.root || 'Project';
796031
796661
  }
796032
- function _endpointDisplayName(endpoint) {
796033
- if (!endpoint) return '';
796034
- try { return new URL(endpoint).host || endpoint; } catch { return String(endpoint); }
796035
- }
796036
796662
  function renderProjectChrome() {
796037
796663
  const proj = $currentProject.get ? $currentProject.get() : null;
796038
- const cfg = $config.get ? ($config.get() || {}) : {};
796039
796664
  const root = proj?.root || '';
796040
796665
  const name = _projectDisplayName(proj);
796041
- const headerName = document.getElementById('header-project-name') || document.querySelector('#header .accent');
796042
- if (headerName) {
796043
- headerName.textContent = name;
796044
- headerName.title = root || name;
796045
- }
796046
796666
  const brand = document.getElementById('sidebar-brand');
796047
796667
  if (brand) {
796048
796668
  brand.textContent = name;
796049
796669
  brand.title = root || name;
796050
796670
  }
796051
796671
  if (statusEl) {
796052
- const selected = ($selectedModel.get && $selectedModel.get()) || (modelSelect && modelSelect.value) || '';
796053
- const endpointObj = cfg.endpoint || {};
796054
- const endpoint = endpointObj.url || cfg.backendUrl || '';
796055
- const endpointType = endpointObj.backendType || cfg.backendType || '';
796056
- const endpointLabel = endpoint ? ((endpointType ? endpointType + ' ' : '') + _endpointDisplayName(endpoint)) : '';
796057
- const bits = [String(_connectionState || 'connecting')];
796058
- if (selected) bits.push(selected);
796059
- if (endpointLabel) bits.push(endpointLabel);
796060
- statusEl.textContent = bits.join(' · ');
796061
- statusEl.title = [root, endpoint].filter(Boolean).join('\\n');
796672
+ statusEl.textContent = String(_connectionState || 'connecting');
796673
+ statusEl.title = root || name;
796062
796674
  statusEl.className = /connected|ready|online/i.test(_connectionState || '') ? 'status live' : 'status';
796063
796675
  }
796064
796676
  try { _syncSidebarStatus(); } catch {}
@@ -796067,6 +796679,171 @@ function setConnectionState(state) {
796067
796679
  _connectionState = state || 'connecting';
796068
796680
  renderProjectChrome();
796069
796681
  }
796682
+
796683
+ // Accessible workspace switcher based on the compact search/listbox pattern
796684
+ // used by Voryn. Project state still flows through Omnius's central store.
796685
+ let workspacePickerProjects = [];
796686
+ let workspacePickerIndex = -1;
796687
+ function workspacePickerElements() {
796688
+ return {
796689
+ trigger: document.getElementById('workspace-switcher-trigger'),
796690
+ picker: document.getElementById('workspace-picker'),
796691
+ search: document.getElementById('workspace-picker-search'),
796692
+ list: document.getElementById('workspace-picker-list'),
796693
+ };
796694
+ }
796695
+ function isWorkspacePickerOpen() {
796696
+ const { picker } = workspacePickerElements();
796697
+ return !!picker && picker.style.display !== 'none';
796698
+ }
796699
+ async function openWorkspacePicker() {
796700
+ const { trigger, picker, search } = workspacePickerElements();
796701
+ if (!trigger || !picker || !search) return;
796702
+ picker.style.display = 'block';
796703
+ trigger.setAttribute('aria-expanded', 'true');
796704
+ workspacePickerIndex = -1;
796705
+ await loadWorkspacePicker();
796706
+ requestAnimationFrame(() => { try { search.focus(); search.select(); } catch {} });
796707
+ }
796708
+ function closeWorkspacePicker(restoreFocus) {
796709
+ const { trigger, picker, search } = workspacePickerElements();
796710
+ if (!trigger || !picker) return;
796711
+ picker.style.display = 'none';
796712
+ trigger.setAttribute('aria-expanded', 'false');
796713
+ workspacePickerIndex = -1;
796714
+ if (search) search.value = '';
796715
+ if (restoreFocus) { try { trigger.focus(); } catch {} }
796716
+ }
796717
+ function toggleWorkspacePicker(event) {
796718
+ if (event) event.stopPropagation();
796719
+ if (isWorkspacePickerOpen()) closeWorkspacePicker(false);
796720
+ else openWorkspacePicker();
796721
+ }
796722
+ function workspaceAgo(ts) {
796723
+ if (!ts) return '';
796724
+ const seconds = Math.max(0, Math.floor((Date.now() - Number(ts)) / 1000));
796725
+ if (seconds < 60) return seconds + 's';
796726
+ if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
796727
+ if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';
796728
+ return Math.floor(seconds / 86400) + 'd';
796729
+ }
796730
+ function renderWorkspacePicker(query) {
796731
+ const { list } = workspacePickerElements();
796732
+ if (!list) return;
796733
+ const currentRoot = $currentProject.get?.()?.root || '';
796734
+ const needle = String(query || '').trim().toLowerCase();
796735
+ const matches = workspacePickerProjects.filter(project => {
796736
+ const haystack = (_projectDisplayName(project) + ' ' + (project.root || '')).toLowerCase();
796737
+ return !needle || haystack.includes(needle);
796738
+ });
796739
+ list.replaceChildren();
796740
+ if (!matches.length) {
796741
+ const empty = document.createElement('div');
796742
+ empty.className = 'workspace-picker__empty';
796743
+ empty.textContent = needle ? 'No matching workspaces' : 'No registered workspaces';
796744
+ list.appendChild(empty);
796745
+ return;
796746
+ }
796747
+ for (const project of matches) {
796748
+ const active = project.root === currentRoot;
796749
+ const option = document.createElement('button');
796750
+ option.type = 'button';
796751
+ option.className = 'workspace-picker__option';
796752
+ option.setAttribute('role', 'option');
796753
+ option.setAttribute('aria-selected', active ? 'true' : 'false');
796754
+ option.dataset.root = project.root || '';
796755
+ const copy = document.createElement('span');
796756
+ copy.style.minWidth = '0';
796757
+ const name = document.createElement('span');
796758
+ name.className = 'workspace-picker__name';
796759
+ name.textContent = _projectDisplayName(project);
796760
+ const path = document.createElement('span');
796761
+ path.className = 'workspace-picker__path';
796762
+ const age = workspaceAgo(project.lastSeen);
796763
+ path.textContent = (project.root || '') + (age ? ' · seen ' + age + ' ago' : '');
796764
+ const mark = document.createElement('span');
796765
+ mark.className = 'workspace-picker__check';
796766
+ mark.textContent = active ? 'active' : 'switch';
796767
+ copy.append(name, path);
796768
+ option.append(copy, mark);
796769
+ option.addEventListener('click', () => switchProjectRoot(project.root, option));
796770
+ list.appendChild(option);
796771
+ }
796772
+ }
796773
+ async function loadWorkspacePicker() {
796774
+ const { list, search } = workspacePickerElements();
796775
+ if (!list) return;
796776
+ list.innerHTML = '<div class="workspace-picker__empty">Loading workspaces...</div>';
796777
+ try {
796778
+ const response = await fetch('/v1/projects', { headers: headers() });
796779
+ if (!response.ok) throw new Error('HTTP ' + response.status);
796780
+ const data = await response.json();
796781
+ workspacePickerProjects = Array.isArray(data.projects) ? data.projects : [];
796782
+ const current = data.current && typeof data.current.root === 'string' ? data.current : null;
796783
+ if ($currentProject.get?.()?.root !== current?.root) $currentProject.set(current);
796784
+ renderWorkspacePicker(search?.value || '');
796785
+ } catch (error) {
796786
+ list.innerHTML = '<div class="workspace-picker__empty">Unable to load workspaces</div>';
796787
+ }
796788
+ }
796789
+ function filterWorkspacePicker(value) {
796790
+ workspacePickerIndex = -1;
796791
+ renderWorkspacePicker(value);
796792
+ }
796793
+ function handleWorkspacePickerKeys(event) {
796794
+ const { list } = workspacePickerElements();
796795
+ if (!list) return;
796796
+ const options = Array.from(list.querySelectorAll('.workspace-picker__option'));
796797
+ if (event.key === 'Escape') {
796798
+ event.preventDefault();
796799
+ closeWorkspacePicker(true);
796800
+ return;
796801
+ }
796802
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
796803
+ event.preventDefault();
796804
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
796805
+ workspacePickerIndex = Math.max(0, Math.min(options.length - 1, workspacePickerIndex + delta));
796806
+ options.forEach((option, index) => option.classList.toggle('is-keyboard-active', index === workspacePickerIndex));
796807
+ options[workspacePickerIndex]?.scrollIntoView({ block: 'nearest' });
796808
+ return;
796809
+ }
796810
+ if (event.key === 'Enter' && options.length) {
796811
+ event.preventDefault();
796812
+ options[Math.max(0, workspacePickerIndex)]?.click();
796813
+ }
796814
+ }
796815
+ async function switchProjectRoot(root, pendingElement) {
796816
+ if (!root) return false;
796817
+ if (pendingElement) pendingElement.disabled = true;
796818
+ try {
796819
+ const response = await fetch('/v1/projects/switch', {
796820
+ method: 'POST',
796821
+ headers: { 'Content-Type': 'application/json', ...headers() },
796822
+ body: JSON.stringify({ root }),
796823
+ });
796824
+ if (!response.ok) {
796825
+ const error = await response.json().catch(() => ({ message: 'HTTP ' + response.status }));
796826
+ throw new Error(error.message || error.error || 'unknown error');
796827
+ }
796828
+ closeWorkspacePicker(true);
796829
+ await loadProjects();
796830
+ return true;
796831
+ } catch (error) {
796832
+ alert('Switch failed: ' + (error && error.message ? error.message : String(error)));
796833
+ if (pendingElement) pendingElement.disabled = false;
796834
+ return false;
796835
+ }
796836
+ }
796837
+ document.addEventListener('pointerdown', (event) => {
796838
+ if (isWorkspacePickerOpen() && !event.target.closest('.workspace-switcher')) closeWorkspacePicker(false);
796839
+ });
796840
+ document.addEventListener('keydown', (event) => {
796841
+ if (event.key === 'Escape' && isWorkspacePickerOpen()) closeWorkspacePicker(true);
796842
+ });
796843
+ window.toggleWorkspacePicker = toggleWorkspacePicker;
796844
+ window.closeWorkspacePicker = closeWorkspacePicker;
796845
+ window.filterWorkspacePicker = filterWorkspacePicker;
796846
+ window.handleWorkspacePickerKeys = handleWorkspacePickerKeys;
796070
796847
  let streaming = false;
796071
796848
  let messages = [];
796072
796849
  let chatAbortController = null; // for stop button
@@ -797791,7 +798568,7 @@ async function _refreshVoiceState() {
797791
798568
  }
797792
798569
 
797793
798570
  function toggleVoicePanel() {
797794
- const p = document.getElementById('voice-panel');
798571
+ const p = document.getElementById('voice-mini-panel');
797795
798572
  if (!p) return;
797796
798573
  if (p.style.display === 'none') {
797797
798574
  p.style.display = 'block';
@@ -797801,7 +798578,7 @@ function toggleVoicePanel() {
797801
798578
  }
797802
798579
  }
797803
798580
  function closeVoicePanel() {
797804
- const p = document.getElementById('voice-panel');
798581
+ const p = document.getElementById('voice-mini-panel');
797805
798582
  if (p) p.style.display = 'none';
797806
798583
  }
797807
798584
  window.toggleVoicePanel = toggleVoicePanel;
@@ -798081,36 +798858,14 @@ window.closeRemoteConnection = closeRemoteConnection;
798081
798858
  })();
798082
798859
 
798083
798860
  // Tab switching + browser route sync
798084
- const allPanels = ['chat-container','agent-panel','jobs-panel','config-panel','activity-panel','projects-panel','voice-panel','generate-panel'];
798861
+ const allPanels = webUiRouteSpecs.map(route => route.panelId);
798085
798862
  function routePathForTab(tab) {
798086
- const map = {
798087
- chat: '/chat',
798088
- agent: '/agent',
798089
- voice: '/voice',
798090
- generate: '/generate',
798091
- projects: '/projects',
798092
- jobs: '/dashboard',
798093
- activity: '/activity',
798094
- config: '/settings',
798095
- };
798096
- return map[tab] || '/chat';
798863
+ return webUiRouteSpecs.find(route => route.tab === tab)?.path || '/chat';
798097
798864
  }
798098
798865
  function tabForRoutePath(pathname) {
798099
798866
  const path = (pathname || '/').replace(/\\/+$/, '') || '/';
798100
- const map = {
798101
- '/': 'chat',
798102
- '/chat': 'chat',
798103
- '/agent': 'agent',
798104
- '/voice': 'voice',
798105
- '/generate': 'generate',
798106
- '/projects': 'projects',
798107
- '/dashboard': 'jobs',
798108
- '/jobs': 'jobs',
798109
- '/activity': 'activity',
798110
- '/settings': 'config',
798111
- '/config': 'config',
798112
- };
798113
- return map[path] || 'chat';
798867
+ const matched = webUiRouteSpecs.find(route => route.path === path || route.aliases.includes(path));
798868
+ return matched?.tab || 'chat';
798114
798869
  }
798115
798870
  function chatSessionFromRouteSearch(search) {
798116
798871
  const qs = String(search || '').replace(/^\\?/, '').trim();
@@ -798132,13 +798887,19 @@ function syncRouteForTab(tab, replace) {
798132
798887
  }
798133
798888
  function switchTab(tab, opts) {
798134
798889
  opts = opts || {};
798135
- const panelMap = {chat:'chat-container',agent:'agent-panel',jobs:'jobs-panel',config:'config-panel',activity:'activity-panel',projects:'projects-panel',voice:'voice-panel',generate:'generate-panel'};
798136
- allPanels.forEach(id => { const el = document.getElementById(id); if(el) el.style.display = 'none'; });
798137
- const panel = document.getElementById(panelMap[tab]);
798890
+ const route = webUiRouteSpecs.find(candidate => candidate.tab === tab) || webUiRouteSpecs[0];
798891
+ allPanels.forEach(id => {
798892
+ const el = document.getElementById(id);
798893
+ if (!el) return;
798894
+ el.style.display = 'none';
798895
+ el.setAttribute('aria-hidden', 'true');
798896
+ });
798897
+ const panel = document.getElementById(route.panelId);
798138
798898
  if (panel) {
798139
798899
  // chat AND agent are flex containers (vertical column with sidebar/topbar);
798140
798900
  // other panels are simple block scrollers.
798141
798901
  panel.style.display = (tab === 'chat' || tab === 'agent') ? 'flex' : 'block';
798902
+ panel.setAttribute('aria-hidden', 'false');
798142
798903
  }
798143
798904
  document.getElementById('footer').style.display = tab === 'chat' ? 'flex' : 'none';
798144
798905
  document.querySelectorAll('.tab').forEach(t => { t.style.borderBottomColor = 'transparent'; t.style.color = 'var(--color-fg-faint)'; });
@@ -798146,7 +798907,10 @@ function switchTab(tab, opts) {
798146
798907
  if (active) { active.style.borderBottomColor = 'var(--color-brand)'; active.style.color = 'var(--color-brand)'; }
798147
798908
  // OWUI-2: mirror active state on the sidebar nav
798148
798909
  document.querySelectorAll('#omnius-sidebar .sb-nav').forEach(b => {
798149
- b.classList.toggle('active', b.getAttribute('data-tab') === tab);
798910
+ const isActive = b.getAttribute('data-tab') === tab;
798911
+ b.classList.toggle('active', isActive);
798912
+ if (isActive) b.setAttribute('aria-current', 'page');
798913
+ else b.removeAttribute('aria-current');
798150
798914
  });
798151
798915
  if ($activeTab.get && $activeTab.get() !== tab) $activeTab.set(tab);
798152
798916
  if (!opts.fromRoute) syncRouteForTab(tab, !!opts.replaceRoute);
@@ -798169,6 +798933,9 @@ function switchTab(tab, opts) {
798169
798933
  }
798170
798934
  }
798171
798935
  window.switchTab = switchTab;
798936
+ // Route activation must not depend on a registered/current project. Apply it
798937
+ // immediately, then project preference hydration may fill route-local state.
798938
+ if (initialGuiRoute.tab) switchTab(initialGuiRoute.tab, { fromRoute: true });
798172
798939
 
798173
798940
  // ════════════════════════════════════════════════════════════
798174
798941
  // Generate tab — global media generation (image/video/audio/music)
@@ -798785,27 +799552,8 @@ async function loadProjects() {
798785
799552
  const root = decodeURIComponent(el.getAttribute('data-root') || '');
798786
799553
  if (!root) return;
798787
799554
  el.style.opacity = '0.5';
798788
- try {
798789
- const r = await fetch('/v1/projects/switch', {
798790
- method: 'POST',
798791
- headers: { 'Content-Type': 'application/json', ...headers() },
798792
- body: JSON.stringify({ root }),
798793
- });
798794
- if (!r.ok) {
798795
- const err = await r.json().catch(() => ({ message: 'HTTP ' + r.status }));
798796
- alert('Switch failed: ' + (err.message || err.error || 'unknown'));
798797
- el.style.opacity = '1';
798798
- return;
798799
- }
798800
- // Successful switch: re-fetch /v1/projects which updates
798801
- // $currentProject; the subscription cascade refreshes models,
798802
- // config, sessions, agent runs, and restores the chat session
798803
- // for the new project. No more brittle ad-hoc fan-out.
798804
- await loadProjects();
798805
- } catch (e) {
798806
- alert('Switch failed: ' + (e && e.message ? e.message : String(e)));
798807
- el.style.opacity = '1';
798808
- }
799555
+ const switched = await switchProjectRoot(root, el);
799556
+ if (!switched) el.style.opacity = '1';
798809
799557
  });
798810
799558
  });
798811
799559
  } catch (e) {
@@ -799169,8 +799917,7 @@ async function loadDaemons() {
799169
799917
  el.innerHTML = '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;color:var(--color-fg-faint);font-size:0.7rem">No active processes</div>';
799170
799918
  return;
799171
799919
  }
799172
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Active Processes</h3>' +
799173
- d.runs.map(j =>
799920
+ el.innerHTML = d.runs.map(j =>
799174
799921
  '<div style="background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);padding:6px 10px;margin:4px 0;font-size:0.72rem">' +
799175
799922
  '<span style="color:var(--color-brand)">' + (j.id||'').slice(0,12) + '</span> ' +
799176
799923
  '<span style="color:var(--color-success)">running</span> ' +
@@ -799207,7 +799954,7 @@ async function loadScheduled() {
799207
799954
  + '</div>';
799208
799955
  return row;
799209
799956
  }).join('');
799210
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Scheduled Tasks</h3>' + rows
799957
+ el.innerHTML = rows
799211
799958
  + '<div style="margin-top:6px;display:flex;gap:8px">'
799212
799959
  + '<button onclick="disableAllScheduled()" title="Disable all scheduled tasks" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable all</button>'
799213
799960
  + '<button onclick="enableAllScheduled()" title="Enable all scheduled tasks" style="background:var(--color-bg-input);border:1px solid #2a3a2a;color:var(--color-success);padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable all</button>'
@@ -799369,7 +800116,7 @@ async function loadServices() {
799369
800116
  + '<div style="margin-top:4px;display:flex;gap:8px">' + stopBtn + disBtn + '</div>'
799370
800117
  + '</div>';
799371
800118
  }).join('');
799372
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
800119
+ el.innerHTML = rows;
799373
800120
  } catch {}
799374
800121
  }
799375
800122
 
@@ -799558,7 +800305,6 @@ async function loadDashboard() {
799558
800305
  const totalIn = d.persistent?.totalIn || d.totalTokensIn || 0;
799559
800306
  const totalOut = d.persistent?.totalOut || d.totalTokensOut || 0;
799560
800307
  document.getElementById('dashboard-usage').innerHTML =
799561
- '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Token Usage by Provider (persistent)</h3>' +
799562
800308
  '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px">' +
799563
800309
  '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1">' +
799564
800310
  '<div style="color:var(--color-fg-faint);font-size:0.6rem">TOTAL IN</div>' +
@@ -802524,6 +803270,7 @@ function toggleSidebar() {
802524
803270
  const sb = document.getElementById('omnius-sidebar');
802525
803271
  if (!sb) return;
802526
803272
  const next = sb.getAttribute('data-collapsed') !== 'true';
803273
+ if (next) closeWorkspacePicker(false);
802527
803274
  sb.setAttribute('data-collapsed', String(next));
802528
803275
  _setLS(SIDEBAR_KEYS.collapsed, next ? '1' : '0');
802529
803276
  }
@@ -803965,8 +804712,22 @@ input.focus();
803965
804712
  </body>
803966
804713
  </html>`;
803967
804714
  }
804715
+ var WEB_UI_ROUTE_SPECS, WEB_UI_ROUTE_PATHS;
803968
804716
  var init_web_ui = __esm({
803969
804717
  "packages/cli/src/api/web-ui.ts"() {
804718
+ WEB_UI_ROUTE_SPECS = Object.freeze([
804719
+ { tab: "chat", page: "chat", path: "/chat", aliases: ["/"], panelId: "chat-container" },
804720
+ { tab: "agent", page: "agent", path: "/agent", aliases: [], panelId: "agent-panel" },
804721
+ { tab: "voice", page: "voice", path: "/voice", aliases: [], panelId: "voice-panel" },
804722
+ { tab: "generate", page: "generate", path: "/generate", aliases: [], panelId: "generate-panel" },
804723
+ { tab: "projects", page: "projects", path: "/projects", aliases: [], panelId: "projects-panel" },
804724
+ { tab: "jobs", page: "dashboard", path: "/dashboard", aliases: ["/jobs"], panelId: "jobs-panel" },
804725
+ { tab: "activity", page: "activity", path: "/activity", aliases: [], panelId: "activity-panel" },
804726
+ { tab: "config", page: "settings", path: "/settings", aliases: ["/config"], panelId: "config-panel" }
804727
+ ]);
804728
+ WEB_UI_ROUTE_PATHS = Object.freeze(
804729
+ WEB_UI_ROUTE_SPECS.flatMap((route) => [route.path, ...route.aliases])
804730
+ );
803970
804731
  }
803971
804732
  });
803972
804733
 
@@ -811291,19 +812052,7 @@ async function handleRequest(req3, res, ollamaUrl, verbose, runtimeDefaults = {}
811291
812052
  res.end(svg);
811292
812053
  return;
811293
812054
  }
811294
- const guiRoutes = /* @__PURE__ */ new Set([
811295
- "/",
811296
- "/chat",
811297
- "/agent",
811298
- "/voice",
811299
- "/generate",
811300
- "/projects",
811301
- "/dashboard",
811302
- "/jobs",
811303
- "/activity",
811304
- "/settings",
811305
- "/config"
811306
- ]);
812055
+ const guiRoutes = new Set(WEB_UI_ROUTE_PATHS);
811307
812056
  if (guiRoutes.has(pathname) && method === "GET" && req3.headers.accept?.includes("text/html")) {
811308
812057
  const htmlHeaders = {
811309
812058
  "Content-Type": "text/html; charset=utf-8",