@geminilight/mindos 0.6.8 → 0.6.13

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 (79) hide show
  1. package/README.md +2 -0
  2. package/README_zh.md +2 -0
  3. package/app/app/api/mcp/install/route.ts +4 -1
  4. package/app/app/api/setup/check-path/route.ts +2 -7
  5. package/app/app/api/setup/ls/route.ts +3 -9
  6. package/app/app/api/setup/path-utils.ts +8 -0
  7. package/app/app/api/setup/route.ts +2 -7
  8. package/app/app/api/uninstall/route.ts +47 -0
  9. package/app/app/globals.css +11 -0
  10. package/app/components/ActivityBar.tsx +10 -3
  11. package/app/components/AskFab.tsx +7 -3
  12. package/app/components/CreateSpaceModal.tsx +1 -1
  13. package/app/components/DirView.tsx +1 -1
  14. package/app/components/FileTree.tsx +30 -23
  15. package/app/components/GuideCard.tsx +1 -1
  16. package/app/components/HomeContent.tsx +137 -109
  17. package/app/components/ImportModal.tsx +16 -477
  18. package/app/components/MarkdownView.tsx +3 -0
  19. package/app/components/OnboardingView.tsx +1 -1
  20. package/app/components/OrganizeToast.tsx +386 -0
  21. package/app/components/Panel.tsx +23 -2
  22. package/app/components/Sidebar.tsx +1 -1
  23. package/app/components/SidebarLayout.tsx +44 -1
  24. package/app/components/agents/AgentDetailContent.tsx +33 -12
  25. package/app/components/agents/AgentsMcpSection.tsx +1 -1
  26. package/app/components/agents/AgentsOverviewSection.tsx +3 -4
  27. package/app/components/agents/AgentsPrimitives.tsx +2 -2
  28. package/app/components/agents/AgentsSkillsSection.tsx +2 -2
  29. package/app/components/agents/SkillDetailPopover.tsx +24 -8
  30. package/app/components/ask/AskContent.tsx +124 -75
  31. package/app/components/ask/HighlightMatch.tsx +14 -0
  32. package/app/components/ask/MentionPopover.tsx +5 -3
  33. package/app/components/ask/MessageList.tsx +39 -11
  34. package/app/components/ask/SlashCommandPopover.tsx +4 -2
  35. package/app/components/changes/ChangesBanner.tsx +20 -2
  36. package/app/components/changes/ChangesContentPage.tsx +10 -2
  37. package/app/components/echo/EchoHero.tsx +1 -1
  38. package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
  39. package/app/components/echo/EchoPageSections.tsx +1 -1
  40. package/app/components/explore/UseCaseCard.tsx +1 -1
  41. package/app/components/panels/DiscoverPanel.tsx +29 -25
  42. package/app/components/panels/ImportHistoryPanel.tsx +195 -0
  43. package/app/components/panels/PluginsPanel.tsx +2 -2
  44. package/app/components/settings/AiTab.tsx +24 -0
  45. package/app/components/settings/KnowledgeTab.tsx +1 -1
  46. package/app/components/settings/McpSkillCreateForm.tsx +1 -1
  47. package/app/components/settings/McpSkillRow.tsx +1 -1
  48. package/app/components/settings/McpSkillsSection.tsx +2 -2
  49. package/app/components/settings/McpTab.tsx +2 -2
  50. package/app/components/settings/PluginsTab.tsx +1 -1
  51. package/app/components/settings/Primitives.tsx +118 -6
  52. package/app/components/settings/SettingsContent.tsx +5 -2
  53. package/app/components/settings/UninstallTab.tsx +179 -0
  54. package/app/components/settings/UpdateTab.tsx +17 -5
  55. package/app/components/settings/types.ts +2 -1
  56. package/app/components/setup/StepDots.tsx +2 -2
  57. package/app/components/ui/dialog.tsx +1 -1
  58. package/app/hooks/useAiOrganize.ts +122 -10
  59. package/app/hooks/useMention.ts +21 -3
  60. package/app/hooks/useSlashCommand.ts +18 -4
  61. package/app/lib/agent/reconnect.ts +40 -0
  62. package/app/lib/core/backlinks.ts +2 -2
  63. package/app/lib/core/git.ts +14 -10
  64. package/app/lib/fs.ts +2 -1
  65. package/app/lib/i18n-en.ts +46 -2
  66. package/app/lib/i18n-zh.ts +46 -2
  67. package/app/lib/organize-history.ts +74 -0
  68. package/app/lib/settings.ts +2 -0
  69. package/app/lib/types.ts +2 -0
  70. package/app/next.config.ts +23 -5
  71. package/bin/cli.js +6 -9
  72. package/bin/lib/mcp-build.js +74 -0
  73. package/bin/lib/mcp-spawn.js +8 -5
  74. package/bin/lib/port.js +17 -2
  75. package/bin/lib/stop.js +12 -2
  76. package/mcp/dist/index.cjs +43 -43
  77. package/mcp/src/index.ts +58 -12
  78. package/package.json +1 -1
  79. package/scripts/setup.js +2 -2
