@geminilight/mindos 0.6.16 → 0.6.18

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.
@@ -26,6 +26,7 @@
26
26
  --color-input: var(--input);
27
27
  --color-border: var(--border);
28
28
  --color-destructive: var(--destructive);
29
+ --color-destructive-foreground: var(--destructive-foreground);
29
30
  --color-success: var(--success);
30
31
  --color-error: var(--error);
31
32
  --color-amber-foreground: var(--amber-foreground);
@@ -69,7 +70,8 @@ body {
69
70
  --muted-foreground: #685f52;
70
71
  --accent: #d9d3c6;
71
72
  --accent-foreground: #1c1a17;
72
- --destructive: oklch(0.58 0.22 27);
73
+ --destructive: oklch(0.56 0.14 24);
74
+ --destructive-foreground: #ffffff;
73
75
  --border: rgba(28, 26, 23, 0.1);
74
76
  --input: rgba(28, 26, 23, 0.12);
75
77
  --ring: var(--amber);
@@ -106,7 +108,8 @@ body {
106
108
  --muted-foreground: #8a8275;
107
109
  --accent: #2e2b22;
108
110
  --accent-foreground: #e8e4dc;
109
- --destructive: oklch(0.704 0.191 22.216);
111
+ --destructive: oklch(0.56 0.14 22);
112
+ --destructive-foreground: #ffffff;
110
113
  --border: rgba(232, 228, 220, 0.08);
111
114
  --input: rgba(232, 228, 220, 0.1);
112
115
  --ring: var(--amber);
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { useRef, useCallback, useState, useEffect } from 'react';
4
4
  import Link from 'next/link';
5
- import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio, History } from 'lucide-react';
5
+ import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio } from 'lucide-react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
8
8
  import type { SyncStatus } from './settings/SyncTab';
9
9
  import Logo from './Logo';
10
10
 
11
- export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover' | 'history';
11
+ export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover';
12
12
 
13
13
  export const RAIL_WIDTH_COLLAPSED = 48;
14
14
  export const RAIL_WIDTH_EXPANDED = 180;
