Skip to content

feat(chat): double-press paste shortcut to expand a collapsed paste - #9299

Closed
chenmingwei23 wants to merge 1 commit into
mainfrom
feat/paste-expand-shortcut-8513
Closed

feat(chat): double-press paste shortcut to expand a collapsed paste#9299
chenmingwei23 wants to merge 1 commit into
mainfrom
feat/paste-expand-shortcut-8513

Conversation

@chenmingwei23

@chenmingwei23 chenmingwei23 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

A large paste into the dashboard composer collapses to a [ Paste #N | M lines ]
token so the box stays readable. Today you cannot see what that token holds without
sending the message. The composer only offers a hover/caret PEEK tooltip
(PasteHoverLayer), which shows the first few lines and cannot be edited. The
click-to-expand chip in PastedChip exists only in the SENT bubble, not in the
composer while you are still writing. So you cannot verify you pasted the right
block, and you cannot trim or edit it in place.

Why it matters

Pasting a log, a diff, or a transcript and being unable to check it before send is
easy to get wrong: the wrong block, an extra copy, a stray line. The user has no way
to open the paste back up and fix it without deleting the whole token and starting
over.

What changed (motivation -> approach -> change)

Goal: let the user open a collapsed paste back into editable text, in place, with a
gesture that matches how Claude Code does it (issue #8513).

Approach and the design trap. The gesture is a SECOND Cmd/Ctrl+V while the caret is
resting on a collapsed token. The important choice is that it is POSITIONAL, not
timed. There is no double-tap interval at all -- the code does not wait, does not
buffer, and does not schedule anything. The first Cmd/Ctrl+V is the ordinary native
paste and fires immediately with zero added latency; it is never delayed and never
undone. The "second press" is simply "the caret is on a paste token and you pressed
Cmd/Ctrl+V again" -- the exact same "caret is on a token" model the composer already
uses for Backspace, Delete, and Arrow keys on these tokens. A timed double-tap would
have made every single paste feel slower while the code waited to see if a second
one was coming; because the trigger is the caret position and not a timer, the
ordinary paste keeps its full speed and the gesture adds nothing to it.

Existing convention. There was no repeated-keypress or double-tap handler in the
composer to match, so none was invented -- the gesture reuses the established
caret-on-token branch structure in ChatInput handleKeyDown. There is already an
expand affordance for a SENT paste (the PastedChip click toggle); this adds the
composer-side counterpart the issue asks for, beside the existing hover peek rather
than replacing it.

The change. expandTokenAt(text, blocks, caret) in pasteTokens.ts replaces the one
token under the caret with its block's verbatim content and returns the caret offsets
that select the inserted text. ChatInput calls it from a new keydown branch on
plain Cmd/Ctrl+V (not Shift+V, which is the existing raw-inline-paste shortcut), calls
preventDefault so the clipboard is not re-pasted on top of the token, and drops the
expanded block from the tracked set -- an expanded paste is now plain editable text
and must not also be re-sent as a separate paste (send maps the remaining tokens via
pruneBlocks).

flowchart LR
  subgraph Before
    A1[caret on paste token]:::ctx --> B1[Cmd/Ctrl+V]:::ctx --> C1[re-paste clipboard]:::removed
    A1 --> P1[hover/caret peek only]:::ctx
  end
  subgraph After
    A2[caret on paste token]:::ctx --> B2[Cmd/Ctrl+V]:::ctx --> C2[token becomes editable text, selected]:::added
  end
  classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px
  classDef changed fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px
  classDef removed fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-dasharray:4 3
  classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E
  linkStyle 2 stroke:#DC2626,stroke-dasharray:4 3
  linkStyle 4 stroke:#16A34A,stroke-width:2px
Loading

Legend: added, changed, removed, unchanged. Pressing the paste key on a collapsed
token now opens it into text you can trim, instead of pasting the clipboard again.

Scope notes for review:

  • Shared file with a sibling. Issue Collapsed paste highlights detach from composer text #8309 (collapsed paste highlights detaching from
    composer text) is being worked in parallel and also touches ChatInput.tsx. My
    edit is one self-contained new branch inside the existing atomic-token block in
    handleKeyDown; it does not touch the highlight-layer wiring (PasteHighlightLayer,
    PasteHoverLayer) or the composer host files (ChatPane, ChatPage, SideChat)
    that Collapsed paste highlights detach from composer text #8309 is editing. If the two land close together this branch is an additive
    hunk and should rebase cleanly.
  • Reversibility. The issue lists re-collapse-on-a-further-press as "ideally", i.e.
    not required for the ask. This PR ships the expand direction, which is the issue's
    stated need (see what a paste holds, trim/edit it in place) -- hence Closes. A
    reversible toggle needs an "expanded but still tracked" block state, and the composer's current model drops a block the moment its
    token leaves the text (the pruneBlocks effect, and pruneBlocks is also what the
    send path uses to decide which pastes to send). Keeping an expanded-yet-tracked
    block would change that send/prune contract and reach into the same composer wiring
    Collapsed paste highlights detach from composer text #8309 is editing. Re-collapse is left as a follow-up rather than widening this PR
    into that shared surface.
  • Accessibility, and the non-gesture path. The gesture is fully keyboard-reachable:
    arrow keys land the caret inside a token (the existing PasteHoverLayer caret
    path), and Cmd/Ctrl+V there expands it, so it needs no pointer and no mouse. For
    READING a collapsed paste without the gesture, the existing hover/caret PEEK
    tooltip stays in place -- it opens on keyboard focus/caret as well as hover and is
    screen-reader announced via aria-describedby, so a user who cannot perform the
    double press can still see the content. Honest limit: there is no non-gesture
    affordance in the COMPOSER that performs the expand-into-editable-text itself
    (no click target, no menu item). The sent-bubble chip (PastedChip) has the full
    title + aria-label + click toggle, but the composer token is drawn in a
    non-interactive mirror layer on purpose -- PasteHoverLayer documents that the
    composer keeps no interactive layer above the textarea, because one would intercept
    clicks and selection. Adding a composer-side click/menu expand affordance would
    reach into that highlight/hover surface, which is Collapsed paste highlights detach from composer text #8309's territory, so it is
    deferred to the same follow-up as re-collapse rather than added here. If review
    wants a non-gesture expand path in this PR, that is a design call for the
    maintainer, since it changes the composer's no-interactive-overlay rule.

Tests

  • website/src/test/pasteTokens.test.ts -- expandTokenAt: replaces the token at the
    caret with its content and selects it; expands ONLY the token under the caret,
    leaving others collapsed; returns null when the caret is not on a token.
  • website/src/test/ChatInput.paste.test.tsx -- the keydown wiring: Cmd+V on a token
    replaces it with the full content; the expanded block is dropped from
    onPasteBlocksChange; a caret NOT on a token leaves it collapsed; Cmd+Shift+V (raw
    inline paste) does not trigger the expand.

Commands run (one file at a time):

npx vitest run src/test/pasteTokens.test.ts
npx vitest run src/test/ChatInput.paste.test.tsx

Both green (46 and 27 tests). The two wiring assertions were mutation-verified:
disabling the new keydown branch reds exactly "replaces the token" and "drops the
block" and nothing else.

Manual verification

Could not render a live frame in this environment: a fresh vite build fails to
resolve @lexical/react / lexical (both are 0.50.0 deps added by #8310 and are
not present in the available install), and npm install is disabled on this host.
The behaviour is pinned by the unit + wiring tests above, including the mutation
check. A rendered frame should come from CI or a maintainer's built environment.

Screenshots / video

Why no screenshot: the diff adds no new rendered element or style. The collapsed
[ Paste #N ] token and the expanded plain text are both produced by unchanged
rendering code; the change only moves existing content into the existing textarea via
onChange. A live frame also could not be produced here (see Manual verification: the
lexical deps are not installed and npm install is disabled).

Related Issues

Closes #8513

@chenmingwei23
chenmingwei23 requested a review from a team September 7, 2026 20:03
@chenmingwei23
chenmingwei23 requested a review from a team as a code owner September 7, 2026 20:03
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 7, 2026
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Intent: Let a user open a collapsed [ Paste #N ] token in the composer back into editable text with a second Cmd/Ctrl+V while the caret is on it, so a paste can be reviewed and trimmed before send -- reusing the existing caret-on-token keydown model, keeping the first press's native paste instant.
Not a goal: re-collapse (the reverse toggle), any change to the paste highlight/hover layers or the composer host wiring (#8309's surface), and any change to the send/prune contract.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of f0be2bf67687e4372021f5cbd8fb338c2335f2cb — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence is in. The base composer already has a live click/tap expand path (onClick={handleTextareaClick} at ChatInput.tsx:3914expandTokenRange at ChatInput.tsx:2895), which the PR description denies; the new helper re-implements that mechanism. The newline-bridge premise, by contrast, checks out against handlePaste (ChatInput.tsx:2842-2851). Final review:

First-Principles-Verdict: CONCERNS

The composer already expands a paste into editable text (double-click/tap → expandTokenRange); the real delta is only a keyboard gesture, and it re-implements the expansion instead of reusing it.

What this change ships

Intent: let a user open a collapsed paste back into editable text in the composer before sending — an ADDITION (issue #8513).

  1. Cmd/Ctrl+V with the caret on a collapsed paste opens it as editable text — justified as a keyboard path, but framed as if no expand existed
  2. Plain Cmd/Ctrl+V on/just past a token no longer re-pastes the clipboard there — declared consequence of the gesture
  3. The gesture also fires one newline past the token (where paste parks the caret) — justified; verified against handlePaste (ChatInput.tsx:2842-2851)
  4. Key-expanded text arrives fully selected; click/tap-expand leaves the caret after it — undeclared divergence between the two expand gestures
  5. New exported helper expandTokenAt — duplicate of expandTokenRange (ChatInput.tsx:2895); 1 consumer

Watch

  • The problem statement is contradicted by base code: "Today you cannot see what that token holds without sending the message" and "no way to open the paste back up and fix it without deleting the whole token" — but handleTextareaClick (ChatInput.tsx:2909-2950, wired at 3914) already expands a composer token into editable text on double-click or single tap, dropping the block identically. The surviving harm is narrower: no keyboard path, plus Claude Code gesture parity. The item earns its place on that ground only.
  • Item 4: two gestures now reach the same capability with different end states (selection vs caret-after); the description justifies selection but never says the click path behaves differently.

Subtractions

  • Drop expandTokenAt from pasteTokens.ts (1 consumer: ChatInput.tsx:2519). In the keydown branch, resolve the range with tokenRangeAt plus the one-newline bridge, then call the existing expandTokenRange (2 existing consumers: ChatInput.tsx:2934, 2949) — folding select-on-expand into it if selection is wanted, so both gestures stay one mechanism.

[FIRST-PRINCIPLES-REVIEWED] f0be2bf

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed f0be2bf67687e4372021f5cbd8fb338c2335f2cb — this comment is updated in place on each push.

Review details

The single candidate hinges on "if the paste event still fires" — an explicit "might/if". The codebase's own rawPasteRef comment at line 2440 documents the established, correct browser semantics: preventDefault() on the Cmd/Ctrl+V keydown does suppress the subsequent paste (which is precisely why the raw-paste path deliberately skips preventDefault to let the paste through). Standard browser behavior confirms this. The candidate cannot establish an observable wrong outcome (c) without assuming behavior contradicted by the code it neighbors, and is self-rated low confidence. It dies under falsification. No other grounded defect exists in the changed lines.

No findings.

[OPUS-REVIEWED] f0be2bf

Verdict parsed from the review's SHA-scoped output markers for commit f0be2bf67687e4372021f5cbd8fb338c2335f2cb.

False positive or not applicable? A repository writer can comment:
/ai-review override fable f0be2bf67687e4372021f5cbd8fb338c2335f2cb: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of f0be2bf67687e4372021f5cbd8fb338c2335f2cb and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] f0be2bf

False positive or not applicable? A repository writer can comment:
/ai-review override gpt f0be2bf67687e4372021f5cbd8fb338c2335f2cb: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of f0be2bf67687e4372021f5cbd8fb338c2335f2cb — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Analysis complete. The gesture's trigger condition is confirmed in tokenRangeAt (caret <= r.end matches the default post-paste caret position), which grounds the main finding.

Design-Verdict: CONCERNS

Positional-only trigger has no recency bound, so it hijacks any later "paste new clipboard here" at the caret's most natural resting spot.

Watch

  • The caret adjacent to a token is the default state after every collapse (tokenRangeAt matches caret <= r.end, and the new branch bridges the trailing \n). A user who copies different content later and presses Cmd/Ctrl+V from that resting position gets an expansion instead of their paste — hours later, regardless of clipboard. The description's rejection of a timed component ("would have made every single paste feel slower") only holds for a leading-delay design; a trailing recency window adds zero latency.
  • An accidental expand has no cheap escape: the rewrite flows through controlled onChange (native undo won't reliably restore the token), the block is already dropped via onPasteBlocksChange, and the inserted content is left selected — so a reflexive second Cmd+V overwrites the entire original paste with the new clipboard.

Suggestions

  • Gate the expand on recency (fire only within a few seconds of that block's collapse) or on the clipboard still matching the block — keeps the first paste instant and confines the gesture to a genuine double-press.
  • Note the gap in the opt-in Lexical composer engine (this branch lives only in the textarea handleKeyDown) in the migration follow-up so the gesture doesn't silently diverge between engines.

[DESIGN-REVIEWED] f0be2bf

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

UX-level review of f0be2bf67687e4372021f5cbd8fb338c2335f2cb — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All evidence is in: no blind read ran (nothing visual was committed), the diff adds one invisible keyboard gesture that overloads Cmd/Ctrl+V on a collapsed paste token, and the repo's custom undo snapshots (value + blocks) make the expand reversible via Ctrl+Z.

UX-Verdict: CONCERNS

An invisible Paste overload: right after pasting, Cmd/Ctrl+V with new clipboard content expands the old paste instead of pasting — undiscoverable until it surprises.

Watch

  • Paste stops keeping its promise exactly where collapse parks the caret: the case-2 bridge (text[caret - 1] === '\n'find(x => x.end === caret - 1)) matches the post-paste resting position, and e.preventDefault() drops the clipboard — so paste-A-then-paste-B explodes A and never inserts B. Moderate frequency (back-to-back pastes) × task diversion × every time at that position; Ctrl+Z recovers (undo snapshots carry blocks), keeping this below block. Smallest fix: compare navigator.clipboard.readText() to the block's content and fall through to native paste when they differ.
  • The gesture has no visible counterpart: no string, hint, or affordance in the diff reveals it, so first-time users never find the PR's value and the caret-dependent meaning of Cmd+V is invisible. Fix: one hint line in the existing PasteHoverLayer peek tooltip ("Press ⌘V again to expand and edit") — display-only, so it respects the no-interactive-overlay rule.

Evidence gaps

  • Blind read did not run: no committed screenshot shows the collapsed [ Paste #N | M lines ] token or the expanded-and-selected result state.
  • The collapse→expand transition on the second Cmd/Ctrl+V has no recording; a committed .webm of the gesture in the composer (CI or maintainer build — the PR notes this env cannot render) would close it.

[UX-REVIEWED] f0be2bf

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision labels Sep 7, 2026
@chenmingwei23
chenmingwei23 force-pushed the feat/paste-expand-shortcut-8513 branch from 9734eb8 to 202b591 Compare September 7, 2026 20:30
@chenmingwei23

Copy link
Copy Markdown
Contributor Author
  • expandTokenAt misses a token the caret is one trailing newline past span=4d576a140324

self-added: yes
mechanism: expandTokenAt caret resolution + the ChatInput Cmd/Ctrl+V-on-token branch

Legitimate and fixed in 202b591. Collapsing a paste inserts token + "\n" and parks the caret AFTER the newline, so the caret-on-token test (tokenRangeAt) missed and a second Cmd/Ctrl+V re-pasted the clipboard when the paste sat before existing text.
Fix at the class level: expandTokenAt now resolves the token whose end is the single separator char (\n or space) immediately before the caret, in addition to a token the caret is directly on -- mirroring the existing "caret just past a token" Backspace branch. Bridging is capped at ONE separator char, so a caret out in ordinary text two or more chars past a token still returns null (covered by a negative test), which is the opposite failure mode a wider match would introduce.
Tests: pasteTokens.test.ts "resolves a token the caret is one trailing newline past" + "does NOT bridge more than a single separator char"; ChatInput.paste.test.tsx "expands when the caret is one trailing newline past the token". Mutation-verified: disabling the separator branch reds exactly the two paste-before-text tests and leaves the single-separator guard test green.

…nline

When the caret rests on a collapsed [ Paste #N ] token in the composer,
pressing Cmd/Ctrl+V again replaces that one token with its full content,
inline and editable, so the paste can be reviewed and trimmed before send.

The first Cmd/Ctrl+V keeps its native, instant paste; the gesture is
positional (caret on a token), not timed, so a single press never feels
slower. Adds expandTokenAt() to pasteTokens.ts and one keydown branch in
ChatInput.tsx, reusing the existing atomic-token handling model.

Refs #8513
@chenmingwei23
chenmingwei23 force-pushed the feat/paste-expand-shortcut-8513 branch from 202b591 to f0be2bf Compare September 7, 2026 20:42
@chenmingwei23

Copy link
Copy Markdown
Contributor Author
  • case-2 space bridge expands on a normal mid-typing caret position span=50166db2a56b

self-added: yes
mechanism: expandTokenAt case-2 separator bridge

Legitimate and fixed in f0be2bf by narrowing the bridge to a NEWLINE only. The earlier code also bridged a single space, and a caret one space past a token's ] ([ Paste #1 ] and more, caret before "and") is a normal mid-typing position where the user means to paste new clipboard content, not re-expand -- so the space case would have hijacked an ordinary paste. The newline case is the only one collapse-on-paste actually produces: it inserts token + "\n" and parks the caret after the newline, which is the paste-before-existing-text re-paste bug case 2 exists for. A space is now left to native paste.
This is the OPPOSITE-failure-mode check on the widening from the prior round: widening case 2 to a space introduced a false-expansion path, so the fix is to keep only the branch the intent needs.
Test: pasteTokens.test.ts "does NOT bridge a space (a caret one space past a token is normal typing)" asserts null for the space case; the newline tests still pass.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 7, 2026
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Standing down: the capability already exists, and the paste-key gesture cannot be made safe

What the composer already does. A collapsed [ Paste #N ] token in the dashboard composer already expands into the full, editable text inline -- on a mouse double-click and on a touch tap. Both routes call expandTokenRange (website/src/components/ChatInput.tsx:2894), which replaces the token with the block's verbatim content, drops the backing block, and places the caret after the inserted text. Tests pin it: ChatInput.test.tsx "expands on a mouse double-click (detail=2)" and "expands the token on a single tap on a touch device". So the issue's need -- see what a collapsed paste holds, and trim or edit it in place -- is met today for pointer and touch. The only thing missing is a keyboard route.

Why the requested keyboard gesture (double-press Cmd/Ctrl+V) cannot be bound safely. The gesture keys on caret position: a second Cmd/Ctrl+V while the caret sits on a token expands it. Three review lanes flagged one defect from three angles -- it is a position-based mode, so a user who pastes, copies something new, and presses paste again at that same caret spot gets the OLD paste expanded instead of their new clipboard content. Their paste silently vanishes. That is the worst surprise a text input can produce, and nothing on screen signals the key means something different there.

The correct binding would be "expand only as a continuation of the paste that just happened" -- a short recency window AND the clipboard content unchanged since. But the paste shortcut fires on keydown, and a keydown carries no clipboard data: clipboard text is only available synchronously in the paste event itself (e.clipboardData.getData('text'), ChatInput.tsx:2817), which is the native paste the gesture must not suppress. Reading the clipboard on keydown means navigator.clipboard.readText(), which is asynchronous and permission-gated -- to await it you must preventDefault the paste first, which already destroys the paste when the answer turns out to be "clipboard changed, this was a real paste." A recency-only window with no clipboard check still expands the old paste when a new copy+paste lands inside the window. There is no way to decide, synchronously on the paste keydown, whether this press is a continuation or a new paste. So the gesture cannot both keep a paste instant and avoid eating a later paste.

Recommendation. Close the PR (its keyboard override is a paste key that sometimes does not paste). The issue's stated need is already served by the existing double-click and tap expand. If a keyboard route is still wanted for accessibility, it should be a DISTINCT chord (not the paste key) that expands the token at the caret and reuses expandTokenRange -- that is a small, safe change, but it changes the gesture this issue requested, so it is a maintainer design call rather than a bug fix.

Evidence gathered on head f0be2bf. This comment records the finding; the disposition (close, redesign to a non-paste chord, or accept the existing pointer/touch expand as sufficient) is the maintainer's.

@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Closing unmerged. The analysis is on this PR and on #8513, and the short version is that the
gesture #8513 asks for cannot be bound safely on the paste key.

Clipboard text is available synchronously only inside the paste event, which is the native paste
this gesture must not suppress. Reading it on keydown requires the async, permission-gated
navigator.clipboard.readText(), and awaiting that means calling preventDefault on the paste
first -- destroying the paste in exactly the case where the answer turns out to be "the clipboard
changed". A recency window alone still expands the old token when a fresh copy-and-paste lands
inside it. There is no synchronous way to distinguish a continuation from a new paste at keydown,
so the key is either a race or a paste that sometimes does not paste.

Three review lanes found that same defect from three directions, which is what prompted the
requirement that broke it: bind the gesture to the paste it continues, or do not ship it.

Worth recording that the capability itself already exists for pointer users -- the composer
expands a collapsed paste token on mouse double-click and on touch tap, both calling
expandTokenRange, both pinned by tests. What is genuinely missing is a keyboard route, and that
is a real accessibility gap; it is being tracked with the other pointer-only affordances rather
than rebuilt on the paste key.

The issue stays open: its underlying need is legitimate and a distinct chord reusing the existing
expansion would serve it. Only the specific gesture is unsafe, and that is a design choice for a
maintainer rather than something to decide by shipping.

@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 7, 2026
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.

Double-press the paste shortcut to expand collapsed pasted text inline in the composer

1 participant