@geminilight/mindos 0.6.15 → 0.6.16
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/app/app/globals.css +49 -2
- package/app/app/page.tsx +1 -19
- package/app/components/CreateSpaceModal.tsx +3 -3
- package/app/components/CustomSelect.tsx +228 -0
- package/app/components/DirPicker.tsx +110 -63
- package/app/components/FileTree.tsx +54 -32
- package/app/components/HomeContent.tsx +69 -13
- package/app/components/ImportModal.tsx +92 -39
- package/app/components/Panel.tsx +87 -21
- package/app/components/SidebarLayout.tsx +18 -5
- package/app/components/changes/ChangesContentPage.tsx +34 -23
- package/app/components/renderers/csv/ConfigPanel.tsx +10 -7
- package/app/components/settings/KnowledgeTab.tsx +38 -12
- package/app/components/settings/McpAgentInstall.tsx +14 -30
- package/app/components/settings/McpSkillsSection.tsx +43 -27
- package/app/components/settings/McpTab.tsx +30 -37
- package/app/components/settings/UninstallTab.tsx +1 -1
- package/app/components/setup/StepAgents.tsx +1 -1
- package/app/lib/core/create-space.ts +12 -0
- package/app/lib/i18n-en.ts +19 -2
- package/app/lib/i18n-zh.ts +21 -4
- package/package.json +1 -1
package/app/components/Panel.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { ChevronsDownUp, ChevronsUpDown, Plus, Import, FileText, Layers } from 'lucide-react';
|
|
5
6
|
import type { PanelId } from './ActivityBar';
|
|
6
7
|
import type { FileNode } from '@/lib/types';
|
|
7
8
|
import FileTree from './FileTree';
|
|
@@ -77,11 +78,48 @@ export default function Panel({
|
|
|
77
78
|
const width = maximized ? undefined : (panelWidth ?? defaultWidth);
|
|
78
79
|
|
|
79
80
|
const { t } = useLocale();
|
|
81
|
+
const router = useRouter();
|
|
80
82
|
|
|
81
83
|
// File tree depth control: null = manual (no override), number = forced max open depth
|
|
82
84
|
const [maxOpenDepth, setMaxOpenDepth] = useState<number | null>(null);
|
|
83
85
|
const treeMaxDepth = useMemo(() => getMaxDepth(fileTree), [fileTree]);
|
|
84
86
|
|
|
87
|
+
// "New" dropdown popover
|
|
88
|
+
const [newPopover, setNewPopover] = useState(false);
|
|
89
|
+
const newBtnRef = useRef<HTMLButtonElement>(null);
|
|
90
|
+
const newPopoverRef = useRef<HTMLDivElement>(null);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!newPopover) return;
|
|
94
|
+
const handler = (e: MouseEvent) => {
|
|
95
|
+
if (
|
|
96
|
+
newBtnRef.current && !newBtnRef.current.contains(e.target as Node) &&
|
|
97
|
+
newPopoverRef.current && !newPopoverRef.current.contains(e.target as Node)
|
|
98
|
+
) {
|
|
99
|
+
setNewPopover(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const keyHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') setNewPopover(false); };
|
|
103
|
+
document.addEventListener('mousedown', handler);
|
|
104
|
+
document.addEventListener('keydown', keyHandler);
|
|
105
|
+
return () => {
|
|
106
|
+
document.removeEventListener('mousedown', handler);
|
|
107
|
+
document.removeEventListener('keydown', keyHandler);
|
|
108
|
+
};
|
|
109
|
+
}, [newPopover]);
|
|
110
|
+
|
|
111
|
+
// Double-click hint: show only until user has used it once
|
|
112
|
+
const [dblHintSeen, setDblHintSeen] = useState(() => {
|
|
113
|
+
if (typeof window === 'undefined') return false;
|
|
114
|
+
return localStorage.getItem('mindos-tree-dblclick-hint') === '1';
|
|
115
|
+
});
|
|
116
|
+
const markDblHintSeen = useCallback(() => {
|
|
117
|
+
if (!dblHintSeen) {
|
|
118
|
+
setDblHintSeen(true);
|
|
119
|
+
try { localStorage.setItem('mindos-tree-dblclick-hint', '1'); } catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
}, [dblHintSeen]);
|
|
122
|
+
|
|
85
123
|
const handleMouseDown = useResizeDrag({
|
|
86
124
|
width: panelWidth ?? defaultWidth,
|
|
87
125
|
minWidth: MIN_PANEL_WIDTH,
|
|
@@ -109,49 +147,77 @@ export default function Panel({
|
|
|
109
147
|
<div className={`flex flex-col h-full ${activePanel === 'files' ? '' : 'hidden'}`}>
|
|
110
148
|
<PanelHeader title={t.sidebar.files}>
|
|
111
149
|
<div className="flex items-center gap-0.5">
|
|
150
|
+
{/* New (File / Space) */}
|
|
151
|
+
<div className="relative">
|
|
152
|
+
<button
|
|
153
|
+
ref={newBtnRef}
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={() => setNewPopover(v => !v)}
|
|
156
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
|
|
157
|
+
aria-label={t.sidebar.new}
|
|
158
|
+
title={t.sidebar.new}
|
|
159
|
+
>
|
|
160
|
+
<Plus size={13} />
|
|
161
|
+
</button>
|
|
162
|
+
{newPopover && (
|
|
163
|
+
<div
|
|
164
|
+
ref={newPopoverRef}
|
|
165
|
+
className="absolute top-full right-0 mt-1 min-w-[152px] bg-card border border-border rounded-lg shadow-lg py-1 z-50"
|
|
166
|
+
>
|
|
167
|
+
<button
|
|
168
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-foreground hover:bg-muted transition-colors text-left"
|
|
169
|
+
onClick={() => { setNewPopover(false); router.push('/view/Untitled.md'); }}
|
|
170
|
+
>
|
|
171
|
+
<FileText size={14} className="shrink-0" />
|
|
172
|
+
{t.sidebar.newFile}
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-foreground hover:bg-muted transition-colors text-left"
|
|
176
|
+
onClick={() => { setNewPopover(false); window.dispatchEvent(new Event('mindos:create-space')); }}
|
|
177
|
+
>
|
|
178
|
+
<Layers size={14} className="shrink-0 text-[var(--amber)]" />
|
|
179
|
+
{t.sidebar.newSpace}
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
{/* Import */}
|
|
112
185
|
<button
|
|
113
186
|
type="button"
|
|
114
187
|
onClick={() => onImport?.()}
|
|
115
|
-
className="p-1 rounded
|
|
188
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
|
|
116
189
|
aria-label={t.sidebar.importFile}
|
|
117
190
|
title={t.sidebar.importFile}
|
|
118
191
|
>
|
|
119
192
|
<Import size={13} />
|
|
120
193
|
</button>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
|
|
125
|
-
aria-label={t.sidebar.newFile}
|
|
126
|
-
title={t.sidebar.newFile}
|
|
127
|
-
>
|
|
128
|
-
<FilePlus size={13} />
|
|
129
|
-
</button>
|
|
194
|
+
{/* Separator: create actions | view actions */}
|
|
195
|
+
<div className="w-px h-3.5 bg-border mx-0.5" />
|
|
196
|
+
{/* Collapse Level */}
|
|
130
197
|
<button
|
|
131
198
|
onClick={() => setMaxOpenDepth(prev => {
|
|
132
199
|
const current = prev ?? treeMaxDepth;
|
|
133
200
|
return Math.max(-1, current - 1);
|
|
134
201
|
})}
|
|
135
|
-
onDoubleClick={() => setMaxOpenDepth(-1)}
|
|
136
|
-
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
202
|
+
onDoubleClick={() => { setMaxOpenDepth(-1); markDblHintSeen(); }}
|
|
203
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
|
|
137
204
|
aria-label={t.sidebar.collapseLevel}
|
|
138
|
-
title={t.sidebar.collapseLevel}
|
|
205
|
+
title={dblHintSeen ? t.sidebar.collapseLevel : (t.sidebar.collapseLevelHint ?? t.sidebar.collapseLevel)}
|
|
139
206
|
>
|
|
140
207
|
<ChevronsDownUp size={13} />
|
|
141
208
|
</button>
|
|
209
|
+
{/* Expand Level */}
|
|
142
210
|
<button
|
|
143
211
|
onClick={() => setMaxOpenDepth(prev => {
|
|
144
212
|
const current = prev ?? 0;
|
|
145
213
|
const next = current + 1;
|
|
146
|
-
if (next > treeMaxDepth)
|
|
147
|
-
return null; // fully expanded → release back to manual
|
|
148
|
-
}
|
|
214
|
+
if (next > treeMaxDepth) return null;
|
|
149
215
|
return next;
|
|
150
216
|
})}
|
|
151
|
-
onDoubleClick={() => setMaxOpenDepth(null)}
|
|
152
|
-
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
217
|
+
onDoubleClick={() => { setMaxOpenDepth(null); markDblHintSeen(); }}
|
|
218
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-1 focus-visible:ring-ring"
|
|
153
219
|
aria-label={t.sidebar.expandLevel}
|
|
154
|
-
title={t.sidebar.expandLevel}
|
|
220
|
+
title={dblHintSeen ? t.sidebar.expandLevel : (t.sidebar.expandLevelHint ?? t.sidebar.expandLevel)}
|
|
155
221
|
>
|
|
156
222
|
<ChevronsUpDown size={13} />
|
|
157
223
|
</button>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import { Search, Settings, Menu, X, FolderInput } from 'lucide-react';
|
|
@@ -13,7 +13,7 @@ import SearchPanel from './panels/SearchPanel';
|
|
|
13
13
|
import AgentsPanel from './panels/AgentsPanel';
|
|
14
14
|
import DiscoverPanel from './panels/DiscoverPanel';
|
|
15
15
|
import EchoPanel from './panels/EchoPanel';
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
import RightAskPanel from './RightAskPanel';
|
|
18
18
|
import RightAgentDetailPanel, {
|
|
19
19
|
RIGHT_AGENT_DETAIL_DEFAULT_WIDTH,
|
|
@@ -29,6 +29,7 @@ import KeyboardShortcuts from './KeyboardShortcuts';
|
|
|
29
29
|
import ChangesBanner from './changes/ChangesBanner';
|
|
30
30
|
import SpaceInitToast from './SpaceInitToast';
|
|
31
31
|
import OrganizeToast from './OrganizeToast';
|
|
32
|
+
import CreateSpaceModal from './CreateSpaceModal';
|
|
32
33
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
33
34
|
import { FileNode } from '@/lib/types';
|
|
34
35
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -43,6 +44,18 @@ import { useAskPanel } from '@/hooks/useAskPanel';
|
|
|
43
44
|
import { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
44
45
|
import type { Tab } from './settings/types';
|
|
45
46
|
|
|
47
|
+
function collectDirPaths(nodes: FileNode[], prefix = ''): string[] {
|
|
48
|
+
const result: string[] = [];
|
|
49
|
+
for (const n of nodes) {
|
|
50
|
+
if (n.type === 'directory' && !n.name.startsWith('.')) {
|
|
51
|
+
const p = prefix ? `${prefix}/${n.name}` : n.name;
|
|
52
|
+
result.push(p);
|
|
53
|
+
if (n.children) result.push(...collectDirPaths(n.children, p));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
interface SidebarLayoutProps {
|
|
47
60
|
fileTree: FileNode[];
|
|
48
61
|
children: React.ReactNode;
|
|
@@ -131,6 +144,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
131
144
|
const { t } = useLocale();
|
|
132
145
|
const router = useRouter();
|
|
133
146
|
const pathname = usePathname();
|
|
147
|
+
const dirPaths = useMemo(() => collectDirPaths(fileTree), [fileTree]);
|
|
134
148
|
const { status: syncStatus, fetchStatus: syncStatusRefresh } = useSyncStatus();
|
|
135
149
|
|
|
136
150
|
const currentFile = pathname.startsWith('/view/')
|
|
@@ -375,9 +389,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
375
389
|
<div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
|
|
376
390
|
<DiscoverPanel active={lp.activePanel === 'discover'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
|
|
377
391
|
</div>
|
|
378
|
-
<div className={`flex flex-col h-full ${lp.activePanel === 'history' ? '' : 'hidden'}`}>
|
|
379
|
-
<ImportHistoryPanel active={lp.activePanel === 'history'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} refreshToken={historyRefreshToken} />
|
|
380
|
-
</div>
|
|
381
392
|
</Panel>
|
|
382
393
|
|
|
383
394
|
{/* ── Right-side Ask AI Panel ── */}
|
|
@@ -504,6 +515,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
504
515
|
</div>
|
|
505
516
|
|
|
506
517
|
<SpaceInitToast />
|
|
518
|
+
<CreateSpaceModal t={t} dirPaths={dirPaths} />
|
|
507
519
|
|
|
508
520
|
{/* Global drag overlay */}
|
|
509
521
|
{dragOverlay && !importModalOpen && (
|
|
@@ -523,6 +535,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
523
535
|
defaultSpace={importDefaultSpace}
|
|
524
536
|
initialFiles={importInitialFiles}
|
|
525
537
|
aiOrganize={aiOrganize}
|
|
538
|
+
dirPaths={dirPaths}
|
|
526
539
|
/>
|
|
527
540
|
|
|
528
541
|
{organizeToastVisible && (
|
|
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
|
|
5
5
|
import { ChevronDown, ChevronRight, History, RefreshCw } from 'lucide-react';
|
|
6
6
|
import { apiFetch } from '@/lib/api';
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
import CustomSelect from '@/components/CustomSelect';
|
|
8
9
|
import { collapseDiffContext, buildLineDiff } from './line-diff';
|
|
9
10
|
|
|
10
11
|
/** Semantic color for operation type badges */
|
|
@@ -102,6 +103,25 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
102
103
|
return ['all', ...ops];
|
|
103
104
|
}, [events, opFilter]);
|
|
104
105
|
|
|
106
|
+
const sourceSelectOptions = useMemo(
|
|
107
|
+
() => [
|
|
108
|
+
{ value: 'all', label: t.changes.filters.all },
|
|
109
|
+
{ value: 'agent', label: t.changes.filters.agent },
|
|
110
|
+
{ value: 'user', label: t.changes.filters.user },
|
|
111
|
+
{ value: 'system', label: t.changes.filters.system },
|
|
112
|
+
],
|
|
113
|
+
[t],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const opSelectOptions = useMemo(
|
|
117
|
+
() =>
|
|
118
|
+
opOptions.map((op) => ({
|
|
119
|
+
value: op,
|
|
120
|
+
label: op === 'all' ? t.changes.filters.operationAll : op,
|
|
121
|
+
})),
|
|
122
|
+
[opOptions, t],
|
|
123
|
+
);
|
|
124
|
+
|
|
105
125
|
const sourceLabel = useCallback((source: ChangeEvent['source']) => {
|
|
106
126
|
if (source === 'agent') return t.changes.filters.agent;
|
|
107
127
|
if (source === 'user') return t.changes.filters.user;
|
|
@@ -161,33 +181,24 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
161
181
|
className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
162
182
|
/>
|
|
163
183
|
</label>
|
|
164
|
-
<
|
|
184
|
+
<div className="block">
|
|
165
185
|
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.source}</span>
|
|
166
|
-
<
|
|
186
|
+
<CustomSelect
|
|
167
187
|
value={sourceFilter}
|
|
168
|
-
onChange={(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<option value="system">{t.changes.filters.system}</option>
|
|
175
|
-
</select>
|
|
176
|
-
</label>
|
|
177
|
-
<label className="block">
|
|
188
|
+
onChange={(v) => setSourceFilter(v as 'all' | 'agent' | 'user' | 'system')}
|
|
189
|
+
options={sourceSelectOptions}
|
|
190
|
+
className="mt-1"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="block">
|
|
178
194
|
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.operation}</span>
|
|
179
|
-
<
|
|
195
|
+
<CustomSelect
|
|
180
196
|
value={opFilter}
|
|
181
|
-
onChange={
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{op === 'all' ? t.changes.filters.operationAll : op}
|
|
187
|
-
</option>
|
|
188
|
-
))}
|
|
189
|
-
</select>
|
|
190
|
-
</label>
|
|
197
|
+
onChange={setOpFilter}
|
|
198
|
+
options={opSelectOptions}
|
|
199
|
+
className="mt-1"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
191
202
|
<label className="block">
|
|
192
203
|
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.keyword}</span>
|
|
193
204
|
<input
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { X } from 'lucide-react';
|
|
4
|
+
import CustomSelect from '@/components/CustomSelect';
|
|
4
5
|
import type { CsvConfig, ViewType } from './types';
|
|
5
6
|
|
|
6
7
|
export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
|
|
@@ -11,18 +12,20 @@ export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
|
|
|
11
12
|
onChange: (cfg: CsvConfig) => void;
|
|
12
13
|
}) {
|
|
13
14
|
const labelStyle: React.CSSProperties = { color: 'var(--muted-foreground)', fontSize: '0.72rem' };
|
|
14
|
-
const selectStyle: React.CSSProperties = { background: 'var(--background)', color: 'var(--foreground)', borderColor: 'var(--border)', fontSize: '0.72rem' };
|
|
15
15
|
|
|
16
16
|
function FieldSelect({ label, value, onChange: onCh }: { label: string; value: string; onChange: (v: string) => void }) {
|
|
17
17
|
return (
|
|
18
18
|
<div className="flex items-center justify-between gap-2">
|
|
19
19
|
<span className="font-display" style={labelStyle}>{label}</span>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
<CustomSelect
|
|
21
|
+
value={value}
|
|
22
|
+
onChange={onCh}
|
|
23
|
+
size="sm"
|
|
24
|
+
options={[
|
|
25
|
+
{ value: '', label: '— none —' },
|
|
26
|
+
...headers.map(h => ({ value: h, label: h })),
|
|
27
|
+
]}
|
|
28
|
+
/>
|
|
26
29
|
</div>
|
|
27
30
|
);
|
|
28
31
|
}
|
|
@@ -4,6 +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 type { KnowledgeTabProps } from './types';
|
|
6
6
|
import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
7
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
7
8
|
import { apiFetch } from '@/lib/api';
|
|
8
9
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
9
10
|
import { formatBytes, formatUptime } from '@/lib/format';
|
|
@@ -99,12 +100,17 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
99
100
|
const [resetting, setResetting] = useState(false);
|
|
100
101
|
// revealed holds the plaintext token after regenerate, until user navigates away
|
|
101
102
|
const [revealedToken, setRevealedToken] = useState<string | null>(null);
|
|
103
|
+
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
|
104
|
+
const [showCleanupConfirm, setShowCleanupConfirm] = useState(false);
|
|
102
105
|
|
|
103
106
|
const hasToken = !!(data.authToken);
|
|
104
107
|
const displayToken = revealedToken ?? data.authToken ?? '';
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
function handleResetToken() {
|
|
110
|
+
setShowResetConfirm(true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function doResetToken() {
|
|
108
114
|
setResetting(true);
|
|
109
115
|
try {
|
|
110
116
|
const res = await apiFetch<{ ok: boolean; token: string }>('/api/settings/reset-token', { method: 'POST' });
|
|
@@ -165,16 +171,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
165
171
|
<div className="text-xs text-muted-foreground mt-0.5">{k.cleanupExamplesHint}</div>
|
|
166
172
|
</div>
|
|
167
173
|
<button
|
|
168
|
-
onClick={
|
|
169
|
-
if (!confirm(k.cleanupExamplesConfirm(exampleCount))) return;
|
|
170
|
-
setCleaningUp(true);
|
|
171
|
-
const r = await cleanupExamplesAction();
|
|
172
|
-
setCleaningUp(false);
|
|
173
|
-
if (r.success) {
|
|
174
|
-
setCleanupResult(r.deleted);
|
|
175
|
-
setExampleCount(0);
|
|
176
|
-
}
|
|
177
|
-
}}
|
|
174
|
+
onClick={() => setShowCleanupConfirm(true)}
|
|
178
175
|
disabled={cleaningUp}
|
|
179
176
|
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"
|
|
180
177
|
>
|
|
@@ -301,6 +298,35 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
301
298
|
|
|
302
299
|
{/* System Monitoring — collapsible */}
|
|
303
300
|
<MonitoringSection />
|
|
301
|
+
|
|
302
|
+
<ConfirmDialog
|
|
303
|
+
open={showResetConfirm}
|
|
304
|
+
title={k.authTokenReset ?? 'Regenerate Token'}
|
|
305
|
+
message={k.authTokenResetConfirm}
|
|
306
|
+
confirmLabel={k.authTokenReset ?? 'Regenerate'}
|
|
307
|
+
cancelLabel="Cancel"
|
|
308
|
+
onConfirm={() => { setShowResetConfirm(false); doResetToken(); }}
|
|
309
|
+
onCancel={() => setShowResetConfirm(false)}
|
|
310
|
+
/>
|
|
311
|
+
<ConfirmDialog
|
|
312
|
+
open={showCleanupConfirm}
|
|
313
|
+
title={k.cleanupExamples ?? 'Cleanup Examples'}
|
|
314
|
+
message={exampleCount !== null ? k.cleanupExamplesConfirm(exampleCount) : ''}
|
|
315
|
+
confirmLabel={k.cleanupExamplesButton ?? 'Clean up'}
|
|
316
|
+
cancelLabel="Cancel"
|
|
317
|
+
variant="destructive"
|
|
318
|
+
onConfirm={async () => {
|
|
319
|
+
setShowCleanupConfirm(false);
|
|
320
|
+
setCleaningUp(true);
|
|
321
|
+
const r = await cleanupExamplesAction();
|
|
322
|
+
setCleaningUp(false);
|
|
323
|
+
if (r.success) {
|
|
324
|
+
setCleanupResult(r.deleted);
|
|
325
|
+
setExampleCount(0);
|
|
326
|
+
}
|
|
327
|
+
}}
|
|
328
|
+
onCancel={() => setShowCleanupConfirm(false)}
|
|
329
|
+
/>
|
|
304
330
|
</div>
|
|
305
331
|
);
|
|
306
332
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
|
|
5
|
+
import CustomSelect from '@/components/CustomSelect';
|
|
5
6
|
import { apiFetch } from '@/lib/api';
|
|
6
7
|
import type { AgentInfo, McpAgentInstallProps } from './types';
|
|
7
8
|
|
|
@@ -90,8 +91,7 @@ export default function AgentInstall({ agents, t, onRefresh }: McpAgentInstallPr
|
|
|
90
91
|
type="checkbox"
|
|
91
92
|
checked={selected.has(agent.key)}
|
|
92
93
|
onChange={() => toggle(agent.key)}
|
|
93
|
-
className="
|
|
94
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
94
|
+
className="form-check"
|
|
95
95
|
/>
|
|
96
96
|
<span className="w-28 shrink-0 text-xs">{agent.name}</span>
|
|
97
97
|
<span className="text-2xs px-1.5 py-0.5 rounded font-mono bg-muted">
|
|
@@ -111,14 +111,16 @@ export default function AgentInstall({ agents, t, onRefresh }: McpAgentInstallPr
|
|
|
111
111
|
)}
|
|
112
112
|
{/* Scope selector */}
|
|
113
113
|
{selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
|
|
114
|
-
<
|
|
114
|
+
<CustomSelect
|
|
115
115
|
value={scopes[agent.key] || 'project'}
|
|
116
|
-
onChange={
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
onChange={v => setScopes({ ...scopes, [agent.key]: v as 'project' | 'global' })}
|
|
117
|
+
size="sm"
|
|
118
|
+
className="ml-auto"
|
|
119
|
+
options={[
|
|
120
|
+
{ value: 'project', label: m?.project ?? 'Project' },
|
|
121
|
+
{ value: 'global', label: m?.global ?? 'Global' },
|
|
122
|
+
]}
|
|
123
|
+
/>
|
|
122
124
|
)}
|
|
123
125
|
</div>
|
|
124
126
|
))}
|
|
@@ -143,33 +145,15 @@ export default function AgentInstall({ agents, t, onRefresh }: McpAgentInstallPr
|
|
|
143
145
|
{/* Transport selector */}
|
|
144
146
|
<div className="flex items-center gap-4 text-xs pt-1">
|
|
145
147
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
146
|
-
<input
|
|
147
|
-
type="radio"
|
|
148
|
-
name="transport"
|
|
149
|
-
checked={transport === 'auto'}
|
|
150
|
-
onChange={() => setTransport('auto')}
|
|
151
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
152
|
-
/>
|
|
148
|
+
<input type="radio" name="transport" checked={transport === 'auto'} onChange={() => setTransport('auto')} className="form-radio" />
|
|
153
149
|
{m?.transportAuto ?? 'auto (recommended)'}
|
|
154
150
|
</label>
|
|
155
151
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
156
|
-
<input
|
|
157
|
-
type="radio"
|
|
158
|
-
name="transport"
|
|
159
|
-
checked={transport === 'stdio'}
|
|
160
|
-
onChange={() => setTransport('stdio')}
|
|
161
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
162
|
-
/>
|
|
152
|
+
<input type="radio" name="transport" checked={transport === 'stdio'} onChange={() => setTransport('stdio')} className="form-radio" />
|
|
163
153
|
{m?.transportStdio ?? 'stdio'}
|
|
164
154
|
</label>
|
|
165
155
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
166
|
-
<input
|
|
167
|
-
type="radio"
|
|
168
|
-
name="transport"
|
|
169
|
-
checked={transport === 'http'}
|
|
170
|
-
onChange={() => setTransport('http')}
|
|
171
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
172
|
-
/>
|
|
156
|
+
<input type="radio" name="transport" checked={transport === 'http'} onChange={() => setTransport('http')} className="form-radio" />
|
|
173
157
|
{m?.transportHttp ?? 'http'}
|
|
174
158
|
</label>
|
|
175
159
|
</div>
|
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
} from 'lucide-react';
|
|
8
8
|
import { apiFetch } from '@/lib/api';
|
|
9
9
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
10
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
10
11
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
11
12
|
import type { SkillInfo, McpSkillsSectionProps } from './types';
|
|
12
13
|
import SkillRow from './McpSkillRow';
|
|
13
14
|
import SkillCreateForm from './McpSkillCreateForm';
|
|
15
|
+
import CustomSelect from '@/components/CustomSelect';
|
|
16
|
+
import type { SelectItem } from '@/components/CustomSelect';
|
|
14
17
|
|
|
15
18
|
/* ── Skills Section ────────────────────────────────────────────── */
|
|
16
19
|
|
|
@@ -33,6 +36,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
33
36
|
const [loadingContent, setLoadingContent] = useState<string | null>(null);
|
|
34
37
|
const [loadErrors, setLoadErrors] = useState<Record<string, string>>({});
|
|
35
38
|
const [switchingLang, setSwitchingLang] = useState(false);
|
|
39
|
+
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
36
40
|
|
|
37
41
|
const fetchSkills = useCallback(async () => {
|
|
38
42
|
try {
|
|
@@ -82,9 +86,11 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
82
86
|
}
|
|
83
87
|
};
|
|
84
88
|
|
|
85
|
-
const handleDelete =
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const handleDelete = (name: string) => {
|
|
90
|
+
setDeleteTarget(name);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const doDelete = async (name: string) => {
|
|
88
94
|
try {
|
|
89
95
|
await apiFetch('/api/skills', {
|
|
90
96
|
method: 'POST',
|
|
@@ -365,6 +371,21 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
365
371
|
})()}
|
|
366
372
|
m={m}
|
|
367
373
|
/>
|
|
374
|
+
|
|
375
|
+
<ConfirmDialog
|
|
376
|
+
open={!!deleteTarget}
|
|
377
|
+
title="Delete skill?"
|
|
378
|
+
message={deleteTarget ? (m?.skillDeleteConfirm?.(deleteTarget) ?? `Delete skill "${deleteTarget}"?`) : ''}
|
|
379
|
+
confirmLabel="Delete"
|
|
380
|
+
cancelLabel="Cancel"
|
|
381
|
+
variant="destructive"
|
|
382
|
+
onConfirm={async () => {
|
|
383
|
+
const name = deleteTarget!;
|
|
384
|
+
setDeleteTarget(null);
|
|
385
|
+
await doDelete(name);
|
|
386
|
+
}}
|
|
387
|
+
onCancel={() => setDeleteTarget(null)}
|
|
388
|
+
/>
|
|
368
389
|
</div>
|
|
369
390
|
);
|
|
370
391
|
}
|
|
@@ -399,30 +420,25 @@ function SkillCliHint({ agents, skillName, m }: {
|
|
|
399
420
|
</p>
|
|
400
421
|
|
|
401
422
|
{/* Agent selector */}
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
</optgroup>
|
|
422
|
-
)}
|
|
423
|
-
</select>
|
|
424
|
-
<ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
|
425
|
-
</div>
|
|
423
|
+
<CustomSelect
|
|
424
|
+
value={selectedAgent}
|
|
425
|
+
onChange={setSelectedAgent}
|
|
426
|
+
size="sm"
|
|
427
|
+
options={[
|
|
428
|
+
...(connected.length > 0 ? [{
|
|
429
|
+
label: m?.connectedGroup ?? 'Connected',
|
|
430
|
+
options: connected.map(a => ({ value: a.key, label: a.name })),
|
|
431
|
+
}] : []),
|
|
432
|
+
...(detected.length > 0 ? [{
|
|
433
|
+
label: m?.detectedGroup ?? 'Detected',
|
|
434
|
+
options: detected.map(a => ({ value: a.key, label: a.name })),
|
|
435
|
+
}] : []),
|
|
436
|
+
...(notFound.length > 0 ? [{
|
|
437
|
+
label: m?.notFoundGroup ?? 'Not Installed',
|
|
438
|
+
options: notFound.map(a => ({ value: a.key, label: a.name })),
|
|
439
|
+
}] : []),
|
|
440
|
+
] as SelectItem[]}
|
|
441
|
+
/>
|
|
426
442
|
|
|
427
443
|
{/* Command */}
|
|
428
444
|
<div className="flex items-center gap-1.5">
|