# Proof audit, pass 1 — mobile New thread project preference

Target: [#24741](https://git.cafe/pingdotgg/t3code/pull/24741) ([GitHub mirror](https://github.com/pingdotgg/t3code/pull/24741)). Read-only inspection of `/home/ubuntu/wt/pr-24741`, local branch `pr-24741`, author branch `PixPMusic:feat/new-thread-project-preference`. Reviewed local head [e1660522ee](https://git.cafe/pingdotgg/t3code/commit/e1660522ee1ba3ff238c8311317f806862fcab3f) against [d145077b4f](https://git.cafe/pingdotgg/t3code/commit/d145077b4f7dc9e476bacf346e3f2c04cc141d6d), using `git diff origin/main HEAD`. The local head is not yet pushed; remote CI/reviews would not prove this head. No captures exist at this pass.

## Findings first

1. **Proof is insufficient: every minimum row R01–R11 is missing.** The preference changes navigation and durable device state. Stills of a final composer cannot establish the initiating action, absence of a picker, preservation of a selection, or behavior after restart. All minimum head rows require **recording**. One recording may cover several rows with explicit timestamps.
2. **Fresh server does not mean fresh mobile preferences.** `T3CODE_DEV_SEED=0` only initializes server data. The pooled device may retain `useLastProjectForNewThreads` and `lastThreadProject`. Reset those fields in the isolated test client's storage before the default/no-history rows, preserving its pairing and other preferences. Record the reset procedure and resulting values; do not silently treat a previously used client as fresh.
3. **A model-less draft cannot visually prove its project.** `NewTaskDraftScreen`'s `!flow.selectedModel` branch shows “New thread”, the environment control, and a connection/model message, but no project hero/title. For R03–R11, expose a real selectable model in the dev server's provider catalog and show the project/environment. No provider turn needs to run: seed idle threads through RPC. If unavailable, label a model-enabled component fixture as a **harness capture**, and retain real-app route/state evidence separately; a generic model-less composer is insufficient project proof.
4. **Two intentional distinctions must survive capture.** Scratch projects are excluded from remembered history, but a current live scratch thread is still a valid current-project destination. The unavailable-thread redirect ignores this global preference and still selects the first project *in the requested environment*; only the fallback to an unrelated environment's first project disappears. R08, R10, and R11 distinguish these cases.

No confirmed code contradiction was established in this proof-planning pass. This is not a merge-readiness verdict. No tests, app, server, simulator, emulator, lease, or browser were started; no source edits, pushes, or forge comments were made.

## Code-derived render and caller inventory

Paths below are relative to the worktree. This enumerates changed render decisions and the unchanged visual branches they can reach; unrelated composer/provider-state permutations are not a new capture Cartesian product.

| Changed surface | Every caller / entry path | Branches, data, layout and interactions | Fixture disposition |
| --- | --- | --- | --- |
| `features/settings/ThreadSettingsSections.tsx`: `NewThreadSettingsSection` | Only `SettingsProjectGroupingRouteScreen`; that component is registered as both `SettingsOrganization` (`settings/organization`, normal General entry) and legacy `SettingsProjectGrouping` (`settings/project-grouping`). New search terms reach the normal General row through `filterSettingsSections`. | Always one “New threads” section after Project grouping and Sidebar/Working section, one `SettingsSwitchRow`, fixed pencil icon, label and wrapping subtitle. Successful preferences with absent/true field => On; false => Off. Non-success => Off at component level. No mixed/null switch, disabled prop, scope permission gate, or save spinner/error text is supplied. `ListRowLayout` renders description, flexible label column and switch; detail/fontScale >=1.5 branch is inapplicable because no detail is supplied. iOS uses RN Switch, Android NativeSwitch and a different icon size; light/dark tokens, safe-area scroll padding, narrow width and enlarged text affect fit. Toggle writes local optimistic preferences, then durable storage; failed save rolls back through existing preferences state. | R01 covers real/default/On/Off, search and persistence on both native platforms. H01 covers artificial pending/failure/write error. X01 covers extra width/text stress. Legacy route is the same screen, no extra capture required. |
| `SettingsProjectGroupingRouteScreen` | The two registrations above. General exists in both configured/cloud and local Settings indexes; no server scope is required for it. | Three grouping choices: repository, repository_path, separate. Pending preferences disable those choices and clear selection; Working row retains its existing On/Off states. New section is unconditional and below existing explanatory text, so scroll-to-bottom and home-indicator clearance matter. New section itself has no project-count/connection-count branch. `SettingsSidebarContext` changes section presentation only when a caller supplies it; the Settings index supplies its own sidebar context, rather than adding a new prop to General. | R01 records surrounding content and scrolling. Repeating all three grouping values adds no changed layout; X02 covers grouped picker data. |
| `features/layout/AdaptiveWorkspaceLayout.tsx`: global `handleStartNewTask` | `Stack.tsx` mounts one `AdaptiveWorkspaceLayout` around workspace children, passing pathname, workspaceRouteKey, selectedPullRequestKey. `startNewTask` reaches Home toolbar/header, Android Home FAB, HomeScreen/empty-detail actions; `WorkspaceRootScreen` iOS toolbar/section toolbar and Android navigation compose; iOS native/sidebar tabs compose; iOS workspace navigator empty-detail/Duo compose; split live thread header (`useThreadHeaderOptions`, omitted for draft/Duo); and the `newTask` hardware keyboard command registered here. | Preferences pending => outer layout renders null; failure => content with default grouping; success => content with saved grouping/filters. Handler reads active root workspace Thread route, accepts scalar or array environment/thread params, and resolves a live thread shell. Non-Thread, draft, or unresolved thread => no current project. False preference => unscoped draft. Otherwise valid current ref wins, else valid last ref, else unscoped draft. Ref validity requires environment AND project id present in projects. `undefined` preference is On. Counts 0/1/many no longer supply first-project fallback. Request id creates a fresh draft. Compact/split/overlay change navigation chrome and sidebar dismissal but call the same handler; Android dismisses keyboard. | R02–R09 cover destination classes; use actual compose/FAB for Home, hardware command or visible compose for open-thread cases. R01's Android session should include R05 to prove Android action reaches shared behavior. No requirement to repeat every button alias or provider. X01 is optional split replay. |
| `features/threads/NewTaskDraftScreen.tsx` | Only `NewTaskDraftRouteScreen`, itself rendered by `ThreadRouteScreen` when params contain `draft`. Inputs: initialProjectRef (environmentId/projectId, branch, worktreePath, cloning), draftId, incomingShareId. Origins include global action, explicit per-project action, branch/worktree action, saved draft, incoming share/prompt link, picker, and unavailable-thread redirect. | New remembering hook accepts only selectedProject whose scoped key equals selectedProjectKey, excluding the provider's implicit first-project fallback. Existing draftId path hydrates and opens saved project or picker. Valid initial ref is applied once (branch preparation remains upstream), preserves later picker choice; missing initial ref with other projects returns to picker, while an unresolved initial ref with zero projects waits. No initial ref: valid explicit selected key is preserved; null/stale key => picker, for 0/1/many projects. No selected project => null; model absent => fallback without project hero; model present => regular project hero or scratch hero. Group title overrides project title, long regular title truncates at one line/max width. Available hero height <180 hides hero; keyboard/platform influence space. Existing clone pending/error banner, workspace local/worktree controls, permissions, share importing/failure/cancel, submission locks and attachment/model errors remain unchanged. | R02–R09 exercise changed resolution and both heroes; R05 includes explicit-project bypass; R07 includes saved draft restoration. X03 inventories unchanged share/branch/clone/small-height/model fallback states, without requiring unrelated screenshots. |
| `features/threads/ThreadRouteScreen.tsx` and `UnavailableThreadDraftRedirect` | Stack `Thread`; fullscreen `ThreadInspectorFullscreen` wrapper; recursive `ThreadRouteScreen` via `ThreadInspectorPreview.renderThread`. `inspectorPresentation`, onReturnToThread, renderInspector add existing chrome. | `draft` params => draft route; otherwise controller. Missing ids or hydrating => Opening thread loading screen. Matching selected live thread => content and remembering hook, even if feed/detail still loads or errors. Settled unavailable root route named Thread without inspector presentation => redirect. Same-env project exists => first same-env project explicit draft; none => unscoped draft, then picker (never unrelated projects[0]). Spinner is transitional. Inspector/fullscreen unavailable => existing “Thread unavailable”, reconnect if environment registered, Manage environments otherwise, no redirect. Android fallback adds AppBar/back for compact; iOS uses native host; optional inspector header. | R06–R08 cover live remembering, R10–R11 cover both redirect outcomes. X03/H01 cover unchanged hydration/error/inspector branches; no separate inspector capture is required for this diff. |
| `features/threads/useRememberThreadProject.ts` | `ThreadRouteContent(selectedThreadProject)` and `NewTaskDraftScreen(explicitly-selected project or null)` only. | On focus/dependency change: no project, no loaded serverConfig, scratch project, preferences not Success, or identical saved ref => no write. Otherwise saves exact environment/project pair. Hook remains active when preference is Off: Off affects use of history, not collection. Leaving focus must not overwrite latest history from an unfocused screen. | R03/R07 prove chosen-project memory; R06/R07 prove opened-thread memory; R08 scratch exclusion; R05/R07 include Off→On. H01 covers artificial config/preferences gating. |
| `new-task-project-selection.ts`, `persistence/mobile-preferences.ts`, search and docs | Helper called only by global handler; `resolveDraftProjectSelection` only by draft selection effect. Existing helper consumers for grouping/environment matching are unchanged. Preferences store used through `state/preferences.ts`. User docs add mobile behavior. | Pure helper has current/last/none plus false preference; explicit-key helper has preserve/pick. Sanitizer accepts only boolean preference and a pair of nonempty strings; malformed/missing fields are dropped. Storage uses SQLite preferences plus existing secure fallback/migration; no new synced/server setting. Search now matches “last project” and “new threads” for General; “new threads” may also match Threads, which is expected. | R01/R07 restart evidence; R09 stale-ref recovery. H02 is exact cross-environment collision/invalid data logic coverage. Existing helper tests cover core precedence/default/off/env matching; no visual claim is made from tests, and tests were not run in this pass. |

## Reusable seed and capture contract

- **Minimum devices/layouts/themes:** iPhone 17 Pro, portrait/compact, light, for R01–R11; Pixel 9, portrait/compact, dark, for R01 and R05. This cross-pair checks both native switches and both themes without repeating shared routing on every platform/theme. Pixel Tablet/split is supplemental X01, not minimum. Record actual logical viewport and font scale from device metadata; do not invent dimensions from the model name.
- **Before/after:** minimum base evidence is B01 (General screen without the new section), and base recordings of R03 and R11 demonstrating the removed first-project fallback. Use the supplied main commit on a lead-owned baseline checkout/build; do not alter the target checkout to collect it. At head, every R row is recording. Show the triggering tap/key command through settled outcome, including brief wrong-project flashes, unexpected duplicate pickers, or indefinite loaders. Label base/head and row IDs; clips can share files with timestamps. Export representative setting On/Off frames from the recordings if helpful.
- **Isolation:** start the lead's server from this worktree with `T3CODE_DEV_SEED=0`; use its actual URL and environment id E. Capture tooling must follow the mobile test/device-pool runbook. Nothing in this audit launches or leases a device. New driver runs belong under a new timestamped directory inside the lead's scratch directory; retain raw logs/evidence until uploaded. Never publish pairing tokens or credentials.
- **Projects:** create A `Alpha`, B `Beta — mobile preference proof with a deliberately long project name`, and optional C `Gamma` as distinct server folders under `/tmp/lead-24741-r1/` using the app's Add project flow, or typed RPC `projects.mutate` with `{type:"project.create", commandId, projectId, title, workspaceRoot, createWorkspaceRootIfMissing:true}`. Use distinct repository identities or General → Keep separate so A/B cannot collapse into one scope. The wire RPC goes to E's authenticated connection; environmentId is part of the client routing wrapper, not an invented extra field in ProjectMutation. Record returned ids and observed list order. Choose B as the non-first target; do not infer first project from title ordering.
- **Idle threads:** create TA in A and TB in B through `orchestration.dispatchCommand` / `thread.create`, with unique commandId/threadId, valid modelSelection from the server catalog, runtimeMode/interactionMode and nullable branch/worktreePath per contracts. Read remaining creation fields from contracts when implementing the driver. This creates idle threads without sending a prompt or starting a provider. Navigate with the app list or dev scheme path `t3code-dev:///threads/<E>/<threadId>` (confirm the build's configured scheme). Home must actually be the active workspace route when a row says “no current thread”; a hidden remembered Thread route may otherwise influence routing.
- **Scratch:** use picker “No project” / “Start without a project”, or `projects.ensureScratch` with `{}`; use the returned scratch project S and its configured root. Never fake scratch classification just by naming a regular project “No project”. Seed an idle TS for the live-scratch case.
- **Preferences:** normal On/Off changes must go through General. Server RPC cannot set device-local history. For a deterministic no-history/stale-field setup, a capture driver may stop only its own isolated dev client and patch the pooled test app's `t3code-client.db`, `client_preferences` singleton payload, preserving other JSON fields and valid updated_at; account for existing SQLite WAL and secure-storage fallback, restart, then verify the loaded state. Prefer UI histories/deletion where sufficient. This is a real-app seeded capture with a disclosed storage seed, not a harness. Never touch the developer's live install. Record at least pre/post values of the two new fields (redact unrelated preferences), environment/project ids and selected route, separately from the video.

## Minimum fixture matrix

“Missing” is the status at pass 1. Every row is minimum. R01 has two platform/theme cells and R05 has two; other rows need only the primary iOS/light cell. UI interactions can be combined into fewer recordings, but each row needs a timestamp and its stated precondition. A still alone does not satisfy any R row.

| ID / named fixture | Data and trigger → visible result | Diff impact | Minimum viewport × theme × proof | Real-app seed / route | Status |
| --- | --- | --- | --- | --- | --- |
| R01 — General, default On / Off / restored | No preference field, then explicit false/true. Open Settings → General, scroll through new section, toggle Off, leave/re-enter, restart and verify Off, toggle On. Search Settings for “last project” and “new threads”; General is discoverable and opens this section. Label, subtitle, switch and bottom safe area remain usable. | Added section and search terms; persistence | iPhone 17 Pro portrait × light AND Pixel 9 portrait × dark; **recording** with readable On/Off holds | General route `settings/organization`; legacy `settings/project-grouping` is equivalent and need not repeat. Fresh device fields before default check; UI writes thereafter. | **MISSING — minimum** |
| R02 — no regular projects, connected | E ready, zero regular projects, absent history, preference unset/On. Global New thread → Choose project empty state (“No projects found”), Add new project recovery available; scratch action available if server supports it. No phantom draft/project. | Unscoped destination now used; existing picker empty layout | iPhone portrait × light; **recording** | Run before creating A/B. Pair E, settle initial shell, invoke global compose; can continue through Add project to create A. Existing scratch-only catalogs collapse into this visible picker state. | **MISSING — minimum** |
| R03 — sole project, no history, default On | Exactly A, no live thread route, no lastThreadProject, preference absent. Global New thread → visible picker containing A, not an auto-selected A draft. Tap A → A draft, leave to Home, global again → A draft without picker. | Removes first-project and sole-project auto-selection; chosen project starts history | iPhone portrait × light; **recording**; also base comparison **recording** | Create A via UI/RPC without opening its thread/draft; clear only history. Enter Home. Second half relies on real focus/picker write, not a preseeded history value. | **MISSING — minimum (base + head)** |
| R04 — multiple projects / long title / search | A/B/C, no current route or history, On → multi-row picker. Off → same picker. Search B, then unmatched text → “No matching projects”, clear, choose long-name B → one correctly labelled B draft. Reopen project picker, choose A → A stays selected rather than reverting to B. | New reachability of picker; explicit choice preservation | iPhone portrait × light; **recording** | Add B/C with separate scopes, reset history for first click. General Off for second click. Picker route `new` / `new/project`; use search and project hero. | **MISSING — minimum** |
| R05 — Off, sole project, current and history present | Only A; visit TA so A is remembered; switch Off. From current TA invoke global New thread → picker, despite both current and history A. Select A → A draft remains selected. From Home global still → picker. Explicit per-project New thread in A still goes directly to A: preference governs global action. | False preference, no sole-project bypass; explicit route control | iPhone portrait × light AND Pixel 9 portrait × dark; **recording** | Separate clean one-project phase before adding B/C, or isolate fixture via normal project removal. Use keyboard command while live TA if compact chrome has no global compose; use actual Android compose/FAB from Home for Android cell. Per-project action through Home project context menu. | **MISSING — minimum** |
| R06 — current live thread, On / unset | A/B, open TB, unset/On. Invoke global action while TB is active → B draft without picker, even when A is first. Show B title/environment and stable transition. Valid explicit route must not reopen picker. | Current-project selection and live remembering | iPhone portrait × light; **recording** | Seed idle TA/TB by RPC; navigate TB, use visible compose or hardware `newTask` binding. Regular focus normally also remembers B; do not claim this alone proves priority over a conflicting saved ref (H02 / R08 cover that distinction). | **MISSING — minimum** |
| R07 — last chosen / last opened survives restart | A/B, On, no current thread. Choose B in picker → leave to Home → global uses B. Change draft to A, return Home → global uses A. Open TB (without sending), return Home and restart client → global uses B. While Off, visit A, then re-enable On → Home global uses A. Saved nonempty draft reopening preserves its chosen project/content. | Draft/live focus memory, local persistence, Off still collects history | iPhone portrait × light; **recording**, including restart and short readable state holds | Use actual picker/draft controls and app thread list to create histories. Save a unique draft text locally for restore check; no agent send. Ensure Home active after restart before clicking global. | **MISSING — minimum** |
| R08 — scratch does not replace history | Remember B. Choose No project → scratch draft S has scratch hero. Return Home → global still uses B. Open live TS; global while TS current → scratch draft S. Return Home → global uses B again. Repeat with no remembered regular project → Home global opens picker, never remembers S. | Scratch exclusion from history, current-project exception | iPhone portrait × light; **recording** | Real `projects.ensureScratch`/picker creates S at configured scratch root; seed idle TS. B is explicit selected/opened before first phase; clear history for final subcase only. | **MISSING — minimum** |
| R09 — remembered project removed | Remember B; return Home, delete B through app/RPC so A is the only remaining regular project. On, global New thread → picker containing A, not direct A. Choosing A recovers normally. | Stale last ref invalidation, no unrelated/sole fallback | iPhone portrait × light; **recording** | `projects.mutate` `{type:"project.delete",commandId,projectId:B,force:true}` only for synthetic fixture, or UI delete; keep A. Verify stale saved pair before click, not merely reset history. | **MISSING — minimum** |
| R10 — unavailable thread, own environment has project | E has A (or recorded first same-env project), route thread id absent after hydration settles. Root Thread automatically becomes explicit A draft; no picker. Repeat with preference Off: redirect still uses A. | Refactored redirect; same-env behavior deliberately preserved | iPhone portrait × light; **recording** | Open `threads/<E>/<nonexistent-id>` using actual dev link or app navigator driver. Use known ready E and settle not-found/error response. Do not use inspector route, which intentionally has different behavior. | **MISSING — minimum** |
| R11 — unavailable environment, unrelated project exists | Only E/A exists; route references unregistered E-missing and missing thread. Root redirect → picker listing A, not an automatically selected A draft. No stuck Opening thread spinner or wrong-environment flash. | Removes `projects[0]` fallback in unavailable-thread path | iPhone portrait × light; **recording**; also base comparison **recording** | Open `threads/<E-missing>/<missing-thread-id>` through actual dev link/navigator after connection inventory settles; unregistered environment needs no second server. Keep E/A available. | **MISSING — minimum (base + head)** |

## Supplemental / collapsed rows — not blockers for the minimum capture set

These explicitly account for additional render/input paths. They are not missing minimum proof, and are not to be reported as captured. If promoted because a failure is observed, interaction/time-dependent rows still require recording.

| ID / named fixture | Input / expected distinction | Impact and seeding feasibility | Proof type / viewport if requested | Status |
| --- | --- | --- | --- | --- |
| B01 — base General | Main General has grouping/Sidebar but no New threads section | Before-image anchor for R01; real baseline client/build on supplied main commit, normal General route | **Still**, primary iPhone portrait/light; match head framing | **MISSING — minimum baseline** |
| X01 — split / enlarged text | Pixel Tablet split R05/R06 replay; narrow font-scaled General wraps subtitle with usable switch. General theme reversal can supplement native cross-pair. | Same shared handler, unchanged responsive primitives. Real device settings + tablet pool. Full compact/split/theme Cartesian product not justified. FontScale detail-stacking is not a new branch for this row because it has no detail. | **Recording** for toggle/route replay; tablet chosen orientation or phone large font; matched screenshot frames useful | Optional, not required |
| X02 — one grouped scope containing multiple workspaces | A/B share repo identity; picker displays “2 workspaces” rather than path; selection prefers flow-selected environment, else representative | Existing grouped picker branch reached more often by change. Real `projects.mutate` with real matching repository folders or two paired environments + General grouping. Identical selectedKey behavior already exercised by R03/R04; no new grouping implementation. | **Recording** if selecting; primary phone/light | Optional, not required |
| X03 — unchanged draft / thread presentation branches | Branch/worktree explicit route; clone pending/error banner; share text/images/files with import pending/error/cancel; model-less fallback; keyboard leaving hero <180px; existing live feed loading/error/approval states; unavailable inspector/fullscreen reconnect/manage screen | All existing branches kept; new hook runs on chosen project, not provider-turn state. Real app can seed branch/local draft and share/clone flows; unregistered inspector may require a navigator driver. Never call one generic model-less screen proof of project selection. | **Recording** for loading/import/clone/keyboard/reconnect/action states; label any isolated component state as **harness** | Inventoried, no extra minimum capture |
| H01 — preferences/config not ready or write fails | Layout pending null vs failure default content; isolated new Settings row non-success appears Off; save rollback; remembering skips null config/non-success prefs/already-equal pair | No deterministic public RPC sets a device preferences AsyncResult or delays config; these need a **labelled harness** with injected preference/store/config states and actual components/hooks. Production preference loading catches store errors as successful empty defaults; do not claim an integrated error state can be forced by deleting a key. Cold-start R01/R07 covers ordinary real hydration. | **Recording**, labelled harness; phone layout | Harness-only for deterministic failure injection; not minimum |
| H02 — exact environment scoping / conflicting refs / malformed fields | Same project id in E1/E2, last E2 selects E2; nonexistent E2 + same id E1 opens picker. Missing current + valid last uses last; valid current + differing last uses current. Invalid boolean/nonempty-string pair is ignored by sanitizer. | Exact duplicate ids can be seeded through `projects.mutate` on **two distinct real environments**, if lead has a second isolated server. One fresh server cannot supply two genuine environment identities. Otherwise **labelled harness** for selection helper/store, with focused behavioral test output rather than pretending pure functions have a visual. Current-vs-last conflict is normally erased by regular current focus; R08 gives a genuine current-scratch/last-regular conflict. Helper tests already cover several of these shapes. | **Recording** for real/harness navigation; test transcript for pure functions, never a fabricated UI screenshot | Supplemental logic coverage, not extra minimum captures |

## Handoff and pass status

Minimum head cells: R01 iOS/light and Android/dark; R02–R04 iOS/light; R05 iOS/light and Android/dark; R06–R11 iOS/light. Minimum before cells: B01 iOS/light still, R03 and R11 iOS/light recordings on supplied main. Total 13 head fixture cells plus 3 base cells; these need not be 16 separate media files. A single timestamped device recording can satisfy multiple fixture cells with disclosed resets and readable transitions. Do not require Pixel Tablet, second environment, all providers, cloud login, relay/tunnel duplicates or every unrelated composer state for this local mobile preference.

All minimum evidence is pending. Capturer should return a manifest mapping row + platform + theme + base/head to uploaded media and timestamps, plus synthetic seed ids, local preference preconditions and observed project/environment outcomes. This matrix is a specification, not a claim that a seed driver or route link has been executed.

Coverage: all 11 changed files read, changed component callers followed through Stack, Home/workspace compose actions, draft provider/picker, preferences storage and missing-thread hydration. No source modification. No devices/processes/leases/worktrees created, so no runtime cleanup required. Worktree status remained clean; free disk was 16 GB at initial check.

FRICTION: none

VERDICT: INSUFFICIENT R01–R11 head recordings; B01 base still; R03 and R11 base recordings
