omnius 1.0.601 → 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
@@ -678990,6 +678990,7 @@ __export(tray_exports, {
678990
678990
  trayRequiresRestart: () => trayRequiresRestart,
678991
678991
  traySupport: () => traySupport,
678992
678992
  trayUpdatePresentation: () => trayUpdatePresentation,
678993
+ trayVersionPresentation: () => trayVersionPresentation,
678993
678994
  uninstallTrayAutostart: () => uninstallTrayAutostart,
678994
678995
  verifyTrayHelper: () => verifyTrayHelper
678995
678996
  });
@@ -679047,6 +679048,23 @@ function trayUpdatePresentation(currentVersion, latestVersion, state = null) {
679047
679048
  action: "check-update"
679048
679049
  };
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,
679065
+ enabled: false
679066
+ };
679067
+ }
679050
679068
  function uidSuffix() {
679051
679069
  return typeof process.getuid === "function" ? String(process.getuid()) : "user";
679052
679070
  }
@@ -679479,7 +679497,7 @@ async function loadSysTray() {
679479
679497
  return constructor;
679480
679498
  }
679481
679499
  function menuForHealth(health, endpoint, registered, update2) {
679482
- const healthItem = { title: health.label, tooltip: health.tooltip, enabled: false };
679500
+ const healthItem = trayVersionPresentation(health, update2);
679483
679501
  const actionItem = {
679484
679502
  title: health.kind === "online" ? "Restart daemon" : "Start daemon",
679485
679503
  tooltip: health.kind === "online" ? "Restart the Omnius API daemon" : "Start the Omnius API daemon",
@@ -679501,7 +679519,8 @@ function menuForHealth(health, endpoint, registered, update2) {
679501
679519
  title: update2.title,
679502
679520
  tooltip: update2.tooltip,
679503
679521
  enabled: update2.enabled,
679504
- action: update2.action
679522
+ action: update2.action,
679523
+ hidden: Boolean(update2.targetVersion)
679505
679524
  };
679506
679525
  return {
679507
679526
  healthItem,
@@ -679747,8 +679766,6 @@ async function runTrayForeground(explicitEndpoint) {
679747
679766
  health = next;
679748
679767
  state = { ...state, health };
679749
679768
  writeProcessState(paths, state);
679750
- menuState.healthItem.title = health.label;
679751
- menuState.healthItem.tooltip = health.tooltip;
679752
679769
  menuState.actionItem.title = health.kind === "online" ? "Restart daemon" : "Start daemon";
679753
679770
  menuState.actionItem.action = health.kind === "online" ? "restart-daemon" : "start-daemon";
679754
679771
  menuState.stopItem.enabled = health.kind === "online";
@@ -679762,6 +679779,13 @@ async function runTrayForeground(explicitEndpoint) {
679762
679779
  menuState.updateItem.tooltip = updateView.tooltip;
679763
679780
  menuState.updateItem.enabled = updateView.enabled;
679764
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;
679765
679789
  const autostartRegistered = existsSync117(paths.autostartFile);
679766
679790
  const autostartChanged = menuState.autostartItem.checked !== autostartRegistered;
679767
679791
  if (autostartChanged) menuState.autostartItem.checked = autostartRegistered;
@@ -679819,14 +679843,32 @@ async function runTrayForeground(explicitEndpoint) {
679819
679843
  trayWasRunning: true
679820
679844
  });
679821
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";
679822
679860
  menuState.updateItem.title = updateView.title;
679823
679861
  menuState.updateItem.tooltip = updateView.tooltip;
679824
679862
  menuState.updateItem.enabled = false;
679863
+ menuState.updateItem.hidden = true;
679825
679864
  } catch (error) {
679826
- menuState.updateItem.title = "Update could not start retry";
679827
- menuState.updateItem.tooltip = error instanceof Error ? error.message : String(error);
679828
- 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";
679829
679870
  }
679871
+ await tray?.sendAction({ type: "update-item", item: menuState.healthItem });
679830
679872
  await tray?.sendAction({ type: "update-item", item: menuState.updateItem });
679831
679873
  break;
679832
679874
  }
@@ -679843,6 +679885,14 @@ async function runTrayForeground(explicitEndpoint) {
679843
679885
  menuState.updateItem.tooltip = updateView.tooltip;
679844
679886
  menuState.updateItem.enabled = updateView.enabled;
679845
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 });
679846
679896
  await tray?.sendAction({ type: "update-item", item: menuState.updateItem });
679847
679897
  break;
679848
679898
  }
@@ -792889,6 +792939,17 @@ var init_routes_v1 = __esm({
792889
792939
  });
792890
792940
 
792891
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
+ }
792892
792953
  function getWebUI() {
792893
792954
  return `<!DOCTYPE html>
792894
792955
  <html lang="en">
@@ -794986,6 +795047,303 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
794986
795047
  }
794987
795048
  *::-webkit-scrollbar-thumb:hover { background: var(--nclp-yellow); }
794988
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
+
794989
795347
  @media (prefers-reduced-motion: reduce) {
794990
795348
  *, *::before, *::after {
794991
795349
  animation-duration: 0.01ms !important;
@@ -795014,8 +795372,22 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795014
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">
795015
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>
795016
795374
  </button>
795017
- <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>
795018
- <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>
795019
795391
  </div>
795020
795392
 
795021
795393
  <!-- New chat button -->
@@ -795051,35 +795423,35 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795051
795423
 
795052
795424
  <!-- Section nav (the legacy tabs migrated into the sidebar footer) -->
795053
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">
795054
- <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">
795055
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>
795056
795428
  <span class="sb-label">Chat</span>
795057
795429
  </button>
795058
- <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">
795059
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>
795060
795432
  <span class="sb-label">Agent</span>
795061
795433
  </button>
795062
- <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">
795063
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>
795064
795436
  <span class="sb-label">Voice</span>
795065
795437
  </button>
795066
- <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">
795067
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>
795068
795440
  <span class="sb-label">Generate</span>
795069
795441
  </button>
795070
- <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">
795071
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>
795072
795444
  <span class="sb-label">Projects</span>
795073
795445
  </button>
795074
- <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">
795075
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>
795076
795448
  <span class="sb-label">Dashboard</span>
795077
795449
  </button>
795078
- <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">
795079
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>
795080
795452
  <span class="sb-label">Activity</span>
795081
795453
  </button>
795082
- <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">
795083
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>
795084
795456
  <span class="sb-label">Settings</span>
795085
795457
  </button>
@@ -795105,8 +795477,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795105
795477
  <div id="omnius-main" style="flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden;background:var(--color-bg)">
795106
795478
 
795107
795479
  <div id="header">
795108
- <span class="accent" id="header-project-name">No project</span>
795109
- <span class="status" id="status">connecting...</span>
795480
+ <span class="status" id="status" aria-live="polite" style="display:none">connecting...</span>
795110
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>
795111
795482
  <select id="model-select"><option>loading...</option></select>
795112
795483
  <button class="key-btn" id="files-btn" onclick="toggleFilesForActiveTab()" title="Toggle workspace sidebar (chat or agent depending on active tab)">files</button>
@@ -795148,7 +795519,9 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795148
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>
795149
795520
  </div>
795150
795521
  </div>
795151
- <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">
795152
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">
795153
795526
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
795154
795527
  <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Workspace</span>
@@ -795158,7 +795531,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795158
795531
  <div id="workspace-tree" style="color:var(--color-fg)"></div>
795159
795532
  </div>
795160
795533
  <!-- Vertical column: session topbar + checklist + conversation -->
795161
- <div style="display:flex;flex-direction:column;flex:1;min-width:0;overflow:hidden">
795534
+ <div class="route-workspace__main">
795162
795535
  <!-- WO-TASK-02 — Chat session topbar (separate storage from agent runs) -->
795163
795536
  <div class="session-topbar" id="chat-session-topbar">
795164
795537
  <span class="topbar-label">chat session</span>
@@ -795175,8 +795548,10 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795175
795548
  </div>
795176
795549
  <div id="conversation" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:4px;min-height:0"></div>
795177
795550
  </div>
795551
+ </div>
795178
795552
  </div>
795179
- <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"] })}
795180
795555
  <!-- WO-TASK-02 — Agent run topbar (separate storage from chat sessions) -->
795181
795556
  <div class="session-topbar" id="agent-session-topbar">
795182
795557
  <span class="topbar-label">agent run</span>
@@ -795188,7 +795563,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795188
795563
  <button onclick="refreshAgentRunSelect()" title="Refresh the run list">⟳</button>
795189
795564
  </div>
795190
795565
  <!-- Body row: agent workspace sidebar (isolated from chat) | scrollable form -->
795191
- <div style="display:flex;flex:1;overflow:hidden;min-height:0">
795566
+ <div class="route-workspace" data-layout="agent-form">
795192
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">
795193
795568
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
795194
795569
  <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Agent Workspace</span>
@@ -795197,10 +795572,12 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795197
795572
  <div id="agent-workspace-cwd" style="color:var(--color-fg-faint);font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
795198
795573
  <div id="agent-workspace-tree" style="color:var(--color-fg)"></div>
795199
795574
  </div>
795200
- <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">
795201
795577
  <!-- Parameter form: all the dials for POST /v1/run -->
795202
- <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">
795203
- <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">
795204
795581
  <div style="display:grid;grid-template-columns:120px 1fr;gap:6px 12px;align-items:center">
795205
795582
  <label style="color:var(--color-fg-muted)">Working dir</label>
795206
795583
  <div style="display:flex;gap:4px;align-items:center">
@@ -795231,36 +795608,75 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795231
795608
  <label style="color:var(--color-fg-muted)">Isolate workspace</label>
795232
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>
795233
795610
  </div>
795234
- </div>
795611
+ </div>
795612
+ </section>
795235
795613
 
795236
- <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>
795237
- <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">
795238
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>
795239
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>
795240
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>
795241
795622
  </div>
795242
- <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>
795243
795631
  </div><!-- /scrollable agent body -->
795244
795632
  </div><!-- /agent body row (sidebar | form) -->
795245
795633
  </div>
795246
- <div id="jobs-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795247
- <div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
795248
- <div id="dashboard-daemons" style="margin-bottom:16px"></div>
795249
- <div id="dashboard-scheduled" style="margin-bottom:16px"></div>
795250
- <div id="dashboard-services" style="margin-bottom:16px"></div>
795251
- <div id="dashboard-usage" style="margin-bottom:16px"></div>
795252
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Job History</h3>
795253
- <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>
795254
795662
  </div>
795255
- <div id="config-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795256
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Server Configuration</h3>
795257
- <div id="config-content" style="font-size:0.78rem"></div>
795258
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Model</h3>
795259
- <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">
795260
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>
795261
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>
795262
- </div>
795263
- <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">
795264
795680
  <div id="config-endpoint" style="font-size:0.78rem;color:var(--color-fg-muted);margin-bottom:8px"></div>
795265
795681
  <div id="config-recent-providers-section" style="display:none;margin-bottom:12px">
795266
795682
  <div style="font-size:0.66rem;color:var(--color-fg-muted);margin-bottom:6px;font-weight:500">Previously connected</div>
@@ -795276,48 +795692,71 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795276
795692
  </select>
795277
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>
795278
795694
  </form>
795279
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Profiles</h3>
795280
- <div id="config-profiles" style="font-size:0.78rem"></div>
795281
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Export Conversation</h3>
795282
- <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">
795283
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>
795284
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>
795285
795706
  </div>
795707
+ </section>
795708
+ </div>
795286
795709
  </div>
795287
- <div id="activity-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795288
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Recent Activity (Audit Log)</h3>
795289
- <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>
795290
795718
  </div>
795291
- <div id="projects-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795292
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
795293
- <h3 style="color:var(--color-brand);font-size:0.7rem;margin:0">Projects</h3>
795294
- <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">
795295
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()">
795296
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>
795297
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>
795298
795728
  </div>
795299
- </div>
795300
- <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>
795301
- <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)">
795302
795733
  <span style="color:var(--color-fg-subtle)">current:</span> <span id="projects-current-name" style="color:var(--color-brand)">(none)</span>
795303
795734
  <span id="projects-current-root" style="color:var(--color-fg-subtle);margin-left:8px"></span>
795304
795735
  </div>
795305
- <div id="projects-list" style="font-size:0.72rem"></div>
795306
- <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">
795307
- <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">
795308
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">
795309
- <div style="display:flex;gap:6px">
795745
+ <div class="observability-actions">
795310
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>
795311
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>
795312
795748
  </div>
795749
+ </div>
795750
+ </section>
795313
795751
  </div>
795314
795752
  </div>
795315
795753
 
795316
795754
  <!-- Generate tab — global media generation (image/video/audio/music) backed by
795317
795755
  the unified ~/.omnius model store. Loads models from /v1/media/models,
795318
795756
  generates via /v1/media/<kind>, and browses the global gallery. -->
795319
- <div id="generate-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795320
- <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">
795321
795760
  <aside class="hud-panel hud-panel--slant hud-rail" id="gen-mode-rail">
795322
795761
  <div class="hud-panel__chrome" aria-hidden="true"></div>
795323
795762
  <header class="hud-panel__header">
@@ -795486,9 +795925,12 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795486
795925
  </div>
795487
795926
 
795488
795927
  <!-- Voice tab — voicechat toggle + clone management. AudioWorklet drives mic capture; WebAudio handles TTS playback. -->
795489
- <div id="voice-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
795490
- <div style="margin-bottom:18px">
795491
- <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">
795492
795934
  <div style="font-size:0.65rem;color:var(--color-fg-subtle);margin-bottom:8px">
795493
795935
  Live mic ↔ ASR ↔ agent ↔ TTS over WebSocket. Audio routes over the same origin
795494
795936
  as this page — works on localhost or over a forwarded public IP. Mic permission
@@ -795500,26 +795942,29 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795500
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>
795501
795943
  </div>
795502
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>
795503
- </div>
795945
+ </div>
795946
+ </section>
795504
795947
 
795505
- <div style="margin-bottom:18px">
795506
- <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">voice model</div>
795507
- <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">
795508
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>
795509
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>
795510
795953
  </div>
795511
- </div>
795954
+ </section>
795512
795955
 
795513
- <div id="voice-model-options-panel" style="margin-bottom:18px">
795514
- <div id="voice-model-options-content" style="font-size:0.7rem;color:var(--color-fg-muted)"></div>
795515
- </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>
795516
795960
 
795517
- <div style="margin-bottom:18px">
795518
- <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">say something (test TTS)</div>
795519
- <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">
795520
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">
795521
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>
795522
795966
  </div>
795967
+ </section>
795523
795968
  </div>
795524
795969
  </div>
795525
795970
 
@@ -795592,7 +796037,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795592
796037
  the mic button on the input row. The panel hits /v1/voice/start
795593
796038
  to warm the engines on first open, /v1/voice/tts to synthesize
795594
796039
  arbitrary text, and /v1/voice/transcribe with mic blob bytes. -->
795595
- <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">
795596
796041
  <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
795597
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>
795598
796043
  <button type="button" onclick="warmVoice()" style="font-size:0.74rem">warm</button>
@@ -795614,7 +796059,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795614
796059
  <div id="input-row">
795615
796060
  <span id="system-prompt-toggle" onclick="toggleSystemPrompt()" style="display:none">sys</span>
795616
796061
  <textarea id="input-area" placeholder="Type a message..." rows="1"></textarea>
795617
- <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>
795618
796063
  <button id="send-btn" onclick="sendMessage()">send</button>
795619
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>
795620
796065
  <!-- WO-CHAT-CHECKIN — teal accent button that takes the place of stop
@@ -795753,6 +796198,7 @@ html { scrollbar-color: var(--nclp-line-strong) var(--nclp-bg-main); }
795753
796198
  </div>
795754
796199
 
795755
796200
  <script>
796201
+ const webUiRouteSpecs = ${JSON.stringify(WEB_UI_ROUTE_SPECS)};
795756
796202
  // ════════════════════════════════════════════════════════════════════
795757
796203
  // Store layer — Svelte-writable shape, vanilla JS.
795758
796204
  // ════════════════════════════════════════════════════════════════════
@@ -796013,18 +796459,8 @@ const $activeTab = writable('chat');
796013
796459
 
796014
796460
  function parseInitialGuiRoute() {
796015
796461
  const path = (location.pathname || '/').replace(/\\/+$/, '') || '/';
796016
- const pathToTab = {
796017
- '/chat': 'chat',
796018
- '/agent': 'agent',
796019
- '/voice': 'voice',
796020
- '/projects': 'projects',
796021
- '/dashboard': 'jobs',
796022
- '/jobs': 'jobs',
796023
- '/activity': 'activity',
796024
- '/settings': 'config',
796025
- '/config': 'config',
796026
- };
796027
- const tab = pathToTab[path] || null;
796462
+ const matched = webUiRouteSpecs.find(route => route.path === path || route.aliases.includes(path));
796463
+ const tab = matched?.tab || null;
796028
796464
  let chatSession = null;
796029
796465
  if (tab === 'chat') {
796030
796466
  const qs = location.search.replace(/^\\?/, '').trim();
@@ -796223,36 +796659,18 @@ function _projectDisplayName(proj) {
796223
796659
  if (!proj) return 'No project';
796224
796660
  return proj.name || _projectPathLeaf(proj.root) || proj.root || 'Project';
796225
796661
  }
796226
- function _endpointDisplayName(endpoint) {
796227
- if (!endpoint) return '';
796228
- try { return new URL(endpoint).host || endpoint; } catch { return String(endpoint); }
796229
- }
796230
796662
  function renderProjectChrome() {
796231
796663
  const proj = $currentProject.get ? $currentProject.get() : null;
796232
- const cfg = $config.get ? ($config.get() || {}) : {};
796233
796664
  const root = proj?.root || '';
796234
796665
  const name = _projectDisplayName(proj);
796235
- const headerName = document.getElementById('header-project-name') || document.querySelector('#header .accent');
796236
- if (headerName) {
796237
- headerName.textContent = name;
796238
- headerName.title = root || name;
796239
- }
796240
796666
  const brand = document.getElementById('sidebar-brand');
796241
796667
  if (brand) {
796242
796668
  brand.textContent = name;
796243
796669
  brand.title = root || name;
796244
796670
  }
796245
796671
  if (statusEl) {
796246
- const selected = ($selectedModel.get && $selectedModel.get()) || (modelSelect && modelSelect.value) || '';
796247
- const endpointObj = cfg.endpoint || {};
796248
- const endpoint = endpointObj.url || cfg.backendUrl || '';
796249
- const endpointType = endpointObj.backendType || cfg.backendType || '';
796250
- const endpointLabel = endpoint ? ((endpointType ? endpointType + ' ' : '') + _endpointDisplayName(endpoint)) : '';
796251
- const bits = [String(_connectionState || 'connecting')];
796252
- if (selected) bits.push(selected);
796253
- if (endpointLabel) bits.push(endpointLabel);
796254
- statusEl.textContent = bits.join(' · ');
796255
- statusEl.title = [root, endpoint].filter(Boolean).join('\\n');
796672
+ statusEl.textContent = String(_connectionState || 'connecting');
796673
+ statusEl.title = root || name;
796256
796674
  statusEl.className = /connected|ready|online/i.test(_connectionState || '') ? 'status live' : 'status';
796257
796675
  }
796258
796676
  try { _syncSidebarStatus(); } catch {}
@@ -796261,6 +796679,171 @@ function setConnectionState(state) {
796261
796679
  _connectionState = state || 'connecting';
796262
796680
  renderProjectChrome();
796263
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;
796264
796847
  let streaming = false;
796265
796848
  let messages = [];
796266
796849
  let chatAbortController = null; // for stop button
@@ -797985,7 +798568,7 @@ async function _refreshVoiceState() {
797985
798568
  }
797986
798569
 
797987
798570
  function toggleVoicePanel() {
797988
- const p = document.getElementById('voice-panel');
798571
+ const p = document.getElementById('voice-mini-panel');
797989
798572
  if (!p) return;
797990
798573
  if (p.style.display === 'none') {
797991
798574
  p.style.display = 'block';
@@ -797995,7 +798578,7 @@ function toggleVoicePanel() {
797995
798578
  }
797996
798579
  }
797997
798580
  function closeVoicePanel() {
797998
- const p = document.getElementById('voice-panel');
798581
+ const p = document.getElementById('voice-mini-panel');
797999
798582
  if (p) p.style.display = 'none';
798000
798583
  }
798001
798584
  window.toggleVoicePanel = toggleVoicePanel;
@@ -798275,36 +798858,14 @@ window.closeRemoteConnection = closeRemoteConnection;
798275
798858
  })();
798276
798859
 
798277
798860
  // Tab switching + browser route sync
798278
- 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);
798279
798862
  function routePathForTab(tab) {
798280
- const map = {
798281
- chat: '/chat',
798282
- agent: '/agent',
798283
- voice: '/voice',
798284
- generate: '/generate',
798285
- projects: '/projects',
798286
- jobs: '/dashboard',
798287
- activity: '/activity',
798288
- config: '/settings',
798289
- };
798290
- return map[tab] || '/chat';
798863
+ return webUiRouteSpecs.find(route => route.tab === tab)?.path || '/chat';
798291
798864
  }
798292
798865
  function tabForRoutePath(pathname) {
798293
798866
  const path = (pathname || '/').replace(/\\/+$/, '') || '/';
798294
- const map = {
798295
- '/': 'chat',
798296
- '/chat': 'chat',
798297
- '/agent': 'agent',
798298
- '/voice': 'voice',
798299
- '/generate': 'generate',
798300
- '/projects': 'projects',
798301
- '/dashboard': 'jobs',
798302
- '/jobs': 'jobs',
798303
- '/activity': 'activity',
798304
- '/settings': 'config',
798305
- '/config': 'config',
798306
- };
798307
- return map[path] || 'chat';
798867
+ const matched = webUiRouteSpecs.find(route => route.path === path || route.aliases.includes(path));
798868
+ return matched?.tab || 'chat';
798308
798869
  }
798309
798870
  function chatSessionFromRouteSearch(search) {
798310
798871
  const qs = String(search || '').replace(/^\\?/, '').trim();
@@ -798326,13 +798887,19 @@ function syncRouteForTab(tab, replace) {
798326
798887
  }
798327
798888
  function switchTab(tab, opts) {
798328
798889
  opts = opts || {};
798329
- 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'};
798330
- allPanels.forEach(id => { const el = document.getElementById(id); if(el) el.style.display = 'none'; });
798331
- 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);
798332
798898
  if (panel) {
798333
798899
  // chat AND agent are flex containers (vertical column with sidebar/topbar);
798334
798900
  // other panels are simple block scrollers.
798335
798901
  panel.style.display = (tab === 'chat' || tab === 'agent') ? 'flex' : 'block';
798902
+ panel.setAttribute('aria-hidden', 'false');
798336
798903
  }
798337
798904
  document.getElementById('footer').style.display = tab === 'chat' ? 'flex' : 'none';
798338
798905
  document.querySelectorAll('.tab').forEach(t => { t.style.borderBottomColor = 'transparent'; t.style.color = 'var(--color-fg-faint)'; });
@@ -798340,7 +798907,10 @@ function switchTab(tab, opts) {
798340
798907
  if (active) { active.style.borderBottomColor = 'var(--color-brand)'; active.style.color = 'var(--color-brand)'; }
798341
798908
  // OWUI-2: mirror active state on the sidebar nav
798342
798909
  document.querySelectorAll('#omnius-sidebar .sb-nav').forEach(b => {
798343
- 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');
798344
798914
  });
798345
798915
  if ($activeTab.get && $activeTab.get() !== tab) $activeTab.set(tab);
798346
798916
  if (!opts.fromRoute) syncRouteForTab(tab, !!opts.replaceRoute);
@@ -798363,6 +798933,9 @@ function switchTab(tab, opts) {
798363
798933
  }
798364
798934
  }
798365
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 });
798366
798939
 
798367
798940
  // ════════════════════════════════════════════════════════════
798368
798941
  // Generate tab — global media generation (image/video/audio/music)
@@ -798979,27 +799552,8 @@ async function loadProjects() {
798979
799552
  const root = decodeURIComponent(el.getAttribute('data-root') || '');
798980
799553
  if (!root) return;
798981
799554
  el.style.opacity = '0.5';
798982
- try {
798983
- const r = await fetch('/v1/projects/switch', {
798984
- method: 'POST',
798985
- headers: { 'Content-Type': 'application/json', ...headers() },
798986
- body: JSON.stringify({ root }),
798987
- });
798988
- if (!r.ok) {
798989
- const err = await r.json().catch(() => ({ message: 'HTTP ' + r.status }));
798990
- alert('Switch failed: ' + (err.message || err.error || 'unknown'));
798991
- el.style.opacity = '1';
798992
- return;
798993
- }
798994
- // Successful switch: re-fetch /v1/projects which updates
798995
- // $currentProject; the subscription cascade refreshes models,
798996
- // config, sessions, agent runs, and restores the chat session
798997
- // for the new project. No more brittle ad-hoc fan-out.
798998
- await loadProjects();
798999
- } catch (e) {
799000
- alert('Switch failed: ' + (e && e.message ? e.message : String(e)));
799001
- el.style.opacity = '1';
799002
- }
799555
+ const switched = await switchProjectRoot(root, el);
799556
+ if (!switched) el.style.opacity = '1';
799003
799557
  });
799004
799558
  });
799005
799559
  } catch (e) {
@@ -799363,8 +799917,7 @@ async function loadDaemons() {
799363
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>';
799364
799918
  return;
799365
799919
  }
799366
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Active Processes</h3>' +
799367
- d.runs.map(j =>
799920
+ el.innerHTML = d.runs.map(j =>
799368
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">' +
799369
799922
  '<span style="color:var(--color-brand)">' + (j.id||'').slice(0,12) + '</span> ' +
799370
799923
  '<span style="color:var(--color-success)">running</span> ' +
@@ -799401,7 +799954,7 @@ async function loadScheduled() {
799401
799954
  + '</div>';
799402
799955
  return row;
799403
799956
  }).join('');
799404
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Scheduled Tasks</h3>' + rows
799957
+ el.innerHTML = rows
799405
799958
  + '<div style="margin-top:6px;display:flex;gap:8px">'
799406
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>'
799407
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>'
@@ -799563,7 +800116,7 @@ async function loadServices() {
799563
800116
  + '<div style="margin-top:4px;display:flex;gap:8px">' + stopBtn + disBtn + '</div>'
799564
800117
  + '</div>';
799565
800118
  }).join('');
799566
- el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
800119
+ el.innerHTML = rows;
799567
800120
  } catch {}
799568
800121
  }
799569
800122
 
@@ -799752,7 +800305,6 @@ async function loadDashboard() {
799752
800305
  const totalIn = d.persistent?.totalIn || d.totalTokensIn || 0;
799753
800306
  const totalOut = d.persistent?.totalOut || d.totalTokensOut || 0;
799754
800307
  document.getElementById('dashboard-usage').innerHTML =
799755
- '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Token Usage by Provider (persistent)</h3>' +
799756
800308
  '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px">' +
799757
800309
  '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1">' +
799758
800310
  '<div style="color:var(--color-fg-faint);font-size:0.6rem">TOTAL IN</div>' +
@@ -802718,6 +803270,7 @@ function toggleSidebar() {
802718
803270
  const sb = document.getElementById('omnius-sidebar');
802719
803271
  if (!sb) return;
802720
803272
  const next = sb.getAttribute('data-collapsed') !== 'true';
803273
+ if (next) closeWorkspacePicker(false);
802721
803274
  sb.setAttribute('data-collapsed', String(next));
802722
803275
  _setLS(SIDEBAR_KEYS.collapsed, next ? '1' : '0');
802723
803276
  }
@@ -804159,8 +804712,22 @@ input.focus();
804159
804712
  </body>
804160
804713
  </html>`;
804161
804714
  }
804715
+ var WEB_UI_ROUTE_SPECS, WEB_UI_ROUTE_PATHS;
804162
804716
  var init_web_ui = __esm({
804163
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
+ );
804164
804731
  }
804165
804732
  });
804166
804733
 
@@ -811485,19 +812052,7 @@ async function handleRequest(req3, res, ollamaUrl, verbose, runtimeDefaults = {}
811485
812052
  res.end(svg);
811486
812053
  return;
811487
812054
  }
811488
- const guiRoutes = /* @__PURE__ */ new Set([
811489
- "/",
811490
- "/chat",
811491
- "/agent",
811492
- "/voice",
811493
- "/generate",
811494
- "/projects",
811495
- "/dashboard",
811496
- "/jobs",
811497
- "/activity",
811498
- "/settings",
811499
- "/config"
811500
- ]);
812055
+ const guiRoutes = new Set(WEB_UI_ROUTE_PATHS);
811501
812056
  if (guiRoutes.has(pathname) && method === "GET" && req3.headers.accept?.includes("text/html")) {
811502
812057
  const htmlHeaders = {
811503
812058
  "Content-Type": "text/html; charset=utf-8",