@geminilight/mindos 0.6.23 → 0.6.25

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 (36) hide show
  1. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  2. package/app/app/api/a2a/route.ts +100 -0
  3. package/app/components/Backlinks.tsx +2 -2
  4. package/app/components/Breadcrumb.tsx +1 -1
  5. package/app/components/CsvView.tsx +41 -19
  6. package/app/components/DirView.tsx +2 -2
  7. package/app/components/GuideCard.tsx +6 -2
  8. package/app/components/HomeContent.tsx +1 -1
  9. package/app/components/SearchModal.tsx +3 -3
  10. package/app/components/SyncStatusBar.tsx +2 -2
  11. package/app/components/ask/AskContent.tsx +1 -1
  12. package/app/components/ask/MentionPopover.tsx +2 -2
  13. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  14. package/app/components/explore/UseCaseCard.tsx +2 -2
  15. package/app/components/help/HelpContent.tsx +6 -1
  16. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  17. package/app/components/panels/DiscoverPanel.tsx +3 -3
  18. package/app/components/panels/PanelNavRow.tsx +2 -2
  19. package/app/components/panels/PluginsPanel.tsx +1 -1
  20. package/app/components/panels/SearchPanel.tsx +3 -3
  21. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  22. package/app/components/settings/AiTab.tsx +4 -4
  23. package/app/components/settings/KnowledgeTab.tsx +1 -1
  24. package/app/components/settings/McpTab.tsx +22 -4
  25. package/app/components/settings/UpdateTab.tsx +1 -1
  26. package/app/components/setup/index.tsx +9 -3
  27. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  28. package/app/lib/a2a/agent-card.ts +107 -0
  29. package/app/lib/a2a/index.ts +23 -0
  30. package/app/lib/a2a/task-handler.ts +228 -0
  31. package/app/lib/a2a/types.ts +158 -0
  32. package/bin/cli.js +10 -0
  33. package/bin/commands/agent.js +18 -0
  34. package/bin/commands/api.js +58 -0
  35. package/bin/commands/search.js +51 -0
  36. package/package.json +1 -1
@@ -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,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
  )}
@@ -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 () => {
@@ -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
  )}
@@ -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"
@@ -442,7 +442,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
442
442
  } else if (typeof errBody?.message === 'string' && errBody.message.trim()) {
443
443
  errorMsg = errBody.message;
444
444
  }
445
- } catch {}
445
+ } catch (err) { console.warn("[AskContent] error body parse failed:", err); }
446
446
  const err = new Error(errorMsg);
447
447
  (err as Error & { httpStatus?: number }).httpStatus = res.status;
448
448
  throw err;
@@ -54,9 +54,9 @@ export default function MentionPopover({ results, selectedIndex, query, onSelect
54
54
  ) : (
55
55
  <FileText size={13} className="text-muted-foreground shrink-0" />
56
56
  )}
57
- <span className="truncate font-medium flex-1"><HighlightMatch text={name} query={query} /></span>
57
+ <span className="truncate font-medium flex-1" title={name}><HighlightMatch text={name} query={query} /></span>
58
58
  {dir && (
59
- <span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0">
59
+ <span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0" title={dir}>
60
60
  <HighlightMatch text={dir} query={query} />
61
61
  </span>
62
62
  )}
@@ -49,7 +49,7 @@ export default function SlashCommandPopover({ results, selectedIndex, query, onS
49
49
  <Zap size={13} className="text-[var(--amber)] shrink-0" />
50
50
  <span className="text-sm font-medium shrink-0">/<HighlightMatch text={item.name} query={query} /></span>
51
51
  {item.description && (
52
- <span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1">{item.description}</span>
52
+ <span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1" title={item.description}>{item.description}</span>
53
53
  )}
54
54
  </button>
55
55
  ))}
@@ -20,10 +20,10 @@ export default function UseCaseCard({ icon, title, description, prompt, tryItLab
20
20
  {icon}
21
21
  </span>
22
22
  <div className="flex-1 min-w-0">
23
- <h3 className="text-sm font-semibold font-display truncate text-foreground">
23
+ <h3 className="text-sm font-semibold font-display truncate text-foreground" title={title}>
24
24
  {title}
25
25
  </h3>
26
- <p className="text-xs leading-relaxed mt-1 line-clamp-2 text-muted-foreground">
26
+ <p className="text-xs leading-relaxed mt-1 line-clamp-2 text-muted-foreground" title={description}>
27
27
  {description}
28
28
  </p>
29
29
  </div>
@@ -59,7 +59,12 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
59
59
  navigator.clipboard.writeText(clean).then(() => {
60
60
  setCopied(true);
61
61
  setTimeout(() => setCopied(false), 1500);
62
- }).catch(() => {});
62
+ }).catch((err) => {
63
+ console.error('[HelpContent] Clipboard copy failed:', err);
64
+ // Show error feedback in UI
65
+ setCopied(true); // Reuse copied state to show error
66
+ setTimeout(() => setCopied(false), 2000);
67
+ });
63
68
  }, [text]);
