iris-chatbot 2.0.0 → 4.1.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/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 +3 -3
- package/template/src/app/page.tsx +20 -3
- package/template/src/components/ChatView.tsx +4 -4
- package/template/src/components/MessageCard.tsx +83 -20
- package/template/src/components/Sidebar.tsx +17 -7
- package/template/src/lib/tooling/tools/files.ts +486 -34
- package/template/src/lib/tooling/tools/schedule.ts +27 -5
package/package.json
CHANGED
package/template/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/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": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "4.1.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -42,25 +42,26 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
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
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).",
|
|
45
|
+
"Files and Folders: You HAVE direct access to the user's files via file_list and file_find. Never say you cannot see, access, or list their files—call the tools. When the user asks 'what files do I have,' 'files from the last 7 days,' 'recent files,' or similar: call file_list immediately with path set to a folder (e.g. ~/Downloads, ~/Desktop, ~/Documents—or call once per root if you want all) and modifiedInLastDays: 7 (or the number of days they said). You have full file system access via file_list, file_mkdir, file_move, file_copy, file_delete_to_trash, file_batch_move, and file_find. When the user gives a folder or file name (e.g. 'us debt clock'), use file_find with searchPath '~' and the name as given. When moving MULTIPLE files, use file_batch_move with { operations: [ ... ] } or { destination, sources }. For organizing: file_find if location unknown, then file_list (use modifiedInLastDays for 'recent'), then file_mkdir/file_batch_move as needed. Never refuse file tasks or ask the user to use Finder manually—you have the tools.",
|
|
45
46
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
46
47
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
47
48
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
48
49
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
49
50
|
"Do not stop after completing just one action when additional requested actions remain.",
|
|
50
51
|
"For multi-step requests, prefer a single workflow tool call when available.",
|
|
51
|
-
"When tools are enabled, never claim you cannot access the user's device or
|
|
52
|
+
"When tools are enabled, never claim you cannot access the user's device, calendar, or file system. You have the tools—use them; do not refuse or ask the user to do tasks manually that you can do with tools.",
|
|
52
53
|
"After tool results arrive, provide a brief final answer and stop.",
|
|
53
54
|
"Summarize tool activity in plain English and avoid raw JSON, stack traces, or script line numbers.",
|
|
54
55
|
"Respect tool errors and provide clear next steps.",
|
|
55
56
|
].join(" ");
|
|
56
57
|
|
|
57
58
|
const ACTION_VERB_PATTERN =
|
|
58
|
-
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus";
|
|
59
|
+
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus|move|copy|delete|organize|reorganize|restructure|split|separate|sort|categorize";
|
|
59
60
|
const SEQUENCE_MARKER_PATTERN = "first|second|third|fourth|fifth|next|then|finally|lastly";
|
|
60
61
|
const TOOL_INTENT_EXPLICIT_PATTERN =
|
|
61
62
|
/\b(use|run|call)\s+(?:the\s+)?(?:local\s+)?tools?\b|\b(on my (?:mac|computer|device)|locally)\b/i;
|
|
62
63
|
const TOOL_INTENT_LEADING_VERB_PATTERN =
|
|
63
|
-
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop)
|
|
64
|
+
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop|organize|reorganize|restructure|split|separate|sort|categorize|put|place)(?:\s+|$)/i;
|
|
64
65
|
const TOOL_INTENT_MESSAGING_PATTERN =
|
|
65
66
|
/\b(text|message|imessage|sms|mail|email)\b[\s\S]{0,80}\b(send|text|message|email|mail)\b|\b(send|text|message|email|mail)\b[\s\S]{0,80}\b(text|message|imessage|sms|mail|email)\b/i;
|
|
66
67
|
const TOOL_INTENT_NOTES_PATTERN =
|
|
@@ -70,7 +71,7 @@ const TOOL_INTENT_MUSIC_PATTERN =
|
|
|
70
71
|
const TOOL_INTENT_SCHEDULE_PATTERN =
|
|
71
72
|
/\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;
|
|
72
73
|
const TOOL_INTENT_FILES_PATTERN =
|
|
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;
|
|
74
|
+
/\b(file|folder|directory|path|document|subfolder|subfolders)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b[\s\S]{0,80}\b(file|folder|directory|path|document|subfolder|subfolders)\b|\b(organize|reorganize|restructure|split|separate)\b[\s\S]{0,80}\b(into|by|by type|by date|by name)\b|\b(put|move|place)\b[\s\S]{0,40}\b(into|in|inside)\b[\s\S]{0,40}\b(folder|directory)\b|\b(create|make)\b[\s\S]{0,40}\b(folders?|directories?)\b[\s\S]{0,40}\b(for|called|named)\b|\b(recent|last \d+|past \d+)\b[\s\S]{0,40}\b(files?|days?|weeks?)\b|\b(files?)\b[\s\S]{0,40}\b(from|in|within|during)\b[\s\S]{0,40}\b(last|past|recent|\d+)\b|\b(what|which|show|list|my)\b[\s\S]{0,40}\b(files?|documents?)\b/i;
|
|
74
75
|
const TOOL_INTENT_APPS_PATTERN =
|
|
75
76
|
/\b(app|application|window|url|website|browser|system)\b[\s\S]{0,80}\b(open|launch|focus|quit|set|mute|unmute|browse)\b|\b(open|launch|focus|quit|set|mute|unmute|browse)\b[\s\S]{0,80}\b(app|application|window|url|website|browser|system)\b/i;
|
|
76
77
|
const TOOL_INTENT_NUMBERS_PATTERN =
|
|
@@ -86,8 +87,8 @@ function splitCompoundActionClauses(input: string): string[] {
|
|
|
86
87
|
const roughParts = normalized.split(
|
|
87
88
|
new RegExp(
|
|
88
89
|
`\\s*(?:,|;|\\n)+\\s*(?=(?:(?:${SEQUENCE_MARKER_PATTERN})\\b|${ACTION_VERB_PATTERN}\\b))|` +
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
`\\s+(?:and then|and also|as well as|after that|plus|then|also|next|finally|lastly)\\s+|` +
|
|
91
|
+
`\\s+and\\s+(?=(?:${ACTION_VERB_PATTERN})\\b)`,
|
|
91
92
|
"i",
|
|
92
93
|
),
|
|
93
94
|
);
|
|
@@ -234,9 +235,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
234
235
|
: "openai_compatible";
|
|
235
236
|
const provider =
|
|
236
237
|
kind === "builtin" &&
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
(connection.provider === "openai" ||
|
|
239
|
+
connection.provider === "anthropic" ||
|
|
240
|
+
connection.provider === "google")
|
|
240
241
|
? connection.provider
|
|
241
242
|
: undefined;
|
|
242
243
|
const baseUrl =
|
|
@@ -264,9 +265,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
264
265
|
apiKey,
|
|
265
266
|
headers: Array.isArray(connection.headers)
|
|
266
267
|
? connection.headers.filter(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
(header): header is { key: string; value: string } =>
|
|
269
|
+
Boolean(header) && typeof header.key === "string" && typeof header.value === "string",
|
|
270
|
+
)
|
|
270
271
|
: [],
|
|
271
272
|
supportsTools,
|
|
272
273
|
};
|
|
@@ -435,47 +436,47 @@ function sanitizeMemoryContext(
|
|
|
435
436
|
|
|
436
437
|
const people = Array.isArray(aliases.people)
|
|
437
438
|
? aliases.people
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
439
|
+
.map((item) => {
|
|
440
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
441
|
+
const target = typeof item?.target === "string" ? item.target.trim() : "";
|
|
442
|
+
if (!alias || !target) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return { alias, target };
|
|
446
|
+
})
|
|
447
|
+
.filter((item): item is { alias: string; target: string } => Boolean(item))
|
|
448
|
+
.slice(0, 16)
|
|
448
449
|
: [];
|
|
449
450
|
|
|
450
451
|
const music = Array.isArray(aliases.music)
|
|
451
452
|
? aliases.music
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
453
|
+
.map((item) => {
|
|
454
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
455
|
+
const query = typeof item?.query === "string" ? item.query.trim() : "";
|
|
456
|
+
if (!alias || !query) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const title =
|
|
460
|
+
typeof item?.title === "string" && item.title.trim()
|
|
461
|
+
? item.title.trim()
|
|
462
|
+
: undefined;
|
|
463
|
+
const artist =
|
|
464
|
+
typeof item?.artist === "string" && item.artist.trim()
|
|
465
|
+
? item.artist.trim()
|
|
466
|
+
: undefined;
|
|
467
|
+
return { alias, query, title, artist };
|
|
468
|
+
})
|
|
469
|
+
.filter(
|
|
470
|
+
(
|
|
471
|
+
item,
|
|
472
|
+
): item is {
|
|
473
|
+
alias: string;
|
|
474
|
+
query: string;
|
|
475
|
+
title: string | undefined;
|
|
476
|
+
artist: string | undefined;
|
|
477
|
+
} => Boolean(item),
|
|
478
|
+
)
|
|
479
|
+
.slice(0, 16)
|
|
479
480
|
: [];
|
|
480
481
|
|
|
481
482
|
return {
|
|
@@ -679,7 +680,7 @@ function humanizeToolErrorMessage(toolName: string, message: string): string {
|
|
|
679
680
|
}
|
|
680
681
|
|
|
681
682
|
if (normalized.includes("missing required")) {
|
|
682
|
-
const fieldMatch = cleaned.match(/missing required (?:string|numeric) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
|
+
const fieldMatch = cleaned.match(/missing required (?:string|numeric|array) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
684
|
if (fieldMatch?.[1]) {
|
|
684
685
|
return `Missing required field "${fieldMatch[1]}".`;
|
|
685
686
|
}
|
|
@@ -1407,8 +1408,8 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1407
1408
|
"Reminder";
|
|
1408
1409
|
const title = sanitizeReminderTitle(
|
|
1409
1410
|
rawTitle
|
|
1410
|
-
|
|
1411
|
-
|
|
1411
|
+
.replace(/^(that|i have to|to|for)\s+/i, "")
|
|
1412
|
+
.trim(),
|
|
1412
1413
|
);
|
|
1413
1414
|
|
|
1414
1415
|
return { title: title || "Reminder", due };
|
|
@@ -1715,18 +1716,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1715
1716
|
const playResult =
|
|
1716
1717
|
playOutput && typeof playOutput === "object"
|
|
1717
1718
|
? (playOutput as {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1719
|
+
playing?: boolean;
|
|
1720
|
+
matched?: boolean;
|
|
1721
|
+
playlistName?: string | null;
|
|
1722
|
+
title?: string | null;
|
|
1723
|
+
artist?: string | null;
|
|
1724
|
+
album?: string | null;
|
|
1725
|
+
reason?: string | null;
|
|
1726
|
+
matchedTitle?: string | null;
|
|
1727
|
+
matchedArtist?: string | null;
|
|
1728
|
+
catalogTitle?: string | null;
|
|
1729
|
+
catalogArtist?: string | null;
|
|
1730
|
+
})
|
|
1730
1731
|
: null;
|
|
1731
1732
|
|
|
1732
1733
|
if (!playResult || playResult.playing !== true) {
|
|
@@ -1823,18 +1824,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1823
1824
|
}
|
|
1824
1825
|
|
|
1825
1826
|
params.send({
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1827
|
+
type: "token",
|
|
1828
|
+
value:
|
|
1829
|
+
hasNearbyMatch
|
|
1830
|
+
? `Started nearby match "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""}. ${playResult.reason}`
|
|
1831
|
+
: resolvedPlaylistName && intent.volume !== null
|
|
1832
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music and set volume to ${intent.volume}%.`
|
|
1833
|
+
: resolvedPlaylistName
|
|
1834
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music.`
|
|
1835
|
+
: intent.volume !== null
|
|
1835
1836
|
? `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music and set volume to ${intent.volume}%.`
|
|
1836
1837
|
: `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music.`,
|
|
1837
|
-
|
|
1838
|
+
});
|
|
1838
1839
|
return true;
|
|
1839
1840
|
}
|
|
1840
1841
|
|
|
@@ -2159,12 +2160,12 @@ async function runToolOrchestrator(params: {
|
|
|
2159
2160
|
const approval = createApprovalRequest();
|
|
2160
2161
|
params.send({
|
|
2161
2162
|
type: "approval_requested",
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2163
|
+
approvalId: approval.approvalId,
|
|
2164
|
+
callId: call.id,
|
|
2165
|
+
name: call.name,
|
|
2166
|
+
args: callInput,
|
|
2167
|
+
reason: summarizeApprovalReason(call.name, callInput),
|
|
2168
|
+
});
|
|
2168
2169
|
|
|
2169
2170
|
emitThinkingUpdate(`Waiting for approval to run ${call.name}.`);
|
|
2170
2171
|
const decision = await approval.promise;
|
|
@@ -784,8 +784,8 @@ a {
|
|
|
784
784
|
height: 38px;
|
|
785
785
|
width: 38px;
|
|
786
786
|
border-radius: 12px;
|
|
787
|
-
border: 1px solid
|
|
788
|
-
background:
|
|
787
|
+
border: 1px solid transparent;
|
|
788
|
+
background: transparent;
|
|
789
789
|
color: var(--text-secondary);
|
|
790
790
|
display: flex;
|
|
791
791
|
align-items: center;
|
|
@@ -794,7 +794,7 @@ a {
|
|
|
794
794
|
}
|
|
795
795
|
|
|
796
796
|
.sidebar-icon-button:hover {
|
|
797
|
-
border-color: var(--border
|
|
797
|
+
border-color: var(--border);
|
|
798
798
|
color: var(--text-primary);
|
|
799
799
|
transform: translateY(-1px);
|
|
800
800
|
}
|
|
@@ -132,6 +132,11 @@ export default function Home() {
|
|
|
132
132
|
[threads, activeThreadId]
|
|
133
133
|
);
|
|
134
134
|
|
|
135
|
+
const sidebarThreads = useMemo(
|
|
136
|
+
() => threads?.filter((t) => t.headMessageId != null) ?? [],
|
|
137
|
+
[threads]
|
|
138
|
+
);
|
|
139
|
+
|
|
135
140
|
const conversationMessages = useConversationMessages(
|
|
136
141
|
activeThread?.conversationId || null
|
|
137
142
|
);
|
|
@@ -333,11 +338,23 @@ export default function Home() {
|
|
|
333
338
|
}
|
|
334
339
|
>
|
|
335
340
|
<Sidebar
|
|
336
|
-
threads={
|
|
341
|
+
threads={sidebarThreads}
|
|
337
342
|
activeThreadId={activeThreadId}
|
|
338
343
|
collapsed={sidebarCollapsed}
|
|
339
|
-
onSelect={(id) =>
|
|
344
|
+
onSelect={async (id) => {
|
|
345
|
+
if (id !== activeThreadId) {
|
|
346
|
+
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
347
|
+
if (prev && prev.headMessageId == null) {
|
|
348
|
+
await deleteThread(prev.id);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
setActiveThreadId(id);
|
|
352
|
+
}}
|
|
340
353
|
onNewChat={async () => {
|
|
354
|
+
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
355
|
+
if (prev && prev.headMessageId == null) {
|
|
356
|
+
await deleteThread(prev.id);
|
|
357
|
+
}
|
|
341
358
|
const thread = await createNewThread();
|
|
342
359
|
setViewMode("chat");
|
|
343
360
|
setActiveThreadId(thread.id);
|
|
@@ -432,7 +449,7 @@ export default function Home() {
|
|
|
432
449
|
) : null}
|
|
433
450
|
{searchOpen ? (
|
|
434
451
|
<SearchModal
|
|
435
|
-
threads={
|
|
452
|
+
threads={sidebarThreads}
|
|
436
453
|
onClose={() => setSearchOpen(false)}
|
|
437
454
|
onSelect={(id) => {
|
|
438
455
|
setActiveThreadId(id);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { ArrowDown } from "lucide-react";
|
|
4
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
4
|
import type {
|
|
6
5
|
ChatCitationSource,
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
} from "../lib/memory";
|
|
28
27
|
import { useUIStore } from "../lib/store";
|
|
29
28
|
import { toChatConnectionPayload } from "../lib/connections";
|
|
29
|
+
import { ArrowDown } from "lucide-react";
|
|
30
30
|
import MessageCard from "./MessageCard";
|
|
31
31
|
import Composer from "./Composer";
|
|
32
32
|
|
|
@@ -1506,9 +1506,9 @@ export default function ChatView({
|
|
|
1506
1506
|
{threadMessages.length > 0 ? (
|
|
1507
1507
|
<>
|
|
1508
1508
|
{showJumpToBottom ? (
|
|
1509
|
-
<div className="pointer-events-none absolute bottom-
|
|
1509
|
+
<div className="pointer-events-none absolute bottom-28 left-1/2 z-30 -translate-x-1/2 opacity-80 transition-opacity duration-200">
|
|
1510
1510
|
<button
|
|
1511
|
-
className="pointer-events-auto inline-flex h-
|
|
1511
|
+
className="pointer-events-auto inline-flex h-9 w-9 items-center justify-center rounded-full border border-[var(--border-strong)] bg-[var(--panel)] text-[var(--text-primary)] shadow-[var(--shadow)] transition hover:translate-y-[1px] hover:border-[var(--accent)] hover:opacity-100"
|
|
1512
1512
|
onClick={() => {
|
|
1513
1513
|
shouldStickToBottomRef.current = true;
|
|
1514
1514
|
setShowJumpToBottom(false);
|
|
@@ -1518,7 +1518,7 @@ export default function ChatView({
|
|
|
1518
1518
|
aria-label="Jump to latest message"
|
|
1519
1519
|
title="Jump to latest"
|
|
1520
1520
|
>
|
|
1521
|
-
<ArrowDown className="h-
|
|
1521
|
+
<ArrowDown className="h-4 w-4" />
|
|
1522
1522
|
</button>
|
|
1523
1523
|
</div>
|
|
1524
1524
|
) : null}
|
|
@@ -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)] 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
|
>
|
|
@@ -90,23 +90,33 @@ export default function Sidebar({
|
|
|
90
90
|
{groups.map(({ root }) => (
|
|
91
91
|
<div key={root.id} className="space-y-1">
|
|
92
92
|
<div
|
|
93
|
-
|
|
93
|
+
role="button"
|
|
94
|
+
tabIndex={0}
|
|
95
|
+
onClick={() => onSelect(root.id)}
|
|
96
|
+
onKeyDown={(e) => {
|
|
97
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
onSelect(root.id);
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
className={`group flex h-11 cursor-pointer items-center justify-between rounded-lg px-3 text-sm transition ${
|
|
94
103
|
activeThreadId === root.id
|
|
95
104
|
? "bg-[var(--panel-2)] text-[var(--text-primary)]"
|
|
96
105
|
: "text-[var(--text-secondary)] hover:bg-[var(--panel)]"
|
|
97
106
|
}`}
|
|
98
107
|
>
|
|
99
|
-
<
|
|
100
|
-
onClick={() => onSelect(root.id)}
|
|
101
|
-
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
|
102
|
-
>
|
|
108
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
103
109
|
<Folder className="h-4 w-4 shrink-0 text-[var(--text-muted)]" />
|
|
104
110
|
<div className="sidebar-text min-w-0 truncate">
|
|
105
111
|
{root.title || "Main chat"}
|
|
106
112
|
</div>
|
|
107
|
-
</
|
|
113
|
+
</div>
|
|
108
114
|
<button
|
|
109
|
-
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={(e) => {
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
onDeleteThread(root);
|
|
119
|
+
}}
|
|
110
120
|
className="ml-2 rounded-full border border-transparent p-1 text-[var(--text-muted)] opacity-0 transition hover:border-[var(--border)] hover:text-[var(--danger)] group-hover:opacity-100"
|
|
111
121
|
>
|
|
112
122
|
<Trash2 className="h-3 w-3" />
|