@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
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
4
4
|
import { BookOpen, Rocket, Brain, Keyboard, HelpCircle, Bot, ChevronDown, Copy, Check } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
|
+
import { toast } from '@/lib/toast';
|
|
6
8
|
|
|
7
9
|
/* ── Collapsible Section ── */
|
|
8
|
-
function Section({ icon, title, defaultOpen = false, children }: {
|
|
10
|
+
function Section({ id, icon, title, defaultOpen = false, children }: {
|
|
11
|
+
id?: string;
|
|
9
12
|
icon: React.ReactNode;
|
|
10
13
|
title: string;
|
|
11
14
|
defaultOpen?: boolean;
|
|
@@ -14,7 +17,7 @@ function Section({ icon, title, defaultOpen = false, children }: {
|
|
|
14
17
|
const [open, setOpen] = useState(defaultOpen);
|
|
15
18
|
|
|
16
19
|
return (
|
|
17
|
-
<div className="bg-card border border-border rounded-lg overflow-hidden">
|
|
20
|
+
<div id={id} className="bg-card border border-border rounded-lg overflow-hidden scroll-mt-4">
|
|
18
21
|
<button
|
|
19
22
|
onClick={() => setOpen(v => !v)}
|
|
20
23
|
className="w-full flex items-center gap-3 px-5 py-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
@@ -55,15 +58,13 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
|
|
|
55
58
|
const [copied, setCopied] = useState(false);
|
|
56
59
|
|
|
57
60
|
const handleCopy = useCallback(() => {
|
|
58
|
-
const clean = text.replace(/^["
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
setCopied(true); // Reuse copied state to show error
|
|
66
|
-
setTimeout(() => setCopied(false), 2000);
|
|
61
|
+
const clean = text.replace(/^["“]|["”]$/g, '');
|
|
62
|
+
copyToClipboard(clean).then((ok) => {
|
|
63
|
+
if (ok) {
|
|
64
|
+
setCopied(true);
|
|
65
|
+
setTimeout(() => setCopied(false), 1500);
|
|
66
|
+
toast.copy();
|
|
67
|
+
}
|
|
67
68
|
});
|
|
68
69
|
}, [text]);
|
|
69
70
|
|
|
@@ -144,8 +145,63 @@ export default function HelpContent() {
|
|
|
144
145
|
{ keys: '@', label: h.shortcuts.attachFile },
|
|
145
146
|
], [mod, h.shortcuts]);
|
|
146
147
|
|
|
148
|
+
const tocItems = useMemo(() => [
|
|
149
|
+
{ id: 'what-is', label: h.whatIs.title },
|
|
150
|
+
{ id: 'concepts', label: h.concepts.title },
|
|
151
|
+
{ id: 'quick-start', label: h.quickStart.title },
|
|
152
|
+
{ id: 'agent-usage', label: h.agentUsage.title },
|
|
153
|
+
{ id: 'shortcuts', label: h.shortcutsTitle },
|
|
154
|
+
{ id: 'faq', label: h.faq.title },
|
|
155
|
+
], [h]);
|
|
156
|
+
|
|
157
|
+
const [activeSection, setActiveSection] = useState('');
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const ids = tocItems.map(i => i.id);
|
|
160
|
+
const observer = new IntersectionObserver(
|
|
161
|
+
(entries) => {
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (entry.isIntersecting) {
|
|
164
|
+
setActiveSection(entry.target.id);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{ rootMargin: '-10% 0px -80% 0px', threshold: 0 },
|
|
170
|
+
);
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
for (const id of ids) {
|
|
173
|
+
const el = document.getElementById(id);
|
|
174
|
+
if (el) observer.observe(el);
|
|
175
|
+
}
|
|
176
|
+
}, 100);
|
|
177
|
+
return () => { clearTimeout(timer); observer.disconnect(); };
|
|
178
|
+
}, [tocItems]);
|
|
179
|
+
|
|
147
180
|
return (
|
|
148
|
-
<div className="content-width px-4 md:px-6 py-8 md:py-12">
|
|
181
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-12 relative">
|
|
182
|
+
{/* ── Floating TOC (wide screens only) ── */}
|
|
183
|
+
<nav className="hidden xl:block fixed top-24 w-44" style={{ left: 'calc(50% + 340px)' }} aria-label="Table of contents">
|
|
184
|
+
<ul className="space-y-1 border-l border-border pl-3">
|
|
185
|
+
{tocItems.map(item => (
|
|
186
|
+
<li key={item.id}>
|
|
187
|
+
<a
|
|
188
|
+
href={`#${item.id}`}
|
|
189
|
+
onClick={(e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
document.getElementById(item.id)?.scrollIntoView({ behavior: 'smooth' });
|
|
192
|
+
}}
|
|
193
|
+
className={`block text-xs py-1 transition-colors ${
|
|
194
|
+
activeSection === item.id
|
|
195
|
+
? 'text-[var(--amber)] font-medium border-l-2 border-[var(--amber)] -ml-[13px] pl-[11px]'
|
|
196
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{item.label}
|
|
200
|
+
</a>
|
|
201
|
+
</li>
|
|
202
|
+
))}
|
|
203
|
+
</ul>
|
|
204
|
+
</nav>
|
|
149
205
|
{/* ── Header ── */}
|
|
150
206
|
<div className="mb-8">
|
|
151
207
|
<div className="flex items-center gap-2 mb-1">
|
|
@@ -158,12 +214,12 @@ export default function HelpContent() {
|
|
|
158
214
|
{/* ── Sections ── */}
|
|
159
215
|
<div className="space-y-3">
|
|
160
216
|
{/* 1. What is MindOS */}
|
|
161
|
-
<Section icon={<BookOpen size={18} />} title={h.whatIs.title} defaultOpen>
|
|
217
|
+
<Section id="what-is" icon={<BookOpen size={18} />} title={h.whatIs.title} defaultOpen>
|
|
162
218
|
<p className="text-sm text-muted-foreground leading-relaxed">{h.whatIs.body}</p>
|
|
163
219
|
</Section>
|
|
164
220
|
|
|
165
221
|
{/* 2. Core Concepts */}
|
|
166
|
-
<Section icon={<Brain size={18} />} title={h.concepts.title} defaultOpen>
|
|
222
|
+
<Section id="concepts" icon={<Brain size={18} />} title={h.concepts.title} defaultOpen>
|
|
167
223
|
<div className="space-y-4">
|
|
168
224
|
<div>
|
|
169
225
|
<p className="text-sm font-medium text-foreground">{h.concepts.spaceTitle}</p>
|
|
@@ -181,7 +237,7 @@ export default function HelpContent() {
|
|
|
181
237
|
</Section>
|
|
182
238
|
|
|
183
239
|
{/* 3. Quick Start */}
|
|
184
|
-
<Section icon={<Rocket size={18} />} title={h.quickStart.title} defaultOpen>
|
|
240
|
+
<Section id="quick-start" icon={<Rocket size={18} />} title={h.quickStart.title} defaultOpen>
|
|
185
241
|
<div className="space-y-4">
|
|
186
242
|
<StepCard step={1} title={h.quickStart.step1Title} desc={h.quickStart.step1Desc} />
|
|
187
243
|
<StepCard step={2} title={h.quickStart.step2Title} desc={h.quickStart.step2Desc} />
|
|
@@ -190,7 +246,7 @@ export default function HelpContent() {
|
|
|
190
246
|
</Section>
|
|
191
247
|
|
|
192
248
|
{/* 4. Using MindOS with AI Agents */}
|
|
193
|
-
<Section icon={<Bot size={18} />} title={h.agentUsage.title} defaultOpen>
|
|
249
|
+
<Section id="agent-usage" icon={<Bot size={18} />} title={h.agentUsage.title} defaultOpen>
|
|
194
250
|
<p className="text-sm text-muted-foreground leading-relaxed mb-4">{h.agentUsage.intro}</p>
|
|
195
251
|
|
|
196
252
|
<div className="space-y-3">
|
|
@@ -215,7 +271,7 @@ export default function HelpContent() {
|
|
|
215
271
|
</Section>
|
|
216
272
|
|
|
217
273
|
{/* 5. Keyboard Shortcuts */}
|
|
218
|
-
<Section icon={<Keyboard size={18} />} title={h.shortcutsTitle}>
|
|
274
|
+
<Section id="shortcuts" icon={<Keyboard size={18} />} title={h.shortcutsTitle}>
|
|
219
275
|
<div className="space-y-0">
|
|
220
276
|
{shortcuts.map((s) => (
|
|
221
277
|
<ShortcutRow key={s.keys} keys={s.keys} label={s.label} />
|
|
@@ -224,7 +280,7 @@ export default function HelpContent() {
|
|
|
224
280
|
</Section>
|
|
225
281
|
|
|
226
282
|
{/* 6. FAQ */}
|
|
227
|
-
<Section icon={<HelpCircle size={18} />} title={h.faq.title}>
|
|
283
|
+
<Section id="faq" icon={<HelpCircle size={18} />} title={h.faq.title}>
|
|
228
284
|
<div>
|
|
229
285
|
{h.faq.items.map((item, i) => (
|
|
230
286
|
<FaqItem key={i} q={item.q} a={item.a} />
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react';
|
|
4
|
-
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy,
|
|
4
|
+
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy, Monitor, Globe } from 'lucide-react';
|
|
5
|
+
import { toast } from '@/lib/toast';
|
|
5
6
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
6
7
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
8
|
import type { AgentInfo, McpStatus } from '../settings/types';
|
|
@@ -48,7 +49,6 @@ export default function AgentsPanelAgentDetail({
|
|
|
48
49
|
const [installing, setInstalling] = useState(false);
|
|
49
50
|
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
50
51
|
const [transport, setTransport] = useState<'stdio' | 'http'>(() => agent.preferredTransport);
|
|
51
|
-
const [copied, setCopied] = useState(false);
|
|
52
52
|
|
|
53
53
|
const snippet = useMemo(() => {
|
|
54
54
|
if (agentStatus === 'notFound') return null;
|
|
@@ -70,10 +70,7 @@ export default function AgentsPanelAgentDetail({
|
|
|
70
70
|
const handleCopy = async () => {
|
|
71
71
|
if (!snippet) return;
|
|
72
72
|
const ok = await copyToClipboard(snippet.snippet);
|
|
73
|
-
if (ok)
|
|
74
|
-
setCopied(true);
|
|
75
|
-
setTimeout(() => setCopied(false), 2000);
|
|
76
|
-
}
|
|
73
|
+
if (ok) toast.copy();
|
|
77
74
|
};
|
|
78
75
|
|
|
79
76
|
const dot =
|
|
@@ -183,8 +180,8 @@ export default function AgentsPanelAgentDetail({
|
|
|
183
180
|
onClick={handleCopy}
|
|
184
181
|
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
185
182
|
>
|
|
186
|
-
|
|
187
|
-
{
|
|
183
|
+
<Copy size={14} />
|
|
184
|
+
{copy.copyConfig}
|
|
188
185
|
</button>
|
|
189
186
|
</>
|
|
190
187
|
)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { Check, ChevronRight, Loader2, RotateCw } from 'lucide-react';
|
|
5
|
+
import { Check, ChevronRight, Globe, Loader2, RotateCw } from 'lucide-react';
|
|
6
6
|
import type { AgentInfo } from '../settings/types';
|
|
7
7
|
import { AgentAvatar } from '../agents/AgentsPrimitives';
|
|
8
8
|
|
|
@@ -23,6 +23,8 @@ export default function AgentsPanelAgentListRow({
|
|
|
23
23
|
detailHref,
|
|
24
24
|
onInstallAgent,
|
|
25
25
|
copy,
|
|
26
|
+
isA2aReady = false,
|
|
27
|
+
a2aTooltip,
|
|
26
28
|
}: {
|
|
27
29
|
agent: AgentInfo;
|
|
28
30
|
agentStatus: AgentsPanelAgentListStatus;
|
|
@@ -30,6 +32,8 @@ export default function AgentsPanelAgentListRow({
|
|
|
30
32
|
detailHref: string;
|
|
31
33
|
onInstallAgent: (key: string) => Promise<boolean>;
|
|
32
34
|
copy: AgentsPanelAgentListRowCopy;
|
|
35
|
+
isA2aReady?: boolean;
|
|
36
|
+
a2aTooltip?: string;
|
|
33
37
|
}) {
|
|
34
38
|
return (
|
|
35
39
|
<div
|
|
@@ -51,6 +55,11 @@ export default function AgentsPanelAgentListRow({
|
|
|
51
55
|
{agent.transport}
|
|
52
56
|
</span>
|
|
53
57
|
)}
|
|
58
|
+
{isA2aReady && (
|
|
59
|
+
<span title={a2aTooltip} className="shrink-0 text-muted-foreground/70">
|
|
60
|
+
<Globe size={12} />
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
54
63
|
<span className="flex-1 min-w-[4px]" />
|
|
55
64
|
<ChevronRight
|
|
56
65
|
size={15}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
4
|
+
import { Globe, LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
5
5
|
import { PanelNavRow } from './PanelNavRow';
|
|
6
6
|
|
|
7
7
|
type HubCopy = {
|
|
8
8
|
navOverview: string;
|
|
9
9
|
navMcp: string;
|
|
10
10
|
navSkills: string;
|
|
11
|
+
navNetwork: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function AgentsPanelHubNav({
|
|
@@ -43,6 +44,12 @@ export function AgentsPanelHubNav({
|
|
|
43
44
|
href="/agents?tab=skills"
|
|
44
45
|
active={inAgentsRoute && tab === 'skills'}
|
|
45
46
|
/>
|
|
47
|
+
<PanelNavRow
|
|
48
|
+
icon={<Globe size={14} className={inAgentsRoute && tab === 'a2a' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
|
|
49
|
+
title={copy.navNetwork}
|
|
50
|
+
href="/agents?tab=a2a"
|
|
51
|
+
active={inAgentsRoute && tab === 'a2a'}
|
|
52
|
+
/>
|
|
46
53
|
</div>
|
|
47
54
|
);
|
|
48
55
|
}
|
|
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
|
|
|
5
5
|
import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
|
|
6
6
|
import PanelHeader from './PanelHeader';
|
|
7
7
|
import { PanelNavRow } from './PanelNavRow';
|
|
8
|
+
import EchoSidebarStats from './EchoSidebarStats';
|
|
8
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
9
10
|
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
10
11
|
|
|
@@ -30,7 +31,7 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
30
31
|
return (
|
|
31
32
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
32
33
|
<PanelHeader title={e.title} maximized={maximized} onMaximize={onMaximize} />
|
|
33
|
-
<div className="flex-1 overflow-y-auto min-h-0">
|
|
34
|
+
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
|
34
35
|
<div className="flex flex-col gap-0.5 py-1.5">
|
|
35
36
|
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
36
37
|
const row = rowBySegment[segment];
|
|
@@ -41,6 +42,9 @@ export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelPr
|
|
|
41
42
|
);
|
|
42
43
|
})}
|
|
43
44
|
</div>
|
|
45
|
+
<div className="mt-auto">
|
|
46
|
+
<EchoSidebarStats />
|
|
47
|
+
</div>
|
|
44
48
|
</div>
|
|
45
49
|
</div>
|
|
46
50
|
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { TrendingUp, MessageSquare, AlertCircle } from 'lucide-react';
|
|
5
|
+
import type { ContentChangeEvent } from '@/lib/fs';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
|
|
9
|
+
interface EchoStats {
|
|
10
|
+
fileCount: number;
|
|
11
|
+
unreadChanges: number;
|
|
12
|
+
sessionCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function EchoSidebarStats() {
|
|
16
|
+
const [stats, setStats] = useState<EchoStats | null>(null);
|
|
17
|
+
const [recentEvents, setRecentEvents] = useState<ContentChangeEvent[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const { t } = useLocale();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const loadStats = async () => {
|
|
23
|
+
try {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
const [monitoring, changes, sessions] = await Promise.all([
|
|
26
|
+
apiFetch<any>('/api/monitoring'),
|
|
27
|
+
apiFetch<any>('/api/changes?op=list&limit=3'),
|
|
28
|
+
apiFetch<any>('/api/ask-sessions'),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
setStats({
|
|
32
|
+
fileCount: monitoring?.knowledgeBase?.fileCount ?? 0,
|
|
33
|
+
unreadChanges: changes?.events?.length ?? 0,
|
|
34
|
+
sessionCount: Array.isArray(sessions) ? sessions.length : 0,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
setRecentEvents((changes?.events ?? []).slice(0, 3));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn('[EchoSidebarStats] Failed to load stats:', err);
|
|
40
|
+
setStats({ fileCount: 0, unreadChanges: 0, sessionCount: 0 });
|
|
41
|
+
setRecentEvents([]);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
loadStats();
|
|
48
|
+
const interval = setInterval(loadStats, 10000); // Refresh every 10s
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
if (loading || !stats) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="px-3 py-2 text-xs text-muted-foreground">
|
|
55
|
+
<div className="h-2 w-20 bg-muted rounded animate-pulse" />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-2 px-3 py-3 border-t border-border">
|
|
62
|
+
{/* Quick Stats */}
|
|
63
|
+
<div className="grid grid-cols-3 gap-2">
|
|
64
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
65
|
+
<div className="text-sm font-semibold text-foreground">{stats.fileCount}</div>
|
|
66
|
+
<div className="text-xs text-muted-foreground truncate">Files</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
69
|
+
<div className="text-sm font-semibold text-foreground">{stats.unreadChanges}</div>
|
|
70
|
+
<div className="text-xs text-muted-foreground truncate">Changes</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex flex-col items-center p-1.5 rounded bg-muted/40 hover:bg-muted/60 transition-colors cursor-default">
|
|
73
|
+
<div className="text-sm font-semibold text-foreground">{stats.sessionCount}</div>
|
|
74
|
+
<div className="text-xs text-muted-foreground truncate">Chats</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Recent Activity */}
|
|
79
|
+
{recentEvents.length > 0 && (
|
|
80
|
+
<div className="flex flex-col gap-1">
|
|
81
|
+
<div className="text-xs font-medium text-muted-foreground px-0.5">Recent</div>
|
|
82
|
+
{recentEvents.map((evt) => {
|
|
83
|
+
const relTime = formatRelativeTime(evt.ts);
|
|
84
|
+
const iconType = getIconForOp(evt.op);
|
|
85
|
+
const fileName = evt.path.split('/').pop() || evt.path;
|
|
86
|
+
return (
|
|
87
|
+
<div key={evt.id} className="flex items-start gap-2 p-1.5 rounded hover:bg-muted/40 transition-colors text-xs">
|
|
88
|
+
<span className="text-muted-foreground shrink-0 mt-0.5">{iconType}</span>
|
|
89
|
+
<div className="min-w-0 flex-1">
|
|
90
|
+
<p className="text-foreground truncate font-medium" title={fileName}>{fileName}</p>
|
|
91
|
+
<p className="text-muted-foreground text-2xs">{relTime}</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getIconForOp(op: string): React.ReactNode {
|
|
103
|
+
switch (op) {
|
|
104
|
+
case 'create':
|
|
105
|
+
return <TrendingUp size={12} className="text-success" />;
|
|
106
|
+
case 'write':
|
|
107
|
+
case 'append':
|
|
108
|
+
return <AlertCircle size={12} className="text-foreground" />;
|
|
109
|
+
case 'delete':
|
|
110
|
+
return <AlertCircle size={12} className="text-destructive" />;
|
|
111
|
+
case 'rename':
|
|
112
|
+
case 'move':
|
|
113
|
+
return <MessageSquare size={12} className="text-muted-foreground" />;
|
|
114
|
+
default:
|
|
115
|
+
return <AlertCircle size={12} className="text-muted-foreground" />;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatRelativeTime(isoString: string): string {
|
|
120
|
+
try {
|
|
121
|
+
const date = new Date(isoString);
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const diffMs = now.getTime() - date.getTime();
|
|
124
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
125
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
126
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
127
|
+
|
|
128
|
+
if (diffMins < 1) return 'now';
|
|
129
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
130
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
131
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
132
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
133
|
+
} catch {
|
|
134
|
+
return 'recently';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
|
|
4
4
|
import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive, RotateCcw } from 'lucide-react';
|
|
5
|
+
import { toast } from '@/lib/toast';
|
|
5
6
|
import type { KnowledgeTabProps } from './types';
|
|
6
7
|
import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
7
8
|
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
@@ -96,7 +97,6 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
96
97
|
const [showPassword, setShowPassword] = useState(false);
|
|
97
98
|
const isPasswordMasked = data.webPassword === '***set***';
|
|
98
99
|
|
|
99
|
-
const [copied, setCopied] = useState(false);
|
|
100
100
|
const [resetting, setResetting] = useState(false);
|
|
101
101
|
// revealed holds the plaintext token after regenerate, until user navigates away
|
|
102
102
|
const [revealedToken, setRevealedToken] = useState<string | null>(null);
|
|
@@ -130,10 +130,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
130
130
|
const text = revealedToken ?? data.authToken ?? '';
|
|
131
131
|
if (!text) return;
|
|
132
132
|
copyToClipboard(text).then((ok) => {
|
|
133
|
-
if (ok)
|
|
134
|
-
setCopied(true);
|
|
135
|
-
setTimeout(() => setCopied(false), 2000);
|
|
136
|
-
}
|
|
133
|
+
if (ok) toast.copy();
|
|
137
134
|
});
|
|
138
135
|
}
|
|
139
136
|
|
|
@@ -229,7 +226,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
229
226
|
className="shrink-0 p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
|
|
230
227
|
title={k.authTokenCopy}
|
|
231
228
|
>
|
|
232
|
-
|
|
229
|
+
<Copy size={13} />
|
|
233
230
|
</button>
|
|
234
231
|
)}
|
|
235
232
|
</div>
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Loader2, ChevronDown, ChevronRight,
|
|
6
|
-
Plus, X, Search, Copy,
|
|
6
|
+
Plus, X, Search, Copy,
|
|
7
7
|
} from 'lucide-react';
|
|
8
|
+
import { toast } from '@/lib/toast';
|
|
8
9
|
import { apiFetch } from '@/lib/api';
|
|
9
10
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
10
11
|
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
@@ -398,14 +399,12 @@ function SkillCliHint({ agents, skillName, m }: {
|
|
|
398
399
|
m: Record<string, any> | undefined;
|
|
399
400
|
}) {
|
|
400
401
|
const [selectedAgent, setSelectedAgent] = useState('claude-code');
|
|
401
|
-
const [copied, setCopied] = useState(false);
|
|
402
|
-
|
|
403
402
|
const cmd = `npx skills add GeminiLight/MindOS --skill ${skillName} -a ${selectedAgent} -g -y`;
|
|
404
403
|
const skillPath = `~/.agents/skills/${skillName}/SKILL.md`;
|
|
405
404
|
|
|
406
405
|
const handleCopy = async () => {
|
|
407
406
|
const ok = await copyToClipboard(cmd);
|
|
408
|
-
if (ok)
|
|
407
|
+
if (ok) toast.copy();
|
|
409
408
|
};
|
|
410
409
|
|
|
411
410
|
// Group agents: connected first, then detected, then not found
|
|
@@ -447,7 +446,7 @@ function SkillCliHint({ agents, skillName, m }: {
|
|
|
447
446
|
</code>
|
|
448
447
|
<button onClick={handleCopy}
|
|
449
448
|
className="p-1.5 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
|
|
450
|
-
|
|
449
|
+
<Copy size={11} />
|
|
451
450
|
</button>
|
|
452
451
|
</div>
|
|
453
452
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
|
2
|
-
import { Loader2, Copy,
|
|
2
|
+
import { Loader2, Copy, Monitor, Globe, AlertCircle, RotateCcw, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { toast } from '@/lib/toast';
|
|
3
4
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
4
5
|
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
5
6
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
@@ -22,7 +23,6 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
22
23
|
const [restarting, setRestarting] = useState(false);
|
|
23
24
|
const [selectedAgent, setSelectedAgent] = useState('');
|
|
24
25
|
const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
|
|
25
|
-
const [copied, setCopied] = useState(false);
|
|
26
26
|
const restartPollRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
27
27
|
|
|
28
28
|
// Cleanup restart poll on unmount
|
|
@@ -99,10 +99,9 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
99
99
|
onSelectAgent={(key) => setSelectedAgent(key)}
|
|
100
100
|
transport={transport}
|
|
101
101
|
onTransportChange={setTransport}
|
|
102
|
-
copied={copied}
|
|
103
102
|
onCopy={async (snippet) => {
|
|
104
103
|
const ok = await copyToClipboard(snippet);
|
|
105
|
-
if (ok)
|
|
104
|
+
if (ok) toast.copy();
|
|
106
105
|
}}
|
|
107
106
|
m={m}
|
|
108
107
|
/>
|
|
@@ -177,7 +176,7 @@ function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
|
|
|
177
176
|
|
|
178
177
|
/* ── Agent Config Viewer (dropdown + snippet) ── */
|
|
179
178
|
|
|
180
|
-
function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange,
|
|
179
|
+
function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange, onCopy, m }: {
|
|
181
180
|
connectedAgents: AgentInfo[];
|
|
182
181
|
detectedAgents: AgentInfo[];
|
|
183
182
|
notFoundAgents: AgentInfo[];
|
|
@@ -187,7 +186,6 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
|
|
|
187
186
|
onSelectAgent: (key: string) => void;
|
|
188
187
|
transport: 'stdio' | 'http';
|
|
189
188
|
onTransportChange: (t: 'stdio' | 'http') => void;
|
|
190
|
-
copied: boolean;
|
|
191
189
|
onCopy: (snippet: string) => void;
|
|
192
190
|
m: Record<string, any> | undefined;
|
|
193
191
|
}) {
|
|
@@ -292,8 +290,8 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
|
|
|
292
290
|
<div className="flex items-center gap-3 text-xs">
|
|
293
291
|
<button onClick={() => onCopy(snippet.snippet)}
|
|
294
292
|
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
|
|
295
|
-
|
|
296
|
-
{
|
|
293
|
+
<Copy size={12} />
|
|
294
|
+
{m?.copyConfig ?? 'Copy config'}
|
|
297
295
|
</button>
|
|
298
296
|
<span className="text-muted-foreground">→</span>
|
|
299
297
|
<span className="font-mono text-muted-foreground truncate text-2xs">{snippet.path}</span>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { Copy,
|
|
4
|
+
import { Copy, RefreshCw } from 'lucide-react';
|
|
5
5
|
import { Field, Input } from '@/components/settings/Primitives';
|
|
6
6
|
import type { SetupMessages } from './types';
|
|
7
7
|
|
|
8
8
|
export interface StepSecurityProps {
|
|
9
9
|
authToken: string;
|
|
10
|
-
tokenCopied: boolean;
|
|
11
10
|
onCopy: () => void;
|
|
12
11
|
onGenerate: (seed?: string) => void;
|
|
13
12
|
webPassword: string;
|
|
@@ -16,7 +15,7 @@ export interface StepSecurityProps {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export default function StepSecurity({
|
|
19
|
-
authToken,
|
|
18
|
+
authToken, onCopy, onGenerate, webPassword, onPasswordChange, s,
|
|
20
19
|
}: StepSecurityProps) {
|
|
21
20
|
const [seed, setSeed] = useState('');
|
|
22
21
|
const [showSeed, setShowSeed] = useState(false);
|
|
@@ -29,8 +28,8 @@ export default function StepSecurity({
|
|
|
29
28
|
<button onClick={onCopy}
|
|
30
29
|
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
31
30
|
style={{ color: 'var(--foreground)' }}>
|
|
32
|
-
|
|
33
|
-
{
|
|
31
|
+
<Copy size={14} />
|
|
32
|
+
{s.copyToken}
|
|
34
33
|
</button>
|
|
35
34
|
<button onClick={() => onGenerate()}
|
|
36
35
|
aria-label={s.generateToken}
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
|
+
import { toast } from '@/lib/toast';
|
|
7
8
|
import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
|
|
8
9
|
import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
|
|
9
10
|
import StepKB from './StepKB';
|
|
@@ -127,7 +128,6 @@ export default function SetupWizard() {
|
|
|
127
128
|
webPassword: '',
|
|
128
129
|
});
|
|
129
130
|
const [homeDir, setHomeDir] = useState('~');
|
|
130
|
-
const [tokenCopied, setTokenCopied] = useState(false);
|
|
131
131
|
const [submitting, setSubmitting] = useState(false);
|
|
132
132
|
const [completed, setCompleted] = useState(false);
|
|
133
133
|
const [error, setError] = useState('');
|
|
@@ -230,15 +230,9 @@ export default function SetupWizard() {
|
|
|
230
230
|
}, []);
|
|
231
231
|
|
|
232
232
|
const copyToken = useCallback(() => {
|
|
233
|
-
copyToClipboard(state.authToken)
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
setTimeout(() => setTokenCopied(false), 2000);
|
|
237
|
-
})
|
|
238
|
-
.catch((err) => {
|
|
239
|
-
console.error('[Setup] Token copy failed:', err);
|
|
240
|
-
// Show error toast instead of success
|
|
241
|
-
});
|
|
233
|
+
copyToClipboard(state.authToken).then((ok) => {
|
|
234
|
+
if (ok) toast.copy();
|
|
235
|
+
});
|
|
242
236
|
}, [state.authToken]);
|
|
243
237
|
|
|
244
238
|
const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
|
|
@@ -384,7 +378,7 @@ export default function SetupWizard() {
|
|
|
384
378
|
)}
|
|
385
379
|
{step === 3 && (
|
|
386
380
|
<StepSecurity
|
|
387
|
-
authToken={state.authToken}
|
|
381
|
+
authToken={state.authToken}
|
|
388
382
|
onCopy={copyToken} onGenerate={generateToken}
|
|
389
383
|
webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
|
|
390
384
|
s={s}
|