main.rs was the only source file without tests, and it held three decisions
with real failure modes:
- Which login a connect goes out with. The choice was a closure body inside
the probe worker; it is now choose_login, tested for every row: typed wins,
then the Keychain, then the local Codeman's own login, which is used only
when no saved password was expected and only for the same username.
- What the Keychain keeps afterwards (update_keychain): saved or deleted by
Remember, never a copy of the local server's login, never a password the
server wants changed, and the old user's item deleted before the new save.
The in-memory Keychain store moves out of credentials' test module so other
test modules can use it.
- When a commit in the main window means the server page is loaded.
The connect page had only substring checks in the repository; the checks that
drove it lived in a scratch script. They are now
test/desktop-connect-page.browser.test.ts: Chromium, the page's real CSP, a
stubbed native side, five ordered walks through every state of both tabs
(first run, logins, failures, redirects, Start, the install wait, the SSH
install, layout, double clicks, keys, a starting server, a WebKit too old).
Listed in BROWSER_TEST_GLOBS, so the nightly browser suite runs it and
`npm test` does not.
Also pins the install one-liner the page shows to the one Copy copies.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Found by the three-part review of this branch. Each was checked against the
source first; every decision that can run without a window has a unit test.
This Mac:
- A Codeman that runs but listens nowhere yet (the first seconds after login,
a restart) was reported as "already running ... it listens at process N,
which this app cannot open", and Start was refused for it. It is now its own
state: the tab says "starting", looks again every second, and opens Codeman
once it is up when a connect to this Mac had just been refused. The state
table is a pure function with a test (setup::local_state).
- A server the app starts runs in the home folder, like one started by launchd
or in a terminal. It ran in the app's own directory (`/` after a Finder
launch), which Codeman then used as the default for sessions.
- Server detection accepts any entry word that `web` follows, so a dev server
under a folder named codeman is seen and never gets a second server beside
it.
- A login shell is polled from 5 ms up instead of every 100 ms, and the port
after a launchd start every 100 ms instead of 500: both were pure waiting.
- The last line of stdout is no longer glued to the first line of stderr.
Addresses and policy:
- A tailnet name typed with a port gets http://, not https://. With a port it
is Codeman's own listener; https there ended in a TLS error with no hint.
- within_base refuses `..%2F` and the other encoded ways out of the base path,
so a sibling app behind a decoding proxy never sees the login.
- A status answer whose body stalls is a timeout, not "something else answers
here".
- The folder pop-out pattern matches the two routes the server has.
- A malformed `window` in desktop.json no longer costs the server list.
Windows:
- Pop-outs step off the window in use. tao centres a window with no position,
so every pop-out opened exactly on top of the one before.
- A window opened by a script no longer brings the app to the front: wry
activated the application for it, whatever the window itself was told.
- The first-run window sits between the menu bar and the Dock. Centred in the
whole display, its bottom was under the Dock on a 900 pt high one.
- The window frame is saved once after a move or resize ends, not once a
second during the drag.
Also makes the connect page independent of TextEncoder and scrollIntoView, so
it runs in jsdom as well as in a web view.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Layout:
- The header no longer moves. The page was centred with align-items, so every
change of the card's height moved everything (up to 150px between states),
and a page taller than its window lost its top above the scroll origin,
where no scrolling reaches it. The top margin is now fixed, the card grows
downward and has a minimum height, and the page scrolls from its top. A
short window gets a compact header.
- The status line sits right under the action it reports on, above the
"not installed there yet?" section, takes no room while empty, and is
scrolled into view in a small window. It was below the fold there.
- Looking at this Mac again keeps what was found on screen (the pane used to
collapse to "Looking..." and grow back).
Feedback and keys:
- Buttons answer hover and press, every control shows keyboard focus, the
primary button and the footer meet WCAG AA contrast, "Opening Codeman" keeps
its spinner while the server page loads.
- Return opens or starts the Codeman on this Mac, Esc cancels a connect.
Install always takes a click.
- The This Mac result is a live region, Forget buttons are named after their
server, a long unbroken token wraps inside the card, the status text can be
selected.
Correctness:
- The second click of a double click acts only on the control that took the
first. It could connect to the server that slid under the pointer after a
Forget, or flip "Connect automatically".
- An address, username or password too long for the app is refused with a
reason. The app drops such a message without an answer, which left the page
on "Connecting..." (the limits are pinned to policy.rs by a test).
- A connect answer goes to the tab that asked, a failed SSH install to its
tab, and the login block no longer claims a saved password for another
server.
- An automatic connect to the Codeman on this Mac no longer looks at this Mac
first; a failed connect, Cancel or an error asks for that look.
- A login over plain http:// to another machine says that it is unencrypted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The findings of the 2026-11-05 review of this branch, each with a unit test
where the code can be tested without a window:
- A window whose web content process ended (a WebKit crash, or macOS taking
the process back under memory pressure) stayed blank: wry answers WebKit's
notice itself, which turns WebKit's own reload off. The window now loads its
page again. A second end within 30 seconds sends the main window back to the
connect page and closes a pop-out, so it cannot loop (policy::recovery).
- A JavaScript dialog from a frame nothing names (about:srcdoc, a data: or
blob: URL under an opaque origin) showed the page's text as its bold first
line. The first line is now always the app's own, the "embedded" flag is set
when the origin names no host (it was the wrong way round), and every
sub-frame counts as embedded, a trusted web tab on Codeman's origin included.
- The connect page lost the text of an error that needs the login fields: the
tab switch cleared the status it had just set.
- Start for http://localhost:PORT missed the LaunchAgent (the compare was
exact against 127.0.0.1) and ran a second, unsupervised server on its port.
- The path picker's preview Open link (api/filesystem/preview) did nothing: it
was the one page Codeman opens in a window that the allowlist lacked.
- The port of a running install was lost whenever the Terminal script that
wraps the installer came first in ps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda,
objc2) that opens Codeman in its own window. The universal app is about 5 MB
(3.2 MB as a DMG) and runs on macOS 11.3 or later.
- Connect screen with two tabs. This Mac finds the local Codeman and offers
Open, Start or Install. Start uses launchd for a LaunchAgent install and
codeman web -d otherwise. It never starts a second server while one runs,
because two servers on one data dir share live sessions. Another machine
connects by address (recent servers, Keychain logins) and can install
Codeman there over SSH.
- Installs run Codeman's install.sh in Terminal from a self-deleting
.command script. The installer needs a terminal for sudo, Homebrew and
ssh. A local install uses --local --service and the first free port from
3000, and the app opens Codeman once it answers.
- The local Codeman's login is read from its LaunchAgent or
~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while
the user's own Codeman process holds it, and is never saved.
- Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge
answers, both scoped to Codeman's own pages, so the page keeps working
after a server restart. App cookies are cleared before every connect.
- Pop-outs through window.CodemanHost, with an allowlist (webview/ never
opens a window), external links in the default browser, downloads to
~/Downloads, real full screen and window state that is remembered.
- The connect page refuses a WebKit older than Safari 16.4, which Codeman's
terminal needs.
Codeman side: viewSubagentTranscript shows a toast when the pop-up is
refused instead of failing, with zh-CN strings and a test that pins them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local echo overlay rebuilt every character of the prompt on each
keystroke, one absolutely positioned span per character, so a keystroke
cost O(prompt length) in DOM work, style, layout and paint. Every
positioned span was also its own paint layer, so the browser re-layerized
and repainted thousands of them per frame. In headless Chromium with a 6x
CPU slowdown, a ~4,100-character prompt ran at a median 813 ms per frame,
and ~300 characters already missed 60 fps (54 ms).
renderOverlay() now keeps a per-container record of the line elements it
drew and rebuilds only lines whose text, position or composition range
changed. It starts over when the font, colour, cell size or Unicode
version changes, or when something else emptied or edited the container.
makeLine() lays the spans out in a flex row instead of positioning each
one, and sets the shared font styles once on the line. Cell edges are
snapped to a quarter pixel, which Blink, WebKit and Gecko all store
exactly, so the widths add up to the grid without drift and every edge
stays within 1/8 px of col * cellW. Same prompt: 17 ms per frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every keyboard open and close resized the PTY, and Claude Code's inline renderer answers each height change by clearing the screen and reprinting only the transcript tail that fits, so rows above that tail were lost from tmux for good. Measured on Claude Code 2.1.306 with tmux 3.2a: five keyboard cycles (62x38 to 62x15 and back) left 75 of 100 numbered lines in the pane's history; with the rows held the PTY stays 62x38, no SIGWINCH reaches the pane and all 100 remain (#672).
While the keyboard is up the rows captured at the show transition are held through getTerminalDimensions, columns still follow the box, and the screen, now taller than its box, is translated up by --kb-clip so the last content row sits at the bottom of the visible area. This is the one documented exception to the columns-only geometry rule: there the extra rows came from another device and nothing kept the composer in view, here they are this device's own rows held on purpose and the clip keeps the composer visible. The local-echo overlay's setRowLimit narrows the renderer's existing totalRows bound, so one place decides where a long draft stops growing downward.
Only inline renderers hold, through a new registry capability keyboardHold: 'inline' (Claude alone) published to the page as window.__codemanKeyboardHoldModes. Full-height layouts such as codex, Claude's fullscreen mode (mouse tracking on, unmeasured), and remote or docker panes resize as before. The hold is released on keyboard close, a shape change, a tab switch, a session update, a reconnect, or the setting turned off. Per-device setting Keep Size Under Keyboard, default on.
Gesture control now moves tiles: with the tile grid open, pinch anywhere
on a tile and carry it onto another tile (they swap) or an empty cell (it
moves there); a session tab carried onto the grid joins it there. The
overlay asks tile-grid.js what is under the hand (tileAtPoint,
tileDropTargetAt) and drops through the mouse drag's own paths
(dropSessionOnTile / dropSessionOnSlot, so _reorderTiles stays the one
move path).
Detach Tiles (App Settings > Appearance, per device, OFF by default,
desktop only):
- a tile's header let go outside the browser window opens the session as
a pop-out (detachSession, which now takes an optional size and screen
position), the tile's size, near the drop point, after a 120 ms settle;
- dropped on another Codeman window's tiles it moves there: that window
docks it through the target's own drop and posts 'tile-adopted' on the
window channel, and only the window it was dragged from lets go of its
copy (a last tile goes to the welcome screen, never onto that session);
- a pop-out's title drags back onto any grid: the session is taken back,
the pop-out is asked to close and is released (stops answering
roll-calls; one a script cannot close says where the session went), and
its stale 'detached' answers are ignored for 2 s;
- a tile's menu offers "Open in a new window", and a hand that lets a tile
go outside the grid pops it out too.
With the setting off nothing changes. No server change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The overlay no longer hides for any scrolled-up viewport, only when the
cursor row leaves the screen. The changeset gains the echo package's patch
bump and the #576 entry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Anything typed on a phone vanished whenever the terminal viewport was not scrolled fully to the bottom. Codeman parks the viewport a few rows above the bottom on purpose (scrollToLastNonEmptyLine after a tab switch, or after the keyboard drops and the terminal regrows, so trailing blank rows stay off screen), and the overlay hid itself on a bare viewportY !== baseY test even though the prompt and the cursor were on screen. The text was buffered the whole time and Enter still sent it, which is what made the keyboard look dead.
The overlay now gates on whether the cursor row is inside the viewport (promptRowInViewport), in both the render path and the scroll handler, so a deep scroll into history still hides it. A buffer without cursorY keeps the old bottom-only rule.
Two of Codeman's custom prompt finders made the matching mistake of treating cursorY, which xterm reports relative to baseY, as a screen row; with the gate relaxed that would have painted the text on the wrong line. Both go through cursorViewportRow now, and the Claude finder looks for its glyph from the cursor's screen row up to the top of the live screen only, since a parked viewport shows scrolled-off history whose old composer rows used to be unreachable and would anchor the overlay on the wrong line.
Merge-time fixes for the three findings of the third review round of #499.
- minor: a composition on an empty prompt did not follow the prompt after
output or a resize. The post-write re-place in flushPendingWrites and the
resize observer both ran rerender() only when hasPending was true, and
hasPending deliberately excludes the composition, so the first word of a
prompt (an overlay holding only a composition) stayed on the old row over
whatever output moved there. Both sites now call rerender() unconditionally;
it already returns early when there is nothing to draw, so nothing changes
without a composition. New browser case drives the real
batchTerminalWrite/flushPendingWrites path against real xterm 6 and the
overlay built from source, moves the prompt from row 0 to row 3 and checks
the overlay follows (it fails on the old guard, overlay left on row 0), with
a parity case for pending text. The structure test pins the post-write site
through vm and the resize site, which is a closure inside initTerminal(), by
source.
- nit: removeChar() dropped the composition but did not repaint on its false
path, leaving a composition-only overlay on screen showing text the addon no
longer held. It now hides the overlay there when a composition was dropped.
Package tests cover that path and the flushed path repainting without the
tail.
- nit: the package README did not document setComposition() or the
composition getter and described hasPending as "any content". Added both to
the API tables plus a short IME composition section, reworded hasPending
(pending or flushed text, excludes the composition), and made the quick
start re-render unconditionally instead of teaching the hasPending guard.
The hasPending JSDoc says the same.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With local echo on, committed text sits in the LocalEchoOverlay and does not
reach the PTY before Enter, so the PTY cursor that places the preview span
stays at the prompt start. The span's z-index 6 only counts inside
.xterm-helpers (its own z-index 5 stacking context), and the overlay is a
z-index 7 layer whose first line is opaque from the prompt column, so every
composition after the first one in a prompt was drawn under the overlay.
- xterm-zerolag-input: add setComposition(text) and a composition getter.
The overlay draws the composition as an underlined, aria-hidden tail after
its pending text, through the same wrapping and grow-upward layout. It is
never part of pendingText, hasPending or anything sent; clear() and
removeChar() drop it, and rerender()/refreshFont() keep it.
- terminal-ui.js: while local echo shows typed text (on, and not handed back
to PTY echo by a nav key), render and clear the preview through
setComposition. The helper span stays for local echo off, and as the
fallback when the overlay cannot place the text (no prompt found).
- Browser test against real xterm 6, the overlay bundled from its source
and styles.css: a second composition after pending text is the topmost
element after that text, and the commit lands in the overlay once. Unit
tests for setComposition in the package and for the routing in the
structure test.
- CLAUDE.md and architecture-invariants: state the preview's effective layer.
Typing a prompt long enough to wrap ran the text off the bottom of the screen: the
tail — the part being typed, where the cursor is — sat behind the on-screen
keyboard, so the user was typing blind. Two independent causes.
**The overlay had no bottom bound.** On touch devices keystrokes are buffered in
the local-echo overlay and do not reach the PTY until Enter, so the CLI never
learns the prompt is long and nothing scrolls or reflows to make room. Meanwhile
the renderer lays its wrapped lines out straight DOWNWARD from the prompt row
(`top = promptRow * cellH`, each line at `i * cellH`) with nothing clamping it to
the visible rows — and with the keyboard up there are only a handful of those.
The block now grows UPWARD once it would pass the last visible row: it is lifted
so its final line lands ON that row. Every line div is opaque, so it covers
transcript above rather than vanishing under the keyboard below — the same thing a
real terminal does when a composer expands. A prompt taller than the whole
viewport keeps its TAIL, for the same reason the fix exists: the end is what the
user is looking at. `startCol` indents only the line that starts at the prompt
marker, so it is dropped along with that line when only the tail fits, and the
cursor follows the last VISIBLE line.
`rows` joins the render key: the layout depends on it, so a keyboard opening —
which changes rows without changing the text — must not be skipped as a redundant
render.
**`_shrinkPaddingToFit()` was reclaiming the bars' own space.** On phones the
toolbar and accessory bar are `position: fixed`, so they occupy no layout space
and `main`'s padding-bottom is the ONLY thing reserving room for them. Shrinking
it by the full sub-row slack pulled the terminal's bottom edge down underneath
them, and the row the following re-fit gained was painted behind them — clipping
the last line of a long prompt. The shrink now has a floor: the MEASURED height of
the currently-visible fixed bars, so genuine over-reservation of the hard-coded
84px is still reclaimed while a device that needs those pixels keeps them. The
floor is `Math.min(currentPadding, measured)`, so it can only ever prevent a
shrink, never cause a grow that would resize the terminal as a side effect.
Overlay behaviour lives in `packages/xterm-zerolag-input/` (single-source; the
vendor bundles are generated), so the fix is in the package with the row count
passed in as an optional `totalRows` — absent, the layout is exactly as before.
Tests: 7 cases in the package's `overlay-renderer.test.ts` (upward lift, tail
retention, indent drop, cursor on the last visible line, and the unclamped
fallbacks) and 7 in a new `test/mobile-keyboard-bottom-padding.test.ts` (reclaim,
floor, partial reclaim, no-grow, hidden bars, CJK strip, whole-row slack). 5 and 4
of them respectively fail without the fix. Package suite 238 pass, including the
codex byte-identity and replay tests.
Verified on Android + Chrome against a live instance: a ~460-character prompt
wrapping ~12 rows stays on screen while typing and arrives at the PTY intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With a real codex login now available, record the one shape the fake-key
lab could never produce: a genuine model reply streaming above the pinned
composer, pushing lines into history (baseY grows) while keystrokes land
mid-stream. The recorder gains an opt-in CODEX_RECORD_REAL=1 scenario
using the user's own ~/.codex (fixture secret-scanned for key/JWT
material before writing; scanned clean). The replay test pins: baseY > 0,
mid-stream predictions painted, exact convergence to the typed text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Mosh-style write-through prediction: the consumer sends every keystroke
unchanged; the addon paints predicted glyphs and reconciles against the
parsed buffer. Confirm = cell match + cursor advance (placeholder-safe,
repaint-safe); two-pass mismatch cascade with neutral blanks (measured:
codex clears its placeholder on first echo); TTL bound; baseY-based line
reads; scroll/resize/off-row clears. Zero edits to zerolag-input-addon.ts.
Tests: 30 addon-law specs + renderer geometry (fake performance clock for
TTL/grace), 6 replay suites running the real algorithm through a real
@xterm/headless parser fed by the recorded codex fixtures, and a
500-iteration seeded fuzz with per-op span/record + grid invariants.
227 total, the pre-existing 175 untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recorder (scripts/dev/record-codex-frames.mjs) captures real codex 0.147
TUI output through the production pipeline (tmux status-off + the codex-mode
full strip from session.ts) into JSONL fixtures with keystroke injection
points; analyzer replays them through @xterm/headless for the measurements
in docs/predictive-echo-plan.md. Composer signature /^> /-style (U+203A),
modal and wrapped rows correctly rejected, echo is unstyled default-fg,
tmux delivers echo as minimal in-place deltas.
Package: types.ts gains optional cursorX/cursorY, getCell, onWriteParsed,
onResize (all additive); prediction-renderer.ts renders per-glyph spans
keyed by prediction seq; @xterm/headless@^6.0.0 devDep for replay tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Antigravity (agy) was wired into the session layer but never propagated to
the surfaces around it, while Gemini CLI stayed documented as a consumer
product despite being enterprise-only since Google's cutover. Gemini keeps
full support; Antigravity now sits beside it everywhere.
Functional fixes:
- docker/agent.Dockerfile never installed agy, so a docker case with
mode 'antigravity' died on command-not-found. agy is not on npm, so it
gets its own installer step. --dir /usr/local/bin is load-bearing: the
default $HOME/.local/bin resolves to root's home at build time and is
unreachable by the `agent` user the container runs as. Verified inside
codeman/agent:base (v1.1.10, reachable as `agent`). Note the binary is
~190MB, the largest layer in the image.
- Welcome screen gained a Run Antigravity action, gated on agy being
present like the other CLI buttons, with a cyan identity matching the
toolbar run button and run-mode dot.
- install.sh now detects agy (search paths mirroring the resolver), counts
it as a satisfying AI CLI, and recommends it over Gemini in the install
hints. Detection only, no new auto-install path.
Docs corrected where they were factually wrong:
- architecture-invariants documented isExternalCliMode() as
opencode/codex/gemini when the code has included antigravity for a
while, said "all three modes", and omitted ANTIGRAVITY_ from the env
prefix allowlist row.
- cron-guide's agentType enum, cron-discovery's SessionMode, and
remote-sessions' RemoteCommandMode were all stale.
Also: README + README.zh-CN (five CLIs, Gemini marked enterprise-only),
package.json keyword, and comment drift in 8 places.
test/run-mode-ui.test.ts now covers the new welcome button; verified it
fails without the settings-ui wiring.
Antigravity nests its whole state under ~/.gemini/antigravity-cli/, not
~/.antigravity, so the existing .gemini docker credential seed already
covers it. Recorded as a comment so nobody adds dead config later.
isAltScreenStripMode() deliberately still excludes antigravity: whether
its TUI needs the alt-screen strip is a behavioural question that needs a
real agy session, not a guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Release 1.9.8 (aicodeman) and 0.1.8 (xterm-zerolag-input).
Fixes macOS session start (`posix_spawnp failed.`, issues #6 and #204):
node-pty ships its macOS spawn-helper as mode 0644 and macOS launches every
PTY through it. `scripts/fix-node-pty.mjs` (npm run fix:node-pty) chmods every
helper, prebuilds/ included, then verifies by really opening a PTY; the blind
Node-22+ rebuild is gone. `spawnPtyWithHelperRepair()` self-heals an already
broken install on the first failed spawn.
Adds the phone home screen (session overview under 430px, per-device
`mobileOverviewEnabled`, default ON) and a guided Tailscale path in
install.sh, plus `install.sh tailscale` to retrofit it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PUT /api/settings service toggles now resolve from `merged` (persisted +
incoming) instead of the raw request body, so a partial PUT no longer
starts the subagent watcher and stops the workflow + image watchers by
treating every omitted key as "apply the default". Pinned by a 4-case
regression test verified to fail against the old handler.
Also trims the links line from the Codeman callout in the
xterm-zerolag-input README.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan-usage chip defaults ON on desktop (handhelds stay OFF), resolved
through a single planUsageChipEnabled() helper so the checkbox, the chip
and the create-time statusLineTelemetry flag cannot disagree. Correct the
stale "Cron button defaults ON" comment (it is OFF in code, template and
CSS) and the styles.css comment claiming the server strips the chip's
hidden class.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the misaligned 8-line keystroke-flow diagram (its branch sat two
columns off the junction it attached to) with a two-lane contrast that
makes the same point in two lines: stock xterm.js waiting 300ms vs the
overlay painting immediately. The mechanism detail it was annotating
moved into the following paragraph.
Add a Codeman callout between the badges and the demo GIF, with links to
getcodeman.com, the install one-liner and the repo, and rewrite the
bottom Origin section so it argues credibility instead of repeating the
promo.
Not released: the npm page updates only on publish, so the next COM
needs an "xterm-zerolag-input": patch changeset for this to ship.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the xterm-zerolag-input README (hero demo GIF, value-first
structure) and fix its drift against the source: 175 tests not 78,
CJK/emoji wide-char support documented instead of listed as a
limitation, setPrompt() documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinch any floating subagent or ultracode run/transcript window with the
camera hand-tracking overlay and move it anywhere. Adds a 'window' grab
kind to entry.ts, slotted into the pinch priority chain
(cg-float panel → agent window → session tab → toolbar button). It moves
the window via its own style.left/top (matching app.js's mouse drag,
incl. bottom:'auto') and calls window.app.updateConnectionLines() so the
glowing connector line to the session tab tracks live — app.js redraws
from fresh rects, so no reach into its internals.
Hardening: el.isConnected guard (ultracode windows tear down mid-grab on
SSE reconnect / auto-close), all window.app calls optional-chained +
try/caught so the standalone playground still works, bring-to-front via
app.js's own z-counters, rAF-coalesced redraws cleared on drop so the
final placement always redraws.
Rebuilt the committed gesture-codeman.js bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cmdPattern's empty-matchable unbounded arg group backtracked exponentially
on wrapped heredoc/table lines — hovering one froze the tab for minutes.
Non-empty tokens + bounded reps make it O(n); regression test extracts the
shipped patterns and pins timing on the real killer shapes.
worker-src 'self' blob: is now unconditional so terminal-ui's _safeYield
tick worker (throttling escape) isn't CSP-blocked on non-gesture installs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bring the Ark0N/codeman-gesture-control repo in-tree as the codeman-gesture-control
workspace package so the hand-tracking overlay can be developed in the Codeman repo.
New npm run build:gesture bundles src/codeman/entry.ts into the served
gesture-codeman.js; scripts/build.mjs reruns it on every production build.
Source formatted to Codeman's prettier style.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers all 7 test plan items: Chinese overlap prevention, double-width
spacing, Japanese/Korean input, cursor positioning, line wrapping at
column boundaries, ASCII regression, and teammate terminal panels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add charCellWidth/stringCellWidth helpers for Unicode-aware width detection,
fix makeLine to use for...of iteration with visual column positioning, and
fix line splitting in _render to use visual column widths instead of string
length. CJK/fullwidth characters now correctly occupy 2 cell widths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Overlay renderer (xterm-zerolag-input):
- Add charTop/charHeight to CellDimensions and RenderParams for precise
vertical text positioning matching xterm's canvas renderer
- Convert device.char dimensions to CSS pixels via devicePixelRatio
- Extend line div background 1px past cell boundary to cover compositing
seam between overlay layer (z-index:7) and canvas layer below
- Remove -webkit-font-smoothing/text-rendering overrides that made overlay
text thinner than canvas text
- Add per-span height/lineHeight for natural CSS vertical centering
- Add setPrompt() method for runtime prompt strategy switching (fixes tab
switching crash with "setPrompt is not a function")
app.js duplicate class members:
- Remove dead formatTokens duplicate (line ~5590 shadowed precise version)
- Remove fire-and-forget resetCircuitBreaker duplicate (shadowed notification version)
- Rename mux-panel killAllSessions to killAllMuxSessions (was shadowing
Codeman session killer, breaking Ctrl+K)
Other:
- Update index.html onclick to use killAllMuxSessions
- Add getTeamTasks mock to test route context
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix repository URL: nicobailon/claudeman → Ark0N/Claudeman
- Rewrite xterm-zerolag-input README with badges, origin story, architecture
diagrams, better API docs, integration patterns, and "Why This Is Hard"
- Add Published Packages section to Claudeman README with quick start example
- Add callout in Claudeman's local echo section linking to the npm package
- Publish as xterm-zerolag-input@0.1.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pitfall 3 (REAL BUG): setFlushed() called _render() unconditionally.
During tab switch, rendering against a stale buffer found the old
session's prompt and locked the column position via the flushed-text
column lock. After the new buffer loaded, rerender() kept the stale
column. Fix: add render parameter (default true), pass false during
tab-switch restore to defer rendering until buffer is loaded.
Pitfall 2: Tab completion "undo" required calling clearFlushed() +
resetBufferDetection() separately — easy to get wrong. Add
undoDetection() convenience method that atomically clears flushed
state and re-enables detection.
Pitfall 1: Backspace Map sync is app-specific (can't be fixed in lib),
but removeChar() return value + getFlushed() make the pattern clean.
Updated tab-switch test to demonstrate the correct pattern.
Tests: 78 total (+4 new)
- setFlushed render=false prevents stale column lock
- setFlushed render=false keeps overlay hidden
- undoDetection clears flushed + re-enables detection
- undoDetection preserves pending text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Second deep audit comparing all 43 integration points in Claudeman's
app.js against the library API found one critical gap:
- Add suppressBufferDetection() — needed when switching to sessions
with UI framework text (Ink) after the prompt that would be falsely
detected. Claudeman sets _bufferDetectDone=true externally; this
method provides the public equivalent.
Tests: 74 total (+13 new)
- Tab-switch save/restore pattern (flushed state roundtrip)
- suppressBufferDetection blocks explicit, implicit (addChar), and
cascade (removeChar) detection paths
- clear() resets suppression
- Methods safe before activate() and after dispose()
- addChar implicit buffer detection on first keystroke
- refreshFont with flushed-only text
README: rewritten from 207 to 459 lines with:
- removeChar cascade explanation with return value table
- Flushed text concept explained
- Integration patterns: buffered, char-at-a-time, tab switching,
tab completion, Ink/TUI frameworks, SSE reconnect, resize, font
- Architecture diagram (keypress → DOM overlay → PTY echo flow)
- Prompt column locking, text wrapping, render cache, scroll awareness
- All known limitations documented
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Regex global flag safety — strip `g` flag before matching to prevent
lastIndex mutation and missing .index on match results
2. state.visible false before activate — null overlay no longer reports
visible: true
3. removeChar() returns 'pending' | 'flushed' | false instead of boolean
so consumers can distinguish whether to send backspace to PTY
4. removeChar() implements buffer detection cascade (step 3) — detects
existing prompt text when both pending and flushed are empty
5. RenderKey includes text content, not just length — prevents stale
renders when setFlushed() called with same count but different text
6. Remove dead `import type { Terminal }` from test file
7. Remove internal `FontStyle` from public exports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract Claudeman's local echo overlay into a reusable xterm.js addon
at packages/xterm-zerolag-input/. Provides instant keystroke feedback
via a DOM overlay, eliminating perceived input latency over high-RTT
connections (SSH, mobile, cloud IDEs).
- Zero dependencies, compatible with xterm v5.x and @xterm/xterm v5.4+
- Configurable prompt detection (character, regex, or custom function)
- Flushed text tracking for tab-switch / deferred echo scenarios
- Per-character grid-aligned rendering matching xterm's canvas output
- 56 tests passing (prompt finder, overlay renderer, full addon lifecycle)
- Dual CJS/ESM build with full TypeScript declarations
Claudeman source is unchanged — migration to consume this lib is a
separate follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>