@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
@@ -135,7 +135,7 @@ function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
135
135
  if (!status) return null;
136
136
  return (
137
137
  <div className="rounded-xl border border-border bg-card p-4 flex items-center justify-between">
138
- <div className="flex items-center gap-2.5 text-xs">
138
+ <div className="flex items-center gap-2.5 text-sm">
139
139
  {restarting ? (
140
140
  <>
141
141
  <Loader2 size={12} className="animate-spin text-[var(--amber)]" />
@@ -161,13 +161,13 @@ function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
161
161
  <div className="flex items-center gap-2">
162
162
  {!status.running && !restarting && (
163
163
  <button onClick={onRestart}
164
- className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors">
165
- <RotateCcw size={12} /> {m?.restart ?? 'Restart'}
164
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors">
165
+ <RotateCcw size={14} /> {m?.restart ?? 'Restart'}
166
166
  </button>
167
167
  )}
168
168
  <button onClick={onRefresh}
169
169
  className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
170
- <RefreshCw size={12} />
170
+ <RefreshCw size={14} />
171
171
  </button>
172
172
  </div>
173
173
  </div>
@@ -257,19 +257,19 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
257
257
  <div className="flex items-center rounded-lg border border-border overflow-hidden w-fit">
258
258
  <button
259
259
  onClick={() => onTransportChange('stdio')}
260
- className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
260
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm transition-colors ${
261
261
  transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
262
262
  }`}
263
263
  >
264
- <Monitor size={12} /> {m?.transportLocal ?? 'Local (stdio)'}
264
+ <Monitor size={14} /> {m?.transportLocal ?? 'Local (stdio)'}
265
265
  </button>
266
266
  <button
267
267
  onClick={() => onTransportChange('http')}
268
- className={`flex items-center gap-1.5 px-3 py-1.5 text-xs transition-colors ${
268
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm transition-colors ${
269
269
  transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
270
270
  }`}
271
271
  >
272
- <Globe size={12} /> {m?.transportRemote ?? 'Remote (HTTP)'}
272
+ <Globe size={14} /> {m?.transportRemote ?? 'Remote (HTTP)'}
273
273
  </button>
274
274
  </div>
275
275
 
@@ -287,10 +287,10 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
287
287
  <pre className="text-[11px] font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre select-all max-h-[240px] overflow-y-auto">
288
288
  {snippet.displaySnippet}
289
289
  </pre>
290
- <div className="flex items-center gap-3 text-xs">
290
+ <div className="flex items-center gap-3 text-sm">
291
291
  <button onClick={() => onCopy(snippet.snippet)}
292
- className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
293
- <Copy size={12} />
292
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
293
+ <Copy size={14} />
294
294
  {m?.copyConfig ?? 'Copy config'}
295
295
  </button>
296
296
  <span className="text-muted-foreground">→</span>
@@ -238,3 +238,63 @@ export function PrimaryButton({ children, disabled, onClick, type = 'button', cl
238
238
  </button>
239
239
  );
240
240
  }
241
+
242
+ /**
243
+ * SettingCard — groups related settings into a visually distinct card.
244
+ * Provides the "breathing room" and grouping that raw Field stacks lack.
245
+ *
246
+ * Usage:
247
+ * <SettingCard icon={<Sparkles size={15} />} title="AI Provider" description="Choose your model">
248
+ * <Field label="Model"> ... </Field>
249
+ * </SettingCard>
250
+ */
251
+ export function SettingCard({ icon, title, description, badge, children, className = '' }: {
252
+ icon: React.ReactNode;
253
+ title: string;
254
+ description?: string;
255
+ badge?: React.ReactNode;
256
+ children: React.ReactNode;
257
+ className?: string;
258
+ }) {
259
+ return (
260
+ <div className={`rounded-xl border border-border/50 bg-card/50 p-5 ${className}`}>
261
+ <div className="flex items-start gap-3 mb-4">
262
+ <div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0 mt-0.5">
263
+ <span className="text-muted-foreground">{icon}</span>
264
+ </div>
265
+ <div className="flex-1 min-w-0">
266
+ <div className="flex items-center gap-2">
267
+ <h3 className="text-sm font-semibold text-foreground">{title}</h3>
268
+ {badge}
269
+ </div>
270
+ {description && (
271
+ <p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{description}</p>
272
+ )}
273
+ </div>
274
+ </div>
275
+ <div className="space-y-4 pl-11">
276
+ {children}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ /**
283
+ * SettingRow — inline label + control on one line.
284
+ * Replaces verbose Field + vertical stacking for simple toggle/select rows.
285
+ */
286
+ export function SettingRow({ label, hint, children }: {
287
+ label: string;
288
+ hint?: string;
289
+ children: React.ReactNode;
290
+ }) {
291
+ return (
292
+ <div className="flex items-center justify-between gap-4">
293
+ <div className="flex-1 min-w-0">
294
+ <div className="text-sm text-foreground">{label}</div>
295
+ {hint && <div className="text-xs text-muted-foreground mt-0.5">{hint}</div>}
296
+ </div>
297
+ <div className="shrink-0">{children}</div>
298
+ </div>
299
+ );
300
+ }
@@ -30,6 +30,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
30
30
  const dataLoaded = useRef(false);
31
31
 
32
32
  const [font, setFont] = useState('lora');
33
+ const [fontSize, setFontSize] = useState('15px');
33
34
  const [contentWidth, setContentWidth] = useState('780px');
34
35
  const [dark, setDark] = useState(true);
35
36
 
@@ -63,6 +64,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
63
64
  if (justOpened) {
64
65
  apiFetch<SettingsData>('/api/settings').then(d => { setData(d); dataLoaded.current = true; }).catch(() => setStatus('load-error'));
65
66
  setFont(localStorage.getItem('prose-font') ?? 'lora');
67
+ setFontSize(localStorage.getItem('prose-font-size') ?? '15px');
66
68
  setContentWidth(localStorage.getItem('content-width') ?? '780px');
67
69
  const stored = localStorage.getItem('theme');
68
70
  setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
@@ -87,6 +89,11 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
87
89
  localStorage.setItem('prose-font', font);
88
90
  }, [font]);
89
91
 
92
+ useEffect(() => {
93
+ document.documentElement.style.setProperty('--prose-font-size-override', fontSize);
94
+ localStorage.setItem('prose-font-size', fontSize);
95
+ }, [fontSize]);
96
+
90
97
  useEffect(() => {
91
98
  document.documentElement.style.setProperty('--content-width-override', contentWidth);
92
99
  localStorage.setItem('content-width', contentWidth);
@@ -178,7 +185,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
178
185
  ) : (
179
186
  <>
180
187
  {tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent} t={t} />}
181
- {tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
188
+ {tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} fontSize={fontSize} setFontSize={setFontSize} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
182
189
  {tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
183
190
  {tab === 'sync' && <SyncTab t={t} />}
184
191
  {tab === 'mcp' && <McpTab t={t} />}
@@ -197,18 +204,18 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
197
204
  <button
198
205
  onClick={restoreFromEnv}
199
206
  disabled={saving || !data}
200
- className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : 'px-3 py-1 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`}
207
+ className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-xs rounded-md' : '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`}
201
208
  >
202
- <RotateCcw size={isPanel ? 11 : 12} />
209
+ <RotateCcw size={isPanel ? 12 : 13} />
203
210
  {t.settings.ai.restoreFromEnv}
204
211
  </button>
205
212
  )}
206
213
  {tab === 'knowledge' && (
207
214
  <a
208
215
  href="/setup?force=1"
209
- className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : 'px-3 py-1 text-xs rounded-lg'} border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors`}
216
+ className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-xs rounded-md' : 'px-3 py-1.5 text-sm rounded-lg'} border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors`}
210
217
  >
211
- <RotateCcw size={isPanel ? 11 : 12} />
218
+ <RotateCcw size={isPanel ? 12 : 13} />
212
219
  {t.settings.reconfigure}
213
220
  </a>
214
221
  )}
@@ -246,7 +253,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
246
253
  <button
247
254
  key={tabItem.id}
248
255
  onClick={() => setTab(tabItem.id)}
249
- className={`flex items-center gap-1 px-2 py-2 text-[11px] font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
256
+ className={`flex items-center gap-1 px-2 py-2 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
250
257
  tab === tabItem.id
251
258
  ? 'border-[var(--amber)] text-foreground'
252
259
  : 'border-transparent text-muted-foreground hover:text-foreground'
@@ -297,7 +304,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
297
304
  <button
298
305
  key={tabItem.id}
299
306
  onClick={() => setTab(tabItem.id)}
300
- className={`flex items-center gap-1.5 px-3 py-2.5 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
307
+ className={`flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
301
308
  tab === tabItem.id
302
309
  ? 'border-[var(--amber)] text-foreground'
303
310
  : 'border-transparent text-muted-foreground hover:text-foreground'
@@ -326,7 +333,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
326
333
  <button
327
334
  key={tabItem.id}
328
335
  onClick={() => setTab(tabItem.id)}
329
- className={`flex items-center gap-2 w-full px-4 py-2 text-xs font-medium transition-colors relative ${
336
+ className={`flex items-center gap-2 w-full px-4 py-2 text-sm font-medium transition-colors relative ${
330
337
  tab === tabItem.id
331
338
  ? 'text-foreground bg-muted'
332
339
  : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
@@ -82,7 +82,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: Messages; onInitComplete: ()
82
82
 
83
83
  {/* Git Remote URL */}
84
84
  <div className="space-y-1.5">
85
- <label className="text-xs font-medium text-foreground block">
85
+ <label className="text-sm font-medium text-foreground block">
86
86
  {syncT?.remoteUrl ?? 'Git Remote URL'}
87
87
  </label>
88
88
  <Input
@@ -108,7 +108,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: Messages; onInitComplete: ()
108
108
  {/* Access Token (HTTPS only) */}
109
109
  {showTokenField && (
110
110
  <div className="space-y-1.5">
111
- <label className="text-xs font-medium text-foreground block">
111
+ <label className="text-sm font-medium text-foreground block">
112
112
  {syncT?.accessToken ?? 'Access Token'}{' '}
113
113
  <span className="text-muted-foreground font-normal">{syncT?.optional ?? '(optional, for private repos)'}</span>
114
114
  </label>
@@ -136,7 +136,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: Messages; onInitComplete: ()
136
136
 
137
137
  {/* Branch */}
138
138
  <div className="space-y-1.5">
139
- <label className="text-xs font-medium text-foreground block">
139
+ <label className="text-sm font-medium text-foreground block">
140
140
  {syncT?.branchLabel ?? 'Branch'}
141
141
  </label>
142
142
  <Input
@@ -267,30 +267,30 @@ export function SyncTab({ t }: SyncTabProps) {
267
267
  <SectionLabel>Sync</SectionLabel>
268
268
 
269
269
  {/* Status overview */}
270
- <div className="space-y-2 text-sm">
270
+ <div className="space-y-2.5 text-sm">
271
271
  <div className="flex items-center gap-2">
272
272
  <span className="text-muted-foreground w-24 shrink-0">Provider</span>
273
- <span className="font-mono text-xs">{status.provider}</span>
273
+ <span className="font-mono text-sm">{status.provider}</span>
274
274
  </div>
275
275
  <div className="flex items-center gap-2">
276
276
  <span className="text-muted-foreground w-24 shrink-0">Remote</span>
277
- <span className="font-mono text-xs truncate" title={status.remote}>{status.remote}</span>
277
+ <span className="font-mono text-sm truncate" title={status.remote}>{status.remote}</span>
278
278
  </div>
279
279
  <div className="flex items-center gap-2">
280
280
  <span className="text-muted-foreground w-24 shrink-0">Branch</span>
281
- <span className="font-mono text-xs">{status.branch}</span>
281
+ <span className="font-mono text-sm">{status.branch}</span>
282
282
  </div>
283
283
  <div className="flex items-center gap-2">
284
284
  <span className="text-muted-foreground w-24 shrink-0">Last sync</span>
285
- <span className="text-xs">{timeAgo(status.lastSync)}</span>
285
+ <span className="text-sm">{timeAgo(status.lastSync)}</span>
286
286
  </div>
287
287
  <div className="flex items-center gap-2">
288
288
  <span className="text-muted-foreground w-24 shrink-0">Unpushed</span>
289
- <span className="text-xs">{status.unpushed} commits</span>
289
+ <span className="text-sm">{status.unpushed} commits</span>
290
290
  </div>
291
291
  <div className="flex items-center gap-2">
292
292
  <span className="text-muted-foreground w-24 shrink-0">Auto-sync</span>
293
- <span className="text-xs">
293
+ <span className="text-sm">
294
294
  commit: {status.autoCommitInterval}s, pull: {Math.floor((status.autoPullInterval || 300) / 60)}min
295
295
  </span>
296
296
  </div>
@@ -303,7 +303,7 @@ export function SyncTab({ t }: SyncTabProps) {
303
303
  onClick={handleSyncNow}
304
304
  disabled={syncing}
305
305
  title={syncing ? t.hints.syncInProgress : undefined}
306
- 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"
306
+ 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"
307
307
  >
308
308
  <RefreshCw size={12} className={syncing ? 'animate-spin' : ''} />
309
309
  Sync Now
@@ -313,7 +313,7 @@ export function SyncTab({ t }: SyncTabProps) {
313
313
  onClick={handleToggle}
314
314
  disabled={toggling}
315
315
  title={toggling ? t.hints.toggleInProgress : undefined}
316
- 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 ${
316
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
317
317
  status.enabled
318
318
  ? 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
319
319
  : 'border-success/30 text-success hover:bg-success/10'
@@ -4,6 +4,7 @@ import { useState } from 'react';
4
4
  import { Trash2, AlertTriangle, CheckCircle2, Loader2, ShieldCheck } from 'lucide-react';
5
5
  import { apiFetch } from '@/lib/api';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
+ import { SettingCard } from './Primitives';
7
8
 
8
9
  type Phase = 'idle' | 'confirming' | 'running' | 'success' | 'error';
9
10
 
@@ -73,25 +74,14 @@ export function UninstallTab() {
73
74
  className="mt-0.5 form-check"
74
75
  />
75
76
  <div>
76
- <p className="text-xs font-medium text-foreground">{label}</p>
77
- <p className="text-[11px] text-muted-foreground">{desc}</p>
77
+ <p className="text-sm font-medium text-foreground">{label}</p>
78
+ <p className="text-xs text-muted-foreground">{desc}</p>
78
79
  </div>
79
80
  </label>
80
81
  );
81
82
 
82
83
  return (
83
- <div className="space-y-5">
84
- {/* Header */}
85
- <div>
86
- <h3 className="text-sm font-medium text-foreground flex items-center gap-2">
87
- <Trash2 size={14} className="text-muted-foreground" />
88
- {u.title}
89
- </h3>
90
- <p className="text-xs text-muted-foreground mt-1">
91
- {isDesktop ? u.descDesktop : u.descCli}
92
- </p>
93
- </div>
94
-
84
+ <SettingCard icon={<Trash2 size={15} />} title={u.title} description={isDesktop ? u.descDesktop : u.descCli}>
95
85
  {/* Knowledge base safety note */}
96
86
  <div className="flex gap-2.5 p-3 rounded-md bg-muted/50 border border-border">
97
87
  <ShieldCheck size={14} className="text-success shrink-0 mt-0.5" />
@@ -116,7 +106,7 @@ export function UninstallTab() {
116
106
  {phase === 'idle' && (
117
107
  <button
118
108
  onClick={() => setPhase('confirming')}
119
- className="px-3 py-1.5 text-xs font-medium rounded-md bg-error/10 text-error border border-error/20 hover:bg-error/20 transition-colors focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-40 disabled:cursor-not-allowed"
109
+ className="px-3.5 py-2 text-sm font-medium rounded-lg bg-error/10 text-error border border-error/20 hover:bg-error/20 transition-colors focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-40 disabled:cursor-not-allowed"
120
110
  >
121
111
  <Trash2 size={12} className="inline mr-1.5 -mt-px" />
122
112
  {u.confirmButton}
@@ -129,13 +119,13 @@ export function UninstallTab() {
129
119
  <div className="flex gap-2">
130
120
  <button
131
121
  onClick={handleUninstall}
132
- className="px-3 py-1.5 text-xs font-medium rounded-md bg-error text-white hover:bg-error/90 transition-colors focus-visible:ring-1 focus-visible:ring-ring"
122
+ className="px-3.5 py-2 text-sm font-medium rounded-lg bg-error text-white hover:bg-error/90 transition-colors focus-visible:ring-1 focus-visible:ring-ring"
133
123
  >
134
124
  {u.confirmButton}
135
125
  </button>
136
126
  <button
137
127
  onClick={() => setPhase('idle')}
138
- className="px-3 py-1.5 text-xs font-medium rounded-md bg-muted text-foreground hover:bg-muted/80 transition-colors focus-visible:ring-1 focus-visible:ring-ring"
128
+ className="px-3.5 py-2 text-sm font-medium rounded-lg bg-muted text-foreground hover:bg-muted/80 transition-colors focus-visible:ring-1 focus-visible:ring-ring"
139
129
  >
140
130
  {u.cancelButton}
141
131
  </button>
@@ -174,6 +164,6 @@ export function UninstallTab() {
174
164
  </button>
175
165
  </div>
176
166
  )}
177
- </div>
167
+ </SettingCard>
178
168
  );
179
169
  }
@@ -178,59 +178,60 @@ function DesktopUpdateTab() {
178
178
  {errorMsg}
179
179
  </div>
180
180
  )}
181
- </div>
182
-
183
- <div className="flex items-center gap-2">
184
- <button
185
- onClick={handleCheck}
186
- disabled={state === 'checking' || state === 'downloading'}
187
- 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"
188
- >
189
- <RefreshCw size={12} className={state === 'checking' ? 'animate-spin' : ''} />
190
- {u?.checkButton ?? 'Check for Updates'}
191
- </button>
192
-
193
- {state === 'idle' && available && (
181
+ {/* Actions */}
182
+ <div className="flex items-center gap-2 pt-1">
194
183
  <button
195
- onClick={handleInstall}
196
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
184
+ onClick={handleCheck}
185
+ disabled={state === 'checking' || state === 'downloading'}
186
+ 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"
197
187
  >
198
- <Download size={12} />
199
- {version ? `Update to v${version}` : 'Update'}
188
+ <RefreshCw size={14} className={state === 'checking' ? 'animate-spin' : ''} />
189
+ {u?.checkButton ?? 'Check for Updates'}
200
190
  </button>
201
- )}
202
191
 
203
- {state === 'ready' && (
204
- <button
205
- onClick={async () => {
206
- try {
207
- await bridge.installUpdate();
208
- } catch {
209
- setState('error');
210
- setErrorMsg(u?.error ?? 'Failed to install update. Please try again.');
211
- }
212
- }}
213
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
214
- >
215
- <RefreshCw size={12} />
216
- {u?.desktopRestart ?? 'Restart Now'}
217
- </button>
218
- )}
219
- </div>
192
+ {state === 'idle' && available && (
193
+ <button
194
+ onClick={handleInstall}
195
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
196
+ >
197
+ <Download size={14} />
198
+ {version ? `Update to v${version}` : 'Update'}
199
+ </button>
200
+ )}
220
201
 
221
- <div className="border-t border-border pt-4 space-y-2">
222
- <a
223
- href={CHANGELOG_URL}
224
- target="_blank"
225
- rel="noopener noreferrer"
226
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
227
- >
228
- <ExternalLink size={12} />
229
- {u?.releaseNotes ?? 'View release notes'}
230
- </a>
231
- <p className="text-2xs text-muted-foreground/60">
232
- {u?.desktopHint ?? 'Updates are delivered through the Desktop app auto-updater.'}
233
- </p>
202
+ {state === 'ready' && (
203
+ <button
204
+ onClick={async () => {
205
+ try {
206
+ await bridge.installUpdate();
207
+ } catch {
208
+ setState('error');
209
+ setErrorMsg(u?.error ?? 'Failed to install update. Please try again.');
210
+ }
211
+ }}
212
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
213
+ >
214
+ <RefreshCw size={14} />
215
+ {u?.desktopRestart ?? 'Restart Now'}
216
+ </button>
217
+ )}
218
+ </div>
219
+
220
+ {/* Info */}
221
+ <div className="border-t border-border/50 pt-3 space-y-2">
222
+ <a
223
+ href={CHANGELOG_URL}
224
+ target="_blank"
225
+ rel="noopener noreferrer"
226
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
227
+ >
228
+ <ExternalLink size={12} />
229
+ {u?.releaseNotes ?? 'View release notes'}
230
+ </a>
231
+ <p className="text-2xs text-muted-foreground/60">
232
+ {u?.desktopHint ?? 'Updates are delivered through the Desktop app auto-updater.'}
233
+ </p>
234
+ </div>
234
235
  </div>
235
236
  </div>
236
237
  );
@@ -527,44 +528,43 @@ function BrowserUpdateTab() {
527
528
  )}
528
529
  </div>
529
530
  )}
530
- </div>
531
-
532
- {/* Actions */}
533
- <div className="flex items-center gap-2">
534
- <button
535
- onClick={checkUpdate}
536
- disabled={state === 'checking' || state === 'updating'}
537
- 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"
538
- >
539
- <RefreshCw size={12} className={state === 'checking' ? 'animate-spin' : ''} />
540
- {u?.checkButton ?? 'Check for Updates'}
541
- </button>
542
-
543
- {info?.hasUpdate && state !== 'updating' && state !== 'updated' && (
531
+ {/* Actions */}
532
+ <div className="flex items-center gap-2 pt-1">
544
533
  <button
545
- onClick={handleUpdate}
546
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
534
+ onClick={checkUpdate}
535
+ disabled={state === 'checking' || state === 'updating'}
536
+ 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"
547
537
  >
548
- <Download size={12} />
549
- {u?.updateButton ? u.updateButton(info.latest) : `Update to v${info.latest}`}
538
+ <RefreshCw size={14} className={state === 'checking' ? 'animate-spin' : ''} />
539
+ {u?.checkButton ?? 'Check for Updates'}
550
540
  </button>
551
- )}
552
- </div>
553
541
 
554
- {/* Info */}
555
- <div className="border-t border-border pt-4 space-y-2">
556
- <a
557
- href={CHANGELOG_URL}
558
- target="_blank"
559
- rel="noopener noreferrer"
560
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
561
- >
562
- <ExternalLink size={12} />
563
- {u?.releaseNotes ?? 'View release notes'}
564
- </a>
565
- <p className="text-2xs text-muted-foreground/60">
566
- {u?.hint ?? 'Updates are installed via npm. Equivalent to running'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos update</code> {u?.inTerminal ?? 'in your terminal.'}
567
- </p>
542
+ {info?.hasUpdate && state !== 'updating' && state !== 'updated' && (
543
+ <button
544
+ onClick={handleUpdate}
545
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg font-medium text-[var(--amber-foreground)] bg-[var(--amber)] transition-colors"
546
+ >
547
+ <Download size={14} />
548
+ {u?.updateButton ? u.updateButton(info.latest) : `Update to v${info.latest}`}
549
+ </button>
550
+ )}
551
+ </div>
552
+
553
+ {/* Info */}
554
+ <div className="border-t border-border/50 pt-3 space-y-2">
555
+ <a
556
+ href={CHANGELOG_URL}
557
+ target="_blank"
558
+ rel="noopener noreferrer"
559
+ className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
560
+ >
561
+ <ExternalLink size={12} />
562
+ {u?.releaseNotes ?? 'View release notes'}
563
+ </a>
564
+ <p className="text-2xs text-muted-foreground/60">
565
+ {u?.hint ?? 'Updates are installed via npm. Equivalent to running'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos update</code> {u?.inTerminal ?? 'in your terminal.'}
566
+ </p>
567
+ </div>
568
568
  </div>
569
569
  </div>
570
570
  );
@@ -37,17 +37,24 @@ export interface SettingsData {
37
37
  export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync' | 'update' | 'uninstall';
38
38
 
39
39
  export const CONTENT_WIDTHS = [
40
- { value: '680px', label: 'Narrow (680px)' },
41
- { value: '780px', label: 'Default (780px)' },
42
- { value: '960px', label: 'Wide (960px)' },
43
- { value: '100%', label: 'Full width' },
40
+ { value: '680px', label: 'Narrow', width: 42 },
41
+ { value: '780px', label: 'Default', width: 56 },
42
+ { value: '960px', label: 'Wide', width: 75 },
43
+ { value: '100%', label: 'Full', width: 100 },
44
+ ];
45
+
46
+ export const FONT_SIZES = [
47
+ { value: '14px', label: '14', numericValue: 14 },
48
+ { value: '15px', label: '15', numericValue: 15, isDefault: true },
49
+ { value: '16px', label: '16', numericValue: 16 },
50
+ { value: '17px', label: '17', numericValue: 17 },
44
51
  ];
45
52
 
46
53
  export const FONTS = [
47
- { value: 'lora', label: 'Lora (serif)', style: { fontFamily: 'Lora, Georgia, serif' } },
48
- { value: 'ibm-plex-sans', label: 'IBM Plex Sans', style: { fontFamily: "'IBM Plex Sans', sans-serif" } },
49
- { value: 'geist', label: 'Geist', style: { fontFamily: 'var(--font-geist-sans), sans-serif' } },
50
- { value: 'ibm-plex-mono', label: 'IBM Plex Mono (mono)', style: { fontFamily: "'IBM Plex Mono', monospace" } },
54
+ { value: 'lora', label: 'Lora', category: 'Serif', style: { fontFamily: 'Lora, Georgia, serif' } },
55
+ { value: 'ibm-plex-sans', label: 'IBM Plex Sans', category: 'Sans', style: { fontFamily: "'IBM Plex Sans', sans-serif" } },
56
+ { value: 'geist', label: 'Geist', category: 'Sans', style: { fontFamily: 'var(--font-geist-sans), sans-serif' } },
57
+ { value: 'ibm-plex-mono', label: 'IBM Plex Mono', category: 'Mono', style: { fontFamily: "'IBM Plex Mono', monospace" } },
51
58
  ];
52
59
 
53
60
  /* ── MCP Types ────────────────────────────────────────────────── */
@@ -127,6 +134,8 @@ export interface McpTabProps {
127
134
  export interface AppearanceTabProps {
128
135
  font: string;
129
136
  setFont: (v: string) => void;
137
+ fontSize: string;
138
+ setFontSize: (v: string) => void;
130
139
  contentWidth: string;
131
140
  setContentWidth: (v: string) => void;
132
141
  dark: boolean;
@@ -125,8 +125,15 @@ export async function createSessionFromEntry(
125
125
  }, 15_000);
126
126
 
127
127
  if (newResponse.error) {
128
- // Non-fatal: some agents may not support explicit session/new
129
- console.warn(`ACP session/new warning for ${entry.id}: ${newResponse.error.message}`);
128
+ const errMsg = newResponse.error.message ?? 'session/new failed';
129
+ // Authentication errors are fatal — agent cannot proceed without auth
130
+ if (/auth/i.test(errMsg)) {
131
+ unsubApproval();
132
+ killAgent(proc);
133
+ throw new Error(`${entry.id}: ${errMsg}`);
134
+ }
135
+ // Other errors are non-fatal: some agents may not support explicit session/new
136
+ console.warn(`ACP session/new warning for ${entry.id}: ${errMsg}`);
130
137
  } else {
131
138
  const newResult = newResponse.result as Record<string, unknown> | undefined;
132
139
  if (newResult) {
@@ -134,7 +141,9 @@ export async function createSessionFromEntry(
134
141
  configOptions = parseConfigOptions(newResult.configOptions);
135
142
  }
136
143
  }
137
- } catch {
144
+ } catch (sessionErr) {
145
+ // Re-throw auth errors — they are fatal
146
+ if (sessionErr instanceof Error && /auth/i.test(sessionErr.message)) throw sessionErr;
138
147
  // Non-fatal: agent may not support explicit session/new (backwards compat)
139
148
  }
140
149