iris-chatbot 4.0.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.
@@ -19,6 +19,13 @@ import { splitContentAndSources } from "../lib/utils";
19
19
 
20
20
  const MAX_VISIBLE_TOOL_ITEMS = 8;
21
21
  const HIDDEN_TIMELINE_TOOLS = new Set(["tooling", "workflow_run"]);
22
+ const FILE_FIND_ALLOWED_ROOTS_ERROR = "Path is outside allowed roots";
23
+
24
+ function isHiddenFileFindError(event: ToolEvent): boolean {
25
+ if (event.toolName !== "file_find" || event.stage !== "result") return false;
26
+ const text = [event.message, event.payloadJson].filter(Boolean).join(" ");
27
+ return text.includes(FILE_FIND_ALLOWED_ROOTS_ERROR);
28
+ }
22
29
  const TOOL_TIMELINE_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
23
30
  month: "short",
24
31
  day: "numeric",
@@ -149,6 +156,8 @@ function humanizeToolName(toolName: string): string {
149
156
  file_copy: "Copy Item",
150
157
  file_mkdir: "Create Folder",
151
158
  file_delete_to_trash: "Move to Trash",
159
+ file_batch_move: "Move Files",
160
+ file_find: "Find Files",
152
161
  notes_create_or_append: "Update Note",
153
162
  notes_find: "Search Notes",
154
163
  app_open: "Open App",
@@ -265,7 +274,7 @@ function summarizeToolError(toolName: string, rawError: string): {
265
274
  if (normalized.includes("missing required")) {
266
275
  return {
267
276
  title: "Missing required information",
268
- detail: "The action did not include all required fields.",
277
+ detail: cleaned.length < 200 ? cleaned : "The action did not include all required fields.",
269
278
  };
270
279
  }
271
280
 
@@ -365,6 +374,23 @@ function summarizeToolCall(toolName: string, payload: Record<string, unknown> |
365
374
  };
366
375
  }
367
376
 
377
+ if (toolName === "file_batch_move") {
378
+ const operations = payload.operations;
379
+ const count = Array.isArray(operations) ? operations.length : 0;
380
+ return {
381
+ title: count > 0 ? `Moving ${count} files` : "Moving files",
382
+ };
383
+ }
384
+
385
+ if (toolName === "file_find") {
386
+ const name = getString(payload, "name");
387
+ const searchPath = getString(payload, "searchPath");
388
+ return {
389
+ title: name ? `Searching for "${name}"` : "Searching for files",
390
+ detail: searchPath ? `In ${shortenPath(searchPath)}` : undefined,
391
+ };
392
+ }
393
+
368
394
  return { title: "Preparing action" };
369
395
  }
370
396
 
@@ -459,9 +485,9 @@ function summarizeToolResult(toolName: string, payload: Record<string, unknown>
459
485
  const detail = queriedRange
460
486
  ? queriedRange
461
487
  : joinDetailParts([
462
- fromResolved ? `From: ${formatDateTime(fromResolved) ?? fromResolved}` : null,
463
- toResolved ? `To: ${formatDateTime(toResolved) ?? toResolved}` : null,
464
- ]);
488
+ fromResolved ? `From: ${formatDateTime(fromResolved) ?? fromResolved}` : null,
489
+ toResolved ? `To: ${formatDateTime(toResolved) ?? toResolved}` : null,
490
+ ]);
465
491
  return {
466
492
  title:
467
493
  count === 0
@@ -559,6 +585,46 @@ function summarizeToolResult(toolName: string, payload: Record<string, unknown>
559
585
  };
560
586
  }
561
587
 
588
+ if (toolName === "file_batch_move") {
589
+ const movedCount = getNumber(payload, "movedCount");
590
+ const failedCount = getNumber(payload, "failedCount");
591
+ const totalOps = getNumber(payload, "totalOperations");
592
+ if (movedCount !== null && failedCount !== null) {
593
+ if (failedCount === 0) {
594
+ return {
595
+ title: `Moved ${movedCount} files successfully`,
596
+ };
597
+ }
598
+ return {
599
+ title: `Moved ${movedCount} files`,
600
+ detail: `${failedCount} failed`,
601
+ };
602
+ }
603
+ if (totalOps !== null) {
604
+ return { title: `Processed ${totalOps} files` };
605
+ }
606
+ }
607
+
608
+ if (toolName === "file_find") {
609
+ const totalFound = getNumber(payload, "totalFound");
610
+ const searchName = getString(payload, "searchName");
611
+ const matches = payload.matches;
612
+ const topMatch = Array.isArray(matches) && matches.length > 0 ? matches[0] as Record<string, unknown> : null;
613
+ const topMatchPath = topMatch ? getString(topMatch, "path") : null;
614
+
615
+ if (totalFound !== null) {
616
+ if (totalFound === 0) {
617
+ return {
618
+ title: searchName ? `No matches for "${searchName}"` : "No matches found",
619
+ };
620
+ }
621
+ return {
622
+ title: `Found ${totalFound} match${totalFound === 1 ? "" : "es"}`,
623
+ detail: topMatchPath ? `Best match: ${shortenPath(topMatchPath)}` : undefined,
624
+ };
625
+ }
626
+ }
627
+
562
628
  return { title: "Completed successfully" };
563
629
  }
564
630
 
@@ -673,6 +739,7 @@ function MessageCard({
673
739
  () =>
674
740
  (toolEvents ?? [])
675
741
  .filter((event) => !HIDDEN_TIMELINE_TOOLS.has(event.toolName))
742
+ .filter((event) => !isHiddenFileFindError(event))
676
743
  .slice()
677
744
  .sort((a, b) => a.createdAt - b.createdAt),
678
745
  [toolEvents],
@@ -874,9 +941,8 @@ function MessageCard({
874
941
  {shelfThreads.map((thread) => (
875
942
  <div key={thread.id} className="thread-box-wrap">
876
943
  <button
877
- className={`thread-box ${
878
- activeThreadId === thread.id ? "active" : ""
879
- }`}
944
+ className={`thread-box ${activeThreadId === thread.id ? "active" : ""
945
+ }`}
880
946
  onClick={() => onSelectThread(thread.id)}
881
947
  >
882
948
  {thread.title}
@@ -903,11 +969,10 @@ function MessageCard({
903
969
  >
904
970
  {isAssistant ? (
905
971
  <button
906
- className={`flex items-center justify-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition ${
907
- canAddThread
908
- ? "hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
909
- : "opacity-60 cursor-not-allowed"
910
- }`}
972
+ className={`flex items-center justify-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition ${canAddThread
973
+ ? "hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
974
+ : "opacity-60 cursor-not-allowed"
975
+ }`}
911
976
  onClick={async () => {
912
977
  if (!canAddThread) return;
913
978
  await onAddThread(message);
@@ -927,9 +992,8 @@ function MessageCard({
927
992
  </button>
928
993
  ) : null}
929
994
  <button
930
- className={`flex items-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)] ${
931
- isAssistant ? "" : "opacity-0 group-hover:opacity-100"
932
- }`}
995
+ className={`flex items-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)] ${isAssistant ? "" : "opacity-0 group-hover:opacity-100"
996
+ }`}
933
997
  onClick={async () => {
934
998
  await navigator.clipboard.writeText(messageTextContent);
935
999
  setCopied(true);
@@ -944,11 +1008,10 @@ function MessageCard({
944
1008
  </button>
945
1009
  {canEditThreads ? (
946
1010
  <button
947
- className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${
948
- threadEditMode
949
- ? "border-[var(--accent)] text-[var(--text-primary)]"
950
- : "border-[var(--border)] text-[var(--text-muted)] hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
951
- }`}
1011
+ className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${threadEditMode
1012
+ ? "border-[var(--accent-ring)] text-[var(--text-primary)]"
1013
+ : "border-[var(--border)] text-[var(--text-muted)] hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
1014
+ }`}
952
1015
  onClick={() => setThreadEditMode((prev) => !prev)}
953
1016
  aria-label="Edit threads"
954
1017
  >
@@ -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