From b75b0f2c5c2d48f320a3b5c403449de4e52e4fa6 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:04:12 +0200 Subject: [PATCH] Require explicit model choice after OAuth connect Stop auto-populating Main and Utility models when an OAuth account is already connected or newly connected. Add a connected-account gate state that routes users to model configuration instead of choosing provider defaults silently. Update OAuth notifications, chat gate copy, DOX contracts, and focused static regressions for the explicit model-selection flow. --- plugins/_oauth/AGENTS.md | 3 +- plugins/_oauth/webui/oauth-config-store.js | 58 ++------------------- tests/test_oauth_static.py | 10 ++-- tests/test_welcome_composer_static.py | 12 +++-- webui/components/chat/AGENTS.md | 4 +- webui/components/chat/model-gate-store.js | 27 ++++++---- webui/components/chat/model-setup-gate.html | 20 ++++++- 7 files changed, 54 insertions(+), 80 deletions(-) diff --git a/plugins/_oauth/AGENTS.md b/plugins/_oauth/AGENTS.md index e56973c19..efd6f98fa 100644 --- a/plugins/_oauth/AGENTS.md +++ b/plugins/_oauth/AGENTS.md @@ -28,8 +28,7 @@ - OAuth settings pending-auth controls such as device codes, manual callback input, and provider setup fields must render inline under the relevant provider row, not as a detached section below all providers. - OAuth device-code polling must honor provider `interval`, `expires_at`, and `slow_down` updates; do not poll immediately or keep a stale fixed interval after a provider asks the client to slow down. - OAuth settings model slots must keep provider choice editable per slot, list only connected OAuth account providers, and persist the selected provider IDs into `chat_model.provider` and `utility_model.provider`. -- The OAuth WebUI store owns connected-provider default selection helpers reused by settings and deferred chat setup gates. -- OAuth settings must dispatch `model-configured` when a provider connection completes. If no chat model is configured, the settings flow may apply the connected provider defaults to Main and Utility before notifying deferred send gates. +- OAuth settings must dispatch `model-setup-changed` when a provider connection completes, but model selection remains explicit and must not be filled automatically. - `helpers/providers/registry.py` is the source of truth for connectable OAuth providers. - OAuth provider config must not expose the dummy `oauth` API key in `conf/model_providers.yaml`; the dummy key is a runtime-only shim supplied by the `get_api_key` extension after the account provider reports connected. - Usage-plan metadata belongs only to connectable providers. Do not add metadata-only subscription families for providers this plugin cannot connect. diff --git a/plugins/_oauth/webui/oauth-config-store.js b/plugins/_oauth/webui/oauth-config-store.js index 2f0fe1447..c77701631 100644 --- a/plugins/_oauth/webui/oauth-config-store.js +++ b/plugins/_oauth/webui/oauth-config-store.js @@ -287,15 +287,6 @@ export const store = createStore("oauthConfig", { return status.display_name || providerId; }, - providerDefaultModel(providerId, slotKey = "chat_model") { - const status = this.providerStatus(providerId); - const defaults = Array.isArray(status.default_models) - ? status.default_models.map((model) => String(model || "").trim()).filter(Boolean) - : []; - if (slotKey === "utility_model" && defaults[1]) return defaults[1]; - return String(status.default_model || defaults[0] || "").trim(); - }, - providerUseLabel(providerId) { const status = this.providerStatus(providerId); return status.use_label || `Use ${status.short_name || status.display_name || providerId}`; @@ -679,59 +670,16 @@ export const store = createStore("oauthConfig", { } }, - async currentChatModelConfigured() { - try { - const response = await callJsonApi(`${MODEL_CONFIG_API}/model_config_get`, {}); - return Boolean(response?.model_configured); - } catch (error) { - console.error("Could not check model configuration:", error); - return true; - } - }, - - async autoApplyConnectedProviderIfNeeded(providerId) { - if (!this.providerConnected(providerId)) return false; - if (await this.currentChatModelConfigured()) return false; - - await this.loadModelConfig(); - if (!this.modelConfig) return false; - - const chatModel = this.providerDefaultModel(providerId, "chat_model"); - if (!chatModel) return false; - const utilityModel = this.providerDefaultModel(providerId, "utility_model") || chatModel; - const chat = this.modelSlot("chat_model"); - const utility = this.modelSlot("utility_model"); - chat.provider = providerId; - chat.name = chatModel; - chat.api_base = ""; - if (!chat.kwargs || typeof chat.kwargs !== "object") chat.kwargs = {}; - utility.provider = providerId; - utility.name = utilityModel; - utility.api_base = ""; - if (!utility.kwargs || typeof utility.kwargs !== "object") utility.kwargs = {}; - this.activeModelProvider = providerId; - this.models = this.activeProviderModels(); - this.modelConfigDirty = true; - this.modelSlotDirty = { chat_model: true, utility_model: true }; - await this.saveModelConfigIfDirty(); - return true; - }, - - notifyModelConfigured(providerId) { + notifyModelSetupChanged(providerId) { if (typeof document === "undefined") return; - document.dispatchEvent(new CustomEvent("model-configured", { + document.dispatchEvent(new CustomEvent("model-setup-changed", { detail: { source: "_oauth", providerId }, })); }, async handleProviderConnected(providerId, { statusLoaded = false } = {}) { if (!statusLoaded) await this.loadStatus(); - try { - await this.autoApplyConnectedProviderIfNeeded(providerId); - } catch (error) { - void toastFrontendError(messageOf(error), "OAuth Connections"); - } - this.notifyModelConfigured(providerId); + this.notifyModelSetupChanged(providerId); }, async loadStatus() { diff --git a/tests/test_oauth_static.py b/tests/test_oauth_static.py index 2a47c5cf7..b45803a64 100644 --- a/tests/test_oauth_static.py +++ b/tests/test_oauth_static.py @@ -116,12 +116,10 @@ def test_oauth_model_slots_reuse_model_config_api(): assert "if (!this.providerConnected(providerId)) return;" in store_js assert "const providerId = slot.provider;" in store_js assert "const providerId = this.isOauthProvider(this.activeModelProvider)" not in store_js - assert "autoApplyConnectedProviderIfNeeded(providerId)" in store_js - assert "currentChatModelConfigured()" in store_js - assert 'providerDefaultModel(providerId, "chat_model")' in store_js - assert 'providerDefaultModel(providerId, "utility_model")' in store_js - assert "this.modelSlotDirty = { chat_model: true, utility_model: true };" in store_js - assert 'new CustomEvent("model-configured"' in store_js + assert "autoApplyConnectedProviderIfNeeded" not in store_js + assert "currentChatModelConfigured" not in store_js + assert "providerDefaultModel" not in store_js + assert 'new CustomEvent("model-setup-changed"' in store_js assert 'detail: { source: "_oauth", providerId }' in store_js assert "await this.handleProviderConnected(providerId)" in store_js diff --git a/tests/test_welcome_composer_static.py b/tests/test_welcome_composer_static.py index 440884e7d..4f17b6d8b 100644 --- a/tests/test_welcome_composer_static.py +++ b/tests/test_welcome_composer_static.py @@ -111,13 +111,15 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None: assert "sessionStorage.getItem(STORAGE_KEY)" in gate_store assert "sessionStorage.removeItem(STORAGE_KEY)" in gate_store assert 'import("/plugins/_oauth/webui/oauth-config-store.js")' in gate_store - assert "tryConnectedAccountDefaults()" in gate_store - assert "oauthConfigStore.connectedProviderCards()[0]?.provider_id" in gate_store - assert "oauthConfigStore.autoApplyConnectedProviderIfNeeded(providerId)" in gate_store - assert "const refreshed = await callJsonApi(\"/plugins/_model_config/model_config_get\", {});" in gate_store + assert "refreshConnectedAccountState()" in gate_store + assert "accountConnected" in gate_store + assert "accountLabel" in gate_store + assert "oauthConfigStore.connectedProviderCards()[0] || null" in gate_store + assert "autoApplyConnectedProviderIfNeeded" not in gate_store assert "void this.dispatchPendingIfConfigured();" in gate_store assert "this.choice = \"\";" in gate_store assert 'document.addEventListener("model-configured"' in gate_store + assert 'document.addEventListener("model-setup-changed"' in gate_store assert "const currentContext = globalThis.getContext?.();" in gate_store assert "bypassModelGate: true" in gate_store assert 'openPluginConfig("_model_config", "Advanced model configuration")' in gate_store @@ -126,6 +128,8 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None: assert "openOnboarding('cloud')" in gate_component assert "openOnboarding('local')" in gate_component assert "openOauthConfiguration()" in gate_component + assert "Choose models" in gate_component + assert "Connected account:" in gate_component assert "Advanced model configuration" in gate_component assert "Advanced settings" not in gate_component assert "model-gate-fields" not in gate_component diff --git a/webui/components/chat/AGENTS.md b/webui/components/chat/AGENTS.md index 2dfcea70d..51b25eecd 100644 --- a/webui/components/chat/AGENTS.md +++ b/webui/components/chat/AGENTS.md @@ -23,8 +23,8 @@ - Missing model setup is gated at send intent: the first unconfigured send renders an in-thread setup card, keeps the pending prompt in browser session storage for refresh recovery, and must not call `/message_async` until a chat model is configured. - While the setup gate is open, the composer remains typeable but send is blocked until setup succeeds. - The setup gate must delegate Cloud/Local setup, account connections, and advanced model configuration to the existing onboarding and plugin settings modals; do not duplicate provider/model/key forms inline. -- Before blocking, the setup gate should reuse existing connected OAuth account defaults when they can make the chat model usable. -- Model setup surfaces that make the chat model usable must notify the gate with `model-configured` or an existing modal/onboarding completion signal so the pending prompt can retry automatically. +- A connected OAuth account without Main/Utility model selection is its own gate state; route to model configuration and do not select models automatically. +- Model setup surfaces that change readiness must notify the gate with `model-setup-changed`, `model-configured`, or an existing modal/onboarding completion signal so the pending prompt can retry automatically. ## Work Guidance diff --git a/webui/components/chat/model-gate-store.js b/webui/components/chat/model-gate-store.js index 5f6740236..4837d6649 100644 --- a/webui/components/chat/model-gate-store.js +++ b/webui/components/chat/model-gate-store.js @@ -13,6 +13,8 @@ export const store = createStore("modelGate", { active: false, connected: false, connectedLabel: "", + accountConnected: false, + accountLabel: "", pending: null, gateMessageId: "", choice: "", @@ -30,6 +32,9 @@ export const store = createStore("modelGate", { if (this.connected) { return `Model connected: ${this.connectedLabel || "ready"}`; } + if (this.accountConnected) { + return `${this.accountLabel || "Your AI account"} is connected. Choose Main and Utility models and I'll answer right away.`; + } return "I'm ready to work on this — I just need a model to think with. Pick one and I'll answer right away."; }, @@ -43,6 +48,9 @@ export const store = createStore("modelGate", { document.addEventListener("model-configured", () => { void this.dispatchPendingIfConfigured(); }); + document.addEventListener("model-setup-changed", () => { + void this.dispatchPendingIfConfigured(); + }); document.addEventListener("modal-closed", () => { if (this.active && !this.connected) { this.choice = ""; @@ -56,25 +64,26 @@ export const store = createStore("modelGate", { try { const data = await callJsonApi("/plugins/_model_config/model_config_get", {}); this.applyModelStatus(data); - if (data?.model_configured) return true; - if (!(await this.tryConnectedAccountDefaults())) return false; - const refreshed = await callJsonApi("/plugins/_model_config/model_config_get", {}); - this.applyModelStatus(refreshed); - return !!refreshed?.model_configured; + if (!data?.model_configured) await this.refreshConnectedAccountState(); + return !!data?.model_configured; } catch (error) { console.error("Could not check model configuration:", error); return true; } }, - async tryConnectedAccountDefaults() { + async refreshConnectedAccountState() { try { const { store: oauthConfigStore } = await import("/plugins/_oauth/webui/oauth-config-store.js"); await oauthConfigStore.loadStatus(); - const providerId = oauthConfigStore.connectedProviderCards()[0]?.provider_id || ""; - return providerId ? oauthConfigStore.autoApplyConnectedProviderIfNeeded(providerId) : false; + const account = oauthConfigStore.connectedProviderCards()[0] || null; + this.accountConnected = Boolean(account); + this.accountLabel = account?.short_name || account?.display_name || account?.provider_id || ""; + return this.accountConnected; } catch (error) { - console.error("Could not apply connected account defaults:", error); + console.error("Could not check connected accounts:", error); + this.accountConnected = false; + this.accountLabel = ""; return false; } }, diff --git a/webui/components/chat/model-setup-gate.html b/webui/components/chat/model-setup-gate.html index b066ab375..a2d24b2c8 100644 --- a/webui/components/chat/model-setup-gate.html +++ b/webui/components/chat/model-setup-gate.html @@ -20,7 +20,19 @@

-
+
+ +
+ +
@@ -90,6 +102,10 @@ gap: 0.6rem; } + .model-gate-fork.is-single { + grid-template-columns: 1fr; + } + .model-gate-option { border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent); border-radius: 8px;