From caf5c2f1293a972ebd544a46cbf1393bde811d3c Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:48:03 +0200 Subject: [PATCH] Refine onboarding model setup flow Remove composer blocking so sends always create or enter a chat and route missing-model messages through the in-thread gate. Make Utility model selection explicit by removing same-as-main sync and defaulting to each provider's utility model when available. Compact onboarding success channel cards and update static regressions for the new onboarding/gate behavior. --- .../discovery-cards.html | 60 ++----------------- plugins/_onboarding/webui/onboarding-store.js | 34 +++-------- plugins/_onboarding/webui/onboarding.html | 12 ++-- tests/test_onboarding_static.py | 4 ++ tests/test_welcome_composer_static.py | 10 +++- .../components/chat/input/chat-bar-input.html | 11 +--- webui/components/chat/input/input-store.js | 13 +--- webui/components/chat/model-gate-store.js | 7 --- webui/index.js | 2 - 9 files changed, 30 insertions(+), 123 deletions(-) diff --git a/plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html b/plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html index c7ca47b30..222364d4b 100644 --- a/plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html +++ b/plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html @@ -250,11 +250,11 @@ } .discovery-feature-card { - display: flex; - flex-direction: column; - align-items: flex-start; - min-height: 142px; - padding: 0.82rem 0.9rem; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + padding: 0.72rem 0.9rem; background: linear-gradient( 180deg, @@ -271,7 +271,6 @@ gap: 0.62rem; width: 100%; min-width: 0; - margin-bottom: 1rem; } .discovery-feature-thumb { @@ -308,16 +307,7 @@ line-height: 1.25; } - .discovery-feature-desc { - margin: 0.42rem 0 0; - padding-bottom: 0.6rem; - color: var(--color-text); - font-size: 0.77rem; - line-height: 1.34; - } - .discovery-feature-card > .btn { - margin-top: auto; min-height: 36px; padding: 0.44rem 0.78rem; } @@ -554,37 +544,6 @@ gap: 0.75rem; } - .discovery-feature-card { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.75rem; - min-height: 0; - align-items: center; - padding: 0.85rem 1rem; - } - - .discovery-feature-head { - grid-column: 1; - min-width: 0; - margin-bottom: 0; - } - - .discovery-feature-thumb { - width: 32px; - height: 32px; - } - - .discovery-feature-desc { - display: none; - } - - .discovery-feature-card > .btn { - grid-column: 2; - grid-row: 1; - align-self: center; - margin-top: 0; - } - .discovery-cta-link { margin-top: 0; } @@ -626,15 +585,6 @@ font-size: 1rem; } - .discovery-feature-card { - grid-template-columns: minmax(0, 1fr) auto; - padding-right: 3rem; - } - - .discovery-feature-card > .btn { - justify-self: end; - } - .discovery-cli-title { font-size: 0.98rem; } diff --git a/plugins/_onboarding/webui/onboarding-store.js b/plugins/_onboarding/webui/onboarding-store.js index f0b107962..868781275 100644 --- a/plugins/_onboarding/webui/onboarding-store.js +++ b/plugins/_onboarding/webui/onboarding-store.js @@ -87,7 +87,6 @@ export const store = createStore("onboarding", { providerDetails: {}, selectedProviderId: "", selectedProviderOrigin: "cloud", - sameAsMain: true, userTouchedModel: { chat_model: false, utility_model: false, @@ -128,7 +127,6 @@ export const store = createStore("onboarding", { this.providerDetails = {}; this.selectedProviderId = ""; this.selectedProviderOrigin = "cloud"; - this.sameAsMain = true; this.userTouchedModel = { chat_model: false, utility_model: false }; this.modelDropdown = { chat_model: { models: [], open: false, loading: false, error: "", source: "" }, @@ -805,50 +803,34 @@ export const store = createStore("onboarding", { this.config[slotKey].name = modelName; this.userTouchedModel[slotKey] = true; this.modelDropdown[slotKey].open = false; - if (slotKey === "chat_model" && this.sameAsMain) { - this.syncUtilityWithMain(); - } }, markModelTouched(slotKey) { this.userTouchedModel[slotKey] = true; - if (slotKey === "chat_model" && this.sameAsMain) { - this.syncUtilityWithMain(); - } }, prepareUtilityDefaults() { - ensureSlot(this.config, "utility_model"); - if (this.sameAsMain) { - this.syncUtilityWithMain(); - return; - } const mainProvider = this.config.chat_model.provider; - const meta = this.providerMeta(mainProvider); - this.applyProviderToSlot("utility_model", mainProvider, meta); - }, - - syncUtilityWithMain() { - ensureSlot(this.config, "utility_model"); - if (!this.sameAsMain || !this.config?.chat_model) return; - this.config.utility_model.provider = this.config.chat_model.provider; - this.config.utility_model.name = this.config.chat_model.name; - this.config.utility_model.api_base = this.config.chat_model.api_base || ""; - this.config.utility_model.kwargs = clone(this.config.chat_model.kwargs || {}); + this.applyProviderToSlot("utility_model", mainProvider, this.providerMeta(mainProvider)); + if (!this.config.utility_model.api_base) { + this.config.utility_model.api_base = this.config.chat_model.api_base || ""; + } }, async utilityProviderChanged() { const providerId = this.config.utility_model.provider; - this.sameAsMain = providerId === this.config.chat_model.provider; this.userTouchedModel.utility_model = false; + this.config.utility_model.api_base = ""; this.applyProviderToSlot("utility_model", providerId, this.providerMeta(providerId)); + if (!this.config.utility_model.api_base && providerId === this.config.chat_model.provider) { + this.config.utility_model.api_base = this.config.chat_model.api_base || ""; + } await this.loadModels("utility_model"); }, async completeSetup() { this.saving = true; try { - if (this.sameAsMain) this.syncUtilityWithMain(); await modelConfigStore.persistApiKeysForConfig(this.config); const response = await fetchApi(`${MODEL_CONFIG_API}/model_config_set`, { method: "POST", diff --git a/plugins/_onboarding/webui/onboarding.html b/plugins/_onboarding/webui/onboarding.html index 018010b0f..e95f09527 100644 --- a/plugins/_onboarding/webui/onboarding.html +++ b/plugins/_onboarding/webui/onboarding.html @@ -821,13 +821,10 @@
- +
The utility model handles quick internal tasks — summaries, naming, memory. A small, fast, cheap model works best here.
- @@ -836,17 +833,16 @@
- + -
+
diff --git a/tests/test_onboarding_static.py b/tests/test_onboarding_static.py index 52813bef9..075ef9e3d 100644 --- a/tests/test_onboarding_static.py +++ b/tests/test_onboarding_static.py @@ -59,6 +59,10 @@ def test_onboarding_contains_unified_provider_step(): assert "Main model" in html assert "Refresh model list" in html assert "Search or enter Utility Model" in html + + # The utility model is an intentional choice: no "same as main" shortcut. + assert "Use same as Main Model" not in html + assert "sameAsMain" not in html + store assert "Advanced Settings" in html assert "selectedProviderName() + ' Docs'" in html assert "openSelectedProviderDocs" in html + store diff --git a/tests/test_welcome_composer_static.py b/tests/test_welcome_composer_static.py index bde0b8628..0b54e6da3 100644 --- a/tests/test_welcome_composer_static.py +++ b/tests/test_welcome_composer_static.py @@ -120,7 +120,6 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None: 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 assert 'openPluginConfig("_oauth"' not in gate_store @@ -143,13 +142,18 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None: display: grid; gap: 0.75rem; }""" in gate_component - assert "Connect a model to send" in input_store + # The composer never hard-blocks sending: the in-chat gate guides users instead. + assert "Connect a model to send" not in input_store + assert "blocked" not in input_store + assert "sendDisabled" not in input_store + assert "isBlockingSend" not in gate_store + assert "isBlockingSend" not in index_js def test_welcome_composer_does_not_overlap_idle_progress_placeholder() -> None: input_store = _read("webui/components/chat/input/input-store.js") - assert "!!chatsStore.selected &&\n ![\"all\", \"blocked\"].includes(this._getSendState())" in input_store + assert "!!chatsStore.selected &&\n this._getSendState() !== \"all\"" in input_store def test_welcome_composer_buttons_keep_target_geometry_without_glow() -> None: diff --git a/webui/components/chat/input/chat-bar-input.html b/webui/components/chat/input/chat-bar-input.html index 40a815eb9..f40494da0 100644 --- a/webui/components/chat/input/chat-bar-input.html +++ b/webui/components/chat/input/chat-bar-input.html @@ -46,8 +46,7 @@
@@ -388,14 +387,6 @@ filter: none; } - #send-button.model-gate-blocked, - #send-button.model-gate-blocked:hover { - background-color: color-mix(in srgb, var(--color-border) 55%, transparent); - color: color-mix(in srgb, var(--color-text) 45%, transparent); - cursor: not-allowed; - opacity: 0.78; - } - #send-button:active { background-color: #2b309c; transform: translateY(1px) scale(0.98); diff --git a/webui/components/chat/input/input-store.js b/webui/components/chat/input/input-store.js index 88e1ec1e0..ca1fd3ead 100644 --- a/webui/components/chat/input/input-store.js +++ b/webui/components/chat/input/input-store.js @@ -5,7 +5,6 @@ import { openLatest as openLatestSurface } from "/js/surfaces.js"; import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js"; import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js"; import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; -import { store as modelGateStore } from "/components/chat/model-gate-store.js"; const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g; const FENCE_LINE_RE = /^```([A-Za-z0-9_-]*)?$/; @@ -84,7 +83,6 @@ const model = { const hasQueue = !!messageQueueStore?.hasQueue; const running = !!chatsStore.selectedContext?.running; - if (modelGateStore?.isBlockingSend) return "blocked"; if (hasQueue && !hasInput) return "all"; if ((running || hasQueue) && hasInput) return "queue"; return "normal"; @@ -93,7 +91,6 @@ const model = { get inputPlaceholder() { if (!chatsStore.selected) return "Ask anything to start a new chat"; const state = this._getSendState(); - if (state === "blocked") return "Connect a model to send"; if (state === "all") return "Press Enter to send queued messages"; if (this.showProgressPlaceholder) return ""; return "Type your message here..."; @@ -102,7 +99,7 @@ const model = { get showProgressPlaceholder() { return ( !!chatsStore.selected && - !["all", "blocked"].includes(this._getSendState()) && + this._getSendState() !== "all" && !!this.progressText && !this.message ); @@ -118,7 +115,6 @@ const model = { // Computed: send button icon type get sendButtonIcon() { const state = this._getSendState(); - if (state === "blocked") return "settings"; if (state === "all") return "send_and_archive"; if (state === "queue") return "schedule_send"; return "arrow_forward"; @@ -127,7 +123,6 @@ const model = { // Computed: send button CSS class get sendButtonClass() { const state = this._getSendState(); - if (state === "blocked") return "model-gate-blocked"; if (state === "all") return "send-queue send-all"; if (state === "queue") return "send-queue queue"; return ""; @@ -136,23 +131,17 @@ const model = { // Computed: send button title get sendButtonTitle() { const state = this._getSendState(); - if (state === "blocked") return "Connect a model to send"; if (state === "all") return "Send all queued messages"; if (state === "queue") return "Add to queue"; return "Send message"; }, - get sendDisabled() { - return this._getSendState() === "blocked"; - }, - init() { console.log("Input store initialized"); // Event listeners are now handled via Alpine directives in the component }, async sendMessage() { - if (this.sendDisabled) return; this._syncMessageFromEditor(); // Capture sent prompt to per-chat history (bash-style) diff --git a/webui/components/chat/model-gate-store.js b/webui/components/chat/model-gate-store.js index 0980f0f54..e6026799a 100644 --- a/webui/components/chat/model-gate-store.js +++ b/webui/components/chat/model-gate-store.js @@ -21,13 +21,6 @@ export const store = createStore("modelGate", { dispatching: false, _initialized: false, - get isBlockingSend() { - this.init(); - if (!this.active || this.connected) return false; - const currentContext = globalThis.getContext?.(); - return !this.pending?.context || !currentContext || this.pending.context === currentContext; - }, - get introText() { if (this.connected) { return `Model connected: ${this.connectedLabel || "ready"}`; diff --git a/webui/index.js b/webui/index.js index 1b91f2d2b..54695165f 100644 --- a/webui/index.js +++ b/webui/index.js @@ -42,8 +42,6 @@ let skipOneSpeech = false; export async function sendMessage(options = {}) { try { - if (!options.bypassModelGate && modelGateStore.isBlockingSend) return; - const hasProvidedMessage = Object.prototype.hasOwnProperty.call(options, "message"); let message = String(hasProvidedMessage ? options.message : inputStore.message).trim(); let attachmentsWithUrls = options.attachments || attachmentsStore.getAttachmentsForSending();