fix(web): preserve provider defaults for optionless threads#23528

closed7
main
fix/issue-23497
opened12d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Problem

With the new-thread default set to Claude Opus 5.5 with High effort, an existing thread saved without options displayed High and sent an effort override when its composer was left untouched. It should keep the provider default, Medium.

Change

Use the thread selection as the source of its options, including when it has none. Fall back to project/settings options only when there is no thread selection. A focused regression test covers the omitted-options case; a synthetic fixture rebuilds the captured states and is registered as a manual entry for Knip.

Scope and approval

Fixes #23497 GitCafe: https://git.cafe/pingdotgg/t3code/issues/23497 Triaged bug via the automated issue flow. This fixes the reproduced composer fallback and preserves the existing new-thread and explicit-option behavior.

Verification

The regression fails on main with expected [{ id: "effort", value: "high" }] to be undefined and passes with the fix. Focused model-selection, draft-store and ChatView logic tests: 142/142 passed. Targeted lint and web typecheck passed. Both independent code reviewers returned MERGE. The final scoped tests, targeted lint, and web typecheck also pass after merging main.

Independent proof audit: V1–V4 PASS; 84 artifact hashes verified, all 24 diffs recomputed, and fresh V1 R/F/O captures match exactly with zero changed pixels.

Evidence

Full evidence index · Fixture matrix

Rendering environment

Saved thread selections omitted options. Environment default was Claude Opus 5.5 / effort High; project override inherited. The live descriptor declares Medium as default. Before: High selected and dispatched. After: Medium selected; effort omitted from dispatch. Both send implicit fastMode:false. All captured drafts remained optionless when opening/closing menus.

Linux / Chrome 154; DPR 2, zoom 1, loaded system fonts, scroll 0,0, pointer 0,450. CSS viewports exactly 1440×900 and 390×844. Built-in snapshot caps wide PNGs to 1280×800; narrow PNGs are 780×1688. Source recordings are 2880×1800 and 780×1688. Sources are published unchanged. Diffs use equal original dimensions, no post-capture resizing/registration. Browser measurement · Recording metadata · All eight source videos decode successfully

Four affected rows, each with before/after recordings, resting (R), focused (F), and open-menu (O) stills. At 390 wide, the real More composer controls overflow is used.

V1 — 1440×900, light — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC)BeforeAfter
Resting2026-10-22T09:39:00.215Z2026-10-22T09:32:59.321Z
Focused/typed2026-10-22T09:39:02.404Z2026-10-22T09:33:01.422Z
Menu open2026-10-22T09:39:05.004Z2026-10-22T09:33:04.048Z
Send2026-10-22T09:39:05.885Z2026-10-22T09:33:05.002Z
Successful receipt2026-10-22T09:39:05.990Z2026-10-22T09:33:05.135Z
Persisted run completed2026-10-22T09:39:09.485Z2026-10-22T09:33:08.822Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V1-clean → proof-V1. Both created optionless through real RPC; exact persisted create events are in the manifest.

StateBeforeAfterDiff0: threshold 0, gain 8DiffT: threshold 26/255, gain 8
Sourceff525478e24c7f479ac9ff525478e2 → 4c7f479ac9ff525478e2 → 4c7f479ac9
RRaw stillRaw still1,103 pixels (0.107715%)792 pixels (0.077344%)
FRaw stillRaw still1,103 pixels (0.107715%)792 pixels (0.077344%)
ORaw stillRaw still11,738 pixels (1.146289%)773 pixels (0.075488%)

V1 before High / after Medium, whole open-menu surface

V2 — 1440×900, dark — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC)BeforeAfter
Resting2026-10-22T09:27:49.942Z2026-10-22T09:35:55.259Z
Focused/typed2026-10-22T09:27:52.109Z2026-10-22T09:35:57.490Z
Menu open2026-10-22T09:27:54.727Z2026-10-22T09:36:00.090Z
Send2026-10-22T09:27:55.596Z2026-10-22T09:36:00.800Z
Successful receipt2026-10-22T09:27:55.786Z2026-10-22T09:36:00.890Z
Persisted run completed2026-10-22T09:27:59.050Z2026-10-22T09:36:03.926Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V2-take2 → proof-V2-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

StateBeforeAfterDiff0: threshold 0, gain 8DiffT: threshold 26/255, gain 8
Sourceff525478e24c7f479ac9ff525478e2 → 4c7f479ac9ff525478e2 → 4c7f479ac9
RRaw stillRaw still1,094 pixels (0.106836%)743 pixels (0.072559%)
FRaw stillRaw still1,094 pixels (0.106836%)743 pixels (0.072559%)
ORaw stillRaw still12,189 pixels (1.190332%)681 pixels (0.066504%)

