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>

View File

@@ -2,7 +2,7 @@ const enUS = {
sidebar: {
home: 'Home',
extensions: 'Extensions',
installedPlugins: 'Installed Extensions',
installedPlugins: 'Installed',
pluginMarket: 'Extension Market',
mcpServers: 'MCP Servers',
addExtension: 'Add Extension',
@@ -765,6 +765,9 @@ const enUS = {
toolsFound: 'tools',
unknownError: 'Unknown error',
noToolsFound: 'No tools found',
tabTools: 'Tools',
tabDocs: 'Docs',
noReadme: 'No documentation available',
parseResultFailed: 'Failed to parse test result',
noResultReturned: 'Test returned no result',
getTaskFailed: 'Failed to get task status',

View File

@@ -2,7 +2,7 @@ const esES = {
sidebar: {
home: 'Inicio',
extensions: 'Extensiones',
installedPlugins: 'Plugins instalados',
installedPlugins: 'Instalados',
pluginMarket: 'Tienda',
mcpServers: 'Servidores MCP',
addExtension: 'Añadir extensión',
@@ -779,6 +779,9 @@ const esES = {
toolsFound: 'herramientas',
unknownError: 'Error desconocido',
noToolsFound: 'No se encontraron herramientas',
tabTools: 'Herramientas',
tabDocs: 'Documentación',
noReadme: 'No hay documentación disponible',
parseResultFailed: 'Error al analizar el resultado de la prueba',
noResultReturned: 'La prueba no devolvió resultados',
getTaskFailed: 'Error al obtener el estado de la tarea',

View File

@@ -2,7 +2,7 @@ const jaJP = {
sidebar: {
home: 'ホーム',
extensions: '拡張機能',
installedPlugins: 'インストール済みプラグイン',
installedPlugins: 'インストール済み',
pluginMarket: 'プラグインマーケット',
mcpServers: 'MCPサーバー',
addExtension: '拡張機能を追加',
@@ -770,6 +770,9 @@ const jaJP = {
toolsFound: '個のツール',
unknownError: '不明なエラー',
noToolsFound: 'ツールが見つかりません',
tabTools: 'ツール',
tabDocs: 'ドキュメント',
noReadme: 'ドキュメントがありません',
parseResultFailed: 'テスト結果の解析に失敗しました',
noResultReturned: 'テスト結果が返されませんでした',
getTaskFailed: 'タスクステータスの取得に失敗しました',

View File

@@ -2,7 +2,7 @@ const ruRU = {
sidebar: {
home: 'Главная',
extensions: 'Расширения',
installedPlugins: 'Установленные плагины',
installedPlugins: 'Установленные',
pluginMarket: 'Маркетплейс',
mcpServers: 'MCP-серверы',
addExtension: 'Добавить расширение',
@@ -775,6 +775,9 @@ const ruRU = {
toolsFound: 'инструментов',
unknownError: 'Неизвестная ошибка',
noToolsFound: 'Инструменты не найдены',
tabTools: 'Инструменты',
tabDocs: 'Документация',
noReadme: 'Документация отсутствует',
parseResultFailed: 'Не удалось разобрать результат теста',
noResultReturned: 'Тест не вернул результат',
getTaskFailed: 'Не удалось получить статус задачи',

View File

@@ -2,7 +2,7 @@ const thTH = {
sidebar: {
home: 'หน้าแรก',
extensions: 'ส่วนขยาย',
installedPlugins: 'ปลั๊กอินที่ติดตั้ง',
installedPlugins: 'ที่ติดตั้งแล้ว',
pluginMarket: 'ตลาดปลั๊กอิน',
mcpServers: 'เซิร์ฟเวอร์ MCP',
addExtension: 'เพิ่มส่วนขยาย',
@@ -755,6 +755,9 @@ const thTH = {
toolsFound: 'เครื่องมือ',
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
noToolsFound: 'ไม่พบเครื่องมือ',
tabTools: 'เครื่องมือ',
tabDocs: 'เอกสาร',
noReadme: 'ไม่มีเอกสาร',
parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้',
noResultReturned: 'การทดสอบไม่ส่งผลลัพธ์กลับมา',
getTaskFailed: 'ไม่สามารถดึงสถานะงานได้',

View File

@@ -2,7 +2,7 @@ const viVN = {
sidebar: {
home: 'Trang chủ',
extensions: 'Tiện ích mở rộng',
installedPlugins: 'Plugin đã cài đặt',
installedPlugins: 'Đã cài đặt',
pluginMarket: 'Chợ ứng dụng',
mcpServers: 'Máy chủ MCP',
addExtension: 'Thêm tiện ích mở rộng',
@@ -769,6 +769,9 @@ const viVN = {
toolsFound: 'công cụ',
unknownError: 'Lỗi không xác định',
noToolsFound: 'Không tìm thấy công cụ nào',
tabTools: 'Công cụ',
tabDocs: 'Tài liệu',
noReadme: 'Không có tài liệu',
parseResultFailed: 'Phân tích kết quả kiểm tra thất bại',
noResultReturned: 'Kiểm tra không trả về kết quả',
getTaskFailed: 'Lấy trạng thái tác vụ thất bại',

View File

@@ -2,7 +2,7 @@ const zhHans = {
sidebar: {
home: '首页',
extensions: '扩展',
installedPlugins: '已安装扩展',
installedPlugins: '已安装',
pluginMarket: '扩展市场',
mcpServers: 'MCP 服务器',
addExtension: '添加扩展',
@@ -737,6 +737,9 @@ const zhHans = {
toolsFound: '个工具',
unknownError: '未知错误',
noToolsFound: '未找到任何工具',
tabTools: '工具',
tabDocs: '文档',
noReadme: '暂无文档',
parseResultFailed: '解析测试结果失败',
noResultReturned: '测试未返回结果',
getTaskFailed: '获取任务状态失败',

View File

@@ -2,7 +2,7 @@ const zhHant = {
sidebar: {
home: '首頁',
extensions: '擴展',
installedPlugins: '已安裝外掛',
installedPlugins: '已安裝',
pluginMarket: '外掛市場',
mcpServers: 'MCP 伺服器',
addExtension: '添加擴展',
@@ -736,6 +736,9 @@ const zhHant = {
toolsFound: '個工具',
unknownError: '未知錯誤',
noToolsFound: '未找到任何工具',
tabTools: '工具',
tabDocs: '文件',
noReadme: '暫無文件',
parseResultFailed: '解析測試結果失敗',
noResultReturned: '測試未返回結果',
getTaskFailed: '獲取任務狀態失敗',