@geminilight/mindos 0.6.28 → 0.6.30

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 (113) hide show
  1. package/README.md +10 -4
  2. package/app/app/api/a2a/agents/route.ts +9 -0
  3. package/app/app/api/a2a/delegations/route.ts +9 -0
  4. package/app/app/api/a2a/discover/route.ts +2 -0
  5. package/app/app/api/a2a/route.ts +6 -6
  6. package/app/app/api/acp/config/route.ts +82 -0
  7. package/app/app/api/acp/detect/route.ts +114 -0
  8. package/app/app/api/acp/install/route.ts +51 -0
  9. package/app/app/api/acp/registry/route.ts +31 -0
  10. package/app/app/api/acp/session/route.ts +185 -0
  11. package/app/app/api/ask/route.ts +116 -13
  12. package/app/app/api/workflows/route.ts +156 -0
  13. package/app/app/layout.tsx +2 -0
  14. package/app/app/page.tsx +7 -2
  15. package/app/components/ActivityBar.tsx +12 -4
  16. package/app/components/AskModal.tsx +4 -1
  17. package/app/components/DirView.tsx +64 -2
  18. package/app/components/FileTree.tsx +40 -10
  19. package/app/components/GuideCard.tsx +7 -17
  20. package/app/components/HomeContent.tsx +1 -0
  21. package/app/components/MarkdownView.tsx +2 -0
  22. package/app/components/Panel.tsx +1 -0
  23. package/app/components/RightAskPanel.tsx +5 -1
  24. package/app/components/SearchModal.tsx +234 -80
  25. package/app/components/SidebarLayout.tsx +6 -0
  26. package/app/components/agents/AgentDetailContent.tsx +266 -52
  27. package/app/components/agents/AgentsContentPage.tsx +32 -6
  28. package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
  29. package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
  30. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  31. package/app/components/agents/agents-content-model.ts +2 -2
  32. package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
  33. package/app/components/ask/AskContent.tsx +197 -239
  34. package/app/components/ask/FileChip.tsx +82 -17
  35. package/app/components/ask/MentionPopover.tsx +21 -3
  36. package/app/components/ask/MessageList.tsx +30 -9
  37. package/app/components/ask/SlashCommandPopover.tsx +21 -3
  38. package/app/components/help/HelpContent.tsx +9 -9
  39. package/app/components/panels/AgentsPanel.tsx +2 -0
  40. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  41. package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
  42. package/app/components/panels/EchoPanel.tsx +5 -1
  43. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  44. package/app/components/panels/WorkflowsPanel.tsx +206 -0
  45. package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
  46. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
  47. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
  48. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
  49. package/app/components/renderers/workflow-yaml/execution.ts +177 -0
  50. package/app/components/renderers/workflow-yaml/index.ts +6 -0
  51. package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
  52. package/app/components/renderers/workflow-yaml/parser.ts +172 -0
  53. package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
  54. package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
  55. package/app/components/renderers/workflow-yaml/types.ts +46 -0
  56. package/app/components/settings/KnowledgeTab.tsx +3 -6
  57. package/app/components/settings/McpSkillsSection.tsx +4 -5
  58. package/app/components/settings/McpTab.tsx +6 -8
  59. package/app/components/setup/StepSecurity.tsx +4 -5
  60. package/app/components/setup/index.tsx +5 -11
  61. package/app/components/ui/Toaster.tsx +39 -0
  62. package/app/hooks/useA2aRegistry.ts +6 -1
  63. package/app/hooks/useAcpConfig.ts +96 -0
  64. package/app/hooks/useAcpDetection.ts +120 -0
  65. package/app/hooks/useAcpRegistry.ts +86 -0
  66. package/app/hooks/useAskModal.ts +12 -5
  67. package/app/hooks/useAskPanel.ts +8 -5
  68. package/app/hooks/useAskSession.ts +19 -2
  69. package/app/hooks/useDelegationHistory.ts +49 -0
  70. package/app/hooks/useImageUpload.ts +152 -0
  71. package/app/lib/a2a/client.ts +49 -5
  72. package/app/lib/a2a/orchestrator.ts +0 -1
  73. package/app/lib/a2a/task-handler.ts +4 -4
  74. package/app/lib/a2a/types.ts +15 -0
  75. package/app/lib/acp/acp-tools.ts +95 -0
  76. package/app/lib/acp/agent-descriptors.ts +274 -0
  77. package/app/lib/acp/bridge.ts +144 -0
  78. package/app/lib/acp/index.ts +40 -0
  79. package/app/lib/acp/registry.ts +202 -0
  80. package/app/lib/acp/session.ts +717 -0
  81. package/app/lib/acp/subprocess.ts +495 -0
  82. package/app/lib/acp/types.ts +274 -0
  83. package/app/lib/agent/model.ts +18 -3
  84. package/app/lib/agent/to-agent-messages.ts +25 -2
  85. package/app/lib/agent/tools.ts +2 -1
  86. package/app/lib/i18n/_core.ts +22 -0
  87. package/app/lib/i18n/index.ts +35 -0
  88. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  89. package/app/lib/i18n/modules/common.ts +71 -0
  90. package/app/lib/i18n/modules/features.ts +153 -0
  91. package/app/lib/i18n/modules/knowledge.ts +429 -0
  92. package/app/lib/i18n/modules/navigation.ts +153 -0
  93. package/app/lib/i18n/modules/onboarding.ts +523 -0
  94. package/app/lib/i18n/modules/panels.ts +1196 -0
  95. package/app/lib/i18n/modules/settings.ts +585 -0
  96. package/app/lib/i18n-en.ts +2 -1518
  97. package/app/lib/i18n-zh.ts +2 -1542
  98. package/app/lib/i18n.ts +3 -6
  99. package/app/lib/pi-integration/skills.ts +21 -6
  100. package/app/lib/renderers/index.ts +2 -2
  101. package/app/lib/settings.ts +10 -0
  102. package/app/lib/toast.ts +79 -0
  103. package/app/lib/types.ts +12 -1
  104. package/app/next-env.d.ts +1 -1
  105. package/app/package.json +3 -1
  106. package/bin/cli.js +25 -25
  107. package/bin/commands/file.js +29 -2
  108. package/bin/commands/space.js +249 -91
  109. package/package.json +1 -1
  110. package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
  111. package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
  112. package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
  113. package/app/components/renderers/workflow/manifest.ts +0 -14
