@geminilight/mindos 0.6.28 → 0.6.30

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 (113) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/a2a/agents/route.ts +9 -0
  3. package/app/app/api/a2a/delegations/route.ts +9 -0
  4. package/app/app/api/a2a/discover/route.ts +2 -0
  5. package/app/app/api/a2a/route.ts +6 -6
  6. package/app/app/api/acp/config/route.ts +82 -0
  7. package/app/app/api/acp/detect/route.ts +114 -0
  8. package/app/app/api/acp/install/route.ts +51 -0
  9. package/app/app/api/acp/registry/route.ts +31 -0
  10. package/app/app/api/acp/session/route.ts +185 -0
  11. package/app/app/api/ask/route.ts +116 -13
  12. package/app/app/api/workflows/route.ts +156 -0
  13. package/app/app/layout.tsx +2 -0
  14. package/app/app/page.tsx +7 -2
  15. package/app/components/ActivityBar.tsx +12 -4
  16. package/app/components/AskModal.tsx +4 -1
  17. package/app/components/DirView.tsx +64 -2
  18. package/app/components/FileTree.tsx +40 -10
  19. package/app/components/GuideCard.tsx +7 -17
  20. package/app/components/HomeContent.tsx +1 -0
  21. package/app/components/MarkdownView.tsx +2 -0
  22. package/app/components/Panel.tsx +1 -0
  23. package/app/components/RightAskPanel.tsx +5 -1
  24. package/app/components/SearchModal.tsx +234 -80
  25. package/app/components/SidebarLayout.tsx +6 -0
  26. package/app/components/agents/AgentDetailContent.tsx +266 -52
  27. package/app/components/agents/AgentsContentPage.tsx +32 -6
  28. package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
  29. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  30. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  31. package/app/components/agents/agents-content-model.ts +2 -2
  32. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  33. package/app/components/ask/AskContent.tsx +197 -239
  34. package/app/components/ask/FileChip.tsx +82 -17
  35. package/app/components/ask/MentionPopover.tsx +21 -3
  36. package/app/components/ask/MessageList.tsx +30 -9
  37. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  38. package/app/components/help/HelpContent.tsx +9 -9
  39. package/app/components/panels/AgentsPanel.tsx +2 -0
  40. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  41. package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
  42. package/app/components/panels/EchoPanel.tsx +5 -1
  43. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  44. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  45. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  46. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  49. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  50. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  51. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  52. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  53. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  54. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  55. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  56. package/app/components/settings/KnowledgeTab.tsx +3 -6
  57. package/app/components/settings/McpSkillsSection.tsx +4 -5
  58. package/app/components/settings/McpTab.tsx +6 -8
  59. package/app/components/setup/StepSecurity.tsx +4 -5
  60. package/app/components/setup/index.tsx +5 -11
  61. package/app/components/ui/Toaster.tsx +39 -0
  62. package/app/hooks/useA2aRegistry.ts +6 -1
  63. package/app/hooks/useAcpConfig.ts +96 -0
  64. package/app/hooks/useAcpDetection.ts +120 -0
  65. package/app/hooks/useAcpRegistry.ts +86 -0
  66. package/app/hooks/useAskModal.ts +12 -5
  67. package/app/hooks/useAskPanel.ts +8 -5
  68. package/app/hooks/useAskSession.ts +19 -2
  69. package/app/hooks/useDelegationHistory.ts +49 -0
  70. package/app/hooks/useImageUpload.ts +152 -0
  71. package/app/lib/a2a/client.ts +49 -5
  72. package/app/lib/a2a/orchestrator.ts +0 -1
  73. package/app/lib/a2a/task-handler.ts +4 -4
  74. package/app/lib/a2a/types.ts +15 -0
  75. package/app/lib/acp/acp-tools.ts +95 -0
  76. package/app/lib/acp/agent-descriptors.ts +274 -0
  77. package/app/lib/acp/bridge.ts +144 -0
  78. package/app/lib/acp/index.ts +40 -0
  79. package/app/lib/acp/registry.ts +202 -0
  80. package/app/lib/acp/session.ts +717 -0
  81. package/app/lib/acp/subprocess.ts +495 -0
  82. package/app/lib/acp/types.ts +274 -0
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/to-agent-messages.ts +25 -2
  85. package/app/lib/agent/tools.ts +2 -1
  86. package/app/lib/i18n/_core.ts +22 -0
  87. package/app/lib/i18n/index.ts +35 -0
  88. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  89. package/app/lib/i18n/modules/common.ts +71 -0
  90. package/app/lib/i18n/modules/features.ts +153 -0
  91. package/app/lib/i18n/modules/knowledge.ts +429 -0
  92. package/app/lib/i18n/modules/navigation.ts +153 -0
  93. package/app/lib/i18n/modules/onboarding.ts +523 -0
  94. package/app/lib/i18n/modules/panels.ts +1196 -0
  95. package/app/lib/i18n/modules/settings.ts +585 -0
  96. package/app/lib/i18n-en.ts +2 -1518
  97. package/app/lib/i18n-zh.ts +2 -1542
  98. package/app/lib/i18n.ts +3 -6
  99. package/app/lib/pi-integration/skills.ts +21 -6
  100. package/app/lib/renderers/index.ts +2 -2
  101. package/app/lib/settings.ts +10 -0
  102. package/app/lib/toast.ts +79 -0
  103. package/app/lib/types.ts +12 -1
  104. package/app/next-env.d.ts +1 -1
  105. package/app/package.json +3 -1
  106. package/bin/cli.js +25 -25
  107. package/bin/commands/file.js +29 -2
  108. package/bin/commands/space.js +249 -91
  109. package/package.json +1 -1
  110. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  111. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  112. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  113. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { X, Sparkles, Upload, MessageCircle, ExternalLink, Check, ChevronRight, Copy } from 'lucide-react';
