@geminilight/mindos 0.6.28 → 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.
Files changed (64) hide show
  1. package/app/app/api/a2a/agents/route.ts +9 -0
  2. package/app/app/api/a2a/delegations/route.ts +9 -0
  3. package/app/app/api/a2a/discover/route.ts +2 -0
  4. package/app/app/api/a2a/route.ts +6 -6
  5. package/app/app/api/acp/detect/route.ts +91 -0
  6. package/app/app/api/acp/registry/route.ts +31 -0
  7. package/app/app/api/acp/session/route.ts +55 -0
  8. package/app/app/layout.tsx +2 -0
  9. package/app/components/DirView.tsx +64 -2
  10. package/app/components/FileTree.tsx +19 -0
  11. package/app/components/GuideCard.tsx +7 -17
  12. package/app/components/MarkdownView.tsx +2 -0
  13. package/app/components/SearchModal.tsx +234 -80
  14. package/app/components/agents/AgentDetailContent.tsx +3 -5
  15. package/app/components/agents/AgentsContentPage.tsx +21 -6
  16. package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
  17. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  18. package/app/components/agents/agents-content-model.ts +2 -2
  19. package/app/components/help/HelpContent.tsx +9 -9
  20. package/app/components/panels/AgentsPanel.tsx +1 -0
  21. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  22. package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
  23. package/app/components/panels/EchoPanel.tsx +5 -1
  24. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  25. package/app/components/settings/KnowledgeTab.tsx +3 -6
  26. package/app/components/settings/McpSkillsSection.tsx +4 -5
  27. package/app/components/settings/McpTab.tsx +6 -8
  28. package/app/components/setup/StepSecurity.tsx +4 -5
  29. package/app/components/setup/index.tsx +5 -11
  30. package/app/components/ui/Toaster.tsx +39 -0
  31. package/app/hooks/useA2aRegistry.ts +6 -1
  32. package/app/hooks/useAcpDetection.ts +65 -0
  33. package/app/hooks/useAcpRegistry.ts +51 -0
  34. package/app/hooks/useDelegationHistory.ts +49 -0
  35. package/app/lib/a2a/client.ts +49 -5
  36. package/app/lib/a2a/orchestrator.ts +0 -1
  37. package/app/lib/a2a/task-handler.ts +4 -4
  38. package/app/lib/a2a/types.ts +15 -0
  39. package/app/lib/acp/acp-tools.ts +93 -0
  40. package/app/lib/acp/bridge.ts +138 -0
  41. package/app/lib/acp/index.ts +24 -0
  42. package/app/lib/acp/registry.ts +135 -0
  43. package/app/lib/acp/session.ts +264 -0
  44. package/app/lib/acp/subprocess.ts +209 -0
  45. package/app/lib/acp/types.ts +136 -0
  46. package/app/lib/agent/tools.ts +2 -1
  47. package/app/lib/i18n/_core.ts +22 -0
  48. package/app/lib/i18n/index.ts +35 -0
  49. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  50. package/app/lib/i18n/modules/common.ts +71 -0
  51. package/app/lib/i18n/modules/features.ts +153 -0
  52. package/app/lib/i18n/modules/knowledge.ts +425 -0
  53. package/app/lib/i18n/modules/navigation.ts +151 -0
  54. package/app/lib/i18n/modules/onboarding.ts +523 -0
  55. package/app/lib/i18n/modules/panels.ts +1052 -0
  56. package/app/lib/i18n/modules/settings.ts +585 -0
  57. package/app/lib/i18n-en.ts +2 -1518
  58. package/app/lib/i18n-zh.ts +2 -1542
  59. package/app/lib/i18n.ts +3 -6
  60. package/app/lib/toast.ts +79 -0
  61. package/bin/cli.js +25 -25
  62. package/bin/commands/file.js +29 -2
  63. package/bin/commands/space.js +249 -91
  64. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ import { NextResponse } from 'next/server';
4
+ import { getDiscoveredAgents } from '@/lib/a2a/client';
5
+
6
+ export async function GET() {
7
+ const agents = getDiscoveredAgents();
8
+ return NextResponse.json({ agents });
9
+ }
@@ -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
+ }
@@ -1,3 +1,5 @@
1
+ export const dynamic = 'force-dynamic';
2
+
1
3
  import { NextResponse } from 'next/server';
2
4
  import { discoverAgent } from '@/lib/a2a/client';
3
5
 
@@ -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 (!task) {
79
- return respond(jsonRpcError(rpc.id, {
80
- ...A2A_ERRORS.TASK_NOT_FOUND,
81
- message: 'Task not found or not cancelable',
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
+ }
@@ -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
- 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">
@@ -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]);