@geminilight/mindos 0.5.8 → 0.5.10

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 (65) hide show
  1. package/README.md +9 -10
  2. package/README_zh.md +8 -9
  3. package/app/app/api/mcp/agents/route.ts +7 -0
  4. package/app/app/api/mcp/install-skill/route.ts +6 -0
  5. package/app/app/api/setup/check-port/route.ts +27 -3
  6. package/app/app/api/setup/route.ts +2 -9
  7. package/app/app/api/skills/route.ts +1 -1
  8. package/app/app/globals.css +28 -4
  9. package/app/app/login/page.tsx +2 -2
  10. package/app/app/view/[...path]/ViewPageClient.tsx +15 -10
  11. package/app/app/view/[...path]/not-found.tsx +1 -1
  12. package/app/components/AskModal.tsx +5 -5
  13. package/app/components/Breadcrumb.tsx +2 -2
  14. package/app/components/DirView.tsx +6 -6
  15. package/app/components/FileTree.tsx +7 -7
  16. package/app/components/HomeContent.tsx +8 -8
  17. package/app/components/OnboardingView.tsx +1 -1
  18. package/app/components/SearchModal.tsx +1 -1
  19. package/app/components/SettingsModal.tsx +2 -2
  20. package/app/components/SetupWizard.tsx +1 -1258
  21. package/app/components/Sidebar.tsx +4 -4
  22. package/app/components/SidebarLayout.tsx +9 -0
  23. package/app/components/SyncStatusBar.tsx +6 -6
  24. package/app/components/TableOfContents.tsx +1 -1
  25. package/app/components/UpdateBanner.tsx +1 -1
  26. package/app/components/ask/FileChip.tsx +1 -1
  27. package/app/components/ask/MentionPopover.tsx +4 -4
  28. package/app/components/ask/MessageList.tsx +3 -3
  29. package/app/components/ask/SessionHistory.tsx +3 -3
  30. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
  31. package/app/components/renderers/config/ConfigRenderer.tsx +4 -4
  32. package/app/components/renderers/csv/BoardView.tsx +2 -2
  33. package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
  34. package/app/components/renderers/csv/GalleryView.tsx +1 -1
  35. package/app/components/renderers/csv/types.ts +1 -1
  36. package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
  37. package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
  38. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  39. package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
  40. package/app/components/renderers/workflow/WorkflowRenderer.tsx +4 -4
  41. package/app/components/settings/KnowledgeTab.tsx +1 -1
  42. package/app/components/settings/McpTab.tsx +93 -47
  43. package/app/components/settings/PluginsTab.tsx +4 -4
  44. package/app/components/settings/Primitives.tsx +4 -4
  45. package/app/components/settings/SyncTab.tsx +13 -13
  46. package/app/components/setup/StepAI.tsx +67 -0
  47. package/app/components/setup/StepAgents.tsx +237 -0
  48. package/app/components/setup/StepDots.tsx +39 -0
  49. package/app/components/setup/StepKB.tsx +237 -0
  50. package/app/components/setup/StepPorts.tsx +121 -0
  51. package/app/components/setup/StepReview.tsx +211 -0
  52. package/app/components/setup/StepSecurity.tsx +78 -0
  53. package/app/components/setup/constants.tsx +13 -0
  54. package/app/components/setup/index.tsx +464 -0
  55. package/app/components/setup/types.ts +53 -0
  56. package/app/lib/i18n.ts +52 -8
  57. package/app/lib/mcp-agents.ts +81 -0
  58. package/bin/lib/gateway.js +44 -4
  59. package/bin/lib/mcp-agents.js +81 -0
  60. package/bin/lib/mcp-install.js +34 -4
  61. package/package.json +3 -1
  62. package/scripts/setup.js +43 -6
  63. package/skills/project-wiki/SKILL.md +92 -63
  64. package/app/public/landing/index.html +0 -353
  65. package/app/public/landing/style.css +0 -216
package/README.md CHANGED
@@ -70,22 +70,21 @@ All that experience from your conversations — gone the moment you close the wi
70
70
 
