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.
- package/package.json +1 -1
- package/template/docs/plan-projects-sidebar-ui.md +11 -0
- package/template/next-env.d.ts +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +81 -80
- package/template/src/app/globals.css +98 -9
- package/template/src/app/page.tsx +22 -7
- package/template/src/components/MapView.tsx +21 -2
- package/template/src/components/MessageCard.tsx +83 -20
- package/template/src/components/SearchModal.tsx +1 -1
- package/template/src/components/SettingsModal.tsx +130 -58
- package/template/src/components/Sidebar.tsx +2 -4
- package/template/src/components/TopBar.tsx +4 -10
- package/template/src/lib/data.ts +5 -0
- package/template/src/lib/model-presets.ts +29 -0
- package/template/src/lib/tooling/tools/files.ts +486 -34
- package/template/src/lib/tooling/tools/schedule.ts +27 -5
|
@@ -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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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 ||
|
|
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 ||
|
|
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:
|
|
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
|
-
? "
|
|
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
|
-
<
|
|
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
|
-
|
|
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.
|
|
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="
|
|
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
|
|
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)]">
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
<
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
|
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-
|
|
87
|
-
Your
|
|
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,
|
|
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}
|
package/template/src/lib/data.ts
CHANGED
|
@@ -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
|
|