Skip to content

🤖 fix: keep gateway IDs and record intent for VS Code webview model picks - #4834

Merged
ThomasK33 merged 1 commit into
mainfrom
fix/webview-model-pick-semantics
Sep 27, 2026
Merged

ThomasK33 merged 1 commit into
mainfrom
fix/webview-model-pick-semantics

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

In the VS Code webview, a model pick now behaves like a desktop pick. An explicit gateway model such as openrouter:openai/gpt-5 stays selected and is sent as picked, instead of being collapsed to the direct openai: model. The pick is also recorded as a deliberate selection, so a sub-agent's metadata refresh no longer overwrites an unsent pick. Sends still never persist AI settings; that is the next PR for #4781.

Background

#4781 turns on persistence of explicit webview picks by reusing the desktop's send-time path. That path needs two things from the pick itself, both missing in the webview (Codex findings on #4778):

  • the gateway-preserving model ID (normalizeSelectedModel), not normalizeToCanonical;
  • the selection intent (markAiSelectionIntent) that marks the pick as deliberate.

This PR adds both, with no persistence change, so it can land on its own.

Implementation

vscode/src/webview/ChatComposer.tsx:

  • onModelChange follows the desktop ChatInput.setPreferredModel (workspace variant): normalizeSelectedModel, ensureModelInSettings, then markAiSelectionIntent(workspaceId, "model", …), the model key and the per-agent cache (🤖 fix: keep a VS Code webview model pick across agent switches #4799).
  • The displayed model (storedModel) is gateway-preserving too, as in the desktop composer, so a gateway pick stays selected in the dropdown and in model cycling.
  • Thinking picks already record intent through the shared ThinkingProvider.

The intent is desktop code (src/browser/utils/aiSelectionIntent.ts). The webview's only new behavior is the desktop's own: seedWorkspaceLocalStorageFromBackend keeps a sub-agent's unsent pick (keepsUnsentPick).

Validation

  • Test-first in vscode/src/webview/App.test.tsx (TestBridge). Both failed on main:
    • "sends a gateway-routed model pick with its gateway ID": with an OpenRouter custom model configured, picking it sends openrouter:openai/gpt-5. On main it sent openai:gpt-5.
    • "keeps a sub-agent's unsent model pick across a metadata refresh": pick Sonnet 5 on a child, refresh the workspace list, send. On main the send carried the seeded openai:gpt-5.6-terra.
  • The AI-settings tests now reset the module-level selection intents in beforeEach, so picks from one test cannot leak into the next.
  • bun test ./vscode/src/: 53 pass.
  • Served-bundle dogfood: the built xumChatView bundle in a page that plays the extension host. The yellow panel shows each posted workspace.sendMessage options. main build vs this branch, same steps:

Gateway pick, main (collapsed to openai:gpt-5), 800px:

Gateway pick on main, 800px

Gateway pick, this branch (openrouter:openai/gpt-5), 800px and 390px:

Gateway pick on this branch, 800px

Gateway pick on this branch, 390px

Sub-agent pick, then a workspace-list refresh, then send. On main the pick is lost (Terra is sent); on this branch Sonnet 5 stays selected and is sent:

Sub-agent refresh on main, 800px

Sub-agent refresh on this branch, 800px

Sub-agent refresh on this branch, 390px

Every send still carries skipAiSettingsPersistence: true, and no updateAgentAISettings call is made.

Risks

Low, webview only. What the webview sends changes only for explicit gateway models, which now match what the desktop sends for the same pick.

Refs #4781


📋 Implementation Plan (both PRs for #4781)

#4781 plan: persist explicit AI-setting picks from the VS Code webview

Result

  • Yes, this fits in 2 small stacked PRs (gh stack), about +37 net product LoC in total:

    • PR 1 (≈ +2 net product LoC): webview model picks use the desktop's pick semantics (gateway-preserving model ID, recorded selection intent). Sends still never persist.
    • PR 2 (≈ +35 net product LoC): webview sends persist explicit picks through the desktop's send-time path, with a gate and an ordering guarantee. Adds one backend test.
  • Approach: reuse the desktop's actual persistence path, which is send-time persistence. The desktop never writes on pick, so the webview will not either. No new updateAgentAISettings calls, no webview write queue, no backend product changes.

  • The rule (PR 2): a webview send persists AI settings only when all of these hold:

    1. The workspace's settings are loaded (selectedWorkspace.ai is present).
    2. The stored model is allowed by admin policy, so the 🤖 fix: fall back to an allowed model when admin policy excludes the VS Code webview's selection #4811 fallback model is never persisted.
    3. No earlier persisting send for this workspace is unresolved in this webview.
    4. The send carries an explicit, still-current pick for the send's agent (getAiSelectionIntentForSendOptions returns an intent).

    In every other case the send keeps skipAiSettingsPersistence: true, exactly as today.

Why send-time persistence, not pick-time updateAgentAISettings

The coordinator's direction names updateAgentAISettings. The code shows that the desktop's persistence path is the send, and that the send reaches the same backend write.

Fact (origin/main 40649ca) Where
The desktop renderer calls workspace.updateAgentAISettings only when it creates a workspace. Model and thinking picks write only localStorage and in-memory selection intent (#3968: picks stay local until the next send). src/browser/features/ChatInput/useCreationWorkspace.ts:662; ChatInput/index.tsx:722-782; ThinkingContext.tsx:195-218
Ordinary desktop sends persist through maybePersistAISettingsFromOptions → persistWorkspaceAISettingsForAgent. That is the same write updateAgentAISettings uses: per-agent bucket plus selected agent, normalizeSelectedModel (gateway-preserving), no thinking clamp. It is best-effort and runs before queueing, refusals and sendMessage's return. src/node/services/workspaceService.ts:11165-11200, :11119, :11286-11335, :13015-13017
Sub-agent picks pin (taskAiPins) only through the send's aiSelectionIntent, which is committed at acceptance or enqueue. updateAgentAISettings never touches pins. workspaceService.ts:12623-12628, :12896-12917, :11237-11248, :11406
updateAgentAISettings requires model and thinkingLevel together, so it cannot write one field on its own either. src/common/orpc/schemas/api.ts:1558-1566; workspaceAiSettings.ts:12-22
The desktop's intent helpers already exist and are shared: markAiSelectionIntent, getAiSelectionIntentForSendOptions and consumeAiSelectionIntent. Tokens are bound to a value and scoped per workspace and agent. They are consumed on success only, and a newer pick survives. src/browser/utils/aiSelectionIntent.ts:20-150

A pick-time write from the webview would need its own write serializer, gateway handling and companion-field handling, and it would still skip pins. Those are the round-1 findings on #4778. Consequence, stated plainly: a pick that is never sent is never persisted. The desktop behaves the same way.

Rejected alternative: pick-time writes (≈ +60–80 net product LoC)

The alternative is to call updateAgentAISettings on each explicit pick, through a per-workspace latest-wins queue in the webview. It is a webview-only variant of a write the desktop does not make. It cannot update taskAiPins, so the Codex P2 on sub-agent selection intent comes back. It must send a companion field anyway, so it has the same semantics as the send path, with more code. It also adds a second writer that races the send's own persistence.

Design

Webview send today (for reference)

vscode/src/webview/ChatComposer.tsx:306-317 builds {...getSendOptionsFromStorage(ws), agentId, skipAiSettingsPersistence: true, ...(policyFallbackModel ? {model: policyFallbackModel} : {})}.

  • getSendOptionsFromStorage already sends the gateway-preserving model and the raw stored thinking level (src/browser/utils/messages/sendOptions.ts:51-63).
  • The shared ThinkingProvider already records thinking intent (ThinkingContext.tsx:202).
  • The model pick does not record intent, and it collapses gateway IDs with normalizeToCanonical (ChatComposer.tsx:229-251).

PR 1: pick semantics (sends still skip persistence)

ChatComposer.onModelChange becomes the desktop's setPreferredModel for the workspace variant:

  • normalizeSelectedModel(model) replaces normalizeToCanonical(model). The gateway route is kept in the model key, the per-agent cache, the ensureModelInSettings input (as on desktop, index.tsx:730,744) and therefore in the send.
  • Add markAiSelectionIntent(props.workspaceId, "model", selectedModel), as the desktop does at index.tsx:749.
  • Everything else stays: the per-agent cache write from 🤖 fix: keep a VS Code webview model pick across agent switches #4799, and setPreferredModel for the local key.
    • setWorkspaceModelWithOrigin is not needed. Its origin is read only by the desktop's context-switch warning and Auto routing, and the webview has neither: its auto-routing flags are always false (sendOptions.ts:91-97, and no webview control sets them).
  • Update the 🤖 fix: VS Code webview first send overwrites workspace AI settings with webview defaults #4755 comment at ChatComposer.tsx:249-251.

User-visible effects, both desktop parity:

  • An explicit gateway model such as openrouter:openai/gpt-5 is sent as picked.
  • A sub-agent's unsent pick survives a metadata refresh, through keepsUnsentPick in WorkspaceContext.tsx:231-240.

PR 2: the persistence gate and ordering

In ChatComposer.tsx:

// Module scope: the composer remounts per workspace (App.tsx key={selectedWorkspaceId}).
// "in-flight": a persisting send has not returned. "unknown": one ended without a server
// result (30 s abort or transport error), so its write may still land later. Fail closed
// until the webview reloads rather than let it overwrite a later pick (#4781).
const aiPersistenceByWorkspace = new Map<string, "in-flight" | "unknown">();

// onSend: add `isSending` to the first guard (desktop handleSend exits on !canSend,
// index.tsx:2017; today Enter bypasses the disabled button).
const baseOptions = { ...getSendOptionsFromStorage(props.workspaceId), agentId };
const mayPersist =
  props.aiSettingsLoaded && storedModelAllowed && !aiPersistenceByWorkspace.has(props.workspaceId);
const selection = getAiSelectionIntentForSendOptions(props.workspaceId, agentId, {
  ...baseOptions,
  skipAiSettingsPersistence: !mayPersist, // the desktop wrapper returns no intent when skipping
});
const persist = selection.intent !== undefined;
assert(!persist || policyFallbackModel === null, "a policy fallback model must never be persisted");
const options = {
  ...baseOptions,
  skipAiSettingsPersistence: !persist,
  ...(persist ? { aiSelectionIntent: selection.intent } : {}),
  ...(policyFallbackModel ? { model: policyFallbackModel } : {}),
};
if (persist) aiPersistenceByWorkspace.set(props.workspaceId, "in-flight");
// Any result (success or {success:false}): delete the entry, because the server replied
// after its awaited persist. On success: consumeAiSelectionIntent(ws, agentId,
// selection.attachedTokens). catch: set "unknown". No `finally` delete: it would erase "unknown".

In App.tsx: pass aiSettingsLoaded={selectedWorkspace?.ai != null} to ChatComposer (App.tsx:723-741). The composer does not receive ai today.

Checked: with no enforced policy, or before policy.get answers, storedModelAllowed is true (src/common/utils/policy/modelPolicy.ts:38-41, useModelsFromSettings.ts:94-95,170-178). The gate therefore does not block the common case. storedModelAllowed also covers #4811's "no allowed listed model" case, where the stored model is policy-excluded and is sent unchanged: that model is never persisted either.

What a persisting send writes (decision D2, default: desktop parity)

  • The backend writes the send's agent bucket {model, thinkingLevel, reasoningMode} and the workspace's selected agent, exactly as a desktop send does. The schema requires model and thinking together.
  • Seeded values are never persisted on their own: without an explicit pick, the send skips persistence.
  • The field the user did not pick is written with the value the turn actually runs with.
  • Residuals, all identical to desktop behavior:
    • (a) On a main workspace the webview snapshots settings once per load (WorkspaceContext.tsx:193-197). A model-only pick therefore writes the snapshot thinking level, which can be older than a change another client made since.
    • (b) A workspace with no buckets at all (seeding returns early, WorkspaceContext.tsx:218-229) gets a bucket whose companion field is the webview's resolved default.
  • Optional stricter knob (≈ +5 LoC, not recommended by default): skip persistence when the un-picked companion differs from the latest backend bucket in selectedWorkspace.ai, which App keeps current. It adds a branch for a multi-client edge case that the desktop accepts.

Ordering guarantee

Within one webview, a later explicit pick is never overwritten by an earlier persisting send:

  1. Picks write nothing to the backend, so rapid picks cannot race each other. The last pick is what the next send carries. markAiSelectionIntent replaces the field's pending token.
  2. At most one persisting send per workspace is unresolved at any time (the module map). It survives the composer's per-workspace remount.
  3. The backend persists before sendMessage returns. When the server replies, that send's write has landed or been skipped, and only then can the next persisting send start.
  4. A send that overlaps an unresolved persisting send goes with skip: true and keeps its tokens. The next send after the earlier one resolves writes the latest pick. With the isSending guard, only a remount (switching workspaces and back while the send is pending) can produce this overlap.
  5. A persisting send that ends without a server result marks the workspace unknown, and later sends do not persist until reload. The webview link rejects on abort, posts orpcCancel and drops late responses (createVscodeOrpcLink.ts:260-264,360-374), so the outcome cannot be known.
  6. {success:false} is a server reply: the entry is cleared, the tokens are kept, and the next send writes the pick again. This is idempotent.

Out of scope, same as desktop multi-window: ordering across different clients, such as the desktop plus a webview, or two VS Code windows.

Sub-agent lock

The lock covers the agent identity. Model and thinking picks on a child are allowed and pinned, as on desktop (#4459).

  • The send already uses the locked agent from useAgent(), resolved by resolvePersistedAgentId through workspaceMetaFallback (App.tsx:63-74).
  • Seeding writes the same resolved agent into the agent key that markAiSelectionIntent reads (WorkspaceContext.tsx:207-216), so the intent scopes match.
  • Any mismatch, for example a pick made before hydration, finds no intent and does not persist (fail closed). Nothing is ever written into another agent's bucket.
  • The backend persists into the sent agent's bucket and pins through aiSelectionIntent. Child execution stays locked to agentType either way (agentResolution.ts:223-228).

How the #4778 round-1 findings are addressed

Finding Resolution
P1 stale agent settings on toggle Already fixed: WorkspaceModeAISync is mounted (#4778) and the per-agent cache is updated on pick (#4799).
P2 no selection intent for sub-agents PR 1 marks model intent (thinking already marked). PR 2 attaches aiSelectionIntent and consumes it on success, as ChatInput/index.tsx:2469,2561 does.
P2 gateway route collapsed PR 1: normalizeSelectedModel. The backend also normalizes with it on persist.
P2 client-clamped thinking persisted No client write at all. The send carries the raw stored level and the backend does not clamp on persist.
P1 unserialized writes No pick-time writes. Send-level ordering as described above.
P2 pre-hydration picks reverted Unchanged (#4778 decision). They can never persist wrongly: no persistence before ai is loaded, and a reverted pick no longer matches its value-bound token.

PR split, tests and quality gates

Gate 0 (before PR 1): rebase on the latest origin/main, and confirm that the cited lines in ChatComposer.tsx, App.tsx and aiSelectionIntent.ts still match. If #4765–#4813 moved, re-anchor the plan before coding.

PR 1: "use the desktop's model-pick semantics in the VS Code webview"

  • Files: vscode/src/webview/ChatComposer.tsx, vscode/src/webview/App.test.tsx.
  • Test hygiene: add resetAiSelectionIntentForTests() to the App.test.tsx beforeEach. It is an existing seam. Intents live at module level and currently leak between tests; 🤖 fix: keep a VS Code webview model pick across agent switches #4799's test leaves a Plan pick pending.
  • Red tests (TestBridge; each fails on main):
Test Setup → assertion Fails on main because
"sends a gateway-routed model pick with its gateway ID" Answer providers.getConfig with openrouter: {apiKeySet, isEnabled, isConfigured, models: ["openai/gpt-5"]} (useModelsFromSettings.ts:42-60 lists it as openrouter:openai/gpt-5), pick that entry, send → options.model === "openrouter:openai/gpt-5" normalizeToCanonical sends the direct openai: ID
"keeps a sub-agent's unsent model pick across a metadata refresh" Child fixture (parentWorkspaceId), pick Sonnet 5, emit workspaces again with an unchanged ai, send → model contains sonnet The reseed overwrites the pick (no pending intent)

PR 2: "persist explicit AI-setting picks from the VS Code webview"

  • Files: vscode/src/webview/ChatComposer.tsx, vscode/src/webview/App.tsx, vscode/src/webview/App.test.tsx, src/node/services/workspaceService.aiSettings.test.ts.
  • Test hygiene:
    • Every test that makes a persisting send answers it (bridge.answer("workspace.sendMessage", { success: true, data: {} })) before it ends. Otherwise in-flight leaks into later tests on WORKSPACE.id.
    • The unknown-outcome test uses its own workspace ID. No new test seam.
  • Tests. "Red" means the test fails on PR 1's head. "Guard" means it passes on PR 1 but fails if its branch of the gate is removed (prove each guard once by locally deleting that condition).
# Test Setup → assertion Type
1 "does not persist a send without an explicit pick" Loaded main workspace (plan/terra/high), send → skip: true, no aiSelectionIntent Guard
2 "persists an explicit model pick once, at the next send" config.getConfig floor high for the picked model, stored thinking low, policy.get unanswered or null; pick → send → skip: false, aiSelectionIntent: {model: true}, the picked model, thinkingLevel: "low" (unclamped companion); answer success; send again → skip: true Red
3 "persists only the last of several rapid picks" Pick Sonnet, then Opus, send → one send, skip: false, Opus, intent {model: true} Red
4 "persists an explicit thinking pick as selected" Seed thinking medium, open [data-thinking-selector-trigger], click the role=option with aria-label="High", send → skip: false, intent {thinkingLevel: true}, thinkingLevel: "high" Red
5 "never persists for a workspace without loaded AI settings" Extend the #4765 test: WORKSPACE without ai, explicit pick, send → skip: true Guard
6 "never persists the admin-policy fallback model" renderWithPolicy (allow only terra) plus providers.getConfig, explicit thinking pick, send → fallback model, skip: true, no intent Guard
7 "persists a locked sub-agent's pick only into its locked agent" Child agentId: "plan", agentType: "exec"; pick → send → agentId: "exec", skip: false, intent present; the toggle stays disabled Red
8 "keeps one persisting send per workspace in flight; the next send writes the latest pick" Two workspaces. Pick A → send 1 (skip: false, unanswered). Enter with new text → still 1 sendMessage. Switch to ws-2 and back (remount), pick B, send 2 → skip: true. Answer both. Send 3 → skip: false, model B Red
9 "stops persisting for a workspace after a send ends without a server result, but retries after a server error" (a) Own workspace ID: pick → send → orpcResponse ok:false (throw) → pick → send → skip: true. (b) Pick → send → answer {success: false, error} → send again → skip: false with the same intent (b) Red; (a) Guard
10 Backend: "sendMessage with skipAiSettingsPersistence leaves aiSettingsByAgent and agentId unchanged" workspaceService.aiSettings.test.ts: fills the gap next to the pins-only case (:267-291) Guard (currently untested)
  • Acceptance criteria:
    • AC1: sends without an explicit pick never persist (1, 5).
    • AC2: an explicit model or thinking pick persists once, with the gateway-preserving ID and the raw thinking level (2, 3, 4).
    • AC3: the 🤖 fix: fall back to an allowed model when admin policy excludes the VS Code webview's selection #4811 fallback, or a policy-excluded stored model, never persists (6).
    • AC4: sub-agents persist only into the locked agent, with pins intent attached (7).
    • AC5: the ordering rules hold (3, 8, 9), and Enter no longer sends while a send is in flight.
    • AC6: the webview makes no updateAgentAISettings calls on any path.
    • AC7: skip: true leaves backend buckets untouched (10).
  • Gate 2: bun test ./vscode/src/, bun test src/node/services/workspaceService.aiSettings.test.ts (Bun 1.3.5 from ~/.bun/bin) and make static-check are green, and PR 2 dogfood D-2 is captured. Then open PR 2 stacked on PR 1.
  • Net product LoC: ≈ +35 (module map + gate + consume/catch + isSending guard in ChatComposer.tsx ≈ +33; App.tsx prop ≈ +2).
  • Pre-agreed scope reduction if PR 2 stops converging (drop a sub-change after its 2nd finding): persist on main workspaces only, and keep skip: true for sub-agents. That removes the pins and lock surface; test 7 then asserts skip: true. Report to the coordinator before shrinking further.

Dogfooding (served bundle, agent-browser)

The #4740 harness was ad hoc (nothing committed), and vscode/out/ is gitignored. All artifacts go under /tmp/ws25-*.

  1. Build: bun run --cwd vscode compile, which emits vscode/out/xumChatView.{js,css} and katex/. If esbuild is missing, run make -C vscode node_modules/.installed first. Copy vscode/out/* to /tmp/ws25-harness/.
  2. Write /tmp/ws25-harness/index.html (sketch below). It stubs acquireVsCodeApi, plays the host, and shows each posted workspace.sendMessage options subset in a visible <pre>, so screenshots and video carry the evidence.
  3. Serve: python3 -m http.server 8765 -d /tmp/ws25-harness as a monitored background task. Save its PID and kill only that PID afterwards.
  4. Drive with agent-browser: open about:blank, then record start /tmp/ws25-evidence/<pr>.webm, then open http://localhost:8765/?scenario=…. Use snapshot -i → click @ref for the model dropdown, thinking selector and Send. Check the payload with agent-browser eval 'JSON.stringify(window.__sends())'. Take screenshot at 800 px and 390 px widths. Run tab list after record start and close stale tabs. Check the decoded frames.
  5. Attach the screenshots with attach_file for review. Post them on the PR with /usr/local/bin/gh pr comment <n> --body-file … --attach …, because the mise gh is too old for --attach.

Scenarios:

  • D-1 (PR 1):
    • Pick the openrouter:openai/gpt-5 entry and send. The payload shows the gateway ID and skip: true. Also run the unmodified main build first as a before/after pair.
    • Child workspace: pick, then window.__emitWorkspaces(). The selector still shows the pick.
    • Screenshots at 800 and 390 px.
  • D-2 (PR 2):
    • (a) Seeded-only send → skip: true.
    • (b) Explicit gateway model pick → skip: false plus aiSelectionIntent.model. A second send → skip: true.
    • (c) Thinking pick → the raw level is persisted.
    • (d) Policy fallback status line plus a thinking pick → skip: true.
    • (e) Locked child: disabled toggle, agentId: exec, intent present.
    • (f) Ordering: harness sendMode=hold, pick A, send, __select('ws-2'), __select('ws-1'), pick B, send → skip: true. __release() all, then send → skip: false with B.
    • (g) sendMode=throw: the next send → skip: true.
    • Take screenshots of (b), (d) and (e) at 800 and 390 px, and one WebM covering (b) and (f).
  • Real-backend replay (PR 2):
    1. Start KEEP_SANDBOX=1 make dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects" (dev-server-sandbox skill). Note BACKEND_PORT and the sandbox root.
    2. Add a temp git repo as a project and create a workspace, through the sandbox UI with agent-browser or through oRPC.
    3. Using a short bun script that uses the same RPCLink fetch client as vscode/src/api/client.ts, replay the exact sendMessage inputs captured in D-2(b) and D-2(a). The second one uses a different model.
    4. Diff config.json for the workspace. The persisting payload writes aiSettingsByAgent.<agent> = {model: "openrouter:openai/gpt-5", thinkingLevel: <raw>} and agentId. The skip payload changes nothing. The stream may fail without provider keys; persistence runs before it.
Harness page sketch
  • window.acquireVsCodeApi = () => ({ postMessage, getState, setState }). postMessage pushes onto window.__posted, re-renders <pre id="sends">, and replies through window.postMessage(...).
  • On the webview's startup message (check WebviewToExtensionMessage in vscode/src/webview/protocol.ts for the exact type), post connectionStatus (mode: "api"), then workspaces, then setSelectedWorkspace, then chatEvent {type: "caught-up"}.
    • The workspace set depends on the scenario:
      • main ws-1, with ai.aiSettingsByAgent plan/exec buckets;
      • ws-2;
      • a child, with parentWorkspaceId, agentId: "plan" and agentType: "exec".
  • orpcCall answers ({type: "orpcResponse", requestId, ok: true, kind: "value", value}):
    • policy.get: null, or an enforced policy for (d).
    • providers.getConfig: anthropic and openai configured, plus openrouter with models: ["openai/gpt-5"].
    • config.getConfig: {minThinkingLevelByModel: {...}}.
    • agents.list: [].
    • workspace.sendMessage, depending on sendMode:
      • ok: {success: true, data: {}}.
      • hold: queue the request for window.__release().
      • throw: {ok: false, error: "harness transport error"}.
  • Helpers: window.__sends() (the options subset: agentId, model, thinkingLevel, skipAiSettingsPersistence, aiSelectionIntent), window.__select(id) and window.__emitWorkspaces().

Risks and residuals

Risk Handling
Review churn: persistence drew 6 findings in #4778 round 1 Each finding is mapped above. Reuse the desktop helpers only. Budget: 6 assessments per PR, including the final independent check. Use the pre-agreed PR 2 scope reduction.
Stale companion field on a main workspace (D2 residual a) Desktop parity (#3968 snapshot rule). The optional knob is available if the coordinator wants it.
unknown stays set after a transient error Persistence stays off for that workspace until the webview reloads. Turns still use the picks; this is the pre-#4781 behavior. Documented in a code comment as a deliberate trade-off.
Backend persistence is best-effort Tokens are consumed on a successful send even if the config write failed server-side. Desktop parity.
Cross-client ordering (desktop plus webview, two VS Code windows) Last writer wins at the backend. Out of scope, same as desktop multi-window.
Enter no longer sends while a send is in flight Desktop parity. The typed text stays in the box.
Pre-hydration picks They can be dropped (fail closed), never persisted wrongly. The #4778 decision stands.

Decisions adopted (the coordinator can override)

  • D1: send-time persistence through the desktop's path, instead of pick-time updateAgentAISettings writes (evidence above).
  • D2: a bucket is written as a unit (desktop parity). The strict companion knob is off.
  • D3: agent-only toggles never persist. When a persisting send writes a pick, the agent it was sent with is saved too, as on desktop. Issue 🤖 feat: persist explicit AI-setting changes from the VS Code webview #4781 also mentions agent choice; persisting agent-only toggles would be a follow-up if wanted.
  • D4: after an unknown outcome, fail closed until the webview reloads.
  • Issue hygiene: both PR bodies reference 🤖 feat: persist explicit AI-setting changes from the VS Code webview #4781 with Refs and no closing keyword. After PR 2 merges, close the issue by hand with a residual comment. File follow-up issues only for residuals the coordinator wants tracked (D2 knob, D3).

Out of scope

  • The mid-turn thinking nudge (workspace.setActiveTurnThinkingLevel is not on the webview allowlist; the call is best-effort and swallowed).
  • Disabling controls before hydration.
  • Backend versioning or CAS for settings writes.
  • Changing the webview's 30 s send timeout.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $8.14

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T04:36:09.326459Z 926c5f5 PR opened
🔒 Security Review ✅ Completed 2026-09-27T04:37:51.738099Z 926c5f5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 61dc6ca Sep 27, 2026
31 checks passed
@ThomasK33
ThomasK33 deleted the fix/webview-model-pick-semantics branch September 27, 2026 04:53
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