@@ -30,7 +30,7 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
30
30
  <div className="flex items-center gap-2 flex-wrap">
31
31
  <span className="text-sm font-medium text-foreground">{renderer.name}</span>
32
32
  {isCore && (
33
- <span className="text-2xs px-1.5 py-0.5 rounded bg-[var(--amber-subtle)] text-[var(--amber)] font-mono">
33
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-[var(--amber-subtle)] text-[var(--amber-text)] font-mono">
34
34
  core
35
35
  </span>
36
36
  )}
@@ -1,5 +1,8 @@
1
1
  'use client';
2
2
 
3
+ import React, { useState, useRef, useEffect, useCallback, useMemo, useId } from 'react';
4
+ import { ChevronDown, Check } from 'lucide-react';
5
+
3
6
  export function SectionLabel({ children }: { children: React.ReactNode }) {
4
7
  return <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">{children}</p>;
5
8
  }
@@ -23,19 +26,128 @@ export function Input({ className = '', ...props }: React.InputHTMLAttributes<HT
23
26
  );
24
27
  }
25
28
 
26
- export function Select({ className = '', ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) {
29
+ interface SelectOption { value: string; label: string }
30
+
31
+ export function Select({ value, onChange, children, className = '', disabled }: {
32
+ value?: string;
33
+ onChange?: (e: { target: { value: string } }) => void;
34
+ children?: React.ReactNode;
35
+ className?: string;
36
+ disabled?: boolean;
37
+ }) {
38
+ const uid = useId();
39
+ const [open, setOpen] = useState(false);
40
+ const [focusIdx, setFocusIdx] = useState(-1);
41
+ const containerRef = useRef<HTMLDivElement>(null);
42
+ const listRef = useRef<HTMLDivElement>(null);
43
+
44
+ const options = useMemo<SelectOption[]>(() =>
45
+ React.Children.toArray(children)
46
+ .filter((c): c is React.ReactElement => React.isValidElement(c) && (c as React.ReactElement).type === 'option')
47
+ .map(c => ({
48
+ value: String((c as React.ReactElement<{ value?: string; children?: React.ReactNode }>).props.value ?? ''),
49
+ label: String((c as React.ReactElement<{ value?: string; children?: React.ReactNode }>).props.children ?? (c as React.ReactElement<{ value?: string }>).props.value ?? ''),
50
+ })),
51
+ [children],
52
+ );
53
+
54
+ const selectedIdx = options.findIndex(o => o.value === value);
55
+ const selectedLabel = options[selectedIdx]?.label ?? '';
56
+
57
+ useEffect(() => {
58
+ if (!open) return;
59
+ const handler = (e: MouseEvent) => {
60
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) setOpen(false);
61
+ };
62
+ document.addEventListener('mousedown', handler);
63
+ return () => document.removeEventListener('mousedown', handler);
64
+ }, [open]);
65
+
66
+ useEffect(() => {
67
+ if (open && listRef.current && focusIdx >= 0) {
68
+ const el = listRef.current.children[focusIdx] as HTMLElement | undefined;
69
+ el?.scrollIntoView({ block: 'nearest' });
70
+ }
71
+ }, [open, focusIdx]);
72
+
73
+ const select = useCallback((idx: number) => {
74
+ if (idx >= 0 && idx < options.length) {
75
+ onChange?.({ target: { value: options[idx].value } });
76
+ setOpen(false);
77
+ }
78
+ }, [options, onChange]);
79
+
80
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
81
+ if (!open) {
82
+ if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
83
+ e.preventDefault();
84
+ setOpen(true);
85
+ setFocusIdx(selectedIdx >= 0 ? selectedIdx : 0);
86
+ }
87
+ return;
88
+ }
89
+ switch (e.key) {
90
+ case 'ArrowDown': e.preventDefault(); setFocusIdx(i => Math.min(i + 1, options.length - 1)); break;
91
+ case 'ArrowUp': e.preventDefault(); setFocusIdx(i => Math.max(i - 1, 0)); break;
92
+ case 'Enter': case ' ': e.preventDefault(); select(focusIdx); break;
93
+ case 'Escape': e.preventDefault(); setOpen(false); break;
94
+ case 'Tab': setOpen(false); break;
95
+ }
96
+ }, [open, options.length, selectedIdx, focusIdx, select]);
97
+
27
98
  return (
28
- <select
29
- {...props}
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
- />
99
+ <div ref={containerRef} className={`relative ${className}`}>
100
+ <button
101
+ type="button"
102
+ disabled={disabled}
103
+ onClick={() => { setOpen(o => !o); setFocusIdx(selectedIdx >= 0 ? selectedIdx : 0); }}
104
+ onKeyDown={handleKeyDown}
105
+ aria-haspopup="listbox"
106
+ aria-expanded={open}
107
+ className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground text-left flex items-center justify-between gap-2 outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
108
+ >
109
+ <span className={`truncate ${selectedLabel ? '' : 'text-muted-foreground'}`}>{selectedLabel || '—'}</span>
110
+ <ChevronDown size={14} className={`shrink-0 text-muted-foreground transition-transform duration-150 ${open ? 'rotate-180' : ''}`} />
111
+ </button>
112
+
113
+ {open && (
114
+ <div
115
+ ref={listRef}
116
+ role="listbox"
117
+ aria-activedescendant={focusIdx >= 0 ? `${uid}-opt-${focusIdx}` : undefined}
118
+ className="absolute z-20 w-full mt-1 py-1 border border-border rounded-lg bg-card shadow-lg max-h-60 overflow-auto animate-in fade-in-0 zoom-in-95 duration-100"
119
+ >
120
+ {options.map((opt, idx) => {
121
+ const isSelected = opt.value === value;
122
+ const isFocused = idx === focusIdx;
123
+ return (
124
+ <button
125
+ key={opt.value}
126
+ id={`${uid}-opt-${idx}`}
127
+ role="option"
128
+ aria-selected={isSelected}
129
+ type="button"
130
+ onMouseDown={e => { e.preventDefault(); select(idx); }}
131
+ onMouseEnter={() => setFocusIdx(idx)}
132
+ className={`w-full px-3 py-1.5 text-sm text-left flex items-center gap-2 transition-colors ${
133
+ isFocused ? 'bg-accent text-accent-foreground' : 'text-foreground'
134
+ }`}
135
+ >
136
+ <Check size={14} className={`shrink-0 ${isSelected ? 'text-[var(--amber)]' : 'invisible'}`} />
137
+ <span className="truncate">{opt.label}</span>
138
+ </button>
139
+ );
140
+ })}
141
+ </div>
142
+ )}
143
+ </div>
32
144
  );
33
145
  }
34
146
 
35
147
  export function EnvBadge({ overridden }: { overridden: boolean }) {
36
148
  if (!overridden) return null;
37
149
  return (
38
- <span className="text-2xs px-1.5 py-0.5 rounded bg-[var(--amber-subtle)] text-[var(--amber)] font-mono ml-1.5">env</span>
150
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-[var(--amber-subtle)] text-[var(--amber-text)] font-mono ml-1.5">env</span>
39
151
  );
40
152
  }
41
153
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect, useState, useCallback, useRef } from 'react';
4
- import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, Download, X } from 'lucide-react';
4
+ import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, Download, X, Trash2 } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
6
  import { apiFetch } from '@/lib/api';
7
7
  import type { AiSettings, AgentSettings, SettingsData, Tab } from './types';
@@ -11,6 +11,7 @@ import { KnowledgeTab } from './KnowledgeTab';
11
11
  import { SyncTab } from './SyncTab';
12
12
  import { McpTab } from './McpTab';
13
13
  import { UpdateTab } from './UpdateTab';
14
+ import { UninstallTab } from './UninstallTab';
14
15
 
15
16
  interface SettingsContentProps {
16
17
  visible: boolean;
@@ -156,6 +157,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
156
157
  { id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
157
158
  { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
158
159
  { id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
160
+ { id: 'uninstall', label: t.settings.tabs.uninstall ?? 'Uninstall', icon: <Trash2 size={iconSize} /> },
159
161
  ];
160
162
 
161
163
  const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
@@ -169,7 +171,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
169
171
  <p className={`${isPanel ? 'text-xs' : 'text-sm'} text-destructive font-medium`}>Failed to load settings</p>
170
172
  {!isPanel && <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>}
171
173
  </div>
172
- ) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' && tab !== 'update' ? (
174
+ ) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' && tab !== 'update' && tab !== 'uninstall' ? (
173
175
  <div className="flex justify-center py-8">
174
176
  <Loader2 size={isPanel ? 16 : 18} className="animate-spin text-muted-foreground" />
175
177
  </div>
@@ -181,6 +183,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
181
183
  {tab === 'sync' && <SyncTab t={t} />}
182
184
  {tab === 'mcp' && <McpTab t={t} />}
183
185
  {tab === 'update' && <UpdateTab />}
186
+ {tab === 'uninstall' && <UninstallTab />}
184
187
  </>
185
188
  )}
