Skip to content

fix(channels): read a channel and its open thread when returning to it - #1835

Merged
johnmatthewtennant merged 8 commits into
mainfrom
jtennant/oliver/channel-read-open-thread
Oct 29, 2026
Merged

johnmatthewtennant merged 8 commits into
mainfrom
jtennant/oliver/channel-read-open-thread

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

Opening a channel whose thread pane is open no longer marks the channel read. Buzz reads only in the focused surface, and returning to a channel with a visible selected thread moved focus to that thread's tab and skipped main composer autofocus, so the thread was read but the channel's own messages stayed unread until you clicked into them. This is a bug fix for the channel visit contract: opening a channel reads what it shows.

What

Returning to a channel focuses the main composer again. While focus stays anywhere in that channel's workspace, its visible selected side-pane thread is also a reading surface, so both the channel and the open thread are read. A collapsed pane, another channel's tab, a members or media-review view, and passive restoration still leave the main conversation as the only reading surface. Pane motion completion still wakes observation. A thread in the main pane reads only while focus is inside it.

Only a plain channel visit reads the open thread this way. A link to a message or thread reads only what it focuses. A thread opened from a link or Activity takes focus once its pane shows it, including when the pane was collapsed on that thread or the thread was already selected in the main pane.

Reviewer-reproducible examples

