mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 05:13:58 +02:00
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user