`smolvm 0.8.0 machine exec -t` allocates an in-VM PTY but never
forwards the host terminal's window size — the PTY starts at
`0 0` and host resizes (tmux pane resize, terminal window
resize) go unnoticed, so the claude TUI inside a smolmachines
bottle renders for whatever tiny box it last saw and ignores
operator resizes. `docker exec -it` propagates window-size
changes automatically; smolvm doesn't.
Workaround: a small Python wrapper
(`backend/smolmachines/pty_resize.py`) that interposes between
the operator's terminal and `smolvm machine exec`. It spawns
smolvm as a child, traps host SIGWINCH, and on every resize
(plus once at startup) runs a side-channel
`smolvm machine exec --name <M> -- sh -c 'for f in /dev/pts/*;
do stty -F $f cols X rows Y; done'`. The kernel delivers
SIGWINCH to the in-VM foreground process group when the slave
PTY's size changes, so claude picks up the new dimensions
without extra signalling.
`SmolmachinesBottle.claude_argv` prepends
`[sys.executable, -m, claude_bottle.backend.smolmachines.
pty_resize, <machine>, --, ...]` to the existing smolvm argv
in TTY mode. Non-TTY mode (provisioning shell-outs) skips the
wrapper — no PTY to resize.
The wrapper survives the dashboard's
`_build_resume_argv_with_fallback` shell-wrap because the
split-at-`claude` token still finds the right position — the
wrapper's prefix wraps the entire smolvm-exec framing.
Tests:
- `test_smolmachines_pty_resize.py` (new): argv parsing, the
side-channel command shape (cols/rows / for-loop over
/dev/pts/*), and `_read_winsize`'s fallback across
stdin/stdout/stderr including the smolvm-allocated-PTY-
reports-`0 0` ironic case.
- `test_smolmachines_bottle.py`: updated TTY-mode assertions
to unwrap the pty_resize prefix; added `TestClaudeArgvNoTTY`
to lock the non-TTY skip.
636 unit tests pass.
Removable when smolvm grows native SIGWINCH forwarding.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>