From 1ab5496e0abfdf3ae8c63b946dd980df012ee570 Mon Sep 17 00:00:00 2001 From: Jerome Date: Thu, 12 Jun 2025 15:28:20 +0100 Subject: [PATCH] Add tabbed sidebar with MCP server management and resizable interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created tabbed interface with MCP, Conversations, and Inference tabs - Set MCP tab as default tab - Added quick connect button for example server with OAuth auth - Created form to add custom MCP servers with auth type selection - Implemented server management UI with connect/disconnect/remove actions - Made sidebar resizable with drag handle (240px-600px range) - Added comprehensive MCP server status display and tools listing - Created dedicated Inference tab for OpenRouter connection management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/components/ConversationApp.tsx | 64 ++++--- src/components/InferenceTab.tsx | 115 +++++++++++++ src/components/MCPTab.tsx | 264 +++++++++++++++++++++++++++++ src/components/TabbedSidebar.tsx | 49 ++++++ src/mcp/connection.ts | 1 + 5 files changed, 469 insertions(+), 24 deletions(-) create mode 100644 src/components/InferenceTab.tsx create mode 100644 src/components/MCPTab.tsx create mode 100644 src/components/TabbedSidebar.tsx diff --git a/src/components/ConversationApp.tsx b/src/components/ConversationApp.tsx index 0463e89..bcdd268 100644 --- a/src/components/ConversationApp.tsx +++ b/src/components/ConversationApp.tsx @@ -1,38 +1,54 @@ // Main conversation application with sidebar and chat interface -import { useState } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { ConversationProvider } from '@/contexts/ConversationContext'; -import { ConversationSidebar } from './ConversationSidebar'; +import { TabbedSidebar } from './TabbedSidebar'; import { ChatInterface } from './ChatInterface'; -import { MCPStatus } from './MCPStatus'; export function ConversationApp() { - const [showMCPStatus, setShowMCPStatus] = useState(false); + const [sidebarWidth, setSidebarWidth] = useState(320); + const isResizing = useRef(false); + + const handleMouseDown = useCallback(() => { + isResizing.current = true; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + }, []); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isResizing.current) return; + + const newWidth = e.clientX; + if (newWidth >= 240 && newWidth <= 600) { + setSidebarWidth(newWidth); + } + }, []); + + const handleMouseUp = useCallback(() => { + isResizing.current = false; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }, [handleMouseMove]); return (
- {/* Left Sidebar - Conversations */} -
-
-
-

- Conversations -

- -
-
+ {/* Left Sidebar - Tabbed Interface */} +
+ - {showMCPStatus ? ( - - ) : ( - - )} + {/* Resize Handle */} +
{/* Main Chat Area */} diff --git a/src/components/InferenceTab.tsx b/src/components/InferenceTab.tsx new file mode 100644 index 0000000..df8f9ae --- /dev/null +++ b/src/components/InferenceTab.tsx @@ -0,0 +1,115 @@ +// Inference tab for managing OpenRouter connection + +import { useState } from 'react'; +import { useInference } from '@/contexts/InferenceContext'; +import { OpenRouterOAuthProvider } from '@/providers/openrouter/oauth-provider'; + +export function InferenceTab() { + const { provider, models, clearProvider, setProvider } = useInference(); + const [isConnecting, setIsConnecting] = useState(false); + + const handleConnect = async () => { + setIsConnecting(true); + try { + const oauthProvider = new OpenRouterOAuthProvider(); + await oauthProvider.authenticate({ type: 'oauth' }); + setProvider(oauthProvider); + } catch (error) { + console.error('Failed to connect to OpenRouter:', error); + } finally { + setIsConnecting(false); + } + }; + + const handleDisconnect = () => { + if (confirm('Are you sure you want to disconnect from OpenRouter? This will clear your authentication.')) { + clearProvider(); + } + }; + + return ( +
+
+ {/* Connection Status */} +
+

+ Inference Provider +

+ + {provider ? ( +
+
+
+

+ 🟢 Connected to OpenRouter +

+

+ {models.length} models available +

+
+ +
+
+ ) : ( +
+

+ ⚪ Not connected +

+ +
+ )} +
+ + {/* Available Models */} + {provider && models.length > 0 && ( +
+

+ Available Models ({models.length}) +

+
+ {models.map((model) => ( +
+
+ {model.id} +
+ {model.name && model.name !== model.id && ( +
+ {model.name} +
+ )} +
+ Context: {model.contextLength?.toLocaleString() || 'Unknown'} +
+
+ ))} +
+
+ )} + + {/* Help Text */} +
+

+ Connect to OpenRouter to access AI models for conversations. +

+

