Skip to content

Commit af67716

Browse files
Refactor MCP server UI and improve config section scrolling
1 parent 2c9782a commit af67716

2 files changed

Lines changed: 110 additions & 125 deletions

File tree

frontend/src/components/settings/McpManager.tsx

Lines changed: 17 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { useState } from 'react'
22
import { Button } from '@/components/ui/button'
3-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3+
import { Card, CardContent } from '@/components/ui/card'
44
import { Dialog, DialogTrigger } from '@/components/ui/dialog'
5-
import { Badge } from '@/components/ui/badge'
6-
import { Switch } from '@/components/ui/switch'
7-
import { Plus, Trash2, Globe, Terminal, Loader2, AlertCircle, RefreshCw, Key, XCircle } from 'lucide-react'
5+
import { Plus, Loader2, RefreshCw } from 'lucide-react'
86
import { DeleteDialog } from '@/components/ui/delete-dialog'
97
import { AddMcpServerDialog } from './AddMcpServerDialog'
8+
import { McpServerCard } from './McpServerCard'
109
import { useMcpServers } from '@/hooks/useMcpServers'
1110
import { useMutation, useQueryClient } from '@tanstack/react-query'
12-
import type { McpStatus } from '@/api/mcp'
1311

1412
interface McpServerConfig {
1513
type: 'local' | 'remote'
@@ -29,37 +27,7 @@ interface McpManagerProps {
2927
onConfigUpdate?: (configName: string, content: Record<string, unknown>) => Promise<void>
3028
}
3129

32-
function getStatusBadge(status: McpStatus) {
33-
switch (status.status) {
34-
case 'connected':
35-
return <Badge variant="default" className="text-xs bg-green-600">Connected</Badge>
36-
case 'disabled':
37-
return <Badge variant="secondary" className="text-xs">Disabled</Badge>
38-
case 'failed':
39-
return (
40-
<Badge variant="destructive" className="text-xs flex items-center gap-1">
41-
<AlertCircle className="h-3 w-3" />
42-
Failed
43-
</Badge>
44-
)
45-
case 'needs_auth':
46-
return (
47-
<Badge variant="outline" className="text-xs flex items-center gap-1 border-yellow-500 text-yellow-600">
48-
<Key className="h-3 w-3" />
49-
Auth Required
50-
</Badge>
51-
)
52-
case 'needs_client_registration':
53-
return (
54-
<Badge variant="outline" className="text-xs flex items-center gap-1 border-orange-500 text-orange-600">
55-
<AlertCircle className="h-3 w-3" />
56-
Registration Required
57-
</Badge>
58-
)
59-
default:
60-
return <Badge variant="outline" className="text-xs">Unknown</Badge>
61-
}
62-
}
30+
6331

6432
export function McpManager({ config, onUpdate, onConfigUpdate }: McpManagerProps) {
6533
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
@@ -130,30 +98,7 @@ export function McpManager({ config, onUpdate, onConfigUpdate }: McpManagerProps
13098
}
13199
}
132100

133-
const getServerDisplayName = (serverId: string): string => {
134-
const name = serverId.replace(/[-_]/g, ' ')
135-
return name.charAt(0).toUpperCase() + name.slice(1)
136-
}
137-
138-
const getServerDescription = (serverConfig: McpServerConfig): string => {
139-
if (serverConfig.type === 'local' && serverConfig.command) {
140-
const command = serverConfig.command.join(' ')
141-
if (command.includes('filesystem')) return 'File system access'
142-
if (command.includes('git')) return 'Git repository operations'
143-
if (command.includes('sqlite')) return 'SQLite database access'
144-
if (command.includes('postgres')) return 'PostgreSQL database access'
145-
if (command.includes('brave-search')) return 'Web search via Brave'
146-
if (command.includes('github')) return 'GitHub repository access'
147-
if (command.includes('slack')) return 'Slack integration'
148-
if (command.includes('puppeteer')) return 'Web automation'
149-
if (command.includes('fetch')) return 'HTTP requests'
150-
if (command.includes('memory')) return 'Persistent memory'
151-
return `Local command: ${command}`
152-
} else if (serverConfig.type === 'remote' && serverConfig.url) {
153-
return `Remote server: ${serverConfig.url}`
154-
}
155-
return 'MCP server'
156-
}
101+
157102

158103
const getErrorMessage = (serverId: string): string | null => {
159104
const status = mcpStatus?.[serverId]
@@ -232,62 +177,18 @@ export function McpManager({ config, onUpdate, onConfigUpdate }: McpManagerProps
232177
const errorMessage = getErrorMessage(serverId)
233178

234179
return (
235-
<Card key={serverId} className={errorMessage ? 'border-red-500/50' : ''}>
236-
<CardHeader className="pb-3">
237-
<div className="flex flex-col gap-4">
238-
<div className="flex items-center gap-3">
239-
<div className="flex items-center gap-2">
240-
{serverConfig.type === 'local' ? (
241-
<Terminal className="h-4 w-4 text-muted-foreground" />
242-
) : (
243-
<Globe className="h-4 w-4 text-muted-foreground" />
244-
)}
245-
<CardTitle className="text-base">{getServerDisplayName(serverId)}</CardTitle>
246-
</div>
247-
<div className="flex items-center gap-2">
248-
{status ? getStatusBadge(status) : (
249-
<Badge variant="outline" className="text-xs">Loading...</Badge>
250-
)}
251-
<Badge variant="outline" className="text-xs">
252-
{serverConfig.type}
253-
</Badge>
254-
</div>
255-
</div>
256-
<div className="flex items-center gap-2">
257-
<Switch
258-
checked={isConnected}
259-
onCheckedChange={() => handleToggleServer(serverId)}
260-
disabled={isAnyOperationPending || togglingServerId === serverId}
261-
/>
262-
<Button
263-
variant="ghost"
264-
size="sm"
265-
onClick={() => setDeleteConfirmServer({ id: serverId, name: getServerDisplayName(serverId) })}
266-
className="text-red-500 hover:text-red-600"
267-
>
268-
<Trash2 className="h-4 w-4" />
269-
</Button>
270-
</div>
271-
</div>
272-
</CardHeader>
273-
<CardContent className='p-2'>
274-
<div className="text-sm text-muted-foreground space-y-1">
275-
<p>{getServerDescription(serverConfig)}</p>
276-
{serverConfig.timeout && (
277-
<p>Timeout: {serverConfig.timeout}ms</p>
278-
)}
279-
{serverConfig.environment && Object.keys(serverConfig.environment).length > 0 && (
280-
<p>Environment variables: {Object.keys(serverConfig.environment).length} configured</p>
281-
)}
282-
{errorMessage && (
283-
<div className="flex items-start gap-2 mt-2 p-2 bg-red-500/10 rounded text-red-600 text-xs">
284-
<XCircle className="h-4 w-4 flex-shrink-0 mt-0.5" />
285-
<span className="break-words">{errorMessage}</span>
286-
</div>
287-
)}
288-
</div>
289-
</CardContent>
290-
</Card>
180+
<McpServerCard
181+
key={serverId}
182+
serverId={serverId}
183+
serverConfig={serverConfig}
184+
status={status}
185+
isConnected={isConnected}
186+
errorMessage={errorMessage}
187+
isAnyOperationPending={isAnyOperationPending}
188+
togglingServerId={togglingServerId}
189+
onToggleServer={handleToggleServer}
190+
onDeleteServer={(id, name) => setDeleteConfirmServer({ id, name })}
191+
/>
291192
)
292193
})}
293194
</div>

