Commit Graph
1328 Commits
Author SHA1 Message Date
Codeman maintainer 6db526dbad Merge pull request #562: fix(cli-install): install npm CLIs to ~/.local when the npm global prefix is not writable 2026-10-10 02:53:29 +02:00
Codeman maintainer e6deab98a6 Merge pull request #563: fix(viewer): make in-document links in rendered markdown scroll to their heading 2026-10-10 02:53:29 +02:00
Codeman maintainer 5e0cea6ec8 Merge pull request #565: feat(settings): add an Apply button that saves without closing 2026-10-10 02:53:28 +02:00
Codeman maintainer 2cc6ea1cfd Merge pull request #557: feat(cli): codeman agent — session-to-session verbs for every CLI mode (#445, phase 1) 2026-10-10 02:53:28 +02:00
Codeman maintainer c5ddb77adc Merge pull request #570: test: bind every test server to an ephemeral port (#440, 2/2) 2026-10-10 02:53:27 +02:00
Codeman maintainer d2d2ba3e4a Merge pull request #569: feat(tabs): a per-device switch to hide the CLI logos on tabs 2026-10-10 02:53:27 +02:00
Codeman maintainer 33ad314bea Merge pull request #578: fix(uploads): show the server's reason when a prompt upload fails 2026-10-10 02:53:26 +02:00
Codeman maintainer 3f4af2aa2e Merge pull request #577: fix(tiles): the wheel scrolls Claude's fullscreen transcript in a tile, and Shift+wheel scrolls local history 2026-10-10 02:53:26 +02:00
Codeman maintainer cf11a253d3 Merge pull request #571: feat(tiles): Tile Animations setting, entrance styles for the tile grid 2026-10-10 02:53:25 +02:00
Codeman maintainer 17824f1847 Merge pull request #572: fix(git): pin git's locale for clone, so failures classify on non-English hosts (#568) 2026-10-10 02:53:25 +02:00
Devvyn b3d3c647cf feat(notifications): configurable toast and browser-notification display time (#564)
Squash-merged so the toast-history half, dropped during review, stays out of master's history.
2026-10-10 02:52:38 +02:00
Codeman maintainer 53f61ca539 fix(tiles): the wheel scrolls Claude's fullscreen transcript in a tile, and Shift+wheel scrolls local history
A grid tile or the split's Pane B (TerminalTile) left the mouse wheel to
xterm for a session running Claude's fullscreen renderer (claude 2.1.187+
with mouse tracking on, cliMouseTracking). That renderer scrolls its own
transcript on SGR wheel reports, which the primary pane sends it, while the
tile's xterm holds only Codeman's replayed repaint frames (tmux keeps no
history for such a pane). So in a grid of fullscreen Claude sessions the
wheel either scrolled nothing or dragged stale frames, Claude's pinned input
box with them, up the tile, and Claude's transcript never moved. This was
tile-grid-plan follow-up 4.

The tile now forwards the wheel the way the primary pane does
(TerminalTile._maybeForwardWheelToCli): the primary pane's own gate,
_shouldForwardWheelToApp(ev, target), asked for the tile's terminal and
session; the cell from _clientPointToCell(x, y, tile.terminal); a scrolled-up
viewport snapped to the live screen first; and the reports flushed through
the tile's own 40 ms coalescer to the tile's session (the primary queue
flushes to the active session). The encoding moved into pure helpers in
terminal-ui.js, CodemanTerminalInput.wheelDeltaWholeLines and
sgrWheelReports, which the primary pane's _wheelScrollLines and
_sendSyntheticSgrWheel now call too, so the two panes send identical bytes.

Shift+wheel, the explicit local-scrollback gesture, was dead in every tile
off macOS: Chrome on Windows delivers it as a horizontal wheel (deltaX), and
xterm's own scroller turns a Shift+vertical wheel into a horizontal one. The
tile now scrolls it itself (_maybeScrollLocalOnShift: scrollLines() on the
dominant axis, sub-line travel carried over, a shell tile's history pull
still asked on the way up), as the primary pane's capture-phase handler does.