@@ -0,0 +1,56 @@
1
+ import YAML from 'js-yaml';
2
+ import type { WorkflowYaml } from './types';
3
+
4
+ /**
5
+ * Serialize a WorkflowYaml object to a YAML string.
6
+ * Strips undefined/empty optional fields to keep output clean.
7
+ */
8
+ export function serializeWorkflowYaml(workflow: WorkflowYaml): string {
9
+ const clean: Record<string, unknown> = {
10
+ title: workflow.title,
11
+ };
12
+ if (workflow.description) clean.description = workflow.description;
13
+ if (workflow.workDir) clean.workDir = workflow.workDir;
14
+ if (workflow.skills?.length) clean.skills = workflow.skills;
15
+ if (workflow.tools?.length) clean.tools = workflow.tools;
16
+
17
+ clean.steps = workflow.steps.map((step) => {
18
+ const s: Record<string, unknown> = {
19
+ id: step.id,
20
+ name: step.name,
21
+ };
22
+ if (step.description) s.description = step.description;
23
+ if (step.agent) s.agent = step.agent;
24
+ if (step.model) s.model = step.model;
25
+ if (step.skill) s.skill = step.skill;
26
+ if (step.skills?.length) s.skills = step.skills;
27
+ if (step.tools?.length) s.tools = step.tools;
28
+ if (step.context?.length) s.context = step.context;
29
+ s.prompt = step.prompt;
30
+ if (step.timeout) s.timeout = step.timeout;
31
+ return s;
32
+ });
33
+
34
+ return YAML.dump(clean, {
35
+ lineWidth: -1,
36
+ noRefs: true,
37
+ quotingType: '"',
38
+ forceQuotes: false,
39
+ });
40
+ }
41
+
42
+ /** Generate a URL-safe step ID from a name */
43
+ export function generateStepId(name: string, existingIds: string[]): string {
44
+ const base = name
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9]+/g, '-')
47
+ .replace(/^-|-$/g, '')
48
+ || 'step';
49
+ let id = base;
50
+ let i = 2;
51
+ while (existingIds.includes(id)) {
52
+ id = `${base}-${i}`;
53
+ i++;
54
+ }
55
+ return id;
56
+ }
@@ -0,0 +1,46 @@
1
+ // Types for YAML Workflow
2
+
3
+ export type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skipped';
4
+
5
+ export interface WorkflowStep {
6
+ id: string;
7
+ name: string;
8
+ description?: string;
9
+ agent?: string;
10
+ model?: string;
11
+ skill?: string;
12
+ skills?: string[];
13
+ tools?: string[];
14
+ context?: string[];
15
+ prompt: string;
16
+ timeout?: number;
17
+ }
18
+
19
+ export interface WorkflowYaml {
20
+ title: string;
21
+ description?: string;
22
+ workDir?: string;
23
+ skills?: string[];
24
+ tools?: string[];
25
+ steps: WorkflowStep[];
26
+ }
27
+
28
+ export interface WorkflowStepRuntime extends WorkflowStep {
29
+ index: number;
30
+ status: StepStatus;
31
+ output: string;
32
+ error?: string;
33
+ startedAt?: Date;
34
+ completedAt?: Date;
35
+ durationMs?: number;
36
+ }
37
+
38
+ export interface ParseResult {
39
+ workflow: WorkflowYaml | null;
40
+ errors: string[];
41
+ }
42
+
43
+ export interface ValidationResult {
44
+ valid: boolean;
45
+ errors: string[];
46
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
4
4
  import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive, RotateCcw } from 'lucide-react';
5
+ import { toast } from '@/lib/toast';
5
6
  import type { KnowledgeTabProps } from './types';
6
7
  import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
7
8
  import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
@@ -96,7 +97,6 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
96
97
  const [showPassword, setShowPassword] = useState(false);
97
98
  const isPasswordMasked = data.webPassword === '***set***';
98
99
 
99
- const [copied, setCopied] = useState(false);
100
100
  const [resetting, setResetting] = useState(false);
101
101
  // revealed holds the plaintext token after regenerate, until user navigates away
102
102
  const [revealedToken, setRevealedToken] = useState<string | null>(null);
@@ -130,10 +130,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
130
130
  const text = revealedToken ?? data.authToken ?? '';
131
131
  if (!text) return;
132
132
  copyToClipboard(text).then((ok) => {
133
- if (ok) {
134
- setCopied(true);
135
- setTimeout(() => setCopied(false), 2000);
136
- }
133
+ if (ok) toast.copy();
137
134
  });
138
135
  }
139
136
 
@@ -229,7 +226,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
229
226
  className="shrink-0 p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
230
227
  title={k.authTokenCopy}
231
228
  >
232
- {copied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
229
+ <Copy size={13} />
233
230
  </button>
234
231
  )}
235
232
  </div>
@@ -3,8 +3,9 @@
3
3
  import { useState, useEffect, useCallback, useMemo } from 'react';
4
4
  import {
5
5
  Loader2, ChevronDown, ChevronRight,
6
- Plus, X, Search, Copy, Check,
6
+ Plus, X, Search, Copy,
7
7
  } from 'lucide-react';
8
+ import { toast } from '@/lib/toast';
8
9
  import { apiFetch } from '@/lib/api';
9
10
  import { useMcpDataOptional } from '@/hooks/useMcpData';
10
11
  import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
