open-agents-ai 0.187.456 → 0.187.458

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
@@ -13007,8 +13007,8 @@ function deleteCustomToolDefinition(name10, scope, repoRoot) {
13007
13007
  const dir = scope === "project" && repoRoot ? projectToolsDir(repoRoot) : globalToolsDir();
13008
13008
  const filePath = join18(dir, `${name10}.json`);
13009
13009
  if (existsSync13(filePath)) {
13010
- const { unlinkSync: unlinkSync24 } = __require("node:fs");
13011
- unlinkSync24(filePath);
13010
+ const { unlinkSync: unlinkSync25 } = __require("node:fs");
13011
+ unlinkSync25(filePath);
13012
13012
  return true;
13013
13013
  }
13014
13014
  return false;
@@ -250314,8 +250314,8 @@ var init_browser_action = __esm({
250314
250314
  const afterDom = await apiCall("/dom", "GET");
250315
250315
  const afterTitle = (afterDom.dom || "").match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || "";
250316
250316
  try {
250317
- const { unlinkSync: unlinkSync24 } = await import("node:fs");
250318
- unlinkSync24(imagePath);
250317
+ const { unlinkSync: unlinkSync25 } = await import("node:fs");
250318
+ unlinkSync25(imagePath);
250319
250319
  } catch {
250320
250320
  }
250321
250321
  return {
@@ -523527,7 +523527,7 @@ ${result}`
523527
523527
  let resizedBase64 = null;
523528
523528
  try {
523529
523529
  const { execSync: execSync57 } = await import("node:child_process");
523530
- const { writeFileSync: writeFileSync56, readFileSync: readFileSync77, unlinkSync: unlinkSync24 } = await import("node:fs");
523530
+ const { writeFileSync: writeFileSync56, readFileSync: readFileSync77, unlinkSync: unlinkSync25 } = await import("node:fs");
523531
523531
  const { join: join115 } = await import("node:path");
523532
523532
  const { tmpdir: tmpdir22 } = await import("node:os");
523533
523533
  const tmpIn = join115(tmpdir22(), `oa_img_in_${Date.now()}.png`);
@@ -523540,11 +523540,11 @@ ${result}`
523540
523540
  const resizedBuf = readFileSync77(tmpOut);
523541
523541
  resizedBase64 = `data:image/jpeg;base64,${resizedBuf.toString("base64")}`;
523542
523542
  try {
523543
- unlinkSync24(tmpIn);
523543
+ unlinkSync25(tmpIn);
523544
523544
  } catch {
523545
523545
  }
523546
523546
  try {
523547
- unlinkSync24(tmpOut);
523547
+ unlinkSync25(tmpOut);
523548
523548
  } catch {
523549
523549
  }
523550
523550
  } catch {
@@ -574787,66 +574787,137 @@ function getWebUI() {
574787
574787
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
574788
574788
  <title>Open Agents</title>
574789
574789
  <style>
574790
+ /* ─── Open WebUI-shaped design tokens (OWUI-1) ────────────────────
574791
+ * Replaces ~80 hardcoded color literals with CSS custom properties.
574792
+ * Palette matches openwebui's neutral grayscale + functional blue
574793
+ * accent. Brand gold preserved as --color-brand for OA-only marks.
574794
+ * Typography flips body to Inter (UI) + JetBrains Mono (code only),
574795
+ * leaving the terminal aesthetic only where we explicitly opt in.
574796
+ *
574797
+ * Source: /tmp/openwebui-ref/src/tailwind.css @theme + parity audit
574798
+ * at .aiwg/owui-parity.md.
574799
+ */
574800
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
574801
+
574802
+ :root {
574803
+ /* Surfaces — neutral grayscale, openwebui dark default */
574804
+ --color-bg: oklch(0.16 0 0); /* gray-950, body background */
574805
+ --color-bg-elevated: oklch(0.20 0 0); /* gray-900, panels + headers */
574806
+ --color-bg-input: oklch(0.27 0 0); /* gray-850, inputs + chips + buttons */
574807
+ --color-bg-hover: oklch(0.32 0 0); /* gray-800, hover surfaces */
574808
+
574809
+ /* Foregrounds */
574810
+ --color-fg: oklch(0.94 0 0); /* primary text */
574811
+ --color-fg-muted: oklch(0.69 0 0); /* secondary text — gray-500 */
574812
+ --color-fg-subtle: oklch(0.51 0 0); /* tertiary — gray-600 */
574813
+ --color-fg-faint: oklch(0.42 0 0); /* faint hints — gray-700 */
574814
+
574815
+ /* Borders */
574816
+ --color-border: oklch(0.27 0 0); /* default — gray-850 */
574817
+ --color-border-strong:oklch(0.32 0 0); /* emphasized — gray-800 */
574818
+
574819
+ /* Functional accents (openwebui pattern: blue for actions, sparing) */
574820
+ --color-accent: #3b82f6; /* blue-500 — primary actions */
574821
+ --color-accent-hover: #60a5fa; /* blue-400 */
574822
+ --color-success: #22c55e; /* green-500 */
574823
+ --color-warning: #f59e0b; /* amber-500 */
574824
+ --color-error: #ef4444; /* red-500 */
574825
+ --color-info: #14b8a6; /* teal-500 — checkin/info */
574826
+
574827
+ /* OA brand — kept narrow scope for OA-only marks (active tab strip,
574828
+ * brand glyphs, status accents). NOT the dominant accent anymore. */
574829
+ --color-brand: #b2920a;
574830
+ --color-brand-hover: #d4ac0e;
574831
+
574832
+ /* Typography */
574833
+ --font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
574834
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', ui-monospace, monospace;
574835
+
574836
+ /* Radii */
574837
+ --radius-sm: 6px;
574838
+ --radius-md: 8px;
574839
+ --radius-lg: 12px; /* openwebui's rounded-xl */
574840
+ --radius-pill: 9999px;
574841
+ }
574842
+
574843
+ /* Light theme — opt in via [data-theme=light] on html.
574844
+ * Minimum-effort palette parity (OWUI-1 scope). Phase OWUI-3 polishes. */
574845
+ html[data-theme=light] {
574846
+ --color-bg: oklch(0.98 0 0);
574847
+ --color-bg-elevated: oklch(0.94 0 0);
574848
+ --color-bg-input: oklch(0.92 0 0);
574849
+ --color-bg-hover: oklch(0.85 0 0);
574850
+ --color-fg: oklch(0.16 0 0);
574851
+ --color-fg-muted: oklch(0.42 0 0);
574852
+ --color-fg-subtle: oklch(0.51 0 0);
574853
+ --color-fg-faint: oklch(0.69 0 0);
574854
+ --color-border: oklch(0.85 0 0);
574855
+ --color-border-strong:oklch(0.77 0 0);
574856
+ }
574857
+
574790
574858
  * { margin: 0; padding: 0; box-sizing: border-box; }
574791
574859
 
574792
574860
  /* ─── Cross-browser custom scrollbars ─────────────────────────────
574793
- * Yellow (#b2920a) thumb, grey (#2a2a30) track. Applies to every
574794
- * scrollable container in the app via the universal selector.
574795
- * Firefox uses the standard scrollbar-* properties; WebKit/Blink
574796
- * uses the ::-webkit-scrollbar pseudo-elements. Both render the
574797
- * same brand colors.
574861
+ * Token-driven so theme changes cascade. Subtle hover.
574798
574862
  */
574799
574863
  html {
574800
574864
  scrollbar-width: thin;
574801
- scrollbar-color: #b2920a #2a2a30;
574865
+ scrollbar-color: var(--color-border-strong) var(--color-bg-elevated);
574802
574866
  }
574803
574867
  *::-webkit-scrollbar {
574804
574868
  width: 10px;
574805
574869
  height: 10px;
574806
574870
  }
574807
574871
  *::-webkit-scrollbar-track {
574808
- background: #2a2a30;
574872
+ background: var(--color-bg-elevated);
574809
574873
  border-radius: 0;
574810
574874
  }
574811
574875
  *::-webkit-scrollbar-thumb {
574812
- background: #b2920a;
574813
- border-radius: 4px;
574814
- border: 2px solid #2a2a30;
574876
+ background: var(--color-border-strong);
574877
+ border-radius: var(--radius-sm);
574878
+ border: 2px solid var(--color-bg-elevated);
574815
574879
  }
574816
574880
  *::-webkit-scrollbar-thumb:hover {
574817
- background: #d4ac0e;
574881
+ background: var(--color-fg-faint);
574818
574882
  }
574819
574883
  *::-webkit-scrollbar-corner {
574820
- background: #2a2a30;
574884
+ background: var(--color-bg-elevated);
574821
574885
  }
574822
574886
 
574823
574887
  body {
574824
- font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
574825
- background: #1a1a1e;
574826
- color: #b0b0b0;
574888
+ font-family: var(--font-ui);
574889
+ background: var(--color-bg);
574890
+ color: var(--color-fg);
574827
574891
  display: flex;
574828
574892
  flex-direction: column;
574829
574893
  height: 100vh;
574830
574894
  overflow: hidden;
574831
574895
  scrollbar-width: thin;
574832
- scrollbar-color: #b2920a #2a2a30;
574896
+ scrollbar-color: var(--color-border-strong) var(--color-bg-elevated);
574897
+ font-size: 14px;
574898
+ -webkit-font-smoothing: antialiased;
574899
+ }
574900
+
574901
+ /* Anywhere we explicitly want the terminal/code feel, opt in via .mono */
574902
+ .mono, code, pre, kbd, samp, tt {
574903
+ font-family: var(--font-mono);
574833
574904
  }
574834
574905
  #header {
574835
574906
  display: flex;
574836
574907
  align-items: center;
574837
574908
  gap: 12px;
574838
574909
  padding: 8px 16px;
574839
- background: #1e1e22;
574840
- border-bottom: 1px solid #2a2a30;
574910
+ background: var(--color-bg-elevated);
574911
+ border-bottom: 1px solid var(--color-bg-input);
574841
574912
  flex-shrink: 0;
574842
574913
  }
574843
- #header .accent { color: #b2920a; font-weight: bold; font-size: 0.8rem; }
574844
- #header .status { font-size: 0.7rem; color: #555; }
574845
- #header .status.live { color: #b2920a; }
574914
+ #header .accent { color: var(--color-brand); font-weight: bold; font-size: 0.8rem; }
574915
+ #header .status { font-size: 0.7rem; color: var(--color-fg-faint); }
574916
+ #header .status.live { color: var(--color-brand); }
574846
574917
  #header select {
574847
- background: #2a2a30;
574848
- border: 1px solid #3a3a42;
574849
- color: #b0b0b0;
574918
+ background: var(--color-bg-input);
574919
+ border: 1px solid var(--color-border);
574920
+ color: var(--color-fg);
574850
574921
  padding: 4px 8px;
574851
574922
  border-radius: 3px;
574852
574923
  font-family: inherit;
@@ -574857,9 +574928,9 @@ body {
574857
574928
  text-overflow: ellipsis;
574858
574929
  }
574859
574930
  #header .key-btn {
574860
- background: #2a2a30;
574861
- border: 1px solid #3a3a42;
574862
- color: #b2920a;
574931
+ background: var(--color-bg-input);
574932
+ border: 1px solid var(--color-border);
574933
+ color: var(--color-brand);
574863
574934
  padding: 4px 10px;
574864
574935
  border-radius: 3px;
574865
574936
  font-family: inherit;
@@ -574867,7 +574938,7 @@ body {
574867
574938
  cursor: pointer;
574868
574939
  transition: background 0.15s;
574869
574940
  }
574870
- #header .key-btn:hover { background: #3a3a42; }
574941
+ #header .key-btn:hover { background: var(--color-border); }
574871
574942
  #conversation {
574872
574943
  flex: 1;
574873
574944
  overflow-y: auto;
@@ -574877,20 +574948,20 @@ body {
574877
574948
  gap: 4px;
574878
574949
  }
574879
574950
  .msg { padding: 6px 0; font-size: 0.82rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
574880
- .msg.user { color: #888; }
574881
- .msg.user::before { content: '\\25B8 '; color: #555; }
574882
- .msg.assistant { color: #b2920a; }
574883
- .msg.assistant::before { content: '\\25B9 '; color: #b2920a; }
574884
- .msg.system { color: #555; font-size: 0.7rem; }
574951
+ .msg.user { color: var(--color-fg-muted); }
574952
+ .msg.user::before { content: '\\25B8 '; color: var(--color-fg-faint); }
574953
+ .msg.assistant { color: var(--color-brand); }
574954
+ .msg.assistant::before { content: '\\25B9 '; color: var(--color-brand); }
574955
+ .msg.system { color: var(--color-fg-faint); font-size: 0.7rem; }
574885
574956
  .msg code {
574886
- background: #2a2a30;
574957
+ background: var(--color-bg-input);
574887
574958
  padding: 1px 4px;
574888
574959
  border-radius: 2px;
574889
574960
  font-size: 0.78rem;
574890
574961
  }
574891
574962
  .msg pre {
574892
- background: #1e1e22;
574893
- border: 1px solid #2a2a30;
574963
+ background: var(--color-bg-elevated);
574964
+ border: 1px solid var(--color-bg-input);
574894
574965
  border-radius: 3px;
574895
574966
  padding: 8px 12px;
574896
574967
  margin: 6px 0;
@@ -574903,9 +574974,9 @@ body {
574903
574974
  position: absolute;
574904
574975
  top: 4px;
574905
574976
  right: 4px;
574906
- background: #2a2a30;
574907
- border: 1px solid #3a3a42;
574908
- color: #b2920a;
574977
+ background: var(--color-bg-input);
574978
+ border: 1px solid var(--color-border);
574979
+ color: var(--color-brand);
574909
574980
  padding: 2px 8px;
574910
574981
  border-radius: 2px;
574911
574982
  font-family: inherit;
@@ -574923,21 +574994,21 @@ body {
574923
574994
  .msg-actions button {
574924
574995
  background: none;
574925
574996
  border: none;
574926
- color: #555;
574997
+ color: var(--color-fg-faint);
574927
574998
  font-family: inherit;
574928
574999
  font-size: 0.6rem;
574929
575000
  cursor: pointer;
574930
575001
  padding: 0;
574931
575002
  }
574932
- .msg-actions button:hover { color: #b2920a; }
575003
+ .msg-actions button:hover { color: var(--color-brand); }
574933
575004
  #footer {
574934
575005
  display: flex;
574935
575006
  flex-direction: column;
574936
575007
  /* gap: 0 — was 4px, removed per user request to tighten footer */
574937
575008
  gap: 0;
574938
575009
  padding: 0;
574939
- background: #1e1e22;
574940
- border-top: 1px solid #2a2a30;
575010
+ background: var(--color-bg-elevated);
575011
+ border-top: 1px solid var(--color-bg-input);
574941
575012
  flex-shrink: 0;
574942
575013
  position: relative;
574943
575014
  }
@@ -574947,14 +575018,14 @@ body {
574947
575018
  gap: 6px;
574948
575019
  padding: 4px 16px;
574949
575020
  min-height: 20px;
574950
- background: #17171a;
574951
- border-bottom: 1px solid #2a2a30;
575021
+ background: var(--color-bg);
575022
+ border-bottom: 1px solid var(--color-bg-input);
574952
575023
  overflow-x: auto;
574953
575024
  scrollbar-width: none;
574954
575025
  }
574955
575026
  #processes-row::-webkit-scrollbar { display: none; }
574956
575027
  #processes-row .proc-label {
574957
- color: #444;
575028
+ color: var(--color-fg-faint);
574958
575029
  font-size: 0.55rem;
574959
575030
  text-transform: uppercase;
574960
575031
  letter-spacing: 0.08em;
@@ -574975,12 +575046,12 @@ body {
574975
575046
  gap: 6px 6px; /* row gap + column gap once wrapping kicks in */
574976
575047
  padding: 4px 16px;
574977
575048
  min-height: 22px;
574978
- background: #17171a;
574979
- border-bottom: 1px solid #2a2a30;
575049
+ background: var(--color-bg);
575050
+ border-bottom: 1px solid var(--color-bg-input);
574980
575051
  /* Wrap instead of horizontal scroll so all tasks are visible */
574981
575052
  }
574982
575053
  #tasks-row .tasks-label {
574983
- color: #444;
575054
+ color: var(--color-fg-faint);
574984
575055
  font-size: 0.55rem;
574985
575056
  text-transform: uppercase;
574986
575057
  letter-spacing: 0.08em;
@@ -574993,8 +575064,8 @@ body {
574993
575064
  gap: 4px;
574994
575065
  padding: 2px 6px;
574995
575066
  border-radius: 3px;
574996
- background: #1e1e22;
574997
- border: 1px solid #2a2a30;
575067
+ background: var(--color-bg-elevated);
575068
+ border: 1px solid var(--color-bg-input);
574998
575069
  font-size: 0.62rem;
574999
575070
  white-space: nowrap;
575000
575071
  flex-shrink: 0;
@@ -575009,15 +575080,15 @@ body {
575009
575080
  text-overflow: ellipsis;
575010
575081
  white-space: nowrap;
575011
575082
  }
575012
- #tasks-row .task-item.pending { color: #555; }
575013
- #tasks-row .task-item.pending .mark { color: #555; }
575014
- #tasks-row .task-item.in_progress { color: #b2920a; border-color: #b2920a; box-shadow: 0 0 4px rgba(178,146,10,0.3); }
575015
- #tasks-row .task-item.in_progress .mark { color: #b2920a; }
575083
+ #tasks-row .task-item.pending { color: var(--color-fg-faint); }
575084
+ #tasks-row .task-item.pending .mark { color: var(--color-fg-faint); }
575085
+ #tasks-row .task-item.in_progress { color: var(--color-brand); border-color: var(--color-brand); box-shadow: 0 0 4px rgba(178,146,10,0.3); }
575086
+ #tasks-row .task-item.in_progress .mark { color: var(--color-brand); }
575016
575087
  #tasks-row .task-item.completed { color: #4a7a4a; }
575017
575088
  #tasks-row .task-item.completed .mark { color: #5fa55f; }
575018
575089
  #tasks-row .task-item.completed .label { text-decoration: line-through; opacity: 0.7; }
575019
- #tasks-row .task-item.blocked { color: #b25f5f; border-color: #5a2a2a; }
575020
- #tasks-row .task-item.blocked .mark { color: #b25f5f; }
575090
+ #tasks-row .task-item.blocked { color: var(--color-error); border-color: var(--color-error); }
575091
+ #tasks-row .task-item.blocked .mark { color: var(--color-error); }
575021
575092
 
575022
575093
  /* WO-TASK-02 — session topbar (chat sessions select + agent runs select).
575023
575094
  Sits above each panel's content; shares the processes-row dark strip
@@ -575028,22 +575099,22 @@ body {
575028
575099
  gap: 8px;
575029
575100
  padding: 4px 16px;
575030
575101
  min-height: 22px;
575031
- background: #17171a;
575032
- border-bottom: 1px solid #2a2a30;
575102
+ background: var(--color-bg);
575103
+ border-bottom: 1px solid var(--color-bg-input);
575033
575104
  flex-shrink: 0;
575034
575105
  font-size: 0.62rem;
575035
575106
  }
575036
575107
  .session-topbar .topbar-label {
575037
- color: #444;
575108
+ color: var(--color-fg-faint);
575038
575109
  font-size: 0.55rem;
575039
575110
  text-transform: uppercase;
575040
575111
  letter-spacing: 0.08em;
575041
575112
  white-space: nowrap;
575042
575113
  }
575043
575114
  .session-topbar select {
575044
- background: #2a2a30;
575045
- border: 1px solid #3a3a42;
575046
- color: #b0b0b0;
575115
+ background: var(--color-bg-input);
575116
+ border: 1px solid var(--color-border);
575117
+ color: var(--color-fg);
575047
575118
  padding: 2px 8px;
575048
575119
  border-radius: 3px;
575049
575120
  font-family: inherit;
@@ -575052,9 +575123,9 @@ body {
575052
575123
  max-width: 280px;
575053
575124
  }
575054
575125
  .session-topbar button {
575055
- background: #2a2a30;
575056
- border: 1px solid #3a3a42;
575057
- color: #b2920a;
575126
+ background: var(--color-bg-input);
575127
+ border: 1px solid var(--color-border);
575128
+ color: var(--color-brand);
575058
575129
  padding: 2px 8px;
575059
575130
  border-radius: 3px;
575060
575131
  font-family: inherit;
@@ -575076,14 +575147,14 @@ body {
575076
575147
  }
575077
575148
  .proc-dot.fading { opacity: 0.3; }
575078
575149
  /* Dot colors by process type */
575079
- .proc-dot.run { background: #4ec94e; color: #4ec94e; } /* agentic runs = green */
575080
- .proc-dot.tool { background: #b2920a; color: #b2920a; } /* tool calls = gold */
575150
+ .proc-dot.run { background: var(--color-success); color: var(--color-success); } /* agentic runs = green */
575151
+ .proc-dot.tool { background: var(--color-brand); color: var(--color-brand); } /* tool calls = gold */
575081
575152
  .proc-dot.skill { background: #c94ec9; color: #c94ec9; } /* skills = magenta */
575082
575153
  .proc-dot.mcp { background: #4ec9c9; color: #4ec9c9; } /* mcp = cyan */
575083
575154
  .proc-dot.cron { background: #c9944e; color: #c9944e; } /* cron = orange */
575084
575155
  .proc-dot.memory { background: #4e94c9; color: #4e94c9; } /* memory = blue */
575085
575156
  .proc-dot.incident{ background: #c94e4e; color: #c94e4e; } /* incidents = red */
575086
- .proc-dot.other { background: #888; color: #888; }
575157
+ .proc-dot.other { background: var(--color-fg-muted); color: var(--color-fg-muted); }
575087
575158
  .proc-dot.running { box-shadow: 0 0 4px currentColor; animation: pulse 1.2s infinite; }
575088
575159
  @keyframes pulse {
575089
575160
  0%,100% { opacity: 1; }
@@ -575095,8 +575166,8 @@ body {
575095
575166
  left: 16px;
575096
575167
  right: 16px;
575097
575168
  margin-bottom: 6px;
575098
- background: #1e1e22;
575099
- border: 1px solid #b2920a;
575169
+ background: var(--color-bg-elevated);
575170
+ border: 1px solid var(--color-brand);
575100
575171
  border-radius: 4px;
575101
575172
  padding: 12px 16px;
575102
575173
  box-shadow: 0 -4px 20px rgba(0,0,0,0.6);
@@ -575108,7 +575179,7 @@ body {
575108
575179
  }
575109
575180
  #proc-popover.visible { display: block; }
575110
575181
  #proc-popover .popover-title {
575111
- color: #b2920a;
575182
+ color: var(--color-brand);
575112
575183
  font-size: 0.75rem;
575113
575184
  font-weight: bold;
575114
575185
  display: flex;
@@ -575119,12 +575190,12 @@ body {
575119
575190
  #proc-popover .popover-close {
575120
575191
  background: none;
575121
575192
  border: none;
575122
- color: #666;
575193
+ color: var(--color-fg-subtle);
575123
575194
  cursor: pointer;
575124
575195
  font-size: 1rem;
575125
575196
  padding: 0 4px;
575126
575197
  }
575127
- #proc-popover .popover-close:hover { color: #b2920a; }
575198
+ #proc-popover .popover-close:hover { color: var(--color-brand); }
575128
575199
  #proc-popover .popover-row {
575129
575200
  display: flex;
575130
575201
  justify-content: space-between;
@@ -575132,26 +575203,26 @@ body {
575132
575203
  padding: 2px 0;
575133
575204
  color: #999;
575134
575205
  }
575135
- #proc-popover .popover-row .k { color: #b2920a; min-width: 90px; }
575136
- #proc-popover .popover-row .v { color: #b0b0b0; word-break: break-all; text-align: right; }
575206
+ #proc-popover .popover-row .k { color: var(--color-brand); min-width: 90px; }
575207
+ #proc-popover .popover-row .v { color: var(--color-fg); word-break: break-all; text-align: right; }
575137
575208
  #proc-popover .popover-actions {
575138
575209
  display: flex;
575139
575210
  gap: 8px;
575140
575211
  margin-top: 10px;
575141
575212
  padding-top: 8px;
575142
- border-top: 1px solid #2a2a30;
575213
+ border-top: 1px solid var(--color-bg-input);
575143
575214
  }
575144
575215
  #proc-popover .popover-actions button {
575145
- background: #2a2a30;
575146
- border: 1px solid #3a3a42;
575147
- color: #b2920a;
575216
+ background: var(--color-bg-input);
575217
+ border: 1px solid var(--color-border);
575218
+ color: var(--color-brand);
575148
575219
  padding: 4px 10px;
575149
575220
  border-radius: 3px;
575150
575221
  font-family: inherit;
575151
575222
  font-size: 0.65rem;
575152
575223
  cursor: pointer;
575153
575224
  }
575154
- #proc-popover .popover-actions button:hover { background: #3a3a42; }
575225
+ #proc-popover .popover-actions button:hover { background: var(--color-border); }
575155
575226
  #proc-popover .popover-actions .kill {
575156
575227
  border-color: #c94e4e;
575157
575228
  color: #c94e4e;
@@ -575164,15 +575235,15 @@ body {
575164
575235
  align-items: flex-end;
575165
575236
  gap: 10px;
575166
575237
  padding: 8px 16px 8px;
575167
- background: #1e1e22;
575238
+ background: var(--color-bg-elevated);
575168
575239
  }
575169
575240
  #input-area {
575170
575241
  flex: 1;
575171
- background: #2a2a30;
575172
- border: 1px solid #3a3a42;
575242
+ background: var(--color-bg-input);
575243
+ border: 1px solid var(--color-border);
575173
575244
  border-radius: 3px;
575174
575245
  padding: 8px 12px;
575175
- color: #b0b0b0;
575246
+ color: var(--color-fg);
575176
575247
  font-family: inherit;
575177
575248
  font-size: 0.82rem;
575178
575249
  resize: none;
@@ -575181,11 +575252,11 @@ body {
575181
575252
  line-height: 1.4;
575182
575253
  outline: none;
575183
575254
  }
575184
- #input-area:focus { border-color: #b2920a; }
575255
+ #input-area:focus { border-color: var(--color-brand); }
575185
575256
  #send-btn {
575186
- background: #2a2a30;
575187
- border: 1px solid #3a3a42;
575188
- color: #b2920a;
575257
+ background: var(--color-bg-input);
575258
+ border: 1px solid var(--color-border);
575259
+ color: var(--color-brand);
575189
575260
  padding: 10px 16px;
575190
575261
  border-radius: 3px;
575191
575262
  font-family: inherit;
@@ -575194,27 +575265,27 @@ body {
575194
575265
  transition: background 0.15s;
575195
575266
  flex-shrink: 0;
575196
575267
  }
575197
- #send-btn:hover { background: #3a3a42; }
575268
+ #send-btn:hover { background: var(--color-border); }
575198
575269
  #send-btn:disabled { opacity: 0.3; cursor: default; }
575199
575270
  #system-prompt-toggle {
575200
575271
  font-size: 0.65rem;
575201
- color: #444;
575272
+ color: var(--color-fg-faint);
575202
575273
  cursor: pointer;
575203
575274
  padding: 4px 0;
575204
575275
  }
575205
- #system-prompt-toggle:hover { color: #b2920a; }
575276
+ #system-prompt-toggle:hover { color: var(--color-brand); }
575206
575277
  #system-prompt-area {
575207
575278
  display: none;
575208
575279
  padding: 0 16px 8px;
575209
- background: #1e1e22;
575280
+ background: var(--color-bg-elevated);
575210
575281
  }
575211
575282
  #system-prompt-area textarea {
575212
575283
  width: 100%;
575213
- background: #2a2a30;
575214
- border: 1px solid #3a3a42;
575284
+ background: var(--color-bg-input);
575285
+ border: 1px solid var(--color-border);
575215
575286
  border-radius: 3px;
575216
575287
  padding: 6px 10px;
575217
- color: #b0b0b0;
575288
+ color: var(--color-fg);
575218
575289
  font-family: inherit;
575219
575290
  font-size: 0.7rem;
575220
575291
  resize: vertical;
@@ -575232,21 +575303,21 @@ body {
575232
575303
  }
575233
575304
  #key-modal.visible { display: flex; }
575234
575305
  #key-modal .modal {
575235
- background: #1e1e22;
575236
- border: 1px solid #2a2a30;
575306
+ background: var(--color-bg-elevated);
575307
+ border: 1px solid var(--color-bg-input);
575237
575308
  border-radius: 6px;
575238
575309
  padding: 20px;
575239
575310
  width: 360px;
575240
575311
  max-width: 90vw;
575241
575312
  }
575242
- #key-modal .modal h3 { color: #b2920a; font-size: 0.8rem; margin-bottom: 12px; }
575313
+ #key-modal .modal h3 { color: var(--color-brand); font-size: 0.8rem; margin-bottom: 12px; }
575243
575314
  #key-modal .modal input {
575244
575315
  width: 100%;
575245
- background: #2a2a30;
575246
- border: 1px solid #3a3a42;
575316
+ background: var(--color-bg-input);
575317
+ border: 1px solid var(--color-border);
575247
575318
  border-radius: 3px;
575248
575319
  padding: 8px 10px;
575249
- color: #b0b0b0;
575320
+ color: var(--color-fg);
575250
575321
  font-family: inherit;
575251
575322
  font-size: 0.75rem;
575252
575323
  outline: none;
@@ -575260,9 +575331,9 @@ body {
575260
575331
  .tab { padding:6px 10px !important; font-size:0.6rem !important; }
575261
575332
  }
575262
575333
  #key-modal .modal button {
575263
- background: #2a2a30;
575264
- border: 1px solid #b2920a;
575265
- color: #b2920a;
575334
+ background: var(--color-bg-input);
575335
+ border: 1px solid var(--color-brand);
575336
+ color: var(--color-brand);
575266
575337
  padding: 6px 16px;
575267
575338
  border-radius: 3px;
575268
575339
  font-family: inherit;
@@ -575270,14 +575341,812 @@ body {
575270
575341
  cursor: pointer;
575271
575342
  margin-right: 8px;
575272
575343
  }
575344
+
575345
+ /* ════════════════════════════════════════════════════════════
575346
+ OWUI-2: Sidebar layout chrome
575347
+ Sidebar-led layout to match openwebui aesthetic.
575348
+ #tabs becomes hidden (still in DOM for legacy switchTab JS).
575349
+ ════════════════════════════════════════════════════════════ */
575350
+ body { display:flex; flex-direction:column; height:100vh; margin:0; overflow:hidden; }
575351
+ #oa-shell { font-family: var(--font-ui); }
575352
+ #oa-main { font-family: inherit; }
575353
+
575354
+ /* Hide the legacy top tabs — sidebar replaces them. */
575355
+ #tabs { display:none !important; }
575356
+
575357
+ /* Sidebar nav buttons */
575358
+ .sb-nav {
575359
+ background: transparent;
575360
+ border: 1px solid transparent;
575361
+ color: var(--color-fg-muted);
575362
+ padding: 7px 10px;
575363
+ border-radius: var(--radius-md);
575364
+ cursor: pointer;
575365
+ display: flex;
575366
+ align-items: center;
575367
+ gap: 10px;
575368
+ font-family: var(--font-ui);
575369
+ font-size: 0.78rem;
575370
+ text-align: left;
575371
+ width: 100%;
575372
+ transition: background 0.12s, color 0.12s;
575373
+ }
575374
+ .sb-nav:hover { background: var(--color-bg-hover); color: var(--color-fg); }
575375
+ .sb-nav.active {
575376
+ background: var(--color-bg-hover);
575377
+ color: var(--color-fg);
575378
+ border-color: var(--color-border-strong);
575379
+ }
575380
+ .sb-nav svg { flex-shrink: 0; }
575381
+
575382
+ /* Sidebar chat row */
575383
+ .sb-chat {
575384
+ display: flex;
575385
+ align-items: center;
575386
+ gap: 8px;
575387
+ padding: 6px 10px;
575388
+ border-radius: var(--radius-md);
575389
+ cursor: pointer;
575390
+ color: var(--color-fg-muted);
575391
+ font-size: 0.78rem;
575392
+ white-space: nowrap;
575393
+ overflow: hidden;
575394
+ text-overflow: ellipsis;
575395
+ }
575396
+ .sb-chat:hover { background: var(--color-bg-hover); color: var(--color-fg); }
575397
+ .sb-chat.active { background: var(--color-bg-input); color: var(--color-fg); }
575398
+
575399
+ /* Resize handle hover affordance */
575400
+ #sidebar-resize:hover { background: var(--color-border-strong); }
575401
+ #sidebar-resize.dragging { background: var(--color-accent); }
575402
+
575403
+ /* Collapsed state — only icons visible */
575404
+ #oa-sidebar[data-collapsed="true"] { width: 56px !important; }
575405
+ #oa-sidebar[data-collapsed="true"] .sb-label { display: none !important; }
575406
+ #oa-sidebar[data-collapsed="true"] #sidebar-search-row,
575407
+ #oa-sidebar[data-collapsed="true"] #sidebar-chats,
575408
+ #oa-sidebar[data-collapsed="true"] #sidebar-pinned-section { display: none !important; }
575409
+ #oa-sidebar[data-collapsed="true"] #sidebar-new-chat-row button { padding: 8px; justify-content: center; }
575410
+ #oa-sidebar[data-collapsed="true"] .sb-nav { justify-content: center; padding: 8px; }
575411
+ #oa-sidebar[data-collapsed="true"] #sidebar-footer { flex-direction: column; gap: 6px; padding: 8px 4px; }
575412
+ #oa-sidebar[data-collapsed="true"] #sidebar-status-text { display: none !important; }
575413
+
575414
+ @media (max-width: 768px) {
575415
+ #oa-sidebar { position:fixed; top:0; left:0; bottom:0; z-index:60; box-shadow:4px 0 20px rgba(0,0,0,0.5); }
575416
+ #oa-sidebar[data-collapsed="true"] { transform: translateX(-100%); }
575417
+ }
575418
+
575419
+ /* ════════════════════════════════════════════════════════════
575420
+ OWUI-3: Chat surface — bubbles, streaming indicator,
575421
+ per-message actions, code blocks, system-message chip,
575422
+ tool-call card.
575423
+ ════════════════════════════════════════════════════════════ */
575424
+
575425
+ /* Wipe the legacy ::before triangle markers; we use avatars/bubbles. */
575426
+ .msg.user::before,
575427
+ .msg.assistant::before { content: none !important; }
575428
+
575429
+ .msg {
575430
+ position: relative;
575431
+ font-family: var(--font-ui);
575432
+ font-size: 0.88rem;
575433
+ line-height: 1.55;
575434
+ padding: 14px 0;
575435
+ max-width: 100%;
575436
+ }
575437
+ .msg.user {
575438
+ display: flex;
575439
+ justify-content: flex-end;
575440
+ padding: 10px 0;
575441
+ color: var(--color-fg);
575442
+ }
575443
+ .msg.user > * {
575444
+ background: var(--color-bg-input);
575445
+ color: var(--color-fg);
575446
+ padding: 10px 14px;
575447
+ border-radius: 18px;
575448
+ max-width: 78%;
575449
+ white-space: pre-wrap;
575450
+ word-break: break-word;
575451
+ }
575452
+ .msg.assistant {
575453
+ display: block;
575454
+ padding: 14px 0 10px 44px;
575455
+ color: var(--color-fg);
575456
+ }
575457
+ .msg.assistant::before {
575458
+ content: 'OA' !important;
575459
+ position: absolute;
575460
+ top: 14px;
575461
+ left: 0;
575462
+ width: 32px;
575463
+ height: 32px;
575464
+ border-radius: 50%;
575465
+ background: var(--color-bg-input);
575466
+ color: var(--color-brand);
575467
+ display: flex !important;
575468
+ align-items: center;
575469
+ justify-content: center;
575470
+ font-size: 0.7rem;
575471
+ font-weight: 600;
575472
+ font-family: var(--font-ui);
575473
+ letter-spacing: 0.02em;
575474
+ }
575475
+
575476
+ /* System messages collapse into a small centered chip */
575477
+ .msg.system {
575478
+ display: flex;
575479
+ justify-content: center;
575480
+ padding: 6px 0;
575481
+ }
575482
+ .msg.system > * {
575483
+ background: var(--color-bg-elevated);
575484
+ color: var(--color-fg-muted);
575485
+ border: 1px solid var(--color-border);
575486
+ padding: 4px 10px;
575487
+ border-radius: 999px;
575488
+ font-size: 0.7rem;
575489
+ max-width: 70%;
575490
+ white-space: nowrap;
575491
+ overflow: hidden;
575492
+ text-overflow: ellipsis;
575493
+ }
575494
+
575495
+ /* Streaming pulsing-dot indicator */
575496
+ .msg-streaming-indicator {
575497
+ display: inline-flex;
575498
+ align-items: center;
575499
+ gap: 8px;
575500
+ margin: 4px 0 0;
575501
+ color: var(--color-fg-muted);
575502
+ font-size: 0.72rem;
575503
+ }
575504
+ .msg-streaming-indicator .dot {
575505
+ width: 8px;
575506
+ height: 8px;
575507
+ border-radius: 50%;
575508
+ background: var(--color-brand);
575509
+ animation: msg-pulse 1.2s ease-in-out infinite;
575510
+ }
575511
+ @keyframes msg-pulse {
575512
+ 0%, 100% { opacity: 0.35; transform: scale(0.85); }
575513
+ 50% { opacity: 1; transform: scale(1.1); }
575514
+ }
575515
+
575516
+ /* Per-message actions — visible on hover for assistant messages.
575517
+ The .msg-actions container already exists, but its old style was
575518
+ always-visible micro-buttons. We restyle to openwebui-shape. */
575519
+ .msg-actions {
575520
+ display: flex !important;
575521
+ gap: 4px;
575522
+ margin-top: 8px;
575523
+ opacity: 0;
575524
+ transition: opacity 0.15s;
575525
+ }
575526
+ .msg.assistant:hover .msg-actions,
575527
+ .msg.assistant:focus-within .msg-actions { opacity: 1; }
575528
+ .msg-actions button {
575529
+ background: transparent;
575530
+ border: 1px solid transparent;
575531
+ color: var(--color-fg-muted);
575532
+ font-family: var(--font-ui);
575533
+ font-size: 0.68rem;
575534
+ padding: 3px 8px;
575535
+ border-radius: var(--radius-sm);
575536
+ cursor: pointer;
575537
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
575538
+ }
575539
+ .msg-actions button:hover {
575540
+ background: var(--color-bg-hover);
575541
+ color: var(--color-fg);
575542
+ border-color: var(--color-border);
575543
+ }
575544
+
575545
+ /* Code blocks — language label + copy button, prism-friendly */
575546
+ .msg pre {
575547
+ background: var(--color-bg-elevated);
575548
+ border: 1px solid var(--color-border);
575549
+ border-radius: var(--radius-md);
575550
+ padding: 28px 14px 12px;
575551
+ margin: 8px 0;
575552
+ overflow-x: auto;
575553
+ font-size: 0.78rem;
575554
+ line-height: 1.5;
575555
+ position: relative;
575556
+ font-family: var(--font-mono);
575557
+ }
575558
+ .msg pre code {
575559
+ background: transparent;
575560
+ padding: 0;
575561
+ font-family: var(--font-mono);
575562
+ font-size: inherit;
575563
+ color: var(--color-fg);
575564
+ }
575565
+ .msg pre::before {
575566
+ content: attr(data-lang);
575567
+ position: absolute;
575568
+ top: 6px;
575569
+ left: 12px;
575570
+ font-size: 0.6rem;
575571
+ text-transform: uppercase;
575572
+ letter-spacing: 0.06em;
575573
+ color: var(--color-fg-faint);
575574
+ font-family: var(--font-ui);
575575
+ }
575576
+ .msg pre .copy-btn {
575577
+ background: var(--color-bg-input);
575578
+ border: 1px solid var(--color-border);
575579
+ color: var(--color-fg-muted);
575580
+ font-family: var(--font-ui);
575581
+ padding: 3px 8px;
575582
+ font-size: 0.6rem;
575583
+ border-radius: var(--radius-sm);
575584
+ opacity: 0.6;
575585
+ }
575586
+ .msg pre:hover .copy-btn { opacity: 1; }
575587
+
575588
+ /* Tool-call cards rendered inside an assistant message */
575589
+ .msg .tool-card {
575590
+ display: block;
575591
+ background: var(--color-bg-elevated);
575592
+ border: 1px solid var(--color-border);
575593
+ border-radius: var(--radius-md);
575594
+ padding: 8px 12px;
575595
+ margin: 6px 0;
575596
+ font-size: 0.74rem;
575597
+ color: var(--color-fg-muted);
575598
+ font-family: var(--font-mono);
575599
+ }
575600
+ .msg .tool-card summary {
575601
+ cursor: pointer;
575602
+ color: var(--color-fg);
575603
+ font-family: var(--font-ui);
575604
+ font-size: 0.74rem;
575605
+ list-style: none;
575606
+ }
575607
+ .msg .tool-card summary::-webkit-details-marker { display: none; }
575608
+ .msg .tool-card summary::before {
575609
+ content: '▸';
575610
+ display: inline-block;
575611
+ margin-right: 6px;
575612
+ color: var(--color-fg-muted);
575613
+ transition: transform 0.12s;
575614
+ }
575615
+ .msg .tool-card[open] summary::before { transform: rotate(90deg); }
575616
+
575617
+ /* ════════════════════════════════════════════════════════════
575618
+ OWUI-4: Input toolbar + slash palette + dropzone
575619
+ ════════════════════════════════════════════════════════════ */
575620
+ #input-toolbar {
575621
+ display: flex;
575622
+ align-items: center;
575623
+ gap: 4px;
575624
+ padding: 6px 14px 4px;
575625
+ background: var(--color-bg);
575626
+ border-bottom: 1px solid transparent;
575627
+ font-family: var(--font-ui);
575628
+ }
575629
+ #input-toolbar .ibtn {
575630
+ background: transparent;
575631
+ border: 1px solid transparent;
575632
+ color: var(--color-fg-muted);
575633
+ padding: 5px 8px;
575634
+ border-radius: var(--radius-sm);
575635
+ cursor: pointer;
575636
+ display: inline-flex;
575637
+ align-items: center;
575638
+ justify-content: center;
575639
+ font-family: var(--font-ui);
575640
+ font-size: 0.7rem;
575641
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
575642
+ height: 26px;
575643
+ min-width: 26px;
575644
+ }
575645
+ #input-toolbar .ibtn:hover {
575646
+ background: var(--color-bg-hover);
575647
+ color: var(--color-fg);
575648
+ border-color: var(--color-border);
575649
+ }
575650
+ #input-toolbar .ibtn.active {
575651
+ background: var(--color-bg-input);
575652
+ color: var(--color-brand);
575653
+ border-color: var(--color-brand);
575654
+ }
575655
+ #input-toolbar .ibtn-model {
575656
+ background: var(--color-bg-input);
575657
+ border: 1px solid var(--color-border);
575658
+ color: var(--color-fg);
575659
+ font-family: var(--font-ui);
575660
+ font-size: 0.7rem;
575661
+ padding: 3px 6px;
575662
+ border-radius: var(--radius-sm);
575663
+ height: 26px;
575664
+ max-width: 200px;
575665
+ cursor: pointer;
575666
+ outline: none;
575667
+ }
575668
+
575669
+ /* Drag & drop visual on the input row */
575670
+ #input-row.dragover {
575671
+ outline: 2px dashed var(--color-accent);
575672
+ outline-offset: -4px;
575673
+ background: var(--color-bg-elevated);
575674
+ }
575675
+
575676
+ /* Attachment chips row */
575677
+ #attach-chips {
575678
+ display: flex;
575679
+ flex-wrap: wrap;
575680
+ gap: 6px;
575681
+ padding: 4px 14px 2px;
575682
+ background: var(--color-bg);
575683
+ font-family: var(--font-ui);
575684
+ }
575685
+ #attach-chips .chip {
575686
+ display: inline-flex;
575687
+ align-items: center;
575688
+ gap: 4px;
575689
+ background: var(--color-bg-input);
575690
+ border: 1px solid var(--color-border);
575691
+ color: var(--color-fg);
575692
+ font-size: 0.7rem;
575693
+ padding: 2px 6px;
575694
+ border-radius: 999px;
575695
+ max-width: 220px;
575696
+ white-space: nowrap;
575697
+ overflow: hidden;
575698
+ }
575699
+ #attach-chips .chip .name {
575700
+ white-space: nowrap;
575701
+ overflow: hidden;
575702
+ text-overflow: ellipsis;
575703
+ max-width: 160px;
575704
+ }
575705
+ #attach-chips .chip .x {
575706
+ cursor: pointer;
575707
+ color: var(--color-fg-muted);
575708
+ margin-left: 2px;
575709
+ border: none;
575710
+ background: transparent;
575711
+ padding: 0 2px;
575712
+ font-size: 0.85rem;
575713
+ line-height: 1;
575714
+ }
575715
+ #attach-chips .chip .x:hover { color: var(--color-error); }
575716
+
575717
+ /* Slash command palette */
575718
+ #slash-palette {
575719
+ position: absolute;
575720
+ bottom: 100%;
575721
+ left: 14px;
575722
+ right: 14px;
575723
+ margin-bottom: 6px;
575724
+ max-height: 240px;
575725
+ overflow-y: auto;
575726
+ background: var(--color-bg-elevated);
575727
+ border: 1px solid var(--color-border);
575728
+ border-radius: var(--radius-md);
575729
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
575730
+ z-index: 30;
575731
+ font-family: var(--font-ui);
575732
+ }
575733
+ #slash-palette .slash-row {
575734
+ padding: 8px 12px;
575735
+ font-size: 0.78rem;
575736
+ cursor: pointer;
575737
+ display: flex;
575738
+ align-items: center;
575739
+ gap: 10px;
575740
+ color: var(--color-fg);
575741
+ border-bottom: 1px solid var(--color-bg);
575742
+ }
575743
+ #slash-palette .slash-row:last-child { border-bottom: none; }
575744
+ #slash-palette .slash-row:hover,
575745
+ #slash-palette .slash-row.selected {
575746
+ background: var(--color-bg-hover);
575747
+ }
575748
+ #slash-palette .slash-name {
575749
+ font-family: var(--font-mono);
575750
+ color: var(--color-brand);
575751
+ font-size: 0.74rem;
575752
+ flex-shrink: 0;
575753
+ }
575754
+ #slash-palette .slash-desc {
575755
+ color: var(--color-fg-muted);
575756
+ font-size: 0.7rem;
575757
+ white-space: nowrap;
575758
+ overflow: hidden;
575759
+ text-overflow: ellipsis;
575760
+ }
575761
+
575762
+ /* Make sure #footer is the slash palette anchor (relative). */
575763
+ #footer { position: relative; }
575764
+
575765
+ /* Bigger textarea growth budget — was 120px (~6 lines), now 240px. */
575766
+ #input-area { max-height: 240px !important; }
575767
+
575768
+ /* ════════════════════════════════════════════════════════════
575769
+ OWUI-5: Modal scaffold — full-screen overlay with left-rail
575770
+ tabs + content pane + footer.
575771
+ ════════════════════════════════════════════════════════════ */
575772
+ .oa-modal {
575773
+ position: fixed;
575774
+ inset: 0;
575775
+ display: flex;
575776
+ align-items: center;
575777
+ justify-content: center;
575778
+ z-index: 100;
575779
+ font-family: var(--font-ui);
575780
+ }
575781
+ .oa-modal[style*="display:none"] { display: none !important; }
575782
+ .oa-modal-backdrop {
575783
+ position: absolute;
575784
+ inset: 0;
575785
+ background: rgba(0, 0, 0, 0.6);
575786
+ backdrop-filter: blur(2px);
575787
+ }
575788
+ .oa-modal-window {
575789
+ position: relative;
575790
+ width: min(90vw, 920px);
575791
+ height: min(86vh, 640px);
575792
+ background: var(--color-bg);
575793
+ border: 1px solid var(--color-border);
575794
+ border-radius: var(--radius-lg);
575795
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
575796
+ display: flex;
575797
+ flex-direction: column;
575798
+ overflow: hidden;
575799
+ }
575800
+ .oa-modal-header {
575801
+ padding: 16px 20px;
575802
+ border-bottom: 1px solid var(--color-border);
575803
+ display: flex;
575804
+ align-items: center;
575805
+ justify-content: space-between;
575806
+ }
575807
+ .oa-modal-header h3 {
575808
+ margin: 0;
575809
+ font-size: 0.98rem;
575810
+ font-weight: 600;
575811
+ color: var(--color-fg);
575812
+ letter-spacing: -0.01em;
575813
+ }
575814
+ .oa-modal-close {
575815
+ background: transparent;
575816
+ border: none;
575817
+ color: var(--color-fg-muted);
575818
+ font-size: 1.4rem;
575819
+ line-height: 1;
575820
+ cursor: pointer;
575821
+ padding: 4px 10px;
575822
+ border-radius: var(--radius-sm);
575823
+ }
575824
+ .oa-modal-close:hover { background: var(--color-bg-hover); color: var(--color-fg); }
575825
+ .oa-modal-body {
575826
+ display: grid;
575827
+ grid-template-columns: 200px 1fr;
575828
+ flex: 1;
575829
+ min-height: 0;
575830
+ }
575831
+ .oa-modal-rail {
575832
+ border-right: 1px solid var(--color-border);
575833
+ padding: 12px 8px;
575834
+ display: flex;
575835
+ flex-direction: column;
575836
+ gap: 2px;
575837
+ background: var(--color-bg-elevated);
575838
+ overflow-y: auto;
575839
+ }
575840
+ .rail-tab {
575841
+ background: transparent;
575842
+ border: 1px solid transparent;
575843
+ color: var(--color-fg-muted);
575844
+ text-align: left;
575845
+ padding: 8px 12px;
575846
+ border-radius: var(--radius-md);
575847
+ cursor: pointer;
575848
+ font-size: 0.8rem;
575849
+ font-family: var(--font-ui);
575850
+ transition: background 0.12s, color 0.12s;
575851
+ }
575852
+ .rail-tab:hover { background: var(--color-bg-hover); color: var(--color-fg); }
575853
+ .rail-tab.active {
575854
+ background: var(--color-bg-input);
575855
+ color: var(--color-fg);
575856
+ border-color: var(--color-border-strong);
575857
+ }
575858
+ .oa-modal-pane {
575859
+ padding: 18px 22px;
575860
+ overflow-y: auto;
575861
+ color: var(--color-fg);
575862
+ font-size: 0.82rem;
575863
+ }
575864
+ .oa-modal-pane h4 {
575865
+ margin: 0 0 6px;
575866
+ color: var(--color-fg);
575867
+ font-size: 0.86rem;
575868
+ }
575869
+ .oa-modal-aside {
575870
+ border-left: 1px solid var(--color-border);
575871
+ background: var(--color-bg-elevated);
575872
+ overflow-y: auto;
575873
+ }
575874
+ .settings-pane[hidden] { display: none !important; }
575875
+ .settings-pane.active { display: block; }
575876
+ .oa-modal-footer {
575877
+ padding: 12px 20px;
575878
+ border-top: 1px solid var(--color-border);
575879
+ display: flex;
575880
+ justify-content: flex-end;
575881
+ gap: 8px;
575882
+ background: var(--color-bg-elevated);
575883
+ }
575884
+ .oa-btn-secondary {
575885
+ background: var(--color-bg-input);
575886
+ border: 1px solid var(--color-border);
575887
+ color: var(--color-fg);
575888
+ font-family: var(--font-ui);
575889
+ font-size: 0.78rem;
575890
+ padding: 6px 14px;
575891
+ border-radius: var(--radius-sm);
575892
+ cursor: pointer;
575893
+ }
575894
+ .oa-btn-secondary:hover {
575895
+ background: var(--color-bg-hover);
575896
+ border-color: var(--color-border-strong);
575897
+ }
575898
+
575899
+ /* Settings models list rows */
575900
+ #settings-models-list .row,
575901
+ #mm-list .row {
575902
+ display: flex;
575903
+ align-items: center;
575904
+ justify-content: space-between;
575905
+ padding: 8px 12px;
575906
+ border: 1px solid transparent;
575907
+ border-radius: var(--radius-sm);
575908
+ cursor: pointer;
575909
+ font-size: 0.8rem;
575910
+ margin: 2px 0;
575911
+ }
575912
+ #settings-models-list .row:hover,
575913
+ #mm-list .row:hover,
575914
+ #mm-list .row.selected {
575915
+ background: var(--color-bg-hover);
575916
+ border-color: var(--color-border);
575917
+ }
575918
+ #settings-models-list .row.active {
575919
+ border-color: var(--color-brand);
575920
+ background: var(--color-bg-input);
575921
+ color: var(--color-brand);
575922
+ }
575923
+ #settings-models-list .row .right,
575924
+ #mm-list .row .right {
575925
+ font-size: 0.7rem;
575926
+ color: var(--color-fg-faint);
575927
+ }
575928
+
575929
+ /* ════════════════════════════════════════════════════════════
575930
+ OWUI-6: Folders + chat-row layout + context menu
575931
+ ════════════════════════════════════════════════════════════ */
575932
+ .sb-chat {
575933
+ display: flex !important;
575934
+ align-items: center;
575935
+ }
575936
+ .sb-chat-title {
575937
+ flex: 1;
575938
+ white-space: nowrap;
575939
+ overflow: hidden;
575940
+ text-overflow: ellipsis;
575941
+ }
575942
+ .sb-chat-menu,
575943
+ .sb-folder-menu {
575944
+ background: transparent;
575945
+ border: none;
575946
+ color: var(--color-fg-faint);
575947
+ cursor: pointer;
575948
+ padding: 0 4px;
575949
+ border-radius: var(--radius-sm);
575950
+ font-family: var(--font-ui);
575951
+ font-size: 0.85rem;
575952
+ opacity: 0;
575953
+ transition: opacity 0.12s, background 0.12s, color 0.12s;
575954
+ }
575955
+ .sb-chat:hover .sb-chat-menu,
575956
+ .sb-folder-row:hover .sb-folder-menu { opacity: 1; }
575957
+ .sb-chat-menu:hover,
575958
+ .sb-folder-menu:hover { background: var(--color-bg-input); color: var(--color-fg); }
575959
+
575960
+ .sb-folder-action {
575961
+ background: transparent;
575962
+ border: 1px solid transparent;
575963
+ color: var(--color-fg-faint);
575964
+ padding: 2px 6px;
575965
+ border-radius: var(--radius-sm);
575966
+ cursor: pointer;
575967
+ font-size: 0.62rem;
575968
+ font-family: var(--font-ui);
575969
+ text-transform: none;
575970
+ letter-spacing: 0;
575971
+ }
575972
+ .sb-folder-action:hover { background: var(--color-bg-hover); color: var(--color-fg); border-color: var(--color-border); }
575973
+
575974
+ .sb-folder { margin: 4px 0; }
575975
+ .sb-folder.drop-target {
575976
+ outline: 1px dashed var(--color-accent);
575977
+ border-radius: var(--radius-sm);
575978
+ }
575979
+ .sb-folder-row {
575980
+ display: flex;
575981
+ align-items: center;
575982
+ gap: 6px;
575983
+ padding: 6px 6px;
575984
+ cursor: pointer;
575985
+ color: var(--color-fg-muted);
575986
+ border-radius: var(--radius-sm);
575987
+ }
575988
+ .sb-folder-row:hover { background: var(--color-bg-hover); color: var(--color-fg); }
575989
+ .sb-folder-caret {
575990
+ width: 10px;
575991
+ display: inline-block;
575992
+ text-align: center;
575993
+ font-size: 0.75rem;
575994
+ color: var(--color-fg-muted);
575995
+ }
575996
+ .sb-folder-name { flex: 1; font-size: 0.78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
575997
+ .sb-folder-count {
575998
+ font-size: 0.6rem;
575999
+ color: var(--color-fg-faint);
576000
+ background: var(--color-bg-input);
576001
+ padding: 1px 6px;
576002
+ border-radius: 999px;
576003
+ }
576004
+ .sb-folder-body {
576005
+ padding: 0 0 0 14px;
576006
+ border-left: 1px solid var(--color-border);
576007
+ margin-left: 8px;
576008
+ }
576009
+ .sb-folder-empty {
576010
+ padding: 6px 8px;
576011
+ color: var(--color-fg-faint);
576012
+ font-size: 0.65rem;
576013
+ }
576014
+
576015
+ /* Context menu shared by chat + folder row ⋮ */
576016
+ #sb-chat-menu .sb-menu-item {
576017
+ display: block;
576018
+ width: 100%;
576019
+ text-align: left;
576020
+ background: transparent;
576021
+ border: none;
576022
+ color: var(--color-fg);
576023
+ padding: 7px 12px;
576024
+ font-family: var(--font-ui);
576025
+ font-size: 0.78rem;
576026
+ cursor: pointer;
576027
+ }
576028
+ #sb-chat-menu .sb-menu-item:hover { background: var(--color-bg-hover); }
576029
+ #sb-chat-menu .sb-menu-item.sub { padding-left: 22px; color: var(--color-fg-muted); font-size: 0.74rem; }
576030
+ #sb-chat-menu .sb-menu-item.danger { color: var(--color-error); }
576031
+ #sb-chat-menu .sb-menu-item.danger:hover { background: var(--color-bg-hover); color: var(--color-error); }
576032
+ #sb-chat-menu .sb-menu-sub {
576033
+ padding: 6px 12px 2px;
576034
+ color: var(--color-fg-faint);
576035
+ font-size: 0.62rem;
576036
+ text-transform: uppercase;
576037
+ letter-spacing: 0.05em;
576038
+ }
575273
576039
  </style>
