refactor(desktop): unify New Agent selectors on the persona dropdown pattern

The Agent runtime, "Who can talk to this agent", and "Run on" fields
used raw <select> elements while the LLM provider/Model fields right
below them used PersonaDropdownField — the form read as two different
design languages. All selector-style controls in the create-agent form
now use PersonaDropdownField.

Also adds an isPlaceholder flag to PersonaDropdownOption so "Default"
provider/model options render faded like placeholder text instead of
looking like actively selected values.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-07-07 01:12:49 -07:00
co-authored by Taylor Ho
parent c192485e95
commit 0af8d39457
7 changed files with 56 additions and 35 deletions
@@ -124,6 +124,9 @@ export function AgentModelProviderFields({
: trimmedProvider || AUTO_PROVIDER_DROPDOWN_VALUE;
const providerDropdownOptions: PersonaDropdownOption[] = [
...providerOptions.map((option) => ({
// The "Default" option means "no explicit provider" — render it faded
// in the trigger, like placeholder text.
isPlaceholder: option.id === "",
label: option.label,
value: option.id || AUTO_PROVIDER_DROPDOWN_VALUE,
})),
@@ -131,6 +134,8 @@ export function AgentModelProviderFields({
];
const modelDropdownOptions: PersonaDropdownOption[] = [
...modelOptions.map((option) => ({
// Same treatment for "Default model" — it's the absence of a choice.
isPlaceholder: option.id === "",
label: option.label,
value: option.id || AUTO_MODEL_DROPDOWN_VALUE,
})),
@@ -40,6 +40,7 @@ import {
ProviderConfigFields,
} from "./ProviderConfigFields";
import { CreateAgentRespondToField } from "./RespondToField";
import { PersonaDropdownField } from "./PersonaDropdownField";
import { RelayMeshAgentSection } from "@/features/mesh-compute/ui/RelayMeshAgentSection";
import { meshPrepareRelayMeshClient } from "@/shared/api/tauriMesh";
import type { MeshServeTarget } from "@/shared/api/tauriMesh";
@@ -501,8 +502,12 @@ export function CreateAgentDialog({
<div className="flex max-h-[85vh] flex-col">
<DialogHeader className="shrink-0 border-b border-border/60 px-6 py-5 pr-14">
<DialogTitle>{draft?.title ?? "Create agent"}</DialogTitle>
<DialogDescription>
{draft?.description ??
{/* A draft with intentionally-blank description keeps the line
for screen readers only. */}
<DialogDescription
className={draft && !draft.description ? "sr-only" : undefined}
>
{draft?.description ||
"Set up a new agent and start it in this workspace."}
</DialogDescription>
</DialogHeader>
@@ -588,19 +593,19 @@ export function CreateAgentDialog({
>
Run on
</label>
<select
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs"
<PersonaDropdownField
id="agent-run-on"
onChange={(e) => handleRunOnChange(e.target.value)}
onValueChange={handleRunOnChange}
options={[
{ label: "This computer", value: "local" },
...backendProviders.map((p) => ({
label: p.id,
value: p.id,
})),
]}
placeholder="Choose where this agent runs"
value={runOn}
>
<option value="local">This computer</option>
{backendProviders.map((p) => (
<option key={p.id} value={p.id}>
{p.id}
</option>
))}
</select>
/>
</div>
) : null}
@@ -2,6 +2,7 @@ import type { AcpRuntime, ManagedAgentPrereqs } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Input } from "@/shared/ui/input";
import { describeResolvedCommand } from "./agentUi";
import { PersonaDropdownField } from "./PersonaDropdownField";
export function CreateAgentBasicsFields({
name,
@@ -55,19 +56,19 @@ export function CreateAgentRuntimeField({
<label className="text-sm font-medium" htmlFor="agent-runtime">
Agent runtime
</label>
<select
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs"
<PersonaDropdownField
id="agent-runtime"
onChange={(event) => onRuntimeChange(event.target.value)}
onValueChange={onRuntimeChange}
options={[
...runtimes.map((runtime) => ({
label: runtime.label,
value: runtime.id,
})),
{ label: "Custom command", value: "custom" },
]}
placeholder="Choose a runtime"
value={selectedRuntimeId}
>
{runtimes.map((runtime) => (
<option key={runtime.id} value={runtime.id}>
{runtime.label}
</option>
))}
<option value="custom">Custom command</option>
</select>
/>
{selectedRuntime ? (
<p className="text-xs text-muted-foreground">
Detected via{" "}
@@ -34,6 +34,7 @@ export function PersonaDropdownField({
}) {
const [open, setOpen] = React.useState(false);
const selectedOption = options.find((option) => option.value === value);
const showAsPlaceholder = !selectedOption || selectedOption.isPlaceholder;
return (
<div className={PERSONA_FIELD_SHELL_CLASS}>
@@ -52,7 +53,7 @@ export function PersonaDropdownField({
<span
className={cn(
"min-w-0 flex-1 truncate",
!selectedOption && "text-muted-foreground/55",
showAsPlaceholder && "text-muted-foreground/55",
)}
>
{selectedOption?.label ?? placeholder}
@@ -12,6 +12,7 @@ import { cn } from "@/shared/lib/cn";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { PersonaDropdownField } from "./PersonaDropdownField";
/**
* Inbound author gate UI for create/edit agent dialogs.
@@ -30,6 +31,12 @@ import { UserAvatar } from "@/shared/ui/UserAvatar";
* `desktop/src-tauri/src/managed_agents/types.rs`.
*/
const RESPOND_TO_OPTIONS = [
{ label: "Owner only (default)", value: "owner-only" },
{ label: "Anyone", value: "anyone" },
{ label: "Allowlist", value: "allowlist" },
] as const;
function formatSearchUserName(user: UserSearchResult) {
return (
user.displayName?.trim() ||
@@ -118,18 +125,14 @@ export function CreateAgentRespondToField({
<label className="text-sm font-medium" htmlFor="agent-respond-to">
Who can talk to this agent
</label>
<select
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs"
data-testid="agent-respond-to-select"
<PersonaDropdownField
disabled={disabled}
id="agent-respond-to"
onChange={(e) => onModeChange(e.target.value as RespondToMode)}
onValueChange={(value) => onModeChange(value as RespondToMode)}
options={RESPOND_TO_OPTIONS}
placeholder="Choose who can talk to this agent"
value={mode}
>
<option value="owner-only">Owner only (default)</option>
<option value="anyone">Anyone</option>
<option value="allowlist">Allowlist</option>
</select>
/>
<p className="text-xs text-muted-foreground">
Controls which Nostr authors the agent listens to (@mentions, DMs,
thread replies). The agent&apos;s owner can always shut it down with
@@ -30,6 +30,12 @@ export type PersonaModelOption = {
export type PersonaDropdownOption = {
disabled?: boolean;
/**
* Marks an "auto"/default option that stands in for "no explicit choice".
* The trigger renders it faded, like placeholder text, so it reads as
* helper copy rather than an actively selected value.
*/
isPlaceholder?: boolean;
label: string;
value: string;
};
+1 -1
View File
@@ -320,7 +320,7 @@ test("agent model options follow the selected LLM provider", async ({
const runtime = page.locator("#agent-runtime");
const llmProvider = page.locator("#agent-llm-provider");
const model = page.locator("#persona-model");
await expect(runtime).toHaveValue("buzz-agent");
await expect(runtime).toContainText("Buzz Agent");
await expect(llmProvider).toBeVisible();
await expect(model).toBeVisible();
// Without live discovery, the only static option is "Default model".