feat(mcp): add Docs/Tools tablist on detail page, tidy sidebar label

Wrap the MCP detail right panel in a compact left-aligned Docs/Tools
tablist (Docs first). Move the tool count into the Tools tab label and
drop the redundant panel title/subtitle; connecting/failed states still
render the status component. Shorten the sidebar 'Installed Extensions'
entry to 'Installed' across all 8 locales, and add tabTools/tabDocs/
noReadme strings.
This commit is contained in:
RockChinQ
2026-06-06 03:52:17 -04:00
parent dff80a0c0a
commit b08e5ca09a
9 changed files with 84 additions and 35 deletions

View File

@@ -40,6 +40,13 @@ import {
CardTitle,
} from '@/components/ui/card';
import { httpClient } from '@/app/infra/http/HttpClient';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs';
import MCPReadme from '@/app/home/mcp/components/mcp-form/MCPReadme';
import {
MCPServerRuntimeInfo,
MCPTool,
@@ -264,35 +271,10 @@ function RuntimePanel({
const isConnected =
!mcpTesting && runtimeInfo.status === MCPSessionStatus.CONNECTED;
// Only treat an explicit error (or box-unavailable) as failed; while testing,
// connecting, or in an initial/unresolved state, show "connecting" so we
// don't flash "connection failed" during a normal connection attempt.
const isFailed =
!mcpTesting &&
(runtimeInfo.status === MCPSessionStatus.ERROR ||
runtimeInfo.error_phase === 'box_unavailable');
const tools = runtimeInfo.tools || [];
return (
<section className="space-y-4">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="space-y-1">
<h3 className="text-sm font-medium">{t('mcp.title')}</h3>
<p className="text-sm text-muted-foreground">
{isConnected
? t('mcp.toolCount', { count: tools.length })
: isFailed
? t('mcp.connectionFailedStatus')
: t('mcp.connecting')}
</p>
</div>
{isConnected && (
<Badge variant="outline">
{t('mcp.toolCount', { count: tools.length })}
</Badge>
)}
</div>
{!isConnected && (
<div className="rounded-md bg-muted/40 p-3">
<StatusDisplay testing={mcpTesting} runtimeInfo={runtimeInfo} t={t} />
@@ -434,6 +416,9 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
null,
);
// README markdown captured from LangBot Space at install time, surfaced in
// the Docs tab of the detail panel. Empty for manually-created servers.
const [readme, setReadme] = useState<string>('');
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const watchMode = form.watch('mode');
const {
@@ -611,6 +596,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
setStdioArgs(newStdioArgs);
form.reset(formValues);
setRuntimeInfo(server.runtime_info ?? null);
setReadme(server.readme ?? '');
} catch (error) {
console.error('Failed to load server:', error);
toast.error(t('mcp.loadFailed'));
@@ -1063,6 +1049,45 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
/>
);
// In edit mode the right side shows a tablist switching between the live
// Tools list and the Docs (README captured from LangBot Space at install).
// Create mode has neither, so it falls back to the bare runtime placeholder.
// The tool count lives in the tab label (only when connected); the panel
// body itself no longer repeats a title/subtitle.
const toolsConnected =
!mcpTesting && runtimeInfo?.status === MCPSessionStatus.CONNECTED;
const toolsCount = runtimeInfo?.tools?.length ?? 0;
const toolsTabLabel = toolsConnected
? `${t('mcp.tabTools')} ${toolsCount}`
: t('mcp.tabTools');
const detailPanel = isEditMode ? (
<Tabs defaultValue="tools" className="flex h-full min-h-0 flex-col">
<TabsList>
<TabsTrigger value="docs" className="flex-none px-4">
{t('mcp.tabDocs')}
</TabsTrigger>
<TabsTrigger value="tools" className="flex-none px-4">
{toolsTabLabel}
</TabsTrigger>
</TabsList>
<TabsContent
value="docs"
className="mt-4 min-h-0 flex-1 overflow-y-auto"
>
<MCPReadme readme={readme} />
</TabsContent>
<TabsContent
value="tools"
className="mt-4 min-h-0 flex-1 overflow-y-auto"
>
{runtimePanel}
</TabsContent>
</Tabs>
) : (
runtimePanel
);
if (layout === 'split') {
return (
<Form {...form}>
@@ -1078,7 +1103,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
</div>
<div className="hidden w-px shrink-0 bg-border lg:block" />
<div className="min-w-0 flex-1 pb-6 lg:min-h-0 lg:overflow-y-auto lg:overflow-x-hidden">
{runtimePanel}
{detailPanel}
</div>
</form>
</Form>
@@ -1093,7 +1118,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
className="space-y-5"
>
{sideHeader}
{runtimePanel}
{detailPanel}
{configSection}
{sideFooter}
</form>