From 2cde3f770bbf7a6ed9d52a6b1f64a471c0303a5a Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Oct 2026 04:34:36 -0700 Subject: [PATCH] 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 --- apps/web/src/terminal/ghostty/surface.test.ts | 24 +++++++++++++++++++ apps/web/src/terminal/ghostty/surface.ts | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/apps/web/src/terminal/ghostty/surface.test.ts b/apps/web/src/terminal/ghostty/surface.test.ts index b235132494558f56a3780825cb4b31ddbc537f6e..82ec9dfc690a1397c7e59a5eeb4b060cdfdea93e 100644 --- a/apps/web/src/terminal/ghostty/surface.test.ts +++ b/apps/web/src/terminal/ghostty/surface.test.ts @@ -540,6 +540,30 @@ describe("GhosttyTerminalSurface visibility", () => { expect(resizePty).toHaveBeenCalledOnce(); }); + it("sends the settled grid when a resize drag is released", async () => { + const harness = createHarness(); + const onResize = vi.fn<(cols: number, rows: number) => void>(); + const surface = await harness.create({ onResize }); + vi.advanceTimersByTime(150); + onResize.mockClear(); + + // Mid-drag, the grid reflows locally while the PTY waits for the debounce. + harness.mount.clientWidth = 120; + harness.resize(); + expect(surface.cols).toBe(14); + expect(onResize).not.toHaveBeenCalled(); + + // The release writes the final width before the observer reports it. + harness.mount.clientWidth = 96; + window.dispatchEvent(new Event("pointerup")); + expect(onResize).toHaveBeenCalledExactlyOnceWith(11, 6); + + harness.resize(); + vi.advanceTimersByTime(150); + window.dispatchEvent(new Event("pointerup")); + expect(onResize).toHaveBeenCalledOnce(); + }); + it("replays the current grid on reveal when a hidden host becomes ready", async () => { const harness = createHarness(); const onResize = vi.fn<(cols: number, rows: number) => void>(); diff --git a/apps/web/src/terminal/ghostty/surface.ts b/apps/web/src/terminal/ghostty/surface.ts index 76908556f0d14d1892b60cf569fb668d2c9f912a..39ae0ebb024ae2c5ef032359043bb0d34c27a77c 100644 --- a/apps/web/src/terminal/ghostty/surface.ts +++ b/apps/web/src/terminal/ghostty/surface.ts @@ -942,6 +942,22 @@ export class GhosttyTerminalSurface { }, 150); } + /** + * Releasing a panel, drawer, or split divider settles the size, so the PTY + * hears it now instead of after the debounce. Otherwise typing right after + * the release reaches a shell that still lays out the old width. + */ + private readonly onWindowPointerRelease = () => { + if (this.disposed || this.resizeNotifyTimer === null) return; + // The drag hook writes the release position before this bubbles to window, + // so fit now rather than wait a frame for the observer. + this.fit(); + if (this.resizeNotifyTimer === null) return; + window.clearTimeout(this.resizeNotifyTimer); + this.resizeNotifyTimer = null; + this.options.onResize(this.cols, this.rows); + }; + focus(): void { if (this.disposed || !this.visible) return; this.input.focus({ preventScroll: true }); @@ -1843,6 +1859,8 @@ export class GhosttyTerminalSurface { this.scrollbar.addEventListener("pointerup", this.onScrollbarPointerUp); this.scrollbar.addEventListener("pointercancel", this.onScrollbarPointerUp); this.scrollbar.addEventListener("keydown", this.onScrollbarKeyDown); + window.addEventListener("pointerup", this.onWindowPointerRelease); + window.addEventListener("pointercancel", this.onWindowPointerRelease); } private removeEvents(): void { @@ -1869,6 +1887,8 @@ export class GhosttyTerminalSurface { this.scrollbar.removeEventListener("pointerup", this.onScrollbarPointerUp); this.scrollbar.removeEventListener("pointercancel", this.onScrollbarPointerUp); this.scrollbar.removeEventListener("keydown", this.onScrollbarKeyDown); + window.removeEventListener("pointerup", this.onWindowPointerRelease); + window.removeEventListener("pointercancel", this.onWindowPointerRelease); } private scrollViewport(deltaRows: number): void { -- 2.54.0