# Proof audit pass 1 — question answer IME ownership

PR [#29420](https://git.cafe/pingdotgg/t3code/pull/29420), issue [#29349](https://git.cafe/pingdotgg/t3code/issues/29349).

Reviewed branch `fix/issue-29349`, head [f16d8770bd](https://git.cafe/pingdotgg/t3code/commit/f16d8770bda7acff940ed18a3557c050aea6651e), against merge-base [ac8dce0cc3](https://git.cafe/pingdotgg/t3code/commit/ac8dce0cc3abd8a337f2d806b5082a6834b9b81a). Live PR metadata and remote branch tip match the requested head. `git merge-base HEAD origin/main` confirms the requested baseline; local `main` is older and must not be used for captures. One changed file, 17 additions and 2 deletions.

## Findings

1. **Evidence is insufficient at these revisions.** The description's prior table mostly uses earlier commits. It is context, not accepted proof for this pass. No recordings were captured or assessed in this pass.
2. **The essential proof is temporal.** Local text state, the synchronous `writtenText` assignment, the draft echo, and adoption of externally cleared drafts all need recordings. Final screenshots cannot establish caret continuity, subsequent character order, clearing before retyping, or submission timing. A mocked TextInput or whole-string text injection cannot prove a native IME fix.
3. **Include mid-text editing.** The issue describes a displaced caret; an independent PR comment reports reversed `xyz` in the middle of `Hello world`, both with and without suggestion acceptance. That report is an unverified reproduction lead, not proof that this head fails. End-of-field autocorrect alone does not cover this reported trigger.
4. **The fixture producers are real supported paths.** `fake-fx.ts:125` emits ACP `elicitation/create` with a `choice` string and First/Second enum. `environment.ts:34,107,239` registers it as `evalbot` and launches it via `orchestration.launchThread`. ACP maps properties into questions without disabling custom answers (`packages/provider-acp/src/server/adapter.ts:6404`). Pi's `editor` maps `prefill ?? ""` into `initialAnswer` and supplies zero options (`packages/provider-pi/src/server/adapter.ts:3312`); tests at `adapter.test.ts:3411` establish exact edited and empty response values. Evalbot's stock question does **not** set `initialAnswer`, so it cannot stand in for multiline proof.
5. **Two lifecycle paths matter.** Android collapse removes the card/input; iOS collapse retains it behind pointer/accessibility guards (`PendingUserInputCard.tsx:66,147`). The new state must survive each path through the shared draft, and request/question changes must start with the correct draft.

## Render and interaction inventory

Only `QuestionAttachments` changes. Its only caller is the question map in `PendingUserInputCard.tsx:310`, keyed by request ID and question ID. The card's only caller is `ThreadDetailScreen.tsx:1678`. `ThreadRouteScreen` supplies `useSelectedThreadRequests()` through `ThreadScreenBinding` → `ThreadScreenHost`/outlet → `ThreadDetailScreen`; the detail body is keyed by environment and thread. The native route is `Thread`, path `threads/:environmentId?/:threadId?` (`Stack.tsx:724`). No Settings, palette, keybinding, web, or desktop rendering of this component exists. Providers converge on the same normalized question, so provider-by-provider recordings add no new branch; Pi is needed specifically for the multiline shape.

| Code branch / data dimension | Distinct behavior or layout and relevant interactions | Capture disposition |
|---|---|---|
| No active request; zero questions; no selected thread; `allowCustomAnswer:false` | No changed input; the parent skips it for forbidden custom answers, and the component also returns null without a selected thread/custom permission. | Code inspected; no independent capture required. |
| One/many questions; request/question/thread identity | One independently keyed input per permitted question; shared answer atom can rerender siblings. New keys remount local state. `questions` also totals sibling attachments. | A4 covers remount and identity. Many sibling questions use the same input branch; no separate count grid. |
| `initialAnswer` absent | Empty or existing single-line custom draft; return has default single-line behavior. Empty/whitespace-only input without an option or attachment cannot submit; nonempty custom text is trimmed on submission. | A1–A4, I1. |
| `initialAnswer` nonempty vs `""` | Both enable multiline and newline return. Suggested text seeds only when no draft exists; edits, including deleting everything, win over the suggestion. Exact whitespace and empty strings are valid answers. | A5/A6, I2. |
| Prop draft equals last written text vs differs | Equal is own-write echo and must not overwrite native IME work. Different adopts external draft, notably an option clearing text. Initial local state comes from the displayed draft. | A1–A3, A5/A6, I1/I2. |
| Single-select vs multi-select; zero/one/many options | Radio vs checkbox choices; valid selections clear `customAnswer` to `""` in both branches. Typing nonblank custom text deselects choices; single-line whitespace retains valid selections. Option values may differ from labels. | A3 captures the changed external-clear boundary with radio choices. Checkbox combinations use the identical empty-prop adoption path; no separate required recording. Zero options is A5/A6. |
| Focus/blur; collapse/expand; thread navigation; new request | Focus changes keyboard ownership; unmount explicitly releases ownership. Android expanded card is in-flow and unmounts on collapse; iOS is retained as an overlay. | A4, I1/I2. |
| `disabled` false/true; canOperate false | `disabled` is `not_resumable` or response in-flight and controls native editability plus attachment actions. Lack of operate scope disables Submit, not the text field. Existing custom draft remains. | Submission transition appears in A2/A5/A6/I1/I2. Permanent non-resumable/read-only variants do not change text synchronization; no extra capture. |
| Submit ready/unanswered/in-flight/success/failure; dismissible; Stop | Submit reads shared draft-derived answers, not new local state. Success prunes draft and resolves request; failure retains draft and clears responding status. Dismiss/Stop remove or resolve the request. | A2 explicitly checks immediate submit and received value; A4 checks fresh request. Error/dismiss/Stop are unchanged paths; no injected-error recording required. |
| Environment capabilities absent/present; attachment counts 0/1/many/100 total; types image/video/document | Attach button only with capabilities. Strip empty/nonempty, image/PDF/video preview modals or document navigation. Media/files picker, cancel, paste images, preview close, removal, upload/retry alter attachment state; count across questions limits additions. | Unchanged attachment renderer/pickers; do not expand into attachment-type/count screenshots. A4 adds/removes one image as a representative draft-atom rerender while text is present. |
| Picker/paste preparation, upload progress, rejection/error; late completion after unmount/other-device resolution | Preparation/upload can block Submit. Errors use alerts; inactive scopes release late files. These operations must not reset the typed answer. | Representative successful attachment round trip in A4; no exhaustive error matrix for unchanged async code. |
| Text length, question/option label length and descriptions, markdown/RTL, card height/scroll, keyboard open/closed | Text/prefill and available space change wrapping/scroll; descriptions are omitted when equal to labels. Parent derives text direction. No changed style or breakpoint. | Short single-line, middle insertion and multiple lines represented; long labels, RTL, tablet/landscape, and extra overflow fixtures excluded from this narrow fix. |
| Theme, width, platform | Same classes; no new theme/width conditional. Platform changes native IME and card lifecycle. `AppTextInput` adds native selection/cursor colors but no text-state branch. | One light portrait viewport for Android and one for iOS; both platforms needed. No theme × width grid. |

## Fixture producers and seeding

Use isolated synthetic app state and each side's own JS source at the pinned revision. Do not use the live database. Existing development/native setup is the capturer's responsibility; this audit did not start a server, Metro, device, or browser.

**E (stock evalbot, real native app):** Reuse the isolated environment/provider setup in `scripts/mobile-behavior/environment.ts`, selecting instance `evalbot`, model `eval/scripted`. Launch with initial text `Ask me first. [[eval:question]]` through `orchestration.launchThread`, or send that marker in a thread using that instance. Open its Thread route/Home row. Question data: `choice`, header `Option`, First/Second choices, custom answer allowed by default, `initialAnswer` absent. A later question is produced by another marker after the first resolves; a different thread can provide a separate existing draft. Capture the provider's elicitation result or authoritative server answer for submission rows; the stock fake provider receives `reply.result` but does not itself write it to a receipt file, so a scratch wrapper may log only the synthetic reply.

**P (Pi editor, real native app):** Use a scratch Pi extension that requests an editor, or a protocol-compatible scripted Pi binary configured in an isolated provider instance. Emit `{type:"extension_ui_request",id:"editor-<unique>",method:"editor",title:"Commit message",prefill:<value>}` on the Pi RPC connection. The real Pi adapter creates the pending question, and the real native client renders it. Record the corresponding `{type:"extension_ui_response",id:...,value:...}` receipt, including JSON-escaped whitespace. This is a **scripted-provider fixture through the real app**, not a component harness. The repository's fake Pi adapter tests show the event contract; they are not a device driver. No model-generated question or manual database patch is needed. Do not set a request timeout for these rows.

**M (main composer control, real native app):** Open an ordinary empty Thread composer using the same paired isolated environment, device, keyboard and settings. Do not dispatch its typed text. This tests the stated unaffected control surface.

**Attachment step in A4:** Add one small synthetic PNG using the normal attachment picker (or image paste), return to the input, then remove it. This exercises a shared draft-atom rerender; native preview/video/document branches are outside the changed logic.

Every required row is reachable in the real app with E/P/M. **No required row needs a labelled component harness.** If a capturer substitutes a harness, label it as such and leave native IME coverage missing.

## Minimum recording matrix

All rows below require **before at ac8dce0cc3 and after at f16d8770bd**. No older clip satisfies a row. Suggested fixed Android profile: Pixel 9, API 36, portrait 1080×2424 (report logical viewport too), real English Gboard with suggestions and autocorrection enabled. Suggested iOS profile: iPhone 17 Pro portrait at its native resolution, light theme, Apple English keyboard; record the exact OS/viewport. Equivalent available phone profiles are acceptable if held fixed within each before/after pair. Record actual keyboard build/settings, app/native fingerprint and loaded JS SHA. Physical Pixel 10 XL / Android 17 is useful follow-up, not required for this proportionate first proof set.

| ID | Named fixture and data | Required interaction / observable acceptance | Diff affected? | Minimum capture | Real-app seed | Status |
|---|---|---|---|---|---|---|
| A1 | Android, empty single-line answer, First/Second options | Tap Gboard keys at about 130 ms/key: `teh cat sat on teh mat and wnet hoem`; let normal space-triggered autocorrection run. Continue typing after corrections. Repeat identical taps in main composer. Compare corrected spelling to the control rather than demanding capitalization the keyboard may choose differently. No dropped/reordered letters or stuck caret on head. | Yes; M is unchanged control | **recording**, Android light portrait, each SHA; show question run plus composer control. Repeat question sequence 3 times per SHA and retain outcomes; at least one visible baseline failure and all head runs intact are needed to establish the claimed fix. | E + M | MISSING |
| A2 | Android, single-line `Hello world`, caret inside first word | First insert `xyz` at index 1 without a suggestion; then reset, move inside `Hello`, accept an available suggestion (e.g. `Hellow`) and type `x`, `y`, `z`. Show cursor advancing and letters ordered at insertion point, including any keyboard-added space. Accept a final correction and tap Submit immediately; received answer must equal visible final text (single-line trim applies). Baseline failure may be recovered by moving to line end, matching the reported workaround. | Yes | **recording**, Android light portrait, each SHA, plus exact synthetic answer receipt | E | MISSING |
| A3 | Android, single-line typed draft with two radio options | Type custom text → tap First while input focused → field clears and First is selected → type again → option deselects → tap Second → field clears again → retype with a suggestion. Both external clears must take effect and the input must remain usable afterward. | Yes, effect's external-change branch and reverse transition | **recording**, Android light portrait, each SHA | E | MISSING |
| A4 | Android, existing custom draft, same request restored then new request | Type a draft, add/remove one synthetic image and confirm text remains; collapse/expand while focused, then accept a suggestion and continue. Navigate to another thread and back; draft must restore. Submit/resolve, ask again with another request ID; new answer starts empty with no stale prior text. | Yes, state initialization/remount and own-write echo under unrelated draft rerenders | **recording**, Android light portrait, each SHA; one continuous flow may cover all checkpoints | E; ordinary second thread; PNG through attachment UI | MISSING |
| A5 | Android, zero options, multiline prefill `"  Proposed commit\n\nDetail  "` | Confirm seed, place caret within text, accept suggestion, continue typing, insert newline. Collapse/expand and continue editing. Submit; exact JSON receipt must preserve intended leading/trailing spaces and blank lines, and contain every inserted character/newline. | Yes, seeded state and multiline native input | **recording**, Android light portrait, each SHA, through Submit + receipt | P with nonempty prefill | MISSING |
| A6 | Android, zero options, `initialAnswer:""` | Confirm empty editor starts as multiline; type with suggestion + newline, select all/delete to empty, collapse/expand, and Submit. Empty must not be reseeded and provider must receive `value:""`, not cancellation. | Yes, empty local state and exact-empty draft round trip | **recording**, Android light portrait, each SHA, through Submit + receipt | P with empty prefill (omitting prefill maps to the same shape) | MISSING |
| I1 | iOS, single-line empty draft, First/Second options | Apple keyboard suggestion → continue typing → select option to clear → retype → collapse/expand retained card → continue → immediate Submit. Exact final visible text must match received answer after normal trim; focus must remain usable. | Yes, shared implementation on iOS and retained-card lifecycle | **recording**, iOS light portrait, each SHA, plus answer receipt | E | MISSING |
| I2 | iOS, multiline prefill and subsequent empty editor | Use A5 prefill: edit mid-text with suggestion + newline, collapse/expand retained card, continue and Submit with exact receipt. Request another editor with empty prefill; type then delete all and Submit empty, preserving the `initialAnswer:""` distinction. | Yes, iOS multiline and new-state initialization | **recording**, iOS light portrait, each SHA, two short editor requests + receipts | P, first nonempty then empty prefill, unique request IDs | MISSING |

This is eight behavior fixtures and sixteen paired recording segments, which may be chapters within fewer video files. A1's composer control and repeated outcomes are part of A1, not extra theme/viewport fixtures. Still frames may aid comparison but never replace these recordings. Capture a full, legible keyboard/caret/input view; synthetic typed text and receipts should remain inspectable. Keep time, SHA, input steps and run result in a small manifest. Native taps must generate text and suggestion acceptance; accessibility snapshots may corroborate final text, but bulk `input text`, clipboard replacement, direct draft writes and mocked inputs do not exercise the Gboard path. If baseline does not reproduce, report that honestly; passing head-only clips do not prove the fix.

Unconfirmed limits retained: reporter's exact Android/Gboard combination, concurrent agent streaming, other keyboard languages/IME composition systems. They do not introduce a new code branch here, so they are not expanded into mandatory rows.

FRICTION: none

VERDICT: INSUFFICIENT A1, A2, A3, A4, A5, A6, I1, I2 (current merge-base/head recordings and submission receipts missing)
