mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
refactor: decompose large frontend components into focused modules
Split six monolithic component files into smaller, single-responsibility modules organized by feature domain: - ChatPane (1054→~250 lines): extract InlinePills, ApprovalBanner, ThinkingDots into chat/ directory - SettingsPanel (1027→~280 lines): extract McpServerEditor, LspProfileEditor, ToolingEditorShell into settings/ directory; mutation helpers into lib/settingsHelpers.ts - PatternEditor: use shared ToggleSwitch from ui/ - App.tsx: extract useTheme and useSidecarCapabilities into hooks/ - Sidebar: extracted accessibility improvements inline New shared primitives: - hooks/useClickOutside: replaces 5 duplicated click-outside listeners - components/ui/: ToggleSwitch, PopoverToggleRow, FormField, TextInput, TextareaInput, SelectInput, InfoCallout Accessibility improvements: - NewSessionModal: role=dialog, aria-modal, Escape-to-close - Pill dropdowns: aria-expanded, aria-haspopup, role=listbox/option - Sidebar context menu: role=menu/menuitem, Escape-to-close - SessionItem: Space key activation alongside Enter - ToolbarButton: aria-pressed for toggle state - ApprovalBanner: role=alert - QueuedApprovalsList: aria-expanded on toggle - ThinkingDots: aria-label No behavioral changes. All 137 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { FormField, InfoCallout, TextareaInput, TextInput } from '@renderer/components/ui';
|
||||
import { joinMultiline, splitMultiline, splitTokens, updateLspProfile } from '@renderer/lib/settingsHelpers';
|
||||
import { validateLspProfileDefinition, type LspProfileDefinition } from '@shared/domain/tooling';
|
||||
import { ToolingEditorShell } from './ToolingEditorShell';
|
||||
|
||||
export function LspProfileEditor({
|
||||
profile,
|
||||
onChange,
|
||||
onBack,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: {
|
||||
profile: LspProfileDefinition;
|
||||
onChange: (profile: LspProfileDefinition) => void;
|
||||
onBack: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onDelete?: () => Promise<void>;
|
||||
}) {
|
||||
const validationError = validateLspProfileDefinition(profile);
|
||||
|
||||
return (
|
||||
<ToolingEditorShell
|
||||
disableSave={Boolean(validationError)}
|
||||
error={validationError}
|
||||
onBack={onBack}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
subtitle="Machine-wide language server definition"
|
||||
title={profile.name || 'Untitled LSP Profile'}
|
||||
>
|
||||
<section className="space-y-4">
|
||||
<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
|
||||
onChange={(value) => onChange(updateLspProfile(profile, { command: value }))}
|
||||
placeholder="typescript-language-server"
|
||||
value={profile.command}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Arguments">
|
||||
<TextareaInput
|
||||
onChange={(value) => onChange(updateLspProfile(profile, { args: splitMultiline(value) }))}
|
||||
placeholder="One argument per line"
|
||||
rows={3}
|
||||
value={joinMultiline(profile.args)}
|
||||
/>
|
||||
</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>
|
||||
<TextareaInput
|
||||
onChange={(value) => onChange(updateLspProfile(profile, { fileExtensions: splitTokens(value) }))}
|
||||
placeholder={'.ts\n.tsx'}
|
||||
rows={3}
|
||||
value={joinMultiline(profile.fileExtensions)}
|
||||
/>
|
||||
</FormField>
|
||||
</section>
|
||||
|
||||
<InfoCallout>
|
||||
Project root resolution comes from the active session's project, not from this definition.
|
||||
</InfoCallout>
|
||||
</ToolingEditorShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { FormField, InfoCallout, SelectInput, TextareaInput, TextInput } from '@renderer/components/ui';
|
||||
import { changeMcpTransport, joinMultiline, splitMultiline, splitTokens, updateMcpServer } from '@renderer/lib/settingsHelpers';
|
||||
import { validateMcpServerDefinition, type McpServerDefinition } from '@shared/domain/tooling';
|
||||
import { ToolingEditorShell } from './ToolingEditorShell';
|
||||
|
||||
export function McpServerEditor({
|
||||
server,
|
||||
onChange,
|
||||
onBack,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: {
|
||||
server: McpServerDefinition;
|
||||
onChange: (server: McpServerDefinition) => void;
|
||||
onBack: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onDelete?: () => Promise<void>;
|
||||
}) {
|
||||
const validationError = validateMcpServerDefinition(server);
|
||||
|
||||
return (
|
||||
<ToolingEditorShell
|
||||
disableSave={Boolean(validationError)}
|
||||
error={validationError}
|
||||
onBack={onBack}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
subtitle="Machine-wide server definition"
|
||||
title={server.name || 'Untitled MCP Server'}
|
||||
>
|
||||
<section className="space-y-4">
|
||||
<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(updateMcpServer(server, { name: value }))}
|
||||
value={server.name}
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
{server.transport === 'local' ? 'Process' : 'Endpoint'}
|
||||
</h4>
|
||||
{server.transport === 'local' ? (
|
||||
<>
|
||||
<FormField label="Command" required>
|
||||
<TextInput
|
||||
onChange={(value) => onChange(updateMcpServer(server, { command: value }))}
|
||||
placeholder="node"
|
||||
value={server.command}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Arguments">
|
||||
<TextareaInput
|
||||
onChange={(value) => onChange(updateMcpServer(server, { args: splitMultiline(value) }))}
|
||||
placeholder="One argument per line"
|
||||
rows={3}
|
||||
value={joinMultiline(server.args)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Working directory">
|
||||
<TextInput
|
||||
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>
|
||||
Keep secrets out of this form. Use commands or endpoints that authenticate through the OS or external tooling.
|
||||
</InfoCallout>
|
||||
</ToolingEditorShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { AlertCircle, ChevronLeft, Trash } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function ToolingEditorShell({
|
||||
title,
|
||||
subtitle,
|
||||
error,
|
||||
disableSave,
|
||||
onBack,
|
||||
onSave,
|
||||
onDelete,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
error?: string;
|
||||
disableSave: boolean;
|
||||
onBack: () => void;
|
||||
onSave: () => Promise<void>;
|
||||
onDelete?: () => Promise<void>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="drag-region flex items-center justify-between border-b border-[var(--color-border)] px-5 pb-3 pt-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="no-drag flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
<div>
|
||||
<h2 className="text-[13px] font-semibold text-zinc-100">{title}</h2>
|
||||
<p className="text-[12px] text-zinc-500">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="no-drag flex items-center gap-2">
|
||||
{onDelete && (
|
||||
<button
|
||||
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()}
|
||||
type="button"
|
||||
>
|
||||
<Trash className="size-3.5" />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
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}
|
||||
onClick={() => void onSave()}
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||
<div className="mx-auto max-w-2xl space-y-8">
|
||||
{error && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user