@geminilight/mindos 0.6.29 → 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 (71) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/acp/config/route.ts +82 -0
  3. package/app/app/api/acp/detect/route.ts +71 -48
  4. package/app/app/api/acp/install/route.ts +51 -0
  5. package/app/app/api/acp/session/route.ts +141 -11
  6. package/app/app/api/ask/route.ts +116 -13
  7. package/app/app/api/workflows/route.ts +156 -0
  8. package/app/app/page.tsx +7 -2
  9. package/app/components/ActivityBar.tsx +12 -4
  10. package/app/components/AskModal.tsx +4 -1
  11. package/app/components/FileTree.tsx +21 -10
  12. package/app/components/HomeContent.tsx +1 -0
  13. package/app/components/Panel.tsx +1 -0
  14. package/app/components/RightAskPanel.tsx +5 -1
  15. package/app/components/SidebarLayout.tsx +6 -0
  16. package/app/components/agents/AgentDetailContent.tsx +263 -47
  17. package/app/components/agents/AgentsContentPage.tsx +11 -0
  18. package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
  19. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  20. package/app/components/agents/agents-content-model.ts +2 -2
  21. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  22. package/app/components/ask/AskContent.tsx +197 -239
  23. package/app/components/ask/FileChip.tsx +82 -17
  24. package/app/components/ask/MentionPopover.tsx +21 -3
  25. package/app/components/ask/MessageList.tsx +30 -9
  26. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  27. package/app/components/panels/AgentsPanel.tsx +1 -0
  28. package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
  29. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  30. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  31. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  32. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  33. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  34. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  35. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  36. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  37. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  38. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  39. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  40. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  41. package/app/hooks/useAcpConfig.ts +96 -0
  42. package/app/hooks/useAcpDetection.ts +69 -14
  43. package/app/hooks/useAcpRegistry.ts +46 -11
  44. package/app/hooks/useAskModal.ts +12 -5
  45. package/app/hooks/useAskPanel.ts +8 -5
  46. package/app/hooks/useAskSession.ts +19 -2
  47. package/app/hooks/useImageUpload.ts +152 -0
  48. package/app/lib/acp/acp-tools.ts +3 -1
  49. package/app/lib/acp/agent-descriptors.ts +274 -0
  50. package/app/lib/acp/bridge.ts +6 -0
  51. package/app/lib/acp/index.ts +20 -4
  52. package/app/lib/acp/registry.ts +74 -7
  53. package/app/lib/acp/session.ts +481 -28
  54. package/app/lib/acp/subprocess.ts +307 -21
  55. package/app/lib/acp/types.ts +158 -20
  56. package/app/lib/agent/model.ts +18 -3
  57. package/app/lib/agent/to-agent-messages.ts +25 -2
  58. package/app/lib/i18n/modules/knowledge.ts +4 -0
  59. package/app/lib/i18n/modules/navigation.ts +2 -0
  60. package/app/lib/i18n/modules/panels.ts +146 -2
  61. package/app/lib/pi-integration/skills.ts +21 -6
  62. package/app/lib/renderers/index.ts +2 -2
  63. package/app/lib/settings.ts +10 -0
  64. package/app/lib/types.ts +12 -1
  65. package/app/next-env.d.ts +1 -1
  66. package/app/package.json +3 -1
  67. package/package.json +1 -1
  68. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  69. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  70. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  71. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -15,7 +15,7 @@ import { NextRequest, NextResponse } from 'next/server';
15
15
  import fs from 'fs';
16
16
  import path from 'path';
17
17
  import { getFileContent, getMindRoot } from '@/lib/fs';
18
- import { getModelConfig } from '@/lib/agent/model';
18
+ import { getModelConfig, hasImages } from '@/lib/agent/model';
19
19
  import { getRequestScopedTools, getOrganizeTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
20
20
  import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT } from '@/lib/agent/prompt';
21
21
  import { toAgentMessages } from '@/lib/agent/to-agent-messages';
@@ -25,8 +25,16 @@ import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
25
25
  import { metrics } from '@/lib/metrics';
26
26
  import { assertNotProtected } from '@/lib/core';
27
27
  import { scanExtensionPaths } from '@/lib/pi-integration/extensions';
