[Bug]: Terminal keeps a stale width after dragging the chat/terminal divider, so full-width output wraps and spinners garble#31013

opened4d agobySsands21

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

Intermittent. It happened during one session of drags while a spinner was running. Later attempts didn't reproduce it, including drags while find / was streaming output.

  1. Open the right-panel terminal in a thread (macOS desktop app, zsh).

  2. Run a command with a progress spinner, for example curl -fsSL https://vite.plus | bash.

  3. While it runs, drag the divider between the chat and the terminal to make the terminal narrower, then let go.

  4. After the command finishes, wait a few seconds without touching anything and run:

    sh
    1stty size; printf '%*s|\n' "$(( $(tput cols) - 1 ))" '' | tr ' ' '='

    This prints the size the PTY reports, then a line exactly that many columns wide.

Expected behavior

stty size matches the width the terminal draws, so the ====| line ends exactly at the right edge. Spinners redraw in place.

Actual behavior

  • While the spinner ran, each redraw landed further along the row. The output became a staircase of repeated Installing dependencies... 1s lines, which stayed in the scrollback (screenshot 1).
  • Afterwards, stty size printed 51 110 while the panel showed about 86 columns. The full-width line wrapped by about 24 characters (screenshot 2). The PTY kept the old width even though nothing had been resized for a while.
  • Dragging the divider again fixed it: stty size then printed 51 84, matching the panel.
  • The same command in macOS Terminal.app, resized the same way while it ran, renders cleanly.

From reading the code (not confirmed): GhosttyTerminalSurface.fit() in apps/web/src/terminal/ghostty/surface.ts stores the new size and sends it once through notifyResize (150 ms debounce, or on pointer release). It only sends again when the measured size changes. If that one message is lost on the way, nothing retries it. Places where it could be lost:

  • resizeTerminal in ThreadTerminalDrawer.tsx returns early when writableSessionGeneration(...) is null.
  • resizeLocked in apps/server/src/terminal/Manager.ts ignores a resize from an unregistered viewer while another viewer has a size.
  • The RPC fails.

Related but different: #25419, where the PTY does get the new size and the shell's redraw disagrees with Ghostty's reflow. Here the PTY never gets the new size.

Impact

Minor bug or occasional failure

Version or commit

T3 Code (Beta) 0.1.7

Environment

macOS 27.2, desktop app, local environment, zsh. The thread was only open in the desktop window.

Logs or stack traces

shell
1

Screenshots, recordings, or supporting files

Screenshot 2026-11-04 at 22.24.02.png Screenshot 2026-11-04 at 22.28.34.png

Workaround

Drag the divider again. Any new width sends a fresh size to the PTY.

Tried this on current main (9e85538535); nothing in this resize path has changed since v0.1.7. It did not reproduce, so we need a bit more to narrow it down.

What I ran, web client against a dev server, right-panel terminal, bash:

  1. Your steps: a spinner redrawing one line every 100 ms, the divider dragged narrower mid-run (67 → 44 cols), then your stty size check. Result: 41 44, the ====| line ended at the edge, and the spinner redrew in place.
  2. Same setup, but the WebSocket was closed at the moment the resize request went out, to simulate a lost resize. The client resent the size about 3 s after reconnecting. Result: 41 68, matching the panel.
  3. A 10 ms spinner with three quick drags in both directions. Result: 41 83, matching the panel.

The surface does resend its size after a reconnect, so a single dropped request does not leave the PTY stale on its own. Your 51 110 while the panel showed about 86 columns means the server kept a size that a later resize did not replace. Three things would help us tell which path that was:

  • Was any other client connected to that desktop app at the time with the same terminal open, such as app.t3.codes, a browser on the local network, or the mobile app? Even one in a background tab counts. The server sizes a shared terminal to its smallest attached viewer.
  • Had the desktop app just woken from sleep or shown a reconnecting state shortly before the drag?
  • The server log from around 22:20–22:30 on Nov 4, in ~/.t3/userdata/logs, especially any lines mentioning terminal.resize or TerminalResizeError.
Synced from GitHub

