@geminilight/mindos 0.5.9 → 0.5.11

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/app/app/api/settings/test-key/route.ts +111 -0
  3. package/app/app/api/skills/route.ts +1 -1
  4. package/app/app/api/sync/route.ts +16 -31
  5. package/app/app/globals.css +10 -2
  6. package/app/app/login/page.tsx +1 -1
  7. package/app/app/view/[...path]/ViewPageClient.tsx +6 -1
  8. package/app/app/view/[...path]/not-found.tsx +1 -1
  9. package/app/components/AskModal.tsx +4 -4
  10. package/app/components/Breadcrumb.tsx +2 -2
  11. package/app/components/DirView.tsx +6 -6
  12. package/app/components/FileTree.tsx +2 -2
  13. package/app/components/HomeContent.tsx +7 -7
  14. package/app/components/OnboardingView.tsx +1 -1
  15. package/app/components/SearchModal.tsx +1 -1
  16. package/app/components/SettingsModal.tsx +2 -2
  17. package/app/components/SetupWizard.tsx +1 -1400
  18. package/app/components/Sidebar.tsx +4 -4
  19. package/app/components/SidebarLayout.tsx +9 -0
  20. package/app/components/SyncStatusBar.tsx +3 -3
  21. package/app/components/TableOfContents.tsx +1 -1
  22. package/app/components/UpdateBanner.tsx +1 -1
  23. package/app/components/ask/FileChip.tsx +1 -1
  24. package/app/components/ask/MentionPopover.tsx +4 -4
  25. package/app/components/ask/MessageList.tsx +1 -1
  26. package/app/components/ask/SessionHistory.tsx +2 -2
  27. package/app/components/renderers/config/ConfigRenderer.tsx +1 -1
  28. package/app/components/renderers/csv/BoardView.tsx +2 -2
  29. package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
  30. package/app/components/renderers/csv/GalleryView.tsx +1 -1
  31. package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
  32. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  33. package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
  34. package/app/components/settings/AiTab.tsx +120 -2
  35. package/app/components/settings/KnowledgeTab.tsx +1 -1
  36. package/app/components/settings/McpTab.tsx +27 -23
  37. package/app/components/settings/PluginsTab.tsx +4 -4
  38. package/app/components/settings/Primitives.tsx +1 -1
  39. package/app/components/settings/SyncTab.tsx +8 -8
  40. package/app/components/setup/StepAI.tsx +67 -0
  41. package/app/components/setup/StepAgents.tsx +237 -0
  42. package/app/components/setup/StepDots.tsx +39 -0
  43. package/app/components/setup/StepKB.tsx +237 -0
  44. package/app/components/setup/StepPorts.tsx +121 -0
  45. package/app/components/setup/StepReview.tsx +211 -0
  46. package/app/components/setup/StepSecurity.tsx +78 -0
  47. package/app/components/setup/constants.tsx +13 -0
  48. package/app/components/setup/index.tsx +464 -0
  49. package/app/components/setup/types.ts +53 -0
  50. package/app/instrumentation.ts +19 -0
  51. package/app/lib/i18n.ts +22 -4
  52. package/app/next.config.ts +1 -1
  53. package/bin/cli.js +8 -1
  54. package/bin/lib/sync.js +61 -11
  55. package/package.json +4 -2
  56. package/skills/project-wiki/SKILL.md +92 -63
  57. package/assets/images/demo-flow-dark.png +0 -0
  58. package/assets/images/demo-flow-light.png +0 -0
  59. package/assets/images/demo-flow-zh-dark.png +0 -0
  60. package/assets/images/demo-flow-zh-light.png +0 -0
  61. package/assets/images/gui-sync-cv.png +0 -0
  62. package/assets/images/wechat-qr.png +0 -0
  63. package/mcp/package-lock.json +0 -1717
package/README.md CHANGED
@@ -417,7 +417,7 @@ MindOS/
417
417
  ~/.mindos/ # User data directory (outside project, never committed)
418
418
  ├── config.json # All configuration (AI keys, port, auth token, sync settings)
