@geminilight/mindos 0.5.36 → 0.5.38
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 +5 -2
- package/app/components/RightAskPanel.tsx +25 -10
- package/app/components/SidebarLayout.tsx +70 -235
- package/app/components/panels/PluginsPanel.tsx +16 -14
- package/app/components/settings/KnowledgeTab.tsx +29 -1
- 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/i18n-en.ts +1 -0
- package/app/lib/i18n-zh.ts +1 -0
- package/package.json +1 -1
- package/scripts/release.sh +18 -4
package/app/app/globals.css
CHANGED
|
@@ -75,7 +75,7 @@ body {
|
|
|
75
75
|
--ring: var(--amber);
|
|
76
76
|
--radius: 0.5rem;
|
|
77
77
|
--amber: #c8873a;
|
|
78
|
-
--amber-dim: rgba(200, 135, 58, 0.
|
|
78
|
+
--amber-dim: rgba(200, 135, 58, 0.18);
|
|
79
79
|
--amber-subtle: rgba(200, 135, 30, 0.08);
|
|
80
80
|
--amber-foreground: #131210;
|
|
81
81
|
--success: #7aad80;
|
|
@@ -110,7 +110,7 @@ body {
|
|
|
110
110
|
--input: rgba(232, 228, 220, 0.1);
|
|
111
111
|
--ring: var(--amber);
|
|
112
112
|
--amber: #d4954a;
|
|
113
|
-
--amber-dim: rgba(212, 149, 74, 0.
|
|
113
|
+
--amber-dim: rgba(212, 149, 74, 0.20);
|
|
114
114
|
--amber-subtle: rgba(212, 149, 74, 0.10);
|
|
115
115
|
--amber-foreground: #131210;
|
|
116
116
|
--success: #7aad80;
|
|
@@ -300,6 +300,9 @@ body {
|
|
|
300
300
|
backdrop-filter: blur(8px);
|
|
301
301
|
-webkit-backdrop-filter: blur(8px);
|
|
302
302
|
}
|
|
303
|
+
.dark .modal-backdrop {
|
|
304
|
+
background: rgba(0, 0, 0, 0.65);
|
|
305
|
+
}
|
|
303
306
|
|
|
304
307
|
/* Micro type scale: text-2xs = 10px (between nothing and text-xs 12px) */
|
|
305
308
|
@layer utilities {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { AlertCircle } from 'lucide-react';
|
|
3
4
|
import AskContent from '@/components/ask/AskContent';
|
|
5
|
+
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
4
6
|
import { useResizeDrag } from '@/hooks/useResizeDrag';
|
|
5
7
|
|
|
6
8
|
const DEFAULT_WIDTH = 380;
|
|
@@ -47,16 +49,29 @@ export default function RightAskPanel({
|
|
|
47
49
|
role="complementary"
|
|
48
50
|
aria-label="MindOS Agent panel"
|
|
49
51
|
>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
<ErrorBoundary fallback={
|
|
53
|
+
<div className="flex flex-col items-center justify-center h-full gap-3 px-6 text-center">
|
|
54
|
+
<AlertCircle size={20} className="text-muted-foreground" />
|
|
55
|
+
<p className="text-sm text-muted-foreground">AI panel encountered an error.</p>
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => window.location.reload()}
|
|
58
|
+
className="text-xs px-3 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
59
|
+
>
|
|
60
|
+
Reload page
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
}>
|
|
64
|
+
<AskContent
|
|
65
|
+
visible={open}
|
|
66
|
+
variant="panel"
|
|
67
|
+
currentFile={open ? currentFile : undefined}
|
|
68
|
+
initialMessage={initialMessage}
|
|
69
|
+
onFirstMessage={onFirstMessage}
|
|
70
|
+
onClose={onClose}
|
|
71
|
+
askMode={askMode}
|
|
72
|
+
onModeSwitch={onModeSwitch}
|
|
73
|
+
/>
|
|
74
|
+
</ErrorBoundary>
|
|
60
75
|
|
|
61
76
|
{/* Drag resize handle — LEFT edge */}
|
|
62
77
|
<div
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
5
|
import { getAllRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
|
|
6
6
|
import { Toggle } from '../settings/Primitives';
|
|
@@ -27,25 +27,25 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
27
27
|
setMounted(true);
|
|
28
28
|
}, []);
|
|
29
29
|
|
|
30
|
-
// Check which entry files exist
|
|
30
|
+
// Check which entry files exist — fetch once on mount, cache result
|
|
31
|
+
const fetchedRef = useRef(false);
|
|
31
32
|
useEffect(() => {
|
|
32
|
-
if (!mounted ||
|
|
33
|
+
if (!mounted || fetchedRef.current) return;
|
|
34
|
+
fetchedRef.current = true;
|
|
33
35
|
const entryPaths = getAllRenderers()
|
|
34
36
|
.map(r => r.entryPath)
|
|
35
37
|
.filter((p): p is string => !!p);
|
|
36
38
|
if (entryPaths.length === 0) return;
|
|
37
39
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
}, [mounted, active]);
|
|
40
|
+
// Single request: fetch all file paths and check which entry paths exist
|
|
41
|
+
fetch('/api/files')
|
|
42
|
+
.then(r => r.ok ? r.json() : [])
|
|
43
|
+
.then((allPaths: string[]) => {
|
|
44
|
+
const pathSet = new Set(allPaths);
|
|
45
|
+
setExistingFiles(new Set(entryPaths.filter(p => pathSet.has(p))));
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
}, [mounted]);
|
|
49
49
|
|
|
50
50
|
const renderers = mounted ? getAllRenderers() : [];
|
|
51
51
|
const enabledCount = mounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
|
|
@@ -86,7 +86,9 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
86
86
|
${!enabled ? 'opacity-50' : ''}
|
|
87
87
|
`}
|
|
88
88
|
onClick={canOpen ? () => handleOpen(r.entryPath!) : undefined}
|
|
89
|
+
onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpen(r.entryPath!); } } : undefined}
|
|
89
90
|
role={canOpen ? 'link' : undefined}
|
|
91
|
+
tabIndex={canOpen ? 0 : undefined}
|
|
90
92
|
>
|
|
91
93
|
{/* Top row: status dot + icon + name + toggle */}
|
|
92
94
|
<div className="flex items-center justify-between gap-2">
|