+ You'll need an OpenRouter account and API key to authenticate. +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/MCPTab.tsx b/src/components/MCPTab.tsx new file mode 100644 index 0000000..febfc2a --- /dev/null +++ b/src/components/MCPTab.tsx @@ -0,0 +1,264 @@ +// MCP tab with server management and connection functionality + +import { useState } from 'react'; +import { useMCP } from '@/contexts/MCPContext'; + +export function MCPTab() { + const { connections, addMcpServer, removeMcpServer, reconnectServer, isLoading } = useMCP(); + const [showAddForm, setShowAddForm] = useState(false); + const [newServerName, setNewServerName] = useState(''); + const [newServerUrl, setNewServerUrl] = useState(''); + const [authType, setAuthType] = useState<'none' | 'oauth'>('none'); + + const handleAddExampleServer = async () => { + try { + await addMcpServer({ + name: 'Example Server', + url: 'https://example-server.modelcontextprotocol.io/sse', + authType: 'oauth', + }); + } catch (error) { + console.error('Failed to add example server:', error); + } + }; + + const handleAddCustomServer = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newServerName.trim() || !newServerUrl.trim()) return; + + try { + await addMcpServer({ + name: newServerName, + url: newServerUrl, + authType, + }); + + // Reset form + setNewServerName(''); + setNewServerUrl(''); + setAuthType('none'); + setShowAddForm(false); + } catch (error) { + console.error('Failed to add custom server:', error); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'connected': + return 'text-green-600 dark:text-green-400'; + case 'connecting': + return 'text-yellow-600 dark:text-yellow-400'; + case 'failed': + return 'text-red-600 dark:text-red-400'; + default: + return 'text-gray-600 dark:text-gray-400'; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'connected': + return '🟢'; + case 'connecting': + return '🟡'; + case 'failed': + return '🔴'; + default: + return '⚪'; + } + }; + + const connectedServers = connections.filter(conn => conn.status === 'connected'); + const allTools = connections.flatMap(conn => conn.tools); + + return ( +
+ {/* Summary */} +
+
+

+ MCP Summary +

+
+

+ {connectedServers.length} servers connected +

+

+ {allTools.length} tools available +

+
+
+
+ + {/* Add Servers Section */} +
+

+ Add Servers +

+ + {/* Example Server Button */} + + + {/* Custom Server Form Toggle */} + + + {/* Custom Server Form */} + {showAddForm && ( +
+
+ + setNewServerName(e.target.value)} + placeholder="My MCP Server" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ + setNewServerUrl(e.target.value)} + placeholder="https://your-server.com/sse" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ + +
+ + +
+ )} +
+ + {/* Server List */} +
+
+

+ Servers ({connections.length}) +

+
+ + {connections.length === 0 ? ( +

+ No MCP servers configured. Add a server above to get started. +

+ ) : ( +
+ {connections.map((connection) => ( +
+
+
+
+ + {getStatusIcon(connection.status)} + +

+ {connection.name} +

+
+

+ {connection.status} +

+ {connection.error && ( +

+ {connection.error} +

+ )} +

+ {connection.url} +

+
+ +
+ {(connection.status === 'failed' || connection.status === 'disconnected') && ( + + )} + +
+
+ + {connection.tools.length > 0 && ( +
+

+ Tools ({connection.tools.length}): +

+
+ {connection.tools.slice(0, 3).map((tool, index) => ( + + {tool.function.name.split('__').pop()} + + ))} + {connection.tools.length > 3 && ( + + +{connection.tools.length - 3} more + + )} +
+
+ )} +
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/TabbedSidebar.tsx b/src/components/TabbedSidebar.tsx new file mode 100644 index 0000000..a827a5f --- /dev/null +++ b/src/components/TabbedSidebar.tsx @@ -0,0 +1,49 @@ +// Tabbed sidebar with Conversations, MCP, and Inference tabs + +import { useState } from 'react'; +import { ConversationSidebar } from './ConversationSidebar'; +import { MCPTab } from './MCPTab'; +import { InferenceTab } from './InferenceTab'; + +type TabType = 'mcp' | 'conversations' | 'inference'; + +export function TabbedSidebar() { + const [activeTab, setActiveTab] = useState('mcp'); + + const tabs = [ + { id: 'mcp' as const, label: 'MCP', icon: '🔧' }, + { id: 'conversations' as const, label: 'Conversations', icon: '💬' }, + { id: 'inference' as const, label: 'Inference', icon: '🤖' }, + ]; + + return ( +
+ {/* Tab Headers */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Tab Content */} +
+ {activeTab === 'mcp' && } + {activeTab === 'conversations' && } + {activeTab === 'inference' && } +
+
+ ); +} \ No newline at end of file diff --git a/src/mcp/connection.ts b/src/mcp/connection.ts index 1b13ad0..9b69a5c 100644 --- a/src/mcp/connection.ts +++ b/src/mcp/connection.ts @@ -522,6 +522,7 @@ export class MCPConnectionManager { try { console.log('Initializing MCP client...'); this.transport = transport; + this.transport.onmessage = console.log.bind(console, 'MCP Client message received:'); this.client = new Client( { name: 'example-remote-client',