Thanks for checking!

  • Other clients: None. Only the desktop app was connected, with the terminal open in that one window.
  • Sleep/reconnect: The desktop trace has no resize, reconnect or suspend entries around then. But the renderer log (renderer-history.ndjson), which samples every 30 s, has a five-minute gap from 22:19:26 to 22:24:26 IST, between the spinner screenshot (about 22:16) and the stty size check (22:28). There's another from 22:06:26 to 22:11:26. That looks like the window was throttled in the background or the Mac was idle, which might relate to #21235.
  • Server log: Unfortunately it has rotated away. The oldest server.trace.ndjson starts at 22:37:26, because a find / I ran in the terminal at about 22:37 produced about 14,000 terminal.drainProcessEvents/terminal.queuePersist spans in five seconds, which filled all ten rotated files. If it happens again, I'll grab the log right away.

Thanks for the details. Ruling out other clients narrows it a lot.

Tried again on current main (95e53a54db), with the web client against a dev server, the right-panel terminal, and bash. The probe was your stty size / ====| check.

  1. Your steps: a spinner redrawing every 100 ms, with the divider dragged during the run (539 → 659 → 459 px, including a pause mid-drag). Result: 41 57, which matches the 459 px panel. The spinner redrew in place.
  2. Stalled connection, to match the background gaps in your renderer log: the client's outgoing frames were dropped mid-spinner, the divider was dragged narrower during the stall (459 → 359 px), and the client was left to time out and reconnect. Result: 41 44, which matches 359 px.
  3. Output flood plus drags: seq 1 4000000 streamed while a spinner ran and the divider was dragged wider, then narrower (→ 489 px). Result: 41 61, which matches.

In each case the PTY ended at the panel's width. The client resends its size whenever it reattaches, so a resize lost to a reconnect recovers by itself. What we haven't been able to exercise is a resize request that fails while the connection stays up. We also can't reach the macOS desktop app's background throttling from here.

If it happens again, these would settle it:

  • Before dragging the divider to fix it, copy ~/.t3/userdata/logs/server.trace.ndjson and desktop.trace.ndjson, and do it before running anything with heavy output. We need the ws.rpc.terminal.resize / terminal.resize spans from the drag onward: whether one arrived after you let go, and whether any has a Failure exit.
  • Whether the window had just come back from the background or from idle when you dragged.
Synced from GitHub

Note

🤖 Claude Opus 5.5 responding on behalf of Muzammil-Bit

Not reproducible here either, but one path still fits every symptom: a resize request that fails while the connection stays up is never retried.

Reproduction attempt (macOS 27.2, current main, web client against a dev server, right-panel terminal): a spinner redrawing every 100 ms, with the divider dragged narrower mid-run. Result: 41 67 before, 41 44 after, and the ====| line ended at the edge. The spinner redrew in place.

Ruled out by reading the code:

  • Reconnects: every new attach snapshot starts a new output generation, and the drawer resends its size on each one.
  • A leftover viewer holding the old width: the server takes the smallest viewer grid, so a stale viewer can only make the PTY narrower, never keep it at 110.
  • A stuck request in the client's latest resize lane: that would also block later resizes, but dragging again fixed it.
  • Renderer timer throttling: the main desktop window sets backgroundThrottling: false. The five-minute gaps in renderer-history.ndjson come from main-process sampling, so they point to system sleep or the whole app being suspended, not the renderer.

The remaining path (code reading, not observed):

  1. The server's resize calls process.resize inside Effect.try (Manager.ts#L259). node-pty throws when ioctl(TIOCSWINSZ) fails, so the RPC fails with TerminalResizeError and session.cols stays at the old width.
  2. The client runs resize with reportFailure: false (ThreadTerminalDrawer.tsx#L585), so the failure is silent.
  3. The surface has already recorded the new grid as sent, and only notifies again when the measured size changes (surface.ts#L905).

That gives a PTY stuck at the old width with no time limit, fixed by the next drag to a new width, which is what was reported.

If it happens again, the useful evidence is a terminal.resize span with a Failure exit, or a TerminalResizeError, in server.trace.ndjson from the drag onward. If one shows up, the fix is small: resend the size once after a failed resize.