@geminilight/mindos 0.6.30 → 0.6.32

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 (52) hide show
  1. package/README_zh.md +10 -4
  2. package/app/app/api/ask/route.ts +12 -7
  3. package/app/app/api/export/route.ts +105 -0
  4. package/app/app/globals.css +2 -2
  5. package/app/app/trash/page.tsx +7 -0
  6. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  7. package/app/components/ExportModal.tsx +220 -0
  8. package/app/components/FileTree.tsx +22 -2
  9. package/app/components/HomeContent.tsx +91 -20
  10. package/app/components/MarkdownView.tsx +45 -10
  11. package/app/components/Sidebar.tsx +10 -1
  12. package/app/components/TrashPageClient.tsx +263 -0
  13. package/app/components/ask/ToolCallBlock.tsx +102 -18
  14. package/app/components/changes/ChangesContentPage.tsx +58 -14
  15. package/app/components/explore/ExploreContent.tsx +4 -7
  16. package/app/components/explore/UseCaseCard.tsx +18 -1
  17. package/app/components/explore/use-cases.generated.ts +76 -0
  18. package/app/components/explore/use-cases.yaml +185 -0
  19. package/app/components/panels/DiscoverPanel.tsx +1 -1
  20. package/app/components/renderers/workflow-yaml/StepEditor.tsx +98 -91
  21. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +72 -72
  22. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +175 -119
  23. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +61 -61
  24. package/app/components/renderers/workflow-yaml/execution.ts +64 -12
  25. package/app/components/renderers/workflow-yaml/selectors.tsx +65 -13
  26. package/app/components/settings/AiTab.tsx +191 -174
  27. package/app/components/settings/AppearanceTab.tsx +168 -77
  28. package/app/components/settings/KnowledgeTab.tsx +131 -136
  29. package/app/components/settings/McpTab.tsx +11 -11
  30. package/app/components/settings/Primitives.tsx +60 -0
  31. package/app/components/settings/SettingsContent.tsx +15 -8
  32. package/app/components/settings/SyncTab.tsx +12 -12
  33. package/app/components/settings/UninstallTab.tsx +8 -18
  34. package/app/components/settings/UpdateTab.tsx +82 -82
  35. package/app/components/settings/types.ts +17 -8
  36. package/app/lib/acp/session.ts +12 -3
  37. package/app/lib/actions.ts +57 -3
  38. package/app/lib/agent/stream-consumer.ts +18 -0
  39. package/app/lib/agent/tools.ts +56 -9
  40. package/app/lib/core/export.ts +116 -0
  41. package/app/lib/core/trash.ts +241 -0
  42. package/app/lib/fs.ts +47 -0
  43. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  44. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  45. package/app/lib/i18n/index.ts +3 -0
  46. package/app/lib/i18n/modules/knowledge.ts +120 -6
  47. package/app/lib/i18n/modules/onboarding.ts +2 -134
  48. package/app/lib/i18n/modules/settings.ts +12 -0
  49. package/app/package.json +8 -2
  50. package/app/scripts/generate-explore.ts +145 -0
  51. package/package.json +1 -1
  52. package/app/components/explore/use-cases.ts +0 -58
@@ -1,27 +1,41 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { ChevronDown, ChevronRight, Sun, Moon, Monitor, Type, Columns3, Globe } from 'lucide-react';
4
+ import { ChevronDown, ChevronRight, Sun, Moon, Monitor, Type, ALargeSmall, Columns3, Globe, BookOpen, Palette } from 'lucide-react';
5
5
  import { Locale } from '@/lib/i18n';
6
- import { CONTENT_WIDTHS, FONTS, AppearanceTabProps } from './types';
6
+ import { CONTENT_WIDTHS, FONTS, FONT_SIZES, AppearanceTabProps } from './types';
7
+ import { SettingCard } from './Primitives';
7
8
 
