@geminilight/mindos 0.5.59 → 0.5.62

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.
@@ -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
- createFile(filePath, typeof content === 'string' ? content : '');
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);
@@ -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) return { ok: true, latency };
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 icon={<Bot size={18} />} label={t.sidebar.agents} active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
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={lp.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">{children}</div>
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
+ }