@@ -398,14 +399,12 @@ function SkillCliHint({ agents, skillName, m }: {
398
399
  m: Record<string, any> | undefined;
399
400
  }) {
400
401
  const [selectedAgent, setSelectedAgent] = useState('claude-code');
401
- const [copied, setCopied] = useState(false);
402
-
403
402
  const cmd = `npx skills add GeminiLight/MindOS --skill ${skillName} -a ${selectedAgent} -g -y`;
404
403
  const skillPath = `~/.agents/skills/${skillName}/SKILL.md`;
405
404
 
406
405
  const handleCopy = async () => {
407
406
  const ok = await copyToClipboard(cmd);
408
- if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); }
407
+ if (ok) toast.copy();
409
408
  };
410
409
 
411
410
  // Group agents: connected first, then detected, then not found
@@ -447,7 +446,7 @@ function SkillCliHint({ agents, skillName, m }: {
447
446
  </code>
448
447
  <button onClick={handleCopy}
449
448
  className="p-1.5 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
450
- {copied ? <Check size={11} /> : <Copy size={11} />}
449
+ <Copy size={11} />
451
450
  </button>
452
451
  </div>
453
452
 
@@ -1,5 +1,6 @@
1
1
  import { useState, useMemo, useRef, useEffect } from 'react';
2
- import { Loader2, Copy, Check, Monitor, Globe, AlertCircle, RotateCcw, RefreshCw } from 'lucide-react';
2
+ import { Loader2, Copy, Monitor, Globe, AlertCircle, RotateCcw, RefreshCw } from 'lucide-react';
3
+ import { toast } from '@/lib/toast';
3
4
  import { useMcpDataOptional } from '@/hooks/useMcpData';
4
5
  import { generateSnippet } from '@/lib/mcp-snippets';
5
6
  import { copyToClipboard } from '@/lib/clipboard';
@@ -22,7 +23,6 @@ export function McpTab({ t }: McpTabProps) {
22
23
  const [restarting, setRestarting] = useState(false);
23
24
  const [selectedAgent, setSelectedAgent] = useState('');
24
25
  const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
25
- const [copied, setCopied] = useState(false);
26
26
  const restartPollRef = useRef<ReturnType<typeof setInterval>>(undefined);
27
27
 
28
28
  // Cleanup restart poll on unmount
@@ -99,10 +99,9 @@ export function McpTab({ t }: McpTabProps) {
99
99
  onSelectAgent={(key) => setSelectedAgent(key)}
100
100
  transport={transport}
101
101
  onTransportChange={setTransport}
102
- copied={copied}
103
102
  onCopy={async (snippet) => {
104
103
  const ok = await copyToClipboard(snippet);
105
- if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); }
104
+ if (ok) toast.copy();
106
105
  }}
107
106
  m={m}
108
107
  />
@@ -177,7 +176,7 @@ function McpStatusCard({ status, restarting, onRestart, onRefresh, m }: {
177
176
 
178
177
  /* ── Agent Config Viewer (dropdown + snippet) ── */
179
178
 
180
- function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange, copied, onCopy, m }: {
179
+ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, currentAgent, mcpStatus, selectedAgent, onSelectAgent, transport, onTransportChange, onCopy, m }: {
181
180
  connectedAgents: AgentInfo[];
182
181
  detectedAgents: AgentInfo[];
183
182
  notFoundAgents: AgentInfo[];
@@ -187,7 +186,6 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
187
186
  onSelectAgent: (key: string) => void;
188
187
  transport: 'stdio' | 'http';
189
188
  onTransportChange: (t: 'stdio' | 'http') => void;
190
- copied: boolean;
191
189
  onCopy: (snippet: string) => void;
192
190
  m: Record<string, any> | undefined;
193
191
  }) {
@@ -292,8 +290,8 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
292
290
  <div className="flex items-center gap-3 text-xs">
293
291
  <button onClick={() => onCopy(snippet.snippet)}
294
292
  className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0">
295
- {copied ? <Check size={12} /> : <Copy size={12} />}
296
- {copied ? (m?.copied ?? 'Copied!') : (m?.copyConfig ?? 'Copy config')}
293
+ <Copy size={12} />
294
+ {m?.copyConfig ?? 'Copy config'}
297
295
  </button>
298
296
  <span className="text-muted-foreground">→</span>
299
297
  <span className="font-mono text-muted-foreground truncate text-2xs">{snippet.path}</span>
@@ -1,13 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { Copy, Check, RefreshCw } from 'lucide-react';
4
+ import { Copy, RefreshCw } from 'lucide-react';
5
5
  import { Field, Input } from '@/components/settings/Primitives';
6
6
  import type { SetupMessages } from './types';
7
7
 
8
8
  export interface StepSecurityProps {
9
9
  authToken: string;
10
- tokenCopied: boolean;
11
10
  onCopy: () => void;
12
11
  onGenerate: (seed?: string) => void;
13
12
  webPassword: string;
@@ -16,7 +15,7 @@ export interface StepSecurityProps {
16
15
  }
17
16
 
18
17
  export default function StepSecurity({
19
- authToken, tokenCopied, onCopy, onGenerate, webPassword, onPasswordChange, s,
18
+ authToken, onCopy, onGenerate, webPassword, onPasswordChange, s,
20
19
  }: StepSecurityProps) {
21
20
  const [seed, setSeed] = useState('');
22
21
  const [showSeed, setShowSeed] = useState(false);
@@ -29,8 +28,8 @@ export default function StepSecurity({
29
28
  <button onClick={onCopy}
30
29
  className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
31
30
  style={{ color: 'var(--foreground)' }}>
32
- {tokenCopied ? <Check size={14} /> : <Copy size={14} />}
33
- {tokenCopied ? s.copiedToken : s.copyToken}
31
+ <Copy size={14} />
32
+ {s.copyToken}
34
33
  </button>
35
34
  <button onClick={() => onGenerate()}
36
35
  aria-label={s.generateToken}
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
4
4
  import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
6
  import { copyToClipboard } from '@/lib/clipboard';
7
+ import { toast } from '@/lib/toast';
7
8
  import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
8
9
  import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
9
10
  import StepKB from './StepKB';
@@ -127,7 +128,6 @@ export default function SetupWizard() {
127
128
  webPassword: '',
128
129
  });
129
130
  const [homeDir, setHomeDir] = useState('~');
130
- const [tokenCopied, setTokenCopied] = useState(false);
131
131
  const [submitting, setSubmitting] = useState(false);
132
132
  const [completed, setCompleted] = useState(false);
133
133
  const [error, setError] = useState('');
@@ -230,15 +230,9 @@ export default function SetupWizard() {
230
230
  }, []);
231
231
 
232
232
  const copyToken = useCallback(() => {
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
- });
233
+ copyToClipboard(state.authToken).then((ok) => {
234
+ if (ok) toast.copy();
235
+ });
242
236
  }, [state.authToken]);
243
237
 
244
238
  const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
@@ -384,7 +378,7 @@ export default function SetupWizard() {
384
378
  )}
385
379
  {step === 3 && (
386
380
  <StepSecurity
387
- authToken={state.authToken} tokenCopied={tokenCopied}
381
+ authToken={state.authToken}
388
382
  onCopy={copyToken} onGenerate={generateToken}
389
383
  webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
390
384
  s={s}
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+ import { Check, AlertCircle, Info, X } from 'lucide-react';
5
+ import { subscribe, getSnapshot, dismiss, type Toast } from '@/lib/toast';
6
+
7
+ const icons: Record<Toast['type'], React.ReactNode> = {
8
+ success: <Check size={15} className="text-success shrink-0" />,
9
+ error: <AlertCircle size={15} className="text-error shrink-0" />,
10
+ info: <Info size={15} className="text-muted-foreground shrink-0" />,
11
+ };
12
+
13
+ export default function Toaster() {
14
+ const toasts = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
15
+
16
+ if (toasts.length === 0) return null;
17
+
18
+ return (
19
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col-reverse gap-2 pointer-events-none" aria-live="polite">
20
+ {toasts.map((t) => (
21
+ <div
22
+ key={t.id}
23
+ className="pointer-events-auto flex items-center gap-2.5 bg-card border border-border rounded-lg shadow-lg px-4 py-2.5 min-w-[180px] max-w-[320px] animate-in slide-in-from-right-4 fade-in duration-200"
24
+ >
25
+ {icons[t.type]}
26
+ <span className="text-sm text-foreground flex-1 truncate">{t.message}</span>
27
+ <button
28
+ type="button"
29
+ onClick={() => dismiss(t.id)}
30
+ className="shrink-0 p-0.5 rounded text-muted-foreground hover:text-foreground transition-colors"
31
+ aria-label="Dismiss"
32
+ >
33
+ <X size={13} />
34
+ </button>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
@@ -8,6 +8,7 @@ interface A2aRegistry {
8
8
  discovering: boolean;
9
9
  error: string | null;
10
10
  discover: (url: string) => Promise<RemoteAgent | null>;
11
+ remove: (id: string) => void;
11
12
  refresh: () => void;
12
13
  }
13
14
 
@@ -44,10 +45,14 @@ export function useA2aRegistry(): A2aRegistry {
44
45
  }
45
46
  }, []);
46
47
 
48
+ const remove = useCallback((id: string) => {
49
+ setAgents(prev => prev.filter(a => a.id !== id));
50
+ }, []);
51
+
47
52
  const refresh = useCallback(() => {
48
53
  setAgents([]);
49
54
  setError(null);
50
55
  }, []);
51
56
 
52
- return { agents, discovering, error, discover, refresh };
57
+ return { agents, discovering, error, discover, remove, refresh };
53
58
  }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import type { AcpAgentOverride } from '@/lib/acp/agent-descriptors';
5
+
6
+ interface AcpConfigState {
7
+ configs: Record<string, AcpAgentOverride>;
8
+ loading: boolean;
9
+ saving: boolean;
10
+ error: string | null;
11
+ /** Save a per-agent override. */
12
+ save: (agentId: string, config: AcpAgentOverride) => Promise<boolean>;
13
+ /** Reset a single agent to defaults. */
14
+ reset: (agentId: string) => Promise<boolean>;
15
+ /** Refresh from server. */
16
+ refresh: () => void;
17
+ }
18
+
19
+ export function useAcpConfig(): AcpConfigState {
20
+ const [configs, setConfigs] = useState<Record<string, AcpAgentOverride>>({});
21
+ const [loading, setLoading] = useState(true);
22
+ const [saving, setSaving] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const [trigger, setTrigger] = useState(0);
25
+
26
+ const refresh = useCallback(() => setTrigger((n) => n + 1), []);
27
+
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ setLoading(true);
31
+ setError(null);
32
+
33
+ fetch('/api/acp/config')
34
+ .then((res) => {
35
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
36
+ return res.json();
37
+ })
38
+ .then((data) => {
39
+ if (cancelled) return;
40
+ setConfigs(data.agents ?? {});
41
+ })
42
+ .catch((err) => {
43
+ if (cancelled) return;
44
+ setError((err as Error).message);
45
+ })
46
+ .finally(() => {
47
+ if (!cancelled) setLoading(false);
48
+ });
49
+
50
+ return () => { cancelled = true; };
51
+ }, [trigger]);
52
+
53
+ const save = useCallback(async (agentId: string, config: AcpAgentOverride): Promise<boolean> => {
54
+ setSaving(true);
55
+ setError(null);
56
+ try {
57
+ const res = await fetch('/api/acp/config', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ agentId, config }),
61
+ });
62
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
63
+ const data = await res.json();
64
+ setConfigs(data.agents ?? {});
65
+ return true;
66
+ } catch (err) {
67
+ setError((err as Error).message);
68
+ return false;
69
+ } finally {
70
+ setSaving(false);
71
+ }
72
+ }, []);
73
+
74
+ const reset = useCallback(async (agentId: string): Promise<boolean> => {
75
+ setSaving(true);
76
+ setError(null);
77
+ try {
78
+ const res = await fetch('/api/acp/config', {
79
+ method: 'DELETE',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ agentId }),
82
+ });
83
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
84
+ const data = await res.json();
85
+ setConfigs(data.agents ?? {});
86
+ return true;
87
+ } catch (err) {
88
+ setError((err as Error).message);
89
+ return false;
90
+ } finally {
91
+ setSaving(false);
92
+ }
93
+ }, []);
94
+
95
+ return { configs, loading, saving, error, save, reset, refresh };
96
+ }
@@ -0,0 +1,120 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+
5
+ export interface DetectedAgent {
6
+ id: string;
7
+ name: string;
8
+ binaryPath: string;
9
+ resolvedCommand?: {
10
+ cmd: string;
11
+ args: string[];
12
+ source: 'user-override' | 'descriptor' | 'registry';
13
+ };
14
+ }
15
+
16
+ export interface NotInstalledAgent {
17
+ id: string;
18
+ name: string;
19
+ installCmd: string;
20
+ packageName?: string;
21
+ }
22
+
23
+ interface AcpDetectionState {
24
+ installedAgents: DetectedAgent[];
25
+ notInstalledAgents: NotInstalledAgent[];
26
+ loading: boolean;
27
+ error: string | null;
28
+ refresh: () => void;
29
+ }
30
+
31
+ const STORAGE_KEY = 'mindos:acp-detection';
32
+ const STALE_TTL_MS = 30 * 60 * 1000;
33
+ const REVALIDATE_TTL_MS = 5 * 60 * 1000;
34
+
35
+ interface DetectionCache {
36
+ installed: DetectedAgent[];
37
+ notInstalled: NotInstalledAgent[];
38
+ ts: number;
39
+ }
40
+
41
+ function readStorage(): DetectionCache | null {
42
+ try {
43
+ const raw = sessionStorage.getItem(STORAGE_KEY);
44
+ if (!raw) return null;
45
+ const parsed = JSON.parse(raw);
46
+ if (typeof parsed.ts !== 'number' || Date.now() - parsed.ts > STALE_TTL_MS) return null;
47
+ return parsed;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function writeStorage(installed: DetectedAgent[], notInstalled: NotInstalledAgent[]) {
54
+ try {
55
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ installed, notInstalled, ts: Date.now() }));
56
+ } catch { /* quota exceeded */ }
57
+ }
58
+
59
+ export function useAcpDetection(): AcpDetectionState {
60
+ const cached = useRef(readStorage());
61
+ const [installedAgents, setInstalledAgents] = useState<DetectedAgent[]>(cached.current?.installed ?? []);
62
+ const [notInstalledAgents, setNotInstalledAgents] = useState<NotInstalledAgent[]>(cached.current?.notInstalled ?? []);
63
+ const [loading, setLoading] = useState(!cached.current);
64
+ const [error, setError] = useState<string | null>(null);
65
+ const [trigger, setTrigger] = useState(0);
66
+ const inflight = useRef(false);
67
+
68
+ const forceRef = useRef(false);
69
+
70
+ const refresh = useCallback(() => {
71
+ try { sessionStorage.removeItem(STORAGE_KEY); } catch { /* ignore */ }
72
+ cached.current = null;
73
+ forceRef.current = true;
74
+ setTrigger((n) => n + 1);
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ const isForce = forceRef.current;
79
+ forceRef.current = false;
80
+
81
+ const fresh = cached.current && Date.now() - cached.current.ts < REVALIDATE_TTL_MS;
82
+ if (fresh && trigger === 0) return;
83
+
84
+ if (inflight.current) return;
85
+ inflight.current = true;
86
+
87
+ const hasCachedData = installedAgents.length > 0 || notInstalledAgents.length > 0;
88
+ if (!hasCachedData) setLoading(true);
89
+ setError(null);
90
+
91
+ let cancelled = false;
92
+
93
+ fetch(`/api/acp/detect${isForce ? '?force=1' : ''}`)
94
+ .then((res) => {
95
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
96
+ return res.json();
97
+ })
98
+ .then((data) => {
99
+ if (cancelled) return;
100
+ const inst: DetectedAgent[] = data.installed ?? [];
101
+ const notInst: NotInstalledAgent[] = data.notInstalled ?? [];
102
+ writeStorage(inst, notInst);
103
+ cached.current = { installed: inst, notInstalled: notInst, ts: Date.now() };
104
+ setInstalledAgents(inst);
105
+ setNotInstalledAgents(notInst);
106
+ })
107
+ .catch((err) => {
108
+ if (cancelled) return;
109
+ if (!hasCachedData) setError((err as Error).message);
110
+ })
111
+ .finally(() => {
112
+ inflight.current = false;
113
+ if (!cancelled) setLoading(false);
114
+ });
115
+
116
+ return () => { cancelled = true; };
117
+ }, [trigger]); // eslint-disable-line react-hooks/exhaustive-deps
118
+
119
+ return { installedAgents, notInstalledAgents, loading, error, refresh };
120
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import type { AcpRegistryEntry } from '@/lib/acp/types';
5
+
6
+ interface AcpRegistryState {
7
+ agents: AcpRegistryEntry[];
8
+ loading: boolean;
9
+ error: string | null;
10
+ retry: () => void;
11
+ }
12
+
13
+ const STORAGE_KEY = 'mindos:acp-registry';
14
+ const STALE_TTL_MS = 30 * 60 * 1000; // 30 min — show stale data instantly
15
+ const REVALIDATE_TTL_MS = 10 * 60 * 1000; // 10 min — background refresh interval
16
+
17
+ function readStorage(): { agents: AcpRegistryEntry[]; ts: number } | null {
18
+ try {
19
+ const raw = sessionStorage.getItem(STORAGE_KEY);
20
+ if (!raw) return null;
21
+ const parsed = JSON.parse(raw);
22
+ if (!Array.isArray(parsed.agents) || typeof parsed.ts !== 'number') return null;
23
+ if (Date.now() - parsed.ts > STALE_TTL_MS) return null;
24
+ return parsed;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function writeStorage(agents: AcpRegistryEntry[]) {
31
+ try {
32
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ agents, ts: Date.now() }));
33
+ } catch { /* quota exceeded — ignore */ }
34
+ }
35
+
36
+ export function useAcpRegistry(): AcpRegistryState {
37
+ const cached = useRef(readStorage());
38
+ const [agents, setAgents] = useState<AcpRegistryEntry[]>(cached.current?.agents ?? []);
39
+ const [loading, setLoading] = useState(!cached.current);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [trigger, setTrigger] = useState(0);
42
+ const inflight = useRef(false);
43
+
44
+ const retry = useCallback(() => {
45
+ setTrigger((n) => n + 1);
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ const fresh = cached.current && Date.now() - cached.current.ts < REVALIDATE_TTL_MS;
50
+ if (fresh && trigger === 0) return;
51
+
52
+ if (inflight.current) return;
53
+ inflight.current = true;
54
+
55
+ const hasCachedData = agents.length > 0;
56
+ if (!hasCachedData) setLoading(true);
57
+ setError(null);
58
+
59
+ let cancelled = false;
60
+
61
+ fetch('/api/acp/registry')
62
+ .then((res) => {
63
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
64
+ return res.json();
65
+ })
66
+ .then((data) => {
67
+ if (cancelled) return;
68
+ const list: AcpRegistryEntry[] = data.registry?.agents ?? [];
69
+ writeStorage(list);
70
+ cached.current = { agents: list, ts: Date.now() };
71
+ setAgents(list);
72
+ })
73
+ .catch((err) => {
74
+ if (cancelled) return;
75
+ if (!hasCachedData) setError((err as Error).message);
76
+ })
77
+ .finally(() => {
78
+ inflight.current = false;
79
+ if (!cancelled) setLoading(false);
80
+ });
81
+
82
+ return () => { cancelled = true; };
83
+ }, [trigger]); // eslint-disable-line react-hooks/exhaustive-deps
84
+
85
+ return { agents, loading, error, retry };
86
+ }