# Proof audit pass 1 — Android keyboard repair

Read-only audit of [#25847](https://git.cafe/pingdotgg/t3code/pull/25847), also mirrored as [#25847](https://github.com/pingdotgg/t3code/pull/25847), in `/home/ubuntu/wt/pr-25847`. Local branch `pr-25847`, requested local/unpushed head [03110f30ca](https://git.cafe/pingdotgg/t3code/commit/03110f30caa021ba422ee77a7b1322db35f0cc32). Diff is `git diff origin/main...HEAD`; verified merge base [1ebbc205f6](https://git.cafe/pingdotgg/t3code/commit/1ebbc205f6d99edd09882341ba1a420732287495). `origin/main` currently resolves to [5e9d56416f](https://git.cafe/pingdotgg/t3code/commit/5e9d56416f6b395095c772c597e79cd5f94e3052). Local `main` was not used. The head link will only resolve after the lead pushes it. No captures, emulator actions, app launches, worktree writes, pushes, or comments were made by this audit.

## Findings first

1. **A native split-screen recording without an event trace cannot establish this fix.** [#22484](https://git.cafe/pingdotgg/t3code/issues/22484), read with `cafe issue view` and `cafe issue comments`, distinguishes the cache/dismissal defect from [#22751](https://git.cafe/pingdotgg/t3code/issues/22751). Its current development baseline already keeps the composer visible. It reports a native Pixel Tablet repair sequence, but does not supply a deterministic recipe for every current emulator image. Capture the exact event order and imperative cache, alongside the affected real UI. An unsuccessful attempt is labelled as such; use the JS-event harness below for deterministic coverage.
2. **The Android impact is broader than the changed terminal file, but plain grep overstates it.** Android-reachable consumers include chat/draft geometry and send completion, sidebar suppression, Android bottom navigation, anchored menus, PR discussion spacing, new-task context pickers, and settings focus restoration. `WorkspaceRootScreen.android.tsx` replaces the generic root; `WorkspaceToolbar.ios.tsx` replaces the generic toolbar on iOS. The generic root height logic is iOS-reachable, while the generic toolbar's keyboard logic has no current shipped native caller. Do not spend Android capture time trying to reach those generic branches.
3. **K06 is a source-derived accessory boundary worth checking, not a claimed native reproduction.** After explicit accessory dismissal, a hide that has no `keyboardWillHide`, followed by didShow-only repair, leaves the external store true throughout. The component retains `dismissed: true`; the new didShow listener does not reset it because the snapshot never changed. A read-only VM execution of the actual store with mocked event bindings, applying the component's render-state rule, produced `didHide: store=true,dismissed=true` then `didShow: store=true,dismissed=true`. Record this case and report the resulting absent accessory; do not silently replace it with a normal willHide sequence. This is an edge of the repair scope, not proof that ordinary native dismissal produces that sequence.
4. **Existing tests do not cover the whole proof matrix.** The two cache tests cover hidden→didShow and dismiss waiting. The store test covers willShow→willHide→didHide→didShow. Completed-height correction (willShow(320)→didShow(376)), mounting after repair, no-willHide dismissal, real terminal geometry, and affected consumers need the rows below. The selector split is visually equivalent; normal mount/show/hide is its control, and a stable render trace can establish that it settles.

## Status and capture contract

`R` = capturable in the real app using the seed/route specified; `H` = labelled harness capture with the real JS consumer, because the exact event/state is not controllable through the ordinary UI; `X` = platform-excluded or unchanged branch, inventoried but not required on the Android pool. **All R/H rows are planned, not yet captured or runtime-verified.** `recording` is mandatory for every interaction, event sequence, loading transition, or timed row; an extracted still can supplement it. `still` is sufficient only for a held static guard state. `Δ yes` means this diff changes the relevant state/timing/layout; `control` means its visual output should stay equivalent. Optional rows enumerate unchanged guards and decorations without demanding a Cartesian product with every keyboard event.

Viewport codes are measured app windows, not screenshots resized afterward:

- **P**: pooled Pixel 9, portrait, default density/font scale, compact app window (roughly 400–430 dp wide). Record actual pixels, density, dp and OS/API version.
- **T**: pooled Pixel Tablet, landscape fullscreen, default density/font scale, app window at least 720×600 dp so T3's internal split layout is active. Typical tablet display is 2560×1600 px, but do not assume its density.
- **N**: the same tablet in Android multi-window, T3 pane below 720 dp wide while retaining at least 600 dp height; this is T3 compact layout within Android split-screen. Record pane bounds and IME top.
- **S**: compact stress window below 360 dp wide or landscape with height below 600 dp. Use the pooled device's supported window controls; a temporary `wm size`/density override is acceptable only on the leased emulator and must be restored. This is optional stress coverage.
- **D/L**: fixed app dark/light appearance through Settings → Appearance (not automatic). Keep the same IME/layout for the paired before/after. The code has no theme branch in event handling, so use D for all minimum rows and repeat the core terminal row T02 in L rather than multiplying every row by both themes.

Minimum proof is **the row×viewport×theme cells explicitly marked MIN** below. Before/after use the same fixture and event program on the verified merge base and requested head, not stale local `main`, a released APK, or the old issue recordings. Several rows may be endpoints in one continuous original-speed recording, but label each endpoint and keep the complete event trace. Native attempts and injected-event captures must have separate titles. No row claims Samsung Keyboard, One UI, a physical device, or iOS native verification.

## Fixture matrix

| ID / named fixture | Data, branch, visible result and interaction | Android | Δ | Seed / route / driver | Minimum cell; evidence | Status |
|---|---|---|---|---|---|---|
| K01 — hidden→shown repair | Hidden cache height 0; then didShow-only height H>0. Mounted real consumer changes to visible/H. Also read the imperative cache. | Yes | yes | Seed A; real terminal or draft mounted; event driver E1 below, with real IME already visible. | MIN P×D; recording + trace | H, planned |
| K02 — final height differs | willShow(h1) then didShow(h2), h1≠h2 and h2>0; corrected final height, no stale terminal grid gap. Use 320→376 only for state proof; geometry uses measured real heights. | Yes | yes | Seed A; E2. | MIN N×D; recording + trace | H, planned |
| K03 — dismissal completion | After K01, call real dismiss; mark call, native willHide/didHide and promise resolution. Resolution follows didHide; ordinary already-hidden dismiss remains immediate. | Yes | yes | Seed A; E3, with actual visible IME; do not mock native dismissal for this cell. | MIN P×D; recording + ordered transcript | R/H event setup, planned |
| K04 — mount after repair | Cache repaired before entering terminal; remount uses current visible/H and store subscription initializes from cache. Leave/re-enter or select another terminal while IME is retained. | Yes | yes | Seed A; E1 on chat, open terminal by route; head and baseline use same sequence. | MIN T×D; recording | H, planned |
| K05 — normal show / early hide | hidden→willShow→didShow→willHide→didHide. Accessory leaves at willHide; bottom inset drops at didHide; show button restores focus. No render loop after selector split; same-height didShow settles. | Yes | control | Seed A; real Show/Dismiss keyboard and Android Back; keep event/render-count trace. | MIN P×D; recording | R, planned |
| K06 — dismissed, no willHide | Start store=true/accessory shown; tap Dismiss (dismissed=true), deliver didHide without willHide, then didShow without willShow. Check accessory returns, or record retained dismissed latch described above. | Yes | boundary | E4 controlled delivery below. OS event recipe not established; exact JS boundary must be labelled. | MIN P×D; recording + trace | H, predicted retained latch; planned |
| T01 — editable terminal, hidden | Connected, authenticated operate permission, workspace exists, running shell, native surface, keyboard hidden; bottom safe-area padding and Android Attach output / Show keyboard row. | Yes | control | Seed A; terminal deep link then Android Back. | MIN P×D; endpoint in K05 recording | R, planned |
| T02 — editable terminal, repaired visible | Same shell, visible H; accessory min-height 52, terminal bottom inset H+52, Android hidden-keyboard row absent, input remains usable. Long output with prompt at bottom exposes clipping. | Yes | yes | Seed A; E1 then type harmless text; dismiss/reopen; accessory horizontal scroll. | MIN P×D, N×D, T×D and N×L; recording | R/H event setup, planned |
| T03 — modifier Ctrl active | T02; Ctrl selected (active style), tap again to deselect, then Ctrl+C to consume; modifier clears after input. | Yes | control, accessory availability yes | Seed A; real accessory; short terminal output and long scrollback covered by T02. | MIN P×D; endpoint in T02 recording | R, planned |
| T04 — modifier Alt / Meta active | Linux/Windows/unknown host produces ctrl+alt; Alt selected, then cleared/consumed. Scroll to remaining actions (Tab, Paste, Clear, ~, pipe, slash, dash); arrows/Esc type into shell. | Yes | control, availability yes | Seed A on local Linux; use harmless shell prompt, do not run destructive commands. | MIN P×D; endpoint in T02 recording | R, planned |
| T05 — mac-host accessory | Host `darwin` (or inferred Mac label) yields cmd+ctrl instead of ctrl+alt; meta active; Cmd+V uses device clipboard. | Yes; host OS differs from device OS | control | Local Linux server cannot truthfully be a Mac. Labelled harness supplies `serverConfigs.environment.platform.os=darwin` / unknown+Mac label to the same real terminal route; no claim of remote Mac transport. | Optional P×D; recording | H, planned |
| T06 — paste/clear result | Nonempty multiline clipboard, empty clipboard, clipboard read failure; clear resets terminal/modifier. Paste does not require an accessory layout variant beyond T02. | Yes | control | Seed A; actual clipboard for empty/nonempty; labelled Clipboard rejection for failure. | Optional P×D; recording | R/H failure, planned |
| T07 — read-only terminal, hidden | Authenticated terminal:read without operate; existing target; output visible, Show keyboard disabled, no accessory; Attach output remains when native module exists. | Yes | control | Seed B; explicitly target a live terminal opened from full-access client. | Optional P×D; still | R, planned |
| T08 — read-only terminal, repaired shown | T07 with globally visible IME/H. Inset H without +52; no accessory and no Android hidden-keyboard row; input remains disabled. | Yes | yes | Seed B; E1 while another input/retained IME owns focus, or labelled event-only harness. | MIN P×D; recording | H, planned |
| T09 — text fallback surface | Native module unavailable: status line + output/$ prompt + text input/Ctrl-C; no Attach output button. Operate running vs not-running/readOnly disable input. Keyboard repair still reserves H+52 for editable case. | Yes fallback, unavailable in correctly installed current client | yes for inset; control surface | Harness overrides only native-surface capability resolver, preserves screen/cache/hook/store and session driver. Include streaming-off/materialized buffer. | Optional P×D; recording | H, planned |
| T10 — capture empty | Native surface capture returns whitespace; Alert “No terminal output”; keyboard-hidden action remains reachable after repair/dismiss. | Yes | control | Seed A; Clear, hide keyboard, Attach output. If real prompt makes output nonempty, harness onCapture("  ") for exact empty branch. | Optional P×D; recording | R/H exact-empty, planned |
| T11 — capture nonempty | Three lines (short, blank, long wrapped); output sheet; select first/last range, cancel and attach; attach returns to chat if back exists. Captured text non-null toggles sheet. | Yes | control | Seed A; print harmless lines; dismiss IME; Attach output; select/attach. Long/oversize selection errors belong to unchanged TerminalContextSheet, not new keyboard branches. | Optional P×D; recording | R, planned |
| T12 — single short session/menu | One current shell; short project title; menu shows current selection, status, Text size and New terminal. Starting/running, subprocess ready/task-running labels. | Yes | control | Seed A; open Terminal options with keyboard hidden and repaired visible (also M01/M02). | MIN P×D; recording | R, planned |
| T13 — many / long session menu | 5 live sessions; long project title, long process/session label and cwd basename; one starting, one running task; current exited/error/closed may remain in menu, noncurrent nonlive filtered. Scroll menu and switch selected session. Empty metadata still inserts current session (not an empty menu). | Yes | control | Open 5 through terminal menu or terminal RPC; use local long-named scratch workspace and harmless long-named process, or labelled metadata harness for exact labels/statuses. | Optional P×D; recording | R/H exact status labels, planned |
| T14 — font bounds | Font min disables decrease, max disables increase; larger output/grid; normal size between bounds. Test keyboard-open terminal and hidden menu. | Yes | control | Settings Appearance → Terminal font / Terminal options Text size; use actual MIN/MAX constants. | Optional P×D; recording | R, planned |
| T15 — terminal identity / navigation | No terminalId picks existing running nondefault once; explicit id bypasses bootstrap; no sessions creates default for operator. New terminal/switch remount by env:thread:terminal key; same id selection no-op. Back goes back or replaces Thread when no history. | Yes | control; cache init yes | Seed A; thread header Terminal, menu New terminal, deep link without/with terminalId; direct launch with no back history. | MIN T×D; merge with K04 recording | R/H exact pre-repair, planned |
| T16 — staged project-script launch | Pending launch supplies cwd/worktree/env/initialInput, new terminal id; shell/project-worktree/detail-worktree/cached-grid precedence changes data but same keyboard layout. Exit chooses previous live shell or goes back, deferred while route unfocused; restart cancels pending exit. | Yes route; normal Android project-script controls are stubbed | control | Hardware command/deep link reaches terminal. Staged script needs labelled call to `stagePendingTerminalLaunch` plus navigation, since Android ThreadGitControls is stubbed; harmless printf command. No provider run needed. | Optional T×D; recording | R/H staged launch, planned |
| T17 — showcase focus gate | SHOWCASE=1 starts without autofocus; explicit Show keyboard increments focus request, repair visibility enables focus path. Same hidden/visible layouts as T01/T02. | Yes | yes state gate; control capture mode | Start Metro with EXPO_PUBLIC_SHOWCASE=1 in capturer-owned setup; pair/open terminal normally. Do not wait for coordinator three-environment showcase gates just to test this focus predicate. | Optional P×D; recording | R/H exact repair, planned |
| G01 — no selected thread, connections loading | selectedThread=null, connectionsReady=false; “Opening terminal…” full-screen loader. | Yes | control | Hold connection bootstrap in labelled query harness; ordinary disconnected cold launch is transient. | Optional P×D; recording | H, planned |
| G02 — missing thread, connections ready | selectedThread=null, connectionsReady=true; “Thread unavailable” with detail. | Yes | control | Invalid thread deep link in an otherwise ready connection. | Optional P×D; still | R, planned |
| G03 — thread without workspace | selected thread/project missing workspaceRoot; “Terminal unavailable” and nowhere-to-open-shell detail. | Yes | control | Labelled selection fixture with project workspaceRoot absent; normal projects require a root. | Optional P×D; still | H, planned |
| G04 — presentation not initialized | Workspace exists, environment.isReady=false and presentation=null; loader. | Yes | control | Hold environment-presentation bootstrap in labelled harness. | Optional P×D; recording | H, planned |
| G05 — disconnected / retry error | Presentation exists but phase not connected; EnvironmentConnectionNotice with environment label fallback and error/retry state; menu absent. | Yes | control; outer Android bottom padding can react | Seed A then stop only its tracked dev server or disconnect disposable connection; Retry after restarting. Hold exact error via harness if needed. | Optional P×D; recording | R/H held error, planned |
| G06 — checking permissions | Connected; session data=null,error=null; “Checking terminal access”. | Yes | control | Hold auth.session response in labelled query harness. | Optional P×D; recording | H, planned |
| G07 — no terminal grants | Auth resolved, neither read nor operate; “Terminal access unavailable”. Unauthenticated is same visual state if no session error. | Yes | control | Seed B variant with terminal scopes omitted; direct route bypasses disabled entry. | Optional P×D; still | R, planned |
| G08 — terminal access lookup error | Session error nonnull, no read/operate; “Could not check terminal access”, error detail. | Yes | control | Labelled auth query rejection while presentation stays connected. | Optional P×D; still | H, planned |
| G09 — operator location loading | Operate=true, launchLocation=null, sessionsError=null; “Loading terminals”. Font preference loading delays attach but is not a new text branch. | Yes | control | Hold metadata/location resolution; no staged pending launch. | Optional P×D; recording | H, planned |
| G10 — terminal list failure | Operator launchLocation=null + sessionsError, or observer no target + error; “Could not load terminals” with detail. | Yes | control | Labelled metadata query rejection; same visible fixture for both predicates. | Optional P×D; still | H, planned |
| G11 — observer list loading | Read-only, no target, sessionsPending=true; “Loading terminals”. Same text as G09, different gate, no new screenshot needed. | Yes | control | Seed B; hold metadata response. | Optional; reuse G09 image only, log predicate separately | H, planned |
| G12 — observer no sessions | Read-only, no explicit/known target, resolved empty metadata; “No terminal sessions”. | Yes | control | Seed B; close all disposable shells; terminal route without terminalId. | Optional P×D; still | R, planned |
| G13 — observer target error | Read-only target exists but terminal.error nonnull; “Terminal unavailable” with returned error. | Yes | control | Labelled terminal.observe failure after selecting explicit target. | Optional P×D; still | H, planned |
| C01 — retained composer, resume | Ready short chat, nonempty retained draft, owned input focused; background/resume/split-screen/rotation; composer should remain visible both versions. New state clears Android suspect quarantine after a repair report; show cache trace. | Yes | yes state; visual change not guaranteed | Seed A; chat deep link, type unsent draft; Native attempts N1–N3 then E1 if needed. | MIN N×D; recording | R attempt / H guaranteed event, planned |
| C02 — send, end-follow on | Ready chat, submitted message becomes next anchor (hasStartedTurn/user-message/queue conditions chosen accordingly), connected and not deferred; blur→dismiss awaited→scroll-to-end. Long feed exposes phantom inset. | Yes | yes dismissal timing | Seed A; harmless test-provider run or labelled successful send response on same real handler. E1, Send, capture didHide before scroll. Do not claim live provider success from harness. | MIN P×D; recording + ordered trace | R/H controlled backend, planned |
| C03 — send, end-follow off / aborted | C02 but user scrolls during dismissal or navigates to another thread; no forced end scroll / stale async result; nonready content or different anchor skips dismiss-await path. | Yes | yes wait boundary; skip branch control | Seed A; E1, Send then scroll or navigate before native hide; labelled timing hold if actual hide too fast. | Optional P×D; recording | R/H scheduling, planned |
| C04 — usage limit overlay | Usage report exists, no active user-input request; limited-height panel reserves last positive keyboard height; final didShow height differs. Pending question hides usage panel (same cache still updates); short/tall panel contents. | Yes | yes height | Labelled provider/usage-report fixture: one exhausted source, long reset text; actual ThreadDetailScreen, E2. No need to exhaust real subscription. | MIN N×D; recording | H, planned |
| D01 — scratch draft hero | Draft mode, scratch hero with no conversation feed, keyboard H changes hero bottom clearance/max height. Short vs multiline draft text and wrapped project/environment labels. | Yes | yes height | App New chat/new-task scratch; pair environment first; type retained draft; E1. | MIN P×D; recording | R/H event setup, planned |
| D02 — project draft context | Draft tied to Git project, branch/worktree/environment controls present, long project/branch label, draft interaction unlocked. Height reduction differs from scratch hero content. | Yes | yes height | New task choose local Git project; no clone/import in progress; E1. Loading/clone/import lock disables pickers but adds no dismissal behavior. | MIN N×D; recording | R/H event setup, planned |
| D03 — choose project after repair | Nonempty draft; chooseProject blurs, dismiss(animated:true), navigates NewTaskProject; dismissal promise ignored. Draft preserved on return. Locked draft does nothing. | Yes | cache changes promise, control navigation | D02 → project control; no project creation/submission needed. | MIN P×D; recording | R/H event setup, planned |
| D04 — choose branch after repair | Same but NewTaskBranch list/search; keyboard dismiss not awaited; selected branch preserved on return. | Yes | cache changes promise, control navigation | D02 → branch/workspace selector. | MIN P×D; recording | R/H event setup, planned |
| D05 — choose environment after repair | Same but NewTaskEnvironment list; one connection suffices, long label stresses wrapping; cancel/return retains draft. | Yes | cache changes promise, control navigation | D02 → environment control. | MIN P×D; recording | R/H event setup, planned |
| S01 — settings focus handoff | Existing chat: focused/repaired visible→settings opening→visible→dismiss→restoring→closed; settings blur/IME departure overlap and focus returns. | Yes | yes cache fallback, promise ignored | ThreadComposer model/settings control → ThreadSettingsSheet; cancel/back. Record transitions, not only final sheet. | MIN P×D; recording | R/H event setup, planned |
| S02 — draft settings / no-focus control | Draft settings route NewTaskThreadSettings; initial editor not focused + cache hidden should not restore keyboard; repaired visible with editor focus-ref false uses isVisible fallback to restore. Reopen during restoring tests phase fallback. | Yes | yes fallback; hidden control | Draft model/settings control; use labelled focus-ref/other-input boundary for exact fallback, with real presentation hook. | MIN P×D; recording | R/H exact focus-ref state, planned |
| W01 — compact Home bottom navigation | Home/threads or PR list with keyboard hidden→repaired visible→hidden; nonrail native bottom nav removed/restored. Empty/single/many list records do not change this branch. | Yes | yes | Seed A; Home Search, E1; retain query; Android Back and type. | MIN P×D; recording | R/H event setup, planned |
| W02 — compact Settings bottom navigation | Settings nested list search; same nonrail component, different AdaptiveWorkspaceLayout parent with bottom safe area zero; hide/reappear around repair and Back; navigation resets correctly. | Yes | yes | Settings search; E1; Back once to hide IME, then select another tab. | MIN P×D; recording | R/H event setup, planned |
| W03 — wide composer/sidebar | Internal split layout, detail chat, sidebar preferred visible, composerEditing=true: repaired IME suppresses sidebar/rail; hide restores preference. Toggle sidebar / command palette or keybinding dismisses keyboard first. | Yes | yes | Seed A; T viewport with chat; focus composer, E1, toolbar/sidebar toggle. `toggleSidebar` command also reachable from command palette/hardware binding. | MIN T×D; recording | R/H event setup, planned |
| W04 — wide sidebar preference / rail controls | User previously hid sidebar; repaired IME must not permanently change preference. Reveal sidebar/rail selection dismisses keyboard when active. No composerEditing, pinned list root, Settings/Usage workspace pages retain their control layout; rail=true never returns null just because keyboard is visible. | Yes | yes only composer guard; others control | T viewport; toggle user preference, repeat focus/repair/hide, open Home/Settings/Usage; real rail overlay selection. | MIN T×D; recording, can share W03 clip | R/H event setup, planned |
| M01 — top short menu, hidden→repair | Top anchor, short flat actions, title absent/present; hidden opens native MenuPopup; repaired visible before opening selects in-window preserving-focus menu. Tap first item; IME stays up. | Yes | yes | Home Search → Filter threads or settings model search → Filter models. E1 before opening. Actual actions; no forge writes. | MIN P×D; recording | R/H event setup, planned |
| M02 — bottom / tall submenu | Bottom anchor (terminal options or composer attachment), enough actions to scroll; opens upward/maxHeight clamp, submenu/back, long title/subtitle, hidden/disabled/check-state items; first tap and Android Back/outside close preserve focus. | Yes | yes usable height/presentation | T02 terminal options or settings Filter models→Provider. Long/empty/custom menu action shapes via labelled menu harness if real catalog too small. Use `AndroidAnchoredMenu`, not a drawn mock. | MIN P×D and N×D; recording | R/H action extremes, planned |
| M03 — menu already open during repair | Open with keyboardWasVisible=false freezes native presentation despite subsequent repair; open with true stays inline as height changes. Snapshot direction/presentation should not switch mid-menu; recomputed usableBottom must keep items tappable. | Yes | yes live height; snapshot control | M01; E1/E2 while menu open; may need harness to retain real IME behind native popup. | MIN N×D; recording | H, planned |
| M04 — narrow / hinge placement | S width <360, large text scale, long actions; horizontal placement clamps. No-hinge Pixel devices are real; hinge coordinates cannot truthfully be produced by these pooled models. | Yes except physical hinge | control | S window for width; labelled WindowHingeContext fixture for hinge (never claim foldable verification). | Optional S×L; recording | R/H hinge, planned |
| P01 — PR discussion reply visible | One review thread, one short + one long comment, multiline draft reply; canReply=true; repaired visible switches composer bottomInset to 8 and scroll padding composerHeight+8+24. Return hide restores max(safeBottom,12). | Yes | yes | Seed C; open PR activity discussion; type unsent reply only; E1. No reply submission, resolution or comment write required. | MIN P×D; recording | H unless disposable PR fixture available, planned |
| P02 — PR discussion without reply | canReply=false or thread missing/loading; no reply surface; list padding fixed 24 despite keyboard hook update. Errors/notice/pagination/resolved/outdated/comment counts are unchanged content variants. | Yes | control | Seed C permission/missing/loading variants; same real Discussion renderer. | Optional P×D; recording for loading, still if held static | H, planned |
| U01 — undo toast | Notice absent/visible=false is null; visible Settled/Snoozed/Unpinned toast expires at 5s or Undo/swipe/replacement. On Android bottom=insets.bottom+12 regardless of keyboardHeight: no repair-driven placement change. | Yes; keyboard-driven geometry is iOS-only | control | Disposable thread → settle/snooze/unpin; focus Home Search; E1; undo or let expiry happen. HomeRouteScreen and ThreadNavigationSidebar callers share component. | Optional P×D; recording | R, planned |
| I01 — iOS terminal branches | iOS top safe area, terminal background, Attach visible output while native surface present, floating Show keyboard when hidden/operable; no Android safe-bottom row; show/accessory and theme branches retained. | No | final didShow cache may affect iOS; Android repair is not native iOS input | Inventory only; not available on Android pool. An Android JS harness cannot verify UIKit native terminal/focus. | No Android minimum | X, native iOS out of scope |
| I02 — iOS root / undo geometry | Generic WorkspaceRootContent: compact/pad/usage-overlay/Duo/system-tabbar/sidebar search clearance variants consume height. ThreadUndoNotice: phone/pad/Duo hinge/compact/vertical-bar placement and workspace search-clearance variants. | No for these height branches | hook final height may change | Android root is replaced; Android undo bottom ignores height. Logical formula harness possible, but not native iOS proof and not part of Android claim. | No Android minimum | X, platform excluded |
| I03 — iOS V5 vertical header | Visible keyboard adds dismiss item only with native metrics + vertical edge, detail/nonprimary or compact primary; regular primary/no edge/hidden excludes item. Dismiss calls library. | No (`.ios.tsx`) | final visibility/state can change | Native UIKit header cannot be captured faithfully on pooled Android; retain as explicitly unverified iOS regression surface. | No Android minimum | X, platform excluded |
| F01 — generic WorkspaceToolbar fallback | workspace=null returns null; resting/active/expanded search, visibility changes navigation dock/show-tabs, Back dismiss vs cancel, width<360 gaps; only generic root imports it, Android root replaces caller and iOS toolbar replaces file. | No current shipped native caller | code-level hook behavior yes, unreachable native branch | Labelled isolated component harness only if testing unsupported fallback platform; not a real Android app route. | No Android minimum | X, unreachable fallback |

The minimum viewport/theme schedule is:

| Viewport × theme | Required row endpoints (may share continuous recordings) |
|---|---|
| P × D | K01,K03,K05,K06,T01,T02,T03,T04,T08,T12,C02,D01,D03,D04,D05,S01,S02,W01,W02,M01,M02,P01 |
| N × D | K02,T02,C01,C04,D02,M02,M03 |
| T × D | K04,T02,T15,W03,W04 |
| N × L | T02 |

This is 35 row×viewport×theme cells; row reuse across a recording is allowed, not silent omission. Each after endpoint uses the requested head and has a matched baseline endpoint. If a native attempt yields the exact traced sequence, it can replace that row's event-injection setup; it cannot replace other consumer rows by implication.

## Source coverage and caller inventory

The only production component file changed is [ThreadTerminalRouteScreen.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx); `ThreadTerminalRouteScreen` calls private `ThreadTerminal`, which calls `TerminalHeader`. Every guard is G01–G13. Route params normalize missing/empty values and string-array first entries via firstRouteParam; these collapse to the valid explicit/default identity fixtures T15 or unavailable fixture G02. Invalid/missing environment ids collapse to disconnected/unavailable guards. Selection is the route-bound thread, not a globally selected different thread. The remaining render predicates are keyboard visible/height, accessory dismissed/store, operate/read grants, native surface and output revision, Android/iOS, SHOWCASE, capture output null/non-null, modifier active, toolbar label length>1 (min widths 56 vs 44), font min/max, current session count/status/cwd/labels, themeId/appearance and measured safe area/window/grid. T01–T17 and I01 cover those without multiplying unchanged content by every keyboard event.

Callers/entry points of the changed screen:

- [Stack.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/Stack.tsx:780) registers `ThreadTerminal`, deep-link path `threads/:environmentId/:threadId/terminal`, optional query terminalId.
- [ThreadRouteScreen.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/threads/ThreadRouteScreen.tsx:662) opens existing/new shells and stages project scripts. Android [ThreadHeader.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/threads/ThreadHeader.tsx) has a Terminal action; generic git/script controls are stubbed by `ThreadGitControls.android.tsx` (T16 therefore labels script staging as harness).
- [HardwareKeyboardCommandProvider.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx:178) handles terminal command/keybinding and the command palette. [useTerminalExitNavigation.ts](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/terminal/useTerminalExitNavigation.ts) replaces the screen with the previous live shell on exit, or returns to chat. Screen menu switches/new shell also replace the route (T15/T16).
- [ShowcaseCaptureCoordinator.tsx](/home/ubuntu/wt/pr-25847/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx:249) opens term-1 only when EXPO_PUBLIC_SHOWCASE=1 and its multi-environment fixture gates are met. SHOWCASE disables initial autofocus until a focus request or visible state; label such a capture. Ordinary minimum uses SHOWCASE off. Do not rely on the showcase terminal fixture's canned output to prove a real PTY session.

All direct app consumers found by `rg 'useKeyboardState|KeyboardController\.(isVisible|dismiss|state)' apps/mobile` (excluding tests), with platform resolution checked:

| Consumer | Values / changed behavior under didShow-only | Android reachability / caller | Matrix |
|---|---|---|---|
| AdaptiveWorkspaceLayoutContent | isVisible; deferred composerKeyboardActive controls sidebar/rail; dismiss in toggle/reveal | Yes; wraps workspace stack, toolbar/command/keybinding entry | W03,W04 |
| ThreadDetailScreen | visibility/height clears resume suspect flag; last height reserves usage panel; draft hero clearance; awaited dismiss before send scroll | Yes; ThreadRouteScreen's chat and NewTaskDraftScreen through ThreadScreenBinding | C01–C04,D01,D02 |
| ThreadTerminal / store | visibility, height, isVisible init, dismiss; layout/focus/accessory | Yes; all entries above | K01–K06,T01–T17,G01–G13 |
| NewTaskDraftScreen | dismiss on project/branch/environment picker, not awaited | Yes; new-task controls; locked branch returns without action | D03–D05 |
| useThreadSettingsSheetPresentation | isVisible fallback in restore intent; dismiss not awaited | Yes; ThreadComposer uses existing-thread and new-task settings routes | S01,S02 |
| AndroidWorkspaceNavigation | isVisible returns null for nonrail; rail remains mounted | Yes; WorkspaceRootScreen.android compact root and AdaptiveWorkspaceLayout compact Settings / wide rail | W01–W04 |
| AndroidAnchoredMenu | visibility snapshot chooses native vs inline; live height limits usable area | Yes; direct callers and Menu.android below | M01–M04 |
| PullRequestDiscussionScreen / Discussion | visible → reply bottomInset=8 vs max(bottom,12); scroll inset follows when reply exists | Yes; PR discussion navigation, no linking path declared for this route | P01,P02 |
| ThreadUndoNotice | reads height; Android final bottom ignores it | Yes component; height-dependent layout no | U01,I02 |
| WorkspaceRootContent (generic) | height enters bottomClearance branch formulas | No; .android replacement | I02 |
| WorkspaceToolbar (generic) | visibility changes dock/show-tabs and Back logic | No current native caller; .ios toolbar replaces file and Android root never calls it | F01 |
| V5StackHeader.ios | visible vertical-bar dismiss item; dismiss command | No | I03 |

There are no other direct application `KeyboardController.state()` readers outside tests. The patched library's own useKeyboardState reads state(); KeyboardToolbar/compound buttons use appearance, but this app does not import those toolbar components. `KeyboardStickyView`, `KeyboardAvoidingView`, `useReanimatedKeyboardAnimation`, `useKeyboardChatComposerInset`, native composer/terminal IME motion and React Native `Keyboard.isVisible()` are separate paths; do not claim the patch directly changes their animation protocol.

AndroidAnchoredMenu fan-out is collapsed by common popup behavior, not omitted: direct call sites are ThreadSettingsSheet (model filters), ThreadListToolbar, FilesHeader, PullRequestToolbar.android, GitOverviewSheet, SettingsEnvironmentsRouteScreen, AndroidScreenHeader overflow and Menu.android. `Menu.android` serves AppBar.android (including TerminalHeader), ThreadComposer send long-press, ComposerAttachmentButton, thread feed/file chips, ThreadQueueControl, thread list/sidebar/swipe/filter menus, LinkedPullRequestList, PR rows/activity/files/reactions/status/merge/agent/guide/outline controls, file/attachment/media actions, device/browser menus, UsageRouteScreen, T3ConnectProfilePage, SettingsScheduledTasksRouteScreen, SettingsEnvironmentFilterHeader and development component catalog. These callers differ in title/action count/anchor/submenu/hidden/disabled/check state/long-press or render-prop trigger, not in keyboard algorithm. M01–M04 cover those shapes; no merge/post/send action is necessary to validate menu selection/placement.

## Seeds and real environment reachability

**Seed A — ordinary real connected environment.** Capturer uses test-t3-mobile: open the leased Android device via device_list/device_open and use exactly its returned agent-device command/target args (or the already-authorized pool session). Reuse the lead's healthy isolated server, or let the lead start one with `vp run dev` in the requested checkout; use its reported port and `.t3` state. Install matching dev client with `vp run mobile:install android --session <session>` for a remote pooled session (or device id on device host), start/reuse Metro for this checkout, and open its dev-client URL. Pair through `.agents/skills/test-t3-mobile/scripts/pair-client.sh <port> <isolated-base-dir> <device-reachable-origin> <agent-device-command> <target-args...>`. Both backend and Metro must be reachable from the device host. For same-host Android backend use 10.0.2.2, not the emulator's localhost; remote pool needs a reachable host/tailnet origin. Do not publish pairing credentials.

Use an existing seeded stopped thread in a project whose workspace exists on the server. If absent, add a disposable local Git workspace/project and thread through the app; retain a harmless draft and a short/long conversation fixture. The default standard pairing grants terminal:read and terminal:operate (`packages/contracts/src/auth.ts`), so **a terminal session is reachable on a local dev server**: Thread header → Terminal, or agent-device open `t3code-dev://threads/<environmentId>/<threadId>/terminal`. The route attaches/starts a real server PTY when connected, authenticated, permissions resolved, font preferences ready and cwd/worktree location resolved. It does not require a running provider agent or provider authentication. Use e.g. harmless printf output to fill scrollback; record shell prompt/output and returned input, not only a terminal-shaped view. A connected mobile UI is insufficient if workspaceRoot/cwd does not exist on the server. Provider selection does not affect this terminal path.

**Seed B — real permission variants.** From the isolated server's auth CLI create a separate disposable pairing credential: `T3CODE_PORT=<port> node apps/server/src/bin.ts auth pairing create --base-dir <isolated-base-dir> --base-url <device-reachable-origin> --ttl 15m --label proof-terminal-read --scope orchestration:read --scope terminal:read`. The repeated `--scope` replaces the standard defaults. Open the returned Pair URL using the existing Add Environment pairing route (do not publish it). Full-access client starts the target PTY first. Read-only mobile connects with that credential. No-grant variant uses only `--scope orchestration:read`; direct terminal navigation displays the denial fixture. Do not edit auth SQLite tables. If pool pairing policy cannot grant arbitrary scopes, those static states are labelled H fixtures at the query/session boundary; this does not excuse skipping the operate-enabled real PTY minimum.

**Seed C — PR discussion.** Ordinary dev seeding does not guarantee a PR or review thread, nor forge credentials. Preferred: an existing disposable/readable PR fixture with a review thread; app PR activity→discussion→type draft only. Otherwise label the capture `HARNESS — real Discussion renderer, fixture PR RPC results`: supply detail/activity/permissions matching its contracts, a known discussionId, one short and one long comment, canReply=true, canResolve=false and no mutations; mount/navigate the actual exported PullRequestDiscussionScreen under normal providers. P02 supplies no-thread/pending/no-reply/error variants. Do not post a comment or resolve a real thread to obtain proof. The existing production route has no direct deep-link declaration; do not invent one.

**Held-state harnesses.** G rows cannot all be reliably held by normal UI gestures. Capturer may create a disposable harness bundle/fixture under their own scratch checkout/config (never modify the lead's worktree for this audit), with actual exported route components and existing navigation/theme/atom providers, supplying only selection/query/native-capability boundary data listed in the row. These are screenshots of real components with controlled data, explicitly labelled H, not claims of successful backend auth/PTY behavior. For the minimum, only K/E event delivery, C04 usage report, S02 exact focus-ref boundary, P01 discussion data and M02/M03 action/timing extremes may need harness plumbing. Ordinary terminal must use Seed A's real connection/PTY. Optional unchanged guard/decorative fixtures are listed for completeness, not blockers to minimum proof.

No UI theme/list-count/label-length branch in changed code selects keyboard behavior. One short and one long terminal output, one/five menu sessions, empty/nonempty capture, min/max font and modifier inactive/active cover distinct terminal endpoints. Generic error/loading branches collapse only when their rendered text and structure are identical (G09/G11); the predicates remain logged separately. Network local/remote/relay/tunnel, other providers, full worktree cwd choices and streaming/native revision differences reuse the same JS event handlers; this audit makes no extra end-to-end transport/provider claims.

## Native attempts — concrete but not deterministic

Read current leased emulator serial, package/activity and display settings; commands below are executed on the device host with `adb -s <leased-serial>`, never another pooled device. Record native KeyboardController events plus cache before, during and after. `KeyboardAnimationCallback.onApplyWindowInsets` calls syncKeyboardPosition only if height changed, no transition/interactive move is active and it is not suspended; the ordinary animation path may run instead. No `adb` command promises to bypass willShow. [Android's multi-window guide](https://developer.android.com/develop/ui/views/layout/support-multi-window-mode) also documents version/device-dependent entry paths.

- **N1 split-screen/resize:** Tablet landscape, real T3 dev chat with focused retained draft and Gboard shown. `adb -s <serial> shell input keyevent KEYCODE_APP_SWITCH`; use AgentDevice's current UI snapshot to choose the T3 task icon's Split screen action and Chrome/Settings as the other app. Return focus to the retained T3 composer, drag divider wider/narrower, exit/re-enter split-screen. Also try Chrome-first taskbar drag into the other pane, but label it separately from the focused-T3 recipe. The issue reports a matching Pixel Tablet trace with focused-T3 entry; this does not make current-image reproduction reliable. `am stack`/task window-mode numbers are version-dependent; do not blindly run old docking recipes.
- **N2 background/resume:** From a focused composer, `adb -s <serial> shell input keyevent KEYCODE_HOME`, then resume the same task by `adb -s <serial> shell am start -n <resolved-com.t3tools.t3code.dev/MainActivity>`. Resolve the activity first through package manager rather than guessing a different app id. Retain process/draft; force-stop is a cold launch, a different fixture. Expected possibility is resumed nonanimated inset repair, not guaranteed.
- **N3 rotation/size:** Inspect `adb -s <serial> shell cmd window help`, preserve original user-rotation mode, then `adb -s <serial> shell cmd window user-rotation lock 1` and `... lock 0` while composer or terminal owns input. Actual portrait/landscape depends on natural device orientation. Optional `adb -s <serial> shell wm size <width>x<height>` changes display bounds; restore the exact prior override (or reset if none). These are resize attempts, not deterministic IME event commands. [AOSP's WindowManager shell implementation](https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/wm/WindowManagerShellCommand.java) defines user-rotation syntax; use the emulator's help as authority for its build. See [ADB documentation](https://developer.android.com/tools/adb) for am/pm/shell invocation.

Success criterion: trace contains didHide(height=0)→didShow(height>0) with no intervening willShow, and preferably duration=0. A visible keyboard alone, a composer that already stayed above it, or a normal willShow/didShow sequence is not native reproduction of this defect. One documented attempt per mechanism is enough before switching to the deterministic harness. Do not spend the capture budget fishing for a particular OS scheduling accident.

## Deterministic fallback — inject into the real running JS layer

Use Metro/Hermes inspector, no native binary edit needed for event injection. The repository already uses Metro `/json/list` and CDP Runtime.evaluate with Origin matching Metro in `apps/mobile/scripts/measure-renders.mts` (`connectInspector`); copy only that connection pattern into a scratch driver, not its benchmark/provider-launch workflow. Select the **com.t3tools.t3code.dev** target for this leased device/Metro. In dev, Metro `globalThis.__r.getModules()` exposes initialized modules and verboseName; resolve initialized modules by suffix/path and fail closed on zero/multiple matches, never guess numeric IDs. Read module exports from `__r(id)`.

Resolve:

- React Native `Libraries/EventEmitter/RCTDeviceEventEmitter.js` default export;
- the initialized react-native-keyboard-controller module exporting KeyboardController (and bindings exporting KeyboardEvents);
- terminalKeyboardShowing's exported getter when terminal is loaded.

Installed library `src/bindings.native.ts` registers on NativeEventEmitter with names `KeyboardController::keyboardDidHide`, `::keyboardDidShow`, `::keyboardWillShow`, `::keyboardWillHide`. React Native NativeEventEmitter forwards through the shared RCTDeviceEventEmitter. Emit **those names** into this real emitter; do not call private test Maps, replace the hook/store, or mutate KeyboardController.isVisible/state directly. Keep actual KeyboardProvider, terminal/chat/menu consumers and native IME installed. Add recording title/sidecar `HARNESS — injected KeyboardController native-event sequence into real JS; not native reproduction`.

Install listeners for all four event names before each program; store event sequence number, name, height, duration, timestamp, target, type, appearance and post-event imperative isVisible/state().height; read the terminal store getter too. Queue a post-React-commit observation or await a visible UI milestone for the consumer result. A cache log alone does not prove a mounted hook. Use one CDP evaluation per event/phase so React can commit; do not batch hide/show into one act that conceals the intermediate layout. Store H from a genuine native show event in that exact viewport/density, using its target/type/appearance; synthetic state-only 376 must be labelled and must not be used for physical geometry assertions. Observe/draw the real screen in the recording; do not use the synthetic timestamp as a timer.

- **E1 basic repair:** With actual IME shown on the selected screen and no real animation underway, emit didHide({...lastRealEvent,height:0,duration:0}), allow UI commit, then emit didShow({...lastRealEvent,height:H,duration:0}) with no willShow. Read cache and real consumer UI. For terminal store hidden→shown coverage, additionally start from a genuinely hidden store or explicitly deliver willHide before the hide; that extra willHide is a distinct labelled program, because changing store state can blur terminal native focus.
- **E2 completed height:** With real IME settled at measured H, emit willShow with h1≠H, allow commit, emit didShow H, allow commit. Native animation values remain the genuine IME's state: claim correction of cache-driven terminal/draft geometry, not a new native keyboard animation. Use synthetic 320→376 only when showing state readouts, not screenshots claiming a matching 376-dp OS IME.
- **E3 dismiss order:** After E1 returns visible/H, register trace of native didHide and Promise settlement, invoke the real KeyboardController.dismiss(); keep native function and event delivery intact. Native didHide must precede settlement; already-hidden control resolves immediately. In real chat send, log/observe scroll completion after hide. If a controlled event-order harness replaces native dismissal to hold it pending, label that separately and retain the real-IME recording as the integrated path.
- **E4 no-willHide accessory boundary:** On the actual terminal accessory Dismiss interaction, temporarily intercept **only** RCTDeviceEventEmitter.emit delivery of `KeyboardController::keyboardWillHide` to reproduce a missing willHide; forward every other event and preserve `this`. Restore emitter immediately after native didHide. Then deliver didShow(H) without willShow and inspect accessory/store. Native IME may now be closed: this cell is explicitly JS-boundary/state proof, not proof of physical geometry. If a visible-IME version is wanted, use a labelled harness that retains native focus while feeding the same events; never claim the synthetic show opened Android's IME. This exact sequence is expected to retain the dismissed latch on the audited head.

For terminal repair/store tests with a real visible IME and a hidden store, another labelled boundary variant can suppress only delivery of the next native willShow while using actual Show keyboard/native focus; native didShow still arrives and exercises library/hook/store. This variant has normal native duration, so it proves didShow subscription, not duration-0 Android resync. Record precisely which program ran.

The driver covers K01–K06, T02/T08 inset changes, C/D/S/W/M/P consumers. It cannot prove syncKeyboardPosition itself ran, native Start/Move/End animation dispatch, Samsung taskbar/keyboard behavior or iOS UIKit. Native repair also dispatches animation lifecycle events; JS-event injection intentionally leaves real settled native geometry in place. That limitation must accompany every harness capture.

Remove injected listeners/temporary emitter interception, restore display/orientation overrides, disconnect debugger and dispose harness connection at teardown. All captures use the requested head/merge-base fingerprints and matching native client. The proof matrix is complete and capturable on the stated Android pool with explicitly labelled harness fallbacks; native scheduling reproducibility and iOS/physical-device verification are not claimed.

FRICTION:
model | The first schedule omitted T08 although its fixture row was marked MIN; a consistency check caught this, and the schedule was corrected before the final upload. | Generate/check the schedule against every MIN row before uploading.
VERDICT: SUFFICIENT
