Repository navigation
Description
Summary
In the TUI's terminal pane, a mouse drag-selection locks the pane's viewport: while the button is held, wheel input neither scrolls the pane nor extends the selection, so anything copied is limited to the rows that were on screen when the drag started. Content that needs scrolling mid-selection cannot be copied at all, which breaks copying a command's output into another agent/message.
Environment
- opencode version: 2.1.4
- OS: Linux 7.2.11-arch1-1 (Arch Linux), x86_64
- Terminal: WezTerm over SSH (
TERM=xterm-256color, noTERM_PROGRAMinside the session) - Shell: /usr/bin/zsh
- Install/channel: release binary at
~/.opencode/bin/opencode,latest - Active plugins: none
Reproduction
- Start the TUI and open the terminal pane (
<leader>t) so the persistent pty pane is focused. - Run
seq 1 200so the output is taller than the pane. - Press and hold the left mouse button on a line in the pane, and while holding, scroll the wheel up and down.
- Observe: the pane's viewport does not move and the selection does not grow. The screen stays exactly as it was when the drag began.
- Release. With the default
terminal.copy: "select"the clipboard receives only the pre-drag screen; withterminal.copy: "manual",ctrl+ccopies the same limited block.
Any scrollback reproduces it — a plain seq is enough, no full-screen app (less, vim, htop) required.
Expected Behavior
Dragging a selection to the top or bottom edge of the pane scrolls the viewport (edge autoscroll), and wheel input during a drag scrolls and extends the selection, the way every terminal emulator behaves. A single selection should be able to span more than one screen.
Actual Behavior
The viewport is frozen for the duration of the drag, so a selection is bounded by the screen that was visible when the drag started. Copying a longer output degrades into copy → scroll → copy again, tracking by hand what was already copied.
Additional Context
Mechanism, from the code and the OpenTUI docs, in case it saves triage time:
-
The pane is
EmbeddedTerminalRenderable(packages/tui/src/component/terminal-pane.tsx→packages/core/src/renderables/EmbeddedTerminal.ts). -
Local scroll happens only when the child does not encode the wheel event:
contextEmbeddedTerminalScroll. OpenTUI docs (components/embedded-terminal, "Mouse and local scroll"): "When the child does not encode a wheel event, the renderable scrolls its own viewport by 3 rows and stops the event." -
Selection is screen-space by design: "The renderable joins the renderer selection and clamps drag endpoints to the visible grid", and "The renderer starts host selection before it dispatches the mouse event. …
preventDefault()does not cancel that selection." -
The clamp is visible in
onSelectionChanged, which is why a drag can never address rows outside the pane:const point = (x: number, y: number) => { if (y < 0) return { x: 0, y: 0 } if (y >= this.height) return { x: this.width - 1, y: this.height - 1 } return { x: Math.max(0, Math.min(this.width - 1, x)), y } } const start = point(local.anchorX, local.anchorY) const end = point(local.focusX, local.focusY) this.lib.contextEmbeddedTerminalSetSelection(this.handle.context, this.handle, start, end)
-
Putting those together: once a drag begins the renderer owns the gesture (per the docs above), wheel events during the drag do not reach the local-scroll branch in
forwardMouse, and the clamped endpoints cannot leave the visible grid. The freeze is those two behaviours meeting, and the fix has to address both.
Two changes would fix it:
- Keep forwarding wheel events (or implement edge autoscroll) while a host selection drag is active, so the viewport can move mid-drag.
- Anchor the selection to absolute scrollback lines instead of visible-grid cells, so a selection keeps referring to the same content when the viewport moves. This is the same root cause as TUI copy should preserve logical text instead of rendered layout #47165 ("TUI copy should preserve logical text instead of rendered layout").
Related issues: #47165 (logical vs rendered text on copy), #60942 (--no-mouse / --mouse overrides, a workaround people ask for), #47632 (selection cleared by messages_page_up/down, V1, closed).
Notes:
- Retained scrollback in the pane is the renderable default
maxScrollbackof 10000 bytes — the TUI does not pass the option (terminal-pane.tsxhas no reference to it). That cap is separate from this bug but worth knowing when a copy "loses" very old output. - Filed on 2.1.4; the newest release at the time of writing is 2.2.3 and this has not been re-tested there.
- Workaround in use: read the pane text over the API instead of copying (
GET /api/experimental/persistent-pty/{ptyID}/snapshotafter listing panes withGET /api/experimental/session/{sessionID}/terminal), which returns the full retained buffer.
Activity
opencodejr commentedon Nov 1, 2026
Thanks for the detailed write-up and for tracing it through the code and docs.
To restate the request: in the TUI terminal pane, a drag-selection should be able to cover more than one screen. Scrolling the wheel during a drag, or dragging to the top or bottom edge, should scroll the pane and extend the selection, and the selection should stay attached to the same scrollback lines while the pane moves.
As you found, the pane uses OpenTUI's embedded terminal, and its docs say the selection is clamped to the visible grid on purpose. So the current behaviour matches the documented design, and I'm treating this as a feature request rather than a bug. Most of the change would need to happen in OpenTUI. Maintainers will consider it.
Related issues:
Thanks also for sharing the snapshot API workaround and the note about the default scrollback limit.
mleivaDesarrollos commentedon Nov 1, 2026
Reopening the classification on this one, with the cause narrowed down: the wheel-during-drag part is a defect, not documented design.
The docs' local-scroll rule is unconditional — "When the child does not encode a wheel event, the renderable scrolls its own viewport by 3 rows and stops the event." So if the wheel reached the renderable during a drag, the viewport would move. It never gets there: the event is misdecoded before any renderable sees it.
packages/core/src/lib/parse.mouse.ts,decodeSgrEvent:A wheel event sent while a button is held carries the motion bit, so
isMotionwins the branch: the decoded event istype: "drag"withscroll: undefined, and the renderer's scroll path (renderer.ts, themouseEvent.type === "scroll"branch) is never reached. That is why the pane's viewport does not move. The legacy decoder has the same ordering —decodeBasicEventtestsif (isMotion)→movebeforeelse if (isScroll)— so it drops the wheel too.Minimal fix: test
isScrollfirst in both decoders.That restores wheel scrolling during a drag. The other half — dragging past the pane edge, and keeping a selection anchored to the same scrollback lines while the viewport moves — is the part that needs the logical-anchor work from #47165, and that one is fairly a feature request.
Happy to split this into two issues (parser wheel decoding vs selection anchoring) if that helps triage.
One correction to my own write-up: I said the renderer's selection owns the gesture, so wheel events during a drag do not reach the local-scroll branch. The effect is right but the cause is upstream — the wheel is already dropped in the parser, before the renderer classifies the event at all.
mleivaDesarrollos commentedon Nov 1, 2026
Splitting this the way I offered, since the two halves have different owners and very different cost:
isMotionbeforeisScroll, so a wheel that arrives while a button is held comes back asdragwithscroll: undefined. Swapping the two branches restores wheel scrolling mid-drag.Worth recording why this matters for a plain user, not just an agent workflow: copying a command's output to share it elsewhere (chat, ticket, another machine) is the single most common reason to touch that pane, and the current behaviour forces copy → scroll → copy while remembering what was already taken. The wheel fix alone removes most of that pain; the anchoring fix removes the rest.
mleivaDesarrollos commentedon Nov 2, 2026
Re-tested on 2.2.5 (upgraded from 2.1.4): still reproducible, unchanged behaviour.
Two distinct symptoms, from the user's side of the pane:
Plain wheel scrolling with no button held works as expected — the content moves normally. So the failure is specifically the combination of a held button and scroll input, which matches the decoder analysis in anomalyco/opentui#1911: the wheel arrives with the motion bit set,
isMotionis tested beforeisScroll, and the event is delivered asdragwithscroll: undefined. Worth noting the packaged 2.2.5 binary still contains themouseButtonsPressed.size > 0branch, so the code path is the same one described there.Symptom 2 is the host-level half that #1911 explicitly does not cover (even with the wheel decoded correctly, nothing in the pane scrolls on pointer position alone), so it stays here.
mleivaDesarrollos commentedon Nov 4, 2026
Captured the raw bytes of the failing gesture (WezTerm, opencode 2.2.5) — and it corrects my earlier diagnosis: the wheel is not dropped by the input parser. 120 mouse packets, 18 wheel ticks, every one
Cb=64/65with no motion bit, so they decode asscrollexactly as they should.What the capture shows instead, for press-at-bottom → drag-up-past-the-top:
Two defects, both host-side:
1. No edge autoscroll. Dragging past the first row leaves
anchorY/focusYclamped to the visible grid (onSelectionChangedpins y to0..height-1), and nothing moves the viewport, so the selection cannot reach the line above the fold. Every terminal scrolls here; this is the one that breaks the copy-a-long-output workflow.2. Scroll routing.
renderer.tsdispatches a scroll event tohitTest(x, y)and only falls back to the focused renderable. During a selection drag the pointer is at or beyond the pane's edge by definition, so the wheel is delivered to whatever is under the pointer (tab strip / neighbouring pane) rather than to the renderable that ownscurrentSelection. Routing scroll to the selection owner whileisDraggingwould fix wheel-scrolling mid-drag generically, for every host, not just this pane.Either one alone would make the gesture usable; the first is the one users actually perform. I have the full packet table if it helps, and I'm withdrawing the parser theory on anomalyco/opentui#1911.
mleivaDesarrollos commentedon Nov 4, 2026
Confirmed the last open question, and it narrows this to a single defect:
Wheel with the button held, pointer inside the pane → the content scrolls. So local scroll in the embedded terminal and the renderer's scroll dispatch both work correctly during a drag. Routing is not broken; my previous "scroll routing" suggestion can be dropped.
That leaves exactly one gap: nothing scrolls when the drag leaves the renderable's bounds, which is the normal way to select more than one screen.
Why this cannot be fixed in the pane component alone:
EmbeddedTerminalRenderable.onSelectionChangedclamps the endpoint —point()pinsyto0..height-1— so the selection can never address a row outside the grid.renderer.processSingleMouseEventsends drag events tohitTest(x, y), i.e. whatever is under the pointer, not to the renderable that ownscurrentSelection._latestPointeris private, so a host has no way to see where the pointer went and autoscroll on its own.So the fix needs a small addition in the renderer: while
currentSelection.isDraggingand the pointer is outside the owner's bounds, scroll that renderable on a timer through a minimal contract (something likescrollBy(delta), which the embedded terminal already has internally ascontextEmbeddedTerminalScroll), keeping the endpoint pinned to the edge row. Filed as anomalyco/opentui#1930 — it also covers scrollboxes, which have the same problem.With that, the copy-a-long-output workflow works: drag past the top row, the viewport follows, and the selection grows into the history.
mleivaDesarrollos commentedon Nov 13, 2026
Not a bump — a triage request. This issue carries no labels while newer TUI reports get
area/tui+type/bugat filing (for example #63753 today). Could this one be given the same, so it sits in the TUI triage flow rather than the general pool?State of the report: it was re-tested on 2.2.5 after upgrade, the mechanism is now byte-level (
/tmpcapture of the gesture showed the wheel is decoded correctly and the drag simply leaves the pane's bounds), and the remaining fix lives in anomalyco/opentui#1930, which a maintainer has already linked from the closure of anomalyco/opentui#1911.One scoping note so it is not merged into the wrong bucket: this is the terminal pane (embedded pty, its own buffer and scroll handling), not the transcript selection failures in #61398 / #62675 / #63753. Different buffer, different code path — those were about selecting chat text, this is about selecting a command's output.