Skip to content

fix(web): follow the thread's model after another client changes it - #10778

Closed
alizenhom wants to merge 1 commit into
pingdotgg:mainfrom
alizenhom:fix/web-release-sent-model-selection
Closed

alizenhom wants to merge 1 commit into
pingdotgg:mainfrom
alizenhom:fix/web-release-sent-model-selection

Conversation

@alizenhom

@alizenhom alizenhom commented Sep 8, 2026 •

Copy link
Copy Markdown

Fixes #10684.

After a send, the web composer kept its stored model selection for that thread. That selection wins over the server thread's model for both the picker and the next send, so when another client switched the thread to a different model, desktop kept showing the old one and silently wrote it back on the next message.

Every explicit model pick on a thread draft now gets its own id. A send captures that id, and once the turn starts the composer releases the selection only while the draft still holds that id. A pick made while the send was in flight, including a same-value re-pick, has a different id and stays. With the override gone, the composer follows thread.modelSelection, so a change from another client shows through and the next send no longer reverts it.

A draft session is only marked sent, because its local placeholder thread shows the project default until the server thread arrives. The marker is persisted, and promotion drops the selection instead of moving it onto the server thread. Draft storage moves to version 10; the migration also strips never-picked seeds from server-thread drafts, which fixes threads created before this change without one more silent reversal.

Known limitation: explicit picks persisted before this change carry no marker, so a stale one is released only after the next send from this client. Seeded ones are covered by the migration. Mobile has its own fix in #10205; no wire, server, or mobile changes here.

Verification

Two paired browser tabs against one isolated dev server, Claude provider, same thread. The thread's model_selection_json was read from the projection table after each step.

Step Before (main) After (this branch)
Tab 1 picks Sonnet 5, sends thread: sonnet-5 thread: sonnet-5
Tab 2 picks Opus 5, sends thread: opus-5, tab 1 still shows Sonnet 5 thread: opus-5, tab 1 shows Opus 5
Tab 1 sends again thread reverts to sonnet-5 thread stays opus-5
Tab 1 picks Fable 5.1 without sending, tab 2 switches to Sonnet 5 and sends not run thread: sonnet-5, tab 1 keeps its unsent Fable 5.1 pick

Recordings of tab 1; the second tab acts off-screen.

Before, on main: tab 1 keeps showing Sonnet 5 after the remote switch to Opus 5, and its next send reverts the thread.

After, on this branch: tab 1 switches to Opus 5 when the remote change lands, and its next send leaves the thread on Opus 5.

Reloading both tabs between the runs migrated their v9 storage to v10 in place.

Tests: composer draft store suite (115 tests, 9 new) covering the pick id rules, release on server threads, a newer pick surviving the in-flight window, the draft-session hold and promotion drop, and the persisted marker across a reload. Model selection, thread action, and ChatView logic suites pass (169 tests). Web typecheck, lint on the changed files, and formatting pass.

Prepared with Claude Fable 5.1 in Claude Code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Model selections now stay aligned with the model used by the conversation after a message is sent.
    • Composer selections are updated after a send is successfully confirmed, including plan follow-ups.
    • New model choices made after sending are preserved until they are used.
    • Draft content and unsent model selections are retained more reliably across conversations.

After a send, the desktop composer kept its stored model selection for
that thread. That selection wins over the server thread's model for both
the picker and the next send, so when another client switched the thread
to a different model, desktop kept showing the old one and silently wrote
it back on the next message.

Every explicit model pick now gets its own id. A send captures that id and
releases the selection after the turn starts, only while the draft still
holds that id, so a pick made in flight survives. A draft session is only
marked sent and promotion drops the selection, since its local placeholder
thread would otherwise show the project default until the server thread
arrives. Draft storage moves to version 10 and the migration strips
never-picked seeds from server-thread drafts.

Fixes pingdotgg#10684.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 8, 2026
@alizenhom alizenhom closed this Sep 8, 2026
@alizenhom
alizenhom deleted the fix/web-release-sent-model-selection branch September 8, 2026 13:34

export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";
const COMPOSER_DRAFT_STORAGE_VERSION = 9;
const COMPOSER_DRAFT_STORAGE_VERSION = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/composerDraftStore.ts:69

Existing v9 drafts containing previewAnnotations lose those annotations during the v10 migration, and the next persistence write permanently discards them. normalizePersistedDraftsByThreadId treats previewAnnotations as user content but does not copy it into the normalized draft; preserve that field alongside the other draft content fields.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/composerDraftStore.ts around line 69:

Existing v9 drafts containing `previewAnnotations` lose those annotations during the v10 migration, and the next persistence write permanently discards them. `normalizePersistedDraftsByThreadId` treats `previewAnnotations` as user content but does not copy it into the normalized draft; preserve that field alongside the other draft content fields.

@macroscopeapp

macroscopeapp Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Would Approve

Macroscope's review found this PR approvable — This is a focused web bug fix that restores server-thread model authority after successful sends, with targeted coverage for concurrent picks, draft promotion, and reloads. The v10 migration has an unresolved high-severity risk of dropping existing preview annotations, which must be addressed independently.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d70e2f82-f4a1-4619-a236-eb95c5b3869e

📥 Commits

Reviewing files that changed from the base of the PR and between 061543e and 0853cd4.

📒 Files selected for processing (3)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts

📝 Walkthrough

Walkthrough

The composer now assigns identities to explicit model picks, retains picks through asynchronous sends, and releases them after successful turn startup. Draft persistence and migration preserve sent state, while regular sends and plan follow-ups invoke the release operation.

Changes

Composer model selection lifecycle

Layer / File(s) Summary
Selection identity and draft state
apps/web/src/composerDraftStore.ts
The draft store adds selection identity and sent-state fields. Explicit model and provider-option writes create fresh identities. Sticky-state application removes transient selection markers.
Selection release and persistence
apps/web/src/composerDraftStore.ts, apps/web/src/composerDraftStore.test.ts
releaseModelSelection clears confirmed server-thread selections or marks draft-session selections for removal during promotion. Persistence, hydration, migration, and tests cover sent selections and legacy model seeds.
Send confirmation integration
apps/web/src/components/ChatView.tsx
Regular sends and plan follow-ups capture the current selection identity and release it after successful turn startup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant ChatView
  participant ComposerDraftStore
  participant TurnStartup
  ChatView->>ComposerDraftStore: Capture modelSelectionId
  ChatView->>TurnStartup: Start turn or plan follow-up
  TurnStartup-->>ChatView: Confirm successful startup
  ChatView->>ComposerDraftStore: releaseModelSelection
Loading

Suggested reviewers: t3dotgg, maria-rcks

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@alizenhom

Copy link
Copy Markdown
Author

Superseded by #10780 after renaming the head branch; the diff is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop keeps the old model after the same thread changes on mobile

2 participants