71
71
  **For Humans**
72
72
 
73
- - **GUI Collaboration Workbench**: use one command entry to browse, edit, and search efficiently (`⌘K` / `⌘/`).
74
- - **Built-in Agent Assistant**: converse in context while edits are captured into managed knowledge.
75
- - **Plugin Views**: use scenario-focused views like TODO, Kanban, and Timeline.
73
+ - **GUI Workbench**: browse, edit, search notes with unified search + AI entry (`⌘K` / `⌘/`), designed for human-AI co-creation.
74
+ - **Built-in Agent Assistant**: converse with the knowledge base in context; edits seamlessly capture human-curated knowledge.
75
+ - **Plugin Extensions**: multiple built-in renderer plugins TODO Board, CSV Views, Wiki Graph, Timeline, Agent Inspector, and more.
76
76
 
77
77
  **For Agents**
78
78
 
79
- - **MCP Server + Skills**: connect any compatible agent to read, write, search, and run workflows.
80
- - **Structured Templates**: start quickly with Profile, Workflows, and Configurations scaffolds.
81
- - **Experience Auto-Distillation**: automatically distill daily work into reusable, executable SOP experience.
79
+ - **MCP Server + Skills**: stdio + HTTP dual transport, full-lineup Agent compatible (OpenClaw, Claude Code, Cursor, etc.). Zero-config access.
80
+ - **Structured Templates**: pre-set directory structures for Profiles, Workflows, Configurations, etc., to jumpstart personal context.
81
+ - **Agent-Ready Docs**: everyday notes naturally double as high-quality executable Agent commands — no format conversion needed, write and dispatch.
82
82
 
83
83
  **Infrastructure**
84
84
 
85
85
  - **Security**: Bearer Token auth, path sandboxing, INSTRUCTION.md write-protection, atomic writes.
86
- - **Knowledge Graph**: visualize relationships and dependencies across notes.
87
- - **Git Time Machine**: track every edit, audit history, and roll back safely.
88
- - **Cross-Device Sync**: auto-commit, push, and pull via Git — edits on one device appear on all others within minutes.
86
+ - **Knowledge Graph**: dynamically parses and visualizes inter-file references and dependencies.
87
+ - **Git Time Machine**: Git auto-sync (commit/push/pull), records every edit by both humans and Agents. One-click rollback, cross-device sync.
89
88
 
90
89
  <details>
91
90
  <summary><strong>Coming Soon</strong></summary>
@@ -418,7 +417,7 @@ MindOS/
418
417
  ~/.mindos/ # User data directory (outside project, never committed)
419
418
  ├── config.json # All configuration (AI keys, port, auth token, sync settings)
420
419
  ├── sync-state.json # Sync state (last sync time, conflicts)
