fix(desktop): make model and thinking-level changes instant without success toasts - #3749
fix(desktop): make model and thinking-level changes instant without success toasts#3749Sun-GLiang wants to merge 11 commits into
Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found blocking issues.
[P2] Stop cannot cancel the pre-send model-setup flight
app-shell-chat-actions.ts:493-508 arms the turn before waiting for settleSessionModelSettings. Stop at that point only calls sessions.stop, which returns immediately when no Host turn exists, so the original send still proceeds after settings settle despite user cancellation.
[P2] Active-Turn follow-up bypasses the settlement barrier
app-shell.tsx:1940-1952 enqueues follow-ups directly without going through the settling actions.send path. A model mutation failing to settle still leaves the follow-up queued under the old model, violating the "latest mutation silently settles before immediate send" contract.
Checks on 3686d2aaf are test: success.
简体中文
存在预发送取消与跟随路径两项阻断。|
Addressed both blocking paths in ee34dd0.
Added regression coverage for local cancellation, preserved Host stop behavior, successful follow-up settlement, and failed settlement. Desktop tests pass 1496/1496; typecheck, Biome, and the production build also pass. |
Astro-Han
left a comment
There was a problem hiding this comment.
Update on ee34dd0026:
I found blocking issues that persist/drift in this head.
[P1] Follow-up can still start a new root after Stop
A follow-up submitted on an active turn waits for settings, then submitFollowUpAfterModelSettings has no cancellation identity. If the user presses Stop during the wait, preSendSettingsRef is cleared but the follow-up waiter is not cancelled — it still enqueues and the idle next_turn root starts.
[P2] Root/follow-up dispatches without re-checking surface owner
After the settings wait, the dispatch does not re-verify surface owner. Switching from surface A to B during the wait still sends with A's intent.
[P2] Thinking intent rolls back another client's new model
A stale thinking intent holding an old (model, thinking) tuple can overwrite a newer model B committed by another client.
Checks on ee34dd0026 are test: failure (slash-menu unrelated flake but still not green) — not green.
简体中文
存在跟随提交与所有者/模型回滚三项阻断。|
Addressed the latest settlement/readiness findings in b1ae2a7.
Added regression coverage for all three races. Desktop tests pass 1516/1516; production build, typecheck, Biome, and diff checks also pass. |
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found blocking issues.
[P2] use-session-model-settings-intent.ts:118 — model equality used as ordering token
A thinking write M/high commits, another client then commits M/low and the new catalog snapshot arrives before the older IPC response. Because model is unchanged, the stale M/high is accepted and stamped at the current revision, leaving the overlay wrong if the best-effort refresh fails. Carry a causal token (host revision) through mutation result and catalog.
[P2] chat-model-switcher.tsx:231 — accessibility loses current/success state after toast removal
Fixed aria-label replaces visible label, current check is aria-hidden, no selected state, and status only announces warnings. After removing the toast, AT cannot read the current model or hear success.
[P3] app-shell.tsx:1773 — cancellation epoch tombstones accumulate
Every Stop inserts an epoch, but clearSessionRendererState never cleans it, leaving renderer-lifetime tombstones.
Checks on b1ae2a754f are test: success.
简体中文
存在两项正确性与一项可及性阻断,另有一项生命周期 P3。|
Thanks for the thorough work here — the test coverage in particular is careful. But I'd like to redirect the approach before this goes further. The seam already exists. Two of the three reported problems need no new machinery. Stacked toasts and the wrong Part of this belongs to #3347, which we have not decided to do. Settle-before-send, Stop cancelling an in-flight settings write, and gating follow-ups on settlement all address "the send may use a configuration the Host has not committed yet". That is #3347's problem, and it puts renderer-side ordering in front of Host turn admission. Please drop those here. Suggested shape — this should come out as a net deletion:
Two smaller things worth closing while you are here: 简体中文感谢你细致的工作,测试尤其扎实。但在继续之前我想调整一下方向。 这个 seam 已经存在。 三个问题里有两个不需要新机制。 toast 堆叠和 有一部分属于 #3347,而我们还没有决定要做它。 发送前 settle、Stop 取消在途的设置写入、follow-up 进队列前先 settle,解决的都是「发送时用的配置可能还没提交到 Host」——那是 #3347 的问题,而且它让渲染层排在了 Host 的 Turn 准入之前。请从本 PR 移除。 建议的形状,结果应该是净删除:
另有两处顺手可以收掉: |
|
Following up with the full reasoning behind that suggestion — it is a fairly large redirect, so it seems only fair to show the whole picture rather than just the conclusion. Two mechanisms for one factDesktop currently has two different answers to "a session setting write is in flight", and which one you get depends on which setting you touched.
Every one of these five settings goes through the same shape on the Host side — the same flowchart TB
subgraph A["useSessionSettingIntent — 144 lines, generic"]
A1["Plan mode"] --> AI["desired / committed<br/>committedAtCatalogRevision / inFlight"]
A2["Orchestration mode"] --> AI
AI --> AO["overlay: shows immediately, latest wins"]
AI --> AW["write loop: re-reads desired<br/>rolls back to committed on failure"]
AW --> AR["refreshCatalog, retire overlay by catalogRevision"]
end
subgraph B["pending registry + toast"]
B1["Model"] --> BR["keysRef: Set of sessionId"]
B2["Thinking level"] --> BR
B3["Permission mode"] --> BR2["keysRef: Set of key"]
BR --> BS["pendingSessionModelBySession"]
BR2 --> BS2["pendingPermissionModeBySession"]
BS --> BD["disabled + spinner"]
BS2 --> BD
BR --> BX["repeat click dropped"]
BW["write succeeds"] --> BT["toast.success"] --> BF["refreshSessions"]
end
What the difference looks like in timesequenceDiagram
autonumber
participant U as User
participant C as Control
participant S as settings-actions
participant R as pending registry
participant H as Runtime Host
Note over U,H: today, model / thinking / permission
U->>C: pick B
C->>S: setSessionModel(B)
S->>R: keysRef.add(sessionId)
S->>S: setPendingBySession(true)
Note over C: disabled + spinner, still shows A
U->>C: quickly pick C
C->>S: setSessionModel(C)
S-->>C: has(sessionId), returns early — C is lost
S->>H: sessions:setModel(B)
H-->>S: SessionSummary(B)
S->>S: latestAssistantModelId(messages) for the "from" label
S->>U: toast "A to B"
S->>H: refreshSessions()
S->>R: delete + setPendingBySession(false)
sequenceDiagram
autonumber
participant U as User
participant C as Control
participant I as useSessionSettingIntent
participant H as Runtime Host
Note over U,H: today, plan / orchestration
U->>C: pick B
C->>I: request(sessionId, B)
I->>C: overlay = B, shown immediately, not disabled
I->>H: write(B)
U->>C: quickly pick C
C->>I: request(sessionId, C)
I->>I: desired = C, no second write started
I->>C: overlay = C
H-->>I: committed(B)
I->>H: write(C)
H-->>I: committed(C)
I->>H: refreshCatalog()
I->>I: newer catalogRevision retires the overlay
The second diagram is the behaviour your issue asks for. It is already in the tree. What disappears if the three settings move over
Nothing new is added: For permission mode, the bypass confirmation moves to the call site — confirm first, then What we give upThe control is no longer disabled while a write is in flight. That is the point of your issue, so I think it is a gain — but it is a real behaviour change and worth stating plainly. Two smaller things in the same area
"Why can't I change this right now" is derived in three places — Neither is a blocker. They are cheap to close while the area is open, and both go away on their own once the three settings share one path. Happy to talk through any of this if you see it differently — you have been in this code more recently than I have. 简体中文补上建议背后的完整理由。这是一次幅度不小的方向调整,只给结论不太公平。 一个事实,两套机制Desktop 现在对「会话设置写入在途」有两个不同的答案,取决于你动的是哪个设置。
这五个设置在 Host 侧走的是完全相同的形状——同一个 上面两张时序图分别对应这两条路径。第二张就是你 issue 里想要的行为,它已经在仓库里了。 三个设置迁过去之后会消失的东西
不需要新增任何东西: 权限模式的 bypass 确认弹窗移到调用点——先确认,再 让出的东西写入在途时控件不再被禁用。这正是你 issue 的目的,所以我认为是收益,但它确实是一次行为变化,应该明说。 同一片区域的两个小问题
「为什么现在不能改」在三个地方各推导了一遍—— 两个都不阻塞。趁这块打开时收掉很便宜,而且三个设置合并到同一条路径后它们会自行消失。 如果你有不同看法,随时聊——这块代码你比我更熟。 |
071ec88 to
3fdf585
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
Update on 3fdf5857a0:
[P2] Model intent fire-and-forget allows send on stale model
app-shell.tsx:3030-3032 fires modelIntent.request() without waiting, and app-shell-chat-actions.ts:349-350 send() no longer waits for settleSessionModelSettings. The Host setModel IPC can still be pending when sessions.send runs, so the turn can start with the old model A even though the UI shows B.
Fix: restore a per-session settlement barrier between model change and send, or carry expected model/version into send and retry on mismatch.
Checks on 3fdf5857a0 pending — code is NO-GO independent of CI.
简体中文
存在模型意图与发送竞态导致的错模型启动。8b55108 to
9a20b96
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
I reviewed this head and found a blocking issue.
[P2] Model selection optimistic write races Session Send
app-shell.tsx:3080-3087 writes model setting optimistically and returns immediately; app-shell-chat-actions.ts:502-516 sends Session without waiting. turn.start may capture old model before session.configuration.update commits — user sees B but first turn runs with A. Shared admission only serializes enqueued ops, not cross-request ordering.
Fix: restore per-Session settlement barrier or add expected-revision fence to turn.start; add delayed-write regression test.
简体中文
模型切换与发送存在竞态。Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
The race described here is a valid configuration-ordering concern, but it is explicitly out of scope for #3745 and tracked by #3347. Earlier review on this PR specifically asked to remove settle-before-send, Stop cancellation, and follow-up gating because renderer-side ordering should not own Host turn admission. The current head and its architecture test follow that direction. |
There was a problem hiding this comment.
Retracting this review. It contradicted my own earlier feedback on this PR: on 2026-08-25 I asked for a causal token carried through the mutation result and catalog, and flagged that a stale thinking intent holding an old (model, thinking) tuple can overwrite another client's newer model. The compound modelConfigurationIntent and the sessions:setModelConfiguration operation are the response to exactly that, so asking to split them back apart would reintroduce the bug I asked to have fixed. The inline comment about thinking writes carrying the model has been deleted for the same reason.
The remaining inline comments stand on their own; I will re-issue anything still outstanding as a fresh review.
Contradicted my own earlier guidance on this PR; re-reviewing.
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewing after retracting my previous review, which contradicted my own earlier feedback here.
To be explicit so the retraction is not ambiguous: the compound modelConfigurationIntent and the sessions:setModelConfiguration operation are the right answer to what I raised on 2026-08-25 — a stale thinking intent carrying an old (model, thinking) tuple overwriting another client's newer model, and the need for a causal token through the mutation result and catalog. committedAtCatalogRevision is that token. Please disregard the earlier suggestion to split them back into independent controllers; #3745's "independent" wording predates that feedback.
The direction of the PR is right and the deletions are real: both change registries, both pending prop chains, the switcher's pending/loading, the success toasts and their copy in two languages are gone.
Two things on convergence, plus the inline notes.
Side chat still runs its own mechanism. use-quote-companion.ts:217,817-833 keeps a permissionModePending boolean with discard-on-reentry, and this PR adds a permissionChanging string to conversation-copy.ts — reviving in a new file the same copy just deleted from shell-copy.ts. That is the "one fact, two mechanisms" this PR sets out to remove. If side chat is deliberately out of scope, worth saying so in the body, and the new copy should not land.
One piece of scope is outside #3745. The catalog-diff teardown effect and session-catalog-removals.ts are not asked for by the issue, and they are where the widest inline note below sits. What this PR needs from that effect is the two new intent overlays cleared.
AI use: Claude Code assisted with source and issue investigation; the analysis and conclusions are my own.
|
On the send-ordering race: you are right, and that automated finding is non-blocking here. Renderer-side settlement was removed at my request because ordering in front of Host turn admission is the wrong place for it, and #3347 is open for exactly this. If we fence it, I agree it should be a Host-side expected-revision check on Separately, my earlier review today was retracted: it asked you to split the compound model/thinking intent back apart, which would have undone the stale-tuple fix I asked for on 08-25. Sorry for the churn — the re-review reflects the corrected position. |
|
Addressed the review summary in e7386e5. Side chat permission changes now use the same useSessionSettingIntent seam: the selected mode is projected immediately, rapid changes coalesce latest-wins, and a stale first response cannot replace the newer overlay. Removed permissionModePending and the newly added permissionChanging copy. The compound modelConfigurationIntent and sessions:setModelConfiguration remain intact. Local verification passes: Desktop 1532/1532, Desktop typecheck, production build, full-repo Biome, and diff checks. |
|
Thanks for continuing to converge this toward the existing Before another review round, could you rebase onto current This issue should ideally land as a net simplification around the existing intent owner. Once the refreshed diff contains only that end state and the necessary behavioral coverage, we should be able to review and merge it quickly. Please push back where an apparently adjacent path is required to preserve the same setting-intent invariant. AI-assisted review disclosure: Codex assisted with the cumulative-diff inventory; Astro-Han reviewed the accepted direction and owns this recommendation. |
8e5c596 to
d27f6fc
Compare
|
Hi! @Astro-Han Thanks for the scope review. I rebuilt this PR on top of the latest Kept because they are directly required by #3745 or by bugs identified during review:
Removed from this PR:
The diff is now 25 files ( Verification:
Please review from |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for carrying this through several rounds of review and simplification. On exact head d27f6fc, the model and thinking-level interaction now converges on one compound configuration path, cleanup follows explicit deletion facts, the dead model-only IPC and brittle source-text tests are removed, and the no-session persistence guard is preserved. The previous review findings are addressed, and I found no remaining P0–P3 issue.
The exact-head test check is currently queued, so that remains the normal merge-time gate.
This was an AI-assisted review using Codex across multiple review rounds. I independently verified the current head, the addressed findings, production ownership, and merge state, and I own this approval.
|
Merging after the CI pass! |
|
Thanks — I was ready to merge once CI passed, but the exact-head |
|
CI update on exact head The checker’s 60 fixture tests pass; the repository check then rejects the PR’s renderer debt growth relative to base
Updating I will keep the reviewed behavior intact — the compound I’ll request another review after the architecture check and targeted regression tests are green. |
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
The model picker now exposes radio semantics, but the accessibility E2E still queried plain menu items. Assert menuitemradio so the test follows the rendered accessibility tree. Generated-by: OpenAI Codex
Model picker options now expose menuitemradio semantics. Update the affected Storybook play assertions and verify the selected thinking level through aria-checked. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
f74f3e6 to
5dff845
Compare
Resolve the renderer architecture inventory against the merged AppShell source. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
|
Hi @Astro-Han , I’ve pushed the latest updates, resolved the merge conflicts with Since these commits were added after the previous approval, could you please take another look at the updated diff and re-approve if everything looks good? Thanks! |
# Conflicts: # apps/desktop/renderer-architecture.json # docs/astryx-surface-file-inventory.md
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed the delta from d27f6fc3 (where I approved on Aug 31) to 96f1682a. Confirming first that all four of my open threads are genuinely closed in code, not by assertion — I've resolved them:
sessions:setModelis gone; onlysessions:setModelConfigurationremains (runtime-host-session-catalog-ipc-main.ts:209).model-settings-visual-contract.test.tsis deleted, so nothing asserts design intent by regex over source any more.session-catalog-removals.tsno longer exists, andclearSessionRendererStateis now reached only fromapp-shell-effects.ts:190/250and the explicit row action — absence and deletion are no longer the same fact.- The no-session permission branch is no longer a hazard:
setNewTaskPermissionModeresolves touseNewTaskChoice's synchronous setter, so there is no async persistence to reorder.
The delta itself holds up. The features/session-settings/ folder with ports.ts + services-context.tsx + a desktop adapter is the same shape features/goals already uses, and packages/ui already carries non-visual state hooks (use-composer-draft, use-transcript-projection, daily-review-view-state), so extracting the intent hook there follows precedent rather than inventing a home. Generalizing it to multi-channel is what lets the main chat and the side chat share one seam, and it is the replacement for the deleted app-shell-session-settings-actions.ts — one mechanism swap, so I'm not asking for a split.
Approving at 96f1682a. Two P2s and one P3.
P2-1 — the side chat gains a Full access confirmation it never had, and the description does not say so. On main, the main chat confirms bypass (app-shell-session-settings-actions.ts:122-127) while the side chat writes it straight through (use-quote-companion.ts:952). Routing both onto one intent seam makes closing that gap part of this change — that's the right call. But Summary line 5 reads as a sequencing note about an existing confirmation ("happens before the permission intent starts"), so a reader of the PR, the squash message, or the release notes cannot learn that unconfirmed Full access in the side chat was possible before and is not now. This is the only security-relevant change in the PR; please give it its own line.
P2-2 — the new Full access coverage tests a three-line helper instead of the path. declining Full access does not persist the side-chat permission mode calls requestPermissionModeWithConfirmation directly with two fake closures. It never mounts useQuoteCompanion, even though the same file already sets up createRoot, WorkbarServicesProvider, and createFakeWorkbarServices for the retry tests. The regression it exists to prevent — setPermissionMode (line 1004) calling requestPermissionMode(id, mode) directly, which is exactly what main does today — leaves this test green. Asserting through the hook is cheap here and is the difference between proving the helper's if and proving the confirmation is on the path.
P3 — the PR roughly doubled after approval and the Summary no longer describes it. 25 files / +759/-929 at d27f6fc3, 47 files / +1734/-1108 now. Added since: the session-settings feature folder and its services wiring, the packages/ui extraction, and the side-chat confirmation. Each is defensible individually, as above, but the Summary still describes only the instant-feedback and toast removal. Naming the structural moves would let the next reader see the same PR I just re-read.
AI use: re-reviewed with Claude Code (comparing each head's diff against its own merge-base to isolate PR-owned changes from what the merges pulled in). I verified each of the four resolved threads against the code at 96f1682a, the pre-existing side-chat gap on main, the confirmation's wiring through WorkbarHost → WorkbarSurface → QuoteCompanionPanel, and the new test's actual entry point myself.
|
Follow-up to my re-review — I went back to answer why the diff doubled, and two of the three blocks are not what I first assumed. Neither of these is a merge condition; the second in particular is a direction, not a request. The joining-the-DI-architecture block is not invention: The At One consequence points the wrong way for where this codebase is heading. Dropping the generalization removes the On the plan-mode split, I'd withdraw the way I framed it. I said three channels reach the Host through The more general representation is the injected closure: the port can be expressed as one, the reverse is not true, and plan mode proves it. So the lowest-entropy shape is for the wrapper to receive four The fair counter is that this is the one thing that would not match the other seven features, and that if session-settings later follows goals and module-hub below AppShell, the context port becomes load-bearing. I don't think that outweighs a four-channel boundary with one channel outside it today, but it is a real trade and it is yours to make. Both of these are structural, and this PR has already doubled once. I would not do either here. They belong with the next round of controller-scoping work, alongside #4316 and #4315. AI use: audited with Claude Code (full read of the shared hook, the desktop wrapper's four channels, the seven sibling feature ports and adapters, and AppShell's direct bridge usage). I verified the pre-PR hook's arity and instantiation sites, the absence of cross-channel coordination, and that |
Summary
useSessionSettingIntentcontrollers. Controls show the latest requested value immediately, rapid changes converge on the latest intent, terminal failures roll back to the last committed value, and newer catalog revisions retire optimistic overlays.Fixes #3745
Verification
Visual verification
Before
Rapid thinking-level changes created stacked success toasts with obsolete intermediate values.
After — instant, no spinner
Model and thinking-level controls reflect each selection immediately without a spinner, disabled flicker, or success toast.
Static final state (
GPT-5.6 Sol / Low):AI use
Tool(s) and scope: OpenAI Codex implemented the session-setting intent integration, transient-feedback cleanup, regression and accessibility coverage, and verification. A
Generated-by: OpenAI Codextrailer is present on each materially affected commit.Checklist
Does this PR entail a user-facing behavior change?