frontend/src/components/settings/OpenCodeConfigManager.tsx

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react'
1+
import { useState, useEffect, useRef } from 'react'
22
import { Loader2, Plus, Trash2, Edit, Star, StarOff, Download, RotateCcw } from 'lucide-react'
33
import { Button } from '@/components/ui/button'
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -16,6 +16,35 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
1616
import { parseJsonc, hasJsoncComments } from '@/lib/jsonc'
1717
import type { OpenCodeConfig } from '@/api/types/settings'
1818

19+
interface Command {
20+
template: string
21+
description?: string
22+
agent?: string
23+
model?: string
24+
subtask?: boolean
25+
topP?: number
26+
}
27+
28+
interface Agent {
29+
prompt?: string
30+
description?: string
31+
mode?: 'subagent' | 'primary' | 'all'
32+
temperature?: number
33+
topP?: number
34+
model?: {
35+
modelID: string
36+
providerID: string
37+
}
38+
tools?: Record<string, boolean>
39+
permission?: {
40+
edit?: 'ask' | 'allow' | 'deny'
41+
bash?: 'ask' | 'allow' | 'deny' | Record<string, 'ask' | 'allow' | 'deny'>
42+
webfetch?: 'ask' | 'allow' | 'deny'
43+
}
44+
disable?: boolean
45+
[key: string]: unknown
46+
}
47+
1948
export function OpenCodeConfigManager() {
2049
const queryClient = useQueryClient()
2150
const [configs, setConfigs] = useState<OpenCodeConfig[]>([])
@@ -32,6 +61,31 @@ export function OpenCodeConfigManager() {
3261
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
3362
const [deleteConfirmConfig, setDeleteConfirmConfig] = useState<OpenCodeConfig | null>(null)
3463

64+
const commandsRef = useRef<HTMLButtonElement>(null)
65+
const agentsRef = useRef<HTMLButtonElement>(null)
66+
const mcpRef = useRef<HTMLButtonElement>(null)
67+
68+
const scrollToSection = (ref: React.RefObject<HTMLButtonElement | null>) => {
69+
if (ref.current) {
70+
const rect = ref.current.getBoundingClientRect()
71+
72+
console.log('Scroll check:', {
73+
section: ref.current.textContent?.trim(),
74+
rectTop: rect.top,
75+
rectBottom: rect.bottom,
76+
viewportHeight: window.innerHeight
77+
})
78+
79+
// Always scroll to ensure the section is at the top when expanded
80+
console.log('Scrolling to section:', ref.current.textContent?.trim())
81+
ref.current.scrollIntoView({
82+
behavior: 'smooth',
83+
block: 'start',
84+
inline: 'nearest'
85+
})
86+
}
87+
}
88+
3589
const restartServerMutation = useMutation({
3690
mutationFn: async () => {
3791
return await settingsApi.restartOpenCodeServer()
@@ -343,21 +397,31 @@ export function OpenCodeConfigManager() {
343397
<>
344398
<div className="bg-card border border-border rounded-lg overflow-hidden min-w-0">
345399
<button
400+
ref={commandsRef}
346401
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors min-w-0"
347-
onClick={() => setExpandedSections(prev => ({ ...prev, commands: !prev.commands }))}
402+
onClick={() => {
403+
const isExpanding = !expandedSections.commands
404+
console.log('Commands clicked, expanding:', isExpanding)
405+
setExpandedSections(prev => ({ ...prev, commands: isExpanding }))
406+
407+
// Only scroll when expanding, not collapsing
408+
if (isExpanding) {
409+
setTimeout(() => scrollToSection(commandsRef), 100)
410+
}
411+
}}
348412
>
349413
<div className="flex items-center gap-3 min-w-0">
350414
<h4 className="text-sm font-medium truncate">Commands</h4>
351415
<span className="text-xs text-muted-foreground">
352-
{Object.keys(selectedConfig.content.command as Record<string, any> || {}).length} configured
416+
{Object.keys(selectedConfig.content.command as Record<string, Command> || {}).length} configured
353417
</span>
354418
</div>
355419
<Edit className={`h-4 w-4 transition-transform ${expandedSections.commands ? 'rotate-90' : ''}`} />
356420
</button>
357421
<div className={`${expandedSections.commands ? 'block' : 'hidden'} border-t border-border`}>
358422
<div className="p-1 sm:p-4 max-h-[50vh] overflow-y-auto">
359423
<CommandsEditor
360-
commands={(selectedConfig.content.command as Record<string, any>) || {}}
424+
commands={(selectedConfig.content.command as Record<string, Command>) || {}}
361425
onChange={(commands) => {
362426
const updatedContent = {
363427
...selectedConfig.content,
@@ -372,21 +436,31 @@ export function OpenCodeConfigManager() {
372436

373437
<div className="bg-card border border-border rounded-lg overflow-hidden min-w-0">
374438
<button
439+
ref={agentsRef}
375440
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors min-w-0"
376-
onClick={() => setExpandedSections(prev => ({ ...prev, agents: !prev.agents }))}
441+
onClick={() => {
442+
const isExpanding = !expandedSections.agents
443+
console.log('Agents clicked, expanding:', isExpanding)
444+
setExpandedSections(prev => ({ ...prev, agents: isExpanding }))
445+
446+
// Only scroll when expanding, not collapsing
447+
if (isExpanding) {
448+
setTimeout(() => scrollToSection(agentsRef), 100)
449+
}
450+
}}
377451
>
378452
<div className="flex items-center gap-3 min-w-0">
379453
<h4 className="text-sm font-medium truncate">Agents</h4>
380454
<span className="text-xs text-muted-foreground">
381-
{Object.keys(selectedConfig.content.agent as Record<string, any> || {}).length} configured
455+
{Object.keys(selectedConfig.content.agent as Record<string, Agent> || {}).length} configured
382456
</span>
383457
</div>
384458
<Edit className={`h-4 w-4 transition-transform ${expandedSections.agents ? 'rotate-90' : ''}`} />
385459
</button>
386460
<div className={`${expandedSections.agents ? 'block' : 'hidden'} border-t border-border`}>
387461
<div className="p-4 max-h-[50vh] overflow-y-auto">
388462
<AgentsEditor
389-
agents={(selectedConfig.content.agent as Record<string, any>) || {}}
463+
agents={(selectedConfig.content.agent as Record<string, Agent>) || {}}
390464
onChange={(agents) => {
391465
const updatedContent = {
392466
...selectedConfig.content,
@@ -401,13 +475,23 @@ export function OpenCodeConfigManager() {
401475

402476
<div className="bg-card border border-border rounded-lg overflow-hidden min-w-0">
403477
<button
478+
ref={mcpRef}
404479
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors min-w-0"
405-
onClick={() => setExpandedSections(prev => ({ ...prev, mcp: !prev.mcp }))}
480+
onClick={() => {
481+
const isExpanding = !expandedSections.mcp
482+
console.log('MCP clicked, expanding:', isExpanding)
483+
setExpandedSections(prev => ({ ...prev, mcp: isExpanding }))
484+
485+
// Only scroll when expanding, not collapsing
486+
if (isExpanding) {
487+
setTimeout(() => scrollToSection(mcpRef), 100)
488+
}
489+
}}
406490
>
407491
<div className="flex items-center gap-3 min-w-0">
408492
<h4 className="text-sm font-medium truncate">MCP Servers</h4>
409493
<span className="text-xs text-muted-foreground">
410-
{Object.keys((selectedConfig.content.mcp as Record<string, any>) || {}).length} configured
494+
{Object.keys((selectedConfig.content.mcp as Record<string, unknown>) || {}).length} configured
411495
</span>
412496
</div>
413497
<Edit className={`h-4 w-4 transition-transform ${expandedSections.mcp ? 'rotate-90' : ''}`} />

0 commit comments

Comments
 (0)