421
- └── my-mind/ # Your private knowledge base (default path, customizable on onboard)
420
+ └── mind/ # Your private knowledge base (default: ~/MindOS/mind, customizable on onboard)
422
421
  ```
423
422
 
424
423
  ---
package/README_zh.md CHANGED
@@ -70,22 +70,21 @@ Agent 记了什么、记对没有,用户无从知晓。**MindOS 将每次读
70
70
 
71
71
  **人类侧**
72
72
 
73
- - **GUI 协作工作台**:以统一入口高效浏览、编辑与搜索(`⌘K` / `⌘/`)。
74
- - **内置 Agent 助手**:在上下文中对话,编辑内容可持续沉淀为可管理知识。
75
- - **插件视图**:按场景使用 TODO、看板、时间线等视图。
73
+ - **GUI 工作台**:浏览、编辑、搜索笔记,统一搜索 + AI 入口(`⌘K` / `⌘/`),专为人机共创设计。
74
+ - **内置 Agent 助手**:在上下文中与知识库对话,编辑无缝沉淀为可管理知识。
75
+ - **插件扩展**:多种内置渲染器插件——TODO Board、CSV Views、Wiki Graph、Timeline、Agent Inspector 等。
76
76
 
77
77
  **Agent 侧**
78
78
 
79
- - **MCP Server + Skills**:让兼容 Agent 统一接入读写、搜索与工作流执行。
80
- - **结构化模板**:通过 Profile、Workflows、Configurations 快速冷启动。
81
- - **经验自动沉淀**:将日常记录自动化沉淀为可执行 SOP 经验。
79
+ - **MCP Server + Skills**:stdio + HTTP 双传输,全阵容 Agent 兼容(OpenClaw, Claude Code, Cursor 等),零配置接入。
80
+ - **结构化模板**:预置 Profile、Workflows、Configurations 等目录骨架,快速冷启动个人 Context。
81
+ - **笔记即指令**:日常笔记天然就是 Agent 可直接执行的高质量指令——无需额外格式转换,写下即可调度。
82
82
 
83
83
  **基础设施**
84
84
 
85
85
  - **安全防线**:Bearer Token 认证、路径沙箱、INSTRUCTION.md 写保护、原子写入。
86
- - **知识图谱**:可视化笔记间关系与依赖。
87
- - **Git 时光机**:记录修改历史,支持审计与安全回滚。
88
- - **跨设备同步**:通过 Git 自动 commit、push、pull —— 一台设备的编辑几分钟内同步到所有设备。
86
+ - **知识图谱**:动态解析并可视化文件间的引用与依赖关系。
87
+ - **Git 时光机**:Git 自动同步(commit/push/pull),记录人类与 Agent 的每次编辑历史,一键回滚,跨设备同步。
89
88
 
90
89
  <details>
91
90
  <summary><strong>即将到来</strong></summary>
@@ -20,6 +20,13 @@ export async function GET() {
20
20
  preferredTransport: agent.preferredTransport,
21
21
  };
22
22
  });
23
+
24
+ // Sort: installed first, then detected, then not found
25
+ agents.sort((a, b) => {
26
+ const rank = (x: typeof a) => x.installed ? 0 : x.present ? 1 : 2;
27
+ return rank(a) - rank(b);
28
+ });
29
+
23
30
  return NextResponse.json({ agents });
24
31
  } catch (err) {
25
32
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -24,6 +24,12 @@ const AGENT_NAME_MAP: Record<string, string> = {
24
24
  'trae': 'trae',
25
25
  'openclaw': 'openclaw',
26
26
  'codebuddy': 'codebuddy',
27
+ 'iflow-cli': 'iflow-cli',
28
+ 'pi': 'pi',
29
+ 'augment': 'augment',
30
+ 'qwen-code': 'qwen-code',
31
+ 'trae-cn': 'trae-cn',
32
+ 'roo': 'roo',
27
33
  };
28
34
 
29
35
  /* ── Helpers ──────────────────────────────────────────────────── */
@@ -27,29 +27,53 @@ async function isSelfPort(port: number): Promise<boolean> {
27
27
  }
28
28
  }
29
29
 
30
- async function findFreePort(start: number): Promise<number | null> {
30
+ async function findFreePort(start: number, selfPorts: Set<number>): Promise<number | null> {
31
31
  for (let p = start; p <= 65535; p++) {
32
+ if (selfPorts.has(p)) continue;
32
33
  if (!await isPortInUse(p)) return p;
33
34
  }
34
35
  return null;
35
36
  }
36
37
 
38
+ /**
39
+ * The port this MindOS web server is actually listening on.
40
+ * Derived from the incoming request URL — always reliable, no network round-trip.
41
+ *
42
+ * Note: We intentionally do NOT read settings here. Settings contain *configured*
43
+ * ports (webPort / mcpPort), which may not actually be listening yet (e.g. during
44
+ * first onboard, or if MCP server hasn't started). Treating configured-but-not-
45
+ * listening ports as "self" would mask real conflicts.
46
+ */
47
+ function getListeningPort(req: NextRequest): number {
48
+ return parseInt(req.nextUrl.port || '0', 10);
49
+ }
50
+
37
51
  export async function POST(req: NextRequest) {
38
52
  try {
39
53
  const { port } = await req.json() as { port: number };
40
54
  if (!port || port < 1024 || port > 65535) {
41
55
  return NextResponse.json({ error: 'Invalid port' }, { status: 400 });
42
56
  }
57
+
58
+ const myPort = getListeningPort(req);
59
+
60
+ // Fast path: if checking the port we're currently listening on, skip network round-trip
61
+ if (myPort > 0 && port === myPort) {
62
+ return NextResponse.json({ available: true, isSelf: true });
63
+ }
64
+
43
65
  const inUse = await isPortInUse(port);
44
66
  if (!inUse) {
45
67
  return NextResponse.json({ available: true, isSelf: false });
46
68
  }
47
- // Port is occupied — check if it's this MindOS instance
69
+ // Port is occupied — check if it's another MindOS instance
48
70
  const self = await isSelfPort(port);
49
71
  if (self) {
50
72
  return NextResponse.json({ available: true, isSelf: true });
51
73
  }
52
- const suggestion = await findFreePort(port + 1);
74
+ const skipPorts = new Set<number>();
75
+ if (myPort > 0) skipPorts.add(myPort);
76
+ const suggestion = await findFreePort(port + 1, skipPorts);
53
77
  return NextResponse.json({ available: false, isSelf: false, suggestion });
54
78
  } catch (err) {
55
79
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -67,17 +67,10 @@ export async function POST(req: NextRequest) {
67
67
  return NextResponse.json({ error: `Invalid MCP port: ${mcpPortNum}` }, { status: 400 });
68
68
  }
69
69
 
70
- // Apply template if mindRoot doesn't exist or is empty
70
+ // Apply template (copyRecursive has skip-existing protection)
71
71
  const dirExists = fs.existsSync(resolvedRoot);
72
- let dirEmpty = true;
73
- if (dirExists) {
74
- try {
75
- const entries = fs.readdirSync(resolvedRoot).filter(e => !e.startsWith('.'));
76
- dirEmpty = entries.length === 0;
77
- } catch { /* treat as empty */ }
78
- }
79
72
 
80
- if (template && (!dirExists || dirEmpty)) {
73
+ if (template) {
81
74
  applyTemplate(template, resolvedRoot);
82
75
  } else if (!dirExists) {
83
76
  fs.mkdirSync(resolvedRoot, { recursive: true });
@@ -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 {
@@ -26,6 +26,9 @@
26
26
  --color-input: var(--input);
27
27
  --color-border: var(--border);
28
28
  --color-destructive: var(--destructive);
29
+ --color-success: var(--success);
30
+ --color-error: var(--error);
31
+ --color-amber-foreground: var(--amber-foreground);
29
32
  --color-accent-foreground: var(--accent-foreground);
30
33
  --color-accent: var(--accent);
31
34
  --color-muted-foreground: var(--muted-foreground);
@@ -63,16 +66,19 @@ body {
63
66
  --secondary: #e8e4db;
64
67
  --secondary-foreground: #1c1a17;
65
68
  --muted: #e8e4db;
66
- --muted-foreground: #7a7568;
69
+ --muted-foreground: #685f52;
67
70
  --accent: #d9d3c6;
68
71
  --accent-foreground: #1c1a17;
69
72
  --destructive: oklch(0.58 0.22 27);
70
73
  --border: rgba(28, 26, 23, 0.1);
71
74
  --input: rgba(28, 26, 23, 0.12);
72
- --ring: rgba(28, 26, 23, 0.3);
75
+ --ring: var(--amber);
73
76
  --radius: 0.5rem;
74
77
  --amber: #c8873a;
75
78
  --amber-dim: rgba(200, 135, 58, 0.12);
79
+ --amber-foreground: #131210;
80
+ --success: #7aad80;
81
+ --error: #c85050;
76
82
  --sidebar: #ede9e1;
77
83
  --sidebar-foreground: #1c1a17;
78
84
  --sidebar-primary: #1c1a17;
@@ -101,9 +107,12 @@ body {
101
107
  --destructive: oklch(0.704 0.191 22.216);
102
108
  --border: rgba(232, 228, 220, 0.08);
103
109
  --input: rgba(232, 228, 220, 0.1);
104
- --ring: rgba(232, 228, 220, 0.2);
110
+ --ring: var(--amber);
105
111
  --amber: #d4954a;
106
112
  --amber-dim: rgba(212, 149, 74, 0.12);
113
+ --amber-foreground: #131210;
114
+ --success: #7aad80;
115
+ --error: #c85050;
107
116
  --sidebar: #1c1a17;
108
117
  --sidebar-foreground: #e8e4dc;
109
118
  --sidebar-primary: #d4954a;
@@ -276,6 +285,11 @@ body {
276
285
  -webkit-backdrop-filter: blur(8px);
277
286
  }
278
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
+
279
293
  /* Hide scrollbar but keep scroll functionality */
280
294
  .scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
281
295
  .scrollbar-none::-webkit-scrollbar { display: none; }
@@ -290,6 +304,16 @@ body {
290
304
  button, a { -webkit-tap-highlight-color: transparent; }
291
305
  }
292
306
 
307
+ /* Respect user's reduced-motion preference */
308
+ @media (prefers-reduced-motion: reduce) {
309
+ *, *::before, *::after {
310
+ animation-duration: 0.01ms !important;
311
+ animation-iteration-count: 1 !important;
312
+ transition-duration: 0.01ms !important;
313
+ scroll-behavior: auto !important;
314
+ }
315
+ }
316
+
293
317
  /* Global focus-visible ring for interactive elements */
294
318
  button:focus-visible,
295
319
  a:focus-visible,
@@ -325,7 +349,7 @@ a:focus-visible,
325
349
  /* Selection */
326
350
  .wysiwyg-editor ::selection {
327
351
  background: var(--amber);
328
- color: #131210;
352
+ color: var(--amber-foreground);
329
353
  opacity: 0.35;
330
354
  }
331
355
 
@@ -90,7 +90,7 @@ function LoginForm() {
90
90
  autoFocus
91
91
  autoComplete="current-password"
92
92
  required
93
- className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
93
+ className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
94
94
  />
95
95
  </div>
96
96
 
@@ -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" />
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useTransition, useCallback, useEffect, useRef, useSyncExternalStore, useMemo, Suspense } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft } from 'lucide-react';
5
+ import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft, Share2, FileText, Code } from 'lucide-react';
6
6
  import { lazy } from 'react';
7
7
  import MarkdownView from '@/components/MarkdownView';
8
8
  import JsonView from '@/components/JsonView';
@@ -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;
@@ -213,13 +218,13 @@ export default function ViewPageClient({
213
218
 
214
219
  <div className="flex items-center gap-1.5 md:gap-2 shrink-0">
215
220
  {saveSuccess && (
216
- <span className="text-xs flex items-center gap-1.5 font-display" style={{ color: '#7aad80' }}>
217
- <span className="w-1.5 h-1.5 rounded-full" style={{ background: '#7aad80' }} />
221
+ <span className="text-xs flex items-center gap-1.5 font-display" style={{ color: 'var(--success)' }}>
222
+ <span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--success)' }} />
218
223
  <span className="hidden sm:inline">saved</span>
219
224
  </span>
220
225
  )}
221
226
  {saveError && (
222
- <span className="text-xs text-red-400 hidden sm:inline">{saveError}</span>
227
+ <span className="text-xs text-error hidden sm:inline">{saveError}</span>
223
228
  )}
224
229
 
225
230
  {/* Graph toggle — only for md files, hidden when graph plugin is disabled */}
@@ -233,8 +238,8 @@ export default function ViewPageClient({
233
238
  }}
234
239
  title={effectiveGraphMode ? 'Switch to document view' : 'Switch to Wiki Graph'}
235
240
  >
236
- <span>🕸️</span>
237
- <span className="hidden sm:inline">Graph</span>
241
+ {effectiveGraphMode ? <FileText size={13} /> : <Share2 size={13} />}
242
+ <span className="hidden sm:inline">{effectiveGraphMode ? 'Doc' : 'Graph'}</span>
238
243
  </button>
239
244
  )}
240
245
 
@@ -244,12 +249,12 @@ export default function ViewPageClient({
244
249
  onClick={handleToggleRaw}
245
250
  className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
246
251
  style={{
247
- background: effectiveUseRaw ? 'var(--muted)' : `${'var(--amber)'}22`,
248
- color: effectiveUseRaw ? 'var(--muted-foreground)' : 'var(--amber)',
252
+ background: effectiveUseRaw ? `${'var(--amber)'}22` : 'var(--muted)',
253
+ color: effectiveUseRaw ? 'var(--amber)' : 'var(--muted-foreground)',
249
254
  }}
250
255
  title={effectiveUseRaw ? `Switch to ${registryRenderer?.name}` : 'View raw'}
251
256
  >
252
- <LayoutTemplate size={13} />
257
+ {effectiveUseRaw ? <LayoutTemplate size={13} /> : <Code size={13} />}
253
258
  <span className="hidden sm:inline">{effectiveUseRaw ? registryRenderer.name : 'Raw'}</span>
254
259
  </button>
255
260
  )}
@@ -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)} />
@@ -310,7 +310,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
310
310
  )}
311
311
 
312
312
  {upload.uploadError && (
313
- <div className="px-4 pb-1 text-xs text-red-400">{upload.uploadError}</div>
313
+ <div className="px-4 pb-1 text-xs text-error">{upload.uploadError}</div>
314
314
  )}
315
315
 
316
316
  {/* @-mention dropdown */}
@@ -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 {
@@ -65,7 +65,7 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
65
65
  className="
66
66
  flex-1 bg-muted border border-border rounded px-2 py-1
67
67
  text-xs text-foreground placeholder:text-muted-foreground
68
- focus:outline-none focus:border-blue-500/60
68
+ focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
69
69
  "
70
70
  />
71
71
  {isPending
@@ -80,7 +80,7 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
80
80
  )
81
81
  }
82
82
  </div>
83
- {error && <p className="text-xs text-red-400 mt-0.5 px-1">{error}</p>}
83
+ {error && <p className="text-xs text-error mt-0.5 px-1">{error}</p>}
84
84
  </div>
85
85
  );
86
86
  }
@@ -161,7 +161,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate }: {
161
161
  if (e.key === 'Escape') setRenaming(false);
162
162
  }}
163
163
  onBlur={commitRename}
164
- className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
164
+ className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
165
165
  />
166
166
  {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
167
167
  </div>
@@ -304,7 +304,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
304
304
  if (e.key === 'Escape') setRenaming(false);
305
305
  }}
306
306
  onBlur={commitRename}
307
- className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
307
+ className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
308
308
  />
309
309
  {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
310
310
  </div>
@@ -334,7 +334,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
334
334
  <button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
335
335
  <Pencil size={12} />
336
336
  </button>
337
- <button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-red-400 hover:bg-muted transition-colors" title={t.fileTree.delete}>
337
+ <button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-error hover:bg-muted transition-colors" title={t.fileTree.delete}>
338
338
  <Trash2 size={12} />
339
339
  </button>
340
340
  </div>
@@ -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
  ))}
@@ -278,7 +278,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
278
278
  className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-100 group-hover:translate-x-0.5 hover:bg-muted"
279
279
  >
280
280
  {isCSV
281
- ? <Table size={13} className="shrink-0" style={{ color: '#7aad80' }} />
281
+ ? <Table size={13} className="shrink-0" style={{ color: 'var(--success)' }} />
282
282
  : <FileText size={13} className="shrink-0" style={{ color: 'var(--muted-foreground)' }} />
283
283
  }
284
284
  <div className="flex-1 min-w-0">
@@ -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}