Skip to content

fix(ui): let users select and copy chat thread text - #3992

Merged
trunk-io[bot] merged 1 commit into
mainfrom
posthog-code/fix-chat-thread-text-selection
Jul 30, 2026
Merged

fix(ui): let users select and copy chat thread text#3992
trunk-io[bot] merged 1 commit into
mainfrom
posthog-code/fix-chat-thread-text-selection

Conversation

@jonathanlab

Copy link
Copy Markdown
Contributor

Problem

You can't drag-select text in the chat thread, so you can't copy it.

Root cause

Quill's ContextMenuTrigger hardcodes select-none on its rendered element:

// node_modules/@posthog/quill/dist/index.js
function Si({ className: e, ...t }) {
  return D(B.Trigger, {
    "data-slot": "context-menu-trigger",
    className: W("select-none", e),   // <-- hardcoded
    ...t
  });
}

MessageContextMenu in ChatThread.tsx wraps every message bubble in that trigger so users can right-click → "Copy message". That put select-none on each ChatMessage root, and since user-select inherits in Chromium, every descendant down to the message <p> computed to user-select: none.

So the feature added to make a single message copyable is what made the whole thread uncopyable.

The failure mode is total, not partial. A Range over the message text is valid and laid out, but serialises to "" — and that's what the clipboard reads:

rangeToString: "test123"     // the Range itself has the text
selToString:   ""            // the Selection refuses to serialise it

Fix

Quill's cn is twMerge(clsx(...)), so passing select-text at the call site wins over the hardcoded select-none — no package change needed:

-<ContextMenuTrigger render={children} />
+<ContextMenuTrigger className="select-text" render={children} />

Scoped deliberately to the chat thread. The other two ContextMenuTrigger call sites (TabStrip pills, ChannelsList rows) are chrome where select-none is the correct behaviour and are left alone.

Verification

Driven against the running dev app over CDP (test-electron-app), same synthetic mouse drag over the same message both ways:

Trigger class getSelection().toString() after drag
select-none (before) ""
select-text (after) "test123"

Also confirmed:

  • Computed user-select on the message <p> goes nonetext.
  • Both message kinds are covered — UserBubble (ChatThread.tsx:400) and AgentProse (:565) both route through this one MessageContextMenu. Checked a thread with real assistant prose: user message and assistant reply both select.
  • Right-click menu still works — "Copy message" and "Show raw logs" both open.
  • biome check clean; vitest chat-thread suite 47/47.

A note on a wrong first guess

My initial diagnosis was that quill's .quill-chat-message-scroller__item { content-visibility: auto } was the culprit. I applied that fix, re-measured, and selection was still empty — content-visibility was not the cause. Reverted it; only the one-line change above ships. Worth recording because the content-visibility theory is superficially plausible and someone will reach for it again.


Created with PostHog Code

Quill's ContextMenuTrigger hardcodes `select-none` on its rendered element
(`className: cn("select-none", className)`). MessageContextMenu wraps every
message bubble in that trigger, so the class landed on each ChatMessage root
and, because `user-select` inherits in Chromium, every descendant down to the
message `<p>` computed to `user-select: none`.

The visible effect was that a drag produced no usable selection at all: a
Range over the text serialised to `""`, so copy came back empty.

Quill's `cn` is tailwind-merge, so passing `select-text` at the call site wins
over the hardcoded `select-none` without touching the package. Scoped to the
chat thread; the other two ContextMenuTrigger call sites (tab pills, channel
rows) are chrome where `select-none` is correct.

Verified against the running dev app over CDP, same synthetic mouse drag both
ways: `select-none` selects `""`, `select-text` selects the message text. User
bubbles and assistant prose both go through this trigger, and the right-click
"Copy message" / "Show raw logs" menu still opens.

Generated-By: PostHog Code
Task-Id: c0412de8-5b4e-4f02-857e-20297eb9c77d
@trunk-io

trunk-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

😎 Merged directly without going through the merge queue, as the queue was empty and the PR was up to date with the target branch - details.

@jonathanlab jonathanlab added the Stamphog This will request an autostamp by stamphog on small changes label Jul 30, 2026
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(ui): let users select and copy chat ..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 7f51366.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Single-line CSS class fix scoped to one component, no risky territory touched, well-documented and verified by author.

  • 👍 on the PR from greptile-apps[bot].
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 2L, 1F substantive — within ceiling
tier T1-agent / T1a-trivial (2L, 1F, single-area, fix)
stamphog 2.0.0b3 .stamphog/policy.yml @ f4fec98 · reviewed head 7f51366

@jonathanlab jonathanlab added the Create Release This will trigger a new release label Jul 30, 2026
@trunk-io
trunk-io Bot merged commit 919a329 into main Jul 30, 2026
40 checks passed
@trunk-io
trunk-io Bot deleted the posthog-code/fix-chat-thread-text-selection branch July 30, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Create Release This will trigger a new release Stamphog This will request an autostamp by stamphog on small changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant