Skip to content

Keep replies intact when following same-thread links - #904

Merged
morgmart merged 4 commits into
mainfrom
morganm/tabs-reply-preservation
Oct 12, 2026
Merged

morgmart merged 4 commits into
mainfrom
morganm/tabs-reply-preservation

Conversation

@morgmart

@morgmart morgmart commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Following a link to another message in an already-open thread keeps the unfinished reply, reply target and expanded branches. The thread selects the linked message without rebuilding the conversation. Activity popups also reset their hover behavior after a navigation action.

This is the first slice of the channel-workspace-tabs stack and provides the reader lifetime that retained tabs need. It supersedes the intentionally closed #853. It is rebased onto current main fa03be638, including #850's blank-tab removal and #911's merged CI flake fixes.

Why it matters

Looking up an earlier message should not interrupt the reply someone is writing. Keeping that work in one live reader also makes the later main/side tab moves possible without introducing a second thread-reading system.

How it works

The existing reader verifies the linked message's signed ancestry before selecting it. History paging and exact-message selection have separate cancellation lifetimes. Missing or failed targets leave the current thread usable and report one recoverable notice. Signing, membership and transport boundaries are unchanged.

Wes's review findings are repaired: the initial candidate stays out of shared reconciliation until its edits/deletions are complete; staged selection overlays remain evidence for finite and live deletions; rejected first-open evidence cannot establish a thread identity. These repairs use the existing visibility/admission callbacks and do not change the protected session file.

Verification

  • Six new regressions failed before their respective fixes and pass afterward: held overlay success/failure/cancellation across two real session readers, finite/live withdrawals of selection-staged edits and reactions, and rejected first-open identity.
  • On isolated head 0c230d0a6, rebased onto fa03be638: type checking and all 100 focused thread/target/retained-panel tests pass.
  • Complete message-navigation, thread-opening, navigation-thread-history and buzz-links files pass in Chromium and WebKit (64 checks).
  • Focused architecture follow-up confirms the two findings and rejection guard are addressed. Review threads remain open for re-review; fresh hosted checks are pending.
  • Earlier desktop trials covered the combined stack, not this newly rebased isolated head. Native/package acceptance and required human approval remain separate. No full scan or hosted CI rerun was performed.
  • The known workflow timeout was not patched in this PR; the upstream test: fix four CI flakes (avatar-loading, membership, buzz-links, WorkflowsPage) #911 test repair is included through the rebase.

Stack

Slice 1 of 6. Based on main. Next: Workspace tabs #905.

Later stack PRs still need restacking onto this rebased head; they are being handled individually rather than rewritten during #904's repair.

@morgmart
morgmart marked this pull request as ready for review October 12, 2026 19:12
@morgmart
morgmart requested a review from a team as a code owner October 12, 2026 19:12

@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 in the new retarget path: defer candidate publication until its overlay fold completes, and retain deletions of selection-staged overlays. Details and bounded fixes are inline.

Star Lord automated source review via Wes’s account. Head a310ea71e0ce371218f1138c3c36b2c801632e55; base 8c151a238855802bde7be2ea65ea377c1b31002c. Source-only: no tests/app execution or independent CI verification; the reported combined-stack checks do not establish isolated-slice runtime acceptance. This is a non-blocking comment review, not approval.

Comment thread src/features/relay/threads.ts Outdated
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] Keep the candidate out of shared reconciliation until its overlays settle. acceptTarget calls session.accept, which synchronously forwards this bare message to the existing thread’s receive. Because its root already matches, receive commits and publishes it as a reply; the pending-target filter still refers to the previous messageId. An older linked reply outside loaded history therefore appears with unedited/deleted content while overlay reads are pending, and remains retained if those reads fail or selection is cancelled. Stage and access-check the candidate without publishing it, then admit the complete fold. Add a held-overlay case asserting that the candidate never enters published replies before success or after failure/cancellation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 0c230d0a6a223ed7c9a7eef72fc60247dbf207e6. The initial candidate now goes through the existing pure visibility filter instead of shared reconciliation. The complete candidate/overlay evidence is admitted only after the overlay and tombstone reads finish; a rejected first-open candidate also cannot latch a root for later live traffic.

Three held-overlay regressions exercise the real session and two owned readers: success, overlay failure and cancellation. The candidate is absent from both published reply lists while held and remains absent after failure/cancellation; on success, only the completed edited content is published. All three failed on the reviewed code and pass after the repair. A final-admission rejection regression also failed before the root-commit guard and passes now.

After rebasing onto main fa03be638 (including #911 and #850), all 100 focused thread/target/retained-panel checks and all 64 affected Chromium/WebKit browser checks pass locally. No protected session.ts change was needed for this repair. Leaving the thread open for your re-review; fresh CI is pending.

Comment on lines +336 to +339
const stage = (events: readonly RelayEvent[]) => {
pending = events;
if (expectedRoot) selectionStaged = events;
else staged = events;

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] Include selection-stage IDs when retaining overlay tombstones. This new staging bucket is absent from related()’s lookup IDs, which still include only remote, staged and incoming rows. During retarget, a newly fetched edit/reaction lives only in selectionStaged: the subsequent tombstone query requests its ID, but related(tombstones, id) discards the returned deletion because that overlay ID is not in its lookup set. The selected reply can therefore display a withdrawn edit/reaction even when the relay returned its deletion. Live deletions of the same staged overlay are also dropped by receive. Include selectionStaged in the auxiliary lookup closure, and test both fetched and live tombstones for a newly retargeted message; the existing repair test covers only staged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 0c230d0a6a223ed7c9a7eef72fc60247dbf207e6. The auxiliary lookup closure now includes selectionStaged IDs alongside the existing remote and repair-staged evidence, so both finite and live deletions of newly fetched edits/reactions survive retargeting.

Two real-session regressions select a previously unloaded reply, stage an edit and reaction, then withdraw both via either the finite tombstone response or the live feed while that read is held. Both failed on the reviewed implementation; both now show the original message with no withdrawn reaction.

The complete target/thread/retained-panel files pass (100 checks), and the four affected browser files pass in Chromium and WebKit (64 checks) after the main rebase. Leaving this thread open for re-review; no CI rerun or resolution performed.

morgmart and others added 3 commits October 12, 2026 14:20
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>
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>
Screen candidates without shared reconciliation, retain selection-stage overlay tombstones, and commit first-open root identity only after final admission. Regression coverage exercises held overlays, failure/cancellation, both finite and live withdrawals, and rejected first-open evidence.

Co-authored-by: Carl <goose@block.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
@morgmart
morgmart force-pushed the morganm/tabs-reply-preservation branch from a310ea7 to 0c230d0 Compare October 12, 2026 21:29

@baxen baxen 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.

🤖 Approving on behalf of baxen. They tested the full stack from #909 on desktop: tab drafts surviving channel switches, moving tabs between panes, reusing an already-open tab, the Main pane setting, resuming the cursor on tab return, the return highlight, and same-thread links.

@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.

One P2 remains in the live-withdrawal repair: final shared admission omits tombstones received while the finite read was pending, so another open reader can display a withdrawn edit/reaction. The focused follow-up is inline; bare-candidate staging and the final-admission identity guard look corrected.

Star Lord’s automated source review via Wes’s account. Head 0c230d0a6a223ed7c9a7eef72fc60247dbf207e6; base dd360ca3c7674cedf44b8775596f153331456607. Source-only: no tests or app/native execution. CI snapshot had one browser shard pending and Windows native skipped. Non-blocking COMMENT, not approval.

Comment thread src/features/relay/threads.ts Outdated
if (!active()) return "cancelled";
if (!stage(union(pending, related(tombstones, id)))) return "error";
}
const accepted = acceptTarget(pending);

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] Include retained live tombstones in the shared admission batch. During the new live-withdrawal case, a deletion received while the tombstone read is held is retained in this reader’s remote through selectionStaged. However, another already-open reader for the same root drops that deletion in related() because it has never seen the staged edit/reaction. If the earlier finite read returns no tombstone, pending still contains only the candidate and its withdrawn overlays. acceptTarget(pending) then synchronously publishes those overlays to the other reader without their deletion; only this reader’s later union(remote, ...) has the correct fold. The other reader therefore displays the withdrawn edit/reaction even though the session observed its deletion.

Include already-observed, target-relevant tombstones in the complete admission batch before shared reconciliation. Extend the live-withdrawal regression with the second owned reader used by the held-overlay tests, and assert both readers retain the original content with no withdrawn reaction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in f3871d2. Final shared admission now includes already-observed live deletions that target the staged candidate or its overlays. Another open reader therefore receives the complete fold rather than briefly or permanently restoring a withdrawn edit/reaction.

Extended the existing finite/live withdrawal regression with a second real session-owned reader and checked both readers. The live case fails without this fix and passes with it; all 100 focused thread/ThreadPanel tests, typecheck, and pre-push checks pass. Fresh hosted CI is running. Leaving this thread open for your verification.

Co-authored-by: Carl <goose@block.xyz>
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.

No changes requested for this follow-up: the final admission batch now carries observed deletions with their staged targets, addressing the prior cross-reader withdrawal finding; the extended finite/live regression checks both session-owned readers.

Star Lord’s automated source review via Wes’s account — head f3871d2625d0af7905508576552e5f80dbabad21, base dd360ca3c7674cedf44b8775596f153331456607.

Source-only: no tests or app execution; CI was still running at the snapshot, Windows validation was skipped, and native acceptance remains unverified. Non-blocking COMMENT, not approval.

@morgmart
morgmart merged commit be861c9 into main Oct 12, 2026
23 checks passed
@morgmart
morgmart deleted the morganm/tabs-reply-preservation branch October 12, 2026 22:33
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