@geminilight/mindos 0.6.25 → 0.7.0
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 +19 -3
- package/README_zh.md +19 -3
- package/app/app/api/a2a/discover/route.ts +23 -0
- package/app/components/CreateSpaceModal.tsx +1 -0
- package/app/components/ImportModal.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -0
- package/app/components/RightAskPanel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +11 -2
- package/app/components/agents/DiscoverAgentModal.tsx +149 -0
- package/app/components/ask/AskContent.tsx +21 -9
- package/app/components/ask/SessionTabBar.tsx +70 -0
- package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
- package/app/components/panels/AgentsPanel.tsx +25 -2
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
- package/app/components/settings/AiTab.tsx +1 -0
- package/app/components/settings/KnowledgeTab.tsx +2 -0
- package/app/components/settings/SyncTab.tsx +2 -0
- package/app/components/setup/StepDots.tsx +5 -1
- package/app/hooks/useA2aRegistry.ts +53 -0
- package/app/hooks/useAskSession.ts +44 -25
- package/app/lib/a2a/a2a-tools.ts +212 -0
- package/app/lib/a2a/client.ts +207 -0
- package/app/lib/a2a/index.ts +8 -0
- package/app/lib/a2a/orchestrator.ts +255 -0
- package/app/lib/a2a/types.ts +54 -0
- package/app/lib/agent/tools.ts +6 -4
- package/app/lib/i18n-en.ts +52 -0
- package/app/lib/i18n-zh.ts +52 -0
- package/app/next-env.d.ts +1 -1
- package/bin/cli.js +180 -171
- package/bin/commands/agent.js +110 -18
- package/bin/commands/api.js +5 -3
- package/bin/commands/ask.js +3 -3
- package/bin/commands/file.js +13 -13
- package/bin/commands/search.js +2 -2
- package/bin/commands/space.js +64 -10
- package/bin/lib/command.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ MindOS/
|
|
|
240
240
|
├── mcp/ # MCP Server — HTTP adapter that maps tools to App API
|
|
241
241
|
├── skills/ # MindOS Skills (`mindos`, `mindos-zh`) — Workflow guides for Agents
|
|
242
242
|
├── templates/ # Preset templates (`en/`, `zh/`, `empty/`) — copied to knowledge base on onboard
|
|
243
|
-
├── bin/ # CLI
|
|
243
|
+
├── bin/ # CLI (`mindos start`, `mindos file`, `mindos ask`, `mindos agent`, ... 22 commands)
|
|
244
244
|
├── scripts/ # Setup wizard and helper scripts
|
|
245
245
|
└── README.md
|
|
246
246
|
|
|
@@ -256,14 +256,30 @@ MindOS/
|
|
|
256
256
|
|
|
257
257
|
| Command | Description |
|
|
258
258
|
| :--- | :--- |
|
|
259
|
+
| **Core** | |
|
|
259
260
|
| `mindos onboard` | Interactive setup (config, template, start mode) |
|
|
260
261
|
| `mindos start` | Start app + MCP server (foreground) |
|
|
261
262
|
| `mindos start --daemon` | Start as background OS service |
|
|
263
|
+
| `mindos stop` / `restart` | Stop or restart running processes |
|
|
264
|
+
| `mindos dev` | Start in dev mode |
|
|
265
|
+
| `mindos build` | Build for production |
|
|
266
|
+
| `mindos status` | Show service status overview |
|
|
262
267
|
| `mindos open` | Open Web UI in browser |
|
|
268
|
+
| **Knowledge** | |
|
|
269
|
+
| `mindos file <sub>` | File operations (list, read, create, delete, search) |
|
|
270
|
+
| `mindos space <sub>` | Space management (list, create, info) |
|
|
271
|
+
| `mindos search "<query>"` | Search knowledge base |
|
|
272
|
+
| `mindos ask "<question>"` | Ask AI using your knowledge base |
|
|
273
|
+
| `mindos agent <sub>` | AI Agent management (list, info) |
|
|
274
|
+
| `mindos api <METHOD> <path>` | Raw API passthrough for developers/agents |
|
|
275
|
+
| **MCP & Config** | |
|
|
263
276
|
| `mindos mcp install` | Auto-install MCP config into your Agent |
|
|
264
|
-
| `mindos
|
|
265
|
-
| `mindos
|
|
277
|
+
| `mindos token` | Show auth token and MCP config |
|
|
278
|
+
| `mindos config <sub>` | View/update config (show, set, validate) |
|
|
279
|
+
| `mindos sync` | Show sync status (init, now, conflicts, on/off) |
|
|
280
|
+
| `mindos gateway <sub>` | Manage background service (install, start, stop) |
|
|
266
281
|
| `mindos doctor` | Health check |
|
|
282
|
+
| `mindos update` | Update to latest version |
|
|
267
283
|
|
|
268
284
|
**Keyboard shortcuts:** `⌘K` Search · `⌘/` AI Assistant · `E` Edit · `⌘S` Save · `Esc` Close
|
|
269
285
|
|
package/README_zh.md
CHANGED
|
@@ -240,7 +240,7 @@ MindOS/
|
|
|
240
240
|
├── mcp/ # MCP Server — 将工具映射到 App API 的 HTTP 适配器
|
|
241
241
|
├── skills/ # MindOS Skills(`mindos`、`mindos-zh`)— Agent 工作流指南
|
|
242
242
|
├── templates/ # 预设模板(`en/`、`zh/`、`empty/`)— onboard 时复制到知识库目录
|
|
243
|
-
├── bin/ # CLI
|
|
243
|
+
├── bin/ # CLI(`mindos start`、`mindos file`、`mindos ask`、`mindos agent` 等 22 个命令)
|
|
244
244
|
├── scripts/ # 配置向导与辅助脚本
|
|
245
245
|
└── README.md
|
|
246
246
|
|
|
@@ -256,14 +256,30 @@ MindOS/
|
|
|
256
256
|
|
|
257
257
|
| 命令 | 说明 |
|
|
258
258
|
| :--- | :--- |
|
|
259
|
+
| **核心** | |
|
|
259
260
|
| `mindos onboard` | 交互式初始化(生成配置、选择模板) |
|
|
260
261
|
| `mindos start` | 前台启动 app + MCP 服务 |
|
|
261
262
|
| `mindos start --daemon` | 以后台 OS 服务方式启动 |
|
|
263
|
+
| `mindos stop` / `restart` | 停止或重启服务 |
|
|
264
|
+
| `mindos dev` | 开发模式启动 |
|
|
265
|
+
| `mindos build` | 构建生产版本 |
|
|
266
|
+
| `mindos status` | 查看服务状态概览 |
|
|
262
267
|
| `mindos open` | 在浏览器中打开 Web UI |
|
|
268
|
+
| **知识库** | |
|
|
269
|
+
| `mindos file <sub>` | 文件操作(list, read, create, delete, search) |
|
|
270
|
+
| `mindos space <sub>` | 空间管理(list, create, info) |
|
|
271
|
+
| `mindos search "<query>"` | 搜索知识库 |
|
|
272
|
+
| `mindos ask "<question>"` | 基于知识库向 AI 提问 |
|
|
273
|
+
| `mindos agent <sub>` | AI Agent 管理(list, info) |
|
|
274
|
+
| `mindos api <METHOD> <path>` | API 透传(开发者/Agent 用) |
|
|
275
|
+
| **MCP 与配置** | |
|
|
263
276
|
| `mindos mcp install` | 自动将 MCP 配置写入 Agent |
|
|
264
|
-
| `mindos
|
|
265
|
-
| `mindos
|
|
277
|
+
| `mindos token` | 查看认证令牌和 MCP 配置 |
|
|
278
|
+
| `mindos config <sub>` | 查看/修改配置(show, set, validate) |
|
|
279
|
+
| `mindos sync` | 同步状态(init, now, conflicts, on/off) |
|
|
280
|
+
| `mindos gateway <sub>` | 管理后台服务(install, start, stop) |
|
|
266
281
|
| `mindos doctor` | 健康检查 |
|
|
282
|
+
| `mindos update` | 更新到最新版本 |
|
|
267
283
|
|
|
268
284
|
**快捷键:** `⌘K` 搜索 · `⌘/` AI 助手 · `E` 编辑 · `⌘S` 保存 · `Esc` 关闭
|
|
269
285
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { discoverAgent } from '@/lib/a2a/client';
|
|
3
|
+
|
|
4
|
+
export async function POST(req: Request) {
|
|
5
|
+
try {
|
|
6
|
+
const { url } = await req.json();
|
|
7
|
+
if (!url || typeof url !== 'string') {
|
|
8
|
+
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const agent = await discoverAgent(url);
|
|
12
|
+
if (!agent) {
|
|
13
|
+
return NextResponse.json({ error: 'No A2A agent found', agent: null });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return NextResponse.json({ agent });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: (err as Error).message, agent: null },
|
|
20
|
+
{ status: 500 },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -224,6 +224,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
|
|
|
224
224
|
role="switch"
|
|
225
225
|
aria-checked={useAi}
|
|
226
226
|
disabled={!aiAvailable}
|
|
227
|
+
title={!aiAvailable ? t.hints.configureAiKey : undefined}
|
|
227
228
|
onClick={() => setUseAi(v => !v)}
|
|
228
229
|
className={`relative mt-0.5 inline-flex shrink-0 h-4 w-7 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
229
230
|
useAi ? 'bg-[var(--amber)]' : 'bg-muted'
|
|
@@ -387,6 +387,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
387
387
|
onClick={() => handleIntentSelect('archive')}
|
|
388
388
|
className="flex flex-col items-center gap-2 p-4 border rounded-lg cursor-pointer transition-all duration-150 border-[var(--amber)]/30 bg-card hover:border-[var(--amber)]/60 hover:shadow-sm active:scale-[0.98] text-left"
|
|
389
389
|
disabled={im.validFiles.length === 0}
|
|
390
|
+
title={im.validFiles.length === 0 ? t.hints.noValidFiles : undefined}
|
|
390
391
|
>
|
|
391
392
|
<FolderInput size={24} className="text-[var(--amber)]" />
|
|
392
393
|
<span className="text-sm font-medium text-foreground">{t.fileImport.archiveTitle}</span>
|
|
@@ -396,6 +397,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
396
397
|
onClick={() => handleIntentSelect('digest')}
|
|
397
398
|
className="flex flex-col items-center gap-2 p-4 border border-border rounded-lg cursor-pointer transition-all duration-150 bg-card hover:border-[var(--amber)]/50 hover:shadow-sm active:scale-[0.98] text-left"
|
|
398
399
|
disabled={im.validFiles.length === 0 || aiOrganize.phase === 'organizing'}
|
|
400
|
+
title={im.validFiles.length === 0 ? t.hints.noValidFiles : aiOrganize.phase === 'organizing' ? t.hints.aiOrganizing : undefined}
|
|
399
401
|
>
|
|
400
402
|
<Sparkles size={24} className="text-[var(--amber)]" />
|
|
401
403
|
<span className="text-sm font-medium text-foreground">{t.fileImport.digestTitle}</span>
|
|
@@ -482,6 +484,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
482
484
|
<button
|
|
483
485
|
onClick={handleArchiveSubmit}
|
|
484
486
|
disabled={isImporting || im.validFiles.length === 0}
|
|
487
|
+
title={isImporting ? t.hints.importInProgress : im.validFiles.length === 0 ? t.hints.noValidFiles : undefined}
|
|
485
488
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
|
486
489
|
showSuccess
|
|
487
490
|
? 'bg-success text-success-foreground'
|
|
@@ -99,6 +99,7 @@ export default function OnboardingView() {
|
|
|
99
99
|
<button
|
|
100
100
|
key={tpl.id}
|
|
101
101
|
disabled={isDisabled}
|
|
102
|
+
title={isDisabled ? t.hints.templateInitializing : undefined}
|
|
102
103
|
onClick={() => handleSelect(tpl.id)}
|
|
103
104
|
className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border border-border bg-card text-left transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber)]/5 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
104
105
|
>
|
|
@@ -23,12 +23,14 @@ interface RightAskPanelProps {
|
|
|
23
23
|
onModeSwitch?: () => void;
|
|
24
24
|
maximized?: boolean;
|
|
25
25
|
onMaximize?: () => void;
|
|
26
|
+
/** Left offset (px) to avoid covering Rail + Sidebar when maximized */
|
|
27
|
+
sidebarOffset?: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export default function RightAskPanel({
|
|
29
31
|
open, onClose, currentFile, initialMessage, onFirstMessage,
|
|
30
32
|
width, onWidthChange, onWidthCommit, askMode, onModeSwitch,
|
|
31
|
-
maximized = false, onMaximize,
|
|
33
|
+
maximized = false, onMaximize, sidebarOffset = 0,
|
|
32
34
|
}: RightAskPanelProps) {
|
|
33
35
|
const handleMouseDown = useResizeDrag({
|
|
34
36
|
width,
|
|
@@ -49,7 +51,7 @@ export default function RightAskPanel({
|
|
|
49
51
|
${open ? 'translate-x-0' : 'translate-x-full pointer-events-none'}
|
|
50
52
|
${maximized ? 'border-l-0' : ''}
|
|
51
53
|
`}
|
|
52
|
-
style={{
|
|
54
|
+
style={maximized ? { left: `${sidebarOffset}px` } : { width: `${width}px` }}
|
|
53
55
|
role="complementary"
|
|
54
56
|
aria-label="MindOS Agent panel"
|
|
55
57
|
>
|
|
@@ -150,6 +150,14 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
150
150
|
const currentFile = pathname.startsWith('/view/')
|
|
151
151
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
152
152
|
: undefined;
|
|
153
|
+
|
|
154
|
+
// Auto-exit Ask panel maximize when navigating to a different page
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (ap.askMaximized) ap.toggleAskMaximized();
|
|
157
|
+
// Only react to pathname changes, not askMaximized changes
|
|
158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
|
+
}, [pathname]);
|
|
160
|
+
|
|
153
161
|
const agentsContentActive = pathname?.startsWith('/agents');
|
|
154
162
|
const railActivePanel = lp.activePanel ?? (agentsContentActive ? 'agents' : null);
|
|
155
163
|
|
|
@@ -415,6 +423,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
415
423
|
onModeSwitch={ap.handleAskModeSwitch}
|
|
416
424
|
maximized={ap.askMaximized}
|
|
417
425
|
onMaximize={ap.toggleAskMaximized}
|
|
426
|
+
sidebarOffset={lp.panelOpen ? lp.railWidth + lp.effectivePanelWidth : lp.railWidth}
|
|
418
427
|
/>
|
|
419
428
|
|
|
420
429
|
<RightAgentDetailPanel
|
|
@@ -495,7 +504,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
495
504
|
|
|
496
505
|
<main
|
|
497
506
|
id="main-content"
|
|
498
|
-
className={`min-h-screen transition-all duration-200 pt-[52px] md:pt-0
|
|
507
|
+
className={`min-h-screen transition-all duration-200 pt-[52px] md:pt-0`}
|
|
499
508
|
onDragEnter={(e) => {
|
|
500
509
|
if (!e.dataTransfer.types.includes('Files')) return;
|
|
501
510
|
e.preventDefault();
|
|
@@ -562,7 +571,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
562
571
|
<style>{`
|
|
563
572
|
@media (min-width: 768px) {
|
|
564
573
|
:root {
|
|
565
|
-
--right-panel-width: ${ap.askMaximized ?
|
|
574
|
+
--right-panel-width: ${ap.askMaximized ? `calc(100vw - ${lp.panelOpen ? lp.railWidth + lp.effectivePanelWidth : lp.railWidth}px)` : `${ap.askPanelOpen ? ap.askPanelWidth : 0}px`};
|
|
566
575
|
--right-agent-detail-width: ${agentDockOpen ? agentDetailWidth : 0}px;
|
|
567
576
|
}
|
|
568
577
|
#main-content {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { X, Loader2, Globe, AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import type { RemoteAgent } from '@/lib/a2a/types';
|
|
7
|
+
|
|
8
|
+
interface DiscoverAgentModalProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onDiscover: (url: string) => Promise<RemoteAgent | null>;
|
|
12
|
+
discovering: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function DiscoverAgentModal({
|
|
17
|
+
open,
|
|
18
|
+
onClose,
|
|
19
|
+
onDiscover,
|
|
20
|
+
discovering,
|
|
21
|
+
error,
|
|
22
|
+
}: DiscoverAgentModalProps) {
|
|
23
|
+
const { t } = useLocale();
|
|
24
|
+
const p = t.panels.agents;
|
|
25
|
+
const [url, setUrl] = useState('');
|
|
26
|
+
const [result, setResult] = useState<RemoteAgent | null>(null);
|
|
27
|
+
|
|
28
|
+
if (!open) return null;
|
|
29
|
+
|
|
30
|
+
const handleDiscover = async () => {
|
|
31
|
+
if (!url.trim() || discovering) return;
|
|
32
|
+
setResult(null);
|
|
33
|
+
const agent = await onDiscover(url.trim());
|
|
34
|
+
if (agent) setResult(agent);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
38
|
+
if (e.key === 'Enter') handleDiscover();
|
|
39
|
+
if (e.key === 'Escape') onClose();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleClose = () => {
|
|
43
|
+
setUrl('');
|
|
44
|
+
setResult(null);
|
|
45
|
+
onClose();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={handleClose}>
|
|
50
|
+
<div
|
|
51
|
+
className="bg-popover border border-border rounded-xl shadow-lg w-full max-w-md mx-4 p-5"
|
|
52
|
+
onClick={e => e.stopPropagation()}
|
|
53
|
+
role="dialog"
|
|
54
|
+
aria-modal="true"
|
|
55
|
+
aria-label={p.a2aDiscover}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center justify-between mb-4">
|
|
58
|
+
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
|
59
|
+
<Globe size={15} className="text-muted-foreground" />
|
|
60
|
+
{p.a2aDiscover}
|
|
61
|
+
</h3>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={handleClose}
|
|
65
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
66
|
+
aria-label="Close"
|
|
67
|
+
>
|
|
68
|
+
<X size={14} />
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<p className="text-2xs text-muted-foreground mb-3">{p.a2aDiscoverHint}</p>
|
|
73
|
+
|
|
74
|
+
<div className="flex gap-2 mb-4">
|
|
75
|
+
<input
|
|
76
|
+
type="url"
|
|
77
|
+
value={url}
|
|
78
|
+
onChange={e => setUrl(e.target.value)}
|
|
79
|
+
onKeyDown={handleKeyDown}
|
|
80
|
+
placeholder={p.a2aDiscoverPlaceholder}
|
|
81
|
+
disabled={discovering}
|
|
82
|
+
className="flex-1 px-3 py-2 text-xs rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
|
83
|
+
autoFocus
|
|
84
|
+
/>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={handleDiscover}
|
|
88
|
+
disabled={discovering || !url.trim()}
|
|
89
|
+
className="px-3 py-2 text-xs font-medium rounded-lg bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring flex items-center gap-1.5 shrink-0"
|
|
90
|
+
>
|
|
91
|
+
{discovering && <Loader2 size={12} className="animate-spin" />}
|
|
92
|
+
{discovering ? p.a2aDiscovering : p.a2aDiscover}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{error && !result && (
|
|
97
|
+
<div className="rounded-lg border border-error/30 bg-error/5 px-3 py-2.5 mb-3">
|
|
98
|
+
<div className="flex items-start gap-2">
|
|
99
|
+
<AlertCircle size={14} className="text-error mt-0.5 shrink-0" />
|
|
100
|
+
<div>
|
|
101
|
+
<p className="text-xs font-medium text-error">{p.a2aDiscoverFailed}</p>
|
|
102
|
+
<p className="text-2xs text-muted-foreground mt-0.5">{p.a2aDiscoverFailedHint}</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{result && (
|
|
109
|
+
<div className="rounded-lg border border-success/30 bg-success/5 px-3 py-2.5">
|
|
110
|
+
<div className="flex items-start gap-2">
|
|
111
|
+
<CheckCircle2 size={14} className="text-success mt-0.5 shrink-0" />
|
|
112
|
+
<div className="min-w-0">
|
|
113
|
+
<p className="text-xs font-medium text-success mb-1.5">{p.a2aDiscoverSuccess}</p>
|
|
114
|
+
<div className="space-y-1">
|
|
115
|
+
<p className="text-xs font-medium text-foreground truncate" title={result.card.name}>
|
|
116
|
+
{result.card.name}
|
|
117
|
+
<span className="text-2xs text-muted-foreground ml-1.5">v{result.card.version}</span>
|
|
118
|
+
</p>
|
|
119
|
+
<p className="text-2xs text-muted-foreground truncate" title={result.card.description}>
|
|
120
|
+
{result.card.description}
|
|
121
|
+
</p>
|
|
122
|
+
{result.card.skills.length > 0 && (
|
|
123
|
+
<div className="mt-1.5">
|
|
124
|
+
<p className="text-2xs text-muted-foreground mb-1">{p.a2aSkills}:</p>
|
|
125
|
+
<div className="flex flex-wrap gap-1">
|
|
126
|
+
{result.card.skills.map(s => (
|
|
127
|
+
<span
|
|
128
|
+
key={s.id}
|
|
129
|
+
className="text-2xs px-1.5 py-0.5 rounded bg-muted/80 text-muted-foreground border border-border/50"
|
|
130
|
+
title={s.description}
|
|
131
|
+
>
|
|
132
|
+
{s.name}
|
|
133
|
+
</span>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
<p className="text-2xs text-muted-foreground mt-1 truncate" title={result.endpoint}>
|
|
139
|
+
{p.a2aEndpoint}: {result.endpoint}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
-
import { Sparkles, Send, Paperclip, StopCircle,
|
|
4
|
+
import { Sparkles, Send, Paperclip, StopCircle, SquarePen, History, X, Zap, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import type { Message } from '@/lib/types';
|
|
7
7
|
import { useAskSession } from '@/hooks/useAskSession';
|
|
@@ -13,6 +13,7 @@ import MessageList from '@/components/ask/MessageList';
|
|
|
13
13
|
import MentionPopover from '@/components/ask/MentionPopover';
|
|
14
14
|
import SlashCommandPopover from '@/components/ask/SlashCommandPopover';
|
|
15
15
|
import SessionHistory from '@/components/ask/SessionHistory';
|
|
16
|
+
import SessionTabBar from '@/components/ask/SessionTabBar';
|
|
16
17
|
import FileChip from '@/components/ask/FileChip';
|
|
17
18
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
18
19
|
import { isRetryableError, retryDelay, sleep } from '@/lib/agent/reconnect';
|
|
@@ -601,30 +602,41 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
601
602
|
</span>
|
|
602
603
|
</div>
|
|
603
604
|
<div className="flex items-center gap-1">
|
|
604
|
-
<button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1.5 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title=
|
|
605
|
+
<button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1.5 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title={t.hints.sessionHistory}>
|
|
605
606
|
<History size={iconSize} />
|
|
606
607
|
</button>
|
|
607
|
-
<button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title=
|
|
608
|
-
<
|
|
608
|
+
<button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title={t.hints.newSession}>
|
|
609
|
+
<SquarePen size={iconSize} />
|
|
609
610
|
</button>
|
|
610
611
|
{isPanel && onMaximize && (
|
|
611
|
-
<button type="button" onClick={onMaximize} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ?
|
|
612
|
+
<button type="button" onClick={onMaximize} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? t.hints.restorePanel : t.hints.maximizePanel}>
|
|
612
613
|
{maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
|
|
613
614
|
</button>
|
|
614
615
|
)}
|
|
615
616
|
{onModeSwitch && (
|
|
616
|
-
<button type="button" onClick={onModeSwitch} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ?
|
|
617
|
+
<button type="button" onClick={onModeSwitch} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? t.hints.dockToSide : t.hints.openAsPopup}>
|
|
617
618
|
{askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
|
|
618
619
|
</button>
|
|
619
620
|
)}
|
|
620
621
|
{onClose && (
|
|
621
|
-
<button type="button" onClick={onClose} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" aria-label="Close">
|
|
622
|
+
<button type="button" onClick={onClose} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.hints.closePanel} aria-label="Close">
|
|
622
623
|
<X size={isPanel ? iconSize : 15} />
|
|
623
624
|
</button>
|
|
624
625
|
)}
|
|
625
626
|
</div>
|
|
626
627
|
</div>
|
|
627
628
|
|
|
629
|
+
{/* Session tabs — panel variant only */}
|
|
630
|
+
{isPanel && session.sessions.length > 0 && (
|
|
631
|
+
<SessionTabBar
|
|
632
|
+
sessions={session.sessions}
|
|
633
|
+
activeSessionId={session.activeSessionId}
|
|
634
|
+
onLoad={handleLoadSession}
|
|
635
|
+
onDelete={session.deleteSession}
|
|
636
|
+
onNew={handleResetSession}
|
|
637
|
+
/>
|
|
638
|
+
)}
|
|
639
|
+
|
|
628
640
|
{showHistory && (
|
|
629
641
|
<SessionHistory
|
|
630
642
|
sessions={session.sessions}
|
|
@@ -770,7 +782,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
770
782
|
isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-end gap-2 px-3 py-3',
|
|
771
783
|
)}
|
|
772
784
|
>
|
|
773
|
-
<button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title=
|
|
785
|
+
<button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title={t.hints.attachFile}>
|
|
774
786
|
<Paperclip size={inputIconSize} />
|
|
775
787
|
</button>
|
|
776
788
|
|
|
@@ -807,7 +819,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
807
819
|
{loadingPhase === 'reconnecting' ? <X size={inputIconSize} /> : <StopCircle size={inputIconSize} />}
|
|
808
820
|
</button>
|
|
809
821
|
) : (
|
|
810
|
-
<button type="submit" disabled={!input.trim() || mention.mentionQuery !== null || slash.slashQuery !== null} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
822
|
+
<button type="submit" disabled={!input.trim() || mention.mentionQuery !== null || slash.slashQuery !== null} title={!input.trim() ? t.hints.typeMessage : mention.mentionQuery !== null || slash.slashQuery !== null ? t.hints.mentionInProgress : undefined} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
811
823
|
<Send size={isPanel ? 13 : 14} />
|
|
812
824
|
</button>
|
|
813
825
|
)}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Plus, X } from 'lucide-react';
|
|
4
|
+
import type { ChatSession } from '@/lib/types';
|
|
5
|
+
import { sessionTitle } from '@/hooks/useAskSession';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
|
|
8
|
+
interface SessionTabBarProps {
|
|
9
|
+
sessions: ChatSession[];
|
|
10
|
+
activeSessionId: string | null;
|
|
11
|
+
onLoad: (id: string) => void;
|
|
12
|
+
onDelete: (id: string) => void;
|
|
13
|
+
onNew: () => void;
|
|
14
|
+
maxTabs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function SessionTabBar({
|
|
18
|
+
sessions, activeSessionId, onLoad, onDelete, onNew, maxTabs = 3,
|
|
19
|
+
}: SessionTabBarProps) {
|
|
20
|
+
const { t } = useLocale();
|
|
21
|
+
const visibleSessions = sessions.slice(0, maxTabs);
|
|
22
|
+
|
|
23
|
+
if (visibleSessions.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center border-b border-border shrink-0 bg-background/50">
|
|
27
|
+
<div className="flex flex-1 min-w-0">
|
|
28
|
+
{visibleSessions.map((s) => {
|
|
29
|
+
const isActive = s.id === activeSessionId;
|
|
30
|
+
const title = sessionTitle(s);
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
key={s.id}
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={() => onLoad(s.id)}
|
|
36
|
+
className={`group relative flex items-center gap-1 min-w-0 max-w-[160px] px-3 py-2 text-xs transition-colors
|
|
37
|
+
${isActive
|
|
38
|
+
? 'text-foreground border-b-2 border-[var(--amber)] bg-card'
|
|
39
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50 border-b-2 border-transparent'
|
|
40
|
+
}`}
|
|
41
|
+
title={title}
|
|
42
|
+
>
|
|
43
|
+
<span className="truncate">{title === '(empty session)' ? t.hints.newChat : title}</span>
|
|
44
|
+
{visibleSessions.length > 1 && (
|
|
45
|
+
<span
|
|
46
|
+
role="button"
|
|
47
|
+
tabIndex={0}
|
|
48
|
+
onClick={(e) => { e.stopPropagation(); onDelete(s.id); }}
|
|
49
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); onDelete(s.id); } }}
|
|
50
|
+
className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-muted hover:text-error transition-opacity"
|
|
51
|
+
title={t.hints.closeSession}
|
|
52
|
+
>
|
|
53
|
+
<X size={10} />
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={onNew}
|
|
63
|
+
className="shrink-0 p-2 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
|
64
|
+
title={t.hints.newChat}
|
|
65
|
+
>
|
|
66
|
+
<Plus size={13} />
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm';
|
|
|
7
7
|
import { cn } from '@/lib/utils';
|
|
8
8
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
9
9
|
import { useSettingsAiAvailable } from '@/hooks/useSettingsAiAvailable';
|
|
10
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
10
11
|
|
|
11
12
|
const proseInsight =
|
|
12
13
|
'prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground ' +
|
|
@@ -50,6 +51,7 @@ export function EchoInsightCollapsible({
|
|
|
50
51
|
const btnId = `${panelId}-btn`;
|
|
51
52
|
const abortRef = useRef<AbortController | null>(null);
|
|
52
53
|
const { ready: aiReady, loading: aiLoading } = useSettingsAiAvailable();
|
|
54
|
+
const { t } = useLocale();
|
|
53
55
|
|
|
54
56
|
useEffect(() => () => abortRef.current?.abort(), []);
|
|
55
57
|
|
|
@@ -145,6 +147,7 @@ export function EchoInsightCollapsible({
|
|
|
145
147
|
<button
|
|
146
148
|
type="button"
|
|
147
149
|
disabled={generateDisabled}
|
|
150
|
+
title={generateDisabled ? t.hints.aiNotConfigured : undefined}
|
|
148
151
|
onClick={runGenerate}
|
|
149
152
|
className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-[var(--amber-foreground)] transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
150
153
|
>
|
|
@@ -160,6 +163,7 @@ export function EchoInsightCollapsible({
|
|
|
160
163
|
type="button"
|
|
161
164
|
onClick={runGenerate}
|
|
162
165
|
disabled={streaming || !aiReady}
|
|
166
|
+
title={streaming || !aiReady ? t.hints.generationInProgress : undefined}
|
|
163
167
|
className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
164
168
|
>
|
|
165
169
|
{retryLabel}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import { Loader2, RefreshCw, Settings } from 'lucide-react';
|
|
5
|
+
import { Globe, Loader2, RefreshCw, Settings } from 'lucide-react';
|
|
6
6
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
7
|
+
import { useA2aRegistry } from '@/hooks/useA2aRegistry';
|
|
7
8
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
9
|
import PanelHeader from './PanelHeader';
|
|
9
10
|
import { AgentsPanelHubNav } from './AgentsPanelHubNav';
|
|
10
11
|
import { AgentsPanelAgentGroups } from './AgentsPanelAgentGroups';
|
|
12
|
+
import DiscoverAgentModal from '../agents/DiscoverAgentModal';
|
|
11
13
|
|
|
12
14
|
interface AgentsPanelProps {
|
|
13
15
|
active: boolean;
|
|
@@ -28,6 +30,8 @@ export default function AgentsPanel({
|
|
|
28
30
|
const pathname = usePathname();
|
|
29
31
|
const [refreshing, setRefreshing] = useState(false);
|
|
30
32
|
const [showNotDetected, setShowNotDetected] = useState(false);
|
|
33
|
+
const [showDiscoverModal, setShowDiscoverModal] = useState(false);
|
|
34
|
+
const a2a = useA2aRegistry();
|
|
31
35
|
|
|
32
36
|
const handleRefresh = async () => {
|
|
33
37
|
setRefreshing(true);
|
|
@@ -82,6 +86,9 @@ export default function AgentsPanel({
|
|
|
82
86
|
{!mcp.loading && (
|
|
83
87
|
<span className="text-2xs text-muted-foreground">
|
|
84
88
|
{connected.length} {p.connected}
|
|
89
|
+
{a2a.agents.length > 0 && (
|
|
90
|
+
<> · {p.a2aLabel} {a2a.agents.length}</>
|
|
91
|
+
)}
|
|
85
92
|
</span>
|
|
86
93
|
)}
|
|
87
94
|
<button
|
|
@@ -146,7 +153,15 @@ export default function AgentsPanel({
|
|
|
146
153
|
)}
|
|
147
154
|
</div>
|
|
148
155
|
|
|
149
|
-
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
156
|
+
<div className="px-3 py-2 border-t border-border shrink-0 space-y-1">
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={() => setShowDiscoverModal(true)}
|
|
160
|
+
className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
161
|
+
>
|
|
162
|
+
<Globe size={11} />
|
|
163
|
+
{p.a2aDiscover}
|
|
164
|
+
</button>
|
|
150
165
|
<button
|
|
151
166
|
type="button"
|
|
152
167
|
onClick={openAdvancedConfig}
|
|
@@ -156,6 +171,14 @@ export default function AgentsPanel({
|
|
|
156
171
|
{p.advancedConfig}
|
|
157
172
|
</button>
|
|
158
173
|
</div>
|
|
174
|
+
|
|
175
|
+
<DiscoverAgentModal
|
|
176
|
+
open={showDiscoverModal}
|
|
177
|
+
onClose={() => setShowDiscoverModal(false)}
|
|
178
|
+
onDiscover={a2a.discover}
|
|
179
|
+
discovering={a2a.discovering}
|
|
180
|
+
error={a2a.error}
|
|
181
|
+
/>
|
|
159
182
|
</div>
|
|
160
183
|
);
|
|
161
184
|
}
|