@geminilight/mindos 0.6.29 → 0.6.31

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 (110) hide show
  1. package/README.md +10 -4
  2. package/README_zh.md +10 -4
  3. package/app/app/api/acp/config/route.ts +82 -0
  4. package/app/app/api/acp/detect/route.ts +71 -48
  5. package/app/app/api/acp/install/route.ts +51 -0
  6. package/app/app/api/acp/session/route.ts +141 -11
  7. package/app/app/api/ask/route.ts +126 -18
  8. package/app/app/api/export/route.ts +105 -0
  9. package/app/app/api/workflows/route.ts +156 -0
  10. package/app/app/globals.css +2 -2
  11. package/app/app/page.tsx +7 -2
  12. package/app/app/trash/page.tsx +7 -0
  13. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  14. package/app/components/ActivityBar.tsx +12 -4
  15. package/app/components/AskModal.tsx +4 -1
  16. package/app/components/ExportModal.tsx +220 -0
  17. package/app/components/FileTree.tsx +42 -11
  18. package/app/components/HomeContent.tsx +92 -20
  19. package/app/components/MarkdownView.tsx +45 -10
  20. package/app/components/Panel.tsx +1 -0
  21. package/app/components/RightAskPanel.tsx +5 -1
  22. package/app/components/Sidebar.tsx +10 -1
  23. package/app/components/SidebarLayout.tsx +6 -0
  24. package/app/components/TrashPageClient.tsx +263 -0
  25. package/app/components/agents/AgentDetailContent.tsx +263 -47
  26. package/app/components/agents/AgentsContentPage.tsx +11 -0
  27. package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
  28. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  29. package/app/components/agents/agents-content-model.ts +2 -2
  30. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  31. package/app/components/ask/AskContent.tsx +197 -239
  32. package/app/components/ask/FileChip.tsx +82 -17
  33. package/app/components/ask/MentionPopover.tsx +21 -3
  34. package/app/components/ask/MessageList.tsx +30 -9
  35. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  36. package/app/components/ask/ToolCallBlock.tsx +102 -18
  37. package/app/components/changes/ChangesContentPage.tsx +58 -14
  38. package/app/components/explore/ExploreContent.tsx +4 -7
  39. package/app/components/explore/UseCaseCard.tsx +18 -1
  40. package/app/components/explore/use-cases.generated.ts +76 -0
  41. package/app/components/explore/use-cases.yaml +185 -0
  42. package/app/components/panels/AgentsPanel.tsx +1 -0
  43. package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
  44. package/app/components/panels/DiscoverPanel.tsx +1 -1
  45. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  46. package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
  49. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  50. package/app/components/renderers/workflow-yaml/execution.ts +229 -0
  51. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  52. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  53. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  54. package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
  55. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  56. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  57. package/app/components/settings/AiTab.tsx +191 -174
  58. package/app/components/settings/AppearanceTab.tsx +168 -77
  59. package/app/components/settings/KnowledgeTab.tsx +131 -136
  60. package/app/components/settings/McpTab.tsx +11 -11
  61. package/app/components/settings/Primitives.tsx +60 -0
  62. package/app/components/settings/SettingsContent.tsx +15 -8
  63. package/app/components/settings/SyncTab.tsx +12 -12
  64. package/app/components/settings/UninstallTab.tsx +8 -18
  65. package/app/components/settings/UpdateTab.tsx +82 -82
  66. package/app/components/settings/types.ts +17 -8
  67. package/app/hooks/useAcpConfig.ts +96 -0
  68. package/app/hooks/useAcpDetection.ts +69 -14
  69. package/app/hooks/useAcpRegistry.ts +46 -11
  70. package/app/hooks/useAskModal.ts +12 -5
  71. package/app/hooks/useAskPanel.ts +8 -5
  72. package/app/hooks/useAskSession.ts +19 -2
  73. package/app/hooks/useImageUpload.ts +152 -0
  74. package/app/lib/acp/acp-tools.ts +3 -1
  75. package/app/lib/acp/agent-descriptors.ts +274 -0
  76. package/app/lib/acp/bridge.ts +6 -0
  77. package/app/lib/acp/index.ts +20 -4
  78. package/app/lib/acp/registry.ts +74 -7
  79. package/app/lib/acp/session.ts +490 -28
  80. package/app/lib/acp/subprocess.ts +307 -21
  81. package/app/lib/acp/types.ts +158 -20
  82. package/app/lib/actions.ts +57 -3
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/stream-consumer.ts +18 -0
  85. package/app/lib/agent/to-agent-messages.ts +25 -2
  86. package/app/lib/agent/tools.ts +56 -9
  87. package/app/lib/core/export.ts +116 -0
  88. package/app/lib/core/trash.ts +241 -0
  89. package/app/lib/fs.ts +47 -0
  90. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  91. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  92. package/app/lib/i18n/index.ts +3 -0
  93. package/app/lib/i18n/modules/knowledge.ts +124 -6
  94. package/app/lib/i18n/modules/navigation.ts +2 -0
  95. package/app/lib/i18n/modules/onboarding.ts +2 -134
  96. package/app/lib/i18n/modules/panels.ts +146 -2
  97. package/app/lib/i18n/modules/settings.ts +12 -0
  98. package/app/lib/pi-integration/skills.ts +21 -6
  99. package/app/lib/renderers/index.ts +2 -2
  100. package/app/lib/settings.ts +10 -0
  101. package/app/lib/types.ts +12 -1
  102. package/app/next-env.d.ts +1 -1
  103. package/app/package.json +11 -3
  104. package/app/scripts/generate-explore.ts +145 -0
  105. package/package.json +1 -1
  106. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  107. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  108. package/app/components/explore/use-cases.ts +0 -58
  109. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  110. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
