fix(desktop): downscale large avatars for agent-share PNG body - #6260
Conversation
Sharing an agent encodes a .agent.png with the avatar as the image body. Non-PNG avatars were transcoded at full resolution, so a large avatar (e.g. a 2764×4096 webp) produced a PNG over the 10 MiB MAX_SNAPSHOT_PNG_BYTES cap and the share failed. Downscale the PNG body to a 512px longest edge — the body is only a card thumbnail and the manifest keeps the untouched source avatar reference. Oversize already-PNG avatars now route through the same downscaling path instead of a verbatim tEXt-chunk injection. Also surface the pipeline's real error in the share-dialog toast instead of the generic "Couldn't send. Try again." via a synchronous getCurrentError() accessor (the render-captured state is stale right after beginSend resolves). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ot-avatar-downscale * origin/main: feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The error-surfacing fix toasts the controller's real message instead of the generic "Couldn't send agent. Try again." fallback, so the two people- sharing e2e tests that asserted the generic string no longer matched. Assert the specific eligibility messages, which verifies the new behavior end-to-end. Also back the oversized-avatar unit test's manifest claim with an assertion: the manifest keeps the untouched source avatar_url reference while only the PNG body is downscaled. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Mordecai, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed source at ad20a1ea0fc453aa41582d263164ebd39349b58d. No blocking findings.
- Oversize and non-PNG avatars enter the decode/downscale/re-encode path; already-PNG bodies at or below 512px retain the existing inexpensive chunk-injection path.
DynamicImage::resize(512, 512, Lanczos3)preserves aspect ratio while bounding both dimensions. The complete materialized PNG still passes through the existing 10 MiB validation, so large manifest payloads remain fail-closed.- The manifest is constructed from the original avatar source independently of PNG-body encoding. The regression now verifies both the 512px body cap and preservation of
avatar_urlwithout inlining the oversized source. - The synchronous error accessor is coherent: every send-state transition uses
commitState, which updates the ref before React state, and both share-dialog call sites read it immediately afterbeginSendreturns false. Team sharing delegates to the sameSnapshotShareDialog. - Updated E2E expectations assert the specific eligibility failures surfaced by this change.
Source-only review; I did not run tests or harnesses. Current GitHub checks are green.
wesbillman
left a comment
There was a problem hiding this comment.
Princess Donut, an automated reviewer, commenting via Wes's GitHub account.
Reviewed exact head ad20a1ea0fc453aa41582d263164ebd39349b58d with the avatar decode/resize boundary, manifest preservation, and stale React state paths as the primary lens.
CLEAR. I found no material correctness, security, or user-trust defect in the change.
Evidence:
- Oversize and non-PNG bodies converge on one decode → aspect-preserving 512px resize → PNG encode path; already-small PNGs retain the existing injection path (
agent_snapshot.rs:336-349,:464-503). - The manifest is built independently from the PNG body and still applies the existing 2 MiB inline/reference policy (
agent_snapshot.rs:249-283); the regression test now verifies both the 512px body cap and preserved source URL. - Every send-pipeline state transition now updates the ref before React state (
useSnapshotSendController.ts:399-406,:424-449), so the post-await beginSendtoast reads the failure that actually occurred rather than a stale render or generic fallback. - The changed branch has green Desktop Core, desktop smoke/integration, macOS build, Windows Rust, and DCO checks in the current PR status.
One non-blocking observation: image decoding still materializes the source before resizing, so this improves output size rather than peak decode memory. That behavior predates this patch on the transcode path and the source bytes already pass existing media boundaries; it is not a regression here.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Consolidated Royal Court review at exact head ad20a1ea0fc453aa41582d263164ebd39349b58d.
No blocking findings. Oversized PNGs and non-PNG raster avatars converge on an aspect-preserving 512 px resize before PNG encoding, while small PNGs retain the existing direct path. The final 10 MiB snapshot guard remains in the shared materialization path. The manifest is built independently from the original avatar bytes/reference, and the regression now verifies both the body dimension cap and preservation of the source URL.
The ref-backed error accessor is coherent: all controller transitions update the ref synchronously before React state, so callers reading immediately after beginSend receive the current eligibility/encode/upload/send failure. Updated E2E expectations verify the user-visible eligibility messages.
I also checked malformed-image fallback, locked/plain shared encoding, output-size enforcement, and source/body separation. git diff --check is clean, and all applicable GitHub checks are green; path-filtered jobs are skipped as expected. I did not rerun CI-equivalent suites locally.
* origin/main: (43 commits) perf(desktop): parallelize relay agent directory rebuild (block#6258) Refine the mobile emoji picker (block#5853) fix(desktop): exclude archived agents from nest, order regeneration (block#5905) Add font size and conversation density preferences (block#5644) fix(desktop): emit camelCase config-write payload fields (block#6062) fix(desktop): downscale large avatars for agent-share PNG body (block#6260) fix(desktop): preserve early relay auth challenges (block#3320) Polish mobile message actions (block#5873) Refine mobile pairing confirmation (block#6018) chore(scripts): add buzz-adopt-prod-agents.sh (block#6250) feat(managed-agents): close five Claude Code agent-config gaps (block#4557) chore(hooks): keep mobile analysis out of pre-commit (block#6236) fix(shared-ui): delay hover disclosures by default (block#5821) fix(desktop-chrome): preserve balanced layout when sidebar collapses (block#6000) Polish mobile timeline navigation (block#5874) chore(release): release Buzz Desktop version 0.5.17 (block#6234) fix(prompt): simplify pickup follow-through (block#6186) fix(mcp): scope todo usage (block#6216) fix(desktop): bound remote agent mention authorization (block#6224) fix: bump h2 for RUSTSEC-2026-0258 (block#6222) ... Signed-off-by: Princess Donut <3cb959c7eb65d61f634e61df318e450f18f82fa0e01849e7010b82666ead0587@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/main.tsx # mobile/ios/Podfile.lock
…-in-thread * origin/main: (32 commits) Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Refine mobile pairing confirmation (#6018) chore(scripts): add buzz-adopt-prod-agents.sh (#6250) feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Polish mobile timeline navigation (#5874) chore(release): release Buzz Desktop version 0.5.17 (#6234) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…ntion-phase1 * origin/main: (71 commits) Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Refine mobile pairing confirmation (#6018) chore(scripts): add buzz-adopt-prod-agents.sh (#6250) feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Polish mobile timeline navigation (#5874) chore(release): release Buzz Desktop version 0.5.17 (#6234) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Sharing an agent encodes a
.agent.pngsnapshot with the avatar as the PNG image body. Non-PNG avatars were transcoded at full resolution and already-PNG avatars were carried through unchanged, so a large source avatar could produce a PNG that exceeds the 10 MiBMAX_SNAPSHOT_PNG_BYTEScap and fail the send. A 2764×4096 webp avatar encoded to ~26 MB — over 2.5× the ceiling — which is why sharing that agent hung for a few seconds (fetching and transcoding the image) and then failed.The share dialog compounded the problem: on failure it discarded the pipeline's real error (
Snapshot exceeds the 10 MiB size limit…) and toasted a genericCouldn't send … Try again., hiding the actual cause.Changes
MAX_PNG_BODY_EDGE) before PNG re-encoding, mirroring the frontend SVG rasterizer's 512×512 cap insnapshotAvatarPng.ts. Already-PNG avatars over the dimension cap route through the same downscaling transcode path instead of a straight tEXt-chunk injection. The manifest'savatar_url/avatar_data_urlsource reference is untouched — only the PNG image body is downscaled.PersonaShareDialog(used bySnapshotShareDialog/TeamShareDialog) andAgentCardViewerDialognow toast the send controller's actual error message, falling back to the generic string only when it is empty. AgetCurrentError()accessor reads the error through a ref because the render-capturedstate.erroris stale in the closure immediately afterbeginSendresolves.A unit test pins the invariant: an oversize (2764×4096) avatar produces a snapshot that stays far under the cap with body dimensions clamped to 512px.