V2 before High / after Medium, whole open-menu surface

V3 — 390×844, light — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC)BeforeAfter
Resting2026-10-22T09:39:26.174Z2026-10-22T09:36:15.730Z
Focused/typed2026-10-22T09:39:28.540Z2026-10-22T09:36:18.134Z
Menu open2026-10-22T09:39:30.808Z2026-10-22T09:36:20.392Z
Send2026-10-22T09:39:31.446Z2026-10-22T09:36:21.039Z
Successful receipt2026-10-22T09:39:31.529Z2026-10-22T09:36:21.091Z
Persisted run completed2026-10-22T09:39:34.335Z2026-10-22T09:36:24.314Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V3-clean → proof-V3-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

StateBeforeAfterDiff0: threshold 0, gain 8DiffT: threshold 26/255, gain 8
Sourceff525478e24c7f479ac9ff525478e2 → 4c7f479ac9ff525478e2 → 4c7f479ac9
RRaw stillRaw still2,974 pixels (0.225878%)2,306 pixels (0.175143%)
FRaw stillRaw still0 pixels (0.000000%)0 pixels (0.000000%)
ORaw stillRaw still56,243 pixels (4.271707%)0 pixels (0.000000%)

V3 before High / after Medium, whole open-menu surface

V4 — 390×844, dark — complete

Steps: start resting; focus and type Reply with OK.; open the traits/overflow menu without selecting; close it; submit; wait for the successful receipt and completed real Claude run. Actual UTC milestones below align the two recordings.

Milestone (actual UTC)BeforeAfter
Resting2026-10-22T09:29:32.366Z2026-10-22T09:37:10.691Z
Focused/typed2026-10-22T09:29:34.697Z2026-10-22T09:37:13.073Z
Menu open2026-10-22T09:29:36.903Z2026-10-22T09:37:15.319Z
Send2026-10-22T09:29:37.563Z2026-10-22T09:37:16.046Z
Successful receipt2026-10-22T09:29:37.645Z2026-10-22T09:37:16.157Z
Persisted run completed2026-10-22T09:29:40.515Z2026-10-22T09:37:19.689Z

Before recording · After recording · Before dispatch/receipt/projection · After dispatch/receipt/projection

Threads: proof-V4 → proof-V4-clean. Both created optionless through real RPC; exact persisted create events are in the manifest.

StateBeforeAfterDiff0: threshold 0, gain 8DiffT: threshold 26/255, gain 8
Sourceff525478e24c7f479ac9ff525478e2 → 4c7f479ac9ff525478e2 → 4c7f479ac9
RRaw stillRaw still2,956 pixels (0.224511%)2,158 pixels (0.163902%)
FRaw stillRaw still0 pixels (0.000000%)0 pixels (0.000000%)
ORaw stillRaw still60,032 pixels (4.559485%)0 pixels (0.000000%)

V4 before High / after Medium, whole open-menu surface

Other visible differences and controls

High changes to Medium, the selected menu row changes, and neighboring controls shift with the wider label. All changed pixels in the paired stills are confined to those areas. Narrow focused pairs are pixel-identical because the trait control is hidden. Diff0 amplifies every nonzero RGB delta by 8; DiffT does the same for deltas ≥26/255. The index includes raw counts and the full unmasked images.

Explicit Low and draft overrides remain Low; new threads still inherit the default. Omitted options and explicit empty options both preserve provider defaults. All eight captured real Claude runs completed successfully.

Web was tested at 1440×900 and 390×844 in light and dark. Desktop shares this web helper; its Electron shell was not separately run. Native mobile already uses the saved selection directly and is unaffected. No wire or persistence changes.

Captures use the source SHAs shown above. The later fixture-only commit leaves production code and tests byte-identical to the after capture.

Main was merged as 09c06b91ac; the three fix files remain byte-identical to the audited head.

