Summary
When running inside a JetBrains IDE terminal (JediTerm), every free mouse movement (no button held) is decoded by MouseParser.decodeSgrEvent as a mousedown event instead of a move event. This causes interactive elements to fire their onMouseDown handlers continuously as the mouse moves, breaking scrollbars, collapsibles, and even the chat.
Related opencode issues:
Root cause
When 1003 (any-event) + 1006 (SGR) mouse tracking is enabled, a correct terminal sends free motion events with Cb=35:
ESC [ < 35 ; col ; row M (35 = 32 | 3 — motion bit set, no button held)
JetBrains JediTerm sends Cb=3 instead:
ESC [ < 3 ; col ; row M (3 = no button — motion bit absent)
In decodeSgrEvent, the motion bit is checked as:
const isMotion = (rawButtonCode & 32) !== 0;
With rawButtonCode=3, isMotion is false. The event falls through to:
type = pressRelease === "M" ? "down" : "up";
Since JetBrains uses the M (press) terminator, every free motion becomes type="down" — a mousedown event. There is never a corresponding mouseup, because JetBrains does not send a release (m terminator) for motion events.
This is a JetBrains bug: the motion bit (32) is defined in the xterm spec for button-motion events and applies equally to SGR mode. JetBrains correctly includes the motion bit for drag events (Cb=32 when left button is held), but omits it for free motion.
A bug report has been filed with JetBrains at https://youtrack.jetbrains.com/issue/IJPL-242176/Terminal-SGR-mouse-tracking-mode-1003-missing-motion-bit-32-for-hovering
Impact
Because every mouse movement becomes a mousedown with button=0:
- Scrollbars jump to the mouse position on every movement —
SliderRenderable.onMouseDown sets isDragging=true and calls updateValueFromMouseDirect immediately.
- Collapsible elements toggle on every movement — any
onMouseDown handler fires continuously.
- Chat moves the cursor to mouse location immediately
- Any other location that interacts with mousedown
Options
A. Wait for JetBrains to fix the bug
The correct long-term fix is on the JetBrains side. Once they send Cb=35 for free motion, the issue disappears with no changes needed here.
B. Add a workaround in decodeSgrEvent
|
private decodeSgrEvent(rawButtonCode: number, wireX: number, wireY: number, pressRelease: "M" | "m"): RawMouseEvent { |
|
const button = rawButtonCode & 3 |
|
const isScroll = (rawButtonCode & 64) !== 0 |
|
const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button] |
|
|
|
const isMotion = (rawButtonCode & 32) !== 0 |
|
const modifiers = { |
|
shift: (rawButtonCode & 4) !== 0, |
|
alt: (rawButtonCode & 8) !== 0, |
|
ctrl: (rawButtonCode & 16) !== 0, |
|
} |
|
|
|
let type: MouseEventType |
|
let scrollInfo: ScrollInfo | undefined |
|
|
|
if (isMotion) { |
|
const isDragging = this.mouseButtonsPressed.size > 0 |
|
|
|
if (button === 3) { |
|
type = "move" |
|
} else if (isDragging) { |
|
type = "drag" |
|
} else { |
|
type = "move" |
|
} |
|
} else if (isScroll && pressRelease === "M") { |
|
type = "scroll" |
|
scrollInfo = { |
|
direction: scrollDirection!, |
|
delta: 1, |
|
} |
|
} else { |
|
type = pressRelease === "M" ? "down" : "up" |
|
|
|
if (type === "down" && button !== 3) { |
|
this.mouseButtonsPressed.add(button) |
|
} else if (type === "up") { |
|
this.mouseButtonsPressed.clear() |
|
} |
|
} |
|
|
|
return { |
|
type, |
|
button: button === 3 ? 0 : button, |
|
x: wireX - 1, |
|
y: wireY - 1, |
|
modifiers, |
|
scroll: scrollInfo, |
|
} |
|
} |
In opentui/packages/core/src/lib/parse.mouse.ts:156, the condition detecting motion events can be extended to also catch the JetBrains-specific broken encoding. button === 3 && pressRelease === "M" has no valid press semantics in SGR mode (button=3 means "no button"; there is nothing to press), so treating it as motion is safe and unambiguous:
const isJetBrainsMissingMotionBit =
process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"
&& button === 3
&& pressRelease === "M";
if (isMotion || isJetBrainsMissingMotionBit) {
// ...
}
Scoping the check to TERMINAL_EMULATOR === "JetBrains-JediTerm" ensures:
- No other terminal is affected.
- Once JetBrains fixes the bug and starts sending
Cb=35, their terminal will take the isMotion=true path and isJetBrainsMissingMotionBit becomes unreachable dead code — no downstream breakage.
Reproduction
The following script enables 1003 + 1006 mouse tracking and prints every raw escape sequence received. Run it inside a JetBrains terminal and move the mouse without clicking — you will see a stream of ESC[<3;...]M events. Run it in Ghostty or another correct terminal and you will see ESC[<35;...]M instead.
#!/usr/bin/env python3
"""
Print raw terminal escape sequences for mouse events.
Enables SGR + any-event mouse tracking,
then dumps every byte received so you can verify how your terminal
encodes mouse motion vs button presses.
When moving the mouse (no buttons held), a correct terminal sends:
ESC [ < 35 ; col ; row M (raw button code 35 = 32|3, motion bit set)
With the IntelliJ bug you will see instead:
ESC [ < 3 ; col ; row M (raw button code 3 = no button, motion bit absent)
Press Ctrl+C to quit.
"""
import os
import signal
import sys
import termios
import tty
ENABLE_ANY_EVENT = "\x1b[?1003h"
ENABLE_SGR = "\x1b[?1006h"
DISABLE_ANY_EVENT = "\x1b[?1003l"
DISABLE_SGR = "\x1b[?1006l"
def main() -> None:
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
def restore(sig=None, frame=None) -> None:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
sys.stdout.write(DISABLE_ANY_EVENT + DISABLE_SGR + "\r\n")
sys.stdout.flush()
os._exit(0)
signal.signal(signal.SIGINT, restore)
tty.setraw(fd)
sys.stdout.write(ENABLE_ANY_EVENT + ENABLE_SGR)
sys.stdout.write("Move mouse, scroll, click. Press Ctrl+C to quit.\r\n")
sys.stdout.flush()
while True:
data = os.read(fd, 64)
if b"\x03" in data:
restore()
hex_bytes = " ".join(f"{b:02x}" for b in data)
readable = data.decode("latin1").replace("\x1b", "ESC").replace("\r", "CR")
sys.stdout.write(f"bytes={hex_bytes} text={readable!r}\r\n")
sys.stdout.flush()
if __name__ == "__main__":
main()
Summary
When running inside a JetBrains IDE terminal (JediTerm), every free mouse movement (no button held) is decoded by
MouseParser.decodeSgrEventas amousedownevent instead of amoveevent. This causes interactive elements to fire theironMouseDownhandlers continuously as the mouse moves, breaking scrollbars, collapsibles, and even the chat.Related opencode issues:
Root cause
When
1003(any-event) +1006(SGR) mouse tracking is enabled, a correct terminal sends free motion events withCb=35:JetBrains JediTerm sends
Cb=3instead:In
decodeSgrEvent, the motion bit is checked as:With
rawButtonCode=3,isMotionisfalse. The event falls through to:Since JetBrains uses the
M(press) terminator, every free motion becomestype="down"— amousedownevent. There is never a correspondingmouseup, because JetBrains does not send a release (mterminator) for motion events.This is a JetBrains bug: the motion bit (32) is defined in the xterm spec for button-motion events and applies equally to SGR mode. JetBrains correctly includes the motion bit for drag events (
Cb=32when left button is held), but omits it for free motion.A bug report has been filed with JetBrains at https://youtrack.jetbrains.com/issue/IJPL-242176/Terminal-SGR-mouse-tracking-mode-1003-missing-motion-bit-32-for-hovering
Impact
Because every mouse movement becomes a
mousedownwithbutton=0:SliderRenderable.onMouseDownsetsisDragging=trueand callsupdateValueFromMouseDirectimmediately.onMouseDownhandler fires continuously.Options
A. Wait for JetBrains to fix the bug
The correct long-term fix is on the JetBrains side. Once they send
Cb=35for free motion, the issue disappears with no changes needed here.B. Add a workaround in
decodeSgrEventopentui/packages/core/src/lib/parse.mouse.ts
Lines 141 to 190 in 72aa752
In
opentui/packages/core/src/lib/parse.mouse.ts:156, the condition detecting motion events can be extended to also catch the JetBrains-specific broken encoding.button === 3 && pressRelease === "M"has no valid press semantics in SGR mode (button=3means "no button"; there is nothing to press), so treating it as motion is safe and unambiguous:Scoping the check to
TERMINAL_EMULATOR === "JetBrains-JediTerm"ensures:Cb=35, their terminal will take theisMotion=truepath andisJetBrainsMissingMotionBitbecomes unreachable dead code — no downstream breakage.Reproduction
The following script enables
1003+1006mouse tracking and prints every raw escape sequence received. Run it inside a JetBrains terminal and move the mouse without clicking — you will see a stream ofESC[<3;...]Mevents. Run it in Ghostty or another correct terminal and you will seeESC[<35;...]Minstead.