Skip to content

Commit 53cd8c1

Browse files
committed
Polish drag-select auto-copy help text and wiring tests
Align Alt+M copy/help strings and docs with mouse-up clipboard copy, and pin the SELECTION event path through a live shell.
1 parent 8e84d7b commit 53cd8c1

7 files changed

Lines changed: 73 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ Format loosely follows [Keep a Changelog](https://keepachangelog.com/). Versions
1919
- Live OTEL collector verify (Phoenix or equivalent) against the merged sink
2020
- Dogfood session migrate: new session under `~/.corbits/projects`, one legacy `.agent-state` migrate, write under state root still asks
2121

22+
### TUI
23+
24+
- **Drag-select auto-copy.** With mouse capture on (the default), finishing a
25+
drag selection in the transcript writes the selected text to the system
26+
clipboard on mouse-up and flashes a short status line. Alt+M still hands the
27+
mouse back for native terminal selection; Alt+C remains the keyboard copy
28+
path for whole messages, tool outputs, and diffs.
29+
2230
## [0.2.95] - 2026-08-09
2331

2432
Tool-only auto-pause that no longer stops healthy work, resume the last session

‎docs/TUI.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,11 @@ terminal. It cannot observe:
626626
test round-trips through a real `pbcopy`/`xclip`/terminal clipboard.
627627
- **Terminal-owned text selection.** Native drag-select only exists once DEC
628628
mouse reporting is off and a real terminal emulator is running; there is
629-
no terminal emulator in the test harness to select text in.
629+
no terminal emulator in the test harness to select text in. OpenTUI
630+
selection auto-copy is unit-tested (`selection-copy.test.ts`) and wired
631+
through a synthetic `SELECTION` event (`copy-wire.test.ts`); a real
632+
mouse-up path still needs a manual terminal check.
633+
630634

631635
Concretely, whole defect classes — a DEC mouse-reporting toggle that silently
632636
no-ops, an Alt+key chord a given terminal never actually delivers, a

‎src/tui/copy-wire.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
2+
import { CliRenderEvents } from "@opentui/core"
23
import { createHarness, type Harness } from "./harness"
34
import {
45
appendStreamRow,
@@ -47,6 +48,44 @@ describe("Alt+C reaches the injected clipboard", () => {
4748
})
4849
})
4950