tests/browser/thread-unread.spec.mjs, "returning to a channel reads it and its already-open thread" (1440 px and 1280 px), drives the production UI and read-state engine against isolated relay fixtures: a channel with unread top-level messages and an unread reply in its open thread, switch to another channel, then explicitly return with the pane open.

  • On main, in Chromium and WebKit, focus lands on the thread tab instead of the main composer, and the thread dot clears while the channel stays unread (its count remains 18 above the unopened neighbor thread's).
  • With this change, the main composer has focus and both clear after reading dwell, while the unopened neighbor thread keeps its dot.

The collapsed-pane case still leaves the hidden thread unread.

Four more regressions in the same file, each failing on the previous head and passing here, in Chromium and WebKit:

  • "opening a message link reads the channel but not its already-open thread": a link to a top-level message reads the channel, and the selected thread beside it stays unread.
  • "opening a thread from Activity reads it when its channel's pane was collapsed": with Alpha's pane collapsed on Thread root 1, open it from another channel through Alpha's Activity popover. Previously focus stayed on the document body and the thread stayed unread; now the thread tab has focus and the thread reads.
  • "a live reply in a main-pane thread stays unread while focus is in the side-pane thread": with Thread root 1 moved to the main pane and Thread root 0 in the side pane, return to Alpha, select Thread root 1, focus Thread root 0's reply composer and deliver a reply to Thread root 1. It stays unread.
  • "opening a main-pane thread from Activity reads it, not the side-pane thread": with Thread root 1 already selected in the main pane and Thread root 0 in the side pane, reopen Thread root 1 from Alpha's Activity popover. Previously Thread root 1 stayed unread; now it reads and Thread root 0 stays unread.

Screenshots

After returning to Alpha with Thread root 1 open, from the regression on this build: the main composer has focus, Thread root 1 has no unread dot, and the unopened Thread root 0 and Broadcast reply keep theirs, which is why Alpha keeps its sidebar dot.

Alpha with its open thread read after returning

🤖 Generated with Claude Code

Oliver and others added 2 commits October 28, 2026 12:51
Returning to a channel with its thread pane open moved focus to the
thread's tab and skipped the main composer, so only the thread was
read and the channel stayed unread. Focus returns to the main composer,
and while it stays in that channel's workspace the visible selected
thread reads too.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
Fold the channel-versus-neighbor assertions into the open-pane branch of
the returning-to-a-channel journey instead of repeating its setup.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant marked this pull request as ready for review October 28, 2026 17:12
@johnmatthewtennant
johnmatthewtennant requested a review from a team as a code owner October 28, 2026 17:12
Oliver and others added 2 commits October 28, 2026 14:00
A link to a top-level message focuses the main timeline, but the
already-open selected thread still read beside it. Only a plain channel
visit, with no requested message or thread, reads that thread now.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
Opening a thread from Activity while its channel's pane was collapsed on
that thread tried to focus the tab while the dock was still inert. The
pane then opened without a retry, so focus stayed on the body and the
thread never read. Hand focus to the selected tab once the pane shows a
routed thread, unless something else already took it.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
Comment thread src/features/messages/use-reading.ts Resolved
The channel workspace marker comes from the side pane's selected thread,
but it also let a main-pane thread read while focus rested in the side
pane, so a live reply in main-pane X was read while the person typed in
side-pane T. Only a reading surface inside the side pane takes the grant.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Larry and Rbot: an independent adversarial review of 812c492. Source review only. Hosted checks were green; neither of us ran the browser specs locally.

Approving. The visit contract now matches the thread discussion. A plain visit puts focus back in the main composer and also reads the open side-pane thread. A link reads only what it focuses. A main-pane thread reads only with focus inside it. We traced the guards: inactive tab panels stay inert and aria-hidden, so the workspace grant cannot read covered tabs. routedThread needs requestedMessage, so a plain visit keeps composer autofocus. requestedMessage comes from the navigation target, so a message link does not start reading the side thread later in the same visit.

Two optional comments below. Neither is must-fix.

Comment thread src/bundled/channels/ChannelsPage.tsx Resolved
Oliver and others added 3 commits October 28, 2026 20:32
Opening a thread from Activity when it was already selected in the
main pane focused the side pane's selected thread instead, so the
routed thread stayed unread and the other one was read. Focus the
routed thread's tab wherever it is visible.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
The test links to Thread root 1. Until Alpha's history confirms that
root, the link opens its thread instead, which now takes focus and
reads it. Wait for confirmed history first, so the link is always the
timeline jump the test describes.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
Waiting for confirmed history did not stop CI from opening the link to
Thread root 1 as that thread, which then takes focus and reads it.
Link to an ordinary top-level message instead, as the test intends. It
still fails without the link narrowing.

Co-authored-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Oliver <e78152627ea32fbacb070e5047afcfb8e409fa3343d9e618227baacca5b5e607@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant merged commit afac5b5 into main Oct 29, 2026
42 checks passed
@johnmatthewtennant
johnmatthewtennant deleted the jtennant/oliver/channel-read-open-thread branch October 29, 2026 01:44
tulsi-builder added a commit that referenced this pull request Nov 4, 2026
…pen conversation, describe toolbar badges (#2275)

## Summary

AIDA-2353. This PR has been cut down so it doesn't overlap two open PRs:
- **#2337** owns the Inbox unread count. tulsi left design feedback
there.
- **#2401** and its stack own finding replies in your threads and old
roots.

The badge, older-thread discovery and the 30-day discovery window that
used to be here were dropped in favor of those PRs. What's left is what
neither of them covers, plus one reading fix from beta feedback:

- **Solo chats in Inbox.** Ordinary incoming top-level responses in a
Solo chat appear as one flat conversation under All. They stay out of
DMs, matching the sidebar's separate Solo and Direct messages sections.
Its nested threads stay separate, and agent-only coordination and system
events stay out. A fresh Solo response reopens an archived flat row, the
same way ordinary DM activity does. Opening the flat row reads it in one
save (`markTimelineRead`): the channel catch-up mark covers ordinary
responses, as at the live bottom, and mentions get individual marks, so
thread rows stay unread. (`unread.ts` evidence filter and
`markTimelineRead`, `InboxItem.timeline`, `archive.ts` ordinary-activity
check.)
- **Toolbar page badges are announced.** In the toolbar placement a
page's badge sits beside its icon-only button, not inside it.
`PageNavigation` now points the button's `aria-describedby` at the
badge, so focusing the button announces its count. This applies to
#2337's Inbox count and Reminders' due count.
- **Live replies in the open conversation are read.** Reported in beta
feedback: a conversation open in the Inbox reader kept showing unread
after a new reply arrived. Opening a row leaves focus on the list, and
the reader's focus gate (`use-reading.ts`) only granted side-pane
reading to Channels' plain visits (`data-reading-thread`, #1835).
`PageTabs` now sets the same grant while the page shows its own content
in the main pane and a selected tab in the side pane. A tab moved to the
main pane still reads only by its own focus, and the reply must still be
in view. New `inbox-live-read.spec.mjs` fails without the fix.
- **Selection survives page navigation (tests only).** This behavior is
already on main. New regression coverage:
- `inbox-restoration.spec.mjs` (real shell) leaves Inbox for Messages
and Reminders and comes back.
  - An RTL check that restoring the page doesn't replay the row's read.

## Tests

- Full Vitest, `tsc --noEmit` and `biome check` pass.
- `inbox-live-read` passes 10/10 repeated in Chromium and WebKit, and
fails in both without the `PageTabs` change.
- Chromium: `inbox-live-read`, `inbox-restoration`, `inbox`,
`thread-unread` and `page-placement` pass. Vitest for
`bundled/channels`, `bundled/inbox` and `use-reading` passes.
- Toolbar description:
- Chromium's AX tree (via CDP) reports name "Inbox" and description "3
unread conversations".
  - Playwright's `toHaveAccessibleDescription` passes in WebKit.
- jsdom's accname reads the wrapper text instead, so the unit test
asserts the `aria-describedby` association.

## Checklist

- Agent review: an independent review of the reading grant found no
defects (closed or kept-mounted panes, inert retained tabs and main-pane
tabs never get the grant; only Inbox uses `ui.Tabs`).
- Agent exercise: Playwright journeys above, in Chromium and WebKit.
- Human test: tulsi tested Solo rows under All and the open-conversation
read in the desktop dev app (2026-11-04).

buzz-review-completed

## Not covered

- Older Solo activity outside the retained unread evidence isn't fetched
here. #2401's per-channel windows cover it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: tulsi <[email protected]>
Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants