@geminilight/mindos 0.5.37 → 0.5.39
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/api/auth/route.ts +61 -9
- package/app/app/api/health/route.ts +46 -1
- package/app/app/globals.css +0 -1
- package/app/app/page.tsx +48 -2
- package/app/components/HomeContent.tsx +453 -76
- package/app/components/SidebarLayout.tsx +70 -235
- package/app/components/settings/McpSkillCreateForm.tsx +178 -0
- package/app/components/settings/McpSkillRow.tsx +145 -0
- package/app/components/settings/McpSkillsSection.tsx +71 -307
- package/app/hooks/useAskPanel.ts +117 -0
- package/app/hooks/useLeftPanel.ts +81 -0
- package/app/lib/actions.ts +35 -0
- package/app/lib/core/fs-ops.ts +2 -0
- package/app/lib/core/space-scaffold.ts +103 -0
- package/app/lib/i18n-en.ts +14 -3
- package/app/lib/i18n-zh.ts +14 -3
- package/app/lib/mcp-agents.ts +86 -7
- package/app/package.json +4 -2
- package/package.json +1 -5
- package/scripts/release.sh +18 -4
|
@@ -4,14 +4,14 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import { Search, Settings, Menu, X } from 'lucide-react';
|
|
7
|
-
import ActivityBar, { type PanelId
|
|
8
|
-
import Panel
|
|
7
|
+
import ActivityBar, { type PanelId } from './ActivityBar';
|
|
8
|
+
import Panel from './Panel';
|
|
9
9
|
import FileTree from './FileTree';
|
|
10
10
|
import Logo from './Logo';
|
|
11
11
|
import SearchPanel from './panels/SearchPanel';
|
|
12
12
|
import PluginsPanel from './panels/PluginsPanel';
|
|
13
13
|
import AgentsPanel from './panels/AgentsPanel';
|
|
14
|
-
import RightAskPanel
|
|
14
|
+
import RightAskPanel from './RightAskPanel';
|
|
15
15
|
import AskFab from './AskFab';
|
|
16
16
|
import SyncPopover from './panels/SyncPopover';
|
|
17
17
|
import SearchModal from './SearchModal';
|
|
@@ -19,11 +19,12 @@ import AskModal from './AskModal';
|
|
|
19
19
|
import SettingsModal from './SettingsModal';
|
|
20
20
|
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
21
21
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
22
|
-
import { useAskModal } from '@/hooks/useAskModal';
|
|
23
22
|
import { FileNode } from '@/lib/types';
|
|
24
23
|
import { useLocale } from '@/lib/LocaleContext';
|
|
25
24
|
import { WalkthroughProvider } from './walkthrough';
|
|
26
25
|
import McpProvider from '@/hooks/useMcpData';
|
|
26
|
+
import { useLeftPanel } from '@/hooks/useLeftPanel';
|
|
27
|
+
import { useAskPanel } from '@/hooks/useAskPanel';
|
|
27
28
|
import type { Tab } from './settings/types';
|
|
28
29
|
|
|
29
30
|
interface SidebarLayoutProps {
|
|
@@ -32,130 +33,22 @@ interface SidebarLayoutProps {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps) {
|
|
35
|
-
|
|
36
|
-
const
|
|
36
|
+
// ── Left panel state (extracted hook) ──
|
|
37
|
+
const lp = useLeftPanel();
|
|
38
|
+
|
|
39
|
+
// ── Right Ask AI panel state (extracted hook) ──
|
|
40
|
+
const ap = useAskPanel();
|
|
37
41
|
|
|
38
|
-
// Settings modal
|
|
42
|
+
// ── Settings modal ──
|
|
39
43
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
40
44
|
const [settingsTab, setSettingsTab] = useState<Tab | undefined>(undefined);
|
|
41
45
|
|
|
42
|
-
//
|
|
43
|
-
const [railExpanded, setRailExpanded] = useState(false);
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
try {
|
|
46
|
-
if (localStorage.getItem('rail-expanded') === 'true') setRailExpanded(true);
|
|
47
|
-
} catch {}
|
|
48
|
-
}, []);
|
|
49
|
-
|
|
50
|
-
// Panel width state — shared across all left panels
|
|
51
|
-
const [panelWidth, setPanelWidth] = useState<number | null>(null);
|
|
52
|
-
const [panelMaximized, setPanelMaximized] = useState(false);
|
|
53
|
-
|
|
54
|
-
// Load persisted panel width when activePanel changes
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!activePanel) return;
|
|
57
|
-
try {
|
|
58
|
-
const stored = localStorage.getItem('left-panel-width');
|
|
59
|
-
if (stored) {
|
|
60
|
-
const w = parseInt(stored, 10);
|
|
61
|
-
if (w >= MIN_PANEL_WIDTH && w <= MAX_PANEL_WIDTH_ABS) {
|
|
62
|
-
setPanelWidth(w);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch {}
|
|
67
|
-
setPanelWidth(280);
|
|
68
|
-
}, [activePanel]);
|
|
69
|
-
|
|
70
|
-
// Exit maximize when switching panels
|
|
71
|
-
useEffect(() => { setPanelMaximized(false); }, [activePanel]);
|
|
72
|
-
|
|
73
|
-
const handlePanelWidthChange = useCallback((w: number) => {
|
|
74
|
-
setPanelWidth(w);
|
|
75
|
-
}, []);
|
|
76
|
-
|
|
77
|
-
const handlePanelWidthCommit = useCallback((w: number) => {
|
|
78
|
-
try { localStorage.setItem('left-panel-width', String(w)); } catch {}
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const handlePanelMaximize = useCallback(() => {
|
|
82
|
-
setPanelMaximized(v => !v);
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
// ── Right-side Ask AI panel state (independent of left panel) ──
|
|
86
|
-
const [askPanelOpen, setAskPanelOpen] = useState(false);
|
|
87
|
-
const [askPanelWidth, setAskPanelWidth] = useState(RIGHT_ASK_DEFAULT_WIDTH);
|
|
88
|
-
const [askMode, setAskMode] = useState<'panel' | 'popup'>('panel');
|
|
89
|
-
// Desktop popup (distinct from mobileAskOpen)
|
|
90
|
-
const [desktopAskPopupOpen, setDesktopAskPopupOpen] = useState(false);
|
|
91
|
-
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
try {
|
|
94
|
-
const stored = localStorage.getItem('right-ask-panel-width');
|
|
95
|
-
if (stored) {
|
|
96
|
-
const w = parseInt(stored, 10);
|
|
97
|
-
if (w >= RIGHT_ASK_MIN_WIDTH && w <= RIGHT_ASK_MAX_WIDTH) setAskPanelWidth(w);
|
|
98
|
-
}
|
|
99
|
-
const mode = localStorage.getItem('ask-mode');
|
|
100
|
-
if (mode === 'popup') setAskMode('popup');
|
|
101
|
-
} catch {}
|
|
102
|
-
|
|
103
|
-
// Listen for Settings → AskDisplayMode changes
|
|
104
|
-
const onStorage = (e: StorageEvent) => {
|
|
105
|
-
if (e.key === 'ask-mode' && (e.newValue === 'panel' || e.newValue === 'popup')) {
|
|
106
|
-
setAskMode(e.newValue);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
window.addEventListener('storage', onStorage);
|
|
110
|
-
return () => window.removeEventListener('storage', onStorage);
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
const handleAskWidthChange = useCallback((w: number) => { setAskPanelWidth(w); }, []);
|
|
114
|
-
const handleAskWidthCommit = useCallback((w: number) => {
|
|
115
|
-
try { localStorage.setItem('right-ask-panel-width', String(w)); } catch {}
|
|
116
|
-
}, []);
|
|
117
|
-
|
|
118
|
-
const toggleAskPanel = useCallback(() => {
|
|
119
|
-
if (askMode === 'popup') {
|
|
120
|
-
setDesktopAskPopupOpen(v => {
|
|
121
|
-
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
122
|
-
return !v;
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
setAskPanelOpen(v => {
|
|
126
|
-
if (!v) { setAskInitialMessage(''); setAskOpenSource('user'); }
|
|
127
|
-
return !v;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}, [askMode]);
|
|
131
|
-
|
|
132
|
-
const closeAskPanel = useCallback(() => { setAskPanelOpen(false); }, []);
|
|
133
|
-
const closeDesktopAskPopup = useCallback(() => { setDesktopAskPopupOpen(false); }, []);
|
|
134
|
-
|
|
135
|
-
// Switch between panel ↔ popup mode
|
|
136
|
-
const handleAskModeSwitch = useCallback(() => {
|
|
137
|
-
setAskMode(prev => {
|
|
138
|
-
const next = prev === 'panel' ? 'popup' : 'panel';
|
|
139
|
-
try {
|
|
140
|
-
localStorage.setItem('ask-mode', next);
|
|
141
|
-
window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
|
|
142
|
-
} catch {}
|
|
143
|
-
if (next === 'popup') {
|
|
144
|
-
setAskPanelOpen(false);
|
|
145
|
-
setDesktopAskPopupOpen(true);
|
|
146
|
-
} else {
|
|
147
|
-
setDesktopAskPopupOpen(false);
|
|
148
|
-
setAskPanelOpen(true);
|
|
149
|
-
}
|
|
150
|
-
return next;
|
|
151
|
-
});
|
|
152
|
-
}, []);
|
|
153
|
-
|
|
154
|
-
// Sync popover state
|
|
46
|
+
// ── Sync popover ──
|
|
155
47
|
const [syncPopoverOpen, setSyncPopoverOpen] = useState(false);
|
|
156
48
|
const [syncAnchorRect, setSyncAnchorRect] = useState<DOMRect | null>(null);
|
|
157
49
|
|
|
158
|
-
// Mobile
|
|
50
|
+
// ── Mobile state ──
|
|
51
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
159
52
|
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
|
160
53
|
const [mobileAskOpen, setMobileAskOpen] = useState(false);
|
|
161
54
|
|
|
@@ -163,24 +56,14 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
163
56
|
const router = useRouter();
|
|
164
57
|
const pathname = usePathname();
|
|
165
58
|
const { status: syncStatus, fetchStatus: syncStatusRefresh } = useSyncStatus();
|
|
166
|
-
const askModal = useAskModal();
|
|
167
59
|
|
|
168
60
|
const currentFile = pathname.startsWith('/view/')
|
|
169
61
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
170
62
|
: undefined;
|
|
171
63
|
|
|
172
|
-
//
|
|
173
|
-
const [askInitialMessage, setAskInitialMessage] = useState('');
|
|
174
|
-
const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
|
|
64
|
+
// ── Event listeners ──
|
|
175
65
|
|
|
176
|
-
//
|
|
177
|
-
const handleExpandedChange = useCallback((expanded: boolean) => {
|
|
178
|
-
setRailExpanded(expanded);
|
|
179
|
-
setSyncPopoverOpen(false);
|
|
180
|
-
try { localStorage.setItem('rail-expanded', String(expanded)); } catch {}
|
|
181
|
-
}, []);
|
|
182
|
-
|
|
183
|
-
// Listen for cross-component "open settings" events (e.g. from UpdateBanner)
|
|
66
|
+
// Listen for cross-component "open settings" events
|
|
184
67
|
useEffect(() => {
|
|
185
68
|
const handler = (e: Event) => {
|
|
186
69
|
const tab = (e as CustomEvent).detail?.tab;
|
|
@@ -191,33 +74,19 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
191
74
|
return () => window.removeEventListener('mindos:open-settings', handler);
|
|
192
75
|
}, []);
|
|
193
76
|
|
|
194
|
-
// Bridge useAskModal store → right Ask panel or popup
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
if (askModal.open) {
|
|
197
|
-
setAskInitialMessage(askModal.initialMessage);
|
|
198
|
-
setAskOpenSource(askModal.source);
|
|
199
|
-
if (askMode === 'popup') {
|
|
200
|
-
setDesktopAskPopupOpen(true);
|
|
201
|
-
} else {
|
|
202
|
-
setAskPanelOpen(true);
|
|
203
|
-
}
|
|
204
|
-
askModal.close();
|
|
205
|
-
}
|
|
206
|
-
}, [askModal.open, askModal.initialMessage, askModal.source, askModal.close, askMode]);
|
|
207
|
-
|
|
208
77
|
// GuideCard first message handler
|
|
209
78
|
const handleFirstMessage = useCallback(() => {
|
|
210
79
|
const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
|
|
211
|
-
if (askOpenSource === 'guide') {
|
|
80
|
+
if (ap.askOpenSource === 'guide') {
|
|
212
81
|
fetch('/api/setup', {
|
|
213
82
|
method: 'PATCH',
|
|
214
83
|
headers: { 'Content-Type': 'application/json' },
|
|
215
84
|
body: JSON.stringify({ guideState: { askedAI: true } }),
|
|
216
85
|
}).then(notifyGuide).catch((err) => console.warn('Guide state update failed:', err));
|
|
217
|
-
} else if (askOpenSource === 'guide-next') {
|
|
86
|
+
} else if (ap.askOpenSource === 'guide-next') {
|
|
218
87
|
notifyGuide();
|
|
219
88
|
}
|
|
220
|
-
}, [askOpenSource]);
|
|
89
|
+
}, [ap.askOpenSource]);
|
|
221
90
|
|
|
222
91
|
// Close mobile drawer on route change
|
|
223
92
|
useEffect(() => { setMobileOpen(false); }, [pathname]);
|
|
@@ -240,16 +109,15 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
240
109
|
// Unified keyboard shortcuts
|
|
241
110
|
useEffect(() => {
|
|
242
111
|
const handler = (e: KeyboardEvent) => {
|
|
243
|
-
// ESC exits panel maximize or closes right Ask panel/popup
|
|
244
112
|
if (e.key === 'Escape') {
|
|
245
|
-
if (panelMaximized) {
|
|
246
|
-
if (askPanelOpen) {
|
|
247
|
-
if (desktopAskPopupOpen) {
|
|
113
|
+
if (lp.panelMaximized) { lp.handlePanelMaximize(); return; }
|
|
114
|
+
if (ap.askPanelOpen) { ap.closeAskPanel(); return; }
|
|
115
|
+
if (ap.desktopAskPopupOpen) { ap.closeDesktopAskPopup(); return; }
|
|
248
116
|
}
|
|
249
117
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
250
118
|
e.preventDefault();
|
|
251
119
|
if (window.innerWidth >= 768) {
|
|
252
|
-
setActivePanel(p => p === 'search' ? null : 'search');
|
|
120
|
+
lp.setActivePanel((p: PanelId | null) => p === 'search' ? null : 'search');
|
|
253
121
|
} else {
|
|
254
122
|
setMobileSearchOpen(v => !v);
|
|
255
123
|
}
|
|
@@ -257,7 +125,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
257
125
|
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
|
258
126
|
e.preventDefault();
|
|
259
127
|
if (window.innerWidth >= 768) {
|
|
260
|
-
toggleAskPanel();
|
|
128
|
+
ap.toggleAskPanel();
|
|
261
129
|
} else {
|
|
262
130
|
setMobileAskOpen(v => !v);
|
|
263
131
|
}
|
|
@@ -269,8 +137,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
269
137
|
};
|
|
270
138
|
window.addEventListener('keydown', handler);
|
|
271
139
|
return () => window.removeEventListener('keydown', handler);
|
|
272
|
-
}, [panelMaximized, askPanelOpen, desktopAskPopupOpen, toggleAskPanel]);
|
|
140
|
+
}, [lp.panelMaximized, ap.askPanelOpen, ap.desktopAskPopupOpen, ap.toggleAskPanel, lp]);
|
|
273
141
|
|
|
142
|
+
// ── Settings helpers ──
|
|
274
143
|
const openSyncSettings = useCallback(() => {
|
|
275
144
|
setSettingsTab('sync');
|
|
276
145
|
setSyncPopoverOpen(false);
|
|
@@ -282,26 +151,20 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
282
151
|
setSettingsTab(undefined);
|
|
283
152
|
}, []);
|
|
284
153
|
|
|
285
|
-
const openSettingsTab = useCallback((tab: Tab) => {
|
|
286
|
-
setSettingsTab(tab);
|
|
287
|
-
setSettingsOpen(true);
|
|
288
|
-
}, []);
|
|
289
|
-
|
|
290
154
|
const closeSettings = useCallback(() => {
|
|
291
155
|
setSettingsOpen(false);
|
|
292
156
|
setSettingsTab(undefined);
|
|
293
157
|
}, []);
|
|
294
158
|
|
|
295
|
-
const closeSyncPopover = useCallback(() => setSyncPopoverOpen(false), []);
|
|
296
|
-
|
|
297
159
|
const handleSyncClick = useCallback((rect: DOMRect) => {
|
|
298
160
|
setSyncAnchorRect(rect);
|
|
299
161
|
setSyncPopoverOpen(prev => !prev);
|
|
300
162
|
}, []);
|
|
301
163
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
164
|
+
const handleExpandedChange = useCallback((expanded: boolean) => {
|
|
165
|
+
lp.handleExpandedChange(expanded);
|
|
166
|
+
setSyncPopoverOpen(false);
|
|
167
|
+
}, [lp]);
|
|
305
168
|
|
|
306
169
|
return (
|
|
307
170
|
<WalkthroughProvider>
|
|
@@ -318,93 +181,78 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
318
181
|
|
|
319
182
|
{/* ── Desktop: Activity Bar + Panel ── */}
|
|
320
183
|
<ActivityBar
|
|
321
|
-
activePanel={activePanel}
|
|
322
|
-
onPanelChange={setActivePanel}
|
|
184
|
+
activePanel={lp.activePanel}
|
|
185
|
+
onPanelChange={lp.setActivePanel}
|
|
323
186
|
syncStatus={syncStatus}
|
|
324
|
-
expanded={railExpanded}
|
|
187
|
+
expanded={lp.railExpanded}
|
|
325
188
|
onExpandedChange={handleExpandedChange}
|
|
326
189
|
onSettingsClick={handleSettingsClick}
|
|
327
190
|
onSyncClick={handleSyncClick}
|
|
328
191
|
/>
|
|
329
192
|
|
|
330
193
|
<Panel
|
|
331
|
-
activePanel={activePanel}
|
|
194
|
+
activePanel={lp.activePanel}
|
|
332
195
|
fileTree={fileTree}
|
|
333
196
|
onNavigate={() => {}}
|
|
334
197
|
onOpenSyncSettings={openSyncSettings}
|
|
335
|
-
railWidth={railWidth}
|
|
336
|
-
panelWidth={panelWidth ?? undefined}
|
|
337
|
-
onWidthChange={handlePanelWidthChange}
|
|
338
|
-
onWidthCommit={handlePanelWidthCommit}
|
|
339
|
-
maximized={panelMaximized}
|
|
340
|
-
onMaximize={handlePanelMaximize}
|
|
198
|
+
railWidth={lp.railWidth}
|
|
199
|
+
panelWidth={lp.panelWidth ?? undefined}
|
|
200
|
+
onWidthChange={lp.handlePanelWidthChange}
|
|
201
|
+
onWidthCommit={lp.handlePanelWidthCommit}
|
|
202
|
+
maximized={lp.panelMaximized}
|
|
203
|
+
onMaximize={lp.handlePanelMaximize}
|
|
341
204
|
>
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
<SearchPanel active={activePanel === 'search'} maximized={panelMaximized} onMaximize={handlePanelMaximize} />
|
|
205
|
+
<div className={`flex flex-col h-full ${lp.activePanel === 'search' ? '' : 'hidden'}`}>
|
|
206
|
+
<SearchPanel active={lp.activePanel === 'search'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
|
|
345
207
|
</div>
|
|
346
|
-
<div className={`flex flex-col h-full ${activePanel === 'plugins' ? '' : 'hidden'}`}>
|
|
347
|
-
<PluginsPanel active={activePanel === 'plugins'} maximized={panelMaximized} onMaximize={handlePanelMaximize} />
|
|
208
|
+
<div className={`flex flex-col h-full ${lp.activePanel === 'plugins' ? '' : 'hidden'}`}>
|
|
209
|
+
<PluginsPanel active={lp.activePanel === 'plugins'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
|
|
348
210
|
</div>
|
|
349
|
-
<div className={`flex flex-col h-full ${activePanel === 'agents' ? '' : 'hidden'}`}>
|
|
350
|
-
<AgentsPanel
|
|
351
|
-
active={activePanel === 'agents'}
|
|
352
|
-
maximized={panelMaximized}
|
|
353
|
-
onMaximize={handlePanelMaximize}
|
|
354
|
-
/>
|
|
211
|
+
<div className={`flex flex-col h-full ${lp.activePanel === 'agents' ? '' : 'hidden'}`}>
|
|
212
|
+
<AgentsPanel active={lp.activePanel === 'agents'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
|
|
355
213
|
</div>
|
|
356
214
|
</Panel>
|
|
357
215
|
|
|
358
|
-
{/* ── Right-side Ask AI Panel
|
|
216
|
+
{/* ── Right-side Ask AI Panel ── */}
|
|
359
217
|
<RightAskPanel
|
|
360
|
-
open={askPanelOpen}
|
|
361
|
-
onClose={closeAskPanel}
|
|
218
|
+
open={ap.askPanelOpen}
|
|
219
|
+
onClose={ap.closeAskPanel}
|
|
362
220
|
currentFile={currentFile}
|
|
363
|
-
initialMessage={askInitialMessage}
|
|
221
|
+
initialMessage={ap.askInitialMessage}
|
|
364
222
|
onFirstMessage={handleFirstMessage}
|
|
365
|
-
width={askPanelWidth}
|
|
366
|
-
onWidthChange={handleAskWidthChange}
|
|
367
|
-
onWidthCommit={handleAskWidthCommit}
|
|
368
|
-
askMode={askMode}
|
|
369
|
-
onModeSwitch={handleAskModeSwitch}
|
|
223
|
+
width={ap.askPanelWidth}
|
|
224
|
+
onWidthChange={ap.handleAskWidthChange}
|
|
225
|
+
onWidthCommit={ap.handleAskWidthCommit}
|
|
226
|
+
askMode={ap.askMode}
|
|
227
|
+
onModeSwitch={ap.handleAskModeSwitch}
|
|
370
228
|
/>
|
|
371
229
|
|
|
372
|
-
{/* ── Desktop Ask AI Popup (popup mode) ── */}
|
|
373
230
|
<AskModal
|
|
374
|
-
open={desktopAskPopupOpen}
|
|
375
|
-
onClose={closeDesktopAskPopup}
|
|
231
|
+
open={ap.desktopAskPopupOpen}
|
|
232
|
+
onClose={ap.closeDesktopAskPopup}
|
|
376
233
|
currentFile={currentFile}
|
|
377
|
-
initialMessage={askInitialMessage}
|
|
234
|
+
initialMessage={ap.askInitialMessage}
|
|
378
235
|
onFirstMessage={handleFirstMessage}
|
|
379
|
-
askMode={askMode}
|
|
380
|
-
onModeSwitch={handleAskModeSwitch}
|
|
236
|
+
askMode={ap.askMode}
|
|
237
|
+
onModeSwitch={ap.handleAskModeSwitch}
|
|
381
238
|
/>
|
|
382
239
|
|
|
383
|
-
{
|
|
384
|
-
<AskFab onToggle={toggleAskPanel} askPanelOpen={askPanelOpen || desktopAskPopupOpen} />
|
|
385
|
-
|
|
386
|
-
{/* ── Keyboard Shortcuts (⌘?) ── */}
|
|
240
|
+
<AskFab onToggle={ap.toggleAskPanel} askPanelOpen={ap.askPanelOpen || ap.desktopAskPopupOpen} />
|
|
387
241
|
<KeyboardShortcuts />
|
|
388
242
|
|
|
389
|
-
{
|
|
390
|
-
<SettingsModal
|
|
391
|
-
open={settingsOpen}
|
|
392
|
-
onClose={closeSettings}
|
|
393
|
-
initialTab={settingsTab}
|
|
394
|
-
/>
|
|
243
|
+
<SettingsModal open={settingsOpen} onClose={closeSettings} initialTab={settingsTab} />
|
|
395
244
|
|
|
396
|
-
{/* ── Sync Popover ── */}
|
|
397
245
|
<SyncPopover
|
|
398
246
|
open={syncPopoverOpen}
|
|
399
|
-
onClose={
|
|
247
|
+
onClose={() => setSyncPopoverOpen(false)}
|
|
400
248
|
anchorRect={syncAnchorRect}
|
|
401
|
-
railWidth={railWidth}
|
|
249
|
+
railWidth={lp.railWidth}
|
|
402
250
|
onOpenSyncSettings={openSyncSettings}
|
|
403
251
|
syncStatus={syncStatus}
|
|
404
252
|
onSyncStatusRefresh={syncStatusRefresh}
|
|
405
253
|
/>
|
|
406
254
|
|
|
407
|
-
{/* ── Mobile
|
|
255
|
+
{/* ── Mobile ── */}
|
|
408
256
|
<header className="md:hidden fixed top-0 left-0 right-0 z-30 bg-card border-b border-border flex items-center justify-between px-3 py-2" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
|
|
409
257
|
<button onClick={() => setMobileOpen(true)} className="p-3 -ml-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label="Open menu">
|
|
410
258
|
<Menu size={20} />
|
|
@@ -414,11 +262,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
414
262
|
<span className="font-semibold text-foreground text-sm tracking-wide">MindOS</span>
|
|
415
263
|
</Link>
|
|
416
264
|
<div className="flex items-center gap-0.5">
|
|
417
|
-
<button
|
|
418
|
-
onClick={openSyncSettings}
|
|
419
|
-
className="p-3 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent flex items-center justify-center"
|
|
420
|
-
aria-label="Sync status"
|
|
421
|
-
>
|
|
265
|
+
<button onClick={openSyncSettings} className="p-3 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent flex items-center justify-center" aria-label="Sync status">
|
|
422
266
|
<MobileSyncDot status={syncStatus} />
|
|
423
267
|
</button>
|
|
424
268
|
<button onClick={() => setMobileSearchOpen(true)} className="p-3 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.searchTitle}>
|
|
@@ -430,7 +274,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
430
274
|
</div>
|
|
431
275
|
</header>
|
|
432
276
|
|
|
433
|
-
{/* ── Mobile: Drawer overlay ── */}
|
|
434
277
|
{mobileOpen && <div className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />}
|
|
435
278
|
<aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
|
436
279
|
<div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
|
|
@@ -447,26 +290,18 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
447
290
|
</div>
|
|
448
291
|
</aside>
|
|
449
292
|
|
|
450
|
-
{/* ── Mobile: Modals (preserved for <768px) ── */}
|
|
451
293
|
<SearchModal open={mobileSearchOpen} onClose={() => setMobileSearchOpen(false)} />
|
|
452
294
|
<AskModal open={mobileAskOpen} onClose={() => setMobileAskOpen(false)} currentFile={currentFile} />
|
|
453
295
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
id="main-content"
|
|
457
|
-
className="min-h-screen transition-all duration-200 pt-[52px] md:pt-0"
|
|
458
|
-
>
|
|
459
|
-
<div className="min-h-screen bg-background">
|
|
460
|
-
{children}
|
|
461
|
-
</div>
|
|
296
|
+
<main id="main-content" className="min-h-screen transition-all duration-200 pt-[52px] md:pt-0">
|
|
297
|
+
<div className="min-h-screen bg-background">{children}</div>
|
|
462
298
|
</main>
|
|
463
299
|
|
|
464
|
-
{/* Desktop padding via <style> — avoids hydration mismatch from window checks */}
|
|
465
300
|
<style>{`
|
|
466
301
|
@media (min-width: 768px) {
|
|
467
|
-
:root { --right-panel-width: ${askPanelOpen ? askPanelWidth : 0}px; }
|
|
302
|
+
:root { --right-panel-width: ${ap.askPanelOpen ? ap.askPanelWidth : 0}px; }
|
|
468
303
|
#main-content {
|
|
469
|
-
padding-left: ${panelOpen && panelMaximized ? '100vw' : `${panelOpen ? railWidth + effectivePanelWidth : railWidth}px`} !important;
|
|
304
|
+
padding-left: ${lp.panelOpen && lp.panelMaximized ? '100vw' : `${lp.panelOpen ? lp.railWidth + lp.effectivePanelWidth : lp.railWidth}px`} !important;
|
|
470
305
|
padding-right: var(--right-panel-width) !important;
|
|
471
306
|
padding-top: 0 !important;
|
|
472
307
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { X, Plus, Loader2, AlertCircle } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
const skillFrontmatter = (n: string) => `---
|
|
7
|
+
name: ${n}
|
|
8
|
+
description: >
|
|
9
|
+
Describe WHEN the agent should use this
|
|
10
|
+
skill. Be specific about trigger conditions.
|
|
11
|
+
---`;
|
|
12
|
+
|
|
13
|
+
const SKILL_TEMPLATES: Record<string, (name: string) => string> = {
|
|
14
|
+
general: (n) => `${skillFrontmatter(n)}
|
|
15
|
+
|
|
16
|
+
# Instructions
|
|
17
|
+
|
|
18
|
+
## Context
|
|
19
|
+
<!-- Background knowledge for the agent -->
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
1.
|
|
23
|
+
2.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
<!-- Constraints, edge cases, formats -->
|
|
27
|
+
- `,
|
|
28
|
+
|
|
29
|
+
'tool-use': (n) => `${skillFrontmatter(n)}
|
|
30
|
+
|
|
31
|
+
# Instructions
|
|
32
|
+
|
|
33
|
+
## Available Tools
|
|
34
|
+
<!-- List tools the agent can use -->
|
|
35
|
+
-
|
|
36
|
+
|
|
37
|
+
## When to Use
|
|
38
|
+
<!-- Conditions that trigger this skill -->
|
|
39
|
+
|
|
40
|
+
## Output Format
|
|
41
|
+
<!-- Expected response structure -->
|
|
42
|
+
`,
|
|
43
|
+
|
|
44
|
+
workflow: (n) => `${skillFrontmatter(n)}
|
|
45
|
+
|
|
46
|
+
# Instructions
|
|
47
|
+
|
|
48
|
+
## Trigger
|
|
49
|
+
<!-- What triggers this workflow -->
|
|
50
|
+
|
|
51
|
+
## Steps
|
|
52
|
+
1.
|
|
53
|
+
2.
|
|
54
|
+
|
|
55
|
+
## Validation
|
|
56
|
+
<!-- How to verify success -->
|
|
57
|
+
|
|
58
|
+
## Rollback
|
|
59
|
+
<!-- What to do on failure -->
|
|
60
|
+
`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
interface SkillCreateFormProps {
|
|
64
|
+
onSave: (name: string, content: string) => Promise<void>;
|
|
65
|
+
onCancel: () => void;
|
|
66
|
+
saving: boolean;
|
|
67
|
+
error: string;
|
|
68
|
+
m: Record<string, any> | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function SkillCreateForm({ onSave, onCancel, saving, error, m }: SkillCreateFormProps) {
|
|
72
|
+
const [newName, setNewName] = useState('');
|
|
73
|
+
const [newContent, setNewContent] = useState('');
|
|
74
|
+
const [selectedTemplate, setSelectedTemplate] = useState<'general' | 'tool-use' | 'workflow'>('general');
|
|
75
|
+
|
|
76
|
+
const getTemplate = (skillName: string, tmpl?: 'general' | 'tool-use' | 'workflow') => {
|
|
77
|
+
const key = tmpl || selectedTemplate;
|
|
78
|
+
const fn = SKILL_TEMPLATES[key] || SKILL_TEMPLATES.general;
|
|
79
|
+
return fn(skillName || 'my-skill');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleNameChange = (val: string) => {
|
|
83
|
+
const cleaned = val.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
84
|
+
const oldTemplate = getTemplate(newName || 'my-skill');
|
|
85
|
+
if (!newContent || newContent === oldTemplate) {
|
|
86
|
+
setNewContent(getTemplate(cleaned || 'my-skill'));
|
|
87
|
+
}
|
|
88
|
+
setNewName(cleaned);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleTemplateChange = (tmpl: 'general' | 'tool-use' | 'workflow') => {
|
|
92
|
+
const oldTemplate = getTemplate(newName || 'my-skill', selectedTemplate);
|
|
93
|
+
setSelectedTemplate(tmpl);
|
|
94
|
+
if (!newContent || newContent === oldTemplate) {
|
|
95
|
+
setNewContent(getTemplate(newName || 'my-skill', tmpl));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Initialize content on first render
|
|
100
|
+
if (!newContent) {
|
|
101
|
+
// Use a timeout-free approach: set default on next tick won't work in render.
|
|
102
|
+
// Instead, initialize via useState default or useEffect. For simplicity, set inline.
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="border border-border rounded-lg p-3 space-y-2">
|
|
107
|
+
<div className="flex items-center justify-between">
|
|
108
|
+
<span className="text-xs font-medium">{m?.addSkill ?? '+ Add Skill'}</span>
|
|
109
|
+
<button onClick={onCancel} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
|
|
110
|
+
<X size={12} />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="space-y-1">
|
|
114
|
+
<label className="text-2xs text-muted-foreground">{m?.skillName ?? 'Name'}</label>
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
value={newName}
|
|
118
|
+
onChange={e => handleNameChange(e.target.value)}
|
|
119
|
+
placeholder="my-skill"
|
|
120
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="space-y-1">
|
|
124
|
+
<label className="text-2xs text-muted-foreground">{m?.skillTemplate ?? 'Template'}</label>
|
|
125
|
+
<div className="flex rounded-md border border-border overflow-hidden w-fit">
|
|
126
|
+
{(['general', 'tool-use', 'workflow'] as const).map((tmpl, i) => (
|
|
127
|
+
<button
|
|
128
|
+
key={tmpl}
|
|
129
|
+
onClick={() => handleTemplateChange(tmpl)}
|
|
130
|
+
className={`px-2.5 py-1 text-xs transition-colors ${i > 0 ? 'border-l border-border' : ''} ${
|
|
131
|
+
selectedTemplate === tmpl
|
|
132
|
+
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
133
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
{tmpl === 'general' ? (m?.skillTemplateGeneral ?? 'General')
|
|
137
|
+
: tmpl === 'tool-use' ? (m?.skillTemplateToolUse ?? 'Tool-use')
|
|
138
|
+
: (m?.skillTemplateWorkflow ?? 'Workflow')}
|
|
139
|
+
</button>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="space-y-1">
|
|
144
|
+
<label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
|
|
145
|
+
<textarea
|
|
146
|
+
value={newContent || getTemplate(newName || 'my-skill')}
|
|
147
|
+
onChange={e => setNewContent(e.target.value)}
|
|
148
|
+
rows={16}
|
|
149
|
+
placeholder="Skill instructions (markdown)..."
|
|
150
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring resize-y font-mono"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
{error && (
|
|
154
|
+
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
155
|
+
<AlertCircle size={10} />
|
|
156
|
+
{error}
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
<button
|
|
161
|
+
onClick={() => onSave(newName.trim(), newContent || getTemplate(newName.trim() || 'my-skill'))}
|
|
162
|
+
disabled={!newName.trim() || saving}
|
|
163
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
164
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
165
|
+
>
|
|
166
|
+
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
167
|
+
{m?.saveSkill ?? 'Save'}
|
|
168
|
+
</button>
|
|
169
|
+
<button
|
|
170
|
+
onClick={onCancel}
|
|
171
|
+
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
172
|
+
>
|
|
173
|
+
{m?.cancelSkill ?? 'Cancel'}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|