Unchanged: inline Claude, opencode and older Claude still take the
PageUp/PageDown route (#555), shells and other modes keep xterm's own plain
wheel, and a tracking xterm or the alternate buffer stays xterm's.

Tests: test/terminal-tile-scroll.test.ts covers forwarding (geometry, tick
cap, coalescing, viewport snap, the tile's session rather than the active
one, Shift/tracking/alternate exclusions, byte equality with the primary
pane) and Shift+wheel (Windows deltaX shape, sub-line carry, shell history
pull). test/terminal-tile-scroll.browser.test.ts adds a real-Chromium case
with trusted page.mouse.wheel() events.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 02:25:54 +02:00
JD 3be03c6896 fix(uploads): show the server's reason when a prompt upload fails
A failed upload only showed a count, so a remote session's refused upload read as a mystery instead of a rule the user could act on. The server already returns the reason in its error envelope and the per-file error carries it, so I surface the first one in the toast. One reason is enough when a whole batch fails for the same cause.
2026-10-09 20:13:58 -04:00
Codeman maintainer b59145effd feat(templates): new cases' CLAUDE.md asks for absolute file paths and points at the codeman skill
Codeman turns absolute paths in the terminal into links that open the file
viewer, but agents usually report created files as relative paths, which
cannot be clicked. The generated CLAUDE.md now asks for the full absolute
path of every created file in the final reply, and says why relative, ~/
and markdown-link forms do not work.

It also tells the agent about the codeman skill (start, prompt, wait on and
clean up worker sessions) when the skill is available, and how the user can
install it when it is not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 23:28:55 +02:00
Codeman maintainer 5206a044bb feat(settings): an Animations section in App Settings
Every animation setting now has its own App Settings section, right after
Appearance (owner: easier to find). It holds the Entrance Theme (the former
Entrance Animations row), Tile Animations, and an Open lab button that closes
settings and opens the per-surface lab (?animlab=1). Appearance keeps the skin,
identity and tab settings. New animation settings go in this section;
test/app-settings-structure.test.ts pins it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 18:22:14 +02:00
Codeman maintainer abc8598100 test(git): pin gitNonInteractiveEnv()'s locale so a revert fails in CI
CI runs in an English locale, where git already answers in English, so the
real-git clone tests stay green even if the LC_ALL/LANG pin from the previous
commit is dropped. A pure assertion on the env is the only check that fails
then. It also covers LANGUAGE, which gettext ignores once LC_ALL is C.

Refs #568

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 17:50:07 +02:00
RandalixandClaude Opus 5.5 6ba311f38d fix(git): pin git's locale for clone, so failures classify on any host
classifyGitFailure() matches git's English stderr, but cloneRepository()
spawned git with gitNonInteractiveEnv(), which left the host's locale in
place. Under de_DE.UTF-8 a missing ref came back as FAILED / 422 instead of
REF_NOT_FOUND / 400, and a missing repository as 422 instead of 404; the
clone dialog showed "git failed: Schwerwiegend: …". gitNonInteractiveEnv()
now sets LC_ALL=C and LANG=C, as git-workspace-status.ts already does.

Two tests depended on the locale on their own:
- git-status-routes: the spy runner called git without the production
  runner's LC_ALL=C, so "Kein Git-Repository" was not recognised.
- custom-model-run-menu-ui: the modal formats with toLocaleString(), the
  test hard-coded 16,384 and 40,000.

Full suite under de_DE.UTF-8: 5 failed before, 0 after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 17:48:28 +02:00
Codeman maintainer decd263b17 feat(tiles): Tile Animations setting, entrance styles for the tile grid
Tiles become the fifth entrance surface (data-tile-anim), switched on in App
Settings > Appearance > Tile Animations and OFF by default: the default is the
grid's own quick fade (`settle`), exactly as before.

A styled tile plays in two beats: its frame enters as it mounts (fly out of
its session tab, dealt from the Tiles button, CRT power-on, beam down from its
tab, cascade, pop, soft), and its screen then plays the terminal pane style of
the entrance theme when its first capture lands, through the same
html[data-term-anim] rules on .tile-body. Each style has its own exit when the
Tiles button closes the grid (back into the tabs, a CRT switch-off, ...).

Picking an Entrance Animations theme presets the tile style (new Launch theme:
tiles fly from the tabs); the theme readout ignores the tile row, so changing
it never shows "Custom". Frames move transform and opacity only (one fit and
one PTY resize per tile), a reload's restore always settles, and nothing moves
under reduced motion. The lab (?animlab=1) gets a Tile grid group, a cascade
order picker and in-place replay / close + reopen.

Also removes the terminal pane's `boot` entrance style (owner decision); a
saved `boot` falls back to off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 17:43:55 +02:00
Codeman maintainer ad57394618 feat(tabs): a per-device switch to hide the CLI logos on tabs
Since 1.40.0 every agent tab draws its CLI logo through the run-mode-dot
slot, and there was no way to turn that off. App Settings → Appearance →
Tabs now has "CLI Logos on Tabs" (showTabCliLogos), right after Tall Tabs.

- Modelled on tabTwoRows: a per-device display key in the server-settings
  merge and an optional boolean in the .strict() SettingsUpdateSchema,
  loaded and saved by the id appSettingsShowTabCliLogos. Default ON on
  every device; only an explicit false turns it off (tabCliLogosEnabled).
- CSS only, no tab re-render: applyTabOrientation() and the pre-paint
  script in index.html stamp html[data-tab-logos="on"|"off"] from the
  same stored blob (so a reload never flashes the logos), and one rule in
  styles.css hides .session-tab .tab-harness and .home-sessions-harness.
  The rows space their children with flex gap, so nothing is left behind.
- A live flip resizes every agent tab with no render behind it, so
  applyTabOrientation() then re-takes the strip's one-row wrap decision
  (updateTabOverflowMode) and re-anchors the lines drawn from tab rects;
  a header that changes height reaches the PTY through the terminal
  container's ResizeObserver, as any header change does.
- Covered: the header strip, vertical rail, sidebar, grouped rail, ledger
  and case clusters and phone chips (all render the same tab markup) and
  the desktop home rail. Untouched: tile and split headers, the Run menus,
  the welcome launchers. The phone overview rows, the command palette and
  the tab action menu draw no logo. The shell's SH pill and the status dot
  stay.
- zh-CN for the label and the description.

test/tab-cli-logos-setting.test.ts drives the real openAppSettings() and
saveAppSettings() in JSDOM (the saved PUT body must pass the schema), the
server-settings merge, the defaults on desktop and phone, the live stamp
and its re-measure without a re-render, the real pre-paint script (on,
off, the phone key, the catch fallback), the CSS rule's exact selectors,
and the translations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:34:09 +02:00
RandalixandClaude Opus 5.5 fda1897109 test(guard): no legacy list left; flag raw listeners on a fixed port (#440)
With the sweep done, LEGACY_FIXED_PORT_FILES and its staleness test go. A second rule flags a raw listen(<number or …PORT>) and a port: with a number or …PORT constant inside listen({ … }) or new WebSocketServer({ … }); a socket path and a lower-case variable pass. CLAUDE.md, AGENTS.md, CONTRIBUTING.md and the wiki's Contributing page lose the mobile exception, and CLAUDE.md names closedPort() instead of port + 1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:29:02 +02:00
RandalixandClaude Opus 5.5 eceaecf076 test: raw test servers listen on port 0 (#440)
http/Fastify servers in daemon-control, deepseek-status-shim, session-input-wait and the three tui tests listen on 0 and read address().port. The two "nothing listens here" ports (a fixed 3243, and the server's port + 1) come from closedPort(): bind 0, read the port, close.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:29:01 +02:00
RandalixandClaude Opus 5.5 6f87186deb test(mobile): createTestServer() binds an ephemeral port (#440)
The helper cached servers by port, but every mobile file starts exactly one, so the cache was never shared: it is now a Set that stopAllTestServers() walks. PORTS leaves helpers/constants.ts, the nine tests set baseUrl after start(), and the README drops the port column and the pick-a-port step.

Committed without the pre-commit hook: accessibility, subagent-windows and visual-regression were not prettier-clean on master already; formatting them would bury the port lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:29:01 +02:00
RandalixandClaude Opus 5.5 aa4217b8b1 test: build every in-process WebServer on port 0 (#440)
The files on the guard's legacy list build new WebServer(0, …) and read server.boundPort after start(), as sse-tile-grid-filter does. Files with several servers read each server's own port; quick-start's local helper lost its port parameter so the literal 0 sits where the server is built. Header comments that named a port say "ephemeral". No assertion changed except where it embedded the port.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:28:26 +02:00
Devvyn c75d62ce04 fix(settings): Apply cannot keep a later Save open, refreshes only after a saved PUT (review)
- Split the one-shot keep-open intent from the in-flight guard and consume it before the
  first await, so a Save clicked while an Apply is in flight closes the modal.
- Refresh the dependent groups only when the settings PUT returned ok (_apiPut answers null
  or a non-ok response instead of throwing), and also when only the webhook save failed.
- Find the 'Apply or Save' hint by a data marker, not its English text; add zh-CN strings
  for the new toast and title.
- Tests run the real saveAppSettings() (Apply then Save mid-flight, a failed PUT, a webhook-only
  failure, a plain Save).
- Narrow the changeset and JSDoc to MCP sync and CLI management; touch the tray comment, the
  architecture note and the wiki.
2026-10-09 19:46:09 +08:00
Devvyn c2e55fc210 fix(cli-install): address review: drop every npm_config_prefix spelling, probe async
- Delete every spelling of npm_config_prefix before setting NPM_CONFIG_PREFIX, in the Compose
  branch too: npm run exports the lowercase key and a sorting /bin/sh let it win. The
  operator guard stays on the uppercase key only.
- A prefix that does not exist yet is judged by its nearest existing ancestor.
- The probe is async (promisified execFile, fs.promises.access, SIGKILL on timeout), awaited
  before the spawn and only for commands that run npm.
- Tests: lowercase/mixed-case keys, and the decision logic against a fake npm on PATH
  (writable, read-only, not yet created, npm missing). Docs: one clause in cli-registry.md.
2026-10-09 19:42:06 +08:00
RandalixandClaude Opus 5.5 24e51a3b8e fix(cli): review — 8-char id floor, marker echo, wake answers on send
From the review of #557:

- An id shorter than 8 characters refuses with exit 4 before any request,
  on every verb. `rm 9` resolved to whichever session was alone with that
  first character (the user's own tab included) and deleted it. Same floor
  as the server's PARENT_SESSION_ID_MIN_PREFIX. `rm` no longer claims a
  lineage check: "Delete any session except this one".
- `wait --match` help and the README example say the marker must not appear
  verbatim in the prompt (its echo matches at once) and show the split form.
- `send` reads the route's wake-on-LAN answers: `buffered` gets its own
  line (exit 0), `dropped` exits 1 instead of printing "accepted".
- `--` for a prompt that starts with "-", in the `send` description and in
  the one-argument refusal.
- `stripAnsi` builds on the shared one (OSC sequences go too); the
  inputRefusal JSDoc sits above inputRefusal again.
- docs/wiki/Driving-Codeman-From-An-Agent.md gets a `codeman agent` section.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 12:59:56 +02:00
RandalixandClaude Opus 5.5 0481db569d feat(cli): add codeman agent — session-to-session verbs for every CLI mode
The agent skill teaches the session verbs to claude only (Codeman seeds its
preamble for local claude sessions). An opencode, codex, pi or gemini agent has
the same environment — CODEMAN_MUX, CODEMAN_SESSION_ID and CODEMAN_API_URL are
exported into every pane — and nothing that teaches it the verbs, so
`codeman agent ls|spawn|send|wait|read|interrupt|rm` packages them as commands.

It is a client of the server, like `codeman tui`, and stays out of
`codeman session` (which drives the in-process SessionManager). No new route and
no second transport: everything goes through CODEMAN_API_URL, so auth,
ownership and the per-session waiter cap apply unchanged. Credentials and the
Basic header come from src/codeman-credentials.ts, in the same order attach and
the TUI use.

Invariants, each in test/cli-agent.test.ts:
- Refuses outside a Codeman session (CODEMAN_MUX=1 + CODEMAN_API_URL); never
  guesses a URL.
- `send` takes the prompt as ONE argument (an unquoted multi-line `$(…)` would
  otherwise be split by the shell and re-joined into one line), transmits
  printable text plus Enter only, and refuses multi-line input loudly instead of
  letting sendInput weld the lines. No resend loop of its own: the server's
  SubmitVerifier owns the swallowed-Enter case. ESC exists only as `interrupt`,
  which never appends Enter.
- `rm` fails closed: empty id, an unprovable self id, or a prefix match in
  either direction refuses.
- Nothing mode-shaped in the CLI: the readiness mark `spawn` waits for comes
  from the registry (new `capabilities.composerReadyMark`: claude's composer
  hint `shift+tab`, deepseek's `❯`), `read` relies on the route's own answer
  dispatch, and a `stop`/`blocked` the session cannot fire is the server's 400,
  passed through. A `/wait` timeout is a 200 with `timedOut`: exit 2 with a
  neutral line, not an error. A worker that dies during spawn's readiness wait
  is exit 3, like every other wait.
- `X-Codeman-Agent-Origin` rides only spawn's quick-start, the one request that
  may create a case directory; every other verb leaves it off. Server-side,
  test/routes/agent-case-marker-routes.test.ts pins that a POST /api/sessions on
  an existing workingDir is never labelled, header or not, and that quick-start
  labels only a directory it creates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 12:59:56 +02:00
RandalixandClaude Opus 5.5 6d862d5323 refactor(cli): one credential reader for every client of the API
`readCodemanEnv()` in cli.ts and `parseEnvFile()`/`readCodemanCredentials()` in
tui-client.ts were two copies of the same `.env` reader (the TUI's said so in a
comment), and `codeman agent` was about to need a third. They move into
`src/codeman-credentials.ts`; `codeman attach` and the TUI read from it, and the
TUI keeps re-exporting its names so nothing that imports them changes.

The lookup order is one pure function, `credentialsFrom(env, fileEnv)`: each
field from the environment, then the file, username defaulting to `admin` — the
order attach and the TUI already used. `readCodemanCredentials` takes the
environment as a parameter so a caller with its own (the agent CLI's guard) gets
the same answer. The parser now also tolerates an `export ` prefix, which the
agent skill's preamble already accepted in the same file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 12:59:48 +02:00
Devvyn 9c63c41283 feat(settings): add an Apply button that saves without closing
Switches such as MCP server sync only unlock their controls once saved, and
Save closed the modal, so the user had to reopen Settings to continue. Apply
runs the same save, keeps the modal open and refreshes the dependent groups
(MCP sync, custom model endpoints, CLI management) in place.
2026-10-09 18:41:59 +08:00
Devvyn 26487f0ec5 fix(cli-install): install npm CLIs to ~/.local when the npm global prefix is not writable
installEnv() only redirected NPM_CONFIG_PREFIX inside the Docker container
(CODEMAN_IN_CONTAINER=1). On a native install with a root-owned system node
(prefix /usr) `npm install -g @deepseek-ai/dsh` run as the server user died
with EACCES (exit 243), so DeepSeek, pi and other npm-based CLIs could not be
installed from Settings. Ask npm for its global prefix and, when the server
user cannot write it, redirect to $HOME/.local like the container path does.
An operator-set NPM_CONFIG_PREFIX and a writable prefix are left alone.
2026-10-09 18:41:58 +08:00
DevvynandClaude Sonnet 5.5 8da4a07a60 fix(viewer): make in-document links in rendered markdown scroll to their heading
marked emits no heading ids and, with <base href="/">, a bare #section href points at the dashboard root, so [Install](#installation) in the File Viewer did nothing. The shared click delegate now resolves fragment links against the rendered document: GitHub-style slugs in data-md-anchor (never ids, so a heading cannot capture an app element), case-insensitive and percent-decoded, repeats numbered -1/-2, # = top, explicit ids supported, an unmatched fragment ignored instead of navigating.

Tests: slug/assign/find unit tests (CI gate) and a real-browser test clicking links in a File Viewer document, which fails without the change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
2026-10-09 17:01:44 +08:00
Codeman maintainer 5e2e9bb833 fix(terminal): the Path and Clear keys act on the tile or Pane B that has the keyboard
The phone keyboard's Path key (insertTerminalText) and its clear-prompt key
(clearTerminalInput) always wrote to the main pane: into its local-echo
overlay, or to the active session. With the tile grid open that overlay is
parked behind the grid, so a picked path landed there unseen and only reached
the session later, and with the split open a path meant for Pane B went to
Pane A.

Both now ask _focusedPane() first. When a tile or Pane B holds the keyboard,
the path is sent to that pane's session through the exactly-once queue, and
clearing sends Ctrl+U there (those panes have no overlay, so the TUI owns the
line), then the pane's own terminal takes focus. The main pane's behaviour is
unchanged. Left over from the final checkup's dictation fix (c10), which
covered voice input only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:22:23 +02:00
Codeman maintainer 28708cfa14 fix(i18n): zh-CN for the Redraw toasts, and comments that named old defaults
Redraw (Ctrl+Shift+R and the header button) shows five literal toasts and a
size report on the main pane, a tile or the split's Pane B. None had a zh-CN
entry, including the two the final checkup's tile Redraw fix added, and
"Failed to restore terminal size" fell to the generic "Failed to" pattern,
which left English behind. They now translate, and the size report keeps its
numbers through a pattern rule. test/redraw-toast-i18n.test.ts reads the
toasts from restoreTerminalSize() itself, so a reworded one without an entry
fails.

Comments and docs that still described an older default:
- styles.css: the Tiles header button is no longer opt-in; it is on by default
  on desktop and off on phones and coarse-pointer tablets.
- terminal-ui.js: the desktop branch of getDefaultSettings is no longer always
  {}; what the comment needs is that it sets no copyStripMargin.
- docs/tile-grid-plan.md: the Tiles default bullet names the tablet default.
- docs/cli-registry.md: codex's footer is read in a two-row window since
  codex 0.162's hint row, not from its last row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:21:25 +02:00
Codeman maintainer c638c88739 Merge the final checkup's fixes into the 1.40.0 release
From the read-only final review of the release, adversarially verified, then reviewed again:
- tiles: a file dropped on the grid uploads to that tile's session instead of navigating away; app-driven tile changes no longer move the keyboard into another session; popping out the last tile no longer leaves a frozen view; "Open group as tiles" no longer merges an open split; the Tiles button defaults off on touch tablets (opt-in)
- voice: dictation with the grid open reaches the focused tile
- css: By case stays one scrolling strip on 600-767px tablets, the needs-you pulse animates opacity only, phone welcome chips are 40px
- i18n: zh-CN for the case picker rows, the git status settings, new toasts, tile and spreadsheet texts
- cli registry: codex launch defaults are registry data, not an id branch; the codex footer reads an ultra effort
- build and docs: a dependency preflight runs before the build deletes dist; docs no longer name 1.36.0

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:19:41 +02:00
Codeman maintainer a4511a0e48 fix: final checkup review follow-ups
- i18n: the spreadsheet notice's feature words (charts, drawings, macros,
  pivot tables, external links) were bare, case-insensitive zh-CN keys, so
  the page translator also renamed a charts/ or macros/ folder in the Files
  panel and a case of that name in the case picker. They are now scoped
  'Spreadsheet feature: <word>' keys; warningText() falls back to the plain
  word when the scoped key has no translation (English, no i18n). Removing
  the bare keys closes this branch's regression. The older 'models' key has
  the same class of problem; it is left alone here, since adding
  .case-combobox-option-label to USER_TEXT_SELECTOR would also untranslate
  the picker's two action rows and still miss the title attribute.
- Spreadsheet notice bar: marked data-i18n-skip. It is written already
  translated, item by item, and ends with a number format's code, which the
  observer's t() over the whole line rewrote ({name}, "Codeman").
- Connection tile (Header Stats Style Tiles): applyLocalization() now repaints
  the indicator, so a switch back to English no longer leaves the Chinese
  value word in its data-i18n-skip span until the next keystroke or ACK.
- Tiles default: loadAppSettingsFromStorage() no longer caches the
  posture-dependent showTileGridButton default, so the init merge never
  persists it; a 2-in-1 first opened as a tablet gets the button once docked.
  Every reader still resolves the absent key through a fresh
  getDefaultSettings(), so phones stay OFF and desktops ON.
- Tile grid over a split: closeSplitPane() skips Pane A's closing resize only
  when Pane A becomes a tile. With mergeSplit false (Open group as tiles, a
  stored grid) a Pane A left out of the set gets its full width back before
  the main terminal parks, instead of keeping the split's half width.
- By-case tab strip on tablets and phones: with the boxes dissolved, the
  -<case> part of a generated name shows again, so w1-alpha and w1-beta no
  longer both read "w1".

Each new assertion fails against the previous source.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:17:44 +02:00
Codeman maintainer 330203c08b fix(tiles): final checkup review follow-ups
- docs/tile-grid-plan.md: the As built bullet on tile loads said a refresh
  clears the screen at its turn in the queue. Since the fetch-first refresh it
  fetches at its turn, keeps the last frame through the wait and its own round
  trip, and resets with the queued in-stream \x1bc only once the capture is in
  hand; a failed, aborted or empty fetch writes nothing and resets nothing.
- docs/architecture-invariants.md: the tile grid's One load queue paragraph
  gets the same correction, and its list of captures that go through the
  TileLoadQueue now names the server {t:'c'} refresh and the dropped-output
  recovery refresh.
- test/terminal-tile-input.test.ts: destroy() cancelling a pending recovery is
  now pinned on the timer itself (armed before destroy(), null right after it,
  read before any timer runs), since the recovery callback's own destroyed
  guard made the fetch check pass either way; a second test pins that
  destroy() starts the live-output count over, so a write callback xterm still
  owed counts nothing. Both fail with the _resetLiveFlow() call removed from
  destroy().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:48:55 +02:00
Codeman maintainer 9d38cbf51a fix(build,docs): dependency preflight before the build wipes dist, docs drift for 1.40.0
- scripts/build.mjs resolves exceljs/dist/exceljs.min.js and fflate first,
  before tsc and before rm -rf dist/web/public. A tree whose node_modules
  predate those devDependencies (pulled but never ran npm install) used to
  fail in prepare-spreadsheet-assets.mjs with the live dist assets already
  deleted, so the running server served an index.html whose hashed files
  were gone. It now exits 1 with "run `npm install` first", nothing touched.
  test/spreadsheet-assets.test.ts pins the order, that the list covers every
  require.resolve in the prepare script, and runs a relocated copy of the
  build to prove the exit and message.
- CLAUDE.md: the header visibility rule's stock desktop default now lists
  Tiles (1180px and wider), which ships ON on desktop.
- docs/wiki/Agent-CLIs.md: "Before 1.36.0" becomes "Before 1.40.0" (four
  places); 1.36.0 never ships.
- docs/wiki/Home.md: the "Everything in the manual" index lists Tile Grid
  and Custom Model Endpoints, matching the sidebar. test/wiki-home-index
  fails when a sidebar page is missing from that index.
- docs/wiki/Tile-Grid.md: the Tiles default is off on tablets too since the
  touch-primary default landed, not only on phones.
- docs/browser-testing-guide.md: the fixed port table and new WebServer(PORT)
  snippet give way to the port-0 pattern (new WebServer(0, false, true),
  server.boundPort) that test/test-ports-guard.test.ts enforces; the
  examples that opened localhost:3000, the live instance, use BASE_URL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:47:22 +02:00
Codeman maintainer ecd577157b fix(cli-registry): codex launch defaults as registry data, ultra footer, schema doc defaults
- Codex footer model detection (c28): the modelDetect.screenLine effort
  alternation is now built from CODEX_REASONING_EFFORTS plus 'default', so
  'ultra' (offered by the codexReasoningEffort App Setting and codex's own
  /model picker) is read and the launch enum and the footer reader cannot
  drift again. Still one capture group, 125 characters, no new quantifier.
  New session-display-model case loops every effort level, ultra included.

- No CLI-id branching for launch defaults (c27): the two mode === 'codex'
  branches the synced codex model/effort defaults added to the create and
  quick-start routes are replaced by a registry capability,
  capabilities.launchDefaults (launch param -> settings key, values from a
  closed enum), declared on the codex entry only. The resolver moved from
  web/codex-launch-defaults.ts to web/launch-defaults.ts as
  applyLaunchDefaults(mode, configs, customEndpoint), filling the entry's
  legacyConfigField object through legacyConfigAliases, still re-validating
  with SettingsUpdateSchema and never overwriting a caller's value. The
  route exclusions are unchanged (create: not remote; quick-start: not
  remote, not Docker, not a custom model endpoint), and quick-start still
  derives the session model from a bag without ompConfig, as before.
  schema.ts refuses an undeclared param, an unknown settings key, an empty
  map, and launchDefaults on an entry with no legacyConfigField.

- The no-id-branching guard now carries an exact occurrence count per
  allowlisted key, so a new copy of an already approved expression fails
  instead of riding the old approval, with a synthetic anti-vacuity case.

- SettingsUpdateSchema JSDoc (c21/c29): 'classic' is the tabArrangement
  default and 'compact' the headerStatsStyle default, matching the
  resolvers and the pre-paint script; state/case/ledger are marked opt-in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:35:56 +02:00
Codeman maintainer 1def7de146 fix(tiles): cap each tile's live-output backlog and recover dropped output
The server applies no WebSocket backpressure (16 KB / 8 ms batches, no
bufferedAmount check), and a tile wrote every live frame straight into
xterm. A flood a tile could not parse as fast (a shell tile running cat on
a huge log) piled up in xterm's own write queue without bound, on a main
thread up to six tiles share, until xterm's WriteBuffer threw past 50M
code units; onmessage's empty catch then dropped every frame silently and
nothing recaptured the screen. The primary pane caps its queues and drops
then recaptures (_onSessionTerminal, _scheduleDroppedOutputRecovery).

Each tile now writes live output through _writeLive:
- unparsed code units are counted, each write's callback counting its own
  back down; frames held behind a replay (_liveQueue) count too;
- the budget is TerminalTile.LIVE_BACKLOG_BUDGET, 4 MiB, deliberately not
  the primary pane's 128 KB: that caps its own rAF-paced queues, while
  xterm itself paces a tile, and a tight cap would trip on ordinary bursts
  and blank-and-reload the tile over and over;
- past it a frame is dropped, the tile stops writing onto the hole, and one
  refresh is scheduled, debounced and bounded by the primary pane's own
  rule (CodemanDroppedOutput: 2 s, DROP_RECOVERY_MAX_ATTEMPTS, never retried
  after a deadline abort). It is an ordinary refresh, so single-flight,
  bounded by lines=/tail= and paced by the grid's TileLoadQueue. The flag
  clears once a capture taken after the last dropped frame has replayed;
- a write that throws is the same drop, never a "malformed frame";
- past the bound the flag is released, so a tile is never left frozen;
- a reconnect starts the accounting over (an epoch makes callbacks from
  before it count nothing) and drops a pending recovery, since its own
  refresh replaces the screen; destroy() cancels it.
The live-queue flush after a pull or a refresh goes through the same path,
so a throwing write there cannot skip the load's marker and trailing
refresh either.

Tests (input harness, real constants and fake timers): the default budget
lets a 1 MiB unparsed burst through, parsed bytes stop counting, a trip
stops writing and ONE debounced refresh recaptures, a write throw takes the
same recovery, a hole in the held queue is recovered by another refresh,
bounded retries then release, no retry after a deadline, a reconnect resets
the count, and destroy cancels. The fake xterm can now hold and release
parses and throw on a write. Live writes now carry a callback, so the unit
tests match them on the data argument (a `.not.toHaveBeenCalledWith(data)`
would otherwise pass for nothing).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:26:51 +02:00
Codeman maintainer 82c87f56d1 fix(i18n): zh-CN for the case picker rows, Bottom bar settings, new toasts, tile and spreadsheet text
- Case picker: the "New or link a case…" and "Case settings…" rows that
  replaced the translated + and gear buttons get zh-CN entries reusing the
  buttons' wording, plus the list's "No cases match".
- App Settings, Bottom bar: the whole group translates as one (heading, the
  four Git status rows with #543's max repositories and git timeout, and
  their descriptions), so it never reads half English. The Git panel's two
  button names, which the new "Git status" key reaches, read naturally too.
- Toasts: the three "Could not open a new window for this ..." errors and
  the dictation-closed warning.
- Spreadsheet preview: "Spreadsheet preview failed (<status>)" gets a
  pattern. Worker refusals map their error code to one user sentence each in
  the renderer (the raw message goes to the console), feature ids read as
  words (pivot tables, external links), and the notice bar translates each
  item before the join; a number format's code passes through untouched.
  The worker and core are unchanged, so their pinned strings and the
  asset version hash stay as they were.
- Header Stats Style Tiles: the connection tile's value word reads in
  Chinese through scoped "Connection tile: <word>" keys, never bare-word
  keys ("retry" is also the orchestrator's Retry button, "LIVE" a resume
  list badge). The indicator cache now includes the UI language, so a
  language switch repaints it on the next update.
- Tile grid: the "Loading…" label was CSS content text the translator
  cannot reach; it is now content: attr(data-loading-label), written through
  the translator when the tile is built and each time it starts loading.

Tests pin every new string (zh differs, no English left, English unchanged)
and fail without these changes: the tile harvest sees the loading label and
the rename field, a CSS guard keeps words out of tile generated content, the
Bottom bar group, the case picker rows and toasts read from their source,
the connection tile across a language switch, and the spreadsheet error
codes, notice bar and status line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:23:05 +02:00
Codeman maintainer cdb3c34ae0 fix(tiles): Redraw on a tile sends the forced resize and reports only what it sent
Redraw (Ctrl+Shift+R and the header button, restoreTerminalSize) on a
focused tile or the split's Pane B called tile.fit({ force: true }) and
always toasted "Terminal restored to CxR". In TerminalTile._sendResize,
force only skipped the client-side dedupe: the frame carried no `f`, so
Session.resize skipped a size equal to the one it last applied and the
server did nothing. And _sendResize returned silently with the socket down
or the session popped out to its own window, while the toast still
claimed success.

Both halves are fixed, the first as parity with the primary pane:
- a forced fit now sends `f: true`, the flag the primary's sendResize sets,
  which the server honours (ws-routes reads msg.f, Session.resize then runs
  tmux resize-window and the PTY resize at the same size);
- fit() and _sendResize() return whether a frame went out, and
  restoreTerminalSize toasts success only then. Otherwise it says why, as
  the primary branch does: "sized by its own window" for a detached
  session, "not connected" while the tile's socket is down (it announces
  its size again on reopen), and the primary's "Could not determine
  terminal size" for a pane that measured nothing.

What this does not claim: a forced resize to the size the PTY already has
changes no geometry, so it is not a cure for a garbled tile whose PTY
already matches; the primary pane's forced resize has the same limit. It
matters when the server's recorded size has drifted from the tmux window.

Tests: the forced frame carries f:true (and plain ones do not), fit()'s
return value on send, dedupe, detached and closed-socket paths, and Redraw
end to end on a real tile (sent, socket down, popped out), plus the three
no-success toasts in focused-pane-shortcuts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:20:19 +02:00
Codeman maintainer eb5d982c38 fix(tiles): refresh fetches first, then resets in-stream and replays
A tile's refresh (a {t:'r'} or {t:'c'} frame, every reconnect) wiped the
pane with a synchronous xterm clear() at the load's turn, BEFORE its fetch,
and wrote live frames straight through the fetch and the replay. That is
the replay clear CLAUDE.md "Terminal resilience" forbids: bytes still
queued in xterm are parsed after a synchronous clear and fuse into the
snapshot, and clear() keeps the cursor's row, column, SGR and margins, so
the capture (raw rows, no home) started wherever the cursor sat. A failed
or empty fetch left the tile blank.

The refresh now runs in the primary pane's order (_onSessionNeedsRefresh,
_resetTerminalForReplay):
- fetch first, so the tile keeps its last frame through the round trip and
  through a grid tile's wait in the load queue;
- from the response on, live frames are held in _liveQueue with their
  arrival time, as _pullHistory already did, and the body read of a bounded
  window (grid tile, shell) gets the pull's 10 s budget, while Pane B's
  unbounded full=1 keeps the request's own budget;
- then the queued in-stream \x1bc immediately before the replay;
- then the held frames that arrived after the response (_flushLiveQueue,
  now shared with _pullHistory), then the owed marker.
A failed, aborted or empty fetch writes nothing and resets nothing.

The _stampMarkerIfOwed guard for a pending trailing refresh stays (that
refresh settles the marker itself either way); only its rationale changed.
The fake xterm now treats an in-stream RIS like clear() in its row
emulation.

Tests: the ones that counted clear() calls on the refresh path now count
the in-stream reset instead, assert it sits right before the replay and
that clear() is never called (unit single-flight block, the marker
ordering tests, the reconnect test, the grid {t:'r'} and marker tests, and
the scroll test's server-clear overflow case, which now goes through a
refresh). New: the screen is untouched on a failed or empty fetch and on a
failed body read (held frames written in order), frames before the
response are written through and later ones held behind the replay, the
cutoff drops frames the capture covers, the body budgets, and a grid tile
keeps its last frame through its own capture's round trip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:18:32 +02:00
Codeman maintainer e39a750749 fix(tiles): treat the server's clear frame as a refresh, not a bare clear
The server sends {t:'c'} from one place only: a fresh Claude pane's first
prompt (Session.startInteractive), meaning "refresh after startup". The
primary pane answers it with a refetch and replay (_onSessionClearTerminal),
and stands aside while the grid is open, so the tile's own handling was the
only one that ran. That handling was a bare xterm clear(), which keeps only
the cursor's row and drops the banner, a resumed transcript and all
scrollback. An idle Claude never repaints static rows, so a Claude session
Run into the grid, or Attached in a tile, came up as a near-empty tile.

_onLiveClear() now calls _refreshBuffer(), the {t:'r'} path: single-flight,
coalesced into one trailing refresh behind a load already running (a shell
pull's held frames included), and paced by the grid's TileLoadQueue. The
queued {clear:true} entry and its branch in _pullHistory's flush are gone,
along with the _clearTerminal helper they used.

Tests: two unit tests pinned the bare clear (a clear frame queued in order
during a pull, and one applied at once before the capture); they are
replaced by tests that the frame coalesces behind the pull and refetches,
plus a socket-level {t:'c'} test, a coalescing test, and a grid test that
the frame waits its turn in the load queue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:13:56 +02:00
Codeman maintainer 41643f2b38 fix(css): tablet By-case strip, compositor-only needs pulse, phone chip height
Tab Layout "By case" at tablet widths (600 to 767px, getDeviceType's
'tablet'): the case boxes could shrink in the tablet's one-row scrolling
strip, so they squeezed and wrapped their tabs inside themselves and every
tab past a box's first line was clipped under the fixed 48px header. The
boxes now dissolve into the chip row as they already do on phones, which
keeps the tablet's own 40px chip geometry. The rule sits in its own
600 to 767px block, not the 768px tablet block, so the desktop path at
768px and up (boxes keep their width, the strip wraps box by box) is
untouched.

Needs-you tile pulse: the glow animated box-shadow on the tile itself, so
the whole tile (DOM-rendered terminal rows included, the whole stage when
zoomed) was repainted every frame for as long as a prompt waited. The tile
keeps its red border; the glow is now a static inset shadow on a
.tile--needs::after overlay (inset because .tile is overflow: hidden and
clips an outer one, z-index 3 above .tile-attach, pointer-events none)
and only its opacity animates. The entering-and-needs animation shorthand
is gone, since it would now blink the whole tile's opacity, and reduced
motion keeps the static ring and hides the overlay.

Welcome chips: the phone block set 36px, below the 40px that styles.css
gives touch screens, and it wins on every phone, so phones got shorter
chips than tablets. It now restates 40px.

Tests: the tablet widths and the 768px boundary in tab-clusters, the
opacity-only pulse overlay in tile-grid-motion, and the phone chip height
in run-mode-ui; each fails against the previous CSS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:01:31 +02:00
Codeman maintainer 37ddcbe2f0 fix(voice): dictation with the tile grid open reaches the focused tile
With the tile grid open the main terminal is parked (display: none), but
local echo stays on, so direct-mode dictation for the focused tile's
session (which is activeSessionId) was appended to the main terminal's
hidden local-echo overlay. Nothing appeared in the tile, Enter in the tile
submitted without the dictated text, and the stranded text was later
flushed into whichever tile had focus when the grid closed, or dropped.

- _insertText: skip the overlay while _tilesOwnTerminal() is true, so the
  text goes through _sendToTarget to the session itself.
- The post-insert refocus gives the keyboard to the focused tile (only if
  it is still the dictation target) instead of the parked main terminal.
  Outside the grid it still focuses the main terminal, so split view keeps
  its behaviour even when Pane B took focus mid-dictation.
- Green send button: with tiles open, send only Enter to the target and
  leave the parked overlay and main-terminal predictions alone.

The gate is _tilesOwnTerminal(), not _focusedPane().isPrimary: in split
view a target equal to activeSessionId is Pane A with a visible overlay,
and focus read at transcript time could otherwise push Pane A's dictation
past its own unflushed overlay text.

Tests: tile-grid dictation and green-send cases (both fail without the
fix) plus a split-view pin in test/voice-input-target.test.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:51:08 +02:00
Codeman maintainer 18c8b5c280 fix(tiles): file drops, focus handoffs, pop-out fallback, split merge, tablet default
- A file dragged onto the tile grid navigated the browser away: the single
  view's drop handler sits on #terminalContainer, hidden while tiles are
  open. The #tileGrid section now cancels every file dragover and drop
  (bubble phase, so tab and tile drags stay with _acceptTabDrops), and a
  drop on a tile uploads its images to THAT tile's session through
  _uploadAndInsertImages, with the same "Only image files are supported"
  toast as image-input.js (now in the zh-CN table).
- App-driven refocus no longer moves DOM focus into another session's
  xterm: a remote delete of the focused tile, _reconcileTileGrid and a
  socket closed with 4003/4004/4010 (_onTileExit) pass focus: false.
  removeTile gains a focus option; user-initiated removes keep focusing.
- Popping out the last tile left the parked terminal's stale content under
  the popped-out tab (and snapshotted it on the next switch).
  _selectAfterTileGrid treats a detached session as unusable for both the
  focused id and the fallback.
- "Open group as tiles" and Ctrl/Cmd+click with the grid closed pass
  mergeSplit: false, so an open split no longer adds its two sessions on
  top of a set already sized to the group, the count and the window.
- Touch-primary devices (primary pointer coarse: iPad, Android tablets)
  default the Tiles button OFF in getDefaultSettings(); touchscreen
  laptops (fine primary pointer) keep the desktop default ON. The button,
  the App Settings chip and the Ctrl+Shift+G gate all resolve an absent key
  through these defaults, so they agree. CLAUDE.md and the invariants doc
  say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:44:52 +02:00
Codeman maintainer a96a94fb7e fix(tiles): send a tile's click report ephemeral, review follow-up
The tile's hand-encoded click report went through _handleDesktopTerminalClick
and _sendSyntheticSgrTap to _sendInputAsync, so it took a seq, was persisted
and would be redelivered after a reload. The documented TerminalTile rule
(CLAUDE.md, Split-pane sessions) is that only typed input enters that queue
and focus/mouse reports go out ephemeral, and the tile's own _onTerminalData
says the same. Before #555 an opencode tile's click went through xterm's
encoder and that ephemeral path. A click still unacknowledged when the page
reloads, or sent during a server restart, could be replayed onto a later
screen, where a press+release can pick a dialog option.

_sendSyntheticSgrTap now takes an opt-in `ephemeral` field on its target and
sends through _sendInputEphemeral when it is set; _handleDesktopTerminalClick
passes the target through unchanged, and TerminalTile._installClickListener
sets it. Without the flag nothing changes, so the primary pane's own click
and touch tap reports stay on _sendInputAsync exactly as before (whether the
primary pane should also go ephemeral is a separate question, out of scope
here).

Tests: the tile case now requires a frame with no seq and nothing pending in
the reliable queue, and the targeted-click case in terminal-touch-tap spies on
both send paths: a target with the flag goes ephemeral, an untargeted click
and an untargeted tap stay durable. Dropping `ephemeral: true` from the tile,
or the branch in _sendSyntheticSgrTap, turns the matching test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:42:03 +02:00
Codeman maintainer 24a73ecd81 fix(tiles): page a hollow tile only from the live screen, review follow-up
A tile counts as hollow when every row above its screen is its own overflow
(baseY minus _overflowRows is 0), so unlike the primary pane, whose hollow
buffer has baseY 0, its viewport can sit above the bottom while it is hollow:
Shift+PageUp, a scrollbar drag or a wheel during the first replay leave it up
there. _maybePageCliTranscript never looked at the viewport, so every wheel,
wheel-down included, was turned into PageUp/PageDown and swallowed. xterm never
scrolled back, the stale rows stayed on screen while the CLI paged out of
view, and clicks were dropped too, because the click report refuses an
off-bottom viewport.

The tile now pages only while _terminalViewportAtBottom holds for its own
terminal, checked before the pending travel is touched. Off the bottom the
wheel stays with xterm, so a wheel-down brings the viewport home and paging
resumes from there. The primary pane is unchanged: its hollow test already
implies a viewport at the bottom, which the twin comment now says.

Tests: a unit case for a tile hollow by the discount with its viewport above
the bottom (no page key, no preventDefault, and no travel carried over once
back home), and the real-browser case now scrolls a hollow tile up and proves
a real wheel-down scrolls xterm home with no page key sent, then pages again.
Both go red with the gate removed, and the unit case also with the gate moved
below the pending-travel update.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:40:04 +02:00
Codeman maintainer 312a8faa06 fix(tiles): wire the Android soft-keyboard controller into every tile (#541 parity)
#541 fixed Android autocorrect duplicating the typed line in the primary
pane: xterm's keyCode-229 textarea diff is append-only, so an autocorrect on
space (delete a word, insert the corrected one) sent the whole line again.
The fix, an edit-based diff that sends one DEL per deleted code point and
then the inserted text, lives in terminal-keycode229-recovery.js together
with #441's next-keydown drain (a character committed in the same task as
Enter goes out ahead of the \r) and the original orphaned-insertText
recovery. Only the primary pane created that controller, so a grid tile or
the split's Pane B still ran xterm's stock behaviour. Both are gated on
width alone (1180 CSS px), which a wide Android tablet clears.

TerminalTile now creates its own controller in connect(), after the xterm
opens and before the first await, handed this tile's textarea, this tile's
CompositionHelper and _onTerminalData as the send path, so recovered bytes
go to the tile's own session through the exactly-once queue. As in the
primary pane, handleKeyEvent runs first in the custom key handler, above the
keyCode-229 early return, and notifyCanonicalData sits in the onData lambda,
gated on the same two CodemanTerminalInput predicates, never in
_onTerminalData, which the recovered bytes also take. destroy() tears the
controller down before disposing the xterm, which restores xterm's own diff
and removes the capture listeners. No mode or device gate, matching the
primary. The module itself is unchanged apart from its header; terminal-ui.js
gains only a comment naming the twin.

Tests: test/terminal-tile-input.test.ts now loads the real module into its
vm harness (with window timers, without which create() would silently throw
and every test would run against no controller) and drives a fake
CompositionHelper carrying xterm's own append-only diff. It covers install
and restore on the tile's own helper and textarea, autocorrect sent as an
edit (with a control reproducing the device-log duplicate), the last
character and an autocorrect each followed by Enter in one task, a
self-rescued 229 key delivered once, the onData gate ignoring query replies
and focus reports, two refused inserts after one keydown both recovered,
robustness when the controller throws, per-tile controllers, and a source pin
keeping the call above the early return. Removing the create, the
handleKeyEvent call, the notify, its gate, or the destroy each turns at least
one of them red, as does moving the notify into _onTerminalData. The browser
suite gains a TerminalTile block in
test/terminal-keycode229-recovery.browser.test.ts (real xterm, trusted
execCommand input, chunks asserted to address the tile's session, with a
destroyed-controller control).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:20:02 +02:00
Codeman maintainer 4fe843a94e fix(tiles): page a hollow tile's CLI transcript and report its clicks (#555 parity)
#555 made the primary pane page opencode's transcript with PageUp/PageDown
from the wheel, because opencode draws in place on the alternate screen and
leaves the browser's buffer with no scrollback. A TerminalTile (a grid tile,
the split's Pane B) left every wheel to xterm, so in an opencode tile the
wheel scrolled nothing, or only stale rows.

The tile now runs the primary pane's own gates aimed at itself (its terminal,
its session, never the active one): xterm's tracking mode, the Claude
forwarding gate, then the hollow-buffer test. A wheel that passes them is
consumed in the capture phase and turned into PageUp/PageDown through the
shared pageKeysForTravel math, coalesced per tile (40 ms, 512 bytes, the twin
of the primary pane's queue) and sent ephemeral on the tile's own socket.
Every other wheel stays with xterm as before, the shell history pull
included. The file names no CLI: the mode rules stay in terminal-ui.js, and
terminal-tile.js joins the frontend no-id-branching guard.

A plain port of the primary's baseY === 0 test would almost never fire in a
grid. A tile's first capture is taken at the PTY's previous size (usually the
taller primary pane's) and written into a shorter xterm, and its own
row-shrinking fits (zoom-out, divider drags, tile count changes) push more
rows above the screen. The tile counts those rows as its own overflow: all of
them after a load whose capture held a single screen (the server's
captureRows), plus whatever a local fit or a PTY geometry report pushes up,
reset by a clear and clamped to baseY. The paging gate gets baseY minus that
count. Output that scrolls real lines still counts as history, so the tile
stops paging there.

#555's other half, stripping opencode's mouse DECSETs so a drag selects text,
is server-side and already reached tile sockets. It also left the tile's
xterm unable to encode opencode's clicks, so the tile now installs the
primary pane's desktop click report (bubble phase, gated on the session's
cliMouseTracking, the tile's own link hover and selection). Both listeners,
the flush timer and the page-key state are torn down in destroy().

Still out of scope, as the fileoverview now says: touch paging (tiles have
no touch path) and SGR wheel forwarding to Claude's fullscreen renderer
(tile-grid-plan follow-up 4), so a fullscreen Claude tile keeps leaving the
wheel to xterm.

Tests: test/terminal-tile-scroll.test.ts drives a real tile in the vm
harness (session targeting, every no-page case, accumulation, the cap,
coalescing, byte parity with the primary pane, the overflow discount through
a load, a fit, a geometry report and a clear, the click report and destroy);
the discount cases fail with it removed. The fake xterm gains opt-in row
emulation. test/terminal-tile-scroll.browser.test.ts checks the same model
against a real xterm with trusted wheel events (browser suite, not the gate).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 07:35:29 +02:00