refactor: align tooling editor styling with PatternEditor conventions

- Match input border/bg to PatternEditor standard (border-zinc-700 bg-zinc-900)
- Tone down form labels from zinc-300 to zinc-400
- Use borderless icon+text Delete button matching PatternEditor
- Group editor fields into named sections with uppercase headers
- Show live entity name in editor title bar instead of generic label
- Shorten header subtitle to a brief role description
- Use AlertCircle icon in validation banners matching PatternEditor style
- Reduce textarea rows from 4 to 3 for tighter layout
- Match body spacing to PatternEditor (space-y-8, px-6 py-5)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-23 23:08:15 +01:00
co-authored by Copilot
parent 9c22ac1475
commit 9180ba8917
+149 -121
View File
@@ -1,5 +1,5 @@
import { useState, type HTMLAttributes, type ReactNode } from 'react'; import { useState, type HTMLAttributes, type ReactNode } from 'react';
import { ChevronLeft, ChevronRight, Code, Cpu, Info, Plus, Server, Workflow } from 'lucide-react'; import { AlertCircle, ChevronLeft, ChevronRight, Code, Cpu, Info, Plus, Server, Trash, Workflow } from 'lucide-react';
import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard'; import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
import { PatternEditor } from '@renderer/components/PatternEditor'; import { PatternEditor } from '@renderer/components/PatternEditor';
@@ -436,93 +436,108 @@ function McpServerEditor({
return ( return (
<ToolingEditorShell <ToolingEditorShell
description="Configure a machine-wide MCP server. Sessions can opt into this server from the Activity panel."
disableSave={Boolean(validationError)} disableSave={Boolean(validationError)}
error={validationError} error={validationError}
onBack={onBack} onBack={onBack}
onDelete={onDelete} onDelete={onDelete}
onSave={onSave} onSave={onSave}
title="MCP Server" subtitle="Machine-wide server definition"
title={server.name || 'Untitled MCP Server'}
> >
<div className="grid gap-4 md:grid-cols-2"> <section className="space-y-4">
<FormField label="Name" required> <h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
<TextInput General
onChange={(value) => onChange(updateMcpServer(server, { name: value }))} </h4>
value={server.name} <div className="grid gap-4 sm:grid-cols-2">
/> <FormField label="Name" required>
</FormField>
<FormField label="Transport" required>
<SelectInput
onChange={(value) => onChange(changeMcpTransport(server, value as McpServerDefinition['transport']))}
options={[
{ value: 'local', label: 'Local process' },
{ value: 'http', label: 'HTTP' },
{ value: 'sse', label: 'SSE' },
]}
value={server.transport}
/>
</FormField>
</div>
{server.transport === 'local' ? (
<div className="grid gap-4 md:grid-cols-2">
<FormField className="md:col-span-2" label="Command" required>
<TextInput <TextInput
onChange={(value) => onChange(updateMcpServer(server, { command: value }))} onChange={(value) => onChange(updateMcpServer(server, { name: value }))}
placeholder="node" value={server.name}
value={server.command}
/> />
</FormField> </FormField>
<FormField className="md:col-span-2" label="Arguments"> <FormField label="Transport" required>
<TextareaInput <SelectInput
onChange={(value) => onChange(updateMcpServer(server, { args: splitMultiline(value) }))} onChange={(value) => onChange(changeMcpTransport(server, value as McpServerDefinition['transport']))}
placeholder="One argument per line" options={[
rows={4} { value: 'local', label: 'Local process' },
value={joinMultiline(server.args)} { value: 'http', label: 'HTTP' },
/> { value: 'sse', label: 'SSE' },
</FormField> ]}
<FormField className="md:col-span-2" label="Working directory"> value={server.transport}
<TextInput
onChange={(value) => onChange(updateMcpServer(server, { cwd: value || undefined }))}
placeholder="Optional"
value={server.cwd ?? ''}
/> />
</FormField> </FormField>
</div> </div>
) : ( </section>
<FormField label="Server URL" required>
<TextInput
onChange={(value) => onChange(updateMcpServer(server, { url: value }))}
placeholder="https://example.com/mcp"
value={server.url}
/>
</FormField>
)}
<div className="grid gap-4 md:grid-cols-2"> <section className="space-y-4">
<FormField label="Allowed tools"> <h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
<TextareaInput {server.transport === 'local' ? 'Process' : 'Endpoint'}
onChange={(value) => onChange(updateMcpServer(server, { tools: splitTokens(value) }))} </h4>
placeholder="Use * for all tools, or list one tool per line" {server.transport === 'local' ? (
rows={4} <>
value={joinMultiline(server.tools)} <FormField label="Command" required>
/> <TextInput
</FormField> onChange={(value) => onChange(updateMcpServer(server, { command: value }))}
<FormField label="Timeout (ms)"> placeholder="node"
<TextInput value={server.command}
inputMode="numeric" />
onChange={(value) => </FormField>
onChange( <FormField label="Arguments">
updateMcpServer(server, { <TextareaInput
timeoutMs: value.trim() ? Number(value) : undefined, onChange={(value) => onChange(updateMcpServer(server, { args: splitMultiline(value) }))}
}), placeholder="One argument per line"
) rows={3}
} value={joinMultiline(server.args)}
placeholder="Optional" />
value={server.timeoutMs?.toString() ?? ''} </FormField>
/> <FormField label="Working directory">
</FormField> <TextInput
</div> onChange={(value) => onChange(updateMcpServer(server, { cwd: value || undefined }))}
placeholder="Optional — defaults to project root"
value={server.cwd ?? ''}
/>
</FormField>
</>
) : (
<FormField label="Server URL" required>
<TextInput
onChange={(value) => onChange(updateMcpServer(server, { url: value }))}
placeholder="https://example.com/mcp"
value={server.url}
/>
</FormField>
)}
</section>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
Advanced
</h4>
<div className="grid gap-4 sm:grid-cols-2">
<FormField label="Allowed tools">
<TextareaInput
onChange={(value) => onChange(updateMcpServer(server, { tools: splitTokens(value) }))}
placeholder="* for all, or one per line"
rows={3}
value={joinMultiline(server.tools)}
/>
</FormField>
<FormField label="Timeout (ms)">
<TextInput
inputMode="numeric"
onChange={(value) =>
onChange(
updateMcpServer(server, {
timeoutMs: value.trim() ? Number(value) : undefined,
}),
)
}
placeholder="Optional"
value={server.timeoutMs?.toString() ?? ''}
/>
</FormField>
</div>
</section>
<InfoCallout> <InfoCallout>
Keep secrets out of this form. Use commands or endpoints that authenticate through the OS or external tooling. Keep secrets out of this form. Use commands or endpoints that authenticate through the OS or external tooling.
@@ -548,59 +563,72 @@ function LspProfileEditor({
return ( return (
<ToolingEditorShell <ToolingEditorShell
description="Configure a machine-wide LSP command. Sessions can opt into this profile from the Activity panel."
disableSave={Boolean(validationError)} disableSave={Boolean(validationError)}
error={validationError} error={validationError}
onBack={onBack} onBack={onBack}
onDelete={onDelete} onDelete={onDelete}
onSave={onSave} onSave={onSave}
title="LSP Profile" subtitle="Machine-wide language server definition"
title={profile.name || 'Untitled LSP Profile'}
> >
<div className="grid gap-4 md:grid-cols-2"> <section className="space-y-4">
<FormField label="Name" required> <h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
General
</h4>
<div className="grid gap-4 sm:grid-cols-2">
<FormField label="Name" required>
<TextInput
onChange={(value) => onChange(updateLspProfile(profile, { name: value }))}
value={profile.name}
/>
</FormField>
<FormField label="Language ID" required>
<TextInput
onChange={(value) => onChange(updateLspProfile(profile, { languageId: value }))}
placeholder="typescript"
value={profile.languageId}
/>
</FormField>
</div>
</section>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
Server
</h4>
<FormField label="Command" required>
<TextInput <TextInput
onChange={(value) => onChange(updateLspProfile(profile, { name: value }))} onChange={(value) => onChange(updateLspProfile(profile, { command: value }))}
value={profile.name} placeholder="typescript-language-server"
value={profile.command}
/> />
</FormField> </FormField>
<FormField label="Language ID" required>
<TextInput
onChange={(value) => onChange(updateLspProfile(profile, { languageId: value }))}
placeholder="typescript"
value={profile.languageId}
/>
</FormField>
</div>
<FormField label="Command" required>
<TextInput
onChange={(value) => onChange(updateLspProfile(profile, { command: value }))}
placeholder="typescript-language-server"
value={profile.command}
/>
</FormField>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="Arguments"> <FormField label="Arguments">
<TextareaInput <TextareaInput
onChange={(value) => onChange(updateLspProfile(profile, { args: splitMultiline(value) }))} onChange={(value) => onChange(updateLspProfile(profile, { args: splitMultiline(value) }))}
placeholder="One argument per line" placeholder="One argument per line"
rows={4} rows={3}
value={joinMultiline(profile.args)} value={joinMultiline(profile.args)}
/> />
</FormField> </FormField>
</section>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
File matching
</h4>
<FormField label="File extensions" required> <FormField label="File extensions" required>
<TextareaInput <TextareaInput
onChange={(value) => onChange(updateLspProfile(profile, { fileExtensions: splitTokens(value) }))} onChange={(value) => onChange(updateLspProfile(profile, { fileExtensions: splitTokens(value) }))}
placeholder={'.ts\n.tsx'} placeholder={'.ts\n.tsx'}
rows={4} rows={3}
value={joinMultiline(profile.fileExtensions)} value={joinMultiline(profile.fileExtensions)}
/> />
</FormField> </FormField>
</div> </section>
<InfoCallout> <InfoCallout>
Profiles are global definitions only. Project root resolution still comes from the active session's project. Project root resolution comes from the active session's project, not from this definition.
</InfoCallout> </InfoCallout>
</ToolingEditorShell> </ToolingEditorShell>
); );
@@ -680,7 +708,7 @@ function EmptyState({ children }: { children: ReactNode }) {
function ToolingEditorShell({ function ToolingEditorShell({
title, title,
description, subtitle,
error, error,
disableSave, disableSave,
onBack, onBack,
@@ -689,7 +717,7 @@ function ToolingEditorShell({
children, children,
}: { }: {
title: string; title: string;
description: string; subtitle: string;
error?: string; error?: string;
disableSave: boolean; disableSave: boolean;
onBack: () => void; onBack: () => void;
@@ -699,7 +727,7 @@ function ToolingEditorShell({
}) { }) {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex items-center justify-between gap-3 border-b border-[var(--color-border)] px-5 pb-3 pt-12"> <div className="flex items-center justify-between border-b border-[var(--color-border)] px-5 pb-3 pt-12">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
className="flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200" className="flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
@@ -710,21 +738,22 @@ function ToolingEditorShell({
</button> </button>
<div> <div>
<h2 className="text-sm font-semibold text-zinc-100">{title}</h2> <h2 className="text-sm font-semibold text-zinc-100">{title}</h2>
<p className="mt-0.5 text-[12px] text-zinc-500">{description}</p> <p className="text-[12px] text-zinc-500">{subtitle}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{onDelete && ( {onDelete && (
<button <button
className="rounded-lg border border-zinc-800 px-3 py-1.5 text-[13px] font-medium text-zinc-400 transition hover:border-red-500/30 hover:bg-red-500/10 hover:text-red-300" className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] text-red-400 transition hover:bg-red-500/10"
onClick={() => void onDelete()} onClick={() => void onDelete()}
type="button" type="button"
> >
<Trash className="size-3.5" />
Delete Delete
</button> </button>
)} )}
<button <button
className="rounded-lg bg-indigo-600 px-3 py-1.5 text-[13px] font-semibold text-white transition hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40" className="rounded-lg bg-indigo-600 px-4 py-1.5 text-[13px] font-medium text-white transition hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
disabled={disableSave} disabled={disableSave}
onClick={() => void onSave()} onClick={() => void onSave()}
type="button" type="button"
@@ -734,10 +763,11 @@ function ToolingEditorShell({
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto px-6 py-5">
<div className="mx-auto max-w-2xl space-y-5 px-8 py-6"> <div className="mx-auto max-w-2xl space-y-8">
{error && ( {error && (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-[12px] text-amber-200"> <div className="flex items-start gap-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[13px] text-amber-300">
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
{error} {error}
</div> </div>
)} )}
@@ -751,19 +781,17 @@ function ToolingEditorShell({
function FormField({ function FormField({
label, label,
required, required,
className,
children, children,
}: { }: {
label: string; label: string;
required?: boolean; required?: boolean;
className?: string;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<label className={`block ${className ?? ''}`}> <label className="block">
<span className="mb-1.5 block text-[12px] font-medium text-zinc-300"> <span className="mb-1.5 block text-[12px] font-medium text-zinc-400">
{label} {label}
{required && <span className="ml-1 text-amber-300">*</span>} {required && <span className="ml-1 text-amber-400">*</span>}
</span> </span>
{children} {children}
</label> </label>
@@ -783,7 +811,7 @@ function TextInput({
}) { }) {
return ( return (
<input <input
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-[13px] text-zinc-100 outline-none transition placeholder:text-zinc-600 focus:border-indigo-500/50" className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition placeholder:text-zinc-600 focus:border-indigo-500/50"
inputMode={inputMode} inputMode={inputMode}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
@@ -805,7 +833,7 @@ function TextareaInput({
}) { }) {
return ( return (
<textarea <textarea
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-[13px] text-zinc-100 outline-none transition placeholder:text-zinc-600 focus:border-indigo-500/50" className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition placeholder:text-zinc-600 focus:border-indigo-500/50"
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
rows={rows} rows={rows}
@@ -825,7 +853,7 @@ function SelectInput({
}) { }) {
return ( return (
<select <select
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-3 py-2 text-[13px] text-zinc-100 outline-none transition focus:border-indigo-500/50" className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition focus:border-indigo-500/50"
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
value={value} value={value}
> >
@@ -840,7 +868,7 @@ function SelectInput({
function InfoCallout({ children }: { children: ReactNode }) { function InfoCallout({ children }: { children: ReactNode }) {
return ( return (
<div className="flex items-start gap-2.5 rounded-xl border border-zinc-800 bg-zinc-900/30 px-4 py-3 text-[12px] leading-relaxed text-zinc-500"> <div className="flex items-start gap-2.5 rounded-lg border border-zinc-800 bg-zinc-900/30 px-3 py-2.5 text-[12px] leading-relaxed text-zinc-500">
<Info className="mt-0.5 size-3.5 shrink-0 text-zinc-600" /> <Info className="mt-0.5 size-3.5 shrink-0 text-zinc-600" />
<span>{children}</span> <span>{children}</span>
</div> </div>