@@ -16,7 +16,9 @@ export const RAIL_WIDTH_EXPANDED = 180;
16
16
  interface ActivityBarProps {
17
17
  activePanel: PanelId | null;
18
18
  onPanelChange: (id: PanelId | null) => void;
19
+ onEchoClick?: () => void;
19
20
  onAgentsClick?: () => void;
21
+ onDiscoverClick?: () => void;
20
22
  syncStatus: SyncStatus | null;
21
23
  expanded: boolean;
22
24
  onExpandedChange: (expanded: boolean) => void;
@@ -77,7 +79,9 @@ function RailButton({ icon, label, shortcut, active = false, expanded, onClick,
77
79
  export default function ActivityBar({
78
80
  activePanel,
79
81
  onPanelChange,
82
+ onEchoClick,
80
83
  onAgentsClick,
84
+ onDiscoverClick,
81
85
  syncStatus,
82
86
  expanded,
83
87
  onExpandedChange,
@@ -188,7 +192,7 @@ export default function ActivityBar({
188
192
  <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
189
193
  <RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
190
194
  <RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
191
- <RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} walkthroughId="echo-panel" />
195
+ <RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => onEchoClick ? debounced(onEchoClick) : toggle('echo')} walkthroughId="echo-panel" />
192
196
  <RailButton
193
197
  icon={<Bot size={18} />}
194
198
  label={t.sidebar.agents}
@@ -197,8 +201,7 @@ export default function ActivityBar({
197
201
  onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
198
202
  walkthroughId="agents-panel"
199
203
  />
200
- <RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
201
- <RailButton icon={<History size={18} />} label={t.sidebar.history} active={activePanel === 'history'} expanded={expanded} onClick={() => toggle('history')} />
204
+ <RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => onDiscoverClick ? debounced(onDiscoverClick) : toggle('discover')} />
202
205
  </div>
203
206
 
204
207
  {/* ── Spacer ── */}
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { ChevronDown, Check } from 'lucide-react';
5
6
 
6
7
  export interface SelectOption {
@@ -50,7 +51,7 @@ export default function CustomSelect({
50
51
  }: CustomSelectProps) {
51
52
  const [open, setOpen] = useState(false);
52
53
  const [highlightIdx, setHighlightIdx] = useState(-1);
53
- const [flipUp, setFlipUp] = useState(false);
54
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
54
55
  const btnRef = useRef<HTMLButtonElement>(null);
55
56
  const listRef = useRef<HTMLDivElement>(null);
56
57
 
@@ -122,20 +123,33 @@ export default function CustomSelect({
122
123
  if (el) el.scrollIntoView({ block: 'nearest' });
123
124
  }, [open, highlightIdx]);
124
125
 
125
- // Initialize highlight + flip direction when opening
126
+ const calcPosition = useCallback(() => {
127
+ if (!btnRef.current) return;
128
+ const rect = btnRef.current.getBoundingClientRect();
129
+ const maxH = size === 'sm' ? 200 : 260;
130
+ const spaceBelow = window.innerHeight - rect.bottom;
131
+ const spaceAbove = rect.top;
132
+ setPanelPos({
133
+ top: spaceBelow < maxH + 8 && spaceAbove > spaceBelow ? rect.top : rect.bottom,
134
+ left: rect.left,
135
+ width: rect.width,
136
+ flipUp: spaceBelow < maxH + 8 && spaceAbove > spaceBelow,
137
+ });
138
+ }, [size]);
139
+
140
+ // Initialize highlight + position when opening; reposition on scroll/resize
126
141
  useEffect(() => {
127
- if (open) {
128
- const idx = allOptions.findIndex(o => o.value === value);
129
- setHighlightIdx(idx >= 0 ? idx : 0);
130
- if (btnRef.current) {
131
- const rect = btnRef.current.getBoundingClientRect();
132
- const maxH = size === 'sm' ? 200 : 260;
133
- const spaceBelow = window.innerHeight - rect.bottom;
134
- const spaceAbove = rect.top;
135
- setFlipUp(spaceBelow < maxH + 8 && spaceAbove > spaceBelow);
136
- }
137
- }
138
- }, [open, allOptions, value, size]);
142
+ if (!open) { setPanelPos(null); return; }
143
+ const idx = allOptions.findIndex(o => o.value === value);
144
+ setHighlightIdx(idx >= 0 ? idx : 0);
145
+ calcPosition();
146
+ window.addEventListener('scroll', calcPosition, true);
147
+ window.addEventListener('resize', calcPosition);
148
+ return () => {
149
+ window.removeEventListener('scroll', calcPosition, true);
150
+ window.removeEventListener('resize', calcPosition);
151
+ };
152
+ }, [open, allOptions, value, calcPosition]);
139
153
 
140
154
  const isSm = size === 'sm';
141
155
 
@@ -147,9 +161,9 @@ export default function CustomSelect({
147
161
  ? 'absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none'
148
162
  : 'absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none';
149
163
 
150
- const listCls = isSm
151
- ? `absolute z-50 min-w-full max-h-[200px] overflow-y-auto rounded-md border border-border bg-card shadow-lg py-0.5 ${flipUp ? 'bottom-full mb-1' : 'top-full mt-1'}`
152
- : `absolute z-50 min-w-full max-h-[260px] overflow-y-auto rounded-lg border border-border bg-card shadow-lg py-1 ${flipUp ? 'bottom-full mb-1' : 'top-full mt-1'}`;
164
+ const listBaseCls = isSm
165
+ ? 'fixed z-[9999] overflow-y-auto rounded-md border border-border bg-card shadow-lg py-0.5'
166
+ : 'fixed z-[9999] overflow-y-auto rounded-lg border border-border bg-card shadow-lg py-1';
153
167
 
154
168
  const itemBaseCls = isSm
155
169
  ? 'w-full flex items-center gap-1.5 px-2 py-1 text-2xs text-left transition-colors cursor-pointer'
@@ -184,6 +198,38 @@ export default function CustomSelect({
184
198
  );
185
199
  }
186
200
 
201
+ const listPortal = open && panelPos && createPortal(
202
+ <div
203
+ ref={listRef}
204
+ className={listBaseCls}
205
+ role="listbox"
206
+ style={{
207
+ left: panelPos.left,
208
+ minWidth: panelPos.width,
209
+ maxHeight: isSm ? 200 : 260,
210
+ ...(panelPos.flipUp
211
+ ? { bottom: window.innerHeight - panelPos.top + 4 }
212
+ : { top: panelPos.top + 4 }),
213
+ }}
214
+ >
215
+ {options.map((item, idx) => {
216
+ if (isGroup(item)) {
217
+ return (
218
+ <div key={item.label}>
219
+ {idx > 0 && <div className="my-0.5 border-t border-border/50" />}
220
+ <div className={`py-1 text-2xs font-medium text-muted-foreground uppercase tracking-wider ${isSm ? 'px-2' : 'px-3'}`}>
221
+ {item.label}
222
+ </div>
223
+ {item.options.map(renderOption)}
224
+ </div>
225
+ );
226
+ }
227
+ return renderOption(item);
228
+ })}
229
+ </div>,
230
+ document.body,
231
+ );
232
+
187
233
  return (
188
234
  <div className="relative">
189
235
  <button
@@ -204,25 +250,7 @@ export default function CustomSelect({
204
250
  className={`${chevronCls} transition-transform duration-150 ${open ? 'rotate-180' : ''}`}
205
251
  />
206
252
  </button>
207
-
208
- {open && (
209
- <div ref={listRef} className={listCls} role="listbox">
210
- {options.map((item, idx) => {
211
- if (isGroup(item)) {
212
- return (
213
- <div key={item.label}>
214
- {idx > 0 && <div className="my-0.5 border-t border-border/50" />}
215
- <div className={`py-1 text-2xs font-medium text-muted-foreground uppercase tracking-wider ${isSm ? 'px-2' : 'px-3'}`}>
216
- {item.label}
217
- </div>
218
- {item.options.map(renderOption)}
219
- </div>
220
- );
221
- }
222
- return renderOption(item);
223
- })}
224
- </div>
225
- )}
253
+ {listPortal}
226
254
  </div>
227
255
  );
228
256
  }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { Folder, ChevronDown, ChevronRight, Check } from 'lucide-react';
5
6
  import { stripEmoji } from '@/lib/utils';
6
7
 
@@ -18,27 +19,44 @@ interface DirPickerProps {
18
19
  const PANEL_MAX_H = 200;
19
20
 
20
21
  /**
21
- * Hierarchical directory picker — always renders as a single-line trigger button.
22
- * When expanded, the tree browser floats as an overlay (absolute) so it never
23
- * pushes sibling content down.
22
+ * Hierarchical directory picker — trigger button stays in layout flow;
23
+ * the expanded panel renders via portal with position:fixed so it escapes
24
+ * any ancestor overflow:hidden / overflow:auto containers.
24
25
  */
25
26
  export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root' }: DirPickerProps) {
26
27
  const [expanded, setExpanded] = useState(false);
27
28
  const [browsing, setBrowsing] = useState(value);
28
- const [flipUp, setFlipUp] = useState(false);
29
- const containerRef = useRef<HTMLDivElement>(null);
29
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
30
30
  const btnRef = useRef<HTMLButtonElement>(null);
31
+ const panelRef = useRef<HTMLDivElement>(null);
31
32
 
32
33
  useEffect(() => { setBrowsing(value); }, [value]);
33
34
 
34
- // Decide flip direction when opening
35
- useEffect(() => {
36
- if (!expanded || !btnRef.current) return;
35
+ const calcPosition = useCallback(() => {
36
+ if (!btnRef.current) return;
37
37
  const rect = btnRef.current.getBoundingClientRect();
38
38
  const spaceBelow = window.innerHeight - rect.bottom;
39
39
  const spaceAbove = rect.top;
40
- setFlipUp(spaceBelow < PANEL_MAX_H + 8 && spaceAbove > spaceBelow);
41
- }, [expanded]);
40
+ const flip = spaceBelow < PANEL_MAX_H + 8 && spaceAbove > spaceBelow;
41
+ setPanelPos({
42
+ top: flip ? rect.top : rect.bottom,
43
+ left: rect.left,
44
+ width: rect.width,
45
+ flipUp: flip,
46
+ });
47
+ }, []);
48
+
49
+ // Recalculate position on open, scroll, and resize
50
+ useEffect(() => {
51
+ if (!expanded) { setPanelPos(null); return; }
52
+ calcPosition();
53
+ window.addEventListener('scroll', calcPosition, true);
54
+ window.addEventListener('resize', calcPosition);
55
+ return () => {
56
+ window.removeEventListener('scroll', calcPosition, true);
57
+ window.removeEventListener('resize', calcPosition);
58
+ };
59
+ }, [expanded, calcPosition]);
42
60
 
43
61
  const collapse = useCallback(() => setExpanded(false), []);
44
62
 
@@ -48,9 +66,12 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
48
66
  if (e.key === 'Escape') { e.preventDefault(); collapse(); }
49
67
  };
50
68
  const handleClick = (e: MouseEvent) => {
51
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
52
- collapse();
53
- }
69
+ const target = e.target as Node;
70
+ if (
71
+ btnRef.current?.contains(target) ||
72
+ panelRef.current?.contains(target)
73
+ ) return;
74
+ collapse();
54
75
  };
55
76
  document.addEventListener('keydown', handleKey);
56
77
  document.addEventListener('mousedown', handleClick);
@@ -87,9 +108,82 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
87
108
  ? value.split('/').map(s => stripEmoji(s)).join(' / ')
88
109
  : '/ ' + rootLabel;
89
110
 
111
+ const panel = expanded && panelPos && createPortal(
112
+ <div
113
+ ref={panelRef}
114
+ className="fixed z-[9999] rounded-lg border border-[var(--amber)] bg-card shadow-lg overflow-hidden flex flex-col"
115
+ style={{
116
+ left: panelPos.left,
117
+ width: panelPos.width,
118
+ maxHeight: PANEL_MAX_H,
119
+ ...(panelPos.flipUp
120
+ ? { bottom: window.innerHeight - panelPos.top + 4 }
121
+ : { top: panelPos.top + 4 }),
122
+ }}
123
+ >
124
+ {/* Breadcrumb */}
125
+ <div className="flex items-center gap-0.5 px-3 py-1.5 bg-muted/30 border-b border-border overflow-x-auto text-xs shrink-0">
126
+ <button
127
+ type="button"
128
+ onClick={() => navigateTo(-1)}
129
+ className={`shrink-0 px-1.5 py-0.5 rounded transition-colors ${
130
+ browsing === '' ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
131
+ }`}
132
+ >
133
+ / {rootLabel}
134
+ </button>
135
+ {segments.map((seg, i) => (
136
+ <span key={i} className="flex items-center gap-0.5 shrink-0">
137
+ <ChevronRight size={10} className="text-muted-foreground/50" />
138
+ <button
139
+ type="button"
140
+ onClick={() => navigateTo(i)}
141
+ className={`px-1.5 py-0.5 rounded transition-colors truncate max-w-[100px] ${
142
+ i === segments.length - 1 ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
143
+ }`}
144
+ >
145
+ {seg}
146
+ </button>
147
+ </span>
148
+ ))}
149
+ </div>
150
+ {/* Child directories */}
151
+ {children.length > 0 ? (
152
+ <div className="flex-1 min-h-0 overflow-y-auto">
153
+ {children.map(childPath => {
154
+ const childName = childPath.split('/').pop() || childPath;
155
+ const hasChildren = dirPaths.some(p => p.startsWith(childPath + '/'));
156
+ return (
157
+ <button
158
+ key={childPath}
159
+ type="button"
160
+ onClick={() => drillInto(childPath)}
161
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-foreground hover:bg-muted/60 transition-colors"
162
+ >
163
+ <Folder size={12} className="shrink-0 text-[var(--amber)]" />
164
+ <span className="flex-1 text-left truncate">{childName}</span>
165
+ {hasChildren && <ChevronRight size={11} className="shrink-0 text-muted-foreground/40" />}
166
+ </button>
167
+ );
168
+ })}
169
+ </div>
170
+ ) : (
171
+ <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
172
+ )}
173
+ {/* Confirm & collapse */}
174
+ <button
175
+ type="button"
176
+ onClick={collapse}
177
+ className="w-full py-1.5 flex items-center justify-center gap-1 text-xs font-medium text-[var(--amber)] border-t border-border hover:bg-muted/30 transition-colors shrink-0"
178
+ >
179
+ <Check size={12} />
180
+ </button>
181
+ </div>,
182
+ document.body,
183
+ );
184
+
90
185
  return (
91
- <div ref={containerRef} className="relative">
92
- {/* Trigger — always in document flow */}
186
+ <>
93
187
  <button
94
188
  ref={btnRef}
95
189
  type="button"
@@ -107,71 +201,7 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
107
201
  className={`shrink-0 text-muted-foreground transition-transform duration-150 ${expanded ? 'rotate-180' : ''}`}
108
202
  />
109
203
  </button>
110
-
111
- {/* Floating panel — absolute, never pushes content */}
112
- {expanded && (
113
- <div className={`absolute z-50 left-0 right-0 rounded-lg border border-[var(--amber)] bg-card shadow-lg overflow-hidden max-h-[200px] flex flex-col ${
114
- flipUp ? 'bottom-full mb-1' : 'top-full mt-1'
115
- }`}>
116
- {/* Breadcrumb */}
117
- <div className="flex items-center gap-0.5 px-3 py-1.5 bg-muted/30 border-b border-border overflow-x-auto text-xs shrink-0">
118
- <button
119
- type="button"
120
- onClick={() => navigateTo(-1)}
121
- className={`shrink-0 px-1.5 py-0.5 rounded transition-colors ${
122
- browsing === '' ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
123
- }`}
124
- >
125
- / {rootLabel}
126
- </button>
127
- {segments.map((seg, i) => (
128
- <span key={i} className="flex items-center gap-0.5 shrink-0">
129
- <ChevronRight size={10} className="text-muted-foreground/50" />
130
- <button
131
- type="button"
132
- onClick={() => navigateTo(i)}
133
- className={`px-1.5 py-0.5 rounded transition-colors truncate max-w-[100px] ${
134
- i === segments.length - 1 ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
135
- }`}
136
- >
137
- {seg}
138
- </button>
139
- </span>
140
- ))}
141
- </div>
142
- {/* Child directories */}
143
- {children.length > 0 ? (
144
- <div className="flex-1 min-h-0 overflow-y-auto">
145
- {children.map(childPath => {
146
- const childName = childPath.split('/').pop() || childPath;
147
- const hasChildren = dirPaths.some(p => p.startsWith(childPath + '/'));
148
- return (
149
- <button
150
- key={childPath}
151
- type="button"
152
- onClick={() => drillInto(childPath)}
153
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-foreground hover:bg-muted/60 transition-colors"
154
- >
155
- <Folder size={12} className="shrink-0 text-[var(--amber)]" />
156
- <span className="flex-1 text-left truncate">{childName}</span>
157
- {hasChildren && <ChevronRight size={11} className="shrink-0 text-muted-foreground/40" />}
158
- </button>
159
- );
160
- })}
161
- </div>
162
- ) : (
163
- <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
164
- )}
165
- {/* Confirm & collapse */}
166
- <button
167
- type="button"
168
- onClick={collapse}
169
- className="w-full py-1.5 flex items-center justify-center gap-1 text-xs font-medium text-[var(--amber)] border-t border-border hover:bg-muted/30 transition-colors shrink-0"
170
- >
171
- <Check size={12} />
172
- </button>
173
- </div>
174
- )}
175
- </div>
204
+ {panel}
205
+ </>
176
206
  );
177
207
  }
@@ -73,11 +73,18 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
73
73
  useEffect(() => {
74
74
  if (!open) return;
75
75
  const handler = (e: KeyboardEvent) => {
76
- if (e.key === 'Escape') { e.stopPropagation(); handleClose(); }
76
+ if (e.key !== 'Escape') return;
77
+ if (showDiscard) {
78
+ e.stopPropagation();
79
+ setShowDiscard(false);
80
+ return;
81
+ }
82
+ e.stopPropagation();
83
+ handleClose();
77
84
  };
78
85
  window.addEventListener('keydown', handler, true);
79
86
  return () => window.removeEventListener('keydown', handler, true);
80
- }, [open, handleClose]);
87
+ }, [open, handleClose, showDiscard]);
81
88
 
82
89
  const checkConflicts = useCallback(async (fileNames: string[], space: string) => {
83
90
  try {
@@ -197,15 +204,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
197
204
 
198
205
  return (
199
206
  <>
200
- <ConfirmDialog
201
- open={showDiscard}
202
- title={t.fileImport.discardTitle}
203
- message={t.fileImport.discardMessage(im.files.length)}
204
- confirmLabel={t.fileImport.discardConfirm}
205
- cancelLabel={t.fileImport.discardCancel}
206
- onConfirm={doClose}
207
- onCancel={() => setShowDiscard(false)}
208
- />
209
207
  <div
210
208
  ref={overlayRef}
211
209
  className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
@@ -507,6 +505,15 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
507
505
  </div>
508
506
  </div>
509
507
  </div>
508
+ <ConfirmDialog
509
+ open={showDiscard}
510
+ title={t.fileImport.discardTitle}
511
+ message={t.fileImport.discardMessage(im.files.length)}
512
+ confirmLabel={t.fileImport.discardConfirm}
513
+ cancelLabel={t.fileImport.discardCancel}
514
+ onConfirm={doClose}
515
+ onCancel={() => setShowDiscard(false)}
516
+ />
510
517
  </>
511
518
  );
512
519
  }
@@ -16,11 +16,8 @@ export default function JsonView({ content }: JsonViewProps) {
16
16
  }, [content]);
17
17
 
18
18
  return (
19
- <pre
20
- className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed font-display"
21
- suppressHydrationWarning
22
- >
23
- <code>{pretty}</code>
19
+ <pre className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed font-display">
20
+ <code suppressHydrationWarning>{pretty}</code>
24
21
  </pre>
25
22
  );
26
23
  }
@@ -30,7 +30,6 @@ const DEFAULT_PANEL_WIDTH: Record<PanelId, number> = {
30
30
  echo: 280,
31
31
  agents: 280,
32
32
  discover: 280,
33
- history: 280,
34
33
  };
35
34
 
36
35
  const MIN_PANEL_WIDTH = 240;
@@ -345,12 +345,22 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
345
345
  <ActivityBar
346
346
  activePanel={railActivePanel}
347
347
  onPanelChange={lp.setActivePanel}
348
+ onEchoClick={() => {
349
+ const wasActive = lp.activePanel === 'echo';
350
+ lp.setActivePanel(wasActive ? null : 'echo');
351
+ if (!wasActive) router.push('/echo/about-you');
352
+ }}
348
353
  onAgentsClick={() => {
349
354
  const wasActive = lp.activePanel === 'agents';
350
355
  lp.setActivePanel(wasActive ? null : 'agents');
351
356
  if (!wasActive) router.push('/agents');
352
357
  setAgentDetailKey(null);
353
358
  }}
359
+ onDiscoverClick={() => {
360
+ const wasActive = lp.activePanel === 'discover';
361
+ lp.setActivePanel(wasActive ? null : 'discover');
362
+ if (!wasActive) router.push('/explore');
363
+ }}
354
364
  syncStatus={syncStatus}
355
365
  expanded={lp.railExpanded}
356
366
  onExpandedChange={handleExpandedChange}
@@ -97,21 +97,21 @@ export default function UpdateOverlay() {
97
97
  >
98
98
  {done ? (
99
99
  <>
100
- <CheckCircle2 size={32} style={{ color: '#7aad80', marginBottom: 12 }} />
101
- <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
100
+ <CheckCircle2 size={32} style={{ color: 'var(--success)', marginBottom: 12 }} />
101
+ <div style={{ color: 'var(--foreground)', fontSize: 18, fontWeight: 600 }}>
102
102
  {zh ? '更新成功!' : 'Update Complete!'}
103
103
  </div>
104
- <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6 }}>
104
+ <div style={{ color: 'var(--muted-foreground)', fontSize: 13, marginTop: 6 }}>
105
105
  {zh ? '正在刷新页面...' : 'Reloading...'}
106
106
  </div>
107
107
  </>
108
108
  ) : (
109
109
  <>
110
- <Loader2 size={32} style={{ color: '#d4954a', marginBottom: 12, animation: 'spin 1s linear infinite' }} />
111
- <div style={{ color: '#e8e4dc', fontSize: 18, fontWeight: 600 }}>
110
+ <Loader2 size={32} style={{ color: 'var(--amber)', marginBottom: 12, animation: 'spin 1s linear infinite' }} />
111
+ <div style={{ color: 'var(--foreground)', fontSize: 18, fontWeight: 600 }}>
112
112
  {zh ? 'MindOS 正在更新...' : 'MindOS is Updating...'}
113
113
  </div>
114
- <div style={{ color: '#8a8275', fontSize: 13, marginTop: 6, textAlign: 'center', maxWidth: 300, lineHeight: 1.5 }}>
114
+ <div style={{ color: 'var(--muted-foreground)', fontSize: 13, marginTop: 6, textAlign: 'center', maxWidth: 300, lineHeight: 1.5 }}>
115
115
  {zh
116
116
  ? '服务正在重启,请勿关闭此页面。完成后将自动刷新。'
117
117
  : 'The server is restarting. Please do not close this page. It will auto-reload when ready.'}
@@ -243,9 +243,9 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
243
243
  <h1 className="text-lg font-semibold tracking-tight font-display text-foreground truncate">{agent.name}</h1>
244
244
  <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 mt-0.5 text-2xs text-muted-foreground/60">
245
245
  <span className={`font-medium px-1.5 py-px rounded-full ${
246
- status === 'connected' ? 'bg-success/10 text-success'
246
+ status === 'connected' ? 'bg-muted text-muted-foreground'
247
247
  : status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber-text)]'
248
- : 'bg-muted text-muted-foreground'
248
+ : 'bg-error/10 text-error'
249
249
  }`}>{status}</span>
250
250
  <span className="font-mono">{agent.transport ?? agent.preferredTransport}</span>
251
251
  <span className="text-muted-foreground/25" aria-hidden="true">·</span>
@@ -296,7 +296,7 @@ function ByAgentView({
296
296
  const mcpServers = agent.configuredMcpServers ?? [];
297
297
  const nativeSkillCount = (agent.installedSkillNames ?? []).length;
298
298
  return (
299
- <div key={agent.key} className={`rounded-xl border bg-card group hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)] transition-all duration-150 ${status === 'connected' ? 'border-l-2 border-l-[var(--success)] border-border' : status === 'detected' ? 'border-l-2 border-l-[var(--amber)] border-border' : 'border-border'}`}>
299
+ <div key={agent.key} className={`rounded-xl border bg-card group hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)] transition-all duration-150 ${status === 'detected' ? 'border-l-2 border-l-[var(--amber)] border-border' : status === 'notFound' ? 'border-l-2 border-l-error border-border' : 'border-border'}`}>
300
300
  {/* Card header with avatar */}
301
301
  <div className="flex items-center gap-3 p-3">
302
302
  <AgentAvatar name={agent.name} status={status} />
@@ -306,7 +306,7 @@ function ByAgentView({
306
306
  {agent.name}
307
307
  </Link>
308
308
  <span className="text-2xs text-muted-foreground font-mono shrink-0">{agent.transport ?? agent.preferredTransport}</span>
309
- <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]' : 'bg-muted text-muted-foreground'}`}>
309
+ <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-muted text-muted-foreground' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]' : 'bg-error/10 text-error'}`}>
310
310
  {copy.status[status]}
311
311
  </span>
312
312
  </div>
@@ -269,15 +269,15 @@ function StatCell({
269
269
  : 'text-muted-foreground';
270
270
  const iconColor =
271
271
  tone === 'ok'
272
- ? 'text-emerald-500/70'
272
+ ? 'text-[var(--success)]/70'
273
273
  : tone === 'warn'
274
- ? 'text-amber-500/70'
274
+ ? 'text-[var(--amber)]/70'
275
275
  : 'text-muted-foreground/40';
276
276
  const hoverBg =
277
277
  tone === 'ok'
278
- ? 'hover:bg-emerald-500/[0.04]'
278
+ ? 'hover:bg-muted/20'
279
279
  : tone === 'warn'
280
- ? 'hover:bg-amber-500/[0.04]'
280
+ ? 'hover:bg-[var(--amber)]/[0.04]'
281
281
  : 'hover:bg-muted/20';
282
282
 
283
283
  return (
@@ -335,8 +335,8 @@ function QuickNavCard({
335
335
  <span
336
336
  className={`text-2xs px-2 py-0.5 rounded-full font-medium select-none ${
337
337
  statTone === 'ok'
338
- ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
339
- : 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
338
+ ? 'bg-muted text-muted-foreground'
339
+ : 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
340
340
  }`}
341
341
  >
342
342
  {stat}
@@ -373,10 +373,10 @@ function AgentCard({
373
373
  status === 'connected' ? copy.connected : status === 'detected' ? copy.detected : copy.notFound;
374
374
  const statusColor =
375
375
  status === 'connected'
376
- ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
376
+ ? 'bg-muted text-muted-foreground'
377
377
  : status === 'detected'
378
- ? 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
379
- : 'bg-zinc-500/10 text-zinc-500';
378
+ ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
379
+ : 'bg-error/10 text-error';
380
380
 
381
381
  return (
382
382
  <Link
@@ -410,10 +410,10 @@ function AgentCard({
410
410
  <span className="flex-1 min-w-[4px]" />
411
411
  {hasRuntime && (
412
412
  <span
413
- className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
413
+ className="flex items-center gap-1 text-[var(--success)]"
414
414
  title={copy.runtimeActive}
415
415
  >
416
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" aria-hidden="true" />
416
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--success)] animate-pulse" aria-hidden="true" />
417
417
  <span className="text-2xs font-medium">{copy.runtimeActive}</span>
418
418
  </span>
419
419
  )}
@@ -27,7 +27,7 @@ export function StatusDot({ tone, label, count }: { tone: 'ok' | 'warn' | 'neutr
27
27
  const countCls = tone === 'ok' ? 'text-foreground' : tone === 'warn' ? 'text-[var(--amber)]' : 'text-muted-foreground';
28
28
  return (
29
29
  <span className="inline-flex items-center gap-1.5 text-muted-foreground">
30
- <span className={`w-2 h-2 rounded-full ${dotCls} ${tone === 'ok' ? 'ring-2 ring-[var(--success)]/20' : ''}`} aria-hidden="true" />
30
+ <span className={`w-2 h-2 rounded-full ${dotCls}`} aria-hidden="true" />
31
31
  <span className="text-xs">{label}</span>
32
32
  <span className={`tabular-nums font-medium ${countCls}`}>{count}</span>
33
33
  </span>
@@ -131,20 +131,20 @@ export function EmptyState({ message, icon, className }: { message: string; icon
131
131
 
132
132
  /* ────────── Agent Avatar ────────── */
133
133
 
134
- /** Soft pastel palette: [bg, border, text] — watercolor aesthetic */
134
+ /** Dual-mode palette: soft pastels in light, muted tones in dark — [bg, border, text] */
135
135
  const AVATAR_PALETTES: [string, string, string][] = [
136
- ['bg-rose-100/70', 'border-rose-300/50', 'text-rose-600/80'],
137
- ['bg-violet-100/70', 'border-violet-300/50', 'text-violet-600/80'],
138
- ['bg-emerald-100/70', 'border-emerald-300/50', 'text-emerald-600/80'],
139
- ['bg-sky-100/70', 'border-sky-300/50', 'text-sky-600/80'],
140
- ['bg-amber-100/70', 'border-amber-300/50', 'text-amber-700/80'],
141
- ['bg-teal-100/70', 'border-teal-300/50', 'text-teal-600/80'],
142
- ['bg-pink-100/70', 'border-pink-300/50', 'text-pink-600/80'],
143
- ['bg-indigo-100/70', 'border-indigo-300/50', 'text-indigo-600/80'],
144
- ['bg-lime-100/70', 'border-lime-300/50', 'text-lime-700/80'],
145
- ['bg-fuchsia-100/70', 'border-fuchsia-300/50', 'text-fuchsia-600/80'],
146
- ['bg-cyan-100/70', 'border-cyan-300/50', 'text-cyan-600/80'],
147
- ['bg-orange-100/70', 'border-orange-300/50', 'text-orange-600/80'],
136
+ ['bg-rose-100/70 dark:bg-rose-900/30', 'border-rose-300/50 dark:border-rose-700/40', 'text-rose-600/80 dark:text-rose-400/80'],
137
+ ['bg-violet-100/70 dark:bg-violet-900/30', 'border-violet-300/50 dark:border-violet-700/40', 'text-violet-600/80 dark:text-violet-400/80'],
138
+ ['bg-emerald-100/70 dark:bg-emerald-900/30', 'border-emerald-300/50 dark:border-emerald-700/40','text-emerald-600/80 dark:text-emerald-400/80'],
139
+ ['bg-sky-100/70 dark:bg-sky-900/30', 'border-sky-300/50 dark:border-sky-700/40', 'text-sky-600/80 dark:text-sky-400/80'],
140
+ ['bg-amber-100/70 dark:bg-amber-900/30', 'border-amber-300/50 dark:border-amber-700/40', 'text-amber-700/80 dark:text-amber-400/80'],
141
+ ['bg-teal-100/70 dark:bg-teal-900/30', 'border-teal-300/50 dark:border-teal-700/40', 'text-teal-600/80 dark:text-teal-400/80'],
142
+ ['bg-pink-100/70 dark:bg-pink-900/30', 'border-pink-300/50 dark:border-pink-700/40', 'text-pink-600/80 dark:text-pink-400/80'],
143
+ ['bg-indigo-100/70 dark:bg-indigo-900/30', 'border-indigo-300/50 dark:border-indigo-700/40', 'text-indigo-600/80 dark:text-indigo-400/80'],
144
+ ['bg-lime-100/70 dark:bg-lime-900/30', 'border-lime-300/50 dark:border-lime-700/40', 'text-lime-700/80 dark:text-lime-400/80'],
145
+ ['bg-fuchsia-100/70 dark:bg-fuchsia-900/30', 'border-fuchsia-300/50 dark:border-fuchsia-700/40','text-fuchsia-600/80 dark:text-fuchsia-400/80'],
146
+ ['bg-cyan-100/70 dark:bg-cyan-900/30', 'border-cyan-300/50 dark:border-cyan-700/40', 'text-cyan-600/80 dark:text-cyan-400/80'],
147
+ ['bg-orange-100/70 dark:bg-orange-900/30', 'border-orange-300/50 dark:border-orange-700/40', 'text-orange-600/80 dark:text-orange-400/80'],
148
148
  ];
149
149
 
150
150
  function hashName(str: string): number {
@@ -696,7 +696,7 @@ function AgentCard({
696
696
  </Link>
697
697
  {skillMode && (
698
698
  <span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${
699
- skillMode === 'universal' ? 'bg-success/10 text-success'
699
+ skillMode === 'universal' ? 'bg-muted text-muted-foreground'
700
700
  : skillMode === 'additional' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
701
701
  : 'bg-muted text-muted-foreground'
702
702
  }`}>
@@ -422,7 +422,7 @@ function MetaCard({
422
422
  tone?: 'ok' | 'muted' | 'default';
423
423
  }) {
424
424
  const valueColor =
425
- tone === 'ok' ? 'text-emerald-600 dark:text-emerald-400'
425
+ tone === 'ok' ? 'text-[var(--success)]'
426
426
  : tone === 'muted' ? 'text-muted-foreground'
427
427
  : 'text-foreground';
428
428
  return (
@@ -18,7 +18,7 @@ function UserMessageContent({ content, skillName }: { content: string; skillName
18
18
  const rest = prefixMatch ? content.slice(prefixMatch[0].length) : content;
19
19
  return (
20
20
  <>
21
- <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber-foreground)]/15 text-[var(--amber-foreground)]/90 mr-1 align-middle">
21
+ <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber)]/15 text-[var(--amber)] mr-1 align-middle">
22
22
  <Zap size={10} className="shrink-0" />
23
23
  {resolved}
24
24
  </span>
@@ -19,12 +19,12 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
19
19
  const e = t.panels.echo;
20
20
  const pathname = usePathname() ?? '';
21
21
 
22
- const rowBySegment: Record<EchoSegment, { icon: ReactNode; title: string; subtitle: string }> = {
23
- 'about-you': { icon: <UserRound size={14} />, title: e.aboutYouTitle, subtitle: e.aboutYouDesc },
24
- continued: { icon: <Bookmark size={14} />, title: e.continuedTitle, subtitle: e.continuedDesc },
25
- daily: { icon: <Sun size={14} />, title: e.dailyEchoTitle, subtitle: e.dailyDesc },
26
- 'past-you': { icon: <History size={14} />, title: e.pastYouTitle, subtitle: e.pastYouDesc },
27
- growth: { icon: <Brain size={14} />, title: e.intentGrowthTitle, subtitle: e.growthDesc },
22
+ const rowBySegment: Record<EchoSegment, { icon: ReactNode; title: string }> = {
23
+ 'about-you': { icon: <UserRound size={14} />, title: e.aboutYouTitle },
24
+ continued: { icon: <Bookmark size={14} />, title: e.continuedTitle },
25
+ daily: { icon: <Sun size={14} />, title: e.dailyEchoTitle },
26
+ 'past-you': { icon: <History size={14} />, title: e.pastYouTitle },
27
+ growth: { icon: <Brain size={14} />, title: e.intentGrowthTitle },
28
28
  };
29
29
 
30
30
  return (
@@ -37,7 +37,7 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
37
37
  const href = ECHO_SEGMENT_HREF[segment];
38
38
  const isActive = pathname === href || pathname.startsWith(`${href}/`);
39
39
  return (
40
- <PanelNavRow key={segment} href={href} icon={row.icon} title={row.title} subtitle={row.subtitle} active={isActive} />
40
+ <PanelNavRow key={segment} href={href} icon={row.icon} title={row.title} active={isActive} />
41
41
  );
42
42
  })}
43
43
  </div>
@@ -207,7 +207,7 @@ Be specific. Reference actual content from the files. Keep the total response un
207
207
 
208
208
  {/* error */}
209
209
  {error && (
210
- <div className="font-display" style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontSize: 12, marginBottom: '1rem' }}>
210
+ <div className="font-display" style={{ padding: '10px 14px', borderRadius: 8, background: 'color-mix(in srgb, var(--error) 10%, transparent)', border: '1px solid color-mix(in srgb, var(--error) 30%, transparent)', color: 'var(--error)', fontSize: 12, marginBottom: '1rem' }}>
211
211
  {error}
212
212
  </div>
213
213
  )}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",