@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.
- package/README.md +10 -4
- package/app/app/api/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +114 -0
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +185 -0
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/layout.tsx +2 -0
- package/app/app/page.tsx +7 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +40 -10
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +266 -52
- package/app/components/agents/AgentsContentPage.tsx +32 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +2 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +177 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +120 -0
- package/app/hooks/useAcpRegistry.ts +86 -0
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +95 -0
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +144 -0
- package/app/lib/acp/index.ts +40 -0
- package/app/lib/acp/registry.ts +202 -0
- package/app/lib/acp/session.ts +717 -0
- package/app/lib/acp/subprocess.ts +495 -0
- package/app/lib/acp/types.ts +274 -0
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +429 -0
- package/app/lib/i18n/modules/navigation.ts +153 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1196 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/toast.ts +79 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
package/app/app/api/ask/route.ts
CHANGED
|
@@ -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
|
|
443
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
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/layout.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import ShellLayout from '@/components/ShellLayout';
|
|
|
6
6
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
7
7
|
import { LocaleProvider } from '@/lib/LocaleContext';
|
|
8
8
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
9
|
+
import Toaster from '@/components/ui/Toaster';
|
|
9
10
|
import RegisterSW from './register-sw';
|
|
10
11
|
import UpdateOverlay from '@/components/UpdateOverlay';
|
|
11
12
|
import { cookies } from 'next/headers';
|
|
@@ -113,6 +114,7 @@ export default async function RootLayout({
|
|
|
113
114
|
</ShellLayout>
|
|
114
115
|
</ErrorBoundary>
|
|
115
116
|
</TooltipProvider>
|
|
117
|
+
<Toaster />
|
|
116
118
|
<RegisterSW />
|
|
117
119
|
<UpdateOverlay />
|
|
118
120
|
</LocaleProvider>
|
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 {
|
|
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={<
|
|
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}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useSyncExternalStore, useMemo } from 'react';
|
|
3
|
+
import { useSyncExternalStore, useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus, ScrollText, BookOpen } from 'lucide-react';
|
|
5
|
+
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus, ScrollText, BookOpen, Copy } from 'lucide-react';
|
|
6
6
|
import Breadcrumb from '@/components/Breadcrumb';
|
|
7
7
|
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
8
|
import { FileNode } from '@/lib/types';
|
|
@@ -11,6 +11,10 @@ import { useLocale } from '@/lib/LocaleContext';
|
|
|
11
11
|
|
|
12
12
|
const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
|
|
13
13
|
|
|
14
|
+
async function copyPathToClipboard(path: string) {
|
|
15
|
+
try { await navigator.clipboard.writeText(path); } catch { /* noop */ }
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
interface DirViewProps {
|
|
15
19
|
dirPath: string;
|
|
16
20
|
entries: FileNode[];
|
|
@@ -126,12 +130,57 @@ function SpacePreviewSection({ preview, dirPath }: {
|
|
|
126
130
|
);
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// ─── Context Menu for DirView entries ─────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function DirContextMenu({ x, y, path, label, onClose }: {
|
|
136
|
+
x: number; y: number; path: string; label: string; onClose: () => void;
|
|
137
|
+
}) {
|
|
138
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
139
|
+
const { t } = useLocale();
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const handleClick = (e: MouseEvent) => {
|
|
143
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) onClose();
|
|
144
|
+
};
|
|
145
|
+
const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
146
|
+
document.addEventListener('mousedown', handleClick);
|
|
147
|
+
document.addEventListener('keydown', handleKey);
|
|
148
|
+
return () => { document.removeEventListener('mousedown', handleClick); document.removeEventListener('keydown', handleKey); };
|
|
149
|
+
}, [onClose]);
|
|
150
|
+
|
|
151
|
+
// Keep within viewport
|
|
152
|
+
const adjX = typeof window !== 'undefined' ? Math.min(x, window.innerWidth - 200) : x;
|
|
153
|
+
const adjY = typeof window !== 'undefined' ? Math.min(y, window.innerHeight - 60) : y;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
ref={menuRef}
|
|
158
|
+
className="fixed z-50 min-w-[160px] bg-card border border-border rounded-lg shadow-lg py-1"
|
|
159
|
+
style={{ top: adjY, left: adjX }}
|
|
160
|
+
>
|
|
161
|
+
<button
|
|
162
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-foreground hover:bg-muted transition-colors text-left"
|
|
163
|
+
onClick={() => { copyPathToClipboard(path); onClose(); }}
|
|
164
|
+
>
|
|
165
|
+
<Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
// ─── DirView ──────────────────────────────────────────────────────────────────
|
|
130
172
|
|
|
131
173
|
export default function DirView({ dirPath, entries, spacePreview }: DirViewProps) {
|
|
132
174
|
const [view, setView] = useDirViewPref();
|
|
133
175
|
const { t } = useLocale();
|
|
134
176
|
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
177
|
+
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; path: string } | null>(null);
|
|
178
|
+
|
|
179
|
+
const handleCtx = useCallback((e: React.MouseEvent, path: string) => {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
setCtxMenu({ x: e.clientX, y: e.clientY, path });
|
|
183
|
+
}, []);
|
|
135
184
|
|
|
136
185
|
const visibleEntries = useMemo(() => {
|
|
137
186
|
if (spacePreview) {
|
|
@@ -198,6 +247,7 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
|
|
|
198
247
|
<Link
|
|
199
248
|
key={entry.path}
|
|
200
249
|
href={`/view/${encodePath(entry.path)}`}
|
|
250
|
+
onContextMenu={(e) => handleCtx(e, entry.path)}
|
|
201
251
|
className={
|
|
202
252
|
entry.type === 'directory'
|
|
203
253
|
? 'flex flex-col items-center gap-1.5 p-3 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
|
|
@@ -227,6 +277,7 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
|
|
|
227
277
|
<Link
|
|
228
278
|
key={entry.path}
|
|
229
279
|
href={`/view/${encodePath(entry.path)}`}
|
|
280
|
+
onContextMenu={(e) => handleCtx(e, entry.path)}
|
|
230
281
|
className="flex items-center gap-3 px-4 py-3 bg-card hover:bg-accent transition-colors duration-100"
|
|
231
282
|
>
|
|
232
283
|
<FileIcon node={entry} />
|
|
@@ -246,6 +297,17 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
|
|
|
246
297
|
)}
|
|
247
298
|
</div>
|
|
248
299
|
</div>
|
|
300
|
+
|
|
301
|
+
{/* Context menu */}
|
|
302
|
+
{ctxMenu && (
|
|
303
|
+
<DirContextMenu
|
|
304
|
+
x={ctxMenu.x}
|
|
305
|
+
y={ctxMenu.y}
|
|
306
|
+
path={ctxMenu.path}
|
|
307
|
+
label={t.fileTree.copyPath}
|
|
308
|
+
onClose={() => setCtxMenu(null)}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
249
311
|
</div>
|
|
250
312
|
);
|
|
251
313
|
}
|
|
@@ -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,10 +567,11 @@ 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 [
|
|
570
|
+
const [, startDeleteTransition] = useTransition();
|
|
571
571
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
572
572
|
const { t } = useLocale();
|
|
573
573
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
574
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
574
575
|
|
|
575
576
|
const handleClick = useCallback(() => {
|
|
576
577
|
if (renaming) return;
|
|
@@ -611,6 +612,12 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
611
612
|
e.dataTransfer.effectAllowed = 'copy';
|
|
612
613
|
}, [node.path]);
|
|
613
614
|
|
|
615
|
+
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
616
|
+
e.preventDefault();
|
|
617
|
+
e.stopPropagation();
|
|
618
|
+
setContextMenu({ x: e.clientX, y: e.clientY });
|
|
619
|
+
}, []);
|
|
620
|
+
|
|
614
621
|
if (renaming) {
|
|
615
622
|
return (
|
|
616
623
|
<div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
|
|
@@ -636,6 +643,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
636
643
|
<button
|
|
637
644
|
onClick={handleClick}
|
|
638
645
|
onDoubleClick={startRename}
|
|
646
|
+
onContextMenu={handleContextMenu}
|
|
639
647
|
draggable
|
|
640
648
|
onDragStart={handleDragStart}
|
|
641
649
|
data-filepath={node.path}
|
|
@@ -653,16 +661,38 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
653
661
|
<span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
|
|
654
662
|
</button>
|
|
655
663
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
|
|
656
|
-
<button
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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} />
|
|
664
675
|
</button>
|
|
665
676
|
</div>
|
|
677
|
+
{contextMenu && (
|
|
678
|
+
<ContextMenuShell
|
|
679
|
+
x={contextMenu.x}
|
|
680
|
+
y={contextMenu.y}
|
|
681
|
+
onClose={() => setContextMenu(null)}
|
|
682
|
+
menuHeight={140}
|
|
683
|
+
>
|
|
684
|
+
<button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); setContextMenu(null); }}>
|
|
685
|
+
<Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
|
|
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>
|
|
694
|
+
</ContextMenuShell>
|
|
695
|
+
)}
|
|
666
696
|
<ConfirmDialog
|
|
667
697
|
open={showDeleteConfirm}
|
|
668
698
|
title={t.fileTree.delete}
|