Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions apps/desktop/src-tauri/src/commands/server_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ pub async fn enable_server_v2(
Some(app_state.data_dir()),
);

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

match result {
Expand All @@ -148,13 +149,9 @@ pub async fn enable_server_v2(
Ok(())
}
ConnectionResult::OAuthRequired { .. } => {
// OAuth is needed - set state to AuthRequired (NOT Authenticating)
// Don't open browser yet - wait for user to click Connect
// Cancel the OAuth flow that was started during connection probe
pool_service
.oauth_manager()
.cancel_flow_for_space(space_uuid, &server_id);

// OAuth is needed - set state to AuthRequired
// auto_reconnect=true prevented OAuth flow from starting, so no cancel needed
// User will click "Connect" to start the actual OAuth flow
manager.set_auth_required(&key, None).await;

// Mark features unavailable - not connected
Expand Down
145 changes: 145 additions & 0 deletions apps/desktop/src/components/ServerDefinitionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useState, useEffect, useCallback } from 'react';
import { X, Copy, Check, Loader2 } from 'lucide-react';
import Editor from '@monaco-editor/react';
import type { ServerViewModel, ServerDefinition } from '../types/registry';

interface ServerDefinitionModalProps {
server: ServerViewModel;
onClose: () => void;
}

/** Extract only ServerDefinition fields, stripping runtime state */
function extractDefinition(server: ServerViewModel): ServerDefinition {
const {
is_installed: _a,
enabled: _b,
oauth_connected: _c,
input_values: _d,
connection_status: _e,
missing_required_inputs: _f,
last_error: _g,
created_at: _h,
installation_source: _i,
env_overrides: _j,
args_append: _k,
extra_headers: _l,
...definition
} = server;
return definition;
}

export function ServerDefinitionModal({ server, onClose }: ServerDefinitionModalProps) {
const [copied, setCopied] = useState(false);
const [editorReady, setEditorReady] = useState(false);

const definition = extractDefinition(server);
const json = JSON.stringify(definition, null, 2);

useEffect(() => {
const timer = setTimeout(() => setEditorReady(true), 100);
return () => clearTimeout(timer);
}, []);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(json);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for environments where clipboard API is unavailable
}
}, [json]);

return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<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">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border))]">
<div className="min-w-0">
<h3 className="text-lg font-semibold truncate">
{server.name}
</h3>
<p className="text-sm text-[rgb(var(--muted))]">
Server Definition
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleCopy}
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"
title="Copy to clipboard"
>
{copied ? (
<>
<Check className="h-4 w-4 text-[rgb(var(--success))]" />
Copied
</>
) : (
<>
<Copy className="h-4 w-4 text-[rgb(var(--muted))]" />
Copy
</>
)}
</button>
<button
onClick={onClose}
className="p-2 hover:bg-[rgb(var(--surface-hover))] rounded-lg transition-colors"
>
<X className="h-5 w-5 text-[rgb(var(--muted))]" />
</button>
</div>
</div>

{/* Editor Area */}
<div className="flex-1 relative min-h-0 bg-[#1e1e1e]">
{!editorReady ? (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-[rgb(var(--muted))]" />
</div>
) : (
<Editor
height="100%"
defaultLanguage="json"
value={json}
theme="vs-dark"
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
fontFamily: "'Fira Code', 'Consolas', monospace",
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on',
folding: true,
bracketPairColorization: { enabled: true },
guides: {
bracketPairs: true,
indentation: true,
},
padding: { top: 12, bottom: 12 },
domReadOnly: true,
}}
loading={
<div className="flex items-center justify-center h-full bg-[#1e1e1e]">
<Loader2 className="h-8 w-8 animate-spin text-[rgb(var(--muted))]" />
</div>
}
/>
)}
</div>
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/desktop/src/features/registry/ServerDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
* Server detail modal for viewing full server information.
*/

import { useState } from 'react';
import { Code } from 'lucide-react';
import type { ServerViewModel } from '../../types/registry';
import { ServerIcon } from '../../components/ServerIcon';
import { ServerDefinitionModal } from '../../components/ServerDefinitionModal';

interface ServerDetailModalProps {
server: ServerViewModel;
Expand All @@ -20,6 +23,8 @@ export function ServerDetailModal({
onUninstall,
isLoading,
}: ServerDetailModalProps) {
const [showDefinition, setShowDefinition] = useState(false);

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
Expand Down Expand Up @@ -360,6 +365,13 @@ export function ServerDetailModal({

{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-[rgb(var(--border))]">
<button
onClick={() => setShowDefinition(true)}
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"
>
<Code className="h-4 w-4 text-[rgb(var(--muted))]" />
View JSON
</button>
<button
onClick={onClose}
className="px-4 py-2 text-sm rounded-lg border border-[rgb(var(--border))] hover:bg-[rgb(var(--surface-hover))] transition-colors"
Expand All @@ -385,6 +397,13 @@ export function ServerDetailModal({
)}
</div>
</div>

{showDefinition && (
<ServerDefinitionModal
server={server}
onClose={() => setShowDefinition(false)}
/>
)}
</div>
);
}
26 changes: 20 additions & 6 deletions apps/desktop/src/features/servers/ServerActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* - Refresh: Quick reconnect with existing credentials
* - Reconnect: Logout + re-authenticate (OAuth only)
* - View Logs: Open log viewer
* - View Definition: View server definition JSON
* - Uninstall: Remove server
*/

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

export interface ServerActionMenuProps {
serverId: string;
Expand All @@ -23,12 +24,12 @@ export interface ServerActionMenuProps {
onRefresh: () => void;
onReconnect: () => void;
onViewLogs: () => void;
onViewDefinition: () => void;
onUninstall: () => void;
disabled?: boolean;
}

export function ServerActionMenu({
serverId: _serverId,
serverId,
serverName: _serverName,
hasInputs,
isOAuth,
Expand All @@ -38,8 +39,8 @@ export function ServerActionMenu({
onRefresh,
onReconnect,
onViewLogs,
onViewDefinition,
onUninstall,
disabled = false,
}: ServerActionMenuProps) {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -88,12 +89,12 @@ export function ServerActionMenu({
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
disabled={disabled}
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"
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"
title="More actions"
aria-label="More actions"
aria-expanded={isOpen}
aria-haspopup="menu"
data-testid={`action-menu-${serverId}`}
>
<MoreVertical className="h-4 w-4" />
</button>
Expand Down Expand Up @@ -145,11 +146,23 @@ export function ServerActionMenu({
onClick={() => handleAction(onViewLogs)}
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"
role="menuitem"
data-testid={`view-logs-${serverId}`}
>
<FileText className="h-4 w-4 text-[rgb(var(--muted))]" />
View Logs
</button>

{/* View Definition - always visible */}
<button
onClick={() => handleAction(onViewDefinition)}
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"
role="menuitem"
data-testid={`view-definition-${serverId}`}
>
<Code className="h-4 w-4 text-[rgb(var(--muted))]" />
View Definition
</button>

{/* Separator */}
<div className="my-1 border-t border-[rgb(var(--border-subtle))]" />

Expand All @@ -158,6 +171,7 @@ export function ServerActionMenu({
onClick={() => handleAction(onUninstall)}
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"
role="menuitem"
data-testid={`uninstall-menu-${serverId}`}
>
<Trash2 className="h-4 w-4" />
Uninstall
Expand Down
Loading