Skip to content

Instantly share code, notes, and snippets.

@GGPrompts
Last active November 17, 2025 18:27
Show Gist options
  • Select an option

  • Save GGPrompts/4423f54ebb3e5151f8baa6dcfe1c3783 to your computer and use it in GitHub Desktop.

Select an option

Save GGPrompts/4423f54ebb3e5151f8baa6dcfe1c3783 to your computer and use it in GitHub Desktop.
Terminal Mouse Coordinate Bug Fix - Opustrator (Git Bisect + Fix) #bug-fix #mouse-events #terminal #xterm.js #debugging #git-bisect #opustrator

Terminal Mouse Coordinate Bug Fix - Opustrator

Date: November 6, 2025 (4:30am)
Fix Commit: 91a7335
Breaking Commit: b57f7e0 (Nov 3, 2025)
Method: Git bisect through 76 commits

TL;DR

Problem: Mouse clicks in WebGL terminals (Claude Code) offset ~180-200px LEFT/UP from cursor, worse toward bottom-right.

Root Cause: Overly broad drag detection broke xterm.js text selection:

// ❌ WRONG - Breaks text selection!
if (e.type === 'mousemove' && e.buttons > 0) {
  return; // Skips transformation
}

Fix: Only skip when actually dragging terminal window:

// ✅ CORRECT - Trust react-draggable's .dragging class
const terminalWrapper = terminalRef.current.closest('.draggable-terminal-wrapper');
if (terminalWrapper && terminalWrapper.classList.contains('dragging')) {
  return; // Let react-draggable handle it
}

The Bug

Symptoms

  • Single clicks accurate in Canvas terminals (TFE, lazygit)
  • Single clicks offset in WebGL terminals (Claude Code, Codex)
  • Text selection started correctly but box drifted away during drag
  • Offset increased proportionally toward bottom-right
  • Right-click context menu always accurate

User's Key Observation

"yellow selection does start at my cursor but the selection box has the same broken issue"

This revealed:

  • mousedown (buttons=0) → transformation APPLIED → start correct ✅
  • mousemove during selection (buttons>0) → transformation SKIPPED → box offset ❌

Git Bisect Investigation

Method: Binary search through 76 commits (8 tests total)

Bisect Path:

  1. c62b2cd ✅ GOOD - Fully accurate
  2. e3f9a6b ✅ GOOD - Still working
  3. 8c00f5e ✅ GOOD - Viewport persistence didn't break it
  4. e1e78bd ✅ GOOD - Last working commit
  5. b57f7e0 ❌ BAD - Breaking commit identified!
  6. f2a204f ❌ BAD - Broken
  7. 66497bf ❌ BAD - Broken
  8. dfc82c8 ❌ BAD - Broken
  9. 29d9cf7 ❌ BAD - Current (broken)

Breaking Commit: b57f7e0 - "fix: resolve drag, bounds, and wheel event issues at non-100% zoom"

Root Cause Analysis

The commit added this logic in Terminal.tsx (lines 369-381):

// BUGGY CODE (introduced Nov 3, 2025):
if (e.type === 'mousemove' && e.buttons > 0) {
  // Mouse button held during move = likely dragging
  // But only skip if we're not at 100% zoom (no transform needed at 100%)
  const rect = terminalRef.current.getBoundingClientRect();
  const offsetWidth = terminalRef.current.offsetWidth;
  const ratio = rect.width / offsetWidth;
  if (Math.abs(ratio - 1) > 0.01) {
    // At non-100% zoom with buttons pressed during move = dragging
    return; // ❌ SKIPS TRANSFORMATION
  }
}

Why This Broke Text Selection

xterm.js text selection process:

  1. User clicks in terminal → mousedown event (e.buttons = 0)
  2. User drags mouse → mousemove events with button held (e.buttons > 0)
  3. User releases mouse → mouseup event