5
+ import { copyToClipboard } from '@/lib/clipboard';
6
+ import { toast } from '@/lib/toast';
5
7
  import Link from 'next/link';
6
8
  import { useLocale } from '@/lib/LocaleContext';
7
9
  import { openAskModal } from '@/hooks/useAskModal';
@@ -14,7 +16,6 @@ export default function GuideCard() {
14
16
  const [guideState, setGuideState] = useState<GuideState | null>(null);
15
17
  const [expanded, setExpanded] = useState<'import' | 'ai' | 'agent' | null>(null);
16
18
  const [isFirstVisit, setIsFirstVisit] = useState(false);
17
- const [copied, setCopied] = useState(false);
18
19
  const [step3Done, setStep3Done] = useState(false);
19
20
 
20
21
  // Fetch guide state from backend
@@ -97,13 +98,8 @@ export default function GuideCard() {
97
98
  // ── Step 3: Cross-agent copy ──
98
99
 
99
100
  const handleCopyPrompt = useCallback(async () => {
100
- try {
101
- await navigator.clipboard.writeText(g.agent.copyPrompt);
102
- setCopied(true);
103
- setTimeout(() => setCopied(false), 2000);
104
- } catch {
105
- // Fallback: no clipboard API
106
- }
101
+ const ok = await copyToClipboard(g.agent.copyPrompt);
102
+ if (ok) toast.copy();
107
103
  }, [g]);
108
104
 
109
105
  const handleStep3Done = useCallback(() => {
@@ -337,16 +333,10 @@ export default function GuideCard() {
337
333
  </p>
338
334
  <button
339
335
  onClick={handleCopyPrompt}
340
- className={`
341
- absolute top-2 right-2 flex items-center gap-1 text-2xs font-medium
342
- px-2.5 py-1.5 rounded-md border transition-all
343
- ${copied
344
- ? 'border-success/30 bg-success/10 text-success'
345
- : 'border-border bg-card text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30'}
346
- `}
336
+ className="absolute top-2 right-2 flex items-center gap-1 text-2xs font-medium px-2.5 py-1.5 rounded-md border transition-all border-border bg-card text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30"
347
337
  >
348
- {copied ? <Check size={11} /> : <Copy size={11} />}
349
- {copied ? g.agent.copied : g.agent.copy}
338
+ <Copy size={11} />
339
+ {g.agent.copy}
350
340
  </button>
351
341
  </div>
352
342
  <div className="flex items-center justify-end mt-3 pt-3 border-t border-border">
@@ -168,6 +168,7 @@ const TOOL_ICONS: Record<string, LucideIcon> = {
168
168
  'agent-inspector': Search,
169
169
  'config-panel': SlidersHorizontal,
170
170
  'todo': ListTodo,
171
+ 'workflow-yaml': Zap,
171
172
  };
172
173
 
173
174
  /** Mini-card for built-in tools — visually distinct from plugin chips */
@@ -8,6 +8,7 @@ import rehypeSlug from 'rehype-slug';
8
8
  import { useState, useCallback } from 'react';
9
9
  import { Copy, Check } from 'lucide-react';
10
10
  import { copyToClipboard } from '@/lib/clipboard';
11
+ import { toast } from '@/lib/toast';
11
12
  import type { Components } from 'react-markdown';
12
13
 
13
14
  interface MarkdownViewProps {
@@ -21,6 +22,7 @@ function CopyButton({ code }: { code: string }) {
21
22
  if (ok) {
22
23
  setCopied(true);
23
24
  setTimeout(() => setCopied(false), 2000);
25
+ toast.copy();
24
26
  }
25
27
  });
26
28
  }, [code]);
@@ -30,6 +30,7 @@ const DEFAULT_PANEL_WIDTH: Record<PanelId, number> = {
30
30
  echo: 280,
31
31
  agents: 280,
32
32
  discover: 280,
33
+ workflows: 280,
33
34
  };
34
35
 
35
36
  const MIN_PANEL_WIDTH = 240;
@@ -10,11 +10,14 @@ const MIN_WIDTH = 300;
10
10
  const MAX_WIDTH_ABS = 700;
11
11
  const MAX_WIDTH_RATIO = 0.45;
12
12
 
13
+ import type { AcpAgentSelection } from '@/hooks/useAskModal';
14
+
13
15
  interface RightAskPanelProps {
14
16
  open: boolean;
15
17
  onClose: () => void;
16
18
  currentFile?: string;
17
19
  initialMessage?: string;
20
+ initialAcpAgent?: AcpAgentSelection | null;
18
21
  onFirstMessage?: () => void;
19
22
  width: number;
20
23
  onWidthChange: (w: number) => void;
@@ -28,7 +31,7 @@ interface RightAskPanelProps {
28
31
  }
29
32
 
30
33
  export default function RightAskPanel({
31
- open, onClose, currentFile, initialMessage, onFirstMessage,
34
+ open, onClose, currentFile, initialMessage, initialAcpAgent, onFirstMessage,
32
35
  width, onWidthChange, onWidthCommit, askMode, onModeSwitch,
33
36
  maximized = false, onMaximize, sidebarOffset = 0,
34
37
  }: RightAskPanelProps) {
@@ -73,6 +76,7 @@ export default function RightAskPanel({
73
76
  variant="panel"
74
77
  currentFile={open ? currentFile : undefined}
75
78
  initialMessage={initialMessage}
79
+ initialAcpAgent={initialAcpAgent}
76
80
  onFirstMessage={onFirstMessage}
77
81
  onClose={onClose}
78
82
  askMode={askMode}
@@ -1,22 +1,32 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
3
+ import { useState, useEffect, useCallback, useRef, useLayoutEffect, useMemo } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { Search, X, FileText, Table } from 'lucide-react';
5
+ import { Search, X, FileText, Table, Settings, RotateCcw, Moon, Sun, Bot, Compass, HelpCircle } from 'lucide-react';
6
6
  import { SearchResult } from '@/lib/types';
7
7
  import { encodePath } from '@/lib/utils';
8
8
  import { apiFetch } from '@/lib/api';
9
9
  import { useLocale } from '@/lib/LocaleContext';
10
+ import { toast } from '@/lib/toast';
10
11
 
11
12
  interface SearchModalProps {
12
13
  open: boolean;
13
14
  onClose: () => void;
14
15
  }
15
16
 
17
+ type PaletteTab = 'search' | 'actions';
18
+
19
+ interface CommandAction {
20
+ id: string;
21
+ label: string;
22
+ icon: React.ReactNode;
23
+ shortcut?: string;
24
+ execute: () => void;
25
+ }
26
+
16
27
  /** Highlight matched text fragments in a snippet based on the query */
17
28
  function highlightSnippet(snippet: string, query: string): React.ReactNode {
18
29
  if (!query.trim()) return snippet;
19
- // Split query into words and escape for regex
20
30
  const words = query.trim().split(/\s+/).filter(Boolean);
21
31
  const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
22
32
  const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
@@ -31,12 +41,74 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
31
41
  const [results, setResults] = useState<SearchResult[]>([]);
32
42
  const [loading, setLoading] = useState(false);
33
43
  const [selectedIndex, setSelectedIndex] = useState(0);
44
+ const [tab, setTab] = useState<PaletteTab>('search');
45
+ const [actionIndex, setActionIndex] = useState(0);
34
46
  const inputRef = useRef<HTMLInputElement>(null);
35
47
  const resultsRef = useRef<HTMLDivElement>(null);
48
+ const actionsRef = useRef<HTMLDivElement>(null);
36
49
  const router = useRouter();
37
50
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
38
51
  const { t } = useLocale();
39
52
 
53
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
54
+
55
+ const actions: CommandAction[] = useMemo(() => [
56
+ {
57
+ id: 'settings',
58
+ label: t.search.openSettings,
59
+ icon: <Settings size={15} />,
60
+ shortcut: '⌘,',
61
+ execute: () => { router.push('/settings'); onClose(); },
62
+ },
63
+ {
64
+ id: 'restart-walkthrough',
65
+ label: t.search.restartWalkthrough,
66
+ icon: <RotateCcw size={15} />,
67
+ execute: () => {
68
+ fetch('/api/setup', {
69
+ method: 'PATCH',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ walkthroughStep: 0, walkthroughDismissed: false }),
72
+ }).then(() => {
73
+ toast.success(t.search.walkthroughRestarted);
74
+ }).catch(() => {
75
+ toast.error('Failed to restart walkthrough');
76
+ });
77
+ onClose();
78
+ },
79
+ },
80
+ {
81
+ id: 'toggle-dark-mode',
82
+ label: t.search.toggleDarkMode,
83
+ icon: isDark ? <Sun size={15} /> : <Moon size={15} />,
84
+ execute: () => {
85
+ const html = document.documentElement;
86
+ const nowDark = html.classList.contains('dark');
87
+ html.classList.toggle('dark', !nowDark);
88
+ try { localStorage.setItem('theme', nowDark ? 'light' : 'dark'); } catch { /* noop */ }
89
+ onClose();
90
+ },
91
+ },
92
+ {
93
+ id: 'go-agents',
94
+ label: t.search.goToAgents,
95
+ icon: <Bot size={15} />,
96
+ execute: () => { router.push('/agents'); onClose(); },
97
+ },
98
+ {
99
+ id: 'go-discover',
100
+ label: t.search.goToDiscover,
101
+ icon: <Compass size={15} />,
102
+ execute: () => { router.push('/discover'); onClose(); },
103
+ },
104
+ {
105
+ id: 'go-help',
106
+ label: t.search.goToHelp,
107
+ icon: <HelpCircle size={15} />,
108
+ execute: () => { router.push('/help'); onClose(); },
109
+ },
110
+ ], [t, router, onClose, isDark]);
111
+
40
112
  // Focus input when modal opens
41
113
  useEffect(() => {
42
114
  if (open) {
@@ -44,6 +116,8 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
44
116
  setQuery('');
45
117
  setResults([]);
46
118
  setSelectedIndex(0);
119
+ setTab('search');
120
+ setActionIndex(0);
47
121
  }
48
122
  }, [open]);
49
123
 
@@ -86,26 +160,51 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
86
160
  const handler = (e: KeyboardEvent) => {
87
161
  if (e.key === 'Escape') {
88
162
  onClose();
163
+ } else if (e.key === 'Tab') {
164
+ // Tab switches between Search/Actions tabs
165
+ e.preventDefault();
166
+ setTab(prev => prev === 'search' ? 'actions' : 'search');
167
+ setActionIndex(0);
168
+ setSelectedIndex(0);
89
169
  } else if (e.key === 'ArrowDown') {
90
170
  e.preventDefault();
91
- setSelectedIndex(i => Math.min(i + 1, results.length - 1));
171
+ if (tab === 'search') {
172
+ setSelectedIndex(i => Math.min(i + 1, results.length - 1));
173
+ } else {
174
+ setActionIndex(i => Math.min(i + 1, actions.length - 1));
175
+ }
92
176
  } else if (e.key === 'ArrowUp') {
93
177
  e.preventDefault();
94
- setSelectedIndex(i => Math.max(i - 1, 0));
178
+ if (tab === 'search') {
179
+ setSelectedIndex(i => Math.max(i - 1, 0));
180
+ } else {
181
+ setActionIndex(i => Math.max(i - 1, 0));
182
+ }
95
183
  } else if (e.key === 'Enter') {
96
- if (results[selectedIndex]) navigate(results[selectedIndex]);
184
+ if (tab === 'search') {
185
+ if (results[selectedIndex]) navigate(results[selectedIndex]);
186
+ } else {
187
+ if (actions[actionIndex]) actions[actionIndex].execute();
188
+ }
97
189
  }
98
190
  };
99
191
  window.addEventListener('keydown', handler);
100
192
  return () => window.removeEventListener('keydown', handler);
101
- }, [open, onClose, results, selectedIndex, navigate]);
193
+ }, [open, onClose, results, selectedIndex, navigate, tab, actions, actionIndex]);
102
194
 
103
195
  useLayoutEffect(() => {
104
- const container = resultsRef.current;
105
- if (!container) return;
106
- const selected = container.children[selectedIndex] as HTMLElement | undefined;
107
- selected?.scrollIntoView({ block: 'nearest' });
108
- }, [selectedIndex]);
196
+ if (tab === 'search') {
197
+ const container = resultsRef.current;
198
+ if (!container) return;
199
+ const selected = container.children[selectedIndex] as HTMLElement | undefined;
200
+ selected?.scrollIntoView({ block: 'nearest' });
201
+ } else {
202
+ const container = actionsRef.current;
203
+ if (!container) return;
204
+ const selected = container.children[actionIndex] as HTMLElement | undefined;
205
+ selected?.scrollIntoView({ block: 'nearest' });
206
+ }
207
+ }, [selectedIndex, actionIndex, tab]);
109
208
 
