mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
Taylor Ho
parent
c192485e95
commit
0af8d39457
@@ -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'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;
|
||||
};
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user