DEV Community

Artemii Amelin
Artemii Amelin

Posted on

A Phone Joining a Shared Terminal Now Gets an 80x40 PTY. An Older CLI Still Gets 80x24.

shell.online 0.11.2 went out on September 10. Most of the release list is web app work: several assignees per session, paginated audit history, self-hosting docs. The change worth a post is smaller and sits in the terminal path. When a phone opens a shared session, the process on the host machine is now resized to 80 columns by 40 rows instead of 80 by 24, and the negotiation that gets there is built so an older CLI never receives a size it does not understand.

One process, one grid

A shell.online session is one PTY on one machine, watched by up to 16 browsers. A PTY has exactly one size. If one viewer is on a 27-inch monitor and another is on a phone, the process cannot be 200 columns wide for the first and 60 for the second. What we gave up is the viewer's ability to resize the process at all.

That rule lives in cmd/shell/session_unix.go. A resize frame from a browser is received and deliberately ignored, with a comment explaining that the shared PTY keeps one canonical grid so simultaneous viewers cannot deform each other's TUI. The default grid is 120x36. Each browser then fits that fixed grid into whatever pane it has: the font size comes from the pane width, and the rows are spread down the height with whatever leading is left over. Nothing a viewer does to its own window reaches the process.

The one message that does reach the process is a terminal_size control message from the relay, and the CLI only applies it if the size is on a short allowlist. Anything else is dropped. The browser has the same allowlist in app/src/terminal/connection.ts, with a comment on what it prevents: drawing a size the CLI would have rejected "is how a viewer ends up drawing 94 columns of a 120-column process".

Why a phone changes the grid for everyone

Before this release the allowlist had two entries: 120x36, and 80x24 while a phone was connected. Eighty columns exists because the font size is derived from width. At 120 columns on a 390-pixel screen the type is unreadable. At 80 it is small but usable. The trouble was the row count: 24 rows on a portrait screen left a large part of a tall pane unused, because rows fill the height and there were not enough of them.

PR #100 adds a third grid, 80x40. The relay's choice is a small pure function in shared/terminal-grid.ts. If any connected viewer is a phone or reports a portrait layout, use the portrait grid, otherwise 120x36. The portrait grid is 80x40 only if the host CLI has said it can handle it, and 80x24 otherwise.

Because the PTY has one size, this is a shared decision. A phone joining a session takes the desktop viewers to 80x40 as well, and their panes redraw the narrower grid at a larger font. When the portrait viewers leave, the relay puts the session back to 120x36.

Three signals, none of them terminal bytes

The relay is a Cloudflare Durable Object in worker/index.ts. It needs three facts to pick a grid and learns all of them from connection metadata.

Whether the viewer is a phone comes from the User-Agent and the Sec-CH-UA-Mobile client hint. iPhone, iPod, or Android with "mobile" in the string count as mobile. iPad, tablet, and other Android strings count as tablet, and tablets keep the desktop grid.

Whether the viewer is portrait comes from the browser itself. It puts layout=portrait or layout=landscape on the WebSocket URL when it connects, and sends a viewer_layout message if the orientation flips later. A desktop window taller than it is wide triggers the tall grid too. While the on-screen keyboard is open the visual viewport is compressed, so the client keeps its pre-keyboard answer instead of deciding the phone has turned sideways and resizing the PTY under the person typing.

The CLI announces that it can handle 80x40 with one header, X-Shell-Terminal-Grid: 80x40, sent when it dials the relay in internal/relay/relay.go. The relay stores that per host socket. A 0.11.1 CLI sends no header, so a phone on its session gets 80x24, the size that CLI already accepts.

None of this touches terminal content. In an encrypted session the relay could not read a resize frame if it wanted to. The worker only checks that an encrypted resize frame is 34 bytes long, closes the socket if it is not, and otherwise leaves it alone. The grid is decided from a header, a query parameter, and one small JSON message, which is the same boundary the end-to-end encryption already draws around the session.

The deployment order falls out of the header. The PR notes the worker can ship before the CLI release, because an old CLI never advertises and so never sees 80x40. A new CLI against an old worker has its header ignored and gets 80x24 as before.

What the phone actually gets

The PR's validation ran a real CLI against a local worker at 390x844: the session negotiated 80x40, the terminal used 640 of 721 available pixels, and there was no horizontal overflow. Rotating the phone restored 120x36. The typography module now uses a line height of 1.8 when the viewport is more than 15 percent taller than it is wide, and 1.18 otherwise, so the 40 rows stay readable instead of cramped.

The same PR adds two-finger pinch zoom, from 50 to 150 percent. Zoom is local to the viewer and saved when the fingers lift. It never changes the grid, and there is a test asserting exactly that: personal zoom without a change in terminal dimensions.

The reason this matters more than a mobile polish item is what the sessions contain. A session started from the web app is often a coding agent, Claude Code, Codex, or Hermes, working through a task on a linked machine. A phone is where people check on that work. shell.online is built by the team behind Pilot Protocol, and a running agent needs a place where a person can see its terminal, on whatever screen they have, without the agent's process being resized by whoever happens to look. Every claim above is readable in the shell.online source; the grid function and the CLI allowlist are a few lines each.

Top comments (0)