This repository has no description
0

Configure Feed

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

Resize to the smallest dimensions across all connected writable clients

Nathan Herald (Mar 23, 2026, 3:24 PM +0100) bcef294a bcf9fc3e

+172 -23
+9 -8
src/server.ts
··· 278 278 279 279 socket.on("close", () => { 280 280 this.clients.delete(socket); 281 + this.negotiateSize(); 281 282 }); 282 283 283 284 socket.on("error", () => { 284 285 this.clients.delete(socket); 286 + this.negotiateSize(); 285 287 }); 286 288 } 287 289 ··· 295 297 return prefix; 296 298 } 297 299 298 - /** Resize the PTY to match the most recently attached client. 300 + /** Resize the PTY to the smallest dimensions across all connected writable clients. 299 301 * Returns true if the size actually changed. */ 300 302 private negotiateSize(): boolean { 301 - // Use the most recently attached/resized non-readonly client's size 302 - let lastClient: Client | null = null; 303 + let rows = 0; 304 + let cols = 0; 305 + 303 306 for (const client of this.clients.values()) { 304 307 if (!client.readonly && client.attachSeq > 0) { 305 - if (!lastClient || client.attachSeq > lastClient.attachSeq) { 306 - lastClient = client; 307 - } 308 + rows = rows === 0 ? client.rows : Math.min(rows, client.rows); 309 + cols = cols === 0 ? client.cols : Math.min(cols, client.cols); 308 310 } 309 311 } 310 312 311 - if (lastClient) { 312 - const { rows, cols } = lastClient; 313 + if (rows > 0 && cols > 0) { 313 314 if (rows !== this.terminal.rows || cols !== this.terminal.cols) { 314 315 this.ptyProcess.resize(cols, rows); 315 316 this.terminal.resize(cols, rows);
+163 -15
tests/integration.test.ts
··· 127 127 }); 128 128 } 129 129 130 + /** Collect DATA payloads until the accumulated text contains the pattern. */ 131 + function waitForContent( 132 + socket: net.Socket, 133 + reader: PacketReader, 134 + pattern: string, 135 + timeoutMs = 5000 136 + ): Promise<string> { 137 + return new Promise((resolve, reject) => { 138 + let accumulated = ""; 139 + const timer = setTimeout( 140 + () => reject(new Error(`Timed out waiting for "${pattern}" in DATA (got: ${JSON.stringify(accumulated)})`)), 141 + timeoutMs 142 + ); 143 + 144 + function onData(data: Buffer) { 145 + const packets = reader.feed(data); 146 + for (const packet of packets) { 147 + if (packet.type === MessageType.DATA) { 148 + accumulated += packet.payload.toString(); 149 + if (accumulated.includes(pattern)) { 150 + clearTimeout(timer); 151 + socket.off("data", onData); 152 + resolve(accumulated); 153 + return; 154 + } 155 + } 156 + } 157 + } 158 + 159 + socket.on("data", onData); 160 + }); 161 + } 162 + 130 163 afterEach(async () => { 131 164 for (const server of servers) { 132 165 await server.close(); ··· 566 599 releaseLock(name); 567 600 }); 568 601 569 - it("last attached client wins for terminal size", async () => { 602 + it("uses smallest connected client size", async () => { 603 + const name = uniqueName(); 604 + await startServer(name, "sh", [], { rows: 24, cols: 80 }); 605 + 606 + // Client 1 attaches with 50x200 607 + const client1 = await connect(name); 608 + const reader1 = new PacketReader(); 609 + client1.write(encodeAttach(50, 200)); 610 + await waitForType(client1, reader1, MessageType.SCREEN); 611 + 612 + // Ask for size — should be 50x200 (only client) 613 + client1.write(encodeData("stty size\n")); 614 + await waitForContent(client1, reader1, "50 200"); 615 + 616 + // Client 2 attaches with smaller size 30x100 617 + const client2 = await connect(name); 618 + const reader2 = new PacketReader(); 619 + client2.write(encodeAttach(30, 100)); 620 + await waitForType(client2, reader2, MessageType.SCREEN); 621 + 622 + // Size should now be the minimum: 30x100 623 + client1.write(encodeData("stty size\n")); 624 + await waitForContent(client1, reader1, "30 100"); 625 + 626 + client1.destroy(); 627 + client2.destroy(); 628 + }); 629 + 630 + it("uses minimum of each dimension independently", async () => { 631 + const name = uniqueName(); 632 + await startServer(name, "sh", [], { rows: 24, cols: 80 }); 633 + 634 + // Client 1: tall and narrow (60 rows, 80 cols) 635 + const client1 = await connect(name); 636 + const reader1 = new PacketReader(); 637 + client1.write(encodeAttach(60, 80)); 638 + await waitForType(client1, reader1, MessageType.SCREEN); 639 + 640 + // Client 2: short and wide (30 rows, 200 cols) 641 + const client2 = await connect(name); 642 + const reader2 = new PacketReader(); 643 + client2.write(encodeAttach(30, 200)); 644 + await waitForType(client2, reader2, MessageType.SCREEN); 645 + 646 + // Should be min of each: 30 rows, 80 cols 647 + client1.write(encodeData("stty size\n")); 648 + await waitForContent(client1, reader1, "30 80"); 649 + 650 + client1.destroy(); 651 + client2.destroy(); 652 + }); 653 + 654 + it("recalculates size when a client disconnects", async () => { 655 + const name = uniqueName(); 656 + await startServer(name, "sh", [], { rows: 24, cols: 80 }); 657 + 658 + // Client 1: large terminal 659 + const client1 = await connect(name); 660 + const reader1 = new PacketReader(); 661 + client1.write(encodeAttach(50, 200)); 662 + await waitForType(client1, reader1, MessageType.SCREEN); 663 + 664 + // Client 2: small terminal (phone) 665 + const client2 = await connect(name); 666 + const reader2 = new PacketReader(); 667 + client2.write(encodeAttach(30, 80)); 668 + await waitForType(client2, reader2, MessageType.SCREEN); 669 + 670 + // Size should be 30x80 (smallest) 671 + client1.write(encodeData("stty size\n")); 672 + await waitForContent(client1, reader1, "30 80"); 673 + 674 + // Phone disconnects 675 + client2.destroy(); 676 + // Give the server a moment to process the disconnect 677 + await new Promise((r) => setTimeout(r, 100)); 678 + 679 + // Size should restore to 50x200 (only remaining client) 680 + client1.write(encodeData("stty size\n")); 681 + await waitForContent(client1, reader1, "50 200"); 682 + 683 + client1.destroy(); 684 + }); 685 + 686 + it("recalculates size on clean detach", async () => { 570 687 const name = uniqueName(); 571 - // Use tput to print terminal dimensions — it reads from the PTY 572 - await startServer(name, "cat", [], { rows: 24, cols: 80 }); 688 + await startServer(name, "sh", [], { rows: 24, cols: 80 }); 573 689 574 - // Client 1 attaches with 30x100 690 + // Client 1: large terminal 575 691 const client1 = await connect(name); 576 692 const reader1 = new PacketReader(); 577 - client1.write(encodeAttach(30, 100)); 693 + client1.write(encodeAttach(50, 200)); 578 694 await waitForType(client1, reader1, MessageType.SCREEN); 579 695 580 - // Client 2 attaches with 40x120 696 + // Client 2: small terminal 581 697 const client2 = await connect(name); 582 698 const reader2 = new PacketReader(); 583 - client2.write(encodeAttach(40, 120)); 699 + client2.write(encodeAttach(25, 90)); 584 700 await waitForType(client2, reader2, MessageType.SCREEN); 585 701 586 - // Now client 1 re-attaches with 50x150 — should win because it's most recent 587 - client1.write(encodeAttach(50, 150)); 702 + // Size should be 25x90 (smallest) 703 + client1.write(encodeData("stty size\n")); 704 + await waitForContent(client1, reader1, "25 90"); 705 + 706 + // Client 2 detaches cleanly 707 + client2.write(encodeDetach()); 708 + await new Promise((r) => setTimeout(r, 100)); 709 + 710 + // Size should restore to 50x200 711 + client1.write(encodeData("stty size\n")); 712 + await waitForContent(client1, reader1, "50 200"); 713 + 714 + client1.destroy(); 715 + }); 716 + 717 + it("resize message updates size negotiation", async () => { 718 + const name = uniqueName(); 719 + await startServer(name, "sh", [], { rows: 24, cols: 80 }); 720 + 721 + // Client 1: starts large 722 + const client1 = await connect(name); 723 + const reader1 = new PacketReader(); 724 + client1.write(encodeAttach(50, 200)); 588 725 await waitForType(client1, reader1, MessageType.SCREEN); 589 726 590 - // Verify the session still works and the size was applied 591 - // (We can't easily read back the PTY size, but we can confirm no crash 592 - // and that input/output still works after the re-attach) 593 - client1.write(encodeData("size-test\n")); 594 - const data = await waitForType(client1, reader1, MessageType.DATA, 3000); 595 - expect(data.payload.toString()).toContain("size-test"); 727 + // Client 2: starts small 728 + const client2 = await connect(name); 729 + const reader2 = new PacketReader(); 730 + client2.write(encodeAttach(30, 100)); 731 + await waitForType(client2, reader2, MessageType.SCREEN); 732 + 733 + // Size should be 30x100 734 + client1.write(encodeData("stty size\n")); 735 + await waitForContent(client1, reader1, "30 100"); 736 + 737 + // Client 2 resizes larger (rotated phone, etc.) 738 + client2.write(encodeResize(60, 250)); 739 + await new Promise((r) => setTimeout(r, 100)); 740 + 741 + // Now client 1 is the smallest: 50x200 742 + client1.write(encodeData("stty size\n")); 743 + await waitForContent(client1, reader1, "50 200"); 596 744 597 745 client1.destroy(); 598 746 client2.destroy();