@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
@@ -5,7 +5,6 @@ import {
5
5
  Plug, CheckCircle2, AlertCircle, Loader2, Copy, Check,
6
6
  ChevronDown, ChevronRight, Trash2, Plus, X,
7
7
  } from 'lucide-react';
8
- import { SectionLabel } from './Primitives';
9
8
  import { apiFetch } from '@/lib/api';
10
9
 
11
10
  /* ── Types ─────────────────────────────────────────────────────── */
@@ -95,8 +94,8 @@ function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
95
94
  <div className="space-y-1.5 text-sm pl-11">
96
95
  <div className="flex items-center gap-2">
97
96
  <span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.status ?? 'Status'}</span>
98
- <span className={`text-xs flex items-center gap-1 ${status.running ? 'text-green-500' : 'text-muted-foreground'}`}>
99
- <span className={`inline-block w-1.5 h-1.5 rounded-full ${status.running ? 'bg-green-500' : 'bg-muted-foreground'}`} />
97
+ <span className={`text-xs flex items-center gap-1 ${status.running ? 'text-success' : 'text-muted-foreground'}`}>
98
+ <span className={`inline-block w-1.5 h-1.5 rounded-full ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
100
99
  {status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
101
100
  </span>
102
101
  </div>
@@ -116,7 +115,7 @@ function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
116
115
  <span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.auth ?? 'Auth'}</span>
117
116
  <span className="text-xs">
118
117
  {status.authConfigured
119
- ? <span className="text-green-500">{m?.authSet ?? 'Token set'}</span>
118
+ ? <span className="text-success">{m?.authSet ?? 'Token set'}</span>
120
119
  : <span className="text-muted-foreground">{m?.authNotSet ?? 'No token'}</span>}
121
120
  </span>
122
121
  </div>
@@ -206,9 +205,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
206
205
  }));
207
206
 
208
207
  return (
209
- <div className="space-y-3">
210
- <SectionLabel>{m?.agentsTitle ?? 'Agent Configuration'}</SectionLabel>
211
-
208
+ <div className="space-y-3 pt-2">
212
209
  {/* Agent list */}
213
210
  <div className="space-y-1">
214
211
  {agents.map(agent => (
@@ -217,22 +214,23 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
217
214
  type="checkbox"
218
215
  checked={selected.has(agent.key)}
219
216
  onChange={() => toggle(agent.key)}
220
- className="rounded border-border accent-amber-500"
217
+ className="rounded border-border"
218
+ style={{ accentColor: 'var(--amber)' }}
221
219
  />
222
220
  <span className="w-28 shrink-0 text-xs">{agent.name}</span>
223
- <span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
221
+ <span className="text-2xs px-1.5 py-0.5 rounded font-mono"
224
222
  style={{ background: 'rgba(100,100,120,0.08)' }}>
225
223
  {getEffectiveTransport(agent)}
226
224
  </span>
227
225
  {agent.installed ? (
228
226
  <>
229
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-green-500/15 text-green-500 font-mono">
227
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-success/15 text-success font-mono">
230
228
  {agent.transport}
231
229
  </span>
232
- <span className="text-[10px] text-muted-foreground">{agent.scope}</span>
230
+ <span className="text-2xs text-muted-foreground">{agent.scope}</span>
233
231
  </>
234
232
  ) : (
235
- <span className="text-[10px] text-muted-foreground">
233
+ <span className="text-2xs text-muted-foreground">
236
234
  {agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
237
235
  </span>
238
236
  )}
@@ -241,7 +239,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
241
239
  <select
242
240
  value={scopes[agent.key] || 'project'}
243
241
  onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
244
- className="ml-auto text-[10px] px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
242
+ className="ml-auto text-2xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
245
243
  >
246
244
  <option value="project">{m?.project ?? 'Project'}</option>
247
245
  <option value="global">{m?.global ?? 'Global'}</option>
@@ -251,6 +249,23 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
251
249
  ))}
252
250
  </div>
253
251
 
252
+ {/* Select detected / Clear buttons */}
253
+ <div className="flex gap-2 text-xs pt-1">
254
+ <button type="button"
255
+ onClick={() => setSelected(new Set(
256
+ agents.filter(a => !a.installed && a.present).map(a => a.key)
257
+ ))}
258
+ className="px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
259
+ style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
260
+ {m?.selectDetected ?? 'Select Detected'}
261
+ </button>
262
+ <button type="button"
263
+ onClick={() => setSelected(new Set())}
264
+ className="px-2.5 py-1 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground">
265
+ {m?.clearSelection ?? 'Clear'}
266
+ </button>
267
+ </div>
268
+
254
269
  {/* Transport selector */}
255
270
  <div className="flex items-center gap-4 text-xs pt-1">
256
271
  <label className="flex items-center gap-1.5 cursor-pointer">
@@ -259,7 +274,8 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
259
274
  name="transport"
260
275
  checked={transport === 'auto'}
261
276
  onChange={() => setTransport('auto')}
262
- className="accent-amber-500"
277
+ className=""
278
+ style={{ accentColor: 'var(--amber)' }}
263
279
  />
264
280
  {m?.transportAuto ?? 'auto (recommended)'}
265
281
  </label>
@@ -269,7 +285,8 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
269
285
  name="transport"
270
286
  checked={transport === 'stdio'}
271
287
  onChange={() => setTransport('stdio')}
272
- className="accent-amber-500"
288
+ className=""
289
+ style={{ accentColor: 'var(--amber)' }}
273
290
  />
274
291
  {m?.transportStdio ?? 'stdio'}
275
292
  </label>
@@ -279,7 +296,8 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
279
296
  name="transport"
280
297
  checked={transport === 'http'}
281
298
  onChange={() => setTransport('http')}
282
- className="accent-amber-500"
299
+ className=""
300
+ style={{ accentColor: 'var(--amber)' }}
283
301
  />
284
302
  {m?.transportHttp ?? 'http'}
285
303
  </label>
@@ -294,7 +312,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
294
312
  type="text"
295
313
  value={httpUrl}
296
314
  onChange={e => setHttpUrl(e.target.value)}
297
- className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
315
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
298
316
  />
299
317
  </div>
300
318
  <div className="space-y-1">
@@ -304,7 +322,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
304
322
  value={httpToken}
305
323
  onChange={e => setHttpToken(e.target.value)}
306
324
  placeholder="Bearer token"
307
- className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
325
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
308
326
  />
309
327
  </div>
310
328
  </div>
@@ -315,7 +333,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
315
333
  onClick={handleInstall}
316
334
  disabled={selected.size === 0 || installing}
317
335
  className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
318
- style={{ background: 'var(--amber)', color: '#131210' }}
336
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
319
337
  >
320
338
  {installing && <Loader2 size={12} className="animate-spin" />}
321
339
  {installing ? (m?.installing ?? 'Installing...') : (m?.installSelected ?? 'Install Selected')}
@@ -325,7 +343,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
325
343
  {message && (
326
344
  <div className="flex items-center gap-1.5 text-xs" role="status">
327
345
  {message.type === 'success' ? (
328
- <><CheckCircle2 size={12} className="text-green-500" /><span className="text-green-500">{message.text}</span></>
346
+ <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{message.text}</span></>
329
347
  ) : (
330
348
  <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
331
349
  )}
@@ -414,9 +432,7 @@ function SkillsSection({ t }: { t: any }) {
414
432
  }
415
433
 
416
434
  return (
417
- <div className="space-y-3">
418
- <SectionLabel>{m?.skillsTitle ?? 'Skills'}</SectionLabel>
419
-
435
+ <div className="space-y-3 pt-2">
420
436
  {/* Skill language switcher */}
421
437
  {(() => {
422
438
  const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
@@ -468,7 +484,7 @@ function SkillsSection({ t }: { t: any }) {
468
484
  >
469
485
  {expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
470
486
  <span className="text-xs font-medium flex-1">{skill.name}</span>
471
- <span className={`text-[10px] px-1.5 py-0.5 rounded ${
487
+ <span className={`text-2xs px-1.5 py-0.5 rounded ${
472
488
  skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
473
489
  }`}>
474
490
  {skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
@@ -477,7 +493,7 @@ function SkillsSection({ t }: { t: any }) {
477
493
  <button
478
494
  onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }}
479
495
  className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
480
- skill.enabled ? 'bg-green-500' : 'bg-muted-foreground/30'
496
+ skill.enabled ? 'bg-success' : 'bg-muted-foreground/30'
481
497
  }`}
482
498
  >
483
499
  <span className={`inline-block h-3 w-3 rounded-full bg-white transition-transform ${
@@ -489,11 +505,11 @@ function SkillsSection({ t }: { t: any }) {
489
505
  {expanded === skill.name && (
490
506
  <div className="px-3 py-2 border-t border-border text-xs space-y-1.5 bg-muted/20">
491
507
  <p className="text-muted-foreground">{skill.description || 'No description'}</p>
492
- <p className="text-muted-foreground font-mono text-[10px]">{skill.path}</p>
508
+ <p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
493
509
  {skill.editable && (
494
510
  <button
495
511
  onClick={() => handleDelete(skill.name)}
496
- className="flex items-center gap-1 text-[10px] text-destructive hover:underline"
512
+ className="flex items-center gap-1 text-2xs text-destructive hover:underline"
497
513
  >
498
514
  <Trash2 size={10} />
499
515
  {m?.deleteSkill ?? 'Delete'}
@@ -514,37 +530,37 @@ function SkillsSection({ t }: { t: any }) {
514
530
  </button>
515
531
  </div>
516
532
  <div className="space-y-1">
517
- <label className="text-[10px] text-muted-foreground">{m?.skillName ?? 'Name'}</label>
533
+ <label className="text-2xs text-muted-foreground">{m?.skillName ?? 'Name'}</label>
518
534
  <input
519
535
  type="text"
520
536
  value={newName}
521
537
  onChange={e => setNewName(e.target.value.replace(/[^a-z0-9-]/g, ''))}
522
538
  placeholder="my-skill"
523
- className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
539
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
524
540
  />
525
541
  </div>
526
542
  <div className="space-y-1">
527
- <label className="text-[10px] text-muted-foreground">{m?.skillDesc ?? 'Description'}</label>
543
+ <label className="text-2xs text-muted-foreground">{m?.skillDesc ?? 'Description'}</label>
528
544
  <input
529
545
  type="text"
530
546
  value={newDesc}
531
547
  onChange={e => setNewDesc(e.target.value)}
532
548
  placeholder="What does this skill do?"
533
- className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring"
549
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
534
550
  />
535
551
  </div>
536
552
  <div className="space-y-1">
537
- <label className="text-[10px] text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
553
+ <label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
538
554
  <textarea
539
555
  value={newContent}
540
556
  onChange={e => setNewContent(e.target.value)}
541
557
  rows={6}
542
558
  placeholder="Skill instructions (markdown)..."
543
- className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring resize-y font-mono"
559
+ className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring resize-y font-mono"
544
560
  />
545
561
  </div>
546
562
  {error && (
547
- <p className="text-[10px] text-destructive flex items-center gap-1">
563
+ <p className="text-2xs text-destructive flex items-center gap-1">
548
564
  <AlertCircle size={10} />
549
565
  {error}
550
566
  </p>
@@ -554,7 +570,7 @@ function SkillsSection({ t }: { t: any }) {
554
570
  onClick={handleCreate}
555
571
  disabled={!newName.trim() || saving}
556
572
  className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
557
- style={{ background: 'var(--amber)', color: '#131210' }}
573
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
558
574
  >
559
575
  {saving && <Loader2 size={10} className="animate-spin" />}
560
576
  {m?.saveSkill ?? 'Save'}
@@ -586,6 +602,8 @@ export function McpTab({ t }: McpTabProps) {
586
602
  const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
587
603
  const [agents, setAgents] = useState<AgentInfo[]>([]);
588
604
  const [loading, setLoading] = useState(true);
605
+ const [showAgents, setShowAgents] = useState(false);
606
+ const [showSkills, setShowSkills] = useState(false);
589
607
 
590
608
  const fetchAll = useCallback(async () => {
591
609
  try {
@@ -609,22 +627,50 @@ export function McpTab({ t }: McpTabProps) {
609
627
  );
610
628
  }
611
629
 
630
+ const m = t.settings?.mcp;
631
+
612
632
  return (
613
633
  <div className="space-y-6">
614
- {/* MCP Server Status */}
615
- <ServerStatus status={mcpStatus} t={t} />
616
-
617
- {/* Divider */}
618
- <div className="border-t border-border" />
619
-
620
- {/* Agent Install */}
621
- <AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
634
+ {/* MCP Server Status — prominent card */}
635
+ <div className="rounded-xl border p-4" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
636
+ <ServerStatus status={mcpStatus} t={t} />
637
+ </div>
622
638
 
623
- {/* Divider */}
624
- <div className="border-t border-border" />
639
+ {/* Agent Install — collapsible */}
640
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
641
+ <button
642
+ type="button"
643
+ onClick={() => setShowAgents(!showAgents)}
644
+ className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
645
+ style={{ color: 'var(--foreground)' }}
646
+ >
647
+ <span>{m?.agentsTitle ?? 'Agent Configuration'}</span>
648
+ <ChevronDown size={14} className={`transition-transform text-muted-foreground ${showAgents ? 'rotate-180' : ''}`} />
649
+ </button>
650
+ {showAgents && (
651
+ <div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
652
+ <AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
653
+ </div>
654
+ )}
655
+ </div>
625
656
 
626
- {/* Skills */}
627
- <SkillsSection t={t} />
657
+ {/* Skills — collapsible */}
658
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
659
+ <button
660
+ type="button"
661
+ onClick={() => setShowSkills(!showSkills)}
662
+ className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
663
+ style={{ color: 'var(--foreground)' }}
664
+ >
665
+ <span>{m?.skillsTitle ?? 'Skills'}</span>
666
+ <ChevronDown size={14} className={`transition-transform text-muted-foreground ${showSkills ? 'rotate-180' : ''}`} />
667
+ </button>
668
+ {showSkills && (
669
+ <div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
670
+ <SkillsSection t={t} />
671
+ </div>
672
+ )}
673
+ </div>
628
674
  </div>
629
675
  );
630
676
  }
@@ -33,25 +33,25 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
33
33
  <div className="flex items-center gap-2 flex-wrap">
34
34
  <span className="text-sm font-medium text-foreground">{renderer.name}</span>
35
35
  {isCore && (
36
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-600/15 text-amber-600 font-mono">
36
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-amber-600/15 text-amber-600 font-mono">
37
37
  core
38
38
  </span>
39
39
  )}
40
40
  {renderer.builtin && !isCore && (
41
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono">
41
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono">
42
42
  {t.settings.plugins.builtinBadge}
43
43
  </span>
44
44
  )}
45
45
  <div className="flex gap-1 flex-wrap">
46
46
  {renderer.tags.map(tag => (
47
- <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded bg-muted/60 text-muted-foreground">
47
+ <span key={tag} className="text-2xs px-1.5 py-0.5 rounded bg-muted/60 text-muted-foreground">
48
48
  {tag}
49
49
  </span>
50
50
  ))}
51
51
  </div>
52
52
  </div>
53
53
  <p className="text-xs text-muted-foreground mt-1 leading-relaxed">{renderer.description}</p>
54
- <p className="text-[11px] text-muted-foreground/60 mt-1.5 font-mono">
54
+ <p className="text-xs text-muted-foreground/60 mt-1.5 font-mono">
55
55
  {t.settings.plugins.matchHint}: <code className="bg-muted px-1 rounded">{renderer.match.toString().match(/\/(.+)\//)?.[1] ?? '—'}</code>
56
56
  </p>
57
57
  </div>
@@ -18,7 +18,7 @@ export function Input({ className = '', ...props }: React.InputHTMLAttributes<HT
18
18
  return (
19
19
  <input
20
20
  {...props}
21
- 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 ${className}`}
21
+ 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 ${className}`}
22
22
  />
23
23
  );
24
24
  }
@@ -27,7 +27,7 @@ export function Select({ className = '', ...props }: React.SelectHTMLAttributes<
27
27
  return (
28
28
  <select
29
29
  {...props}
30
- className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 ${className}`}
30
+ className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 ${className}`}
31
31
  />
32
32
  );
33
33
  }
@@ -35,7 +35,7 @@ export function Select({ className = '', ...props }: React.SelectHTMLAttributes<
35
35
  export function EnvBadge({ overridden }: { overridden: boolean }) {
36
36
  if (!overridden) return null;
37
37
  return (
38
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-500 font-mono ml-1.5">env</span>
38
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-500 font-mono ml-1.5">env</span>
39
39
  );
40
40
  }
41
41
 
@@ -53,7 +53,7 @@ export function ApiKeyInput({ value, onChange, placeholder, disabled }: {
53
53
  onChange={e => onChange(e.target.value)}
54
54
  placeholder={placeholder ?? 'sk-...'}
55
55
  disabled={disabled}
56
- 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"
56
+ 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"
57
57
  onFocus={() => { if (isMasked) onChange(''); }}
58
58
  />
59
59
  );
@@ -104,19 +104,19 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
104
104
  value={remoteUrl}
105
105
  onChange={e => { setRemoteUrl(e.target.value); setError(''); }}
106
106
  placeholder="https://github.com/user/my-mind.git"
107
- className="w-full px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
107
+ className="w-full px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
108
108
  style={{
109
109
  borderColor: remoteUrl.trim() && !isValid ? 'var(--destructive, red)' : 'var(--border)',
110
110
  color: 'var(--foreground)',
111
111
  }}
112
112
  />
113
113
  {remoteUrl.trim() && !isValid && (
114
- <p className="text-[11px]" style={{ color: 'var(--destructive, red)' }}>
114
+ <p className="text-xs" style={{ color: 'var(--destructive, red)' }}>
115
115
  {syncT?.invalidUrl ?? 'Invalid Git URL — use HTTPS (https://...) or SSH (git@...)'}
116
116
  </p>
117
117
  )}
118
118
  {urlType === 'ssh' && (
119
- <p className="text-[11px] text-muted-foreground flex items-center gap-1">
119
+ <p className="text-xs text-muted-foreground flex items-center gap-1">
120
120
  <AlertCircle size={11} className="shrink-0" />
121
121
  {syncT?.sshHint ?? 'SSH URLs require SSH key configured on this machine. HTTPS with token recommended.'}
122
122
  </p>
@@ -136,7 +136,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
136
136
  value={token}
137
137
  onChange={e => setToken(e.target.value)}
138
138
  placeholder="ghp_xxxxxxxxxxxx"
139
- className="w-full px-3 py-2 pr-9 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
139
+ className="w-full px-3 py-2 pr-9 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
140
140
  style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}
141
141
  />
142
142
  <button
@@ -147,7 +147,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
147
147
  {showToken ? <EyeOff size={14} /> : <Eye size={14} />}
148
148
  </button>
149
149
  </div>
150
- <p className="text-[11px] text-muted-foreground">
150
+ <p className="text-xs text-muted-foreground">
151
151
  {syncT?.tokenHint ?? 'GitHub: Settings → Developer settings → Personal access tokens → repo scope'}
152
152
  </p>
153
153
  </div>
@@ -163,7 +163,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
163
163
  value={branch}
164
164
  onChange={e => setBranch(e.target.value)}
165
165
  placeholder="main"
166
- className="w-full max-w-[200px] px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
166
+ className="w-full max-w-[200px] px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
167
167
  style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}
168
168
  />
169
169
  </div>
@@ -174,7 +174,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
174
174
  onClick={handleConnect}
175
175
  disabled={!isValid || connecting}
176
176
  className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
177
- style={{ background: 'var(--amber)', color: '#131210' }}
177
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
178
178
  >
179
179
  {connecting && <Loader2 size={14} className="animate-spin" />}
180
180
  {connecting
@@ -184,14 +184,14 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
184
184
 
185
185
  {/* Error */}
186
186
  {error && (
187
- <div className="flex items-start gap-2 text-xs p-3 rounded-lg" role="alert" aria-live="polite" style={{ background: 'rgba(239,68,68,0.1)', color: 'var(--destructive, red)' }}>
187
+ <div className="flex items-start gap-2 text-xs p-3 rounded-lg" role="alert" aria-live="polite" style={{ background: 'rgba(200,80,80,0.1)', color: 'var(--error)' }}>
188
188
  <AlertCircle size={13} className="shrink-0 mt-0.5" />
189
189
  <span>{error}</span>
190
190
  </div>
191
191
  )}
192
192
 
193
193
  {/* Features */}
194
- <div className="grid grid-cols-2 gap-2 text-[11px] text-muted-foreground pt-2">
194
+ <div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground pt-2">
195
195
  {[
196
196
  syncT?.featureAutoCommit ?? 'Auto-commit on save',
197
197
  syncT?.featureAutoPull ?? 'Auto-pull from remote',
@@ -199,7 +199,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
199
199
  syncT?.featureMultiDevice ?? 'Works across devices',
200
200
  ].map((f, i) => (
201
201
  <div key={i} className="flex items-center gap-1.5">
202
- <CheckCircle2 size={11} className="text-green-500/60 shrink-0" />
202
+ <CheckCircle2 size={11} className="text-success/60 shrink-0" />
203
203
  <span>{f}</span>
204
204
  </div>
205
205
  ))}
@@ -336,7 +336,7 @@ export function SyncTab({ t }: SyncTabProps) {
336
336
  className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
337
337
  status.enabled
338
338
  ? 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
339
- : 'border-green-500/30 text-green-500 hover:bg-green-500/10'
339
+ : 'border-success/30 text-success hover:bg-success/10'
340
340
  }`}
341
341
  >
342
342
  {status.enabled ? 'Disable Auto-sync' : 'Enable Auto-sync'}
@@ -347,7 +347,7 @@ export function SyncTab({ t }: SyncTabProps) {
347
347
  {message && (
348
348
  <div className="flex items-center gap-1.5 text-xs" role="status" aria-live="polite">
349
349
  {message.type === 'success' ? (
350
- <><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{message.text}</span></>
350
+ <><CheckCircle2 size={13} className="text-success" /><span className="text-success">{message.text}</span></>
351
351
  ) : (
352
352
  <><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
353
353
  )}
@@ -361,7 +361,7 @@ export function SyncTab({ t }: SyncTabProps) {
361
361
  <div className="space-y-1.5">
362
362
  {conflicts.map((c, i) => (
363
363
  <div key={i} className="flex items-center gap-2 text-xs group">
364
- <AlertCircle size={12} className="text-red-500 shrink-0" />
364
+ <AlertCircle size={12} className="text-error shrink-0" />
365
365
  <a
366
366
  href={`/view/${encodeURIComponent(c.file)}`}
367
367
  className="font-mono truncate hover:text-foreground hover:underline transition-colors"
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { Brain, Zap, SkipForward, CheckCircle2 } from 'lucide-react';
4
+ import { Field, Input, ApiKeyInput } from '@/components/settings/Primitives';
5
+ import type { SetupState, SetupMessages } from './types';
6
+
7
+ export interface StepAIProps {
8
+ state: SetupState;
9
+ update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
10
+ s: SetupMessages;
11
+ }
12
+
13
+ export default function StepAI({ state, update, s }: StepAIProps) {
14
+ const providers = [
15
+ { id: 'anthropic' as const, icon: <Brain size={18} />, label: 'Anthropic', desc: 'Claude — claude-sonnet-4-6' },
16
+ { id: 'openai' as const, icon: <Zap size={18} />, label: 'OpenAI', desc: 'GPT or any OpenAI-compatible API' },
17
+ { id: 'skip' as const, icon: <SkipForward size={18} />, label: s.aiSkipTitle, desc: s.aiSkipDesc },
18
+ ];
19
+ return (
20
+ <div className="space-y-5">
21
+ <div className="grid grid-cols-1 gap-3">
22
+ {providers.map(p => (
23
+ <button key={p.id} onClick={() => update('provider', p.id)}
24
+ className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
25
+ style={{
26
+ background: state.provider === p.id ? 'var(--amber-dim)' : 'var(--card)',
27
+ borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
28
+ }}>
29
+ <span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
30
+ {p.icon}
31
+ </span>
32
+ <div>
33
+ <p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
34
+ <p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
35
+ </div>
36
+ {state.provider === p.id && (
37
+ <CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
38
+ )}
39
+ </button>
40
+ ))}
41
+ </div>
42
+ {state.provider !== 'skip' && (
43
+ <div className="space-y-4 pt-2">
44
+ <Field label={s.apiKey}>
45
+ <ApiKeyInput
46
+ value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
47
+ onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
48
+ placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
49
+ />
50
+ </Field>
51
+ <Field label={s.model}>
52
+ <Input
53
+ value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
54
+ onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
55
+ />
56
+ </Field>
57
+ {state.provider === 'openai' && (
58
+ <Field label={s.baseUrl} hint={s.baseUrlHint}>
59
+ <Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
60
+ placeholder="https://api.openai.com/v1" />
61
+ </Field>
62
+ )}
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }