Problem
Reading a long Claude response in the builder terminal is a manual mouse-wheel grind. VS Code's terminal has page-up/page-down baked in (workbench.action.terminal.scrollUpPage / scrollDownPage) but most users don't know it exists, and there's nothing finer-grained than "one line" or "one page" available. Navigating Claude's output by paragraph or by turn — which is how the content is actually structured — requires scrolling guesswork.
Source of truth: we have the full stream
CodevPseudoterminal (packages/vscode/src/terminal-adapter.ts) is the write boundary for everything that reaches the renderer. Tower captures Claude's stdout/stderr and forwards every byte through the WebSocket → CodevPseudoterminal.handleData() → this.writeEmitter.fire(chunk). Detecting paragraph boundaries (\n\n) is exact, not heuristic — we own the byte stream end-to-end.
Proposed behavior
Add three Codev-scoped scroll commands, each bound by default, all gated on codev.terminalFocused && terminalFocus so they don't shadow plain VS Code terminals:
1. Scroll by viewport page
| Command |
Default keybinding |
Implementation |
codev.scrollPageUp |
Cmd+Alt+Up |
workbench.action.terminal.scrollUpPage |
codev.scrollPageDown |
Cmd+Alt+Down |
workbench.action.terminal.scrollDownPage |
Zero new logic — wraps existing VS Code commands under Codev-branded keybindings to surface them.
2. Scroll by Claude turn (shell-integration based)
| Command |
Default keybinding |
Implementation |
codev.scrollTurnUp |
Cmd+Shift+Alt+Up |
workbench.action.terminal.scrollToPreviousCommand |
codev.scrollTurnDown |
Cmd+Shift+Alt+Down |
workbench.action.terminal.scrollToNextCommand |
VS Code's shell-integration tracks command boundaries via OSC 633 / 133 markers. Claude Code CLI emits these on prompt. Wiring our commands through scrollToPreviousCommand / Next jumps between Claude's turns automatically.
Prerequisite to verify: confirm Claude Code is emitting the shell-integration markers through our PTY path. If not, document the constraint OR have CodevPseudoterminal inject the OSC markers itself when it detects turn boundaries in the byte stream.
3. Scroll by paragraph
CodevPseudoterminal sees every byte, so paragraph detection is exact. The engineering work is in three pieces — all solvable, none of which is a platform blocker:
(a) Boundary detection — In CodevPseudoterminal.handleData() (the write path), track:
- Cumulative count of
\n emitted to the renderer (logicalLineCount).
- An ordered list of paragraph-boundary line indices: detect
\n\n transitions (or any sequence of ≥2 consecutive newlines after non-whitespace) and record the line index immediately after the blank line.
(b) Visual-row translation — xterm.js wraps long logical lines at viewport width; the scrollUp/scrollDown commands operate on visual rows, not logical newlines. We need to translate logical-line positions → visual-row offsets:
- On each
\n, count how many visual rows the preceding logical line consumed = ceil(logicalLineWidth / terminalColumns). Maintain a parallel list of visualRowAtLogicalLine[i].
- Subscribe to
vscode.window.onDidChangeTerminalDimensions and recompute when the user resizes.
(c) Viewport position bookkeeping — VS Code doesn't expose "current scroll position" to extensions. Maintain it ourselves:
- A
viewportOffsetFromBottom counter, incremented by our own scrollUp calls, decremented by scrollDown, reset to 0 on new output arrival or user input (best-effort: the user typing into the prompt resets the offset).
- When the user mouse-scrolls or hits a non-Codev scroll command, we have no signal. The fallback: a "resync" path — if our tracked position seems impossible (negative offset, beyond total visual rows), reset to 0 (scroll-to-bottom semantics).
Execution — On codev.scrollParagraphUp:
- Find the largest paragraph-boundary index whose visual-row position is above the current viewport top.
- Compute the visual-row delta from current viewport top to that boundary.
- Execute
workbench.action.terminal.scrollUp that many times (one VS Code command per visual row).
Mirror logic for scrollParagraphDown.
| Command |
Default keybinding |
codev.scrollParagraphUp |
Alt+Up |
codev.scrollParagraphDown |
Alt+Down |
Acceptance criteria
Page
Claude turn
Paragraph
Out of scope
- Migrating to a webview-based terminal to gain xterm.js APIs directly.
- A proposed-API path (
onDidWriteTerminalData) for buffer reading.
- User-configurable boundary heuristics (paragraph =
\n\n, end of story).
- Cross-terminal paragraph navigation.
Problem
Reading a long Claude response in the builder terminal is a manual mouse-wheel grind. VS Code's terminal has page-up/page-down baked in (
workbench.action.terminal.scrollUpPage/scrollDownPage) but most users don't know it exists, and there's nothing finer-grained than "one line" or "one page" available. Navigating Claude's output by paragraph or by turn — which is how the content is actually structured — requires scrolling guesswork.Source of truth: we have the full stream
CodevPseudoterminal(packages/vscode/src/terminal-adapter.ts) is the write boundary for everything that reaches the renderer. Tower captures Claude's stdout/stderr and forwards every byte through the WebSocket →CodevPseudoterminal.handleData()→this.writeEmitter.fire(chunk). Detecting paragraph boundaries (\n\n) is exact, not heuristic — we own the byte stream end-to-end.Proposed behavior
Add three Codev-scoped scroll commands, each bound by default, all gated on
codev.terminalFocused && terminalFocusso they don't shadow plain VS Code terminals:1. Scroll by viewport page
codev.scrollPageUpCmd+Alt+Upworkbench.action.terminal.scrollUpPagecodev.scrollPageDownCmd+Alt+Downworkbench.action.terminal.scrollDownPageZero new logic — wraps existing VS Code commands under Codev-branded keybindings to surface them.
2. Scroll by Claude turn (shell-integration based)
codev.scrollTurnUpCmd+Shift+Alt+Upworkbench.action.terminal.scrollToPreviousCommandcodev.scrollTurnDownCmd+Shift+Alt+Downworkbench.action.terminal.scrollToNextCommandVS Code's shell-integration tracks command boundaries via OSC 633 / 133 markers. Claude Code CLI emits these on prompt. Wiring our commands through
scrollToPreviousCommand/Nextjumps between Claude's turns automatically.Prerequisite to verify: confirm Claude Code is emitting the shell-integration markers through our PTY path. If not, document the constraint OR have
CodevPseudoterminalinject the OSC markers itself when it detects turn boundaries in the byte stream.3. Scroll by paragraph
CodevPseudoterminalsees every byte, so paragraph detection is exact. The engineering work is in three pieces — all solvable, none of which is a platform blocker:(a) Boundary detection — In
CodevPseudoterminal.handleData()(the write path), track:\nemitted to the renderer (logicalLineCount).\n\ntransitions (or any sequence of ≥2 consecutive newlines after non-whitespace) and record the line index immediately after the blank line.(b) Visual-row translation — xterm.js wraps long logical lines at viewport width; the
scrollUp/scrollDowncommands operate on visual rows, not logical newlines. We need to translate logical-line positions → visual-row offsets:\n, count how many visual rows the preceding logical line consumed =ceil(logicalLineWidth / terminalColumns). Maintain a parallel list ofvisualRowAtLogicalLine[i].vscode.window.onDidChangeTerminalDimensionsand recompute when the user resizes.(c) Viewport position bookkeeping — VS Code doesn't expose "current scroll position" to extensions. Maintain it ourselves:
viewportOffsetFromBottomcounter, incremented by our ownscrollUpcalls, decremented byscrollDown, reset to 0 on new output arrival or user input (best-effort: the user typing into the prompt resets the offset).Execution — On
codev.scrollParagraphUp:workbench.action.terminal.scrollUpthat many times (one VS Code command per visual row).Mirror logic for
scrollParagraphDown.codev.scrollParagraphUpAlt+Upcodev.scrollParagraphDownAlt+DownAcceptance criteria
Page
codev.scrollPageUp/codev.scrollPageDownregistered, bound, scoped to Codev terminals.Claude turn
codev.scrollTurnUp/Downwired toscrollToPreviousCommand/Nextand bound.CodevPseudoterminalinjects OSC 633 markers on detected turn boundaries before forwarding to the renderer.Paragraph
CodevPseudoterminalrecords logical-line and paragraph-boundary indices on everyhandleData()chunk.onDidChangeTerminalDimensions.viewportOffsetFromBottombookkeeping updated by our own scroll commands; reset on new output / user input.codev.scrollParagraphUp/Downregistered, bound, scoped to Codev terminals.Out of scope
onDidWriteTerminalData) for buffer reading.\n\n, end of story).