[codex] Polish agent selectors and settings layout (#283)

This commit is contained in:
Wes
2026-04-09 09:04:37 -07:00
committed by GitHub
parent 4d44f4741f
commit 3d130a1460
10 changed files with 187 additions and 194 deletions

View File

@@ -1,6 +1,7 @@
import { isCatalogPersonaSelected } from "@/features/agents/lib/catalog";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import type { AgentPersona } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { promptPreview } from "@/shared/lib/promptPreview";
import {
Sheet,
@@ -74,7 +75,13 @@ export function PersonaCatalogDetailsSheet({
isSelected,
)}
aria-pressed={isSelected}
className="w-full rounded-xl border border-border/70 bg-card/70 p-4 text-left transition-colors hover:border-primary/40 hover:bg-primary/[0.03]"
className={cn(
"w-full rounded-xl border p-4 text-left transition-[background-color,border-color,box-shadow] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2",
isSelected
? "border-primary bg-primary/10 text-foreground"
: "border-border/80 bg-background/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
isPending && "cursor-not-allowed opacity-70",
)}
data-state={isSelected ? "selected" : "available"}
data-testid={`persona-catalog-detail-selection-target-${persona.id}`}
disabled={isPending}

View File

@@ -80,13 +80,13 @@ export function PersonaCatalogSection({
return (
<div
className={cn(
"group relative flex flex-col gap-4 rounded-xl border bg-card/80 p-3 shadow-sm transition-colors",
"group relative flex flex-col gap-4 rounded-xl border p-3 shadow-sm transition-[background-color,border-color,box-shadow]",
isPending
? "cursor-not-allowed opacity-70"
: "cursor-pointer hover:border-primary/40 hover:bg-primary/[0.03]",
: "cursor-pointer",
isSelected
? "border-primary/40 bg-primary/[0.04]"
: "border-border/70",
? "border-primary bg-primary/10 text-foreground"
: "border-border/80 bg-background/60 hover:bg-accent hover:text-accent-foreground",
)}
data-testid={`persona-catalog-card-${persona.id}`}
data-state={isSelected ? "selected" : "available"}
@@ -98,7 +98,7 @@ export function PersonaCatalogSection({
isSelected,
)}
aria-pressed={isSelected}
className="absolute inset-0 z-0 rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
className="absolute inset-0 z-0 rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2"
data-testid={`persona-catalog-card-target-${persona.id}`}
disabled={isPending}
onClick={() => {
@@ -123,7 +123,12 @@ export function PersonaCatalogSection({
{preview}
</p>
<div className="mt-auto flex items-center justify-between gap-3 border-t border-border/60 pt-3">
<div
className={cn(
"mt-auto flex items-center justify-between gap-3 border-t pt-3",
isSelected ? "border-primary/20" : "border-border/60",
)}
>
<Button
className="pointer-events-auto"
data-testid={`persona-catalog-details-${persona.id}`}

View File

@@ -1,3 +1,5 @@
import { Check } from "lucide-react";
import { cn } from "@/shared/lib/cn";
import { personaCatalogCopy } from "./personaLibraryCopy";
@@ -12,12 +14,13 @@ export function PersonaCatalogSelectionBadge({
return (
<span
className={cn(
"whitespace-nowrap rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.14em]",
"inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.14em]",
isActive
? "bg-primary/15 text-primary"
: "bg-muted text-muted-foreground",
? "bg-primary text-primary-foreground shadow-sm"
: "border border-border/70 bg-background/85 text-muted-foreground",
)}
>
{isActive ? <Check className="h-3 w-3" /> : null}
{isActive
? personaCatalogCopy.selectedState
: personaCatalogCopy.availableState}

View File

@@ -34,11 +34,11 @@ function SelectionChipButton({
<button
aria-pressed={selected}
className={cn(
"inline-flex min-h-9 items-center gap-2 rounded-full border py-1.5 text-sm font-medium transition-colors",
"inline-flex min-h-9 items-center gap-2 rounded-full border py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
showAvatar ? "pl-1.5 pr-3" : "px-3",
selected
? "border-foreground bg-foreground text-background shadow-sm"
: "border-border/70 bg-muted/25 text-foreground hover:bg-muted/55",
? "border-primary bg-primary/10 text-foreground"
: "border-border/80 bg-background/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
disabled && "cursor-not-allowed opacity-50",
)}
disabled={disabled}
@@ -51,8 +51,8 @@ function SelectionChipButton({
className={cn(
"h-6 w-6 rounded-full text-[10px]",
selected
? "bg-background/20 text-background"
: "bg-primary/20 text-primary",
? "bg-primary/20 text-primary ring-1 ring-primary/20"
: "bg-background/80 text-muted-foreground ring-1 ring-border/70",
)}
iconClassName="h-3.5 w-3.5"
label={label}
@@ -109,9 +109,7 @@ export function AddChannelBotPersonasSection({
<Bot
className={cn(
"h-4 w-4",
includeGeneric
? "text-background/70"
: "text-muted-foreground",
includeGeneric ? "text-primary" : "text-current",
)}
/>
Generic
@@ -142,7 +140,7 @@ export function AddChannelBotPersonasSection({
className={cn(
"inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
isSelected
? "bg-background/20 text-background/80"
? "bg-primary/15 text-primary"
: "bg-muted/60 text-muted-foreground",
)}
>

View File

@@ -31,10 +31,10 @@ function SelectionChipButton({
<button
aria-pressed={selected}
className={cn(
"inline-flex min-h-9 items-center gap-2 rounded-full border py-1.5 px-3 text-sm font-medium transition-colors",
"inline-flex min-h-9 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
selected
? "border-foreground bg-foreground text-background shadow-sm"
: "border-border/70 bg-muted/25 text-foreground hover:bg-muted/55",
? "border-primary bg-primary/10 text-foreground"
: "border-border/80 bg-background/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
disabled && "cursor-not-allowed opacity-50",
)}
disabled={disabled}
@@ -109,18 +109,14 @@ export function AddChannelBotTeamsSection({
<Users
className={cn(
"h-4 w-4",
allSelected
? "text-background/70"
: "text-muted-foreground",
allSelected ? "text-primary" : "text-current",
)}
/>
{team.name}
<span
className={cn(
"text-xs",
allSelected
? "text-background/60"
: "text-muted-foreground",
allSelected ? "text-primary/70" : "text-current/70",
)}
>
({validIds.length})
@@ -130,7 +126,7 @@ export function AddChannelBotTeamsSection({
className={cn(
"inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
allSelected
? "bg-background/20 text-background/80"
? "bg-primary/15 text-primary"
: "bg-muted/60 text-muted-foreground",
)}
>

View File

@@ -198,169 +198,165 @@ export function DoctorSettingsPanel() {
!prereqs.mcp.available);
return (
<div className="space-y-4" data-testid="settings-doctor">
<section className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<section className="space-y-5" data-testid="settings-doctor">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2">
<Stethoscope className="h-4 w-4 text-primary" />
<h2 className="text-sm font-semibold tracking-tight">Doctor</h2>
</div>
<p className="mt-1 text-sm text-muted-foreground">
Verify the local Sprout tools and ACP runtime commands used by the
desktop app.
</p>
</div>
<Button
className="shrink-0"
disabled={isRefreshing}
onClick={() => {
void providersQuery.refetch();
void prereqsQuery.refetch();
}}
size="sm"
type="button"
variant="outline"
>
<RefreshCw
className={cn("h-4 w-4", isRefreshing && "animate-spin")}
/>
Re-run
</Button>
</div>
<div className="mt-5 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<div className="space-y-4">
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<div className="flex items-center gap-2">
<Stethoscope className="h-4 w-4 text-primary" />
<h2 className="text-sm font-semibold tracking-tight">Doctor</h2>
<TerminalSquare className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold tracking-tight">
Local Sprout binaries
</h3>
</div>
<p className="mt-1 text-sm text-muted-foreground">
Verify the local Sprout tools and ACP runtime commands used by the
desktop app.
These checks replace the old binary status card from Create agent.
</p>
<div className="mt-4 space-y-2">
{toolChecks.map((check) => (
<CommandCheckRow
availability={check.availability}
id={check.id}
isLoading={prereqsQuery.isLoading}
key={check.id}
label={check.label}
purpose={check.purpose}
/>
))}
</div>
{hasMissingSproutTools ? (
<p className="mt-4 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
Build the workspace binaries with{" "}
<code className="font-mono">
cargo build --release --workspace
</code>{" "}
or point agent creation at custom ACP and MCP commands.
</p>
) : null}
{prereqsQuery.error instanceof Error ? (
<p className="mt-4 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{prereqsQuery.error.message}
</p>
) : null}
</div>
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<h3 className="text-sm font-semibold tracking-tight">
Custom harness commands
</h3>
<p className="mt-1 text-sm text-muted-foreground">
Verify non-default ACP or MCP binaries before using them in agent
creation.
</p>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<label className="text-sm font-medium" htmlFor="doctor-acp">
ACP command
</label>
<Input
data-testid="doctor-acp-command"
id="doctor-acp"
onChange={(event) => setAcpCommand(event.target.value)}
value={acpCommand}
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium" htmlFor="doctor-mcp">
MCP command
</label>
<Input
data-testid="doctor-mcp-command"
id="doctor-mcp"
onChange={(event) => setMcpCommand(event.target.value)}
value={mcpCommand}
/>
</div>
</div>
<p className="mt-3 text-xs text-muted-foreground">
Token minting always checks the default{" "}
<code className="font-mono">sprout-admin</code> command.
</p>
</div>
<Button
className="shrink-0"
disabled={isRefreshing}
onClick={() => {
void providersQuery.refetch();
void prereqsQuery.refetch();
}}
size="sm"
type="button"
variant="outline"
>
<RefreshCw
className={cn("h-4 w-4", isRefreshing && "animate-spin")}
/>
Re-run
</Button>
</div>
<div className="mt-5 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
<div className="space-y-4">
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<div className="flex items-center gap-2">
<TerminalSquare className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold tracking-tight">
Local Sprout binaries
</h3>
</div>
<p className="mt-1 text-sm text-muted-foreground">
These checks replace the old binary status card from Create
agent.
</p>
<div className="space-y-4">
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<h3 className="text-sm font-semibold tracking-tight">
ACP runtimes
</h3>
<p className="mt-1 text-sm text-muted-foreground">
Installed runtimes that the desktop app can offer in Create agent.
</p>
<div className="mt-4 space-y-2">
{toolChecks.map((check) => (
<CommandCheckRow
availability={check.availability}
id={check.id}
isLoading={prereqsQuery.isLoading}
key={check.id}
label={check.label}
purpose={check.purpose}
<div className="mt-4 space-y-2">
{providersQuery.isLoading ? (
<p className="text-sm text-muted-foreground">
Looking for installed ACP runtimes...
</p>
) : providers.length > 0 ? (
providers.map((provider) => (
<ProviderRow
command={provider.command}
defaultArgs={provider.defaultArgs}
key={provider.id}
label={provider.label}
providerId={provider.id}
resolvedPath={provider.binaryPath}
/>
))}
</div>
{hasMissingSproutTools ? (
<p className="mt-4 rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
Build the workspace binaries with{" "}
<code className="font-mono">
cargo build --release --workspace
</code>{" "}
or point agent creation at custom ACP and MCP commands.
</p>
) : null}
{prereqsQuery.error instanceof Error ? (
<p className="mt-4 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{prereqsQuery.error.message}
</p>
) : null}
))
) : (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
No known ACP runtime was detected on your PATH yet. You can
still use a custom command in Create agent.
</div>
)}
</div>
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<h3 className="text-sm font-semibold tracking-tight">
Custom harness commands
</h3>
<p className="mt-1 text-sm text-muted-foreground">
Verify non-default ACP or MCP binaries before using them in
agent creation.
{providersQuery.error instanceof Error ? (
<p className="mt-4 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{providersQuery.error.message}
</p>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<label className="text-sm font-medium" htmlFor="doctor-acp">
ACP command
</label>
<Input
data-testid="doctor-acp-command"
id="doctor-acp"
onChange={(event) => setAcpCommand(event.target.value)}
value={acpCommand}
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium" htmlFor="doctor-mcp">
MCP command
</label>
<Input
data-testid="doctor-mcp-command"
id="doctor-mcp"
onChange={(event) => setMcpCommand(event.target.value)}
value={mcpCommand}
/>
</div>
</div>
<p className="mt-3 text-xs text-muted-foreground">
Token minting always checks the default{" "}
<code className="font-mono">sprout-admin</code> command.
</p>
</div>
) : null}
</div>
<div className="space-y-4">
<div className="rounded-xl border border-border/70 bg-muted/20 p-4">
<h3 className="text-sm font-semibold tracking-tight">
ACP runtimes
</h3>
<p className="mt-1 text-sm text-muted-foreground">
Installed runtimes that the desktop app can offer in Create
agent.
</p>
<div className="mt-4 space-y-2">
{providersQuery.isLoading ? (
<p className="text-sm text-muted-foreground">
Looking for installed ACP runtimes...
</p>
) : providers.length > 0 ? (
providers.map((provider) => (
<ProviderRow
command={provider.command}
defaultArgs={provider.defaultArgs}
key={provider.id}
label={provider.label}
providerId={provider.id}
resolvedPath={provider.binaryPath}
/>
))
) : (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
No known ACP runtime was detected on your PATH yet. You can
still use a custom command in Create agent.
</div>
)}
</div>
{providersQuery.error instanceof Error ? (
<p className="mt-4 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{providersQuery.error.message}
</p>
) : null}
</div>
<SetupHelpCard />
</div>
<SetupHelpCard />
</div>
</section>
</div>
</div>
</section>
);
}

View File

@@ -102,10 +102,7 @@ export function NotificationSettingsCard({
onSetNeedsActionNotificationsEnabled: (enabled: boolean) => void;
}) {
return (
<section
className="rounded-xl border border-border/80 bg-card/80 p-4 shadow-sm"
data-testid="settings-notifications"
>
<section className="min-w-0" data-testid="settings-notifications">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<h2 className="text-sm font-semibold tracking-tight">

View File

@@ -117,10 +117,7 @@ export function ProfileSettingsCard({
const nip05Handle = profile?.nip05Handle ?? "Not set";
return (
<section
className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm"
data-testid="settings-profile"
>
<section className="min-w-0" data-testid="settings-profile">
<div className="flex min-w-0 items-start gap-4">
<ProfileAvatar
avatarUrl={resolvedAvatarUrl}

View File

@@ -104,10 +104,7 @@ function ThemeSettingsCard() {
}, [search]);
return (
<section
className="rounded-xl border border-border/80 bg-card/80 p-4 shadow-sm"
data-testid="settings-theme"
>
<section className="min-w-0" data-testid="settings-theme">
<div className="mb-3 min-w-0">
<h2 className="text-sm font-semibold tracking-tight">Appearance</h2>
<p className="text-sm text-muted-foreground">

View File

@@ -673,10 +673,7 @@ export function TokenSettingsCard({
const hasReachedTokenLimit = activeTokens.length >= MAX_ACTIVE_TOKENS;
return (
<section
className="rounded-xl border border-border/80 bg-card/80 p-4 shadow-sm"
data-testid="settings-tokens"
>
<section className="min-w-0" data-testid="settings-tokens">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">