CI initially found the manual fixture as unused. Registering scripts/fixtures/*.mjs as a Knip entry fixes it: vp run knip:check, scoped formatting/lint, and both generated-file checks pass. Production code, test, and fixture are unchanged.

Models: Claude Opus 5.5 and GPT-6.1-Sol, through Claude Code and Codex in T3 Code.

Checks

23 successful · 17 skipped
All checks passed
c0e3d7b

Added 4 commits

  • Merge main into fix/issue-2349709c06b9
  • test(web): reproduce #23497 optionless thread inherits default options16f40c0
  • fix(web): threads saved without model options no longer inherit default options4c7f479
  • test(web): add synthetic composer option fallback fixturebe2c111
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — Existing optionless threads will use provider defaults instead of inheriting project/settings options, changing their effective model configuration. Human review is required for this default-policy change and the broadened Knip fixture entry-point configuration.

No code changes detected at c0e3d7b. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB+6 B (+0.1%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.3 KiB+6 B (+0.5%)2.0 KiB✅
CodexLive turn WebSocket decoded21.6 KiB21.6 KiB0 B (0.0%)24.4 KiB✅
CodexLive turn messages220 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.1 KiB+6 B (+0.1%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.3 KiB1.3 KiB+6 B (+0.5%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages220 (0.0%)8✅

Baseline: da8cc59 · PR result: c0e3d7b · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Added 1 commit

  • chore: register composer option fallback fixture as a knip entry7f004b1
Synced from GitHub

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 430afd607a895c84ae9ff26d018ef4abf91bc632 and c0e3d7b1555a6fb4187ab643e0ffa3bed6fe83ef.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 7f004b1622a7cdc44b3bed768c23b5e84ecfcef2 and 430afd607a895c84ae9ff26d018ef4abf91bc632.

External image Files selected for processing: 2
  • apps/web/src/composerDraftStore.ts
  • knip.jsonc

Limit details: You’ve used all 10 included reviews currently available.

Walkthrough

When a thread model selection exists, the composer uses only that selection’s options, even when they are absent. A test and fixture cover option resolution, and the scripts workspace includes fixture files as Knip entries.

Changes: Composer model option resolution

Layer / File(s)Summary
Thread option precedence
apps/web/src/composerDraftStore.ts, apps/web/src/modelSelection.test.ts, scripts/fixtures/composer-option-fallback.mjs, knip.jsonc
The resolver uses project options only when there is no thread model selection. The test checks that an optionless thread selection does not inherit project options. The fixture seeds threads with different option configurations, and Knip includes script fixtures as entry files.

Priority: External image Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

  • pingdotgg/t3code#23497 — Reports that an optionless thread selection incorrectly inherits project or settings options.

Possibly related PRs

Suggested reviewers: t3dotgg, macroscopeapp

Merge Risk: External image Minimal · previous review 430af

No actionable merge-blocking risk was found in the reviewed changes.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: preserving provider defaults for threads without options.
Description checkPassedThe description includes complete Problem, Change, Scope and approval, and Verification sections. It explains the bug, fix, linked issue, test results, limitations, and supporting evidence.
Linked Issues checkPassed[#23497] requires that a thread saved without options keep provider defaults, not project/settings options. At the reviewed head, deriveEffectiveComposerModelState in `apps/web/src/composerDraftStor…
Out of Scope Changes checkPassedAll changed code connects to [#23497]. The product change is limited to the option-fallback expression in composerDraftStore.ts. The regression test in modelSelection.test.ts verifies that express…
Full details: Linked Issues check

Explanation

[#23497] requires that a thread saved without options keep provider defaults, not project/settings options. At the reviewed head, deriveEffectiveComposerModelState in apps/web/src/composerDraftStore.ts changes the modelOptions fallback. Draft options still win. Next, the thread selection supplies options when it exists, even when it has none. Project options apply only when threadModelSelection is absent. This matches the issue's stated cause, where options fell back to project/settings defaults while the model resolved from the thread. apps/web/src/modelSelection.test.ts adds a regression case. It uses an optionless thread selection and a project selection with effort: high, and asserts state.modelOptions?.[instanceId] is undefined. Two caveats. The ?? null terminator was removed, so modelOptions can now be undefined rather than null when the thread has no options. The test accepts this, and the author reports web typecheck passed. Second, the send-path dispatch is not traced in this review. The author's browser evidence reports that effort is omitted from dispatch after the change. This review relies on that evidence and on the shared resolver. Coding requirements are met at the state-derivation level.

Full details: Out of Scope Changes check

Explanation

All changed code connects to [#23497]. The product change is limited to the option-fallback expression in composerDraftStore.ts. The regression test in modelSelection.test.ts verifies that expression. scripts/fixtures/composer-option-fallback.mjs is a manual synthetic fixture. It builds the optionless, explicit-low, and inherited thread states used for the bug's before/after proof. knip.jsonc registers scripts/fixtures/*.mjs as Knip entries so that the fixture is not reported as unused. These are verification support for the same fallback behavior, not unrelated product, wire, or persistence changes. The PR description reports no wire or persistence changes. The diffs reviewed here do not show any such change.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Added 2 commits

  • Merge remote-tracking branch 'origin/main' into HEAD430afd6
  • Merge remote-tracking branch 'origin/main' into HEADc0e3d7b