28
+ import { createSession, promptStream, closeSession } from '@/lib/acp/session';
29
+ import type { AcpSessionUpdate } from '@/lib/acp/types';
28
30
  import type { Message as FrontendMessage } from '@/lib/types';
29
31
 
32
+ /** Safe JSON parse — returns {} on invalid input */
33
+ function safeParseJson(raw: string | undefined): Record<string, unknown> {
34
+ if (!raw) return {};
35
+ try { return JSON.parse(raw); } catch { return {}; }
36
+ }
37
+
30
38
  // ---------------------------------------------------------------------------
31
39
  // MindOS SSE format — 6 event types (front-back contract)
32
40
  // ---------------------------------------------------------------------------
@@ -254,6 +262,8 @@ export async function POST(req: NextRequest) {
254
262
  maxSteps?: number;
255
263
  /** 'organize' = lean prompt for file import organize; default = full prompt */
256
264
  mode?: 'organize' | 'default';
265
+ /** ACP agent selection: if present, route to ACP instead of MindOS */
266
+ selectedAcpAgent?: { id: string; name: string } | null;
257
267
  };
258
268
  try {
259
269
  body = await req.json();
@@ -261,7 +271,7 @@ export async function POST(req: NextRequest) {
261
271
  return apiError(ErrorCodes.INVALID_REQUEST, 'Invalid JSON body', 400);
262
272
  }
263
273
 
264
- const { messages, currentFile, attachedFiles, uploadedFiles } = body;
274
+ const { messages, currentFile, attachedFiles, uploadedFiles, selectedAcpAgent } = body;
265
275
  const isOrganizeMode = body.mode === 'organize';
266
276
 
267
277
  // Read agent config from settings
@@ -433,15 +443,18 @@ export async function POST(req: NextRequest) {
433
443
  }
434
444
 
435
445
  try {
436
- const { model, modelName, apiKey, provider } = getModelConfig();
446
+ const { model, modelName, apiKey, provider } = getModelConfig({ hasImages: hasImages(messages) });
437
447
 
438
448
  // Convert frontend messages to AgentMessage[]
439
449
  const agentMessages = toAgentMessages(messages);
440
450
 
441
451
  // Extract the last user message for agent.prompt()
442
- const lastUserContent = messages.length > 0 && messages[messages.length - 1].role === 'user'
443
- ? messages[messages.length - 1].content
444
- : '';
452
+ const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
453
+ const lastUserContent = lastMsg?.role === 'user' ? lastMsg.content : '';
454
+ // Extract images for prompt options (pi-ai ImageContent format, skip stripped)
455
+ const lastUserImages = lastMsg?.role === 'user' && lastMsg.images?.length
456
+ ? lastMsg.images.filter((img: any) => img.data).map((img: any) => ({ type: 'image' as const, data: img.data, mimeType: img.mimeType }))
457
+ : undefined;
445
458
 
446
459
  // History = all messages except the last user message (agent.prompt adds it)
447
460
  const historyMessages = agentMessages.slice(0, -1);
@@ -603,15 +616,105 @@ export async function POST(req: NextRequest) {
603
616
  }
604
617
  });
605
618
 
