@geminilight/mindos 0.6.29 → 0.6.31
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 +10 -4
- package/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useTransition, useCallback, useEffect, useRef, useSyncExternalStore, useMemo, Suspense } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft, Share2, FileText, Code } from 'lucide-react';
|
|
5
|
+
import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft, Share2, FileText, Code, MoreHorizontal, Copy, Pencil, Trash2, Star, Download } from 'lucide-react';
|
|
6
6
|
import { lazy } from 'react';
|
|
7
7
|
import MarkdownView from '@/components/MarkdownView';
|
|
8
8
|
import JsonView from '@/components/JsonView';
|
|
@@ -17,6 +17,11 @@ import { resolveRenderer, isRendererEnabled } from '@/lib/renderers/registry';
|
|
|
17
17
|
import { encodePath } from '@/lib/utils';
|
|
18
18
|
import { useLocale } from '@/lib/LocaleContext';
|
|
19
19
|
import DirPicker from '@/components/DirPicker';
|
|
20
|
+
import { renameFileAction, deleteFileAction } from '@/lib/actions';
|
|
21
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
22
|
+
import { buildLineDiff } from '@/components/changes/line-diff';
|
|
23
|
+
import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
24
|
+
import ExportModal from '@/components/ExportModal';
|
|
20
25
|
|
|
21
26
|
interface ViewPageClientProps {
|
|
22
27
|
filePath: string;
|
|
@@ -42,6 +47,9 @@ export default function ViewPageClient({
|
|
|
42
47
|
createDraftAction,
|
|
43
48
|
}: ViewPageClientProps) {
|
|
44
49
|
const { t } = useLocale();
|
|
50
|
+
const { isPinned, togglePin } = usePinnedFiles();
|
|
51
|
+
const pinned = isPinned(filePath);
|
|
52
|
+
const [exportOpen, setExportOpen] = useState(false);
|
|
45
53
|
const hydrated = useSyncExternalStore(
|
|
46
54
|
() => () => {},
|
|
47
55
|
() => true,
|
|
@@ -55,18 +63,85 @@ export default function ViewPageClient({
|
|
|
55
63
|
const [editing, setEditing] = useState(initialEditing || content === '');
|
|
56
64
|
const [editContent, setEditContent] = useState(content);
|
|
57
65
|
const [savedContent, setSavedContent] = useState(content);
|
|
66
|
+
|
|
67
|
+
// Sync savedContent when server re-renders with new content (e.g. after router.refresh)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!editing) {
|
|
70
|
+
setSavedContent(content);
|
|
71
|
+
}
|
|
72
|
+
}, [content, editing]);
|
|
58
73
|
const [isPending, startTransition] = useTransition();
|
|
59
74
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
60
75
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
61
76
|
const [mdViewMode, setMdViewMode] = useState<MdViewMode>('wysiwyg');
|
|
62
77
|
const [findOpen, setFindOpen] = useState(false);
|
|
63
78
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
79
|
+
const [moreOpen, setMoreOpen] = useState(false);
|
|
80
|
+
const moreRef = useRef<HTMLButtonElement>(null);
|
|
81
|
+
const moreMenuRef = useRef<HTMLDivElement>(null);
|
|
82
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
83
|
+
const [renaming, setRenaming] = useState(false);
|
|
84
|
+
const [renameValue, setRenameValue] = useState('');
|
|
85
|
+
const [, startRenameTransition] = useTransition();
|
|
64
86
|
|
|
65
87
|
const inferredName = filePath.split('/').pop() || 'Untitled.md';
|
|
66
88
|
const [showSaveAs, setShowSaveAs] = useState(isDraft);
|
|
67
89
|
const [saveDir, setSaveDir] = useState('');
|
|
68
90
|
const [saveName, setSaveName] = useState(inferredName);
|
|
69
91
|
|
|
92
|
+
// Close more menu on outside click
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!moreOpen) return;
|
|
95
|
+
const handler = (e: MouseEvent) => {
|
|
96
|
+
if (
|
|
97
|
+
moreRef.current && !moreRef.current.contains(e.target as Node) &&
|
|
98
|
+
moreMenuRef.current && !moreMenuRef.current.contains(e.target as Node)
|
|
99
|
+
) setMoreOpen(false);
|
|
100
|
+
};
|
|
101
|
+
const keyHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') setMoreOpen(false); };
|
|
102
|
+
document.addEventListener('mousedown', handler);
|
|
103
|
+
document.addEventListener('keydown', keyHandler);
|
|
104
|
+
return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('keydown', keyHandler); };
|
|
105
|
+
}, [moreOpen]);
|
|
106
|
+
|
|
107
|
+
const handleCopyPath = useCallback(() => {
|
|
108
|
+
navigator.clipboard.writeText(filePath).catch(() => {});
|
|
109
|
+
setMoreOpen(false);
|
|
110
|
+
}, [filePath]);
|
|
111
|
+
|
|
112
|
+
const handleStartRename = useCallback(() => {
|
|
113
|
+
setMoreOpen(false);
|
|
114
|
+
const name = filePath.split('/').pop() ?? '';
|
|
115
|
+
setRenameValue(name);
|
|
116
|
+
setRenaming(true);
|
|
117
|
+
}, [filePath]);
|
|
118
|
+
|
|
119
|
+
const handleCommitRename = useCallback(() => {
|
|
120
|
+
const newName = renameValue.trim();
|
|
121
|
+
if (!newName || newName === filePath.split('/').pop()) { setRenaming(false); return; }
|
|
122
|
+
startRenameTransition(async () => {
|
|
123
|
+
const result = await renameFileAction(filePath, newName);
|
|
124
|
+
setRenaming(false);
|
|
125
|
+
if (result.success && result.newPath) {
|
|
126
|
+
router.push(`/view/${encodePath(result.newPath)}`);
|
|
127
|
+
router.refresh();
|
|
128
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}, [renameValue, filePath, router]);
|
|
132
|
+
|
|
133
|
+
const handleConfirmDelete = useCallback(() => {
|
|
134
|
+
setShowDeleteConfirm(false);
|
|
135
|
+
startTransition(async () => {
|
|
136
|
+
const result = await deleteFileAction(filePath);
|
|
137
|
+
if (result.success) {
|
|
138
|
+
router.push('/');
|
|
139
|
+
router.refresh();
|
|
140
|
+
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}, [filePath, router]);
|
|
144
|
+
|
|
70
145
|
// Keep first paint deterministic between server and client to avoid hydration mismatch.
|
|
71
146
|
const effectiveUseRaw = hydrated ? useRaw : false;
|
|
72
147
|
|
|
@@ -202,6 +277,65 @@ export default function ViewPageClient({
|
|
|
202
277
|
return () => window.removeEventListener('keydown', handler);
|
|
203
278
|
}, [editing, handleSave, handleEdit, handleCancel]);
|
|
204
279
|
|
|
280
|
+
// Auto-refresh when AI agent modifies files + compute changed lines for highlight
|
|
281
|
+
const [fileUpdated, setFileUpdated] = useState(false);
|
|
282
|
+
const [changedLines, setChangedLines] = useState<number[]>([]);
|
|
283
|
+
const prevContentRef = useRef(content);
|
|
284
|
+
const aiTriggeredRef = useRef(false);
|
|
285
|
+
const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
286
|
+
const updatedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
287
|
+
|
|
288
|
+
// When content prop changes after an AI-triggered refresh, compute diff highlights
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!editing && aiTriggeredRef.current && content !== prevContentRef.current && prevContentRef.current !== '') {
|
|
291
|
+
aiTriggeredRef.current = false;
|
|
292
|
+
const diff = buildLineDiff(prevContentRef.current, content);
|
|
293
|
+
const lines: number[] = [];
|
|
294
|
+
let lineNum = 1;
|
|
295
|
+
for (const row of diff) {
|
|
296
|
+
if (row.type === 'insert') {
|
|
297
|
+
lines.push(lineNum);
|
|
298
|
+
lineNum++;
|
|
299
|
+
} else if (row.type === 'equal') {
|
|
300
|
+
lineNum++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (lines.length > 0) {
|
|
304
|
+
setChangedLines(lines);
|
|
305
|
+
// Clear previous timer if any
|
|
306
|
+
if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current);
|
|
307
|
+
highlightTimerRef.current = setTimeout(() => setChangedLines([]), 6000);
|
|
308
|
+
// Auto-scroll to change banner
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
const el = document.querySelector('[data-highlight-line]');
|
|
311
|
+
if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
312
|
+
}, 100);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
prevContentRef.current = content;
|
|
316
|
+
}, [content, editing]);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
320
|
+
const handler = () => {
|
|
321
|
+
if (editing) return;
|
|
322
|
+
// Debounce rapid file changes (AI may write multiple files in sequence)
|
|
323
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
324
|
+
debounceTimer = setTimeout(() => {
|
|
325
|
+
aiTriggeredRef.current = true;
|
|
326
|
+
router.refresh();
|
|
327
|
+
setFileUpdated(true);
|
|
328
|
+
if (updatedTimerRef.current) clearTimeout(updatedTimerRef.current);
|
|
329
|
+
updatedTimerRef.current = setTimeout(() => setFileUpdated(false), 3000);
|
|
330
|
+
}, 300);
|
|
331
|
+
};
|
|
332
|
+
window.addEventListener('mindos:files-changed', handler);
|
|
333
|
+
return () => {
|
|
334
|
+
window.removeEventListener('mindos:files-changed', handler);
|
|
335
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
336
|
+
};
|
|
337
|
+
}, [editing, router]);
|
|
338
|
+
|
|
205
339
|
return (
|
|
206
340
|
<div className="flex flex-col min-h-screen">
|
|
207
341
|
{/* Top bar */}
|
|
@@ -219,6 +353,12 @@ export default function ViewPageClient({
|
|
|
219
353
|
</div>
|
|
220
354
|
|
|
221
355
|
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
|
|
356
|
+
{fileUpdated && !editing && (
|
|
357
|
+
<span className="text-xs flex items-center gap-1.5 text-[var(--amber)] animate-in fade-in-0 duration-200">
|
|
358
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]" />
|
|
359
|
+
<span className="hidden sm:inline">updated</span>
|
|
360
|
+
</span>
|
|
361
|
+
)}
|
|
222
362
|
{saveSuccess && (
|
|
223
363
|
<span className="text-xs flex items-center gap-1.5 font-display" style={{ color: 'var(--success)' }}>
|
|
224
364
|
<span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--success)' }} />
|
|
@@ -297,6 +437,54 @@ export default function ViewPageClient({
|
|
|
297
437
|
</button>
|
|
298
438
|
</>
|
|
299
439
|
)}
|
|
440
|
+
|
|
441
|
+
{/* More menu (rename, copy path, delete) */}
|
|
442
|
+
{!isDraft && (
|
|
443
|
+
<div className="relative">
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
onClick={() => togglePin(filePath)}
|
|
447
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
448
|
+
title={pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
|
|
449
|
+
>
|
|
450
|
+
<Star size={16} className={pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''} />
|
|
451
|
+
</button>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
onClick={() => setExportOpen(true)}
|
|
455
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
456
|
+
title={t.fileTree.export}
|
|
457
|
+
>
|
|
458
|
+
<Download size={16} />
|
|
459
|
+
</button>
|
|
460
|
+
<button
|
|
461
|
+
ref={moreRef}
|
|
462
|
+
type="button"
|
|
463
|
+
onClick={() => setMoreOpen(v => !v)}
|
|
464
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
465
|
+
title="More"
|
|
466
|
+
>
|
|
467
|
+
<MoreHorizontal size={16} />
|
|
468
|
+
</button>
|
|
469
|
+
{moreOpen && (
|
|
470
|
+
<div
|
|
471
|
+
ref={moreMenuRef}
|
|
472
|
+
className="absolute right-0 top-full mt-1 z-50 min-w-[160px] rounded-lg border border-border bg-card shadow-lg py-1"
|
|
473
|
+
>
|
|
474
|
+
<button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors text-left" onClick={handleCopyPath}>
|
|
475
|
+
<Copy size={14} className="shrink-0" /> Copy Path
|
|
476
|
+
</button>
|
|
477
|
+
<button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors text-left" onClick={handleStartRename}>
|
|
478
|
+
<Pencil size={14} className="shrink-0" /> Rename
|
|
479
|
+
</button>
|
|
480
|
+
<div className="my-1 border-t border-border/50" />
|
|
481
|
+
<button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-error hover:bg-error/10 transition-colors text-left" onClick={() => { setMoreOpen(false); setShowDeleteConfirm(true); }}>
|
|
482
|
+
<Trash2 size={14} className="shrink-0" /> Delete
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
300
488
|
</div>
|
|
301
489
|
</div>
|
|
302
490
|
</div>
|
|
@@ -375,7 +563,7 @@ export default function ViewPageClient({
|
|
|
375
563
|
<JsonView content={savedContent} />
|
|
376
564
|
) : (
|
|
377
565
|
<>
|
|
378
|
-
<MarkdownView content={savedContent} />
|
|
566
|
+
<MarkdownView content={savedContent} highlightLines={changedLines} onDismissHighlight={() => setChangedLines([])} />
|
|
379
567
|
<TableOfContents content={savedContent} />
|
|
380
568
|
</>
|
|
381
569
|
)}
|
|
@@ -383,6 +571,50 @@ export default function ViewPageClient({
|
|
|
383
571
|
</div>
|
|
384
572
|
)}
|
|
385
573
|
</div>
|
|
574
|
+
|
|
575
|
+
{/* Inline rename dialog */}
|
|
576
|
+
{renaming && (
|
|
577
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm">
|
|
578
|
+
<div className="bg-card border border-border rounded-lg shadow-lg p-4 w-80">
|
|
579
|
+
<h3 className="text-sm font-medium mb-2">Rename</h3>
|
|
580
|
+
<input
|
|
581
|
+
autoFocus
|
|
582
|
+
value={renameValue}
|
|
583
|
+
onChange={e => setRenameValue(e.target.value)}
|
|
584
|
+
onKeyDown={e => {
|
|
585
|
+
if (e.key === 'Enter') handleCommitRename();
|
|
586
|
+
if (e.key === 'Escape') setRenaming(false);
|
|
587
|
+
}}
|
|
588
|
+
className="w-full bg-muted border border-border rounded-md px-3 py-1.5 text-sm text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
589
|
+
/>
|
|
590
|
+
<div className="flex justify-end gap-2 mt-3">
|
|
591
|
+
<button onClick={() => setRenaming(false)} className="px-3 py-1.5 rounded-md text-xs bg-muted text-muted-foreground hover:bg-accent transition-colors">Cancel</button>
|
|
592
|
+
<button onClick={handleCommitRename} className="px-3 py-1.5 rounded-md text-xs bg-[var(--amber)] text-[var(--amber-foreground)] transition-colors">Rename</button>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
)}
|
|
597
|
+
|
|
598
|
+
{/* Delete confirm */}
|
|
599
|
+
<ConfirmDialog
|
|
600
|
+
open={showDeleteConfirm}
|
|
601
|
+
title="Delete"
|
|
602
|
+
message={`Delete "${filePath.split('/').pop()}"? This cannot be undone.`}
|
|
603
|
+
confirmLabel="Delete"
|
|
604
|
+
cancelLabel="Cancel"
|
|
605
|
+
variant="destructive"
|
|
606
|
+
onCancel={() => setShowDeleteConfirm(false)}
|
|
607
|
+
onConfirm={handleConfirmDelete}
|
|
608
|
+
/>
|
|
609
|
+
|
|
610
|
+
{/* Export modal */}
|
|
611
|
+
<ExportModal
|
|
612
|
+
open={exportOpen}
|
|
613
|
+
onClose={() => setExportOpen(false)}
|
|
614
|
+
filePath={filePath}
|
|
615
|
+
isDirectory={false}
|
|
616
|
+
fileName={filePath.split('/').pop() ?? filePath}
|
|
617
|
+
/>
|
|
386
618
|
</div>
|
|
387
619
|
);
|
|
388
620
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio } from 'lucide-react';
|
|
5
|
+
import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio, Zap } from 'lucide-react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
|
|
8
8
|
import type { SyncStatus } from './settings/SyncTab';
|
|
9
9
|
import Logo from './Logo';
|
|
10
10
|
|
|
11
|
-
export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover';
|
|
11
|
+
export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover' | 'workflows';
|
|
12
12
|
|
|
13
13
|
export const RAIL_WIDTH_COLLAPSED = 48;
|
|
14
14
|
export const RAIL_WIDTH_EXPANDED = 180;
|
|
@@ -19,6 +19,7 @@ interface ActivityBarProps {
|
|
|
19
19
|
onEchoClick?: () => void;
|
|
20
20
|
onAgentsClick?: () => void;
|
|
21
21
|
onDiscoverClick?: () => void;
|
|
22
|
+
onWorkflowsClick?: () => void;
|
|
22
23
|
syncStatus: SyncStatus | null;
|
|
23
24
|
expanded: boolean;
|
|
24
25
|
onExpandedChange: (expanded: boolean) => void;
|
|
@@ -82,6 +83,7 @@ export default function ActivityBar({
|
|
|
82
83
|
onEchoClick,
|
|
83
84
|
onAgentsClick,
|
|
84
85
|
onDiscoverClick,
|
|
86
|
+
onWorkflowsClick,
|
|
85
87
|
syncStatus,
|
|
86
88
|
expanded,
|
|
87
89
|
onExpandedChange,
|
|
@@ -191,8 +193,8 @@ export default function ActivityBar({
|
|
|
191
193
|
{/* ── Middle: Core panel toggles ── */}
|
|
192
194
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
193
195
|
<RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
|
|
194
|
-
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
|
|
195
196
|
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => onEchoClick ? debounced(onEchoClick) : toggle('echo')} walkthroughId="echo-panel" />
|
|
197
|
+
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
|
|
196
198
|
<RailButton
|
|
197
199
|
icon={<Bot size={18} />}
|
|
198
200
|
label={t.sidebar.agents}
|
|
@@ -201,12 +203,18 @@ export default function ActivityBar({
|
|
|
201
203
|
onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
|
|
202
204
|
walkthroughId="agents-panel"
|
|
203
205
|
/>
|
|
204
|
-
<RailButton icon={<
|
|
206
|
+
<RailButton icon={<Zap size={18} />} label={t.sidebar.workflows ?? 'Flows'} active={activePanel === 'workflows'} expanded={expanded} onClick={() => onWorkflowsClick ? debounced(onWorkflowsClick) : toggle('workflows')} />
|
|
205
207
|
</div>
|
|
206
208
|
|
|
207
209
|
{/* ── Spacer ── */}
|
|
208
210
|
<div className="flex-1" />
|
|
209
211
|
|
|
212
|
+
{/* ── Secondary: Explore ── */}
|
|
213
|
+
<div className={`${expanded ? 'mx-3' : 'mx-auto w-6'} border-t border-border`} />
|
|
214
|
+
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
215
|
+
<RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => onDiscoverClick ? debounced(onDiscoverClick) : toggle('discover')} />
|
|
216
|
+
</div>
|
|
217
|
+
|
|
210
218
|
{/* ── Bottom: Action buttons (not panel toggles) ── */}
|
|
211
219
|
<div className={`${expanded ? 'mx-3' : 'mx-auto w-6'} border-t border-border`} />
|
|
212
220
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { useLocale } from '@/lib/LocaleContext';
|
|
4
4
|
import AskContent from '@/components/ask/AskContent';
|
|
5
|
+
import type { AcpAgentSelection } from '@/hooks/useAskModal';
|
|
5
6
|
|
|
6
7
|
interface AskModalProps {
|
|
7
8
|
open: boolean;
|
|
8
9
|
onClose: () => void;
|
|
9
10
|
currentFile?: string;
|
|
10
11
|
initialMessage?: string;
|
|
12
|
+
initialAcpAgent?: AcpAgentSelection | null;
|
|
11
13
|
onFirstMessage?: () => void;
|
|
12
14
|
askMode?: 'panel' | 'popup';
|
|
13
15
|
onModeSwitch?: () => void;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export default function AskModal({ open, onClose, currentFile, initialMessage, onFirstMessage, askMode, onModeSwitch }: AskModalProps) {
|
|
18
|
+
export default function AskModal({ open, onClose, currentFile, initialMessage, initialAcpAgent, onFirstMessage, askMode, onModeSwitch }: AskModalProps) {
|
|
17
19
|
const { t } = useLocale();
|
|
18
20
|
|
|
19
21
|
if (!open) return null;
|
|
@@ -35,6 +37,7 @@ export default function AskModal({ open, onClose, currentFile, initialMessage, o
|
|
|
35
37
|
onClose={onClose}
|
|
36
38
|
currentFile={currentFile}
|
|
37
39
|
initialMessage={initialMessage}
|
|
40
|
+
initialAcpAgent={initialAcpAgent}
|
|
38
41
|
onFirstMessage={onFirstMessage}
|
|
39
42
|
askMode={askMode}
|
|
40
43
|
onModeSwitch={onModeSwitch}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { X, Download, FileText, Globe, Archive, Check, Loader2 } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { toast } from '@/lib/toast';
|
|
7
|
+
|
|
8
|
+
type ExportFormat = 'md' | 'html' | 'zip' | 'zip-html';
|
|
9
|
+
|
|
10
|
+
interface ExportModalProps {
|
|
11
|
+
open: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
filePath: string;
|
|
14
|
+
isDirectory: boolean;
|
|
15
|
+
fileName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ExportModal({ open, onClose, filePath, isDirectory, fileName }: ExportModalProps) {
|
|
19
|
+
const { t } = useLocale();
|
|
20
|
+
const [format, setFormat] = useState<ExportFormat>(isDirectory ? 'zip' : 'md');
|
|
21
|
+
const [state, setState] = useState<'idle' | 'exporting' | 'done' | 'error'>('idle');
|
|
22
|
+
const [error, setError] = useState('');
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
24
|
+
|
|
25
|
+
const handleExport = useCallback(() => {
|
|
26
|
+
setState('exporting');
|
|
27
|
+
setError('');
|
|
28
|
+
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
abortRef.current = controller;
|
|
31
|
+
|
|
32
|
+
const url = `/api/export?path=${encodeURIComponent(filePath)}&format=${format}`;
|
|
33
|
+
|
|
34
|
+
fetch(url, { signal: controller.signal })
|
|
35
|
+
.then(res => {
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
38
|
+
if (ct.includes('json')) {
|
|
39
|
+
return res.json().then((data: { error?: string }) => { throw new Error(data.error || 'Export failed'); });
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Export failed (${res.status})`);
|
|
42
|
+
}
|
|
43
|
+
return res.blob().then(blob => ({ blob, res }));
|
|
44
|
+
})
|
|
45
|
+
.then(({ blob, res }) => {
|
|
46
|
+
const disposition = res.headers.get('Content-Disposition') ?? '';
|
|
47
|
+
const match = disposition.match(/filename="?([^"]+)"?/);
|
|
48
|
+
const downloadName = match ? decodeURIComponent(match[1]) : fileName;
|
|
49
|
+
|
|
50
|
+
const link = document.createElement('a');
|
|
51
|
+
link.href = URL.createObjectURL(blob);
|
|
52
|
+
link.download = downloadName;
|
|
53
|
+
document.body.appendChild(link);
|
|
54
|
+
link.click();
|
|
55
|
+
document.body.removeChild(link);
|
|
56
|
+
URL.revokeObjectURL(link.href);
|
|
57
|
+
|
|
58
|
+
setState('done');
|
|
59
|
+
toast.success(t.export?.downloaded ?? 'Downloaded');
|
|
60
|
+
})
|
|
61
|
+
.catch((err: Error) => {
|
|
62
|
+
if (err.name === 'AbortError') return; // user cancelled
|
|
63
|
+
setState('error');
|
|
64
|
+
setError(err.message ?? 'Export failed');
|
|
65
|
+
})
|
|
66
|
+
.finally(() => { abortRef.current = null; });
|
|
67
|
+
}, [filePath, format, fileName, t]);
|
|
68
|
+
|
|
69
|
+
const handleCancel = useCallback(() => {
|
|
70
|
+
if (abortRef.current) {
|
|
71
|
+
abortRef.current.abort();
|
|
72
|
+
abortRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
setState('idle');
|
|
75
|
+
setError('');
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleRetry = useCallback(() => {
|
|
79
|
+
setState('idle');
|
|
80
|
+
setError('');
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleClose = useCallback(() => {
|
|
84
|
+
if (abortRef.current) abortRef.current.abort();
|
|
85
|
+
abortRef.current = null;
|
|
86
|
+
setState('idle');
|
|
87
|
+
setError('');
|
|
88
|
+
onClose();
|
|
89
|
+
}, [onClose]);
|
|
90
|
+
|
|
91
|
+
if (!open) return null;
|
|
92
|
+
|
|
93
|
+
const formats: { value: ExportFormat; label: string; desc: string; icon: React.ReactNode; disabled?: boolean }[] = isDirectory
|
|
94
|
+
? [
|
|
95
|
+
{ value: 'zip', label: t.export?.formatZipMd ?? 'Markdown ZIP', desc: t.export?.formatZipMdDesc ?? 'All files in original format', icon: <Archive size={14} /> },
|
|
96
|
+
{ value: 'zip-html', label: t.export?.formatZipHtml ?? 'HTML ZIP', desc: t.export?.formatZipHtmlDesc ?? 'All files as webpages', icon: <Globe size={14} /> },
|
|
97
|
+
]
|
|
98
|
+
: [
|
|
99
|
+
{ value: 'md', label: t.export?.formatMd ?? 'Markdown (.md)', desc: t.export?.formatMdDesc ?? 'Original format, editable', icon: <FileText size={14} /> },
|
|
100
|
+
{ value: 'html', label: t.export?.formatHtml ?? 'HTML (.html)', desc: t.export?.formatHtmlDesc ?? 'Static webpage, shareable', icon: <Globe size={14} /> },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={state === 'exporting' ? undefined : handleClose}>
|
|
105
|
+
<div
|
|
106
|
+
className="bg-card border border-border rounded-xl shadow-xl max-w-md w-full mx-4 animate-in fade-in-0 zoom-in-95 duration-200"
|
|
107
|
+
onClick={e => e.stopPropagation()}
|
|
108
|
+
>
|
|
109
|
+
{/* Header */}
|
|
110
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
111
|
+
<div className="flex items-center gap-2">
|
|
112
|
+
<Download size={15} className="text-[var(--amber)]" />
|
|
113
|
+
<h3 className="text-sm font-semibold font-display">
|
|
114
|
+
{isDirectory ? (t.export?.exportSpace ?? 'Export Space') : (t.export?.exportFile ?? 'Export File')}
|
|
115
|
+
</h3>
|
|
116
|
+
</div>
|
|
117
|
+
<button onClick={handleClose} className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
118
|
+
<X size={14} />
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Body */}
|
|
123
|
+
<div className="px-5 py-4">
|
|
124
|
+
{state === 'done' ? (
|
|
125
|
+
<div className="text-center py-6">
|
|
126
|
+
<div className="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center mx-auto mb-3">
|
|
127
|
+
<Check size={20} className="text-success" />
|
|
128
|
+
</div>
|
|
129
|
+
<p className="text-sm font-medium font-display">{t.export?.done ?? 'Export Complete'}</p>
|
|
130
|
+
<p className="text-xs text-muted-foreground mt-1">{fileName}</p>
|
|
131
|
+
</div>
|
|
132
|
+
) : state === 'error' ? (
|
|
133
|
+
<div className="text-center py-6">
|
|
134
|
+
<p className="text-sm font-medium text-error">{t.export?.error ?? 'Export failed'}</p>
|
|
135
|
+
<p className="text-xs text-muted-foreground mt-1">{error}</p>
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
<>
|
|
139
|
+
<p className="text-xs text-muted-foreground mb-3 truncate" title={filePath}>{filePath}</p>
|
|
140
|
+
<p className="text-xs font-medium font-display text-foreground mb-2">{t.export?.chooseFormat ?? 'Choose format'}</p>
|
|
141
|
+
<div className="space-y-1.5">
|
|
142
|
+
{formats.map(f => (
|
|
143
|
+
<label
|
|
144
|
+
key={f.value}
|
|
145
|
+
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
|
|
146
|
+
format === f.value
|
|
147
|
+
? 'border-[var(--amber)]/40 bg-[var(--amber-dim)]'
|
|
148
|
+
: 'border-border hover:border-border/80 hover:bg-muted/30'
|
|
149
|
+
} ${f.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
type="radio"
|
|
153
|
+
name="export-format"
|
|
154
|
+
value={f.value}
|
|
155
|
+
checked={format === f.value}
|
|
156
|
+
onChange={() => !f.disabled && setFormat(f.value)}
|
|
157
|
+
disabled={f.disabled}
|
|
158
|
+
className="sr-only"
|
|
159
|
+
/>
|
|
160
|
+
<span className={`mt-0.5 ${format === f.value ? 'text-[var(--amber)]' : 'text-muted-foreground'}`}>
|
|
161
|
+
{f.icon}
|
|
162
|
+
</span>
|
|
163
|
+
<div>
|
|
164
|
+
<span className="text-sm font-medium text-foreground">{f.label}</span>
|
|
165
|
+
<span className="text-xs text-muted-foreground block mt-0.5">{f.desc}</span>
|
|
166
|
+
</div>
|
|
167
|
+
</label>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Footer */}
|
|
175
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border">
|
|
176
|
+
{state === 'done' ? (
|
|
177
|
+
<>
|
|
178
|
+
<button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
179
|
+
{t.export?.downloadAgain ?? 'Download Again'}
|
|
180
|
+
</button>
|
|
181
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
|
|
182
|
+
{t.export?.cancel === 'Cancel' ? 'Done' : (t.export?.done ?? 'Done')}
|
|
183
|
+
</button>
|
|
184
|
+
</>
|
|
185
|
+
) : state === 'error' ? (
|
|
186
|
+
<>
|
|
187
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
188
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
189
|
+
</button>
|
|
190
|
+
<button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
|
|
191
|
+
{t.export?.retry ?? 'Retry'}
|
|
192
|
+
</button>
|
|
193
|
+
</>
|
|
194
|
+
) : state === 'exporting' ? (
|
|
195
|
+
<>
|
|
196
|
+
<button onClick={handleCancel} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
197
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
198
|
+
</button>
|
|
199
|
+
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-[var(--amber-text)] opacity-70">
|
|
200
|
+
<Loader2 size={12} className="animate-spin" /> {t.export?.exporting ?? 'Exporting...'}
|
|
201
|
+
</span>
|
|
202
|
+
</>
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
206
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
onClick={handleExport}
|
|
210
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors"
|
|
211
|
+
>
|
|
212
|
+
<Download size={12} /> {t.export?.exportButton ?? 'Export'}
|
|
213
|
+
</button>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|