Skip to content

Keep the reply when following a link within the same thread - #853

Open
morgmart wants to merge 2 commits into
mainfrom
morganm/thread-links-keep-reply
Open

morgmart wants to merge 2 commits into
mainfrom
morganm/thread-links-keep-reply

Conversation

@morgmart

@morgmart morgmart commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What this does

When a thread is open in the channel side pane and you follow a link to a message in that same thread, the thread now stays in place. It scrolls to and highlights the linked message instead of rebuilding itself. The reply you're writing stays exactly as it was, including which message you're replying to, along with your scroll position and any expanded branches. Before, the thread reloaded: your typed text came back from its saved draft, but the reply target, scroll position and expanded branches reset.

Links into a different thread still open that thread as before. Other places that show threads (previews, Inbox, media review) are unchanged.

Why it matters

This is mainly groundwork for upcoming channel workspace tabs, where several threads can stay open at once in the main and side panes. With tabs, history and links will routinely point at a thread that's already open. Back and Forward will switch between open thread tabs, and a link to a message will select the thread tab that already holds it. Each of those needs to land in the existing thread without rebuilding it, so you don't lose your place or what you were writing. This PR gives the thread view that ability. The tabs themselves come in a follow-up PR.

On its own, the visible change today is small. Following a link within the thread you're reading no longer resets your reply target or jumps your scroll.

New rule: Navigating within an open thread selects a message in that thread. It never rebuilds the thread or its reply editor.

How it works

  • The existing thread reader can now switch which message it highlights. It uses the same checked lookup that already handles links to specific messages, including messages beyond what's loaded, deleted messages, and access checks.
  • The thread is only treated as "the same" if the linked message's signed thread root matches the thread that's already open. The root hint in a link is never trusted on its own.
  • Nothing on screen changes until the new message is fully verified. Switching messages runs separately from loading older history, so neither interrupts the other. A newer click cancels an older one, and losing access, clearing the cache, or closing the thread cancels both.
  • If the linked message is gone or can't load, the thread and your reply stay put. A short note appears in the thread's status area. "Try again" shows only for failures a retry can fix, and it stays in place with keyboard focus while the retry runs.
  • Keeping a thread mounted exposed an existing bug in two activity popups: the "Working now" avatar above a composer, and the channel activity popup in the sidebar. Choosing an agent closed the popup without telling the popup library, so the trigger wouldn't open again on hover. Both now close through the library's own close action. On main the sidebar version showed up as an intermittent failure in the Me sidebar browser journey.
  • One small, approved change in the relay session (a FOUNDATION file): ordinary threads now use the same guarded message lookup that exact-message links already used. No new reader, cache, permission or signing path.

Verification

All on this branch's head, rebased onto current main:

  • pnpm typecheck, Biome, git diff --check, and the pre-push hook (types and related unit tests, design checks, Clippy).
  • Thread reader tests cover:
    • switching within the same thread without reloading
    • links into a different thread, or to a missing or failed message, leaving the current view untouched
    • a newer click winning
    • access loss mid-switch
    • switching while older history or a refresh is still loading
    • deleted targets
    • a refresh racing a failed switch
  • Thread panel tests cover:
    • the same reply editor staying mounted
    • failures keeping the same reader and showing one notice
    • "Try again" staying mounted while it runs
    • a different thread replacing the reader
    • local retry when there's no navigation
  • Deliberate mutations confirm the new checks catch regressions. With each break in place, the matching test fails:
    • disposing the reader on failure
    • treating a different thread as a failure
    • letting a failed switch interrupt a pending refresh, which was a real bug the reviews found
  • Chromium and WebKit pass all six browser files that exercise activity popups (agent-activity, channel-activity-corners, activity-navigation-focus, layout, me-workspace, thread-unread). The Me sidebar journey that failed intermittently on main passes 60 of 60 repeated WebKit runs with the fix.
  • Chromium and WebKit pass all of message-navigation, thread-opening, and navigation-thread-history. A new journey checks that the actual editor element and the chosen reply target survive two in-thread navigations.

Not yet checked

  • Hands-on try in the desktop app: start a reply, follow a link to another message in the same thread, and confirm the draft and reply target stay.
  • Packaged native runtimes. The new code uses AbortSignal.any, which the app already depends on elsewhere.

Following a link to another message in an already-open channel thread now selects that message in the existing thread view instead of rebuilding it. The unfinished reply, its reply target and expanded branches stay. Same-thread membership comes from the linked message's signed root, never the link hint; nothing changes on screen until the new message is verified. Missing or failed targets keep the current thread with a single status notice.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
@morgmart
morgmart marked this pull request as ready for review October 11, 2026 04:50
@morgmart
morgmart requested a review from a team as a code owner October 11, 2026 04:50

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two correctness changes are needed; details and suggested repair scope are inline.

Star Lord automated source review via Wes’s account. Reviewed head c0b5252aea069a788213d39e7cf77aef0cfa6be5 against base d55d70a071bdaa2b499673dcd40ffa37a7a5d793. Source-only: no tests or app execution; CI was not checked, and desktop/packaged-runtime and keyboard-focus behavior remain unverified.

Comment on lines +237 to +240
navigation?.complete({
status: "failed",
reason: result === "unavailable" ? "not-found" : "unavailable",
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Keep a failed selection from failing the entire page

For a routed target that is missing or has a read error, this call reaches createNavigationController.finish(), which sets the route to failed. src/app/App.tsx:253–272 then replaces PageView with the destination-error screen. That unmounts RetainedThreadPanel, disposes the old reader, and loses the mounted editor, reply target and expanded branches; the local notice/“Try again” cannot survive either. Keep recoverable selection failure local without failing away the retained page, while still settling the navigation attempt accurately. Add coverage through the real controller and app boundary—the new panel tests mock complete, so they cannot catch this teardown.

Comment on lines +347 to +350
const event = acceptTarget(response).find(
(event) =>
event.id === id && contentKind(event) && inChannel(event, channelId),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Stage the candidate without publishing it before overlays finish

acceptTarget is session.accept(events, false), which still broadcasts to every observation, including this thread’s receive. With an existing root, a newly fetched same-root reply is therefore merged into remote and published immediately here, before the overlay read below. An edited/deleted reply outside the loaded history can appear with its original content; if that overlay read fails, the unsafe intermediate row remains displayed even though retarget reports failure. Validate visibility and stage this first response without shared observation, then admit it with the completed overlay/tombstone evidence. Extend the real-session test to inspect the snapshot after the ID response while overlays are held, and after an overlay failure.

The thread composer's Working now popup and the sidebar's channel activity popup closed by flipping their open state directly when a row action navigated. Base UI's hover and press tracking never reset, so a trigger that stayed mounted would not open again on hover. Keeping a thread open across same-thread navigation made this reachable in the thread composer; the sidebar popup already showed it as an intermittent failure in the Me sidebar journey. Both now close through Base UI's own close action.

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two previously reported P2 changes remain unresolved: preserve the thread/editor on failed routed retargets and stage target content until overlay verification completes. Both production paths are unchanged; I found no additional source-demonstrated defect in the two-file popup-close follow-up.

Star Lord automated source review via Wes’s account. Head 40890abef86ffdeb61fa45a62a44e0dc31c921e0, base d55d70a071bdaa2b499673dcd40ffa37a7a5d793. Source-only: no tests or app execution; CI not checked. Desktop/native and actual keyboard/pointer focus remain unverified.

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.

2 participants