@geminilight/mindos 0.6.7 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/ask/route.ts +35 -2
- package/app/app/api/file/route.ts +27 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/check-port/route.ts +18 -13
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +104 -60
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -70
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +450 -0
- package/app/hooks/useFileImport.ts +39 -2
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +85 -4
- package/app/lib/i18n-zh.ts +85 -4
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +23 -5
- package/app/package.json +1 -1
- package/bin/cli.js +21 -18
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- package/scripts/release.sh +1 -1
- package/scripts/setup.js +2 -2
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import {
|
|
6
|
+
Check, X, Loader2, Sparkles, AlertCircle, Undo2,
|
|
7
|
+
ChevronDown, FilePlus, FileEdit, ExternalLink,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
+
import type { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
11
|
+
import type { OrganizeStageHint } from '@/hooks/useAiOrganize';
|
|
12
|
+
import { encodePath } from '@/lib/utils';
|
|
13
|
+
import {
|
|
14
|
+
appendEntry, updateEntry, generateEntryId,
|
|
15
|
+
type OrganizeHistoryEntry,
|
|
16
|
+
} from '@/lib/organize-history';
|
|
17
|
+
|
|
18
|
+
const AUTO_DISMISS_MS = 3 * 60 * 1000; // 3 minutes
|
|
19
|
+
const THINKING_TIMEOUT_MS = 5000;
|
|
20
|
+
|
|
21
|
+
type AiOrganize = ReturnType<typeof useAiOrganize>;
|
|
22
|
+
|
|
23
|
+
function stageText(
|
|
24
|
+
t: ReturnType<typeof useLocale>['t'],
|
|
25
|
+
hint: { stage: OrganizeStageHint; detail?: string } | null,
|
|
26
|
+
): string {
|
|
27
|
+
const fi = t.fileImport as Record<string, unknown>;
|
|
28
|
+
if (!hint) return fi.organizeProcessing as string;
|
|
29
|
+
switch (hint.stage) {
|
|
30
|
+
case 'connecting': return fi.organizeConnecting as string;
|
|
31
|
+
case 'analyzing': return fi.organizeAnalyzing as string;
|
|
32
|
+
case 'reading': return (fi.organizeReading as (d?: string) => string)(hint.detail);
|
|
33
|
+
case 'thinking': return fi.organizeThinking as string;
|
|
34
|
+
case 'writing': return (fi.organizeWriting as (d?: string) => string)(hint.detail);
|
|
35
|
+
default: return fi.organizeProcessing as string;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Self-contained timer for organizing phase */
|
|
40
|
+
function useOrganizeTimer(isOrganizing: boolean, stageHint: AiOrganize['stageHint']) {
|
|
41
|
+
const [elapsed, setElapsed] = useState(0);
|
|
42
|
+
const [thinkingOverride, setThinkingOverride] = useState(false);
|
|
43
|
+
const lastEventRef = useRef(Date.now());
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
lastEventRef.current = Date.now();
|
|
47
|
+
setThinkingOverride(false);
|
|
48
|
+
}, [stageHint]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOrganizing) { setElapsed(0); setThinkingOverride(false); return; }
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
setElapsed(e => e + 1);
|
|
54
|
+
if (Date.now() - lastEventRef.current >= THINKING_TIMEOUT_MS) {
|
|
55
|
+
setThinkingOverride(true);
|
|
56
|
+
}
|
|
57
|
+
}, 1000);
|
|
58
|
+
return () => clearInterval(timer);
|
|
59
|
+
}, [isOrganizing]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
elapsed,
|
|
63
|
+
displayHint: thinkingOverride ? { stage: 'thinking' as const } : stageHint,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface OrganizeToastProps {
|
|
68
|
+
aiOrganize: AiOrganize;
|
|
69
|
+
onDismiss: () => void;
|
|
70
|
+
/** Called when AI organize flow should be cancelled entirely */
|
|
71
|
+
onCancel: () => void;
|
|
72
|
+
/** Callback to notify history panel of updates */
|
|
73
|
+
onHistoryUpdate?: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function OrganizeToast({
|
|
77
|
+
aiOrganize, onDismiss, onCancel, onHistoryUpdate,
|
|
78
|
+
}: OrganizeToastProps) {
|
|
79
|
+
const { t } = useLocale();
|
|
80
|
+
const router = useRouter();
|
|
81
|
+
const fi = t.fileImport as Record<string, unknown>;
|
|
82
|
+
|
|
83
|
+
const isOrganizing = aiOrganize.phase === 'organizing';
|
|
84
|
+
const { elapsed, displayHint } = useOrganizeTimer(isOrganizing, aiOrganize.stageHint);
|
|
85
|
+
|
|
86
|
+
const [expanded, setExpanded] = useState(false);
|
|
87
|
+
const [undoing, setUndoing] = useState(false);
|
|
88
|
+
const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
89
|
+
const historyIdRef = useRef<string | null>(null);
|
|
90
|
+
|
|
91
|
+
const isDone = aiOrganize.phase === 'done';
|
|
92
|
+
const isError = aiOrganize.phase === 'error';
|
|
93
|
+
const isActive = isOrganizing || isDone || isError;
|
|
94
|
+
|
|
95
|
+
const okCount = aiOrganize.changes.filter(c => c.ok && !c.undone).length;
|
|
96
|
+
|
|
97
|
+
// Reset historyId when a new organize session starts
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (isOrganizing) {
|
|
100
|
+
historyIdRef.current = null;
|
|
101
|
+
}
|
|
102
|
+
}, [isOrganizing]);
|
|
103
|
+
|
|
104
|
+
// Write to history when organize completes
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (isDone && !historyIdRef.current) {
|
|
107
|
+
const id = generateEntryId();
|
|
108
|
+
historyIdRef.current = id;
|
|
109
|
+
appendEntry({
|
|
110
|
+
id,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
sourceFiles: aiOrganize.sourceFileNames,
|
|
113
|
+
files: aiOrganize.changes.map(c => ({
|
|
114
|
+
action: c.action,
|
|
115
|
+
path: c.path,
|
|
116
|
+
ok: c.ok,
|
|
117
|
+
undone: c.undone,
|
|
118
|
+
})),
|
|
119
|
+
status: 'completed',
|
|
120
|
+
});
|
|
121
|
+
onHistoryUpdate?.();
|
|
122
|
+
}
|
|
123
|
+
}, [isDone, aiOrganize.changes, aiOrganize.sourceFileNames, onHistoryUpdate]);
|
|
124
|
+
|
|
125
|
+
// Auto-dismiss timer (3 min after done, reset on user interaction)
|
|
126
|
+
const resetTimer = useCallback(() => {
|
|
127
|
+
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
128
|
+
if (isDone || isError) {
|
|
129
|
+
dismissTimerRef.current = setTimeout(() => {
|
|
130
|
+
onDismiss();
|
|
131
|
+
}, AUTO_DISMISS_MS);
|
|
132
|
+
}
|
|
133
|
+
}, [isDone, isError, onDismiss]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
resetTimer();
|
|
137
|
+
return () => { if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current); };
|
|
138
|
+
}, [resetTimer]);
|
|
139
|
+
|
|
140
|
+
const handleUserAction = useCallback(() => {
|
|
141
|
+
resetTimer();
|
|
142
|
+
}, [resetTimer]);
|
|
143
|
+
|
|
144
|
+
const handleUndoOne = useCallback(async (path: string) => {
|
|
145
|
+
handleUserAction();
|
|
146
|
+
setUndoing(true);
|
|
147
|
+
const ok = await aiOrganize.undoOne(path);
|
|
148
|
+
setUndoing(false);
|
|
149
|
+
if (ok) {
|
|
150
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
151
|
+
// History sync deferred to next render when aiOrganize.changes is updated
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
if (historyIdRef.current) {
|
|
154
|
+
const updated = aiOrganize.changes.map(c => ({
|
|
155
|
+
action: c.action, path: c.path, ok: c.ok,
|
|
156
|
+
undone: c.path === path ? true : c.undone,
|
|
157
|
+
}));
|
|
158
|
+
const allUndone = updated.every(c => !c.ok || c.undone);
|
|
159
|
+
updateEntry(historyIdRef.current, {
|
|
160
|
+
files: updated,
|
|
161
|
+
status: allUndone ? 'undone' : 'partial',
|
|
162
|
+
});
|
|
163
|
+
onHistoryUpdate?.();
|
|
164
|
+
}
|
|
165
|
+
}, 0);
|
|
166
|
+
}
|
|
167
|
+
}, [aiOrganize, handleUserAction, onHistoryUpdate]);
|
|
168
|
+
|
|
169
|
+
const handleUndoAll = useCallback(async () => {
|
|
170
|
+
handleUserAction();
|
|
171
|
+
setUndoing(true);
|
|
172
|
+
const reverted = await aiOrganize.undoAll();
|
|
173
|
+
setUndoing(false);
|
|
174
|
+
if (reverted > 0) {
|
|
175
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
176
|
+
if (historyIdRef.current) {
|
|
177
|
+
updateEntry(historyIdRef.current, {
|
|
178
|
+
files: aiOrganize.changes.map(c => ({
|
|
179
|
+
action: c.action, path: c.path, ok: c.ok,
|
|
180
|
+
undone: c.ok ? true : c.undone,
|
|
181
|
+
})),
|
|
182
|
+
status: 'undone',
|
|
183
|
+
});
|
|
184
|
+
onHistoryUpdate?.();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, [aiOrganize, handleUserAction, onHistoryUpdate]);
|
|
188
|
+
|
|
189
|
+
const handleViewFile = useCallback((path: string) => {
|
|
190
|
+
handleUserAction();
|
|
191
|
+
router.push(`/view/${encodePath(path)}`);
|
|
192
|
+
}, [router, handleUserAction]);
|
|
193
|
+
|
|
194
|
+
const handleDismiss = useCallback(() => {
|
|
195
|
+
onDismiss();
|
|
196
|
+
}, [onDismiss]);
|
|
197
|
+
|
|
198
|
+
if (!isActive) return null;
|
|
199
|
+
|
|
200
|
+
// Expanded panel (file list with per-file undo)
|
|
201
|
+
if (expanded && (isDone || isError)) {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-[420px] max-w-[calc(100vw-2rem)] bg-card border border-border rounded-xl shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200"
|
|
205
|
+
onClick={handleUserAction}
|
|
206
|
+
>
|
|
207
|
+
{/* Header */}
|
|
208
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
{isDone ? <Check size={14} className="text-success" /> : <AlertCircle size={14} className="text-error" />}
|
|
211
|
+
<span className="text-xs font-medium text-foreground">
|
|
212
|
+
{isDone ? fi.organizeReviewTitle as string : fi.organizeErrorTitle as string}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={() => setExpanded(false)}
|
|
218
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
219
|
+
>
|
|
220
|
+
<ChevronDown size={14} />
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* File list */}
|
|
225
|
+
{isDone && (
|
|
226
|
+
<div className="max-h-[240px] overflow-y-auto p-2 space-y-0.5">
|
|
227
|
+
{aiOrganize.changes.map((c, idx) => {
|
|
228
|
+
const wasUndone = c.undone;
|
|
229
|
+
const undoable = aiOrganize.canUndo(c.path);
|
|
230
|
+
const fileName = c.path.split('/').pop() ?? c.path;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
key={`${c.path}-${idx}`}
|
|
235
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${wasUndone ? 'bg-muted/30 opacity-50' : 'bg-muted/50'}`}
|
|
236
|
+
>
|
|
237
|
+
{wasUndone ? (
|
|
238
|
+
<Undo2 size={14} className="text-muted-foreground shrink-0" />
|
|
239
|
+
) : c.action === 'create' ? (
|
|
240
|
+
<FilePlus size={14} className="text-success shrink-0" />
|
|
241
|
+
) : (
|
|
242
|
+
<FileEdit size={14} className="text-[var(--amber)] shrink-0" />
|
|
243
|
+
)}
|
|
244
|
+
<span className={`truncate flex-1 ${wasUndone ? 'line-through text-muted-foreground' : 'text-foreground'}`}>
|
|
245
|
+
{fileName}
|
|
246
|
+
</span>
|
|
247
|
+
{wasUndone ? (
|
|
248
|
+
<span className="text-xs text-muted-foreground shrink-0">{fi.organizeUndone as string}</span>
|
|
249
|
+
) : (
|
|
250
|
+
<span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
|
|
251
|
+
{!c.ok ? fi.organizeFailed as string
|
|
252
|
+
: c.action === 'create' ? fi.organizeCreated as string
|
|
253
|
+
: fi.organizeUpdated as string}
|
|
254
|
+
</span>
|
|
255
|
+
)}
|
|
256
|
+
{undoable && (
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
onClick={() => handleUndoOne(c.path)}
|
|
260
|
+
disabled={undoing}
|
|
261
|
+
className="text-2xs text-muted-foreground/60 hover:text-foreground transition-colors shrink-0 px-1 disabled:opacity-40"
|
|
262
|
+
title={fi.organizeUndoOne as string}
|
|
263
|
+
>
|
|
264
|
+
<Undo2 size={12} />
|
|
265
|
+
</button>
|
|
266
|
+
)}
|
|
267
|
+
{c.ok && !c.undone && (
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => handleViewFile(c.path)}
|
|
271
|
+
className="text-2xs text-muted-foreground/60 hover:text-[var(--amber)] transition-colors shrink-0 px-1"
|
|
272
|
+
title={fi.organizeViewFile as string}
|
|
273
|
+
>
|
|
274
|
+
<ExternalLink size={12} />
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{isError && (
|
|
284
|
+
<div className="p-4 text-center">
|
|
285
|
+
<p className="text-xs text-muted-foreground">{aiOrganize.error}</p>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Actions */}
|
|
290
|
+
<div className="flex items-center justify-end gap-3 px-4 py-3 border-t border-border">
|
|
291
|
+
{isDone && aiOrganize.hasAnyUndoable && (
|
|
292
|
+
<button
|
|
293
|
+
onClick={handleUndoAll}
|
|
294
|
+
disabled={undoing}
|
|
295
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1.5 disabled:opacity-50"
|
|
296
|
+
>
|
|
297
|
+
{undoing ? <Loader2 size={12} className="animate-spin" /> : <Undo2 size={12} />}
|
|
298
|
+
{fi.organizeUndoAll as string}
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
<button
|
|
302
|
+
onClick={handleDismiss}
|
|
303
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all"
|
|
304
|
+
>
|
|
305
|
+
<Check size={12} />
|
|
306
|
+
{fi.organizeDone as string}
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Compact toast bar
|
|
314
|
+
return (
|
|
315
|
+
<div
|
|
316
|
+
className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-card border border-border rounded-xl shadow-lg px-4 py-3 max-w-md animate-in fade-in-0 slide-in-from-bottom-2 duration-200"
|
|
317
|
+
onClick={handleUserAction}
|
|
318
|
+
>
|
|
319
|
+
{isDone ? (
|
|
320
|
+
<>
|
|
321
|
+
<Check size={16} className="text-success shrink-0" />
|
|
322
|
+
<span className="text-xs text-foreground truncate">
|
|
323
|
+
{(fi.organizeReviewDesc as (n: number) => string)(okCount)}
|
|
324
|
+
</span>
|
|
325
|
+
{aiOrganize.changes.length > 0 && (
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
onClick={() => { setExpanded(true); handleUserAction(); }}
|
|
329
|
+
className="flex items-center gap-1 text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
|
|
330
|
+
>
|
|
331
|
+
<ChevronDown size={12} className="rotate-180" />
|
|
332
|
+
{fi.organizeExpand as string}
|
|
333
|
+
</button>
|
|
334
|
+
)}
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
onClick={handleDismiss}
|
|
338
|
+
className="flex items-center gap-1 text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
|
|
339
|
+
>
|
|
340
|
+
<Check size={12} />
|
|
341
|
+
{fi.organizeDone as string}
|
|
342
|
+
</button>
|
|
343
|
+
</>
|
|
344
|
+
) : isError ? (
|
|
345
|
+
<>
|
|
346
|
+
<AlertCircle size={16} className="text-error shrink-0" />
|
|
347
|
+
<span className="text-xs text-foreground truncate">{fi.organizeError as string}</span>
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
onClick={() => { setExpanded(true); handleUserAction(); }}
|
|
351
|
+
className="text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
|
|
352
|
+
>
|
|
353
|
+
{fi.organizeExpand as string}
|
|
354
|
+
</button>
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onClick={handleDismiss}
|
|
358
|
+
className="text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
|
|
359
|
+
>
|
|
360
|
+
<X size={14} />
|
|
361
|
+
</button>
|
|
362
|
+
</>
|
|
363
|
+
) : (
|
|
364
|
+
<>
|
|
365
|
+
<div className="relative shrink-0">
|
|
366
|
+
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
367
|
+
<Loader2 size={10} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
|
|
368
|
+
</div>
|
|
369
|
+
<span className="text-xs text-foreground truncate">
|
|
370
|
+
{stageText(t, displayHint)}
|
|
371
|
+
</span>
|
|
372
|
+
<span className="text-xs text-muted-foreground/60 tabular-nums shrink-0">
|
|
373
|
+
{(fi.organizeElapsed as (s: number) => string)(elapsed)}
|
|
374
|
+
</span>
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={handleDismiss}
|
|
378
|
+
className="text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
|
|
379
|
+
>
|
|
380
|
+
<X size={14} />
|
|
381
|
+
</button>
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
package/app/components/Panel.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
|
-
import { ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
|
|
4
|
+
import { ChevronsDownUp, ChevronsUpDown, FilePlus, Import } from 'lucide-react';
|
|
5
5
|
import type { PanelId } from './ActivityBar';
|
|
6
6
|
import type { FileNode } from '@/lib/types';
|
|
7
7
|
import FileTree from './FileTree';
|
|
@@ -29,6 +29,7 @@ const DEFAULT_PANEL_WIDTH: Record<PanelId, number> = {
|
|
|
29
29
|
echo: 280,
|
|
30
30
|
agents: 280,
|
|
31
31
|
discover: 280,
|
|
32
|
+
history: 280,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const MIN_PANEL_WIDTH = 240;
|
|
@@ -52,7 +53,7 @@ interface PanelProps {
|
|
|
52
53
|
/** Callback to toggle maximize */
|
|
53
54
|
onMaximize?: () => void;
|
|
54
55
|
/** Callback to open import modal for a space */
|
|
55
|
-
onImport?: (space
|
|
56
|
+
onImport?: (space?: string) => void;
|
|
56
57
|
/** Lazy-loaded panel content for search/ask/plugins */
|
|
57
58
|
children?: React.ReactNode;
|
|
58
59
|
}
|
|
@@ -108,11 +109,30 @@ export default function Panel({
|
|
|
108
109
|
<div className={`flex flex-col h-full ${activePanel === 'files' ? '' : 'hidden'}`}>
|
|
109
110
|
<PanelHeader title={t.sidebar.files}>
|
|
110
111
|
<div className="flex items-center gap-0.5">
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
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"
|
|
116
|
+
aria-label={t.sidebar.importFile}
|
|
117
|
+
title={t.sidebar.importFile}
|
|
118
|
+
>
|
|
119
|
+
<Import size={13} />
|
|
120
|
+
</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>
|
|
111
130
|
<button
|
|
112
131
|
onClick={() => setMaxOpenDepth(prev => {
|
|
113
132
|
const current = prev ?? treeMaxDepth;
|
|
114
133
|
return Math.max(-1, current - 1);
|
|
115
134
|
})}
|
|
135
|
+
onDoubleClick={() => setMaxOpenDepth(-1)}
|
|
116
136
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
117
137
|
aria-label={t.sidebar.collapseLevel}
|
|
118
138
|
title={t.sidebar.collapseLevel}
|
|
@@ -128,6 +148,7 @@ export default function Panel({
|
|
|
128
148
|
}
|
|
129
149
|
return next;
|
|
130
150
|
})}
|
|
151
|
+
onDoubleClick={() => setMaxOpenDepth(null)}
|
|
131
152
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
132
153
|
aria-label={t.sidebar.expandLevel}
|
|
133
154
|
title={t.sidebar.expandLevel}
|
|
@@ -167,7 +167,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
167
167
|
</div>
|
|
168
168
|
</header>
|
|
169
169
|
|
|
170
|
-
{mobileOpen && <div className="md:hidden fixed inset-0 z-40
|
|
170
|
+
{mobileOpen && <div className="md:hidden fixed inset-0 z-40 overlay-backdrop" onClick={() => setMobileOpen(false)} />}
|
|
171
171
|
|
|
172
172
|
<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'}`}>
|
|
173
173
|
{sidebarContent}
|
|
@@ -13,6 +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
|
import RightAskPanel from './RightAskPanel';
|
|
17
18
|
import RightAgentDetailPanel, {
|
|
18
19
|
RIGHT_AGENT_DETAIL_DEFAULT_WIDTH,
|
|
@@ -27,6 +28,7 @@ import SettingsModal from './SettingsModal';
|
|
|
27
28
|
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
28
29
|
import ChangesBanner from './changes/ChangesBanner';
|
|
29
30
|
import SpaceInitToast from './SpaceInitToast';
|
|
31
|
+
import OrganizeToast from './OrganizeToast';
|
|
30
32
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
31
33
|
import { FileNode } from '@/lib/types';
|
|
32
34
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -38,6 +40,7 @@ import McpProvider from '@/hooks/useMcpData';
|
|
|
38
40
|
import '@/lib/renderers/index'; // client-side renderer registration source of truth
|
|
39
41
|
import { useLeftPanel } from '@/hooks/useLeftPanel';
|
|
40
42
|
import { useAskPanel } from '@/hooks/useAskPanel';
|
|
43
|
+
import { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
41
44
|
import type { Tab } from './settings/types';
|
|
42
45
|
|
|
43
46
|
interface SidebarLayoutProps {
|
|
@@ -74,6 +77,33 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
74
77
|
return RIGHT_AGENT_DETAIL_DEFAULT_WIDTH;
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
// ── AI Organize (lifted from ImportModal so toast shares state) ──
|
|
81
|
+
const aiOrganize = useAiOrganize();
|
|
82
|
+
const [organizeToastVisible, setOrganizeToastVisible] = useState(false);
|
|
83
|
+
const [historyRefreshToken, setHistoryRefreshToken] = useState(0);
|
|
84
|
+
|
|
85
|
+
// Show toast whenever organize is active
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (aiOrganize.phase === 'organizing' || aiOrganize.phase === 'done' || aiOrganize.phase === 'error') {
|
|
88
|
+
setOrganizeToastVisible(true);
|
|
89
|
+
}
|
|
90
|
+
}, [aiOrganize.phase]);
|
|
91
|
+
|
|
92
|
+
const handleOrganizeToastDismiss = useCallback(() => {
|
|
93
|
+
setOrganizeToastVisible(false);
|
|
94
|
+
if (aiOrganize.phase !== 'organizing') {
|
|
95
|
+
aiOrganize.reset();
|
|
96
|
+
} else {
|
|
97
|
+
aiOrganize.abort();
|
|
98
|
+
aiOrganize.reset();
|
|
99
|
+
}
|
|
100
|
+
}, [aiOrganize]);
|
|
101
|
+
|
|
102
|
+
const handleHistoryUpdate = useCallback(() => {
|
|
103
|
+
setHistoryRefreshToken(t => t + 1);
|
|
104
|
+
window.dispatchEvent(new Event('mindos:organize-history-update'));
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
77
107
|
// ── Import modal state ──
|
|
78
108
|
const [importModalOpen, setImportModalOpen] = useState(false);
|
|
79
109
|
const [importDefaultSpace, setImportDefaultSpace] = useState<string | undefined>(undefined);
|
|
@@ -345,6 +375,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
345
375
|
<div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
|
|
346
376
|
<DiscoverPanel active={lp.activePanel === 'discover'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
|
|
347
377
|
</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>
|
|
348
381
|
</Panel>
|
|
349
382
|
|
|
350
383
|
{/* ── Right-side Ask AI Panel ── */}
|
|
@@ -418,7 +451,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
418
451
|
</div>
|
|
419
452
|
</header>
|
|
420
453
|
|
|
421
|
-
{mobileOpen && <div className="md:hidden fixed inset-0 z-40
|
|
454
|
+
{mobileOpen && <div className="md:hidden fixed inset-0 z-40 overlay-backdrop" onClick={() => setMobileOpen(false)} />}
|
|
422
455
|
<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'}`}>
|
|
423
456
|
<div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
|
|
424
457
|
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
@@ -489,8 +522,18 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
489
522
|
onClose={handleCloseImport}
|
|
490
523
|
defaultSpace={importDefaultSpace}
|
|
491
524
|
initialFiles={importInitialFiles}
|
|
525
|
+
aiOrganize={aiOrganize}
|
|
492
526
|
/>
|
|
493
527
|
|
|
528
|
+
{organizeToastVisible && (
|
|
529
|
+
<OrganizeToast
|
|
530
|
+
aiOrganize={aiOrganize}
|
|
531
|
+
onDismiss={handleOrganizeToastDismiss}
|
|
532
|
+
onCancel={() => { aiOrganize.abort(); aiOrganize.reset(); setOrganizeToastVisible(false); }}
|
|
533
|
+
onHistoryUpdate={handleHistoryUpdate}
|
|
534
|
+
/>
|
|
535
|
+
)}
|
|
536
|
+
|
|
494
537
|
<style>{`
|
|
495
538
|
@media (min-width: 768px) {
|
|
496
539
|
:root {
|
|
@@ -192,14 +192,35 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
192
192
|
}, [a.detail.mcpServerHint]);
|
|
193
193
|
|
|
194
194
|
if (!agent) {
|
|
195
|
+
const connectedAgents = mcp.agents
|
|
196
|
+
.filter((ag) => ag.key !== agentKey && resolveAgentStatus(ag) === 'connected')
|
|
197
|
+
.slice(0, 3);
|
|
198
|
+
|
|
195
199
|
return (
|
|
196
200
|
<div className="content-width px-4 md:px-6 py-8 md:py-10">
|
|
197
201
|
<Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground mb-4">
|
|
198
202
|
<ArrowLeft size={14} />
|
|
199
203
|
{a.backToOverview}
|
|
200
204
|
</Link>
|
|
201
|
-
<div className="rounded-lg border border-border bg-card p-6">
|
|
202
|
-
<p className="text-sm text-foreground">{a.detailNotFound}</p>
|
|
205
|
+
<div className="rounded-lg border border-border bg-card p-6 space-y-4">
|
|
206
|
+
<p className="text-sm text-foreground font-medium">{a.detailNotFound}</p>
|
|
207
|
+
<p className="text-xs text-muted-foreground">{a.detailNotFoundHint}</p>
|
|
208
|
+
{connectedAgents.length > 0 && (
|
|
209
|
+
<div className="pt-2 border-t border-border">
|
|
210
|
+
<p className="text-xs text-muted-foreground mb-2">{a.detailNotFoundSuggestion}</p>
|
|
211
|
+
<div className="flex flex-wrap gap-2">
|
|
212
|
+
{connectedAgents.map((ag) => (
|
|
213
|
+
<Link
|
|
214
|
+
key={ag.key}
|
|
215
|
+
href={`/agents/${encodeURIComponent(ag.key)}`}
|
|
216
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-muted text-foreground hover:bg-muted/80 transition-colors"
|
|
217
|
+
>
|
|
218
|
+
{ag.name}
|
|
219
|
+
</Link>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
203
224
|
</div>
|
|
204
225
|
</div>
|
|
205
226
|
);
|
|
@@ -219,11 +240,11 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
219
240
|
<div className="flex items-center gap-3.5 px-5 py-4">
|
|
220
241
|
<AgentAvatar name={agent.name} status={status} size="md" />
|
|
221
242
|
<div className="min-w-0 flex-1">
|
|
222
|
-
<h1 className="text-lg font-semibold tracking-tight font-display text-foreground">{agent.name}</h1>
|
|
243
|
+
<h1 className="text-lg font-semibold tracking-tight font-display text-foreground truncate">{agent.name}</h1>
|
|
223
244
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 mt-0.5 text-2xs text-muted-foreground/60">
|
|
224
245
|
<span className={`font-medium px-1.5 py-px rounded-full ${
|
|
225
246
|
status === 'connected' ? 'bg-success/10 text-success'
|
|
226
|
-
: status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber)]'
|
|
247
|
+
: status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber-text)]'
|
|
227
248
|
: 'bg-muted text-muted-foreground'
|
|
228
249
|
}`}>{status}</span>
|
|
229
250
|
<span className="font-mono">{agent.transport ?? agent.preferredTransport}</span>
|
|
@@ -240,12 +261,12 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
240
261
|
|
|
241
262
|
{/* ═══════════ MCP MANAGEMENT ═══════════ */}
|
|
242
263
|
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
243
|
-
<div className="flex items-center justify-between">
|
|
244
|
-
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
264
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
265
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
245
266
|
<Server size={12} className="text-muted-foreground/50" />
|
|
246
267
|
{a.detail.mcpManagement}
|
|
247
268
|
</h2>
|
|
248
|
-
<div className="flex items-center gap-1.5">
|
|
269
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
249
270
|
{!isMindOS && (
|
|
250
271
|
<ActionButton
|
|
251
272
|
onClick={() => void handleCopySnippet()}
|
|
@@ -339,8 +360,8 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
339
360
|
|
|
340
361
|
{/* ═══════════ SKILL ASSIGNMENTS ═══════════ */}
|
|
341
362
|
<section className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
342
|
-
<div className="flex items-center justify-between">
|
|
343
|
-
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
363
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
364
|
+
<h2 className="text-xs font-semibold text-foreground flex items-center gap-2 shrink-0">
|
|
344
365
|
<Zap size={12} className="text-muted-foreground/50" />
|
|
345
366
|
{a.detail.skillAssignments}
|
|
346
367
|
</h2>
|
|
@@ -395,7 +416,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
395
416
|
>
|
|
396
417
|
{skill.name}
|
|
397
418
|
</button>
|
|
398
|
-
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-dim)] text-[var(--amber)]'}`}>
|
|
419
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-dim)] text-[var(--amber-text)]'}`}>
|
|
399
420
|
{skill.source === 'builtin' ? a.detail.skillsSourceBuiltin : a.detail.skillsSourceUser}
|
|
400
421
|
</span>
|
|
401
422
|
|
|
@@ -520,9 +541,9 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
520
541
|
|
|
521
542
|
function DetailLine({ label, value }: { label: string; value: string }) {
|
|
522
543
|
return (
|
|
523
|
-
<div className="flex items-baseline gap-2 px-0.5">
|
|
544
|
+
<div className="flex items-baseline gap-2 px-0.5 min-w-0">
|
|
524
545
|
<span className="text-2xs text-muted-foreground/50 uppercase tracking-wider shrink-0 min-w-[60px]">{label}</span>
|
|
525
|
-
<span className="text-xs text-foreground/80 font-mono truncate">{value}</span>
|
|
546
|
+
<span className="text-xs text-foreground/80 font-mono truncate min-w-0">{value}</span>
|
|
526
547
|
</div>
|
|
527
548
|
);
|
|
528
549
|
}
|
|
@@ -306,7 +306,7 @@ function ByAgentView({
|
|
|
306
306
|
{agent.name}
|
|
307
307
|
</Link>
|
|
308
308
|
<span className="text-2xs text-muted-foreground font-mono shrink-0">{agent.transport ?? agent.preferredTransport}</span>
|
|
309
|
-
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber)]' : 'bg-muted text-muted-foreground'}`}>
|
|
309
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]' : 'bg-muted text-muted-foreground'}`}>
|
|
310
310
|
{copy.status[status]}
|
|
311
311
|
</span>
|
|
312
312
|
</div>
|