419
419
  ├── sync-state.json # Sync state (last sync time, conflicts)
420
- └── my-mind/ # Your private knowledge base (default path, customizable on onboard)
420
+ └── mind/ # Your private knowledge base (default: ~/MindOS/mind, customizable on onboard)
421
421
  ```
422
422
 
423
423
  ---
@@ -0,0 +1,111 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { effectiveAiConfig } from '@/lib/settings';
4
+
5
+ const TIMEOUT = 10_000;
6
+
7
+ type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
8
+
9
+ function classifyError(status: number, body: string): { code: ErrorCode; error: string } {
10
+ if (status === 401 || status === 403) return { code: 'auth_error', error: 'Invalid API key' };
11
+ if (status === 404) return { code: 'model_not_found', error: 'Model not found' };
12
+ if (status === 429) return { code: 'rate_limited', error: 'Rate limited' };
13
+ // Try to extract error message from response body
14
+ try {
15
+ const parsed = JSON.parse(body);
16
+ const msg = parsed?.error?.message || parsed?.error || '';
17
+ if (typeof msg === 'string' && msg.length > 0) return { code: 'unknown', error: msg.slice(0, 200) };
18
+ } catch { /* not JSON */ }
19
+ return { code: 'unknown', error: `HTTP ${status}` };
20
+ }
21
+
22
+ async function testAnthropic(apiKey: string, model: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string }> {
23
+ const start = Date.now();
24
+ const ctrl = new AbortController();
25
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
26
+ try {
27
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ 'x-api-key': apiKey,
32
+ 'anthropic-version': '2023-06-01',
33
+ },
34
+ body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
35
+ signal: ctrl.signal,
36
+ });
37
+ const latency = Date.now() - start;
38
+ if (res.ok) return { ok: true, latency };
39
+ const body = await res.text();
40
+ return { ok: false, ...classifyError(res.status, body) };
41
+ } catch (e: unknown) {
42
+ if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
43
+ return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ }
48
+
49
+ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string }> {
50
+ const start = Date.now();
51
+ const ctrl = new AbortController();
52
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
53
+ const url = (baseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '') + '/chat/completions';
54
+ try {
55
+ const res = await fetch(url, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'Authorization': `Bearer ${apiKey}`,
60
+ },
61
+ body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
62
+ signal: ctrl.signal,
63
+ });
64
+ const latency = Date.now() - start;
65
+ if (res.ok) return { ok: true, latency };
66
+ const body = await res.text();
67
+ return { ok: false, ...classifyError(res.status, body) };
68
+ } catch (e: unknown) {
69
+ if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
70
+ return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
71
+ } finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
75
+
76
+ export async function POST(req: NextRequest) {
77
+ try {
78
+ const body = await req.json();
79
+ const { provider, apiKey, model, baseUrl } = body as {
80
+ provider?: string;
81
+ apiKey?: string;
82
+ model?: string;
83
+ baseUrl?: string;
84
+ };
85
+
86
+ if (provider !== 'anthropic' && provider !== 'openai') {
87
+ return NextResponse.json({ ok: false, code: 'unknown', error: 'Invalid provider' }, { status: 400 });
88
+ }
89
+
90
+ // Resolve actual API key: use provided key, fallback to config/env for masked or missing
91
+ const cfg = effectiveAiConfig();
92
+ let resolvedKey = apiKey || '';
93
+ if (!resolvedKey || resolvedKey === '***set***') {
94
+ resolvedKey = provider === 'anthropic' ? cfg.anthropicApiKey : cfg.openaiApiKey;
95
+ }
96
+
97
+ if (!resolvedKey) {
98
+ return NextResponse.json({ ok: false, code: 'auth_error', error: 'No API key configured' });
99
+ }
100
+
101
+ const resolvedModel = model || (provider === 'anthropic' ? cfg.anthropicModel : cfg.openaiModel);
102
+
103
+ const result = provider === 'anthropic'
104
+ ? await testAnthropic(resolvedKey, resolvedModel)
105
+ : await testOpenAI(resolvedKey, resolvedModel, baseUrl || cfg.openaiBaseUrl);
106
+
107
+ return NextResponse.json(result);
108
+ } catch (err) {
109
+ return NextResponse.json({ ok: false, code: 'unknown', error: String(err) }, { status: 500 });
110
+ }
111
+ }
@@ -9,7 +9,7 @@ const PROJECT_ROOT = path.resolve(process.cwd(), '..');
9
9
 
10
10
  function getMindRoot(): string {
11
11
  const s = readSettings();
12
- return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS');
12
+ return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
13
13
  }
14
14
 
15
15
  interface SkillInfo {
@@ -95,25 +95,13 @@ export async function POST(req: NextRequest) {
95
95
  return NextResponse.json({ error: 'Sync already configured' }, { status: 400 });
96
96
  }
97
97
 
98
- // Build the effective remote URL (inject token for HTTPS)
99
- let effectiveRemote = remote;
100
- if (isHTTPS && body.token) {
101
- try {
102
- const urlObj = new URL(remote);
103
- urlObj.username = 'oauth2';
104
- urlObj.password = body.token;
105
- effectiveRemote = urlObj.toString();
106
- } catch {
107
- return NextResponse.json({ error: 'Invalid remote URL' }, { status: 400 });
108
- }
109
- }
110
-
111
98
  const branch = body.branch?.trim() || 'main';
112
99
 
113
- // Call CLI's sync init via execFile (avoids module resolution issues with Turbopack)
100
+ // Call CLI's sync init pass clean remote + token separately (never embed token in URL)
114
101
  try {
115
102
  const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
116
- const args = ['sync', 'init', '--non-interactive', '--remote', effectiveRemote, '--branch', branch];
103
+ const args = ['sync', 'init', '--non-interactive', '--remote', remote, '--branch', branch];
104
+ if (body.token) args.push('--token', body.token);
117
105
 
118
106
  await new Promise<void>((res, rej) => {
119
107
  execFile('node', [cliPath, ...args], { timeout: 30000 }, (err, stdout, stderr) => {
@@ -132,23 +120,20 @@ export async function POST(req: NextRequest) {
132
120
  if (!isGitRepo(mindRoot)) {
133
121
  return NextResponse.json({ error: 'Not a git repository' }, { status: 400 });
134
122
  }
135
- // Pull
136
- try { execSync('git pull --rebase --autostash', { cwd: mindRoot, stdio: 'pipe' }); } catch {
137
- try { execSync('git rebase --abort', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
138
- try { execSync('git pull --no-rebase', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
139
- }
140
- // Commit + push
141
- execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
142
- const status = execSync('git status --porcelain', { cwd: mindRoot, encoding: 'utf-8' }).trim();
143
- if (status) {
144
- const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
145
- execSync(`git commit -m "auto-sync: ${timestamp}"`, { cwd: mindRoot, stdio: 'pipe' });
146
- execSync('git push', { cwd: mindRoot, stdio: 'pipe' });
123
+ // Delegate to CLI for unified conflict handling
124
+ try {
125
+ const cliPath = resolve(process.cwd(), '..', 'bin', 'cli.js');
126
+ await new Promise<void>((res, rej) => {
127
+ execFile('node', [cliPath, 'sync', 'now'], { timeout: 60000 }, (err, stdout, stderr) => {
128
+ if (err) rej(new Error(stderr?.trim() || err.message));
129
+ else res();
130
+ });
131
+ });
132
+ return NextResponse.json({ ok: true });
133
+ } catch (err: unknown) {
134
+ const errMsg = err instanceof Error ? err.message : String(err);
135
+ return NextResponse.json({ error: errMsg }, { status: 500 });
147
136
  }
148
- const state = loadSyncState();
149
- state.lastSync = new Date().toISOString();
150
- writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2) + '\n');
151
- return NextResponse.json({ ok: true });
152
137
  }
153
138
 
154
139
  case 'on': {
@@ -28,6 +28,7 @@
28
28
  --color-destructive: var(--destructive);
29
29
  --color-success: var(--success);
30
30
  --color-error: var(--error);
31
+ --color-amber-foreground: var(--amber-foreground);
31
32
  --color-accent-foreground: var(--accent-foreground);
32
33
  --color-accent: var(--accent);
33
34
  --color-muted-foreground: var(--muted-foreground);
@@ -65,7 +66,7 @@ body {
65
66
  --secondary: #e8e4db;
66
67
  --secondary-foreground: #1c1a17;
67
68
  --muted: #e8e4db;
68
- --muted-foreground: #7a7568;
69
+ --muted-foreground: #685f52;
69
70
  --accent: #d9d3c6;
70
71
  --accent-foreground: #1c1a17;
71
72
  --destructive: oklch(0.58 0.22 27);
@@ -75,6 +76,7 @@ body {
75
76
  --radius: 0.5rem;
76
77
  --amber: #c8873a;
77
78
  --amber-dim: rgba(200, 135, 58, 0.12);
79
+ --amber-foreground: #131210;
78
80
  --success: #7aad80;
79
81
  --error: #c85050;
80
82
  --sidebar: #ede9e1;
@@ -108,6 +110,7 @@ body {
108
110
  --ring: var(--amber);
109
111
  --amber: #d4954a;
110
112
  --amber-dim: rgba(212, 149, 74, 0.12);
113
+ --amber-foreground: #131210;
111
114
  --success: #7aad80;
112
115
  --error: #c85050;
113
116
  --sidebar: #1c1a17;
@@ -282,6 +285,11 @@ body {
282
285
  -webkit-backdrop-filter: blur(8px);
283
286
  }
284
287
 
288
+ /* Micro type scale: text-2xs = 10px (between nothing and text-xs 12px) */
289
+ @layer utilities {
290
+ .text-2xs { font-size: 10px; line-height: 1.4; }
291
+ }
292
+
285
293
  /* Hide scrollbar but keep scroll functionality */
286
294
  .scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
287
295
  .scrollbar-none::-webkit-scrollbar { display: none; }
@@ -341,7 +349,7 @@ a:focus-visible,
341
349
  /* Selection */
342
350
  .wysiwyg-editor ::selection {
343
351
  background: var(--amber);
344
- color: #131210;
352
+ color: var(--amber-foreground);
345
353
  opacity: 0.35;
346
354
  }
347
355
 
@@ -101,7 +101,7 @@ function LoginForm() {
101
101
  <button
102
102
  type="submit"
103
103
  disabled={loading || !password}
104
- className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[#131210]"
104
+ className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[var(--amber-foreground)]"
105
105
  >
106
106
  {loading ? (
107
107
  <Loader2 size={14} className="animate-spin" />
@@ -118,6 +118,11 @@ export default function ViewPageClient({
118
118
  setSaveError('Please enter a file name');
119
119
  return;
120
120
  }
121
+ // Reject path traversal and illegal filename characters
122
+ if (/[/\\:*?"<>|]/.test(trimmed) || trimmed.includes('..')) {
123
+ setSaveError('File name contains invalid characters');
124
+ return;
125
+ }
121
126
  if (!createDraftAction) {
122
127
  setSaveError('Draft save is not available');
123
128
  return;
@@ -283,7 +288,7 @@ export default function ViewPageClient({
283
288
  onClick={isDraft && showSaveAs ? handleConfirmDraftSave : handleSave}
284
289
  disabled={isPending}
285
290
  className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50 font-display"
286
- style={{ background: 'var(--amber)', color: '#131210' }}
291
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
287
292
  >
288
293
  {isPending ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
289
294
  <span className="hidden sm:inline">Save</span>
@@ -69,7 +69,7 @@ export default function ViewNotFound() {
69
69
  onClick={handleCreate}
70
70
  disabled={creating}
71
71
  className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
72
- style={{ background: 'var(--amber)', color: '#131210' }}
72
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
73
73
  >
74
74
  <FilePlus size={14} />
75
75
  {creating
@@ -289,7 +289,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
289
289
  {/* Attached file chips */}
290
290
  {attachedFiles.length > 0 && (
291
291
  <div className="px-4 pt-2.5 pb-1">
292
- <div className="text-[11px] text-muted-foreground/70 mb-1.5">Knowledge Base Context</div>
292
+ <div className="text-xs text-muted-foreground/70 mb-1.5">Knowledge Base Context</div>
293
293
  <div className="flex flex-wrap gap-1.5">
294
294
  {attachedFiles.map(f => (
295
295
  <FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
@@ -300,7 +300,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
300
300
 
301
301
  {upload.localAttachments.length > 0 && (
302
302
  <div className="px-4 pb-1">
303
- <div className="text-[11px] text-muted-foreground/70 mb-1.5">Uploaded Files</div>
303
+ <div className="text-xs text-muted-foreground/70 mb-1.5">Uploaded Files</div>
304
304
  <div className="flex flex-wrap gap-1.5">
305
305
  {upload.localAttachments.map((f, idx) => (
306
306
  <FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
@@ -371,7 +371,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
371
371
  <StopCircle size={15} />
372
372
  </button>
373
373
  ) : (
374
- <button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: '#131210' }}>
374
+ <button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
375
375
  <Send size={14} />
376
376
  </button>
377
377
  )}
@@ -384,7 +384,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
384
384
  <span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
385
385
  <span className="inline-flex items-center gap-1">
386
386
  <span>Agent steps</span>
387
- <select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-[11px] text-foreground">
387
+ <select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-xs text-foreground">
388
388
  <option value={10}>10</option>
389
389
  <option value={20}>20</option>
390
390
  <option value={30}>30</option>
@@ -5,8 +5,8 @@ import { ChevronRight, Home, FileText, Table, Folder } from 'lucide-react';
5
5
 
6
6
  function FileTypeIcon({ name }: { name: string }) {
7
7
  const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')).toLowerCase() : '';
8
- if (ext === '.csv') return <Table size={13} className="text-emerald-400 shrink-0" />;
9
- if (ext) return <FileText size={13} className="text-zinc-400 shrink-0" />;
8
+ if (ext === '.csv') return <Table size={13} className="text-success shrink-0" />;
9
+ if (ext) return <FileText size={13} className="text-muted-foreground shrink-0" />;
10
10
  return <Folder size={13} className="text-yellow-400 shrink-0" />;
11
11
  }
12
12
 
@@ -15,14 +15,14 @@ interface DirViewProps {
15
15
 
16
16
  function FileIcon({ node }: { node: FileNode }) {
17
17
  if (node.type === 'directory') return <Folder size={16} className="text-yellow-400 shrink-0" />;
18
- if (node.extension === '.csv') return <Table size={16} className="text-emerald-400 shrink-0" />;
19
- return <FileText size={16} className="text-zinc-400 shrink-0" />;
18
+ if (node.extension === '.csv') return <Table size={16} className="text-success shrink-0" />;
19
+ return <FileText size={16} className="text-muted-foreground shrink-0" />;
20
20
  }
21
21
 
22
22
  function FileIconLarge({ node }: { node: FileNode }) {
23
23
  if (node.type === 'directory') return <FolderOpen size={28} className="text-yellow-400" />;
24
- if (node.extension === '.csv') return <Table size={28} className="text-emerald-400" />;
25
- return <FileText size={28} className="text-zinc-400" />;
24
+ if (node.extension === '.csv') return <Table size={28} className="text-success" />;
25
+ return <FileText size={28} className="text-muted-foreground" />;
26
26
  }
27
27
 
28
28
  function countFiles(node: FileNode): number {
@@ -125,10 +125,10 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
125
125
  {entry.name}
126
126
  </span>
127
127
  {entry.type === 'directory' && (
128
- <span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
128
+ <span className="text-2xs text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
129
129
  )}
130
130
  {entry.type === 'file' && entry.mtime && (
131
- <span className="text-[10px] text-muted-foreground font-display" suppressHydrationWarning>
131
+ <span className="text-2xs text-muted-foreground font-display" suppressHydrationWarning>
132
132
  {formatTime(entry.mtime)}
133
133
  </span>
134
134
  )}
@@ -16,8 +16,8 @@ interface FileTreeProps {
16
16
 
17
17
  function getIcon(node: FileNode) {
18
18
  if (node.type === 'directory') return null;
19
- if (node.extension === '.csv') return <Table size={14} className="text-emerald-400 shrink-0" />;
20
- return <FileText size={14} className="text-zinc-400 shrink-0" />;
19
+ if (node.extension === '.csv') return <Table size={14} className="text-success shrink-0" />;
20
+ return <FileText size={14} className="text-muted-foreground shrink-0" />;
21
21
  }
22
22
 
23
23
  function getCurrentFilePath(pathname: string): string {
@@ -102,7 +102,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
102
102
  {suggestions[suggestionIdx]}
103
103
  </span>
104
104
  <kbd
105
- className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[11px] font-mono font-medium"
105
+ className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-mono font-medium"
106
106
  style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}
107
107
  >
108
108
  ⌘/
@@ -118,7 +118,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
118
118
  >
119
119
  <Search size={14} />
120
120
  <span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
121
- <kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono" style={{ background: 'var(--muted)' }}>
121
+ <kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-2xs font-mono" style={{ background: 'var(--muted)' }}>
122
122
  ⌘K
123
123
  </kbd>
124
124
  </button>
@@ -189,17 +189,17 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
189
189
  {r.name}
190
190
  </span>
191
191
  </div>
192
- <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
192
+ <p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
193
193
  {r.description}
194
194
  </p>
195
195
  {hintId === r.id ? (
196
- <p className="text-[10px] animate-in" style={{ color: 'var(--amber)' }} role="status">
196
+ <p className="text-2xs animate-in" style={{ color: 'var(--amber)' }} role="status">
197
197
  {(t.home.createToActivate ?? 'Create {file} to activate').replace('{file}', entryPath ?? '')}
198
198
  </p>
199
199
  ) : (
200
200
  <div className="flex flex-wrap gap-1">
201
201
  {r.tags.slice(0, 3).map(tag => (
202
- <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
202
+ <span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
203
203
  {tag}
204
204
  </span>
205
205
  ))}
@@ -222,12 +222,12 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
222
222
  {r.name}
223
223
  </span>
224
224
  </div>
225
- <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
225
+ <p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
226
226
  {r.description}
227
227
  </p>
228
228
  <div className="flex flex-wrap gap-1">
229
229
  {r.tags.slice(0, 3).map(tag => (
230
- <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
230
+ <span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
231
231
  {tag}
232
232
  </span>
233
233
  ))}
@@ -110,7 +110,7 @@ export default function OnboardingView() {
110
110
 
111
111
  {/* Directory preview */}
112
112
  <div
113
- className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed font-display"
113
+ className="w-full rounded-lg px-3 py-2 text-xs leading-relaxed font-display"
114
114
  style={{
115
115
  background: 'var(--muted)',
116
116
  color: 'var(--muted-foreground)',
@@ -158,7 +158,7 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
158
158
  `}
159
159
  >
160
160
  {ext === '.csv'
161
- ? <Table size={14} className="text-emerald-400 shrink-0 mt-0.5" />
161
+ ? <Table size={14} className="text-success shrink-0 mt-0.5" />
162
162
  : <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
163
163
  }
164
164
  <div className="min-w-0 flex-1">
@@ -234,7 +234,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
234
234
  )}
235
235
  <div className="flex items-center gap-1.5 text-xs">
236
236
  {status === 'saved' && (
237
- <><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{t.settings.saved}</span></>
237
+ <><CheckCircle2 size={13} className="text-success" /><span className="text-success">{t.settings.saved}</span></>
238
238
  )}
239
239
  {status === 'error' && (
240
240
  <><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>
@@ -245,7 +245,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
245
245
  onClick={handleSave}
246
246
  disabled={saving || !data}
247
247
  className="flex items-center gap-1.5 px-4 py-1.5 text-sm rounded-lg disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
248
- style={{ background: 'var(--amber)', color: '#131210' }}
248
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
249
249
  >
250
250
  {saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
251
251
  {t.settings.save}