64
69
 
65
70
  return (
@@ -85,7 +85,7 @@ export default function AgentsPanelAgentDetail({
85
85
  <header className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-4 py-3 bg-card">
86
86
  <div className="flex items-center gap-2.5 min-w-0">
87
87
  <span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
88
- <h2 className="text-sm font-semibold text-foreground truncate font-display">{agent.name}</h2>
88
+ <h2 className="text-sm font-semibold text-foreground truncate font-display" title={agent.name}>{agent.name}</h2>
89
89
  </div>
90
90
  <button
91
91
  type="button"
@@ -107,7 +107,7 @@ export default function AgentsPanelAgentDetail({
107
107
  {copy.backToList}
108
108
  </button>
109
109
  <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
110
- <span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
110
+ <span className="text-sm font-medium text-foreground truncate" title={agent.name}>{agent.name}</span>
111
111
  </div>
112
112
  )}
113
113
 
@@ -32,7 +32,7 @@ function UseCaseRow({
32
32
  return (
33
33
  <div className="group flex items-center gap-2.5 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
34
34
  <span className="text-muted-foreground shrink-0">{icon}</span>
35
- <span className="text-xs text-foreground truncate flex-1">{title}</span>
35
+ <span className="text-xs text-foreground truncate flex-1" title={title}>{title}</span>
36
36
  <button
37
37
  onClick={() => openAskModal(prompt, 'user')}
38
38
  className="opacity-0 group-hover:opacity-100 text-2xs px-2 py-0.5 rounded text-[var(--amber-text)] bg-[var(--amber-dim)] hover:opacity-80 transition-all duration-150 shrink-0"
@@ -85,7 +85,7 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
85
85
  const pathSet = new Set(allPaths);
86
86
  setExistingFiles(new Set(entryPaths.filter(ep => pathSet.has(ep))));
87
87
  })
88
- .catch(() => {});
88
+ .catch((err) => { console.warn("[DiscoverPanel] fetch /api/files failed:", err); });
89
89
  }, [pluginsMounted]);
90
90
 
91
91
  const handleToggle = useCallback((id: string, enabled: boolean) => {
@@ -170,7 +170,7 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
170
170
  onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
171
171
  >
172
172
  <span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
173
- <span className="text-xs text-foreground truncate flex-1">{r.name}</span>
173
+ <span className="text-xs text-foreground truncate flex-1" title={r.name}>{r.name}</span>
174
174
  {r.core ? (
175
175
  <span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
176
176
  ) : (
@@ -28,9 +28,9 @@ export function PanelNavRow({
28
28
  <>
29
29
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted">{icon}</span>
30
30
  <span className="flex-1 min-w-0">
31
- <span className="block text-left text-sm font-medium text-foreground truncate">{title}</span>
31
+ <span className="block text-left text-sm font-medium text-foreground truncate" title={title}>{title}</span>
32
32
  {subtitle ? (
33
- <span className="block text-left text-2xs text-muted-foreground truncate">{subtitle}</span>
33
+ <span className="block text-left text-2xs text-muted-foreground truncate" title={subtitle}>{subtitle}</span>
34
34
  ) : null}
35
35
  </span>
36
36
  {badge}
@@ -44,7 +44,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
44
44
  const pathSet = new Set(allPaths);
45
45
  setExistingFiles(new Set(entryPaths.filter(p => pathSet.has(p))));
46
46
  })
47
- .catch(() => {});
47
+ .catch((err) => { console.warn("[PluginsPanel] fetch /api/files failed:", err); });
48
48
  }, [mounted]);
49
49
 
50
50
  const renderers = mounted ? getPluginRenderers() : [];
@@ -150,13 +150,13 @@ export default function SearchPanel({ active, onNavigate, maximized, onMaximize
150
150
  }
151
151
  <div className="min-w-0 flex-1">
152
152
  <div className="flex items-baseline gap-2 flex-wrap">
153
- <span className="text-sm text-foreground font-medium truncate">{fileName}</span>
153
+ <span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
154
154
  {dirPath && (
155
- <span className="text-xs text-muted-foreground truncate">{dirPath}</span>
155
+ <span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
156
156
  )}
157
157
  </div>
158
158
  {result.snippet && (
159
- <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
159
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
160
160
  {highlightSnippet(result.snippet, query)}
161
161
  </p>
162
162
  )}
@@ -55,7 +55,7 @@ export function SummaryRenderer({ filePath }: RendererContext) {
55
55
  useEffect(() => {
56
56
  apiFetch<RecentFile[]>(`/api/recent-files?limit=${LIMIT}`)
57
57
  .then((data) => setRecentFiles(data.filter(f => f.path.endsWith('.md'))))
58
- .catch(() => {});
58
+ .catch((err) => { console.warn("[SummaryRenderer] fetch recent-files failed:", err); });
59
59
  }, [filePath]);
60
60
 
61
61
  async function generate() {
@@ -51,7 +51,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
51
51
  // Sync reconnectRetries to localStorage so AskContent can read it without fetching settings
52
52
  useEffect(() => {
53
53
  const v = data.agent?.reconnectRetries ?? 3;
54
- try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
54
+ try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
55
55
  }, [data.agent?.reconnectRetries]);
56
56
 
57
57
  const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
@@ -271,7 +271,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
271
271
  onChange={e => {
272
272
  const v = Number(e.target.value);
273
273
  updateAgent({ reconnectRetries: v });
274
- try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
274
+ try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
275
275
  }}
276
276
  >
277
277
  <option value="0">Off</option>
@@ -434,13 +434,13 @@ function AskDisplayMode() {
434
434
  try {
435
435
  const stored = localStorage.getItem('ask-mode');
436
436
  if (stored === 'popup') setMode('popup');
437
- } catch {}
437
+ } catch (err) { console.warn("[AiTab] localStorage getItem ask-mode failed:", err); }
438
438
  }, []);
439
439
 
440
440
  const handleChange = (value: string) => {
441
441
  const next = value as 'panel' | 'popup';
442
442
  setMode(next);
443
- try { localStorage.setItem('ask-mode', next); } catch {}
443
+ try { localStorage.setItem('ask-mode', next); } catch (err) { console.warn("[AiTab] localStorage setItem ask-mode failed:", err); }
444
444
  // Notify SidebarLayout to pick up the change
445
445
  window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
446
446
  };
@@ -26,7 +26,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
26
26
  const [cleanupResult, setCleanupResult] = useState<number | null>(null);
27
27
 
28
28
  useEffect(() => {
29
- scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch(() => {});
29
+ scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch((err) => { console.warn("[KnowledgeTab] scanExampleFilesAction failed:", err); });
30
30
  }, []);
31
31
 
32
32
  // Guide state toggle
@@ -52,15 +52,33 @@ export function McpTab({ t }: McpTabProps) {
52
52
  restarting={restarting}
53
53
  onRestart={async () => {
54
54
  setRestarting(true);
55
- try { await apiFetch('/api/mcp/restart', { method: 'POST' }); } catch {}
55
+ try {
56
+ await apiFetch('/api/mcp/restart', { method: 'POST' });
57
+ } catch (err) {
58
+ console.error('[McpTab] Restart request failed:', err);
59
+ setRestarting(false);
60
+ return; // Exit early, don't start polling if restart request fails
61
+ }
56
62
  const deadline = Date.now() + 60_000;
57
63
  clearInterval(restartPollRef.current);
58
64
  restartPollRef.current = setInterval(async () => {
59
- if (Date.now() > deadline) { clearInterval(restartPollRef.current); setRestarting(false); return; }
65
+ if (Date.now() > deadline) {
66
+ clearInterval(restartPollRef.current);
67
+ setRestarting(false);
68
+ console.warn('[McpTab] MCP restart timed out after 60s');
69
+ return;
70
+ }
60
71
  try {
61
72
  const s = await apiFetch<McpStatus>('/api/mcp/status', { timeout: 3000 });
62
- if (s.running) { clearInterval(restartPollRef.current); setRestarting(false); mcp.refresh(); }
63
- } catch {}
73
+ if (s.running) {
74
+ clearInterval(restartPollRef.current);
75
+ setRestarting(false);
76
+ mcp.refresh();
77
+ }
78
+ } catch (err) {
79
+ console.warn('[McpTab] Status poll attempt failed:', err);
80
+ // Continue polling on individual failures
81
+ }
64
82
  }, 3000);
65
83
  }}
66
84
  onRefresh={mcp.refresh}
@@ -80,7 +80,7 @@ function DesktopUpdateTab() {
80
80
  useEffect(() => {
81
81
  bridge.getAppInfo?.().then((info) => {
82
82
  if (info?.version) setAppVersion(info.version);
83
- }).catch(() => {});
83
+ }).catch((err) => { console.warn("[UpdateTab] getAppInfo failed:", err); });
84
84
  handleCheck();
85
85
  const cleanups: Array<() => void> = [];
86
86
  if (bridge.onUpdateProgress) {
@@ -230,9 +230,15 @@ export default function SetupWizard() {
230
230
  }, []);
231
231
 
232
232
  const copyToken = useCallback(() => {
233
- copyToClipboard(state.authToken).catch(() => {});
234
- setTokenCopied(true);
235
- setTimeout(() => setTokenCopied(false), 2000);
233
+ copyToClipboard(state.authToken)
234
+ .then(() => {
235
+ setTokenCopied(true);
236
+ setTimeout(() => setTokenCopied(false), 2000);
237
+ })
238
+ .catch((err) => {
239
+ console.error('[Setup] Token copy failed:', err);
240
+ // Show error toast instead of success
241
+ });
236
242
  }, [state.authToken]);
237
243
 
238
244
  const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {