@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.
- package/app/app/.well-known/agent-card.json/route.ts +34 -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/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/SearchModal.tsx +3 -3
- package/app/components/SyncStatusBar.tsx +2 -2
- package/app/components/ask/AskContent.tsx +1 -1
- package/app/components/ask/MentionPopover.tsx +2 -2
- package/app/components/ask/SlashCommandPopover.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +2 -2
- package/app/components/help/HelpContent.tsx +6 -1
- 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/settings/AiTab.tsx +4 -4
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +22 -4
- package/app/components/settings/UpdateTab.tsx +1 -1
- package/app/components/setup/index.tsx +9 -3
- package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
- package/app/lib/a2a/agent-card.ts +107 -0
- package/app/lib/a2a/index.ts +23 -0
- package/app/lib/a2a/task-handler.ts +228 -0
- package/app/lib/a2a/types.ts +158 -0
- package/bin/cli.js +10 -0
- package/bin/commands/agent.js +18 -0
- package/bin/commands/api.js +58 -0
- package/bin/commands/search.js +51 -0
- 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
|
-
|
|
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 () => {
|
|
@@ -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 {
|
|
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) {
|
|
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) {
|
|
63
|
-
|
|
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)
|
|
234
|
-
|
|
235
|
-
|
|
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') => {
|