Skip to content

Commit b4598e6

Browse files
authored
fix: stdio enable error UI state (#104)
Signed-off-by: Myko <myko@mcpmux.com> Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 9b60e0b commit b4598e6

10 files changed

Lines changed: 410 additions & 57 deletions

File tree

apps/desktop/src-tauri/src/commands/server_manager.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ pub async fn enable_server_v2(
137137
Some(app_state.data_dir()),
138138
);
139139

140-
// Attempt connection (manual connect from user clicking Connect button)
141-
let ctx = ConnectionContext::new(space_uuid, server_id.clone(), transport);
140+
// Attempt connection with auto_reconnect=true to avoid starting OAuth flow
141+
// If OAuth is needed, we just set AuthRequired and let user click Connect
142+
let ctx = ConnectionContext::auto(space_uuid, server_id.clone(), transport);
142143
let result = pool_service.connect_server(&ctx).await;
143144

144145
match result {
@@ -148,13 +149,9 @@ pub async fn enable_server_v2(
148149
Ok(())
149150
}
150151
ConnectionResult::OAuthRequired { .. } => {
151-
// OAuth is needed - set state to AuthRequired (NOT Authenticating)
152-
// Don't open browser yet - wait for user to click Connect
153-
// Cancel the OAuth flow that was started during connection probe
154-
pool_service
155-
.oauth_manager()
156-
.cancel_flow_for_space(space_uuid, &server_id);
157-
152+
// OAuth is needed - set state to AuthRequired
153+
// auto_reconnect=true prevented OAuth flow from starting, so no cancel needed
154+
// User will click "Connect" to start the actual OAuth flow
158155
manager.set_auth_required(&key, None).await;
159156

160157
// Mark features unavailable - not connected
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
import { X, Copy, Check, Loader2 } from 'lucide-react';
3+
import Editor from '@monaco-editor/react';
4+
import type { ServerViewModel, ServerDefinition } from '../types/registry';
5+
6+
interface ServerDefinitionModalProps {
7+
server: ServerViewModel;
8+
onClose: () => void;
9+
}
10+
11+
/** Extract only ServerDefinition fields, stripping runtime state */
12+
function extractDefinition(server: ServerViewModel): ServerDefinition {
13+
const {
14+
is_installed: _a,
15+
enabled: _b,
16+
oauth_connected: _c,
17+
input_values: _d,
18+
connection_status: _e,
19+
missing_required_inputs: _f,
20+
last_error: _g,
21+
created_at: _h,
22+
installation_source: _i,
23+
env_overrides: _j,
24+
args_append: _k,
25+
extra_headers: _l,
26+
...definition
27+
} = server;
28+
return definition;
29+
}
30+
31+
export function ServerDefinitionModal({ server, onClose }: ServerDefinitionModalProps) {
32+
const [copied, setCopied] = useState(false);
33+
const [editorReady, setEditorReady] = useState(false);
34+
35+
const definition = extractDefinition(server);
36+
const json = JSON.stringify(definition, null, 2);
37+
38+
useEffect(() => {
39+
const timer = setTimeout(() => setEditorReady(true), 100);
40+
return () => clearTimeout(timer);
41+
}, []);
42+
43+
useEffect(() => {
44+
const handleKeyDown = (e: KeyboardEvent) => {
45+
if (e.key === 'Escape') {
46+
onClose();
47+
}
48+
};
49+
window.addEventListener('keydown', handleKeyDown);
50+
return () => window.removeEventListener('keydown', handleKeyDown);
51+
}, [onClose]);
52+
53+
const handleCopy = useCallback(async () => {
54+
try {
55+
await navigator.clipboard.writeText(json);
56+
setCopied(true);
57+
setTimeout(() => setCopied(false), 2000);
58+
} catch {
59+
// Fallback for environments where clipboard API is unavailable
60+
}
61+
}, [json]);
62+
63+
return (
64+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
65+
<div className="bg-[rgb(var(--surface))] w-full max-w-3xl h-[70vh] rounded-xl shadow-2xl flex flex-col border border-[rgb(var(--border))] animate-in fade-in scale-in duration-150">
66+
{/* Header */}
67+
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border))]">
68+
<div className="min-w-0">
69+
<h3 className="text-lg font-semibold truncate">
70+
{server.name}
71+
</h3>
72+
<p className="text-sm text-[rgb(var(--muted))]">
73+
Server Definition
74+
</p>
75+
</div>
76+
<div className="flex items-center gap-2">
77+
<button
78+
onClick={handleCopy}
79+
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-[rgb(var(--border))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
80+
title="Copy to clipboard"
81+
>
82+
{copied ? (
83+
<>
84+
<Check className="h-4 w-4 text-[rgb(var(--success))]" />
85+
Copied
86+
</>
87+
) : (
88+
<>
89+
<Copy className="h-4 w-4 text-[rgb(var(--muted))]" />
90+
Copy
91+
</>
92+
)}
93+
</button>
94+
<button
95+
onClick={onClose}
96+
className="p-2 hover:bg-[rgb(var(--surface-hover))] rounded-lg transition-colors"
97+
>
98+
<X className="h-5 w-5 text-[rgb(var(--muted))]" />
99+
</button>
100+
</div>
101+
</div>
102+
103+
{/* Editor Area */}
104+
<div className="flex-1 relative min-h-0 bg-[#1e1e1e]">
105+
{!editorReady ? (
106+
<div className="absolute inset-0 flex items-center justify-center">
107+
<Loader2 className="h-8 w-8 animate-spin text-[rgb(var(--muted))]" />
108+
</div>
109+
) : (
110+
<Editor
111+
height="100%"
112+
defaultLanguage="json"
113+
value={json}
114+
theme="vs-dark"
115+
options={{
116+
readOnly: true,
117+
minimap: { enabled: false },
118+
fontSize: 14,
119+
fontFamily: "'Fira Code', 'Consolas', monospace",
120+
lineNumbers: 'on',
121+
scrollBeyondLastLine: false,
122+
automaticLayout: true,
123+
tabSize: 2,
124+
wordWrap: 'on',
125+
folding: true,
126+
bracketPairColorization: { enabled: true },
127+
guides: {
128+
bracketPairs: true,
129+
indentation: true,
130+
},
131+
padding: { top: 12, bottom: 12 },
132+
domReadOnly: true,
133+
}}
134+
loading={
135+
<div className="flex items-center justify-center h-full bg-[#1e1e1e]">
136+
<Loader2 className="h-8 w-8 animate-spin text-[rgb(var(--muted))]" />
137+
</div>
138+
}
139+
/>
140+
)}
141+
</div>
142+
</div>
143+
</div>
144+
);
145+
}

apps/desktop/src/features/registry/ServerDetailModal.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
* Server detail modal for viewing full server information.
33
*/
44

5+
import { useState } from 'react';
6+
import { Code } from 'lucide-react';
57
import type { ServerViewModel } from '../../types/registry';
68
import { ServerIcon } from '../../components/ServerIcon';
9+
import { ServerDefinitionModal } from '../../components/ServerDefinitionModal';
710

811
interface ServerDetailModalProps {
912
server: ServerViewModel;
@@ -20,6 +23,8 @@ export function ServerDetailModal({
2023
onUninstall,
2124
isLoading,
2225
}: ServerDetailModalProps) {
26+
const [showDefinition, setShowDefinition] = useState(false);
27+
2328
return (
2429
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
2530
{/* Backdrop */}
@@ -360,6 +365,13 @@ export function ServerDetailModal({
360365

361366
{/* Footer */}
362367
<div className="flex justify-end gap-3 p-6 border-t border-[rgb(var(--border))]">
368+
<button
369+
onClick={() => setShowDefinition(true)}
370+
className="flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg border border-[rgb(var(--border))] hover:bg-[rgb(var(--surface-hover))] transition-colors mr-auto"
371+
>
372+
<Code className="h-4 w-4 text-[rgb(var(--muted))]" />
373+
View JSON
374+
</button>
363375
<button
364376
onClick={onClose}
365377
className="px-4 py-2 text-sm rounded-lg border border-[rgb(var(--border))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
@@ -385,6 +397,13 @@ export function ServerDetailModal({
385397
)}
386398
</div>
387399
</div>
400+
401+
{showDefinition && (
402+
<ServerDefinitionModal
403+
server={server}
404+
onClose={() => setShowDefinition(false)}
405+
/>
406+
)}
388407
</div>
389408
);
390409
}

apps/desktop/src/features/servers/ServerActionMenu.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
* - Refresh: Quick reconnect with existing credentials
77
* - Reconnect: Logout + re-authenticate (OAuth only)
88
* - View Logs: Open log viewer
9+
* - View Definition: View server definition JSON
910
* - Uninstall: Remove server
1011
*/
1112

1213
import { useState, useRef, useEffect } from 'react';
13-
import { MoreVertical, Settings, RefreshCw, RotateCcw, FileText, Trash2 } from 'lucide-react';
14+
import { MoreVertical, Settings, RefreshCw, RotateCcw, FileText, Code, Trash2 } from 'lucide-react';
1415

1516
export interface ServerActionMenuProps {
1617
serverId: string;
@@ -23,12 +24,12 @@ export interface ServerActionMenuProps {
2324
onRefresh: () => void;
2425
onReconnect: () => void;
2526
onViewLogs: () => void;
27+
onViewDefinition: () => void;
2628
onUninstall: () => void;
27-
disabled?: boolean;
2829
}
2930

3031
export function ServerActionMenu({
31-
serverId: _serverId,
32+
serverId,
3233
serverName: _serverName,
3334
hasInputs,
3435
isOAuth,
@@ -38,8 +39,8 @@ export function ServerActionMenu({
3839
onRefresh,
3940
onReconnect,
4041
onViewLogs,
42+
onViewDefinition,
4143
onUninstall,
42-
disabled = false,
4344
}: ServerActionMenuProps) {
4445
const [isOpen, setIsOpen] = useState(false);
4546
const menuRef = useRef<HTMLDivElement>(null);
@@ -88,12 +89,12 @@ export function ServerActionMenu({
8889
<button
8990
ref={buttonRef}
9091
onClick={() => setIsOpen(!isOpen)}
91-
disabled={disabled}
92-
className="p-2 text-sm rounded-lg border border-[rgb(var(--border))] text-[rgb(var(--muted))] hover:bg-[rgb(var(--surface-hover))] transition-colors disabled:opacity-50"
92+
className="p-2 text-sm rounded-lg bg-[rgb(var(--surface-hover))] border border-[rgb(var(--border))] text-[rgb(var(--foreground))]/70 hover:bg-[rgb(var(--surface-elevated))] hover:text-[rgb(var(--foreground))] transition-colors"
9393
title="More actions"
9494
aria-label="More actions"
9595
aria-expanded={isOpen}
9696
aria-haspopup="menu"
97+
data-testid={`action-menu-${serverId}`}
9798
>
9899
<MoreVertical className="h-4 w-4" />
99100
</button>
@@ -145,11 +146,23 @@ export function ServerActionMenu({
145146
onClick={() => handleAction(onViewLogs)}
146147
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
147148
role="menuitem"
149+
data-testid={`view-logs-${serverId}`}
148150
>
149151
<FileText className="h-4 w-4 text-[rgb(var(--muted))]" />
150152
View Logs
151153
</button>
152154

155+
{/* View Definition - always visible */}
156+
<button
157+
onClick={() => handleAction(onViewDefinition)}
158+
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--foreground))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
159+
role="menuitem"
160+
data-testid={`view-definition-${serverId}`}
161+
>
162+
<Code className="h-4 w-4 text-[rgb(var(--muted))]" />
163+
View Definition
164+
</button>
165+
153166
{/* Separator */}
154167
<div className="my-1 border-t border-[rgb(var(--border-subtle))]" />
155168

@@ -158,6 +171,7 @@ export function ServerActionMenu({
158171
onClick={() => handleAction(onUninstall)}
159172
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[rgb(var(--error))] hover:bg-[rgb(var(--error))]/10 transition-colors"
160173
role="menuitem"
174+
data-testid={`uninstall-menu-${serverId}`}
161175
>
162176
<Trash2 className="h-4 w-4" />
163177
Uninstall

0 commit comments

Comments
 (0)