@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.
Files changed (110) hide show
  1. package/README.md +10 -4
  2. package/README_zh.md +10 -4
  3. package/app/app/api/acp/config/route.ts +82 -0
  4. package/app/app/api/acp/detect/route.ts +71 -48
  5. package/app/app/api/acp/install/route.ts +51 -0
  6. package/app/app/api/acp/session/route.ts +141 -11
  7. package/app/app/api/ask/route.ts +126 -18
  8. package/app/app/api/export/route.ts +105 -0
  9. package/app/app/api/workflows/route.ts +156 -0
  10. package/app/app/globals.css +2 -2
  11. package/app/app/page.tsx +7 -2
  12. package/app/app/trash/page.tsx +7 -0
  13. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  14. package/app/components/ActivityBar.tsx +12 -4
  15. package/app/components/AskModal.tsx +4 -1
  16. package/app/components/ExportModal.tsx +220 -0
  17. package/app/components/FileTree.tsx +42 -11
  18. package/app/components/HomeContent.tsx +92 -20
  19. package/app/components/MarkdownView.tsx +45 -10
  20. package/app/components/Panel.tsx +1 -0
  21. package/app/components/RightAskPanel.tsx +5 -1
  22. package/app/components/Sidebar.tsx +10 -1
  23. package/app/components/SidebarLayout.tsx +6 -0
  24. package/app/components/TrashPageClient.tsx +263 -0
  25. package/app/components/agents/AgentDetailContent.tsx +263 -47
  26. package/app/components/agents/AgentsContentPage.tsx +11 -0
  27. package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
  28. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  29. package/app/components/agents/agents-content-model.ts +2 -2
  30. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  31. package/app/components/ask/AskContent.tsx +197 -239
  32. package/app/components/ask/FileChip.tsx +82 -17
  33. package/app/components/ask/MentionPopover.tsx +21 -3
  34. package/app/components/ask/MessageList.tsx +30 -9
  35. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  36. package/app/components/ask/ToolCallBlock.tsx +102 -18
  37. package/app/components/changes/ChangesContentPage.tsx +58 -14
  38. package/app/components/explore/ExploreContent.tsx +4 -7
  39. package/app/components/explore/UseCaseCard.tsx +18 -1
  40. package/app/components/explore/use-cases.generated.ts +76 -0
  41. package/app/components/explore/use-cases.yaml +185 -0
  42. package/app/components/panels/AgentsPanel.tsx +1 -0
  43. package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
  44. package/app/components/panels/DiscoverPanel.tsx +1 -1
  45. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  46. package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
  49. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  50. package/app/components/renderers/workflow-yaml/execution.ts +229 -0
  51. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  52. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  53. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  54. package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
  55. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  56. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  57. package/app/components/settings/AiTab.tsx +191 -174
  58. package/app/components/settings/AppearanceTab.tsx +168 -77
  59. package/app/components/settings/KnowledgeTab.tsx +131 -136
  60. package/app/components/settings/McpTab.tsx +11 -11
  61. package/app/components/settings/Primitives.tsx +60 -0
  62. package/app/components/settings/SettingsContent.tsx +15 -8
  63. package/app/components/settings/SyncTab.tsx +12 -12
  64. package/app/components/settings/UninstallTab.tsx +8 -18
  65. package/app/components/settings/UpdateTab.tsx +82 -82
  66. package/app/components/settings/types.ts +17 -8
  67. package/app/hooks/useAcpConfig.ts +96 -0
  68. package/app/hooks/useAcpDetection.ts +69 -14
  69. package/app/hooks/useAcpRegistry.ts +46 -11
  70. package/app/hooks/useAskModal.ts +12 -5
  71. package/app/hooks/useAskPanel.ts +8 -5
  72. package/app/hooks/useAskSession.ts +19 -2
  73. package/app/hooks/useImageUpload.ts +152 -0
  74. package/app/lib/acp/acp-tools.ts +3 -1
  75. package/app/lib/acp/agent-descriptors.ts +274 -0
  76. package/app/lib/acp/bridge.ts +6 -0
  77. package/app/lib/acp/index.ts +20 -4
  78. package/app/lib/acp/registry.ts +74 -7
  79. package/app/lib/acp/session.ts +490 -28
  80. package/app/lib/acp/subprocess.ts +307 -21
  81. package/app/lib/acp/types.ts +158 -20
  82. package/app/lib/actions.ts +57 -3
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/stream-consumer.ts +18 -0
  85. package/app/lib/agent/to-agent-messages.ts +25 -2
  86. package/app/lib/agent/tools.ts +56 -9
  87. package/app/lib/core/export.ts +116 -0
  88. package/app/lib/core/trash.ts +241 -0
  89. package/app/lib/fs.ts +47 -0
  90. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  91. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  92. package/app/lib/i18n/index.ts +3 -0
  93. package/app/lib/i18n/modules/knowledge.ts +124 -6
  94. package/app/lib/i18n/modules/navigation.ts +2 -0
  95. package/app/lib/i18n/modules/onboarding.ts +2 -134
  96. package/app/lib/i18n/modules/panels.ts +146 -2
  97. package/app/lib/i18n/modules/settings.ts +12 -0
  98. package/app/lib/pi-integration/skills.ts +21 -6
  99. package/app/lib/renderers/index.ts +2 -2
  100. package/app/lib/settings.ts +10 -0
  101. package/app/lib/types.ts +12 -1
  102. package/app/next-env.d.ts +1 -1
  103. package/app/package.json +11 -3
  104. package/app/scripts/generate-explore.ts +145 -0
  105. package/package.json +1 -1
  106. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  107. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  108. package/app/components/explore/use-cases.ts +0 -58
  109. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  110. 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={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => onDiscoverClick ? debounced(onDiscoverClick) : toggle('discover')} />
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
+ }