@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.
Files changed (38) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/api/a2a/discover/route.ts +23 -0
  4. package/app/components/CreateSpaceModal.tsx +1 -0
  5. package/app/components/ImportModal.tsx +3 -0
  6. package/app/components/OnboardingView.tsx +1 -0
  7. package/app/components/RightAskPanel.tsx +4 -2
  8. package/app/components/SidebarLayout.tsx +11 -2
  9. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  10. package/app/components/ask/AskContent.tsx +21 -9
  11. package/app/components/ask/SessionTabBar.tsx +70 -0
  12. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  13. package/app/components/panels/AgentsPanel.tsx +25 -2
  14. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  15. package/app/components/settings/AiTab.tsx +1 -0
  16. package/app/components/settings/KnowledgeTab.tsx +2 -0
  17. package/app/components/settings/SyncTab.tsx +2 -0
  18. package/app/components/setup/StepDots.tsx +5 -1
  19. package/app/hooks/useA2aRegistry.ts +53 -0
  20. package/app/hooks/useAskSession.ts +44 -25
  21. package/app/lib/a2a/a2a-tools.ts +212 -0
  22. package/app/lib/a2a/client.ts +207 -0
  23. package/app/lib/a2a/index.ts +8 -0
  24. package/app/lib/a2a/orchestrator.ts +255 -0
  25. package/app/lib/a2a/types.ts +54 -0
  26. package/app/lib/agent/tools.ts +6 -4
  27. package/app/lib/i18n-en.ts +52 -0
  28. package/app/lib/i18n-zh.ts +52 -0
  29. package/app/next-env.d.ts +1 -1
  30. package/bin/cli.js +180 -171
  31. package/bin/commands/agent.js +110 -18
  32. package/bin/commands/api.js +5 -3
  33. package/bin/commands/ask.js +3 -3
  34. package/bin/commands/file.js +13 -13
  35. package/bin/commands/search.js +2 -2
  36. package/bin/commands/space.js +64 -10
  37. package/bin/lib/command.js +10 -0
  38. 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 entry point (`mindos onboard`, `mindos start`, `mindos open`, `mindos sync`, `mindos token`)
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 sync init` | Setup Git remote sync |
265
- | `mindos update` | Update to latest version |
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 入口(`mindos onboard`、`mindos start`、`mindos open`、`mindos sync`、`mindos token`)
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 sync init` | 配置 Git 远程同步 |
265
- | `mindos update` | 更新到最新版本 |
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={{ width: maximized ? '100vw' : `${width}px` }}
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 ${ap.askMaximized ? 'hidden' : ''}`}
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 ? '100vw' : `${ap.askPanelOpen ? ap.askPanelWidth : 0}px`};
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, RotateCcw, History, X, Zap, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
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="Session history">
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="New session">
608
- <RotateCcw size={iconSize} />
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 ? 'Restore panel' : 'Maximize panel'}>
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' ? 'Dock to side panel' : 'Open as 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="Attach local file">
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
+ <> &middot; {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
  }