The bug:

  • The code assumed mousemove + e.buttons > 0 = terminal window dragging
  • But xterm text selection ALSO uses mousemove + e.buttons > 0
  • During text selection, transformation was skipped
  • xterm received untransformed coordinates in visual space
  • Selection box appeared at wrong location (~180-200px offset)

The Fix

File: frontend/src/components/Terminal.tsx (lines 358-365)

Before (19 lines of buggy code):

const terminalWrapper = terminalRef.current.closest('.draggable-terminal-wrapper');
if (terminalWrapper) {
  if (terminalWrapper.classList.contains('dragging')) {
    return;
  }

  // ❌ BUGGY: Overly broad check
  if (e.type === 'mousemove' && e.buttons > 0) {
    const rect = terminalRef.current.getBoundingClientRect();
    const offsetWidth = terminalRef.current.offsetWidth;
    const ratio = rect.width / offsetWidth;
    if (Math.abs(ratio - 1) > 0.01) {
      return; // Breaks text selection!
    }
  }
}

After (5 lines of clean code):

const terminalWrapper = terminalRef.current.closest('.draggable-terminal-wrapper');
if (terminalWrapper && terminalWrapper.classList.contains('dragging')) {
  // A drag is in progress - let react-draggable handle the event
  return;
}

Why This Works

The .dragging class:

  • Set by react-draggable only when dragging terminal header
  • NOT set during text selection inside terminal body
  • Provides reliable state without false positives

Result:

  • Terminal window dragging still works smoothly ✅
  • Text selection gets proper coordinate transformation ✅
  • Single clicks accurate at all zoom levels ✅
  • Both WebGL and Canvas renderers fixed ✅

Testing Results

Verified working:

  • ✅ Single clicks land exactly at cursor (all zoom levels)
  • ✅ Text selection box follows cursor during drag (no drift)
  • ✅ Terminal dragging by header still smooth
  • ✅ Claude Code (WebGL) terminals fixed
  • ✅ TFE (Canvas) terminals still work
  • ✅ Right-click context menu still accurate

Lessons Learned

  1. Git bisect is gold - Found exact breaking commit in 76 commits with only 8 builds
  2. User observations are clues - "Selection starts correct but box drifts" revealed the mousemove issue
  3. Don't reinvent the wheel - Trust library state (.dragging class) over heuristics (e.buttons > 0)
  4. Test edge cases - Text selection uses same events as dragging
  5. Document fixes - This bug cost 6 hours debugging, documentation prevents repeat

Prevention

❌ Never Do This

// DON'T use generic button state to detect dragging
if (e.buttons > 0) return; // Too broad!

✅ Always Do This

// DO use specific library-provided state
if (element.classList.contains('dragging')) return; // Reliable!

Checklist for Event Handling

  • Check what events your library/component uses (e.g., xterm uses mousemove for selection)
  • Don't block events based on generic state (e.buttons, e.type alone)
  • Use library-provided state indicators (classes, data attributes)
  • Test both primary use case AND edge cases (click vs text selection)
  • Document why you're skipping event transformation

References

  • Full Investigation: MOUSE_COORDINATE_BUG_BISECT.md in project root
  • CLAUDE.md: New section "Terminal Mouse Coordinate Transformation"
  • Fix Commit: 91a7335 - "fix: mouse coordinates accurate at all zoom levels"
  • Breaking Commit: b57f7e0 - "fix: resolve drag, bounds, and wheel event issues"

Stats

  • Debugging time: 6 hours (2 sessions)
  • Commits bisected: 76
  • Tests required: 8
  • Lines removed: 19 (buggy code)
  • Lines added: 5 (clean fix) + 312 (documentation)
  • Coffee consumed: Too much ☕
  • Time fixed: 4:30am 😴
  • Result: Pixel-perfect mouse accuracy 🎯

Project: Opustrator - Multi-agent orchestration with infinite canvas
Author: Matt (@GGPrompts)
Assistant: Claude Code (Anthropic)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment