authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 04:34:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 04:34:36-07:00
log2cde3f770bbf7a6ed9d52a6b1f64a471c0303a5a
tree3a5c22e5c188f36080263b8fa4df85b4c1cb8bbd
parent7fd2ba7526b94080810de575f5ab15fe96d2bb5d
signature Signed by PGP key B5690EEEBB952194

fix(web): terminal tells the shell its new width as soon as you release a resize drag (#19395)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 44 insertions(+), 0 deletions(-)

apps/web/src/terminal/ghostty/surface.test.ts+24
......@@ -540,6 +540,30 @@ describe("GhosttyTerminalSurface visibility", () => {
540540 expect(resizePty).toHaveBeenCalledOnce();
541541 });
542542
543 it("sends the settled grid when a resize drag is released", async () => {
544 const harness = createHarness();
545 const onResize = vi.fn<(cols: number, rows: number) => void>();
546 const surface = await harness.create({ onResize });
547 vi.advanceTimersByTime(150);
548 onResize.mockClear();
549
550 // Mid-drag, the grid reflows locally while the PTY waits for the debounce.
551 harness.mount.clientWidth = 120;
552 harness.resize();
553 expect(surface.cols).toBe(14);
554 expect(onResize).not.toHaveBeenCalled();
555
556 // The release writes the final width before the observer reports it.
557 harness.mount.clientWidth = 96;
558 window.dispatchEvent(new Event("pointerup"));
559 expect(onResize).toHaveBeenCalledExactlyOnceWith(11, 6);
560
561 harness.resize();
562 vi.advanceTimersByTime(150);
563 window.dispatchEvent(new Event("pointerup"));
564 expect(onResize).toHaveBeenCalledOnce();
565 });
566
543567 it("replays the current grid on reveal when a hidden host becomes ready", async () => {
544568 const harness = createHarness();
545569 const onResize = vi.fn<(cols: number, rows: number) => void>();
apps/web/src/terminal/ghostty/surface.ts+20
......@@ -942,6 +942,22 @@ export class GhosttyTerminalSurface {
942942 }, 150);
943943 }
944944
945 /**
946 * Releasing a panel, drawer, or split divider settles the size, so the PTY
947 * hears it now instead of after the debounce. Otherwise typing right after
948 * the release reaches a shell that still lays out the old width.
949 */
950 private readonly onWindowPointerRelease = () => {
951 if (this.disposed || this.resizeNotifyTimer === null) return;
952 // The drag hook writes the release position before this bubbles to window,
953 // so fit now rather than wait a frame for the observer.
954 this.fit();
955 if (this.resizeNotifyTimer === null) return;
956 window.clearTimeout(this.resizeNotifyTimer);
957 this.resizeNotifyTimer = null;
958 this.options.onResize(this.cols, this.rows);
959 };
960
945961 focus(): void {
946962 if (this.disposed || !this.visible) return;
947963 this.input.focus({ preventScroll: true });
......@@ -1843,6 +1859,8 @@ export class GhosttyTerminalSurface {
18431859 this.scrollbar.addEventListener("pointerup", this.onScrollbarPointerUp);
18441860 this.scrollbar.addEventListener("pointercancel", this.onScrollbarPointerUp);
18451861 this.scrollbar.addEventListener("keydown", this.onScrollbarKeyDown);
1862 window.addEventListener("pointerup", this.onWindowPointerRelease);
1863 window.addEventListener("pointercancel", this.onWindowPointerRelease);
18461864 }
18471865
18481866 private removeEvents(): void {
......@@ -1869,6 +1887,8 @@ export class GhosttyTerminalSurface {
18691887 this.scrollbar.removeEventListener("pointerup", this.onScrollbarPointerUp);
18701888 this.scrollbar.removeEventListener("pointercancel", this.onScrollbarPointerUp);
18711889 this.scrollbar.removeEventListener("keydown", this.onScrollbarKeyDown);
1890 window.removeEventListener("pointerup", this.onWindowPointerRelease);
1891 window.removeEventListener("pointercancel", this.onWindowPointerRelease);
18721892 }
18731893
18741894 private scrollViewport(deltaRows: number): void {