fix(terminal): trim the padding and shared indent out of a copied selection

xterm hands back whole screen rows and trims only the cells that were
never written to, so the real spaces a full-screen TUI paints across the
unused part of a row count as content and reach the clipboard. Measured
against Claude Code in a 282-column pane, single lines arrived carrying
138 trailing spaces, and every line carried the two-space transcript
indent as well. Windows Terminal, iTerm2 and GNOME Terminal all trim that
for you, decideAutoCopy already calls a wall of spaces "never what the
gesture meant", and _selectTouchSelectionLine already treats those cells
as padding — the mouse and keyboard paths never had the same rule.

CodemanCopySelection.clean lives in constants.js beside decideAutoCopy,
its pure sibling. It drops the trailing run from each line, and removes
the leading run only where every selected row shares one. A selection of
a single row keeps its run, because one row shares nothing with anything
and stripping it would silently reindent one line of `git log` body text
or one line out of `less`. A drag that began inside a row keeps its
partial first line untouched and out of the measurement, which otherwise
pins the shared run to zero and leaves every following row indented.

Every pass over a line is a scan rather than a regex. `/[ \t]+(\r?)$/` is
quadratic on a line whose spaces are followed by a non-space character,
which is what right-aligned or centred TUI content looks like: measured
over 50 000 rows with a 280-column run it took 2.9s, against 1.3ms for
the scan, and a 2 000-column run took 16s. The scan is also the faster of
the two on an ordinary padded row.

cleanedTerminalSelection in terminal-ui.js is the half that needs the
live terminal. It returns a COLUMN selection untouched: Alt+drag makes
one, and a rectangle's rows lining up is the point of the gesture, so
both halves of the clean would destroy it. xterm exposes the mode nowhere
public, so the check reads terminal._core._selectionService, the way this
file already reads terminal._core for cell dimensions, and cleans
normally if a future xterm renames the field. A test pins that assumption
against the library rather than against a stub repeating the literal.

The Ctrl+C chord decides on the cleaned selection, not the raw one. A
drag across the blank part of a row selects real padding spaces, so the
raw text is truthy, and testing it would spend that press on a copy of
nothing and make the user press again to interrupt. A padding-only
selection is now dropped and the press falls through to the PTY, while
Ctrl+Shift+C still never falls through. copyTerminalSelection gates on
trim() for the same reason, since a multi-row drag across padding cleans
to line breaks alone and a bare newline pasted into a chat composer
submits it.

All four of the main terminal's copy paths go through it: the Ctrl+C
chord, right-click, the phone selection button and Auto Copy. The
browser's own Edit menu copy, a disabled copy shortcut and the subagent
windows still copy raw rows, as they did before, and the invariants doc
now says so rather than claiming every copy is cleaned. Auto Copy
resolves its own toggle before it reads the selection, since it is off by
default and a selection can run to the 50 000-row scrollback ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Grundberg
2026-09-18 20:18:25 +02:00
co-authored by Claude Opus 5
parent 20fc7b3c3d
commit f9edb33d15
7 changed files with 505 additions and 9 deletions
@@ -0,0 +1,31 @@
---
"aicodeman": patch
---
fix(terminal): trim the padding and shared indent out of a copied selection
Copying several rows out of a pane put a wall of spaces on the clipboard and
repeated the program's own margin on every line. xterm hands back whole screen
rows and trims only the cells that were never written to, so the real spaces a
full-screen TUI paints across the unused part of a row count as content:
measured against Claude Code in a 282-column pane, single lines arrived
carrying 138 trailing spaces on top of a two-space transcript indent. Pasting
that into a chat client or an editor meant deleting the whitespace by hand,
while Windows Terminal, iTerm2 and GNOME Terminal all trim it for you.
A copy now drops the trailing run from every line. It also removes the leading
run, but only the one that every selected row shares and only when the
selection covers more than one row, so shell output is untouched, a single line
keeps the indentation you can see, and anything nested inside a copied block
keeps its relative indentation. A drag that starts inside a row keeps that
partial first line exactly as it was. An `Alt+drag` rectangular selection is
copied verbatim, because its columns lining up is the point of that gesture.
The main terminal's four copy paths go through it: the Ctrl+C chord,
right-click, the phone selection button and Auto Copy. The browser's own
Edit menu copy, a copy shortcut you disabled in settings, and the subagent
windows still copy the raw rows, as they did before. A selection holding nothing but padding is now
refused rather than copied as bare line breaks, and it clears the selection on
the way out so Ctrl+C goes straight back to interrupting. Auto Copy reads its
own toggle before it reads the selection, so a pane nobody is copying from
costs nothing on mouseup.