8
- /* ── Segmented Control ── */
9
- function SegmentedControl<T extends string>({ options, value, onChange }: {
9
+ /* ── Setting Group ── */
10
+ function SettingGroup({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) {
11
+ return (
12
+ <div className="space-y-3">
13
+ <div className="flex items-center gap-2">
14
+ <span className="text-muted-foreground">{icon}</span>
15
+ <span className="text-sm font-medium text-foreground">{label}</span>
16
+ </div>
17
+ {children}
18
+ </div>
19
+ );
20
+ }
21
+
22
+ /* ── Pill Selector — compact horizontal pills ── */
23
+ function PillSelector<T extends string>({ options, value, onChange }: {
10
24
  options: { value: T; label: string; icon?: React.ReactNode }[];
11
25
  value: T;
12
26
  onChange: (v: T) => void;
13
27
  }) {
14
28
  return (
15
- <div className="flex rounded-lg border border-border bg-muted/30 p-0.5 gap-0.5">
29
+ <div className="flex gap-2">
16
30
  {options.map(opt => (
17
31
  <button
18
32
  key={opt.value}
19
33
  type="button"
20
34
  onClick={() => onChange(opt.value)}
21
- className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all ${
35
+ className={`flex items-center gap-1.5 px-3.5 py-2 text-sm font-medium rounded-xl transition-all ${
22
36
  value === opt.value
23
- ? 'bg-card text-foreground shadow-sm'
24
- : 'text-muted-foreground hover:text-foreground'
37
+ ? 'bg-[var(--amber)] text-[var(--amber-foreground)] shadow-sm'
38
+ : 'bg-muted/50 text-muted-foreground hover:text-foreground hover:bg-muted'
25
39
  }`}
26
40
  >
27
41
  {opt.icon}
@@ -32,20 +46,7 @@ function SegmentedControl<T extends string>({ options, value, onChange }: {
32
46
  );
33
47
  }
34
48
 
35
- /* ── Setting Group ── */
36
- function SettingGroup({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) {
37
- return (
38
- <div className="space-y-2.5">
39
- <div className="flex items-center gap-2">
40
- <span className="text-muted-foreground">{icon}</span>
41
- <span className="text-xs font-medium text-foreground">{label}</span>
42
- </div>
43
- {children}
44
- </div>
45
- );
46
- }
47
-
48
- export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
49
+ export function AppearanceTab({ font, setFont, fontSize, setFontSize, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
49
50
  const [showShortcuts, setShowShortcuts] = useState(false);
50
51
  const [themePref, setThemePref] = useState<string>(() =>
51
52
  typeof window !== 'undefined' ? (localStorage.getItem('theme') ?? 'system') : 'system'
@@ -56,61 +57,150 @@ export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, da
56
57
  const a = t.settings.appearance;
57
58
 
58
59
  return (
59
- <div className="space-y-6">
60
- {/* Font */}
60
+ <div className="space-y-4">
61
+
62
+ {/* ── Card 1: Reading — font, size, width ── */}
63
+ <SettingCard icon={<BookOpen size={15} />} title={a.readingTitle ?? 'Reading'} description={a.readingDesc ?? 'Customize how your notes look'}>
64
+
65
+ {/* ── Font — each font previews itself ── */}
61
66
  <SettingGroup icon={<Type size={14} />} label={a.readingFont}>
62
- <div className="flex flex-wrap gap-1.5">
63
- {FONTS.map(f => (
64
- <button
65
- key={f.value}
66
- type="button"
67
- onClick={() => setFont(f.value)}
68
- className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
69
- font === f.value
70
- ? 'border-[var(--amber)] bg-[var(--amber-subtle)] text-foreground font-medium shadow-sm'
71
- : 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
72
- }`}
73
- style={{ fontFamily: f.style.fontFamily }}
74
- >
75
- {f.label}
76
- </button>
77
- ))}
67
+ <div className="space-y-0.5">
68
+ {FONTS.map(f => {
69
+ const selected = font === f.value;
70
+ return (
71
+ <button
72
+ key={f.value}
73
+ type="button"
74
+ onClick={() => setFont(f.value)}
75
+ className={`flex items-center gap-4 w-full px-3 py-2.5 rounded-lg transition-all text-left relative ${
76
+ selected
77
+ ? 'bg-[var(--amber-subtle)]'
78
+ : 'hover:bg-muted/50'
79
+ }`}
80
+ >
81
+ {/* Active indicator — left bar */}
82
+ {selected && (
83
+ <div className="absolute left-0 top-2 bottom-2 w-[3px] rounded-r-full bg-[var(--amber)]" />
84
+ )}
85
+ {/* Large Aa preview */}
86
+ <span
87
+ className={`text-xl font-medium w-10 text-center shrink-0 ${selected ? 'text-foreground' : 'text-muted-foreground/60'}`}
88
+ style={{ fontFamily: f.style.fontFamily }}
89
+ >
90
+ Aa
91
+ </span>
92
+ {/* Font name + sample */}
93
+ <div className="flex-1 min-w-0">
94
+ <div className="flex items-baseline gap-2">
95
+ <span
96
+ className={`text-sm font-medium ${selected ? 'text-foreground' : 'text-muted-foreground'}`}
97
+ style={{ fontFamily: f.style.fontFamily }}
98
+ >
99
+ {f.label}
100
+ </span>
101
+ <span className="text-xs text-muted-foreground/50">{f.category}</span>
102
+ </div>
103
+ <p
104
+ className={`text-sm truncate mt-0.5 ${selected ? 'text-muted-foreground' : 'text-muted-foreground/40'}`}
105
+ style={{ fontFamily: f.style.fontFamily }}
106
+ >
107
+ {a.fontPreview}
108
+ </p>
109
+ </div>
110
+ </button>
111
+ );
112
+ })}
78
113
  </div>
79
- <p
80
- className="text-sm text-muted-foreground leading-relaxed px-0.5 mt-1"
81
- style={{ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily }}
82
- >
83
- {a.fontPreview}
84
- </p>
85
114
  </SettingGroup>
86
115
 
87
- {/* Content Width */}
116
+ {/* ── Font Size — elegant range slider ── */}
117
+ <SettingGroup icon={<ALargeSmall size={14} />} label={a.fontSize}>
118
+ <div className="px-1">
119
+ {/* Slider */}
120
+ <div className="flex items-center gap-3">
121
+ <span className="text-xs text-muted-foreground/60 shrink-0" style={{ fontSize: '11px' }}>A</span>
122
+ <input
123
+ type="range"
124
+ min={14}
125
+ max={17}
126
+ step={1}
127
+ value={parseInt(fontSize)}
128
+ onChange={e => setFontSize(`${e.target.value}px`)}
129
+ className="flex-1 h-1.5 rounded-full appearance-none bg-muted cursor-pointer accent-[var(--amber)]
130
+ [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4
131
+ [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--amber)]
132
+ [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer
133
+ [&::-webkit-slider-thumb]:transition-transform [&::-webkit-slider-thumb]:hover:scale-110
134
+ [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full
135
+ [&::-moz-range-thumb]:bg-[var(--amber)] [&::-moz-range-thumb]:border-0
136
+ [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:cursor-pointer"
137
+ />
138
+ <span className="text-base text-muted-foreground/60 shrink-0">A</span>
139
+ </div>
140
+ {/* Current value */}
141
+ <div className="text-center mt-1.5">
142
+ <span className="text-xs tabular-nums text-muted-foreground">{parseInt(fontSize)}px</span>
143
+ </div>
144
+ {/* Live preview */}
145
+ <p
146
+ className="text-muted-foreground leading-relaxed mt-2 px-1"
147
+ style={{
148
+ fontSize,
149
+ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily,
150
+ }}
151
+ >
152
+ {a.fontSizePreview}
153
+ </p>
154
+ </div>
155
+ </SettingGroup>
156
+
157
+ {/* ── Content Width — visual width bars ── */}
88
158
  <SettingGroup icon={<Columns3 size={14} />} label={a.contentWidth}>
89
- <div className="flex flex-wrap gap-1.5">
90
- {CONTENT_WIDTHS.map(w => (
91
- <button
92
- key={w.value}
93
- type="button"
94
- onClick={() => setContentWidth(w.value)}
95
- className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
96
- contentWidth === w.value
97
- ? 'border-[var(--amber)] bg-[var(--amber-subtle)] text-foreground font-medium shadow-sm'
98
- : 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
99
- }`}
100
- >
101
- {w.label}
102
- </button>
103
- ))}
159
+ <div className="space-y-1">
160
+ {CONTENT_WIDTHS.map(w => {
161
+ const selected = contentWidth === w.value;
162
+ return (
163
+ <button
164
+ key={w.value}
165
+ type="button"
166
+ onClick={() => setContentWidth(w.value)}
167
+ className={`flex items-center gap-3 w-full px-3 py-2 rounded-lg transition-all ${
168
+ selected ? 'bg-[var(--amber-subtle)]' : 'hover:bg-muted/50'
169
+ }`}
170
+ >
171
+ {/* Width indicator bar */}
172
+ <div className="flex-1 h-2 rounded-full bg-muted/40 overflow-hidden">
173
+ <div
174
+ className={`h-full rounded-full transition-all ${
175
+ selected ? 'bg-[var(--amber)]' : 'bg-muted-foreground/20'
176
+ }`}
177
+ style={{ width: `${w.width}%` }}
178
+ />
179
+ </div>
180
+ {/* Label */}
181
+ <span className={`text-sm shrink-0 w-14 text-right ${
182
+ selected ? 'text-foreground font-medium' : 'text-muted-foreground'
183
+ }`}>
184
+ {w.label}
185
+ </span>
186
+ </button>
187
+ );
188
+ })}
104
189
  </div>
105
190
  </SettingGroup>
106
191
 
107
- {/* Theme */}
192
+ </SettingCard>
193
+
194
+ {/* ── Card 2: Preferences — theme, language ── */}
195
+ <SettingCard icon={<Palette size={15} />} title={a.preferencesTitle ?? 'Preferences'}>
196
+
197
+ {/* ── Theme — amber pills ── */}
108
198
  <SettingGroup icon={<Sun size={14} />} label={a.colorTheme}>
109
- <SegmentedControl
199
+ <PillSelector
110
200
  options={[
111
- { value: 'system', label: a.system, icon: <Monitor size={12} /> },
112
- { value: 'dark', label: a.dark, icon: <Moon size={12} /> },
113
- { value: 'light', label: a.light, icon: <Sun size={12} /> },
201
+ { value: 'system', label: a.system, icon: <Monitor size={14} /> },
202
+ { value: 'dark', label: a.dark, icon: <Moon size={14} /> },
203
+ { value: 'light', label: a.light, icon: <Sun size={14} /> },
114
204
  ]}
115
205
  value={themePref}
116
206
  onChange={v => {
@@ -125,12 +215,12 @@ export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, da
125
215
  />
126
216
  </SettingGroup>
127
217
 
128
- {/* Language */}
218
+ {/* ── Language — amber pills ── */}
129
219
  <SettingGroup icon={<Globe size={14} />} label={a.language}>
130
- <SegmentedControl
220
+ <PillSelector
131
221
  options={[
132
- { value: 'system', label: a.system, icon: <Monitor size={12} /> },
133
- { value: 'en', label: 'English' },
222
+ { value: 'system', label: a.system, icon: <Monitor size={14} /> },
223
+ { value: 'en', label: 'EN' },
134
224
  { value: 'zh', label: '中文' },
135
225
  ]}
136
226
  value={localePref}
@@ -141,29 +231,30 @@ export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, da
141
231
  ? (navigator.language.startsWith('zh') ? 'zh' : 'en')
142
232
  : v as Locale;
143
233
  setLocale(resolved);
144
- // Sync cookie for SSR (write resolved value, not 'system')
145
234
  document.cookie = `locale=${resolved};path=/;max-age=31536000;SameSite=Lax`;
146
235
  }}
147
236
  />
148
237
  </SettingGroup>
149
238
 
150
- <p className="text-xs text-muted-foreground/60 px-0.5">{a.browserNote}</p>
239
+ </SettingCard>
240
+
241
+ <p className="text-xs text-muted-foreground/40 px-0.5">{a.browserNote}</p>
151
242
 
152
- {/* Keyboard Shortcuts */}
243
+ {/* ── Keyboard Shortcuts ── */}
153
244
  <div className="border-t border-border pt-4">
154
245
  <button
155
246
  type="button"
156
247
  onClick={() => setShowShortcuts(!showShortcuts)}
157
- className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
248
+ className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
158
249
  >
159
- {showShortcuts ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
250
+ {showShortcuts ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
160
251
  {t.settings.tabs.shortcuts}
161
252
  </button>
162
253
  {showShortcuts && (
163
254
  <div className="mt-3 space-y-0.5">
164
255
  {t.shortcuts.map((s: { readonly description: string; readonly keys: readonly string[] }, i: number) => (
165
256
  <div key={i} className="flex items-center justify-between py-1.5 px-1">
166
- <span className="text-xs text-foreground">{s.description}</span>
257
+ <span className="text-sm text-foreground">{s.description}</span>
167
258
  <div className="flex items-center gap-1">
168
259
  {s.keys.map((k: string, j: number) => (
169
260
  <kbd key={j} className="px-1.5 py-0.5 text-2xs font-mono bg-muted border border-border rounded text-muted-foreground">{k}</kbd>
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from '
4
4
  import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive, RotateCcw } from 'lucide-react';
5
5
  import { toast } from '@/lib/toast';
6
6
  import type { KnowledgeTabProps } from './types';
7
- import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
7
+ import { Field, Input, EnvBadge, SectionLabel, Toggle, SettingCard, SettingRow } from './Primitives';
8
8
  import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
9
9
  import { apiFetch } from '@/lib/api';
10
10
  import { copyToClipboard } from '@/lib/clipboard';
@@ -135,164 +135,159 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
135
135
  }
136
136
 
137
137
  return (
138
- <div className="space-y-6">
139
- <SectionLabel>Knowledge Base</SectionLabel>
140
-
141
- <Field
142
- label={<>{k.sopRoot} <EnvBadge overridden={env.MIND_ROOT} /></>}
143
- hint={env.MIND_ROOT ? k.envNote : k.sopRootHint}
138
+ <div className="space-y-4">
139
+ {/* ── Card 1: Knowledge Base ── */}
140
+ <SettingCard
141
+ icon={<DatabaseIcon size={15} />}
142
+ title="Knowledge Base"
143
+ description={k.sopRootHint}
144
144
  >
145
- <Input
146
- value={data.mindRoot}
147
- onChange={e => setData(d => d ? { ...d, mindRoot: e.target.value } : d)}
148
- placeholder="/path/to/your/notes"
149
- />
150
- </Field>
151
-
152
- <div className="flex items-center justify-between">
153
- <div>
154
- <div className="text-sm text-foreground">{k.showHiddenFiles}</div>
155
- <div className="text-xs text-muted-foreground mt-0.5">{k.showHiddenFilesHint}</div>
156
- </div>
157
- <Toggle checked={showHidden} onChange={() => {
158
- const next = !showHidden;
159
- setShowHidden(next);
160
- setShowHiddenFiles(next);
161
- }} />
162
- </div>
163
-
164
- {exampleCount !== null && exampleCount > 0 && cleanupResult === null && (
165
- <div className="flex items-center justify-between">
166
- <div>
167
- <div className="text-sm text-foreground">{k.cleanupExamples}</div>
168
- <div className="text-xs text-muted-foreground mt-0.5">{k.cleanupExamplesHint}</div>
169
- </div>
170
- <button
171
- onClick={() => setShowCleanupConfirm(true)}
172
- disabled={cleaningUp}
173
- title={cleaningUp ? t.hints.cleanupInProgress : undefined}
174
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
175
- >
176
- {cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
177
- {k.cleanupExamplesButton}
178
- <span className="ml-1 tabular-nums text-2xs opacity-70">{exampleCount}</span>
179
- </button>
180
- </div>
181
- )}
182
- {cleanupResult !== null && (
183
- <div className="flex items-center gap-2 text-xs text-success">
184
- <Check size={14} />
185
- {k.cleanupExamplesDone(cleanupResult)}
186
- </div>
187
- )}
188
-
189
- <div className="border-t border-border pt-5">
190
- <SectionLabel>Security</SectionLabel>
191
- </div>
192
-
193
- <Field label={k.webPassword} hint={k.webPasswordHint}>
194
- <div className="flex gap-2">
145
+ <Field
146
+ label={<>{k.sopRoot} <EnvBadge overridden={env.MIND_ROOT} /></>}
147
+ hint={env.MIND_ROOT ? k.envNote : k.sopRootHint}
148
+ >
195
149
  <Input
196
- type={showPassword ? 'text' : 'password'}
197
- value={isPasswordMasked ? '••••••••' : (data.webPassword ?? '')}
198
- onChange={e => setData(d => d ? { ...d, webPassword: e.target.value } : d)}
199
- onFocus={() => { if (isPasswordMasked) setData(d => d ? { ...d, webPassword: '' } : d); }}
200
- placeholder="Leave empty to disable"
150
+ value={data.mindRoot}
151
+ onChange={e => setData(d => d ? { ...d, mindRoot: e.target.value } : d)}
152
+ placeholder="/path/to/your/notes"
201
153
  />
202
- <button
203
- type="button"
204
- onClick={() => setShowPassword(v => !v)}
205
- className="px-3 py-2 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
206
- >
207
- {showPassword ? 'Hide' : 'Show'}
208
- </button>
209
- </div>
210
- </Field>
154
+ </Field>
155
+
156
+ <SettingRow label={k.showHiddenFiles} hint={k.showHiddenFilesHint}>
157
+ <Toggle checked={showHidden} onChange={() => {
158
+ const next = !showHidden;
159
+ setShowHidden(next);
160
+ setShowHiddenFiles(next);
161
+ }} />
162
+ </SettingRow>
163
+
164
+ {exampleCount !== null && exampleCount > 0 && cleanupResult === null && (
165
+ <SettingRow label={k.cleanupExamples} hint={k.cleanupExamplesHint}>
166
+ <button
167
+ onClick={() => setShowCleanupConfirm(true)}
168
+ disabled={cleaningUp}
169
+ title={cleaningUp ? t.hints.cleanupInProgress : undefined}
170
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
171
+ >
172
+ {cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
173
+ {k.cleanupExamplesButton}
174
+ <span className="ml-1 tabular-nums text-2xs opacity-70">{exampleCount}</span>
175
+ </button>
176
+ </SettingRow>
177
+ )}
178
+ {cleanupResult !== null && (
179
+ <div className="flex items-center gap-2 text-xs text-success">
180
+ <Check size={14} />
181
+ {k.cleanupExamplesDone(cleanupResult)}
182
+ </div>
183
+ )}
184
+ </SettingCard>
211
185
 
212
- <Field
213
- label={k.authToken}
214
- hint={hasToken ? k.authTokenHint : k.authTokenNone}
186
+ {/* ── Card 2: Security ── */}
187
+ <SettingCard
188
+ icon={<HardDrive size={15} />}
189
+ title="Security"
215
190
  >
216
- <div className="space-y-2">
217
- {/* Token display */}
218
- <div className="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg min-h-[38px]">
219
- <code className="flex-1 text-xs font-mono text-foreground break-all select-all">
220
- {displayToken || <span className="text-muted-foreground italic">— not set —</span>}
221
- </code>
222
- {displayToken && (
223
- <button
224
- type="button"
225
- onClick={handleCopy}
226
- className="shrink-0 p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
227
- title={k.authTokenCopy}
228
- >
229
- <Copy size={13} />
230
- </button>
231
- )}
232
- </div>
233
- {/* MCP port info */}
234
- {data.mcpPort && (
235
- <p className="text-xs text-muted-foreground">
236
- {k.authTokenMcpPort}: <code className="font-mono">{data.mcpPort}</code>
237
- {displayToken && (
238
- <> &nbsp;·&nbsp; MCP URL: <code className="font-mono select-all">
239
- {`${origin}:${data.mcpPort}/mcp`}
240
- </code></>
241
- )}
242
- </p>
243
- )}
244
- {/* Action buttons */}
191
+ <Field label={k.webPassword} hint={k.webPasswordHint}>
245
192
  <div className="flex gap-2">
193
+ <Input
194
+ type={showPassword ? 'text' : 'password'}
195
+ value={isPasswordMasked ? '••••••••' : (data.webPassword ?? '')}
196
+ onChange={e => setData(d => d ? { ...d, webPassword: e.target.value } : d)}
197
+ onFocus={() => { if (isPasswordMasked) setData(d => d ? { ...d, webPassword: '' } : d); }}
198
+ placeholder="Leave empty to disable"
199
+ />
246
200
  <button
247
201
  type="button"
248
- onClick={handleResetToken}
249
- disabled={resetting}
250
- title={resetting ? t.hints.tokenResetInProgress : undefined}
251
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
202
+ onClick={() => setShowPassword(v => !v)}
203
+ className="px-3 py-2 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
252
204
  >
253
- <RefreshCw size={12} className={resetting ? 'animate-spin' : ''} />
254
- {k.authTokenReset}
205
+ {showPassword ? 'Hide' : 'Show'}
255
206
  </button>
256
- {hasToken && (
207
+ </div>
208
+ </Field>
209
+
210
+ <Field
211
+ label={k.authToken}
212
+ hint={hasToken ? k.authTokenHint : k.authTokenNone}
213
+ >
214
+ <div className="space-y-2">
215
+ {/* Token display */}
216
+ <div className="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg min-h-[38px]">
217
+ <code className="flex-1 text-xs font-mono text-foreground break-all select-all">
218
+ {displayToken || <span className="text-muted-foreground italic">— not set —</span>}
219
+ </code>
220
+ {displayToken && (
221
+ <button
222
+ type="button"
223
+ onClick={handleCopy}
224
+ className="shrink-0 p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
225
+ title={k.authTokenCopy}
226
+ >
227
+ <Copy size={13} />
228
+ </button>
229
+ )}
230
+ </div>
231
+ {/* MCP port info */}
232
+ {data.mcpPort && (
233
+ <p className="text-xs text-muted-foreground">
234
+ {k.authTokenMcpPort}: <code className="font-mono">{data.mcpPort}</code>
235
+ {displayToken && (
236
+ <> &nbsp;·&nbsp; MCP URL: <code className="font-mono select-all">
237
+ {`${origin}:${data.mcpPort}/mcp`}
238
+ </code></>
239
+ )}
240
+ </p>
241
+ )}
242
+ {/* Action buttons */}
243
+ <div className="flex gap-2">
257
244
  <button
258
245
  type="button"
259
- onClick={handleClearToken}
260
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-destructive hover:border-destructive/50 transition-colors"
246
+ onClick={handleResetToken}
247
+ disabled={resetting}
248
+ title={resetting ? t.hints.tokenResetInProgress : undefined}
249
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
261
250
  >
262
- <Trash2 size={12} />
263
- {k.authTokenClear}
251
+ <RefreshCw size={14} className={resetting ? 'animate-spin' : ''} />
252
+ {k.authTokenReset}
264
253
  </button>
254
+ {hasToken && (
255
+ <button
256
+ type="button"
257
+ onClick={handleClearToken}
258
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-destructive hover:border-destructive/50 transition-colors"
259
+ >
260
+ <Trash2 size={14} />
261
+ {k.authTokenClear}
262
+ </button>
263
+ )}
264
+ </div>
265
+ {revealedToken && (
266
+ <p className="text-xs text-[var(--amber-text)]">
267
+ New token generated. Copy it now — it won&apos;t be shown in full again.
268
+ </p>
265
269
  )}
266
270
  </div>
267
- {revealedToken && (
268
- <p className="text-xs text-[var(--amber-text)]">
269
- New token generated. Copy it now — it won&apos;t be shown in full again.
270
- </p>
271
- )}
272
- </div>
273
- </Field>
271
+ </Field>
272
+ </SettingCard>
274
273
 
275
- {/* Getting Started Guide toggle */}
274
+ {/* ── Card 3: Getting Started ── */}
276
275
  {guideActive !== null && (
277
- <div className="border-t border-border pt-5">
278
- <SectionLabel>{t.guide?.title ?? 'Getting Started'}</SectionLabel>
279
- <div className="flex items-center justify-between py-2">
280
- <div className="flex items-center gap-2">
281
- <Sparkles size={14} className="text-[var(--amber)]" />
282
- <div>
283
- <div className="text-sm text-foreground">{t.guide?.showGuide ?? 'Show getting started guide'}</div>
284
- </div>
285
- </div>
276
+ <SettingCard
277
+ icon={<Sparkles size={15} />}
278
+ title={t.guide?.title ?? 'Getting Started'}
279
+ >
280
+ <SettingRow label={t.guide?.showGuide ?? 'Show getting started guide'}>
286
281
  <Toggle checked={!guideDismissed} onChange={() => handleGuideToggle()} />
287
- </div>
282
+ </SettingRow>
288
283
  <button
289
284
  onClick={handleRestartWalkthrough}
290
- className="flex items-center gap-1.5 mt-2 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
285
+ className="flex items-center gap-1.5 mt-2 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
291
286
  >
292
- <RotateCcw size={12} />
287
+ <RotateCcw size={14} />
293
288
  {k.restartWalkthrough ?? 'Restart walkthrough'}
294
289
  </button>
295
- </div>
290
+ </SettingCard>
296
291
  )}
297
292
 
298
293
  {/* System Monitoring — collapsible */}
@@ -380,7 +375,7 @@ function MonitoringSection() {
380
375
  <div className="border-t border-border pt-5">
381
376
  <button
382
377
  onClick={() => setExpanded(v => !v)}
383
- className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors w-full"
378
+ className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors w-full"
384
379
  >
385
380
  {expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
386
381
  <Cpu size={12} />