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:
Serhii Dziupin
2026-07-18 00:11:05 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 1b402fcb67
commit 17f1b24709
103 changed files with 6406 additions and 6110 deletions
@@ -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"