Hmbown / Hmbown/Codewhale

bug(tui): copying a partial selection pastes the whole cell instead of the fragment (default since #6156)

Open
#6,228 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Rust
Stars
41k
Forks
3.6k
Avg merge
13h 59m
Merged PRs (30d)
299

Description

## Description

Since #6156, copying a transcript drag selection copies **whole cells** instead of the selected
text. Selecting one passage — for example a single table inside an answer — and pressing `Ctrl+C`
puts the entire answer on the clipboard. `tui.selection_copy_markdown` defaults to `true`, so this
is what every user gets without opting in.

The feature set out to fix "Markdown structure is lost when a selection is copied", but it did so by
rounding the selection out to the cells it intersects
(`crates/tui/src/tui/mouse_ui.rs:1863-1864`: *"A selection that cuts a cell in half rounds out to the
whole cell"*). Partial selections therefore cannot be copied at all any more — the very case the
feature's own problem statement names:

> the whole message has to be copied instead, **even when only one passage was wanted** (#6156)

## Steps to reproduce

1. In the TUI, drag-select a fragment of an assistant answer — one table, or a couple of lines.
2. Press `Ctrl+C` with the selection active (or right-click → *Copy selection*).
3. Paste into an editor: the clipboard holds the whole intersected cell(s), i.e. the entire answer,
not the selection. The toast says *"Selection copied as Markdown (N)"*, where N counts cells.

On Linux the automatic drag-release path still publishes rendered text to PRIMARY
(`crates/tui/src/tui/mouse_ui.rs:588-598`, `crates/tui/src/tui/clipboard.rs:402-404`), so the
surprise surfaces on `Ctrl+C` / context menu / pasting from the regular clipboard. On non-Linux the
release itself takes the whole-cell Markdown path (`crates/tui/src/tui/mouse_ui.rs:861-866`).

## Expected behavior

Copying a selection should copy **the selected fragment**:

- selecting a passage and copying yields that passage — as Markdown source where the projection can
be resolved, otherwise as the rendered text selection copy produced before #6156;
- copying a whole answer as Markdown stays on `Ctrl-Y` and `/copy`, which already do exactly that
(`history_cell_to_clipboard_text` returns the authored source, `crates/tui/src/tui/ui_text.rs:175-180`;
`Ctrl-Y` → `copy_cell_to_clipboard`, `crates/tui/src/tui/ui/activity_detail.rs:510-520`; `/copy` →
the last completed response, `crates/tui/src/commands/groups/core/copy.rs:28-42`), so nothing is lost
by keeping selection copy precise; both are already documented (`docs/KEYBINDINGS.md:169`,
`docs/GUIDE.md:383`) and `/copy` also exports `last-copy.md`, so a whole answer stays easy to find;
- `tui.selection_copy_markdown` should default to **`false`** until projection is per-fragment, so
the "whole answer" payload is opt-in rather than the default.

Today's workaround is to set `[tui] selection_copy_markdown = false` — that restores accurate
rendered-text selection copy, at the cost of the Markdown structure #6156 was about.

## Actual behavior

`copy_active_selection` (`crates/tui/src/tui/mouse_ui.rs:1808`) prefers `selection_to_markdown`
(`mouse_ui.rs:1868`) whenever `tui.selection_copy_markdown` is on. That function resolves the
selection to cell indices through the line metadata and serializes whole cells
(`mouse_ui.rs:1868-1911`); the selection's columns are never consulted. The exact-text path
`selection_to_text` (`mouse_ui.rs:1916`) is reached only when the key is off or no cell metadata
intersects the range.

The default is `true`: `ViewportState::default()` (`crates/tui/src/tui/app.rs:992`),
`init.rs:734-737` resolves `.unwrap_or(true)`, and the docs state default `true`
(`docs/CONFIGURATION.md:2365`, `config.example.toml:1141`).

A partial selection cannot simply be projected: per-rendered-line metadata carries only
`cell_index`, `line_in_cell`, `copy_prefix_width` and a separator
(`crates/tui/src/tui/scrolling.rs:37-48`) — there is no mapping from a rendered line to a source
Markdown range. The whole-cell behaviour is a consequence of that gap, not an independent choice.

## Proposed change

1. Default `tui.selection_copy_markdown` to **`false`**, restoring accurate selection copy now.
2. Implement fragment-level Markdown copy: keep a source-range mapping in the render metadata so a
selection covering part of a cell projects to that part of the source; where a range cannot be
resolved, fall back to `selection_to_text` instead of expanding the range.
3. Keep whole-answer Markdown on `Ctrl-Y` and `/copy`. If the Markdown projection should still apply
to selections, apply it only when the selection covers whole cells.

## Impact

Copying a passage — the gesture users reach for first — is unusable for anything shorter than a
whole cell: you get the whole answer, or repair the paste by hand. The behaviour is on by default,
so it affects every user of dev builds since #6156. Reported from a workflow that needed a single
table copied out of an answer.

## Environment

- OS: Arch Linux
- codewhale version: 0.9.13 (dev)
- Install method: source build
- Model/provider: DeepSeek
- Terminal app: tmux (`TERM=tmux-256color`)
- Shell: fish

## Logs, screenshots, or recordings

No logs — UI behaviour; the three steps above are the reproduction.

Related: #6156 (the feature this follows up), #4208 (copy polluted by box-drawing decorations — same
copy path).

Contributor guide

Open the contributing guide

Research direction

Start in crates/tui/src/tui/mouse_ui.rs with copy_active_selection, selection_to_markdown, and selection_to_text, then trace the metadata in scrolling.rs and the default resolution in app.rs and init.rs. Reproduce partial selection copying in the TUI before changing behavior. Done means partial selections preserve the selected fragment or fall back to rendered text, while whole-answer Markdown remains on Ctrl-Y and /copy and the selection-copy default is false.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.