@geminilight/mindos 0.6.27 → 0.6.29
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/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/detect/route.ts +91 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +55 -0
- package/app/app/layout.tsx +2 -0
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +19 -0
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/agents/AgentDetailContent.tsx +51 -6
- package/app/components/agents/AgentsContentPage.tsx +24 -6
- package/app/components/agents/AgentsOverviewSection.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AskContent.tsx +8 -0
- package/app/components/help/HelpContent.tsx +74 -18
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelAgentListRow.tsx +10 -1
- package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -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/useAcpDetection.ts +65 -0
- package/app/hooks/useAcpRegistry.ts +51 -0
- package/app/hooks/useDelegationHistory.ts +49 -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 +93 -0
- package/app/lib/acp/bridge.ts +138 -0
- package/app/lib/acp/index.ts +24 -0
- package/app/lib/acp/registry.ts +135 -0
- package/app/lib/acp/session.ts +264 -0
- package/app/lib/acp/subprocess.ts +209 -0
- package/app/lib/acp/types.ts +136 -0
- 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 +425 -0
- package/app/lib/i18n/modules/navigation.ts +151 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1052 -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/toast.ts +79 -0
- 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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { getDelegationHistory } from '@/lib/a2a/client';
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const delegations = getDelegationHistory();
|
|
8
|
+
return NextResponse.json({ delegations });
|
|
9
|
+
}
|
package/app/app/api/a2a/route.ts
CHANGED
|
@@ -74,12 +74,12 @@ export async function POST(req: NextRequest) {
|
|
|
74
74
|
if (!params?.id) {
|
|
75
75
|
return respond(jsonRpcError(rpc.id, A2A_ERRORS.INVALID_PARAMS));
|
|
76
76
|
}
|
|
77
|
-
const task = handleCancelTask(params);
|
|
78
|
-
if (
|
|
79
|
-
return respond(jsonRpcError(rpc.id,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
const { task, reason } = handleCancelTask(params);
|
|
78
|
+
if (reason === 'not_found') {
|
|
79
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.TASK_NOT_FOUND));
|
|
80
|
+
}
|
|
81
|
+
if (reason === 'not_cancelable') {
|
|
82
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.TASK_NOT_CANCELABLE));
|
|
83
83
|
}
|
|
84
84
|
return respond(jsonRpcOk(rpc.id, task));
|
|
85
85
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getAcpAgents } from '@/lib/acp/registry';
|
|
6
|
+
|
|
7
|
+
/* ── Agent ID → binary name mapping ─────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
const AGENT_BINARY_MAP: Record<string, string> = {
|
|
10
|
+
claude: 'claude',
|
|
11
|
+
'claude-code': 'claude',
|
|
12
|
+
gemini: 'gemini',
|
|
13
|
+
'gemini-cli': 'gemini',
|
|
14
|
+
codex: 'codex',
|
|
15
|
+
cursor: 'cursor',
|
|
16
|
+
cline: 'cline',
|
|
17
|
+
goose: 'goose',
|
|
18
|
+
opencode: 'opencode',
|
|
19
|
+
kilo: 'kilo',
|
|
20
|
+
codebuddy: 'codebuddy',
|
|
21
|
+
openclaw: 'openclaw',
|
|
22
|
+
pi: 'pi',
|
|
23
|
+
augment: 'auggie',
|
|
24
|
+
iflow: 'iflow',
|
|
25
|
+
kimi: 'kimi',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const INSTALL_COMMANDS: Record<string, string> = {
|
|
29
|
+
claude: 'npm install -g @anthropic-ai/claude-code',
|
|
30
|
+
'claude-code': 'npm install -g @anthropic-ai/claude-code',
|
|
31
|
+
gemini: 'npm install -g @anthropic-ai/claude-code && npx @anthropic-ai/claude-code',
|
|
32
|
+
'gemini-cli': 'npm install -g @anthropic-ai/claude-code',
|
|
33
|
+
codex: 'npm install -g @openai/codex',
|
|
34
|
+
goose: 'pip install goose-ai',
|
|
35
|
+
opencode: 'go install github.com/opencode-ai/opencode@latest',
|
|
36
|
+
codebuddy: 'npm install -g @anthropic-ai/claude-code',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface InstalledAgent {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
binaryPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NotInstalledAgent {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
installCmd: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function whichBinary(binary: string): string | null {
|
|
52
|
+
try {
|
|
53
|
+
return execSync(`which ${binary}`, { encoding: 'utf-8', timeout: 3000 }).trim() || null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function GET() {
|
|
60
|
+
try {
|
|
61
|
+
const agents = await getAcpAgents();
|
|
62
|
+
const installed: InstalledAgent[] = [];
|
|
63
|
+
const notInstalled: NotInstalledAgent[] = [];
|
|
64
|
+
|
|
65
|
+
for (const agent of agents) {
|
|
66
|
+
const binary = AGENT_BINARY_MAP[agent.id] ?? AGENT_BINARY_MAP[agent.command] ?? agent.command;
|
|
67
|
+
if (!binary) {
|
|
68
|
+
notInstalled.push({ id: agent.id, name: agent.name, installCmd: '' });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const binaryPath = whichBinary(binary);
|
|
73
|
+
if (binaryPath) {
|
|
74
|
+
installed.push({ id: agent.id, name: agent.name, binaryPath });
|
|
75
|
+
} else {
|
|
76
|
+
notInstalled.push({
|
|
77
|
+
id: agent.id,
|
|
78
|
+
name: agent.name,
|
|
79
|
+
installCmd: INSTALL_COMMANDS[agent.id] ?? `npm install -g ${agent.command}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return NextResponse.json({ installed, notInstalled });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: (err as Error).message, installed: [], notInstalled: [] },
|
|
88
|
+
{ status: 500 },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { fetchAcpRegistry, findAcpAgent } from '@/lib/acp/registry';
|
|
5
|
+
|
|
6
|
+
export async function GET(req: Request) {
|
|
7
|
+
try {
|
|
8
|
+
const { searchParams } = new URL(req.url);
|
|
9
|
+
const agentId = searchParams.get('agent');
|
|
10
|
+
|
|
11
|
+
if (agentId) {
|
|
12
|
+
const agent = await findAcpAgent(agentId);
|
|
13
|
+
if (!agent) {
|
|
14
|
+
return NextResponse.json({ error: 'Agent not found', agent: null }, { status: 404 });
|
|
15
|
+
}
|
|
16
|
+
return NextResponse.json({ agent });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const registry = await fetchAcpRegistry();
|
|
20
|
+
if (!registry) {
|
|
21
|
+
return NextResponse.json({ error: 'Failed to fetch registry', registry: null }, { status: 502 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({ registry });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: (err as Error).message },
|
|
28
|
+
{ status: 500 },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
import { createSession, closeSession, getSession, getActiveSessions } from '@/lib/acp/session';
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const sessions = getActiveSessions();
|
|
9
|
+
return NextResponse.json({ sessions });
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: (err as Error).message },
|
|
13
|
+
{ status: 500 },
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(req: Request) {
|
|
19
|
+
try {
|
|
20
|
+
const { agentId, env } = await req.json();
|
|
21
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
22
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const session = await createSession(agentId, { env });
|
|
26
|
+
return NextResponse.json({ session });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: (err as Error).message },
|
|
30
|
+
{ status: 500 },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function DELETE(req: Request) {
|
|
36
|
+
try {
|
|
37
|
+
const { sessionId } = await req.json();
|
|
38
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
39
|
+
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const session = getSession(sessionId);
|
|
43
|
+
if (!session) {
|
|
44
|
+
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await closeSession(sessionId);
|
|
48
|
+
return NextResponse.json({ ok: true });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
{ error: (err as Error).message },
|
|
52
|
+
{ status: 500 },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
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>
|
|
@@ -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
|
}
|
|
@@ -571,6 +571,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
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}
|
|
@@ -663,6 +671,17 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
663
671
|
{isPendingDelete ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
664
672
|
</button>
|
|
665
673
|
</div>
|
|
674
|
+
{contextMenu && (
|
|
675
|
+
<ContextMenuShell
|
|
676
|
+
x={contextMenu.x}
|
|
677
|
+
y={contextMenu.y}
|
|
678
|
+
onClose={() => setContextMenu(null)}
|
|
679
|
+
>
|
|
680
|
+
<button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); setContextMenu(null); }}>
|
|
681
|
+
<Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
|
|
682
|
+
</button>
|
|
683
|
+
</ContextMenuShell>
|
|
684
|
+
)}
|
|
666
685
|
<ConfirmDialog
|
|
667
686
|
open={showDeleteConfirm}
|
|
668
687
|
title={t.fileTree.delete}
|
|
@@ -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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
349
|
-
{
|
|
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">
|
|
@@ -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]);
|