iris-chatbot 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris-chatbot",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "private": false,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ # Projects plan – Sidebar UI addendum
2
+
3
+ When implementing the Sidebar for Projects, apply these UI details:
4
+
5
+ 1. **Folder icon**
6
+ - **Remove** the folder icon from chat/thread rows (individual chats).
7
+ - **Add** the folder icon next to **project names** (each project in the project list, including "Inbox" if shown as a project).
8
+
9
+ 2. **"Your Chats" label**
10
+ - Use the exact label **'Your Chats'** (title case).
11
+ - Do **not** render it in all caps: remove the `uppercase` class from the section heading (currently in Sidebar around line 86: `text-[11px] uppercase tracking-[0.18em]` → use the same size/tracking but drop `uppercase`) so it displays as **Your Chats**, not "YOUR CHATS".
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "iris",
9
- "version": "4.1.0",
9
+ "version": "5.0.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.72.1",
12
12
  "clsx": "^2.1.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "private": true,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "engines": {
@@ -3,7 +3,7 @@
3
3
 
4
4
  :root {
5
5
  --topbar-height: 64px;
6
- --bg: #171819;
6
+ --bg: #212121;
7
7
  --bg-alt: #1b1c1d;
8
8
  --sidebar: #181818;
9
9
  --sidebar-border: #202020;
@@ -15,6 +15,7 @@
15
15
  --text-muted: #b1b7bf;
16
16
  --accent: #10a37f;
17
17
  --accent-2: #0e8e6f;
18
+ --accent-ring: var(--accent-2);
18
19
  --danger: #ef4444;
19
20
  --border: #2a2a2a;
20
21
  --border-strong: #3a3a3a;
@@ -48,6 +49,21 @@ a {
48
49
  color: var(--accent);
49
50
  }
50
51
 
52
+ input:focus,
53
+ select:focus,
54
+ textarea:focus,
55
+ button:focus {
56
+ outline: none;
57
+ }
58
+
59
+ input:focus-visible,
60
+ select:focus-visible,
61
+ textarea:focus-visible,
62
+ button:focus-visible {
63
+ outline: none;
64
+ box-shadow: inset 0 0 0 1px var(--border-strong);
65
+ }
66
+
51
67
  .chat-shell {
52
68
  display: grid;
53
69
  grid-template-columns: 248px minmax(0, 1fr);
@@ -82,10 +98,21 @@ a {
82
98
  background: transparent !important;
83
99
  backdrop-filter: none;
84
100
  box-shadow: none;
85
- position: sticky;
101
+ position: absolute;
86
102
  top: 0;
103
+ left: 0;
104
+ right: 0;
87
105
  z-index: 40;
88
106
  height: var(--topbar-height);
107
+ pointer-events: none;
108
+ }
109
+
110
+ .topbar > *,
111
+ .topbar button,
112
+ .topbar a,
113
+ .topbar [role="listbox"],
114
+ .topbar [role="option"] {
115
+ pointer-events: auto;
89
116
  }
90
117
 
91
118
 
@@ -249,7 +276,7 @@ a {
249
276
  }
250
277
 
251
278
  .assistant-card .message-content {
252
- color: #f8fafc;
279
+ color: var(--text-primary);
253
280
  }
254
281
 
255
282
  .message-loading-spinner {
@@ -271,7 +298,7 @@ a {
271
298
  .composer-bar {
272
299
  position: sticky;
273
300
  bottom: 0;
274
- background: transparent;
301
+ background: var(--bg);
275
302
  z-index: 30;
276
303
  }
277
304
 
@@ -299,13 +326,14 @@ a {
299
326
  --sidebar: #ffffff;
300
327
  --sidebar-border: #e5e5e5;
301
328
  --panel: #ffffff;
302
- --panel-2: #f0f0f0;
303
- --panel-3: #e8e8e8;
329
+ --panel-2: #f6f6f6;
330
+ --panel-3: #f0f0f0;
304
331
  --text-primary: #111111;
305
332
  --text-secondary: #4a4a4a;
306
333
  --text-muted: #6f6f6f;
307
334
  --accent: #0e8e6f;
308
335
  --accent-2: #0b7a5f;
336
+ --accent-ring: color-mix(in srgb, var(--accent) 55%, #000);
309
337
  --danger: #dc2626;
310
338
  --border: #d5d5d5;
311
339
  --border-strong: #bdbdbd;
@@ -313,6 +341,51 @@ a {
313
341
  --user-bubble: #d7f2ea;
314
342
  }
315
343
 
344
+ /* Dark mode: white button background (no data-theme or data-theme="dark") */
345
+ :root .settings-tab-active,
346
+ :root .settings-btn-accent {
347
+ background: white !important;
348
+ color: #111111 !important;
349
+ }
350
+
351
+ [data-theme="dark"] .settings-tab-active,
352
+ [data-theme="dark"] .settings-btn-accent {
353
+ background: white !important;
354
+ color: #111111 !important;
355
+ }
356
+
357
+ /* Light mode: black for selected tab and Save button */
358
+ [data-theme="light"] .settings-tab-active,
359
+ [data-theme="light"] .settings-btn-accent {
360
+ background: #111111 !important;
361
+ color: #ffffff !important;
362
+ }
363
+
364
+ [data-theme="light"] .message-content pre,
365
+ [data-theme="light"] .message-content pre code {
366
+ color: #e5e7eb;
367
+ }
368
+
369
+ [data-theme="light"] .message-content code {
370
+ background: rgba(0, 0, 0, 0.06);
371
+ color: var(--text-primary);
372
+ }
373
+
374
+ [data-theme="light"] .message-loading-spinner {
375
+ border-color: rgba(0, 0, 0, 0.15);
376
+ border-top-color: #111111;
377
+ }
378
+
379
+ [data-theme="light"] .send-button.active {
380
+ background: #111111;
381
+ color: #ffffff;
382
+ border-color: transparent;
383
+ }
384
+
385
+ [data-theme="light"] .send-button.active svg {
386
+ color: #ffffff;
387
+ }
388
+
316
389
  .message-content h1,
317
390
  .message-content h2,
318
391
  .message-content h3,
@@ -506,12 +579,16 @@ a {
506
579
  }
507
580
 
508
581
  .composer {
509
- background: var(--panel);
582
+ background: #303030;
510
583
  border: 1px solid var(--border);
511
584
  border-radius: 18px;
512
585
  padding: 10px 12px;
513
586
  }
514
587
 
588
+ [data-theme="light"] .composer {
589
+ background: transparent;
590
+ }
591
+
515
592
  .thread-shelf {
516
593
  margin-top: 12px;
517
594
  padding-top: 8px;
@@ -582,10 +659,22 @@ a {
582
659
  line-height: 1.4;
583
660
  padding-top: 7px;
584
661
  padding-bottom: 9px;
662
+ padding-left: 6px;
585
663
  /* One line by default; height grows with content in Composer.tsx */
586
664
  min-height: calc(1.4em + 7px + 9px);
587
665
  }
588
666
 
667
+ .composer-textarea:focus,
668
+ .composer-textarea:focus-visible {
669
+ box-shadow: none;
670
+ }
671
+
672
+ .search-modal-input:focus,
673
+ .search-modal-input:focus-visible {
674
+ outline: none;
675
+ box-shadow: none;
676
+ }
677
+
589
678
  .send-button {
590
679
  height: 40px;
591
680
  width: 40px;
@@ -800,9 +889,9 @@ a {
800
889
  }
801
890
 
802
891
  .chat-scroll {
803
- padding-top: calc(var(--topbar-height) - 20px);
892
+ padding-top: calc(var(--topbar-height) + 24px);
804
893
  }
805
894
 
806
895
  .chat-scroll.empty {
807
- padding-top: var(--topbar-height);
896
+ padding-top: calc(var(--topbar-height) + 24px);
808
897
  }
@@ -12,6 +12,8 @@ import {
12
12
  createNewThread,
13
13
  deleteConversation,
14
14
  deleteThread,
15
+ DEFAULT_ACCENT_DARK,
16
+ DEFAULT_ACCENT_LIGHT,
15
17
  } from "../lib/data";
16
18
  import { db } from "../lib/db";
17
19
  import {
@@ -331,10 +333,23 @@ export default function Home() {
331
333
  <div
332
334
  className={`chat-shell ${sidebarCollapsed ? "collapsed" : ""}`}
333
335
  style={
334
- {
335
- "--accent": settings?.accentColor || "#66706e",
336
- "--accent-2": settings?.accentColor || "#66706e",
337
- } as CSSProperties
336
+ (() => {
337
+ const stored = settings?.accentColor || DEFAULT_ACCENT_DARK;
338
+ const isDefaultGray = stored === DEFAULT_ACCENT_DARK;
339
+ const isLight =
340
+ settings?.theme === "light" ||
341
+ (typeof document !== "undefined" &&
342
+ document.documentElement.dataset.theme === "light");
343
+ const effective =
344
+ isDefaultGray && isLight ? DEFAULT_ACCENT_LIGHT : stored;
345
+ return {
346
+ "--accent": effective,
347
+ "--accent-2": effective,
348
+ ...(isLight && {
349
+ "--accent-ring": "color-mix(in srgb, var(--accent) 55%, #000)",
350
+ }),
351
+ } as CSSProperties;
352
+ })()
338
353
  }
339
354
  >
340
355
  <Sidebar
@@ -387,7 +402,7 @@ export default function Home() {
387
402
  onOpenSearch={() => setSearchOpen(true)}
388
403
  />
389
404
 
390
- <div className="flex h-screen min-w-0 flex-col overflow-hidden">
405
+ <div className="relative flex h-screen min-w-0 flex-col overflow-hidden">
391
406
  <TopBar
392
407
  connectionId={connection?.id ?? ""}
393
408
  connectionName={connection?.name ?? "No connection"}
@@ -16,6 +16,21 @@ import { useUIStore } from "../lib/store";
16
16
  const NODE_WIDTH = 220;
17
17
  const NODE_HEIGHT = 80;
18
18
 
19
+ /** Strip markdown syntax for a short plain-text preview (e.g. "## Hi there" → "Hi there"). */
20
+ function stripMarkdownForPreview(text: string): string {
21
+ return text
22
+ .replace(/^#+\s*/m, "") // headings: ## Title -> Title
23
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // **bold**
24
+ .replace(/\*([^*]+)\*/g, "$1") // *italic*
25
+ .replace(/_([^_]+)_/g, "$1") // _italic_
26
+ .replace(/`([^`]+)`/g, "$1") // `code`
27
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
28
+ .replace(/^[-*]\s+/gm, "") // list bullets
29
+ .replace(/^>\s*/gm, "") // blockquote
30
+ .replace(/\s+/g, " ")
31
+ .trim();
32
+ }
33
+
19
34
  const graph = new dagre.graphlib.Graph();
20
35
  graph.setDefaultEdgeLabel(() => ({}));
21
36
 
@@ -120,7 +135,10 @@ export default function MapView({
120
135
 
121
136
  const { nodes, edges } = useMemo(() => {
122
137
  const nodes: Node[] = visibleMessages.map((message) => {
123
- const preview = splitContentAndSources(message.content).content.trim().slice(0, 60) || "(empty)";
138
+ const raw = splitContentAndSources(message.content).content.trim();
139
+ const preview = raw
140
+ ? stripMarkdownForPreview(raw).slice(0, 60)
141
+ : "(empty)";
124
142
  const isActive = activePathIds.has(message.id);
125
143
  const roleClass =
126
144
  message.role === "user"
@@ -169,6 +187,7 @@ export default function MapView({
169
187
  padding: 0.2,
170
188
  duration,
171
189
  includeHiddenNodes: false,
190
+ minZoom: 0.85,
172
191
  });
173
192
  }, []);
174
193
 
@@ -198,7 +217,7 @@ export default function MapView({
198
217
  nodes={nodes}
199
218
  edges={edges}
200
219
  fitView
201
- fitViewOptions={{ padding: 0.2, includeHiddenNodes: false }}
220
+ fitViewOptions={{ padding: 0.2, includeHiddenNodes: false, minZoom: 0.85 }}
202
221
  onInit={(instance) => {
203
222
  flowRef.current = instance;
204
223
  refitView(0);
@@ -1009,7 +1009,7 @@ function MessageCard({
1009
1009
  {canEditThreads ? (
1010
1010
  <button
1011
1011
  className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${threadEditMode
1012
- ? "border-[var(--accent)] text-[var(--text-primary)]"
1012
+ ? "border-[var(--accent-ring)] text-[var(--text-primary)]"
1013
1013
  : "border-[var(--border)] text-[var(--text-muted)] hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
1014
1014
  }`}
1015
1015
  onClick={() => setThreadEditMode((prev) => !prev)}
@@ -33,7 +33,7 @@ export default function SearchModal({
33
33
  <div className="flex items-center gap-3 border-b border-[var(--border)] px-4 py-3">
34
34
  <Search className="h-4 w-4 text-[var(--text-muted)]" />
35
35
  <input
36
- className="flex-1 bg-transparent text-sm text-[var(--text-primary)] outline-none"
36
+ className="search-modal-input flex-1 bg-transparent text-sm text-[var(--text-primary)] outline-none"
37
37
  placeholder="Search chats..."
38
38
  value={query}
39
39
  onChange={(event) => setQuery(event.target.value)}
@@ -7,10 +7,11 @@ import {
7
7
  ensureBuiltinConnections,
8
8
  normalizeBaseUrl,
9
9
  } from "../lib/connections";
10
+ import { DEFAULT_ACCENT_DARK, DEFAULT_ACCENT_LIGHT } from "../lib/data";
10
11
  import { db } from "../lib/db";
11
12
  import { useMemories } from "../lib/hooks";
12
13
  import { normalizeMemoryKey } from "../lib/memory";
13
- import { filterModelIdsForConnection, getConnectionModelPresets } from "../lib/model-presets";
14
+ import { filterModelIdsForConnection, getConnectionModelPresets, getModelDisplayLabel } from "../lib/model-presets";
14
15
  import {
15
16
  DEFAULT_MEMORY_SETTINGS,
16
17
  DEFAULT_LOCAL_TOOLS_SETTINGS,
@@ -24,7 +25,6 @@ import {
24
25
  type ModelConnection,
25
26
  type SafetyProfile,
26
27
  type Settings,
27
- type WebSearchBackend,
28
28
  } from "../lib/types";
29
29
 
30
30
  type TabId =
@@ -112,7 +112,7 @@ export default function SettingsModal({
112
112
  const [showOpenAIKey, setShowOpenAIKey] = useState(false);
113
113
  const [showAnthropicKey, setShowAnthropicKey] = useState(false);
114
114
  const [showGeminiKey, setShowGeminiKey] = useState(false);
115
- const [accentColor, setAccentColor] = useState(settings?.accentColor || "#66706e");
115
+ const [accentColor, setAccentColor] = useState(settings?.accentColor || DEFAULT_ACCENT_DARK);
116
116
  const [theme, setTheme] = useState<"dark" | "light">(settings?.theme || "dark");
117
117
  const [font, setFont] = useState<"ibm" | "manrope" | "sora" | "space" | "poppins">(
118
118
  settings?.font || "manrope",
@@ -132,8 +132,6 @@ export default function SettingsModal({
132
132
  const [enableMail, setEnableMail] = useState(localTools.enableMail);
133
133
  const [enableWorkflow, setEnableWorkflow] = useState(localTools.enableWorkflow);
134
134
  const [enableSystem, setEnableSystem] = useState(localTools.enableSystem);
135
- const [webSearchBackend, setWebSearchBackend] = useState<WebSearchBackend>(localTools.webSearchBackend);
136
- const [dryRun, setDryRun] = useState(localTools.dryRun);
137
135
  const memory = settings?.memory ?? DEFAULT_MEMORY_SETTINGS;
138
136
  const [memoryEnabled, setMemoryEnabled] = useState(memory.enabled);
139
137
  const [memoryAutoCapture, setMemoryAutoCapture] = useState(memory.autoCapture);
@@ -216,6 +214,49 @@ export default function SettingsModal({
216
214
  return text.includes(query);
217
215
  });
218
216
 
217
+ const memoryKindLabel: Record<MemoryKind, string> = {
218
+ profile: "Profile",
219
+ preference: "Preference",
220
+ person_alias: "Person alias",
221
+ music_alias: "Music alias",
222
+ note: "Note",
223
+ };
224
+ const memoryScopeLabel: Record<MemoryScope, string> = {
225
+ global: "All chats",
226
+ conversation: "This conversation",
227
+ };
228
+ const memorySourceLabel: Record<MemorySource, string> = {
229
+ auto: "Auto-captured",
230
+ explicit: "Explicit",
231
+ manual: "Manual",
232
+ };
233
+ function formatMemoryValue(value: string): string {
234
+ try {
235
+ const parsed = JSON.parse(value) as Record<string, unknown>;
236
+ if (parsed && typeof parsed === "object") {
237
+ const title = typeof parsed.title === "string" ? parsed.title : "";
238
+ const artist = typeof parsed.artist === "string" ? parsed.artist : "";
239
+ const query = typeof parsed.query === "string" ? parsed.query : "";
240
+ if (title && artist) return `${title} by ${artist}`;
241
+ if (query) return query;
242
+ if (title) return title;
243
+ }
244
+ } catch {
245
+ // not JSON, use as-is
246
+ }
247
+ return value;
248
+ }
249
+ function formatMemoryMeta(entry: MemoryEntry): string {
250
+ const parts = [
251
+ memoryKindLabel[entry.kind],
252
+ entry.scope === "conversation" && entry.conversationId
253
+ ? `${memoryScopeLabel[entry.scope]} (${entry.conversationId})`
254
+ : memoryScopeLabel[entry.scope],
255
+ memorySourceLabel[entry.source],
256
+ ];
257
+ return parts.join(" · ");
258
+ }
259
+
219
260
  const resetConnectionForm = () => {
220
261
  setEditingConnectionId(null);
221
262
  setConnectionFormName("");
@@ -486,7 +527,7 @@ export default function SettingsModal({
486
527
  defaultModelByConnection: resolvedDefaultModelMap,
487
528
  showExtendedOpenAIModels,
488
529
  enableWebSources,
489
- accentColor: accentColor || "#66706e",
530
+ accentColor: accentColor || DEFAULT_ACCENT_DARK,
490
531
  font,
491
532
  theme,
492
533
  localTools: {
@@ -504,8 +545,8 @@ export default function SettingsModal({
504
545
  enableMail,
505
546
  enableWorkflow,
506
547
  enableSystem,
507
- webSearchBackend,
508
- dryRun,
548
+ webSearchBackend: DEFAULT_LOCAL_TOOLS_SETTINGS.webSearchBackend,
549
+ dryRun: localTools.dryRun,
509
550
  },
510
551
  memory: {
511
552
  enabled: memoryEnabled,
@@ -518,7 +559,7 @@ export default function SettingsModal({
518
559
  };
519
560
 
520
561
  const accentPresets = [
521
- { id: "default", label: "Default", color: "#66706e" },
562
+ { id: "default", label: "Default", color: DEFAULT_ACCENT_DARK },
522
563
  { id: "blue", label: "Blue", color: "#2563eb" },
523
564
  { id: "green", label: "Green", color: "#16a34a" },
524
565
  { id: "yellow", label: "Yellow", color: "#f59e0b" },
@@ -545,9 +586,9 @@ export default function SettingsModal({
545
586
  <button
546
587
  key={tab.id}
547
588
  onClick={() => setActiveTab(tab.id)}
548
- className={`rounded-full px-3 py-1.5 text-xs ${
589
+ className={`rounded-full px-3 py-1.5 text-xs settings-tab ${
549
590
  activeTab === tab.id
550
- ? "bg-[var(--accent)] text-white"
591
+ ? "settings-tab-active"
551
592
  : "border border-[var(--border)] bg-[var(--panel-2)] text-[var(--text-secondary)]"
552
593
  }`}
553
594
  >
@@ -581,7 +622,7 @@ export default function SettingsModal({
581
622
  <div className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
582
623
  Default model for {selectedDefaultConnection.name}
583
624
  </div>
584
- <input
625
+ <select
585
626
  className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
586
627
  value={modelInput || selectedDefaultModel}
587
628
  onChange={(event) => {
@@ -592,10 +633,25 @@ export default function SettingsModal({
592
633
  [selectedDefaultConnection.id]: value,
593
634
  }));
594
635
  }}
595
- placeholder="Enter model id"
596
- />
636
+ >
637
+ {selectableModels.length === 0 ? (
638
+ <option value="">No models — fetch from Connections tab</option>
639
+ ) : (
640
+ (() => {
641
+ const current = modelInput || selectedDefaultModel;
642
+ const ids = current && !selectableModels.includes(current)
643
+ ? [current, ...selectableModels]
644
+ : selectableModels;
645
+ return ids.map((modelId) => (
646
+ <option key={modelId} value={modelId}>
647
+ {getModelDisplayLabel(modelId, selectedDefaultConnection)}
648
+ </option>
649
+ ));
650
+ })()
651
+ )}
652
+ </select>
597
653
  <div className="flex flex-wrap gap-2">
598
- {selectableModels.slice(0, 20).map((preset) => (
654
+ {selectableModels.map((preset) => (
599
655
  <button
600
656
  key={preset}
601
657
  className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs text-[var(--text-secondary)]"
@@ -607,7 +663,7 @@ export default function SettingsModal({
607
663
  }));
608
664
  }}
609
665
  >
610
- {preset}
666
+ {getModelDisplayLabel(preset, selectedDefaultConnection)}
611
667
  </button>
612
668
  ))}
613
669
  {selectableModels.length === 0 ? (
@@ -618,7 +674,7 @@ export default function SettingsModal({
618
674
  </div>
619
675
  {selectedDefaultConnection?.kind === "builtin" &&
620
676
  selectedDefaultConnection?.provider === "openai" ? (
621
- <label className="mt-3 flex items-center gap-2 text-sm text-[var(--text-secondary)]">
677
+ <label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
622
678
  <input
623
679
  type="checkbox"
624
680
  checked={showExtendedOpenAIModels}
@@ -852,7 +908,7 @@ export default function SettingsModal({
852
908
  </div>
853
909
  <div className="mt-3 flex gap-2">
854
910
  <button
855
- className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
911
+ className="rounded-full px-4 py-2 text-xs settings-btn-accent"
856
912
  onClick={upsertConnectionFromForm}
857
913
  >
858
914
  {editingConnectionId ? "Update Connection" : "Add Connection"}
@@ -930,7 +986,6 @@ export default function SettingsModal({
930
986
  { label: "Enable mail/messages tools", checked: enableMail, setter: setEnableMail },
931
987
  { label: "Enable workflow tool", checked: enableWorkflow, setter: setEnableWorkflow },
932
988
  { label: "Enable system controls", checked: enableSystem, setter: setEnableSystem },
933
- { label: "Dry-run only (no writes)", checked: dryRun, setter: setDryRun },
934
989
  ].map((item) => (
935
990
  <label key={item.label} className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
936
991
  <input
@@ -942,19 +997,6 @@ export default function SettingsModal({
942
997
  </label>
943
998
  ))}
944
999
  </div>
945
- <div>
946
- <label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
947
- Web Search Backend
948
- </label>
949
- <select
950
- className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
951
- value={webSearchBackend}
952
- onChange={(event) => setWebSearchBackend(event.target.value as WebSearchBackend)}
953
- >
954
- <option value="no_key">No-key (default)</option>
955
- <option value="hybrid">Hybrid</option>
956
- </select>
957
- </div>
958
1000
  </div>
959
1001
  ) : null}
960
1002
 
@@ -1020,13 +1062,11 @@ export default function SettingsModal({
1020
1062
  <div className="flex items-start justify-between gap-3">
1021
1063
  <div>
1022
1064
  <div className="text-sm font-medium text-[var(--text-primary)]">{entry.key}</div>
1023
- <div className="mt-0.5 text-xs text-[var(--text-secondary)]">{entry.value}</div>
1065
+ <div className="mt-0.5 text-xs text-[var(--text-secondary)]">
1066
+ {formatMemoryValue(entry.value)}
1067
+ </div>
1024
1068
  <div className="mt-1 text-[11px] text-[var(--text-muted)]">
1025
- {entry.kind} | {entry.scope}
1026
- {entry.scope === "conversation" && entry.conversationId
1027
- ? ` | ${entry.conversationId}`
1028
- : ""}
1029
- {` | ${entry.source}`}
1069
+ {formatMemoryMeta(entry)}
1030
1070
  </div>
1031
1071
  </div>
1032
1072
  <div className="flex shrink-0 gap-2">
@@ -1130,7 +1170,7 @@ export default function SettingsModal({
1130
1170
  ) : null}
1131
1171
  <div className="flex gap-2">
1132
1172
  <button
1133
- className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
1173
+ className="rounded-full px-4 py-2 text-xs settings-btn-accent"
1134
1174
  onClick={() => {
1135
1175
  void upsertMemoryEntry();
1136
1176
  }}
@@ -1157,7 +1197,11 @@ export default function SettingsModal({
1157
1197
  <select
1158
1198
  className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
1159
1199
  value={theme}
1160
- onChange={(event) => setTheme(event.target.value as "dark" | "light")}
1200
+ onChange={(event) => {
1201
+ const value = event.target.value as "dark" | "light";
1202
+ setTheme(value);
1203
+ document.documentElement.dataset.theme = value;
1204
+ }}
1161
1205
  >
1162
1206
  <option value="dark">Dark</option>
1163
1207
  <option value="light">Light</option>
@@ -1170,9 +1214,22 @@ export default function SettingsModal({
1170
1214
  <select
1171
1215
  className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
1172
1216
  value={font}
1173
- onChange={(event) =>
1174
- setFont(event.target.value as "ibm" | "manrope" | "sora" | "space" | "poppins")
1175
- }
1217
+ onChange={(event) => {
1218
+ const value = event.target.value as "ibm" | "manrope" | "sora" | "space" | "poppins";
1219
+ setFont(value);
1220
+ const fontVar =
1221
+ value === "manrope"
1222
+ ? "var(--font-manrope)"
1223
+ : value === "poppins"
1224
+ ? "var(--font-poppins)"
1225
+ : value === "sora"
1226
+ ? "var(--font-sora)"
1227
+ : value === "space"
1228
+ ? "var(--font-space)"
1229
+ : "var(--font-sans)";
1230
+ document.documentElement.style.setProperty("--app-font", fontVar);
1231
+ document.body.style.fontFamily = fontVar;
1232
+ }}
1176
1233
  >
1177
1234
  <option value="ibm">IBM Plex Sans (Default)</option>
1178
1235
  <option value="manrope">Manrope</option>
@@ -1186,20 +1243,35 @@ export default function SettingsModal({
1186
1243
  Accent Color
1187
1244
  </label>
1188
1245
  <div className="grid grid-cols-4 gap-2">
1189
- {accentPresets.map((preset) => (
1190
- <button
1191
- key={preset.id}
1192
- className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${
1193
- accentColor === preset.color
1194
- ? "border-[var(--accent)] text-[var(--text-primary)]"
1195
- : "border-[var(--border)] text-[var(--text-secondary)]"
1196
- }`}
1197
- onClick={() => setAccentColor(preset.color)}
1198
- >
1199
- <span className="h-3 w-3 rounded-full" style={{ background: preset.color }} />
1200
- {preset.label}
1201
- </button>
1202
- ))}
1246
+ {accentPresets.map((preset) => {
1247
+ const isDefault = preset.id === "default";
1248
+ const displayColor =
1249
+ isDefault && theme === "light"
1250
+ ? DEFAULT_ACCENT_LIGHT
1251
+ : preset.color;
1252
+ const isSelected = isDefault
1253
+ ? accentColor === DEFAULT_ACCENT_DARK
1254
+ : accentColor === preset.color;
1255
+ return (
1256
+ <button
1257
+ key={preset.id}
1258
+ className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${
1259
+ isSelected
1260
+ ? "border-[var(--accent-ring)] text-[var(--text-primary)]"
1261
+ : "border-[var(--border)] text-[var(--text-secondary)]"
1262
+ }`}
1263
+ onClick={() =>
1264
+ setAccentColor(isDefault ? DEFAULT_ACCENT_DARK : preset.color)
1265
+ }
1266
+ >
1267
+ <span
1268
+ className="h-3 w-3 rounded-full"
1269
+ style={{ background: displayColor }}
1270
+ />
1271
+ {preset.label}
1272
+ </button>
1273
+ );
1274
+ })}
1203
1275
  </div>
1204
1276
  </div>
1205
1277
  </div>
@@ -1248,7 +1320,7 @@ export default function SettingsModal({
1248
1320
  Cancel
1249
1321
  </button>
1250
1322
  <button
1251
- className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
1323
+ className="rounded-full px-4 py-2 text-xs settings-btn-accent"
1252
1324
  onClick={handleSave}
1253
1325
  >
1254
1326
  Save
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { useMemo } from "react";
4
4
  import {
5
- Folder,
6
5
  PanelLeftClose,
7
6
  PenSquare,
8
7
  Search,
@@ -83,8 +82,8 @@ export default function Sidebar({
83
82
  </div>
84
83
 
85
84
  <div className="flex-1 overflow-y-auto px-2">
86
- <div className="sidebar-text px-2 py-2 text-[11px] uppercase tracking-[0.18em] text-[var(--text-muted)]">
87
- Your chats
85
+ <div className="sidebar-text px-2 py-2 text-sm text-[var(--text-muted)]">
86
+ Your Chats
88
87
  </div>
89
88
  <div className="space-y-2">
90
89
  {groups.map(({ root }) => (
@@ -106,7 +105,6 @@ export default function Sidebar({
106
105
  }`}
107
106
  >
108
107
  <div className="flex min-w-0 flex-1 items-center gap-2">
109
- <Folder className="h-4 w-4 shrink-0 text-[var(--text-muted)]" />
110
108
  <div className="sidebar-text min-w-0 truncate">
111
109
  {root.title || "Main chat"}
112
110
  </div>
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useRef, useState } from "react";
4
- import { ChevronDown, MessageSquare, Settings, Waypoints } from "lucide-react";
4
+ import { ChevronDown, MessageSquare, Network, Settings } from "lucide-react";
5
5
  import type { ModelConnection } from "../lib/types";
6
6
  import { getModelDisplayLabel } from "../lib/model-presets";
7
7
 
@@ -67,7 +67,7 @@ export default function TopBar({
67
67
  <button
68
68
  type="button"
69
69
  onClick={() => setConnectionMenuOpen((current) => !current)}
70
- className="inline-flex max-w-[42vw] items-center gap-2 rounded-md px-1.5 py-1 text-left sm:max-w-[240px] sm:px-2"
70
+ className="inline-flex max-w-[42vw] items-center gap-2 rounded-md px-1.5 py-1 text-left text-[var(--text-primary)] sm:max-w-[240px] sm:px-2"
71
71
  aria-haspopup="listbox"
72
72
  aria-expanded={connectionMenuOpen}
73
73
  aria-label="Model provider"
@@ -144,15 +144,9 @@ export default function TopBar({
144
144
  onClick={onToggleView}
145
145
  >
146
146
  {viewMode === "chat" ? (
147
- <>
148
- <Waypoints className="h-5 w-5" />
149
- <span className="hidden sm:inline">Map</span>
150
- </>
147
+ <Network className="h-5 w-5" />
151
148
  ) : (
152
- <>
153
- <MessageSquare className="h-5 w-5" />
154
- <span className="hidden sm:inline">Chat</span>
155
- </>
149
+ <MessageSquare className="h-5 w-5" />
156
150
  )}
157
151
  </button>
158
152
  ) : null}
@@ -30,6 +30,11 @@ const DEFAULT_SETTINGS: Settings = {
30
30
  memory: { ...DEFAULT_MEMORY_SETTINGS },
31
31
  };
32
32
 
33
+ /** Default accent gray in dark mode (chat bubble, etc.). */
34
+ export const DEFAULT_ACCENT_DARK = "#66706e";
35
+ /** Default accent gray in light mode (light gray so bubble isn’t too dark on white). */
36
+ export const DEFAULT_ACCENT_LIGHT = "#e4e6e5";
37
+
33
38
  const LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS: LocalToolsSettings = {
34
39
  ...DEFAULT_LOCAL_TOOLS_SETTINGS,
35
40
  approvalMode: "always_confirm_writes",
@@ -136,6 +136,32 @@ export function filterModelIdsForConnection(params: {
136
136
  return ids.slice(0, Math.min(ids.length, OPENAI_FRONTIER_MODEL_PRESETS.length));
137
137
  }
138
138
 
139
+ const OPENAI_MODEL_LABELS: Record<string, string> = {
140
+ "gpt-5.2": "GPT 5.2",
141
+ "gpt-5.2-pro": "GPT 5.2 Pro",
142
+ "gpt-5.2-chat-latest": "GPT 5.2 Chat (Latest)",
143
+ "gpt-5.2-codex": "GPT 5.2 Codex",
144
+ "gpt-5.1": "GPT 5.1",
145
+ "gpt-5.1-chat-latest": "GPT 5.1 Chat (Latest)",
146
+ "gpt-5.1-codex": "GPT 5.1 Codex",
147
+ "gpt-5.1-codex-max": "GPT 5.1 Codex Max",
148
+ "gpt-5": "GPT 5",
149
+ "gpt-5-chat-latest": "GPT 5 Chat (Latest)",
150
+ "gpt-5-mini": "GPT 5 Mini",
151
+ "gpt-5-nano": "GPT 5 Nano",
152
+ "gpt-5-codex": "GPT 5 Codex",
153
+ "gpt-5-pro": "GPT 5 Pro",
154
+ "gpt-4.1": "GPT 4.1",
155
+ "gpt-4.1-mini": "GPT 4.1 Mini",
156
+ "gpt-4.1-nano": "GPT 4.1 Nano",
157
+ "gpt-4o": "GPT 4o",
158
+ "gpt-4o-mini": "GPT 4o Mini",
159
+ "o3-pro": "O3 Pro",
160
+ "o3": "O3",
161
+ "o4-mini": "O4 Mini",
162
+ "o3-mini": "O3 Mini",
163
+ };
164
+
139
165
  const ANTHROPIC_MODEL_LABELS: Record<string, string> = {
140
166
  "claude-opus-4-61": "Claude Opus 4.6",
141
167
  "claude-sonnet-4-51": "Claude Sonnet 4.5",
@@ -209,6 +235,9 @@ export function getModelDisplayLabel(modelId: string, connection: ModelConnectio
209
235
  if (connection.provider === "google") {
210
236
  return GOOGLE_MODEL_LABELS[modelId] ?? humanizeModelId(modelId);
211
237
  }
238
+ if (connection.provider === "openai") {
239
+ return OPENAI_MODEL_LABELS[modelId] ?? humanizeModelId(modelId);
240
+ }
212
241
  return modelId;
213
242
  }
214
243