Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/web-composer-default-model-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: The composer model switcher switches the active session's model as before and additionally bumps the global default model, so new sessions inherit the choice.
24 changes: 22 additions & 2 deletions apps/kimi-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,27 @@ function openLogin(): void {

async function handleSelectModel(modelId: string): Promise<void> {
showModelPicker.value = false;
await client.setModel(modelId);
// Same semantics as the composer dropdown rows: the overlay is just the
// "more models" continuation of the same flow, so it must also bump the
// global default (see handleComposerSelectModel).
await handleComposerSelectModel(modelId);
}

async function handleComposerSelectModel(modelId: string): Promise<void> {
// Primary action: switch the active session's model via POST /sessions/{id}/profile
// (same as the model picker overlay). Awaited so the model pill reflects the
// result and failures surface. In the onboarding draft this just stores the
// pick for the first session.
const switched = await client.setModel(modelId);

// Side effect: also bump the daemon-wide default model via POST /config so
// new sessions inherit the choice. Fire-and-forget — it must not block the UI
// or mask the session switch. Only after a confirmed switch (a stale/invalid
// alias must not become the global default), and skip when it already
// matches the default.
if (switched && modelId !== client.defaultModel.value) {
void client.updateConfig({ defaultModel: modelId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid racing default-model persistence

When a user changes the model in an existing session and immediately starts a new session, this fire-and-forget POST /config can still be in flight while createSession is sent without an explicit model (unless there is a draft pick), so the daemon may create that first new session with the old persisted default even though the UI just accepted the switch. Please either serialize/await the default update before new-session creation or optimistically carry the selected model into the next draft/default path.

Useful? React with 👍 / 👎.

}
}

async function handleAddProvider(input: { type: string; apiKey?: string; baseUrl?: string; defaultModel?: string }): Promise<void> {
Expand Down Expand Up @@ -759,7 +779,7 @@ function openPr(url: string): void {
@archive-session="(id) => client.archiveSession(id)"
@compact="client.compact()"
@pick-model="openModelPicker()"
@select-model="client.setModel($event)"
@select-model="handleComposerSelectModel($event)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route composer picker selections through the default update

When a user opens the composer dropdown and uses “More models”, Composer.vue emits pickModel, this @pick-model opens the shared ModelPicker, and the picker’s @select still goes through handleSelectModel, which only calls client.setModel. As a result, choosing any non-current-provider/non-starred model from the composer flow still leaves defaultModel unchanged for new sessions, so the fix only works for the quick rows in the dropdown.

Useful? React with 👍 / 👎.

@open-file="openFilePreview($event)"
@open-media="openMediaPreview($event)"
@open-thinking="openThinkingPanel($event)"
Expand Down
11 changes: 8 additions & 3 deletions apps/kimi-web/src/composables/client/useModelProviderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ export function useModelProviderState(
* can return model '', so the authoritative current model comes from
* GET /sessions/{id}/status, which we re-read right after. Optimistically show
* the chosen id meanwhile. Never crashes.
*
* Returns whether the switch was accepted (true for the draft path too), so
* callers can gate follow-up persistence (e.g. bumping the global default) on
* a confirmed switch — errors are surfaced here, not thrown.
*/
async function setModel(modelId: string): Promise<void> {
async function setModel(modelId: string): Promise<boolean> {
const sid = rawState.activeSessionId;
const nextThinking = coerceThinkingForModel(modelById(modelId), rawState.thinking);
const prevThinking = rawState.thinking;
Expand All @@ -191,7 +195,7 @@ export function useModelProviderState(
// Remember the pick — startSessionAndSendPrompt applies it at create time.
draftModel.value = modelId;
applyThinkingLevel(nextThinking);
return;
return true;
}
// Optimistic: show the chosen model immediately, but remember the previous
// one so we can roll back if the switch never reaches the daemon.
Expand All @@ -217,12 +221,13 @@ export function useModelProviderState(
saveThinkingToStorage(prevThinking);
}
pushOperationFailure('setModel', err, { sessionId: sid });
return;
return false;
}
// refreshSessionStatus folds the authoritative current model from /status
// back into the session (the profile echo can return ''). Best-effort: a
// failure here does not mean the switch failed, so it must not roll back.
await refreshSessionStatus(sid);
return true;
}

/** Toggle whether a model is starred (favorited) in the model picker. */
Expand Down
Loading