refactor: polish MCP/LSP settings and activity panel UI

- Use indigo accent consistently instead of blue for toggle states
- Replace bulky toggle cards with compact toggle switches in Activity panel
- Fix input focus states to use app-standard indigo glow
- Use semantic nav icons: Server for MCP, Code for LSP
- Style Save button with indigo primary, Delete with subtle destructive hover
- Add Info icon to callout hints
- Center empty states, clean up list item detail text
- Fix indentation in App.tsx settings overlay wiring

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-23 23:03:42 +01:00
co-authored by Copilot
parent efa5c44e07
commit 9c22ac1475
3 changed files with 110 additions and 131 deletions
+73 -95
View File
@@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo, type ReactNode } from 'react';
import { Activity, Bot, Sparkles } from 'lucide-react'; import { Activity, Bot, Server, Code, Sparkles } from 'lucide-react';
import { import {
buildAgentActivityRows, buildAgentActivityRows,
@@ -80,66 +80,60 @@ export function ActivityPanel({
{/* Agent cards */} {/* Agent cards */}
<div className="flex-1 overflow-y-auto px-3 py-3"> <div className="flex-1 overflow-y-auto px-3 py-3">
<div className="space-y-3"> <div className="space-y-3">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-3"> <div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div> <h3 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-zinc-500">
<h3 className="text-[12px] font-semibold text-zinc-200">Session tools</h3> Session tools
<p className="mt-0.5 text-[11px] text-zinc-500"> </h3>
Enable globally configured MCPs and LSPs for this session.
</p>
</div>
{toolsDisabled && ( {toolsDisabled && (
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium text-zinc-400"> <span className="text-[10px] text-zinc-600">
{projectIsScratchpad ? 'Scratchpad disabled' : 'Locked while running'} {projectIsScratchpad ? 'Scratchpad' : 'Running'}
</span> </span>
)} )}
</div> </div>
{projectIsScratchpad ? ( {projectIsScratchpad ? (
<p className="mt-3 text-[11px] leading-relaxed text-zinc-500"> <p className="mt-2 text-[11px] leading-relaxed text-zinc-600">
Scratchpad stays tool-free. Start a project-backed session to use MCPs or LSPs. Start a project-backed session to use MCPs or LSPs.
</p>
) : mcpServers.length === 0 && lspProfiles.length === 0 ? (
<p className="mt-2 text-[11px] leading-relaxed text-zinc-600">
Add MCP servers or LSP profiles in Settings to enable them here.
</p> </p>
) : ( ) : (
<div className="mt-3 space-y-3"> <div className="mt-2 space-y-0.5">
<ToolToggleGroup {mcpServers.map((server) => (
description="Globally configured MCP servers" <ToolToggleRow
emptyMessage="No MCP servers configured in Settings." detail={server.transport === 'local' ? server.command : server.url}
enabledIds={selection.enabledMcpServerIds} disabled={toolsDisabled}
items={mcpServers.map((server) => ({ enabled={selection.enabledMcpServerIds.includes(server.id)}
id: server.id, icon={<Server className="size-3 text-zinc-600" />}
label: server.name, key={server.id}
detail: label={server.name}
server.transport === 'local' onToggle={() =>
? server.command
: server.url,
}))}
onToggle={(id) =>
onUpdateSessionTooling({ onUpdateSessionTooling({
...selection, ...selection,
enabledMcpServerIds: toggleId(selection.enabledMcpServerIds, id), enabledMcpServerIds: toggleId(selection.enabledMcpServerIds, server.id),
}) })
} }
title="MCP servers"
disabled={toolsDisabled}
/> />
<ToolToggleGroup ))}
description="Globally configured LSP profiles" {lspProfiles.map((profile) => (
emptyMessage="No LSP profiles configured in Settings." <ToolToggleRow
enabledIds={selection.enabledLspProfileIds} detail={profile.command}
items={lspProfiles.map((profile) => ({ disabled={toolsDisabled}
id: profile.id, enabled={selection.enabledLspProfileIds.includes(profile.id)}
label: profile.name, icon={<Code className="size-3 text-zinc-600" />}
detail: `${profile.languageId} · ${profile.command}`, key={profile.id}
}))} label={profile.name}
onToggle={(id) => onToggle={() =>
onUpdateSessionTooling({ onUpdateSessionTooling({
...selection, ...selection,
enabledLspProfileIds: toggleId(selection.enabledLspProfileIds, id), enabledLspProfileIds: toggleId(selection.enabledLspProfileIds, profile.id),
}) })
} }
title="LSP profiles"
disabled={toolsDisabled}
/> />
))}
</div> </div>
)} )}
</div> </div>
@@ -235,71 +229,55 @@ export function ActivityPanel({
); );
} }
function ToolToggleGroup({ function ToolToggleRow({
title, label,
description, detail,
items, icon,
enabledIds, enabled,
onToggle,
emptyMessage,
disabled, disabled,
onToggle,
}: { }: {
title: string; label: string;
description: string; detail?: string;
items: Array<{ id: string; label: string; detail?: string }>; icon: ReactNode;
enabledIds: string[]; enabled: boolean;
onToggle: (id: string) => void;
emptyMessage: string;
disabled: boolean; disabled: boolean;
onToggle: () => void;
}) { }) {
return (
<div>
<div className="mb-2">
<h4 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-zinc-500">
{title}
</h4>
<p className="mt-0.5 text-[11px] text-zinc-600">{description}</p>
</div>
{items.length === 0 ? (
<p className="text-[11px] text-zinc-600">{emptyMessage}</p>
) : (
<div className="space-y-1.5">
{items.map((item) => {
const enabled = enabledIds.includes(item.id);
return ( return (
<button <button
className={`flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left transition ${ className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition ${
enabled disabled ? 'cursor-not-allowed opacity-50' : 'hover:bg-zinc-800/60'
? 'border-blue-500/30 bg-blue-500/5' }`}
: 'border-zinc-800 bg-zinc-900/30'
} ${disabled ? 'cursor-not-allowed opacity-60' : 'hover:border-zinc-700 hover:bg-zinc-900/60'}`}
disabled={disabled} disabled={disabled}
key={item.id} onClick={onToggle}
onClick={() => onToggle(item.id)}
type="button" type="button"
> >
<div className="min-w-0"> {icon}
<div className="text-[12px] font-medium text-zinc-200">{item.label}</div> <div className="min-w-0 flex-1">
{item.detail && ( <div className="truncate text-[12px] font-medium text-zinc-300">{label}</div>
<div className="truncate text-[11px] text-zinc-500">{item.detail}</div> {detail && (
<div className="truncate text-[10px] text-zinc-600">{detail}</div>
)} )}
</div> </div>
<span <ToggleSwitch enabled={enabled} />
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
enabled
? 'bg-blue-500/10 text-blue-300'
: 'bg-zinc-800 text-zinc-500'
}`}
>
{enabled ? 'Enabled' : 'Disabled'}
</span>
</button> </button>
); );
})} }
</div>
)} function ToggleSwitch({ enabled }: { enabled: boolean }) {
</div> return (
<span
className={`relative inline-flex h-[16px] w-[28px] shrink-0 items-center rounded-full transition-colors ${
enabled ? 'bg-indigo-500' : 'bg-zinc-700'
}`}
>
<span
className={`inline-block size-[12px] rounded-full bg-white shadow-sm transition-transform ${
enabled ? 'translate-x-[14px]' : 'translate-x-[2px]'
}`}
/>
</span>
); );
} }
+16 -15
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, Cpu, Layers, Plus, Workflow } from 'lucide-react'; import { ChevronLeft, ChevronRight, Code, Cpu, Info, Plus, Server, 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';
@@ -65,8 +65,8 @@ const navGroups: NavGroup[] = [
{ {
label: 'Tooling', label: 'Tooling',
items: [ items: [
{ id: 'mcp-servers', label: 'MCP Servers', icon: <Layers className="size-3.5" /> }, { id: 'mcp-servers', label: 'MCP Servers', icon: <Server className="size-3.5" /> },
{ id: 'lsp-profiles', label: 'LSP Profiles', icon: <Cpu className="size-3.5" /> }, { id: 'lsp-profiles', label: 'LSP Profiles', icon: <Code className="size-3.5" /> },
], ],
}, },
]; ];
@@ -367,8 +367,8 @@ function McpServersSection({
<ToolingListButton <ToolingListButton
detail={ detail={
server.transport === 'local' server.transport === 'local'
? `${server.command || 'No command'} · ${server.tools.length} tool filter${server.tools.length === 1 ? '' : 's'}` ? server.command || 'No command set'
: `${server.url} · ${server.transport.toUpperCase()}` : server.url || 'No URL set'
} }
key={server.id} key={server.id}
label={server.name} label={server.name}
@@ -407,10 +407,10 @@ function LspProfilesSection({
)} )}
{profiles.map((profile) => ( {profiles.map((profile) => (
<ToolingListButton <ToolingListButton
detail={`${profile.languageId} · ${profile.command || 'No command'}`} detail={profile.command || 'No command set'}
key={profile.id} key={profile.id}
label={profile.name} label={profile.name}
meta={profile.fileExtensions.join(', ')} meta={profile.languageId}
onClick={() => onEditProfile(profile)} onClick={() => onEditProfile(profile)}
/> />
))} ))}
@@ -672,7 +672,7 @@ function ToolingListButton({
function EmptyState({ children }: { children: ReactNode }) { function EmptyState({ children }: { children: ReactNode }) {
return ( return (
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/30 px-4 py-6 text-[12px] leading-relaxed text-zinc-500"> <div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-900/20 px-5 py-8 text-center text-[12px] leading-relaxed text-zinc-500">
{children} {children}
</div> </div>
); );
@@ -716,7 +716,7 @@ function ToolingEditorShell({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{onDelete && ( {onDelete && (
<button <button
className="rounded-lg border border-zinc-700 px-3 py-1.5 text-[13px] font-medium text-zinc-300 transition hover:border-red-500/40 hover:bg-red-500/10 hover:text-red-300" 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"
onClick={() => void onDelete()} onClick={() => void onDelete()}
type="button" type="button"
> >
@@ -724,7 +724,7 @@ function ToolingEditorShell({
</button> </button>
)} )}
<button <button
className="rounded-lg bg-zinc-100 px-3 py-1.5 text-[13px] font-semibold text-zinc-900 transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50" 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"
disabled={disableSave} disabled={disableSave}
onClick={() => void onSave()} onClick={() => void onSave()}
type="button" type="button"
@@ -783,7 +783,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-zinc-600" 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"
inputMode={inputMode} inputMode={inputMode}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
@@ -805,7 +805,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-zinc-600" 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"
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
rows={rows} rows={rows}
@@ -825,7 +825,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-zinc-600" 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"
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
value={value} value={value}
> >
@@ -840,8 +840,9 @@ function SelectInput({
function InfoCallout({ children }: { children: ReactNode }) { function InfoCallout({ children }: { children: ReactNode }) {
return ( return (
<div className="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-xl border border-zinc-800 bg-zinc-900/30 px-4 py-3 text-[12px] leading-relaxed text-zinc-500">
{children} <Info className="mt-0.5 size-3.5 shrink-0 text-zinc-600" />
<span>{children}</span>
</div> </div>
); );
} }