Skip to content

Known Issue: Claude interactive menu options clipped under the mobile soft keyboard

Status: 2026-06-19 - while the soft keyboard is open, the default resize mode now reflows the PTY down to the band at natural font size (per the user's choice). The resize->fit remap added 2026-06-18 is reverted: fitAddon.fit() tells the PTY to use fewer rows so Claude windows its own menu at the smaller size; the font stays put and there is no CSS scaling (applyScaleToFit is skipped because fit() already matched the grid to the band). fit (scale only, no reflow) and scroll (keep size, scroll) remain as alternatives. Pending on-device verification + deploy. Surface: Android app, embedded xterm terminal, claim-owner session First reported: recurring through 2026; latest reproduction 2026-06-08

Symptom

When a Claude Code session is open in the Hive Android app and the on-screen keyboard is up, Claude's interactive selection menu (the numbered options list: "1. ... 2. ... Enter to select") is clipped - only the top options or the surrounding message text are visible, and the actionable options / footer fall out of view. Dismissing the keyboard makes the full menu appear again.

Root cause (confirmed empirically)

The Android activity runs with SOFT_INPUT_ADJUST_NOTHING and reports the IME height to the WebView as window.__hiveKeyboardInsetPx (MainActivity.kt). main.ts:updateAppViewportHeight() then shrinks the --app-vh CSS variable to innerHeight - keyboardInset, which shrinks .app-root and therefore the terminal slot. The slot's ResizeObserver calls doFit() (useTerminalManager.ts), which for the claim owner calls fitAddon.fit() and shrinks the PTY row count to fit the reduced height.

Frontend diagnostics pulled from the phone at the moment of the 2026-06-02 reproduction (hive logs frontend --client <app>):

Statekeyboard_inset--app-vhPTY size
keyboard closed0px792px47 x 37
keyboard open320.25px472px47 x 18

Claude's selection menu in that session needed ~22-25 rows (header + wrapped question + five multi-line options + footer). With the keyboard open the PTY is only 18 rows, so the menu physically cannot fit and the options spill out of the visible band. This is a real-estate problem, not a scroll glitch alone.

There is also a likely secondary effect: after the shrink the xterm viewport is not reliably pinned to the bottom, so the visible 18 rows can show scrollback (the assistant's prose) instead of the live menu region (selection cursor + footer). See screenshot evidence in the 2026-06-02 report.

What has been tried (do not repeat blindly)

DateCommitApproachOutcome
2026-04-30805b19a2Introduced SOFT_INPUT_ADJUST_NOTHING + IME inset -> shrink --app-vh -> PTY rows shrink with the keyboard.Keyboard no longer overlaps blindly, but rows drop below menu height -> menu clipped.
2026-05-25bfde1ed1Hold rows when keyboard open (touch only): apply proposed cols but never shrink rows below the menu height; let the user scroll xterm to read what's behind the keyboard. Exposed window.__hiveKeyboardOpen().Fixed the clip, but the live cursor + footer sat behind the keyboard (held rows render full height) and required manual scrolling. Partial UX win.
2026-05-2866916ce8Refactor removed the hold-rows branch and __hiveKeyboardOpen / isSoftKeyboardOpen().Hold-rows mitigation silently lost.
2026-06-0185b4f22aOwner-authoritative PTY sizing refactor: doFit() always fitAddon.fit() for the owner.Regressed to the 805b19a2 behavior - rows shrink to 18, menu clipped again. This is the current state.

Net: the project has flip-flopped between shrink rows (menu too tall to fit) and hold rows (menu fits but the actionable bottom is behind the keyboard). Neither alone solves it because, with the keyboard up, there are only ~18 rows of real screen above it while the menu wants ~25.

Fix applied (2026-06-02) - mobile setting

Exposed the behavior as an explicit, mobile-only setting instead of hard-coding one mode (which is what kept flip-flopping). New Settings > Terminal > On-screen keyboard dropdown (shown only on coarse-pointer devices), default Scroll (keep size):

  • Scroll (keep size) (default): when the keyboard is open, doFit() does no PTY resize at all - the host keeps its keyboard-closed grid, Claude's full menu stays rendered, and the shrunk band (above the keyboard) becomes a scrollable xterm viewport. The user scrolls up to read what is behind the keyboard; the existing restoreViewport snap keeps the live cursor/menu in view. doFit also skips applyScaleToFit in this mode, otherwise it would scale the taller-than-band grid down to tiny text and defeat scrolling. No onResize fires, so no shrink-SIGWINCH reaches the PTY.
  • Resize terminal: the prior behavior - fitAddon.fit() shrinks the PTY rows to fit above the keyboard (reflows the TUI; can clip tall menus).

Implementation: keyboardViewportMode in useTerminalSettings.ts (localStorage hive-terminal-keyboard-mode); doFit() gate + isSoftKeyboardOpen() (driven by native window.__hiveKeyboardInsetPx) in useTerminalManager.ts; dropdown in SettingsView.vue. vue-tsc clean. Not yet verified on a real device or deployed.

Default changed (2026-06-05) - "Fit to screen (shrink)"

Scroll-mode shipped as the default but stayed unsatisfying in practice: with the keyboard up, xterm snaps the shrunk band to the bottom, so the highlighted top option scrolls out of view and the cursor/input line can sit off the visible band ("cursor is off where I actually type"). The menu is simply taller than the ~18 rows above the keyboard, and scrolling only ever shows one end of it.

New default Fit to screen (shrink) keeps the PTY at its keyboard-closed size (no reflow, same as Scroll) but lets doFit() run applyScaleToFit() so the whole grid is CSS-scaled down to fit the band above the keyboard. Every row - all options and the cursor/input line - stays visible at once; the only cost is smaller text on very tall menus. Because the scale is a paint-only transform, the PTY never reflows and no shrink-SIGWINCH reaches the host.

The three modes now:

  • Fit to screen (shrink) (default): keep PTY size, scale the grid to fit above the keyboard. doFit() skips fitAddon.fit() (no resize) but applies applyScaleToFit().
  • Scroll (keep size): keep PTY size, natural scale, scroll within the band (doFit() skips both the fit and applyScaleToFit).
  • Resize terminal: fitAddon.fit() shrinks the PTY rows to fit (reflows; can clip tall menus).

Implementation: kbMode/keepPtySize/scrollMode gate in doFit() (useTerminalManager.ts); 'fit' added to KeyboardViewportMode and made the default in useTerminalSettings.ts; option + description in SettingsView.vue. vue-tsc clean. Not yet verified on a real device or deployed.

Fix applied (2026-06-08) - Resize default + bottom-pin + settle-refit

Fit-to-screen shipped as the default but still clipped on the user's device: the 06-08 reproduction showed full-size text with option 1 scrolled off the top, which means applyScaleToFit computed k = 1 (no shrink) - it ran while the IME open animation / focus churn left window.__hiveKeyboardInsetPx transiently 0, so --app-vh (and the measured container) had not shrunk yet. The displayed result is only as good as the last doFit, and nothing re-fit after the IME settled, so it stuck unscaled intermittently. Confirmed live via hive logs frontend: the connected app kept the PTY at 49x37 (fit keeps PTY size), so the grid never reflowed and the scale never engaged.

The user chose the Terminus model (resize the terminal, let Claude reflow and window its own menu at normal text size) over shrink-to-fit. Three changes:

  1. Default KeyboardViewportMode -> 'resize' (useTerminalSettings.ts). resize shrinks the PTY (fitAddon.fit()) to the band above the keyboard, so the TUI reflows and Claude windows the option list, keeping the selected option visible - like Terminus. (Existing users with no explicit choice pick this up; the setting is unchanged for anyone who set it manually.)
  2. Bottom-pin the live region after a keyboard-driven resize (doFit, useTerminalManager.ts): when kbMode === 'resize', force autoFollow and snapToBottom after the fit so the viewport lands on the freshly re-rendered menu (cursor/footer just above the keyboard) instead of scrollback. This is the bottom-pin the earlier resize attempts (bfde1ed1) lacked - the reason they looked like they clipped.
  3. Trailing settle-refit (initWatchers): a 250ms debounced refitAll() on visualViewport resize (the only keyboard signal under SOFT_INPUT_ADJUST_NOTHING - window.innerHeight does not change). Locks the PTY onto the band's resting height even if the final ResizeObserver fit landed mid-animation. Bound to resize only, not scroll, so reading scrollback with the keyboard up never yanks the viewport.

vue-tsc clean. Not yet verified on a real device or deployed.

Candidate fixes (alternatives, for reference)

  1. Revive hold-rows + pin viewport to bottom. Clamp the owner's PTY rows to the keyboard-closed count (or a floor) while the keyboard is open, AND snap the xterm viewport to the bottom so the selection cursor + footer stay in the visible band; user scrolls up for the question. Breaks the flip-flop by adding the bottom-pin that bfde1ed1 lacked.
  2. Shrink rows + trust Claude's windowed list + pin to bottom. Keep shrinking to 18 rows but guarantee the viewport is pinned to the bottom every keyboard resize, relying on Claude Code's own scrolling selection list to keep the selected option visible. Lowest code change; depends on Claude windowing its menu at small heights (needs verification).
  3. Raise the row floor. Reclaim rows by collapsing the in-terminal mobile helper toolbar and/or reserving less than the full IME inset, giving Claude a few more rows. Complementary, not sufficient alone.

How to reproduce / verify

  1. Open a Claude session on the Android app; become the claim owner.
  2. Trigger an interactive selection menu with 4+ multi-line options.
  3. Tap the input to raise the keyboard; observe clipped options.
  4. Pull diagnostics: hive clients ls then hive logs frontend --client <app-connection-id> --lines 1500, grep viewport and terminal-resize to read keyboard_inset, --app-vh, and the PTY rows at the moment of failure.

2026-06-14 — true root cause found and fixed (mode-independent)

Reproduced again on the deployed v1.12.30 build, default mode resize: selection menu clipped with the keyboard up, footer behind the keyboard, the selected top option scrolled off. The bug is not specific to any keyboard mode — which is why the fit/scroll/resize flip-flops never resolved it.

Root cause: the trailing settle-refit that is supposed to lock in the final PTY/scale size after the IME settles was wired only to window.visualViewport.addEventListener('resize', ...). Under SOFT_INPUT_ADJUST_NOTHING the keyboard is a pure overlay — the window does not resize and window.visualViewport does not change, so that event never fires. The only reliable keyboard signal is the native IME inset (window.__hiveKeyboardInsetPx, set by MainActivity.kt), which drives updateAppViewportHeight() in main.ts to shrink --app-vh. The slot ResizeObserver reacted to the first --app-vh write — a transient, mid-animation frame — and nothing re-fit after the IME settled. The PTY/scale therefore stayed sized to the wrong band. This is the same timing failure the 2026-06-08 note attributed to k = 1 in fit mode; switching the default to resize only relabeled it.

Fix (main.ts, useTerminalManager.ts):

  • updateAppViewportHeight() now dispatches a hive:viewport-changedCustomEvent whenever the viewport signature changes (i.e. whenever the native inset moves), carrying { keyboardInset, viewportHeight }.
  • initWatchers() schedules the debounced KEYBOARD_SETTLE_MS refit off window 'hive:viewport-changed' (primary, Android) and keeps visualViewport 'resize' (secondary, web/PWA / future ADJUST_RESIZE). The debounce coalesces both into a single refit ~250ms after the IME stops moving, so the final fit/scale is computed against the settled band.

Default keyboard mode left at resize per the user's choice ("fix timing only"); the mode default is unchanged. vue-tsc -b green. Not yet committed or deployed; on-device verification pending (needs the phone connected to a reachable node to pull hive logs frontend).

2026-06-18 — resize-while-keyboard-open now maps to fit (reflow eliminated)

Reproduced again on the Android app, default mode resize: Claude's selection menu showed the top options missing / wrapped under the preceding prose with the keyboard up. Two distinct failures, both intrinsic to resizing the PTY:

  1. Reflow debris. The moment fitAddon.fit() shrinks the row count, xterm reflows the previous in-place TUI frame into the shorter grid before Claude's redraw arrives over the wire. Claude does not clear scrollback, so its redraw lands over the wrapped remnants — the "first options under previous text".
  2. Top-clip. A menu taller than the ~18-row band cannot be shown at full font; snapToBottom pins the bottom, so the top options scroll into scrollback.

The earlier (2026-06-08/2026-06-14) work made the timing correct (one settle-refit at the resting band height) but could not remove the reflow itself — that only went away while the server ran the per-client VtMux reflow, which has since been reverted to owner-exact PTY sizing. With no server-side reflow, the robust client-side answer is to not reflow the PTY at all while the keyboard is open.

Fix (useTerminalManager.ts, user-approved "auto fit-while-kbd-open"):

  • In doFit(), configuredKbMode === 'resize' is remapped to 'fit' while the keyboard is open (kbMode = configuredKbMode === 'resize' ? 'fit' : configuredKbMode). fit keeps the PTY at its keyboard-closed size and CSS- scales the whole grid to fit the band, so every option and the cursor stay visible with no reflow and no debris. scroll is unchanged. Closing the keyboard returns to the normal owner fitAddon.fit() path (the PTY was never shrunk, so it is a no-op).
  • Removed the now-unreachable kbMode === 'resize' branches (the mid-animation skip and the post-resize bottom-pin) and the dead keyboardSettling flag. The trailing settle-refit (scheduleKeyboardSettleRefit) is kept — it still lands the final scale after the IME settles.

Net effect: resize and fit are now equivalent while the keyboard is open; the setting still exists and only matters for scroll. vue-tsc -b green. Not yet committed or deployed; on-device verification pending.

2026-06-19 — resize reflows the PTY at natural font size (remap reverted)

Per the user's choice — the font size stays and the PTY is simply told to change its rows to fit — the 2026-06-18 resize->fit remap is reverted so the default resize mode reflows the PTY to the band while the keyboard is open, without any CSS scaling.

Fix (useTerminalManager.ts, doFit()):

  • Removed kbMode = configuredKbMode === 'resize' ? 'fit' : configuredKbMode; kbMode is now the configured mode directly. So resize no longer sets keepPtySize, and the owner reaches fitAddon.fit() even with the keyboard open — the container is the band above the keyboard, so the PTY reflows down to it at the natural font size.
  • applyScaleToFit() no longer runs after an owner fitAddon.fit(): a new filledContainer flag gates the trailing scale so it only applies where the grid was not sized to the container — a viewer rendering the owner's (larger) authoritative grid, or fit mode keeping the keyboard-closed grid. This keeps the font fixed in resize mode and avoids a transient font shrink mid-IME animation.
  • fit (keep PTY size, scale) and scroll (keep PTY size, natural scale, scrollable band) are unchanged.

Trade-off acknowledged: resize reintroduces the reflow path the 2026-06-18 note moved away from (xterm can briefly reflow the previous in-place TUI frame before Claude's redraw arrives, and a menu taller than the band can clip). Users who hit that can switch to fit. vue-tsc -b green. Not yet committed or deployed; on-device verification pending.

Hive - remote AI coding agents over WebSocket.