186
189
  </div>
@@ -0,0 +1,179 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Trash2, AlertTriangle, CheckCircle2, Loader2, ShieldCheck } from 'lucide-react';
5
+ import { apiFetch } from '@/lib/api';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+
8
+ type Phase = 'idle' | 'confirming' | 'running' | 'success' | 'error';
9
+
10
+ interface DesktopBridge {
11
+ uninstallApp?: () => Promise<{ ok: boolean; error?: string }>;
12
+ }
13
+
14
+ function getDesktopBridge(): DesktopBridge | null {
15
+ if (typeof window === 'undefined') return null;
16
+ const w = window as unknown as { mindos?: DesktopBridge };
17
+ return w.mindos?.uninstallApp ? (w.mindos as DesktopBridge) : null;
18
+ }
19
+
20
+ export function UninstallTab() {
21
+ const { t } = useLocale();
22
+ const u = t.settings.uninstall;
23
+ const isDesktop = !!getDesktopBridge();
24
+
25
+ const [phase, setPhase] = useState<Phase>('idle');
26
+ const [errorMsg, setErrorMsg] = useState('');
27
+
28
+ // Checkboxes — "stop services" is always on (not toggleable)
29
+ // CLI mode: stop + config + npm uninstall (npm always runs as part of CLI uninstall)
30
+ // Desktop mode: stop + config + move app to Trash
31
+ const [removeConfig, setRemoveConfig] = useState(true);
32
+ const [removeApp, setRemoveApp] = useState(true); // Desktop only
33
+
34
+ const handleUninstall = async () => {
35
+ setPhase('running');
36
+ setErrorMsg('');
37
+ try {
38
+ // Step 1: Server-side cleanup (stop services, daemon, config, npm)
39
+ await apiFetch('/api/uninstall', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ removeConfig }),
43
+ });
44
+
45
+ // Step 2: Desktop self-deletion (if selected)
46
+ if (isDesktop && removeApp) {
47
+ const bridge = getDesktopBridge();
48
+ if (bridge?.uninstallApp) {
49
+ const result = await bridge.uninstallApp();
50
+ if (!result.ok) throw new Error(result.error || 'Failed to remove app');
51
+ // Desktop will quit after this — show success briefly
52
+ setPhase('success');
53
+ return;
54
+ }
55
+ }
56
+
57
+ setPhase('success');
58
+ } catch (err) {
59
+ setErrorMsg(err instanceof Error ? err.message : String(err));
60
+ setPhase('error');
61
+ }
62
+ };
63
+
64
+ const Checkbox = ({ checked, onChange, label, desc, disabled }: {
65
+ checked: boolean; onChange: (v: boolean) => void; label: string; desc: string; disabled?: boolean;
66
+ }) => (
67
+ <label className={`flex items-start gap-2.5 p-2.5 rounded bg-muted/30 cursor-pointer select-none ${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-muted/50'}`}>
68
+ <input
69
+ type="checkbox"
70
+ checked={checked}
71
+ onChange={e => !disabled && onChange(e.target.checked)}
72
+ disabled={disabled}
73
+ className="mt-0.5 accent-[var(--amber)] focus-visible:ring-1 focus-visible:ring-ring"
74
+ />
75
+ <div>
76
+ <p className="text-xs font-medium text-foreground">{label}</p>
77
+ <p className="text-[11px] text-muted-foreground">{desc}</p>
78
+ </div>
79
+ </label>
80
+ );
81
+
82
+ 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
+
95
+ {/* Knowledge base safety note */}
96
+ <div className="flex gap-2.5 p-3 rounded-md bg-muted/50 border border-border">
97
+ <ShieldCheck size={14} className="text-success shrink-0 mt-0.5" />
98
+ <p className="text-xs text-muted-foreground leading-relaxed">{u.kbSafe}</p>
99
+ </div>
100
+
101
+ {/* Checklist */}
102
+ {phase === 'idle' || phase === 'confirming' ? (
103
+ <div className="space-y-2">
104
+ <Checkbox checked disabled label={u.stopServices} desc={u.stopServicesDesc} onChange={() => {}} />
105
+ <Checkbox checked={removeConfig} onChange={setRemoveConfig} label={u.removeConfig} desc={u.removeConfigDesc} />
106
+ {!isDesktop && (
107
+ <Checkbox checked disabled label={u.removeNpm} desc={u.removeNpmDesc} onChange={() => {}} />
108
+ )}
109
+ {isDesktop && (
110
+ <Checkbox checked={removeApp} onChange={setRemoveApp} label={u.removeApp} desc={u.removeAppDesc} />
111
+ )}
112
+ </div>
113
+ ) : null}
114
+
115
+ {/* Action area */}
116
+ {phase === 'idle' && (
117
+ <button
118
+ 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"
120
+ >
121
+ <Trash2 size={12} className="inline mr-1.5 -mt-px" />
122
+ {u.confirmButton}
123
+ </button>
124
+ )}
125
+
126
+ {phase === 'confirming' && (
127
+ <div className="p-3 rounded-md border border-error/30 bg-error/5 space-y-2.5">
128
+ <p className="text-xs font-medium text-error">{u.confirmTitle}</p>
129
+ <div className="flex gap-2">
130
+ <button
131
+ 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"
133
+ >
134
+ {u.confirmButton}
135
+ </button>
136
+ <button
137
+ 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"
139
+ >
140
+ {u.cancelButton}
141
+ </button>
142
+ </div>
143
+ </div>
144
+ )}
145
+
146
+ {phase === 'running' && (
147
+ <div className="flex items-center gap-2 py-2">
148
+ <Loader2 size={14} className="animate-spin text-muted-foreground" />
149
+ <span className="text-xs text-muted-foreground">{u.running}</span>
150
+ </div>
151
+ )}
152
+
153
+ {phase === 'success' && (
154
+ <div className="flex items-center gap-2 py-2">
155
+ <CheckCircle2 size={14} className="text-success" />
156
+ <span className="text-xs text-success font-medium">
157
+ {isDesktop && removeApp ? u.successDesktop : u.success}
158
+ </span>
159
+ </div>
160
+ )}
161
+
162
+ {phase === 'error' && (
163
+ <div className="space-y-2">
164
+ <div className="flex items-center gap-2">
165
+ <AlertTriangle size={14} className="text-error" />
166
+ <span className="text-xs text-error font-medium">{u.error}</span>
167
+ </div>
168
+ {errorMsg && <p className="text-[11px] text-muted-foreground font-mono">{errorMsg}</p>}
169
+ <button
170
+ onClick={() => setPhase('idle')}
171
+ 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"
172
+ >
173
+ {u.cancelButton}
174
+ </button>
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink, Circle, Monitor } from 'lucide-react';
5
- import { apiFetch } from '@/lib/api';
5
+ import { apiFetch, ApiError } from '@/lib/api';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
 
8
8
  interface MindosDesktopBridge {
@@ -144,7 +144,7 @@ function DesktopUpdateTab() {
144
144
  )}
145
145
 
146
146
  {state === 'idle' && available && (
147
- <div className="flex items-center gap-2 text-xs text-[var(--amber)]">
147
+ <div className="flex items-center gap-2 text-xs text-[var(--amber-text)]">
148
148
  <Download size={13} />
149
149
  {version ? `Update available: v${version}` : 'Update available'}
150
150
  </div>
@@ -262,6 +262,12 @@ function BrowserUpdateTab() {
262
262
  localStorage.removeItem('mindos_update_latest');
263
263
  localStorage.removeItem('mindos_update_dismissed');
264
264
  window.dispatchEvent(new Event('mindos:update-dismissed'));
265
+ } else {
266
+ const dismissed = localStorage.getItem('mindos_update_dismissed');
267
+ localStorage.setItem('mindos_update_latest', data.latest);
268
+ window.dispatchEvent(new Event(
269
+ data.latest === dismissed ? 'mindos:update-dismissed' : 'mindos:update-available',
270
+ ));
265
271
  }
266
272
  setState('idle');
267
273
  } catch {
@@ -387,7 +393,13 @@ function BrowserUpdateTab() {
387
393
 
388
394
  try {
389
395
  await apiFetch('/api/update', { method: 'POST' });
390
- } catch {
396
+ } catch (err) {
397
+ if (err instanceof ApiError) {
398
+ localStorage.removeItem(UPDATE_STATE_KEY);
399
+ setUpdateError(err.message || 'Update failed');
400
+ setState('error');
401
+ return;
402
+ }
391
403
  // Expected — server may die during update
392
404
  }
393
405
 
@@ -429,7 +441,7 @@ function BrowserUpdateTab() {
429
441
  )}
430
442
 
431
443
  {state === 'idle' && info?.hasUpdate && (
432
- <div className="flex items-center gap-2 text-xs text-[var(--amber)]">
444
+ <div className="flex items-center gap-2 text-xs text-[var(--amber-text)]">
433
445
  <Download size={13} />
434
446
  {u?.available ? u.available(info.current, info.latest) : `Update available: v${info.current} → v${info.latest}`}
435
447
  </div>
@@ -474,7 +486,7 @@ function BrowserUpdateTab() {
474
486
 
475
487
  {state === 'timeout' && (
476
488
  <div className="space-y-2">
477
- <div className="flex items-center gap-2 text-xs text-[var(--amber)]">
489
+ <div className="flex items-center gap-2 text-xs text-[var(--amber-text)]">
478
490
  <AlertCircle size={13} />
479
491
  {u?.timeout ?? 'Update may still be in progress.'}
480
492
  </div>
@@ -20,6 +20,7 @@ export interface AgentSettings {
20
20
  enableThinking?: boolean;
21
21
  thinkingBudget?: number;
22
22
  contextStrategy?: 'auto' | 'off';
23
+ reconnectRetries?: number;
23
24
  }
24
25
 
25
26
  export interface SettingsData {
@@ -33,7 +34,7 @@ export interface SettingsData {
33
34
  envValues?: Record<string, string>;
34
35
  }
35
36
 
36
- export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync' | 'update';
37
+ export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync' | 'update' | 'uninstall';
37
38
 
38
39
  export const CONTENT_WIDTHS = [
39
40
  { value: '680px', label: 'Narrow (680px)' },
@@ -16,7 +16,7 @@ export default function StepDots({ step, setStep, stepTitles, disabled }: StepDo
16
16
  <button onClick={() => setStep(i)}
17
17
  aria-current={i === step ? 'step' : undefined}
18
18
  aria-label={title}
19
- className="flex items-center gap-1.5 p-1 -m-1 disabled:cursor-not-allowed disabled:opacity-60"
19
+ className="flex flex-col items-center gap-1 p-1 -m-1 disabled:cursor-not-allowed disabled:opacity-60"
20
20
  disabled={disabled || i >= step}>
21
21
  <div
22
22
  className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
@@ -27,7 +27,7 @@ export default function StepDots({ step, setStep, stepTitles, disabled }: StepDo
27
27
  }}>
28
28
  {i + 1}
29
29
  </div>
30
- <span className="text-xs hidden sm:inline"
30
+ <span className="text-[10px] leading-tight hidden sm:inline max-w-[4rem] text-center truncate"
31
31
  style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
32
32
  {title}
33
33
  </span>
@@ -31,7 +31,7 @@ function DialogOverlay({
31
31
  <DialogPrimitive.Backdrop
32
32
  data-slot="dialog-overlay"
33
33
  className={cn(
34
- "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
34
+ "fixed inset-0 isolate z-50 overlay-backdrop duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
35
35
  className
36
36
  )}
37
37
  {...props}