606
- session.prompt(lastUserContent).then(() => {
607
- metrics.recordRequest(Date.now() - requestStartTime);
608
- if (!hasContent && lastModelError) {
609
- send({ type: 'error', message: lastModelError });
619
+ // ── Route to ACP agent if selected, otherwise use MindOS agent ──
620
+ const runAgent = async () => {
621
+ if (selectedAcpAgent) {
622
+ // Route to ACP agent with real-time streaming
623
+ let acpSessionId: string | undefined;
624
+ try {
625
+ const acpSession = await createSession(selectedAcpAgent.id, {
626
+ cwd: getMindRoot(),
627
+ });
628
+ acpSessionId = acpSession.id;
629
+
630
+ await promptStream(acpSessionId, lastUserContent, (update: AcpSessionUpdate) => {
631
+ switch (update.type) {
632
+ // Text chunks → standard text_delta
633
+ case 'agent_message_chunk':
634
+ case 'text':
635
+ if (update.text) {
636
+ hasContent = true;
637
+ send({ type: 'text_delta', delta: update.text });
638
+ }
639
+ break;
640
+
641
+ // Agent thinking → thinking_delta (reuses existing Anthropic thinking UI)
642
+ case 'agent_thought_chunk':
643
+ if (update.text) {
644
+ hasContent = true;
645
+ send({ type: 'thinking_delta', delta: update.text });
646
+ }
647
+ break;
648
+
649
+ // Tool calls → tool_start (reuses existing tool call UI)
650
+ case 'tool_call':
651
+ if (update.toolCall) {
652
+ hasContent = true;
653
+ send({
654
+ type: 'tool_start',
655
+ toolCallId: update.toolCall.toolCallId,
656
+ toolName: update.toolCall.title ?? update.toolCall.kind ?? 'tool',
657
+ args: safeParseJson(update.toolCall.rawInput),
658
+ });
659
+ }
660
+ break;
661
+
662
+ // Tool call updates → tool_end when completed/failed
663
+ case 'tool_call_update':
664
+ if (update.toolCall && (update.toolCall.status === 'completed' || update.toolCall.status === 'failed')) {
665
+ send({
666
+ type: 'tool_end',
667
+ toolCallId: update.toolCall.toolCallId,
668
+ output: update.toolCall.rawOutput ?? '',
669
+ isError: update.toolCall.status === 'failed',
670
+ });
671
+ }
672
+ break;
673
+
674
+ // Plan → emit as text with structured format
675
+ case 'plan':
676
+ if (update.plan?.entries) {
677
+ const planText = update.plan.entries
678
+ .map(e => {
679
+ const icon = e.status === 'completed' ? '\u2705' : e.status === 'in_progress' ? '\u26a1' : '\u23f3';
680
+ return `${icon} ${e.content}`;
681
+ })
682
+ .join('\n');
683
+ send({ type: 'text_delta', delta: `\n\n${planText}\n\n` });
684
+ }
685
+ break;
686
+
687
+ // Error → stream error
688
+ case 'error':
689
+ send({ type: 'error', message: update.error ?? 'ACP agent error' });
690
+ break;
691
+ }
692
+ });
693
+
694
+ send({ type: 'done' });
695
+ } catch (acpErr) {
696
+ const errMsg = acpErr instanceof Error ? acpErr.message : String(acpErr);
697
+ send({ type: 'error', message: `ACP Agent Error: ${errMsg}` });
698
+ } finally {
699
+ if (acpSessionId) {
700
+ await closeSession(acpSessionId).catch(() => {});
701
+ }
702
+ }
703
+ controller.close();
610
704
  } else {
611
- send({ type: 'done' });
705
+ // Route to MindOS agent (existing logic)
706
+ await session.prompt(lastUserContent, lastUserImages ? { images: lastUserImages } : undefined);
707
+ metrics.recordRequest(Date.now() - requestStartTime);
708
+ if (!hasContent && lastModelError) {
709
+ send({ type: 'error', message: lastModelError });
710
+ } else {
711
+ send({ type: 'done' });
712
+ }
713
+ controller.close();
612
714
  }
613
- controller.close();
614
- }).catch((err) => {
715
+ };
716
+
717
+ runAgent().catch((err) => {
615
718
  metrics.recordRequest(Date.now() - requestStartTime);
616
719
  metrics.recordError();
617
720
  send({ type: 'error', message: err instanceof Error ? err.message : String(err) });
@@ -0,0 +1,156 @@
1
+ import { NextResponse } from 'next/server';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import YAML from 'js-yaml';
5
+ import { getMindRoot } from '@/lib/fs';
6
+
7
+ const WORKFLOWS_DIR = '.mindos/workflows';
8
+
9
+ function getWorkflowsDir(): string {
10
+ return path.join(getMindRoot(), WORKFLOWS_DIR);
11
+ }
12
+
13
+ interface WorkflowListItem {
14
+ path: string;
15
+ fileName: string;
16
+ title: string;
17
+ description?: string;
18
+ stepCount: number;
19
+ mtime: number;
20
+ error?: string;
21
+ }
22
+
23
+ function listWorkflows(): WorkflowListItem[] {
24
+ const dir = getWorkflowsDir();
25
+ if (!fs.existsSync(dir)) return [];
26
+
27
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
28
+ const items: WorkflowListItem[] = [];
29
+
30
+ for (const entry of entries) {
31
+ if (!entry.isFile()) continue;
32
+ if (!/\.flow\.(yaml|yml)$/i.test(entry.name)) continue;
33
+
34
+ const fullPath = path.join(dir, entry.name);
35
+ const relativePath = path.join(WORKFLOWS_DIR, entry.name);
36
+ const stat = fs.statSync(fullPath);
37
+
38
+ try {
39
+ const content = fs.readFileSync(fullPath, 'utf-8');
40
+ const parsed = YAML.load(content, { schema: YAML.JSON_SCHEMA }) as Record<string, unknown> | null;
41
+
42
+ if (!parsed || typeof parsed !== 'object') {
43
+ items.push({
44
+ path: relativePath,
45
+ fileName: entry.name,
46
+ title: entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
47
+ stepCount: 0,
48
+ mtime: stat.mtimeMs,
49
+ error: 'Invalid YAML',
50
+ });
51
+ continue;
52
+ }
53
+
54
+ const steps = Array.isArray(parsed.steps) ? parsed.steps : [];
55
+ items.push({
56
+ path: relativePath,
57
+ fileName: entry.name,
58
+ title: typeof parsed.title === 'string' ? parsed.title : entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
59
+ description: typeof parsed.description === 'string' ? parsed.description : undefined,
60
+ stepCount: steps.length,
61
+ mtime: stat.mtimeMs,
62
+ });
63
+ } catch (err) {
64
+ items.push({
65
+ path: relativePath,
66
+ fileName: entry.name,
67
+ title: entry.name.replace(/\.flow\.(yaml|yml)$/i, ''),
68
+ stepCount: 0,
69
+ mtime: stat.mtimeMs,
70
+ error: err instanceof Error ? err.message : 'Parse error',
71
+ });
72
+ }
73
+ }
74
+
75
+ // Sort by mtime descending (most recent first)
76
+ items.sort((a, b) => b.mtime - a.mtime);
77
+ return items;
78
+ }
79
+
80
+ const BLANK_TEMPLATE = `title: {TITLE}
81
+ description: ""
82
+
83
+ steps:
84
+ - id: step-1
85
+ name: Step 1
86
+ prompt: |
87
+ Describe what this step should do.
88
+ `;
89
+
90
+ export async function GET() {
91
+ try {
92
+ const workflows = listWorkflows();
93
+ return NextResponse.json({ workflows });
94
+ } catch (err) {
95
+ return NextResponse.json(
96
+ { error: err instanceof Error ? err.message : 'Failed to list workflows' },
97
+ { status: 500 },
98
+ );
99
+ }
100
+ }
101
+
102
+ export async function POST(req: Request) {
103
+ try {
104
+ const body = await req.json();
105
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
106
+ if (!name) {
107
+ return NextResponse.json({ error: 'name is required' }, { status: 400 });
108
+ }
109
+
110
+ // Sanitize filename
111
+ const safeName = name.replace(/[/\\:*?"<>|]/g, '-');
112
+ const fileName = `${safeName}.flow.yaml`;
113
+ const dir = getWorkflowsDir();
114
+
115
+ // Create Workflows dir if needed
116
+ if (!fs.existsSync(dir)) {
117
+ fs.mkdirSync(dir, { recursive: true });
118
+ }
119
+
120
+ const fullPath = path.join(dir, fileName);
121
+ if (fs.existsSync(fullPath)) {
122
+ return NextResponse.json({ error: 'Workflow already exists' }, { status: 409 });
123
+ }
124
+
125
+ // Generate content from template or blank
126
+ const template = typeof body.template === 'string' ? body.template : 'blank';
127
+ let content: string;
128
+
129
+ if (template !== 'blank') {
130
+ // Try to read template from templates dir
131
+ const templateDir = path.join(getMindRoot(), WORKFLOWS_DIR);
132
+ const templateFile = fs.readdirSync(templateDir).find(f =>
133
+ f.toLowerCase().includes(template.toLowerCase()) && /\.workflow\.(yaml|yml)$/i.test(f)
134
+ );
135
+ if (templateFile) {
136
+ content = fs.readFileSync(path.join(templateDir, templateFile), 'utf-8');
137
+ // Replace title with user's name
138
+ content = content.replace(/^title:.*$/m, `title: ${name}`);
139
+ } else {
140
+ content = BLANK_TEMPLATE.replace('{TITLE}', name);
141
+ }
142
+ } else {
143
+ content = BLANK_TEMPLATE.replace('{TITLE}', name);
144
+ }
145
+
146
+ fs.writeFileSync(fullPath, content, 'utf-8');
147
+
148
+ const relativePath = path.join(WORKFLOWS_DIR, fileName);
149
+ return NextResponse.json({ path: relativePath });
150
+ } catch (err) {
151
+ return NextResponse.json(
152
+ { error: err instanceof Error ? err.message : 'Failed to create workflow' },
153
+ { status: 500 },
154
+ );
155
+ }
156
+ }
package/app/app/page.tsx CHANGED
@@ -1,6 +1,8 @@
1
1
  import { redirect } from 'next/navigation';
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
  import { readSettings } from '@/lib/settings';
3
- import { getRecentlyModified, getFileContent, getFileTree } from '@/lib/fs';
5
+ import { getRecentlyModified, getFileContent, getFileTree, getMindRoot } from '@/lib/fs';
4
6
  import { getAllRenderers } from '@/lib/renderers/registry';
5
7
  import HomeContent from '@/components/HomeContent';
6
8
  import type { FileNode } from '@/lib/core/types';
@@ -52,8 +54,11 @@ function getTopLevelDirs(): SpaceInfo[] {
52
54
  }
53
55
 
54
56
  function getExistingFiles(paths: string[]): string[] {
57
+ const root = getMindRoot();
55
58
  return paths.filter(p => {
56
- try { getFileContent(p); return true; } catch { return false; }
59
+ try {
60
+ return fs.existsSync(path.join(root, p));
61
+ } catch { return false; }
57
62
  });
58
63
  }
59
64
 
@@ -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}
@@ -6,7 +6,7 @@ import { FileNode } from '@/lib/types';
6
6
  import { encodePath } from '@/lib/utils';
7
7
  import {
8
8
  ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
9
- Trash2, Pencil, Layers, ScrollText, FolderInput, Copy,
9
+ Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal,
10
10
  } from 'lucide-react';
11
11
  import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
12
12
  import { useLocale } from '@/lib/LocaleContext';
@@ -567,7 +567,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
567
567
  const [renaming, setRenaming] = useState(false);
568
568
  const [renameValue, setRenameValue] = useState(node.name);
569
569
  const [isPending, startTransition] = useTransition();
570
- const [isPendingDelete, startDeleteTransition] = useTransition();
570
+ const [, startDeleteTransition] = useTransition();
571
571
  const renameRef = useRef<HTMLInputElement>(null);
572
572
  const { t } = useLocale();
573
573
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -661,14 +661,17 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
661
661
  <span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
662
662
  </button>
663
663
  <div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
664
- <button onClick={() => copyPathToClipboard(node.path)} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.copyPath}>
665
- <Copy size={12} />
666
- </button>
667
- <button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
668
- <Pencil size={12} />
669
- </button>
670
- <button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-error hover:bg-muted transition-colors" title={t.fileTree.delete}>
671
- {isPendingDelete ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
664
+ <button
665
+ type="button"
666
+ onClick={(e) => {
667
+ e.stopPropagation();
668
+ const rect = e.currentTarget.getBoundingClientRect();
669
+ setContextMenu({ x: rect.left, y: rect.bottom + 4 });
670
+ }}
671
+ className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
672
+ title="More"
673
+ >
674
+ <MoreHorizontal size={14} />
672
675
  </button>
673
676
  </div>
674
677
  {contextMenu && (
@@ -676,10 +679,18 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
676
679
  x={contextMenu.x}
677
680
  y={contextMenu.y}
678
681
  onClose={() => setContextMenu(null)}
682
+ menuHeight={140}
679
683
  >
680
684
  <button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); setContextMenu(null); }}>
681
685
  <Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
682
686
  </button>
687
+ <button className={MENU_ITEM} onClick={(e) => { setContextMenu(null); startRename(e); }}>
688
+ <Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
689
+ </button>
690
+ <div className={MENU_DIVIDER} />
691
+ <button className={MENU_DANGER} onClick={(e) => { setContextMenu(null); handleDelete(e); }}>
692
+ <Trash2 size={14} className="shrink-0" /> {t.fileTree.delete}
693
+ </button>
683
694
  </ContextMenuShell>
684
695
  )}
685
696
  <ConfirmDialog
@@ -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 */
@@ -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}
@@ -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}