4
- import { Clock, Globe, Loader2, Network, RefreshCw, Trash2, Wifi, WifiOff, Zap } from 'lucide-react';
3
+ import { useState, useCallback } from 'react';
4
+ import { Check, ChevronDown, ChevronUp, Clock, Code2, Download, Globe, Loader2, MessageSquare, Network, RefreshCw, RotateCcw, Save, Settings2, Trash2, Wifi, WifiOff, Wrench, Zap } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
+ import { useAcpConfig } from '@/hooks/useAcpConfig';
6
7
  import type { RemoteAgent, DelegationRecord } from '@/lib/a2a/types';
7
8
  import type { AcpRegistryEntry } from '@/lib/acp/types';
8
9
  import { useDelegationHistory } from '@/hooks/useDelegationHistory';
@@ -138,6 +139,20 @@ function NetworkEmptyState({
138
139
  );
139
140
  }
140
141
 
142
+ /* ────────── Quick Actions ────────── */
143
+
144
+ interface QuickAction {
145
+ labelKey: 'acpQuickReview' | 'acpQuickFix' | 'acpQuickExplain';
146
+ icon: typeof Code2;
147
+ promptSuffix: string;
148
+ }
149
+
150
+ const QUICK_ACTIONS: QuickAction[] = [
151
+ { labelKey: 'acpQuickReview', icon: Code2, promptSuffix: 'review the code in this project' },
152
+ { labelKey: 'acpQuickFix', icon: Wrench, promptSuffix: 'find and fix bugs in this project' },
153
+ { labelKey: 'acpQuickExplain', icon: MessageSquare, promptSuffix: 'explain the structure of this project' },
154
+ ];
155
+
141
156
  /* ────────── ACP Registry Section ────────── */
142
157
 
