@geminilight/mindos 0.6.14 → 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.
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useMemo, useState } from 'react';
4
- import { ChevronsDownUp, ChevronsUpDown, FilePlus, Import } from 'lucide-react';
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 text-[var(--amber)] hover:bg-muted hover:text-[var(--amber)] transition-colors focus-visible:ring-1 focus-visible:ring-ring"
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
- <button
122
- type="button"
123
- onClick={() => window.location.assign('/view/Untitled.md')}
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
- import ImportHistoryPanel from './panels/ImportHistoryPanel';
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
- <label className="block">
184
+ <div className="block">
165
185
  <span className="text-xs text-muted-foreground font-display">{t.changes.filters.source}</span>
166
- <select
186
+ <CustomSelect
167
187
  value={sourceFilter}
168
- onChange={(e) => setSourceFilter(e.target.value as 'all' | 'agent' | 'user' | 'system')}
169
- 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"
170
- >
171
- <option value="all">{t.changes.filters.all}</option>
172
- <option value="agent">{t.changes.filters.agent}</option>
173
- <option value="user">{t.changes.filters.user}</option>
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
- <select
195
+ <CustomSelect
180
196
  value={opFilter}
181
- onChange={(e) => setOpFilter(e.target.value)}
182
- 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"
183
- >
184
- {opOptions.map((op) => (
185
- <option key={op} value={op}>
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
- <select value={value} onChange={e => onCh(e.target.value)}
21
- className="rounded px-2 py-1 outline-none border font-display" style={selectStyle}
22
- >
23
- <option value="">— none —</option>
24
- {headers.map(h => <option key={h} value={h}>{h}</option>)}
25
- </select>
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
- async function handleResetToken() {
107
- if (!confirm(k.authTokenResetConfirm)) return;
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={async () => {
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="rounded border-border"
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
- <select
114
+ <CustomSelect
115
115
  value={scopes[agent.key] || 'project'}
116
- onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
117
- className="ml-auto text-2xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
118
- >
119
- <option value="project">{m?.project ?? 'Project'}</option>
120
- <option value="global">{m?.global ?? 'Global'}</option>
121
- </select>
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 = async (name: string) => {
86
- const confirmMsg = m?.skillDeleteConfirm ? m.skillDeleteConfirm(name) : `Delete skill "${name}"?`;
87
- if (!confirm(confirmMsg)) return;
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
- <div className="relative">
403
- <select
404
- value={selectedAgent}
405
- onChange={(e) => setSelectedAgent(e.target.value)}
406
- className="w-full appearance-none px-2.5 py-1.5 pr-7 text-2xs rounded-md border border-border bg-background text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
407
- >
408
- {connected.length > 0 && (
409
- <optgroup label={m?.connectedGroup ?? 'Connected'}>
410
- {connected.map(a => <option key={a.key} value={a.key}>✓ {a.name}</option>)}
411
- </optgroup>
412
- )}
413
- {detected.length > 0 && (
414
- <optgroup label={m?.detectedGroup ?? 'Detected'}>
415
- {detected.map(a => <option key={a.key} value={a.key}>○ {a.name}</option>)}
416
- </optgroup>
417
- )}
418
- {notFound.length > 0 && (
419
- <optgroup label={m?.notFoundGroup ?? 'Not Installed'}>
420
- {notFound.map(a => <option key={a.key} value={a.key}>· {a.name}</option>)}
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">