This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Track and replay mouse tracking modes (1000/1002/1003) on reattach

The server already replayed SGR encoding (1006), cursor visibility, and
kitty keyboard flags via getModePrefix(), but not the tracking modes
themselves. Clients attaching mid-session saw SGR on but tracking off,
so mouse forwarding decisions (e.g. in pty-layout) got it wrong.

Nathan Herald (Apr 15, 2026, 10:58 PM +0200) 5281cfbf b112689e

+114
+3
CHANGELOG.md
··· 2 2 3 3 ## Unreleased 4 4 5 + ### Fixes 6 + - Fix mouse tracking modes (1000/1002/1003) not being replayed when a client reattaches to a session with mouse tracking already enabled — the server previously only replayed the SGR encoding (1006), cursor visibility, and kitty keyboard flags. Clients (e.g., pty-layout) checking tracking mode to decide whether to forward mouse events will now see the correct state. 7 + 5 8 ### Interactive TUI 6 9 - Add `--preselect-new` flag: `pty --preselect-new` opens the interactive TUI with "Create new session..." pre-selected (useful for pty-layout panes that should land on the create prompt) 7 10 - Add `--filter-tag key=value` flag (repeatable): filters the TUI to sessions matching all given tags AND auto-applies those tags to any session created from this TUI instance — so new sessions (local and remote) stay in the filtered view (e.g., pty-layout layouts)
+18
src/server.ts
··· 131 131 private sgrMouseMode = false; 132 132 private cursorHidden = false; 133 133 private kittyKeyboardStack: number[] = []; 134 + // Mouse tracking modes — these are separate DEC private modes (set/cleared 135 + // independently by the child process) that control WHICH events the 136 + // terminal should report. SGR mode (1006) only controls the ENCODING of 137 + // reports, not whether tracking is active. Clients attaching to a session 138 + // that's already mid-stream need all active modes replayed so their own 139 + // mouse forwarding logic sees the correct state. 140 + private mouseTracking1000 = false; // button press/release tracking 141 + private mouseTracking1002 = false; // button-motion tracking 142 + private mouseTracking1003 = false; // any-motion tracking 134 143 private lastResizeTime = 0; 135 144 private eventWriter: EventWriter; 136 145 private lastTitle = ""; ··· 158 167 for (const p of params) { 159 168 const v = typeof p === "number" ? p : p[0]; 160 169 if (v === 1006) this.sgrMouseMode = true; 170 + if (v === 1000) this.mouseTracking1000 = true; 171 + if (v === 1002) this.mouseTracking1002 = true; 172 + if (v === 1003) this.mouseTracking1003 = true; 161 173 if (v === 25) { 162 174 if (this.cursorHidden) this.emitEvent(EventType.CURSOR_VISIBLE); 163 175 this.cursorHidden = false; ··· 173 185 for (const p of params) { 174 186 const v = typeof p === "number" ? p : p[0]; 175 187 if (v === 1006) this.sgrMouseMode = false; 188 + if (v === 1000) this.mouseTracking1000 = false; 189 + if (v === 1002) this.mouseTracking1002 = false; 190 + if (v === 1003) this.mouseTracking1003 = false; 176 191 if (v === 25) this.cursorHidden = true; 177 192 } 178 193 return false; ··· 542 557 543 558 private getModePrefix(): string { 544 559 let prefix = ""; 560 + if (this.mouseTracking1000) prefix += "\x1b[?1000h"; 561 + if (this.mouseTracking1002) prefix += "\x1b[?1002h"; 562 + if (this.mouseTracking1003) prefix += "\x1b[?1003h"; 545 563 if (this.sgrMouseMode) prefix += "\x1b[?1006h"; 546 564 if (this.cursorHidden) prefix += "\x1b[?25l"; 547 565 for (const flags of this.kittyKeyboardStack) {
+93
tests/ratatui-compat.test.ts
··· 599 599 }, 600 600 20000 601 601 ); 602 + 603 + it( 604 + "mouse tracking modes (1000/1002/1003) are replayed in getModePrefix on reattach", 605 + async () => { 606 + const dir = makeTmpDir(); 607 + const scriptPath = path.join(dir, "mouse-tracking.ts"); 608 + fs.writeFileSync( 609 + scriptPath, 610 + ` 611 + // Enable button-motion tracking (what TUI apps like claude/vim typically use) 612 + process.stdout.write('\\x1b[?1002h'); 613 + // Also enable SGR encoding (the existing case — should still work) 614 + process.stdout.write('\\x1b[?1006h'); 615 + process.stdout.write('MOUSE-TRACKING-OK\\n'); 616 + setTimeout(() => {}, 300000); 617 + ` 618 + ); 619 + 620 + const session = await createSession( 621 + process.execPath, 622 + [scriptPath], 623 + { rows: 24, cols: 80, cwd: dir } 624 + ); 625 + 626 + await session.waitForText("MOUSE-TRACKING-OK", 10000); 627 + 628 + const rawScreen = await getRawScreenPayload(session.name, 24, 80); 629 + // Bug #NN: the tracking mode itself (1002) must be replayed, not just 630 + // the SGR encoding (1006). Without this, clients checking whether 631 + // mouse tracking is active see it as off. 632 + expect(rawScreen).toMatch(/\x1b\[\?1002h/); 633 + expect(rawScreen).toMatch(/\x1b\[\?1006h/); 634 + }, 635 + 20000 636 + ); 637 + 638 + it( 639 + "mouse tracking modes are cleared when child disables them", 640 + async () => { 641 + const dir = makeTmpDir(); 642 + const scriptPath = path.join(dir, "mouse-tracking-off.ts"); 643 + fs.writeFileSync( 644 + scriptPath, 645 + ` 646 + process.stdout.write('\\x1b[?1003h'); // turn on any-motion 647 + process.stdout.write('\\x1b[?1003l'); // turn off again 648 + process.stdout.write('MOUSE-OFF-OK\\n'); 649 + setTimeout(() => {}, 300000); 650 + ` 651 + ); 652 + 653 + const session = await createSession( 654 + process.execPath, 655 + [scriptPath], 656 + { rows: 24, cols: 80, cwd: dir } 657 + ); 658 + 659 + await session.waitForText("MOUSE-OFF-OK", 10000); 660 + 661 + const rawScreen = await getRawScreenPayload(session.name, 24, 80); 662 + // Neither the set nor the tracking mode should be in the prefix 663 + expect(rawScreen).not.toMatch(/\x1b\[\?1003h/); 664 + }, 665 + 20000 666 + ); 667 + 668 + it( 669 + "button tracking (1000) is also tracked and replayed", 670 + async () => { 671 + const dir = makeTmpDir(); 672 + const scriptPath = path.join(dir, "mouse-1000.ts"); 673 + fs.writeFileSync( 674 + scriptPath, 675 + ` 676 + process.stdout.write('\\x1b[?1000h'); 677 + process.stdout.write('MOUSE-1000-OK\\n'); 678 + setTimeout(() => {}, 300000); 679 + ` 680 + ); 681 + 682 + const session = await createSession( 683 + process.execPath, 684 + [scriptPath], 685 + { rows: 24, cols: 80, cwd: dir } 686 + ); 687 + 688 + await session.waitForText("MOUSE-1000-OK", 10000); 689 + 690 + const rawScreen = await getRawScreenPayload(session.name, 24, 80); 691 + expect(rawScreen).toMatch(/\x1b\[\?1000h/); 692 + }, 693 + 20000 694 + ); 602 695 }); 603 696 604 697 // ============================================================================