feat(ipad): a browser terminal for this machine, driven from an iPad - #29
Merged
Merged
Conversation
`npm run ipad:lan` prints a URL; Safari on the tablet opens a real shell - vim, top, tab completion, colours - on the computer pixelpets runs on. It deliberately does not do the thing that cannot be done: iPadOS sandboxes every app and exposes no API for cross-app control, so nothing running on the tablet can drive other iPad apps. Shortcuts is the only sanctioned path there, and only for apps publishing App Intents. This puts the shell where a shell is worth having and lets the iPad be the screen and the keyboard. Built for a tablet specifically: - Safari suspends a backgrounded tab, so the shell outlives its connection (--idle, default 120s) and the client counts rendered bytes to ask for exactly the gap on reconnect - missed output replayed, read output not repeated. - A key bar supplies the Esc/Tab/Ctrl/arrows the soft keyboard lacks, with a sticky Ctrl. Taps use pointerdown rather than click, because moving focus to a button on iPadOS dismisses the keyboard mid-command. - visualViewport drives the layout, since the soft keyboard covers the window rather than resizing it. Runs on plain node with nothing installed: output over SSE, input over POST, rather than hand-rolled RFC 6455 framing. `npm install` inside the tool adds a real pty and vendors xterm.js locally; without it the server drops to line mode and the page pulls xterm from a pinned CDN. The tool is its own package on purpose - node-pty is native and xterm is browser-side, so neither belongs in the Electron bundle. Loopback unless --lan. Every request needs a token, ten bad ones lock the caller out for a minute, and the Host header is checked so a rebound DNS name cannot probe the port. The page strips the token from the address bar on load and authenticates its own stylesheet and script with a SameSite=Strict cookie that the API itself refuses. Two bugs found while testing and fixed here: a session created but never streamed to held a live shell open forever, because only a disconnecting client armed the cleanup; and the vendored xterm files 401'd in a real browser because a <script src> cannot carry an auth header. Tests drive the reconnect-replay path, the reaper and the auth surface against a real shell over real HTTP, and pass with and without node-pty installed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaZc4CaHkaynMT3i5qoyLt
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The shell outlives a dropped connection by --idle seconds and no longer, which is right for a terminal and wrong for a six-hour training run: put the iPad down and two minutes later the reaper takes the shell, and the job with it. Verified both halves rather than asserting them - a bare background job is killed with its shell once the idle window passes, and the same job inside tmux is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaZc4CaHkaynMT3i5qoyLt
…app-access-m9izv8
JOhnsonKC201
marked this pull request as ready for review
August 28, 2026 03:08
…app-access-m9izv8 # Conflicts: # README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this change?
Adds
tools/ipad-terminal/— a terminal for this machine that you drive from an iPad.It prints a URL. Open it in Safari on the iPad and you get a real shell —
vim,top, tab completion, colours — on the computer pixelpets runs on. Share → Add to Home Screen gives it an icon and a full-screen window.The limitation this is built around
Nothing running on an iPad can control other iPad apps. iPadOS sandboxes every app, Apple exposes no API for cross-app control, and no terminal gets around that — Shortcuts is the only sanctioned path, and only for apps that publish App Intents. So this puts the shell on the computer, where a shell is worth having, and lets the iPad be the screen and the keyboard.
Built for a tablet specifically
--idle, default 120s), and the client counts the bytes it has rendered so a reconnect asks for exactly the gap — missed output replayed, already-read output not repeated.pointerdownrather than click, because moving focus to a button on iPadOS dismisses the keyboard mid-command.visualViewport, since the soft keyboard covers the window rather than resizing it.Shape of it
Runs on plain
nodewith nothing installed: output over SSE, input over POST, rather than hand-rolled RFC 6455 framing for something that shuttles a few hundred bytes per keypress.npm installinside the tool adds a real pty and vendors xterm.js locally; without it the server drops to line mode and the page pulls xterm from a pinned CDN, and the boot banner says which you're getting.It is its own package on purpose —
node-ptyis native and xterm is browser-side, so neither belongs in the Electron bundle. The rootpackage.jsongains only two scripts;package-lock.jsonis untouched.Security
Loopback unless
--lan. Every request needs a token, ten bad ones lock the caller out for a minute, and theHostheader is checked so a hostname resolving to this box can't probe the port. The page strips the token out of the address bar on load and authenticates its own stylesheet and script with aSameSite=Strictcookie that the API itself refuses. It's plain HTTP — fine on your own Wi-Fi, and the README says to use a TLS tunnel rather than a port forward for anything else.Two bugs found while testing, fixed here
<script src>can't carry an auth header. That's what the subresource cookie is for.Verification
Beyond the test suite, this was driven in Chromium at an iPad viewport: opened
vim, typed in insert mode, hit Esc from the key bar,:wq, andcatconfirmed the file;ttyreports/dev/pts/0andtput colsmatches the fitted terminal; colours and CJK render; no console errors. The offline path was checked too — with the CDN blocked the page shows the "runnpm install" message and a retry button rather than a blank screen.Checklist
npm testis green — 200 passing (193 existing + 7 new), with and withoutnode-ptyinstallednpx eslint src/ tests/is clean (npm run lintover the whole tree is clean too)src/cat-sprite.js: n/a — not touchedGenerated by Claude Code