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;