This repository has no description
0

Configure Feed

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

Fix doubled keystrokes after session exits in interactive list

The EXIT packet handler in attach() didn't set exitHandled, so the
socket close event called onExit a second time, which added a
duplicate stdin listener via resumeTui → setupInput.

Added regression test that types after session exit and verifies
characters aren't doubled.

Nathan Herald (Mar 26, 2026, 2:31 PM +0100) 9f15b9d5 ef1c8a4a

+48 -1
+1 -1
package.json
··· 1 1 { 2 2 "name": "@myobie/pty", 3 - "version": "0.1.2", 3 + "version": "0.1.3", 4 4 "description": "Persistent terminal sessions with detach/attach support", 5 5 "type": "module", 6 6 "license": "MIT",
+1
src/client.ts
··· 317 317 318 318 case MessageType.EXIT: 319 319 exitCode = decodeExit(packet.payload); 320 + exitHandled = true; 320 321 cleanExit(); 321 322 stdout.write(TERMINAL_SANITIZE + CURSOR_TO_BOTTOM + `\r\n[session exited with code ${exitCode}]\r\n`); 322 323 options.onExit?.(exitCode);
+46
tests/tui.test.ts
··· 563 563 ); 564 564 565 565 it( 566 + "after session exits and returning to list, keystrokes are not doubled", 567 + async () => { 568 + const sessionDir = makeSessionDir(); 569 + const name = uniqueName(); 570 + 571 + // A session that exits immediately after printing a marker 572 + const { pid } = await createBackgroundSession( 573 + sessionDir, 574 + name, 575 + "sh", 576 + ["-c", "echo WILL_EXIT; exec cat"], 577 + os.tmpdir() 578 + ); 579 + bgPids.push(pid); 580 + 581 + const tui = createTuiSession(sessionDir); 582 + 583 + // Attach 584 + await tui.waitForText(name, 10000); 585 + tui.press("return"); 586 + await tui.waitForText("WILL_EXIT", 10000); 587 + 588 + // Make the session exit (Ctrl+D sends EOF to cat) 589 + tui.sendKeys("\x04"); 590 + 591 + // Wait for return to list 592 + await tui.waitForText("Create new session...", 10000); 593 + 594 + // Type a filter string and verify each character appears exactly once. 595 + // If stdin has duplicate listeners, each keystroke fires twice and 596 + // the filter will contain doubled characters (e.g. "xxyz" instead of "xyz"). 597 + tui.type("x"); 598 + await new Promise(r => setTimeout(r, 300)); 599 + tui.type("y"); 600 + await new Promise(r => setTimeout(r, 300)); 601 + tui.type("z"); 602 + await new Promise(r => setTimeout(r, 300)); 603 + 604 + const ss = tui.screenshot(); 605 + expect(ss.text).toContain("xyz"); 606 + expect(ss.text).not.toContain("xxyyzz"); 607 + }, 608 + 20000 609 + ); 610 + 611 + it( 566 612 "exited session shows as exited when returning to list during cleanup window", 567 613 async () => { 568 614 const sessionDir = makeSessionDir();