110
209
  if (!open) return null;
111
210
 
@@ -114,85 +213,140 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
114
213
  className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[15vh] modal-backdrop"
115
214
  onClick={(e) => e.target === e.currentTarget && onClose()}
116
215
  >
117
- <div role="dialog" aria-modal="true" aria-label="Search" className="w-full md:max-w-xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl overflow-hidden max-h-[85vh] md:max-h-none flex flex-col">
216
+ <div role="dialog" aria-modal="true" aria-label="Command palette" className="w-full md:max-w-xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl overflow-hidden max-h-[85vh] md:max-h-none flex flex-col">
118
217
  {/* Mobile drag indicator */}
119
218
  <div className="flex justify-center pt-2 pb-0 md:hidden">
120
219
  <div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
121
220
  </div>
122
- {/* Search input */}
123
- <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
124
- <Search size={16} className="text-muted-foreground shrink-0" />
125
- <input
126
- ref={inputRef}
127
- type="text"
128
- value={query}
129
- onChange={handleChange}
130
- placeholder={t.search.placeholder}
131
- className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none"
132
- />
133
- {loading && (
134
- <div className="w-4 h-4 border-2 border-muted-foreground/40 border-t-foreground rounded-full animate-spin shrink-0" />
135
- )}
136
- {!loading && query && (
137
- <button onClick={() => { setQuery(''); setResults([]); inputRef.current?.focus(); }}>
138
- <X size={14} className="text-muted-foreground hover:text-foreground" />
139
- </button>
140
- )}
141
- <kbd className="hidden md:inline text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5 font-mono">ESC</kbd>
221
+
222
+ {/* Tab bar */}
223
+ <div className="flex items-center gap-1 px-4 pt-2 pb-0">
224
+ <button
225
+ onClick={() => { setTab('search'); setTimeout(() => inputRef.current?.focus(), 50); }}
226
+ className={`px-3 py-1.5 text-xs font-medium rounded-t transition-colors ${
227
+ tab === 'search'
228
+ ? 'text-foreground border-b-2 border-[var(--amber)]'
229
+ : 'text-muted-foreground hover:text-foreground'
230
+ }`}
231
+ >
232
+ {t.search.tabSearch}
233
+ </button>
234
+ <button
235
+ onClick={() => { setTab('actions'); setActionIndex(0); }}
236
+ className={`px-3 py-1.5 text-xs font-medium rounded-t transition-colors ${
237
+ tab === 'actions'
238
+ ? 'text-foreground border-b-2 border-[var(--amber)]'
239
+ : 'text-muted-foreground hover:text-foreground'
240
+ }`}
241
+ >
242
+ {t.search.tabActions}
243
+ </button>
142
244
  </div>
