Repository navigation
fix(mobile): hold the PTY rows while the on-screen keyboard is up - #711
Conversation
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 (Ark0N#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.
|
Thanks @JDProfresh, this is careful work. It keeps a local Claude session's PTY rows while the phone keyboard is up and translates the screen with Everything from my answers on #672 is here: default ON as a per-device setting, the Three small things, none of which needs a round trip:
I will apply these three on top when I merge, so there is nothing more for you to push. Thank you for asking first on #672 and for the measurements behind it; they made this an easy review. |
- terminal-ui.js _applyKeyboardClip(): read container.clientHeight only while rows are held, so the release path and the setting-off path skip the forced layout on every render and cursor move - constants.js keyboardClipPx JSDoc: drop the stray "ponytail:" marker, the note reads as plain text - src/web/CLAUDE.md "Per-device vs synced settings": list keyboardHoldsRows among the per-device keys SettingsUpdateSchema does not declare Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged in 1.46.0 (https://github.com/Ark0N/Codeman/releases/tag/codeman%401.46.0), on by default as agreed in #672. Thank you for the measurements; 100 of 100 lines kept against 75 made the case. I applied the three items at merge: the terminal height is read only while rows are held, the stray marker in the |
Every phone keyboard open and close resized the PTY, and Claude Code's inline renderer answers each height change by reprinting only the transcript tail that fits, so the rows above it are lost from tmux for good. This holds the rows while the keyboard is up, as agreed in #672.
Closes #672
Before and after
Claude Code 2.1.306, tmux 3.2a, a fresh session selected from the phone, one prompt printing
KBTEST-001toKBTEST-100, five keyboard cycles:Running on my own install since 2026-10-21, used from iOS Safari on my iPhone (iOS 26.6.1).
What it does, per your answers on #672
Default ON. Per-device setting "Keep Size Under Keyboard" (App Settings, beside Local Echo). It sits in
displayKeysand stays out of the strict settings schema, which a test pins. Off restores today's behaviour.Capability
keyboardHold: 'inline'onCliCapabilities(src/config/cli-registry/types.ts). The JSDoc gives the measurement above and says fullscreen Claude is unmeasured. Only the claude entry sets it.src/web/server.tspublishes it aswindow.__codemanKeyboardHoldModes, built fromenabledClis()the same way as__codemanTranscriptPageKeys. The frontend reads only that list (_keyboardHoldEligiblein terminal-ui.js), andtest/frontend-cli-no-id-branching.test.tsstays green. Schema tests accept'inline'or absent and rejecttrue,'fullscreen'and''. They also pin that the stock list is exactly['claude'].The documented exception is a paragraph right after the Text gets muffled sometimes, in both TUI default and fullscreen #464 one in
docs/architecture-invariants.md(PTY and browser terminal geometry). It says why this is not the COLUMNS ONLY regression: there, the rows came from another device and nothing kept the composer in view; here, this device's own rows are held on purpose and--kb-clipkeeps the last content row at the bottom of the box. The Text gets muffled sometimes, in both TUI default and fullscreen #464 paragraph now points to it, and the rootCLAUDE.mdsummary names it. The composer test istest/keyboard-held-rows.test.ts("the composer row stays visible with the rows held"). It runs a real@xterm/headlessterminal at 38 rows with Claude's inline frame (100 transcript lines, the rule, the❯composer, the footer) through the shippedgetTerminalDimensionsand_applyKeyboardClip. It asserts:A real-browser companion in
test/mobile/keyboard.test.tsmeasures the composer and footer rects against#terminalContainerwith the actual transform after a keyboard show.setRowLimit()stays in the package and feedstotalRows._layoutRows()inzerolag-input-addon.tsis the one place that computes thetotalRowshanded torenderOverlay: the terminal's rows, narrowed by the limit, and never widened past them. Both the render key andRenderParams.totalRowsread it. Six package tests are intest/zerolag-input-addon.test.ts("setRowLimit").Fullscreen veto kept. While
cliMouseTrackingis on, the session resizes as today. The capability JSDoc, the invariants paragraph and the wiki all say this mode is unmeasured.The hold is released when the keyboard closes, on a shape change (rotation, fold; this also arms the viewport settle, so the terminal refits to the new display instead of keeping the held rows unclipped until the keyboard closes), on a tab switch, on a
session:updatedthat makes the session stop qualifying (mouse tracking on), on a reconnect, and when the setting is turned off. Remote and Docker panes never hold.Also in the PR: zh-CN strings for the label and description (pinned through the real translator over
index.html), a row inSettings-Reference.md, a bullet inMobile-Guide.md, and an entry insrc/web/CLAUDE.mdpointing to the invariants paragraph. Two JSDoc comments the change made untrue are corrected (reconcilePtyGeometry,_geometryForResizeRequest).Tests
session:updatedrelease is tested through the shipped_onSessionUpdated. The tab-switch, reconnect and settings-save call sites are covered through_reconcileKeyboardHold's own tests, not through their large enclosing functions. A browser test that drives a tab switch with the keyboard up is easy to add if you want one.npm test: 12,603 passed. Two failures also fail on unmodified master here, and neither file is touched:deepseek-transcript"decodes every frame": Node 24.22+/26.10+ behaviour, fixed in test(deepseek): stop pinning Node's one-shot zstd decoder to the first frame #710.tui-switch-guardline 239: tmux 3.2a exits 0 onshow-options -gvof an unset option.packages/xterm-zerolag-input): 267 passed.npm run test:mobile -- test/mobile/keyboard.test.ts: 5 failed / 48 passed on the branch, against 5 failed / 47 passed on master. The five failures and their messages are identical: toolbar transform, CJK local echo, and the three tap-intent cases. The extra test is the new composer test, and it passes.