mirror of
https://github.com/block/buzz.git
synced 2026-11-09 19:02:00 +00:00
feat(desktop): add explicit Edit avatar CTA in agent instance edit dialog (#1736)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -20,12 +20,13 @@ const rules = [
|
||||
];
|
||||
|
||||
// Decorative / chrome exceptions: `relativePath:lineNumber`. The avatar emoji
|
||||
// glyph is a fixed display size sized to its avatar box (not readable message
|
||||
// text), so it stays as the lone documented `text-[6rem]` literal.
|
||||
// glyphs are fixed display sizes sized to their avatar box (not readable
|
||||
// message text), so they are exempted from the readable-text px rule.
|
||||
const overrides = new Set([
|
||||
"src/features/settings/ui/ProfileSettingsCard.tsx:584",
|
||||
"src/features/onboarding/ui/AvatarStep.tsx:89",
|
||||
"src/features/agents/ui/AgentCreationPreview.tsx:711",
|
||||
"src/features/agents/ui/AgentCreationPreview.tsx:666",
|
||||
"src/features/agents/ui/AgentCreationPreview.tsx:747",
|
||||
]);
|
||||
|
||||
await runPxTextCheck({
|
||||
|
||||
@@ -57,6 +57,7 @@ type EmojiMartEmoji = {
|
||||
export function AgentCreationPreview({
|
||||
avatarUrl,
|
||||
disabled = false,
|
||||
hideEditControl = false,
|
||||
label,
|
||||
onClearAvatar,
|
||||
onUploadPendingChange,
|
||||
@@ -64,6 +65,10 @@ export function AgentCreationPreview({
|
||||
}: {
|
||||
avatarUrl: string | null;
|
||||
disabled?: boolean;
|
||||
/** When true, omit all upload/edit controls and render the avatar as a
|
||||
* plain display element. Use in contexts where avatar editing is
|
||||
* handled by an external affordance (e.g. AgentInstanceEditDialog). */
|
||||
hideEditControl?: boolean;
|
||||
label: string;
|
||||
onClearAvatar?: () => void;
|
||||
onUploadPendingChange?: (isPending: boolean) => void;
|
||||
@@ -644,6 +649,37 @@ export function AgentCreationPreview({
|
||||
</PopoverContent>
|
||||
);
|
||||
|
||||
// Display-only path: no upload controls, no pencil badge, no popover.
|
||||
// Used when the caller provides its own edit affordance.
|
||||
if (hideEditControl) {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[220px] lg:sticky lg:top-0">
|
||||
<div className="group/avatar-preview relative m-0 flex min-h-[190px] min-w-0 flex-col items-center justify-center gap-3 rounded-xl border border-transparent p-0">
|
||||
<div className="relative h-36 w-36">
|
||||
{emojiAvatarPreview ? (
|
||||
<div
|
||||
aria-label={`${label} avatar`}
|
||||
className="relative flex h-full w-full shrink-0 items-center justify-center overflow-hidden rounded-full shadow-xs transition-[background-color] duration-200 ease-out"
|
||||
role="img"
|
||||
style={{ backgroundColor: emojiAvatarPreview.color }}
|
||||
>
|
||||
<span className="flex h-full w-full items-center justify-center text-[4rem] leading-none">
|
||||
{emojiAvatarPreview.emoji}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<ProfileAvatar
|
||||
avatarUrl={avatarUrl}
|
||||
className="h-full w-full text-4xl"
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[220px] lg:sticky lg:top-0">
|
||||
<fieldset
|
||||
|
||||
@@ -45,6 +45,13 @@ type AgentDialogInstanceEditProps = {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onUpdated?: (agent: ManagedAgent) => void;
|
||||
initialFocus?: EditAgentFocusTarget;
|
||||
/**
|
||||
* Called when the user clicks "Edit avatar" inside the instance-edit dialog.
|
||||
* Caller (UserProfilePanel) is responsible for closing this dialog and
|
||||
* opening the definition-edit dialog. Only passed when the linked definition
|
||||
* is editable (non-built-in, resolved).
|
||||
*/
|
||||
onEditLinkedPersona?: () => void;
|
||||
};
|
||||
|
||||
type AgentDialogDefinitionEditProps = {
|
||||
@@ -88,6 +95,7 @@ export function AgentDialog(props: AgentDialogProps) {
|
||||
return (
|
||||
<AgentInstanceEditDialog
|
||||
agent={props.agent}
|
||||
onEditLinkedPersona={props.onEditLinkedPersona}
|
||||
onOpenChange={props.onOpenChange}
|
||||
onUpdated={props.onUpdated}
|
||||
open={props.open}
|
||||
|
||||
@@ -79,6 +79,7 @@ export function AgentInstanceEditDialog({
|
||||
agent,
|
||||
initialFocus,
|
||||
open,
|
||||
onEditLinkedPersona,
|
||||
onOpenChange,
|
||||
onUpdated,
|
||||
}: {
|
||||
@@ -86,6 +87,9 @@ export function AgentInstanceEditDialog({
|
||||
/** Optional field to scroll/focus when the dialog opens from a card deep-link. */
|
||||
initialFocus?: EditAgentFocusTarget;
|
||||
open: boolean;
|
||||
/** Present only when the linked definition is editable (non-built-in,
|
||||
* resolved). Caller closes this dialog and enters definition-edit. */
|
||||
onEditLinkedPersona?: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onUpdated?: (agent: ManagedAgent) => void;
|
||||
}) {
|
||||
@@ -822,18 +826,36 @@ export function AgentInstanceEditDialog({
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
{/* Avatar is definition-level identity (Wes decision 2026-07-09,
|
||||
"2a"): edit it on the agent profile; instances inherit and
|
||||
re-sync on restart. Read-only here by design. */}
|
||||
<AgentCreationPreview
|
||||
avatarUrl={previewAvatarUrl}
|
||||
disabled
|
||||
label={previewLabel}
|
||||
onClearAvatar={() => setAvatarUrl("")}
|
||||
onUploadPendingChange={setIsAvatarUploadPending}
|
||||
onSelectAvatar={setAvatarUrl}
|
||||
/>
|
||||
|
||||
{/* Avatar is definition-level identity. hideEditControl suppresses
|
||||
the internal pencil badge; the CTA below is the only edit path. */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<AgentCreationPreview
|
||||
avatarUrl={previewAvatarUrl}
|
||||
hideEditControl
|
||||
label={previewLabel}
|
||||
onClearAvatar={() => setAvatarUrl("")}
|
||||
onUploadPendingChange={setIsAvatarUploadPending}
|
||||
onSelectAvatar={setAvatarUrl}
|
||||
/>
|
||||
{onEditLinkedPersona ? (
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
handleOpenChange(false);
|
||||
onEditLinkedPersona();
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Edit avatar
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
Avatar is shared identity
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
{/* Agent name */}
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -59,6 +59,7 @@ test("instance-edit routes to AgentInstanceEditDialog with its contract props",
|
||||
assert.equal(element.type, AgentInstanceEditDialog);
|
||||
assert.deepEqual(element.props, {
|
||||
agent,
|
||||
onEditLinkedPersona: undefined,
|
||||
onOpenChange,
|
||||
onUpdated,
|
||||
open: true,
|
||||
|
||||
@@ -917,6 +917,15 @@ export function UserProfilePanel({
|
||||
agent={managedAgent}
|
||||
mode="instance-edit"
|
||||
initialFocus={editAgentFocus}
|
||||
onEditLinkedPersona={
|
||||
resolvedPersona && !resolvedPersona.isBuiltIn
|
||||
? () => {
|
||||
setEditAgentOpen(false);
|
||||
setEditAgentFocus(undefined);
|
||||
setPersonaDialogState(editPersonaDialogState(resolvedPersona));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onOpenChange={(next) => {
|
||||
setEditAgentOpen(next);
|
||||
if (!next) setEditAgentFocus(undefined);
|
||||
|
||||
Reference in New Issue
Block a user