# Proof audit pass 1 — seeded composer model precedence

Target: [#21140](https://git.cafe/pingdotgg/t3code/pulls/21140), mirrored [#21140](https://github.com/pingdotgg/t3code/pull/21140).
Contributor: [SwiggitySwerve/t3code](https://github.com/SwiggitySwerve/t3code), branch `fix/web-seeded-model-no-handoff`, original head [093c34f](https://github.com/SwiggitySwerve/t3code/commit/093c34f843ecb338362636a54b93b241f9a8ecb1).

Read-only source audit of `/home/ubuntu/wt/pr-21140`, requested branch `takeover/pr-21140` (actual checkout is detached at the pinned head), head [a514925](https://git.cafe/pingdotgg/t3code/commit/a51492518429f805be2ed3f6d323d02382d98023), full diff against verified merge-base/origin/main [eea9895](https://git.cafe/pingdotgg/t3code/commit/eea989536849e5b5049987ee24318b5a77326542).

This is a capture plan, not captured proof or a correctness verdict. No browser, dev server, automated tests, or live user data was used. Source tree was clean before and after the audit. No source changes made. The matrix was derived from all 12 changed files before any captures.

## Changed paths and propagation

Paths below are relative to the worktree. Line numbers refer to audited head.

| Changed source | Render path / direct caller | Inputs, data branches, interactions | Fixture |
|---|---|---|---|
| `components/ChatView.tsx:568,1178,2500` | Thread/draft chat page → provider lock and model picker; timeline footer → `HandoffRow` | Reads `modelSelectionExplicit`; non-explicit draft provider/model ignored after start. Pending handoff uses same precedence, subject to available/enabled provider and thread handoff support. Explicit choices still win. `paintOnlyDisplayedTimeline` suppresses footer as before. | A, B |
| `components/chat/ChatComposer.tsx:950` | `ChatView` → `ChatComposer` → `useComposerProviderModel`; model picker, traits, provider-dependent controls | Supplies `threadHasStarted(activeThread)`; stale draft provider no longer unconditional first candidate. Candidate order thereafter is runtime instance, thread instance, project instance. | A–C |
| `components/chat/useComposerProviderModel.ts:32` | Sole production caller `ChatComposer` | Includes draft provider only when seed applies; passes start state into effective model/options derivation. Output includes selected instance/model, catalog, effective options, and composer provider state. Thus picker checks, trait choices, provider-specific affordances must agree. | A–C |
| `composerDraftStore.ts:1094` | `deriveEffectiveComposerModelState` via hook above | Unstarted or explicit → draft applies. Started non-explicit → thread/project/server fallback. Active instance entry, legacy driver-key entry, missing entry. Active selection without options means clear (`[]`), rather than inherit thread/project options. Existing custom/unavailable model fallback and Antigravity instance rules remain. | A–C |
| `composerDraftStore.ts:3031` | `setProviderModelOptions` ← `TraitsMenuContent` in `chat/TraitsPicker.tsx:354`; also composer imperative `stepReasoningEffort` at `ChatComposer.tsx:3160` | Passed displayed `model` wins over stored seed model; null/empty options creates a model-only selection even with no prior entry. Updates active instance when supplied and marks choice explicit, including formerly no-op equal values. Sticky option bookkeeping retained. | C |
| `composerDraftStore.ts:4349` | `useComposerDraftModelState` → provider/model hook | Reactive selector now carries explicit bit; model/provider selection must update without reload on explicit picker/trait actions. | B, C |
| `components/CommandPalette.tsx:1273` | Open palette → `composerPromptLink` → “Copy prompt link with repo and model” → clipboard | Passes active thread start state. Plain prompt link remains content-only. Context action is offered only when project repository identity resolves to GitHub; composer must have nonempty prompt within link size limit. | D |
| `components/promptLinks/copyPromptLink.ts:46` | Sole production caller: command palette | Context model/provider follows applicable draft, otherwise thread selection. Provider driver is resolved from selected instance catalog; missing provider omits model. Plain link short-circuits unchanged. | D |
| `hooks/useHandleNewThread.ts:106` | `useNewThreadHandler` → all new-thread callers listed below | Carry source is current route's server shell or draft. Non-explicit seed ignored for started source, explicit pick retained. Destination project default still wins over carry. Both fresh destination and reusable empty stored draft use guarded carry; explicit destination is preserved. Same draft reuse remains unchanged. | D; entry coverage A |
| `lib/chatThreadActions.ts:71` | New `draftModelSelectionApplies` shared by chat, hook, copy, new-thread handler, draft derivation | `!threadStarted || draft.modelSelectionExplicit === true`. Existing `threadHasStarted` relocated unchanged: `latestRun !== null || itemCount > 0 || runtime`. | A–D |
| `components/ChatView.logic.ts:975` | `deriveLockedProvider`, `waitForStartedServerThread` | Imports relocated predicate, same behavior as main. Provider lock and promotion waiting retain item-count semantics. | Unaffected relocation; visible provider precedence covered A/B |
| `components/ThreadRouteView.tsx:110` | Server/draft routing, hero transition and promoted-draft finalization | Imports same relocated predicate. No route/render branch changes. | Unaffected; promotion is observed within normal entry flow if used |
| `composerDraftStore.test.ts` and `modelSelection.test.ts` | Automated test inputs, no production rendering | Added focused started/non-explicit, explicit, trait-target, seeded/no-entry clear cases; existing calls supply unstarted input. | No separate visual fixture |

`useNewThreadHandler` production callers (direct or through `useHandleNewThread`): `ChatView`, `CommandPalette`, `Sidebar`, `LegacySidebar`, `chat/DraftHeroHeadline`, `promptLinks/NewThreadLinkView`, `_chat` route shortcut handler, `_chat.index` auto-open, `welcome`, `AddProjectToMachineDialog`, `ProjectCloneToastCoordinator`, `pullRequest/PullRequestDetailPanel`, `desktop/DesktopAppActivationCoordinator`, `desktop/DesktopWebLinkCoordinator`, and `desktop/SnapShotCoordinator`. These converge on the same carry resolver; do not multiply screenshot fixtures for each wrapper. Cover sidebar action, palette action and keyboard entry in recordings; desktop-only launch wrappers are source-traced, not claimed tested in web.

## State dictionary and reproducibility

Use isolated synthetic project content and messages only. Suggested visible names: project “Model precedence proof”; started thread “Existing B”; prompts “Proof A draft” and “Proof continuation”. Use actual enabled/installed provider instances and their offered models from the isolated server; do not hardcode a model that the catalog cannot offer.

- Provider P has two distinct models A and B with an observable trait (for example reasoning low/high). Thread S is stopped, has a real persisted run/message and thread model B with low trait. Seed selection A differs visibly from B.
- Provider Q is another available instance/driver supporting handoff with S. Use a real handoff-capable thread for the pending-handoff checkpoints. If no Q exists, provision an isolated provider fixture using an existing supported test mechanism, label it harness, and disclose it. Do not call an unavailable-provider empty state proof of handoff correctness.
- S-seeded: S's composer draft has `activeProvider=P`, entry A/high, and `modelSelectionExplicit` false/absent. Separately use Q/A as the seed for the cross-provider checkpoint. Server thread/runtime remain B/P. Keep a nonempty unsent prompt so persisted composer content is retained.
- S-clean: same started thread B/low with no composer model entry or active provider. No-entry means genuinely missing selection map entry, not just `options: []` on an entry.
- D: unstarted draft with A selected by project defaults/sticky carry or a prompt link. No run, zero items, no runtime. Model A must be visible before any submission.

Natural entry setup: add a synthetic local project through the app, configure provider and project defaults through Settings, create S and one short completed turn through the real app if provider execution is available. Change project/default model to A and create/open an unstarted draft via sidebar New thread, command palette “New thread in …”, and configured `chat.newLocal` shortcut. Derive shortcut labels from current bindings, not an assumed platform chord. With a nonempty draft, use prompt link `/new?prompt=Proof%20A%20draft&provider=<driver>&model=<A>` at the same dev origin; adding `repo=<owner>/<name>` selects a matching registered project. Without repo it opens scratch instead. URL parameters must be encoded. Never follow a real pull-request link or run its setup for this proof.

Important: `promptLinks/NewThreadLinkView.tsx:117` calls `setModelSelection(..., { explicit:true, replaceOptions:true })` at this head and main. A prompt link is therefore a real explicit-seed entry flow, not proof that non-explicit legacy state exists on a started thread. Switching drafts/threads alone also does not place one draft's selection onto another thread. Do not present that navigation as the stale-seed reproduction unless state inspection confirms it.

Stale-seed harness only when unreachable naturally: after creating S in isolated state, use the real dev app's store module (browser evaluation imports `/src/composerDraftStore.ts`) and its `setModelSelection(threadRef, selectionA, {replaceOptions:true})`, deliberately omitting `explicit:true`. Here `threadRef` is `{environmentId,threadId}` from that isolated server and `selectionA` is `{instanceId,model,options}` from its real catalog. This changes client composer draft state only, never the rendered DOM. Navigate away and back, inspect `getComposerDraft(threadRef)` to verify the conflict remains, and record its state summary without secrets. Label this checkpoint “harness: stale persisted composer seed”. Use the identical fixture setup at baseline and candidate. Prefer a fresh S-clean thread/profile for no-entry clearing rather than silently rewriting selection state during the interaction.

If no provider can execute, create synthetic stopped history only through an existing isolated fixture/service entry point; document exact command/script and payload. Do not invent a SQL schema or hand-edit event projections. That is harness state under the real application, not a provider execution claim. A capturer must supply this recipe if necessary; this pass did not execute or validate a runtime seeder.

For context-copy action only, ensure synthetic project's `repositoryIdentity` is a GitHub repository identity (a temporary local git project with a GitHub origin is suitable); a GitCafe-only identity hides that action. No forge fetch is required. The global takeover checkout's origin must remain untouched. Do not seed the real contributor checkout as an executable project.

## Minimum fixture matrix

Every affected row is interactive and requires a recording, not a still. Each recording may contain the specified reset-separated checkpoints, with clear visible labels in the evidence index. Record the complete web viewport and the actual interactions; supplementary extracted stills can show endpoints but cannot replace a recording.

| ID | Affected? | Visually distinct path | Recording sequence and required visible endpoints | Seed route / setup |
|---|---|---|---|---|
| A | Affected | Draft composer → started-thread composer + automatic pending-handoff footer | Show real entry via prompt link with A; open palette New thread and sidebar New thread in controlled resets; exercise configured new-thread shortcut. Show A on the unstarted draft, then switch to S-seeded. Candidate must show thread B/low, matching provider and selected picker check; seed must not show pending handoff. Include a Q seed checkpoint so stale provider override would visibly produce a pending handoff on baseline. Show seed and thread names/catalog identity before/after navigation. | Natural draft entry; stale conflicting started selection harness only if no natural retained seed. Check P same-provider conflict and Q cross-provider conflict within this row. |
| B | Affected | Explicit picker selection → selected model/provider and intentional handoff footer | On started S-seeded displaying B, open model picker and explicitly choose A. Show A selected and preserved after switch away/back. On handoff-capable fixture explicitly choose Q; show intentional pending handoff and matching Q model; choose original B/P again and show row disappears. No send needed to prove pending state. | Same S; use real picker, not store to mark explicit. Restore independent state for baseline/candidate. |
| C | Affected | Traits picker and reasoning shortcut; explicit empty options | Start S-seeded: candidate B/low despite A seed. Open traits and choose high; model remains displayed B. Close/reopen and navigate away/back to prove high sticks to B. Clear all trait selections and show low thread trait does not reappear. Reset to S-clean (no draft entry) with thread B/low: clear traits as first action, reopen and navigate away/back; options stay cleared. Exercise reasoning step via configured command/keybinding on B and show it targets B too. Include an explicit model pick with cleared/default traits and show it does not refill from thread traits. | Real traits menu and configured reasoning action. Reset branch starts must be recorded/labeled; no-entry branch must be inspected before action. A “default” UI choice may serialize a value; confirm actual absent/empty options before claiming empty clear. If no UI choice yields empty options, label a store `setProviderModelOptions(..., null, {instanceId:P,model:B})` checkpoint as harness and show resulting real UI. |
| D | Affected | Palette context-copy output and new-thread model carry | On S-seeded B with nonempty prompt, open “Copy prompt link with repo and model”; paste link into an unsent composer (do not submit) so recorded output shows B/P query, not stale A/Q. Plain copy omits model as control. From same S invoke New thread: destination without project default shows B. Reset S, explicitly pick A, repeat copy/carry: A is exported/carried. Repeat with destination project default C set: C wins. Include empty reusable destination draft once, not only fresh destinations. Return to S after each carry to prove S stayed B unless explicitly picked. | GitHub repository identity required for context action; settings default null for carry checkpoint, C for override. Keep source prompt nonempty to expose copy action; destination starts empty for reuse branch. Reuse may require a second project or controlled return to its existing empty draft; document actual IDs/route. |
| U1 | Unaffected | Context-window meter layout, including item-only history | No dedicated capture. `reserveContextWindowMeter` still uses `threadShellHasStarted(props.activeThreadShell)` at ChatComposer:1057: run, latest user timestamp or runtime. It was not replaced with item-count predicate. Keep ordinary footer visible in affected recordings as incidental regression context. | No item-only fixture required unless head changes this code. |
| U2 | Unaffected | Route missing/loading/hero behavior; provider setup/unavailable/custom catalog; Antigravity special fallback; queued editor; palette plain-link availability | No dedicated capture. Predicate relocation and untouched fallback/visibility branches do not create new layouts. Record blockers if actual catalog/route availability prevents affected cases. | Source traced; not claimed runtime verified. |
| U3 | Unaffected code surface | Native mobile, server, contracts, Electron shell chrome | No dedicated capture. Production diff is web only; desktop renders web components but native shell integrations are not verified by a web recording. Narrow 390px web is not native-mobile proof. | No native app launch in this task. |

Four affected rows × two viewport widths × two themes = **16 recordings per revision**. Before/after = **32 recordings total**. Minimum independent stills = **0**; optionally extract paired endpoint frames from the recordings. Do not multiply this by every caller or every provider. If all four scenarios fit one recording at a given width/theme/revision, four labeled scenario chapters in each of eight files are equivalent, but all 32 scenario cells must remain indexed with timestamps. All changed behavior remains reviewable at both widths/themes.

Viewports: **1440×1000** and **390×844** CSS pixels, consistent device scale and browser zoom for both revisions. Themes: explicit **light** and **dark** through real app theme settings, not OS auto. At 390 use responsive web UI, reveal sidebar/palette/model/traits through actual controls and close overlays to expose endpoints; retain whole viewport, no composer-only crop. At 1440 include sidebar, thread title, timeline footer and composer. Keep pointer/clicks and transitions visible. Show settled endpoints without adding arbitrary sleeps as logic synchronization. Use app readiness/persisted event state to establish completion.

## Capture environment and handoff

No browser/server started by this audit. Later capturer must use the repository `test-t3-app` workflow after authorization; keep all state/artifacts in `/tmp/ev-21140`. Suggested separate home roots `/tmp/ev-21140/before-home` and `/tmp/ev-21140/after-home`, fresh browser storage/profile per revision; do not copy any live data or auth storage. Never access `~/.t3/userdata`, including through automatic dev seed.

Future startup from the authorized worktree, not run in this pass:

```sh
T3CODE_DEV_SEED=0 vp run dev --no-seed --home-dir /tmp/ev-21140/after-home
```

Read actual origin/ports from `[dev-runner]`; do not set `VITE_HTTP_URL` or `VITE_WS_URL`. Do not publish token-bearing startup/pairing links, auth headers, private prompts, credentials, or raw runtime logs. Track only the PID the capturer starts and stop only that process. Browser authorization here is for a later capture pass, not this pass.

Baseline is merge-base [eea9895](https://git.cafe/pingdotgg/t3code/commit/eea989536849e5b5049987ee24318b5a77326542), not the contributor's original head. Candidate is [a514925](https://git.cafe/pingdotgg/t3code/commit/a51492518429f805be2ed3f6d323d02382d98023). Do not checkout/reset the shared worktree while parallel readers run. Parent must arrange a safe sequential baseline/candidate build in the only authorized worktree or explicitly authorize another checkout. This audit does not create one. Recheck head before captures; changed production diff invalidates this plan until reconciled.

Capture index should include revision, viewport, theme, scenario A–D, checkpoint/timestamp, synthetic thread/model/instance names, real-entry versus harness label, and expected/observed result. A before screenshot need not exhibit every bug; explicit-selection and draft-entry endpoints may match baseline as controls. Never stage a broken “before” by changing rendered DOM or altering baseline implementation.

No repo-wide checks. This pass ran only read-only source/git inspection. Source proof is not a substitute for the requested integrated real-app captures. Runtime availability of providers, handoff support, empty trait clear affordance, and baseline startup remain to be established by the capturer.

All helper scripts and evidence stay in `/tmp`, are uploaded with ghdrop, and are never committed. Upload script accompanies this matrix. No seeding script was executed or committed; any later capture seeder must also be saved under `/tmp/ev-21140`, uploaded, and cited in the index.

## Friction

FRICTION source-discovery | A guessed composer model-picker filename did not exist; an rg search reported it and discovery continued through actual hook files. | Start with rg --files before targeting guessed component filenames.

FRICTION checkout-state | Final git branch inspection returned no branch name: the authorized worktree is detached at the requested head. | Parent should confirm or restore the intended branch before later implementation/captures; this read-only audit did not switch it.
