Repository navigation
fix(github): show private-repo body images from signed links - #3252
Conversation
Private repositories' attachment links (github.com/user-attachments/assets/...) need a GitHub browser sign-in, so the panel showed "Image unavailable". The rendered body HTML GitHub already returns carries signed private-user-images.githubusercontent.com links that load without one. Use that signed link as the media source for each asset, keeping the original link as the fallback. Signed links expire within minutes and change on every read, so the body keeps the links it shows until shown media fails, then reads the panel's object again once and takes the new links. Media that fails again before loading does not read again. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
…ed reads Review follow-ups for private-repo body media: - A renewal read that failed or changed nothing blocked every later renewal. Allow one read in flight and clear it when the read settles; media that failed and has not loaded since still does not read again. - Adopting renewed HTML remounted every media element, because react-markdown got new component types on every render, restarting playing videos and reloading loaded images. Make the components stable (they read the media renderer from context) and have each element keep the signed link it shows until that link fails, so a renewal swaps only the media that failed. This replaces the body-wide HTML pinning. - Reading an issue panel again for an expired image replaced the shown issue with an error when the read failed. A failed read now keeps a shown object; a first read's error is unchanged. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
|
🤖 Addressed the 3 findings from an automated review in 0de78e3:
|
|
🤖 Second automated review raised one P2: after a renewal read fails, a failed media element stays latched until it loads, so on issue panels (no polling) it shows the original link until the panel reopens. Not changing this. The latch is what stops a read loop for a deleted or broken asset. Retrying would need a timed backoff, which is more than this fix needs. Pull request panels recover on the next 30s poll. Issue panels recover when reopened. Noted under "Not checked yet" in the description. |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
The GitHub panel loaded a private-repository signed link before media's own link. A stale or expired signed link (for example in a body read more than five minutes earlier) then broke media that loads fine from its own link, which failed the packaged media policy browser journey. Media now loads its own link first, exactly as before this change. Only when that fails and the rendered HTML has a signed link for the asset does that element switch to it; when the signed link fails, the panel reads GitHub again (one read at a time, no read again for media that failed and has not loaded since) and that element takes the renewed link. Public repositories make no extra requests. Adds a browser case for a private repository's media: its own links 404, its signed links load under the enforced packaged media policy, in both engines. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
|
🤖 Agent review at f4de506 raised one P2: signed-link pairing misses the legacy
Private-repo bodies I sampled render signed images with no |
When a renewal read fails, or the renewed signed link fails too, media that still has no newer link to try now reads again after 30 seconds, at most three times until a signed link loads. Issue panels do not poll, so before this they stayed broken until reopened. One read and one timer at most; a link a poll delivers is tried first; unmount clears the timer. Public media and media whose own link loads are unchanged. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
A load reset the retry count, and media that loaded and then failed on the same link read again at once, so a partly loading video could read GitHub in a loop. Each source now reads at once only on its first failure, and the 3 retries never refill, so a panel reads at most once per signed source plus three times. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
A network blip on a signed video renewed its link and remounted the player, which aborted the player's own quiet reload. Expired and missing links fail as unsupported (code 4) in Chromium and WebKit, so only those take another link. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
kazemek21
left a comment
There was a problem hiding this comment.
Kyle reviewed this PR and the Nexus review-tracker notes and approves, with the non-blocking comment below.
Posted by Codex (gpt-6.1-sol) on behalf of kylez.
Only video reloads quietly after a network error, so audio network errors take the normal failure path again. A video network error now shows the original link until the video loads again, so it stays beside "Video unavailable" when the player's reloads give up. Co-authored-by: Squirtle Squad <ec57954349ef49ce554b9cda062c34933dd3a085cf82278bb0e955efea492c7e@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Allen Tsai <[email protected]>
…y-untagged-reactions * origin/main: (324 commits) fix(github): show private-repo body images from signed links (#3252) NEXUS-4029: Preserve semantic chips in the core composer (#2303) Make plugin signature enforcement opt-in (#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788) feat(plugins): report conclusively retired update providers (#3263) feat(messages): let attachment renderers open files from the host file card (#2239) feat(charts): bundle a Charts code-block renderer (#3226) Expose native reply grouping through Conversation.ui (#3245) NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787) NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786) NEXUS-4022: Start region chats in the New Chat editor (#2784) feat(link-previews): unfurl web links below messages by default (#3225) fix(messages): keep names in literal Markdown as written when editing (#2834) feat(plugins): fetch a packaged managed plugin policy from a URL (#2504) fix(goose): enable deferred tool loading for goose agents (#3227) fix(unread): preserve manual intent in captured explicit reads (#3228) NEXUS-4337: Expose retained plugin update reviews (#3075) feat(conversation): add a link preview slot below messages (#3219) feat(plugins): let link previews read a public page's preview fields (#3217) Let sidebar row plugins decorate ordinary Solo chats (#3222) ...
* origin/main: (121 commits) Expose Solo region state before a component mounts (#3296) fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (#3241) feat(agents): file new Solo chats in explicit personal workspaces (#2857) perf(css): limit broad style invalidation (#3267) Delay and fade in the Bestie badge celebration (#3292) fix(github): show private-repo body images from signed links (#3252) NEXUS-4029: Preserve semantic chips in the core composer (#2303) Make plugin signature enforcement opt-in (#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788) feat(plugins): report conclusively retired update providers (#3263) feat(messages): let attachment renderers open files from the host file card (#2239) feat(charts): bundle a Charts code-block renderer (#3226) Expose native reply grouping through Conversation.ui (#3245) NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787) NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786) NEXUS-4022: Start region chats in the New Chat editor (#2784) feat(link-previews): unfurl web links below messages by default (#3225) fix(messages): keep names in literal Markdown as written when editing (#2834) feat(plugins): fetch a packaged managed plugin policy from a URL (#2504) fix(goose): enable deferred tool loading for goose agents (#3227) ... Signed-off-by: Tyler Longwell <[email protected]> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
* origin/main: NEXUS-4029: Offer folder chips in the composer (block#2789) perf(css): narrow effort button invalidation (block#3297) NEXUS-4433: Expose saved plugin installation source kind (block#3242) fix(identity): keep an unbacked key safe on Replace and unblock a corrupt saved key (block#3233) Expose Solo region state before a component mounts (block#3296) fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (block#3241) feat(agents): file new Solo chats in explicit personal workspaces (block#2857) perf(css): limit broad style invalidation (block#3267) Delay and fade in the Bestie badge celebration (block#3292) fix(github): show private-repo body images from signed links (block#3252) NEXUS-4029: Preserve semantic chips in the core composer (block#2303) Make plugin signature enforcement opt-in (block#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (block#2788) feat(plugins): report conclusively retired update providers (block#3263) feat(messages): let attachment renderers open files from the host file card (block#2239) feat(charts): bundle a Charts code-block renderer (block#3226) Expose native reply grouping through Conversation.ui (block#3245) Signed-off-by: Joah Gerstenberg <[email protected]> # Conflicts: # src/features/conversation/ComposerCompletions.test.tsx # src/features/conversation/ComposerCompletions.tsx # src/features/messages/MessageComposer.tsx
Problem
In the GitHub panel, images and videos in a private repository's PR or issue body show as "Image unavailable".
The panel loads each one from its markdown link,
https://github.com/user-attachments/assets/<id>. For a private repository, that link needs a signed-in GitHub browser session. Buzz has none, so GitHub returns 404. Public repositories are not affected.The rendered body HTML that the panel already reads (
body_html/bodyHTML) has a signed link for each asset onprivate-user-images.githubusercontent.com. Signed links load without a sign-in, but they expire after about 5 minutes and change on every read. Each reader gets them from their ownghsign-in, so only people who can read the repository see the media. Nothing goes through the relay.Fix
Media loads its own markdown link first, exactly as on
main. The signed link is used only when that fails.attachments.ts:attachmentMetadata()also keeps each asset's signed link, under the same asset key. A newsignedUrl()looks it up.attachmentFor()still returns the markdown link.GitHubBody.tsx, per media element:main.The markdown
componentsobject moved to module scope (with a small context for the media renderer), so a new read no longer remounts every media element.index.tsx: passes the panel's existingloadtoGitHubBodyasrefresh. No new fetch code. A failed reload of an issue panel now keeps the issue that is shown, the same as pull request panels. A failed first load still shows the error.The app's CSP already allows
private-user-images.githubusercontent.com(media-src) andhttps:images (img-src). No config change.Screenshots
Explanatory guide; unmodified screenshots below. Captured from the real
GitHubPanel(opened by clicking a PR link in a message) with Playwright, under the packagedimg-src/media-srcpolicy. GitHub responses are mocked: each asset's own link returns 404, as for a private repository, and its signed link returns 200. All data is fictional. 1440×950 CSS px viewport, 2x. Chromium shown; WebKit passed the same assertions.mainat 7f35bb1Light theme, wide panel, larger text, keyboard
Wide panel (774 px). Media keeps the shared
MediaAttachmentmaximum sizes.Root text at 20px. The title wraps cleanly; the video is reachable by scrolling the panel.
Keyboard: one Tab from the title reaches "Open image fullscreen" with a visible focus ring. Enter opens the viewer with the signed image; Escape closes it and returns focus to the preview.
Tests
RTL (
GitHubBody.test.tsx,attachments.test.ts):own link first for images and videos;
own-link failure switches to the signed link with no read;
signed-link failure reads once, with the original link as the fallback link;
only the failed image takes the renewed link, and the loaded video keeps its DOM node and link;
a renewed link that fails before loading does not read again;
one read at a time, and a later expiry reads again after a failed read;
public and raw media keep their own links;
GitHubPanelreads the PR again when its signed image fails;an issue panel stays shown when that read fails;
signedUrl()finds private assets and returns nothing for public and raw ones.with fake timers: after a failed renewal, reads again at 30s, at most 3 times; a renewed link that loads stops retrying without refilling them; loads of other media do not refill them; media that loads then fails on each link stays within the cap; unmount clears the timer; no timer for public media or media whose own link loads; a link a poll delivers is tried before reading again; a network failure (code 2) on a signed video neither reads again nor remounts the player, and shows the original link until the video loads again; loaded audio that fails on a network error shows its original link (both fail on 2220d8e, pass on d057182).
Run against the previous signed-first head, 9 of these fail; all pass on this change. The retry tests fail on f4de506 (no retry) and pass on db501c7.
Browser journey, 1 added, 0 removed (
tests/browser/github-body.spec.mjs, "private repository media › private PR media falls back to its signed links under the packaged media policy"). jsdom loads no media and enforces no CSP, so only a real engine can show that a real 404 on<img>and<video>triggers the switch, and that the signed link decodes under the packaged policy. It reuses the existing spec's fixtures. The deliberate 404s use the fixture's existingexpectedStartupLoadFailureoption, scoped to this case.mainin Chromium and WebKit (the image becomes "Image unavailable").bin/pnpm exec vitest run src/bundled/github: 22 files, 374 passed.tsc --noEmitandbiome check --error-on-warningspass.bin/pnpm test:browser tests/browser/github-body.spec.mjs --project chromium --project webkit: 4 passed.Hooks: committed and pushed through lhm with lefthook 2.2.2. Pre-commit (literal names, icons, biome, sadscan) and pre-push (types and related unit tests, design system) passed.
Real data: for a private
squareupPR read withgh, the asset's own link returns 404 without cookies and its signed link frombody_htmlreturns 200image/png. This is the shape the mocks use.Human test (Allen): in a dev build of this branch (
bin/just desktop me) signed in as himself, the images in a privatesquareupPR body load in the GitHub panel; the same PR in the released app shows "Image unavailable". Tested at db501c7; the later commits change only failure handling, and Allen confirmed the test covers 2220d8e.Agent app run: skipped by Allen; his test in the native app covers it. The browser journey exercises the fallback in Chromium and WebKit.
Review
user-attachments/files. Those are downloads, not inline media. Legacyuser-imageslinks are already paired by the CDN path.Limits
buzz-review-completed
🤖 Generated with Claude Code