@geminilight/mindos 0.6.8 → 0.6.12
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.
- package/README.md +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +16 -477
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -75
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +122 -10
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +46 -2
- package/app/lib/i18n-zh.ts +46 -2
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next.config.ts +23 -5
- package/bin/cli.js +6 -9
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
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)' },
|
|
@@ -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
|
|
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}
|