Skip to content

feat(fork): a main-list Cite in Chat + Replies goes to the message's replies - #154

Merged
orbitingflea merged 1 commit into
mainfrom
feat/subthread-cite-routing
Nov 14, 2026
Merged

orbitingflea merged 1 commit into
mainfrom
feat/subthread-cite-routing

Conversation

@orbitingflea

Copy link
Copy Markdown
Owner

This pull request sends a Cite on a main-list message in Chat + Replies to that message's replies.

改了什么行为

Behaviour change: yes, in Chat + Replies a Cite on a main-list message opens that message's replies and puts the quote in their reply box, unless the main box or the open reply box already shows text.

  • The main box comes first, then the open reply box. A quote or file chip counts as text. Attachments alone do not.
  • When the replies open, the quote goes after any saved reply draft, and focus stays on its comment box.
  • If no reply box opens, a warning shows and the selection stays.
  • Closing the reply box's model picker returns focus to it.
  • A Cite inside the replies, in other modes, on the dashboard or in a side chat is unchanged.

Why

Before, a sent quote started a new first message, not a reply to the message it quotes.

Checks

Unit tests cover each route, refusals and blank boxes. A browser test at panel and sheet width checks the saved draft, a single quote and focus. The sheet case failed before its fix. The repo's checks for changed files pass. In the web app, each route worked at desktop and phone width.

How to undo

Revert the commit. Nothing is stored.

Design notes
  • The new fork file subThreadCite.ts keeps one entry per thread for the mounted reply box. SubThreadView passes its own composer through ComposerHandleContext, which was null before, and registers it. That non-null context is why closing the reply box's model picker now moves focus back to the box.
  • citeInThread applies the rule as three early returns. It reads both boxes' text from the composer handles at click time, so typing redraws nothing. While a question owns the main box, its typed answer is the text that counts.
  • An open runs inside flushSync. React runs a sync commit's passive effects before flushSync returns, so the reply box has mounted and loaded its draft before the quote goes in, and the Cite button gets that box's real answer. No pending state is kept. A design review replaced an earlier step that parked the quote until the box mounted. That step reported success before the insert and could drop a quote or insert it again later.
  • The open counts as the user's own panel choice, so an automatic Diff that waits for its checkpoint cannot replace it. A maximized panel is unmaximized, so the cited text stays visible.
  • At 980 px wide or less, Replies opens as a modal sheet. sheetInitialFocus reads a one-shot flag that a Cite's open sets, so the sheet leaves focus to the comment box. Without it, focus landed on "Close Replies", and Space or Enter closed the tab.
  • If the reply box refuses the quote, for example because a pending question owns it, the usual warning shows and the selection stays.
  • On a phone, the second route cannot happen, because the Replies sheet covers the main list. The comment box stays anchored to the cited text in the main list, so on a phone it shows over the sheet's edge, as a Cite does elsewhere.
  • Upstream files are apps/web/src/components/ChatView.tsx +2 −17, apps/web/src/components/RightPanelSheet.tsx +2 and apps/web/test/fakeEnvironment.ts +7 −3. In ChatView.tsx, the cite is one call to the fork hook, off in side chats, and its local helper moved to the fork file. RightPanelSheet.tsx passes initialFocus={sheetInitialFocus}. fakeEnvironment.ts takes a settings option, so a browser test can set Chat + Replies. The README's Chat + Replies line gains one sentence.
  • No contract, server or mobile change. Mobile shows no display modes.

…replies

In Chat + Replies, a Cite on a main-list message now opens that message's
Replies view and puts the quote in its reply box. When the main box already
shows text, the quote goes there; else when the open Replies view's box shows
text, it goes into that box. Cites inside a Replies view, in other modes, from
the dashboard and in side chats still go into their own box.

The mounted reply box registers its composer for its thread, and a main-list
cite reads what both boxes show at click time. A cold open runs inside
flushSync, so the reply box has mounted and loaded its draft before the quote
goes in, and the toolbar gets that box's real answer. When the Replies view
opens as a sheet on a narrow screen, the sheet leaves focus to the quote's
comment box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:L labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown

This PR changes the test suite by +9 test cases (browser +1, unit +8, server 0).

New browser test files: apps/web/src/fork/subThreads/subThreadCite.browser.test.tsx.
Each browser test file costs CI seconds of module loading before its first test runs; a unit test costs milliseconds.

Keep tests that would fail for a real reason, and test logic below the UI where it can be. See Testing.

@orbitingflea
orbitingflea merged commit 004b44a into main Nov 14, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant