jupyterlab_terminal_cpr_escape_fix
Fix the JupyterLab terminado issue where returning to an idle terminal causes cursor position report (CPR) escape sequences to appear as literal text. This is particularly noticeable with fish shell, where sequences like [2;2R[3;1R or [?1;2c[>0;276;0c appear at the prompt after reconnecting.
How it works
When a JupyterLab terminal sits idle, the shell (especially fish) periodically queries terminal capabilities. These queries accumulate in terminado's buffer. On reconnect, terminado replays that buffer, xterm.js answers the old queries again, and the shell inserts the answers as literal text.
This extension patches TermSocket.on_pty_read() server-side to filter terminal query responses before they reach the browser. It handles both ESC-prefixed sequences and bare remnants where fish shell has stripped the ESC byte.
Filtered sequences (terminal query responses):
- CPR - Cursor Position Report (
ESC[row;colR) - DA/DA2 - Device Attributes (
ESC[?...c,ESC[>...c) - DECRPM - DEC Report Mode (
ESC[?mode;value$y) - OSC 4/10/11/12 - Color query responses
Preserved sequences (functional terminal output):
- All SGR color codes, cursor movement, erase, scroll
- OSC 0 (window/tab title), OSC 7 (cwd), OSC 8 (hyperlinks)
- OSC 52 (clipboard - used by companion clipboard extension)
- OSC 133 (shell integration prompt marks)
- Bracketed paste mode, alternate screen, all DEC private modes
Repaint on attach (window refresh recovery):
- After a client attaches and the buffer replay drains, the PTY is nudged one row taller and back
- The kernel delivers SIGWINCH, so the foreground app (Claude Code, vim, htop) repaints its full screen
- Without it, a refreshed browser shows a torn screen: the replay holds only incremental diff frames, so static regions (status line, input box) are never delivered and never self-heal
- Toggle:
DEFAULTS['repaint_on_attach']in__init__.py(default on), debounced 5s per terminal
Replay query stripping (no replies on reconnect):
On attach, terminado replays the terminal's buffered output and xterm.js answers every query in it as if it were live, so the shell receives the answers as typed text. The extension removes those queries from the replay only.
- Stripped: OSC 4/10/11/12 color queries, DA/DA2 queries,
ESC[5n,ESC[6n,ESC[?6n, DECRQM, DECRQSS - Queries at the very end of the replay are kept, because the program that sent them may still wait for the answer
- Live output after the attach passes every query unchanged
- Toggle:
DEFAULTS['strip_replay_queries']in__init__.py(default on);suppress_buffer_replaytakes precedence
Background terminal redraw (history at the real size):
After a page load JupyterLab gives a terminal in a background tab an 80x24 screen until its tab is shown, reports that size to the server and writes the replayed history into it. The frontend part of this extension sends no size to the server before the terminal is opened, so the PTY keeps its real size. When the terminal is first shown it reconnects once and the server replays the history into the fitted terminal.
- Only output that arrives while the tab is hidden leads to a redraw; a terminal that is visible when its output arrives, a new one included, is not reconnected
- The redraw is one more replay from the server, so it relies on replay query stripping (on by default): with
strip_replay_queriesoff, old queries in the history are answered once more when the tab is first shown - Limits: the first row of the redrawn history can start with the text that was on the cursor's line before the redraw (usually the prompt), and history printed before a program switched to the alternate screen is not drawn again while that program runs
- Limit: the redraw replaces what the terminal held with the server's replay, which is the last 1000 output chunks (nothing when
suppress_buffer_replayis on). Output older than that, received while the tab was in the background, is dropped. With the setting "Redraw background terminals when first shown" off it stays - Toggle: the setting "Redraw background terminals when first shown" in the Settings Editor, section "Terminal CPR Escape Fix" (default on)
Requirements
- JupyterLab >= 4.0.0
Installation
pip install jupyterlab_terminal_cpr_escape_fix
Uninstall
pip uninstall jupyterlab_terminal_cpr_escape_fix
Metadata
Release files for jupyterlab-terminal-cpr-escape-fix 1.0.18
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jupyterlab_terminal_cpr_escape_fix-1.0.18.tar.gz | 390.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jupyterlab_terminal_cpr_escape_fix-1.0.18-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 436.7 kB
Release files / jupyterlab_terminal_cpr_escape_fix-1.0.18.tar.gz
| Download URL | jupyterlab_terminal_cpr_escape_fix-1.0.18.tar.gz |
|---|---|
| Size | 390.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1728f7c7e491b8f5a3b9fdd7b5cb9a10c50aab5ef403b7174c3a1188393ebef0
|
|
BLAKE2b-256 checksum How to use checksums |
ff1424842e6b631c1e348d1c007098648ae74220494a74156cb36fcf62e45773
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / jupyterlab_terminal_cpr_escape_fix-1.0.18-py3-none-any.whl
| Download URL | jupyterlab_terminal_cpr_escape_fix-1.0.18-py3-none-any.whl |
|---|---|
| Size | 46.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
881a6dd45c4e70f8689e0a6450c5dac7f73210706403251d830f7df690724570
|
|
BLAKE2b-256 checksum How to use checksums |
52563a8d291ddf7ee412f648554d5002613e8e6c9971b995b3faf8b2ed799101
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|