Independent post-build review found three gaps, all one family: input that changes the composer without a prediction leaves the DISPLAYED cursor stale for one RTT, and anchoring a new run on it painted ghosts one cell off (blank-neutral, so they lived out the full TTL: "tehh" on backspace-then-retype, exactly on the links the feature targets). Fix: the addon now HOLDS new predictions after any such edit (backspace with nothing outstanding = deleting echoed text, clearPredictions, and now also IME/plain-paste 'text' commits, which the hook clears like 'clear') until the next PARSED write releases the hold. The inline predictChar reconcile deliberately does not count: only the emitter pass or the public reconcile() is the display-caught-up contract. Worst case is exactly one unpredicted keystroke, whose own echo releases the hold. Also patched the one bypass path the PR had missed: _handleCjkInput now clears predictions like insertTerminalText and the other bypass sends. Package suite 230, vm gating 85, E2E 10/10 all green after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 KiB
xterm-zerolag-input
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