feat(desktop): open markdown attachments in an in-app viewer panel - #6731
rileycrane wants to merge 18 commits into
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed head aeffcce256f20e30b45d92d0a8db8216916a31a8 against current base 30d2fc52f96138311f2006627ffc1a6d5ff1865b (merge-base f6e6617a9dcc2308d5039f8afaab974b49fb9577). Requesting changes for one channel-crash regression and one narrow-layout accessibility defect.
I also traced the attachment producer/consumer surfaces, download fallbacks, pane lifecycle, native same-relay/auth boundary, redirect policy, streaming 2 MiB cap, strict UTF-8 decode, and safe markdown rendering. I found no additional blocking defect there. All current GitHub checks pass, but they do not exercise either behavior below faithfully.
Review P1 (#6731): tiptap's pre-mount editor.view is a truthy Proxy — only reading a property off it throws — so the previous Boolean(editor.view) probe reported the view as mounted and let the tray's wiring effect repeat the channel-crashing editor.view.dom read. Probe editor.view.dom instead, extract the tracking into a shared useEditorViewMounted hook, and replace the shallow throwing-getter test with a Proxy-faithful lifecycle suite: pre-mount, mount, unmount, remount, an already-mounted editor at subscription time, stale events from a replaced editor, and listener cleanup. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…own row Review P2 (#6731): in the narrow single-panel layout, opening a document unmounted the focused attachment card and closing unmounted the focused panel, dropping keyboard/screen-reader users onto <body> (then the remounted channel's composer autofocus). The panel now moves focus to its close control on mount and returns it to the opening card on unmount, matched by data-doc-url since the original element was remounted meanwhile; the restore yields to any control that already claimed focus, overriding only <body> and the composer autofocus. A narrow-layout e2e covers open-focus and Escape-restore. Also moves the Preview/Code picker out of the title row onto its own pinned row below the header (review feedback: the shared row squeezed the filename out), inside the chrome-padded body since the header band overlays anything placed after it in the header slot. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aeffcce to
c4175f4
Compare
|
Updated for the review-feedback commits (view picker moved to its own row below the title). Preview viewMarkdown attachment card with the open-in-panel affordance; the document renders in the right auxiliary panel with the Preview/Code picker on its own row, so the filename keeps the title row. Code viewRaw source with syntax highlighting; download fallback stays in the header. |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed the follow-up at c4175f4ee50733b071d6123c13c956605308ea44. The Tiptap lifecycle correction is sound: the new probe exercises .dom on the real pre-mount truthy proxy, mount/unmount events match actual view availability, replacement and listener cleanup are covered, and the targeted narrow-layout focus test passes.
The focus-restoration follow-up still has one blocking P2 edge case: a document URL is not a unique opener identity. I reproduced the duplicate-attachment case below in the E2E app; opening the second card and closing the panel focuses the first card. Requesting a stable per-invocation restoration identity and regression coverage.
Review P1 (#6731): tiptap's pre-mount editor.view is a truthy Proxy — only reading a property off it throws — so the previous Boolean(editor.view) probe reported the view as mounted and let the tray's wiring effect repeat the channel-crashing editor.view.dom read. Probe editor.view.dom instead, extract the tracking into a shared useEditorViewMounted hook, and replace the shallow throwing-getter test with a Proxy-faithful lifecycle suite: pre-mount, mount, unmount, remount, an already-mounted editor at subscription time, stale events from a replaced editor, and listener cleanup. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…own row Review P2 (#6731): in the narrow single-panel layout, opening a document unmounted the focused attachment card and closing unmounted the focused panel, dropping keyboard/screen-reader users onto <body> (then the remounted channel's composer autofocus). The panel now moves focus to its close control on mount and returns it to the opening card on unmount, matched by data-doc-url since the original element was remounted meanwhile; the restore yields to any control that already claimed focus, overriding only <body> and the composer autofocus. A narrow-layout e2e covers open-focus and Escape-restore. Also moves the Preview/Code picker out of the title row onto its own pinned row below the header (review feedback: the shared row squeezed the filename out), inside the chrome-padded body since the header band overlays anything placed after it in the header slot. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c4175f4 to
6f11ce5
Compare
Shared .md/.markdown/.mdx attachments now open in a right auxiliary
panel with a Preview/Code toggle instead of triggering a download,
mirroring berd's markdown-artifact viewer.
Nostr-specific adaptations vs berd: the relay stores markdown as
application/octet-stream ({sha256}.bin — no magic bytes), so
classification is by imeta filename only; relay media URLs require
Blossom auth (401 in plain browsers), so the panel fetches through the
authenticated fetch_media_bytes Tauri command; content-addressed URLs
allow staleTime: Infinity caching. Panel state is URL-search-param
backed (doc/docName) like the other channel panes. Strict UTF-8 decode
with a 2 MiB cap keeps binary/oversized files on the download path,
and surfaces without a panel host (forum, projects) keep download
behavior via a nullable context.
Signed-off-by: Riley Crane <rileycrane@squareup.com>
Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract pane-open handlers (manage channel + markdown doc) into useChannelPaneOpeners, bringing ChannelScreen back under the 1000-line file-size ratchet (1016 -> 984). - Include the markdown doc panel in hasAuxiliaryPanel so narrow layouts enter single-panel view (and Escape-to-close engages) the same way sibling panes do, instead of squeezing the timeline. - Key MarkdownDocPanel by URL so opening a different document resets the Preview/Code toggle. - Pre-gate the open-in-viewer affordance on the advertised imeta size so a >2MiB .md is not fetched twice just to be refused (decoder still re-checks — imeta size is untrusted). - Document the 150-line syntax-highlight cap inherited from SyntaxHighlightedCode in the Code view. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fetch Routing the viewer through the generic fetch_media_bytes command meant an attachment with a forged or absent imeta size could occupy up to the 50 MiB download cap in memory and IPC before the frontend decoder rejected it. Add a markdown-specific fetch_markdown_doc_bytes command whose 2 MiB cap is owned by Rust: an oversized Content-Length is refused before the body is read, and the streamed byte count aborts mid-transfer when the header is missing or dishonest. The untrusted imeta pre-gate stays UX-only. The cap checks are extracted into pure helpers (declared_length_refusal_error, append_chunk_within_cap) so both enforcement points are unit-tested without a Tauri State; media_download.rs tests move to a sibling file per the repo's _tests.rs convention to stay under the per-file line cap. The panel maps the native refusal onto the existing too-large download fallback, and new e2e coverage proves the forged-size fallback plus the reload/back-forward contract. The viewer command skips MIME sniffing deliberately: bytes are strictly UTF-8-decoded and rendered escaped, and a legitimate .md may begin with bytes that sniff as a blocked type. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing onto current main tripped the repository file-size ratchet: main's own growth left ChannelPane.tsx at 1011 lines (frozen) and ChannelScreen.tsx at 1010, so the viewer's pane wiring no longer fits in-file. Extract the agent-session and markdown-doc pane assemblies into AgentSessionAuxiliaryPanel and MarkdownDocAuxiliaryPanel (following the ChannelManagementAuxiliaryPanel pattern), leaving both files under their caps. No behavior change. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…own row Review P2 (#6731): in the narrow single-panel layout, opening a document unmounted the focused attachment card and closing unmounted the focused panel, dropping keyboard/screen-reader users onto <body> (then the remounted channel's composer autofocus). The panel now moves focus to its close control on mount and returns it to the opening card on unmount, matched by data-doc-url since the original element was remounted meanwhile; the restore yields to any control that already claimed focus, overriding only <body> and the composer autofocus. A narrow-layout e2e covers open-focus and Escape-restore. Also moves the Preview/Code picker out of the title row onto its own pinned row below the header (review feedback: the shared row squeezed the filename out), inside the chrome-padded body since the header band overlays anything placed after it in the header slot. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atch The panel-close focus restore located the opener card by document URL alone, but the same attachment can appear in several messages — several cards, one URL — so querySelector always returned the first DOM match and closing a panel opened from a later duplicate focused the wrong card. FileCard now passes the clicked element through the open call, and the channel pane opener records it as its DOM-order index among the cards sharing that URL — a per-invocation identity that survives the narrow-layout unmount/remount where an element reference cannot. The restore consumes the record (deep-link/reload opens fall back to the first match, as before), clamps when the recorded card is gone, and still yields to any control that already claimed focus. Covered by a JSDOM unit suite for the record/restore contract and a duplicate-URL narrow-layout keyboard e2e. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChannelScreen is over the file-size ratchet on main, and wrapping its 140-line channel-pane JSX in MarkdownDocViewerProvider re-indented the block past the line limit. GuardedChannelPane — the wrapper the channel branch already mounts and the forum branch never does — is the natural host: same provider scoping (forum FileCards keep download behavior), no indent shift, and ChannelScreen shrinks back under its allowance. Signed-off-by: Riley Crane <rileycrane@squareup.com> Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6f11ce5 to
7aae92b
Compare
* origin/main: (38 commits) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822) fix(client): resurface hidden DMs from live activity (#6885) fix(desktop): keep the draft space when typing right after a mention pick (#6875) broker: define the agent-to-broker action contract (#6742) fix(desktop): keep project sheets independent from threads (#6901) Add gated security reviews (#6816) fix(desktop): accent-colored mention badges that count thread mentions (#6900) Add Buzz benchmark evaluation layers (#6823) fix(desktop): show edited head content in thread panel (#6887) fix(desktop-tooltip): increase surface contrast (#6897) Deduplicate ACP thread prompt context (#6706) Apply access policy when reusing channel agents (#6838) feat(sidebar): prioritize unread DMs in overflow navigation (#6842) ... Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/features/channels/ui/ChannelPane.tsx # desktop/src/features/channels/ui/ChannelPane.types.ts # desktop/src/features/channels/ui/ChannelScreen.tsx
🔐 Codex Security Review
|
Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed head 263e1aeed116d9bd02839ecbea7289c335f188bb against base 0ccf934b88f610f5f235862ecd51e2dcbae2cb74. Requesting changes for three reproduced P2 UI/accessibility defects below.
The original static duplicate-card and narrow-layout focus cases now pass, and the branch inherits main's Tiptap lifecycle fix. The remaining issues are dynamic opener identity, thread-origin return focus, and project-home auxiliary-pane arbitration.
Validation: source/contract review plus focused Chromium workflows against a fresh current-head E2E build with the mock Tauri/live-event bridge. I independently reran the live-deletion and thread-origin reproductions and reproduced the project-home conflict. This is not native WKWebView or live-relay validation. Focused decoder/attachment tests reported 91 passes; a separate markdown renderer test file could not load because the shared review checkout lacks linkifyjs. No fresh CI or full-suite/native pass is claimed.
* origin/main: (32 commits) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) refactor(db): extract domain stores from database runtime (#6987) feat(desktop): add team sharing to community catalog (#3995) Refresh mobile utility surfaces and theme picker (#6944) fix(desktop): complete project empty and context states (#6980) Fix mobile jump-to-latest flicker (#6807) refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777) refactor(db): split channel membership store (#6782) ... Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/features/channels/ui/ChannelPane.tsx
Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz>
Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz>
Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz>
|
The markdown files aren't getting this functionality from the Inbox view, but seems like a pretty easy fix (my agent patched it locally for me). Ideally it would be great if we can have it work from any view. Having the file fully replace a thread in that right panel when you open from there is a little jarring, so I wonder if it could either push the thread into the center like it does when you hit the 3 dots overflow menu in the top right OR if it could pop out over view the view like it does when you hit the "Expand thread" action in the top left of the panel? |
Signed-off-by: Clay Delk <clay.delk@gmail.com>
|
🤖 I tested this branch against the team relay and put the follow-up fixes into one atomic commit for consideration: Suggested commit: 032d2bd4d git cherry-pick 032d2bd4dWhat this changes
Verification
I used a commit rather than inline suggestion blocks because the change adds several coordinated files and needs to apply atomically. |
* origin/main: (77 commits) fix(acp): pace targeted overflow recovery on consumer capacity (#7325) fix(link-preview): keep composer fetches user-paced (#7211) feat(mesh): upgrade to mesh-llm 0.76.0-rc8 and recommend Qwen3.8 27B (#6189) fix(agent): route GPT-5+ model-service FQNs to Responses (#7358) fix(buzz-acp): wake held ACP threads and fence forked sessions (#7340) fix(mobile): style inline code with the app mono face (#6631) chore(release): release Buzz Desktop version 0.5.23 (#7381) fix(desktop): keep packaged frontendDist relative so Windows embeds assets (#7177) fix(sidebar): simplify unread indicators and emphasize priority activity (#7134) Add generic information-flow control core (#7293) feat(buzz-acp): update base prompt; add buzz context and skills to Pi agents (#7335) fix(desktop): restore mention chip identity icons (#7338) Persist video playback speed preference (#7336) Verify ACP relay events before prompt routing (#7010) fix(buzz-acp): bound busy-owner hold to prevent cross-channel starvation (#7337) feat(desktop): invite owned agents from standalone forums (#7125) fix(desktop): authorize remote mentions at publication (#7124) fix(acp): rename system tag to agent-instructions (#7332) fix(desktop): bind duplicate mention selections to exact recipients (#7133) refactor(relay): extract NIP-29 membership authorization (#7285) ... Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> # Conflicts: # desktop/src-tauri/src/commands/media_download.rs # desktop/src-tauri/src/lib.rs
Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz>
* origin/main: Configure ACP session scope per agent (#7578) refactor(buzz-acp): point agents at buzz --help instead of a command table (#7586) feat(buzz-cli): render an agent-friendly command tree in --help (#7584) fix(avatars): scale agent squircles from normalized paths (#7307) fix(mobile): bind same-name mentions to exact selected identities (#7385) fix(desktop): isolate quota backoff and reuse channel discovery rosters (#6998) test(desktop): isolate login-shell probe measurements (#7570) feat(git): add default-branch management to relay and CLI (#7562) fix(acp): integrate the Buzz Pi adapter fork (#7552) fix(markdown): align mention chip wrapping (#7501) fix(relay): reject presence updates when Redis storage fails (#7532) fix(desktop): let inbox title and message author names truncate under narrow panes (#7550) fix(buzz-acp): report missing models without retrying (#7538) fix(desktop): require a Codex adapter with Astra support (#7427) fix(desktop): order unnamed roster members by full canonical npub (#7503) fix(mobile): standardize public-key identity display on npub (#7493) fix(desktop): npub identity controls across profile, agents, and workflows (#7489) fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495) fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488) fix(mobile): render push notification sender identity as npub (#7494) Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> # Conflicts: # desktop/src-tauri/src/commands/media_download.rs
Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz>



Summary
Port of berd's markdown-link viewer into Buzz desktop. Clicking a
.md/.markdown/.mdxattachment card now opens a right-side auxiliary panel rendering the document with the same markdown pipeline as chat messages, instead of forcing a download.What's in it
MarkdownDocPanel— right auxiliary panel with Preview / Code toggle, download button, and close. Hosted inChannelScreenalongside threads/profiles; participates in narrow-layout single-panel mode and Esc-to-close like the other aux panels.doc/docNameparams viauseChannelPanelHistoryState) — back/forward and reload restore the open document.fetch_markdown_doc_bytesTauri command whose cap is owned by Rust: oversized Content-Length is refused before the body is read, and the streamed byte count aborts mid-transfer when the header is missing or dishonest (a forged/absent imetasizecan no longer buy a 50 MiB download + IPC copy through the generic media path). Both enforcement points are pure, unit-tested helpers. The command deliberately skips MIME sniffing — bytes are strictly UTF-8-decoded and rendered escaped, and a legitimate.mdmay begin with bytes that sniff as a blocked type.markdownDocFile.ts— strict UTF-8 decode + matching frontend cap; binary-or-oversized files fall back to a friendly error + Download.useChannelPaneOpeners— pane-open handlers extracted fromChannelScreen; upstream'srequireThreadEditResolution()guard runs first in every opener (same contract asuseChannelProfilePanel).MarkdownDocViewerProvider— surfaces without a panel host (forum/projects) keep plain download behavior.ChannelPane.tsx/ChannelScreen.tsxover the frozen file-size ratchet, so the agent-session and markdown-doc pane assemblies are extracted intoAgentSessionAuxiliaryPanel.tsx+MarkdownDocAuxiliaryPanel.tsx(pure JSX moves, no behavior change; repo policy: split, never bump).editor.viewaccessor throws until EditorContent attaches the view, andSelectionFormattingTray's wiring effect readeditor.view.domunguarded; navigating to a channel in a dev build could crash the whole channel route into TanStack Router's error screen. The tray now tracks view availability (probe + tiptapmount/unmountevents) and gates its DOM wiring on it, with a regression test pinning the probe.Why it works differently from berd — the Nostr/relay foundation changed three design points:
{sha256}.bin,application/octet-stream) and markdown has no magic bytes — the ".md-ness" survives only in the NIP-92 imeta filename tag, so classification is filename-based./media/origin). This is also why in-app rendering is the only viable UX — "open in browser" can never work.staleTime: Infinityper session.No DOMPurify dance needed: buzz's markdown pipeline has no raw-HTML support (no rehype-raw), so doc HTML renders escaped and
javascript:hrefs are stripped.Known limit (documented in-code): Code view skips syntax highlighting past 150 lines — inherited shiki perf guard shared with chat code blocks.
Related issue
N/A — none found (searched issues/PRs for "markdown viewer": no matches).
Testing
At tip
aeffcce25, based on mainf6e6617a9(no file overlap with newer main commits):desktop-tauri-checkslane), including 4 new cap tests: over-cap Content-Length refused, at-cap/absent admitted, streamed cutoff at exactly 2 MiB + 1 with nothing past the cap buffered;cargo clippy -D warnings+cargo fmt --checkcleanmarkdownDocFiledecoder tests + 2 tray view-guard tests)markdown-doc-viewer.spec.ts(upload → open panel → Preview/Code → download fallback → non-md regression → reload/back-forward contract → forged-size over-cap fallback) +file-attachment.spec.tsbaseline: 21/21 passeddesktop-check(biome/px-text/pubkey-truncation),desktop-typecheck,file-size-check: cleanScreenshots: post via
scripts/post-screenshots.sh <PR#> ~/.buzz/.scratch/md-viewerafter the PR exists (repo policy — no relay media URLs in PR bodies).