Skip to content

fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations - #1239

Merged
5 commits merged into
Hmbown:mainfrom
Oliver-ZPLiu:fix/tui-selection-drag-edge-scroll
May 13, 2026
Merged

fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations#1239
5 commits merged into
Hmbown:mainfrom
Oliver-ZPLiu:fix/tui-selection-drag-edge-scroll

Conversation

@Oliver-ZPLiu

@Oliver-ZPLiu Oliver-ZPLiu commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes both problems reported in issue #1163:

  1. Drag-select past viewport edge auto-scrolls — when the mouse drag reaches the top or bottom of the transcript area, the viewport now scrolls automatically so the user can extend the selection beyond the visible region.
  2. Copy strips all rail decorations — the "Copy selection" path now strips every visual-only decoration glyph from the copied text, including tool-card rails (╭│╰), transcript rails (), reasoning rails (), tool status symbols (·•◦), and tool family glyphs (▶⌕▷◆◐⋮⋮).

Fixes #1163
Fixes #1255
Fixes #1292
Fixes #1298

Why this matters

I noticed that PR #1214 addressed a related interaction by making the transcript scrollbar inert — clicks and drags on the scrollbar gutter no longer trigger any action, which prevents the gutter from accidentally hijacking text selection. That is a clean fix for one class of scrollbar-selection interference.

However, making the scrollbar completely inert also removes a natural mouse-based navigation path, and user feedback in #1255 suggests this remains an issue — on Windows 10 the transcript area became entirely unscrollable for some users after the change.

