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>