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:
Will Pfleger
2026-07-10 17:58:26 -04:00
committed by GitHub
parent 67a2d047eb
commit 26966aacec
6 changed files with 92 additions and 15 deletions

View File

@@ -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({

View File

@@ -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

View File

@@ -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}

View File

@@ -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">

View File

@@ -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,

View File

@@ -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);