Repository navigation
fix(links): say why a message preview is unavailable, and preview unjoined public channels - #2136
Conversation
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One remaining public-channel preview case is not covered by the fix.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 The prior leave-to-preview failure is addressed. One non-blocking regression-test gap remains.
beba65e to
f3a29f3
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage. One minor P3: a missing exact-message response is presented as confirmed deletion; details inline. The earlier left-channel re-admission and five-minute recheck findings are addressed in source.
Reviewed head f3a29f3 against base/merge-base 3d8699e. Source-only: no tests or app execution. Hosted checks were successful except skipped Windows validation at the snapshot. Keyboard/screen-reader behavior, native acceptance and performance remain unverified. The public description was inspected; its attached video was not viewed.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage. No new actionable findings. The previous P3 is addressed: an empty exact-message read now says the message is unavailable and may have been deleted; the regression assertion, help text and description agree.
Reviewed head f3906b9 against base 3d8699e, focusing on the three-file change since f3a29f3. Source-only: no tests or app execution. The hosted snapshot showed successful DCO, Semgrep and zizmor checks; this does not establish full-suite or runtime validation. Keyboard/screen-reader behavior, native acceptance and performance remain unverified. The public description was inspected; its historical video attachment was not viewed.
f3906b9 to
9f80fcf
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage. No new actionable findings. The rebase preserves the prior deletion-copy repair and lookup/toolbar behavior; the integration with linkedMessage and ThreadPanel retains the reviewed terminal-state handling.
Reviewed head 9f80fcf against base 90ce5e3, comparing the feature patches with previously reviewed f3906b9. Source-only: no tests or app execution. Hosted checks were successful except skipped Windows validation at the snapshot. Live keyboard/screen-reader behavior, native acceptance and performance remain unverified. The public description was inspected; its historical video attachment was not viewed.
…rd opens
Hovering a buzz://message link into a public channel the viewer hadn't
joined said "Message preview unavailable." even though clicking opened the
message. The preview read the message before looking the channel up, so
discovery refused the read. The same text also covered private channels,
deleted messages, read errors and links from another community.
The preview now looks the channel up first, as the click path does. While
the lookup is pending it shows "Loading message…". Once the channel is
found, a fresh read runs, and a read from before the lookup is never
shown. A withheld channel is never read. A cached-only store can't look
anything up, so it no longer offers `refer`, and its preview keeps the
read's own answer.
Failures come from one table: withheld ("Private channel. You aren't a
member, or it no longer exists."), not-found ("This message was deleted,
or the link is from another community.") and unavailable ("Couldn't load
this preview."). When the channel is found, the card starts with the
channel byline. It ends with a muted "Open link" line; PreviewCard is
unchanged, so the card is still the link. The target-to-reason mapping
moves from ThreadPanel into `threadFailure` in threads.ts, so ThreadPanel
completes navigation with the same reasons as before. The success card
now names an unjoined channel instead of "Channel unavailable".
App.tsx's failure page and the "Message" link label are out of scope.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
… on the way The preview showed "Loading message…" whenever the channel reference was `unknown` and the store offered `refer`. Unknown also covers a lookup that failed and is backing off, and a list that isn't ready or has errored, in which case the hook never asks for one. With a refused read, the card then loaded until a later lookup succeeded; offline, or with a list that keeps erroring, that never came, and a read error never showed "Couldn't load this preview." The store now answers `looking` only while a lookup for that channel is queued or in flight, from the queue and pending sets it already keeps, and notifies list subscribers when a lookup starts and when it fails, as it already did on success. The preview waits only on `looking`; a backoff, an unready or errored list, and a cached-only store all fall back to the read's own answer. A still-demanded reference is looked up again after a cache reset, so it reads `looking` there too. A link from another community almost always reaches `withheld`, since its channel ID resolves to nothing here, so that clause moves from the not-found text to the withheld one, which MessageLink's tooltip shares: "Private channel. You aren't a member, it no longer exists, or the link is from another community." A channel that resolves here is in this community, so not-found is "This message was deleted." buzz-help follows. Tests cover a lookup that fails while the read is refused and a list that isn't ready, the store's `looking` answers and their notifications, and the new withheld wording in the browser journey. Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…t while the list loads A queued channel lookup notified every list subscriber synchronously, once per id. A cache reset or the wake timer queues every demanded channel in a loop, so D mounted links cost 1 + D rounds of session-wide list work (interest updates, live snapshot, activity sort and the rest), and rechecking an expired withheld answer notified although nothing on screen changed. A listener that threw there also left the id queued with no microtask, so `describe` answered `looking` indefinitely. The batch's microtask now notifies once, after its reads start, and skips it when every id in the batch is already withheld. A component that renders in between reads `looking` from the queue anyway. A cache reset with three demanded links now notifies twice: once for the reset and once as their lookup starts. useChannelReference re-ran its demand effect on every answer change, so each lookup released and re-demanded the reference and walked every demanded id in wakeReferences. The effect now depends on whether the answer is `found`, which is the only thing that changes the demand. While discovery is still loading the channel list, the preview treated a refused read as final and showed "Couldn't load this preview.", then flipped back to "Loading message..." once the list was ready and the lookup started. Both are status text, so screen readers announced each switch. The preview now waits while the list is `loading`; discovery either finishes, and the channel is looked up, or errors. An errored list, a lookup backing off and a cached-only store, which never loads a list, still fall back to the read's own answer. Tests cover the per-batch notification, a withheld recheck that notifies no one, the bounded count on a cache reset, a listener that throws while a lookup is queued, and a preview that waits on a loading list and stops once it errors. Each failed on the previous commit. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…er a preview waiting through a ready list When a channel lookup failed, the store told list subscribers before arming the backoff retry. A listener that threw there, inside the read's rejection handler, skipped wakeReferences, so no retry timer was set and a mounted link stayed `unknown` until something else woke it, such as a fresh `refer` or a cache reset. The retry is now armed first. wakeReferences only sets a timer and doesn't change what `describe` answers, so links see the same thing either way. BuzzLinkPreview had no test for a preview whose channel list goes from loading to ready. The harness's `refer` never moved the answer to `looking`, so it couldn't express that path. Now it does, and it tells list subscribers from a microtask, as the store's batch does. The new test checks the settled states: once the list is ready the card is still "Loading message…" and the lookup was demanded once, and when the channel is found it reads again and shows the message. It doesn't check that the refusal never renders. That refusal is in the DOM for the one render before the demand effect runs, by design. The test fails if the preview stops waiting on `looking`, and it is the only test that fails if useChannelReference re-demands on every answer change. No store test covers the new ordering, because the throwing listener surfaces as an unhandled rejection, which fails a Vitest run. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
On this branch the hovered row's floating toolbar drew over a message link's preview card, which PreviewCard means to stay on top. The card opens as "Loading message…", then grows to its final content. Base UI places it from its top edge, so for one frame it grows down over its trigger before moving back up. The pointer, still on the link, is on the card for that frame. The row's pointerout read that as leaving, so useFloatingActionBar hid the toolbar, and the pointerover back on the link re-showed it at the top of the top layer, above the card. Moving the pointer from the link into the card and back did the same. PreviewCard now records which trigger owns each card it mounts, and previewCardTrigger looks it up. A pointerout into the row's own open card no longer counts as leaving the row. The row never hears the pointer leave a portaled card, so the retained hover ends when the card closes and its trigger loses data-popup-open. The layout effect's cleanup now also removes data-actions-revealed. A hover update removes it before React commits the hidden bar, and an overlay or positioning update in between could restore it, leaving the bubble in its hover colour after the toolbar hid. A card closing while it holds the hover always hit that. A MessageActionBar test covers a pointerout into the row's own open card, the return to the row's empty space, the card closing, and a closed card no longer holding the hover. It fails on the previous commit. The card still grows over its trigger for one frame. This change keeps the toolbar from reacting to it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…is open ef8bedf stopped a pointerout into the row's own open card from hiding the toolbar. Other routes off the content still hid it: a jiggle off the bubble's edge into row space and back inside the card's 75 ms close grace, or a diagonal through row space into a card wider than a short bubble. The next pointerover on content then showed the toolbar again, above a card that never closed, where a click meant for the card could land on a reaction button. useFloatingActionBar now keeps a toolbar that is already shown while the row holds an open preview trigger. PreviewCard marks its trigger with data-preview-card-trigger, so it can be told apart from other popups' triggers, which Base UI also marks data-popup-open. data-popup-open stays in the observer's filter, so the card's close releases the hold. An open card never reveals a hidden toolbar. This replaces the pointerout early return, its observer clause and PreviewCard's previewCardTrigger registry. The cleanup that removes data-actions-revealed stays. Tradeoff: leaving the link for the row's empty space, or for its reactions, keeps the toolbar for the card's close grace instead of hiding it at once. Tabbing into the card keeps the toolbar beneath it until the card closes. The MessageActionBar test now expects the toolbar to stay through a return to the row's empty space while the card is open, to hide there once the card has closed, and to stay hidden when the card reopens. The return step fails on ef8bedf. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…bove the row toolbar 9d95f55's hold ends when the card's trigger loses data-popup-open, which Base UI drops as soon as the card starts closing. The card stays mounted through its 150 ms exit. If the pointer comes back to the same link during that exit, Base UI 1.9.0 reopens the card in place, so `promote` never runs again. Leave the link for row space, wait 75–225 ms and come back: the toolbar hid at about 75 ms, showed again on return, and stayed above the reopened card. A click meant for the card could then land on a reaction button. The reviewer's probe reproduced this at 120 and 160 ms on the own and peer message, in Chromium and WebKit. The review suggested hidePopover() then showPopover() in onOpenChange. That runs too early. Base UI flushes a hover change with flushSync, and React commits the row's pending pointerover reveal in the same flush, after onOpenChange returns. The toolbar's showPopover came after the card's, and the probe still had the toolbar on top in every check. onOpenChange now marks a card that reopens while its popup is still data-closed. The overlay observer then runs after the reopening has committed, which flips data-open, and joins the top layer again, after any toolbar that commit showed. A card that is hidden behind an overlay waits for it to close, as before. Hiding and showing a manual popover in one task keeps focus inside it in Chromium and WebKit, with no focusout, so the dialog (actionRef) variant keeps its focus. The hold's comment now says that its selector matches a preview trigger with any popup open. MessageLink's context menu shares the link, so that menu also holds the toolbar, hidden behind it. A MessageActionBar test models the top layer with popover stubs, holds the card's exit at Base UI's getAnimations boundary, and sends the return's pointerover and mouseenter before React commits either. It fails on 9d95f55 and with the hide-and-show in onOpenChange. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…g its message After the viewer joined and then left a public channel, hovering a link to one of its messages failed. Discovery denies the old admission until a fresh lookup confirms the channel is still open, but describeReference answered `found` from its cached open metadata. lookUpReference skipped found answers, and useChannelReference only demanded a lookup while an answer wasn't found, so no lookup or read ever ran. The preview showed "Couldn't load this preview." on every hover, while a click, which resolves the channel, opened the message. Admission now decides. A channel named only by its open metadata answers `found` with `readable: false`, and `looking: true` while its lookup is queued or in flight, so a link keeps the name. lookUpReference skips a channel only when discovery admits it. useChannelReference demands a lookup until the answer is readable. A lookup that still admits nothing is kept in `withheld` for the usual five minutes, as a withheld answer is. BuzzLinkPreview keys its read on readability instead of `found`, so it reads again when the lookup admits the channel, even though the metadata is unchanged. It waits while that lookup is on the way. A channel-only link to a left public channel now runs one background lookup too. Once admitted, its hover card can show the member count, as it already does for an unjoined channel that has been resolved. The store test that expected no lookup for a left channel now covers leave, refer, the found-and-looking answer, and readable recovery. A preview test covers a left channel that the lookup admits again. Both fail on the previous commit. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…hat stays unreadable e607882 keeps a lookup that still admits a left public channel in `withheld`, so a mounted link rechecks it after five minutes. No test covered that branch. Putting back the old predicate, which keeps the channel out of `withheld` because its open metadata still answers `found`, passed the relay and conversation suites. With that predicate no recheck is ever scheduled, and the link stays unreadable until something else asks for a lookup. A fake-timer store test now leaves a public channel, mounts a link to it, and answers the lookup with no admission. It checks that the link still names the channel as `found` with `readable: false` and no `looking`, that nothing is looked up before five minutes, and that the store's own wake-up looks it up again at five minutes. When that lookup admits the channel, the link is readable again. The test fails with the old predicate and passes on the current code. No product change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
A well-formed message link whose ID wasn't in a readable channel said "This message was deleted." readTarget in threads.ts returns `unavailable` as soon as the exact read has no matching event, before it reads any tombstone, and threadFailure maps that to `not-found`. A nonexistent ID, or a message ID paired with the wrong channel, got the same definite deletion claim as a deleted message. The not-found text is now "This message isn't available. It may have been deleted." No reader path tells an observed deletion apart from an empty read: a tombstone that hides the target also ends as `targetStatus: unavailable`. So no case keeps the definite wording, and the reader is unchanged. MessageLink's tooltip uses only the withheld text, and ThreadPanel passes the reason code to navigation, whose page already says "This message isn't available.", so neither changes. buzz-help follows. The preview test for a readable channel without the message now checks the wording itself. It fails with the old text. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
9f80fcf to
fde2585
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage. No new actionable findings. The feature patches are unchanged from the previously reviewed revision; the current toolbar geometry, preview placement and ThreadPanel integration preserve the reviewed behavior. The earlier deletion-copy fix remains intact.
Reviewed head fde2585 against base 8668dbd, comparing with prior head 9f80fcf. Source-only: no tests, builds or app execution; CI was not assessed. Live keyboard/screen-reader behavior, native acceptance and performance remain unverified. The public description and six sampled frames of its 37.64-second historical fixture video were inspected; remaining footage was not reviewed.
* origin/main: (52 commits) feat(agents): give plugin agent types the Buzz side of a turn (#2364) feat(plugins): pin buzz://plugin-install links to a path and commit (#2315) feat(plugins): tint currentColor SVG page icons like host icons (#2314) feat(notifications): add a soft notification sound (#1536) feat(agents): let agents propose a profile picture (#2267) fix(plugins): default reminders and workflows off for fresh installs (#2291) Show all reactors in compact, separate emoji capsules (#2179) fix(panels): remember page split widths per workspace (#2264) feat(search): browse open channels from a Channels tab in search (#2164) perf(relay): page combined replay filters after a reconnect (#2308) fix(composer): make native Mac text shortcuts work past link chips (#2390) perf(relay): read creator proofs for many channels per filter (#2312) fix(channels): let published leave and delete confirm through access loss (#2357) fix(media): save native context menu media downloads and hide loopback links on macOS (#2131) fix(links): say why a message preview is unavailable, and preview unjoined public channels (#2136) test(browser): serve emoji artwork in the reaction hit-area journey (#2138) test(browser): click a reply link only after its row settles (#2358) fix(agents): restore delegated workflow mentions (#2399) feat(identity): always back up keys in managed builds (#2398) test(threads): render before checking the reader across activity resizes (#2389) ... Signed-off-by: Gil Birman <[email protected]> Co-authored-by: Claude <[email protected]> # Conflicts: # src/plugins/author.test.mjs
…w-tab * origin/main: (41 commits) [3/5] Add protection to the Claude harness plugin (#1878) fix(settings): keep the bottom padding on long sections (#2361) fix(mermaid): cap diagram output size (#2231) feat(agents): give plugin agent types the Buzz side of a turn (#2364) feat(plugins): pin buzz://plugin-install links to a path and commit (#2315) feat(plugins): tint currentColor SVG page icons like host icons (#2314) feat(notifications): add a soft notification sound (#1536) feat(agents): let agents propose a profile picture (#2267) fix(plugins): default reminders and workflows off for fresh installs (#2291) Show all reactors in compact, separate emoji capsules (#2179) fix(panels): remember page split widths per workspace (#2264) feat(search): browse open channels from a Channels tab in search (#2164) perf(relay): page combined replay filters after a reconnect (#2308) fix(composer): make native Mac text shortcuts work past link chips (#2390) perf(relay): read creator proofs for many channels per filter (#2312) fix(channels): let published leave and delete confirm through access loss (#2357) fix(media): save native context menu media downloads and hide loopback links on macOS (#2131) fix(links): say why a message preview is unavailable, and preview unjoined public channels (#2136) test(browser): serve emoji artwork in the reaction hit-area journey (#2138) test(browser): click a reply link only after its row settles (#2358) ... # Conflicts: # src/buzz-mcp/tools.ts
Summary
Hovering a
buzz://messagelink showed "Message preview unavailable." for five different failures. One of them was simply wrong: a link into a public channel the viewer hasn't joined, which opens fine when clicked. The preview read the message before looking up its channel, so discovery refused the read. Now the preview looks up the channel first, as clicking does, and each failure says why it failed.#channel, text), after a brief "Loading message…"Every failure card ends with a muted "Open link" line. The card is still the same link, so click, Enter and Tab work as before. ThreadPanel and the preview now share one
threadFailureclassifier inthreads.ts. MessageLink's tooltip uses the same withheld wording, anddocs/buzz-help.mddescribes the new behavior.Channel lookup state:
lookingChannelReferencegains alookinganswer, read from the store's existing queue and pending sets. The card waits only while a lookup is queued or in flight, or while discovery is still loading the channel list. A failed lookup that is backing off, a list that errored, or a cached-only store (which no longer offersrefer) falls back to the read's own answer, so the card can't load forever. A queued batch tells list subscribers once, from its microtask. A failed lookup arms its retry before telling them.useChannelReferenceasks for the channel again only when whether it was found changes.Row toolbar stays under the card
The card opens as "Loading message…" and grows to its final size, covering its link for one frame. During that frame the row's toolbar hid, then showed again above the card, where a click meant for the card could land on a reaction button.
mainhas the same problem when the pointer flicks off the bubble and back within the card's close delay.9d95f5564:useFloatingActionBarkeeps a toolbar that is already showing while the row has an open preview card (PreviewCard marks its link withdata-preview-card-trigger). An open card never brings back a hidden toolbar. This replaces the pointerout exemption fromef8bedfae; that commit's cleanup ofdata-actions-revealedstays.f7db65b26: a card that reopens during its 150 ms exit animation re-enters the top layer after the reopen renders, so it ends up above any toolbar that render showed.Video
Before (merge base
ac8495a8b) on the left, after (9d95f5564) on the right, in headless Chromium. Watch the card in each step:unavailable-link-preview-before-after.mp4
The video was recorded at
9d95f5564, so it doesn't includef7db65b26. That commit only changes what happens when the card reopens during its exit animation (leave the link for 75–225 ms, then return). The video never takes that path; it was checked with a probe in Chromium and WebKit.Tradeoffs / known residuals
App.tsx's failure page and the "Message" link label.Testing
At
f7db65b26, on local macOS:store-system-discovery.test.tstimed out once while the machine's load average was about 50, then passed 5/5 when rerun on its own.playwright test --project=chromium --project=webkit --no-deps tests/browser/buzz-links.spec.mjs: 18 passed.tsc --noEmit,biome check --error-on-warningson the changed files, andpnpm design:check: clean.Earlier on the branch:
218098689,ef8bedfae,9d95f5564andf7db65b26each failed on the previous commit.9d95f5564, and Chromium and WebKit probes of the reopen-during-exit route atf7db65b26.Not yet checked: a human trying it in the app, dark theme, narrow widths, enlarged text, keyboard-only use, and the native desktop app. Push hooks were skipped (
--no-verify), so hosted CI is the first full run.🤖 Generated with Claude Code