13 KiB
xterm-zerolag-input
0.3.0
Minor Changes
-
55bff4a: Zero-lag predictive echo for Codex sessions (mosh-style write-through prediction).Codex's per-keystroke composer forced 1.12.2 to disable the local-echo overlay (issues #218/#219/#220/#222), leaving Codex typing at full round-trip latency on remote links. This release adds a second echo mode instead of re-enabling the first: every keystroke still goes to the PTY exactly as before (byte-identical wire behavior, pinned by vm-level and end-to-end trace-equality tests), while the new
PredictiveEchoAddoninxterm-zerolag-input0.2.0 paints the predicted glyph at the predicted cell. When the real echo lands, the prediction is confirmed and its span removed (an invisible swap); mispredictions self-heal via a two-pass mismatch cascade and a TTL.- Reconciliation reads the parsed terminal buffer, never the raw stream: full-line redraws, ECH gap painting and tmux's in-place deltas all converge to the same cells. Confirmation requires the cell match PLUS a cursor advance, so placeholder glyphs and identical repaints never false-confirm; blank cells are neutral (codex clears its placeholder on the first echo).
- Predictions paint only while the cursor sits on the measured Codex composer row (
/^› /, codex-cli 0.147): trust/approval modals and wrapped continuation rows get no ghosts, deliberately falling back to real echo. - Ships as a SEPARATE
vendor/xterm-predictive-echo.jsbundle: the existing zerolag bundle is byte-identical (sha256-verified), and a missing or broken bundle degrades Codex to exact 1.12.2 behavior. The per-devicelocalEchoEnabledtoggle is the kill switch. - Claude/Gemini/OpenCode/Antigravity keep buffer mode untouched; shell stays off.
- A post-build adversarial review added the anchor-hold rule: after an unpredicted wire edit (backspace into echoed text, cleared input, IME text commits) new predictions hold until the next parsed write, so a stale displayed cursor can never mis-anchor a run.
- Tests: 55 new package tests including replay suites driven by fixtures recorded from a real codex TUI through the production tmux+strip pipeline (
scripts/dev/record-codex-frames.mjs) and a 500-iteration seeded fuzz; new vm policy/wire-neutrality suites; a 10-scenario Playwright E2E against real codex covering the #218/#219/#220/#222 retests, byte-identity, and a simulated 300ms-RTT run. The package test suite now runs in CI.
0.2.0
Minor Changes
- New addon:
PredictiveEchoAddon, mosh-style write-through prediction. The second echo mode for per-keystroke TUIs (OpenAI Codex's composer, live pickers) that buffer-until-Enter starves. Every keystroke is sent by the consumer immediately and unchanged; the addon paints the predicted glyph at the predicted cell and reconciles against the PARSED terminal buffer: confirmation requires the cell match plus a cursor advance past the record, foreign non-blank content on two consecutive passes cascades a drop, blank cells are neutral, a TTL bounds everything, and scroll/resize/sustained cursor moves clear the run. Visual-only by construction; it cannot gate, delay or rewrite input.- Anchor-hold rule: after an unpredicted wire edit (backspace into echoed text, cleared input, an IME text commit) new predictions hold until the next parsed write, so a stale displayed cursor can never mis-anchor a run (worst case: exactly one unpredicted keystroke).
- New exports:
PredictiveEchoAddon,PredictiveEchoOptions,PredictionState, plus the long-intendedcharCellWidth/stringCellWidthhelpers. XtermTerminaltype gains OPTIONAL members (buffer.active.cursorX/cursorY,getLine().getCell?,onWriteParsed?,onResize?). Additive only: existing consumers and mocks are unaffected.- IIFE build exposes
window.PredictiveEchoAddonand a self-activatingwindow.PredictiveEchoOverlay, alongside the unchangedZerolagInputAddon/LocalEchoOverlayglobals. - Tests: 52 new (30 addon-law specs, renderer geometry, 6 replay suites driven by fixtures recorded from real codex 0.147 through tmux + the production strip, and a 500-iteration seeded fuzz with per-op invariants).
@xterm/headlessas a devDependency; runtime dependencies remain zero.
0.1.8
Patch Changes
-
Fixed: sessions failed to start on macOS with
Error: posix_spawnp failed.(issues #6 and #204)node-pty@1.1.0publishes its macOS prebuilt helper asprebuilds/darwin-<arch>/spawn-helperwith mode 0644, i.e. no execute bit. macOS launches every PTY through that helper, so a stock install failed on every session start. The bug is macOS-only:spawn-helperis a mac-only gyp target and node-pty ships no Linux prebuild, so Linux always compiles a correctly-permissioned helper from source.The previous fix chmodded only
build/Release/spawn-helper, which on macOS does not exist (the prebuild is used, so node-gyp never runs), and it derived that path fromrequire.resolve('node-pty'), landing on<pkg>/lib/build/Release/.... It was a no-op on every platform.- New
scripts/fix-node-pty.mjs(alsonpm run fix:node-pty) chmods everyspawn-helperit finds, inbuild/Release,build/Debugand eachprebuilds/*/, then verifies the result by actually opening a PTY. Arequire()alone passes on a broken install, because the helper is only touched at spawn time. postinstallno longer force-rebuilds node-pty from source on Node 22+. That step needed Xcode command line tools, cost 30-120s on every install, and deleted theprebuilds/tree before compiling, so a Mac without a compiler was left with no working binary at all. A rebuild now happens only when the chmod plus spawn probe still fails, and the prebuilds tree is backed up and restored around it.- New
spawnPtyWithHelperRepair()(src/utils/node-pty-repair.ts) wraps everypty.spawn()insession.ts, so an install that is already broken repairs itself on the first failed spawn and retries in-process instead of showing a dead session. Unrelated spawn errors are rethrown untouched; a second failure carries thenpm run fix:node-ptyhint. scripts/fix-node-pty.mjsis now in the publishedfileslist, so global npm installs get the repair too.- Direct-PTY Claude spawns use the resolved absolute binary path (new
getClaudeBinaryPath()) instead of the bare nameclaude, so a CLI installed outside the server's PATH still launches.
Verified end to end on macOS 26.4 arm64: a stock
npm ireproducesposix_spawnp failed., and after the fix the same install spawns a PTY successfully with the prebuilds preserved.Added: phone home screen (session overview)
Under 430px the "C" logo now opens a session overview (current sessions, past sessions, spaces) instead of the welcome overlay: on a small screen "which session needs me" beats "how do I start one". Rows resume a session in place, and "New session here" goes through the normal quick-start path so remote and Docker cases keep their routing. Per-device setting
mobileOverviewEnabled(phones only, default ON) in App Settings. Tablet and desktop are unchanged.Added: guided Tailscale setup in
install.shThe network-access prompt is now 3-way: Tailscale, LAN, or local-only. The Tailscale path binds loopback and walks through installing Tailscale, logging in, the operator grant, the tailnet HTTPS-certificates toggle, and
tailscale serve --bg <port>, then verifies the result end to end with curl. That gives HTTPS on a real certificate with no app password and no0.0.0.0bind, which is also what PWA install and web push need.install.sh tailscaleretrofits it onto an existing install, andCODEMAN_TAILSCALE=1presets the choice. Serve state is detected fromtailscale serve status --json; the installer never runstailscale serve resetand never touches serve mappings other than 443 to Codeman's port. README anddocs/security-architecture.mdupdated to match.Docs: replaced a real tailnet hostname with placeholders in
docs/web-tabs-fixes-plan.md.xterm-zerolag-input: npm description and keywords only, no code change.
- New
0.1.7
Patch Changes
- Fix a latent bug where a partial settings PUT silently reset live service state, and trim the
xterm-zerolag-inputREADME callout.PUT /api/settingsno longer resets watchers on a partial body. The threetoggleServicecalls (subagent watcher, workflow-run watcher, image watcher) read the raw request body with??defaults, so every key a caller omitted was treated as "apply the default". A body of just{statusLineTelemetry:true}would START the subagent watcher and STOP the workflow and image watchers, undoing the persisted config. They now resolve frommerged(persisted settings + incoming), the same convention thetmuxHistoryLimitbranch in that handler already used, so any PUT reconciles services to the effective stored state. Nothing triggered this in practice because every shipped client sends a full settings payload rebuilt from the DOM, but it was a trap for the next partial-update caller.- Regression test:
test/routes/system-routes-settings-partial-put.test.ts(4 cases) pins both directions, omitted keys preserve state and explicit keys still take effect. Verified to fail against the pre-fix handler. - CLAUDE.md records the rule under "Adding Features → App setting": anything acting on a setting in that handler must resolve from
merged, never the request body. xterm-zerolag-inputREADME: removed the links line (getcodeman.com / install one-liner / star link) from the Codeman callout above the demo GIF. The callout keeps its links in the heading and body.
0.1.6
Patch Changes
- Plan-usage chip now defaults ON on desktop, plus the reworked
xterm-zerolag-inputREADME.- Plan-usage chip defaults ON (desktop). The
showPlanUsageLimitschip (live 5-hour and weekly plan usage from the Claude statusline) used to be opt-in and default OFF, so most users never saw it. Desktop now defaults ON; handhelds still default OFF so the phone header stays minimal and themobile-header-buttons-policyguard keeps passing. Devices with an explicitly stored preference keep whatever they chose, so nobody's OFF gets overridden. - One resolver behind the chip. Added
planUsageChipEnabled()in settings-ui.js and routed all three call sites through it: the App Settings checkbox, the chip's visibility, and the create-timestatusLineTelemetryflag in session-ui.js. Those three had independent?? false/=== truedefaults, and a chip revealed without the telemetry flag renders—forever, so a default flip on one site alone would have shipped a permanently empty chip. - Cron button comment corrected. The App Settings comment claimed "Cron button defaults ON" while the code, the template (
btn-cron--hidden) and the CSS all default it OFF. Verified against a fresh browser profile: the button is hidden and its checkbox unchecked out of the box. Comment now matches, and states why the two halves stay consistent. - Docs. CLAUDE.md,
docs/architecture-invariants.mdanddocs/usage-limits-display-plan.mdupdated for the new default and the single-resolver rule; the stalestyles.csscomment claiming the server strips the chip's hidden class at render was corrected (display is per-device, so the client reveals it). xterm-zerolag-inputREADME rework (0.1.5 shipped the content; this republishes with the graphic and promo changes): replaced the misaligned 8-line keystroke-flow diagram with a two-line stock-vs-zerolag contrast, added a Codeman callout above the demo GIF with links to getcodeman.com and the repo, and rewrote the Origin section so it argues the extraction story instead of repeating the promo.
- Plan-usage chip defaults ON (desktop). The
0.1.5
Patch Changes
-
Rewrite the
xterm-zerolag-inputpackage README as a value-first document and correct the drift that had accumulated against the source.- Added the side-by-side phone demo GIF (
docs/images/zerolag-demo-20260728.gif) as the hero image, referenced by absolute raw URL so it renders on npmjs.com as well as GitHub. The two-phone comparison shows 0ms local echo next to a 600ms-2.7s server echo on the same session. - New "Why this one" comparison table, an explicit list of target use cases (SSH web clients, cloud IDEs, mobile terminals, container consoles), and a bundle-size badge (6.1 kB gzipped, measured from the ESM build).
- Corrected the test-count badge from 78 to the actual 175 tests across 5 files, in both the package README and the Published Packages section of the root README.
- Removed the stale "Unicode/emoji rendered at single-cell width" limitation. CJK, fullwidth forms and emoji have had double-width rendering and visual-column positioning since the wide-character fix; the honest remaining caveat (per-code-point width summing over-counts ZWJ grapheme clusters) replaces it.
- Documented the previously undocumented public
setPrompt()method for switching prompt strategies at runtime, and the new "Wide characters (CJK, emoji)" integration section covering the optionalUnicode11Addonpath and the built-in range-table fallback. - Documented
backgroundColor: 'transparent', corrected theforegroundColordefault, and updated the grid-alignment math to reflect visual-column positioning rather than character index.
No source changes, docs only.
- Added the side-by-side phone demo GIF (
0.1.4
Patch Changes
- Initial changelog entry for changesets-based versioning