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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris-chatbot",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "private": false,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ # Projects plan – Sidebar UI addendum
2
+
3
+ When implementing the Sidebar for Projects, apply these UI details:
4
+
5
+ 1. **Folder icon**
6
+ - **Remove** the folder icon from chat/thread rows (individual chats).
7
+ - **Add** the folder icon next to **project names** (each project in the project list, including "Inbox" if shown as a project).
8
+
9
+ 2. **"Your Chats" label**
10
+ - Use the exact label **'Your Chats'** (title case).
11
+ - Do **not** render it in all caps: remove the `uppercase` class from the section heading (currently in Sidebar around line 86: `text-[11px] uppercase tracking-[0.18em]` → use the same size/tracking but drop `uppercase`) so it displays as **Your Chats**, not "YOUR CHATS".
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "iris",
9
- "version": "4.0.0",
9
+ "version": "5.0.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.72.1",
12
12
  "clsx": "^2.1.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "private": true,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "engines": {
@@ -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 calendar. You have calendar tools—use them; do not refuse or ask the user to paste/export/screenshot their calendar.",
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)\b/i;
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
- `\\s+(?:and then|and also|as well as|after that|plus|then|also|next|finally|lastly)\\s+|` +
90
- `\\s+and\\s+(?=(?:${ACTION_VERB_PATTERN})\\b)`,
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
- (connection.provider === "openai" ||
238
- connection.provider === "anthropic" ||
239
- connection.provider === "google")
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
- (header): header is { key: string; value: string } =>
268
- Boolean(header) && typeof header.key === "string" && typeof header.value === "string",
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
- .map((item) => {
439
- const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
440
- const target = typeof item?.target === "string" ? item.target.trim() : "";
441
- if (!alias || !target) {
442
- return null;
443
- }
444
- return { alias, target };
445
- })
446
- .filter((item): item is { alias: string; target: string } => Boolean(item))
447
- .slice(0, 16)
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
- .map((item) => {
453
- const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
454
- const query = typeof item?.query === "string" ? item.query.trim() : "";
455
- if (!alias || !query) {
456
- return null;
457
- }
458
- const title =
459
- typeof item?.title === "string" && item.title.trim()
460
- ? item.title.trim()
461
- : undefined;
462
- const artist =
463
- typeof item?.artist === "string" && item.artist.trim()
464
- ? item.artist.trim()
465
- : undefined;
466
- return { alias, query, title, artist };
467
- })
468
- .filter(
469
- (
470
- item,
471
- ): item is {
472
- alias: string;
473
- query: string;
474
- title: string | undefined;
475
- artist: string | undefined;
476
- } => Boolean(item),
477
- )
478
- .slice(0, 16)
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
- .replace(/^(that|i have to|to|for)\s+/i, "")
1411
- .trim(),
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
- playing?: boolean;
1719
- matched?: boolean;
1720
- playlistName?: string | null;
1721
- title?: string | null;
1722
- artist?: string | null;
1723
- album?: string | null;
1724
- reason?: string | null;
1725
- matchedTitle?: string | null;
1726
- matchedArtist?: string | null;
1727
- catalogTitle?: string | null;
1728
- catalogArtist?: string | null;
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
- type: "token",
1827
- value:
1828
- hasNearbyMatch
1829
- ? `Started nearby match "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""}. ${playResult.reason}`
1830
- : resolvedPlaylistName && intent.volume !== null
1831
- ? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music and set volume to ${intent.volume}%.`
1832
- : resolvedPlaylistName
1833
- ? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music.`
1834
- : intent.volume !== null
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
- approvalId: approval.approvalId,
2163
- callId: call.id,
2164
- name: call.name,
2165
- args: callInput,
2166
- reason: summarizeApprovalReason(call.name, callInput),
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;
@@ -3,7 +3,7 @@
3
3
 
4
4
  :root {
5
5
  --topbar-height: 64px;
6
- --bg: #171819;
6
+ --bg: #212121;
7
7
  --bg-alt: #1b1c1d;
8
8
  --sidebar: #181818;
9
9
  --sidebar-border: #202020;
@@ -15,6 +15,7 @@
15
15
  --text-muted: #b1b7bf;
16
16
  --accent: #10a37f;
17
17
  --accent-2: #0e8e6f;
18
+ --accent-ring: var(--accent-2);
18
19
  --danger: #ef4444;
19
20
  --border: #2a2a2a;
20
21
  --border-strong: #3a3a3a;
@@ -48,6 +49,21 @@ a {
48
49
  color: var(--accent);
49
50
  }
50
51
 
52
+ input:focus,
53
+ select:focus,
54
+ textarea:focus,
55
+ button:focus {
56
+ outline: none;
57
+ }
58
+
59
+ input:focus-visible,
60
+ select:focus-visible,
61
+ textarea:focus-visible,
62
+ button:focus-visible {
63
+ outline: none;
64
+ box-shadow: inset 0 0 0 1px var(--border-strong);
65
+ }
66
+
51
67
  .chat-shell {
52
68
  display: grid;
53
69
  grid-template-columns: 248px minmax(0, 1fr);
@@ -82,10 +98,21 @@ a {
82
98
  background: transparent !important;
83
99
  backdrop-filter: none;
84
100
  box-shadow: none;
85
- position: sticky;
101
+ position: absolute;
86
102
  top: 0;
103
+ left: 0;
104
+ right: 0;
87
105
  z-index: 40;
88
106
  height: var(--topbar-height);
107
+ pointer-events: none;
108
+ }
109
+
110
+ .topbar > *,
111
+ .topbar button,
112
+ .topbar a,
113
+ .topbar [role="listbox"],
114
+ .topbar [role="option"] {
115
+ pointer-events: auto;
89
116
  }
90
117
 
91
118
 
@@ -249,7 +276,7 @@ a {
249
276
  }
250
277
 
251
278
  .assistant-card .message-content {
252
- color: #f8fafc;
279
+ color: var(--text-primary);
253
280
  }
254
281
 
255
282
  .message-loading-spinner {
@@ -271,7 +298,7 @@ a {
271
298
  .composer-bar {
272
299
  position: sticky;
273
300
  bottom: 0;
274
- background: transparent;
301
+ background: var(--bg);
275
302
  z-index: 30;
276
303
  }
277
304
 
@@ -299,13 +326,14 @@ a {
299
326
  --sidebar: #ffffff;
300
327
  --sidebar-border: #e5e5e5;
301
328
  --panel: #ffffff;
302
- --panel-2: #f0f0f0;
303
- --panel-3: #e8e8e8;
329
+ --panel-2: #f6f6f6;
330
+ --panel-3: #f0f0f0;
304
331
  --text-primary: #111111;
305
332
  --text-secondary: #4a4a4a;
306
333
  --text-muted: #6f6f6f;
307
334
  --accent: #0e8e6f;
308
335
  --accent-2: #0b7a5f;
336
+ --accent-ring: color-mix(in srgb, var(--accent) 55%, #000);
309
337
  --danger: #dc2626;
310
338
  --border: #d5d5d5;
311
339
  --border-strong: #bdbdbd;
@@ -313,6 +341,51 @@ a {
313
341
  --user-bubble: #d7f2ea;
314
342
  }
315
343
 
344
+ /* Dark mode: white button background (no data-theme or data-theme="dark") */
345
+ :root .settings-tab-active,
346
+ :root .settings-btn-accent {
347
+ background: white !important;
348
+ color: #111111 !important;
349
+ }
350
+
351
+ [data-theme="dark"] .settings-tab-active,
352
+ [data-theme="dark"] .settings-btn-accent {
353
+ background: white !important;
354
+ color: #111111 !important;
355
+ }
356
+
357
+ /* Light mode: black for selected tab and Save button */
358
+ [data-theme="light"] .settings-tab-active,
359
+ [data-theme="light"] .settings-btn-accent {
360
+ background: #111111 !important;
361
+ color: #ffffff !important;
362
+ }
363
+
364
+ [data-theme="light"] .message-content pre,
365
+ [data-theme="light"] .message-content pre code {
366
+ color: #e5e7eb;
367
+ }
368
+
369
+ [data-theme="light"] .message-content code {
370
+ background: rgba(0, 0, 0, 0.06);
371
+ color: var(--text-primary);
372
+ }
373
+
374
+ [data-theme="light"] .message-loading-spinner {
375
+ border-color: rgba(0, 0, 0, 0.15);
376
+ border-top-color: #111111;
377
+ }
378
+
379
+ [data-theme="light"] .send-button.active {
380
+ background: #111111;
381
+ color: #ffffff;
382
+ border-color: transparent;
383
+ }
384
+
385
+ [data-theme="light"] .send-button.active svg {
386
+ color: #ffffff;
387
+ }
388
+
316
389
  .message-content h1,
317
390
  .message-content h2,
318
391
  .message-content h3,
@@ -506,12 +579,16 @@ a {
506
579
  }
507
580
 
508
581
  .composer {
509
- background: var(--panel);
582
+ background: #303030;
510
583
  border: 1px solid var(--border);
511
584
  border-radius: 18px;
512
585
  padding: 10px 12px;
513
586
  }
514
587
 
588
+ [data-theme="light"] .composer {
589
+ background: transparent;
590
+ }
591
+
515
592
  .thread-shelf {
516
593
  margin-top: 12px;
517
594
  padding-top: 8px;
@@ -582,10 +659,22 @@ a {
582
659
  line-height: 1.4;
583
660
  padding-top: 7px;
584
661
  padding-bottom: 9px;
662
+ padding-left: 6px;
585
663
  /* One line by default; height grows with content in Composer.tsx */
586
664
  min-height: calc(1.4em + 7px + 9px);
587
665
  }
588
666
 
667
+ .composer-textarea:focus,
668
+ .composer-textarea:focus-visible {
669
+ box-shadow: none;
670
+ }
671
+
672
+ .search-modal-input:focus,
673
+ .search-modal-input:focus-visible {
674
+ outline: none;
675
+ box-shadow: none;
676
+ }
677
+
589
678
  .send-button {
590
679
  height: 40px;
591
680
  width: 40px;
@@ -800,9 +889,9 @@ a {
800
889
  }
801
890
 
802
891
  .chat-scroll {
803
- padding-top: calc(var(--topbar-height) - 20px);
892
+ padding-top: calc(var(--topbar-height) + 24px);
804
893
  }
805
894
 
806
895
  .chat-scroll.empty {
807
- padding-top: var(--topbar-height);
896
+ padding-top: calc(var(--topbar-height) + 24px);
808
897
  }
@@ -12,6 +12,8 @@ import {
12
12
  createNewThread,
13
13
  deleteConversation,
14
14
  deleteThread,
15
+ DEFAULT_ACCENT_DARK,
16
+ DEFAULT_ACCENT_LIGHT,
15
17
  } from "../lib/data";
16
18
  import { db } from "../lib/db";
17
19
  import {
@@ -331,10 +333,23 @@ export default function Home() {
331
333
  <div
332
334
  className={`chat-shell ${sidebarCollapsed ? "collapsed" : ""}`}
333
335
  style={
334
- {
335
- "--accent": settings?.accentColor || "#66706e",
336
- "--accent-2": settings?.accentColor || "#66706e",
337
- } as CSSProperties
336
+ (() => {
337
+ const stored = settings?.accentColor || DEFAULT_ACCENT_DARK;
338
+ const isDefaultGray = stored === DEFAULT_ACCENT_DARK;
339
+ const isLight =
340
+ settings?.theme === "light" ||
341
+ (typeof document !== "undefined" &&
342
+ document.documentElement.dataset.theme === "light");
343
+ const effective =
344
+ isDefaultGray && isLight ? DEFAULT_ACCENT_LIGHT : stored;
345
+ return {
346
+ "--accent": effective,
347
+ "--accent-2": effective,
348
+ ...(isLight && {
349
+ "--accent-ring": "color-mix(in srgb, var(--accent) 55%, #000)",
350
+ }),
351
+ } as CSSProperties;
352
+ })()
338
353
  }
339
354
  >
340
355
  <Sidebar
@@ -344,7 +359,7 @@ export default function Home() {
344
359
  onSelect={async (id) => {
345
360
  if (id !== activeThreadId) {
346
361
  const prev = threads?.find((t) => t.id === activeThreadId);
347
- if (prev?.headMessageId == null) {
362
+ if (prev && prev.headMessageId == null) {
348
363
  await deleteThread(prev.id);
349
364
  }
350
365
  }
@@ -352,7 +367,7 @@ export default function Home() {
352
367
  }}
353
368
  onNewChat={async () => {
354
369
  const prev = threads?.find((t) => t.id === activeThreadId);
355
- if (prev?.headMessageId == null) {
370
+ if (prev && prev.headMessageId == null) {
356
371
  await deleteThread(prev.id);
357
372
  }
358
373
  const thread = await createNewThread();
@@ -387,7 +402,7 @@ export default function Home() {
387
402
  onOpenSearch={() => setSearchOpen(true)}
388
403
  />
389
404
 
390
- <div className="flex h-screen min-w-0 flex-col overflow-hidden">
405
+ <div className="relative flex h-screen min-w-0 flex-col overflow-hidden">
391
406
  <TopBar
392
407
  connectionId={connection?.id ?? ""}
393
408
  connectionName={connection?.name ?? "No connection"}
@@ -16,6 +16,21 @@ import { useUIStore } from "../lib/store";
16
16
  const NODE_WIDTH = 220;
17
17
  const NODE_HEIGHT = 80;
18
18
 
19
+ /** Strip markdown syntax for a short plain-text preview (e.g. "## Hi there" → "Hi there"). */
20
+ function stripMarkdownForPreview(text: string): string {
21
+ return text
22
+ .replace(/^#+\s*/m, "") // headings: ## Title -> Title
23
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // **bold**
24
+ .replace(/\*([^*]+)\*/g, "$1") // *italic*
25
+ .replace(/_([^_]+)_/g, "$1") // _italic_
26
+ .replace(/`([^`]+)`/g, "$1") // `code`
27
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
28
+ .replace(/^[-*]\s+/gm, "") // list bullets
29
+ .replace(/^>\s*/gm, "") // blockquote
30
+ .replace(/\s+/g, " ")
31
+ .trim();
32
+ }
33
+
19
34
  const graph = new dagre.graphlib.Graph();
20
35
  graph.setDefaultEdgeLabel(() => ({}));
21
36
 
@@ -120,7 +135,10 @@ export default function MapView({
120
135
 
121
136
  const { nodes, edges } = useMemo(() => {
122
137
  const nodes: Node[] = visibleMessages.map((message) => {
123
- const preview = splitContentAndSources(message.content).content.trim().slice(0, 60) || "(empty)";
138
+ const raw = splitContentAndSources(message.content).content.trim();
139
+ const preview = raw
140
+ ? stripMarkdownForPreview(raw).slice(0, 60)
141
+ : "(empty)";
124
142
  const isActive = activePathIds.has(message.id);
125
143
  const roleClass =
126
144
  message.role === "user"
@@ -169,6 +187,7 @@ export default function MapView({
169
187
  padding: 0.2,
170
188
  duration,
171
189
  includeHiddenNodes: false,
190
+ minZoom: 0.85,
172
191
  });
173
192
  }, []);
174
193
 
@@ -198,7 +217,7 @@ export default function MapView({
198
217
  nodes={nodes}
199
218
  edges={edges}
200
219
  fitView
201
- fitViewOptions={{ padding: 0.2, includeHiddenNodes: false }}
220
+ fitViewOptions={{ padding: 0.2, includeHiddenNodes: false, minZoom: 0.85 }}
202
221
  onInit={(instance) => {
203
222
  flowRef.current = instance;
204
223
  refitView(0);