Unify onboarding provider selection

Replace the separate Cloud/Local illustrated path screen with a single provider step that switches between Cloud, Account, and Local choices. Route the first-send gate account choice through onboarding, remove the old Cloud/Local card assets, and keep model setup in the existing onboarding flow. Update static onboarding and welcome composer regressions for the merged provider step.

Alessandro committed Jul 2, 2026 at 15:23 UTC 8d887e6f6d011a64e47c610e52b792e7bb9c84d3
8 files changed +185 -310
plugins/_onboarding/webui/assets/cloud-card.webp
Binary files a/plugins/_onboarding/webui/assets/cloud-card.webp and /dev/null differ
plugins/_onboarding/webui/assets/local-card.webp
Binary files a/plugins/_onboarding/webui/assets/local-card.webp and /dev/null differ
plugins/_onboarding/webui/onboarding-store.js
+16 -44
@@ -78,16 +78,15 @@ function safeProviderName(provider) {
78 }
79
80 export const store = createStore("onboarding", {
81 - step: "path",
82 - pathChoice: "",
81 + step: "connect",
82 + providerMode: "cloud",
83 + presetMode: "",
84 loading: true,
85 saving: false,
86 config: null,
87 providerDetails: {},
88 selectedProviderId: "",
89 selectedProviderOrigin: "cloud",
89 - moreProviderQuery: "",
90 - moreCloudOpen: false,
90 sameAsMain: true,
91 userTouchedModel: {
92 chat_model: false,
@@ -109,7 +108,7 @@ export const store = createStore("onboarding", {
108 oauthProviderUi: {},
109
110 steps: [
112 - { step: "path", label: "Choose path" },
111 + { step: "connect", label: "Choose provider" },
112 { step: "setup", label: "Connect" },
113 { step: "utility", label: "Utility" },
114 { step: "ready", label: "Ready" },
@@ -120,16 +119,15 @@ export const store = createStore("onboarding", {
119 },
120
121 resetState() {
123 - this.step = "path";
124 - this.pathChoice = "";
122 + this.step = "connect";
123 + this.providerMode = ["local", "account"].includes(this.presetMode) ? this.presetMode : "cloud";
124 + this.presetMode = "";
125 this.loading = true;
126 this.saving = false;
127 this.config = null;
128 this.providerDetails = {};
129 this.selectedProviderId = "";
130 this.selectedProviderOrigin = "cloud";
131 - this.moreProviderQuery = "";
132 - this.moreCloudOpen = false;
131 this.sameAsMain = true;
132 this.userTouchedModel = { chat_model: false, utility_model: false };
133 this.modelDropdown = {
@@ -217,22 +215,8 @@ export const store = createStore("onboarding", {
215 };
216 },
217
220 - topCloudProviders() {
221 - return TOP_CLOUD_IDS.map((id) => this.providerMeta(id));
222 - },
223 -
224 - moreCloudProviders() {
225 - return MORE_CLOUD_IDS.map((id) => this.providerMeta(id));
226 - },
227 -
228 - filteredMoreCloudProviders() {
229 - const query = this.moreProviderQuery.trim().toLowerCase();
230 - const providers = this.moreCloudProviders();
231 - if (!query) return providers;
232 - return providers.filter((provider) => {
233 - const haystack = `${provider.name} ${provider.short_description} ${provider.id}`.toLowerCase();
234 - return haystack.includes(query);
235 - });
218 + cloudProviders() {
219 + return [...TOP_CLOUD_IDS, ...MORE_CLOUD_IDS].map((id) => this.providerMeta(id));
220 },
221
222 localProviderCards() {
@@ -315,12 +299,7 @@ export const store = createStore("onboarding", {
299 if (this.step === "setup") return "Choose your main model";
300 if (this.step === "utility") return "Choose your utility model";
301 if (this.step === "ready") return "Agent Zero is ready";
318 - if (this.step === "path") return "Choose how to use AI models in Agent Zero";
319 - if (this.step === "cloud") return "Choose your cloud AI provider";
320 - if (this.step === "local") {
321 - return "Choose your local LLM provider";
322 - }
323 - return "Choose how to use AI models in Agent Zero";
302 + return "Choose your AI provider";
303 },
304
305 stepNumber(stepName) {
@@ -329,7 +308,6 @@ export const store = createStore("onboarding", {
308 },
309
310 currentStepNumber() {
332 - if (this.step === "cloud" || this.step === "local") return 1;
311 return this.stepNumber(this.step);
312 },
313
@@ -337,18 +315,13 @@ export const store = createStore("onboarding", {
315 return this.step === name;
316 },
317
340 - choosePath(path) {
341 - this.pathChoice = path;
342 - this.step = path === "local" ? "local" : "cloud";
318 + setProviderMode(mode) {
319 + this.providerMode = ["local", "account"].includes(mode) ? mode : "cloud";
320 },
321
322 goBack() {
346 - if (this.step === "cloud" || this.step === "local") {
347 - this.step = "path";
348 - return;
349 - }
323 if (this.step === "setup") {
351 - this.step = this.pathChoice === "local" ? "local" : "cloud";
324 + this.step = "connect";
325 return;
326 }
327 if (this.step === "utility") {
@@ -361,7 +334,7 @@ export const store = createStore("onboarding", {
334 },
335
336 showBackButton() {
364 - return !["path", "ready"].includes(this.step);
337 + return !["connect", "ready"].includes(this.step);
338 },
339
340 showPrimaryButton() {
@@ -410,7 +383,7 @@ export const store = createStore("onboarding", {
383 async selectProvider(providerId, origin = "cloud") {
384 this.selectedProviderId = providerId;
385 this.selectedProviderOrigin = origin;
413 - this.pathChoice = origin;
386 + this.providerMode = ["local", "account"].includes(origin) ? origin : "cloud";
387 const meta = this.providerMeta(providerId);
388 this.applyProviderToSlot("chat_model", providerId, meta, { forceApiBase: origin === "local" });
389 if (this.isOAuthProvider(providerId)) {
@@ -427,8 +400,7 @@ export const store = createStore("onboarding", {
400 },
401
402 async selectOAuthProvider(providerId) {
430 - this.pathChoice = "cloud";
431 - await this.selectProvider(providerId, "cloud");
403 + await this.selectProvider(providerId, "account");
404 },
405
406 applyProviderToSlot(slotKey, providerId, meta, options = {}) {
plugins/_onboarding/webui/onboarding.html
+96 -218
@@ -20,7 +20,7 @@
20 padding: 24px 30px 28px;
21 background: var(--color-panel, #151515);
22 }
23 - .onboarding-content { position: relative; z-index: 1; }
23 + .onboarding-content { position: relative; z-index: 1; container-type: inline-size; }
24 .onboarding-header {
25 display: flex;
26 align-items: center;
@@ -85,63 +85,57 @@
85 box-shadow: 0 18px 55px rgba(0, 0, 0, 0.22);
86 padding: 18px;
87 }
88 - .path-grid {
89 - display: grid;
90 - grid-template-columns: repeat(2, minmax(0, 1fr));
91 - gap: 16px;
88 + .mode-switch {
89 + display: flex;
90 + gap: 6px;
91 + padding: 6px;
92 + margin-bottom: 16px;
93 + border: 1px solid var(--onboard-line);
94 + border-radius: 16px;
95 + background: color-mix(in srgb, var(--color-background) 45%, transparent);
96 }
93 - .path-card {
94 - position: relative;
95 - aspect-ratio: 21 / 9;
96 - min-height: 0;
97 - overflow: hidden;
98 - border: 1px solid color-mix(in srgb, var(--onboard-line) 78%, transparent);
99 - border-radius: 18px;
100 - background: var(--onboard-soft);
97 + .mode-option {
98 + flex: 1 1 0;
99 + min-width: 0;
100 + display: flex;
101 + align-items: center;
102 + justify-content: center;
103 + gap: 11px;
104 + min-height: 56px;
105 + padding: 9px 16px;
106 + border: 1px solid transparent;
107 + border-radius: 12px;
108 + background: transparent;
109 + color: var(--onboard-muted);
110 text-align: left;
111 cursor: pointer;
103 - padding: 0;
104 - color: var(--onboard-ink);
112 font-family: "Rubik", var(--font-family, sans-serif);
106 - transition: border-color 0.18s ease, box-shadow 0.18s ease;
113 + transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
114 }
108 - .path-card:hover, .path-card:focus-visible {
109 - border-color: var(--color-border);
110 - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
115 + .mode-option:hover, .mode-option:focus-visible {
116 + border-color: color-mix(in srgb, var(--onboard-line) 82%, transparent);
117 + background: color-mix(in srgb, var(--color-background-hover) 44%, transparent);
118 outline: none;
119 }
113 - .path-card img {
114 - position: absolute;
115 - inset: 0;
116 - width: 100%;
117 - height: 100%;
118 - object-fit: cover;
119 - object-position: center;
120 - opacity: 0.92;
120 + .mode-option.active {
121 + color: var(--onboard-ink);
122 + border-color: color-mix(in srgb, #d9ad68 42%, var(--onboard-line));
123 + background: color-mix(in srgb, #d9ad68 9%, var(--color-panel));
124 }
122 - .path-card::after {
123 - content: "";
124 - position: absolute;
125 - inset: 0;
126 - background: linear-gradient(180deg, rgba(0,0,0,0.06), rgba(0,0,0,0.58));
125 + .mode-option .material-symbols-outlined {
126 + font-size: 22px;
127 }
128 - .path-card-copy {
129 - position: absolute;
130 - left: 20px;
131 - right: 20px;
132 - bottom: 18px;
133 - z-index: 1;
134 - color: #f8f4ec;
135 - text-shadow: 0 1px 18px rgba(0,0,0,0.45);
128 + .mode-option.active .material-symbols-outlined {
129 + color: #d9ad68;
130 }
137 - .path-card-title {
138 - display: block;
139 - font-size: clamp(1.18rem, 2.4vw, 1.65rem);
140 - font-weight: 700;
141 - letter-spacing: -0.02em;
142 - margin-bottom: 5px;
131 + .mode-copy { display: grid; gap: 1px; min-width: 0; }
132 + .mode-title { font-weight: 900; line-height: 1.15; }
133 + .mode-sub { font-size: 0.78rem; line-height: 1.3; color: var(--onboard-muted); }
134 + .pane-note { margin-top: 14px; }
135 + @container (max-width: 700px) {
136 + .mode-switch { flex-direction: column; }
137 + .mode-option { justify-content: flex-start; }
138 }
144 - .path-card-text { display: block; max-width: 38ch; line-height: 1.45; }
139 .account-strip {
140 display: flex;
141 align-items: center;
@@ -175,35 +169,6 @@
169 .section-title { color: var(--onboard-ink); font-weight: 900; }
170 .setup-hero .section-title { font-weight: 720; }
171 .account-subtitle, .section-description { color: var(--onboard-muted); line-height: 1.45; }
178 - .oauth-account-section {
179 - display: grid;
180 - gap: 10px;
181 - margin-top: 14px;
182 - }
183 - .path-oauth-account-section {
184 - margin-top: 24px;
185 - }
186 - .path-oauth-account-section .oauth-account-section-head {
187 - justify-content: center;
188 - text-align: center;
189 - }
190 - .path-oauth-account-section .oauth-account-section-head span {
191 - font-weight: 760;
192 - }
193 - .oauth-account-section-head {
194 - display: flex;
195 - align-items: baseline;
196 - justify-content: space-between;
197 - gap: 12px;
198 - color: var(--onboard-ink);
199 - }
200 - .oauth-account-section-head span {
201 - font-weight: 900;
202 - }
203 - .oauth-account-section-head small {
204 - color: var(--onboard-muted);
205 - font-weight: 650;
206 - }
172 .oauth-account-grid {
173 display: grid;
174 grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -307,40 +272,6 @@
272 margin-bottom: 0;
273 }
274 .provider-name { font-weight: 900; line-height: 1.18; }
310 - .more-providers {
311 - margin: 10px 0 14px;
312 - padding: 0;
313 - overflow: hidden;
314 - border: 0;
315 - background: transparent;
316 - box-shadow: none;
317 - }
318 - .more-provider-toggle {
319 - width: 100%;
320 - min-height: 44px;
321 - display: flex;
322 - align-items: center;
323 - justify-content: center;
324 - gap: 8px;
325 - border: 0;
326 - background: transparent;
327 - color: var(--onboard-ink);
328 - cursor: pointer;
329 - font-weight: 900;
330 - }
331 - .more-provider-toggle .material-symbols-outlined {
332 - transition: transform 0.18s ease;
333 - }
334 - .more-provider-toggle.open .material-symbols-outlined {
335 - transform: rotate(180deg);
336 - }
337 - .more-provider-list {
338 - display: grid;
339 - grid-template-columns: repeat(5, minmax(0, 1fr));
340 - gap: 10px;
341 - padding: 0;
342 - margin-top: 8px;
343 - }
275 .local-grid {
276 display: grid;
277 grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -623,7 +554,6 @@
554 .status-pill.connected { color: #8fd8a8; border-color: color-mix(in srgb, #8fd8a8 44%, var(--onboard-line)); }
555 @media (max-width: 980px) {
556 .provider-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
626 - .more-provider-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
557 .oauth-account-grid { grid-template-columns: 1fr; }
558 .setup-layout { grid-template-columns: 1fr; }
559 }
@@ -631,10 +561,10 @@
561 .onboarding-shell { padding: 18px 14px 22px; }
562 .onboarding-header { align-items: flex-start; flex-direction: column; }
563 .onboarding-progress { justify-content: flex-start; }
634 - .path-grid, .local-grid { grid-template-columns: 1fr; }
564 + .local-grid { grid-template-columns: 1fr; }
565 .provider-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
636 - .more-provider-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
637 - .path-card { aspect-ratio: 21 / 10; }
566 + .mode-switch { flex-direction: column; }
567 + .mode-option { justify-content: flex-start; }
568 .account-strip { align-items: flex-start; flex-direction: column; }
569 .oauth-account-card { grid-template-columns: auto minmax(0, 1fr); }
570 .oauth-account-action { grid-column: 2; justify-self: start; }
@@ -647,43 +577,8 @@
577 }
578 @media (max-width: 480px) {
579 .provider-grid { grid-template-columns: 1fr; }
650 - .more-provider-list { grid-template-columns: 1fr; }
580 .provider-card, .local-card { min-height: auto; }
581 }
653 -
654 - .more-providers.onboarding-panel {
655 - border: 0 !important;
656 - background: transparent !important;
657 - box-shadow: none !important;
658 - }
659 -
660 - .more-provider-toggle,
661 - .more-provider-toggle.open {
662 - border: 0 !important;
663 - border-top: 0 !important;
664 - border-bottom: 0 !important;
665 - box-shadow: none !important;
666 - background: transparent !important;
667 - }
668 -
669 - .more-provider-toggle:focus,
670 - .more-provider-toggle:focus-visible,
671 - .more-provider-toggle.open:focus,
672 - .more-provider-toggle.open:focus-visible {
673 - outline: none !important;
674 - text-decoration: underline;
675 - text-decoration-thickness: 1px;
676 - text-underline-offset: 5px;
677 - text-decoration-color: color-mix(in srgb, #d9ad68 62%, transparent);
678 - }
679 -
680 - .more-provider-toggle,
681 - .more-provider-toggle:focus,
682 - .more-provider-toggle:focus-visible,
683 - .more-provider-toggle.open:focus,
684 - .more-provider-toggle.open:focus-visible {
685 - text-decoration: none !important;
686 - }
582 </style>
583 </head>
584 <body>
@@ -712,62 +607,64 @@
607
608 <template x-if="!$store.onboarding.loading && $store.onboarding.config">
609 <div>
715 - <section x-show="$store.onboarding.isStep('path')" x-transition.opacity>
716 - <div class="path-grid">
717 - <button type="button" class="path-card" @click="$store.onboarding.choosePath('cloud')" aria-label="Choose Cloud provider setup">
718 - <img src="/plugins/_onboarding/webui/assets/cloud-card.webp" alt="">
719 - <span class="path-card-copy">
720 - <span class="path-card-title">Cloud</span>
721 - <span class="path-card-text">Use an online provider with an API key or account connection.</span>
610 + <section x-show="$store.onboarding.isStep('connect')" x-transition.opacity>
611 + <div class="mode-switch" role="tablist" aria-label="Where should your AI model run?">
612 + <button type="button"
613 + class="mode-option"
614 + role="tab"
615 + :class="{ active: $store.onboarding.providerMode === 'cloud' }"
616 + :aria-selected="$store.onboarding.providerMode === 'cloud' ? 'true' : 'false'"
617 + @click="$store.onboarding.setProviderMode('cloud')">
618 + <span class="material-symbols-outlined" aria-hidden="true">cloud</span>
619 + <span class="mode-copy">
620 + <span class="mode-title">Cloud</span>
621 + <span class="mode-sub">Connect with an API key</span>
622 </span>
623 </button>
724 - <button type="button" class="path-card" @click="$store.onboarding.choosePath('local')" aria-label="Choose Local model setup">
725 - <img src="/plugins/_onboarding/webui/assets/local-card.webp" alt="">
726 - <span class="path-card-copy">
727 - <span class="path-card-title">Local</span>
728 - <span class="path-card-text">Use a local LLM running on your own machine, with more control over where requests go.</span>
624 + <button type="button"
625 + class="mode-option"
626 + role="tab"
627 + x-show="$store.onboarding.oauthProviderCards().length"
628 + :class="{ active: $store.onboarding.providerMode === 'account' }"
629 + :aria-selected="$store.onboarding.providerMode === 'account' ? 'true' : 'false'"
630 + @click="$store.onboarding.setProviderMode('account')">
631 + <span class="material-symbols-outlined" aria-hidden="true">account_circle</span>
632 + <span class="mode-copy">
633 + <span class="mode-title">Account</span>
634 + <span class="mode-sub">Codex, Copilot, Gemini, Grok</span>
635 + </span>
636 + </button>
637 + <button type="button"
638 + class="mode-option"
639 + role="tab"
640 + :class="{ active: $store.onboarding.providerMode === 'local' }"
641 + :aria-selected="$store.onboarding.providerMode === 'local' ? 'true' : 'false'"
642 + @click="$store.onboarding.setProviderMode('local')">
643 + <span class="material-symbols-outlined" aria-hidden="true">memory</span>
644 + <span class="mode-copy">
645 + <span class="mode-title">Local</span>
646 + <span class="mode-sub">Runs on your machine</span>
647 </span>
648 </button>
649 </div>
732 - <div class="oauth-account-section path-oauth-account-section" x-show="$store.onboarding.oauthProviderCards().length">
733 - <div class="oauth-account-section-head">
734 - <span>or connect your AI account</span>
735 - </div>
736 - <div class="oauth-account-grid">
737 - <template x-for="provider in $store.onboarding.oauthProviderCards()" :key="`path-${provider.provider_id}`">
738 - <button type="button" class="oauth-account-card" :class="{ connected: provider.connected }" @click="$store.onboarding.selectOAuthProvider(provider.provider_id)">
739 - <span class="oauth-account-mark" :class="{ connected: provider.connected }">
740 - <span class="material-symbols-outlined" x-text="$store.onboarding.oauthMark(provider)"></span>
741 - </span>
742 - <span class="oauth-account-copy">
743 - <span class="oauth-account-title" x-text="$store.onboarding.oauthCardTitle(provider)"></span>
744 - <span class="oauth-account-subtitle" x-text="$store.onboarding.oauthCardSubtitle(provider)"></span>
650 +
651 + <div x-show="$store.onboarding.providerMode === 'local'" x-transition.opacity>
652 + <div class="local-grid">
653 + <template x-for="provider in $store.onboarding.localProviderCards()" :key="provider.id + provider.name">
654 + <button type="button" class="local-card" @click="$store.onboarding.selectProvider(provider.id, 'local')">
655 + <span class="provider-card-top">
656 + <img class="provider-logo" :src="provider.logo" :alt="provider.name + ' logo'" @error="$el.style.display='none'">
657 + <span class="provider-name" x-text="provider.name"></span>
658 </span>
746 - <span class="oauth-account-action" x-text="$store.onboarding.oauthAccountActionLabel(provider.provider_id)"></span>
659 </button>
660 </template>
661 </div>
662 + <div class="soft-note pane-note">Pick the app you use to run models on this machine. No API key needed — Agent Zero connects to it directly.</div>
663 </div>
751 - </section>
664
753 - <section x-show="$store.onboarding.isStep('cloud')" x-transition.opacity>
754 - <div class="provider-grid" aria-label="Cloud providers">
755 - <template x-for="provider in $store.onboarding.topCloudProviders()" :key="provider.id">
756 - <button type="button" class="provider-card" @click="$store.onboarding.selectProvider(provider.id, 'cloud')">
757 - <span class="provider-card-top">
758 - <img class="provider-logo" :src="provider.logo" :alt="provider.name + ' logo'" @error="$el.style.display='none'">
759 - <span class="provider-name" x-text="provider.name"></span>
760 - </span>
761 - </button>
762 - </template>
763 - </div>
764 - <div class="oauth-account-section" x-show="$store.onboarding.oauthProviderCards().length">
765 - <div class="oauth-account-section-head">
766 - <span>Accounts</span>
767 - <small>Account-backed providers can be used without API keys.</small>
768 - </div>
665 + <div x-show="$store.onboarding.providerMode === 'account'" x-transition.opacity>
666 <div class="oauth-account-grid">
770 - <template x-for="provider in $store.onboarding.oauthProviderCards()" :key="`cloud-${provider.provider_id}`">
667 + <template x-for="provider in $store.onboarding.oauthProviderCards()" :key="`account-${provider.provider_id}`">
668 <button type="button" class="oauth-account-card" :class="{ connected: provider.connected }" @click="$store.onboarding.selectOAuthProvider(provider.provider_id)">
669 <span class="oauth-account-mark" :class="{ connected: provider.connected }">
670 <span class="material-symbols-outlined" x-text="$store.onboarding.oauthMark(provider)"></span>
@@ -780,18 +677,12 @@
677 </button>
678 </template>
679 </div>
680 + <div class="soft-note pane-note">Sign in with a subscription you already have — no API key needed.</div>
681 </div>
784 - <div class="more-providers onboarding-panel">
785 - <button type="button"
786 - class="more-provider-toggle"
787 - :class="{ open: $store.onboarding.moreCloudOpen }"
788 - @click="$store.onboarding.moreCloudOpen = !$store.onboarding.moreCloudOpen"
789 - :aria-expanded="$store.onboarding.moreCloudOpen ? 'true' : 'false'">
790 - <span>Click here if you don't see your provider</span>
791 - <span class="material-symbols-outlined">keyboard_arrow_down</span>
792 - </button>
793 - <div class="more-provider-list" x-show="$store.onboarding.moreCloudOpen" x-transition.opacity>
794 - <template x-for="provider in $store.onboarding.moreCloudProviders()" :key="provider.id">
682 +
683 + <div x-show="$store.onboarding.providerMode === 'cloud'" x-transition.opacity>
684 + <div class="provider-grid" aria-label="Cloud providers">
685 + <template x-for="provider in $store.onboarding.cloudProviders()" :key="provider.id">
686 <button type="button" class="provider-card" @click="$store.onboarding.selectProvider(provider.id, 'cloud')">
687 <span class="provider-card-top">
688 <img class="provider-logo" :src="provider.logo" :alt="provider.name + ' logo'" @error="$el.style.display='none'">
@@ -803,19 +694,6 @@
694 </div>
695 </section>
696
806 - <section x-show="$store.onboarding.isStep('local')" x-transition.opacity>
807 - <div class="local-grid">
808 - <template x-for="provider in $store.onboarding.localProviderCards()" :key="provider.id + provider.name">
809 - <button type="button" class="local-card" @click="$store.onboarding.selectProvider(provider.id, 'local')">
810 - <span class="provider-card-top">
811 - <img class="provider-logo" :src="provider.logo" :alt="provider.name + ' logo'" @error="$el.style.display='none'">
812 - <span class="provider-name" x-text="provider.name"></span>
813 - </span>
814 - </button>
815 - </template>
816 - </div>
817 - </section>
818 -
697 <section x-show="$store.onboarding.isStep('setup')" x-transition.opacity>
698 <div class="setup-layout">
699 <div class="setup-hero">
@@ -984,7 +862,7 @@
862 <x-extension id="onboarding-success-end"></x-extension>
863 </section>
864
987 - <div class="onboarding-advanced-link" x-show="!$store.onboarding.isStep('ready') && !$store.onboarding.isStep('cloud') && !$store.onboarding.isStep('local')">
865 + <div class="onboarding-advanced-link" x-show="!$store.onboarding.isStep('ready')">
866 <button type="button" class="advanced-settings-toggle" @click="$store.onboarding.openAdvancedSettings()">
867 <span>Advanced Settings</span>
868 <span class="material-symbols-outlined">keyboard_arrow_right</span>
tests/test_onboarding_static.py
+41 -13
@@ -5,25 +5,56 @@ import yaml
5 PROJECT_ROOT = Path(__file__).resolve().parents[1]
6
7
8 -def test_onboarding_contains_guided_cloud_local_flow():
8 +def test_onboarding_contains_unified_provider_step():
9 html = (PROJECT_ROOT / "plugins/_onboarding/webui/onboarding.html").read_text(encoding="utf-8")
10 store = (PROJECT_ROOT / "plugins/_onboarding/webui/onboarding-store.js").read_text(encoding="utf-8")
11 + gate_store = (PROJECT_ROOT / "webui/components/chat/model-gate-store.js").read_text(encoding="utf-8")
12
13 assert "Cloud" in html
14 assert "Local" in html
15 assert "Welcome to Agent Zero" in html
15 - assert "Choose how to use AI models in Agent Zero" in html + store
16 - assert "Choose your cloud AI provider" in html + store
17 - assert "Choose your local LLM provider" in html + store
18 - assert "cloud-card.webp" in html
19 - assert "local-card.webp" in html
16 + assert "Choose your AI provider" in store
17 +
18 + # The illustrated Cloud/Local path screen is gone: one merged provider step.
19 + assert "Choose how to use AI models in Agent Zero" not in html + store
20 + assert "path-card" not in html
21 + assert "cloud-card.webp" not in html
22 + assert "local-card.webp" not in html
23 + assert not (PROJECT_ROOT / "plugins/_onboarding/webui/assets/cloud-card.webp").exists()
24 + assert not (PROJECT_ROOT / "plugins/_onboarding/webui/assets/local-card.webp").exists()
25 + assert "choosePath" not in html + store + gate_store
26 + assert "isStep('connect')" in html
27 + assert "isStep('path')" not in html
28 + assert "isStep('cloud')" not in html
29 + assert "isStep('local')" not in html
30 +
31 + # Cloud/Account/Local segmented switch on the merged step.
32 + assert "mode-switch" in html
33 + assert "setProviderMode('cloud')" in html
34 + assert "setProviderMode('account')" in html
35 + assert "setProviderMode('local')" in html
36 + assert "providerMode === 'cloud'" in html
37 + assert "providerMode === 'account'" in html
38 + assert "providerMode === 'local'" in html
39 + assert html.index("setProviderMode('cloud')") < html.index("setProviderMode('account')") < html.index("setProviderMode('local')")
40 + assert '{ step: "connect", label: "Choose provider" }' in store
41 +
42 + # Cloud pane lists every provider directly and only mentions API keys;
43 + # accounts live in their own pane.
44 + assert "Connect with an API key" in html
45 + assert "cloudProviders()" in html
46 + assert "Click here if you don't see your provider" not in html
47 + assert "moreCloudOpen" not in html + store
48 + assert "API key or account connection" not in html
49 +
50 + # The chat model gate presets the mode before the modal opens (no flash).
51 + assert "presetMode" in store
52 + assert "onboardingStore.presetMode = this.choice;" in gate_store
53 + assert "applyOnboardingChoice" not in gate_store
54 +
55 assert "oauthProviderCards()" in html + store
56 assert "selectOAuthProvider(provider.provider_id)" in html
22 - assert "path-oauth-account-section" in html
23 - assert "or connect your AI account" in html
57 assert "Connect one or more account-backed providers." not in html
25 - assert ".path-oauth-account-section .oauth-account-section-head" in html
26 - assert "font-weight: 760;" in html
58 assert "Connect ChatGPT/Codex Account" not in html
59 assert "Main model" in html
60 assert "Refresh model list" in html
@@ -37,10 +68,7 @@ def test_onboarding_contains_guided_cloud_local_flow():
68 assert "submitOAuthManualCallback" in html + store
69 assert 'const OAUTH_START_API = "/plugins/_oauth/start_login";' in store
70 assert "/plugins/_oauth/start_device_login" not in store
40 - assert "Click here if you don't see your provider" in html
71 assert "provider-description" not in html
42 - assert "!$store.onboarding.isStep('cloud')" in html
43 - assert "!$store.onboarding.isStep('local')" in html
72 assert "main-model-field" in html
73 assert "wide-inline-field" in html
74 assert "utility-panel" in html
tests/test_welcome_composer_static.py
+6 -3
@@ -123,13 +123,16 @@ def test_welcome_composer_can_create_a_chat_before_sending() -> None:
123 assert "const currentContext = globalThis.getContext?.();" in gate_store
124 assert "bypassModelGate: true" in gate_store
125 assert 'openPluginConfig("_model_config", "Advanced model configuration")' in gate_store
126 - assert 'openPluginConfig("_oauth", "OAuth Connections")' in gate_store
126 + assert 'openPluginConfig("_oauth"' not in gate_store
127 assert "Your message sends automatically once a model is connected." in gate_component
128 assert "openOnboarding('cloud')" in gate_component
129 + assert "openOnboarding('account')" in gate_component
130 assert "openOnboarding('local')" in gate_component
130 - assert "openOauthConfiguration()" in gate_component
131 + assert gate_component.index("openOnboarding('cloud')") < gate_component.index("openOnboarding('account')") < gate_component.index("openOnboarding('local')")
132 + assert "openOauthConfiguration" not in gate_component
133 + assert "Show accounts" not in gate_component
134 + assert "model-gate-accounts" not in gate_component
135 assert "Choose models" in gate_component
132 - assert "Connected account:" in gate_component
136 assert "Advanced model configuration" in gate_component
137 assert "Advanced settings" not in gate_component
138 assert "model-gate-fields" not in gate_component
webui/components/chat/model-gate-store.js
+3 -17
@@ -134,31 +134,17 @@ export const store = createStore("modelGate", {
134 },
135
136 openOnboarding(choice) {
137 - this.choice = choice === "local" ? "local" : "cloud";
137 + this.choice = ["local", "account"].includes(choice) ? choice : "cloud";
138 this.savePending();
139 + onboardingStore.presetMode = this.choice;
140 const modalPromise = window.openModal?.(ONBOARDING_MODAL_PATH);
140 - void this.applyOnboardingChoice(this.choice);
141 void Promise.resolve(modalPromise).then(() => this.dispatchPendingIfConfigured());
142 },
143
144 - async applyOnboardingChoice(choice) {
145 - for (let attempt = 0; attempt < 40; attempt += 1) {
146 - if (onboardingStore.config && !onboardingStore.loading) {
147 - onboardingStore.choosePath(choice);
148 - return;
149 - }
150 - await new Promise((resolve) => setTimeout(resolve, 50));
151 - }
152 - },
153 -
144 async openAdvancedModelConfiguration() {
145 await this.openPluginConfig("_model_config", "Advanced model configuration");
146 },
147
158 - async openOauthConfiguration() {
159 - await this.openPluginConfig("_oauth", "OAuth Connections");
160 - },
161 -
148 async openPluginConfig(pluginName, title) {
149 try {
150 await pluginSettingsStore.openConfig(pluginName);
@@ -239,7 +225,7 @@ export const store = createStore("modelGate", {
225 this.connected = false;
226 this.pending = { message, attachments: [], messageId, context };
227 this.gateMessageId = String(saved?.gateMessageId || `model-gate-${messageId}`);
242 - this.choice = saved?.choice === "local" || saved?.choice === "cloud" ? saved.choice : "";
228 + this.choice = ["local", "cloud", "account"].includes(saved?.choice) ? saved.choice : "";
229 },
230
231 clearSavedPending() {
webui/components/chat/model-setup-gate.html
+23 -15
@@ -41,7 +41,17 @@
41 <span class="material-symbols-outlined" aria-hidden="true">cloud</span>
42 <span>Cloud provider</span>
43 </span>
44 - <span class="model-gate-option-sub">API key or account connection</span>
44 + <span class="model-gate-option-sub">Connect with an API key</span>
45 + </button>
46 + <button type="button"
47 + class="model-gate-option"
48 + :class="{ selected: $store.modelGate.choice === 'account' }"
49 + @click="$store.modelGate.openOnboarding('account')">
50 + <span class="model-gate-option-title">
51 + <span class="material-symbols-outlined" aria-hidden="true">account_circle</span>
52 + <span>AI account</span>
53 + </span>
54 + <span class="model-gate-option-sub">Codex, Copilot, Gemini, Grok</span>
55 </button>
56 <button type="button"
57 class="model-gate-option"
@@ -54,13 +64,6 @@
64 <span class="model-gate-option-sub">Runs on your machine</span>
65 </button>
66 </div>
57 -
58 - <div class="model-gate-accounts">
59 - <span x-text="$store.modelGate.accountConnected ? `Connected account: ${$store.modelGate.accountLabel || 'ready'}` : 'Or connect an account: Codex, Copilot, Gemini, Grok'"></span>
60 - <button type="button" @click="$store.modelGate.openOauthConfiguration()">
61 - <span>Show accounts</span>
62 - </button>
63 - </div>
67 </div>
68
69 <p class="model-gate-footnote">
@@ -83,6 +86,7 @@
86 .model-gate-body {
87 display: grid;
88 gap: 0.65rem;
89 + container-type: inline-size;
90 }
91
92 .model-gate-intro {
@@ -97,13 +101,19 @@
101 }
102
103 .model-gate-fork {
100 - display: grid;
101 - grid-template-columns: repeat(2, minmax(0, 1fr));
104 + display: flex;
105 gap: 0.6rem;
106 }
107
105 - .model-gate-fork.is-single {
106 - grid-template-columns: 1fr;
108 + .model-gate-fork .model-gate-option {
109 + flex: 1 1 0;
110 + min-width: 0;
111 + }
112 +
113 + @container (max-width: 30rem) {
114 + .model-gate-fork {
115 + flex-direction: column;
116 + }
117 }
118
119 .model-gate-option {
@@ -149,7 +159,6 @@
159 line-height: 1.35;
160 }
161
152 - .model-gate-accounts,
162 .model-gate-footnote {
163 display: flex;
164 align-items: center;
@@ -158,7 +167,6 @@
167 flex-wrap: wrap;
168 }
169
161 - .model-gate-accounts button,
170 .model-gate-footnote button {
171 border: 0;
172 background: transparent;
@@ -187,7 +195,7 @@
195
196 @media (max-width: 480px) {
197 .model-gate-fork {
190 - grid-template-columns: 1fr;
198 + flex-direction: column;
199 }
200 }
201 </style>