Repository navigation
feat: smooth selection bar, select across a reply's paragraphs, copy as plain text - #165
Conversation
A reply's paragraphs, headings, list items and code blocks are separate text views, and AppKit keeps one selection per view. A drag stopped at the edge of the paragraph it started in, and a selection made in another view stayed behind in grey. ReplySelection treats one transcript entry (ConversationEntryContainer, or a user bubble that is a row's root) as one selection: - A drag continues into the views above or below. The anchor keeps a real selection and stays first responder. The other views show their part with a temporary background in the selection color, because AppKit draws an inactive view's selection grey. A timer in the event-tracking run loop mode follows the pointer, since NSTextView's drag loop stops asking for ranges once the pointer has left the anchor. - Copy (⌘C, the context menu) reads the whole span in reading order. ⌘A selects the whole entry. A click in another view ends the previous selection, including a double-clicked word. - The reply's context menu adds 复制整条回复(纯文本), using ReplyPlainText: markers, quotes, tab-separated tables and code source, without Markdown syntax. The range arithmetic (TextSelectionSpan) and the plain text are Core code with WorkbenchChecks coverage. The new host check `reply-selection` drags across a native reply with queued events and checks the span, copy, clearing, Select All and the menu. Previews, the scroll-following check and the performance export test compile the new source; check-preview-sources.py now also reads Python file lists like the scroll-following one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar felt janky because it was rebuilt on every selection change. During a drag that means every pointer step: a new NSHostingView, a synchronous layout, and a forced redisplay of a shadowed glass panel. It also appeared mid-drag and jumped with the selection's first line, and while the transcript scrolled it chased each step a frame late. - It appears after the mouse button is released, or 0.12 s after a keyboard change, and fades in (skipped with Reduce Motion). - One hosting view is reused and only moved. It is rebuilt only when quoting availability or the language changes; actions read the selection when they run. - While a clip view that contains the selection scrolls, or the window resizes, the bar hides and returns 0.15 s after it stops. Bounds changes elsewhere are ignored. - Copy and Quote read a span across text views (ReplySelection), and the bar anchors to its first line. The design note covers selection and the bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2e60b79df
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| override func resignFirstResponder() -> Bool { | ||
| let resigned = super.resignFirstResponder() | ||
| ReplySelection.shared.refreshHighlight() |
There was a problem hiding this comment.
Clear cross-view selection when focus leaves the reply
After making a cross-view selection, clicking the composer or any other non-ReplyTextView only causes the anchor to resign first responder. This handler merely recolors the temporary extension, while AppKit retains the anchor's inactive selection, so the newly added multi-paragraph highlight remains visibly stale until another reply text view is clicked. Add a click/focus transition that clears the span when focus leaves the reply, while excluding interactions with the selection bar itself.
Useful? React with 👍 / 👎.
| let visible = window.convertToScreen(textView.convert(textView.visibleRect, to: nil)) | ||
| guard rect.width > 0 || rect.height > 0, visible.intersects(rect) else { return nil } | ||
| let transcript = textView.enclosingScrollView.map { window.convertToScreen($0.convert($0.bounds, to: nil)) } ?? visible | ||
| guard rect.width > 0 || rect.height > 0, transcript.intersects(rect) || visible.intersects(rect) else { return nil } |
There was a problem hiding this comment.
Use the outer transcript viewport for selection anchors
When a cross-view drag starts in a code block and extends upward into an earlier paragraph, firstLineRect returns a rectangle in that paragraph, but textView.enclosingScrollView is the code card's nested horizontal scroll view rather than the transcript. Neither that small viewport nor the code text's visibleRect intersects the earlier paragraph, so anchor(for:) returns nil and the selection bar never appears for this valid selection direction. The visibility check needs to locate the outer transcript scroll view or test the returned candidate view's own visibility.
Useful? React with 👍 / 👎.
Summary
ReplySelectiontreats one transcript entry as one selection:ReplyPlainText): list markers, quotes, tab-separated tables and code source.Implementation notes
ReplyTextViewhooksmouseDown,setSelectedRanges,copy,selectAlland the first-responder changes.TextSelectionSpan) and the plain text are in WorkbenchCore and have tests.check-preview-sources.pynow also reads Python file lists such ascheck-scroll-following.py, which missed new sources twice yesterday. It caught this one.Verification
d2e60b7: 8/8.reply-selection:🤖 Generated with Claude Code