Skip to content

Terminal viewport bounces during Claude Code sessions with selection locked #925

@gregpriday

Description

@gregpriday

Summary

During long-running Claude Code sessions in Canopy terminals (particularly with Codex MCP active), the terminal viewport intermittently bounces between the bottom and mid-buffer positions, with text selection locked during the bouncing phase. The behavior occurs in both Canopy and VS Code's integrated terminal, indicating it's triggered by Claude's escape sequences interacting with xterm.js rather than Canopy-specific scroll logic.

Current Behavior

Viewport bouncing:

  • Terminal scrolls to bottom where new output appears
  • Immediately jumps upward to a fixed position in the middle of chat history
  • Continues bouncing between bottom and mid-buffer region as new content arrives

Selection lock:

  • Text selection is disabled or severely impaired during bouncing
  • Mouse-based selection behaves as if terminal is in mouse-tracking mode
  • Dragging over text doesn't select as expected

Spontaneous recovery:

  • After additional output (often when MCP call finishes), bouncing stops
  • Selection functionality returns to normal
  • Subsequent output scrolls to bottom and stays there

When it occurs:

  • Most visible during active Claude Code work (e.g., /codex:plan with Codex MCP)
  • Requires long chat history in terminal
  • Typically starts mid-session, not from beginning
  • Reproducibility: Intermittent during intensive Claude sessions

Expected Behavior

When terminal is at bottom and user hasn't scrolled up:

  • Viewport should track new output and remain pinned to bottom
  • No jumps to earlier lines in the buffer

When user has scrolled up:

  • Respect user's scroll position
  • Don't forcibly re-scroll unless explicitly requested

Text selection:

  • Should always be possible when dragging with mouse
  • No alternative mode should prevent selection without explicit user choice

Steps to Reproduce

  1. Open Canopy terminal and launch Claude Code CLI
  2. Start a conversation that generates significant history
  3. Execute /codex:plan or other intensive MCP operations
  4. Observe viewport behavior as Codex processes the request
  5. Attempt to select text during active processing

Reproducibility: Intermittent during Claude Code sessions with long history and active MCP calls

Environment

  • OS: macOS 14.2+ (likely affects all platforms)
  • App: Canopy Command Center (Electron-based)
  • Terminal stack:
    • node-pty (Electron main/utility processes)
    • xterm.js (renderer)
    • SAB-based frame buffering in TerminalInstanceService
  • Agent: Claude Code CLI with Codex MCP
  • Version: Post-PR Fix: Restore clear command detection in terminals #919 ("fix(terminal): restore clear command detection")

Evidence

Affected Files:

Observations:

  • Also observed in VS Code's integrated terminal with Claude Code (not Canopy-specific)
  • Does not affect plain shell sessions (bash/zsh) or other agents the same way
  • Persists after PR Fix: Restore clear command detection in terminals #919 scroll/buffer refactor, ruling out our scroll logic as root cause

Related context:
PR #919 changes:

  • Removed agent-driven scroll snapping
  • Simplified setAgentState to no-op for scrolling
  • Added frame-aware SAB buffering for TUI detection
  • setFocused no longer calls scrollToBottom on focus changes
  • Resizes don't auto-scroll

Root Cause

Hypothesized: Claude Code CLI emits ANSI escape sequences that xterm.js interprets as viewport repositioning commands:

  1. Cursor movement to earlier lines

    • CSI <row>;<col>H / CSI <row>;<col>f (absolute cursor position)
    • CSI nA / CSI nB (cursor up/down)
    • ESC 7 / ESC 8 (save/restore cursor)
    • When cursor moves off-screen above viewport, xterm auto-adjusts to keep cursor visible
  2. Mouse tracking enables (selection lock)

    • CSI ?1000h, CSI ?1002h, CSI ?1006h (mouse reporting modes)
    • Routes mouse events to application instead of using for selection
    • Disabled with corresponding ...l sequences when bouncing stops
  3. Scroll-region manipulation

    • CSI <top>;<bottom> r (set scrolling region)
    • Combined with cursor movement causes viewport repositioning

Supporting evidence:

  • Bouncing only occurs during intensive Claude work (Codex MCP calls)
  • Stops when additional output produced (mode exits)
  • Selection locked during bounce, returns after
  • Same behavior in VS Code terminal (CLI-driven, not app-specific)

Most plausible: Claude Code enters TUI-like mode for progress/status rendering, using cursor movement and mouse tracking for in-place updates. Xterm follows these sequences, causing visible bouncing and selection hijacking.

Deliverables

Code Changes

Diagnostic phase:

  • Add escape sequence logging to TerminalInstanceService for agent terminals
  • Capture raw PTY output to identify exact sequences causing bouncing

