Standardize Settings layout and save feedback (#2122)
* Group settings navigation menu Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Revert "Group settings navigation menu" This reverts commit 5983a4e82074b8dab1084af1cadd803ba28ea65d. * Standardize settings layout feedback Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings save status timer typing Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Retain settings save status Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Report color mode save state Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Match Appearance settings to two-column layout Rebuild Appearance into Color mode & Theme, Localization, and Density & type sections with responsive two-column grids, consistent section headers, page description, and green save status. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate settings pages to SettingsPageLayout and SettingsSection Replace ScrollableOverlay/max-w-3xl shells with the shared settings layout primitives across entity and static settings pages, normalize section headers, and add settings.page.behavior.description locales. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Unify remaining settings pages on shared section chrome Wire Appearance, Projects, and Remote Instances through SettingsSection/SettingsPageLayout so every settings surface shares the same header, divider, and page shell treatment. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align settings UI with OpenChamber design system kit Apply kit geometry and control specs: 840px content width, 32/48 padding, fixed 260/280 sidebars, radius/spacing tokens, settings select height, stepper dimensions, and shared field/link typography. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate OpenChamberVisualSettings to shared settings primitives Replace ad-hoc radio/checkbox/chip/field layouts with SettingsSection shared chrome for Appearance, Density, Navigation, Chat/behavior, and Privacy while preserving handlers and data-settings-item anchors. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate settings pages to shared field/checkbox/radio primitives Unify Defaults, Notifications, Behavior, Git, Session Retention, Passkeys, OpenCode CLI, Commands, About, Keyboard Shortcuts, and Desktop Network on SettingsFieldRow / SettingsCheckboxRow / SettingsRadioGroup / SettingsChipGroup for consistent grid, spacing, and DRY layout. Also remove the GitPage double SettingsSection wrap around GitHubSettings. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Unify remaining settings pages onto shared field primitives Migrate Agents, Snippets, Skills, Usage, MCP identity rows and selects to SettingsFieldRow/CheckboxRow/ChipGroup and SETTINGS_SELECT_*; align page titles; light-touch Voice/Tunnel/Providers/Plugins without rewriting complex OAuth, permissions, or tunnel flows. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Polish settings section dividers and transport helper text Slightly stronger section borders for clearer group separation, and keep message-stream transport description under the chip control. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings grid alignment, control heights, and Chat section titles Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Tighten settings grid: flat Chat 2x2 and full-width stacked selects Message options use a flat two-column grid so row headers share a baseline. Stacked selects fill their column; field-row selects keep a fixed sm:w-56 width. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align mixed settings controls to shared FieldRow/CheckboxRow grid Separate FieldRows from CheckboxRows with SettingsInset, move enum radios into ControlGroups, and convert misplaced StackedFields to full-width FieldRows so left edges no longer clash. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Translate Behavior response-style preset labels for es and pl Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Group settings nav into categories and improve icons/order Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings layout: fewer hrs, shared control widths, chat transport section Remove SettingsInset top borders, align App install/Density controls to full cluster width, give Message Stream Transport its own Chat section, and fold Sessions Small Model into the first section to cut extra dividers. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align Voice and MCP settings labels with shared heading classes Swap form field labels to SETTINGS_FIELD_LABEL_CLASS and use SettingsGroupTitle for MCP control-group headings (manual auth fallback, request headers). Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Differentiate settings heading levels by context Page titles are larger and quieter than section titles; group and field labels use dedicated shared classes so hierarchy is consistent across settings surfaces without ad-hoc typography mixes. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Use shared settings title classes in SettingsView home Wire home and unavailable headings through the shared L1/L2 class constants so they stay aligned with SettingsPageLayout. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Keep settings number steppers from stretching +/- buttons Density & type NumberInputs no longer flex-grow across the row, and NumberInput locks minus/plus to fixed width so the plus side cannot inflate when the control is placed in a full-width cluster. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Place spacing and input-bar offset on their own row Density & type now lays out as font families, then font sizes, then Spacing Density / Input Bar Offset on the row below. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Regroup settings nav and improve Voice layout Drop Advanced/Usage/Git nav categories: Voice and About under Interface, Usage under OpenCode, Git under Workspace. Voice provider chips and STT model cards use shared settings primitives with roomier spacing and a two-column model grid. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Space out chat feature groups in visual settings * Polish settings: save-state wiring, container-query layouts, unified dropdown triggers - Wire shared save indicator into Behavior page saves and git identity CRUD - Convert settings layout primitives and page rows from viewport to container queries so narrow panes stack instead of clipping - Unify custom dropdown triggers (model/agent/provider pickers) and remaining page selects on the settings control size - Bump global radius scale by 1px; align variant input with select size - Migrate stray raw controls (MCP OAuth checkbox, worktree remove button, git identity dialog rows, skills dialog labels) onto shared primitives - Make settings nav items span full sidebar width; trim nav to 240px * Add General settings page, regroup nav, cap control widths, promote chat feature headers * Mobile settings nav: plain background and touch-sized rows * Hide secondary settings descriptions behind clickable info hints * Move quota credentials to Usage, navigation settings to General, rename External Tunnel * Quiet settings save indicator: silent success, delayed spinner, visible errors * Rewrite settings-ui-patterns skill around shared primitives and refactored conventions * Remove settings starter page; open last visited page, defaulting to General * Settings polish: spacing, control sizes, trigger widths, readable model names, device dates * Centralize dropdown trigger chrome, settings nav polish, mobile-only input bar offset * Fix global line-height regression, auto-hide first-section divider, shortcut row spacing * Global line-height 1.45, align scheduled tasks header button with select * Hide editor toolbar and About in VS Code, animate chat render preview outside desktop dialog * Rebuild agent tool permissions on source-of-truth model Edit the agent's own permission map verbatim (inherit vs explicit actions, pattern rules only for pattern-capable keys), save permission-only, drop the server-side non-wildcard re-merge that resurrected deleted rules, and surface session-granted rules as read-only. * Agents model parameters polish: row spacing, variant dropdown, unified widths, dash for unset numbers --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
Bohdan Triapitsyn
parent
1b402fcb67
commit
17f1b24709
@@ -3,6 +3,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { useMcpStore } from '@/stores/useMcpStore';
|
||||
import { parseMcpOAuthCallbackContext, parseMcpOAuthCallbackStateKey } from '@/components/sections/mcp/mcpOAuth';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { SETTINGS_PAGE_TITLE_CLASS } from '@/components/sections/shared/SettingsSection';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const parseQueryParam = (params: URLSearchParams, key: string): string | null => {
|
||||
const value = params.get(key);
|
||||
@@ -94,14 +96,21 @@ export const McpOAuthCallbackPage: React.FC = () => {
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-6 py-12 text-foreground">
|
||||
<div className="w-full max-w-xl rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-8 shadow-sm">
|
||||
<div className="space-y-3 text-center">
|
||||
<div
|
||||
className={status === 'error' ? 'text-[var(--status-error)]' : status === 'success' ? 'text-[var(--status-success)]' : 'text-[var(--status-info)]'}
|
||||
<h1 className={SETTINGS_PAGE_TITLE_CLASS}>
|
||||
{status === 'working' ? 'Completing Authorization' : status === 'success' ? 'Authorization Complete' : 'Authorization Failed'}
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
'typography-body',
|
||||
status === 'error'
|
||||
? 'text-[var(--status-error)]'
|
||||
: status === 'success'
|
||||
? 'text-[var(--status-success)]'
|
||||
: 'text-[var(--status-info)]',
|
||||
)}
|
||||
>
|
||||
<h1 className="typography-hero font-semibold">
|
||||
{status === 'working' ? 'Completing Authorization' : status === 'success' ? 'Authorization Complete' : 'Authorization Failed'}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="typography-body text-muted-foreground">{message}</p>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status !== 'working' && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from '@/components/ui';
|
||||
@@ -23,7 +22,18 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsFieldRow,
|
||||
SettingsCheckboxRow,
|
||||
SettingsStackedField,
|
||||
SettingsChipGroup,
|
||||
SettingsGroupTitle,
|
||||
SETTINGS_SELECT_SIZE,
|
||||
SETTINGS_FIELD_LABEL_CLASS,
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
import { SettingsInfoHint } from '@/components/sections/shared/SettingsInfoHint';
|
||||
import { MCP_OAUTH_CALLBACK_PATH, parseMcpOAuthCallbackContext, parseMcpOAuthCallbackStateKey } from '@/components/sections/mcp/mcpOAuth';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
@@ -1323,88 +1333,78 @@ export const McpPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-4">
|
||||
<div className="min-w-0">
|
||||
{isNewServer ? (
|
||||
<h2 className="typography-ui-header font-semibold text-foreground truncate">{t('settings.mcp.page.header.newServer')}</h2>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground truncate">{selectedMcpName}</h2>
|
||||
<StatusBadge status={effectiveRuntimeStatus?.status} enabled={enabled} getStatusLabel={getStatusLabel} variant="pill" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<p className="typography-meta text-muted-foreground truncate">
|
||||
{isNewServer
|
||||
? t('settings.mcp.page.header.configureNewServer')
|
||||
: t('settings.mcp.page.header.transport', { type: mcpType === 'local' ? t('settings.mcp.page.transport.local') : t('settings.mcp.page.transport.remote') })}
|
||||
</p>
|
||||
{!isNewServer && (
|
||||
<>
|
||||
<Button
|
||||
variant={isConnected ? 'outline' : 'default'}
|
||||
size="xs"
|
||||
className="!font-normal"
|
||||
onClick={handleToggleConnect}
|
||||
disabled={isConnecting || !enabled}
|
||||
>
|
||||
{isConnecting ? t('settings.mcp.page.actions.working') : isConnected ? t('settings.mcp.page.actions.disconnect') : t('settings.mcp.page.actions.connect')}
|
||||
</Button>
|
||||
{mcpType === 'remote' && (
|
||||
<>
|
||||
<Button
|
||||
variant={needsAuthorization ? 'default' : 'outline'}
|
||||
size="xs"
|
||||
className="!font-normal"
|
||||
onClick={() => void handleStartAuthorization()}
|
||||
disabled={isAuthorizing || !enabled}
|
||||
>
|
||||
{isAuthorizing
|
||||
? t('settings.mcp.page.actions.starting')
|
||||
: needsAuthorization
|
||||
? t('settings.mcp.page.actions.authorize')
|
||||
: t('settings.mcp.page.actions.reauthorize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="!font-normal gap-1 text-muted-foreground"
|
||||
onClick={() => void handleClearAuthorization()}
|
||||
disabled={isClearingAuth || !enabled}
|
||||
>
|
||||
{isClearingAuth ? t('settings.mcp.page.actions.clearing') : t('settings.mcp.page.actions.clearAuth')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isConnected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="!font-normal gap-1 text-muted-foreground"
|
||||
onClick={() => void handleTestConnection()}
|
||||
disabled={isTestingConnection || !enabled}
|
||||
>
|
||||
{isTestingConnection ? t('settings.mcp.page.actions.testing') : t('settings.mcp.page.actions.test')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<SettingsPageLayout
|
||||
title={isNewServer ? t('settings.mcp.page.header.newServer') : selectedMcpName}
|
||||
titleAccessory={!isNewServer ? (
|
||||
<StatusBadge status={effectiveRuntimeStatus?.status} enabled={enabled} getStatusLabel={getStatusLabel} variant="pill" />
|
||||
) : undefined}
|
||||
description={isNewServer
|
||||
? t('settings.mcp.page.header.configureNewServer')
|
||||
: t('settings.mcp.page.header.transport', { type: mcpType === 'local' ? t('settings.mcp.page.transport.local') : t('settings.mcp.page.transport.remote') })}
|
||||
headerEnd={!isNewServer ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant={isConnected ? 'outline' : 'default'}
|
||||
size="xs"
|
||||
className="!font-normal"
|
||||
onClick={handleToggleConnect}
|
||||
disabled={isConnecting || !enabled}
|
||||
>
|
||||
{isConnecting ? t('settings.mcp.page.actions.working') : isConnected ? t('settings.mcp.page.actions.disconnect') : t('settings.mcp.page.actions.connect')}
|
||||
</Button>
|
||||
{mcpType === 'remote' && (
|
||||
<>
|
||||
<Button
|
||||
variant={needsAuthorization ? 'default' : 'outline'}
|
||||
size="xs"
|
||||
className="!font-normal"
|
||||
onClick={() => void handleStartAuthorization()}
|
||||
disabled={isAuthorizing || !enabled}
|
||||
>
|
||||
{isAuthorizing
|
||||
? t('settings.mcp.page.actions.starting')
|
||||
: needsAuthorization
|
||||
? t('settings.mcp.page.actions.authorize')
|
||||
: t('settings.mcp.page.actions.reauthorize')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="!font-normal gap-1 text-muted-foreground"
|
||||
onClick={() => void handleClearAuthorization()}
|
||||
disabled={isClearingAuth || !enabled}
|
||||
>
|
||||
{isClearingAuth ? t('settings.mcp.page.actions.clearing') : t('settings.mcp.page.actions.clearAuth')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isConnected && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="!font-normal gap-1 text-muted-foreground"
|
||||
onClick={() => void handleTestConnection()}
|
||||
disabled={isTestingConnection || !enabled}
|
||||
>
|
||||
{isTestingConnection ? t('settings.mcp.page.actions.testing') : t('settings.mcp.page.actions.test')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : undefined}
|
||||
showSaveStatus={false}
|
||||
>
|
||||
|
||||
|
||||
|
||||
{/* Runtime Status - Simplified for connected, expanded for errors */}
|
||||
{!isNewServer && shouldShowFullStatusCard(effectiveRuntimeStatus?.status, authUrl, needsAuthorization, isAuthPolling) && (
|
||||
<div className="mb-6 px-2">
|
||||
<SettingsSection divider={false}>
|
||||
<div className={cn('rounded-lg border p-3', statusCardClass(effectiveRuntimeStatus?.status))}>
|
||||
<div className="space-y-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.status.runtimeStatus')}</span>
|
||||
<span className={SETTINGS_FIELD_LABEL_CLASS}>{t('settings.mcp.page.status.runtimeStatus')}</span>
|
||||
<StatusBadge status={effectiveRuntimeStatus?.status} enabled={enabled} getStatusLabel={getStatusLabel} />
|
||||
</div>
|
||||
<p className="typography-meta text-muted-foreground">{runtimeDescription}</p>
|
||||
@@ -1452,7 +1452,7 @@ export const McpPage: React.FC = () => {
|
||||
<div className="rounded-md border border-[var(--interactive-border)] bg-[var(--surface-background)] px-3 py-3">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.mcp.page.auth.manualFallbackTitle')}</div>
|
||||
<SettingsGroupTitle as="div">{t('settings.mcp.page.auth.manualFallbackTitle')}</SettingsGroupTitle>
|
||||
<p className="mt-1 typography-micro text-muted-foreground">
|
||||
{t('settings.mcp.page.auth.manualFallbackDescription')}
|
||||
</p>
|
||||
@@ -1489,23 +1489,18 @@ export const McpPage: React.FC = () => {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Server Identity */}
|
||||
<div data-settings-item="mcp.server" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.mcp.page.server.title')}</h3>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
<SettingsSection
|
||||
title={t('settings.mcp.page.server.title')}
|
||||
divider={false}
|
||||
settingsItem="mcp.server"
|
||||
contentClassName="space-y-0"
|
||||
>
|
||||
|
||||
{isNewServer && (
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.server.name')}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
||||
<SettingsFieldRow label={t('settings.mcp.page.server.name')}>
|
||||
<Input
|
||||
value={draftName}
|
||||
onChange={(e) => setDraftName(e.target.value.toLowerCase().replace(/[^a-z0-9_-]/g, '-'))}
|
||||
@@ -1514,7 +1509,7 @@ export const McpPage: React.FC = () => {
|
||||
autoFocus
|
||||
/>
|
||||
<Select value={draftScope} onValueChange={(value) => setDraftScope(value as McpScope)}>
|
||||
<SelectTrigger className="!h-7 !w-7 !min-w-0 !px-0 !py-0 justify-center [&>svg:last-child]:hidden" title={draftScope === 'user' ? t('settings.common.scope.global') : t('settings.common.scope.project')}>
|
||||
<SelectTrigger size={SETTINGS_SELECT_SIZE} className="!h-7 !w-7 !min-w-0 !px-0 !py-0 justify-center [&>svg:last-child]:hidden" title={draftScope === 'user' ? t('settings.common.scope.global') : t('settings.common.scope.project')}>
|
||||
{draftScope === 'user' ? <Icon name="user-3" className="h-3.5 w-3.5" /> : <Icon name="folder" className="h-3.5 w-3.5" />}
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
@@ -1532,8 +1527,7 @@ export const McpPage: React.FC = () => {
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsFieldRow>
|
||||
)}
|
||||
|
||||
{/* Import JSON - prominent placement for new servers */}
|
||||
@@ -1553,65 +1547,31 @@ export const McpPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={enabled}
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
setEnabled(!enabled);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
ariaLabel={t('settings.mcp.page.server.enableAria')}
|
||||
<SettingsCheckboxRow
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
label={t('settings.mcp.page.server.enable')}
|
||||
ariaLabel={t('settings.mcp.page.server.enableAria')}
|
||||
/>
|
||||
|
||||
<SettingsStackedField label={t('settings.mcp.page.server.transportMode')}>
|
||||
<SettingsChipGroup
|
||||
aria-label={t('settings.mcp.page.server.transportMode')}
|
||||
value={mcpType}
|
||||
onChange={setMcpType}
|
||||
options={[
|
||||
{ value: 'local', label: t('settings.mcp.page.transport.local') },
|
||||
{ value: 'remote', label: t('settings.mcp.page.transport.remote') },
|
||||
]}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.server.enable')}</span>
|
||||
</div>
|
||||
</SettingsStackedField>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.server.transportMode')}</span>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mcpType === 'local'}
|
||||
onClick={() => setMcpType('local')}
|
||||
className="!font-normal"
|
||||
>
|
||||
{t('settings.mcp.page.transport.local')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="chip"
|
||||
size="xs"
|
||||
aria-pressed={mcpType === 'remote'}
|
||||
onClick={() => setMcpType('remote')}
|
||||
className="!font-normal"
|
||||
>
|
||||
{t('settings.mcp.page.transport.remote')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Connection */}
|
||||
<div data-settings-item="mcp.command" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{mcpType === 'local' ? t('settings.mcp.page.connection.command') : t('settings.mcp.page.connection.serverUrl')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<SettingsSection
|
||||
title={mcpType === 'local' ? t('settings.mcp.page.connection.command') : t('settings.mcp.page.connection.serverUrl')}
|
||||
settingsItem="mcp.command"
|
||||
>
|
||||
{mcpType === 'local' ? (
|
||||
<CommandTextarea
|
||||
value={command}
|
||||
@@ -1630,16 +1590,13 @@ export const McpPage: React.FC = () => {
|
||||
className="font-mono typography-meta"
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
{mcpType === 'remote' && (
|
||||
<div data-settings-item="mcp.advanced" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.mcp.page.advanced.title')}</h3>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<SettingsSection
|
||||
title={t('settings.mcp.page.advanced.title')}
|
||||
settingsItem="mcp.advanced"
|
||||
>
|
||||
<Collapsible
|
||||
open={isAdvancedRemoteOptionsOpen}
|
||||
onOpenChange={setIsAdvancedRemoteOptionsOpen}
|
||||
@@ -1660,9 +1617,10 @@ export const McpPage: React.FC = () => {
|
||||
<CollapsibleContent className="pt-2">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.advanced.timeoutMs')}</span>
|
||||
<div className="flex flex-col gap-2 @xl:flex-row @xl:items-center @xl:gap-8">
|
||||
<div className="flex min-w-0 flex-row items-center gap-1 @xl:w-56 shrink-0">
|
||||
<span className={SETTINGS_FIELD_LABEL_CLASS}>{t('settings.mcp.page.advanced.timeoutMs')}</span>
|
||||
<SettingsInfoHint>{t('settings.mcp.page.advanced.timeoutHint')}</SettingsInfoHint>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
@@ -1678,18 +1636,15 @@ export const McpPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="typography-micro text-muted-foreground sm:pl-64">
|
||||
{t('settings.mcp.page.advanced.timeoutHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 typography-ui-label text-foreground">
|
||||
<SettingsGroupTitle as="div" className="mb-2">
|
||||
{t('settings.mcp.page.advanced.requestHeaders')}
|
||||
{headerEntries.length > 0 && (
|
||||
<span className="ml-1.5 typography-micro text-muted-foreground font-normal">({headerEntries.length})</span>
|
||||
)}
|
||||
</div>
|
||||
</SettingsGroupTitle>
|
||||
<EnvEditor
|
||||
value={headerEntries}
|
||||
onChange={setHeaderEntries}
|
||||
@@ -1713,24 +1668,15 @@ export const McpPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={oauthEnabled}
|
||||
onClick={() => setOauthEnabled(!oauthEnabled)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
setOauthEnabled(!oauthEnabled);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={oauthEnabled} onChange={setOauthEnabled} ariaLabel={t('settings.mcp.page.advanced.oauthAutoDetectionAria')} />
|
||||
<span className="typography-ui-label text-foreground">{t('settings.mcp.page.advanced.oauthAutoDetection')}</span>
|
||||
</div>
|
||||
<SettingsCheckboxRow
|
||||
checked={oauthEnabled}
|
||||
onChange={setOauthEnabled}
|
||||
label={t('settings.mcp.page.advanced.oauthAutoDetection')}
|
||||
ariaLabel={t('settings.mcp.page.advanced.oauthAutoDetectionAria')}
|
||||
info={t('settings.mcp.page.advanced.oauthHint')}
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid gap-3 @xl:grid-cols-2">
|
||||
<Input
|
||||
value={oauthClientId}
|
||||
onChange={(e) => setOauthClientId(e.target.value)}
|
||||
@@ -1769,9 +1715,6 @@ export const McpPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
{t('settings.mcp.page.advanced.oauthHint')}
|
||||
</p>
|
||||
{suggestedRedirectUri && (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
{t('settings.mcp.page.advanced.oauthCallbackHint')}
|
||||
@@ -1782,24 +1725,20 @@ export const McpPage: React.FC = () => {
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</section>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div data-settings-item="mcp.environment" className="mb-2">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.mcp.page.env.title')}
|
||||
{envEntries.length > 0 && (
|
||||
<span className="ml-1.5 typography-micro text-muted-foreground font-normal">
|
||||
({envEntries.length})
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<SettingsSection
|
||||
title={t('settings.mcp.page.env.title')}
|
||||
titleAccessory={
|
||||
envEntries.length > 0 ? (
|
||||
<span className="typography-micro text-muted-foreground font-normal">
|
||||
({envEntries.length})
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
settingsItem="mcp.environment"
|
||||
>
|
||||
{envEntries.length === 0 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -1830,11 +1769,9 @@ export const McpPage: React.FC = () => {
|
||||
removeVariableAria={t('settings.mcp.page.env.removeVariableAria')}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex items-center gap-2 pb-8">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || (!isDirty && !isNewServer)}
|
||||
@@ -1853,8 +1790,8 @@ export const McpPage: React.FC = () => {
|
||||
{t('settings.common.actions.delete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> </SettingsPageLayout>
|
||||
|
||||
|
||||
{/* Import JSON dialog */}
|
||||
<Dialog
|
||||
@@ -1962,6 +1899,7 @@ export const McpPage: React.FC = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ScrollableOverlay>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { SETTINGS_PANEL_TITLE_CLASS } from '@/components/sections/shared/SettingsSection';
|
||||
|
||||
interface McpSidebarProps {
|
||||
onItemSelect?: () => void;
|
||||
@@ -183,7 +184,7 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<div className="border-b px-3 pt-4 pb-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-semibold text-foreground">{t('settings.mcp.sidebar.title')}</h2>
|
||||
<h2 className={SETTINGS_PANEL_TITLE_CLASS}>{t('settings.mcp.sidebar.title')}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
|
||||
Reference in New Issue
Block a user