@geminilight/mindos 0.6.23 → 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/.well-known/agent-card.json/route.ts +34 -0
- package/app/app/api/a2a/discover/route.ts +23 -0
- package/app/app/api/a2a/route.ts +100 -0
- package/app/components/Backlinks.tsx +2 -2
- package/app/components/Breadcrumb.tsx +1 -1
- package/app/components/CreateSpaceModal.tsx +1 -0
- package/app/components/CsvView.tsx +41 -19
- package/app/components/DirView.tsx +2 -2
- package/app/components/GuideCard.tsx +6 -2
- package/app/components/HomeContent.tsx +1 -1
- package/app/components/ImportModal.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -0
- package/app/components/RightAskPanel.tsx +4 -2
- package/app/components/SearchModal.tsx +3 -3
- package/app/components/SidebarLayout.tsx +11 -2
- package/app/components/SyncStatusBar.tsx +2 -2
- package/app/components/agents/DiscoverAgentModal.tsx +149 -0
- package/app/components/ask/AskContent.tsx +22 -10
- package/app/components/ask/MentionPopover.tsx +2 -2
- package/app/components/ask/SessionTabBar.tsx +70 -0
- package/app/components/ask/SlashCommandPopover.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
- package/app/components/explore/UseCaseCard.tsx +2 -2
- package/app/components/help/HelpContent.tsx +6 -1
- package/app/components/panels/AgentsPanel.tsx +25 -2
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/DiscoverPanel.tsx +3 -3
- package/app/components/panels/PanelNavRow.tsx +2 -2
- package/app/components/panels/PluginsPanel.tsx +1 -1
- package/app/components/panels/SearchPanel.tsx +3 -3
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
- package/app/components/settings/AiTab.tsx +5 -4
- package/app/components/settings/KnowledgeTab.tsx +3 -1
- package/app/components/settings/McpTab.tsx +22 -4
- package/app/components/settings/SyncTab.tsx +2 -0
- package/app/components/settings/UpdateTab.tsx +1 -1
- package/app/components/setup/StepDots.tsx +5 -1
- package/app/components/setup/index.tsx +9 -3
- package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
- 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/agent-card.ts +107 -0
- package/app/lib/a2a/client.ts +207 -0
- package/app/lib/a2a/index.ts +31 -0
- package/app/lib/a2a/orchestrator.ts +255 -0
- package/app/lib/a2a/task-handler.ts +228 -0
- package/app/lib/a2a/types.ts +212 -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 +183 -164
- package/bin/commands/agent.js +110 -0
- package/bin/commands/api.js +60 -0
- package/bin/commands/ask.js +3 -3
- package/bin/commands/file.js +13 -13
- package/bin/commands/search.js +51 -0
- 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,34 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import { buildAgentCard } from '@/lib/a2a/agent-card';
|
|
5
|
+
|
|
6
|
+
const CORS_HEADERS: Record<string, string> = {
|
|
7
|
+
'Access-Control-Allow-Origin': '*',
|
|
8
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
9
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
10
|
+
'Cache-Control': 'public, max-age=300',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function GET(req: NextRequest) {
|
|
14
|
+
// Prefer explicit config; fall back to request headers
|
|
15
|
+
const configuredUrl = process.env.MINDOS_BASE_URL;
|
|
16
|
+
let baseUrl: string;
|
|
17
|
+
if (configuredUrl) {
|
|
18
|
+
baseUrl = configuredUrl.replace(/\/+$/, '');
|
|
19
|
+
} else {
|
|
20
|
+
const proto = req.headers.get('x-forwarded-proto') ?? 'http';
|
|
21
|
+
const host = req.headers.get('host') ?? `localhost:${process.env.PORT || 3456}`;
|
|
22
|
+
baseUrl = `${proto}://${host}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const card = buildAgentCard(baseUrl);
|
|
26
|
+
|
|
27
|
+
const res = NextResponse.json(card);
|
|
28
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) res.headers.set(k, v);
|
|
29
|
+
return res;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function OPTIONS() {
|
|
33
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
|
+
import type { JsonRpcRequest, JsonRpcResponse, SendMessageParams, GetTaskParams, CancelTaskParams } from '@/lib/a2a/types';
|
|
5
|
+
import { A2A_ERRORS } from '@/lib/a2a/types';
|
|
6
|
+
import { handleSendMessage, handleGetTask, handleCancelTask } from '@/lib/a2a/task-handler';
|
|
7
|
+
|
|
8
|
+
const CORS_HEADERS: Record<string, string> = {
|
|
9
|
+
'Access-Control-Allow-Origin': '*',
|
|
10
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
11
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, A2A-Version',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function jsonRpcOk(id: string | number | null, result: unknown): JsonRpcResponse {
|
|
15
|
+
return { jsonrpc: '2.0', id, result };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function jsonRpcError(id: string | number | null, error: { code: number; message: string; data?: unknown }): JsonRpcResponse {
|
|
19
|
+
return { jsonrpc: '2.0', id, error };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function respond(body: JsonRpcResponse, status = 200) {
|
|
23
|
+
const res = NextResponse.json(body, { status });
|
|
24
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) res.headers.set(k, v);
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_REQUEST_BYTES = 100_000; // 100KB max request body
|
|
29
|
+
|
|
30
|
+
export async function POST(req: NextRequest) {
|
|
31
|
+
// Check content length to prevent OOM from oversized payloads
|
|
32
|
+
const contentLength = Number(req.headers.get('content-length') || 0);
|
|
33
|
+
if (contentLength > MAX_REQUEST_BYTES) {
|
|
34
|
+
return respond(jsonRpcError(null, { code: -32600, message: `Request too large (max ${MAX_REQUEST_BYTES} bytes)` }), 413);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse JSON-RPC request
|
|
38
|
+
let rpc: JsonRpcRequest;
|
|
39
|
+
try {
|
|
40
|
+
rpc = await req.json();
|
|
41
|
+
} catch {
|
|
42
|
+
return respond(jsonRpcError(null, A2A_ERRORS.PARSE_ERROR), 400);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (rpc.jsonrpc !== '2.0' || typeof rpc.method !== 'string') {
|
|
46
|
+
return respond(jsonRpcError(rpc.id ?? null, A2A_ERRORS.INVALID_REQUEST), 400);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
switch (rpc.method) {
|
|
51
|
+
case 'SendMessage': {
|
|
52
|
+
const params = rpc.params as unknown as SendMessageParams;
|
|
53
|
+
if (!params?.message?.parts?.length) {
|
|
54
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.INVALID_PARAMS));
|
|
55
|
+
}
|
|
56
|
+
const task = await handleSendMessage(params);
|
|
57
|
+
return respond(jsonRpcOk(rpc.id, task));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'GetTask': {
|
|
61
|
+
const params = rpc.params as unknown as GetTaskParams;
|
|
62
|
+
if (!params?.id) {
|
|
63
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.INVALID_PARAMS));
|
|
64
|
+
}
|
|
65
|
+
const task = handleGetTask(params);
|
|
66
|
+
if (!task) {
|
|
67
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.TASK_NOT_FOUND));
|
|
68
|
+
}
|
|
69
|
+
return respond(jsonRpcOk(rpc.id, task));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'CancelTask': {
|
|
73
|
+
const params = rpc.params as unknown as CancelTaskParams;
|
|
74
|
+
if (!params?.id) {
|
|
75
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.INVALID_PARAMS));
|
|
76
|
+
}
|
|
77
|
+
const task = handleCancelTask(params);
|
|
78
|
+
if (!task) {
|
|
79
|
+
return respond(jsonRpcError(rpc.id, {
|
|
80
|
+
...A2A_ERRORS.TASK_NOT_FOUND,
|
|
81
|
+
message: 'Task not found or not cancelable',
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return respond(jsonRpcOk(rpc.id, task));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
return respond(jsonRpcError(rpc.id, A2A_ERRORS.METHOD_NOT_FOUND));
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return respond(jsonRpcError(rpc.id, {
|
|
92
|
+
...A2A_ERRORS.INTERNAL_ERROR,
|
|
93
|
+
data: (err as Error).message,
|
|
94
|
+
}), 500);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function OPTIONS() {
|
|
99
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
100
|
+
}
|
|
@@ -58,10 +58,10 @@ export default function Backlinks({ filePath }: { filePath: string }) {
|
|
|
58
58
|
<FileText size={14} className="text-muted-foreground group-hover:text-[var(--amber)]" />
|
|
59
59
|
</div>
|
|
60
60
|
<div className="min-w-0 flex-1">
|
|
61
|
-
<div className="font-medium text-sm text-foreground group-hover:text-[var(--amber)] transition-colors truncate mb-1">
|
|
61
|
+
<div className="font-medium text-sm text-foreground group-hover:text-[var(--amber)] transition-colors truncate mb-1" title={link.filePath}>
|
|
62
62
|
{link.filePath}
|
|
63
63
|
</div>
|
|
64
|
-
<div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed italic opacity-80 group-hover:opacity-100 transition-opacity">
|
|
64
|
+
<div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed italic opacity-80 group-hover:opacity-100 transition-opacity" title={link.snippets[0] || ''}>
|
|
65
65
|
{link.snippets[0] || ''}
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
@@ -29,7 +29,7 @@ export default function Breadcrumb({ filePath }: { filePath: string }) {
|
|
|
29
29
|
<span suppressHydrationWarning>{part}</span>
|
|
30
30
|
</span>
|
|
31
31
|
) : (
|
|
32
|
-
<Link href={href} className="px-2 py-0.5 rounded-md hover:bg-muted/50 transition-colors truncate max-w-[200px]">
|
|
32
|
+
<Link href={href} className="px-2 py-0.5 rounded-md hover:bg-muted/50 transition-colors truncate max-w-[200px]" title={part}>
|
|
33
33
|
<span suppressHydrationWarning>{part}</span>
|
|
34
34
|
</Link>
|
|
35
35
|
)}
|
|
@@ -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'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import Papa from 'papaparse';
|
|
5
|
-
import { ChevronUp, ChevronDown, ChevronsUpDown, Plus, Trash2 } from 'lucide-react';
|
|
5
|
+
import { ChevronUp, ChevronDown, ChevronsUpDown, Plus, Trash2, Loader2 } from 'lucide-react';
|
|
6
6
|
|
|
7
7
|
interface CsvViewProps {
|
|
8
8
|
content: string;
|
|
@@ -135,6 +135,7 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
135
135
|
const [sortCol, setSortCol] = useState<number | null>(null);
|
|
136
136
|
const [sortDir, setSortDir] = useState<SortDir>(null);
|
|
137
137
|
const [showAdd, setShowAdd] = useState(false);
|
|
138
|
+
const [saving, setSaving] = useState(false);
|
|
138
139
|
|
|
139
140
|
const parsed = useMemo(() => {
|
|
140
141
|
const result = Papa.parse<string[]>(content, { skipEmptyLines: true });
|
|
@@ -163,38 +164,57 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
163
164
|
// Update a single cell and persist
|
|
164
165
|
const handleCellCommit = useCallback(async (rowIdx: number, colIdx: number, newVal: string) => {
|
|
165
166
|
if (!saveAction) return;
|
|
166
|
-
|
|
167
|
-
const updatedRows = rows.map((r, i) => {
|
|
168
|
-
// find which original row matches this sorted row
|
|
167
|
+
const updatedRows = rows.map((r) => {
|
|
169
168
|
const sorted = sortedRows[rowIdx];
|
|
170
169
|
if (r === sorted) return r.map((cell, ci) => ci === colIdx ? newVal : cell);
|
|
171
170
|
return r;
|
|
172
171
|
});
|
|
173
172
|
const newContent = serializeRows(headers, updatedRows);
|
|
174
173
|
setContent(newContent);
|
|
175
|
-
|
|
174
|
+
setSaving(true);
|
|
175
|
+
try {
|
|
176
|
+
await saveAction(newContent);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('[CsvView] Cell save failed:', err);
|
|
179
|
+
} finally {
|
|
180
|
+
setSaving(false);
|
|
181
|
+
}
|
|
176
182
|
}, [saveAction, rows, sortedRows, headers]);
|
|
177
183
|
|
|
178
184
|
// Delete a row and persist
|
|
179
185
|
const handleDeleteRow = useCallback(async (rowIdx: number) => {
|
|
180
|
-
if (!saveAction) return;
|
|
186
|
+
if (!saveAction || saving) return;
|
|
181
187
|
const sorted = sortedRows[rowIdx];
|
|
182
188
|
const updatedRows = rows.filter(r => r !== sorted);
|
|
183
189
|
const newContent = serializeRows(headers, updatedRows);
|
|
184
190
|
setContent(newContent);
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
setSaving(true);
|
|
192
|
+
try {
|
|
193
|
+
await saveAction(newContent);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('[CsvView] Row delete failed:', err);
|
|
196
|
+
} finally {
|
|
197
|
+
setSaving(false);
|
|
198
|
+
}
|
|
199
|
+
}, [saveAction, rows, sortedRows, headers, saving]);
|
|
187
200
|
|
|
188
201
|
// Append a new row
|
|
189
202
|
const handleAddRow = useCallback(async (newRow: string[]) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
setSaving(true);
|
|
204
|
+
try {
|
|
205
|
+
if (appendAction) {
|
|
206
|
+
const result = await appendAction(newRow);
|
|
207
|
+
setContent(result.newContent);
|
|
208
|
+
} else if (saveAction) {
|
|
209
|
+
const newContent = serializeRows(headers, [...rows, newRow]);
|
|
210
|
+
setContent(newContent);
|
|
211
|
+
await saveAction(newContent);
|
|
212
|
+
}
|
|
213
|
+
setShowAdd(false);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error('[CsvView] Add row failed:', err);
|
|
216
|
+
} finally {
|
|
217
|
+
setSaving(false);
|
|
198
218
|
}
|
|
199
219
|
}, [appendAction, saveAction, headers, rows]);
|
|
200
220
|
|
|
@@ -266,9 +286,10 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
266
286
|
{saveAction && (
|
|
267
287
|
<button
|
|
268
288
|
onClick={() => handleDeleteRow(rowIdx)}
|
|
269
|
-
|
|
289
|
+
disabled={saving}
|
|
290
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-destructive/10 disabled:opacity-30"
|
|
270
291
|
style={{ color: 'var(--muted-foreground)' }}
|
|
271
|
-
title=
|
|
292
|
+
title={saving ? 'Saving...' : 'Delete row'}
|
|
272
293
|
>
|
|
273
294
|
<Trash2 size={12} />
|
|
274
295
|
</button>
|
|
@@ -295,8 +316,9 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
295
316
|
className="px-4 py-2 flex items-center justify-between"
|
|
296
317
|
style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}
|
|
297
318
|
>
|
|
298
|
-
<span className="text-xs font-display" style={{ color: 'var(--muted-foreground)' }}>
|
|
319
|
+
<span className="text-xs font-display flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
299
320
|
{rows.length} rows · {headers.length} cols
|
|
321
|
+
{saving && <Loader2 size={10} className="animate-spin" style={{ color: 'var(--amber)' }} />}
|
|
300
322
|
</span>
|
|
301
323
|
|
|
302
324
|
{canEdit && !showAdd && (
|
|
@@ -207,7 +207,7 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
|
|
|
207
207
|
{entry.type === 'directory'
|
|
208
208
|
? <FolderOpen size={22} className="text-yellow-400" />
|
|
209
209
|
: <FileIconLarge node={entry} />}
|
|
210
|
-
<span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
|
|
210
|
+
<span className="text-xs text-foreground leading-snug line-clamp-2 w-full" title={entry.name} suppressHydrationWarning>
|
|
211
211
|
{entry.name}
|
|
212
212
|
</span>
|
|
213
213
|
{entry.type === 'directory' && (
|
|
@@ -230,7 +230,7 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
|
|
|
230
230
|
className="flex items-center gap-3 px-4 py-3 bg-card hover:bg-accent transition-colors duration-100"
|
|
231
231
|
>
|
|
232
232
|
<FileIcon node={entry} />
|
|
233
|
-
<span className="flex-1 text-sm text-foreground truncate" suppressHydrationWarning>
|
|
233
|
+
<span className="flex-1 text-sm text-foreground truncate" title={entry.name} suppressHydrationWarning>
|
|
234
234
|
{entry.name}
|
|
235
235
|
</span>
|
|
236
236
|
{entry.type === 'directory' ? (
|
|
@@ -29,7 +29,9 @@ export default function GuideCard() {
|
|
|
29
29
|
setGuideState(null);
|
|
30
30
|
}
|
|
31
31
|
})
|
|
32
|
-
.catch(() => {
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
console.warn('[GuideCard] Fetch guide state failed:', err);
|
|
34
|
+
});
|
|
33
35
|
}, []);
|
|
34
36
|
|
|
35
37
|
useEffect(() => {
|
|
@@ -54,7 +56,9 @@ export default function GuideCard() {
|
|
|
54
56
|
method: 'PATCH',
|
|
55
57
|
headers: { 'Content-Type': 'application/json' },
|
|
56
58
|
body: JSON.stringify({ guideState: patch }),
|
|
57
|
-
}).catch(() => {
|
|
59
|
+
}).catch((err) => {
|
|
60
|
+
console.warn('[GuideCard] PATCH guide state failed:', err);
|
|
61
|
+
});
|
|
58
62
|
}, []);
|
|
59
63
|
|
|
60
64
|
const handleDismiss = useCallback(() => {
|
|
@@ -618,7 +618,7 @@ function ExampleCleanupBanner() {
|
|
|
618
618
|
useEffect(() => {
|
|
619
619
|
scanExampleFilesAction().then(r => {
|
|
620
620
|
if (r.files.length > 0) setCount(r.files.length);
|
|
621
|
-
}).catch(() => {});
|
|
621
|
+
}).catch((err) => { console.warn("[HomeContent] scanExampleFilesAction failed:", err); });
|
|
622
622
|
}, []);
|
|
623
623
|
|
|
624
624
|
const handleCleanup = useCallback(async () => {
|
|
@@ -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
|
>
|
|
@@ -171,13 +171,13 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
|
|
|
171
171
|
}
|
|
172
172
|
<div className="min-w-0 flex-1">
|
|
173
173
|
<div className="flex items-baseline gap-2 flex-wrap">
|
|
174
|
-
<span className="text-sm text-foreground font-medium truncate">{fileName}</span>
|
|
174
|
+
<span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
|
|
175
175
|
{dirPath && (
|
|
176
|
-
<span className="text-xs text-muted-foreground truncate">{dirPath}</span>
|
|
176
|
+
<span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
|
|
177
177
|
)}
|
|
178
178
|
</div>
|
|
179
179
|
{result.snippet && (
|
|
180
|
-
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
|
|
180
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
|
|
181
181
|
{highlightSnippet(result.snippet, query)}
|
|
182
182
|
</p>
|
|
183
183
|
)}
|
|
@@ -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 {
|
|
@@ -168,7 +168,7 @@ export default function SyncStatusBar({ collapsed, onOpenSyncSettings }: SyncSta
|
|
|
168
168
|
const prevLevelRef = useRef<StatusLevel>('off');
|
|
169
169
|
const [hintDismissed, setHintDismissed] = useState(() => {
|
|
170
170
|
if (typeof window !== 'undefined') {
|
|
171
|
-
try { return !!localStorage.getItem('sync-hint-dismissed'); } catch {}
|
|
171
|
+
try { return !!localStorage.getItem('sync-hint-dismissed'); } catch (err) { console.warn("[SyncStatusBar] localStorage read failed:", err); }
|
|
172
172
|
}
|
|
173
173
|
return false;
|
|
174
174
|
});
|
|
@@ -219,7 +219,7 @@ export default function SyncStatusBar({ collapsed, onOpenSyncSettings }: SyncSta
|
|
|
219
219
|
<button
|
|
220
220
|
onClick={(e) => {
|
|
221
221
|
e.stopPropagation();
|
|
222
|
-
try { localStorage.setItem('sync-hint-dismissed', '1'); } catch {}
|
|
222
|
+
try { localStorage.setItem('sync-hint-dismissed', '1'); } catch (err) { console.warn("[SyncStatusBar] localStorage write dismissed:", err); }
|
|
223
223
|
setHintDismissed(true);
|
|
224
224
|
}}
|
|
225
225
|
className="p-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0 ml-2 text-muted-foreground/50 hover:text-muted-foreground"
|