fix(desktop): stop a stale composer model pinning every new chat
Settings → Model while a chat is open flips the composer source to 'default' but leaves the live session's model painted. Sending that value on session.create pinned every new chat and skipped model.default. Only a manual composer pick is a per-session override. Co-authored-by: Tharanee <tharanee@tharanee.net>
This commit is contained in:
committed by
brooklyn!
parent
f98f5e74e0
commit
8b28bdceb5
@@ -60,6 +60,7 @@ import {
|
||||
setCurrentCwd,
|
||||
setCurrentFastMode,
|
||||
setCurrentModel,
|
||||
setCurrentModelSource,
|
||||
setCurrentProvider,
|
||||
setCurrentReasoningEffort,
|
||||
setMessages,
|
||||
@@ -567,6 +568,7 @@ describe('createBackendSessionForSend profile routing', () => {
|
||||
$currentFastMode.set(false)
|
||||
$currentModel.set('')
|
||||
$currentProvider.set('')
|
||||
setCurrentModelSource('')
|
||||
$currentReasoningEffort.set('')
|
||||
setNewChatWorkspaceTarget(undefined)
|
||||
vi.restoreAllMocks()
|
||||
@@ -609,6 +611,67 @@ describe('createBackendSessionForSend profile routing', () => {
|
||||
expect(params).toMatchObject({ source: 'desktop' })
|
||||
})
|
||||
|
||||
// Regression (Settings → Model doesn't stick): a stale composer selection
|
||||
// must not be shipped as a per-session override on a NEW chat.
|
||||
//
|
||||
// Saving Settings → Model while a session is live deliberately leaves
|
||||
// $currentModel painted with the LIVE agent's model (applySavedMainModel
|
||||
// keeps the live session authoritative) and only flips the source to
|
||||
// 'default'. If session.create still sent that value, every new chat was
|
||||
// pinned to the old model and the backend never resolved model.default —
|
||||
// the user-visible "my default won't change" bug.
|
||||
it('omits a default-sourced selection so the backend resolves model.default', async () => {
|
||||
const params = await createWith(() => {
|
||||
// What the composer holds after Settings saved a new default while a
|
||||
// chat was open: the previous session's model, marked default-sourced.
|
||||
setCurrentModel('openai/gpt-5.6-sol')
|
||||
setCurrentProvider('openai-codex')
|
||||
setCurrentModelSource('default')
|
||||
})
|
||||
|
||||
expect(params).not.toHaveProperty('model')
|
||||
expect(params).not.toHaveProperty('provider')
|
||||
})
|
||||
|
||||
it('still sends an explicit manual pick as a per-session override', async () => {
|
||||
const params = await createWith(() => {
|
||||
setCurrentModel('anthropic/claude-opus-5')
|
||||
setCurrentProvider('anthropic')
|
||||
setCurrentModelSource('manual')
|
||||
})
|
||||
|
||||
expect(params).toMatchObject({
|
||||
model: 'anthropic/claude-opus-5',
|
||||
provider: 'anthropic'
|
||||
})
|
||||
})
|
||||
|
||||
// An unset source is the first-run/cleared state — nothing the user picked,
|
||||
// so it must not pin the session either.
|
||||
it('omits the model when no selection source is recorded', async () => {
|
||||
const params = await createWith(() => {
|
||||
setCurrentModel('openai/gpt-5.6-sol')
|
||||
setCurrentProvider('openai-codex')
|
||||
setCurrentModelSource('')
|
||||
})
|
||||
|
||||
expect(params).not.toHaveProperty('model')
|
||||
expect(params).not.toHaveProperty('provider')
|
||||
})
|
||||
|
||||
// Effort and fast mode are independent of the model-override decision.
|
||||
it('keeps sending reasoning effort even when the model is omitted', async () => {
|
||||
const params = await createWith(() => {
|
||||
setCurrentModel('openai/gpt-5.6-sol')
|
||||
setCurrentProvider('openai-codex')
|
||||
setCurrentModelSource('default')
|
||||
setCurrentReasoningEffort('high')
|
||||
})
|
||||
|
||||
expect(params).not.toHaveProperty('model')
|
||||
expect(params).toMatchObject({ reasoning_effort: 'high' })
|
||||
})
|
||||
|
||||
it('passes the current workspace cwd into session.create', async () => {
|
||||
const params = await createWith(() => {
|
||||
$currentCwd.set('/remote/worktree')
|
||||
@@ -658,6 +721,10 @@ describe('createBackendSessionForSend profile routing', () => {
|
||||
|
||||
setCurrentModel('anthropic/claude-sonnet-4.6')
|
||||
setCurrentProvider('anthropic')
|
||||
// A real composer pick marks the selection manual; this test drives the
|
||||
// atoms directly, so set the source explicitly. Only a manual selection
|
||||
// rides along as a per-session override.
|
||||
setCurrentModelSource('manual')
|
||||
setCurrentReasoningEffort('high')
|
||||
setCurrentFastMode(false)
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
$newChatWorkspaceTarget,
|
||||
$sessions,
|
||||
$yoloActive,
|
||||
getCurrentModelSource,
|
||||
getSessionOwnerHint,
|
||||
type NewChatWorkspaceTarget,
|
||||
resolveComposerSessionKey,
|
||||
@@ -242,8 +243,9 @@ function reconcileAuthoritativeMessages(
|
||||
// mode one backend serves every profile, so an omitted profile silently lands the
|
||||
// chat on the launch (default) profile — the "rubberbands back to default" bug.
|
||||
// A no-op for single-profile/local-pooled users (a backend resolves its own launch
|
||||
// profile to None). The sticky UI model/effort/fast ride as per-session overrides,
|
||||
// never the profile default (that lives in Settings → Model).
|
||||
// profile to None). Effort/fast still ride as per-session overrides. Model and
|
||||
// provider only ride when the composer source is 'manual' — a default-sourced
|
||||
// value is a mirror of Settings → Model and must not pin the new chat.
|
||||
async function desktopSessionCreateParams(
|
||||
cwd: string,
|
||||
capturedRoute = resolveNewChatOwnerRoute()
|
||||
@@ -252,11 +254,17 @@ async function desktopSessionCreateParams(
|
||||
// profile handshake below can yield long enough for background config/model
|
||||
// refreshes to finish; reading atoms afterward would silently create the
|
||||
// session with a different selection than the one the user submitted.
|
||||
// Settings → Model while a session is live leaves $currentModel painted with
|
||||
// the live agent (applySavedMainModel) and only flips the source to 'default'.
|
||||
// Shipping that stale value as an override pins every new chat to the old
|
||||
// model. Omit model/provider unless the source is 'manual'.
|
||||
const isManualSelection = getCurrentModelSource() === 'manual'
|
||||
|
||||
const selection = {
|
||||
effort: $currentReasoningEffort.get().trim(),
|
||||
fast: $currentFastMode.get(),
|
||||
model: $currentModel.get().trim(),
|
||||
provider: $currentProvider.get().trim()
|
||||
model: isManualSelection ? $currentModel.get().trim() : '',
|
||||
provider: isManualSelection ? $currentProvider.get().trim() : ''
|
||||
}
|
||||
|
||||
const profile = capturedRoute?.profile || $newChatProfile.get() || normalizeProfileKey($activeGatewayProfile.get())
|
||||
|
||||
Reference in New Issue
Block a user