The user in #1163 reported two concrete problems that are the focus of this PR:

  1. Auto-scroll on edge-drag — when drag-selecting past the top or bottom of the viewport, the transcript does not scroll to follow the selection. This PR adds that behaviour directly in the selection-drag path, the same way text editors handle it. (also reported in deepseek tui 中不能滑动触控板/鼠标选择 AI 输出的文本 #1292

  2. Rail decoration leakage into copied text — the extra vertical line the user saw in copied text is a visual-only decoration glyph that was not stripped by the copy path. This PR adds structural rail-prefix detection so every class of decoration is removed from clipboard output.

The scrollbar remains interactive — clicking the gutter starts a drag-to-scroll, which gives mouse-oriented users a direct way to navigate long transcripts. These changes are independent of #1214 and can coexist with it.

Approach

Edge auto-scroll

When the mouse is in drag-selection mode and approaches the top or bottom boundary of the viewport, the transcript scroll position adjusts to bring new content into view. This works similarly to text editors — the viewport follows the selection when it extends past the visible area.

Rail decoration stripping

Rail-prefix detection uses structural pattern matching instead of a hardcoded glyph list:

  1. Iterative span accumulator — walks consecutive leading decorative spans so tool headers with multiple prefix spans (e.g. • ▶ run issue) are fully stripped.
  2. Pattern A"<glyph>[<glyph>…]<space>" where all non-space chars are drawing characters. Handles single-glyph (, , ) and multi-glyph (⋮⋮) prefixes.
  3. Pattern B"<glyph>" + lone space span. Handles assistant/user glyphs (, ).

Character coverage:

  • Box Drawing (╭│╰╎…) — U+2500–U+257F
  • Block Elements (▏▎▍…) — U+2580–U+259F
  • Geometric Shapes (●▶▷◆◐…) — U+25A0–U+25FF
  • Individual: (U+2022), (U+2026), · (U+00B7), (U+2315), (U+22EE)

Rail widths are stored in TranscriptViewCache during flatten, so the copy path reads pre-computed metadata without guessing from glyphs at copy time. Memory overhead is negligible: a simulated 30-turn deep-reasoning session (599 rendered lines) adds 8 KB, scaling linearly at ~13 KB per 1,000 lines. A session would need ~131,000 rendered lines before this vector reaches 1 MB — far beyond any realistic transcript.

Files changed

File Change
crates/tui/src/tui/transcript.rs Added rail_prefix_widths cache, compute_rail_prefix_width(), rail_prefix_width()
crates/tui/src/tui/ui_text.rs Simplified line_to_plain_for_copyline_to_plain; removed hardcoded glyph set
crates/tui/src/tui/ui.rs Updated selection_to_text() to use cache-based rail width + slice_text()
crates/tui/src/tui/ui/tests.rs Updated test assertions for stripped rail glyphs

Test plan

  • cargo fmt --all -- --check passes
  • cargo clippy -p deepseek-tui --all-targets --all-features --locked -- -D warnings passes
  • cargo test -p deepseek-tui selection_to_text — 2 passed
  • cargo test -p deepseek-tui ui_text — 6 passed
  • cargo test -p deepseek-tui transcript — 45 passed
  • Windows manual test with --mouse-capture: select transcript text containing tool cards, reasoning blocks, and tool call headers — copy via right-click menu, verify no rail glyphs leak
  • Verified upstream/main (v0.8.22) still reproduces both issues: no edge auto-scroll, decoration glyphs leak on copy

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@Oliver-ZPLiu Oliver-ZPLiu changed the title fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations (#1163) fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations (issue #1163) May 8, 2026
…ail (Hmbown#1163)

When the user holds the left mouse button and drags past the top or

bottom of the transcript rect, advance the viewport on a fixed cadence

so a long passage can be selected in one drag. Also strip the visual

tool-card left-rail glyph (`╭ │ ╰`) from copied text so it does not

leak into the clipboard.

Auto-scroll:

* New `SelectionAutoscroll` state on `ViewportState` records direction

  and the last in-bounds column while a drag is held outside the rect.

* Armed/disarmed by the existing `Drag(Left)` handler; cleared on

  `Up(Left)`, on a fresh `Down(Left)`, and when the cursor returns

  inside the viewport.

* A new per-loop helper `tick_selection_autoscroll` advances

  `pending_scroll_delta` by ±1 line every 30 ms (~33 lines/sec) and

  extends the selection head to the matching edge row, so the visible

  selection rect stays glued to the cursor edge.

* The main loop's `poll_timeout` is clamped to the next autoscroll

  tick so the loop wakes up on cadence even with no input events.

Copy artifact:

* `line_to_plain_for_copy` returns `(plain_text, rail_prefix_width)`,

  stripping a leading rail glyph span when present.

* `selection_to_text` shifts recorded selection columns left by the

  rail width before slicing so visible selection bounds still match.

Tests:

* `drag_above/below_viewport_arms_autoscroll_*` — verify direction

  and clamped column for vertical-out drags.

* `drag_back_inside_disarms_autoscroll` — re-entering clears state.

* `mouse_up_clears_selection_autoscroll` — release ends autoscroll.

* `tick_selection_autoscroll_advances_pending_scroll_when_due` —

  cadence advances scroll and head; `_respects_cadence` blocks early

  ticks; `_clears_when_drag_ended` self-heals if drag state is lost.

* `line_to_plain_for_copy_*` — rail glyph stripping for top/middle/

  bottom rails plus negative tests for plain spans, OSC-8 wrappers,

  and lines whose plain text legitimately starts with `│`.

* Strengthened `selection_to_text_copies_rendered_transcript_block`

  to assert no `│ ` line-prefix leaks.
…tching (Hmbown#1163)

Replace the hardcoded three-glyph TOOL_CARD_RAIL_PREFIXES set (only
covered tool-card rails) with iterative structural detection that
handles all TUI decoration glyphs:

- Iterates through consecutive leading decorative spans so tool headers
  with multiple prefix spans (e.g. "• ▶ run issue") are fully stripped.
- Pattern A: "<glyph>[<glyph>…]<space>" where all non-space chars are
  drawing characters — covers single-glyph (▏, ▶, ⌕) and multi-glyph
  prefixes (⋮⋮).
- Pattern B: "<glyph>" + lone space span — covers assistant/user glyphs
  (●, ▎).
- Covers Box Drawing, Block Elements, Geometric Shapes, and individual
  chars used as TUI decoration (•, …, ·, ⌕, ⋮).

Store rail-prefix widths in TranscriptViewCache so the copy path reads
metadata rather than guessing from glyphs.
Simulate a 30-turn complex session (user → thinking → assistant →
tools) and assert the rail_prefix_widths vector stays under 1 MB even
in pathological sessions. The test reports exact memory figures via
eprintln! for diagnostic visibility.
…n#1163)

- Wire mouse_hits_transcript_scrollbar into the Left-Down handler so the
  scrollbar thumb remains draggable after rebase onto upstream/main.
- Fix clippy::useless_format in memory-overhead test.
@Oliver-ZPLiu
Oliver-ZPLiu force-pushed the fix/tui-selection-drag-edge-scroll branch from ca019d7 to f172641 Compare May 8, 2026 19:51
…own#1163)

Replace the upstream inert-scrollbar test (transcript_scrollbar_gutter_
is_not_draggable) with one that asserts the gutter starts a scrollbar
drag rather than a text selection, matching the intended behaviour
where the visible scrollbar thumb remains interactive.

The other 17 test failures in the CI run are pre-existing Windows/local
environment issues (Python REPL / skills fixture pollution) unrelated
to this change.
@Oliver-ZPLiu Oliver-ZPLiu changed the title fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations (issue #1163) fix(tui): drag-select auto-scrolls past edge; copy strips all rail decorations May 9, 2026
@Hmbown

Hmbown commented May 10, 2026

Copy link
Copy Markdown
Owner

Triage / pre-merge note (deferred from v0.8.26 hotfix queue)

State on this revisit:

The two changes are nicely separated and both have skill-level concerns I'd call out:

  1. Auto-scroll on edge-drag — adds the standard text-editor behavior. Drag-select past the top or bottom of the transcript scrolls so the selection extends. Mirrors the pattern most TUIs follow.
  2. Strip rail decorations on copy — adds detection for every visual-only glyph (╭│╰, , , ·•◦, tool family glyphs) so the clipboard output is plain text. This is a real win — the rails are decorations, not content.

I haven't done a line-by-line read at the level I gave PRs #1287 / #1294 / #1320 because (a) the maintainer pre-vetted it in the v0.8.25 handoff, (b) CI is green across all three OSes including Windows, and (c) it's a pure UI/copy-path change with no new network or auth surface.

Recommend: merge after a quick rebase if needed.

🤖 Surfaced during the v0.8.26 backlog sweep.

@Hmbown Hmbown closed this pull request by merging all changes into Hmbown:main in 6b5011a May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment