Skip to content

fix(web): remember the thinking level per model#1838

Merged
wbxl2000 merged 12 commits into
mainfrom
fix/web-per-model-thinking
Jul 17, 2026
Merged

fix(web): remember the thinking level per model#1838
wbxl2000 merged 12 commits into
mainfrom
fix/web-per-model-thinking

Conversation

@wbxl2000

@wbxl2000 wbxl2000 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below.

Problem

kimi-web persisted the thinking level as a single global localStorage value (kimi-web.thinking), shown and submitted verbatim without validation against the active model's catalog. When the active model does not declare the stored level — e.g. a max-only always-on model while a low picked for another model is stored — the composer highlights no segment, and the single-segment control is read-only, leaving an empty thinking picker with no way to switch.

What changed

  • Per-model storage (useKimiWebClient.ts): kimi-web.thinking now holds a JSON map of model id → level. The runtime source of truth is an in-memory map hydrated from localStorage at startup; explicit picks update it first and persist best-effort with a whole-map read-modify-write merge (same pattern as saveUnread), so a pick takes effect even when storage is unavailable, concurrent tabs don't clobber each other's entries, and another tab's pick can't silently change what this tab displays or submits mid-session. A legacy plain-string value (the pre-map global pick) is migrated into the map under a '*' key — a fallback for models that declare it, which per-model entries override but never delete, even across the first rewrite of the key into map format — while a max-only model still can't be trapped by it.
  • Resolution against the catalog (useModelProviderState.ts): new thinkingLevelForModel() — use the model's stored pick when the model still declares it, else fall back to the model's catalog default. Applied at all three moments the active model becomes known or changes: loadModels() (always re-resolved, not only when unset), setModel() (a switch now restores the target model's own stored pick instead of unconditionally pinning the default; switches update the in-memory level only — storage writes stay reserved for explicit picker picks via setThinking), and a watcher on currentModelId() covering session switches, snapshot adoption and late catalog arrival. applyThinkingLevel persists explicit picks under the current model id.
  • modelThinking.ts: new levelDeclaredBy() helper; thinkingLevelForModelSwitch gains an optional storedLevel parameter.
  • Composer.vue / MobileSettingsSheet.vue: comment updates only — the resolver guarantees any level reaching the components is declared by the active model, so the dead "empty picker" state can no longer occur.
  • Submit path (useWorkspaceState.ts): submitPromptInternal and the steer path now resolve the thinking level from the prompt's OWN session model (new thinkingLevelForModelId) instead of the single active-session rawState.thinking — a queue drain for a background session no longer submits the level of the session the user has since switched to (the same cross-model leak on the submit path; found in review). The BTW side-chat path (useSideChat.ts) resolves from the captured parent's model too (a session switch during the startBtw await no longer leaks the new view's level), and skill activation (activateSkill) first persists the resolved level to the target session profile — mirroring the new-session skill path, which itself now resolves against the new session's model — so skills can't run at a stale profile effort; persistSessionProfile now resolves a success flag (it surfaces failures itself and never rejects), and both activation paths abort instead of launching at the stale effort when the persist fails.
  • Tests (modelThinking.test.ts, workspace-state.test.ts): updated for the new behavior and extended with cases for undeclared stored level → default fallback, session-switch re-resolution, per-model persistence, switch restoring the target model's stored pick, and queue-drain resolving the level from the drained session's own model (plus catalog-miss fallback), and model switches not persisting derived defaults (incl. the rollback path), BTW resolving from the parent model across a session switch, skill activation persisting the resolved level before activation, and the legacy pre-map pick falling back per declaring model (incl. per-model override). kimi-web suite: 632 passed; vue-tsc clean; oxlint 0 errors (no new warnings).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1837681

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@1837681
npx https://pkg.pr.new/@moonshot-ai/kimi-code@1837681

commit: 1837681

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ab7898416

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (id === undefined || id === prevId) return;
const model = modelById(id);
if (model === undefined) return;
rawState.thinking = thinkingLevelForModel(model);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve thinking for the session being submitted

When a user queues a follow-up in a session using model A and opens a session using model B before A finishes, this watcher replaces the sole rawState.thinking value with B's preference. The background completion path then drains A's queue through finishPromptLocal(sid)submitPromptInternal(sid), but useWorkspaceState.ts:1488-1494 combines A's model with that global value, so the original cross-model leak can recur and may submit an undeclared effort. Resolve thinking from the submitted session's model (or retain it with the queued prompt) rather than relying on the active-session value.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review


P2 Badge Avoid persisting derived defaults during model switches

When switching to a model with no saved preference, nextThinking is the catalog default, but this call routes it through applyThinkingLevel, which unconditionally writes it to the per-model storage map. Consequently, if the catalog later changes that model's default while the old value remains supported, the old derived value is treated as an explicit user choice and overrides the new default; the same write also occurs before a failed switch is rolled back. Update the in-memory level without persisting here, and reserve storage writes for explicit thinking-picker actions.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b7c1e86c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// which tracks whatever session the user is looking at now: a queue
// drain for a background session would otherwise submit the level of
// the session the user switched to since enqueueing.
thinking: modelProvider.thinkingLevelForModelId(model) ?? rawState.thinking,

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 Resolve thinking for BTW's captured parent model

When openSideChatOn(parent, initialPrompt) is awaiting startBtw, the user can switch sessions and the new active-model watcher changes rawState.thinking; the later sendSideChatPromptOn call still combines the captured parent's model with that active session's level at useSideChat.ts:206-215. This defeats the explicit-parent race protection and can send an unsupported effort that the provider silently drops. Use thinkingLevelForModelId(model) for the BTW submission as done here for the other submitPrompt paths.

Useful? React with 👍 / 👎.

if (id === undefined || id === prevId) return;
const model = modelById(id);
if (model === undefined) return;
rawState.thinking = thinkingLevelForModel(model);

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 Apply restored levels before activating skills

When an existing session's model has a stored level that differs from that session's agent profile, this watcher updates only the picker state. activateSkill at useModelProviderState.ts:351-382 sends only the skill name and arguments, so the daemon runs with the session's stale profile effort even though the UI displays the restored per-model level. Ensure skill activation first applies the resolved level to the target session profile, as the new-session skill path already does.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b76ffb9a1a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +147 to +150
function thinkingLevelForModel(model: AppModel): ThinkingLevel {
const stored = loadThinkingForModel(model.id);
if (stored !== undefined && levelDeclaredBy(model, stored)) return stored;
return defaultThinkingLevelFor(model);

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 Honor thinking picks when localStorage is unavailable

When localStorage is unavailable or setItem throws—for example because of a storage policy or quota—the safe storage helpers swallow the failure, so setThinking updates only rawState.thinking. This resolver then rereads an empty map and returns the catalog default; all prompt, steer, BTW, and skill paths now use that result, causing the UI to show the user's selected level while the daemon receives a different default. Keep the per-model choices in memory as the runtime source of truth and use localStorage only for hydration and best-effort persistence.

Useful? React with 👍 / 👎.

Comment on lines +147 to +150
function thinkingLevelForModel(model: AppModel): ThinkingLevel {
const stored = loadThinkingForModel(model.id);
if (stored !== undefined && levelDeclaredBy(model, stored)) return stored;
return defaultThinkingLevelFor(model);

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 Synchronize the displayed level with cross-tab storage

When another tab changes the thinking level for the same model, this resolver sees the new localStorage map on every submission, but rawState.thinking remains unchanged because the facade only installs a storage listener for unread state. Consequently, one tab can continue highlighting low while silently submitting max after the other tab changes it. Either handle storage events for the thinking key and re-resolve the displayed state, or avoid using an unseen cross-tab value for runtime submission.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70f8582f85

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +164 to +166
function loadThinkingMap(): Record<string, ThinkingLevel> {
const parsed = safeGetJson<unknown>(THINKING_STORAGE_KEY);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};

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 Migrate the existing thinking preference before parsing the map

On the first launch after upgrading, every existing installation has a plain-string value such as "low" under kimi-web.thinking; safeGetJson cannot parse that value, so this returns an empty map and permanently discards the user's explicit preference. The catalog default is then displayed and submitted instead, changing behavior until the user manually selects the level again. Preserve the legacy value and associate it with the active model once the catalog/model ID is available before rewriting the key in map format.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bebfb18b96

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +195 to +197
const thinkingPicks: Record<string, ThinkingLevel> = loadThinkingMap();
const legacyThinkingPick: ThinkingLevel | undefined =
Object.keys(thinkingPicks).length === 0 ? loadLegacyThinkingValue() : undefined;

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 Preserve the legacy fallback after the first per-model write

For an upgraded user with a legacy raw value such as low, the first explicit pick for any model rewrites this storage key as a JSON map containing only that model. On the next reload, this condition sees a nonempty map and drops legacyThinkingPick, so every other model loses the promised legacy fallback. Persist the legacy value alongside per-model overrides, or migrate it before replacing the raw value, so a per-model entry can override it without deleting it for all other models.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9a8ba18bf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// legacy fallback) over the stored map, so concurrent tabs' entries survive
// and the legacy value is rewritten in map format rather than dropped.
const map = loadThinkingMap();
for (const [id, pick] of Object.entries(thinkingPicks)) map[id] = pick;

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 Merge only the changed model pick

When two tabs hydrate an existing multi-model map, changing model A in one tab and then model B in the other causes the second write to copy its entire stale thinkingPicks snapshot over the freshly loaded map, reverting model A's new preference. Persist only the changed modelId entry (plus the legacy fallback when migration requires it), as the delta-based saveUnread pattern does, so edits for different models survive across tabs and reloads.

Useful? React with 👍 / 👎.

wbxl2000 added 7 commits July 17, 2026 19:07
Persist kimi-web.thinking as a JSON map of model id to level instead of a
single global value, and resolve the active level against the model's
catalog (stored pick when still declared, else the model default) at
loadModels, setModel, and on active-model changes via a watcher.

Fixes the empty, unresponsive thinking picker shown for a model that does
not declare a previously stored level (e.g. a max-only model with a stale
global 'low').
submitPromptInternal and the steer path read the single active-session
rawState.thinking, so a queue drain for a background session submitted the
level of whichever session the user had switched to since enqueueing —
the same cross-model leak on the submit path. Thinking now joins model and
the per-session modes in being resolved from the prompt's own session
model (its stored pick when declared, else the catalog default), falling
back to the active value only when the model has left the catalog.
setModel routed the resolved level through applyThinkingLevel, which
writes per-model storage unconditionally — a switch to a model with no
saved pick stored the catalog default as if it were an explicit choice,
pinning the user to it across later default changes, and the rollback
path did the same write for a switch that never happened. Model switches
now update the in-memory level only; storage writes stay with
setThinking, the explicit picker path.
sendSideChatPromptOn combined the captured parent's model with the
active-session level, so a session switch during the startBtw await sent
the BTW first turn at the wrong model's effort — resolve it from the
parent's own model, falling back to the active value off-catalog, same
as the other submit paths.

activateSkill carries no thinking either, so the daemon ran skills at
the session profile effort, which can predate the per-model restore the
picker now shows. Persist the resolved level to the session profile
first, mirroring the new-session skill path; that path itself now
resolves against the new session's model instead of the raw active
value.
The resolver re-read localStorage on every submission, letting storage —
not the displayed state — decide what the daemon receives: with storage
unavailable (policy/quota) an explicit pick reached the UI while every
submit path fell back to the catalog default, and a pick made in another
tab silently changed what this tab submits mid-session.

Per-model picks now live in an in-memory map hydrated from localStorage
at startup; explicit picks update it first and persist best-effort
(read-modify-write merge, so concurrent tabs' entries still survive).
localStorage is only hydration plus persistence — another tab's pick can
no longer alter this tab's runtime level.
Pre-map installs stored a single global level as a raw string; the map
parser dropped it, silently resetting the user's explicit preference to
the catalog default on upgrade. The legacy value is now carried as a
fallback for models without their own entry — validated against each
model's catalog at resolution, so effort models keep the user's pick
while a max-only model still falls through to its default and can never
be trapped by it.
The first explicit pick after an upgrade rewrote the raw legacy value
into a map containing only that one model, so the next reload saw a
nonempty map and dropped the legacy fallback for every other model.
The migrated value now lives inside the map under a '*' key that no
real model id can collide with: per-model entries override it, and
rewrites persist it alongside them instead of deleting it.
@wbxl2000
wbxl2000 force-pushed the fix/web-per-model-thinking branch from a9a8ba1 to 0b844e7 Compare July 17, 2026 11:09
Overlaying the whole in-memory map on write could revert a newer pick
made in another tab for a model this tab still held a stale copy of.
Write the changed entry alone (delta-style, like saveUnread), carrying
only the migrated legacy '*' fallback along so it survives the first
rewrite into map format.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 818bfb5f74

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 388 to 392
await persistSessionProfile(
{ thinking: thinkingLevelForModelId(skillModel) ?? rawState.thinking },
sid,
);
await getKimiWebApi().activateSkill(sid, skillName, args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Abort skill activation when the thinking profile update fails

When the /profile request fails but the skill activation endpoint remains available, this still starts the skill with the session's stale thinking effort. The injected persistSessionProfile in useKimiWebClient.ts catches every update/refresh error and resolves its promise, so awaiting it here does not enforce the ordering claimed by the comment and execution continues to activateSkill. Have profile persistence return a success result or reject for this call, and do not activate the skill after a failed thinking update.

Useful? React with 👍 / 👎.

persistSessionProfile surfaces failures itself and resolves, so awaiting
it never blocked a following activation: a failed /profile write still
launched the skill at the session's stale effort. It now resolves a
success flag; both activation paths (existing session and new-session
draft) gate on it and skip activating when the persist fails, without
reporting a second, synthetic error.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94383b31e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +398 to +401
const persisted = await persistSessionProfile(
{ thinking: thinkingLevelForModelId(skillModel) ?? rawState.thinking },
sid,
);

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 persisting the new-session skill profile twice

When a skill is launched from the empty composer, startSessionAndActivateSkill already awaits persistSessionProfile with the resolved thinking level before calling activateSkill (useWorkspaceState.ts:1207-1220). This unconditional persist repeats the profile update and status refresh; if that redundant second update transiently fails, the sentinel path cancels the skill even though the prerequisite profile was successfully applied. Let the new-session path skip this second persist or consolidate profile persistence inside a single activation call.

Useful? React with 👍 / 👎.

startSessionAndActivateSkill persisted the resolved thinking and then
activateSkill persisted it again unconditionally — a redundant profile
update and status refresh whose transient failure would false-veto an
activation whose prerequisite profile was already applied. Thinking is
now written by activateSkill alone (the single, gated writer); the
draft patch carries only model, plan/swarm and permission.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex review

oxlint --type-aware (only-throw-error) rejects throwing a Symbol; the
identity-based sentinel works the same as a shared Error instance.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2175508baa

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +222 to +226
const map = loadThinkingMap();
const legacy = thinkingPicks[LEGACY_THINKING_PICK_KEY];
if (legacy !== undefined) map[LEGACY_THINKING_PICK_KEY] = legacy;
map[modelId] = level;
safeSetJson(THINKING_STORAGE_KEY, map);

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 Serialize per-model storage updates across tabs

When two tabs pick levels for different models at nearly the same time, both can call loadThinkingMap() before either calls safeSetJson(), after which the last writer replaces the first tab's entry. Individual localStorage operations are synchronous, but this read-modify-write sequence is not atomic, so overlaying only the changed entry does not provide the claimed concurrent-tab preservation. Store models under separate keys or add a synchronization/version-and-retry mechanism.

Useful? React with 👍 / 👎.

Comment on lines +397 to +400
const skillModel = rawState.sessions.find((s) => s.id === sid)?.model;
const persisted = await persistSessionProfile(
{ thinking: thinkingLevelForModelId(skillModel) ?? rawState.thinking },
sid,

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 Resolve empty session models through the default before skills

When activateSkill targets an explicit session whose cached model is empty—a state the client already handles for daemon profile echoes—thinkingLevelForModelId(skillModel) returns undefined and the fallback uses rawState.thinking, which belongs to the currently active view. In the new-session skill flow, a session switch during the preceding profile request can therefore persist another model's effort onto the target session if the status refresh leaves its cached model empty. Resolve session.model || rawState.defaultModel here, as the prompt and side-chat paths do, before selecting the thinking level.

Useful? React with 👍 / 👎.

…kills

session.model can be '' transiently (daemon profile echo), so
activateSkill fell back to the raw active-view level; in the new-session
flow a concurrent switch could persist another model's effort onto the
target session. Normalize '' through the configured default_model first,
same as the prompt/BTW/steer paths.
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 1837681f3f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@wbxl2000
wbxl2000 merged commit 9e12484 into main Jul 17, 2026
14 checks passed
@wbxl2000
wbxl2000 deleted the fix/web-per-model-thinking branch July 17, 2026 11:56
@github-actions github-actions Bot mentioned this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant