iris-chatbot 0.2.5 → 2.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/= +0 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +4 -3
- package/template/src/app/api/local-sync/route.ts +20 -9
- package/template/src/app/globals.css +32 -32
- package/template/src/app/page.tsx +10 -9
- package/template/src/components/ChatView.tsx +16 -2
- package/template/src/components/Composer.tsx +13 -4
- package/template/src/components/MessageCard.tsx +15 -7
- package/template/src/components/SettingsModal.tsx +11 -8
- package/template/src/components/Sidebar.tsx +0 -4
- package/template/src/components/TopBar.tsx +20 -16
- package/template/src/lib/memory.ts +133 -2
- package/template/src/lib/tooling/tools/music.ts +16 -5
- package/template/src/lib/tooling/tools/schedule.ts +813 -43
package/package.json
CHANGED
package/template/=
ADDED
|
File without changes
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iris",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "2.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -41,13 +41,14 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
41
41
|
"You can access the user's local computer through provided tools.",
|
|
42
42
|
"When the user asks for file, app, notes, music, web, calendar, or system actions, call tools directly.",
|
|
43
43
|
"Default to execution instead of clarification. Ask at most one question only when a truly required value is missing.",
|
|
44
|
+
"Calendar: You have direct access to the user's Apple Calendar via calendar_list_events and related tools. You CAN see their events—call the tools; do not say you cannot access or see their calendar. Never ask the user to paste their calendar, export it, or send a screenshot; you already have access. When they ask what's on the calendar, what's happening this/next weekend/month, or say 'check' or 'you can see', call calendar_list_events immediately with from/to for the range (e.g. next month → from: 'today', to: 'end of next month'; specific day → YYYY-MM-DD for both). Always pass calendar: 'all'. from/to support 'today', 'tomorrow', 'next week', 'end of next week', 'next month', 'end of next month'. Report the exact queried range from the tool result. For move/delete/reschedule: call calendar_list_events first to get uids. For rescheduling to a new time on the same calendar use calendar_update_event (preserves recurrence); for delete use calendar_delete_event; use calendar_move_event only when update is not sufficient (e.g. user says move and you need delete+recreate).",
|
|
44
45
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
45
46
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
46
47
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
47
48
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
48
49
|
"Do not stop after completing just one action when additional requested actions remain.",
|
|
49
50
|
"For multi-step requests, prefer a single workflow tool call when available.",
|
|
50
|
-
"When tools are enabled, never
|
|
51
|
+
"When tools are enabled, never claim you cannot access the user's device or calendar. You have calendar tools—use them; do not refuse or ask the user to paste/export/screenshot their calendar.",
|
|
51
52
|
"After tool results arrive, provide a brief final answer and stop.",
|
|
52
53
|
"Summarize tool activity in plain English and avoid raw JSON, stack traces, or script line numbers.",
|
|
53
54
|
"Respect tool errors and provide clear next steps.",
|
|
@@ -67,7 +68,7 @@ const TOOL_INTENT_NOTES_PATTERN =
|
|
|
67
68
|
const TOOL_INTENT_MUSIC_PATTERN =
|
|
68
69
|
/\b(music|song|track|album|playlist|apple music)\b[\s\S]{0,80}\b(play|pause|resume|skip|next|previous|volume|set|stop)\b|\b(play|pause|resume|skip|next|previous|volume|set|stop)\b[\s\S]{0,80}\b(music|song|track|album|playlist|apple music)\b/i;
|
|
69
70
|
const TOOL_INTENT_SCHEDULE_PATTERN =
|
|
70
|
-
/\b(calendar|event|reminder|schedule)\b[\s\S]{0,80}\b(create|add|list|show|set|remind|schedule)\b|\b(create|add|list|show|set|remind|schedule)\b[\s\S]{0,80}\b(calendar|event|reminder|schedule)\b/i;
|
|
71
|
+
/\b(calendar|event|reminder|schedule|events)\b[\s\S]{0,80}\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b|\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b[\s\S]{0,80}\b(calendar|event|reminder|schedule|events)\b|\b(move|reschedule|change|delete)\b[\s\S]{0,80}\b(event|meeting|appointment)\b|\b(what's happening|whats happening|what's on|whats on|next weekend|this weekend|you can see|just check)\b/i;
|
|
71
72
|
const TOOL_INTENT_FILES_PATTERN =
|
|
72
73
|
/\b(file|folder|directory|path|document)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b[\s\S]{0,80}\b(file|folder|directory|path|document)\b/i;
|
|
73
74
|
const TOOL_INTENT_APPS_PATTERN =
|
|
@@ -659,7 +660,7 @@ function humanizeToolErrorMessage(toolName: string, message: string): string {
|
|
|
659
660
|
const cleaned = stripTechnicalToolError(message);
|
|
660
661
|
const normalized = cleaned.toLowerCase().replace(/[\u2019]/g, "'");
|
|
661
662
|
|
|
662
|
-
if (toolName === "calendar_create_event" && normalized.includes("can't get date")) {
|
|
663
|
+
if ((toolName === "calendar_create_event" || toolName === "calendar_list_events") && normalized.includes("can't get date")) {
|
|
663
664
|
const requestedDateMatch = cleaned.match(/can't get date "([^"]+)"/i);
|
|
664
665
|
if (requestedDateMatch?.[1]) {
|
|
665
666
|
const parsed = new Date(requestedDateMatch[1]);
|
|
@@ -5,11 +5,11 @@ import path from "path";
|
|
|
5
5
|
export const runtime = "nodejs";
|
|
6
6
|
export const dynamic = "force-dynamic";
|
|
7
7
|
|
|
8
|
-
const DATA_DIR = ".iris-data";
|
|
9
8
|
const STATE_FILE = "state.json";
|
|
9
|
+
const DATA_DIR = ".iris-data";
|
|
10
10
|
|
|
11
|
-
function getStatePath(): string {
|
|
12
|
-
return path.join(process.cwd(),
|
|
11
|
+
function getStatePath(dir: string): string {
|
|
12
|
+
return path.join(process.cwd(), dir, STATE_FILE);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function getDataDir(): string {
|
|
@@ -26,18 +26,29 @@ const EMPTY_PAYLOAD = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export async function GET() {
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const payload = {
|
|
30
|
+
threads: EMPTY_PAYLOAD.threads,
|
|
31
|
+
messages: EMPTY_PAYLOAD.messages,
|
|
32
|
+
settings: EMPTY_PAYLOAD.settings,
|
|
33
|
+
toolEvents: EMPTY_PAYLOAD.toolEvents,
|
|
34
|
+
toolApprovals: EMPTY_PAYLOAD.toolApprovals,
|
|
35
|
+
memories: EMPTY_PAYLOAD.memories,
|
|
36
|
+
};
|
|
37
|
+
const tryRead = async (dir: string) => {
|
|
38
|
+
const statePath = getStatePath(dir);
|
|
31
39
|
const raw = await fs.readFile(statePath, "utf-8");
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
const data = await tryRead(DATA_DIR);
|
|
44
|
+
Object.assign(payload, {
|
|
34
45
|
threads: Array.isArray(data.threads) ? data.threads : EMPTY_PAYLOAD.threads,
|
|
35
46
|
messages: Array.isArray(data.messages) ? data.messages : EMPTY_PAYLOAD.messages,
|
|
36
47
|
settings: Array.isArray(data.settings) ? data.settings : EMPTY_PAYLOAD.settings,
|
|
37
48
|
toolEvents: Array.isArray(data.toolEvents) ? data.toolEvents : EMPTY_PAYLOAD.toolEvents,
|
|
38
49
|
toolApprovals: Array.isArray(data.toolApprovals) ? data.toolApprovals : EMPTY_PAYLOAD.toolApprovals,
|
|
39
50
|
memories: Array.isArray(data.memories) ? data.memories : EMPTY_PAYLOAD.memories,
|
|
40
|
-
};
|
|
51
|
+
});
|
|
41
52
|
return Response.json(payload);
|
|
42
53
|
} catch (err) {
|
|
43
54
|
const code = err && typeof err === "object" && "code" in err ? (err as NodeJS.ErrnoException).code : null;
|
|
@@ -49,7 +60,7 @@ export async function GET() {
|
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
export async function POST(request: NextRequest) {
|
|
52
|
-
const statePath = getStatePath();
|
|
63
|
+
const statePath = getStatePath(DATA_DIR);
|
|
53
64
|
const dir = getDataDir();
|
|
54
65
|
let body: unknown;
|
|
55
66
|
try {
|
|
@@ -120,8 +120,8 @@ a {
|
|
|
120
120
|
margin: 0;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
.message-row.user .message-content
|
|
124
|
-
.message-row.user .message-content
|
|
123
|
+
.message-row.user .message-content> :first-child,
|
|
124
|
+
.message-row.user .message-content> :last-child {
|
|
125
125
|
margin-top: 0;
|
|
126
126
|
margin-bottom: 0;
|
|
127
127
|
}
|
|
@@ -323,12 +323,23 @@ a {
|
|
|
323
323
|
margin: 1.05em 0 0.55em;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
.message-content h1 {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
.message-content h1 {
|
|
327
|
+
font-size: 1.5rem;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.message-content h2 {
|
|
331
|
+
font-size: 1.3rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.message-content h3 {
|
|
335
|
+
font-size: 1.14rem;
|
|
336
|
+
}
|
|
330
337
|
|
|
331
|
-
.message-content
|
|
338
|
+
.message-content h4 {
|
|
339
|
+
font-size: 1.03rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.message-content> :first-child {
|
|
332
343
|
margin-top: 0;
|
|
333
344
|
}
|
|
334
345
|
|
|
@@ -342,15 +353,20 @@ a {
|
|
|
342
353
|
padding-left: 1.5rem;
|
|
343
354
|
}
|
|
344
355
|
|
|
345
|
-
.message-content ul {
|
|
346
|
-
|
|
356
|
+
.message-content ul {
|
|
357
|
+
list-style: disc;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.message-content ol {
|
|
361
|
+
list-style: decimal;
|
|
362
|
+
}
|
|
347
363
|
|
|
348
364
|
.message-content li {
|
|
349
365
|
margin: 0.28em 0;
|
|
350
366
|
}
|
|
351
367
|
|
|
352
|
-
.message-content li
|
|
353
|
-
.message-content li
|
|
368
|
+
.message-content li>ul,
|
|
369
|
+
.message-content li>ol {
|
|
354
370
|
margin: 0.35em 0 0.15em;
|
|
355
371
|
}
|
|
356
372
|
|
|
@@ -378,7 +394,7 @@ a {
|
|
|
378
394
|
border-bottom: 1px solid var(--border-strong);
|
|
379
395
|
}
|
|
380
396
|
|
|
381
|
-
.message-content tr
|
|
397
|
+
.message-content tr> :last-child {
|
|
382
398
|
border-right: 0;
|
|
383
399
|
}
|
|
384
400
|
|
|
@@ -691,35 +707,17 @@ a {
|
|
|
691
707
|
}
|
|
692
708
|
|
|
693
709
|
.chat-shell:not(.collapsed) {
|
|
694
|
-
grid-template-columns:
|
|
710
|
+
grid-template-columns: var(--mobile-sidebar-width) minmax(0, 1fr);
|
|
695
711
|
}
|
|
696
712
|
|
|
697
713
|
.sidebar:not(.collapsed) {
|
|
698
|
-
position: fixed;
|
|
699
|
-
left: 0;
|
|
700
|
-
top: 0;
|
|
701
714
|
width: var(--mobile-sidebar-width);
|
|
702
715
|
min-width: var(--mobile-sidebar-width);
|
|
703
716
|
max-width: var(--mobile-sidebar-width);
|
|
704
|
-
height: 100vh;
|
|
705
|
-
z-index: 60;
|
|
706
717
|
border-right: 1px solid var(--sidebar-border);
|
|
707
|
-
box-shadow: 4px 0 14px rgba(0, 0, 0, 0.16);
|
|
708
718
|
background: var(--sidebar);
|
|
709
719
|
}
|
|
710
720
|
|
|
711
|
-
.chat-shell:not(.collapsed)::after {
|
|
712
|
-
content: "";
|
|
713
|
-
position: fixed;
|
|
714
|
-
top: 0;
|
|
715
|
-
left: var(--mobile-sidebar-width);
|
|
716
|
-
right: 0;
|
|
717
|
-
bottom: 0;
|
|
718
|
-
background: rgba(0, 0, 0, 0.18);
|
|
719
|
-
pointer-events: none;
|
|
720
|
-
z-index: 50;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
721
|
.chat-scroll {
|
|
724
722
|
padding-left: 12px;
|
|
725
723
|
padding-right: 12px;
|
|
@@ -743,6 +741,7 @@ a {
|
|
|
743
741
|
max-width: 92%;
|
|
744
742
|
}
|
|
745
743
|
}
|
|
744
|
+
|
|
746
745
|
.model-menu {
|
|
747
746
|
position: absolute;
|
|
748
747
|
top: calc(100% + 10px);
|
|
@@ -799,10 +798,11 @@ a {
|
|
|
799
798
|
color: var(--text-primary);
|
|
800
799
|
transform: translateY(-1px);
|
|
801
800
|
}
|
|
801
|
+
|
|
802
802
|
.chat-scroll {
|
|
803
803
|
padding-top: calc(var(--topbar-height) - 20px);
|
|
804
804
|
}
|
|
805
805
|
|
|
806
806
|
.chat-scroll.empty {
|
|
807
807
|
padding-top: var(--topbar-height);
|
|
808
|
-
}
|
|
808
|
+
}
|
|
@@ -118,11 +118,11 @@ export default function Home() {
|
|
|
118
118
|
? "var(--font-manrope)"
|
|
119
119
|
: settings.font === "poppins"
|
|
120
120
|
? "var(--font-poppins)"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
: settings.font === "sora"
|
|
122
|
+
? "var(--font-sora)"
|
|
123
|
+
: settings.font === "space"
|
|
124
|
+
? "var(--font-space)"
|
|
125
|
+
: "var(--font-sans)";
|
|
126
126
|
document.documentElement.style.setProperty("--app-font", fontVar);
|
|
127
127
|
document.body.style.fontFamily = fontVar;
|
|
128
128
|
}, [settings, connectionOverrideId, setConnectionOverrideId]);
|
|
@@ -370,7 +370,7 @@ export default function Home() {
|
|
|
370
370
|
onOpenSearch={() => setSearchOpen(true)}
|
|
371
371
|
/>
|
|
372
372
|
|
|
373
|
-
<div className="flex h-
|
|
373
|
+
<div className="flex h-screen min-w-0 flex-col overflow-hidden">
|
|
374
374
|
<TopBar
|
|
375
375
|
connectionId={connection?.id ?? ""}
|
|
376
376
|
connectionName={connection?.name ?? "No connection"}
|
|
@@ -380,8 +380,6 @@ export default function Home() {
|
|
|
380
380
|
localToolsEnabled={Boolean(settings?.localTools?.enabled)}
|
|
381
381
|
modelPresets={modelPresets}
|
|
382
382
|
viewMode={viewMode}
|
|
383
|
-
onConnectionChange={handleConnectionChange}
|
|
384
|
-
onModelChange={handleModelChange}
|
|
385
383
|
onToggleView={() => {
|
|
386
384
|
const nextViewMode = viewMode === "chat" ? "map" : "chat";
|
|
387
385
|
if (nextViewMode === "chat") {
|
|
@@ -389,11 +387,14 @@ export default function Home() {
|
|
|
389
387
|
}
|
|
390
388
|
setViewMode(nextViewMode);
|
|
391
389
|
}}
|
|
390
|
+
showMapButton={Boolean(conversationMessages?.length)}
|
|
391
|
+
onConnectionChange={handleConnectionChange}
|
|
392
|
+
onModelChange={handleModelChange}
|
|
392
393
|
onEnableLocalTools={handleEnableLocalTools}
|
|
393
394
|
onOpenSettings={() => setSettingsOpen(true)}
|
|
394
395
|
/>
|
|
395
396
|
|
|
396
|
-
<div className="flex-1 min-w-0">
|
|
397
|
+
<div className="flex-1 min-w-0 h-full overflow-hidden">
|
|
397
398
|
{viewMode === "map" ? (
|
|
398
399
|
<MapView
|
|
399
400
|
threads={threads || []}
|
|
@@ -31,7 +31,8 @@ import MessageCard from "./MessageCard";
|
|
|
31
31
|
import Composer from "./Composer";
|
|
32
32
|
|
|
33
33
|
const SYSTEM_PROMPT =
|
|
34
|
-
"You are a helpful assistant. Always return valid Markdown. Use ATX headings (##, ###), bold labels, lists, and tables when useful."
|
|
34
|
+
"You are a helpful assistant. Always return valid Markdown. Use ATX headings (##, ###), bold labels, lists, and tables when useful. " +
|
|
35
|
+
"If the user has a stored preference for default calendar or timezone (see on-device memory context), use it when calling calendar tools and when interpreting dates, unless the user says otherwise.";
|
|
35
36
|
|
|
36
37
|
const EMPTY_PROMPTS = [
|
|
37
38
|
"What are you working on?",
|
|
@@ -899,6 +900,15 @@ export default function ChatView({
|
|
|
899
900
|
provider: connection.kind === "builtin" ? connection.provider ?? connection.id : connection.id,
|
|
900
901
|
model,
|
|
901
902
|
});
|
|
903
|
+
const lastAssistantMessage = userMessage.parentId
|
|
904
|
+
? messageMap.get(userMessage.parentId)
|
|
905
|
+
: undefined;
|
|
906
|
+
const lastAssistantMessageContent =
|
|
907
|
+
lastAssistantMessage?.role === "assistant"
|
|
908
|
+
? (typeof lastAssistantMessage.content === "string"
|
|
909
|
+
? lastAssistantMessage.content
|
|
910
|
+
: "")
|
|
911
|
+
: undefined;
|
|
902
912
|
const memoryEnabled = settings.memory?.enabled !== false;
|
|
903
913
|
const memoryAutoCaptureEnabled =
|
|
904
914
|
memoryEnabled && settings.memory?.autoCapture !== false;
|
|
@@ -915,6 +925,7 @@ export default function ChatView({
|
|
|
915
925
|
text: trimmed,
|
|
916
926
|
conversationId: resolvedThread.conversationId,
|
|
917
927
|
settingsMemory: settings.memory,
|
|
928
|
+
lastAssistantMessageContent,
|
|
918
929
|
}).catch(() => {
|
|
919
930
|
// Memory capture is best-effort and must not block chat.
|
|
920
931
|
});
|
|
@@ -1368,8 +1379,11 @@ export default function ChatView({
|
|
|
1368
1379
|
return;
|
|
1369
1380
|
}
|
|
1370
1381
|
pendingSendScrollThreadIdRef.current = null;
|
|
1382
|
+
// Double rAF so we scroll after React has committed and the browser has laid out the new message
|
|
1371
1383
|
requestAnimationFrame(() => {
|
|
1372
|
-
|
|
1384
|
+
requestAnimationFrame(() => {
|
|
1385
|
+
scrollToBottom("smooth");
|
|
1386
|
+
});
|
|
1373
1387
|
});
|
|
1374
1388
|
}, [thread?.id, threadMessages.length, activeStreamingMessageId, scrollToBottom]);
|
|
1375
1389
|
|
|
@@ -64,7 +64,7 @@ export default function Composer({
|
|
|
64
64
|
[onChange, value],
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
-
const resizeTextarea = useCallback(() => {
|
|
67
|
+
const resizeTextarea = useCallback((currentValue: string) => {
|
|
68
68
|
const element = textareaRef.current;
|
|
69
69
|
if (!element) {
|
|
70
70
|
return;
|
|
@@ -78,6 +78,15 @@ export default function Composer({
|
|
|
78
78
|
const minHeight = lineHeight * minRowsRef.current + paddingTop + paddingBottom;
|
|
79
79
|
const maxHeight = lineHeight * maxRowsRef.current + paddingTop + paddingBottom;
|
|
80
80
|
|
|
81
|
+
// When empty, always use single-line height. Otherwise we can get a bogus scrollHeight
|
|
82
|
+
// if the textarea hasn't received its final width yet (e.g. on mount or when switching chats),
|
|
83
|
+
// which makes the field appear super extended until the user types.
|
|
84
|
+
if (currentValue.trim().length === 0) {
|
|
85
|
+
element.style.height = `${minHeight}px`;
|
|
86
|
+
element.style.overflowY = "hidden";
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
// Use scrollHeight so wrapped lines (no explicit newline) are included; min 1 line, max 8
|
|
82
91
|
element.style.height = "0px";
|
|
83
92
|
const scrollHeight = element.scrollHeight;
|
|
@@ -87,18 +96,18 @@ export default function Composer({
|
|
|
87
96
|
}, []);
|
|
88
97
|
|
|
89
98
|
useLayoutEffect(() => {
|
|
90
|
-
resizeTextarea();
|
|
99
|
+
resizeTextarea(value);
|
|
91
100
|
}, [value, resizeTextarea]);
|
|
92
101
|
|
|
93
102
|
useEffect(() => {
|
|
94
103
|
const handleResize = () => {
|
|
95
|
-
resizeTextarea();
|
|
104
|
+
resizeTextarea(value);
|
|
96
105
|
};
|
|
97
106
|
window.addEventListener("resize", handleResize);
|
|
98
107
|
return () => {
|
|
99
108
|
window.removeEventListener("resize", handleResize);
|
|
100
109
|
};
|
|
101
|
-
}, [resizeTextarea]);
|
|
110
|
+
}, [resizeTextarea, value]);
|
|
102
111
|
|
|
103
112
|
return (
|
|
104
113
|
<div className="composer flex items-end gap-3">
|
|
@@ -249,15 +249,16 @@ function summarizeToolError(toolName: string, rawError: string): {
|
|
|
249
249
|
const cleaned = cleanTechnicalError(rawError);
|
|
250
250
|
const normalized = cleaned.toLowerCase().replace(/[\u2019]/g, "'");
|
|
251
251
|
|
|
252
|
-
if (toolName === "calendar_create_event" && normalized.includes("can't get date")) {
|
|
252
|
+
if ((toolName === "calendar_create_event" || toolName === "calendar_list_events") && normalized.includes("can't get date")) {
|
|
253
253
|
const requestedDateMatch = cleaned.match(/can't get date "([^"]+)"/i);
|
|
254
254
|
const requestedDate = requestedDateMatch?.[1] ? formatDateTime(requestedDateMatch[1]) : null;
|
|
255
|
+
const isList = toolName === "calendar_list_events";
|
|
255
256
|
return {
|
|
256
|
-
title: "Could not add the calendar event",
|
|
257
|
+
title: isList ? "Could not list calendar events" : "Could not add the calendar event",
|
|
257
258
|
detail:
|
|
258
259
|
requestedDate
|
|
259
|
-
? `Calendar could not read the date/time (${requestedDate}). Try a format like "Feb 12 at 9:00 AM".`
|
|
260
|
-
: 'Calendar could not read the date/time. Try a format like "Feb 12 at 9:00 AM".',
|
|
260
|
+
? `Calendar could not read the date/time (${requestedDate}). Try a format like "Feb 12 at 9:00 AM" or "next Monday".`
|
|
261
|
+
: 'Calendar could not read the date/time. Try a format like "Feb 12 at 9:00 AM" or "next Monday".',
|
|
261
262
|
};
|
|
262
263
|
}
|
|
263
264
|
|
|
@@ -451,15 +452,22 @@ function summarizeToolResult(toolName: string, payload: Record<string, unknown>
|
|
|
451
452
|
|
|
452
453
|
if (toolName === "calendar_list_events") {
|
|
453
454
|
const count = getNumber(payload, "count");
|
|
454
|
-
const
|
|
455
|
-
const
|
|
455
|
+
const queriedRange = getString(payload, "queriedRange");
|
|
456
|
+
const fromResolved = getString(payload, "fromResolved");
|
|
457
|
+
const toResolved = getString(payload, "toResolved");
|
|
456
458
|
if (count !== null) {
|
|
459
|
+
const detail = queriedRange
|
|
460
|
+
? queriedRange
|
|
461
|
+
: joinDetailParts([
|
|
462
|
+
fromResolved ? `From: ${formatDateTime(fromResolved) ?? fromResolved}` : null,
|
|
463
|
+
toResolved ? `To: ${formatDateTime(toResolved) ?? toResolved}` : null,
|
|
464
|
+
]);
|
|
457
465
|
return {
|
|
458
466
|
title:
|
|
459
467
|
count === 0
|
|
460
468
|
? "No events found"
|
|
461
469
|
: `Found ${formatCount(count)} calendar ${count === 1 ? "event" : "events"}`,
|
|
462
|
-
detail
|
|
470
|
+
detail,
|
|
463
471
|
};
|
|
464
472
|
}
|
|
465
473
|
}
|
|
@@ -559,14 +559,6 @@ export default function SettingsModal({
|
|
|
559
559
|
<div className="mt-5 max-h-[68vh] overflow-y-auto pr-1 text-sm">
|
|
560
560
|
{activeTab === "models" ? (
|
|
561
561
|
<div className="space-y-4">
|
|
562
|
-
<label className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-3 text-sm text-[var(--text-secondary)]">
|
|
563
|
-
<input
|
|
564
|
-
type="checkbox"
|
|
565
|
-
checked={showExtendedOpenAIModels}
|
|
566
|
-
onChange={(event) => setShowExtendedOpenAIModels(event.target.checked)}
|
|
567
|
-
/>
|
|
568
|
-
Show extended OpenAI model list (non-frontier models)
|
|
569
|
-
</label>
|
|
570
562
|
<div>
|
|
571
563
|
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
572
564
|
Default Connection
|
|
@@ -624,6 +616,17 @@ export default function SettingsModal({
|
|
|
624
616
|
</div>
|
|
625
617
|
) : null}
|
|
626
618
|
</div>
|
|
619
|
+
{selectedDefaultConnection?.kind === "builtin" &&
|
|
620
|
+
selectedDefaultConnection?.provider === "openai" ? (
|
|
621
|
+
<label className="mt-3 flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
622
|
+
<input
|
|
623
|
+
type="checkbox"
|
|
624
|
+
checked={showExtendedOpenAIModels}
|
|
625
|
+
onChange={(event) => setShowExtendedOpenAIModels(event.target.checked)}
|
|
626
|
+
/>
|
|
627
|
+
Show extended OpenAI model list (non-frontier models)
|
|
628
|
+
</label>
|
|
629
|
+
) : null}
|
|
627
630
|
</div>
|
|
628
631
|
) : null}
|
|
629
632
|
</div>
|
|
@@ -116,10 +116,6 @@ export default function Sidebar({
|
|
|
116
116
|
))}
|
|
117
117
|
</div>
|
|
118
118
|
</div>
|
|
119
|
-
|
|
120
|
-
<div className="border-t border-[var(--border)] px-4 py-4 text-xs text-[var(--text-muted)]">
|
|
121
|
-
<span className="sidebar-text">Local mode</span>
|
|
122
|
-
</div>
|
|
123
119
|
</>
|
|
124
120
|
) : (
|
|
125
121
|
<div className="flex-1 px-2 pb-4 pt-4">
|
|
@@ -16,6 +16,7 @@ export default function TopBar({
|
|
|
16
16
|
onModelChange,
|
|
17
17
|
viewMode,
|
|
18
18
|
onToggleView,
|
|
19
|
+
showMapButton,
|
|
19
20
|
onEnableLocalTools,
|
|
20
21
|
onOpenSettings,
|
|
21
22
|
modelPresets,
|
|
@@ -30,6 +31,7 @@ export default function TopBar({
|
|
|
30
31
|
onModelChange: (model: string) => void;
|
|
31
32
|
viewMode: "chat" | "map";
|
|
32
33
|
onToggleView: () => void;
|
|
34
|
+
showMapButton: boolean;
|
|
33
35
|
onEnableLocalTools: () => void | Promise<void>;
|
|
34
36
|
onOpenSettings: () => void;
|
|
35
37
|
modelPresets: string[];
|
|
@@ -136,22 +138,24 @@ export default function TopBar({
|
|
|
136
138
|
Enable Local Tools
|
|
137
139
|
</button>
|
|
138
140
|
) : null}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
{showMapButton ? (
|
|
142
|
+
<button
|
|
143
|
+
className="flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-[var(--bg)] px-3 py-2 text-xs text-[var(--text-secondary)] hover:border-[var(--border-strong)] sm:gap-2 sm:px-4 sm:text-sm"
|
|
144
|
+
onClick={onToggleView}
|
|
145
|
+
>
|
|
146
|
+
{viewMode === "chat" ? (
|
|
147
|
+
<>
|
|
148
|
+
<Waypoints className="h-5 w-5" />
|
|
149
|
+
<span className="hidden sm:inline">Map</span>
|
|
150
|
+
</>
|
|
151
|
+
) : (
|
|
152
|
+
<>
|
|
153
|
+
<MessageSquare className="h-5 w-5" />
|
|
154
|
+
<span className="hidden sm:inline">Chat</span>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</button>
|
|
158
|
+
) : null}
|
|
155
159
|
<button
|
|
156
160
|
className="rounded-full border border-[var(--border)] bg-[var(--bg)] p-2.5 text-[var(--text-secondary)] hover:border-[var(--border-strong)]"
|
|
157
161
|
onClick={onOpenSettings}
|