Date: November 6, 2025 (4:30am)
Fix Commit: 91a7335
Breaking Commit: b57f7e0 (Nov 3, 2025)
Method: Git bisect through 76 commits
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
}- 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
"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 ✅mousemoveduring selection (buttons>0) → transformation SKIPPED → box offset ❌
Method: Binary search through 76 commits (8 tests total)
Bisect Path:
- c62b2cd ✅ GOOD - Fully accurate
- e3f9a6b ✅ GOOD - Still working
- 8c00f5e ✅ GOOD - Viewport persistence didn't break it
- e1e78bd ✅ GOOD - Last working commit
- b57f7e0 ❌ BAD - Breaking commit identified!
- f2a204f ❌ BAD - Broken
- 66497bf ❌ BAD - Broken
- dfc82c8 ❌ BAD - Broken
- 29d9cf7 ❌ BAD - Current (broken)
Breaking Commit: b57f7e0 - "fix: resolve drag, bounds, and wheel event issues at non-100% zoom"
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
}
}xterm.js text selection process:
- User clicks in terminal →
mousedownevent (e.buttons = 0) - User drags mouse →
mousemoveevents with button held (e.buttons > 0) - User releases mouse →
mouseupevent
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)
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;
}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 ✅
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
- Git bisect is gold - Found exact breaking commit in 76 commits with only 8 builds
- User observations are clues - "Selection starts correct but box drifts" revealed the mousemove issue
- Don't reinvent the wheel - Trust library state (
.draggingclass) over heuristics (e.buttons > 0) - Test edge cases - Text selection uses same events as dragging
- Document fixes - This bug cost 6 hours debugging, documentation prevents repeat
// DON'T use generic button state to detect dragging
if (e.buttons > 0) return; // Too broad!// DO use specific library-provided state
if (element.classList.contains('dragging')) return; // Reliable!- 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.typealone) - 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
- Full Investigation:
MOUSE_COORDINATE_BUG_BISECT.mdin 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"
- 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)