@geminilight/mindos 0.6.8 → 0.6.13
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/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- 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 +16 -477
- 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 -75
- 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/setup/StepDots.tsx +2 -2
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +122 -10
- 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 +46 -2
- package/app/lib/i18n-zh.ts +46 -2
- 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.config.ts +23 -5
- package/bin/cli.js +6 -9
- 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/setup.js +2 -2
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
X, FolderInput, FolderOpen, Sparkles, FileText, AlertCircle,
|
|
6
|
-
AlertTriangle, Loader2, Check,
|
|
6
|
+
AlertTriangle, Loader2, Check, ChevronDown,
|
|
7
7
|
} from 'lucide-react';
|
|
8
|
-
import ReactMarkdown from 'react-markdown';
|
|
9
|
-
import remarkGfm from 'remark-gfm';
|
|
10
8
|
import { useLocale } from '@/lib/LocaleContext';
|
|
11
9
|
import { useFileImport, type ImportIntent, type ConflictMode } from '@/hooks/useFileImport';
|
|
12
|
-
import { useAiOrganize
|
|
13
|
-
import type { OrganizeStageHint } from '@/hooks/useAiOrganize';
|
|
10
|
+
import type { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
14
11
|
import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
|
|
15
12
|
import type { LocalAttachment } from '@/lib/types';
|
|
16
13
|
|
|
@@ -19,279 +16,23 @@ interface ImportModalProps {
|
|
|
19
16
|
onClose: () => void;
|
|
20
17
|
defaultSpace?: string;
|
|
21
18
|
initialFiles?: File[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
|
|
25
|
-
|
|
26
|
-
const THINKING_TIMEOUT_MS = 5000;
|
|
27
|
-
|
|
28
|
-
function stageHintText(
|
|
29
|
-
t: { fileImport: Record<string, unknown> },
|
|
30
|
-
hint: { stage: OrganizeStageHint; detail?: string } | null,
|
|
31
|
-
): string {
|
|
32
|
-
const fi = t.fileImport as {
|
|
33
|
-
organizeConnecting: string;
|
|
34
|
-
organizeAnalyzing: string;
|
|
35
|
-
organizeReading: (d?: string) => string;
|
|
36
|
-
organizeThinking: string;
|
|
37
|
-
organizeWriting: (d?: string) => string;
|
|
38
|
-
organizeProcessing: string;
|
|
39
|
-
};
|
|
40
|
-
if (!hint) return fi.organizeProcessing;
|
|
41
|
-
switch (hint.stage) {
|
|
42
|
-
case 'connecting': return fi.organizeConnecting;
|
|
43
|
-
case 'analyzing': return fi.organizeAnalyzing;
|
|
44
|
-
case 'reading': return fi.organizeReading(hint.detail);
|
|
45
|
-
case 'thinking': return fi.organizeThinking;
|
|
46
|
-
case 'writing': return fi.organizeWriting(hint.detail);
|
|
47
|
-
default: return fi.organizeProcessing;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Hook: elapsed timer + thinking-override for the organizing phase.
|
|
53
|
-
* Lifted to ImportModal level so both full modal and minimized bar share the same state.
|
|
54
|
-
*/
|
|
55
|
-
function useOrganizeTimer(isOrganizing: boolean, stageHint: ReturnType<typeof useAiOrganize>['stageHint']) {
|
|
56
|
-
const [elapsed, setElapsed] = useState(0);
|
|
57
|
-
const [thinkingOverride, setThinkingOverride] = useState(false);
|
|
58
|
-
const lastEventRef = useRef(Date.now());
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
lastEventRef.current = Date.now();
|
|
62
|
-
setThinkingOverride(false);
|
|
63
|
-
}, [stageHint]);
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (!isOrganizing) { setElapsed(0); setThinkingOverride(false); return; }
|
|
67
|
-
const timer = setInterval(() => {
|
|
68
|
-
setElapsed(e => e + 1);
|
|
69
|
-
if (Date.now() - lastEventRef.current >= THINKING_TIMEOUT_MS) {
|
|
70
|
-
setThinkingOverride(true);
|
|
71
|
-
}
|
|
72
|
-
}, 1000);
|
|
73
|
-
return () => clearInterval(timer);
|
|
74
|
-
}, [isOrganizing]);
|
|
75
|
-
|
|
76
|
-
const displayHint = thinkingOverride
|
|
77
|
-
? { stage: 'thinking' as const }
|
|
78
|
-
: stageHint;
|
|
79
|
-
|
|
80
|
-
return { elapsed, displayHint };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Full-size organizing progress view (shown inside the modal).
|
|
85
|
-
*/
|
|
86
|
-
function OrganizingProgress({
|
|
87
|
-
aiOrganize,
|
|
88
|
-
t,
|
|
89
|
-
elapsed,
|
|
90
|
-
displayHint,
|
|
91
|
-
onMinimize,
|
|
92
|
-
onCancel,
|
|
93
|
-
}: {
|
|
19
|
+
/** Lifted AI organize hook from SidebarLayout (shared with OrganizeToast) */
|
|
94
20
|
aiOrganize: ReturnType<typeof useAiOrganize>;
|
|
95
|
-
t: ReturnType<typeof useLocale>['t'];
|
|
96
|
-
elapsed: number;
|
|
97
|
-
displayHint: { stage: OrganizeStageHint; detail?: string } | null;
|
|
98
|
-
onMinimize: () => void;
|
|
99
|
-
onCancel: () => void;
|
|
100
|
-
}) {
|
|
101
|
-
const fi = t.fileImport as { organizeElapsed: (s: number) => string };
|
|
102
|
-
const summaryPreview = aiOrganize.summary ? stripThinkingTags(aiOrganize.summary).trim().slice(0, 200) : '';
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="mt-4 space-y-3">
|
|
106
|
-
{/* Status header */}
|
|
107
|
-
<div className="flex items-center gap-3">
|
|
108
|
-
<div className="relative shrink-0">
|
|
109
|
-
<Sparkles size={20} className="text-[var(--amber)]" />
|
|
110
|
-
<Loader2 size={12} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
|
|
111
|
-
</div>
|
|
112
|
-
<div className="flex-1 min-w-0">
|
|
113
|
-
<p className="text-sm text-foreground font-medium truncate">
|
|
114
|
-
{stageHintText(t, displayHint)}
|
|
115
|
-
</p>
|
|
116
|
-
<span className="text-xs text-muted-foreground/60 tabular-nums">
|
|
117
|
-
{fi.organizeElapsed(elapsed)}
|
|
118
|
-
</span>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{/* Live activity feed */}
|
|
123
|
-
<div className="bg-muted/30 rounded-lg border border-border/50 overflow-hidden">
|
|
124
|
-
<div className="max-h-[180px] overflow-y-auto p-3 space-y-2">
|
|
125
|
-
{/* Streaming AI text */}
|
|
126
|
-
{summaryPreview && (
|
|
127
|
-
<p className="text-xs text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
|
128
|
-
{summaryPreview}
|
|
129
|
-
{summaryPreview.length >= 200 ? '...' : ''}
|
|
130
|
-
</p>
|
|
131
|
-
)}
|
|
132
|
-
|
|
133
|
-
{/* Current tool being executed */}
|
|
134
|
-
{aiOrganize.currentTool && (
|
|
135
|
-
<div className="flex items-center gap-2 text-xs text-[var(--amber)] animate-pulse">
|
|
136
|
-
<Loader2 size={11} className="animate-spin shrink-0" />
|
|
137
|
-
<span className="truncate">
|
|
138
|
-
{aiOrganize.currentTool.name.startsWith('create')
|
|
139
|
-
? (t.fileImport as { organizeCreating: (p: string) => string }).organizeCreating(aiOrganize.currentTool.path)
|
|
140
|
-
: (t.fileImport as { organizeUpdating: (p: string) => string }).organizeUpdating(aiOrganize.currentTool.path)}
|
|
141
|
-
</span>
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
|
|
145
|
-
{/* Completed file operations */}
|
|
146
|
-
{aiOrganize.changes.map((c, idx) => (
|
|
147
|
-
<div key={`${c.path}-${idx}`} className="flex items-center gap-2 text-xs">
|
|
148
|
-
{c.action === 'create' ? (
|
|
149
|
-
<FilePlus size={12} className="text-success shrink-0" />
|
|
150
|
-
) : (
|
|
151
|
-
<FileEdit size={12} className="text-[var(--amber)] shrink-0" />
|
|
152
|
-
)}
|
|
153
|
-
<span className="truncate text-foreground/80">{c.path}</span>
|
|
154
|
-
<Check size={11} className="text-success shrink-0 ml-auto" />
|
|
155
|
-
</div>
|
|
156
|
-
))}
|
|
157
|
-
|
|
158
|
-
{/* Empty state — show pulsing dots */}
|
|
159
|
-
{!summaryPreview && !aiOrganize.currentTool && aiOrganize.changes.length === 0 && (
|
|
160
|
-
<div className="flex items-center justify-center gap-1 py-2">
|
|
161
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse" />
|
|
162
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse [animation-delay:150ms]" />
|
|
163
|
-
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]/40 animate-pulse [animation-delay:300ms]" />
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
{/* Actions */}
|
|
170
|
-
<div className="flex items-center justify-center gap-4">
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
onClick={onMinimize}
|
|
174
|
-
className="text-xs text-muted-foreground/70 hover:text-foreground transition-colors px-3 py-1.5"
|
|
175
|
-
>
|
|
176
|
-
{(t.fileImport as { organizeMinimize: string }).organizeMinimize}
|
|
177
|
-
</button>
|
|
178
|
-
<button
|
|
179
|
-
type="button"
|
|
180
|
-
onClick={onCancel}
|
|
181
|
-
className="text-xs text-muted-foreground/70 hover:text-muted-foreground transition-colors px-3 py-1.5"
|
|
182
|
-
>
|
|
183
|
-
{(t.fileImport as { organizeCancel: string }).organizeCancel}
|
|
184
|
-
</button>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
);
|
|
188
21
|
}
|
|
189
22
|
|
|
190
|
-
const
|
|
191
|
-
'prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground',
|
|
192
|
-
'prose-p:my-1 prose-p:leading-relaxed',
|
|
193
|
-
'prose-headings:font-semibold prose-headings:my-2 prose-headings:text-[13px]',
|
|
194
|
-
'prose-ul:my-1 prose-li:my-0.5 prose-ol:my-1',
|
|
195
|
-
'prose-code:text-[0.8em] prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none',
|
|
196
|
-
'prose-pre:bg-muted prose-pre:text-foreground prose-pre:text-xs',
|
|
197
|
-
'prose-blockquote:border-l-amber-400 prose-blockquote:text-muted-foreground',
|
|
198
|
-
'prose-a:text-amber-500 prose-a:no-underline hover:prose-a:underline',
|
|
199
|
-
'prose-strong:text-foreground prose-strong:font-semibold',
|
|
200
|
-
'prose-table:text-xs prose-th:py-1 prose-td:py-1',
|
|
201
|
-
].join(' ');
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Clean raw AI markdown for plain-text preview (progress view):
|
|
205
|
-
* strip heading markers, excess blank lines, truncate.
|
|
206
|
-
*/
|
|
207
|
-
function cleanSummaryForDisplay(raw: string): string {
|
|
208
|
-
return stripThinkingTags(raw)
|
|
209
|
-
.replace(/^#{1,4}\s+/gm, '')
|
|
210
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
211
|
-
.trim()
|
|
212
|
-
.slice(0, 500);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Clean raw AI markdown for rendered display:
|
|
217
|
-
* strip thinking tags & excess blank lines, keep markdown formatting.
|
|
218
|
-
*/
|
|
219
|
-
function cleanSummaryForMarkdown(raw: string): string {
|
|
220
|
-
return stripThinkingTags(raw)
|
|
221
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
222
|
-
.trim();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Organize result when no tracked file changes were detected.
|
|
227
|
-
* Two sub-states:
|
|
228
|
-
* 1. AI completed work + provided summary → show summary as primary content
|
|
229
|
-
* 2. No summary → brief "up to date" message
|
|
230
|
-
*/
|
|
231
|
-
function OrganizeNoChangesView({
|
|
232
|
-
summary,
|
|
233
|
-
toolCallCount,
|
|
234
|
-
t,
|
|
235
|
-
onDone,
|
|
236
|
-
}: {
|
|
237
|
-
summary: string;
|
|
238
|
-
toolCallCount: number;
|
|
239
|
-
t: ReturnType<typeof useLocale>['t'];
|
|
240
|
-
onDone: () => void;
|
|
241
|
-
}) {
|
|
242
|
-
const fi = t.fileImport as Record<string, unknown>;
|
|
243
|
-
const mdSummary = summary ? cleanSummaryForMarkdown(summary) : '';
|
|
244
|
-
const hasSubstance = !!mdSummary;
|
|
23
|
+
const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
|
|
245
24
|
|
|
246
|
-
return (
|
|
247
|
-
<div className="flex flex-col gap-3 py-4">
|
|
248
|
-
{hasSubstance ? (
|
|
249
|
-
<>
|
|
250
|
-
<div className="flex items-start gap-2.5">
|
|
251
|
-
<Sparkles size={16} className="text-[var(--amber)] mt-0.5 shrink-0" />
|
|
252
|
-
<div className={SUMMARY_PROSE}>
|
|
253
|
-
<ReactMarkdown remarkPlugins={[remarkGfm]}>{mdSummary}</ReactMarkdown>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
{toolCallCount > 0 && (
|
|
257
|
-
<p className="text-xs text-muted-foreground/50 text-center">
|
|
258
|
-
{(fi.organizeToolCallsInfo as ((n: number) => string) | undefined)?.(toolCallCount)}
|
|
259
|
-
</p>
|
|
260
|
-
)}
|
|
261
|
-
</>
|
|
262
|
-
) : (
|
|
263
|
-
<div className="flex flex-col items-center gap-2">
|
|
264
|
-
<Sparkles size={24} className="text-muted-foreground" />
|
|
265
|
-
<p className="text-sm text-muted-foreground">
|
|
266
|
-
{fi.organizeNoChanges as string}
|
|
267
|
-
</p>
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
<div className="flex justify-center pt-1">
|
|
271
|
-
<button
|
|
272
|
-
onClick={onDone}
|
|
273
|
-
className="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
|
|
274
|
-
>
|
|
275
|
-
{fi.organizeDone as string}
|
|
276
|
-
</button>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
25
|
|
|
282
|
-
export default function ImportModal({ open, onClose, defaultSpace, initialFiles }: ImportModalProps) {
|
|
26
|
+
export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize }: ImportModalProps) {
|
|
283
27
|
const { t } = useLocale();
|
|
284
28
|
const im = useFileImport();
|
|
285
|
-
const aiOrganize = useAiOrganize();
|
|
286
29
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
287
30
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
288
31
|
const [spaces, setSpaces] = useState<Array<{ name: string; path: string }>>([]);
|
|
289
32
|
const [closing, setClosing] = useState(false);
|
|
290
33
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
291
|
-
const [undoing, setUndoing] = useState(false);
|
|
292
34
|
const [conflictFiles, setConflictFiles] = useState<string[]>([]);
|
|
293
35
|
const [showConflictOptions, setShowConflictOptions] = useState(false);
|
|
294
|
-
const [minimized, setMinimized] = useState(false);
|
|
295
36
|
const initializedRef = useRef(false);
|
|
296
37
|
|
|
297
38
|
useEffect(() => {
|
|
@@ -302,11 +43,8 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
302
43
|
if (initializedRef.current) return;
|
|
303
44
|
initializedRef.current = true;
|
|
304
45
|
im.reset();
|
|
305
|
-
aiOrganize.reset();
|
|
306
|
-
setUndoing(false);
|
|
307
46
|
setConflictFiles([]);
|
|
308
47
|
setShowConflictOptions(false);
|
|
309
|
-
setMinimized(false);
|
|
310
48
|
if (defaultSpace) im.setTargetSpace(defaultSpace);
|
|
311
49
|
if (initialFiles && initialFiles.length > 0) {
|
|
312
50
|
im.addFiles(initialFiles);
|
|
@@ -318,16 +56,12 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
318
56
|
}, [open, defaultSpace, initialFiles, im]);
|
|
319
57
|
|
|
320
58
|
const handleClose = useCallback(() => {
|
|
321
|
-
if (im.step
|
|
322
|
-
setMinimized(true);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (im.files.length > 0 && im.step !== 'done' && im.step !== 'organize_review') {
|
|
59
|
+
if (im.files.length > 0 && im.step !== 'done') {
|
|
326
60
|
if (!confirm(t.fileImport.discardMessage(im.files.length))) return;
|
|
327
61
|
}
|
|
328
62
|
setClosing(true);
|
|
329
|
-
setTimeout(() => { setClosing(false); onClose(); im.reset();
|
|
330
|
-
}, [im, onClose, t
|
|
63
|
+
setTimeout(() => { setClosing(false); onClose(); im.reset(); setConflictFiles([]); setShowConflictOptions(false); }, 150);
|
|
64
|
+
}, [im, onClose, t]);
|
|
331
65
|
|
|
332
66
|
useEffect(() => {
|
|
333
67
|
if (!open) return;
|
|
@@ -366,10 +100,12 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
366
100
|
const prompt = attachments.length === 1
|
|
367
101
|
? (t.fileImport.digestPromptSingle as (name: string, space?: string) => string)(attachments[0].name, space)
|
|
368
102
|
: (t.fileImport.digestPromptMulti as (n: number, space?: string) => string)(attachments.length, space);
|
|
369
|
-
|
|
103
|
+
// Start AI organize and immediately close modal — toast takes over
|
|
370
104
|
aiOrganize.start(attachments, prompt);
|
|
105
|
+
onClose();
|
|
106
|
+
im.reset();
|
|
371
107
|
}
|
|
372
|
-
}, [im, t, aiOrganize]);
|
|
108
|
+
}, [im, t, aiOrganize, onClose]);
|
|
373
109
|
|
|
374
110
|
const handleArchiveSubmit = useCallback(async () => {
|
|
375
111
|
await im.doArchive();
|
|
@@ -396,53 +132,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
396
132
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
397
133
|
}, [im.targetSpace]);
|
|
398
134
|
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
if (im.step === 'organizing' && (aiOrganize.phase === 'done' || aiOrganize.phase === 'error')) {
|
|
401
|
-
im.setStep('organize_review');
|
|
402
|
-
}
|
|
403
|
-
}, [im.step, aiOrganize.phase, im]);
|
|
404
|
-
|
|
405
|
-
const handleOrganizeDone = useCallback(() => {
|
|
406
|
-
setClosing(true);
|
|
407
|
-
setTimeout(() => {
|
|
408
|
-
setClosing(false);
|
|
409
|
-
onClose();
|
|
410
|
-
im.reset();
|
|
411
|
-
aiOrganize.reset();
|
|
412
|
-
setUndoing(false);
|
|
413
|
-
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
414
|
-
}, 150);
|
|
415
|
-
}, [onClose, im, aiOrganize]);
|
|
416
|
-
|
|
417
|
-
const handleOrganizeUndo = useCallback(async () => {
|
|
418
|
-
setUndoing(true);
|
|
419
|
-
const reverted = await aiOrganize.undoAll();
|
|
420
|
-
setUndoing(false);
|
|
421
|
-
setClosing(true);
|
|
422
|
-
setTimeout(() => {
|
|
423
|
-
setClosing(false);
|
|
424
|
-
onClose();
|
|
425
|
-
im.reset();
|
|
426
|
-
aiOrganize.reset();
|
|
427
|
-
if (reverted > 0) {
|
|
428
|
-
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
429
|
-
}
|
|
430
|
-
}, 150);
|
|
431
|
-
}, [onClose, im, aiOrganize]);
|
|
432
|
-
|
|
433
|
-
const handleOrganizeRetry = useCallback(() => {
|
|
434
|
-
const attachments: LocalAttachment[] = im.validFiles.map(f => ({
|
|
435
|
-
name: f.name,
|
|
436
|
-
content: f.content!,
|
|
437
|
-
}));
|
|
438
|
-
const space = im.targetSpace || undefined;
|
|
439
|
-
const prompt = attachments.length === 1
|
|
440
|
-
? (t.fileImport.digestPromptSingle as (name: string, space?: string) => string)(attachments[0].name, space)
|
|
441
|
-
: (t.fileImport.digestPromptMulti as (n: number, space?: string) => string)(attachments.length, space);
|
|
442
|
-
aiOrganize.reset();
|
|
443
|
-
im.setStep('organizing');
|
|
444
|
-
aiOrganize.start(attachments, prompt);
|
|
445
|
-
}, [im, t, aiOrganize]);
|
|
446
135
|
|
|
447
136
|
useEffect(() => {
|
|
448
137
|
if (im.step === 'done' && im.result) {
|
|
@@ -467,61 +156,14 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
467
156
|
const isSelectStep = im.step === 'select';
|
|
468
157
|
const isArchiveConfig = im.step === 'archive_config';
|
|
469
158
|
const isImporting = im.step === 'importing';
|
|
470
|
-
const isOrganizing = im.step === 'organizing';
|
|
471
|
-
const isOrganizeReview = im.step === 'organize_review';
|
|
472
|
-
|
|
473
|
-
const { elapsed, displayHint } = useOrganizeTimer(isOrganizing, aiOrganize.stageHint);
|
|
474
|
-
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (minimized && im.step === 'organize_review') {
|
|
477
|
-
setMinimized(false);
|
|
478
|
-
}
|
|
479
|
-
}, [minimized, im.step]);
|
|
480
159
|
|
|
481
160
|
if (!open && !closing) return null;
|
|
482
161
|
|
|
483
|
-
const fi = t.fileImport as {
|
|
484
|
-
organizeElapsed: (s: number) => string;
|
|
485
|
-
organizeCancel: string;
|
|
486
|
-
organizeExpand: string;
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
if (minimized && isOrganizing) {
|
|
490
|
-
return (
|
|
491
|
-
<div 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-sm">
|
|
492
|
-
<div className="relative shrink-0">
|
|
493
|
-
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
494
|
-
<Loader2 size={10} className="absolute -bottom-0.5 -right-0.5 text-[var(--amber)] animate-spin" />
|
|
495
|
-
</div>
|
|
496
|
-
<span className="text-xs text-foreground truncate">
|
|
497
|
-
{stageHintText(t, displayHint)}
|
|
498
|
-
</span>
|
|
499
|
-
<span className="text-xs text-muted-foreground/60 tabular-nums shrink-0">
|
|
500
|
-
{fi.organizeElapsed(elapsed)}
|
|
501
|
-
</span>
|
|
502
|
-
<button
|
|
503
|
-
type="button"
|
|
504
|
-
onClick={() => setMinimized(false)}
|
|
505
|
-
className="text-xs font-medium text-[var(--amber)] hover:opacity-80 transition-colors shrink-0"
|
|
506
|
-
>
|
|
507
|
-
{fi.organizeExpand}
|
|
508
|
-
</button>
|
|
509
|
-
<button
|
|
510
|
-
type="button"
|
|
511
|
-
onClick={() => { aiOrganize.abort(); aiOrganize.reset(); im.setStep('select'); setMinimized(false); }}
|
|
512
|
-
className="text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors shrink-0"
|
|
513
|
-
>
|
|
514
|
-
<X size={14} />
|
|
515
|
-
</button>
|
|
516
|
-
</div>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
162
|
return (
|
|
521
163
|
<>
|
|
522
164
|
<div
|
|
523
165
|
ref={overlayRef}
|
|
524
|
-
className={`fixed inset-0 z-50
|
|
166
|
+
className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
|
|
525
167
|
onClick={(e) => { if (e.target === overlayRef.current) handleClose(); }}
|
|
526
168
|
>
|
|
527
169
|
<div
|
|
@@ -542,20 +184,11 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
542
184
|
</button>
|
|
543
185
|
)}
|
|
544
186
|
<h2 className="text-base font-semibold text-foreground">
|
|
545
|
-
{
|
|
546
|
-
: isOrganizeReview
|
|
547
|
-
? (aiOrganize.phase === 'error' ? t.fileImport.organizeErrorTitle : t.fileImport.organizeReviewTitle)
|
|
548
|
-
: isArchiveConfig ? t.fileImport.archiveConfigTitle
|
|
549
|
-
: t.fileImport.title}
|
|
187
|
+
{isArchiveConfig ? t.fileImport.archiveConfigTitle : t.fileImport.title}
|
|
550
188
|
</h2>
|
|
551
189
|
{isSelectStep && (
|
|
552
190
|
<p className="text-xs text-muted-foreground mt-0.5">{t.fileImport.subtitle}</p>
|
|
553
191
|
)}
|
|
554
|
-
{isOrganizeReview && aiOrganize.phase === 'done' && aiOrganize.changes.length > 0 && (
|
|
555
|
-
<p className="text-xs text-muted-foreground mt-0.5">
|
|
556
|
-
{t.fileImport.organizeReviewDesc(aiOrganize.changes.filter(c => c.ok).length)}
|
|
557
|
-
</p>
|
|
558
|
-
)}
|
|
559
192
|
</div>
|
|
560
193
|
<button
|
|
561
194
|
onClick={handleClose}
|
|
@@ -619,7 +252,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
619
252
|
/>
|
|
620
253
|
|
|
621
254
|
{/* File list */}
|
|
622
|
-
{hasFiles &&
|
|
255
|
+
{hasFiles && (
|
|
623
256
|
<div className="mt-3">
|
|
624
257
|
<div className="flex items-center justify-between mb-1.5">
|
|
625
258
|
<span className="text-xs text-muted-foreground">{t.fileImport.fileCount(im.files.length)}</span>
|
|
@@ -713,7 +346,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
713
346
|
<button
|
|
714
347
|
onClick={() => handleIntentSelect('digest')}
|
|
715
348
|
className="flex flex-col items-center gap-2 p-4 border border-border rounded-lg cursor-pointer transition-all duration-150 bg-card hover:border-[var(--amber)]/50 hover:shadow-sm active:scale-[0.98] text-left"
|
|
716
|
-
disabled={im.validFiles.length === 0}
|
|
349
|
+
disabled={im.validFiles.length === 0 || aiOrganize.phase === 'organizing'}
|
|
717
350
|
>
|
|
718
351
|
<Sparkles size={24} className="text-[var(--amber)]" />
|
|
719
352
|
<span className="text-sm font-medium text-foreground">{t.fileImport.digestTitle}</span>
|
|
@@ -818,100 +451,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles
|
|
|
818
451
|
</div>
|
|
819
452
|
)}
|
|
820
453
|
|
|
821
|
-
{/* AI Organizing (progress) */}
|
|
822
|
-
{isOrganizing && (
|
|
823
|
-
<OrganizingProgress
|
|
824
|
-
aiOrganize={aiOrganize}
|
|
825
|
-
t={t}
|
|
826
|
-
elapsed={elapsed}
|
|
827
|
-
displayHint={displayHint}
|
|
828
|
-
onMinimize={() => setMinimized(true)}
|
|
829
|
-
onCancel={() => { aiOrganize.abort(); aiOrganize.reset(); im.setStep('select'); }}
|
|
830
|
-
/>
|
|
831
|
-
)}
|
|
832
|
-
|
|
833
|
-
{/* AI Organize review */}
|
|
834
|
-
{isOrganizeReview && (
|
|
835
|
-
<div className="mt-4 space-y-4">
|
|
836
|
-
{aiOrganize.phase === 'error' ? (
|
|
837
|
-
<div className="flex flex-col items-center gap-3 py-4">
|
|
838
|
-
<AlertCircle size={28} className="text-error" />
|
|
839
|
-
<p className="text-xs text-muted-foreground text-center max-w-[300px]">{aiOrganize.error}</p>
|
|
840
|
-
<div className="flex gap-3 mt-2">
|
|
841
|
-
<button
|
|
842
|
-
onClick={handleClose}
|
|
843
|
-
className="text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2"
|
|
844
|
-
>
|
|
845
|
-
{t.fileImport.cancel}
|
|
846
|
-
</button>
|
|
847
|
-
<button
|
|
848
|
-
onClick={handleOrganizeRetry}
|
|
849
|
-
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
|
|
850
|
-
>
|
|
851
|
-
{t.fileImport.organizeRetry}
|
|
852
|
-
</button>
|
|
853
|
-
</div>
|
|
854
|
-
</div>
|
|
855
|
-
) : aiOrganize.changes.length === 0 ? (
|
|
856
|
-
<OrganizeNoChangesView
|
|
857
|
-
summary={aiOrganize.summary}
|
|
858
|
-
toolCallCount={aiOrganize.toolCallCount}
|
|
859
|
-
t={t}
|
|
860
|
-
onDone={handleOrganizeDone}
|
|
861
|
-
/>
|
|
862
|
-
) : (
|
|
863
|
-
<>
|
|
864
|
-
<div className="max-h-[200px] overflow-y-auto space-y-1">
|
|
865
|
-
{aiOrganize.changes.map((c, idx) => (
|
|
866
|
-
<div key={`${c.path}-${idx}`} className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-md text-sm">
|
|
867
|
-
{c.action === 'create' ? (
|
|
868
|
-
<FilePlus size={14} className="text-success shrink-0" />
|
|
869
|
-
) : (
|
|
870
|
-
<FileEdit size={14} className="text-[var(--amber)] shrink-0" />
|
|
871
|
-
)}
|
|
872
|
-
<span className="truncate flex-1 text-foreground">{c.path}</span>
|
|
873
|
-
<span className={`text-xs shrink-0 ${c.ok ? 'text-muted-foreground' : 'text-error'}`}>
|
|
874
|
-
{!c.ok ? t.fileImport.organizeFailed
|
|
875
|
-
: c.action === 'create' ? t.fileImport.organizeCreated
|
|
876
|
-
: t.fileImport.organizeUpdated}
|
|
877
|
-
</span>
|
|
878
|
-
</div>
|
|
879
|
-
))}
|
|
880
|
-
</div>
|
|
881
|
-
{aiOrganize.summary?.trim() && (
|
|
882
|
-
<div className={SUMMARY_PROSE}>
|
|
883
|
-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
884
|
-
{cleanSummaryForMarkdown(aiOrganize.summary)}
|
|
885
|
-
</ReactMarkdown>
|
|
886
|
-
</div>
|
|
887
|
-
)}
|
|
888
|
-
<div className="flex items-center justify-end gap-3 pt-2">
|
|
889
|
-
{aiOrganize.changes.some(c => c.action === 'create' && c.ok) && (
|
|
890
|
-
<button
|
|
891
|
-
onClick={handleOrganizeUndo}
|
|
892
|
-
disabled={undoing}
|
|
893
|
-
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2 disabled:opacity-50"
|
|
894
|
-
>
|
|
895
|
-
{undoing ? (
|
|
896
|
-
<Loader2 size={14} className="animate-spin" />
|
|
897
|
-
) : (
|
|
898
|
-
<Undo2 size={14} />
|
|
899
|
-
)}
|
|
900
|
-
{t.fileImport.organizeUndoAll}
|
|
901
|
-
</button>
|
|
902
|
-
)}
|
|
903
|
-
<button
|
|
904
|
-
onClick={handleOrganizeDone}
|
|
905
|
-
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-all duration-200"
|
|
906
|
-
>
|
|
907
|
-
<Check size={14} />
|
|
908
|
-
{t.fileImport.organizeDone}
|
|
909
|
-
</button>
|
|
910
|
-
</div>
|
|
911
|
-
</>
|
|
912
|
-
)}
|
|
913
|
-
</div>
|
|
914
|
-
)}
|
|
915
454
|
</div>
|
|
916
455
|
</div>
|
|
917
456
|
</div>
|
|
@@ -137,7 +137,7 @@ export default function OnboardingView() {
|
|
|
137
137
|
<button
|
|
138
138
|
type="button"
|
|
139
139
|
onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
|
|
140
|
-
className="text-xs text-[var(--amber)] hover:underline transition-colors"
|
|
140
|
+
className="text-xs text-[var(--amber-text)] hover:underline transition-colors"
|
|
141
141
|
>
|
|
142
142
|
{t.fileImport.onboardingHint}
|
|
143
143
|
</button>
|