143
158
  function AcpRegistrySection() {
@@ -145,6 +160,7 @@ function AcpRegistrySection() {
145
160
  const p = t.panels.agents;
146
161
  const acp = useAcpRegistry();
147
162
  const detection = useAcpDetection();
163
+ const acpConfig = useAcpConfig();
148
164
 
149
165
  if (acp.loading) {
150
166
  return (
@@ -182,8 +198,26 @@ function AcpRegistrySection() {
182
198
 
183
199
  if (acp.agents.length === 0) return null;
184
200
 
201
+ // Separate installed and not-installed agents
202
+ const installedAgents: { agent: AcpRegistryEntry; info: { id: string; name: string; binaryPath: string } }[] = [];
203
+ const notInstalledAgents: { agent: AcpRegistryEntry; installCmd: string | null; packageName: string | null }[] = [];
204
+
205
+ for (const agent of acp.agents) {
206
+ const installed = detection.installedAgents.find((d) => d.id === agent.id);
207
+ if (installed) {
208
+ installedAgents.push({ agent, info: installed });
209
+ } else {
210
+ const notInstalled = detection.notInstalledAgents.find((d) => d.id === agent.id);
211
+ notInstalledAgents.push({
212
+ agent,
213
+ installCmd: notInstalled?.installCmd ?? null,
214
+ packageName: notInstalled?.packageName ?? agent.packageName ?? null,
215
+ });
216
+ }
217
+ }
218
+
185
219
  return (
186
- <div className="space-y-2">
220
+ <div className="space-y-3">
187
221
  <div className="flex items-center justify-between">
188
222
  <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
189
223
  {p.acpSectionTitle}
@@ -203,26 +237,45 @@ function AcpRegistrySection() {
203
237
  </span>
204
238
  </div>
205
239
  </div>
206
- <div className="space-y-1.5">
207
- {acp.agents.map((agent) => {
208
- const installed = detection.installedAgents.find((d) => d.id === agent.id);
209
- const notInstalled = detection.notInstalledAgents.find((d) => d.id === agent.id);
210
- return (
211
- <AcpAgentRow
240
+
241
+ {/* Installed agents prominent cards */}
242
+ {installedAgents.length > 0 && (
243
+ <div className="space-y-2">
244
+ {installedAgents.map(({ agent, info }) => (
245
+ <AcpAgentCard
212
246
  key={agent.id}
213
247
  agent={agent}
214
- installed={installed ?? null}
215
- installCmd={notInstalled?.installCmd ?? null}
248
+ installed={info}
249
+ installCmd={null}
250
+ packageName={null}
216
251
  detectionDone={!detection.loading}
252
+ onInstalled={detection.refresh}
253
+ acpConfig={acpConfig}
217
254
  />
218
- );
219
- })}
220
- </div>
255
+ ))}
256
+ </div>
257
+ )}
258
+
259
+ {/* Not-installed agents — compact rows grouped at bottom */}
260
+ {notInstalledAgents.length > 0 && (
261
+ <div className="space-y-1">
262
+ {notInstalledAgents.map(({ agent, installCmd, packageName }) => (
263
+ <AcpAgentCompactRow
264
+ key={agent.id}
265
+ agent={agent}
266
+ installCmd={installCmd}
267
+ packageName={packageName}
268
+ detectionDone={!detection.loading}
269
+ onInstalled={detection.refresh}
270
+ />
271
+ ))}
272
+ </div>
273
+ )}
221
274
  </div>
222
275
  );
223
276
  }
224
277
 
225
- /* ────────── ACP Agent Row ────────── */
278
+ /* ────────── ACP Agent Card (Installed — prominent) ────────── */
226
279
 
227
280
  const TRANSPORT_STYLES: Record<string, string> = {
228
281
  npx: 'bg-[var(--amber)]/15 text-[var(--amber)]',
@@ -231,14 +284,21 @@ const TRANSPORT_STYLES: Record<string, string> = {
231
284
  stdio: 'bg-muted text-muted-foreground',
232
285
  };
233
286
 
234
- function AcpAgentRow({ agent, installed, installCmd, detectionDone }: {
287
+ function AcpAgentCard({ agent, installed, detectionDone, acpConfig }: {
235
288
  agent: AcpRegistryEntry;
236
- installed: { id: string; name: string; binaryPath: string } | null;
289
+ installed: { id: string; name: string; binaryPath: string; resolvedCommand?: { cmd: string; args: string[]; source: string } } | null;
237
290
  installCmd: string | null;
291
+ packageName: string | null;
238
292
  detectionDone: boolean;
293
+ onInstalled: () => void;
294
+ acpConfig: ReturnType<typeof useAcpConfig>;
239
295
  }) {
240
296
  const { t } = useLocale();
241
297
  const p = t.panels.agents;
298
+ const [configOpen, setConfigOpen] = useState(false);
299
+ const [editCmd, setEditCmd] = useState('');
300
+ const [editArgs, setEditArgs] = useState('');
301
+ const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle');
242
302
  const transportLabels: Record<string, string> = {
243
303
  npx: p.acpTransportNpx,
244
304
  binary: p.acpTransportBinary,
@@ -247,24 +307,61 @@ function AcpAgentRow({ agent, installed, installCmd, detectionDone }: {
247
307
  };
248
308
 
249
309
  const isReady = !!installed;
310
+ const resolved = installed?.resolvedCommand;
311
+ const sourceLabel = resolved?.source === 'user-override' ? p.acpConfigSourceUser
312
+ : resolved?.source === 'descriptor' ? p.acpConfigSourceBuiltin
313
+ : p.acpConfigSourceRegistry;
314
+
315
+ const handleToggleConfig = useCallback(() => {
316
+ if (!configOpen && resolved) {
317
+ // Pre-fill with current resolved values
318
+ setEditCmd(resolved.cmd);
319
+ setEditArgs(resolved.args.join(' '));
320
+ }
321
+ setConfigOpen(v => !v);
322
+ setSaveState('idle');
323
+ }, [configOpen, resolved]);
324
+
325
+ const handleSave = useCallback(async () => {
326
+ const args = editArgs.trim() ? editArgs.trim().split(/\s+/) : [];
327
+ const ok = await acpConfig.save(agent.id, {
328
+ command: editCmd.trim() || undefined,
329
+ args: args.length > 0 ? args : undefined,
330
+ });
331
+ if (ok) {
332
+ setSaveState('saved');
333
+ setTimeout(() => setSaveState('idle'), 2000);
334
+ }
335
+ }, [acpConfig, agent.id, editCmd, editArgs]);
336
+
337
+ const handleReset = useCallback(async () => {
338
+ await acpConfig.reset(agent.id);
339
+ setSaveState('idle');
340
+ setConfigOpen(false);
341
+ }, [acpConfig, agent.id]);
250
342
 
251
343
  const handleUse = () => {
252
- openAskModal(`Use ${agent.name} to help me with `);
253
- window.dispatchEvent(
254
- new CustomEvent('mindos:ask-with-agent', {
255
- detail: { agentId: agent.id, agentName: agent.name },
256
- }),
257
- );
344
+ openAskModal('', 'user', { id: agent.id, name: agent.name });
345
+ };
346
+
347
+ const handleQuickAction = (action: QuickAction) => {
348
+ openAskModal(action.promptSuffix, 'user', { id: agent.id, name: agent.name });
258
349
  };
259
350
 
260
351
  return (
261
- <div className="group rounded-xl border border-border bg-card p-3 hover:border-border/80 transition-all duration-150">
352
+ <div className="rounded-xl border border-[var(--amber)]/20 bg-card p-3 hover:border-[var(--amber)]/40 transition-all duration-150">
353
+ {/* Header row */}
262
354
  <div className="flex items-center gap-2.5">
263
- <div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
264
- <Network size={14} className="text-muted-foreground" />
355
+ <div className="w-8 h-8 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
356
+ <Network size={14} className="text-[var(--amber)]" />
265
357
  </div>
266
358
  <div className="flex-1 min-w-0">
267
- <p className="text-sm font-medium text-foreground truncate">{agent.name}</p>
359
+ <div className="flex items-center gap-1.5">
360
+ <p className="text-sm font-medium text-foreground truncate">{agent.name}</p>
361
+ {agent.version && (
362
+ <span className="text-2xs text-muted-foreground/60 shrink-0">v{agent.version}</span>
363
+ )}
364
+ </div>
268
365
  {agent.description && (
269
366
  <p className="text-2xs text-muted-foreground truncate">{agent.description}</p>
270
367
  )}
@@ -273,34 +370,176 @@ function AcpAgentRow({ agent, installed, installCmd, detectionDone }: {
273
370
  {transportLabels[agent.transport] ?? agent.transport}
274
371
  </span>
275
372
  {detectionDone && (
276
- <span className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 ${
277
- isReady
278
- ? 'bg-[var(--success)]/15 text-[var(--success)]'
279
- : 'bg-muted text-muted-foreground/60'
280
- }`}>
281
- {isReady ? p.acpReady : p.acpNotInstalled}
373
+ <span className="text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 bg-[var(--success)]/15 text-[var(--success)]">
374
+ {p.acpReady}
282
375
  </span>
283
376
  )}
284
377
  <button
285
378
  type="button"
286
- disabled={!isReady}
379
+ onClick={handleToggleConfig}
380
+ className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
381
+ title={p.acpConfigToggle}
382
+ aria-expanded={configOpen}
383
+ >
384
+ <Settings2 size={13} />
385
+ </button>
386
+ <button
387
+ type="button"
287
388
  onClick={handleUse}
288
- className={`inline-flex items-center gap-1 px-2 py-1 text-2xs font-medium rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
289
- isReady
290
- ? 'border-[var(--amber)] text-[var(--amber)] hover:bg-[var(--amber)]/10 cursor-pointer'
291
- : 'border-border text-muted-foreground/50 cursor-not-allowed'
292
- }`}
293
- title={
294
- isReady
295
- ? undefined
296
- : installCmd
297
- ? p.acpInstallHint(installCmd)
298
- : p.acpComingSoon
299
- }
389
+ className="inline-flex items-center gap-1 px-2.5 py-1 text-2xs font-medium rounded-md border border-[var(--amber)] text-[var(--amber)] hover:bg-[var(--amber)]/10 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer"
300
390
  >
391
+ <Zap size={10} />
301
392
  {p.acpUseAgent}
302
393
  </button>
303
394
  </div>
395
+
396
+ {/* Collapsible config section */}
397
+ {configOpen && resolved && (
398
+ <div className="mt-2.5 pt-2.5 border-t border-border/40 space-y-2">
399
+ {/* Command */}
400
+ <div className="flex items-center gap-2">
401
+ <label className="text-2xs text-muted-foreground w-12 shrink-0">{p.acpConfigCommand}</label>
402
+ <input
403
+ type="text"
404
+ value={editCmd}
405
+ onChange={e => { setEditCmd(e.target.value); setSaveState('idle'); }}
406
+ className="flex-1 rounded-md border border-border bg-background text-foreground text-xs font-mono px-2 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
407
+ placeholder={resolved.cmd}
408
+ />
409
+ </div>
410
+ {/* Args */}
411
+ <div className="flex items-center gap-2">
412
+ <label className="text-2xs text-muted-foreground w-12 shrink-0">{p.acpConfigArgs}</label>
413
+ <input
414
+ type="text"
415
+ value={editArgs}
416
+ onChange={e => { setEditArgs(e.target.value); setSaveState('idle'); }}
417
+ className="flex-1 rounded-md border border-border bg-background text-foreground text-xs font-mono px-2 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
418
+ placeholder={resolved.args.join(' ')}
419
+ />
420
+ </div>
421
+ {/* Status row */}
422
+ <div className="flex items-center gap-2 text-2xs text-muted-foreground">
423
+ <span>{p.acpConfigSource}:</span>
424
+ <span className={`px-1.5 py-0.5 rounded font-medium ${
425
+ resolved.source === 'user-override' ? 'bg-[var(--amber)]/15 text-[var(--amber)]'
426
+ : 'bg-muted text-muted-foreground'
427
+ }`}>{sourceLabel}</span>
428
+ <span className="text-muted-foreground/50">|</span>
429
+ <span>{p.acpConfigPath}: <span className="font-mono">{installed?.binaryPath}</span></span>
430
+ </div>
431
+ {/* Action buttons */}
432
+ <div className="flex items-center justify-end gap-2 pt-1">
433
+ {acpConfig.configs[agent.id] && (
434
+ <button
435
+ type="button"
436
+ onClick={handleReset}
437
+ className="inline-flex items-center gap-1 px-2 py-1 text-2xs font-medium rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
438
+ >
439
+ <RotateCcw size={10} />
440
+ {p.acpConfigReset}
441
+ </button>
442
+ )}
443
+ <button
444
+ type="button"
445
+ onClick={handleSave}
446
+ disabled={acpConfig.saving}
447
+ className="inline-flex items-center gap-1 px-2.5 py-1 text-2xs font-medium rounded-md bg-[var(--amber)] text-[var(--amber-foreground)] hover:bg-[var(--amber)]/90 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
448
+ >
449
+ {saveState === 'saved' ? <><Check size={10} /> {p.acpConfigSaved}</> : <><Save size={10} /> {p.acpConfigSave}</>}
450
+ </button>
451
+ </div>
452
+ </div>
453
+ )}
454
+
455
+ {/* Quick action chips */}
456
+ {isReady && !configOpen && (
457
+ <div className="mt-2.5 pt-2 border-t border-border/40 flex items-center gap-1.5 flex-wrap">
458
+ {QUICK_ACTIONS.map((action) => {
459
+ const Icon = action.icon;
460
+ return (
461
+ <button
462
+ key={action.labelKey}
463
+ type="button"
464
+ onClick={() => handleQuickAction(action)}
465
+ className="inline-flex items-center gap-1 px-2 py-0.5 text-2xs font-medium rounded-md bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground border border-border/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
466
+ >
467
+ <Icon size={10} />
468
+ {p[action.labelKey]}
469
+ </button>
470
+ );
471
+ })}
472
+ </div>
473
+ )}
474
+ </div>
475
+ );
476
+ }
477
+
478
+ /* ────────── ACP Agent Compact Row (Not Installed — subtle) ────────── */
479
+
480
+ function AcpAgentCompactRow({ agent, installCmd, packageName, detectionDone, onInstalled }: {
481
+ agent: AcpRegistryEntry;
482
+ installCmd: string | null;
483
+ packageName: string | null;
484
+ detectionDone: boolean;
485
+ onInstalled: () => void;
486
+ }) {
487
+ const { t } = useLocale();
488
+ const p = t.panels.agents;
489
+ const [installState, setInstallState] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
490
+
491
+ const handleInstall = async () => {
492
+ if (!packageName || installState === 'installing') return;
493
+ setInstallState('installing');
494
+ try {
495
+ const res = await fetch('/api/acp/install', {
496
+ method: 'POST',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ agentId: agent.id, packageName }),
499
+ });
500
+ if (!res.ok) {
501
+ setInstallState('error');
502
+ return;
503
+ }
504
+ // Wait a bit for npm install to complete, then re-detect
505
+ await new Promise((r) => setTimeout(r, 8000));
506
+ onInstalled();
507
+ setInstallState('done');
508
+ } catch {
509
+ setInstallState('error');
510
+ }
511
+ };
512
+
513
+ return (
514
+ <div className="group rounded-lg border border-border/60 bg-card/60 px-3 py-2 hover:border-border transition-all duration-150">
515
+ <div className="flex items-center gap-2">
516
+ <div className="w-6 h-6 rounded-md bg-muted/40 flex items-center justify-center shrink-0">
517
+ <Network size={11} className="text-muted-foreground/60" />
518
+ </div>
519
+ <div className="flex-1 min-w-0">
520
+ <p className="text-xs text-muted-foreground truncate">{agent.name}</p>
521
+ </div>
522
+ {detectionDone && (
523
+ <span className="text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 bg-muted text-muted-foreground/60">
524
+ {p.acpNotInstalled}
525
+ </span>
526
+ )}
527
+ {detectionDone && packageName && (
528
+ <button
529
+ type="button"
530
+ disabled={installState === 'installing'}
531
+ onClick={handleInstall}
532
+ className="inline-flex items-center gap-1 px-2 py-0.5 text-2xs font-medium rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
533
+ title={installCmd ? p.acpInstallHint(installCmd) : undefined}
534
+ >
535
+ {installState === 'installing' ? (
536
+ <><Loader2 size={10} className="animate-spin" /> {p.acpInstalling}</>
537
+ ) : (
538
+ <><Download size={10} /> {p.acpInstall}</>
539
+ )}
540
+ </button>
541
+ )}
542
+ </div>
304
543
  </div>
305
544
  );
306
545
  }
@@ -0,0 +1,166 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { History, Play, Trash2 } from 'lucide-react';
5
+ import type { AcpSession } from '@/lib/acp/types';
6
+
7
+ interface SessionEntry {
8
+ id: string;
9
+ agentId: string;
10
+ state: string;
11
+ cwd?: string;
12
+ createdAt: string;
13
+ lastActivityAt: string;
14
+ }
15
+
16
+ export default function AgentsPanelSessionsTab() {
17
+ const [sessions, setSessions] = useState<SessionEntry[]>([]);
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ const fetchSessions = useCallback(async () => {
22
+ try {
23
+ setLoading(true);
24
+ const res = await fetch('/api/acp/session');
25
+ if (!res.ok) throw new Error(`Failed to fetch sessions: ${res.status}`);
26
+ const data = await res.json();
27
+ setSessions(data.sessions ?? []);
28
+ setError(null);
29
+ } catch (err) {
30
+ setError((err as Error).message);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => { fetchSessions(); }, [fetchSessions]);
37
+
38
+ const handleClose = useCallback(async (sessionId: string) => {
39
+ try {
40
+ await fetch('/api/acp/session', {
41
+ method: 'DELETE',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ sessionId }),
44
+ });
45
+ setSessions(prev => prev.filter(s => s.id !== sessionId));
46
+ } catch (err) {
47
+ console.error('Failed to close session:', err);
48
+ }
49
+ }, []);
50
+
51
+ if (loading) {
52
+ return (
53
+ <div className="space-y-3 animate-pulse">
54
+ {Array.from({ length: 3 }).map((_, i) => (
55
+ <div key={i} className="rounded-lg border border-border bg-card p-4">
56
+ <div className="flex items-center gap-3">
57
+ <div className="w-8 h-8 rounded-full bg-muted" />
58
+ <div className="flex-1 space-y-2">
59
+ <div className="h-4 w-32 bg-muted rounded" />
60
+ <div className="h-3 w-48 bg-muted rounded" />
61
+ </div>
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (error) {
70
+ return (
71
+ <div className="rounded-lg border border-border bg-card p-6 text-center">
72
+ <p className="text-sm text-muted-foreground mb-3">{error}</p>
73
+ <button
74
+ onClick={fetchSessions}
75
+ className="text-sm text-[var(--amber)] hover:underline"
76
+ >
77
+ Retry
78
+ </button>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ if (sessions.length === 0) {
84
+ return (
85
+ <div className="rounded-lg border border-border bg-card p-8 text-center">
86
+ <History size={32} className="mx-auto mb-3 text-muted-foreground/40" />
87
+ <p className="text-sm font-medium text-foreground mb-1">No active sessions</p>
88
+ <p className="text-xs text-muted-foreground">
89
+ Sessions appear here when you chat with ACP agents.
90
+ <br />
91
+ Select an agent from the Network tab and send a message to start.
92
+ </p>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ return (
98
+ <div className="space-y-2">
99
+ <div className="flex items-center justify-between mb-3">
100
+ <p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">
101
+ Active Sessions ({sessions.length})
102
+ </p>
103
+ <button
104
+ onClick={fetchSessions}
105
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
106
+ >
107
+ Refresh
108
+ </button>
109
+ </div>
110
+
111
+ {sessions.map(session => (
112
+ <div
113
+ key={session.id}
114
+ className="group rounded-lg border border-border bg-card hover:border-[var(--amber)]/30 transition-colors p-3.5"
115
+ >
116
+ <div className="flex items-start justify-between gap-2">
117
+ <div className="flex-1 min-w-0">
118
+ <div className="flex items-center gap-2 mb-1">
119
+ <span className={`inline-block w-2 h-2 rounded-full ${
120
+ session.state === 'active' ? 'bg-[var(--success)]' :
121
+ session.state === 'error' ? 'bg-[var(--error)]' :
122
+ 'bg-muted-foreground/40'
123
+ }`} />
124
+ <span className="text-sm font-medium text-foreground truncate">
125
+ {session.agentId}
126
+ </span>
127
+ <span className="text-2xs text-muted-foreground/60 px-1.5 py-0.5 rounded bg-muted/40">
128
+ {session.state}
129
+ </span>
130
+ </div>
131
+ {session.cwd && (
132
+ <p className="text-xs text-muted-foreground truncate font-mono">
133
+ {session.cwd}
134
+ </p>
135
+ )}
136
+ <p className="text-2xs text-muted-foreground/60 mt-1">
137
+ {formatRelativeTime(session.lastActivityAt)}
138
+ </p>
139
+ </div>
140
+
141
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
142
+ <button
143
+ onClick={() => handleClose(session.id)}
144
+ className="p-1.5 rounded-md hover:bg-muted/60 text-muted-foreground hover:text-[var(--error)] transition-colors"
145
+ title="Close session"
146
+ >
147
+ <Trash2 size={14} />
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ ))}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function formatRelativeTime(isoString: string): string {
158
+ const now = Date.now();
159
+ const then = new Date(isoString).getTime();
160
+ const diff = now - then;
161
+
162
+ if (diff < 60_000) return 'just now';
163
+ if (diff < 3600_000) return `${Math.floor(diff / 60_000)}m ago`;
164
+ if (diff < 86400_000) return `${Math.floor(diff / 3600_000)}h ago`;
165
+ return `${Math.floor(diff / 86400_000)}d ago`;
166
+ }
@@ -1,6 +1,6 @@
1
1
  import type { AgentInfo, SkillInfo } from '@/components/settings/types';
2
2
 
3
- export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills' | 'a2a';
3
+ export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills' | 'a2a' | 'sessions';
4
4
  export type AgentResolvedStatus = 'connected' | 'detected' | 'notFound';
5
5
  export type SkillCapability = 'research' | 'coding' | 'docs' | 'ops' | 'memory';
6
6
  export type SkillSourceFilter = 'all' | 'builtin' | 'user';
@@ -24,7 +24,7 @@ export interface AgentBuckets {
24
24
  }
25
25
 
26
26
  export function parseAgentsTab(tab: string | undefined): AgentsDashboardTab {
27
- if (tab === 'mcp' || tab === 'skills' || tab === 'a2a') return tab;
27
+ if (tab === 'mcp' || tab === 'skills' || tab === 'a2a' || tab === 'sessions') return tab;
28
28
  return 'overview';
29
29
  }
30
30