51+
describe("drag-select auto-copy", () => {
52+
test("SELECTION event writes finished text and flashes", () => {
53+
const clipboard = createRecordingClipboard()
54+
const shell = createAppShell(harness.renderer, { clipboard })
55+
harness.renderer.emit(CliRenderEvents.SELECTION, {
56+
isDragging: false,
57+
getSelectedText: () => "dragged snippet",
58+
})
59+
expect(clipboard.writes).toEqual(["dragged snippet"])
60+
expect(shell.statusFlash).toContain("Copied 15 chars")
61+
expect(shell.statusFlash).toContain("dragged snippet")
62+
shell.dispose()
63+
})
64+
65+
test("SELECTION while dragging is a no-op", () => {
66+
const clipboard = createRecordingClipboard()
67+
const shell = createAppShell(harness.renderer, { clipboard })
68+
harness.renderer.emit(CliRenderEvents.SELECTION, {
69+
isDragging: true,
70+
getSelectedText: () => "partial",
71+
})
72+
expect(clipboard.writes).toEqual([])
73+
expect(shell.statusFlash).toBeNull()
74+
shell.dispose()
75+
})
76+
77+
test("empty SELECTION is a no-op", () => {
78+
const clipboard = createRecordingClipboard()
79+
const shell = createAppShell(harness.renderer, { clipboard })
80+
harness.renderer.emit(CliRenderEvents.SELECTION, {
81+
isDragging: false,
82+
getSelectedText: () => "",
83+
})
84+
expect(clipboard.writes).toEqual([])
85+
shell.dispose()
86+
})
87+
})
88+
5089
describe("Alt+M mouse capture", () => {
5190
test("toggles the host port and reports the new state", () => {
5291
let enabled = false
@@ -60,6 +99,7 @@ describe("Alt+M mouse capture", () => {
6099
})
61100
expect(toggleMouseCapture(shell)).toBe(true)
62101
expect(enabled).toBe(true)
102+
expect(shell.statusFlash).toContain("drag text to copy")
63103
expect(toggleMouseCapture(shell)).toBe(false)
64104
expect(enabled).toBe(false)
65105
shell.dispose()

‎src/tui/keybindings.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const SHELL_SHORTCUTS: readonly ShellShortcut[] = [
2525
{ keys: "Ctrl+C", description: "interrupt the run, or clear the prompt when idle; press twice to exit" },
2626
{ keys: "Ctrl+G", description: "cancel the most recently queued or steered message before it dispatches" },
2727
{ keys: "Alt+C", description: "copy mode: pick a message, tool output, or diff; press again to close it" },
28-
{ keys: "Alt+M", description: "release the mouse to the terminal for native drag-select and copy; on by default for wheel scroll and click-to-expand" },
28+
{ keys: "Alt+M", description: "hand the mouse to the terminal for native drag-select; on by default so wheel scroll, click-to-expand, and drag-to-copy work" },
2929
{ keys: "Alt+E", description: "expand or collapse every collapsible row (tool call, diff, skill, reasoning)" },
3030
{ keys: "Alt+T", description: "show or hide the task list above the prompt" },
3131
{ keys: "Alt+O", description: "observe a live subagent session; a system row says so when there is none" },

‎src/tui/product-host.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,9 @@ export type ProductHostConfig = {
220220
/**
221221
* Take DEC mouse reporting. Default true: wheel/trackpad scroll only
222222
* reaches OpenTUI when the terminal is told to report it, otherwise the
223-
* terminal's own alternate-scroll mode resends it as arrow keys. Alt+M
224-
* hands the mouse back to the terminal for native drag-select.
223+
* terminal's own alternate-scroll mode resends it as arrow keys. With
224+
* reporting on, drag-select is OpenTUI-owned and auto-copies on mouse-up;
225+
* Alt+M hands the mouse back for native terminal selection.
225226
*/
226227
readonly useMouse?: boolean
227228
}
@@ -317,8 +318,9 @@ export async function mountProductHost(
317318
// never reaches OpenTUI — the terminal's own alternate-scroll mode
318319
// swallows it and resends it as arrow keys, which the prompt then
319320
// reads as history navigation instead of the transcript scrolling.
320-
// Cost accepted: this suppresses the terminal's native drag-select
321-
// in the main shell. Alt+M hands the mouse back when that is wanted.
321+
// Cost accepted: this suppresses the terminal's *native* drag-select
322+
// in the main shell. OpenTUI selection still works and auto-copies
323+
// on mouse-up; Alt+M hands the mouse back when native select is wanted.
322324
// enableMouseMovement stays off (no ?1003): only clicks and wheel
323325
// are needed.
324326
useMouse: config.useMouse ?? true,

‎src/tui/shell.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,21 +524,24 @@ export type AppShellOptions = {
524524
*/
525525
readonly telemetryNotice?: string
526526
/**
527-
* Clipboard port for Alt+C. Defaults to an in-memory recorder so tests and
528-
* demos never shell out; the product host injects the system clipboard.
527+
* Clipboard port for Alt+C and drag-select auto-copy. Defaults to an
528+
* in-memory recorder so tests and demos never shell out; the product host
529+
* injects the system clipboard.
529530
*/
530531
readonly clipboard?: ClipboardPort
531532
/**
532533
* Mouse-reporting switch behind Alt+M. Absent means the shell has no
533534
* renderer-level control (tests, demos) and reports the toggle unavailable.
535+
* While reporting is on, OpenTUI owns drag-select and auto-copies on
536+
* mouse-up; Alt+M hands the mouse back for native terminal selection.
534537
*/
535538
readonly mouseCapture?: MouseCapturePort
536539
}
537540

538541
/**
539542
* Renderer-level DEC mouse reporting control. While reporting is on the
540-
* terminal hands drags to us instead of selecting text, so the user needs a
541-
* way to hand it back.
543+
* terminal hands drags to OpenTUI (drag-to-copy on mouse-up); Alt+M hands
544+
* reporting back so the terminal can run its own selection again.
542545
*/
543546
export type MouseCapturePort = {
544547
readonly get: () => boolean

‎src/tui/system-clipboard.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
2-
* System clipboard port for the keyboard copy path.
2+
* System clipboard port for app-owned copy paths.
33
*
4-
* Terminal drag-select is unavailable while DEC mouse reporting is on, so
5-
* Alt+C must reach the OS clipboard itself rather than relying on the
6-
* terminal's own selection. Native helpers are preferred; OSC 52 is the
7-
* fallback for remote sessions where no helper binary exists.
4+
* Used by drag-select auto-copy (OpenTUI selection on mouse-up), Alt+C copy
5+
* mode, and related keyboard paths. Native terminal drag-select is still
6+
* unavailable while DEC mouse reporting is on — Alt+M hands the mouse back
7+
* when that is wanted. Native helpers are preferred; OSC 52 is the fallback
8+
* for remote sessions where no helper binary exists.
89
*/
910

1011
import type { ClipboardPort } from "./copy-path.js"

0 commit comments

Comments
 (0)