feat: redesign UI for modern look and clean UX

- Refined dark theme with zinc palette and indigo accents
- Clean sidebar showing only project/session tree
- Moved pattern management to full-screen settings panel
- New session modal with project + pattern picker
- Modern chat with avatar icons, inline send button, Enter-to-send
- Welcome pane when no session is selected
- Added lucide-react for consistent iconography
- Custom scrollbar styling and auto-resize textarea
- Removed clutter: no sidebar pattern list, no verbose headers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot CLI
2026-03-21 11:22:51 +01:00
co-authored by Copilot
parent 88ca754e7b
commit 32f8de6485
11 changed files with 911 additions and 598 deletions
+218 -206
View File
@@ -1,3 +1,5 @@
import { AlertCircle, CheckCircle, ChevronLeft, Plus, Trash2 } from 'lucide-react';
import { validatePatternDefinition, type OrchestrationMode, type PatternDefinition } from '@shared/domain/pattern';
interface PatternEditorProps {
@@ -6,244 +8,254 @@ interface PatternEditorProps {
onChange: (pattern: PatternDefinition) => void;
onDelete?: () => void;
onSave: () => void;
onBack: () => void;
}
const modes: OrchestrationMode[] = ['single', 'sequential', 'concurrent', 'handoff', 'group-chat', 'magentic'];
export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave }: PatternEditorProps) {
function InputField({
label,
value,
onChange,
multiline,
placeholder,
}: {
label: string;
value: string;
onChange: (value: string) => void;
multiline?: boolean;
placeholder?: string;
}) {
const baseClasses =
'w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 placeholder-zinc-600 outline-none transition focus:border-indigo-500/50';
return (
<label className="block space-y-1.5">
<span className="text-[12px] font-medium text-zinc-400">{label}</span>
{multiline ? (
<textarea
className={`${baseClasses} min-h-20 resize-y`}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
value={value}
/>
) : (
<input
className={baseClasses}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
value={value}
/>
)}
</label>
);
}
export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave, onBack }: PatternEditorProps) {
const issues = validatePatternDefinition(pattern);
function updateAgent(agentId: string, patch: Record<string, string>) {
onChange({
...pattern,
agents: pattern.agents.map((a) => (a.id === agentId ? { ...a, ...patch } : a)),
});
}
return (
<div className="flex h-full flex-col">
<div className="border-b border-slate-800 px-8 py-6">
<div className="flex items-start justify-between gap-4">
{/* Header */}
<div className="flex items-center justify-between border-b border-zinc-800 px-6 py-3">
<div className="flex items-center gap-3">
<button
className="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>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">Pattern</div>
<h2 className="mt-2 text-2xl font-semibold text-white">{pattern.name || 'Untitled pattern'}</h2>
<p className="mt-2 max-w-3xl text-sm text-slate-400">
Define a reusable orchestration blueprint that can be launched against any project in the
workspace.
<h3 className="text-sm font-semibold text-zinc-100">
{pattern.name || 'Untitled pattern'}
</h3>
<p className="text-[12px] text-zinc-500">
{isBuiltin ? 'Built-in pattern' : 'Custom pattern'}
</p>
</div>
<div className="flex gap-3">
{!isBuiltin && onDelete ? (
<button
className="rounded-lg border border-rose-500/40 px-4 py-2 text-sm font-medium text-rose-200 hover:bg-rose-500/10"
onClick={onDelete}
type="button"
>
Delete
</button>
) : null}
</div>
<div className="flex items-center gap-2">
{!isBuiltin && onDelete && (
<button
className="rounded-lg bg-sky-500 px-4 py-2 text-sm font-medium text-slate-950 hover:bg-sky-400"
onClick={onSave}
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={onDelete}
type="button"
>
Save Pattern
<Trash2 className="size-3.5" />
Delete
</button>
</div>
)}
<button
className="rounded-lg bg-indigo-600 px-4 py-1.5 text-[13px] font-medium text-white transition hover:bg-indigo-500"
onClick={onSave}
type="button"
>
Save
</button>
</div>
</div>
<div className="grid flex-1 grid-cols-[minmax(0,1fr)_320px] gap-0 overflow-hidden">
<div className="overflow-y-auto px-8 py-6">
<div className="space-y-8">
<section className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-200">Name</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) => onChange({ ...pattern, name: event.target.value })}
value={pattern.name}
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-200">Mode</span>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-5">
<div className="mx-auto max-w-2xl space-y-6">
{/* Validation banner */}
{issues.length > 0 ? (
<div className="space-y-2">
{issues.map((issue, i) => (
<div
className={`flex items-start gap-2 rounded-lg px-3 py-2 text-[13px] ${
issue.level === 'error'
? 'bg-red-500/10 text-red-300'
: 'bg-amber-500/10 text-amber-300'
}`}
key={`${issue.field ?? 'v'}-${i}`}
>
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
{issue.message}
</div>
))}
</div>
) : (
<div className="flex items-center gap-2 rounded-lg bg-emerald-500/10 px-3 py-2 text-[13px] text-emerald-300">
<CheckCircle className="size-3.5" />
Pattern is valid and ready to use
</div>
)}
{/* Basic fields */}
<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">
<InputField
label="Name"
onChange={(v) => onChange({ ...pattern, name: v })}
placeholder="Pattern name"
value={pattern.name}
/>
<label className="block space-y-1.5">
<span className="text-[12px] font-medium text-zinc-400">Mode</span>
<select
className="w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
mode: event.target.value as OrchestrationMode,
})
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={(e) =>
onChange({ ...pattern, mode: e.target.value as OrchestrationMode })
}
value={pattern.mode}
>
{modes.map((mode) => (
<option
key={mode}
value={mode}
>
{mode}
{modes.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-200">Description</span>
<textarea
className="min-h-24 w-full rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100"
onChange={(event) => onChange({ ...pattern, description: event.target.value })}
value={pattern.description}
/>
</label>
</section>
</div>
<InputField
label="Description"
multiline
onChange={(v) => onChange({ ...pattern, description: v })}
placeholder="What this pattern does..."
value={pattern.description}
/>
</section>
<section>
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-white">Agents</h3>
<p className="mt-1 text-sm text-slate-400">
Configure the participating Copilot-backed agents and their model selections.
</p>
</div>
<button
className="rounded-lg border border-slate-700 px-3 py-2 text-sm font-medium text-slate-100 hover:bg-slate-800"
onClick={() =>
onChange({
...pattern,
agents: [
...pattern.agents,
{
id: `agent-${crypto.randomUUID()}`,
name: `Agent ${pattern.agents.length + 1}`,
description: 'New participant',
instructions: 'You are a helpful specialist in this orchestration.',
model: 'gpt-5.4',
},
],
})
}
type="button"
{/* Agents */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
Agents ({pattern.agents.length})
</h4>
<button
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1 text-[12px] font-medium text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
onClick={() =>
onChange({
...pattern,
agents: [
...pattern.agents,
{
id: `agent-${crypto.randomUUID()}`,
name: `Agent ${pattern.agents.length + 1}`,
description: '',
instructions: '',
model: 'gpt-5.4',
},
],
})
}
type="button"
>
<Plus className="size-3" />
Add
</button>
</div>
<div className="space-y-3">
{pattern.agents.map((agent, index) => (
<div
className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4"
key={agent.id}
>
Add Agent
</button>
</div>
<div className="space-y-4">
{pattern.agents.map((agent, index) => (
<div
className="rounded-2xl border border-slate-800 bg-slate-900/70 p-4"
key={agent.id}
>
<div className="mb-4 flex items-center justify-between gap-4">
<div className="text-sm font-medium text-slate-200">Agent {index + 1}</div>
{pattern.agents.length > 1 ? (
<button
className="text-sm text-rose-200 hover:text-rose-100"
onClick={() =>
onChange({
...pattern,
agents: pattern.agents.filter((current) => current.id !== agent.id),
})
}
type="button"
>
Remove
</button>
) : null}
<div className="mb-3 flex items-center justify-between">
<span className="text-[12px] font-medium text-zinc-500">
Agent {index + 1}
</span>
{pattern.agents.length > 1 && (
<button
className="text-[12px] text-zinc-600 transition hover:text-red-400"
onClick={() =>
onChange({
...pattern,
agents: pattern.agents.filter((a) => a.id !== agent.id),
})
}
type="button"
>
Remove
</button>
)}
</div>
<div className="grid gap-3 sm:grid-cols-2">
<InputField
label="Name"
onChange={(v) => updateAgent(agent.id, { name: v })}
value={agent.name}
/>
<InputField
label="Model"
onChange={(v) => updateAgent(agent.id, { model: v })}
placeholder="e.g. gpt-5.4"
value={agent.model}
/>
<div className="sm:col-span-2">
<InputField
label="Description"
onChange={(v) => updateAgent(agent.id, { description: v })}
value={agent.description}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-300">Name</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, name: event.target.value } : current,
),
})
}
value={agent.name}
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-300">Model</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, model: event.target.value } : current,
),
})
}
value={agent.model}
/>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-300">Description</span>
<input
className="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, description: event.target.value } : current,
),
})
}
value={agent.description}
/>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-sm font-medium text-slate-300">Instructions</span>
<textarea
className="min-h-28 w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100"
onChange={(event) =>
onChange({
...pattern,
agents: pattern.agents.map((current) =>
current.id === agent.id ? { ...current, instructions: event.target.value } : current,
),
})
}
value={agent.instructions}
/>
</label>
<div className="sm:col-span-2">
<InputField
label="Instructions"
multiline
onChange={(v) => updateAgent(agent.id, { instructions: v })}
placeholder="System prompt for this agent..."
value={agent.instructions}
/>
</div>
</div>
))}
</div>
</section>
</div>
</div>
<aside className="border-l border-slate-800 bg-slate-900/70 px-6 py-6">
<h3 className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Validation</h3>
<div className="mt-4 space-y-3">
{issues.length === 0 ? (
<div className="rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-200">
This pattern is ready to launch.
</div>
) : (
issues.map((issue, index) => (
<div
className={`rounded-xl px-4 py-3 text-sm ${
issue.level === 'error'
? 'border border-rose-500/40 bg-rose-500/10 text-rose-200'
: 'border border-amber-500/40 bg-amber-500/10 text-amber-200'
}`}
key={`${issue.field ?? 'issue'}-${index}`}
>
<div className="font-medium">{issue.level.toUpperCase()}</div>
<div className="mt-1">{issue.message}</div>
</div>
))
)}
{isBuiltin ? (
<div className="rounded-xl border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-slate-400">
Built-in patterns can be edited and saved, but they cannot be deleted from the global library.
</div>
) : null}
</div>
</aside>
))}
</div>
</section>
</div>
</div>
</div>
);