143
245
 
144
- {/* Results */}
145
- <div ref={resultsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
146
- {results.length === 0 && query && !loading && (
147
- <div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
148
- )}
149
- {results.length === 0 && !query && (
150
- <div className="px-4 py-8 text-center text-sm text-muted-foreground/60">{t.search.prompt}</div>
151
- )}
152
- {results.map((result, i) => {
153
- const ext = result.path.endsWith('.csv') ? '.csv' : '.md';
154
- const parts = result.path.split('/');
155
- const fileName = parts[parts.length - 1];
156
- const dirPath = parts.slice(0, -1).join('/');
157
- return (
246
+ {/* Search tab */}
247
+ {tab === 'search' && (
248
+ <>
249
+ {/* Search input */}
250
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
251
+ <Search size={16} className="text-muted-foreground shrink-0" />
252
+ <input
253
+ ref={inputRef}
254
+ type="text"
255
+ value={query}
256
+ onChange={handleChange}
257
+ placeholder={t.search.placeholder}
258
+ className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none"
259
+ />
260
+ {loading && (
261
+ <div className="w-4 h-4 border-2 border-muted-foreground/40 border-t-foreground rounded-full animate-spin shrink-0" />
262
+ )}
263
+ {!loading && query && (
264
+ <button onClick={() => { setQuery(''); setResults([]); inputRef.current?.focus(); }}>
265
+ <X size={14} className="text-muted-foreground hover:text-foreground" />
266
+ </button>
267
+ )}
268
+ <kbd className="hidden md:inline text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5 font-mono">ESC</kbd>
269
+ </div>
270
+
271
+ {/* Results */}
272
+ <div ref={resultsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
273
+ {results.length === 0 && query && !loading && (
274
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
275
+ )}
276
+ {results.length === 0 && !query && (
277
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground/60">{t.search.prompt}</div>
278
+ )}
279
+ {results.map((result, i) => {
280
+ const ext = result.path.endsWith('.csv') ? '.csv' : '.md';
281
+ const parts = result.path.split('/');
282
+ const fileName = parts[parts.length - 1];
283
+ const dirPath = parts.slice(0, -1).join('/');
284
+ return (
285
+ <button
286
+ key={result.path}
287
+ onClick={() => navigate(result)}
288
+ onMouseEnter={() => setSelectedIndex(i)}
289
+ className={`
290
+ w-full px-4 py-3 flex items-start gap-3 text-left transition-colors duration-75
291
+ ${i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'}
292
+ ${i < results.length - 1 ? 'border-b border-border' : ''}
293
+ `}
294
+ >
295
+ {ext === '.csv'
296
+ ? <Table size={14} className="text-success shrink-0 mt-0.5" />
297
+ : <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
298
+ }
299
+ <div className="min-w-0 flex-1">
300
+ <div className="flex items-baseline gap-2 flex-wrap">
301
+ <span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
302
+ {dirPath && (
303
+ <span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
304
+ )}
305
+ </div>
306
+ {result.snippet && (
307
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
308
+ {highlightSnippet(result.snippet, query)}
309
+ </p>
310
+ )}
311
+ </div>
312
+ </button>
313
+ );
314
+ })}
315
+ </div>
316
+
317
+ {/* Footer — desktop only */}
318
+ {results.length > 0 && (
319
+ <div className="hidden md:flex px-4 py-2 border-t border-border items-center gap-3 text-xs text-muted-foreground/60">
320
+ <span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
321
+ <span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
322
+ <span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
323
+ </div>
324
+ )}
325
+ </>
326
+ )}
327
+
328
+ {/* Actions tab */}
329
+ {tab === 'actions' && (
330
+ <div ref={actionsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1 py-1">
331
+ {actions.map((action, i) => (
158
332
  <button
159
- key={result.path}
160
- onClick={() => navigate(result)}
161
- onMouseEnter={() => setSelectedIndex(i)}
333
+ key={action.id}
334
+ onClick={() => action.execute()}
335
+ onMouseEnter={() => setActionIndex(i)}
162
336
  className={`
163
- w-full px-4 py-3 flex items-start gap-3 text-left transition-colors duration-75
164
- ${i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'}
165
- ${i < results.length - 1 ? 'border-b border-border' : ''}
337
+ w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors duration-75
338
+ ${i === actionIndex ? 'bg-muted' : 'hover:bg-muted/50'}
166
339
  `}
167
340
  >
168
- {ext === '.csv'
169
- ? <Table size={14} className="text-success shrink-0 mt-0.5" />
170
- : <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
171
- }
172
- <div className="min-w-0 flex-1">
173
- <div className="flex items-baseline gap-2 flex-wrap">
174
- <span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
175
- {dirPath && (
176
- <span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
177
- )}
178
- </div>
179
- {result.snippet && (
180
- <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
181
- {highlightSnippet(result.snippet, query)}
182
- </p>
183
- )}
184
- </div>
341
+ <span className="text-muted-foreground shrink-0">{action.icon}</span>
342
+ <span className="text-sm text-foreground flex-1">{action.label}</span>
343
+ {action.shortcut && (
344
+ <kbd className="text-xs text-muted-foreground/60 font-mono border border-border rounded px-1.5 py-0.5">
345
+ {action.shortcut}
346
+ </kbd>
347
+ )}
185
348
  </button>
186
- );
187
- })}
188
- </div>
189
-
190
- {/* Footer — desktop only */}
191
- {results.length > 0 && (
192
- <div className="hidden md:flex px-4 py-2 border-t border-border items-center gap-3 text-xs text-muted-foreground/60">
193
- <span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
194
- <span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
195
- <span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
349
+ ))}
196
350
  </div>
197
351
  )}
198
352
  </div>
@@ -13,6 +13,7 @@ import SearchPanel from './panels/SearchPanel';
13
13
  import AgentsPanel from './panels/AgentsPanel';
14
14
  import DiscoverPanel from './panels/DiscoverPanel';
15
15
  import EchoPanel from './panels/EchoPanel';
16
+ import WorkflowsPanel from './panels/WorkflowsPanel';
16
17
 
17
18
  import RightAskPanel from './RightAskPanel';
18
19
  import RightAgentDetailPanel, {
@@ -407,6 +408,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
407
408
  <div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
408
409
  <DiscoverPanel active={lp.activePanel === 'discover'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
409
410
  </div>
411
+ <div className={`flex flex-col h-full ${lp.activePanel === 'workflows' ? '' : 'hidden'}`}>
412
+ <WorkflowsPanel active={lp.activePanel === 'workflows'} maximized={lp.panelMaximized} onMaximize={lp.handlePanelMaximize} />
413
+ </div>
410
414
  </Panel>
411
415
 
412
416
  {/* ── Right-side Ask AI Panel ── */}
@@ -415,6 +419,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
415
419
  onClose={ap.closeAskPanel}
416
420
  currentFile={currentFile}
417
421
  initialMessage={ap.askInitialMessage}
422
+ initialAcpAgent={ap.askAcpAgent}
418
423
  onFirstMessage={handleFirstMessage}
419
424
  width={ap.askPanelWidth}
420
425
  onWidthChange={ap.handleAskWidthChange}
@@ -441,6 +446,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
441
446
  onClose={ap.closeDesktopAskPopup}
442
447
  currentFile={currentFile}
443
448
  initialMessage={ap.askInitialMessage}
449
+ initialAcpAgent={ap.askAcpAgent}
444
450
  onFirstMessage={handleFirstMessage}
445
451
  askMode={ap.askMode}
446
452
  onModeSwitch={ap.handleAskModeSwitch}