Mitigation (escape sequence filtering):

  • Create ANSI sequence sanitizer layer for agent terminals in "follow mode"
  • Strip problematic sequences before xterm receives them:
    • Mouse tracking enables (?1000h, ?1002h, ?1006h)
    • Off-screen cursor positioning when user is at bottom
    • Optional: scroll-region changes

Scope: Apply only when:

  • Terminal type is agent/Claude terminal
  • User is at bottom (not manually scrolled up)
  • Terminal is not focused

Tests

  • Integration test: capture known problematic sequences, verify filtering
  • Unit test: ANSI parser correctly identifies mouse tracking and cursor positioning
  • Regression test: ensure basic ANSI features (colors, bold) preserved
  • Manual test: long Claude Code session with Codex MCP shows no bouncing

Documentation

  • Add comment in TerminalInstanceService explaining sequence filtering rationale
  • Document configuration option for stabilization (if feature-flagged)

Technical Specifications

Footprint:

  • src/services/TerminalInstanceService.ts - Add sequence filtering layer
  • New utility: ANSI sequence parser/filter
  • Possible config flag: "Stabilize agent terminal scrolling"

Performance:

  • Minimal overhead: regex-based sequence detection in data pipeline
  • Only active for agent terminals in follow mode
  • No impact on plain shell terminals or when user has scrolled up

Dependencies

Blocking:

  • Need to capture raw session with script command to confirm exact sequences:
    script -q -f /tmp/claude-bounce.log -c "claude <codex command>"

Informational:

Tasks

  • Record bouncing Claude session with script command to capture raw PTY output
  • Extract and analyze escape sequences from captured session
  • Identify exact sequences correlated with bounce start/stop and selection lock
  • Design ANSI sequence filter for agent terminals (regex-based parser)
  • Implement filtering layer in TerminalInstanceService.ts
  • Add detection for "agent terminal in follow mode" state
  • Strip mouse-tracking sequences (?1000h, ?1002h, ?1006h)
  • Strip or neutralize off-screen cursor moves when user at bottom
  • Add optional debug logging for filtered sequences
  • Create integration test with known problematic sequences
  • Test with long Claude Code + Codex MCP session
  • Verify no regression in basic ANSI features (colors, bold, etc.)
  • Consider feature flag for opt-in/out during testing phase
  • Document filtering logic and rationale in code

Acceptance Criteria

  • Viewport remains stable at bottom during Claude Code sessions with Codex MCP
  • Text selection works normally throughout agent sessions
  • No bouncing during intensive MCP operations
  • Basic ANSI features (colors, bold, progress bars) still render correctly
  • Plain shell terminals unaffected by changes
  • User-initiated scroll-up still respected (filtering only applies in follow mode)
  • Tests verify filtering behavior and prevent regression

Edge Cases & Risks

Edge cases:

  • Ensure filtering doesn't break Claude's legitimate progress indicators
  • Handle terminals where user explicitly wants TUI mode (e.g., htop, vim)
  • Respect user scroll position: disable filtering when manually scrolled up
  • Focused terminals: consider whether to apply filtering or trust user intent

Risks:

  • Over-aggressive filtering may break useful Claude UI feedback
  • Need precise sequence detection to avoid false positives
  • Performance impact if filtering is applied to all output (mitigation: scope to agent terminals only)

Mitigation strategies:

  • Feature flag for testing before wide rollout
  • Scope filtering to specific terminal types and states
  • Optional "plain mode" for users who prefer unfiltered output
  • Logging/diagnostics to debug future sequence interactions

Additional Context

Alternative mitigation approaches considered:

  1. Pinned-bottom re-scroll - Allow sequences through but force scroll-to-bottom after detection

    • Pro: Preserves Claude's logic
    • Con: May cause flicker, fights the application
  2. Plain-mode option - User toggle for no-TUI mode

    • Pro: Opt-in workaround
    • Con: Requires CLI support or external sanitizer
  3. Observability only - Add diagnostics without changing behavior

    • Pro: Non-invasive
    • Con: Doesn't solve the problem

Recommended approach: Escape sequence sanitization (option 1 in proposal) scoped to agent terminals in follow mode, with feature flag for testing.

Diagnostic script for sequence extraction:

import re
data = open("/tmp/claude-bounce.log", "rb").read()
for m in re.finditer(b"\x1b[^\x1b]*", data):
    seq = m.group()
    print(repr(seq))

Next steps after issue creation:

  1. Capture bouncing session with script command
  2. Run diagnostic script to extract sequences
  3. Correlate sequences with bounce timing
  4. Implement minimal filter targeting confirmed problematic sequences

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions