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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris-chatbot",
3
- "version": "0.2.5",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "bin": {
package/template/= ADDED
File without changes
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "0.1.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.1.0",
9
+ "version": "2.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": "0.2.5",
3
+ "version": "2.0.0",
4
4
  "private": true,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "engines": {
@@ -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 say you cannot access the user's device.",
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(), DATA_DIR, STATE_FILE);
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 statePath = getStatePath();
30
- try {
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
- const data = JSON.parse(raw);
33
- const payload = {
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 > :first-child,
124
- .message-row.user .message-content > :last-child {
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 { font-size: 1.5rem; }
327
- .message-content h2 { font-size: 1.3rem; }
328
- .message-content h3 { font-size: 1.14rem; }
329
- .message-content h4 { font-size: 1.03rem; }
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 > :first-child {
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 { list-style: disc; }
346
- .message-content ol { list-style: decimal; }
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 > ul,
353
- .message-content li > ol {
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 > :last-child {
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: 56px minmax(0, 1fr);
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
- : settings.font === "sora"
122
- ? "var(--font-sora)"
123
- : settings.font === "space"
124
- ? "var(--font-space)"
125
- : "var(--font-sans)";
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-full min-h-screen min-w-0 flex-col">
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
- scrollToBottom("smooth");
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 from = formatDateTime(getString(payload, "from"));
455
- const to = formatDateTime(getString(payload, "to"));
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: joinDetailParts([from ? `From: ${from}` : null, to ? `To: ${to}` : null]),
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
- <button
140
- 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"
141
- onClick={onToggleView}
142
- >
143
- {viewMode === "chat" ? (
144
- <>
145
- <Waypoints className="h-5 w-5" />
146
- <span className="hidden sm:inline">Map</span>
147
- </>
148
- ) : (
149
- <>
150
- <MessageSquare className="h-5 w-5" />
151
- <span className="hidden sm:inline">Chat</span>
152
- </>
153
- )}
154
- </button>
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}