@geminilight/mindos 0.5.60 → 0.5.63
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/agents/[agentKey]/page.tsx +17 -0
- package/app/app/agents/page.tsx +20 -0
- package/app/app/api/changes/route.ts +57 -0
- package/app/app/api/file/route.ts +146 -1
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/settings/test-key/route.ts +43 -1
- package/app/app/changes/page.tsx +16 -0
- package/app/components/ActivityBar.tsx +9 -1
- package/app/components/SidebarLayout.tsx +12 -2
- package/app/components/agents/AgentDetailContent.tsx +117 -0
- package/app/components/agents/AgentsContentPage.tsx +87 -0
- package/app/components/agents/AgentsMcpSection.tsx +123 -0
- package/app/components/agents/AgentsOverviewSection.tsx +89 -0
- package/app/components/agents/AgentsSkillsSection.tsx +146 -0
- package/app/components/agents/agents-content-model.ts +80 -0
- package/app/components/ask/AskContent.tsx +15 -6
- package/app/components/changes/ChangesBanner.tsx +51 -0
- package/app/components/changes/ChangesContentPage.tsx +190 -0
- package/app/components/changes/line-diff.ts +75 -0
- package/app/components/panels/AgentsPanel.tsx +11 -0
- package/app/components/settings/UpdateTab.tsx +190 -1
- package/app/lib/core/content-changes.ts +153 -0
- package/app/lib/core/index.ts +14 -0
- package/app/lib/fs.ts +22 -0
- package/app/lib/i18n-en.ts +92 -0
- package/app/lib/i18n-zh.ts +92 -0
- package/app/package.json +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
3
|
+
import AgentDetailContent from '@/components/agents/AgentDetailContent';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export default async function AgentDetailPage({
|
|
8
|
+
params,
|
|
9
|
+
}: {
|
|
10
|
+
params: Promise<{ agentKey: string }>;
|
|
11
|
+
}) {
|
|
12
|
+
const settings = readSettings();
|
|
13
|
+
if (settings.setupPending) redirect('/setup');
|
|
14
|
+
|
|
15
|
+
const { agentKey } = await params;
|
|
16
|
+
return <AgentDetailContent agentKey={decodeURIComponent(agentKey)} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
3
|
+
import AgentsContentPage from '@/components/agents/AgentsContentPage';
|
|
4
|
+
import { parseAgentsTab } from '@/components/agents/agents-content-model';
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export default async function AgentsPage({
|
|
9
|
+
searchParams,
|
|
10
|
+
}: {
|
|
11
|
+
searchParams: Promise<{ tab?: string }>;
|
|
12
|
+
}) {
|
|
13
|
+
const settings = readSettings();
|
|
14
|
+
if (settings.setupPending) redirect('/setup');
|
|
15
|
+
|
|
16
|
+
const params = await searchParams;
|
|
17
|
+
const tab = parseAgentsTab(params.tab);
|
|
18
|
+
|
|
19
|
+
return <AgentsContentPage tab={tab} />;
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
listContentChanges,
|
|
4
|
+
getContentChangeSummary,
|
|
5
|
+
markContentChangesSeen,
|
|
6
|
+
} from '@/lib/fs';
|
|
7
|
+
|
|
8
|
+
export const dynamic = 'force-dynamic';
|
|
9
|
+
|
|
10
|
+
function err(message: string, status = 400) {
|
|
11
|
+
return NextResponse.json({ error: message }, { status });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(req: NextRequest) {
|
|
15
|
+
const op = req.nextUrl.searchParams.get('op') ?? 'summary';
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (op === 'summary') {
|
|
19
|
+
const summary = getContentChangeSummary();
|
|
20
|
+
return NextResponse.json(summary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (op === 'list') {
|
|
24
|
+
const path = req.nextUrl.searchParams.get('path') ?? undefined;
|
|
25
|
+
const limitParam = req.nextUrl.searchParams.get('limit');
|
|
26
|
+
const limit = limitParam ? Number(limitParam) : 50;
|
|
27
|
+
if (!Number.isFinite(limit) || limit <= 0) return err('invalid limit');
|
|
28
|
+
return NextResponse.json({ events: listContentChanges({ path, limit }) });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return err(`unknown op: ${op}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return err((error as Error).message, 500);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function POST(req: NextRequest) {
|
|
38
|
+
let body: Record<string, unknown>;
|
|
39
|
+
try {
|
|
40
|
+
body = await req.json();
|
|
41
|
+
} catch {
|
|
42
|
+
return err('invalid JSON');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const op = body.op;
|
|
46
|
+
if (typeof op !== 'string') return err('missing op');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (op === 'mark_seen') {
|
|
50
|
+
markContentChangesSeen();
|
|
51
|
+
return NextResponse.json({ ok: true });
|
|
52
|
+
}
|
|
53
|
+
return err(`unknown op: ${op}`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return err((error as Error).message, 500);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getMindRoot,
|
|
20
20
|
invalidateCache,
|
|
21
21
|
listMindSpaces,
|
|
22
|
+
appendContentChange,
|
|
22
23
|
} from '@/lib/fs';
|
|
23
24
|
import { createSpaceFilesystem } from '@/lib/core/create-space';
|
|
24
25
|
|
|
@@ -26,6 +27,22 @@ function err(msg: string, status = 400) {
|
|
|
26
27
|
return NextResponse.json({ error: msg }, { status });
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function safeRead(filePath: string): string {
|
|
31
|
+
try {
|
|
32
|
+
return getFileContent(filePath);
|
|
33
|
+
} catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sourceFromRequest(req: NextRequest, body: Record<string, unknown>) {
|
|
39
|
+
const bodySource = body.source;
|
|
40
|
+
if (bodySource === 'agent' || bodySource === 'user' || bodySource === 'system') return bodySource;
|
|
41
|
+
const headerSource = req.headers.get('x-mindos-source');
|
|
42
|
+
if (headerSource === 'agent' || headerSource === 'user' || headerSource === 'system') return headerSource;
|
|
43
|
+
return 'user' as const;
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
// GET /api/file?path=foo.md&op=read_file|read_lines | GET ?op=list_spaces (no path)
|
|
30
47
|
export async function GET(req: NextRequest) {
|
|
31
48
|
const filePath = req.nextUrl.searchParams.get('path');
|
|
@@ -73,13 +90,32 @@ export async function POST(req: NextRequest) {
|
|
|
73
90
|
|
|
74
91
|
try {
|
|
75
92
|
let resp: NextResponse;
|
|
93
|
+
let changeEvent:
|
|
94
|
+
| {
|
|
95
|
+
op: string;
|
|
96
|
+
path: string;
|
|
97
|
+
summary: string;
|
|
98
|
+
before?: string;
|
|
99
|
+
after?: string;
|
|
100
|
+
beforePath?: string;
|
|
101
|
+
afterPath?: string;
|
|
102
|
+
}
|
|
103
|
+
| null = null;
|
|
76
104
|
|
|
77
105
|
switch (op) {
|
|
78
106
|
|
|
79
107
|
case 'save_file': {
|
|
80
108
|
const { content } = params as { content: string };
|
|
81
109
|
if (typeof content !== 'string') return err('missing content');
|
|
110
|
+
const before = safeRead(filePath);
|
|
82
111
|
saveFileContent(filePath, content);
|
|
112
|
+
changeEvent = {
|
|
113
|
+
op,
|
|
114
|
+
path: filePath,
|
|
115
|
+
summary: 'Updated file content',
|
|
116
|
+
before,
|
|
117
|
+
after: content,
|
|
118
|
+
};
|
|
83
119
|
resp = NextResponse.json({ ok: true });
|
|
84
120
|
break;
|
|
85
121
|
}
|
|
@@ -87,7 +123,15 @@ export async function POST(req: NextRequest) {
|
|
|
87
123
|
case 'append_to_file': {
|
|
88
124
|
const { content } = params as { content: string };
|
|
89
125
|
if (typeof content !== 'string') return err('missing content');
|
|
126
|
+
const before = safeRead(filePath);
|
|
90
127
|
appendToFile(filePath, content);
|
|
128
|
+
changeEvent = {
|
|
129
|
+
op,
|
|
130
|
+
path: filePath,
|
|
131
|
+
summary: 'Appended content to file',
|
|
132
|
+
before,
|
|
133
|
+
after: safeRead(filePath),
|
|
134
|
+
};
|
|
91
135
|
resp = NextResponse.json({ ok: true });
|
|
92
136
|
break;
|
|
93
137
|
}
|
|
@@ -96,7 +140,15 @@ export async function POST(req: NextRequest) {
|
|
|
96
140
|
const { after_index, lines } = params as { after_index: number; lines: string[] };
|
|
97
141
|
if (typeof after_index !== 'number') return err('missing after_index');
|
|
98
142
|
if (!Array.isArray(lines)) return err('lines must be array');
|
|
143
|
+
const before = safeRead(filePath);
|
|
99
144
|
insertLines(filePath, after_index, lines);
|
|
145
|
+
changeEvent = {
|
|
146
|
+
op,
|
|
147
|
+
path: filePath,
|
|
148
|
+
summary: `Inserted ${lines.length} line(s)`,
|
|
149
|
+
before,
|
|
150
|
+
after: safeRead(filePath),
|
|
151
|
+
};
|
|
100
152
|
resp = NextResponse.json({ ok: true });
|
|
101
153
|
break;
|
|
102
154
|
}
|
|
@@ -107,7 +159,15 @@ export async function POST(req: NextRequest) {
|
|
|
107
159
|
if (!Array.isArray(lines)) return err('lines must be array');
|
|
108
160
|
if (start < 0 || end < 0) return err('start/end must be >= 0');
|
|
109
161
|
if (start > end) return err('start must be <= end');
|
|
162
|
+
const before = safeRead(filePath);
|
|
110
163
|
updateLines(filePath, start, end, lines);
|
|
164
|
+
changeEvent = {
|
|
165
|
+
op,
|
|
166
|
+
path: filePath,
|
|
167
|
+
summary: `Updated lines ${start}-${end}`,
|
|
168
|
+
before,
|
|
169
|
+
after: safeRead(filePath),
|
|
170
|
+
};
|
|
111
171
|
resp = NextResponse.json({ ok: true });
|
|
112
172
|
break;
|
|
113
173
|
}
|
|
@@ -116,7 +176,15 @@ export async function POST(req: NextRequest) {
|
|
|
116
176
|
const { heading, content } = params as { heading: string; content: string };
|
|
117
177
|
if (typeof heading !== 'string') return err('missing heading');
|
|
118
178
|
if (typeof content !== 'string') return err('missing content');
|
|
179
|
+
const before = safeRead(filePath);
|
|
119
180
|
insertAfterHeading(filePath, heading, content);
|
|
181
|
+
changeEvent = {
|
|
182
|
+
op,
|
|
183
|
+
path: filePath,
|
|
184
|
+
summary: `Inserted content after heading "${heading}"`,
|
|
185
|
+
before,
|
|
186
|
+
after: safeRead(filePath),
|
|
187
|
+
};
|
|
120
188
|
resp = NextResponse.json({ ok: true });
|
|
121
189
|
break;
|
|
122
190
|
}
|
|
@@ -125,13 +193,29 @@ export async function POST(req: NextRequest) {
|
|
|
125
193
|
const { heading, content } = params as { heading: string; content: string };
|
|
126
194
|
if (typeof heading !== 'string') return err('missing heading');
|
|
127
195
|
if (typeof content !== 'string') return err('missing content');
|
|
196
|
+
const before = safeRead(filePath);
|
|
128
197
|
updateSection(filePath, heading, content);
|
|
198
|
+
changeEvent = {
|
|
199
|
+
op,
|
|
200
|
+
path: filePath,
|
|
201
|
+
summary: `Updated section "${heading}"`,
|
|
202
|
+
before,
|
|
203
|
+
after: safeRead(filePath),
|
|
204
|
+
};
|
|
129
205
|
resp = NextResponse.json({ ok: true });
|
|
130
206
|
break;
|
|
131
207
|
}
|
|
132
208
|
|
|
133
209
|
case 'delete_file': {
|
|
210
|
+
const before = safeRead(filePath);
|
|
134
211
|
deleteFile(filePath);
|
|
212
|
+
changeEvent = {
|
|
213
|
+
op,
|
|
214
|
+
path: filePath,
|
|
215
|
+
summary: 'Deleted file',
|
|
216
|
+
before,
|
|
217
|
+
after: '',
|
|
218
|
+
};
|
|
135
219
|
resp = NextResponse.json({ ok: true });
|
|
136
220
|
break;
|
|
137
221
|
}
|
|
@@ -139,14 +223,32 @@ export async function POST(req: NextRequest) {
|
|
|
139
223
|
case 'rename_file': {
|
|
140
224
|
const { new_name } = params as { new_name: string };
|
|
141
225
|
if (typeof new_name !== 'string' || !new_name) return err('missing new_name');
|
|
226
|
+
const before = safeRead(filePath);
|
|
142
227
|
const newPath = renameFile(filePath, new_name);
|
|
228
|
+
changeEvent = {
|
|
229
|
+
op,
|
|
230
|
+
path: newPath,
|
|
231
|
+
summary: `Renamed file to ${new_name}`,
|
|
232
|
+
before,
|
|
233
|
+
after: safeRead(newPath),
|
|
234
|
+
beforePath: filePath,
|
|
235
|
+
afterPath: newPath,
|
|
236
|
+
};
|
|
143
237
|
resp = NextResponse.json({ ok: true, newPath });
|
|
144
238
|
break;
|
|
145
239
|
}
|
|
146
240
|
|
|
147
241
|
case 'create_file': {
|
|
148
242
|
const { content } = params as { content?: string };
|
|
149
|
-
|
|
243
|
+
const after = typeof content === 'string' ? content : '';
|
|
244
|
+
createFile(filePath, after);
|
|
245
|
+
changeEvent = {
|
|
246
|
+
op,
|
|
247
|
+
path: filePath,
|
|
248
|
+
summary: 'Created file',
|
|
249
|
+
before: '',
|
|
250
|
+
after,
|
|
251
|
+
};
|
|
150
252
|
resp = NextResponse.json({ ok: true });
|
|
151
253
|
break;
|
|
152
254
|
}
|
|
@@ -154,7 +256,17 @@ export async function POST(req: NextRequest) {
|
|
|
154
256
|
case 'move_file': {
|
|
155
257
|
const { to_path } = params as { to_path: string };
|
|
156
258
|
if (typeof to_path !== 'string' || !to_path) return err('missing to_path');
|
|
259
|
+
const before = safeRead(filePath);
|
|
157
260
|
const result = moveFile(filePath, to_path);
|
|
261
|
+
changeEvent = {
|
|
262
|
+
op,
|
|
263
|
+
path: result.newPath,
|
|
264
|
+
summary: `Moved file to ${result.newPath}`,
|
|
265
|
+
before,
|
|
266
|
+
after: safeRead(result.newPath),
|
|
267
|
+
beforePath: filePath,
|
|
268
|
+
afterPath: result.newPath,
|
|
269
|
+
};
|
|
158
270
|
resp = NextResponse.json({ ok: true, ...result });
|
|
159
271
|
break;
|
|
160
272
|
}
|
|
@@ -169,6 +281,13 @@ export async function POST(req: NextRequest) {
|
|
|
169
281
|
try {
|
|
170
282
|
const { path: spacePath } = createSpaceFilesystem(getMindRoot(), name, description, parent_path);
|
|
171
283
|
invalidateCache();
|
|
284
|
+
changeEvent = {
|
|
285
|
+
op,
|
|
286
|
+
path: spacePath,
|
|
287
|
+
summary: 'Created space',
|
|
288
|
+
before: '',
|
|
289
|
+
after: description,
|
|
290
|
+
};
|
|
172
291
|
resp = NextResponse.json({ ok: true, path: spacePath });
|
|
173
292
|
} catch (e) {
|
|
174
293
|
const msg = (e as Error).message;
|
|
@@ -186,6 +305,13 @@ export async function POST(req: NextRequest) {
|
|
|
186
305
|
const { new_name } = params as { new_name: string };
|
|
187
306
|
if (typeof new_name !== 'string' || !new_name.trim()) return err('missing new_name');
|
|
188
307
|
const newPath = renameSpace(filePath, new_name.trim());
|
|
308
|
+
changeEvent = {
|
|
309
|
+
op,
|
|
310
|
+
path: newPath,
|
|
311
|
+
summary: `Renamed space to ${new_name.trim()}`,
|
|
312
|
+
beforePath: filePath,
|
|
313
|
+
afterPath: newPath,
|
|
314
|
+
};
|
|
189
315
|
resp = NextResponse.json({ ok: true, newPath });
|
|
190
316
|
break;
|
|
191
317
|
}
|
|
@@ -193,7 +319,15 @@ export async function POST(req: NextRequest) {
|
|
|
193
319
|
case 'append_csv': {
|
|
194
320
|
const { row } = params as { row: string[] };
|
|
195
321
|
if (!Array.isArray(row) || row.length === 0) return err('row must be non-empty array');
|
|
322
|
+
const before = safeRead(filePath);
|
|
196
323
|
const result = appendCsvRow(filePath, row);
|
|
324
|
+
changeEvent = {
|
|
325
|
+
op,
|
|
326
|
+
path: filePath,
|
|
327
|
+
summary: `Appended CSV row (${row.length} cell${row.length === 1 ? '' : 's'})`,
|
|
328
|
+
before,
|
|
329
|
+
after: safeRead(filePath),
|
|
330
|
+
};
|
|
197
331
|
resp = NextResponse.json({ ok: true, ...result });
|
|
198
332
|
break;
|
|
199
333
|
}
|
|
@@ -207,6 +341,17 @@ export async function POST(req: NextRequest) {
|
|
|
207
341
|
try { revalidatePath('/', 'layout'); } catch { /* noop in test env */ }
|
|
208
342
|
}
|
|
209
343
|
|
|
344
|
+
if (changeEvent) {
|
|
345
|
+
try {
|
|
346
|
+
appendContentChange({
|
|
347
|
+
...changeEvent,
|
|
348
|
+
source: sourceFromRequest(req, body),
|
|
349
|
+
});
|
|
350
|
+
} catch (logError) {
|
|
351
|
+
console.warn('[file.route] failed to append content change log:', (logError as Error).message);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
210
355
|
return resp;
|
|
211
356
|
} catch (e) {
|
|
212
357
|
return err((e as Error).message, 500);
|
|
@@ -17,7 +17,7 @@ function parseHostname(host: string): string {
|
|
|
17
17
|
export async function GET(req: NextRequest) {
|
|
18
18
|
try {
|
|
19
19
|
const settings = readSettings();
|
|
20
|
-
const port = settings.mcpPort
|
|
20
|
+
const port = Number(process.env.MINDOS_MCP_PORT) || settings.mcpPort || 8781;
|
|
21
21
|
const token = settings.authToken ?? '';
|
|
22
22
|
const authConfigured = !!token;
|
|
23
23
|
|
|
@@ -62,7 +62,49 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
62
62
|
signal: ctrl.signal,
|
|
63
63
|
});
|
|
64
64
|
const latency = Date.now() - start;
|
|
65
|
-
if (res.ok)
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
// `/api/ask` always sends tool definitions, so key test should verify this
|
|
67
|
+
// compatibility as well (not just plain chat completion).
|
|
68
|
+
const toolRes = await fetch(url, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
model,
|
|
76
|
+
max_tokens: 1,
|
|
77
|
+
messages: [
|
|
78
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
79
|
+
{ role: 'user', content: 'hi' },
|
|
80
|
+
],
|
|
81
|
+
tools: [{
|
|
82
|
+
type: 'function',
|
|
83
|
+
function: {
|
|
84
|
+
name: 'noop',
|
|
85
|
+
description: 'No-op function used for compatibility checks.',
|
|
86
|
+
parameters: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {},
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}],
|
|
93
|
+
tool_choice: 'none',
|
|
94
|
+
}),
|
|
95
|
+
signal: ctrl.signal,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (toolRes.ok) return { ok: true, latency };
|
|
99
|
+
|
|
100
|
+
const toolBody = await toolRes.text();
|
|
101
|
+
const toolErr = classifyError(toolRes.status, toolBody);
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
code: toolErr.code,
|
|
105
|
+
error: `Model endpoint passes basic test but is incompatible with agent tool calls: ${toolErr.error}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
66
108
|
const body = await res.text();
|
|
67
109
|
return { ok: false, ...classifyError(res.status, body) };
|
|
68
110
|
} catch (e: unknown) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
3
|
+
import ChangesContentPage from '@/components/changes/ChangesContentPage';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export default async function ChangesPage({
|
|
8
|
+
searchParams,
|
|
9
|
+
}: {
|
|
10
|
+
searchParams: Promise<{ path?: string }>;
|
|
11
|
+
}) {
|
|
12
|
+
const settings = readSettings();
|
|
13
|
+
if (settings.setupPending) redirect('/setup');
|
|
14
|
+
const params = await searchParams;
|
|
15
|
+
return <ChangesContentPage initialPath={params.path ?? ''} />;
|
|
16
|
+
}
|
|
@@ -16,6 +16,7 @@ export const RAIL_WIDTH_EXPANDED = 180;
|
|
|
16
16
|
interface ActivityBarProps {
|
|
17
17
|
activePanel: PanelId | null;
|
|
18
18
|
onPanelChange: (id: PanelId | null) => void;
|
|
19
|
+
onAgentsClick?: () => void;
|
|
19
20
|
syncStatus: SyncStatus | null;
|
|
20
21
|
expanded: boolean;
|
|
21
22
|
onExpandedChange: (expanded: boolean) => void;
|
|
@@ -76,6 +77,7 @@ function RailButton({ icon, label, shortcut, active = false, expanded, onClick,
|
|
|
76
77
|
export default function ActivityBar({
|
|
77
78
|
activePanel,
|
|
78
79
|
onPanelChange,
|
|
80
|
+
onAgentsClick,
|
|
79
81
|
syncStatus,
|
|
80
82
|
expanded,
|
|
81
83
|
onExpandedChange,
|
|
@@ -160,7 +162,13 @@ export default function ActivityBar({
|
|
|
160
162
|
<RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
|
|
161
163
|
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
|
|
162
164
|
<RailButton icon={<Blocks size={18} />} label={t.sidebar.plugins} active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
|
|
163
|
-
<RailButton
|
|
165
|
+
<RailButton
|
|
166
|
+
icon={<Bot size={18} />}
|
|
167
|
+
label={t.sidebar.agents}
|
|
168
|
+
active={activePanel === 'agents'}
|
|
169
|
+
expanded={expanded}
|
|
170
|
+
onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
|
|
171
|
+
/>
|
|
164
172
|
<RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
|
|
165
173
|
</div>
|
|
166
174
|
|
|
@@ -25,6 +25,7 @@ import SearchModal from './SearchModal';
|
|
|
25
25
|
import AskModal from './AskModal';
|
|
26
26
|
import SettingsModal from './SettingsModal';
|
|
27
27
|
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
28
|
+
import ChangesBanner from './changes/ChangesBanner';
|
|
28
29
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
29
30
|
import { FileNode } from '@/lib/types';
|
|
30
31
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -81,6 +82,8 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
81
82
|
const currentFile = pathname.startsWith('/view/')
|
|
82
83
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
83
84
|
: undefined;
|
|
85
|
+
const agentsContentActive = pathname?.startsWith('/agents');
|
|
86
|
+
const railActivePanel = lp.activePanel ?? (agentsContentActive ? 'agents' : null);
|
|
84
87
|
|
|
85
88
|
// ── Event listeners ──
|
|
86
89
|
|
|
@@ -228,8 +231,12 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
228
231
|
|
|
229
232
|
{/* ── Desktop: Activity Bar + Panel ── */}
|
|
230
233
|
<ActivityBar
|
|
231
|
-
activePanel={
|
|
234
|
+
activePanel={railActivePanel}
|
|
232
235
|
onPanelChange={lp.setActivePanel}
|
|
236
|
+
onAgentsClick={() => {
|
|
237
|
+
lp.setActivePanel((current) => (current === 'agents' ? null : 'agents'));
|
|
238
|
+
setAgentDetailKey(null);
|
|
239
|
+
}}
|
|
233
240
|
syncStatus={syncStatus}
|
|
234
241
|
expanded={lp.railExpanded}
|
|
235
242
|
onExpandedChange={handleExpandedChange}
|
|
@@ -364,7 +371,10 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
364
371
|
<AskModal open={mobileAskOpen} onClose={() => setMobileAskOpen(false)} currentFile={currentFile} />
|
|
365
372
|
|
|
366
373
|
<main id="main-content" className="min-h-screen transition-all duration-200 pt-[52px] md:pt-0">
|
|
367
|
-
<div className="min-h-screen bg-background">
|
|
374
|
+
<div className="min-h-screen bg-background">
|
|
375
|
+
<ChangesBanner />
|
|
376
|
+
{children}
|
|
377
|
+
</div>
|
|
368
378
|
</main>
|
|
369
379
|
|
|
370
380
|
<style>{`
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { ArrowLeft, Server, ShieldCheck, Activity, Compass } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { useMcpData } from '@/hooks/useMcpData';
|
|
8
|
+
import { resolveAgentStatus } from './agents-content-model';
|
|
9
|
+
|
|
10
|
+
export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
11
|
+
const { t } = useLocale();
|
|
12
|
+
const a = t.agentsContent;
|
|
13
|
+
const mcp = useMcpData();
|
|
14
|
+
|
|
15
|
+
const agent = useMemo(() => mcp.agents.find((item) => item.key === agentKey), [mcp.agents, agentKey]);
|
|
16
|
+
const enabledSkills = useMemo(() => mcp.skills.filter((s) => s.enabled), [mcp.skills]);
|
|
17
|
+
|
|
18
|
+
if (!agent) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-10">
|
|
21
|
+
<Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground mb-4">
|
|
22
|
+
<ArrowLeft size={14} />
|
|
23
|
+
{a.backToOverview}
|
|
24
|
+
</Link>
|
|
25
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
26
|
+
<p className="text-sm text-foreground">{a.detailNotFound}</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const status = resolveAgentStatus(agent);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-10 space-y-4">
|
|
36
|
+
<div>
|
|
37
|
+
<Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground mb-3">
|
|
38
|
+
<ArrowLeft size={14} />
|
|
39
|
+
{a.backToOverview}
|
|
40
|
+
</Link>
|
|
41
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">{agent.name}</h1>
|
|
42
|
+
<p className="mt-1 text-sm text-muted-foreground">{a.detailSubtitle}</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
46
|
+
<h2 className="text-sm font-medium text-foreground mb-2">{a.detail.identity}</h2>
|
|
47
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
|
48
|
+
<DetailLine label={a.detail.agentKey} value={agent.key} />
|
|
49
|
+
<DetailLine label={a.detail.status} value={status} />
|
|
50
|
+
<DetailLine label={a.detail.transport} value={agent.transport ?? agent.preferredTransport} />
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
55
|
+
<h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
|
|
56
|
+
<Server size={14} className="text-muted-foreground" />
|
|
57
|
+
{a.detail.connection}
|
|
58
|
+
</h2>
|
|
59
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
|
60
|
+
<DetailLine label={a.detail.endpoint} value={mcp.status?.endpoint ?? a.na} />
|
|
61
|
+
<DetailLine label={a.detail.port} value={String(mcp.status?.port ?? a.na)} />
|
|
62
|
+
<DetailLine label={a.detail.auth} value={mcp.status?.authConfigured ? a.detail.authConfigured : a.detail.authMissing} />
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
67
|
+
<h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
|
|
68
|
+
<ShieldCheck size={14} className="text-muted-foreground" />
|
|
69
|
+
{a.detail.capabilities}
|
|
70
|
+
</h2>
|
|
71
|
+
<ul className="text-sm text-muted-foreground space-y-1">
|
|
72
|
+
<li>{a.detail.projectScope}: {agent.hasProjectScope ? a.detail.yes : a.detail.no}</li>
|
|
73
|
+
<li>{a.detail.globalScope}: {agent.hasGlobalScope ? a.detail.yes : a.detail.no}</li>
|
|
74
|
+
<li>{a.detail.format}: {agent.format}</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
79
|
+
<h2 className="text-sm font-medium text-foreground mb-2">{a.detail.skillAssignments}</h2>
|
|
80
|
+
{enabledSkills.length === 0 ? (
|
|
81
|
+
<p className="text-sm text-muted-foreground">{a.detail.noSkills}</p>
|
|
82
|
+
) : (
|
|
83
|
+
<ul className="text-sm text-muted-foreground space-y-1">
|
|
84
|
+
{enabledSkills.map((skill) => (
|
|
85
|
+
<li key={skill.name}>- {skill.name}</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
)}
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
92
|
+
<h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
|
|
93
|
+
<Activity size={14} className="text-muted-foreground" />
|
|
94
|
+
{a.detail.recentActivity}
|
|
95
|
+
</h2>
|
|
96
|
+
<p className="text-sm text-muted-foreground">{a.detail.noActivity}</p>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<section className="rounded-lg border border-border bg-card p-4">
|
|
100
|
+
<h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
|
|
101
|
+
<Compass size={14} className="text-muted-foreground" />
|
|
102
|
+
{a.detail.spaceReach}
|
|
103
|
+
</h2>
|
|
104
|
+
<p className="text-sm text-muted-foreground">{a.detail.noSpaceReach}</p>
|
|
105
|
+
</section>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function DetailLine({ label, value }: { label: string; value: string }) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="rounded-md border border-border px-3 py-2">
|
|
113
|
+
<p className="text-2xs text-muted-foreground mb-1">{label}</p>
|
|
114
|
+
<p className="text-sm text-foreground truncate">{value}</p>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|