575274
576040
  </head>
575275
576041
  <body>
575276
576042
 
576043
+ <!-- ════════════════════════════════════════════════════════════════
576044
+ OWUI-2: Open WebUI-shaped sidebar layout
576045
+ ────────────────────────────────────────────────────────────────
576046
+ Top-level structure: <aside id="oa-sidebar"> + <div id="oa-main">
576047
+ Sidebar holds: brand + new-chat, search, recents (pinned + recent),
576048
+ and a footer rail with tab links + settings/key buttons.
576049
+ The existing tab-bar moves to be HIDDEN (kept in DOM so legacy JS
576050
+ works); switchTab is driven by sidebar nav buttons instead.
576051
+ Resize handle at the right edge updates $sidebarWidth.
576052
+ ════════════════════════════════════════════════════════════════ -->
576053
+ <div id="oa-shell" style="display:flex;flex:1;overflow:hidden;height:100vh">
576054
+
576055
+ <aside id="oa-sidebar" data-collapsed="false" style="width:260px;flex-shrink:0;background:var(--color-bg-elevated);border-right:1px solid var(--color-border);display:flex;flex-direction:column;position:relative;transition:width 0.18s ease">
576056
+ <!-- Brand + collapse toggle -->
576057
+ <div style="padding:14px 12px 8px;display:flex;align-items:center;gap:10px">
576058
+ <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">
576059
+ <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>
576060
+ </button>
576061
+ <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">Open Agents</span>
576062
+ </div>
576063
+
576064
+ <!-- New chat button -->
576065
+ <div id="sidebar-new-chat-row" style="padding:0 12px 10px">
576066
+ <button onclick="newChatSession(); switchTab('chat')" title="New chat (Cmd/Ctrl+Shift+O)" style="width:100%;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:8px 12px;border-radius:var(--radius-md);cursor:pointer;display:flex;align-items:center;gap:10px;font-size:0.82rem;font-weight:500;transition:background 0.12s">
576067
+ <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 5v14"/><path d="M5 12h14"/></svg>
576068
+ <span class="sb-label">New chat</span>
576069
+ </button>
576070
+ </div>
576071
+
576072
+ <!-- Search bar -->
576073
+ <div id="sidebar-search-row" style="padding:0 12px 8px">
576074
+ <div style="position:relative">
576075
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--color-fg-faint);pointer-events:none"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
576076
+ <input id="sidebar-search" placeholder="Search chats..." class="sb-label" oninput="filterSidebarChats(this.value)" style="width:100%;background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-fg);padding:6px 10px 6px 32px;border-radius:var(--radius-md);font-family:var(--font-ui);font-size:0.78rem;outline:none">
576077
+ </div>
576078
+ </div>
576079
+
576080
+ <!-- Chats scroll region — pinned + folders + recent -->
576081
+ <nav id="sidebar-chats" class="sb-label" style="flex:1;overflow-y:auto;padding:4px 8px 8px">
576082
+ <div id="sidebar-pinned-section" style="display:none">
576083
+ <div style="padding:8px 8px 4px;font-size:0.62rem;color:var(--color-fg-faint);text-transform:uppercase;letter-spacing:0.06em">Pinned</div>
576084
+ <div id="sidebar-pinned-list"></div>
576085
+ </div>
576086
+ <div style="padding:8px 8px 4px;font-size:0.62rem;color:var(--color-fg-faint);text-transform:uppercase;letter-spacing:0.06em;display:flex;align-items:center;gap:6px">
576087
+ <span style="flex:1">Chats</span>
576088
+ <span id="sidebar-folder-tools"></span>
576089
+ </div>
576090
+ <div id="sidebar-recent-list">
576091
+ <div style="padding:14px 8px;color:var(--color-fg-faint);font-size:0.7rem;text-align:center">No chats yet</div>
576092
+ </div>
576093
+ </nav>
576094
+
576095
+ <!-- Section nav (the legacy tabs migrated into the sidebar footer) -->
576096
+ <div id="sidebar-nav" class="sb-label" style="border-top:1px solid var(--color-border);padding:8px;display:flex;flex-direction:column;gap:2px">
576097
+ <button class="sb-nav" data-tab="chat" onclick="switchTab('chat')" title="Chat">
576098
+ <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>
576099
+ <span class="sb-label">Chat</span>
576100
+ </button>
576101
+ <button class="sb-nav" data-tab="agent" onclick="switchTab('agent')" title="Agent">
576102
+ <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>
576103
+ <span class="sb-label">Agent</span>
576104
+ </button>
576105
+ <button class="sb-nav" data-tab="voice" onclick="switchTab('voice')" title="Voice">
576106
+ <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>
576107
+ <span class="sb-label">Voice</span>
576108
+ </button>
576109
+ <button class="sb-nav" data-tab="projects" onclick="switchTab('projects')" title="Projects">
576110
+ <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>
576111
+ <span class="sb-label">Projects</span>
576112
+ </button>
576113
+ <button class="sb-nav" data-tab="jobs" onclick="switchTab('jobs')" title="Dashboard">
576114
+ <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>
576115
+ <span class="sb-label">Dashboard</span>
576116
+ </button>
576117
+ <button class="sb-nav" data-tab="activity" onclick="switchTab('activity')" title="Activity">
576118
+ <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>
576119
+ <span class="sb-label">Activity</span>
576120
+ </button>
576121
+ <button class="sb-nav" data-tab="config" onclick="switchTab('config')" title="Config">
576122
+ <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>
576123
+ <span class="sb-label">Settings</span>
576124
+ </button>
576125
+ </div>
576126
+
576127
+ <!-- Sidebar footer — connection + key + update -->
576128
+ <div id="sidebar-footer" class="sb-label" style="border-top:1px solid var(--color-border);padding:10px 12px;display:flex;align-items:center;gap:8px;font-size:0.65rem;color:var(--color-fg-muted)">
576129
+ <span id="sidebar-status-dot" style="width:8px;height:8px;border-radius:50%;background:var(--color-fg-faint);flex-shrink:0" title="Backend connection"></span>
576130
+ <span id="sidebar-status-text" style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">connecting...</span>
576131
+ <button id="sidebar-update-btn" onclick="doUpdate()" style="display:none;background:var(--color-warning);border:none;color:#000;padding:2px 6px;border-radius:var(--radius-sm);font-size:0.6rem;cursor:pointer;font-weight:600">update</button>
576132
+ <button id="sidebar-key-btn" onclick="document.getElementById('key-modal').style.display='flex'" title="Set API key" style="background:transparent;border:1px solid var(--color-border);color:var(--color-fg-muted);padding:2px 6px;border-radius:var(--radius-sm);font-size:0.6rem;cursor:pointer">key</button>
576133
+ </div>
576134
+
576135
+ <!-- Resize handle -->
576136
+ <div id="sidebar-resize" title="Drag to resize sidebar" style="position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;z-index:10"></div>
576137
+ </aside>
576138
+
576139
+ <!-- Main pane — wraps the existing header + tabs + panels.
576140
+ We keep the existing #header / #tabs / panels structure intact so
576141
+ legacy JS keeps working; the sidebar simply becomes the primary
576142
+ navigation surface and the old tabs get visually de-emphasized
576143
+ (still in DOM, hidden via display:none below — see #tabs override). -->
576144
+ <div id="oa-main" style="flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden;background:var(--color-bg)">
576145
+
575277
576146
  <div id="header">
575278
576147
  <span class="accent">OA</span>
575279
576148
  <span class="status" id="status">connecting...</span>
575280
- <span id="update-btn" style="display:none;background:#3a2a10;border:1px solid #b2920a;color:#b2920a;padding:2px 8px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer" onclick="doUpdate()">update</span>
576149
+ <span id="update-btn" style="display:none;background:#3a2a10;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>
575281
576150
  <select id="model-select"><option>loading...</option></select>
575282
576151
  <button class="key-btn" id="files-btn" onclick="toggleFilesForActiveTab()" title="Toggle workspace sidebar (chat or agent depending on active tab)">files</button>
575283
576152
  <button class="key-btn" id="sandbox-toggle" onclick="toggleSandbox()" title="Toggle Docker sandbox" style="opacity:0.5">sandbox: off</button>
@@ -575292,40 +576161,40 @@ body {
575292
576161
  together when wrapping: (1) tab buttons, (2) session selector,
575293
576162
  (3) sys metrics + token counter. On narrow viewports the sections
575294
576163
  flow onto multiple rows instead of overflowing the header. -->
575295
- <div id="tabs" style="display:flex;flex-wrap:wrap;gap:0.25rem;background:#1e1e22;border-bottom:1px solid #2a2a30;padding:0 16px;flex-shrink:0">
576164
+ <div id="tabs" style="display:flex;flex-wrap:wrap;gap:0.25rem;background:var(--color-bg-elevated);border-bottom:1px solid var(--color-bg-input);padding:0 16px;flex-shrink:0">
575296
576165
  <!-- Section 1: tab buttons -->
575297
576166
  <div id="tabs-section-buttons" style="display:flex;gap:0;flex-shrink:0">
575298
- <button class="tab" onclick="switchTab('projects')" id="tab-projects" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">projects</button>
575299
- <button class="tab active" onclick="switchTab('chat')" id="tab-chat" style="background:none;border:none;border-bottom:2px solid #b2920a;color:#b2920a;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">chat</button>
575300
- <button class="tab" onclick="switchTab('agent')" id="tab-agent" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">agent</button>
575301
- <button class="tab" onclick="switchTab('jobs')" id="tab-jobs" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">dashboard</button>
575302
- <button class="tab" onclick="switchTab('config')" id="tab-config" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">config</button>
575303
- <button class="tab" onclick="switchTab('activity')" id="tab-activity" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">activity</button>
575304
- <button class="tab" onclick="switchTab('voice')" id="tab-voice" style="background:none;border:none;border-bottom:2px solid transparent;color:#555;padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">voice</button>
576167
+ <button class="tab" onclick="switchTab('projects')" id="tab-projects" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">projects</button>
576168
+ <button class="tab active" onclick="switchTab('chat')" id="tab-chat" style="background:none;border:none;border-bottom:2px solid var(--color-brand);color:var(--color-brand);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">chat</button>
576169
+ <button class="tab" onclick="switchTab('agent')" id="tab-agent" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">agent</button>
576170
+ <button class="tab" onclick="switchTab('jobs')" id="tab-jobs" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">dashboard</button>
576171
+ <button class="tab" onclick="switchTab('config')" id="tab-config" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">config</button>
576172
+ <button class="tab" onclick="switchTab('activity')" id="tab-activity" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">activity</button>
576173
+ <button class="tab" onclick="switchTab('voice')" id="tab-voice" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--color-fg-faint);padding:6px 16px;font-family:inherit;font-size:0.7rem;cursor:pointer">voice</button>
575305
576174
  </div>
575306
576175
  <!-- Section 2: session dropdown (legacy slot — the per-tab dropdowns
575307
576176
  now live in #session-topbar above the active panel; this slot is
575308
576177
  kept hidden so existing JS that touches #session-select still works
575309
576178
  without forcing a switch). -->
575310
576179
  <div id="tabs-section-sessions" style="display:none;gap:6px;align-items:center;flex-shrink:0">
575311
- <select id="session-select" onchange="switchSession(this.value)" style="background:#2a2a30;border:1px solid #3a3a42;color:#b0b0b0;padding:2px 6px;border-radius:3px;font-family:inherit;font-size:0.6rem;max-width:160px" title="Chat sessions">
576180
+ <select id="session-select" onchange="switchSession(this.value)" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:2px 6px;border-radius:3px;font-family:inherit;font-size:0.6rem;max-width:160px" title="Chat sessions">
575312
576181
  <option value="">new session</option>
575313
576182
  </select>
575314
576183
  </div>
575315
576184
  <!-- Section 3: sys metrics + token counter -->
575316
576185
  <div id="tabs-section-metrics" style="display:flex;flex-flow:wrap;align-items:center;gap:10px;flex-shrink:0">
575317
- <span id="sys-metrics" style="font-size:0.6rem;color:#555;display:flex;flex-flow:column;justify-content:center"></span>
575318
- <span id="token-counter" style="font-size:0.6rem;color:#555;display:flex;flex-flow:column;justify-content:center;padding:0 6px">0 tokens</span>
576186
+ <span id="sys-metrics" style="font-size:0.6rem;color:var(--color-fg-faint);display:flex;flex-flow:column;justify-content:center"></span>
576187
+ <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>
575319
576188
  </div>
575320
576189
  </div>
575321
576190
  <div id="chat-container" style="display:flex;flex:1;overflow:hidden">
575322
- <div id="workspace-sidebar" style="display:none;width:250px;background:#1e1e22;border-right:1px solid #2a2a30;overflow-y:auto;padding:8px;flex-shrink:0;font-size:0.7rem">
576191
+ <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">
575323
576192
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
575324
- <span style="color:#b2920a;font-size:0.7rem;font-weight:bold">Workspace</span>
575325
- <button onclick="toggleWorkspace()" style="background:none;border:none;color:#555;cursor:pointer;font-size:0.8rem">x</button>
576193
+ <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Workspace</span>
576194
+ <button onclick="toggleWorkspace()" style="background:none;border:none;color:var(--color-fg-faint);cursor:pointer;font-size:0.8rem">x</button>
575326
576195
  </div>
575327
- <div id="workspace-cwd" style="color:#555;font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
575328
- <div id="workspace-tree" style="color:#b0b0b0"></div>
576196
+ <div id="workspace-cwd" style="color:var(--color-fg-faint);font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
576197
+ <div id="workspace-tree" style="color:var(--color-fg)"></div>
575329
576198
  </div>
575330
576199
  <!-- Vertical column: session topbar + checklist + conversation -->
575331
576200
  <div style="display:flex;flex-direction:column;flex:1;min-width:0;overflow:hidden">
@@ -575336,11 +576205,11 @@ body {
575336
576205
  <option value="">+ new session</option>
575337
576206
  </select>
575338
576207
  <button onclick="newChatSession()" title="Start a fresh chat session">new</button>
575339
- <button onclick="deleteChatSession()" title="Delete the current chat session" style="color:#b25f5f;border-color:#5a2a2a">del</button>
576208
+ <button onclick="deleteChatSession()" title="Delete the current chat session" style="color:var(--color-error);border-color:var(--color-error)">del</button>
575340
576209
  </div>
575341
576210
  <!-- WO-TASK-02: Agent-emitted checklist (todos) — visible to user above conversation -->
575342
- <div id="todo-checklist" style="display:none;padding:8px 16px;background:#17171a;border-bottom:1px solid #2a2a30">
575343
- <div style="font-size:0.6rem;color:#b2920a;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px">Plan</div>
576211
+ <div id="todo-checklist" style="display:none;padding:8px 16px;background:var(--color-bg);border-bottom:1px solid var(--color-bg-input)">
576212
+ <div style="font-size:0.6rem;color:var(--color-brand);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px">Plan</div>
575344
576213
  <ul id="todo-list" style="list-style:none;margin:0;padding:0;font-size:0.78rem"></ul>
575345
576214
  </div>
575346
576215
  <div id="conversation" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:4px;min-height:0"></div>
@@ -575354,60 +576223,60 @@ body {
575354
576223
  <option value="">+ new run</option>
575355
576224
  </select>
575356
576225
  <button onclick="newAgentRun()" title="Start a fresh agent run">new</button>
575357
- <button onclick="deleteAgentRun()" title="Delete the current agent run" style="color:#b25f5f;border-color:#5a2a2a">del</button>
576226
+ <button onclick="deleteAgentRun()" title="Delete the current agent run" style="color:var(--color-error);border-color:var(--color-error)">del</button>
575358
576227
  <button onclick="refreshAgentRunSelect()" title="Refresh the run list">⟳</button>
575359
576228
  </div>
575360
576229
  <!-- Body row: agent workspace sidebar (isolated from chat) | scrollable form -->
575361
576230
  <div style="display:flex;flex:1;overflow:hidden;min-height:0">
575362
- <div id="agent-workspace-sidebar" style="display:none;width:250px;background:#1e1e22;border-right:1px solid #2a2a30;overflow-y:auto;padding:8px;flex-shrink:0;font-size:0.7rem">
576231
+ <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">
575363
576232
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
575364
- <span style="color:#b2920a;font-size:0.7rem;font-weight:bold">Agent Workspace</span>
575365
- <button onclick="toggleAgentWorkspace()" style="background:none;border:none;color:#555;cursor:pointer;font-size:0.8rem">x</button>
576233
+ <span style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">Agent Workspace</span>
576234
+ <button onclick="toggleAgentWorkspace()" style="background:none;border:none;color:var(--color-fg-faint);cursor:pointer;font-size:0.8rem">x</button>
575366
576235
  </div>
575367
- <div id="agent-workspace-cwd" style="color:#555;font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
575368
- <div id="agent-workspace-tree" style="color:#b0b0b0"></div>
576236
+ <div id="agent-workspace-cwd" style="color:var(--color-fg-faint);font-size:0.6rem;margin-bottom:8px;word-break:break-all"></div>
576237
+ <div id="agent-workspace-tree" style="color:var(--color-fg)"></div>
575369
576238
  </div>
575370
576239
  <div style="flex:1;overflow-y:auto;padding:12px 16px;min-height:0;min-width:0">
575371
576240
  <!-- Parameter form: all the dials for POST /v1/run -->
575372
- <div id="agent-params" style="background:#17171a;border:1px solid #2a2a30;border-radius:4px;padding:10px 12px;margin-bottom:10px;font-size:0.68rem">
575373
- <div style="color:#b2920a;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px">Task Parameters</div>
576241
+ <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">
576242
+ <div style="color:var(--color-brand);font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px">Task Parameters</div>
575374
576243
  <div style="display:grid;grid-template-columns:120px 1fr;gap:6px 12px;align-items:center">
575375
- <label style="color:#888">Working dir</label>
576244
+ <label style="color:var(--color-fg-muted)">Working dir</label>
575376
576245
  <div style="display:flex;gap:4px;align-items:center">
575377
- <input id="agent-working-dir" placeholder="(daemon cwd)" style="flex:1;background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none">
575378
- <button onclick="syncFromWorkspace()" title="Use workspace selection" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">from ws</button>
575379
- <button onclick="document.getElementById('agent-working-dir').value=''" title="Clear" style="background:#2a2a30;border:1px solid #3a3a42;color:#666;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">×</button>
576246
+ <input id="agent-working-dir" placeholder="(daemon cwd)" style="flex:1;background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none">
576247
+ <button onclick="syncFromWorkspace()" title="Use workspace selection" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">from ws</button>
576248
+ <button onclick="document.getElementById('agent-working-dir').value=''" title="Clear" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg-subtle);padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">×</button>
575380
576249
  </div>
575381
576250
 
575382
- <label style="color:#888">Model override</label>
575383
- <input id="agent-model-override" placeholder="(use global model)" style="background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none">
576251
+ <label style="color:var(--color-fg-muted)">Model override</label>
576252
+ <input id="agent-model-override" placeholder="(use global model)" style="background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none">
575384
576253
 
575385
- <label style="color:#888">Max turns</label>
575386
- <input id="agent-max-turns" type="number" min="1" max="200" placeholder="25" style="background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none;width:120px">
576254
+ <label style="color:var(--color-fg-muted)">Max turns</label>
576255
+ <input id="agent-max-turns" type="number" min="1" max="200" placeholder="25" style="background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none;width:120px">
575387
576256
 
575388
- <label style="color:#888">Timeout (s)</label>
575389
- <input id="agent-timeout-s" type="number" min="10" max="3600" placeholder="300" style="background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none;width:120px">
576257
+ <label style="color:var(--color-fg-muted)">Timeout (s)</label>
576258
+ <input id="agent-timeout-s" type="number" min="10" max="3600" placeholder="300" style="background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none;width:120px">
575390
576259
 
575391
- <label style="color:#888">Sandbox</label>
575392
- <select id="agent-sandbox" style="background:#2a2a30;border:1px solid #3a3a42;color:#b0b0b0;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem">
576260
+ <label style="color:var(--color-fg-muted)">Sandbox</label>
576261
+ <select id="agent-sandbox" 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">
575393
576262
  <option value="none">none (host process)</option>
575394
576263
  <option value="workspace">workspace (ephemeral dir)</option>
575395
576264
  <option value="container">container (docker)</option>
575396
576265
  </select>
575397
576266
 
575398
- <label style="color:#888">Stream events</label>
575399
- <div><input type="checkbox" id="agent-stream" checked style="accent-color:#b2920a"> <span style="color:#555;font-size:0.6rem">SSE with live tool calls</span></div>
576267
+ <label style="color:var(--color-fg-muted)">Stream events</label>
576268
+ <div><input type="checkbox" id="agent-stream" checked style="accent-color:var(--color-brand)"> <span style="color:var(--color-fg-faint);font-size:0.6rem">SSE with live tool calls</span></div>
575400
576269
 
575401
- <label style="color:#888">Isolate workspace</label>
575402
- <div><input type="checkbox" id="agent-isolate" style="accent-color:#b2920a"> <span style="color:#555;font-size:0.6rem">fresh temp dir per run</span></div>
576270
+ <label style="color:var(--color-fg-muted)">Isolate workspace</label>
576271
+ <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>
575403
576272
  </div>
575404
576273
  </div>
575405
576274
 
575406
- <textarea id="agent-task" placeholder="Describe the task for the agent..." style="width:100%;min-height:80px;background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:8px 12px;color:#b0b0b0;font-family:inherit;font-size:0.82rem;resize:vertical;outline:none;margin-bottom:8px"></textarea>
576275
+ <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>
575407
576276
  <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
575408
- <select id="agent-profile" style="background:#2a2a30;border:1px solid #3a3a42;color:#b0b0b0;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem"><option value="">no profile</option></select>
575409
- <button onclick="submitAgentTask()" id="agent-submit" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer">run task</button>
575410
- <button onclick="abortAgentTask()" id="agent-abort" style="display:none;background:#2a2a30;border:1px solid #ff4444;color:#ff4444;padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer">abort</button>
576277
+ <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>
576278
+ <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>
576279
+ <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>
575411
576280
  </div>
575412
576281
  <div id="agent-events" style="font-size:0.78rem;line-height:1.5"></div>
575413
576282
  </div><!-- /scrollable agent body -->
@@ -575419,47 +576288,47 @@ body {
575419
576288
  <div id="dashboard-scheduled" style="margin-bottom:16px"></div>
575420
576289
  <div id="dashboard-services" style="margin-bottom:16px"></div>
575421
576290
  <div id="dashboard-usage" style="margin-bottom:16px"></div>
575422
- <h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Job History</h3>
576291
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Job History</h3>
575423
576292
  <div id="jobs-list" style="font-size:0.78rem"></div>
575424
576293
  </div>
575425
576294
  <div id="config-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
575426
- <h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:12px">Server Configuration</h3>
576295
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Server Configuration</h3>
575427
576296
  <div id="config-content" style="font-size:0.78rem"></div>
575428
- <h3 style="color:#b2920a;font-size:0.7rem;margin:16px 0 8px">Model</h3>
576297
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Model</h3>
575429
576298
  <div style="display:flex;gap:8px;align-items:center">
575430
- <select id="config-model-select" style="background:#2a2a30;border:1px solid #3a3a42;color:#b0b0b0;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem;flex:1"></select>
575431
- <button onclick="switchModel()" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">switch</button>
576299
+ <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>
576300
+ <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>
575432
576301
  </div>
575433
- <h3 style="color:#b2920a;font-size:0.7rem;margin:16px 0 8px">Inference Provider</h3>
575434
- <div id="config-endpoint" style="font-size:0.78rem;color:#888;margin-bottom:8px"></div>
575435
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px">
575436
- <input id="config-ep-url" placeholder="https://api.example.com/v1" style="flex:1;min-width:200px;background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none">
575437
- <input id="config-ep-key" placeholder="Bearer key (optional)" type="password" style="flex:1;min-width:150px;background:#2a2a30;border:1px solid #3a3a42;border-radius:3px;padding:4px 8px;color:#b0b0b0;font-family:inherit;font-size:0.7rem;outline:none">
575438
- <select id="config-ep-type" style="background:#2a2a30;border:1px solid #3a3a42;color:#b0b0b0;padding:4px 8px;border-radius:3px;font-family:inherit;font-size:0.7rem">
576302
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Inference Provider</h3>
576303
+ <div id="config-endpoint" style="font-size:0.78rem;color:var(--color-fg-muted);margin-bottom:8px"></div>
576304
+ <form onsubmit="event.preventDefault(); switchEndpoint();" autocomplete="off" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:4px">
576305
+ <input id="config-ep-url" placeholder="https://api.example.com/v1" autocomplete="off" style="flex:1;min-width:200px;background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none">
576306
+ <input id="config-ep-key" placeholder="Bearer key (optional)" type="password" autocomplete="new-password" style="flex:1;min-width:150px;background:var(--color-bg-input);border:1px solid var(--color-border);border-radius:3px;padding:4px 8px;color:var(--color-fg);font-family:inherit;font-size:0.7rem;outline:none">
576307
+ <select id="config-ep-type" 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">
575439
576308
  <option value="ollama">ollama</option><option value="vllm">vllm</option><option value="openai">openai</option>
575440
576309
  </select>
575441
- <button onclick="switchEndpoint()" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">set</button>
575442
- </div>
575443
- <h3 style="color:#b2920a;font-size:0.7rem;margin:16px 0 8px">Profiles</h3>
576310
+ <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>
576311
+ </form>
576312
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Profiles</h3>
575444
576313
  <div id="config-profiles" style="font-size:0.78rem"></div>
575445
- <h3 style="color:#b2920a;font-size:0.7rem;margin:16px 0 8px">Export Conversation</h3>
576314
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin:16px 0 8px">Export Conversation</h3>
575446
576315
  <div style="display:flex;gap:8px">
575447
- <button onclick="exportChat('md')" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">markdown</button>
575448
- <button onclick="exportChat('json')" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">JSON</button>
576316
+ <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>
576317
+ <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>
575449
576318
  </div>
575450
576319
  </div>
575451
576320
  <div id="activity-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
575452
- <h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:12px">Recent Activity (Audit Log)</h3>
576321
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:12px">Recent Activity (Audit Log)</h3>
575453
576322
  <div id="activity-feed" style="font-size:0.72rem"></div>
575454
576323
  </div>
575455
576324
  <div id="projects-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
575456
576325
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
575457
- <h3 style="color:#b2920a;font-size:0.7rem;margin:0">Projects</h3>
575458
- <div style="font-size:0.65rem;color:#666">Every folder where you have run <code style="color:#b2920a">oa</code> is registered here. Click to switch workspace.</div>
576326
+ <h3 style="color:var(--color-brand);font-size:0.7rem;margin:0">Projects</h3>
576327
+ <div style="font-size:0.65rem;color:var(--color-fg-subtle)">Every folder where you have run <code style="color:var(--color-brand)">oa</code> is registered here. Click to switch workspace.</div>
575459
576328
  </div>
575460
- <div id="projects-current" style="background:#1a1a1e;border-left:2px solid #b2920a;padding:8px 12px;margin-bottom:12px;font-size:0.72rem;color:#b0b0b0">
575461
- <span style="color:#666">current:</span> <span id="projects-current-name" style="color:#b2920a">(none)</span>
575462
- <span id="projects-current-root" style="color:#666;margin-left:8px"></span>
576329
+ <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)">
576330
+ <span style="color:var(--color-fg-subtle)">current:</span> <span id="projects-current-name" style="color:var(--color-brand)">(none)</span>
576331
+ <span id="projects-current-root" style="color:var(--color-fg-subtle);margin-left:8px"></span>
575463
576332
  </div>
575464
576333
  <div id="projects-list" style="font-size:0.72rem"></div>
575465
576334
  </div>
@@ -575467,35 +576336,35 @@ body {
575467
576336
  <!-- Voice tab — voicechat toggle + clone management. AudioWorklet drives mic capture; WebAudio handles TTS playback. -->
575468
576337
  <div id="voice-panel" style="display:none;flex:1;overflow-y:auto;padding:12px 16px">
575469
576338
  <div style="margin-bottom:18px">
575470
- <div style="color:#b2920a;font-size:0.85rem;margin-bottom:4px">voicechat</div>
575471
- <div style="font-size:0.65rem;color:#666;margin-bottom:8px">
576339
+ <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">voicechat</div>
576340
+ <div style="font-size:0.65rem;color:var(--color-fg-subtle);margin-bottom:8px">
575472
576341
  Live mic ↔ ASR ↔ agent ↔ TTS over WebSocket. Audio routes over the same origin
575473
576342
  as this page — works on localhost or over a forwarded public IP. Mic permission
575474
576343
  is requested when you start a session.
575475
576344
  </div>
575476
576345
  <div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
575477
- <button id="voice-toggle-btn" onclick="toggleVoiceChat()" style="background:#2a2a30;border:1px solid #b2920a;color:#b2920a;padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.72rem;cursor:pointer">start voicechat</button>
575478
- <span id="voice-state-pill" style="background:#1a1a1e;border-left:2px solid #555;padding:4px 10px;font-size:0.65rem;color:#888">idle</span>
575479
- <span id="voice-mic-pill" style="display:none;background:#1a1a1e;border-left:2px solid #4ec94e;padding:4px 10px;font-size:0.6rem;color:#4ec94e">● mic active</span>
576346
+ <button id="voice-toggle-btn" onclick="toggleVoiceChat()" style="background:var(--color-bg-input);border:1px solid var(--color-brand);color:var(--color-brand);padding:6px 16px;border-radius:3px;font-family:inherit;font-size:0.72rem;cursor:pointer">start voicechat</button>
576347
+ <span id="voice-state-pill" style="background:var(--color-bg);border-left:2px solid var(--color-fg-faint);padding:4px 10px;font-size:0.65rem;color:var(--color-fg-muted)">idle</span>
576348
+ <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>
575480
576349
  </div>
575481
- <div id="voice-transcript-pane" style="display:none;background:#0e0e10;border:1px solid #2a2a30;border-radius:3px;padding:10px;margin-top:6px;min-height:80px;max-height:280px;overflow-y:auto;font-size:0.7rem"></div>
576350
+ <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>
575482
576351
  </div>
575483
576352
 
575484
576353
  <div style="margin-bottom:18px">
575485
- <div style="color:#b2920a;font-size:0.85rem;margin-bottom:4px">voice model</div>
576354
+ <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">voice model</div>
575486
576355
  <div style="display:flex;gap:8px;align-items:center">
575487
- <select id="voice-model-select" style="background:#1a1a1e;border:1px solid #3a3a42;color:#b0b0b0;padding:5px 10px;border-radius:3px;font-family:inherit;font-size:0.7rem;flex:1"></select>
575488
- <button onclick="switchVoiceModel()" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:5px 12px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">switch</button>
576356
+ <select id="voice-model-select" 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>
576357
+ <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>
575489
576358
  </div>
575490
576359
  </div>
575491
576360
 
575492
576361
  <div style="margin-bottom:18px">
575493
576362
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
575494
- <div style="color:#b2920a;font-size:0.85rem">voice clone references</div>
575495
- <button onclick="document.getElementById('clone-upload-input').click()" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">+ upload</button>
576363
+ <div style="color:var(--color-brand);font-size:0.85rem">voice clone references</div>
576364
+ <button onclick="document.getElementById('clone-upload-input').click()" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.65rem;cursor:pointer">+ upload</button>
575496
576365
  <input type="file" id="clone-upload-input" accept="audio/wav,audio/mp3,audio/mpeg,audio/ogg,audio/flac,audio/m4a,audio/aac,audio/opus,.wav,.mp3,.ogg,.flac,.m4a,.aac,.opus" style="display:none" onchange="uploadCloneRef(this.files)">
575497
576366
  </div>
575498
- <div style="font-size:0.6rem;color:#666;margin-bottom:8px">
576367
+ <div style="font-size:0.6rem;color:var(--color-fg-subtle);margin-bottom:8px">
575499
576368
  LuxTTS clones a voice from a 3+ second reference clip. Upload a clean WAV/MP3 sample,
575500
576369
  then activate it. Active reference is used whenever the LuxTTS voice model is selected.
575501
576370
  </div>
@@ -575503,10 +576372,10 @@ body {
575503
576372
  </div>
575504
576373
 
575505
576374
  <div style="margin-bottom:18px">
575506
- <div style="color:#b2920a;font-size:0.85rem;margin-bottom:4px">say something (test TTS)</div>
576375
+ <div style="color:var(--color-brand);font-size:0.85rem;margin-bottom:4px">say something (test TTS)</div>
575507
576376
  <div style="display:flex;gap:8px">
575508
- <input type="text" id="voice-test-text" placeholder="Type and the voice model speaks it..." style="flex:1;background:#1a1a1e;border:1px solid #3a3a42;color:#b0b0b0;padding:6px 10px;border-radius:3px;font-family:inherit;font-size:0.7rem">
575509
- <button onclick="testTTS()" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:5px 14px;border-radius:3px;font-family:inherit;font-size:0.7rem;cursor:pointer">speak</button>
576377
+ <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">
576378
+ <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>
575510
576379
  </div>
575511
576380
  </div>
575512
576381
  </div>
@@ -575518,11 +576387,11 @@ body {
575518
576387
  <!-- WO-CHAT-AUTOSCROLL — tiny tag that appears far-right just above
575519
576388
  the tasks-row when the user has scrolled up from live. Click
575520
576389
  jumps to bottom and re-enables auto-scroll. -->
575521
- <div id="scroll-bottom-tag" style="display:none;position:absolute;right:14px;top:-22px;background:#17171a;border:1px solid #b2920a;color:#b2920a;padding:2px 8px;border-radius:3px;font-size:0.6rem;cursor:pointer;z-index:5;line-height:1.4" onclick="scrollChatToBottom()">scroll to bottom &darr;</div>
576390
+ <div id="scroll-bottom-tag" style="display:none;position:absolute;right:14px;top:-22px;background:var(--color-bg);border:1px solid var(--color-brand);color:var(--color-brand);padding:2px 8px;border-radius:3px;font-size:0.6rem;cursor:pointer;z-index:5;line-height:1.4" onclick="scrollChatToBottom()">scroll to bottom &darr;</div>
575522
576391
 
575523
576392
  <!-- WO-TASK-06: Compact aggregated pill label above the dots row.
575524
576393
  Hidden when no processes are active; click to toggle dots row visibility. -->
575525
- <div id="processes-pill" style="display:none;padding:4px 16px;background:#17171a;border-bottom:1px solid #2a2a30;color:#666;font-size:0.62rem;cursor:pointer" onclick="toggleProcessesRow()">
576394
+ <div id="processes-pill" style="display:none;padding:4px 16px;background:var(--color-bg);border-bottom:1px solid var(--color-bg-input);color:var(--color-fg-subtle);font-size:0.62rem;cursor:pointer" onclick="toggleProcessesRow()">
575526
576395
  <span id="processes-pill-text">idle</span>
575527
576396
  </div>
575528
576397
 
@@ -575538,29 +576407,174 @@ body {
575538
576407
  <span class="proc-empty" id="proc-empty">idle</span>
575539
576408
  </div>
575540
576409
 
576410
+ <!-- ════════════════════════════════════════════════════════════
576411
+ OWUI-4: Input toolbar (above the textarea)
576412
+ Buttons: file attach, mic toggle (uses /v1/voicechat/ws),
576413
+ slash palette opener, model picker (compact), system-prompt
576414
+ toggle. The legacy 'sys' toggle moves here too so #input-row
576415
+ stays focused on text + send/stop only.
576416
+ ════════════════════════════════════════════════════════════ -->
576417
+ <div id="input-toolbar">
576418
+ <button class="ibtn" id="attach-btn" title="Attach files" onclick="document.getElementById('attach-input').click()">
576419
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
576420
+ </button>
576421
+ <input type="file" id="attach-input" multiple style="display:none" onchange="handleAttachInput(this.files); this.value=''">
576422
+
576423
+ <button class="ibtn" id="mic-btn" title="Toggle voicechat (Cmd/Ctrl+Shift+M)" onclick="toggleVoiceMicFromInput()">
576424
+ <svg width="14" height="14" 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>
576425
+ </button>
576426
+
576427
+ <button class="ibtn" id="slash-btn" title="Slash commands (/)" onclick="openSlashPalette()">
576428
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="5" x2="5" y2="19"/></svg>
576429
+ </button>
576430
+
576431
+ <button class="ibtn" id="sys-btn" title="Toggle system prompt" onclick="toggleSystemPrompt()">
576432
+ sys
576433
+ </button>
576434
+
576435
+ <span class="ibtn-spacer" style="flex:1"></span>
576436
+
576437
+ <select id="input-model-mini" class="ibtn-model" onchange="if(this.value){const m=document.getElementById('model-select');if(m){m.value=this.value;m.dispatchEvent(new Event('change'));}}" title="Model"></select>
576438
+ </div>
576439
+
576440
+ <!-- Attachments chip row (populated by handleAttachInput) -->
576441
+ <div id="attach-chips" style="display:none"></div>
576442
+
576443
+ <!-- Slash command palette (floating overlay anchored above the input) -->
576444
+ <div id="slash-palette" style="display:none">
576445
+ <div id="slash-palette-list"></div>
576446
+ </div>
576447
+
575541
576448
  <!-- Lower sub-row: existing input + send/stop -->
575542
576449
  <div id="input-row">
575543
- <span id="system-prompt-toggle" onclick="toggleSystemPrompt()">sys</span>
576450
+ <span id="system-prompt-toggle" onclick="toggleSystemPrompt()" style="display:none">sys</span>
575544
576451
  <textarea id="input-area" placeholder="Type a message..." rows="1"></textarea>
575545
576452
  <button id="send-btn" onclick="sendMessage()">send</button>
575546
- <button id="stop-btn" onclick="stopChat()" style="display:none;background:#2a2a30;border:1px solid #ff4444;color:#ff4444;padding:10px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">stop</button>
576453
+ <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>
575547
576454
  <!-- WO-CHAT-CHECKIN — teal accent button that takes the place of stop
575548
576455
  when the user starts typing during an active run. Click sends a
575549
576456
  side-channel check-in to the triage sub-agent (route /v1/chat/check-in)
575550
576457
  which expands the input into a steering instruction the main agent
575551
576458
  picks up at its next turn. -->
575552
- <button id="checkin-btn" onclick="sendCheckin()" style="display:none;background:#0a2a2e;border:1px solid #2db4b4;color:#2db4b4;padding:10px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">check in</button>
576459
+ <button id="checkin-btn" onclick="sendCheckin()" style="display:none;background:var(--color-info);border:1px solid var(--color-info);color:var(--color-info);padding:10px 16px;border-radius:3px;font-family:inherit;font-size:0.75rem;cursor:pointer;flex-shrink:0">check in</button>
575553
576460
  </div>
575554
576461
  </div>
575555
576462
 
575556
576463
  <div id="key-modal">
575557
- <div class="modal">
576464
+ <form class="modal" onsubmit="event.preventDefault(); saveKey();" autocomplete="off">
575558
576465
  <h3>API Key</h3>
575559
- <input id="key-input" type="password" placeholder="Bearer token (leave empty if auth disabled)">
576466
+ <input id="key-input" type="password" placeholder="Bearer token (leave empty if auth disabled)" autocomplete="new-password">
575560
576467
  <div>
575561
- <button onclick="saveKey()">save</button>
575562
- <button onclick="clearKey()">clear</button>
575563
- <button onclick="closeKeyModal()">cancel</button>
576468
+ <button type="submit">save</button>
576469
+ <button type="button" onclick="clearKey()">clear</button>
576470
+ <button type="button" onclick="closeKeyModal()">cancel</button>
576471
+ </div>
576472
+ </form>
576473
+ </div>
576474
+
576475
+ <!-- ════════════════════════════════════════════════════════════════
576476
+ OWUI-5: Settings modal — full-screen overlay with left-rail
576477
+ tabs + content pane + Save/Cancel footer.
576478
+ ════════════════════════════════════════════════════════════════ -->
576479
+ <div id="settings-modal" class="oa-modal" role="dialog" aria-modal="true" aria-labelledby="settings-modal-title" style="display:none">
576480
+ <div class="oa-modal-backdrop" onclick="closeSettingsModal()"></div>
576481
+ <div class="oa-modal-window" role="document">
576482
+ <div class="oa-modal-header">
576483
+ <h3 id="settings-modal-title">Settings</h3>
576484
+ <button class="oa-modal-close" onclick="closeSettingsModal()" aria-label="Close">&times;</button>
576485
+ </div>
576486
+ <div class="oa-modal-body">
576487
+ <nav class="oa-modal-rail" role="tablist">
576488
+ <button class="rail-tab active" role="tab" data-pane="general" onclick="openSettingsPane('general')">General</button>
576489
+ <button class="rail-tab" role="tab" data-pane="connections" onclick="openSettingsPane('connections')">Connections</button>
576490
+ <button class="rail-tab" role="tab" data-pane="models" onclick="openSettingsPane('models')">Models</button>
576491
+ <button class="rail-tab" role="tab" data-pane="voice" onclick="openSettingsPane('voice')">Voice</button>
576492
+ <button class="rail-tab" role="tab" data-pane="profiles" onclick="openSettingsPane('profiles')">Profiles</button>
576493
+ <button class="rail-tab" role="tab" data-pane="advanced" onclick="openSettingsPane('advanced')">Advanced</button>
576494
+ </nav>
576495
+ <div class="oa-modal-pane">
576496
+ <section class="settings-pane active" id="settings-pane-general" role="tabpanel">
576497
+ <h4>Theme</h4>
576498
+ <label style="display:block;margin:8px 0;font-size:0.78rem">
576499
+ <input type="radio" name="oa-theme" value="dark" checked onchange="setOATheme(this.value)"> Dark
576500
+ </label>
576501
+ <label style="display:block;margin:8px 0;font-size:0.78rem">
576502
+ <input type="radio" name="oa-theme" value="light" onchange="setOATheme(this.value)"> Light
576503
+ </label>
576504
+ <h4 style="margin-top:18px">Font scale</h4>
576505
+ <label style="display:block;margin:8px 0;font-size:0.78rem">
576506
+ <input type="range" min="0.85" max="1.25" step="0.05" value="1" oninput="setOAFontScale(this.value)" style="width:200px;vertical-align:middle">
576507
+ <span id="oa-font-scale-display">1.00x</span>
576508
+ </label>
576509
+ </section>
576510
+ <section class="settings-pane" id="settings-pane-connections" role="tabpanel" hidden>
576511
+ <h4>Backend</h4>
576512
+ <p style="font-size:0.74rem;color:var(--color-fg-muted);margin:6px 0 12px">Loaded from /v1/config — edits POST /v1/config/endpoint.</p>
576513
+ <div id="settings-connections-host">
576514
+ <button onclick="loadSettingsConnections()" style="font-size:0.74rem">load current</button>
576515
+ </div>
576516
+ </section>
576517
+ <section class="settings-pane" id="settings-pane-models" role="tabpanel" hidden>
576518
+ <h4>Available models</h4>
576519
+ <p style="font-size:0.74rem;color:var(--color-fg-muted);margin:6px 0 12px">Sourced from /v1/models. Click a row to set it as active.</p>
576520
+ <input type="search" id="settings-models-search" placeholder="Filter..." oninput="filterSettingsModels(this.value)" style="width:100%;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:6px 10px;border-radius:var(--radius-sm);font-size:0.78rem;margin-bottom:8px">
576521
+ <div id="settings-models-list" style="max-height:300px;overflow-y:auto;font-size:0.78rem"></div>
576522
+ <div style="margin-top:10px"><button onclick="loadSettingsModels()" style="font-size:0.74rem">refresh</button></div>
576523
+ </section>
576524
+ <section class="settings-pane" id="settings-pane-voice" role="tabpanel" hidden>
576525
+ <h4>Voice clone references</h4>
576526
+ <p style="font-size:0.74rem;color:var(--color-fg-muted);margin:6px 0 12px">Manage TTS clone refs (relocated from the legacy Voice tab).</p>
576527
+ <div id="settings-voice-host">
576528
+ <button onclick="loadSettingsVoice()" style="font-size:0.74rem">load</button>
576529
+ </div>
576530
+ </section>
576531
+ <section class="settings-pane" id="settings-pane-profiles" role="tabpanel" hidden>
576532
+ <h4>Agent profiles</h4>
576533
+ <p style="font-size:0.74rem;color:var(--color-fg-muted);margin:6px 0 12px">Same as Agent tab profile list.</p>
576534
+ <div id="settings-profiles-host">
576535
+ <button onclick="loadSettingsProfiles()" style="font-size:0.74rem">load</button>
576536
+ </div>
576537
+ </section>
576538
+ <section class="settings-pane" id="settings-pane-advanced" role="tabpanel" hidden>
576539
+ <h4>Advanced</h4>
576540
+ <p style="font-size:0.74rem;color:var(--color-fg-muted);margin:6px 0 12px">Diagnostics, raw config, danger zone.</p>
576541
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
576542
+ <button onclick="window.open('/api/docs', '_blank')" style="font-size:0.74rem">open swagger</button>
576543
+ <button onclick="window.open('/redoc', '_blank')" style="font-size:0.74rem">open redoc</button>
576544
+ <button onclick="window.open('/asyncapi.json', '_blank')" style="font-size:0.74rem">asyncapi spec</button>
576545
+ <button onclick="window.open('/openapi.json', '_blank')" style="font-size:0.74rem">openapi spec</button>
576546
+ </div>
576547
+ </section>
576548
+ </div>
576549
+ </div>
576550
+ <div class="oa-modal-footer">
576551
+ <button class="oa-btn-secondary" onclick="closeSettingsModal()">Close</button>
576552
+ </div>
576553
+ </div>
576554
+ </div>
576555
+
576556
+ <!-- ════════════════════════════════════════════════════════════════
576557
+ OWUI-5: Model manager modal — search + list + details.
576558
+ ════════════════════════════════════════════════════════════════ -->
576559
+ <div id="model-manager-modal" class="oa-modal" role="dialog" aria-modal="true" aria-labelledby="model-manager-title" style="display:none">
576560
+ <div class="oa-modal-backdrop" onclick="closeModelManager()"></div>
576561
+ <div class="oa-modal-window" role="document">
576562
+ <div class="oa-modal-header">
576563
+ <h3 id="model-manager-title">Model manager</h3>
576564
+ <button class="oa-modal-close" onclick="closeModelManager()" aria-label="Close">&times;</button>
576565
+ </div>
576566
+ <div class="oa-modal-body" style="grid-template-columns:1fr 280px">
576567
+ <div class="oa-modal-pane" style="padding:14px">
576568
+ <input type="search" id="mm-search" placeholder="Search models..." oninput="filterModelManager(this.value)" style="width:100%;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:8px 12px;border-radius:var(--radius-sm);font-size:0.82rem;margin-bottom:10px">
576569
+ <div id="mm-list" style="max-height:420px;overflow-y:auto"></div>
576570
+ </div>
576571
+ <aside class="oa-modal-aside" id="mm-detail">
576572
+ <div style="padding:14px;color:var(--color-fg-muted);font-size:0.78rem">Select a model on the left.</div>
576573
+ </aside>
576574
+ </div>
576575
+ <div class="oa-modal-footer">
576576
+ <button class="oa-btn-secondary" onclick="loadModelManager()">refresh</button>
576577
+ <button class="oa-btn-secondary" onclick="closeModelManager()">Close</button>
575564
576578
  </div>
575565
576579
  </div>
575566
576580
  </div>
@@ -575917,7 +576931,10 @@ if (conv) {
575917
576931
  // Auto-resize textarea + WO-CHAT-CHECKIN typing detection
575918
576932
  input.addEventListener('input', () => {
575919
576933
  input.style.height = 'auto';
575920
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
576934
+ // OWUI-4: bump max from 120 (~6 lines) to 240 (~12 lines).
576935
+ input.style.height = Math.min(input.scrollHeight, 240) + 'px';
576936
+ // OWUI-4: open slash palette as soon as the input starts with "/".
576937
+ try { _maybeUpdateSlashPalette(); } catch {}
575921
576938
  // While a run is streaming, swap stop button → check-in button as
575922
576939
  // soon as the user types anything. When the input is cleared, swap back.
575923
576940
  if (streaming) {
@@ -575933,6 +576950,22 @@ input.addEventListener('input', () => {
575933
576950
  }
575934
576951
  });
575935
576952
  input.addEventListener('keydown', (e) => {
576953
+ // OWUI-4: arrow / enter / escape navigation for the slash palette
576954
+ // takes priority over the normal Enter behaviour.
576955
+ const palOpen = _slashPaletteIsOpen();
576956
+ if (palOpen) {
576957
+ if (e.key === 'ArrowDown') { e.preventDefault(); _slashPaletteMove(1); return; }
576958
+ if (e.key === 'ArrowUp') { e.preventDefault(); _slashPaletteMove(-1); return; }
576959
+ if (e.key === 'Escape') { e.preventDefault(); closeSlashPalette(); return; }
576960
+ if (e.key === 'Tab') { e.preventDefault(); _slashPaletteAccept(); return; }
576961
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); _slashPaletteAccept(); return; }
576962
+ }
576963
+ // OWUI-4: Cmd/Ctrl+Enter — always sends, never inserts a newline.
576964
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
576965
+ e.preventDefault();
576966
+ if (streaming && input.value.trim().length > 0) sendCheckin(); else sendMessage();
576967
+ return;
576968
+ }
575936
576969
  if (e.key === 'Enter' && !e.shiftKey) {
575937
576970
  e.preventDefault();
575938
576971
  // While a run is streaming, Enter dispatches a check-in instead of
@@ -576041,29 +577074,106 @@ function persistSelectedModel() {
576041
577074
  function addMessage(role, content) {
576042
577075
  const div = document.createElement('div');
576043
577076
  div.className = 'msg ' + role;
576044
- // Always run markdown rendering for assistant. For user messages,
576045
- // detect markdown patterns and render IF present, otherwise keep
576046
- // plain text (whitespace preserved by the .msg pre-wrap rule).
577077
+ div.dataset.role = role;
577078
+ // OWUI-3: wrap user/system content in an inner element so the bubble/
577079
+ // chip CSS can target it via .msg.user > * / .msg.system > * .
577080
+ // Assistant messages stay structurally flat — the avatar is rendered
577081
+ // via ::before pseudo-element so all child nodes are content.
577082
+ let host = div;
577083
+ if (role === 'user' || role === 'system') {
577084
+ host = document.createElement('div');
577085
+ host.className = 'msg-bubble';
577086
+ div.appendChild(host);
577087
+ }
576047
577088
  if (role === 'assistant' || (role === 'user' && looksLikeMarkdown(content))) {
576048
- div.innerHTML = renderMarkdown(content);
577089
+ host.innerHTML = renderMarkdown(content);
576049
577090
  } else {
576050
- div.textContent = content;
577091
+ host.textContent = content;
576051
577092
  }
576052
- // Copy button on assistant messages
577093
+ // OWUI-3: rich per-message action row on assistant messages.
576053
577094
  if (role === 'assistant') {
576054
577095
  const actions = document.createElement('div');
576055
577096
  actions.className = 'msg-actions';
576056
- const copyBtn = document.createElement('button');
576057
- copyBtn.textContent = 'copy';
576058
- copyBtn.onclick = () => { navigator.clipboard.writeText(content); copyBtn.textContent = 'copied'; setTimeout(() => copyBtn.textContent = 'copy', 1500); };
576059
- actions.appendChild(copyBtn);
577097
+ const mkBtn = (label, title, fn) => {
577098
+ const b = document.createElement('button');
577099
+ b.textContent = label;
577100
+ b.title = title;
577101
+ b.onclick = fn;
577102
+ return b;
577103
+ };
577104
+ actions.appendChild(mkBtn('copy', 'Copy message', () => {
577105
+ // Copy what's actually visible (post-render text), not the raw arg —
577106
+ // the closure-captured "content" may be empty when this message was
577107
+ // built incrementally during streaming.
577108
+ const txt = host.innerText || host.textContent || content || '';
577109
+ try { navigator.clipboard.writeText(txt); } catch {}
577110
+ const orig = actions.querySelector('button').textContent;
577111
+ actions.querySelector('button').textContent = 'copied';
577112
+ setTimeout(() => { actions.querySelector('button').textContent = orig; }, 1500);
577113
+ }));
577114
+ actions.appendChild(mkBtn('regen', 'Regenerate this response', () => {
577115
+ try { regenerateAssistantMessage(div); } catch (e) { console.warn('regen failed', e); }
577116
+ }));
577117
+ actions.appendChild(mkBtn('del', 'Delete this message', () => {
577118
+ try { deleteMessageDiv(div); } catch (e) { console.warn('del failed', e); }
577119
+ }));
576060
577120
  div.appendChild(actions);
576061
577121
  }
576062
577122
  conv.appendChild(div);
577123
+ // OWUI-3: schedule syntax highlight for any new code blocks.
577124
+ try { _highlightCodeBlocks(div); } catch {}
576063
577125
  maybeAutoScroll();
576064
577126
  return div;
576065
577127
  }
576066
577128
 
577129
+ // OWUI-3: regenerate by re-running the most recent user message's send
577130
+ // flow. We delete the assistant message + any messages after the
577131
+ // preceding user message, then re-dispatch send().
577132
+ function regenerateAssistantMessage(msgDiv) {
577133
+ if (!msgDiv || !conv || streaming) return;
577134
+ // Walk backwards in the DOM to find the immediately-preceding user msg.
577135
+ let prev = msgDiv.previousElementSibling;
577136
+ while (prev && !(prev.classList && prev.classList.contains('msg') && prev.dataset.role === 'user')) {
577137
+ prev = prev.previousElementSibling;
577138
+ }
577139
+ if (!prev) return;
577140
+ const userText = (prev.querySelector('.msg-bubble')?.innerText || prev.innerText || '').trim();
577141
+ if (!userText) return;
577142
+ // Drop the assistant + any siblings between user and assistant.
577143
+ let cur = prev.nextElementSibling;
577144
+ while (cur) {
577145
+ const next = cur.nextElementSibling;
577146
+ cur.remove();
577147
+ cur = next;
577148
+ }
577149
+ // Drop the matching backend message-history entry too so it doesn't get
577150
+ // double-counted on resend.
577151
+ if (Array.isArray(messages)) {
577152
+ while (messages.length && messages[messages.length - 1].role !== 'user') messages.pop();
577153
+ if (messages.length && messages[messages.length - 1].role === 'user') messages.pop();
577154
+ }
577155
+ // Now also drop the user's DOM bubble — send() re-adds it.
577156
+ prev.remove();
577157
+ // Repopulate the input and re-fire send.
577158
+ input.value = userText;
577159
+ try { send(); } catch (e) { console.warn('regen send failed', e); }
577160
+ }
577161
+
577162
+ // OWUI-3: delete a single message div from the DOM + memory.
577163
+ // Trims trailing message-history entries beyond the deleted index so the
577164
+ // next turn doesn't carry orphaned context.
577165
+ function deleteMessageDiv(msgDiv) {
577166
+ if (!msgDiv) return;
577167
+ const role = msgDiv.dataset.role;
577168
+ msgDiv.remove();
577169
+ if (Array.isArray(messages)) {
577170
+ // Remove the LAST entry matching this role (best-effort).
577171
+ for (let i = messages.length - 1; i >= 0; i--) {
577172
+ if (messages[i].role === role) { messages.splice(i, 1); break; }
577173
+ }
577174
+ }
577175
+ }
577176
+
576067
577177
  // WO-TASK-02 — heuristic detection: does this string contain markdown
576068
577178
  // constructs that warrant parsing? Used for .msg.user content where we
576069
577179
  // don't want to render every plain string as HTML, but we DO want to
@@ -576115,14 +577225,14 @@ function renderMarkdown(text) {
576115
577225
  // Headers — h1..h6
576116
577226
  text = text.replace(/(^|\\n)(#{1,6}) (.+?)(\\n|$)/g, (_, pre, hashes, body, post) => {
576117
577227
  const lvl = hashes.length;
576118
- return pre + '<h' + lvl + ' style="margin:6px 0 4px;color:#b2920a;font-size:' + (1.05 - lvl * 0.05) + 'rem">' + body + '</h' + lvl + '>' + (post === '\\n' ? '' : post);
577228
+ return pre + '<h' + lvl + ' style="margin:6px 0 4px;color:var(--color-brand);font-size:' + (1.05 - lvl * 0.05) + 'rem">' + body + '</h' + lvl + '>' + (post === '\\n' ? '' : post);
576119
577229
  });
576120
577230
  // Blockquote
576121
577231
  text = text.replace(/(^|\\n)> (.+?)(\\n|$)/g, (_, pre, body, post) => {
576122
- return pre + '<blockquote style="margin:4px 0;padding:2px 8px;border-left:2px solid #b2920a;color:#888">' + body + '</blockquote>' + (post === '\\n' ? '' : post);
577232
+ return pre + '<blockquote style="margin:4px 0;padding:2px 8px;border-left:2px solid var(--color-brand);color:var(--color-fg-muted)">' + body + '</blockquote>' + (post === '\\n' ? '' : post);
576123
577233
  });
576124
577234
  // Horizontal rule
576125
- text = text.replace(/(^|\\n)(?:---|\\*\\*\\*|___)(\\n|$)/g, '$1<hr style="border:none;border-top:1px solid #2a2a30;margin:8px 0">$2');
577235
+ text = text.replace(/(^|\\n)(?:---|\\*\\*\\*|___)(\\n|$)/g, '$1<hr style="border:none;border-top:1px solid var(--color-bg-input);margin:8px 0">$2');
576126
577236
  // Unordered lists — collapse consecutive items into a single <ul>
576127
577237
  text = text.replace(/(?:(^|\\n)[-*+] [^\\n]+)+/g, m => {
576128
577238
  const items = m.trim().split(/\\n/).map(l => l.replace(/^[-*+] /, '')).map(l => '<li>' + l + '</li>').join('');
@@ -576146,7 +577256,7 @@ function renderMarkdown(text) {
576146
577256
  text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (_, label, url) => {
576147
577257
  // Only allow http(s) or relative URLs to prevent javascript: links
576148
577258
  const safe = /^(https?:\\/\\/|\\/|\\.\\.?\\/|#)/.test(url) ? url : '#';
576149
- return '<a href="' + safe + '" target="_blank" rel="noopener noreferrer" style="color:#b2920a">' + label + '</a>';
577259
+ return '<a href="' + safe + '" target="_blank" rel="noopener noreferrer" style="color:var(--color-brand)">' + label + '</a>';
576150
577260
  });
576151
577261
 
576152
577262
  // 5) Re-insert code blocks
@@ -576183,7 +577293,7 @@ function appendExpandableContent(parent, fullText, opts) {
576183
577293
  if (isLong) {
576184
577294
  const btn = document.createElement('button');
576185
577295
  btn.type = 'button';
576186
- btn.style.cssText = 'align-self:flex-start;margin-top:2px;padding:2px 8px;background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;font-size:0.6rem;border-radius:2px;cursor:pointer;font-family:inherit;';
577296
+ btn.style.cssText = 'align-self:flex-start;margin-top:2px;padding:2px 8px;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);font-size:0.6rem;border-radius:2px;cursor:pointer;font-family:inherit;';
576187
577297
  btn.textContent = 'Show more (' + text.length + ' chars)';
576188
577298
  let expanded = false;
576189
577299
  btn.addEventListener('click', (e) => {
@@ -576206,9 +577316,9 @@ function appendExpandableContent(parent, fullText, opts) {
576206
577316
  // session message shape ({role:'tool_call', tool, args}).
576207
577317
  function renderToolCallEvent(parent, chunkLike) {
576208
577318
  const details = document.createElement('details');
576209
- details.style.cssText = 'background:#1e1e22;border-left:2px solid #b2920a;margin:2px 0;font-size:0.7rem';
577319
+ details.style.cssText = 'background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);margin:2px 0;font-size:0.7rem';
576210
577320
  const summary = document.createElement('summary');
576211
- summary.style.cssText = 'padding:4px 8px;color:#b2920a;cursor:pointer';
577321
+ summary.style.cssText = 'padding:4px 8px;color:var(--color-brand);cursor:pointer';
576212
577322
 
576213
577323
  const toolName = chunkLike.tool || 'tool';
576214
577324
  let a = (chunkLike.args && typeof chunkLike.args === 'object') ? chunkLike.args : {};
@@ -576253,18 +577363,18 @@ function renderToolCallEvent(parent, chunkLike) {
576253
577363
 
576254
577364
  if (a && typeof a === 'object') {
576255
577365
  const argsDiv = document.createElement('div');
576256
- argsDiv.style.cssText = 'padding:4px 8px 6px 16px;color:#888;font-size:0.65rem;border-top:1px solid #2a2a30';
577366
+ argsDiv.style.cssText = 'padding:4px 8px 6px 16px;color:var(--color-fg-muted);font-size:0.65rem;border-top:1px solid var(--color-bg-input)';
576257
577367
  if (toolName === 'todo_write' && Array.isArray(a.todos)) {
576258
577368
  for (const t of a.todos) {
576259
577369
  if (!t || typeof t !== 'object') continue;
576260
577370
  const row = document.createElement('div');
576261
577371
  row.style.cssText = 'padding:2px 0';
576262
577372
  let mark = '\\u25CB';
576263
- let color = '#666';
577373
+ let color = 'var(--color-fg-subtle)';
576264
577374
  if (t.status === 'completed') { mark = '\\u25C9'; color = '#4a7a4a'; }
576265
- else if (t.status === 'in_progress') { mark = '\\u25D0'; color = '#b2920a'; }
576266
- else if (t.status === 'blocked') { mark = '\\u25CD'; color = '#b25f5f'; }
576267
- row.innerHTML = '<span style="color:' + color + '">' + mark + '</span> <span style="color:#b0b0b0">' + escHtml(String(t.content || '').slice(0, 300)) + '</span>' + (t.blocker ? ' <span style="color:#b25f5f">(blocked: ' + escHtml(String(t.blocker).slice(0, 100)) + ')</span>' : '');
577375
+ else if (t.status === 'in_progress') { mark = '\\u25D0'; color = 'var(--color-brand)'; }
577376
+ else if (t.status === 'blocked') { mark = '\\u25CD'; color = 'var(--color-error)'; }
577377
+ row.innerHTML = '<span style="color:' + color + '">' + mark + '</span> <span style="color:var(--color-fg)">' + escHtml(String(t.content || '').slice(0, 300)) + '</span>' + (t.blocker ? ' <span style="color:var(--color-error)">(blocked: ' + escHtml(String(t.blocker).slice(0, 100)) + ')</span>' : '');
576268
577378
  argsDiv.appendChild(row);
576269
577379
  }
576270
577380
  } else {
@@ -576278,13 +577388,13 @@ function renderToolCallEvent(parent, chunkLike) {
576278
577388
  else { try { vs = JSON.stringify(v, null, 2); } catch { vs = '[object]'; } }
576279
577389
 
576280
577390
  const keyEl = document.createElement('span');
576281
- keyEl.style.cssText = 'color:#b2920a;min-width:60px;flex-shrink:0';
577391
+ keyEl.style.cssText = 'color:var(--color-brand);min-width:60px;flex-shrink:0';
576282
577392
  keyEl.textContent = k;
576283
577393
  row.appendChild(keyEl);
576284
577394
 
576285
577395
  const valWrap = document.createElement('div');
576286
- valWrap.style.cssText = 'flex:1;min-width:0;color:#b0b0b0';
576287
- appendExpandableContent(valWrap, vs, { truncateAt: 500, baseStyle: 'color:#b0b0b0;' });
577396
+ valWrap.style.cssText = 'flex:1;min-width:0;color:var(--color-fg)';
577397
+ appendExpandableContent(valWrap, vs, { truncateAt: 500, baseStyle: 'color:var(--color-fg);' });
576288
577398
  row.appendChild(valWrap);
576289
577399
 
576290
577400
  argsDiv.appendChild(row);
@@ -576301,8 +577411,8 @@ function renderToolCallEvent(parent, chunkLike) {
576301
577411
  function renderToolResultEvent(parent, chunkLike) {
576302
577412
  const resultEl = document.createElement('div');
576303
577413
  const errStyle = chunkLike.success === false
576304
- ? 'background:#2a1e1e;border-left:2px solid #b25f5f;color:#b25f5f;'
576305
- : 'background:#1e1e22;color:#888;';
577414
+ ? 'background:#2a1e1e;border-left:2px solid var(--color-error);color:var(--color-error);'
577415
+ : 'background:var(--color-bg-elevated);color:var(--color-fg-muted);';
576306
577416
  resultEl.style.cssText = errStyle + 'padding:4px 8px 4px 18px;margin:0 0 2px 0;font-size:0.65rem';
576307
577417
  appendExpandableContent(resultEl, chunkLike.output || '', { truncateAt: 150, baseStyle: 'color:inherit;' });
576308
577418
  parent.appendChild(resultEl);
@@ -576315,9 +577425,9 @@ function renderToolResultEvent(parent, chunkLike) {
576315
577425
  // active assistant turn's tool dropdowns.
576316
577426
  function renderCheckinEvent(parent, content) {
576317
577427
  const el = document.createElement('div');
576318
- el.style.cssText = 'background:#0a2a2e;border-left:3px solid #2db4b4;color:#7fdada;padding:6px 10px 6px 14px;margin:3px 0;font-size:0.7rem;font-family:inherit';
577428
+ el.style.cssText = 'background:var(--color-info);border-left:3px solid var(--color-info);color:#7fdada;padding:6px 10px 6px 14px;margin:3px 0;font-size:0.7rem;font-family:inherit';
576319
577429
  const label = document.createElement('div');
576320
- label.style.cssText = 'color:#2db4b4;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px';
577430
+ label.style.cssText = 'color:var(--color-info);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px';
576321
577431
  label.textContent = '\\u25B8 user check-in';
576322
577432
  el.appendChild(label);
576323
577433
  const body = document.createElement('div');
@@ -576334,9 +577444,9 @@ function renderCheckinEvent(parent, content) {
576334
577444
  // stack of dropdowns.
576335
577445
  function renderTriageResponseEvent(parent, ack, steering) {
576336
577446
  const el = document.createElement('div');
576337
- el.style.cssText = 'background:#0a2628;border-left:3px solid #2db4b4;color:#7fdada;padding:6px 10px 6px 14px;margin:3px 0;font-size:0.7rem';
577447
+ el.style.cssText = 'background:#0a2628;border-left:3px solid var(--color-info);color:#7fdada;padding:6px 10px 6px 14px;margin:3px 0;font-size:0.7rem';
576338
577448
  const label = document.createElement('div');
576339
- label.style.cssText = 'color:#2db4b4;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px';
577449
+ label.style.cssText = 'color:var(--color-info);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px';
576340
577450
  label.textContent = '\\u25B8 triage \\u2192 main agent';
576341
577451
  el.appendChild(label);
576342
577452
  if (ack) {
@@ -576408,7 +577518,7 @@ async function sendCheckin() {
576408
577518
  if (!r.ok) {
576409
577519
  if (toolsContainer) {
576410
577520
  const errEl = document.createElement('div');
576411
- errEl.style.cssText = 'color:#b25f5f;font-size:0.6rem;padding:2px 14px';
577521
+ errEl.style.cssText = 'color:var(--color-error);font-size:0.6rem;padding:2px 14px';
576412
577522
  errEl.textContent = 'Check-in failed: HTTP ' + r.status;
576413
577523
  toolsContainer.appendChild(errEl);
576414
577524
  }
@@ -576422,7 +577532,7 @@ async function sendCheckin() {
576422
577532
  } catch (err) {
576423
577533
  if (toolsContainer) {
576424
577534
  const errEl = document.createElement('div');
576425
- errEl.style.cssText = 'color:#b25f5f;font-size:0.6rem;padding:2px 14px';
577535
+ errEl.style.cssText = 'color:var(--color-error);font-size:0.6rem;padding:2px 14px';
576426
577536
  errEl.textContent = 'Check-in network error: ' + (err && err.message || String(err));
576427
577537
  toolsContainer.appendChild(errEl);
576428
577538
  }
@@ -576465,6 +577575,11 @@ async function sendMessage() {
576465
577575
  const contentDiv = document.createElement('div');
576466
577576
  msgDiv.appendChild(contentDiv);
576467
577577
 
577578
+ // OWUI-3: streaming indicator — pulsing dot + 'thinking' label
577579
+ // Stays visible until the first content chunk arrives, then is
577580
+ // replaced by the running content. Hidden in the finally{} block too.
577581
+ showStreamingIndicator(msgDiv, 'thinking');
577582
+
576468
577583
  try {
576469
577584
  // Prepend context files as a FILES block so the agent can read them
576470
577585
  // without having to guess paths. The user picked these via right-click
@@ -576568,9 +577683,9 @@ async function sendMessage() {
576568
577683
  if (chunk.type === 'tool_call') {
576569
577684
  chatTools.push(chunk);
576570
577685
  const details = document.createElement('details');
576571
- details.style.cssText = 'background:#1e1e22;border-left:2px solid #b2920a;margin:2px 0;font-size:0.7rem';
577686
+ details.style.cssText = 'background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);margin:2px 0;font-size:0.7rem';
576572
577687
  const summary = document.createElement('summary');
576573
- summary.style.cssText = 'padding:4px 8px;color:#b2920a;cursor:pointer';
577688
+ summary.style.cssText = 'padding:4px 8px;color:var(--color-brand);cursor:pointer';
576574
577689
 
576575
577690
  // Build a compact one-line label so the user sees what the tool
576576
577691
  // is actually doing without expanding. todo_write gets a special
@@ -576628,18 +577743,18 @@ async function sendMessage() {
576628
577743
  // todo_write specifically render a checklist instead of a blob.
576629
577744
  if (chunk.args && typeof chunk.args === 'object') {
576630
577745
  const argsDiv = document.createElement('div');
576631
- argsDiv.style.cssText = 'padding:4px 8px 6px 16px;color:#888;font-size:0.65rem;border-top:1px solid #2a2a30';
577746
+ argsDiv.style.cssText = 'padding:4px 8px 6px 16px;color:var(--color-fg-muted);font-size:0.65rem;border-top:1px solid var(--color-bg-input)';
576632
577747
  if (toolName === 'todo_write' && Array.isArray(a.todos)) {
576633
577748
  for (const t of a.todos) {
576634
577749
  if (!t || typeof t !== 'object') continue;
576635
577750
  const row = document.createElement('div');
576636
577751
  row.style.cssText = 'padding:2px 0';
576637
577752
  let mark = '○';
576638
- let color = '#666';
577753
+ let color = 'var(--color-fg-subtle)';
576639
577754
  if (t.status === 'completed') { mark = '◉'; color = '#4a7a4a'; }
576640
- else if (t.status === 'in_progress') { mark = '◐'; color = '#b2920a'; }
576641
- else if (t.status === 'blocked') { mark = '◍'; color = '#b25f5f'; }
576642
- row.innerHTML = '<span style="color:' + color + '">' + mark + '</span> <span style="color:#b0b0b0">' + escHtml(String(t.content || '').slice(0, 300)) + '</span>' + (t.blocker ? ' <span style="color:#b25f5f">(blocked: ' + escHtml(String(t.blocker).slice(0, 100)) + ')</span>' : '');
577755
+ else if (t.status === 'in_progress') { mark = '◐'; color = 'var(--color-brand)'; }
577756
+ else if (t.status === 'blocked') { mark = '◍'; color = 'var(--color-error)'; }
577757
+ row.innerHTML = '<span style="color:' + color + '">' + mark + '</span> <span style="color:var(--color-fg)">' + escHtml(String(t.content || '').slice(0, 300)) + '</span>' + (t.blocker ? ' <span style="color:var(--color-error)">(blocked: ' + escHtml(String(t.blocker).slice(0, 100)) + ')</span>' : '');
576643
577758
  argsDiv.appendChild(row);
576644
577759
  }
576645
577760
  } else {
@@ -576654,15 +577769,15 @@ async function sendMessage() {
576654
577769
  else { try { vs = JSON.stringify(v, null, 2); } catch { vs = '[object]'; } }
576655
577770
 
576656
577771
  const keyEl = document.createElement('span');
576657
- keyEl.style.cssText = 'color:#b2920a;min-width:60px;flex-shrink:0';
577772
+ keyEl.style.cssText = 'color:var(--color-brand);min-width:60px;flex-shrink:0';
576658
577773
  keyEl.textContent = k;
576659
577774
  row.appendChild(keyEl);
576660
577775
 
576661
577776
  const valWrap = document.createElement('div');
576662
- valWrap.style.cssText = 'flex:1;min-width:0;color:#b0b0b0';
577777
+ valWrap.style.cssText = 'flex:1;min-width:0;color:var(--color-fg)';
576663
577778
  // Use the show-more helper so long values are collapsed
576664
577779
  // by default and the user can expand inline.
576665
- appendExpandableContent(valWrap, vs, { truncateAt: 500, baseStyle: 'color:#b0b0b0;' });
577780
+ appendExpandableContent(valWrap, vs, { truncateAt: 500, baseStyle: 'color:var(--color-fg);' });
576666
577781
  row.appendChild(valWrap);
576667
577782
 
576668
577783
  argsDiv.appendChild(row);
@@ -576680,8 +577795,8 @@ async function sendMessage() {
576680
577795
  // 150 chars. The button sits underneath the result block.
576681
577796
  if (chunk.type === 'tool_result') {
576682
577797
  const resultEl = document.createElement('div');
576683
- resultEl.style.cssText = 'background:#1e1e22;padding:4px 8px 4px 18px;margin:0 0 2px 0;color:#888;font-size:0.65rem';
576684
- appendExpandableContent(resultEl, chunk.output || '', { truncateAt: 150, baseStyle: 'color:#888;' });
577798
+ resultEl.style.cssText = 'background:var(--color-bg-elevated);padding:4px 8px 4px 18px;margin:0 0 2px 0;color:var(--color-fg-muted);font-size:0.65rem';
577799
+ appendExpandableContent(resultEl, chunk.output || '', { truncateAt: 150, baseStyle: 'color:var(--color-fg-muted);' });
576685
577800
  toolsContainer.appendChild(resultEl);
576686
577801
  continue;
576687
577802
  }
@@ -576695,8 +577810,15 @@ async function sendMessage() {
576695
577810
  // Content delta
576696
577811
  const delta = chunk.choices?.[0]?.delta?.content || '';
576697
577812
  if (delta) {
577813
+ // OWUI-3: switch indicator from 'thinking' -> 'writing' on
577814
+ // first delta, and remove on subsequent deltas (it lives at
577815
+ // the bottom of the message, below content).
577816
+ if (!fullContent) {
577817
+ showStreamingIndicator(msgDiv, 'writing');
577818
+ }
576698
577819
  fullContent += delta;
576699
577820
  contentDiv.innerHTML = renderMarkdown(fullContent);
577821
+ try { _highlightCodeBlocks(contentDiv); } catch {}
576700
577822
  maybeAutoScroll();
576701
577823
  }
576702
577824
  } catch {}
@@ -576718,11 +577840,14 @@ async function sendMessage() {
576718
577840
 
576719
577841
  // Final render: content + collapsible tools + metadata
576720
577842
  contentDiv.innerHTML = renderMarkdown(fullContent);
577843
+ // OWUI-3: streaming complete — drop the pulsing dot.
577844
+ hideStreamingIndicator(msgDiv);
577845
+ try { _highlightCodeBlocks(contentDiv); } catch {}
576721
577846
 
576722
577847
  // Metadata bar: turns, tokens, duration (always shown, compact)
576723
577848
  if (metaInfo) {
576724
577849
  const metaBar = document.createElement('div');
576725
- metaBar.style.cssText = 'margin:6px 0 2px;padding:4px 8px;background:#1e1e22;border-radius:3px;font-size:0.6rem;color:#555;display:flex;gap:12px;flex-wrap:wrap';
577850
+ metaBar.style.cssText = 'margin:6px 0 2px;padding:4px 8px;background:var(--color-bg-elevated);border-radius:3px;font-size:0.6rem;color:var(--color-fg-faint);display:flex;gap:12px;flex-wrap:wrap';
576726
577851
  const parts = [];
576727
577852
  if (metaInfo.turns) parts.push(metaInfo.turns + ' turn' + (metaInfo.turns > 1 ? 's' : ''));
576728
577853
  if (metaInfo.toolCalls) parts.push(metaInfo.toolCalls + ' tool call' + (metaInfo.toolCalls > 1 ? 's' : ''));
@@ -576735,9 +577860,9 @@ async function sendMessage() {
576735
577860
  // Collapse tool calls into a dropdown
576736
577861
  if (chatTools.length > 0) {
576737
577862
  const details = document.createElement('details');
576738
- details.style.cssText = 'margin:2px 0;font-size:0.65rem;color:#555';
577863
+ details.style.cssText = 'margin:2px 0;font-size:0.65rem;color:var(--color-fg-faint)';
576739
577864
  const summary = document.createElement('summary');
576740
- summary.style.cssText = 'cursor:pointer;color:#888;font-size:0.6rem';
577865
+ summary.style.cssText = 'cursor:pointer;color:var(--color-fg-muted);font-size:0.6rem';
576741
577866
  summary.textContent = 'show ' + chatTools.length + ' tool call' + (chatTools.length > 1 ? 's' : '');
576742
577867
  details.appendChild(summary);
576743
577868
  while (toolsContainer.firstChild) details.appendChild(toolsContainer.firstChild);
@@ -576756,13 +577881,17 @@ async function sendMessage() {
576756
577881
  // Match the red left-border styling used by failed tool_result
576757
577882
  // and the stop-button. Sits inside the assistant bubble so it
576758
577883
  // visually parents to the same turn the user initiated.
576759
- msgDiv.innerHTML = '<div style="background:#2a1e1e;border-left:3px solid #ff4444;color:#ff4444;padding:6px 10px 6px 14px;margin:3px 0;font-family:inherit"><div style="color:#ff4444;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div><div style="color:#ff7777;white-space:pre-wrap;word-break:break-word">' + escHtml(err.message) + '</div></div>';
577884
+ msgDiv.innerHTML = '<div style="background:#2a1e1e;border-left:3px solid var(--color-error);color:var(--color-error);padding:6px 10px 6px 14px;margin:3px 0;font-family:inherit"><div style="color:var(--color-error);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div><div style="color:#ff7777;white-space:pre-wrap;word-break:break-word">' + escHtml(err.message) + '</div></div>';
576760
577885
  }
576761
577886
 
576762
577887
  streaming = false;
576763
577888
  chatAbortController = null;
576764
577889
  document.getElementById('send-btn').style.display = 'inline-block';
576765
577890
  document.getElementById('stop-btn').style.display = 'none';
577891
+ // OWUI-3: belt-and-braces — make sure the indicator is gone if we
577892
+ // reach this finally{} via an error path before the success branch
577893
+ // had a chance to call hideStreamingIndicator.
577894
+ try { hideStreamingIndicator(msgDiv); } catch {}
576766
577895
  maybeAutoScroll();
576767
577896
  }
576768
577897
 
@@ -576805,9 +577934,13 @@ function switchTab(tab) {
576805
577934
  panel.style.display = (tab === 'chat' || tab === 'agent') ? 'flex' : 'block';
576806
577935
  }
576807
577936
  document.getElementById('footer').style.display = tab === 'chat' ? 'flex' : 'none';
576808
- document.querySelectorAll('.tab').forEach(t => { t.style.borderBottomColor = 'transparent'; t.style.color = '#555'; });
577937
+ document.querySelectorAll('.tab').forEach(t => { t.style.borderBottomColor = 'transparent'; t.style.color = 'var(--color-fg-faint)'; });
576809
577938
  const active = document.getElementById('tab-' + tab);
576810
- if (active) { active.style.borderBottomColor = '#b2920a'; active.style.color = '#b2920a'; }
577939
+ if (active) { active.style.borderBottomColor = 'var(--color-brand)'; active.style.color = 'var(--color-brand)'; }
577940
+ // OWUI-2: mirror active state on the sidebar nav
577941
+ document.querySelectorAll('#oa-sidebar .sb-nav').forEach(b => {
577942
+ b.classList.toggle('active', b.getAttribute('data-tab') === tab);
577943
+ });
576811
577944
  if (tab === 'jobs') loadJobs();
576812
577945
  if (tab === 'agent') {
576813
577946
  loadProfiles();
@@ -576832,7 +577965,7 @@ async function loadProjects() {
576832
577965
  const resp = await fetch('/v1/projects', { headers: headers() });
576833
577966
  if (!resp.ok) {
576834
577967
  document.getElementById('projects-list').innerHTML =
576835
- '<div style="color:#ff6b6b">Failed to load projects: HTTP ' + resp.status + '</div>';
577968
+ '<div style="color:var(--color-error)">Failed to load projects: HTTP ' + resp.status + '</div>';
576836
577969
  return;
576837
577970
  }
576838
577971
  const data = await resp.json();
@@ -576856,8 +577989,8 @@ async function loadProjects() {
576856
577989
  }
576857
577990
  if (projects.length === 0) {
576858
577991
  document.getElementById('projects-list').innerHTML =
576859
- '<div style="color:#666;padding:20px;text-align:center;border:1px dashed #3a3a42;border-radius:4px">' +
576860
- 'No projects registered yet. Every time you start <code style="color:#b2920a">oa</code> in a folder, it will appear here.' +
577992
+ '<div style="color:var(--color-fg-subtle);padding:20px;text-align:center;border:1px dashed var(--color-border);border-radius:4px">' +
577993
+ 'No projects registered yet. Every time you start <code style="color:var(--color-brand)">oa</code> in a folder, it will appear here.' +
576861
577994
  '</div>';
576862
577995
  return;
576863
577996
  }
@@ -576871,17 +578004,17 @@ async function loadProjects() {
576871
578004
  };
576872
578005
  const isCur = (p) => current && current.root === p.root;
576873
578006
  const rows = projects.map(p => {
576874
- const accent = isCur(p) ? '#b2920a' : '#3a3a42';
578007
+ const accent = isCur(p) ? 'var(--color-brand)' : 'var(--color-border)';
576875
578008
  const label = (p.name || p.root.split('/').pop() || p.root)
576876
578009
  .replace(/</g, '&lt;').replace(/>/g, '&gt;');
576877
578010
  const rootDisplay = p.root.replace(/</g, '&lt;').replace(/>/g, '&gt;');
576878
- const pidInfo = p.pid ? ' <span style="color:#555">pid ' + p.pid + '</span>' : '';
576879
- return '<div class="proj-row" data-root="' + encodeURIComponent(p.root) + '" style="background:#1a1a1e;border-left:2px solid ' + accent + ';padding:8px 12px;margin:6px 0;cursor:pointer;display:flex;justify-content:space-between;align-items:center">' +
578011
+ const pidInfo = p.pid ? ' <span style="color:var(--color-fg-faint)">pid ' + p.pid + '</span>' : '';
578012
+ return '<div class="proj-row" data-root="' + encodeURIComponent(p.root) + '" style="background:var(--color-bg);border-left:2px solid ' + accent + ';padding:8px 12px;margin:6px 0;cursor:pointer;display:flex;justify-content:space-between;align-items:center">' +
576880
578013
  '<div>' +
576881
- '<div style="color:' + (isCur(p) ? '#b2920a' : '#b0b0b0') + ';font-weight:' + (isCur(p) ? '600' : '400') + '">' + label + (isCur(p) ? ' <span style="color:#4ec94e;font-size:0.6rem">(active)</span>' : '') + '</div>' +
576882
- '<div style="color:#666;font-size:0.62rem">' + rootDisplay + '</div>' +
578014
+ '<div style="color:' + (isCur(p) ? 'var(--color-brand)' : 'var(--color-fg)') + ';font-weight:' + (isCur(p) ? '600' : '400') + '">' + label + (isCur(p) ? ' <span style="color:var(--color-success);font-size:0.6rem">(active)</span>' : '') + '</div>' +
578015
+ '<div style="color:var(--color-fg-subtle);font-size:0.62rem">' + rootDisplay + '</div>' +
576883
578016
  '</div>' +
576884
- '<div style="color:#555;font-size:0.62rem;text-align:right">' +
578017
+ '<div style="color:var(--color-fg-faint);font-size:0.62rem;text-align:right">' +
576885
578018
  fmtAgo(p.lastSeen) + pidInfo +
576886
578019
  '</div>' +
576887
578020
  '</div>';
@@ -576917,7 +578050,7 @@ async function loadProjects() {
576917
578050
  });
576918
578051
  } catch (e) {
576919
578052
  document.getElementById('projects-list').innerHTML =
576920
- '<div style="color:#ff6b6b">Failed to load projects: ' + (e && e.message ? e.message : String(e)) + '</div>';
578053
+ '<div style="color:var(--color-error)">Failed to load projects: ' + (e && e.message ? e.message : String(e)) + '</div>';
576921
578054
  }
576922
578055
  }
576923
578056
  window.loadProjects = loadProjects;
@@ -576938,8 +578071,8 @@ async function loadConfig() {
576938
578071
  document.getElementById('config-content').innerHTML =
576939
578072
  '<table style="width:100%">' +
576940
578073
  Object.entries(c).map(([k,v]) =>
576941
- '<tr style="border-bottom:1px solid #2a2a30"><td style="padding:4px;color:#888">' + k + '</td>' +
576942
- '<td style="padding:4px;color:#b0b0b0">' + (v === '[redacted]' ? '<span style="color:#555">[redacted]</span>' : String(v)) + '</td></tr>'
578074
+ '<tr style="border-bottom:1px solid var(--color-bg-input)"><td style="padding:4px;color:var(--color-fg-muted)">' + k + '</td>' +
578075
+ '<td style="padding:4px;color:var(--color-fg)">' + (v === '[redacted]' ? '<span style="color:var(--color-fg-faint)">[redacted]</span>' : String(v)) + '</td></tr>'
576943
578076
  ).join('') + '</table>';
576944
578077
  document.getElementById('config-endpoint').textContent = ep.url + ' (' + (ep.backendType || 'unknown') + ')';
576945
578078
  // Populate model switcher
@@ -576958,11 +578091,11 @@ async function loadConfig() {
576958
578091
  const r = await fetch('/v1/profiles', { headers: headers() });
576959
578092
  const d = await r.json();
576960
578093
  document.getElementById('config-profiles').innerHTML = (d.profiles || []).map(p =>
576961
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:6px 10px;margin:4px 0">' +
576962
- '<span style="color:#b2920a">' + p.name + '</span>' +
576963
- (p.encrypted ? ' <span style="color:#555;font-size:0.6rem">(encrypted)</span>' : '') +
576964
- ' <span style="color:#555;font-size:0.6rem">' + (p.source || '') + '</span></div>'
576965
- ).join('') || '<span style="color:#555">No profiles</span>';
578094
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:6px 10px;margin:4px 0">' +
578095
+ '<span style="color:var(--color-brand)">' + p.name + '</span>' +
578096
+ (p.encrypted ? ' <span style="color:var(--color-fg-faint);font-size:0.6rem">(encrypted)</span>' : '') +
578097
+ ' <span style="color:var(--color-fg-faint);font-size:0.6rem">' + (p.source || '') + '</span></div>'
578098
+ ).join('') || '<span style="color:var(--color-fg-faint)">No profiles</span>';
576966
578099
  } catch {}
576967
578100
  }
576968
578101
 
@@ -577015,17 +578148,17 @@ async function loadActivity() {
577015
578148
  const r = await fetch('/v1/audit?limit=50', { headers: headers() });
577016
578149
  const d = await r.json();
577017
578150
  const feed = document.getElementById('activity-feed');
577018
- if (!d.records?.length) { feed.innerHTML = '<span style="color:#555">No activity yet</span>'; return; }
578151
+ if (!d.records?.length) { feed.innerHTML = '<span style="color:var(--color-fg-faint)">No activity yet</span>'; return; }
577019
578152
  feed.innerHTML = d.records.map(r => {
577020
578153
  const time = r.ts?.split('T')[1]?.slice(0,8) || '';
577021
- const color = r.status >= 400 ? '#ff4444' : r.status >= 300 ? '#b2920a' : '#4ec94e';
577022
- return '<div style="padding:3px 0;border-bottom:1px solid #1e1e22">' +
577023
- '<span style="color:#555">' + time + '</span> ' +
578154
+ const color = r.status >= 400 ? 'var(--color-error)' : r.status >= 300 ? 'var(--color-brand)' : 'var(--color-success)';
578155
+ return '<div style="padding:3px 0;border-bottom:1px solid var(--color-bg-elevated)">' +
578156
+ '<span style="color:var(--color-fg-faint)">' + time + '</span> ' +
577024
578157
  '<span style="color:' + color + '">' + r.status + '</span> ' +
577025
- '<span style="color:#888">' + r.method + '</span> ' +
577026
- '<span style="color:#b0b0b0">' + r.path + '</span> ' +
577027
- '<span style="color:#555">' + r.latencyMs + 'ms</span> ' +
577028
- '<span style="color:#555;font-size:0.6rem">' + (r.user || '') + '</span></div>';
578158
+ '<span style="color:var(--color-fg-muted)">' + r.method + '</span> ' +
578159
+ '<span style="color:var(--color-fg)">' + r.path + '</span> ' +
578160
+ '<span style="color:var(--color-fg-faint)">' + r.latencyMs + 'ms</span> ' +
578161
+ '<span style="color:var(--color-fg-faint);font-size:0.6rem">' + (r.user || '') + '</span></div>';
577029
578162
  }).join('');
577030
578163
  } catch {}
577031
578164
  }
@@ -577039,15 +578172,15 @@ async function loadDaemons() {
577039
578172
  const d = await r.json();
577040
578173
  const el = document.getElementById('dashboard-daemons');
577041
578174
  if (!d.runs?.length) {
577042
- el.innerHTML = '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;color:#555;font-size:0.7rem">No active processes</div>';
578175
+ 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>';
577043
578176
  return;
577044
578177
  }
577045
- el.innerHTML = '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Active Processes</h3>' +
578178
+ el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Active Processes</h3>' +
577046
578179
  d.runs.map(j =>
577047
- '<div style="background:#1e1e22;border-left:2px solid #b2920a;padding:6px 10px;margin:4px 0;font-size:0.72rem">' +
577048
- '<span style="color:#b2920a">' + (j.id||'').slice(0,12) + '</span> ' +
577049
- '<span style="color:#4ec94e">running</span> ' +
577050
- '<span style="color:#888">' + (j.task||'').slice(0,50) + '</span></div>'
578180
+ '<div style="background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);padding:6px 10px;margin:4px 0;font-size:0.72rem">' +
578181
+ '<span style="color:var(--color-brand)">' + (j.id||'').slice(0,12) + '</span> ' +
578182
+ '<span style="color:var(--color-success)">running</span> ' +
578183
+ '<span style="color:var(--color-fg-muted)">' + (j.task||'').slice(0,50) + '</span></div>'
577051
578184
  ).join('');
577052
578185
  } catch {}
577053
578186
  }
@@ -577061,33 +578194,33 @@ async function loadScheduled() {
577061
578194
  if (!el) return;
577062
578195
  const tasks = Array.isArray(d.tasks) ? d.tasks : [];
577063
578196
  if (tasks.length === 0) {
577064
- el.innerHTML = '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;color:#555;font-size:0.7rem">No scheduled tasks found</div>';
578197
+ 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 scheduled tasks found</div>';
577065
578198
  return;
577066
578199
  }
577067
578200
  const rows = tasks.map(t => {
577068
578201
  const enabled = !!t.enabled;
577069
578202
  const btn = enabled
577070
- ? '<button onclick="toggleScheduled(\\'' + t.id + '\\',false)" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>'
577071
- : '<button onclick="toggleScheduled(\\'' + t.id + '\\',true)" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable</button>';
577072
- const color = enabled ? '#4ec94e' : '#5a2a2a';
578203
+ ? '<button onclick="toggleScheduled(\\'' + t.id + '\\',false)" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>'
578204
+ : '<button onclick="toggleScheduled(\\'' + t.id + '\\',true)" style="background:var(--color-bg-input);border:1px solid #2a3a2a;color:var(--color-success);padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable</button>';
578205
+ const color = enabled ? 'var(--color-success)' : 'var(--color-error)';
577073
578206
  const procInfo = t.procs && t.procs.length ? (' (' + t.procs.length + ' proc)') : '';
577074
578207
  const up = t.procs && t.procs[0] && t.procs[0].uptime_s ? (' • up ' + Math.max(1, Math.round(t.procs[0].uptime_s/60)) + 'm') : '';
577075
- const killBtn = '<button onclick="killScheduledTask(\\'' + t.id + '\\')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill</button>';
577076
- const row = '<div style="background:#1e1e22;border-left:2px solid ' + color + ';padding:6px 10px;margin:4px 0;font-size:0.72rem">'
577077
- + '<div style="color:#b0b0b0">' + (t.name || '(task)') + ' <span style="color:#555">' + (t.schedule || '') + '</span>' + procInfo + up + '</div>'
577078
- + '<div style="color:#555;font-size:0.6rem">' + t.file + '#' + t.index + '</div>'
578208
+ const killBtn = '<button onclick="killScheduledTask(\\'' + t.id + '\\')" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill</button>';
578209
+ const row = '<div style="background:var(--color-bg-elevated);border-left:2px solid ' + color + ';padding:6px 10px;margin:4px 0;font-size:0.72rem">'
578210
+ + '<div style="color:var(--color-fg)">' + (t.name || '(task)') + ' <span style="color:var(--color-fg-faint)">' + (t.schedule || '') + '</span>' + procInfo + up + '</div>'
578211
+ + '<div style="color:var(--color-fg-faint);font-size:0.6rem">' + t.file + '#' + t.index + '</div>'
577079
578212
  + '<div style="margin-top:4px;display:flex;gap:8px">' + btn + killBtn + '</div>'
577080
578213
  + '</div>';
577081
578214
  return row;
577082
578215
  }).join('');
577083
- el.innerHTML = '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Scheduled Tasks</h3>' + rows
578216
+ el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Scheduled Tasks</h3>' + rows
577084
578217
  + '<div style="margin-top:6px;display:flex;gap:8px">'
577085
- + '<button onclick="disableAllScheduled()" title="Disable all scheduled tasks" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable all</button>'
577086
- + '<button onclick="enableAllScheduled()" title="Enable all scheduled tasks" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable all</button>'
577087
- + '<button onclick="killScheduled()" title="Kill OA scheduler processes" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill OA schedulers</button>'
577088
- + '<button onclick="adoptScheduled()" title="Adopt legacy cron jobs into tasks.json" style="background:#2a2a30;border:1px solid #2a2a5a;color:#6e7bd9;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">adopt</button>'
577089
- + '<button onclick="fixupScheduled()" title="Rewrite cron entries to canonical form" style="background:#2a2a30;border:1px solid #5a2a2a;color:#dba15f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">fixup</button>'
577090
- + '<button onclick="migrateScheduled()" title="Migrate cron → systemd user timers" style="background:#2a2a30;border:1px solid #5a2a2a;color:#a1db5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">migrate</button>'
578218
+ + '<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>'
578219
+ + '<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>'
578220
+ + '<button onclick="killScheduled()" title="Kill OA scheduler processes" 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">kill OA schedulers</button>'
578221
+ + '<button onclick="adoptScheduled()" title="Adopt legacy cron jobs into tasks.json" style="background:var(--color-bg-input);border:1px solid #2a2a5a;color:#6e7bd9;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">adopt</button>'
578222
+ + '<button onclick="fixupScheduled()" title="Rewrite cron entries to canonical form" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:#dba15f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">fixup</button>'
578223
+ + '<button onclick="migrateScheduled()" title="Migrate cron → systemd user timers" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:#a1db5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">migrate</button>'
577091
578224
  + '</div>';
577092
578225
  } catch {}
577093
578226
  }
@@ -577234,15 +578367,15 @@ async function loadServices() {
577234
578367
  const svcs = Array.isArray(d.services) ? d.services : [];
577235
578368
  if (!svcs.length) { el.innerHTML = ''; return; }
577236
578369
  const rows = svcs.map(s => {
577237
- const stopBtn = '<button onclick="svcAction(\\'' + s.name + '\\',\\'stop\\')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">stop</button>';
577238
- const disBtn = '<button onclick="svcAction(\\'' + s.name + '\\',\\'disable\\')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>';
577239
- return '<div style="background:#1e1e22;border-left:2px solid #3a3a42;padding:6px 10px;margin:4px 0;font-size:0.72rem">'
577240
- + '<div style="color:#b0b0b0">' + s.name + '</div>'
577241
- + '<div style="color:#555;font-size:0.6rem">enabled: ' + s.enabled + ' • active: ' + s.active + '</div>'
578370
+ const stopBtn = '<button onclick="svcAction(\\'' + s.name + '\\',\\'stop\\')" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">stop</button>';
578371
+ const disBtn = '<button onclick="svcAction(\\'' + s.name + '\\',\\'disable\\')" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>';
578372
+ return '<div style="background:var(--color-bg-elevated);border-left:2px solid var(--color-border);padding:6px 10px;margin:4px 0;font-size:0.72rem">'
578373
+ + '<div style="color:var(--color-fg)">' + s.name + '</div>'
578374
+ + '<div style="color:var(--color-fg-faint);font-size:0.6rem">enabled: ' + s.enabled + ' • active: ' + s.active + '</div>'
577242
578375
  + '<div style="margin-top:4px;display:flex;gap:8px">' + stopBtn + disBtn + '</div>'
577243
578376
  + '</div>';
577244
578377
  }).join('');
577245
- el.innerHTML = '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
578378
+ el.innerHTML = '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
577246
578379
  } catch {}
577247
578380
  }
577248
578381
 
@@ -577334,7 +578467,7 @@ async function submitAgentTask() {
577334
578467
  div.style.padding = '2px 0';
577335
578468
  if (evt.type === 'run_started') {
577336
578469
  currentRunId = evt.run_id;
577337
- div.innerHTML = '<span style="color:#b2920a">Task started</span> — run_id: ' + evt.run_id;
578470
+ div.innerHTML = '<span style="color:var(--color-brand)">Task started</span> — run_id: ' + evt.run_id;
577338
578471
  // WO-TASK-02: Persist this run into the agent runs storage so
577339
578472
  // it appears in the #agent-session-select dropdown immediately.
577340
578473
  try {
@@ -577350,12 +578483,12 @@ async function submitAgentTask() {
577350
578483
  currentAgentRunId = evt.run_id;
577351
578484
  } catch {}
577352
578485
  } else if (evt.type === 'run_completed') {
577353
- div.innerHTML = '<span style="color:' + (evt.exit_code === 0 ? '#4ec94e' : '#ff4444') + '">Task ' + (evt.exit_code === 0 ? 'completed' : 'failed') + '</span> — exit: ' + evt.exit_code;
578486
+ div.innerHTML = '<span style="color:' + (evt.exit_code === 0 ? 'var(--color-success)' : 'var(--color-error)') + '">Task ' + (evt.exit_code === 0 ? 'completed' : 'failed') + '</span> — exit: ' + evt.exit_code;
577354
578487
  document.getElementById('agent-submit').style.display = 'inline-block';
577355
578488
  document.getElementById('agent-abort').style.display = 'none';
577356
578489
  currentRunId = null;
577357
578490
  } else if (evt.type === 'stdout') {
577358
- div.style.color = '#888';
578491
+ div.style.color = 'var(--color-fg-muted)';
577359
578492
  div.style.fontFamily = 'inherit';
577360
578493
  div.textContent = evt.data?.slice?.(0, 200) || '';
577361
578494
  }
@@ -577365,7 +578498,7 @@ async function submitAgentTask() {
577365
578498
  }
577366
578499
  }
577367
578500
  } catch (err) {
577368
- eventsDiv.innerHTML += '<div style="background:#2a1e1e;border-left:3px solid #ff4444;color:#ff7777;padding:6px 10px 6px 14px;margin:3px 0"><div style="color:#ff4444;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div>' + escHtml(err.message) + '</div>';
578501
+ eventsDiv.innerHTML += '<div style="background:#2a1e1e;border-left:3px solid var(--color-error);color:#ff7777;padding:6px 10px 6px 14px;margin:3px 0"><div style="color:var(--color-error);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div>' + escHtml(err.message) + '</div>';
577369
578502
  }
577370
578503
  document.getElementById('agent-submit').style.display = 'inline-block';
577371
578504
  document.getElementById('agent-abort').style.display = 'none';
@@ -577382,15 +578515,15 @@ async function loadDashboard() {
577382
578515
  const r = await fetch('/health', { headers: headers() });
577383
578516
  const d = await r.json();
577384
578517
  document.getElementById('dashboard-health').innerHTML =
577385
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577386
- '<div style="color:#555;font-size:0.6rem">STATUS</div>' +
577387
- '<div style="color:#4ec94e;font-size:0.8rem">' + d.status + '</div></div>' +
577388
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577389
- '<div style="color:#555;font-size:0.6rem">UPTIME</div>' +
577390
- '<div style="color:#b2920a;font-size:0.8rem">' + Math.floor(d.uptime_s/60) + 'm</div></div>' +
577391
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577392
- '<div style="color:#555;font-size:0.6rem">VERSION</div>' +
577393
- '<div style="color:#b0b0b0;font-size:0.8rem">' + d.version + '</div></div>';
578518
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578519
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">STATUS</div>' +
578520
+ '<div style="color:var(--color-success);font-size:0.8rem">' + d.status + '</div></div>' +
578521
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578522
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">UPTIME</div>' +
578523
+ '<div style="color:var(--color-brand);font-size:0.8rem">' + Math.floor(d.uptime_s/60) + 'm</div></div>' +
578524
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578525
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">VERSION</div>' +
578526
+ '<div style="color:var(--color-fg);font-size:0.8rem">' + d.version + '</div></div>';
577394
578527
  } catch {}
577395
578528
  // System info + model recommendations
577396
578529
  try {
@@ -577399,15 +578532,15 @@ async function loadDashboard() {
577399
578532
  const gpuHtml = (sys.gpu || []).map(g => g.name + ' (' + g.vram_gb + 'GB)').join(', ') || 'No GPU detected';
577400
578533
  const healthEl = document.getElementById('dashboard-health');
577401
578534
  healthEl.innerHTML +=
577402
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577403
- '<div style="color:#555;font-size:0.6rem">GPU</div>' +
577404
- '<div style="color:#b0b0b0;font-size:0.7rem">' + gpuHtml + '</div></div>' +
577405
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577406
- '<div style="color:#555;font-size:0.6rem">RAM</div>' +
577407
- '<div style="color:#b0b0b0;font-size:0.8rem">' + sys.ram_gb + 'GB</div></div>' +
577408
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
577409
- '<div style="color:#555;font-size:0.6rem">MAX MODEL</div>' +
577410
- '<div style="color:#b2920a;font-size:0.8rem">' + sys.recommended_max_params + '</div></div>';
578535
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578536
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">GPU</div>' +
578537
+ '<div style="color:var(--color-fg);font-size:0.7rem">' + gpuHtml + '</div></div>' +
578538
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578539
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">RAM</div>' +
578540
+ '<div style="color:var(--color-fg);font-size:0.8rem">' + sys.ram_gb + 'GB</div></div>' +
578541
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1;min-width:120px">' +
578542
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">MAX MODEL</div>' +
578543
+ '<div style="color:var(--color-brand);font-size:0.8rem">' + sys.recommended_max_params + '</div></div>';
577411
578544
  // Store for model badges
577412
578545
  window._sysMaxParams = sys.recommended_max_params;
577413
578546
  } catch {}
@@ -577420,30 +578553,30 @@ async function loadDashboard() {
577420
578553
  for (const [label, stats] of Object.entries(ps)) {
577421
578554
  const s = stats;
577422
578555
  providerCards +=
577423
- '<div style="background:#1e1e22;border-left:2px solid #b2920a;padding:8px 12px;margin:4px 0">' +
577424
- '<div style="color:#b2920a;font-size:0.7rem;font-weight:bold">' + label + '</div>' +
578556
+ '<div style="background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);padding:8px 12px;margin:4px 0">' +
578557
+ '<div style="color:var(--color-brand);font-size:0.7rem;font-weight:bold">' + label + '</div>' +
577425
578558
  '<div style="display:flex;gap:16px;margin-top:4px">' +
577426
- '<span style="color:#555;font-size:0.65rem">in: <span style="color:#b0b0b0">' + (s.tokensIn || 0).toLocaleString() + '</span></span>' +
577427
- '<span style="color:#555;font-size:0.65rem">out: <span style="color:#b0b0b0">' + (s.tokensOut || 0).toLocaleString() + '</span></span>' +
577428
- '<span style="color:#555;font-size:0.65rem">reqs: <span style="color:#b0b0b0">' + (s.requests || 0).toLocaleString() + '</span></span>' +
578559
+ '<span style="color:var(--color-fg-faint);font-size:0.65rem">in: <span style="color:var(--color-fg)">' + (s.tokensIn || 0).toLocaleString() + '</span></span>' +
578560
+ '<span style="color:var(--color-fg-faint);font-size:0.65rem">out: <span style="color:var(--color-fg)">' + (s.tokensOut || 0).toLocaleString() + '</span></span>' +
578561
+ '<span style="color:var(--color-fg-faint);font-size:0.65rem">reqs: <span style="color:var(--color-fg)">' + (s.requests || 0).toLocaleString() + '</span></span>' +
577429
578562
  '</div></div>';
577430
578563
  }
577431
578564
  const totalIn = d.persistent?.totalIn || d.totalTokensIn || 0;
577432
578565
  const totalOut = d.persistent?.totalOut || d.totalTokensOut || 0;
577433
578566
  document.getElementById('dashboard-usage').innerHTML =
577434
- '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Token Usage by Provider (persistent)</h3>' +
578567
+ '<h3 style="color:var(--color-brand);font-size:0.7rem;margin-bottom:8px">Token Usage by Provider (persistent)</h3>' +
577435
578568
  '<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px">' +
577436
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1">' +
577437
- '<div style="color:#555;font-size:0.6rem">TOTAL IN</div>' +
577438
- '<div style="color:#b2920a;font-size:0.9rem;font-weight:bold">' + totalIn.toLocaleString() + '</div></div>' +
577439
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1">' +
577440
- '<div style="color:#555;font-size:0.6rem">TOTAL OUT</div>' +
577441
- '<div style="color:#b2920a;font-size:0.9rem;font-weight:bold">' + totalOut.toLocaleString() + '</div></div>' +
577442
- '<div style="background:#1e1e22;border:1px solid #2a2a30;border-radius:3px;padding:8px 12px;flex:1">' +
577443
- '<div style="color:#555;font-size:0.6rem">TOTAL REQUESTS</div>' +
577444
- '<div style="color:#b2920a;font-size:0.9rem;font-weight:bold">' + (d.persistent?.totalRequests || 0).toLocaleString() + '</div></div>' +
578569
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1">' +
578570
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">TOTAL IN</div>' +
578571
+ '<div style="color:var(--color-brand);font-size:0.9rem;font-weight:bold">' + totalIn.toLocaleString() + '</div></div>' +
578572
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1">' +
578573
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">TOTAL OUT</div>' +
578574
+ '<div style="color:var(--color-brand);font-size:0.9rem;font-weight:bold">' + totalOut.toLocaleString() + '</div></div>' +
578575
+ '<div style="background:var(--color-bg-elevated);border:1px solid var(--color-bg-input);border-radius:3px;padding:8px 12px;flex:1">' +
578576
+ '<div style="color:var(--color-fg-faint);font-size:0.6rem">TOTAL REQUESTS</div>' +
578577
+ '<div style="color:var(--color-brand);font-size:0.9rem;font-weight:bold">' + (d.persistent?.totalRequests || 0).toLocaleString() + '</div></div>' +
577445
578578
  '</div>' +
577446
- (providerCards || '<div style="color:#555;font-size:0.7rem">No provider usage recorded yet</div>');
578579
+ (providerCards || '<div style="color:var(--color-fg-faint);font-size:0.7rem">No provider usage recorded yet</div>');
577447
578580
  } catch {}
577448
578581
  }
577449
578582
 
@@ -577454,20 +578587,20 @@ async function loadJobs() {
577454
578587
  try {
577455
578588
  const r = await fetch('/v1/runs', { headers: headers() });
577456
578589
  const d = await r.json();
577457
- if (!d.runs?.length) { list.innerHTML = '<div style="color:#555">No jobs yet</div>'; return; }
578590
+ if (!d.runs?.length) { list.innerHTML = '<div style="color:var(--color-fg-faint)">No jobs yet</div>'; return; }
577458
578591
  let html = '<table style="width:100%;border-collapse:collapse">';
577459
- html += '<tr style="color:#b2920a;font-size:0.65rem"><th style="text-align:left;padding:4px">ID</th><th>Status</th><th>Task</th><th>Duration</th></tr>';
578592
+ html += '<tr style="color:var(--color-brand);font-size:0.65rem"><th style="text-align:left;padding:4px">ID</th><th>Status</th><th>Task</th><th>Duration</th></tr>';
577460
578593
  for (const j of d.runs.slice(0, 20)) {
577461
- const color = j.status === 'completed' ? '#4ec94e' : j.status === 'running' ? '#b2920a' : '#ff4444';
578594
+ const color = j.status === 'completed' ? 'var(--color-success)' : j.status === 'running' ? 'var(--color-brand)' : 'var(--color-error)';
577462
578595
  const dur = j.durationMs ? (j.durationMs / 1000).toFixed(1) + 's' : '—';
577463
- html += '<tr style="border-top:1px solid #2a2a30"><td style="padding:4px;color:#888">' + (j.id||'').slice(0,12) + '</td>';
578596
+ html += '<tr style="border-top:1px solid var(--color-bg-input)"><td style="padding:4px;color:var(--color-fg-muted)">' + (j.id||'').slice(0,12) + '</td>';
577464
578597
  html += '<td style="color:' + color + '">' + (j.status||'?') + '</td>';
577465
- html += '<td style="color:#b0b0b0;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((j.task||'').slice(0,60)) + '</td>';
577466
- html += '<td style="color:#555">' + dur + '</td></tr>';
578598
+ html += '<td style="color:var(--color-fg);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((j.task||'').slice(0,60)) + '</td>';
578599
+ html += '<td style="color:var(--color-fg-faint)">' + dur + '</td></tr>';
577467
578600
  }
577468
578601
  html += '</table>';
577469
578602
  list.innerHTML = html;
577470
- } catch { list.innerHTML = '<div style="color:#ff4444">Failed to load jobs</div>'; }
578603
+ } catch { list.innerHTML = '<div style="color:var(--color-error)">Failed to load jobs</div>'; }
577471
578604
  }
577472
578605
 
577473
578606
  // Session storage (localStorage for persistence across page reloads)
@@ -577694,7 +578827,7 @@ function switchSession(id) {
577694
578827
  // Restore metadata bar
577695
578828
  if (m.meta && m.role === 'assistant') {
577696
578829
  const metaBar = document.createElement('div');
577697
- metaBar.style.cssText = 'margin:6px 0 2px;padding:4px 8px;background:#1e1e22;border-radius:3px;font-size:0.6rem;color:#555;display:flex;gap:12px';
578830
+ metaBar.style.cssText = 'margin:6px 0 2px;padding:4px 8px;background:var(--color-bg-elevated);border-radius:3px;font-size:0.6rem;color:var(--color-fg-faint);display:flex;gap:12px';
577698
578831
  const parts = [];
577699
578832
  if (m.meta.turns) parts.push(m.meta.turns + ' turns');
577700
578833
  if (m.meta.toolCalls) parts.push(m.meta.toolCalls + ' tool calls');
@@ -577705,24 +578838,24 @@ function switchSession(id) {
577705
578838
  // Restore tool call provenance with expandable args
577706
578839
  if (m.tools?.length && m.role === 'assistant') {
577707
578840
  const outerDetails = document.createElement('details');
577708
- outerDetails.style.cssText = 'margin:2px 0;font-size:0.6rem;color:#555';
578841
+ outerDetails.style.cssText = 'margin:2px 0;font-size:0.6rem;color:var(--color-fg-faint)';
577709
578842
  const outerSummary = document.createElement('summary');
577710
- outerSummary.style.cssText = 'cursor:pointer;color:#888';
578843
+ outerSummary.style.cssText = 'cursor:pointer;color:var(--color-fg-muted)';
577711
578844
  outerSummary.textContent = 'show ' + m.tools.length + ' tool calls';
577712
578845
  outerDetails.appendChild(outerSummary);
577713
578846
  for (const t of m.tools) {
577714
578847
  const toolObj = typeof t === 'string' ? { tool: t } : t;
577715
578848
  const td = document.createElement('details');
577716
- td.style.cssText = 'background:#1e1e22;border-left:2px solid #b2920a;margin:2px 0';
578849
+ td.style.cssText = 'background:var(--color-bg-elevated);border-left:2px solid var(--color-brand);margin:2px 0';
577717
578850
  const ts = document.createElement('summary');
577718
- ts.style.cssText = 'padding:4px 8px;color:#b2920a;cursor:pointer;font-size:0.65rem';
578851
+ ts.style.cssText = 'padding:4px 8px;color:var(--color-brand);cursor:pointer;font-size:0.65rem';
577719
578852
  ts.textContent = toolObj.tool || String(t);
577720
578853
  td.appendChild(ts);
577721
578854
  if (toolObj.args) {
577722
578855
  const ad = document.createElement('div');
577723
- ad.style.cssText = 'padding:4px 8px 6px 16px;color:#888;font-size:0.6rem';
578856
+ ad.style.cssText = 'padding:4px 8px 6px 16px;color:var(--color-fg-muted);font-size:0.6rem';
577724
578857
  for (const [k, v] of Object.entries(toolObj.args)) {
577725
- ad.innerHTML += '<div style="padding:1px 0"><span style="color:#b2920a">' + k + ':</span> ' + String(v).slice(0, 200) + '</div>';
578858
+ ad.innerHTML += '<div style="padding:1px 0"><span style="color:var(--color-brand)">' + k + ':</span> ' + String(v).slice(0, 200) + '</div>';
577726
578859
  }
577727
578860
  td.appendChild(ad);
577728
578861
  }
@@ -577808,13 +578941,13 @@ async function loadAgentWorkspaceRoot() {
577808
578941
  const rootPath = d.path || '.';
577809
578942
  cwdEl.innerHTML = '';
577810
578943
  const wsSpan = document.createElement('span');
577811
- wsSpan.style.cssText = 'color:#888';
578944
+ wsSpan.style.cssText = 'color:var(--color-fg-muted)';
577812
578945
  wsSpan.textContent = 'ROOT: ' + rootPath;
577813
578946
  cwdEl.appendChild(wsSpan);
577814
578947
  cwdEl.appendChild(document.createElement('br'));
577815
578948
  const wdSpan = document.createElement('span');
577816
578949
  wdSpan.id = 'agent-workspace-dir-label';
577817
- wdSpan.style.cssText = 'color:#b2920a';
578950
+ wdSpan.style.cssText = 'color:var(--color-brand)';
577818
578951
  wdSpan.textContent = 'CWD: ' + (agentWorkingDir || rootPath);
577819
578952
  cwdEl.appendChild(wdSpan);
577820
578953
 
@@ -577824,7 +578957,7 @@ async function loadAgentWorkspaceRoot() {
577824
578957
  renderAgentWorkspaceTree();
577825
578958
  } catch {
577826
578959
  const tree = document.getElementById('agent-workspace-tree');
577827
- if (tree) tree.innerHTML = '<div style="color:#555">Could not load files</div>';
578960
+ if (tree) tree.innerHTML = '<div style="color:var(--color-fg-faint)">Could not load files</div>';
577828
578961
  }
577829
578962
  }
577830
578963
 
@@ -577855,7 +578988,7 @@ function renderAgentTreeNode(parentEl, absPath, depth, isRoot) {
577855
578988
 
577856
578989
  if (e.type === 'dir') {
577857
578990
  const caret = document.createElement('span');
577858
- caret.style.cssText = 'color:#666; font-size:0.55rem; width:10px; display:inline-block; text-align:center';
578991
+ caret.style.cssText = 'color:var(--color-fg-subtle); font-size:0.55rem; width:10px; display:inline-block; text-align:center';
577859
578992
  caret.textContent = agentTreeExpanded.has(childAbs) ? '▾' : '▸';
577860
578993
  row.appendChild(caret);
577861
578994
  const icon = document.createElement('span');
@@ -577863,7 +578996,7 @@ function renderAgentTreeNode(parentEl, absPath, depth, isRoot) {
577863
578996
  row.appendChild(icon);
577864
578997
  const name = document.createElement('span');
577865
578998
  name.textContent = e.name;
577866
- name.style.cssText = 'color:#b2920a';
578999
+ name.style.cssText = 'color:var(--color-brand)';
577867
579000
  if (agentWorkingDir && childAbs === agentWorkingDir) {
577868
579001
  name.style.cssText += ';font-weight:bold;text-decoration:underline';
577869
579002
  }
@@ -577907,8 +579040,8 @@ function renderAgentTreeNode(parentEl, absPath, depth, isRoot) {
577907
579040
  const name = document.createElement('span');
577908
579041
  name.textContent = e.name;
577909
579042
  name.style.cssText = agentContextFiles.includes(childAbs)
577910
- ? 'color:#b2920a;font-weight:bold'
577911
- : 'color:#b0b0b0';
579043
+ ? 'color:var(--color-brand);font-weight:bold'
579044
+ : 'color:var(--color-fg)';
577912
579045
  row.appendChild(name);
577913
579046
  // Click toggles file in/out of agent context
577914
579047
  row.addEventListener('click', () => {
@@ -577947,13 +579080,13 @@ async function loadWorkspaceRoot() {
577947
579080
  const rootPath = d.path || '.';
577948
579081
  cwdEl.innerHTML = '';
577949
579082
  const wsSpan = document.createElement('span');
577950
- wsSpan.style.cssText = 'color:#888';
579083
+ wsSpan.style.cssText = 'color:var(--color-fg-muted)';
577951
579084
  wsSpan.textContent = 'ROOT: ' + rootPath;
577952
579085
  cwdEl.appendChild(wsSpan);
577953
579086
  cwdEl.appendChild(document.createElement('br'));
577954
579087
  const wdSpan = document.createElement('span');
577955
579088
  wdSpan.id = 'workspace-dir-label';
577956
- wdSpan.style.cssText = 'color:#b2920a';
579089
+ wdSpan.style.cssText = 'color:var(--color-brand)';
577957
579090
  wdSpan.textContent = 'CWD: ' + (chatWorkingDir || rootPath);
577958
579091
  cwdEl.appendChild(wdSpan);
577959
579092
 
@@ -577964,7 +579097,7 @@ async function loadWorkspaceRoot() {
577964
579097
  treeRoot = rootPath;
577965
579098
  renderWorkspaceTree();
577966
579099
  } catch {
577967
- document.getElementById('workspace-tree').innerHTML = '<div style="color:#555">Could not load files</div>';
579100
+ document.getElementById('workspace-tree').innerHTML = '<div style="color:var(--color-fg-faint)">Could not load files</div>';
577968
579101
  }
577969
579102
  }
577970
579103
 
@@ -578000,7 +579133,7 @@ function renderTreeNode(parentEl, absPath, depth, isRoot) {
578000
579133
  if (e.type === 'dir') {
578001
579134
  const caret = document.createElement('span');
578002
579135
  caret.className = 'tree-caret';
578003
- caret.style.cssText = 'color:#666; font-size:0.55rem; width:10px; display:inline-block; text-align:center';
579136
+ caret.style.cssText = 'color:var(--color-fg-subtle); font-size:0.55rem; width:10px; display:inline-block; text-align:center';
578004
579137
  caret.textContent = treeExpanded.has(childAbs) ? '▾' : '▸';
578005
579138
  row.appendChild(caret);
578006
579139
  const icon = document.createElement('span');
@@ -578008,7 +579141,7 @@ function renderTreeNode(parentEl, absPath, depth, isRoot) {
578008
579141
  row.appendChild(icon);
578009
579142
  const name = document.createElement('span');
578010
579143
  name.textContent = e.name;
578011
- name.style.cssText = 'color:#b2920a';
579144
+ name.style.cssText = 'color:var(--color-brand)';
578012
579145
  // Highlight the current workspace
578013
579146
  if (chatWorkingDir && childAbs === chatWorkingDir) {
578014
579147
  name.style.cssText += ';font-weight:bold;text-decoration:underline';
@@ -578047,7 +579180,7 @@ function renderTreeNode(parentEl, absPath, depth, isRoot) {
578047
579180
  name.textContent = e.name;
578048
579181
  name.style.cssText = contextFiles.includes(childAbs)
578049
579182
  ? 'color:#4e94c9;font-weight:bold'
578050
- : 'color:#b0b0b0';
579183
+ : 'color:var(--color-fg)';
578051
579184
  row.appendChild(name);
578052
579185
  row.addEventListener('click', (ev) => {
578053
579186
  ev.stopPropagation();
@@ -578078,7 +579211,7 @@ function showTreeContextMenu(x, y, path, type) {
578078
579211
  if (!treeMenuEl) {
578079
579212
  treeMenuEl = document.createElement('div');
578080
579213
  treeMenuEl.id = 'tree-menu';
578081
- treeMenuEl.style.cssText = 'position:fixed; background:#1e1e22; border:1px solid #b2920a; border-radius:4px; padding:4px 0; box-shadow:0 4px 20px rgba(0,0,0,0.6); z-index:200; font-size:0.7rem; min-width:180px';
579214
+ treeMenuEl.style.cssText = 'position:fixed; background:var(--color-bg-elevated); border:1px solid var(--color-brand); border-radius:4px; padding:4px 0; box-shadow:0 4px 20px rgba(0,0,0,0.6); z-index:200; font-size:0.7rem; min-width:180px';
578082
579215
  document.body.appendChild(treeMenuEl);
578083
579216
  document.addEventListener('click', () => { if (treeMenuEl) treeMenuEl.style.display = 'none'; });
578084
579217
  }
@@ -578098,9 +579231,9 @@ function showTreeContextMenu(x, y, path, type) {
578098
579231
  for (const it of items) {
578099
579232
  if (it.show === false) continue;
578100
579233
  const row = document.createElement('div');
578101
- row.style.cssText = 'padding:6px 16px; cursor:pointer; color:#b0b0b0';
579234
+ row.style.cssText = 'padding:6px 16px; cursor:pointer; color:var(--color-fg)';
578102
579235
  row.textContent = it.label;
578103
- row.addEventListener('mouseenter', () => row.style.background = '#2a2a30');
579236
+ row.addEventListener('mouseenter', () => row.style.background = 'var(--color-bg-input)');
578104
579237
  row.addEventListener('mouseleave', () => row.style.background = '');
578105
579238
  row.addEventListener('click', (ev) => {
578106
579239
  ev.stopPropagation();
@@ -578125,7 +579258,7 @@ function setChatWorkingDir(path) {
578125
579258
  if (s) {
578126
579259
  const old = s.textContent;
578127
579260
  s.textContent = path ? 'workspace → ' + path.split('/').slice(-2).join('/') : 'workspace cleared';
578128
- s.style.color = '#4ec94e';
579261
+ s.style.color = 'var(--color-success)';
578129
579262
  setTimeout(() => { s.textContent = old; s.style.color = ''; }, 3000);
578130
579263
  }
578131
579264
  // Also push to the Agent tab input if visible
@@ -578192,18 +579325,18 @@ async function previewFile(path) {
578192
579325
  modal.style.cssText = 'position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:300; display:flex; align-items:center; justify-content:center; padding:40px';
578193
579326
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
578194
579327
  const card = document.createElement('div');
578195
- card.style.cssText = 'background:#1e1e22; border:1px solid #2a2a30; border-radius:4px; max-width:90vw; max-height:90vh; overflow:hidden; display:flex; flex-direction:column';
579328
+ card.style.cssText = 'background:var(--color-bg-elevated); border:1px solid var(--color-bg-input); border-radius:4px; max-width:90vw; max-height:90vh; overflow:hidden; display:flex; flex-direction:column';
578196
579329
  const title = document.createElement('div');
578197
- title.style.cssText = 'padding:10px 16px; border-bottom:1px solid #2a2a30; color:#b2920a; font-size:0.75rem; display:flex; justify-content:space-between; align-items:center; gap:16px';
579330
+ title.style.cssText = 'padding:10px 16px; border-bottom:1px solid var(--color-bg-input); color:var(--color-brand); font-size:0.75rem; display:flex; justify-content:space-between; align-items:center; gap:16px';
578198
579331
  title.innerHTML = '<span>' + escHtml(path) + '</span>';
578199
579332
  const closeBtn = document.createElement('button');
578200
579333
  closeBtn.textContent = '×';
578201
- closeBtn.style.cssText = 'background:none; border:none; color:#666; font-size:1.2rem; cursor:pointer';
579334
+ closeBtn.style.cssText = 'background:none; border:none; color:var(--color-fg-subtle); font-size:1.2rem; cursor:pointer';
578202
579335
  closeBtn.onclick = () => modal.remove();
578203
579336
  title.appendChild(closeBtn);
578204
579337
  card.appendChild(title);
578205
579338
  const pre = document.createElement('pre');
578206
- pre.style.cssText = 'padding:12px 16px; color:#b0b0b0; overflow:auto; font-size:0.7rem; margin:0; background:#17171a';
579339
+ pre.style.cssText = 'padding:12px 16px; color:var(--color-fg); overflow:auto; font-size:0.7rem; margin:0; background:var(--color-bg)';
578207
579340
  pre.textContent = d.content || '(empty)';
578208
579341
  card.appendChild(pre);
578209
579342
  modal.appendChild(card);
@@ -578223,7 +579356,7 @@ function toggleSandbox() {
578223
579356
  const btn = document.getElementById('sandbox-toggle');
578224
579357
  btn.textContent = 'sandbox: ' + (sandboxMode === 'none' ? 'off' : 'docker');
578225
579358
  btn.style.opacity = sandboxMode === 'none' ? '0.5' : '1';
578226
- btn.style.borderColor = sandboxMode === 'none' ? '#3a3a42' : '#b2920a';
579359
+ btn.style.borderColor = sandboxMode === 'none' ? 'var(--color-border)' : 'var(--color-brand)';
578227
579360
  }
578228
579361
 
578229
579362
  // Live system metrics polling
@@ -578387,8 +579520,8 @@ async function doUpdate() {
578387
579520
 
578388
579521
  btn.textContent = 'updated v' + newVersion;
578389
579522
  btn.style.background = '#1a3a1a';
578390
- btn.style.borderColor = '#4ec94e';
578391
- btn.style.color = '#4ec94e';
579523
+ btn.style.borderColor = 'var(--color-success)';
579524
+ btn.style.color = 'var(--color-success)';
578392
579525
  try { seenVersion = newVersion || seenVersion; } catch {}
578393
579526
 
578394
579527
  // Flash status bar
@@ -578396,7 +579529,7 @@ async function doUpdate() {
578396
579529
  if (statusEl) {
578397
579530
  const origStatusColor = statusEl.style.color;
578398
579531
  statusEl.textContent = 'updated → v' + newVersion;
578399
- statusEl.style.color = '#4ec94e';
579532
+ statusEl.style.color = 'var(--color-success)';
578400
579533
  setTimeout(() => { statusEl.style.color = origStatusColor; }, 5000);
578401
579534
  }
578402
579535
 
@@ -578606,7 +579739,7 @@ function renderInlineError(message) {
578606
579739
  }
578607
579740
  const wrap = document.createElement('div');
578608
579741
  wrap.className = 'msg assistant';
578609
- wrap.innerHTML = '<div style="background:#2a1e1e;border-left:3px solid #ff4444;color:#ff7777;padding:6px 10px 6px 14px;margin:3px 0;font-family:inherit"><div style="color:#ff4444;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div><div style="color:#ff7777;white-space:pre-wrap;word-break:break-word">' + escHtml(message) + '</div></div>';
579742
+ wrap.innerHTML = '<div style="background:#2a1e1e;border-left:3px solid var(--color-error);color:#ff7777;padding:6px 10px 6px 14px;margin:3px 0;font-family:inherit"><div style="color:var(--color-error);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:2px">\\u25B8 error</div><div style="color:#ff7777;white-space:pre-wrap;word-break:break-word">' + escHtml(message) + '</div></div>';
578610
579743
  conv.appendChild(wrap);
578611
579744
  maybeAutoScroll();
578612
579745
  }
@@ -578702,7 +579835,7 @@ function renderTasksRow(todos) {
578702
579835
  const total = todos.length;
578703
579836
  const counter = document.createElement('span');
578704
579837
  counter.className = 'tasks-label';
578705
- counter.style.color = completed === total ? '#5fa55f' : '#b2920a';
579838
+ counter.style.color = completed === total ? '#5fa55f' : 'var(--color-brand)';
578706
579839
  counter.textContent = completed + '/' + total;
578707
579840
  tasksRowEl.appendChild(counter);
578708
579841
  // One pill per task — color-coded by status
@@ -578750,13 +579883,13 @@ async function refreshTodos(sessionId) {
578750
579883
  const li = document.createElement('li');
578751
579884
  li.style.cssText = 'padding:2px 0;display:flex;gap:8px;align-items:flex-start';
578752
579885
  const mark = document.createElement('span');
578753
- mark.style.cssText = 'color:#b2920a;font-family:monospace;flex-shrink:0';
579886
+ mark.style.cssText = 'color:var(--color-brand);font-family:monospace;flex-shrink:0';
578754
579887
  mark.textContent = statusMark(t.status);
578755
579888
  li.appendChild(mark);
578756
579889
  const content = document.createElement('span');
578757
579890
  content.style.cssText = t.status === 'completed'
578758
- ? 'color:#666;text-decoration:line-through'
578759
- : 'color:#b0b0b0';
579891
+ ? 'color:var(--color-fg-subtle);text-decoration:line-through'
579892
+ : 'color:var(--color-fg)';
578760
579893
  content.textContent = t.content + (t.blocker ? ' (blocked: ' + t.blocker + ')' : '');
578761
579894
  li.appendChild(content);
578762
579895
  todoListEl.appendChild(li);
@@ -579043,9 +580176,9 @@ async function refreshVoiceState() {
579043
580176
  const pill = document.getElementById('voice-state-pill');
579044
580177
  if (pill) {
579045
580178
  pill.textContent = data.state;
579046
- pill.style.borderLeftColor = data.state === 'listening' ? '#4ec94e'
579047
- : data.state === 'speaking' ? '#b2920a'
579048
- : data.state === 'error' ? '#ff4444' : '#555';
580179
+ pill.style.borderLeftColor = data.state === 'listening' ? 'var(--color-success)'
580180
+ : data.state === 'speaking' ? 'var(--color-brand)'
580181
+ : data.state === 'error' ? 'var(--color-error)' : 'var(--color-fg-faint)';
579049
580182
  }
579050
580183
  } catch {}
579051
580184
  }
@@ -579087,33 +580220,33 @@ async function loadCloneRefs() {
579087
580220
  if (!list) return;
579088
580221
  try {
579089
580222
  const r = await fetch('/v1/voice/clone-refs', { headers: headers() });
579090
- if (!r.ok) { list.innerHTML = '<div style="color:#666">failed to load refs</div>'; return; }
580223
+ if (!r.ok) { list.innerHTML = '<div style="color:var(--color-fg-subtle)">failed to load refs</div>'; return; }
579091
580224
  const data = await r.json();
579092
580225
  const refs = data.refs || [];
579093
580226
  if (refs.length === 0) {
579094
- list.innerHTML = '<div style="color:#666;padding:14px;text-align:center;border:1px dashed #3a3a42;border-radius:4px">No clone references yet. Upload a clean 3+ second voice sample to clone it.</div>';
580227
+ list.innerHTML = '<div style="color:var(--color-fg-subtle);padding:14px;text-align:center;border:1px dashed var(--color-border);border-radius:4px">No clone references yet. Upload a clean 3+ second voice sample to clone it.</div>';
579095
580228
  return;
579096
580229
  }
579097
580230
  list.innerHTML = refs.map(ref => {
579098
- const accent = ref.isActive ? '#b2920a' : '#3a3a42';
579099
- const statusBadge = ref.isActive ? ' <span style="color:#4ec94e;font-size:0.6rem">(active)</span>' : '';
580231
+ const accent = ref.isActive ? 'var(--color-brand)' : 'var(--color-border)';
580232
+ const statusBadge = ref.isActive ? ' <span style="color:var(--color-success);font-size:0.6rem">(active)</span>' : '';
579100
580233
  const sizeKb = (ref.size / 1024).toFixed(1);
579101
580234
  const fnAttr = escAttr(ref.filename);
579102
580235
  const nameAttr = escAttr(ref.name);
579103
- return '<div style="background:#1a1a1e;border-left:2px solid ' + accent + ';padding:8px 12px;margin:6px 0;display:flex;justify-content:space-between;align-items:center">' +
580236
+ return '<div style="background:var(--color-bg);border-left:2px solid ' + accent + ';padding:8px 12px;margin:6px 0;display:flex;justify-content:space-between;align-items:center">' +
579104
580237
  '<div>' +
579105
- '<div style="color:' + (ref.isActive ? '#b2920a' : '#b0b0b0') + ';font-weight:' + (ref.isActive ? '600' : '400') + '">' + escHtml(ref.name) + statusBadge + '</div>' +
579106
- '<div style="color:#666;font-size:0.6rem">' + escHtml(ref.filename) + ' &middot; ' + sizeKb + ' KB</div>' +
580238
+ '<div style="color:' + (ref.isActive ? 'var(--color-brand)' : 'var(--color-fg)') + ';font-weight:' + (ref.isActive ? '600' : '400') + '">' + escHtml(ref.name) + statusBadge + '</div>' +
580239
+ '<div style="color:var(--color-fg-subtle);font-size:0.6rem">' + escHtml(ref.filename) + ' &middot; ' + sizeKb + ' KB</div>' +
579107
580240
  '</div>' +
579108
580241
  '<div style="display:flex;gap:6px">' +
579109
- (ref.isActive ? '' : '<button onclick="activateCloneRef('' + fnAttr + '')" style="background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">activate</button>') +
579110
- '<button onclick="renameCloneRef('' + fnAttr + '','' + nameAttr + '')" style="background:#2a2a30;border:1px solid #3a3a42;color:#888;padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">rename</button>' +
579111
- '<button onclick="deleteCloneRef('' + fnAttr + '')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">del</button>' +
580242
+ (ref.isActive ? '' : '<button onclick="activateCloneRef(\\'' + fnAttr + '\\')" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-brand);padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">activate</button>') +
580243
+ '<button onclick="renameCloneRef(\\'' + fnAttr + '\\',\\'' + nameAttr + '\\')" style="background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg-muted);padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">rename</button>' +
580244
+ '<button onclick="deleteCloneRef(\\'' + fnAttr + '\\')" style="background:var(--color-bg-input);border:1px solid var(--color-error);color:var(--color-error);padding:4px 10px;border-radius:3px;font-family:inherit;font-size:0.6rem;cursor:pointer">del</button>' +
579112
580245
  '</div>' +
579113
580246
  '</div>';
579114
580247
  }).join('');
579115
580248
  } catch (e) {
579116
- list.innerHTML = '<div style="color:#ff6b6b">failed: ' + (e?.message || e) + '</div>';
580249
+ list.innerHTML = '<div style="color:var(--color-error)">failed: ' + (e?.message || e) + '</div>';
579117
580250
  }
579118
580251
  }
579119
580252
 
@@ -579125,7 +580258,7 @@ async function uploadCloneRef(files) {
579125
580258
  if (file.size < 4096) { alert('Audio file too small (min ~4 KB)'); return; }
579126
580259
  if (file.size > 50 * 1024 * 1024) { alert('Audio file too large (max 50 MB)'); return; }
579127
580260
  const list = document.getElementById('clone-refs-list');
579128
- if (list) list.innerHTML = '<div style="color:#666;padding:14px;text-align:center">uploading ' + escHtml(file.name) + '...</div>';
580261
+ if (list) list.innerHTML = '<div style="color:var(--color-fg-subtle);padding:14px;text-align:center">uploading ' + escHtml(file.name) + '...</div>';
579129
580262
  try {
579130
580263
  const buf = await file.arrayBuffer();
579131
580264
  let binary = '';
@@ -579221,7 +580354,7 @@ async function startVoiceChat() {
579221
580354
  }
579222
580355
  if (_voiceAudioCtx.state === 'suspended') await _voiceAudioCtx.resume();
579223
580356
  $voiceConnected.set(true);
579224
- if (btn) { btn.textContent = 'stop voicechat'; btn.style.borderColor = '#ff4444'; btn.style.color = '#ff4444'; btn.disabled = false; }
580357
+ if (btn) { btn.textContent = 'stop voicechat'; btn.style.borderColor = 'var(--color-error)'; btn.style.color = 'var(--color-error)'; btn.disabled = false; }
579225
580358
  document.getElementById('voice-mic-pill').style.display = 'inline-block';
579226
580359
  document.getElementById('voice-transcript-pane').style.display = 'block';
579227
580360
  addVoiceTranscriptLine('system', 'connected — speak naturally');
@@ -579239,7 +580372,7 @@ async function stopVoiceChat(silent) {
579239
580372
  try { if (_voiceWs) _voiceWs.close(); _voiceWs = null; } catch {}
579240
580373
  $voiceConnected.set(false);
579241
580374
  const btn = document.getElementById('voice-toggle-btn');
579242
- if (btn) { btn.textContent = 'start voicechat'; btn.style.borderColor = '#b2920a'; btn.style.color = '#b2920a'; btn.disabled = false; }
580375
+ if (btn) { btn.textContent = 'start voicechat'; btn.style.borderColor = 'var(--color-brand)'; btn.style.color = 'var(--color-brand)'; btn.disabled = false; }
579243
580376
  const micPill = document.getElementById('voice-mic-pill');
579244
580377
  if (micPill) micPill.style.display = 'none';
579245
580378
  if (!silent) addVoiceTranscriptLine('system', 'disconnected');
@@ -579333,12 +580466,12 @@ function addVoiceTranscriptLine(speaker, text) {
579333
580466
  const pane = document.getElementById('voice-transcript-pane');
579334
580467
  if (!pane) return;
579335
580468
  const div = document.createElement('div');
579336
- const accent = speaker === 'agent' ? '#b2920a'
579337
- : (speaker === 'you' || speaker === 'you (partial)') ? '#4ec94e'
579338
- : speaker === 'error' ? '#ff4444' : '#888';
579339
- div.style.cssText = 'margin:4px 0;padding:4px 6px;border-left:2px solid ' + accent + ';background:#1a1a1e';
579340
- div.innerHTML = '<span style="color:#888;font-size:0.55rem">' + escHtml(speaker) + '</span> ' +
579341
- '<span style="color:#b0b0b0">' + escHtml(text) + '</span>';
580469
+ const accent = speaker === 'agent' ? 'var(--color-brand)'
580470
+ : (speaker === 'you' || speaker === 'you (partial)') ? 'var(--color-success)'
580471
+ : speaker === 'error' ? 'var(--color-error)' : 'var(--color-fg-muted)';
580472
+ div.style.cssText = 'margin:4px 0;padding:4px 6px;border-left:2px solid ' + accent + ';background:var(--color-bg)';
580473
+ div.innerHTML = '<span style="color:var(--color-fg-muted);font-size:0.55rem">' + escHtml(speaker) + '</span> ' +
580474
+ '<span style="color:var(--color-fg)">' + escHtml(text) + '</span>';
579342
580475
  pane.appendChild(div);
579343
580476
  pane.scrollTop = pane.scrollHeight;
579344
580477
  while (pane.children.length > 200) pane.removeChild(pane.firstChild);
@@ -579415,8 +580548,1063 @@ setInterval(pollMetrics, 10000);
579415
580548
  setInterval(loadScheduled, 15000);
579416
580549
  setInterval(loadServices, 30000);
579417
580550
  setInterval(pollVersionBump, 5000);
580551
+
580552
+ // ════════════════════════════════════════════════════════════
580553
+ // OWUI-3: Prism.js syntax highlight loader (CDN with offline-
580554
+ // graceful fallback). When Prism isn't available we silently
580555
+ // fall back to plain text — the served HTML doesn't need it
580556
+ // to be syntactically meaningful.
580557
+ // ════════════════════════════════════════════════════════════
580558
+ let _prismLoadAttempted = false;
580559
+ let _prismReady = false;
580560
+ function _ensurePrismLoaded() {
580561
+ if (_prismLoadAttempted) return;
580562
+ _prismLoadAttempted = true;
580563
+ // Skip on file:// and totally offline contexts.
580564
+ if (location.protocol === 'file:') return;
580565
+ const cssLink = document.createElement('link');
580566
+ cssLink.rel = 'stylesheet';
580567
+ cssLink.href = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css';
580568
+ cssLink.onerror = () => { /* ignore — fallback is plain code */ };
580569
+ document.head.appendChild(cssLink);
580570
+ const script = document.createElement('script');
580571
+ script.src = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js';
580572
+ script.onload = () => {
580573
+ const auto = document.createElement('script');
580574
+ auto.src = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js';
580575
+ auto.onload = () => {
580576
+ try {
580577
+ if (window.Prism && window.Prism.plugins && window.Prism.plugins.autoloader) {
580578
+ window.Prism.plugins.autoloader.languages_path = 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/';
580579
+ }
580580
+ } catch {}
580581
+ _prismReady = true;
580582
+ // Highlight any code blocks that already exist.
580583
+ try { _highlightCodeBlocks(document); } catch {}
580584
+ };
580585
+ auto.onerror = () => { /* fallback: leave code unhighlighted */ };
580586
+ document.head.appendChild(auto);
580587
+ };
580588
+ script.onerror = () => { /* fallback: leave code unhighlighted */ };
580589
+ document.head.appendChild(script);
580590
+ }
580591
+
580592
+ function _highlightCodeBlocks(root) {
580593
+ if (!root) return;
580594
+ if (!_prismReady) {
580595
+ // Defer until prism finishes loading.
580596
+ if (!_prismLoadAttempted) _ensurePrismLoaded();
580597
+ return;
580598
+ }
580599
+ const codes = root.querySelectorAll ? root.querySelectorAll('pre > code[data-lang]') : [];
580600
+ codes.forEach(c => {
580601
+ if (c.dataset.prismHighlighted === '1') return;
580602
+ const lang = (c.getAttribute('data-lang') || '').toLowerCase();
580603
+ if (!lang) return;
580604
+ c.classList.add('language-' + lang);
580605
+ try {
580606
+ window.Prism && window.Prism.highlightElement && window.Prism.highlightElement(c);
580607
+ c.dataset.prismHighlighted = '1';
580608
+ } catch {}
580609
+ });
580610
+ }
580611
+
580612
+ // Kick the Prism loader once on init — no-op if it's already running.
580613
+ _ensurePrismLoaded();
580614
+
580615
+ // ════════════════════════════════════════════════════════════
580616
+ // OWUI-3: Streaming indicator — pulsing dot + status label.
580617
+ // Owned by send()/agent flows. Mounts inside the active
580618
+ // assistant .msg div; auto-removed when the next chunk lands
580619
+ // or when streaming flips to false.
580620
+ // ════════════════════════════════════════════════════════════
580621
+ function showStreamingIndicator(msgDiv, label) {
580622
+ if (!msgDiv) return null;
580623
+ let ind = msgDiv.querySelector('.msg-streaming-indicator');
580624
+ if (!ind) {
580625
+ ind = document.createElement('div');
580626
+ ind.className = 'msg-streaming-indicator';
580627
+ ind.innerHTML = '<span class="dot"></span><span class="label"></span>';
580628
+ msgDiv.appendChild(ind);
580629
+ }
580630
+ ind.querySelector('.label').textContent = label || 'thinking';
580631
+ return ind;
580632
+ }
580633
+ function hideStreamingIndicator(msgDiv) {
580634
+ if (!msgDiv) return;
580635
+ const ind = msgDiv.querySelector('.msg-streaming-indicator');
580636
+ if (ind) ind.remove();
580637
+ }
580638
+
580639
+ // ════════════════════════════════════════════════════════════
580640
+ // OWUI-2: Sidebar wiring — toggle, resize, status sync, recent
580641
+ // chat list. State persists in localStorage (per-project scoped
580642
+ // via the existing project-namespaced storage helpers).
580643
+ // ════════════════════════════════════════════════════════════
580644
+ const SIDEBAR_KEYS = { collapsed: 'oa.sidebar.collapsed', width: 'oa.sidebar.width' };
580645
+
580646
+ function _getLS(k, fallback) {
580647
+ try { const v = (typeof projLS === 'function' ? projLS() : localStorage).getItem(k); return v == null ? fallback : v; }
580648
+ catch { return fallback; }
580649
+ }
580650
+ function _setLS(k, v) {
580651
+ try { (typeof projLS === 'function' ? projLS() : localStorage).setItem(k, v); } catch {}
580652
+ }
580653
+
580654
+ function toggleSidebar() {
580655
+ const sb = document.getElementById('oa-sidebar');
580656
+ if (!sb) return;
580657
+ const next = sb.getAttribute('data-collapsed') !== 'true';
580658
+ sb.setAttribute('data-collapsed', String(next));
580659
+ _setLS(SIDEBAR_KEYS.collapsed, next ? '1' : '0');
580660
+ }
580661
+
580662
+ function _restoreSidebarState() {
580663
+ const sb = document.getElementById('oa-sidebar');
580664
+ if (!sb) return;
580665
+ const collapsed = _getLS(SIDEBAR_KEYS.collapsed, '0') === '1';
580666
+ sb.setAttribute('data-collapsed', String(collapsed));
580667
+ const w = parseInt(_getLS(SIDEBAR_KEYS.width, '260'), 10);
580668
+ if (!collapsed && Number.isFinite(w) && w >= 200 && w <= 480) {
580669
+ sb.style.width = w + 'px';
580670
+ }
580671
+ }
580672
+
580673
+ function _wireSidebarResize() {
580674
+ const handle = document.getElementById('sidebar-resize');
580675
+ const sb = document.getElementById('oa-sidebar');
580676
+ if (!handle || !sb) return;
580677
+ let dragging = false;
580678
+ let startX = 0, startW = 0;
580679
+ handle.addEventListener('mousedown', (e) => {
580680
+ if (sb.getAttribute('data-collapsed') === 'true') return;
580681
+ dragging = true;
580682
+ startX = e.clientX;
580683
+ startW = sb.getBoundingClientRect().width;
580684
+ handle.classList.add('dragging');
580685
+ document.body.style.userSelect = 'none';
580686
+ e.preventDefault();
580687
+ });
580688
+ window.addEventListener('mousemove', (e) => {
580689
+ if (!dragging) return;
580690
+ const next = Math.max(200, Math.min(480, startW + (e.clientX - startX)));
580691
+ sb.style.width = next + 'px';
580692
+ });
580693
+ window.addEventListener('mouseup', () => {
580694
+ if (!dragging) return;
580695
+ dragging = false;
580696
+ handle.classList.remove('dragging');
580697
+ document.body.style.userSelect = '';
580698
+ const w = Math.round(sb.getBoundingClientRect().width);
580699
+ _setLS(SIDEBAR_KEYS.width, String(w));
580700
+ });
580701
+ }
580702
+
580703
+ // Mirror the legacy #status into the sidebar footer (text + dot color).
580704
+ function _syncSidebarStatus() {
580705
+ const src = document.getElementById('status');
580706
+ const dst = document.getElementById('sidebar-status-text');
580707
+ const dot = document.getElementById('sidebar-status-dot');
580708
+ if (!src || !dst) return;
580709
+ const text = (src.textContent || '').trim();
580710
+ dst.textContent = text || 'idle';
580711
+ if (dot) {
580712
+ let color = 'var(--color-fg-faint)';
580713
+ if (/connect/i.test(text)) color = 'var(--color-warning)';
580714
+ else if (/online|ready|ok/i.test(text)) color = 'var(--color-success)';
580715
+ else if (/error|fail|down/i.test(text)) color = 'var(--color-error)';
580716
+ else if (text) color = 'var(--color-success)';
580717
+ dot.style.background = color;
580718
+ }
580719
+ // Mirror the legacy update-btn visibility into the sidebar.
580720
+ const upd = document.getElementById('update-btn');
580721
+ const sUpd = document.getElementById('sidebar-update-btn');
580722
+ if (upd && sUpd) sUpd.style.display = upd.style.display === 'none' ? 'none' : 'inline-block';
580723
+ }
580724
+
580725
+ // Populate the sidebar recent-chat list from $chatSessions.
580726
+ // ════════════════════════════════════════════════════════════
580727
+ // OWUI-6: Folders + pinned + inline rename/delete
580728
+ // State lives in per-project localStorage:
580729
+ // oa.pinned -> [chatId, ...]
580730
+ // oa.folders -> [{ id, name }]
580731
+ // oa.chatFolderMap -> { chatId: folderId }
580732
+ // oa.foldersOpen -> { folderId: bool }
580733
+ // ════════════════════════════════════════════════════════════
580734
+ const CHAT_KEYS = {
580735
+ pinned: 'oa.pinned',
580736
+ folders: 'oa.folders',
580737
+ map: 'oa.chatFolderMap',
580738
+ foldersOpen: 'oa.foldersOpen',
580739
+ };
580740
+
580741
+ function _readJSON(key, fallback) {
580742
+ try {
580743
+ const raw = (typeof projLS === 'function' ? projLS() : localStorage).getItem(key);
580744
+ if (!raw) return fallback;
580745
+ return JSON.parse(raw);
580746
+ } catch { return fallback; }
580747
+ }
580748
+ function _writeJSON(key, val) {
580749
+ try { (typeof projLS === 'function' ? projLS() : localStorage).setItem(key, JSON.stringify(val)); } catch {}
580750
+ }
580751
+
580752
+ function getChatPinned() { return _readJSON(CHAT_KEYS.pinned, []); }
580753
+ function getChatFolders() { return _readJSON(CHAT_KEYS.folders, []); }
580754
+ function getChatFolderMap() { return _readJSON(CHAT_KEYS.map, {}); }
580755
+ function getFoldersOpenState() { return _readJSON(CHAT_KEYS.foldersOpen, {}); }
580756
+
580757
+ function setChatPinned(arr) { _writeJSON(CHAT_KEYS.pinned, arr || []); }
580758
+ function setChatFolders(arr) { _writeJSON(CHAT_KEYS.folders, arr || []); }
580759
+ function setChatFolderMap(map) { _writeJSON(CHAT_KEYS.map, map || {}); }
580760
+ function setFoldersOpenState(s) { _writeJSON(CHAT_KEYS.foldersOpen, s || {}); }
580761
+
580762
+ function pinChat(id) {
580763
+ const p = getChatPinned();
580764
+ if (!p.includes(id)) { p.push(id); setChatPinned(p); }
580765
+ _renderSidebarChats(_lastSidebarFilter || '');
580766
+ }
580767
+ function unpinChat(id) {
580768
+ const p = getChatPinned().filter(x => x !== id);
580769
+ setChatPinned(p);
580770
+ _renderSidebarChats(_lastSidebarFilter || '');
580771
+ }
580772
+ function isPinned(id) { return getChatPinned().includes(id); }
580773
+
580774
+ function createChatFolder(name) {
580775
+ const list = getChatFolders();
580776
+ const id = 'fld-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
580777
+ list.push({ id, name: String(name || 'New folder') });
580778
+ setChatFolders(list);
580779
+ _renderSidebarChats(_lastSidebarFilter || '');
580780
+ return id;
580781
+ }
580782
+ function renameChatFolder(folderId, newName) {
580783
+ const list = getChatFolders().map(f => f.id === folderId ? { ...f, name: String(newName || f.name) } : f);
580784
+ setChatFolders(list);
580785
+ _renderSidebarChats(_lastSidebarFilter || '');
580786
+ }
580787
+ function deleteChatFolder(folderId) {
580788
+ // Move every chat out of this folder, then remove the folder.
580789
+ const map = getChatFolderMap();
580790
+ for (const k of Object.keys(map)) {
580791
+ if (map[k] === folderId) delete map[k];
580792
+ }
580793
+ setChatFolderMap(map);
580794
+ setChatFolders(getChatFolders().filter(f => f.id !== folderId));
580795
+ _renderSidebarChats(_lastSidebarFilter || '');
580796
+ }
580797
+ function moveChatToFolder(chatId, folderId) {
580798
+ const map = getChatFolderMap();
580799
+ if (folderId == null || folderId === '__none__') delete map[chatId];
580800
+ else map[chatId] = folderId;
580801
+ setChatFolderMap(map);
580802
+ _renderSidebarChats(_lastSidebarFilter || '');
580803
+ }
580804
+ function toggleChatFolder(folderId) {
580805
+ const open = getFoldersOpenState();
580806
+ open[folderId] = !open[folderId];
580807
+ setFoldersOpenState(open);
580808
+ _renderSidebarChats(_lastSidebarFilter || '');
580809
+ }
580810
+
580811
+ // Inline rename / delete — best-effort: updates the in-memory store and
580812
+ // localStorage. The server-side session store is touched too via the
580813
+ // existing deleteChatSession() / saveSessions().
580814
+ function renameChatTitle(chatId, newTitle) {
580815
+ if (typeof $chatSessions === 'undefined' || !$chatSessions.set || !$chatSessions.get) return;
580816
+ const all = { ...$chatSessions.get() };
580817
+ const s = all[chatId];
580818
+ if (!s) return;
580819
+ all[chatId] = { ...s, title: String(newTitle || s.title || '') };
580820
+ $chatSessions.set(all);
580821
+ try { if (typeof saveSessions === 'function') saveSessions(); } catch {}
580822
+ }
580823
+ function deleteChatById(chatId) {
580824
+ // Drop from the active session if it matches; otherwise just remove
580825
+ // from the store and let the saveSessions hook persist.
580826
+ if (typeof $chatSessions === 'undefined' || !$chatSessions.set || !$chatSessions.get) return;
580827
+ const all = { ...$chatSessions.get() };
580828
+ delete all[chatId];
580829
+ $chatSessions.set(all);
580830
+ // Also drop from pinned + folder map.
580831
+ setChatPinned(getChatPinned().filter(x => x !== chatId));
580832
+ const map = getChatFolderMap(); delete map[chatId]; setChatFolderMap(map);
580833
+ try { if (typeof saveSessions === 'function') saveSessions(); } catch {}
580834
+ // If we just deleted the active chat, route the user to a fresh new chat.
580835
+ try {
580836
+ const active = (typeof $currentSession !== 'undefined' && $currentSession.get) ? $currentSession.get() : null;
580837
+ if (active === chatId && typeof newChatSession === 'function') newChatSession();
580838
+ } catch {}
580839
+ }
580840
+
580841
+ // Context-menu popover for chat rows
580842
+ function _showChatRowMenu(evt, chatId) {
580843
+ evt.stopPropagation();
580844
+ evt.preventDefault();
580845
+ _hideChatRowMenu();
580846
+ const menu = document.createElement('div');
580847
+ menu.id = 'sb-chat-menu';
580848
+ menu.style.cssText = 'position:fixed;background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 16px rgba(0,0,0,0.4);min-width:160px;z-index:120;font-family:var(--font-ui);font-size:0.78rem;overflow:hidden';
580849
+ const folders = getChatFolders();
580850
+ const safeChat = String(chatId).replace(/'/g, "\\\\'");
580851
+ const pinLabel = isPinned(chatId) ? 'Unpin' : 'Pin';
580852
+ const pinFn = isPinned(chatId) ? 'unpinChat' : 'pinChat';
580853
+ let html = '';
580854
+ html += '<button class="sb-menu-item" onclick="_chatMenuRename(\\'' + safeChat + '\\')">Rename</button>';
580855
+ html += '<button class="sb-menu-item" onclick="' + pinFn + '(\\'' + safeChat + '\\'); _hideChatRowMenu()">' + pinLabel + '</button>';
580856
+ if (folders.length === 0) {
580857
+ html += '<button class="sb-menu-item" onclick="_chatMenuNewFolder(\\'' + safeChat + '\\')">Move to new folder</button>';
580858
+ } else {
580859
+ html += '<div class="sb-menu-sub">Move to folder</div>';
580860
+ html += '<button class="sb-menu-item sub" onclick="moveChatToFolder(\\'' + safeChat + '\\', null); _hideChatRowMenu()">(none)</button>';
580861
+ for (const f of folders) {
580862
+ const safeF = String(f.id).replace(/'/g, "\\\\'");
580863
+ const safeName = String(f.name).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
580864
+ html += '<button class="sb-menu-item sub" onclick="moveChatToFolder(\\'' + safeChat + '\\', \\'' + safeF + '\\'); _hideChatRowMenu()">' + safeName + '</button>';
580865
+ }
580866
+ html += '<button class="sb-menu-item sub" onclick="_chatMenuNewFolder(\\'' + safeChat + '\\')">+ New folder...</button>';
580867
+ }
580868
+ html += '<button class="sb-menu-item danger" onclick="_chatMenuDelete(\\'' + safeChat + '\\')">Delete</button>';
580869
+ menu.innerHTML = html;
580870
+ document.body.appendChild(menu);
580871
+ // Position near the click
580872
+ const x = Math.min(evt.clientX, window.innerWidth - 200);
580873
+ const y = Math.min(evt.clientY, window.innerHeight - 220);
580874
+ menu.style.left = x + 'px';
580875
+ menu.style.top = y + 'px';
580876
+ setTimeout(() => document.addEventListener('click', _hideChatRowMenu, { once: true }), 0);
580877
+ }
580878
+ function _hideChatRowMenu() {
580879
+ const m = document.getElementById('sb-chat-menu');
580880
+ if (m) m.remove();
580881
+ }
580882
+ function _chatMenuRename(chatId) {
580883
+ _hideChatRowMenu();
580884
+ const sessions = (typeof $chatSessions !== 'undefined' && $chatSessions.get) ? $chatSessions.get() : {};
580885
+ const cur = (sessions[chatId] && (sessions[chatId].title || sessions[chatId].name)) || '';
580886
+ const next = prompt('Rename chat to:', cur || '');
580887
+ if (next === null) return;
580888
+ renameChatTitle(chatId, next);
580889
+ _renderSidebarChats(_lastSidebarFilter || '');
580890
+ }
580891
+ function _chatMenuDelete(chatId) {
580892
+ _hideChatRowMenu();
580893
+ if (!confirm('Delete this chat?')) return;
580894
+ deleteChatById(chatId);
580895
+ _renderSidebarChats(_lastSidebarFilter || '');
580896
+ }
580897
+ function _chatMenuNewFolder(chatId) {
580898
+ _hideChatRowMenu();
580899
+ const name = prompt('New folder name:', 'Folder');
580900
+ if (!name) return;
580901
+ const id = createChatFolder(name);
580902
+ if (chatId) moveChatToFolder(chatId, id);
580903
+ }
580904
+
580905
+ // Folder row context menu
580906
+ function _showFolderRowMenu(evt, folderId) {
580907
+ evt.stopPropagation();
580908
+ evt.preventDefault();
580909
+ _hideChatRowMenu();
580910
+ const menu = document.createElement('div');
580911
+ menu.id = 'sb-chat-menu';
580912
+ menu.style.cssText = 'position:fixed;background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 16px rgba(0,0,0,0.4);min-width:160px;z-index:120;font-family:var(--font-ui);font-size:0.78rem;overflow:hidden';
580913
+ const safeF = String(folderId).replace(/'/g, "\\\\'");
580914
+ menu.innerHTML =
580915
+ '<button class="sb-menu-item" onclick="_folderMenuRename(\\'' + safeF + '\\')">Rename folder</button>' +
580916
+ '<button class="sb-menu-item danger" onclick="_folderMenuDelete(\\'' + safeF + '\\')">Delete folder</button>';
580917
+ document.body.appendChild(menu);
580918
+ const x = Math.min(evt.clientX, window.innerWidth - 200);
580919
+ const y = Math.min(evt.clientY, window.innerHeight - 100);
580920
+ menu.style.left = x + 'px';
580921
+ menu.style.top = y + 'px';
580922
+ setTimeout(() => document.addEventListener('click', _hideChatRowMenu, { once: true }), 0);
580923
+ }
580924
+ function _folderMenuRename(folderId) {
580925
+ _hideChatRowMenu();
580926
+ const f = getChatFolders().find(x => x.id === folderId);
580927
+ if (!f) return;
580928
+ const next = prompt('Rename folder to:', f.name);
580929
+ if (next == null) return;
580930
+ renameChatFolder(folderId, next);
580931
+ }
580932
+ function _folderMenuDelete(folderId) {
580933
+ _hideChatRowMenu();
580934
+ if (!confirm('Delete folder? Chats inside will move to the unsorted list.')) return;
580935
+ deleteChatFolder(folderId);
580936
+ }
580937
+
580938
+ // Drag handlers — chat rows are draggable, folder headers are drop targets.
580939
+ function _onChatDragStart(ev, chatId) {
580940
+ ev.dataTransfer.setData('text/plain', 'oa-chat:' + chatId);
580941
+ ev.dataTransfer.effectAllowed = 'move';
580942
+ }
580943
+ function _onFolderDragOver(ev) {
580944
+ ev.preventDefault();
580945
+ ev.dataTransfer.dropEffect = 'move';
580946
+ ev.currentTarget.classList.add('drop-target');
580947
+ }
580948
+ function _onFolderDragLeave(ev) {
580949
+ ev.currentTarget.classList.remove('drop-target');
580950
+ }
580951
+ function _onFolderDrop(ev, folderId) {
580952
+ ev.preventDefault();
580953
+ ev.currentTarget.classList.remove('drop-target');
580954
+ const data = ev.dataTransfer.getData('text/plain') || '';
580955
+ if (!data.startsWith('oa-chat:')) return;
580956
+ const chatId = data.slice('oa-chat:'.length);
580957
+ moveChatToFolder(chatId, folderId);
580958
+ }
580959
+
580960
+ // Build the new-folder UI button (lives at the top of the chats nav)
580961
+ function _renderSidebarFoldersToolbar() {
580962
+ const host = document.getElementById('sidebar-folder-tools');
580963
+ if (!host) return;
580964
+ host.innerHTML =
580965
+ '<button class="sb-folder-action" onclick="(function(){const n=prompt(\\'New folder name:\\',\\'Folder\\');if(n)createChatFolder(n);})()" title="New folder">+ folder</button>';
580966
+ }
580967
+
580968
+ let _lastSidebarFilter = '';
580969
+
580970
+ function _renderSidebarChats(filter) {
580971
+ const list = document.getElementById('sidebar-recent-list');
580972
+ if (!list) return;
580973
+ _lastSidebarFilter = filter || '';
580974
+ _renderSidebarFoldersToolbar();
580975
+
580976
+ const sessions = (typeof $chatSessions !== 'undefined' && $chatSessions.get) ? $chatSessions.get() : {};
580977
+ const allIds = Object.keys(sessions || {});
580978
+ const q = (filter || '').trim().toLowerCase();
580979
+ const activeId = (typeof $currentSession !== 'undefined' && $currentSession.get) ? $currentSession.get() : null;
580980
+
580981
+ const folders = getChatFolders();
580982
+ const pinned = getChatPinned();
580983
+ const folderMap = getChatFolderMap();
580984
+ const folderOpen = getFoldersOpenState();
580985
+
580986
+ // Sort helper
580987
+ const sortByRecency = (a, b) => {
580988
+ const sa = sessions[a] || {}, sb = sessions[b] || {};
580989
+ const ta = sa.updated_at || sa.created_at || a;
580990
+ const tb = sb.updated_at || sb.created_at || b;
580991
+ return String(tb).localeCompare(String(ta));
580992
+ };
580993
+ allIds.sort(sortByRecency);
580994
+
580995
+ // Filter helper — match title OR last message snippet (best effort).
580996
+ const matches = (id) => {
580997
+ if (!q) return true;
580998
+ const s = sessions[id] || {};
580999
+ const title = String(s.title || s.name || '');
581000
+ if (title.toLowerCase().includes(q)) return true;
581001
+ const last = (Array.isArray(s.messages) && s.messages.length)
581002
+ ? String(s.messages[s.messages.length - 1].content || '')
581003
+ : '';
581004
+ return last.toLowerCase().includes(q);
581005
+ };
581006
+
581007
+ // Build rendering buckets
581008
+ const inFolder = {};
581009
+ for (const f of folders) inFolder[f.id] = [];
581010
+ const unsorted = [];
581011
+ for (const id of allIds) {
581012
+ if (!matches(id)) continue;
581013
+ const fId = folderMap[id];
581014
+ if (fId && inFolder[fId]) inFolder[fId].push(id);
581015
+ else unsorted.push(id);
581016
+ }
581017
+
581018
+ const renderRow = (id) => {
581019
+ const s = sessions[id] || {};
581020
+ const title = (s.title || s.name || ('Chat ' + String(id).slice(0, 8))).toString();
581021
+ const cls = 'sb-chat' + (id === activeId ? ' active' : '');
581022
+ const safeId = String(id).replace(/'/g, "\\\\'");
581023
+ const safeTitle = title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
581024
+ return '<div class="' + cls + '" draggable="true" ondragstart="_onChatDragStart(event,\\'' + safeId + '\\')" onclick="switchTab(\\'chat\\'); switchSession(\\'' + safeId + '\\')" title="' + safeTitle + '">' +
581025
+ '<span class="sb-chat-title">' + safeTitle + '</span>' +
581026
+ '<button class="sb-chat-menu" onclick="_showChatRowMenu(event,\\'' + safeId + '\\')" title="More">&#x22EE;</button>' +
581027
+ '</div>';
581028
+ };
581029
+
581030
+ // Pinned section
581031
+ const pinnedHost = document.getElementById('sidebar-pinned-list');
581032
+ const pinnedSect = document.getElementById('sidebar-pinned-section');
581033
+ if (pinnedHost && pinnedSect) {
581034
+ const pinnedFiltered = pinned.filter(id => allIds.includes(id) && matches(id));
581035
+ if (pinnedFiltered.length === 0) {
581036
+ pinnedSect.style.display = 'none';
581037
+ pinnedHost.innerHTML = '';
581038
+ } else {
581039
+ pinnedSect.style.display = 'block';
581040
+ pinnedHost.innerHTML = pinnedFiltered.map(renderRow).join('');
581041
+ }
581042
+ }
581043
+
581044
+ // Build folders + unsorted
581045
+ const items = [];
581046
+ for (const f of folders) {
581047
+ const ids = inFolder[f.id] || [];
581048
+ if (q && ids.length === 0) continue; // hide empty folders during a search
581049
+ const open = folderOpen[f.id] !== false; // default open
581050
+ const safeF = String(f.id).replace(/'/g, "\\\\'");
581051
+ const safeName = String(f.name).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
581052
+ items.push(
581053
+ '<div class="sb-folder" data-folder="' + safeF + '" ondragover="_onFolderDragOver(event)" ondragleave="_onFolderDragLeave(event)" ondrop="_onFolderDrop(event,\\'' + safeF + '\\')">' +
581054
+ '<div class="sb-folder-row" onclick="toggleChatFolder(\\'' + safeF + '\\')">' +
581055
+ '<span class="sb-folder-caret">' + (open ? '▾' : '▸') + '</span>' +
581056
+ '<span class="sb-folder-name">' + safeName + '</span>' +
581057
+ '<span class="sb-folder-count">' + ids.length + '</span>' +
581058
+ '<button class="sb-folder-menu" onclick="_showFolderRowMenu(event,\\'' + safeF + '\\')" title="More">&#x22EE;</button>' +
581059
+ '</div>' +
581060
+ (open ? '<div class="sb-folder-body">' + (ids.length ? ids.map(renderRow).join('') : '<div class="sb-folder-empty">empty</div>') + '</div>' : '') +
581061
+ '</div>'
581062
+ );
581063
+ }
581064
+ for (const id of unsorted) {
581065
+ items.push(renderRow(id));
581066
+ }
581067
+
581068
+ if (items.length === 0) {
581069
+ list.innerHTML = '<div style="padding:14px 8px;color:var(--color-fg-faint);font-size:0.7rem;text-align:center">' + (q ? 'No matches' : 'No chats yet') + '</div>';
581070
+ } else {
581071
+ list.innerHTML = items.join('');
581072
+ }
581073
+ }
581074
+
581075
+ function filterSidebarChats(q) {
581076
+ _renderSidebarChats(q);
581077
+ }
581078
+
581079
+ // Initialize sidebar state + observers.
581080
+ (function _initSidebar() {
581081
+ _restoreSidebarState();
581082
+ _wireSidebarResize();
581083
+ _syncSidebarStatus();
581084
+ _renderSidebarChats('');
581085
+ // Re-render recent list whenever the sessions store changes.
581086
+ if (typeof $chatSessions !== 'undefined' && $chatSessions.subscribe) {
581087
+ $chatSessions.subscribe(() => {
581088
+ const search = document.getElementById('sidebar-search');
581089
+ _renderSidebarChats(search ? search.value : '');
581090
+ });
581091
+ }
581092
+ if (typeof $currentSession !== 'undefined' && $currentSession.subscribe) {
581093
+ $currentSession.subscribe(() => {
581094
+ const search = document.getElementById('sidebar-search');
581095
+ _renderSidebarChats(search ? search.value : '');
581096
+ });
581097
+ }
581098
+ // Poll the legacy status DOM every 2s — cheap and avoids touching every
581099
+ // call site that updates #status.
581100
+ setInterval(_syncSidebarStatus, 2000);
581101
+ // Mark the initial active tab on the sidebar nav.
581102
+ const initial = (typeof $activeTab !== 'undefined' && $activeTab.get) ? $activeTab.get() : 'chat';
581103
+ document.querySelectorAll('#oa-sidebar .sb-nav').forEach(b => {
581104
+ b.classList.toggle('active', b.getAttribute('data-tab') === initial);
581105
+ });
581106
+ // Cmd/Ctrl+B toggles sidebar.
581107
+ document.addEventListener('keydown', (e) => {
581108
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'b' || e.key === 'B')) {
581109
+ e.preventDefault();
581110
+ toggleSidebar();
581111
+ }
581112
+ });
581113
+ })();
581114
+
581115
+ // ════════════════════════════════════════════════════════════
581116
+ // OWUI-5: Settings + Model manager modals
581117
+ // ════════════════════════════════════════════════════════════
581118
+ function openSettingsModal(initialPane) {
581119
+ const m = document.getElementById('settings-modal');
581120
+ if (!m) return;
581121
+ m.style.display = 'flex';
581122
+ document.body.style.overflow = 'hidden';
581123
+ if (initialPane) openSettingsPane(initialPane);
581124
+ }
581125
+ function closeSettingsModal() {
581126
+ const m = document.getElementById('settings-modal');
581127
+ if (!m) return;
581128
+ m.style.display = 'none';
581129
+ document.body.style.overflow = '';
581130
+ }
581131
+ function openSettingsPane(name) {
581132
+ document.querySelectorAll('#settings-modal .rail-tab').forEach(t => {
581133
+ t.classList.toggle('active', t.getAttribute('data-pane') === name);
581134
+ });
581135
+ document.querySelectorAll('#settings-modal .settings-pane').forEach(p => {
581136
+ const isThis = (p.id === 'settings-pane-' + name);
581137
+ p.hidden = !isThis;
581138
+ p.classList.toggle('active', isThis);
581139
+ });
581140
+ // Lazy-load each pane's data on first open.
581141
+ if (name === 'connections') { try { loadSettingsConnections(); } catch {} }
581142
+ if (name === 'models') { try { loadSettingsModels(); } catch {} }
581143
+ if (name === 'voice') { try { loadSettingsVoice(); } catch {} }
581144
+ if (name === 'profiles') { try { loadSettingsProfiles(); } catch {} }
581145
+ }
581146
+
581147
+ // Connections — pulls /v1/config and renders an inline edit form.
581148
+ async function loadSettingsConnections() {
581149
+ const host = document.getElementById('settings-connections-host');
581150
+ if (!host) return;
581151
+ host.innerHTML = '<div style="color:var(--color-fg-muted);font-size:0.74rem">loading...</div>';
581152
+ try {
581153
+ const r = await fetch('/v1/config', { headers: headers() });
581154
+ if (r.status !== 200) {
581155
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">/v1/config returned ' + r.status + '</div>';
581156
+ return;
581157
+ }
581158
+ const cfg = await r.json();
581159
+ const ep = (cfg && (cfg.endpoint || cfg.backend_url)) || '';
581160
+ const safeEp = String(ep).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
581161
+ host.innerHTML =
581162
+ '<label style="display:block;font-size:0.78rem;margin-bottom:6px">Backend endpoint</label>' +
581163
+ '<input id="settings-conn-endpoint" type="text" value="' + safeEp + '" style="width:100%;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:6px 10px;border-radius:var(--radius-sm);font-size:0.78rem">' +
581164
+ '<div style="margin-top:10px"><button class="oa-btn-secondary" onclick="saveSettingsConnections()">save</button></div>' +
581165
+ '<div id="settings-conn-status" style="margin-top:8px;font-size:0.7rem;color:var(--color-fg-muted)"></div>';
581166
+ } catch (e) {
581167
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">' + e.message + '</div>';
581168
+ }
581169
+ }
581170
+ async function saveSettingsConnections() {
581171
+ const inp = document.getElementById('settings-conn-endpoint');
581172
+ const status = document.getElementById('settings-conn-status');
581173
+ if (!inp || !status) return;
581174
+ status.textContent = 'saving...';
581175
+ try {
581176
+ const r = await fetch('/v1/config/endpoint', {
581177
+ method: 'POST',
581178
+ headers: headers(),
581179
+ body: JSON.stringify({ endpoint: inp.value }),
581180
+ });
581181
+ if (r.status === 200) status.textContent = 'saved';
581182
+ else status.textContent = 'failed (' + r.status + ')';
581183
+ } catch (e) {
581184
+ status.textContent = 'error: ' + e.message;
581185
+ }
581186
+ }
581187
+
581188
+ // Models pane
581189
+ let _settingsModelsCache = [];
581190
+ async function loadSettingsModels() {
581191
+ const host = document.getElementById('settings-models-list');
581192
+ if (!host) return;
581193
+ host.innerHTML = '<div style="padding:8px;color:var(--color-fg-muted);font-size:0.74rem">loading...</div>';
581194
+ try {
581195
+ const r = await fetch('/v1/models', { headers: headers() });
581196
+ if (r.status !== 200) {
581197
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">/v1/models returned ' + r.status + '</div>';
581198
+ return;
581199
+ }
581200
+ const j = await r.json();
581201
+ const arr = Array.isArray(j) ? j : (Array.isArray(j.data) ? j.data : (Array.isArray(j.models) ? j.models : []));
581202
+ _settingsModelsCache = arr.map(m => ({
581203
+ id: m.id || m.name || String(m),
581204
+ detail: (m.size ? Math.round(Number(m.size) / 1e9 * 10) / 10 + 'GB' : '') + (m.modified_at ? '' : ''),
581205
+ }));
581206
+ filterSettingsModels(document.getElementById('settings-models-search').value || '');
581207
+ } catch (e) {
581208
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">' + e.message + '</div>';
581209
+ }
581210
+ }
581211
+ function filterSettingsModels(q) {
581212
+ const host = document.getElementById('settings-models-list');
581213
+ if (!host) return;
581214
+ const sel = document.getElementById('model-select');
581215
+ const active = sel ? sel.value : '';
581216
+ const ql = String(q || '').toLowerCase();
581217
+ const rows = _settingsModelsCache
581218
+ .filter(m => !ql || m.id.toLowerCase().includes(ql))
581219
+ .map(m => {
581220
+ const isAct = m.id === active;
581221
+ const safe = m.id.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;');
581222
+ return '<div class="row' + (isAct ? ' active' : '') + '" onclick="setActiveModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')"><span>' + safe + '</span><span class="right">' + (m.detail || '') + '</span></div>';
581223
+ }).join('');
581224
+ host.innerHTML = rows || '<div style="padding:8px;color:var(--color-fg-muted);font-size:0.74rem">No models</div>';
581225
+ }
581226
+ function setActiveModel(id) {
581227
+ const sel = document.getElementById('model-select');
581228
+ if (!sel) return;
581229
+ sel.value = id;
581230
+ sel.dispatchEvent(new Event('change'));
581231
+ filterSettingsModels(document.getElementById('settings-models-search').value || '');
581232
+ }
581233
+
581234
+ // Voice settings — clone refs CRUD via existing /v1/voice/clones.
581235
+ async function loadSettingsVoice() {
581236
+ const host = document.getElementById('settings-voice-host');
581237
+ if (!host) return;
581238
+ host.innerHTML = '<div style="color:var(--color-fg-muted);font-size:0.74rem">loading...</div>';
581239
+ try {
581240
+ const r = await fetch('/v1/voice/clones', { headers: headers() });
581241
+ if (r.status !== 200) {
581242
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">/v1/voice/clones returned ' + r.status + '</div>';
581243
+ return;
581244
+ }
581245
+ const j = await r.json();
581246
+ const arr = Array.isArray(j) ? j : (Array.isArray(j.clones) ? j.clones : (Array.isArray(j.refs) ? j.refs : []));
581247
+ if (!arr.length) {
581248
+ host.innerHTML = '<div style="color:var(--color-fg-muted);font-size:0.78rem">No clone refs registered. Use the Voice tab to upload a reference.</div>';
581249
+ return;
581250
+ }
581251
+ host.innerHTML = '<div style="font-size:0.78rem;display:flex;flex-direction:column;gap:6px">' +
581252
+ arr.map(c => {
581253
+ const id = c.id || c.name || '';
581254
+ const lbl = (c.label || c.name || id).toString();
581255
+ const safe = lbl.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
581256
+ return '<div class="row" style="display:flex;align-items:center;justify-content:space-between"><span>' + safe + '</span><span class="right">' + (id || '') + '</span></div>';
581257
+ }).join('') +
581258
+ '</div><div style="margin-top:10px"><button class="oa-btn-secondary" onclick="switchTab(\\'voice\\'); closeSettingsModal()">manage in voice tab</button></div>';
581259
+ } catch (e) {
581260
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">' + e.message + '</div>';
581261
+ }
581262
+ }
581263
+
581264
+ // Profiles pane — defers to existing loadProfiles()
581265
+ async function loadSettingsProfiles() {
581266
+ const host = document.getElementById('settings-profiles-host');
581267
+ if (!host) return;
581268
+ host.innerHTML = '<div style="color:var(--color-fg-muted);font-size:0.74rem">opening agent profiles...</div>';
581269
+ try {
581270
+ if (typeof loadProfiles === 'function') await loadProfiles();
581271
+ host.innerHTML = '<div style="color:var(--color-fg-muted);font-size:0.78rem">Profiles loaded into the Agent tab — switch to Agent to manage them.</div>' +
581272
+ '<div style="margin-top:10px"><button class="oa-btn-secondary" onclick="switchTab(\\'agent\\'); closeSettingsModal()">open agent tab</button></div>';
581273
+ } catch (e) {
581274
+ host.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">' + e.message + '</div>';
581275
+ }
581276
+ }
581277
+
581278
+ // Theme + font scale (stub wiring; OWUI-5 introduces the toggle, light
581279
+ // mode polish is explicitly out of scope per the audit doc).
581280
+ function setOATheme(t) {
581281
+ document.documentElement.setAttribute('data-theme', t);
581282
+ try { localStorage.setItem('oa.theme', t); } catch {}
581283
+ }
581284
+ function setOAFontScale(v) {
581285
+ const n = parseFloat(v);
581286
+ if (!Number.isFinite(n)) return;
581287
+ document.documentElement.style.fontSize = (n * 100) + '%';
581288
+ const disp = document.getElementById('oa-font-scale-display');
581289
+ if (disp) disp.textContent = n.toFixed(2) + 'x';
581290
+ try { localStorage.setItem('oa.font-scale', String(n)); } catch {}
581291
+ }
581292
+
581293
+ // Restore preferences on init
581294
+ (function _restoreOAPrefs() {
581295
+ try {
581296
+ const t = localStorage.getItem('oa.theme'); if (t) document.documentElement.setAttribute('data-theme', t);
581297
+ const f = localStorage.getItem('oa.font-scale'); if (f) document.documentElement.style.fontSize = (parseFloat(f) * 100) + '%';
581298
+ } catch {}
581299
+ })();
581300
+
581301
+ // Model manager modal — separate from the settings/Models pane: bigger
581302
+ // list with a side detail pane. Reuses /v1/models.
581303
+ let _modelManagerCache = [];
581304
+ let _modelManagerSelected = null;
581305
+ function openModelManager() {
581306
+ const m = document.getElementById('model-manager-modal');
581307
+ if (!m) return;
581308
+ m.style.display = 'flex';
581309
+ document.body.style.overflow = 'hidden';
581310
+ loadModelManager();
581311
+ }
581312
+ function closeModelManager() {
581313
+ const m = document.getElementById('model-manager-modal');
581314
+ if (!m) return;
581315
+ m.style.display = 'none';
581316
+ document.body.style.overflow = '';
581317
+ }
581318
+ async function loadModelManager() {
581319
+ const list = document.getElementById('mm-list');
581320
+ if (!list) return;
581321
+ list.innerHTML = '<div style="padding:8px;color:var(--color-fg-muted);font-size:0.74rem">loading...</div>';
581322
+ try {
581323
+ const r = await fetch('/v1/models', { headers: headers() });
581324
+ if (r.status !== 200) {
581325
+ list.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">/v1/models returned ' + r.status + '</div>';
581326
+ return;
581327
+ }
581328
+ const j = await r.json();
581329
+ const arr = Array.isArray(j) ? j : (Array.isArray(j.data) ? j.data : (Array.isArray(j.models) ? j.models : []));
581330
+ _modelManagerCache = arr;
581331
+ filterModelManager(document.getElementById('mm-search').value || '');
581332
+ } catch (e) {
581333
+ list.innerHTML = '<div style="color:var(--color-error);font-size:0.74rem">' + e.message + '</div>';
581334
+ }
581335
+ }
581336
+ function filterModelManager(q) {
581337
+ const list = document.getElementById('mm-list');
581338
+ if (!list) return;
581339
+ const ql = String(q || '').toLowerCase();
581340
+ const rows = _modelManagerCache
581341
+ .filter(m => {
581342
+ const id = m.id || m.name || '';
581343
+ return !ql || String(id).toLowerCase().includes(ql);
581344
+ })
581345
+ .map((m, i) => {
581346
+ const id = m.id || m.name || '';
581347
+ const safe = String(id).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;');
581348
+ const sel = (_modelManagerSelected === id) ? ' selected' : '';
581349
+ return '<div class="row' + sel + '" data-id="' + safe + '" onclick="_modelManagerSelect(\\'' + String(id).replace(/'/g, "\\\\'") + '\\')"><span>' + safe + '</span><span class="right">' + (m.size ? Math.round(Number(m.size) / 1e9 * 10) / 10 + 'GB' : '') + '</span></div>';
581350
+ }).join('');
581351
+ list.innerHTML = rows || '<div style="padding:8px;color:var(--color-fg-muted);font-size:0.74rem">No models</div>';
581352
+ }
581353
+ function _modelManagerSelect(id) {
581354
+ _modelManagerSelected = id;
581355
+ filterModelManager(document.getElementById('mm-search').value || '');
581356
+ const detail = document.getElementById('mm-detail');
581357
+ if (!detail) return;
581358
+ const m = _modelManagerCache.find(x => (x.id || x.name) === id) || {};
581359
+ const safe = String(id).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;');
581360
+ const rows = [
581361
+ ['ID', safe],
581362
+ ['Size', m.size ? Math.round(Number(m.size) / 1e9 * 10) / 10 + 'GB' : 'unknown'],
581363
+ ['Modified', m.modified_at || m.modifiedAt || '—'],
581364
+ ['Family', m.details && m.details.family ? m.details.family : '—'],
581365
+ ['Quantization', m.details && m.details.quantization_level ? m.details.quantization_level : '—'],
581366
+ ];
581367
+ detail.innerHTML =
581368
+ '<div style="padding:14px;font-size:0.78rem">' +
581369
+ '<h4 style="margin:0 0 10px;color:var(--color-fg)">' + safe + '</h4>' +
581370
+ '<table style="width:100%;border-collapse:collapse">' +
581371
+ rows.map(([k, v]) => '<tr><td style="color:var(--color-fg-muted);padding:3px 6px 3px 0;width:35%">' + k + '</td><td style="color:var(--color-fg);padding:3px 0">' + v + '</td></tr>').join('') +
581372
+ '</table>' +
581373
+ '<div style="margin-top:14px;display:flex;gap:6px;flex-wrap:wrap">' +
581374
+ '<button class="oa-btn-secondary" onclick="setActiveModel(\\'' + String(id).replace(/'/g, "\\\\'") + '\\')">set active</button>' +
581375
+ '<a class="oa-btn-secondary" href="/v1/models" target="_blank" style="text-decoration:none;display:inline-block">raw json</a>' +
581376
+ '</div>' +
581377
+ '</div>';
581378
+ }
581379
+
581380
+ // Global keybindings
581381
+ document.addEventListener('keydown', (e) => {
581382
+ // Escape closes the topmost modal.
581383
+ if (e.key === 'Escape') {
581384
+ const sm = document.getElementById('settings-modal');
581385
+ const mm = document.getElementById('model-manager-modal');
581386
+ if (mm && mm.style.display === 'flex') { closeModelManager(); return; }
581387
+ if (sm && sm.style.display === 'flex') { closeSettingsModal(); return; }
581388
+ }
581389
+ // Cmd/Ctrl+, opens settings.
581390
+ if ((e.metaKey || e.ctrlKey) && e.key === ',') {
581391
+ e.preventDefault();
581392
+ openSettingsModal('general');
581393
+ }
581394
+ });
581395
+
581396
+ // ════════════════════════════════════════════════════════════
581397
+ // OWUI-4: Input toolbar + slash palette + drop-to-attach
581398
+ // ════════════════════════════════════════════════════════════
581399
+
581400
+ let _attachments = []; // { name, size, file }
581401
+ let _slashItems = []; // cached commands list from /v1/commands
581402
+ let _slashSelectedIdx = 0;
581403
+ let _slashLoaded = false;
581404
+
581405
+ function _slashPaletteIsOpen() {
581406
+ const p = document.getElementById('slash-palette');
581407
+ return p && p.style.display !== 'none';
581408
+ }
581409
+ async function _ensureSlashList() {
581410
+ if (_slashLoaded) return _slashItems;
581411
+ try {
581412
+ const r = await fetch('/v1/commands', { headers: headers() });
581413
+ if (r.status === 200) {
581414
+ const j = await r.json();
581415
+ // The endpoint returns either an array of {name, description} or
581416
+ // an envelope { commands: [...] }. Be tolerant.
581417
+ const arr = Array.isArray(j) ? j : (Array.isArray(j.commands) ? j.commands : []);
581418
+ _slashItems = arr.map(c => ({
581419
+ name: (c.name || c.cmd || c.command || '').replace(/^\\//, ''),
581420
+ desc: c.description || c.desc || c.summary || '',
581421
+ })).filter(c => c.name);
581422
+ }
581423
+ } catch {}
581424
+ _slashLoaded = true;
581425
+ return _slashItems;
581426
+ }
581427
+
581428
+ function openSlashPalette() {
581429
+ const v = (input.value || '').trim();
581430
+ if (!v.startsWith('/')) {
581431
+ input.value = '/' + (input.value || '');
581432
+ }
581433
+ input.focus();
581434
+ _maybeUpdateSlashPalette();
581435
+ }
581436
+
581437
+ function closeSlashPalette() {
581438
+ const p = document.getElementById('slash-palette');
581439
+ if (p) p.style.display = 'none';
581440
+ _slashSelectedIdx = 0;
581441
+ }
581442
+
581443
+ async function _maybeUpdateSlashPalette() {
581444
+ const v = input.value || '';
581445
+ const p = document.getElementById('slash-palette');
581446
+ if (!p) return;
581447
+ if (!v.startsWith('/')) {
581448
+ p.style.display = 'none';
581449
+ return;
581450
+ }
581451
+ await _ensureSlashList();
581452
+ const q = v.slice(1).split(/\\s/)[0].toLowerCase();
581453
+ const matches = _slashItems
581454
+ .filter(c => c.name.toLowerCase().startsWith(q))
581455
+ .slice(0, 12);
581456
+ if (matches.length === 0) { p.style.display = 'none'; return; }
581457
+ if (_slashSelectedIdx >= matches.length) _slashSelectedIdx = 0;
581458
+ const list = document.getElementById('slash-palette-list');
581459
+ if (!list) return;
581460
+ list.innerHTML = matches.map((c, i) => {
581461
+ const sel = (i === _slashSelectedIdx) ? ' selected' : '';
581462
+ const safeDesc = String(c.desc || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
581463
+ const safeName = c.name.replace(/[^a-zA-Z0-9_:.\\-]/g, '');
581464
+ return '<div class="slash-row' + sel + '" data-cmd="' + safeName + '" onclick="_slashPaletteAcceptByName(\\'' + safeName + '\\')"><span class="slash-name">/' + safeName + '</span><span class="slash-desc">' + safeDesc + '</span></div>';
581465
+ }).join('');
581466
+ p.style.display = 'block';
581467
+ }
581468
+
581469
+ function _slashPaletteMove(delta) {
581470
+ const list = document.getElementById('slash-palette-list');
581471
+ if (!list) return;
581472
+ const rows = list.querySelectorAll('.slash-row');
581473
+ if (rows.length === 0) return;
581474
+ _slashSelectedIdx = (_slashSelectedIdx + delta + rows.length) % rows.length;
581475
+ rows.forEach((r, i) => r.classList.toggle('selected', i === _slashSelectedIdx));
581476
+ rows[_slashSelectedIdx].scrollIntoView({ block: 'nearest' });
581477
+ }
581478
+
581479
+ function _slashPaletteAccept() {
581480
+ const list = document.getElementById('slash-palette-list');
581481
+ if (!list) return;
581482
+ const row = list.querySelectorAll('.slash-row')[_slashSelectedIdx];
581483
+ if (!row) return;
581484
+ const name = row.getAttribute('data-cmd');
581485
+ _slashPaletteAcceptByName(name);
581486
+ }
581487
+
581488
+ function _slashPaletteAcceptByName(name) {
581489
+ if (!name) return;
581490
+ // Replace just the slash-token in the input, preserving any args the
581491
+ // user may have already typed after a space.
581492
+ const v = input.value || '';
581493
+ const idx = v.indexOf(' ');
581494
+ const trailing = idx >= 0 ? v.slice(idx) : '';
581495
+ input.value = '/' + name + (trailing || ' ');
581496
+ closeSlashPalette();
581497
+ input.focus();
581498
+ // Cursor after the inserted command name.
581499
+ try {
581500
+ const pos = ('/' + name + ' ').length;
581501
+ input.setSelectionRange(pos, pos);
581502
+ } catch {}
581503
+ }
581504
+
581505
+ // Attachments
581506
+ function handleAttachInput(fileList) {
581507
+ if (!fileList || fileList.length === 0) return;
581508
+ for (const f of fileList) {
581509
+ _attachments.push({ name: f.name, size: f.size || 0, file: f });
581510
+ }
581511
+ _renderAttachChips();
581512
+ }
581513
+
581514
+ function _renderAttachChips() {
581515
+ const row = document.getElementById('attach-chips');
581516
+ if (!row) return;
581517
+ if (_attachments.length === 0) {
581518
+ row.innerHTML = '';
581519
+ row.style.display = 'none';
581520
+ return;
581521
+ }
581522
+ row.style.display = 'flex';
581523
+ row.innerHTML = _attachments.map((a, i) => {
581524
+ const safeName = String(a.name).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
581525
+ const sizeKB = a.size > 0 ? ' (' + Math.round(a.size / 1024) + 'KB)' : '';
581526
+ return '<span class="chip"><span class="name" title="' + safeName + '">' + safeName + sizeKB + '</span><button class="x" onclick="_removeAttachment(' + i + ')" title="Remove">&times;</button></span>';
581527
+ }).join('');
581528
+ }
581529
+ function _removeAttachment(i) {
581530
+ _attachments.splice(i, 1);
581531
+ _renderAttachChips();
581532
+ }
581533
+
581534
+ // Drag & drop on the input row
581535
+ (function _wireDropZone() {
581536
+ const row = document.getElementById('input-row');
581537
+ if (!row) return;
581538
+ ['dragover', 'dragenter'].forEach(ev => row.addEventListener(ev, (e) => {
581539
+ e.preventDefault(); e.stopPropagation();
581540
+ row.classList.add('dragover');
581541
+ }));
581542
+ ['dragleave', 'drop'].forEach(ev => row.addEventListener(ev, (e) => {
581543
+ e.preventDefault(); e.stopPropagation();
581544
+ row.classList.remove('dragover');
581545
+ }));
581546
+ row.addEventListener('drop', (e) => {
581547
+ const files = e.dataTransfer && e.dataTransfer.files;
581548
+ if (files && files.length) handleAttachInput(files);
581549
+ });
581550
+ })();
581551
+
581552
+ // Mic toggle — defers to the existing voice-tab plumbing; this is
581553
+ // a shortcut from the input toolbar so users don't have to navigate
581554
+ // to the voice tab to toggle voicechat.
581555
+ function toggleVoiceMicFromInput() {
581556
+ // Two paths to flip the live mic, depending on which one is wired
581557
+ // in the current session.
581558
+ let triggered = false;
581559
+ try {
581560
+ if (typeof toggleVoiceChat === 'function') { toggleVoiceChat(); triggered = true; }
581561
+ else if (typeof toggleListenEngine === 'function') { toggleListenEngine(); triggered = true; }
581562
+ else if (typeof toggleVoice === 'function') { toggleVoice(); triggered = true; }
581563
+ } catch {}
581564
+ // As a last resort, jump the user to the voice tab so they can
581565
+ // engage from there.
581566
+ if (!triggered) { try { switchTab('voice'); } catch {} }
581567
+ // Active-state visualization on the toolbar mic icon.
581568
+ const btn = document.getElementById('mic-btn');
581569
+ if (btn) btn.classList.toggle('active');
581570
+ }
581571
+
581572
+ // Model picker mirror — keep #input-model-mini in sync with the legacy
581573
+ // #model-select so OWUI-4's compact picker reflects the truth.
581574
+ (function _mirrorModelPicker() {
581575
+ const sync = () => {
581576
+ const src = document.getElementById('model-select');
581577
+ const dst = document.getElementById('input-model-mini');
581578
+ if (!src || !dst) return;
581579
+ if (src.options.length !== dst.options.length) {
581580
+ dst.innerHTML = '';
581581
+ for (const o of src.options) {
581582
+ const opt = document.createElement('option');
581583
+ opt.value = o.value;
581584
+ opt.textContent = o.textContent;
581585
+ dst.appendChild(opt);
581586
+ }
581587
+ }
581588
+ dst.value = src.value;
581589
+ };
581590
+ // Run after every load + on a small interval.
581591
+ setTimeout(sync, 600);
581592
+ setInterval(sync, 4000);
581593
+ })();
581594
+
581595
+ // Close palette when the user clicks elsewhere.
581596
+ document.addEventListener('click', (e) => {
581597
+ const p = document.getElementById('slash-palette');
581598
+ if (!p) return;
581599
+ if (!p.contains(e.target) && e.target !== input && e.target.id !== 'slash-btn') {
581600
+ p.style.display = 'none';
581601
+ }
581602
+ });
581603
+
579418
581604
  input.focus();
579419
581605
  </script>
581606
+ </div><!-- /#oa-main -->
581607
+ </div><!-- /#oa-shell -->
579420
581608
  </body>
579421
581609
  </html>`;
579422
581610
  }
@@ -580986,12 +583174,11 @@ import { fileURLToPath as fileURLToPath17 } from "node:url";
580986
583174
  import { dirname as dirname33, join as join109, resolve as resolve37 } from "node:path";
580987
583175
  import { homedir as homedir40 } from "node:os";
580988
583176
  import { spawn as spawn25, execSync as execSync55 } from "node:child_process";
580989
- import { mkdirSync as mkdirSync59, writeFileSync as writeFileSync52, readFileSync as readFileSync74, readdirSync as readdirSync30, existsSync as existsSync92, watch as fsWatch3 } from "node:fs";
583177
+ import { mkdirSync as mkdirSync59, writeFileSync as writeFileSync52, readFileSync as readFileSync74, readdirSync as readdirSync30, existsSync as existsSync92, watch as fsWatch3, renameSync as renameSync8, unlinkSync as unlinkSync24 } from "node:fs";
580990
583178
  import { randomBytes as randomBytes21, randomUUID as randomUUID14 } from "node:crypto";
580991
583179
  import { createHash as createHash14 } from "node:crypto";
580992
583180
  function getVersion3() {
580993
583181
  try {
580994
- const require3 = createRequire4(import.meta.url);
580995
583182
  const thisDir = dirname33(fileURLToPath17(import.meta.url));
580996
583183
  const candidates = [
580997
583184
  join109(thisDir, "..", "package.json"),
@@ -581707,19 +583894,18 @@ function autoSeedTodosFromPrompt(prompt) {
581707
583894
  return [];
581708
583895
  }
581709
583896
  function atomicJobWrite(dir, id, job) {
581710
- const fs7 = __require("node:fs");
581711
583897
  const finalPath = join109(dir, `${id}.json`);
581712
583898
  const tmpPath = `${finalPath}.tmp.${process.pid}.${Date.now()}`;
581713
583899
  try {
581714
- fs7.writeFileSync(tmpPath, JSON.stringify(job, null, 2), "utf-8");
581715
- fs7.renameSync(tmpPath, finalPath);
583900
+ writeFileSync52(tmpPath, JSON.stringify(job, null, 2), "utf-8");
583901
+ renameSync8(tmpPath, finalPath);
581716
583902
  } catch {
581717
583903
  try {
581718
- fs7.writeFileSync(finalPath, JSON.stringify(job, null, 2), "utf-8");
583904
+ writeFileSync52(finalPath, JSON.stringify(job, null, 2), "utf-8");
581719
583905
  } catch {
581720
583906
  }
581721
583907
  try {
581722
- fs7.unlinkSync(tmpPath);
583908
+ unlinkSync24(tmpPath);
581723
583909
  } catch {
581724
583910
  }
581725
583911
  }
@@ -582801,12 +584987,11 @@ function readUpdateState() {
582801
584987
  function writeUpdateState(state) {
582802
584988
  try {
582803
584989
  const dir = join109(homedir40(), ".open-agents");
582804
- const fs7 = __require("node:fs");
582805
- fs7.mkdirSync(dir, { recursive: true });
584990
+ mkdirSync59(dir, { recursive: true });
582806
584991
  const finalPath = updateStateFile();
582807
584992
  const tmpPath = `${finalPath}.tmp.${process.pid}`;
582808
- fs7.writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
582809
- fs7.renameSync(tmpPath, finalPath);
584993
+ writeFileSync52(tmpPath, JSON.stringify(state, null, 2), "utf-8");
584994
+ renameSync8(tmpPath, finalPath);
582810
584995
  } catch {
582811
584996
  }
582812
584997
  }
@@ -582843,10 +585028,10 @@ async function handleV1Update(req2, res, requestId) {
582843
585028
  from: currentVersion,
582844
585029
  to: targetVersion
582845
585030
  }, { subject: req2._authUser ?? "anonymous" });
582846
- const fs7 = __require("node:fs");
585031
+ const fs7 = require3("node:fs");
582847
585032
  const nodeBin = process.execPath;
582848
585033
  const nodeDir = dirname33(nodeBin);
582849
- const { execSync: es } = __require("node:child_process");
585034
+ const { execSync: es } = require3("node:child_process");
582850
585035
  const isWin2 = process.platform === "win32";
582851
585036
  let npmBin = "";
582852
585037
  for (const candidate of isWin2 ? [join109(nodeDir, "npm.cmd"), join109(nodeDir, "npm")] : [join109(nodeDir, "npm"), "/usr/local/bin/npm", "/usr/bin/npm"]) {
@@ -583912,9 +586097,9 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
583912
586097
  return;
583913
586098
  }
583914
586099
  if (pathname === "/v1/system" && method === "GET") {
583915
- const os8 = __require("node:os");
586100
+ const os8 = require3("node:os");
583916
586101
  const version4 = getVersion3();
583917
- const { execSync: es } = __require("node:child_process");
586102
+ const { execSync: es } = require3("node:child_process");
583918
586103
  let gpus = [];
583919
586104
  try {
583920
586105
  const nv = es("nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits", { encoding: "utf8", timeout: 5e3, stdio: "pipe" });
@@ -584138,7 +586323,7 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
584138
586323
  return;
584139
586324
  }
584140
586325
  const { tmpdir: tmpdir22 } = await import("node:os");
584141
- const { writeFileSync: writeFileSync56, unlinkSync: unlinkSync24 } = await import("node:fs");
586326
+ const { writeFileSync: writeFileSync56, unlinkSync: unlinkSync25 } = await import("node:fs");
584142
586327
  const { join: pjoin } = await import("node:path");
584143
586328
  const tmpPath = pjoin(tmpdir22(), `oa-clone-upload-${Date.now()}-${safeName}`);
584144
586329
  writeFileSync56(tmpPath, buf);
@@ -584153,7 +586338,7 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
584153
586338
  });
584154
586339
  } finally {
584155
586340
  try {
584156
- unlinkSync24(tmpPath);
586341
+ unlinkSync25(tmpPath);
584157
586342
  } catch {
584158
586343
  }
584159
586344
  }
@@ -584795,8 +586980,8 @@ data: ${JSON.stringify(data)}
584795
586980
  } catch {
584796
586981
  }
584797
586982
  try {
584798
- const { execSync: es, spawnSync: ss } = __require("node:child_process");
584799
- const home = process.env.HOME || __require("node:os").homedir();
586983
+ const { execSync: es, spawnSync: ss } = require3("node:child_process");
586984
+ const home = process.env.HOME || require3("node:os").homedir();
584800
586985
  const userDir = `${home}/.config/systemd/user`;
584801
586986
  for (const suffix of [".timer", ".service"]) {
584802
586987
  try {
@@ -585795,7 +587980,7 @@ function listScheduledTasks() {
585795
587980
  function listOaUserTimers() {
585796
587981
  const out = [];
585797
587982
  try {
585798
- const { execSync: es } = __require("node:child_process");
587983
+ const { execSync: es } = require3("node:child_process");
585799
587984
  const files = es("systemctl --user list-unit-files --type=timer --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
585800
587985
  const enabledMap = /* @__PURE__ */ new Map();
585801
587986
  for (const line of files) {
@@ -585893,7 +588078,7 @@ function deleteScheduledById(id) {
585893
588078
  if (id) candidates.push(id);
585894
588079
  if (typeof entry?.id === "string" && entry.id && !candidates.includes(entry.id)) candidates.push(entry.id);
585895
588080
  try {
585896
- const { createHash: createHash15 } = __require("node:crypto");
588081
+ const { createHash: createHash15 } = require3("node:crypto");
585897
588082
  const fallback = createHash15("sha1").update(`${target.file}#${target.index}`).digest("hex").slice(0, 16);
585898
588083
  if (!candidates.includes(fallback)) candidates.push(fallback);
585899
588084
  } catch {
@@ -585942,7 +588127,7 @@ function removeCronByDirectory(dir) {
585942
588127
  function killProcessGroups(pids, pattern) {
585943
588128
  const killed = [];
585944
588129
  try {
585945
- const { execSync: es } = __require("node:child_process");
588130
+ const { execSync: es } = require3("node:child_process");
585946
588131
  const targets = /* @__PURE__ */ new Map();
585947
588132
  if (pids && pids.length > 0) {
585948
588133
  for (const pid of pids) {
@@ -586003,8 +588188,8 @@ function killProcessGroups(pids, pattern) {
586003
588188
  function disableAllOaTimers() {
586004
588189
  let disabled = 0;
586005
588190
  try {
586006
- const { execSync: es } = __require("node:child_process");
586007
- const home = process.env.HOME || __require("node:os").homedir();
588191
+ const { execSync: es } = require3("node:child_process");
588192
+ const home = process.env.HOME || require3("node:os").homedir();
586008
588193
  const userDir = `${home}/.config/systemd/user`;
586009
588194
  const timers = listOaUserTimers();
586010
588195
  for (const t2 of timers) {
@@ -586055,7 +588240,7 @@ function removeAllOaCrons() {
586055
588240
  function listMatchingProcesses(pattern) {
586056
588241
  const list = [];
586057
588242
  try {
586058
- const { execSync: es } = __require("node:child_process");
588243
+ const { execSync: es } = require3("node:child_process");
586059
588244
  const re = new RegExp(pattern, "i");
586060
588245
  const ps = es("ps -eo pid,etimes,pcpu,pmem,command", { encoding: "utf8", stdio: "pipe" });
586061
588246
  for (const line of ps.split("\n")) {
@@ -586075,7 +588260,7 @@ function listMatchingProcesses(pattern) {
586075
588260
  }
586076
588261
  function sampleGpuUtil() {
586077
588262
  try {
586078
- const { execSync: es } = __require("node:child_process");
588263
+ const { execSync: es } = require3("node:child_process");
586079
588264
  const out = es("nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits", { encoding: "utf8", timeout: 3e3, stdio: "pipe" });
586080
588265
  const arr = [];
586081
588266
  for (const line of out.trim().split("\n")) {
@@ -586092,7 +588277,7 @@ function sampleGpuUtil() {
586092
588277
  }
586093
588278
  function getCurrentCrontabLines() {
586094
588279
  try {
586095
- const { execSync: es } = __require("node:child_process");
588280
+ const { execSync: es } = require3("node:child_process");
586096
588281
  return es("crontab -l 2>/dev/null", { encoding: "utf8", stdio: "pipe" }).split("\n");
586097
588282
  } catch {
586098
588283
  return [];
@@ -586175,7 +588360,7 @@ function reconcileScheduledTasks(apply) {
586175
588360
  }
586176
588361
  function findOaBinary4() {
586177
588362
  try {
586178
- const { execSync: es } = __require("node:child_process");
588363
+ const { execSync: es } = require3("node:child_process");
586179
588364
  for (const cmd of ["oa", "open-agents"]) {
586180
588365
  try {
586181
588366
  const p2 = es(`which ${cmd} 2>/dev/null`, { encoding: "utf8", stdio: "pipe" }).trim();
@@ -586192,7 +588377,7 @@ function getCrontabLines() {
586192
588377
  }
586193
588378
  function writeCrontabLines(lines) {
586194
588379
  try {
586195
- const { spawnSync: spawnSync6 } = __require("node:child_process");
588380
+ const { spawnSync: spawnSync6 } = require3("node:child_process");
586196
588381
  const result = spawnSync6("crontab", ["-"], { input: lines.join("\n") + "\n", encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
586197
588382
  if (result.status !== 0) throw new Error(`crontab exited ${result.status}: ${String(result.stderr || "").trim()}`);
586198
588383
  } catch (e2) {
@@ -586294,7 +588479,7 @@ WantedBy=timers.target
586294
588479
  writeFileSync52(svc, svcText);
586295
588480
  writeFileSync52(tim, timText);
586296
588481
  try {
586297
- const { execSync: es } = __require("node:child_process");
588482
+ const { execSync: es } = require3("node:child_process");
586298
588483
  es("systemctl --user daemon-reload", { stdio: "pipe" });
586299
588484
  es(`systemctl --user enable --now ${unitBase}.timer`, { stdio: "pipe" });
586300
588485
  } catch {
@@ -586321,7 +588506,7 @@ WantedBy=timers.target
586321
588506
  function listUserServices(pattern) {
586322
588507
  const out = [];
586323
588508
  try {
586324
- const { execSync: es } = __require("node:child_process");
588509
+ const { execSync: es } = require3("node:child_process");
586325
588510
  const files = es("systemctl --user list-unit-files --type=service --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
586326
588511
  const enabledMap = /* @__PURE__ */ new Map();
586327
588512
  for (const line of files) {
@@ -586347,7 +588532,7 @@ function listUserServices(pattern) {
586347
588532
  }
586348
588533
  function userServiceAction(unit, action) {
586349
588534
  try {
586350
- const { execSync: es } = __require("node:child_process");
588535
+ const { execSync: es } = require3("node:child_process");
586351
588536
  if (action === "stop") {
586352
588537
  es(`systemctl --user stop ${unit}`, { stdio: "pipe" });
586353
588538
  return true;
@@ -586515,8 +588700,8 @@ function startApiServer(options2 = {}) {
586515
588700
  const job = JSON.parse(readFileSync74(jobPath, "utf-8"));
586516
588701
  const jobTime = new Date(job.startedAt ?? job.completedAt ?? 0).getTime();
586517
588702
  if (jobTime > 0 && jobTime < cutoff && job.status !== "running") {
586518
- const { unlinkSync: unlinkSync24 } = __require("node:fs");
586519
- unlinkSync24(jobPath);
588703
+ const { unlinkSync: unlinkSync25 } = require3("node:fs");
588704
+ unlinkSync25(jobPath);
586520
588705
  }
586521
588706
  } catch {
586522
588707
  }
@@ -586837,7 +589022,7 @@ function startApiServer(options2 = {}) {
586837
589022
  log22(` Port ${port} in use — reclaiming...
586838
589023
  `);
586839
589024
  try {
586840
- const { execSync: es } = __require("node:child_process");
589025
+ const { execSync: es } = require3("node:child_process");
586841
589026
  const pids = es(`lsof -ti :${port} 2>/dev/null || fuser ${port}/tcp 2>/dev/null || true`, { encoding: "utf8" }).trim().split(/[\n\s]+/).filter(Boolean);
586842
589027
  for (const pid of pids) {
586843
589028
  const numPid = parseInt(pid, 10);
@@ -587134,8 +589319,8 @@ function removeCronByMarker(id) {
587134
589319
  }
587135
589320
  function disableUserTimerById(id) {
587136
589321
  try {
587137
- const { execSync: es } = __require("node:child_process");
587138
- const home = process.env.HOME || __require("node:os").homedir();
589322
+ const { execSync: es } = require3("node:child_process");
589323
+ const home = process.env.HOME || require3("node:os").homedir();
587139
589324
  const userDir = `${home}/.config/systemd/user`;
587140
589325
  const names = [`oa-${id}`, `oa-sched-${id}`, id];
587141
589326
  for (const n2 of names) {
@@ -587164,7 +589349,7 @@ function disableUserTimerById(id) {
587164
589349
  function setTimerEnabled(name10, enabled2) {
587165
589350
  try {
587166
589351
  const unit = `${name10}.timer`;
587167
- const { execSync: es } = __require("node:child_process");
589352
+ const { execSync: es } = require3("node:child_process");
587168
589353
  if (enabled2) es(`systemctl --user enable --now ${unit}`, { stdio: "pipe" });
587169
589354
  else es(`systemctl --user disable --now ${unit}`, { stdio: "pipe" });
587170
589355
  return true;
@@ -587172,7 +589357,7 @@ function setTimerEnabled(name10, enabled2) {
587172
589357
  return false;
587173
589358
  }
587174
589359
  }
587175
- var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, runningProcesses, perKeyUsage, CRON_MARKER2;
589360
+ var require3, endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, runningProcesses, perKeyUsage, CRON_MARKER2;
587176
589361
  var init_serve = __esm({
587177
589362
  "packages/cli/src/api/serve.ts"() {
587178
589363
  "use strict";
@@ -587200,6 +589385,7 @@ var init_serve = __esm({
587200
589385
  init_profiles();
587201
589386
  init_docker();
587202
589387
  init_typed_node_events();
589388
+ require3 = createRequire4(import.meta.url);
587203
589389
  endpointRegistry = [];
587204
589390
  modelRouteMap = /* @__PURE__ */ new Map();
587205
589391
  endpointUsage = /* @__PURE__ */ new Map();
@@ -587237,7 +589423,7 @@ function formatTimeAgo2(date) {
587237
589423
  }
587238
589424
  function getVersion4() {
587239
589425
  try {
587240
- const require3 = createRequire5(import.meta.url);
589426
+ const require4 = createRequire5(import.meta.url);
587241
589427
  const thisDir = dirname34(fileURLToPath18(import.meta.url));
587242
589428
  const candidates = [
587243
589429
  join110(thisDir, "..", "package.json"),
@@ -587246,7 +589432,7 @@ function getVersion4() {
587246
589432
  ];
587247
589433
  for (const pkgPath of candidates) {
587248
589434
  if (existsSync93(pkgPath)) {
587249
- const pkg = require3(pkgPath);
589435
+ const pkg = require4(pkgPath);
587250
589436
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli" || pkg.name === "@open-agents/monorepo") {
587251
589437
  return pkg.version ?? "0.0.0";
587252
589438
  }
@@ -594979,9 +597165,9 @@ init_spinner();
594979
597165
  init_output();
594980
597166
  function getVersion5() {
594981
597167
  try {
594982
- const require3 = createRequire6(import.meta.url);
597168
+ const require4 = createRequire6(import.meta.url);
594983
597169
  const pkgPath = join114(dirname35(fileURLToPath19(import.meta.url)), "..", "package.json");
594984
- const pkg = require3(pkgPath);
597170
+ const pkg = require4(pkgPath);
594985
597171
  return pkg.version;
594986
597172
  } catch {
594987
597173
  return "0.1.0";