@geminilight/mindos 0.5.20 → 0.5.21

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,172 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import { Plug, Copy, Check, ChevronDown } from 'lucide-react';
5
+ import type { McpStatus, AgentInfo, McpServerStatusProps } from './types';
6
+
7
+ /* ── Helpers ───────────────────────────────────────────────────── */
8
+
9
+ function CopyButton({ text, label, copiedLabel }: { text: string; label: string; copiedLabel?: string }) {
10
+ const [copied, setCopied] = useState(false);
11
+ const handleCopy = async () => {
12
+ try {
13
+ await navigator.clipboard.writeText(text);
14
+ setCopied(true);
15
+ setTimeout(() => setCopied(false), 2000);
16
+ } catch { /* clipboard unavailable */ }
17
+ };
18
+ return (
19
+ <button
20
+ onClick={handleCopy}
21
+ className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
22
+ >
23
+ {copied ? <Check size={11} /> : <Copy size={11} />}
24
+ {copied ? (copiedLabel ?? 'Copied!') : label}
25
+ </button>
26
+ );
27
+ }
28
+
29
+ /* ── Config Snippet Generator ─────────────────────────────────── */
30
+
31
+ function generateConfigSnippet(
32
+ agent: AgentInfo,
33
+ status: McpStatus,
34
+ token?: string,
35
+ ): { snippet: string; path: string } {
36
+ const isRunning = status.running;
37
+
38
+ // Determine entry (stdio vs http)
39
+ const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
40
+ const httpEntry: Record<string, unknown> = { url: status.endpoint };
41
+ if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
42
+ const entry = isRunning ? httpEntry : stdioEntry;
43
+
44
+ // TOML format (Codex)
45
+ if (agent.format === 'toml') {
46
+ const lines: string[] = [`[${agent.configKey}.mindos]`];
47
+ if (isRunning) {
48
+ lines.push(`type = "http"`);
49
+ lines.push(`url = "${status.endpoint}"`);
50
+ if (token) {
51
+ lines.push('');
52
+ lines.push(`[${agent.configKey}.mindos.headers]`);
53
+ lines.push(`Authorization = "Bearer ${token}"`);
54
+ }
55
+ } else {
56
+ lines.push(`command = "mindos"`);
57
+ lines.push(`args = ["mcp"]`);
58
+ lines.push('');
59
+ lines.push(`[${agent.configKey}.mindos.env]`);
60
+ lines.push(`MCP_TRANSPORT = "stdio"`);
61
+ }
62
+ return { snippet: lines.join('\n'), path: agent.globalPath };
63
+ }
64
+
65
+ // JSON with globalNestedKey (VS Code project-level uses flat key)
66
+ if (agent.globalNestedKey) {
67
+ // project-level: flat key structure
68
+ const projectSnippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
69
+ return { snippet: projectSnippet, path: agent.projectPath ?? agent.globalPath };
70
+ }
71
+
72
+ // Standard JSON
73
+ const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
74
+ return { snippet, path: agent.globalPath };
75
+ }
76
+
77
+ /* ── MCP Server Status ─────────────────────────────────────────── */
78
+
79
+ export default function ServerStatus({ status, agents, t }: McpServerStatusProps) {
80
+ const m = t.settings?.mcp;
81
+ const [expanded, setExpanded] = useState(false);
82
+ const [selectedAgent, setSelectedAgent] = useState<string>('');
83
+
84
+ // Auto-select first installed or first detected agent
85
+ useEffect(() => {
86
+ if (agents.length > 0 && !selectedAgent) {
87
+ const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
88
+ if (first) setSelectedAgent(first.key);
89
+ }
90
+ }, [agents, selectedAgent]);
91
+
92
+ if (!status) return null;
93
+
94
+ const currentAgent = agents.find(a => a.key === selectedAgent);
95
+ // 🟡 MINOR #9: Memoize snippet generation to avoid recomputing on every render
96
+ const snippetResult = useMemo(() => currentAgent ? generateConfigSnippet(currentAgent, status) : null, [currentAgent, status]);
97
+
98
+ return (
99
+ <div>
100
+ {/* Summary line — always visible */}
101
+ <button
102
+ type="button"
103
+ onClick={() => setExpanded(!expanded)}
104
+ className="w-full flex items-center gap-2.5 text-xs"
105
+ >
106
+ <Plug size={14} className="text-muted-foreground shrink-0" />
107
+ <span className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
108
+ <span className="text-foreground font-medium">
109
+ {status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
110
+ </span>
111
+ {status.running && (
112
+ <>
113
+ <span className="text-muted-foreground">·</span>
114
+ <span className="font-mono text-muted-foreground">{status.transport.toUpperCase()}</span>
115
+ <span className="text-muted-foreground">·</span>
116
+ <span className="text-muted-foreground">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} tools`}</span>
117
+ <span className="text-muted-foreground">·</span>
118
+ <span className={status.authConfigured ? 'text-success' : 'text-muted-foreground'}>
119
+ {status.authConfigured ? (m?.authSet ?? 'Token set') : (m?.authNotSet ?? 'No token')}
120
+ </span>
121
+ </>
122
+ )}
123
+ <ChevronDown size={12} className={`ml-auto text-muted-foreground transition-transform shrink-0 ${expanded ? 'rotate-180' : ''}`} />
124
+ </button>
125
+
126
+ {/* Expanded details */}
127
+ {expanded && (
128
+ <div className="pt-3 mt-3 border-t border-border space-y-3">
129
+ {/* Endpoint + copy */}
130
+ <div className="flex items-center gap-2 text-xs">
131
+ <span className="text-muted-foreground shrink-0">{m?.endpoint ?? 'Endpoint'}</span>
132
+ <span className="font-mono text-foreground truncate">{status.endpoint}</span>
133
+ <CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy'} copiedLabel={m?.copied} />
134
+ </div>
135
+
136
+ {/* Quick Setup */}
137
+ {agents.length > 0 && (
138
+ <div className="space-y-2.5">
139
+ <div className="flex items-center gap-2 text-xs">
140
+ <span className="text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
141
+ <select
142
+ value={selectedAgent}
143
+ onChange={e => setSelectedAgent(e.target.value)}
144
+ className="text-xs px-2 py-1 rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
145
+ >
146
+ {agents.map(a => (
147
+ <option key={a.key} value={a.key}>
148
+ {a.name}{a.installed ? ' ✓' : a.present ? ' ·' : ''}
149
+ </option>
150
+ ))}
151
+ </select>
152
+ </div>
153
+
154
+ {snippetResult && (
155
+ <>
156
+ <div className="flex items-center gap-2 text-xs">
157
+ <span className="text-muted-foreground shrink-0">{m?.configPath ?? 'Config path'}</span>
158
+ <span className="font-mono text-foreground text-2xs">{snippetResult.path}</span>
159
+ </div>
160
+ <pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre">
161
+ {snippetResult.snippet}
162
+ </pre>
163
+ <CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} copiedLabel={m?.copied} />
164
+ </>
165
+ )}
166
+ </div>
167
+ )}
168
+ </div>
169
+ )}
170
+ </div>
171
+ );
172
+ }