@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.
Files changed (64) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/discover/route.ts +23 -0
  5. package/app/app/api/a2a/route.ts +100 -0
  6. package/app/components/Backlinks.tsx +2 -2
  7. package/app/components/Breadcrumb.tsx +1 -1
  8. package/app/components/CreateSpaceModal.tsx +1 -0
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/GuideCard.tsx +6 -2
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/ImportModal.tsx +3 -0
  14. package/app/components/OnboardingView.tsx +1 -0
  15. package/app/components/RightAskPanel.tsx +4 -2
  16. package/app/components/SearchModal.tsx +3 -3
  17. package/app/components/SidebarLayout.tsx +11 -2
  18. package/app/components/SyncStatusBar.tsx +2 -2
  19. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  20. package/app/components/ask/AskContent.tsx +22 -10
  21. package/app/components/ask/MentionPopover.tsx +2 -2
  22. package/app/components/ask/SessionTabBar.tsx +70 -0
  23. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  24. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  25. package/app/components/explore/UseCaseCard.tsx +2 -2
  26. package/app/components/help/HelpContent.tsx +6 -1
  27. package/app/components/panels/AgentsPanel.tsx +25 -2
  28. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  29. package/app/components/panels/DiscoverPanel.tsx +3 -3
  30. package/app/components/panels/PanelNavRow.tsx +2 -2
  31. package/app/components/panels/PluginsPanel.tsx +1 -1
  32. package/app/components/panels/SearchPanel.tsx +3 -3
  33. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  34. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  35. package/app/components/settings/AiTab.tsx +5 -4
  36. package/app/components/settings/KnowledgeTab.tsx +3 -1
  37. package/app/components/settings/McpTab.tsx +22 -4
  38. package/app/components/settings/SyncTab.tsx +2 -0
  39. package/app/components/settings/UpdateTab.tsx +1 -1
  40. package/app/components/setup/StepDots.tsx +5 -1
  41. package/app/components/setup/index.tsx +9 -3
  42. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  43. package/app/hooks/useA2aRegistry.ts +53 -0
  44. package/app/hooks/useAskSession.ts +44 -25
  45. package/app/lib/a2a/a2a-tools.ts +212 -0
  46. package/app/lib/a2a/agent-card.ts +107 -0
  47. package/app/lib/a2a/client.ts +207 -0
  48. package/app/lib/a2a/index.ts +31 -0
  49. package/app/lib/a2a/orchestrator.ts +255 -0
  50. package/app/lib/a2a/task-handler.ts +228 -0
  51. package/app/lib/a2a/types.ts +212 -0
  52. package/app/lib/agent/tools.ts +6 -4
  53. package/app/lib/i18n-en.ts +52 -0
  54. package/app/lib/i18n-zh.ts +52 -0
  55. package/app/next-env.d.ts +1 -1
  56. package/bin/cli.js +183 -164
  57. package/bin/commands/agent.js +110 -0
  58. package/bin/commands/api.js +60 -0
  59. package/bin/commands/ask.js +3 -3
  60. package/bin/commands/file.js +13 -13
  61. package/bin/commands/search.js +51 -0
  62. package/bin/commands/space.js +64 -10
  63. package/bin/lib/command.js +10 -0
  64. 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,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
- // rowIdx here is index into sortedRows — need to map back to original rows
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
- await saveAction(newContent);
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
- await saveAction(newContent);
186
- }, [saveAction, rows, sortedRows, headers]);
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
- setShowAdd(false);
191
- if (appendAction) {
192
- const result = await appendAction(newRow);
193
- setContent(result.newContent);
194
- } else if (saveAction) {
195
- const newContent = serializeRows(headers, [...rows, newRow]);
196
- setContent(newContent);
197
- await saveAction(newContent);
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
- className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-destructive/10"
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="Delete row"
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={{ 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
  >
@@ -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 ${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 {
@@ -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"