mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
Agents are now the only concept users touch. Persona records remain a pack-internal primitive; nothing in the UI creates, lists, or edits them. - Agents grid: UnifiedAgentsSection renders one card per managed agent in a single flat grid with a new AgentActionsMenu (Edit, Duplicate, Add to channel, Export, Remove). Persona-keyed grouping, persona cards, and the persona profile-panel path are deleted (unifiedAgentGroups, AgentGroupRows, ManagedAgentRow, PersonaActionsMenu, PersonaIdentity, PersonaCatalog*, PersonaLibraryEntryPoints, usePersonaActions, and the persona dialog state helpers) - Create Agent wizard: new CreateAgentStartDialog (blank, built-in template, or import from file) hands an AgentDraft to CreateAgentDialog; submit only creates a managed agent — no persona record. PersonaDialog is gone; batch import creates agents directly; the system prompt is a top-level "Agent instructions" field with model/provider fields shared via AgentModelProviderFields - Duplicate opens the wizard prefilled from the source agent record's pinned fields (prompt, model, provider, env vars, avatar, runtime) - Teams: TeamDialog picks managed agents and stores pubkeys via the unified TeamCardMember model (pack persona members render read-only); TeamsSection, import/export, and AddTeamToChannelDialog resolve members from agent_pubkeys plus pack persona_ids, deploying pack personas through the existing persona path - Channel deploy: AddChannelBotDialog lists managed agents (AddChannelBotAgentsSection + useInChannelAgentPubkeys) and attaches existing agents instead of spawning persona instances; QuickBotBar and the persona section/runtime-override helpers are deleted. The dialog also prevents open-autofocus: focus landing on the first agent chip opened its tooltip, so the first Escape dismissed the tooltip layer instead of closing the dialog - Sharing: PersonaShareDialog, catalog-visibility localStorage, and the standalone catalog dialog are removed — Export in the card menu is the sharing path - Copy sweep: remaining user-visible "persona" strings renamed (config provenance is "Inherited from agent profile", delete/import/toast copy, ModelPicker, EnvVarsEditor); profile panel actions are agent-centric and the profilePersona route param is gone; channel templates resolve agent pubkeys, persona ids, or builtin template ids polymorphically; the Fizz welcome guide spawns from its AgentTemplate - shared/api: AgentTemplate type, agentPubkeys on team types, provider on CreateManagedAgentInput, listAgentTemplates/exportAgentToJson bindings; e2eBridge mocks the new surface for the Playwright suites; colocated unit tests (.test.mjs) updated alongside their sources Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import * as React from "react";
|
|
import { AnimatePresence } from "motion/react";
|
|
|
|
import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { Input } from "@/shared/ui/input";
|
|
import type { EnvVarsValue } from "./EnvVarsEditor";
|
|
import { PersonaDropdownField } from "./PersonaDropdownField";
|
|
import { PersonaModelField } from "./PersonaModelField";
|
|
import { PersonaProviderApiKeyField } from "./PersonaProviderApiKeyField";
|
|
import {
|
|
AUTO_MODEL_DROPDOWN_VALUE,
|
|
AUTO_PROVIDER_DROPDOWN_VALUE,
|
|
CUSTOM_MODEL_DROPDOWN_VALUE,
|
|
CUSTOM_PROVIDER_DROPDOWN_VALUE,
|
|
getDefaultLlmProviderLabel,
|
|
getModelSelectValue,
|
|
getPersonaModelOptions,
|
|
getPersonaProviderOptions,
|
|
getProviderApiKeyConfig,
|
|
getProviderApiKeyEnvVar,
|
|
getRuntimePersonaModelOptions,
|
|
hasPersonaModelOption,
|
|
PERSONA_FIELD_CONTROL_CLASS,
|
|
PERSONA_FIELD_SHELL_CLASS,
|
|
PERSONA_LABEL_OPTIONAL_CLASS,
|
|
providerRequiresExplicitModel,
|
|
runtimeSupportsLlmProviderSelection,
|
|
shouldClearKnownModelForSelectionScope,
|
|
type PersonaDropdownOption,
|
|
} from "./personaDialogPickers";
|
|
import {
|
|
MODEL_DISCOVERY_LOADING_VALUE,
|
|
usePersonaModelDiscovery,
|
|
} from "./usePersonaModelDiscovery";
|
|
|
|
const FIELD_TRANSITION = { duration: 0.18, ease: [0.23, 1, 0.32, 1] } as const;
|
|
|
|
/**
|
|
* LLM provider + model pickers (with live model discovery) and the managed
|
|
* provider API-key field, for a given ACP runtime. Controlled: the parent
|
|
* owns `model`, `provider`, and `envVars`.
|
|
*/
|
|
export function AgentModelProviderFields({
|
|
disabled,
|
|
envVars,
|
|
model,
|
|
open,
|
|
provider,
|
|
runtimeId,
|
|
selectedRuntime,
|
|
onEnvVarsChange,
|
|
onModelChange,
|
|
onProviderChange,
|
|
}: {
|
|
disabled: boolean;
|
|
envVars: EnvVarsValue;
|
|
model: string;
|
|
open: boolean;
|
|
provider: string;
|
|
runtimeId: string;
|
|
selectedRuntime: AcpRuntimeCatalogEntry | undefined;
|
|
onEnvVarsChange: (next: EnvVarsValue) => void;
|
|
onModelChange: (next: string) => void;
|
|
onProviderChange: (next: string) => void;
|
|
}) {
|
|
const [isCustomModelEditing, setIsCustomModelEditing] = React.useState(false);
|
|
const [isCustomProviderEditing, setIsCustomProviderEditing] =
|
|
React.useState(false);
|
|
|
|
const trimmedRuntime = runtimeId.trim();
|
|
const llmProviderFieldVisible =
|
|
trimmedRuntime.length > 0 &&
|
|
runtimeSupportsLlmProviderSelection(trimmedRuntime);
|
|
const modelFieldVisible = trimmedRuntime.length > 0;
|
|
const providerForModelScope = llmProviderFieldVisible ? provider : "";
|
|
const trimmedProvider = provider.trim();
|
|
const providerApiKeyConfig =
|
|
llmProviderFieldVisible && !isCustomProviderEditing
|
|
? getProviderApiKeyConfig(trimmedProvider)
|
|
: null;
|
|
const providerApiKeyValue = providerApiKeyConfig
|
|
? (envVars[providerApiKeyConfig.envVar] ?? "")
|
|
: "";
|
|
const isExplicitModelRequired =
|
|
modelFieldVisible && providerRequiresExplicitModel(providerForModelScope);
|
|
|
|
const {
|
|
discoveredModelOptions,
|
|
modelDiscoveryLoading,
|
|
modelDiscoveryStatus,
|
|
} = usePersonaModelDiscovery({
|
|
envVars,
|
|
isCustomProviderEditing,
|
|
modelFieldVisible,
|
|
open,
|
|
provider: providerForModelScope,
|
|
selectedRuntime,
|
|
});
|
|
|
|
const staticModelOptions = getPersonaModelOptions(
|
|
trimmedRuntime,
|
|
providerForModelScope,
|
|
);
|
|
const runtimeModelOptions = getRuntimePersonaModelOptions(trimmedRuntime);
|
|
const modelOptions = discoveredModelOptions ?? staticModelOptions;
|
|
const isModelCustom = !hasPersonaModelOption(
|
|
discoveredModelOptions ?? runtimeModelOptions,
|
|
model,
|
|
);
|
|
const modelSelectValue = getModelSelectValue({
|
|
isCustomModelEditing,
|
|
isModelCustom,
|
|
model,
|
|
});
|
|
const showCustomModelInput =
|
|
modelFieldVisible && (isCustomModelEditing || isModelCustom);
|
|
const providerOptions = getPersonaProviderOptions(
|
|
providerForModelScope,
|
|
trimmedRuntime,
|
|
);
|
|
const providerSelectValue = isCustomProviderEditing
|
|
? CUSTOM_PROVIDER_DROPDOWN_VALUE
|
|
: trimmedProvider || AUTO_PROVIDER_DROPDOWN_VALUE;
|
|
const providerDropdownOptions: PersonaDropdownOption[] = [
|
|
...providerOptions.map((option) => ({
|
|
label: option.label,
|
|
value: option.id || AUTO_PROVIDER_DROPDOWN_VALUE,
|
|
})),
|
|
{ label: "Custom provider...", value: CUSTOM_PROVIDER_DROPDOWN_VALUE },
|
|
];
|
|
const modelDropdownOptions: PersonaDropdownOption[] = [
|
|
...modelOptions.map((option) => ({
|
|
label: option.label,
|
|
value: option.id || AUTO_MODEL_DROPDOWN_VALUE,
|
|
})),
|
|
...(modelDiscoveryLoading && discoveredModelOptions === null
|
|
? [
|
|
{
|
|
disabled: true,
|
|
label: "Loading models...",
|
|
value: MODEL_DISCOVERY_LOADING_VALUE,
|
|
},
|
|
]
|
|
: []),
|
|
{ label: "Custom model...", value: CUSTOM_MODEL_DROPDOWN_VALUE },
|
|
];
|
|
|
|
React.useEffect(() => {
|
|
if (
|
|
!open ||
|
|
!modelFieldVisible ||
|
|
isCustomModelEditing ||
|
|
!shouldClearKnownModelForSelectionScope({
|
|
model,
|
|
provider: providerForModelScope,
|
|
runtime: trimmedRuntime,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
onModelChange("");
|
|
setIsCustomModelEditing(false);
|
|
}, [
|
|
isCustomModelEditing,
|
|
model,
|
|
modelFieldVisible,
|
|
onModelChange,
|
|
open,
|
|
providerForModelScope,
|
|
trimmedRuntime,
|
|
]);
|
|
|
|
// Reset custom-editing state and clear a provider that the new runtime
|
|
// can't choose whenever the runtime changes underneath us.
|
|
const previousRuntimeRef = React.useRef(trimmedRuntime);
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally runs only on runtime change
|
|
React.useEffect(() => {
|
|
if (previousRuntimeRef.current === trimmedRuntime) {
|
|
return;
|
|
}
|
|
previousRuntimeRef.current = trimmedRuntime;
|
|
if (!llmProviderFieldVisible && trimmedProvider.length > 0) {
|
|
clearManagedProviderApiKeyWhenLeaving(trimmedProvider, "");
|
|
setIsCustomProviderEditing(false);
|
|
onProviderChange("");
|
|
}
|
|
setIsCustomModelEditing(false);
|
|
}, [trimmedRuntime]);
|
|
|
|
function updateProviderApiKey(envKey: string, value: string) {
|
|
const current = envVars[envKey] ?? "";
|
|
if (current === value) {
|
|
return;
|
|
}
|
|
const next = { ...envVars };
|
|
if (value.length > 0) {
|
|
next[envKey] = value;
|
|
} else {
|
|
delete next[envKey];
|
|
}
|
|
onEnvVarsChange(next);
|
|
}
|
|
|
|
function clearManagedProviderApiKeyWhenLeaving(
|
|
previousProvider: string,
|
|
nextProvider: string,
|
|
) {
|
|
const previousEnvVar = getProviderApiKeyEnvVar(previousProvider);
|
|
const nextEnvVar = getProviderApiKeyEnvVar(nextProvider);
|
|
if (previousEnvVar && previousEnvVar !== nextEnvVar) {
|
|
if (previousEnvVar in envVars) {
|
|
const next = { ...envVars };
|
|
delete next[previousEnvVar];
|
|
onEnvVarsChange(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleProviderDropdownChange(nextValue: string) {
|
|
if (nextValue === CUSTOM_PROVIDER_DROPDOWN_VALUE) {
|
|
clearManagedProviderApiKeyWhenLeaving(provider, "");
|
|
setIsCustomProviderEditing(true);
|
|
onProviderChange("");
|
|
return;
|
|
}
|
|
|
|
const nextProvider =
|
|
nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue;
|
|
clearManagedProviderApiKeyWhenLeaving(provider, nextProvider);
|
|
setIsCustomProviderEditing(false);
|
|
onProviderChange(nextProvider);
|
|
const requiredEnvVar = getProviderApiKeyEnvVar(nextProvider);
|
|
if (requiredEnvVar && !envVars[requiredEnvVar]?.trim()) {
|
|
onModelChange("");
|
|
setIsCustomModelEditing(false);
|
|
return;
|
|
}
|
|
if (
|
|
!isCustomModelEditing &&
|
|
shouldClearKnownModelForSelectionScope({
|
|
model,
|
|
provider: nextProvider,
|
|
runtime: trimmedRuntime,
|
|
})
|
|
) {
|
|
onModelChange("");
|
|
setIsCustomModelEditing(false);
|
|
}
|
|
}
|
|
|
|
function handleModelDropdownChange(nextValue: string) {
|
|
if (nextValue === CUSTOM_MODEL_DROPDOWN_VALUE) {
|
|
setIsCustomModelEditing(true);
|
|
if (!isModelCustom) {
|
|
onModelChange("");
|
|
}
|
|
return;
|
|
}
|
|
|
|
setIsCustomModelEditing(false);
|
|
onModelChange(nextValue === AUTO_MODEL_DROPDOWN_VALUE ? "" : nextValue);
|
|
}
|
|
|
|
if (!modelFieldVisible && !llmProviderFieldVisible) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{llmProviderFieldVisible ? (
|
|
<div className="space-y-1.5">
|
|
<label
|
|
className="text-sm font-medium text-foreground"
|
|
htmlFor="agent-llm-provider"
|
|
>
|
|
LLM provider
|
|
<span className={PERSONA_LABEL_OPTIONAL_CLASS}>Optional</span>
|
|
</label>
|
|
<PersonaDropdownField
|
|
disabled={disabled}
|
|
id="agent-llm-provider"
|
|
onValueChange={handleProviderDropdownChange}
|
|
options={providerDropdownOptions}
|
|
placeholder={getDefaultLlmProviderLabel(trimmedRuntime)}
|
|
value={providerSelectValue}
|
|
/>
|
|
{llmProviderFieldVisible && isCustomProviderEditing ? (
|
|
<div
|
|
className={cn(
|
|
"mt-2 flex min-h-11 items-center px-3",
|
|
PERSONA_FIELD_SHELL_CLASS,
|
|
)}
|
|
>
|
|
<Input
|
|
aria-label="Custom provider ID"
|
|
autoCorrect="off"
|
|
className={cn(
|
|
"h-8 px-0 py-0 leading-6",
|
|
PERSONA_FIELD_CONTROL_CLASS,
|
|
)}
|
|
disabled={disabled}
|
|
id="agent-custom-provider"
|
|
onChange={(event) => onProviderChange(event.target.value)}
|
|
placeholder="Custom provider ID"
|
|
value={provider}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
{providerApiKeyConfig ? (
|
|
<PersonaProviderApiKeyField
|
|
config={providerApiKeyConfig}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
updateProviderApiKey(providerApiKeyConfig.envVar, value)
|
|
}
|
|
value={providerApiKeyValue}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
<AnimatePresence initial={false}>
|
|
{modelFieldVisible ? (
|
|
<PersonaModelField
|
|
disabled={disabled}
|
|
isExplicitModelRequired={isExplicitModelRequired}
|
|
model={model}
|
|
modelDiscoveryStatus={modelDiscoveryStatus}
|
|
modelDropdownOptions={modelDropdownOptions}
|
|
modelSelectValue={modelSelectValue}
|
|
onCustomModelChange={onModelChange}
|
|
onModelValueChange={handleModelDropdownChange}
|
|
showCustomModelInput={showCustomModelInput}
|
|
transition={FIELD_TRANSITION}
|
|
/>
|
|
) : null}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|