Polish onboarding and email setup UX
Add a clearer onboarding progression for Main Model, Utility Model, and ready states with streamlined copy and calmer calls to action. Rework email integration setup around provider-first selection, guided defaults, and cleaner account fields.
Alessandro committed
Apr 27, 2026 at 03:00 UTC
061b43298b3db1fe8efc71af5b43535096f77876
5 files changed
+625
-192
plugins/_email_integration/default_config.yaml
+1
@@ -2,6 +2,7 @@ handlers: []
2
# Example handler:
3
# - name: support
4
# enabled: true
5
+# provider: gmail
6
# account_type: imap
7
# imap_server: imap.gmail.com
8
# imap_port: 993
plugins/_email_integration/webui/config.html
+389
-170
@@ -50,209 +50,243 @@
50
51
<template x-if="$store.emailConfig.editing === idx">
52
<div class="email-card-body">
53
- <div class="email-intro-copy">
54
- <div x-text="$store.emailConfig.providerHint(handler)"></div>
55
- <template x-if="$store.emailConfig.providerHelpUrl(handler)">
56
- <a class="email-intro-link"
57
- :href="$store.emailConfig.providerHelpUrl(handler)"
58
- target="_blank"
59
- rel="noopener"
60
- x-text="$store.emailConfig.providerHelpLabel(handler)"></a>
61
- </template>
62
- </div>
63
-
64
- <div class="field">
65
- <div class="field-label">
66
- <div class="field-title">Turn on this inbox</div>
67
- <div class="field-description">Enable when you are ready for Agent Zero to start
68
- checking mail.</div>
69
- </div>
70
- <div class="field-control">
71
- <label class="toggle">
72
- <input type="checkbox" x-model="handler.enabled" />
73
- <span class="toggler"></span>
74
- </label>
53
+ <div class="email-provider-panel"
54
+ :class="{ 'has-provider': $store.emailConfig.hasProvider(handler) }">
55
+ <div class="email-provider-topline">
56
+ <div class="email-provider-copy">
57
+ <div class="email-provider-title"
58
+ x-text="$store.emailConfig.hasProvider(handler) ? $store.emailConfig.providerLabel($store.emailConfig.providerValue(handler)) : 'Choose your email provider'">
59
+ </div>
60
+ <div class="email-provider-description"
61
+ x-text="$store.emailConfig.hasProvider(handler) ? $store.emailConfig.providerHint(handler) : 'Pick a provider and Agent Zero will prepare the usual incoming and outgoing mail settings.'">
62
+ </div>
63
+ <template x-if="$store.emailConfig.providerHelpUrl(handler)">
64
+ <a class="email-intro-link"
65
+ :href="$store.emailConfig.providerHelpUrl(handler)"
66
+ target="_blank"
67
+ rel="noopener"
68
+ x-text="$store.emailConfig.providerHelpLabel(handler)"></a>
69
+ </template>
70
+ </div>
71
+ <template x-if="$store.emailConfig.hasProvider(handler)">
72
+ <button type="button" class="btn btn-field email-change-provider"
73
+ @click.stop="$store.emailConfig.changeProvider(idx)"
74
+ x-text="$store.emailConfig.showProviderChoices(handler, idx) ? 'Done' : 'Change'">
75
+ </button>
76
+ </template>
77
</div>
76
- </div>
78
78
- <div class="field">
79
- <div class="field-label">
80
- <div class="field-title">Provider</div>
81
- <div class="field-description">Pick the provider first. We will fill in the
82
- common server settings for you.</div>
83
- </div>
84
- <div class="field-control">
85
- <select :value="$store.emailConfig.providerValue(handler)"
86
- @change="$store.emailConfig.applyProvider(handler, $event.target.value)">
87
- <option value="">Choose a provider</option>
88
- <option value="gmail">Gmail</option>
89
- <option value="icloud">iCloud Mail</option>
90
- <option value="microsoft365">Outlook / Microsoft 365</option>
91
- <option value="yahoo">Yahoo Mail</option>
92
- <option value="exchange">Exchange</option>
93
- <option value="custom-imap">Custom IMAP</option>
94
- </select>
79
+ <div class="email-provider-grid"
80
+ x-show="$store.emailConfig.showProviderChoices(handler, idx)">
81
+ <template x-for="provider in $store.emailConfig.providerOptions"
82
+ :key="provider.value">
83
+ <button type="button" class="email-provider-option"
84
+ :class="($store.emailConfig.providerValue(handler) === provider.value ? 'is-selected ' : '') + 'logo-' + provider.logo"
85
+ :aria-pressed="$store.emailConfig.providerValue(handler) === provider.value ? 'true' : 'false'"
86
+ :title="provider.label"
87
+ @click.stop="$store.emailConfig.selectProvider(handler, provider.value, idx)">
88
+ <span class="email-provider-logo" aria-hidden="true">
89
+ <template x-if="provider.logo === 'microsoft365'">
90
+ <span class="email-microsoft-mark">
91
+ <span></span>
92
+ <span></span>
93
+ <span></span>
94
+ <span></span>
95
+ </span>
96
+ </template>
97
+ <template
98
+ x-if="provider.logo !== 'microsoft365' && (provider.logo === 'icloud' || provider.logo === 'custom-imap')">
99
+ <span class="material-symbols-outlined"
100
+ x-text="provider.logoText"></span>
101
+ </template>
102
+ <template
103
+ x-if="provider.logo !== 'microsoft365' && provider.logo !== 'icloud' && provider.logo !== 'custom-imap'">
104
+ <span x-text="provider.logoText"></span>
105
+ </template>
106
+ </span>
107
+ <span class="email-provider-name" x-text="provider.label"></span>
108
+ <span class="email-provider-hint" x-text="provider.hint"></span>
109
+ </button>
110
+ </template>
111
</div>
112
</div>
113
98
- <div class="field">
99
- <div class="field-label">
100
- <div class="field-title">Email address</div>
101
- <div class="field-description">Usually the full address for this inbox.</div>
102
- </div>
103
- <div class="field-control">
104
- <input type="text" x-model="handler.username"
105
- @input="$store.emailConfig.maybeAutoname(handler)"
106
- placeholder="name@company.com" />
114
+ <div class="email-post-provider" x-show="$store.emailConfig.hasProvider(handler)">
115
+ <div class="field">
116
+ <div class="field-label">
117
+ <div class="field-title">Turn on this inbox</div>
118
+ <div class="field-description">Enable when you are ready for Agent Zero to
119
+ start checking mail.</div>
120
+ </div>
121
+ <div class="field-control">
122
+ <label class="toggle">
123
+ <input type="checkbox" x-model="handler.enabled" />
124
+ <span class="toggler"></span>
125
+ </label>
126
+ </div>
127
</div>
108
- </div>
128
110
- <div class="field">
111
- <div class="field-label">
112
- <div class="field-title">Password</div>
113
- <div class="field-description">An app password is best. Many mail providers
114
- block regular account passwords.</div>
115
- </div>
116
- <div class="field-control">
117
- <input type="password" x-model="handler.password" />
129
+ <div class="field">
130
+ <div class="field-label">
131
+ <div class="field-title">Email address</div>
132
+ <div class="field-description">Usually the full address for this inbox.</div>
133
+ </div>
134
+ <div class="field-control">
135
+ <input type="text" x-model="handler.username"
136
+ @input="$store.emailConfig.maybeAutoname(handler)"
137
+ placeholder="name@company.com" />
138
+ </div>
139
</div>
119
- </div>
140
121
- <template x-if="$store.emailConfig.showExchangeServer(handler)">
122
- <div>
123
- <div class="field">
124
- <div class="field-label">
125
- <div class="field-title"
126
- x-text="$store.emailConfig.incomingLabel(handler)"></div>
127
- <div class="field-description"
128
- x-text="$store.emailConfig.incomingDescription(handler)"></div>
129
- </div>
130
- <div class="field-control">
131
- <input type="text" x-model="handler.imap_server"
132
- :placeholder="$store.emailConfig.incomingPlaceholder(handler)" />
133
- </div>
141
+ <div class="field">
142
+ <div class="field-label">
143
+ <div class="field-title">Password</div>
144
+ <div class="field-description">An app password is best. Many mail providers
145
+ block regular account passwords.</div>
146
+ </div>
147
+ <div class="field-control">
148
+ <input type="password" x-model="handler.password" />
149
</div>
150
</div>
136
- </template>
151
138
- <template x-if="$store.emailConfig.showManualServers(handler)">
139
- <div class="email-grid">
140
- <div class="field">
141
- <div class="field-label">
142
- <div class="field-title">Incoming mail server</div>
143
- <div class="field-description">The IMAP server for this inbox.</div>
144
- </div>
145
- <div class="field-control">
146
- <input type="text" x-model="handler.imap_server"
147
- placeholder="imap.your-provider.com" />
152
+ <template x-if="$store.emailConfig.showExchangeServer(handler)">
153
+ <div>
154
+ <div class="field">
155
+ <div class="field-label">
156
+ <div class="field-title"
157
+ x-text="$store.emailConfig.incomingLabel(handler)"></div>
158
+ <div class="field-description"
159
+ x-text="$store.emailConfig.incomingDescription(handler)"></div>
160
+ </div>
161
+ <div class="field-control">
162
+ <input type="text" x-model="handler.imap_server"
163
+ :placeholder="$store.emailConfig.incomingPlaceholder(handler)" />
164
+ </div>
165
</div>
166
</div>
167
+ </template>
168
151
- <div class="field">
152
- <div class="field-label">
153
- <div class="field-title">Incoming port</div>
154
- <div class="field-description">993 is the usual SSL port.</div>
155
- </div>
156
- <div class="field-control">
157
- <input type="number" x-model.number="handler.imap_port"
158
- placeholder="993" />
169
+ <template x-if="$store.emailConfig.showManualServers(handler)">
170
+ <div class="email-grid">
171
+ <div class="field">
172
+ <div class="field-label">
173
+ <div class="field-title">Incoming mail server</div>
174
+ <div class="field-description">The IMAP server for this inbox.</div>
175
+ </div>
176
+ <div class="field-control">
177
+ <input type="text" x-model="handler.imap_server"
178
+ placeholder="imap.your-provider.com" />
179
+ </div>
180
</div>
160
- </div>
181
162
- <div class="field">
163
- <div class="field-label">
164
- <div class="field-title">Outgoing mail server</div>
165
- <div class="field-description">The SMTP server used for replies.</div>
166
- </div>
167
- <div class="field-control">
168
- <input type="text" x-model="handler.smtp_server"
169
- placeholder="smtp.your-provider.com" />
182
+ <div class="field">
183
+ <div class="field-label">
184
+ <div class="field-title">Incoming port</div>
185
+ <div class="field-description">993 is the usual SSL port.</div>
186
+ </div>
187
+ <div class="field-control">
188
+ <input type="number" x-model.number="handler.imap_port"
189
+ placeholder="993" />
190
+ </div>
191
</div>
171
- </div>
192
173
- <div class="field">
174
- <div class="field-label">
175
- <div class="field-title">Outgoing port</div>
176
- <div class="field-description">587 is the usual TLS port.</div>
193
+ <div class="field">
194
+ <div class="field-label">
195
+ <div class="field-title">Outgoing mail server</div>
196
+ <div class="field-description">The SMTP server used for replies.
197
+ </div>
198
+ </div>
199
+ <div class="field-control">
200
+ <input type="text" x-model="handler.smtp_server"
201
+ placeholder="smtp.your-provider.com" />
202
+ </div>
203
</div>
178
- <div class="field-control">
179
- <input type="number" x-model.number="handler.smtp_port"
180
- placeholder="587" />
204
+
205
+ <div class="field">
206
+ <div class="field-label">
207
+ <div class="field-title">Outgoing port</div>
208
+ <div class="field-description">587 is the usual TLS port.</div>
209
+ </div>
210
+ <div class="field-control">
211
+ <input type="number" x-model.number="handler.smtp_port"
212
+ placeholder="587" />
213
+ </div>
214
</div>
215
</div>
183
- </div>
184
- </template>
185
-
186
- <template
187
- x-if="!$store.emailConfig.showManualServers(handler) && !$store.emailConfig.showExchangeServer(handler) && $store.emailConfig.providerValue(handler)">
188
- <div class="email-note">
189
- Server settings are filled in for <span
190
- x-text="$store.emailConfig.providerLabel($store.emailConfig.providerValue(handler))"></span>.
191
- You can change them any time in Advanced.
192
- </div>
193
- </template>
216
+ </template>
217
195
- <div class="email-missing" x-show="$store.emailConfig.missingBits(handler).length > 0">
196
- Still needed:
197
- <span x-text="$store.emailConfig.missingBits(handler).join(', ')"></span>
198
- </div>
218
+ <template
219
+ x-if="!$store.emailConfig.showManualServers(handler) && !$store.emailConfig.showExchangeServer(handler) && $store.emailConfig.providerValue(handler)">
220
+ <div class="email-note">
221
+ Server settings are filled in for <span
222
+ x-text="$store.emailConfig.providerLabel($store.emailConfig.providerValue(handler))"></span>.
223
+ You can change them any time in Advanced.
224
+ </div>
225
+ </template>
226
200
- <div class="field">
201
- <div class="field-label">
202
- <div class="field-title">Routing instructions</div>
203
- <div class="field-description">Extra guidance for how incoming email should be
204
- routed into chats.</div>
205
- </div>
206
- <div class="field-control">
207
- <textarea x-model="handler.dispatcher_instructions" rows="3"
208
- placeholder="Always start a new chat for invoices or billing questions."></textarea>
227
+ <div class="email-missing"
228
+ x-show="$store.emailConfig.missingBits(handler).length > 0">
229
+ Still needed:
230
+ <span x-text="$store.emailConfig.missingBits(handler).join(', ')"></span>
231
</div>
210
- </div>
232
212
- <div class="field">
213
- <div class="field-label">
214
- <div class="field-title">Reply instructions</div>
215
- <div class="field-description">Extra guidance for the agent when it replies by
216
- email.</div>
233
+ <div class="field">
234
+ <div class="field-label">
235
+ <div class="field-title">Routing instructions</div>
236
+ <div class="field-description">Extra guidance for how incoming email should be
237
+ routed into chats.</div>
238
+ </div>
239
+ <div class="field-control">
240
+ <textarea x-model="handler.dispatcher_instructions" rows="3"
241
+ placeholder="Always start a new chat for invoices or billing questions."></textarea>
242
+ </div>
243
</div>
218
- <div class="field-control">
219
- <textarea x-model="handler.agent_instructions" rows="3"
220
- placeholder="Reply in a calm, concise tone."></textarea>
244
+
245
+ <div class="field">
246
+ <div class="field-label">
247
+ <div class="field-title">Reply instructions</div>
248
+ <div class="field-description">Extra guidance for the agent when it replies by
249
+ email.</div>
250
+ </div>
251
+ <div class="field-control">
252
+ <textarea x-model="handler.agent_instructions" rows="3"
253
+ placeholder="Reply in a calm, concise tone."></textarea>
254
+ </div>
255
</div>
222
- </div>
256
224
- <div class="email-test-panel">
225
- <div class="email-test-copy">
226
- <div class="email-test-title">Check the connection</div>
227
- <div class="email-test-description"
228
- x-text="$store.emailConfig.testIntro(handler)"></div>
257
+ <div class="email-test-panel">
258
+ <div class="email-test-copy">
259
+ <div class="email-test-title">Check the connection</div>
260
+ <div class="email-test-description"
261
+ x-text="$store.emailConfig.testIntro(handler)"></div>
262
+ </div>
263
+ <button class="btn btn-field" @click.stop="$store.emailConfig.testConnection(idx)"
264
+ :disabled="$store.emailConfig.testing === idx || !$store.emailConfig.canTest(handler)">
265
+ <span x-text="$store.emailConfig.testButtonLabel(handler, idx)"></span>
266
+ </button>
267
</div>
230
- <button class="btn btn-field" @click.stop="$store.emailConfig.testConnection(idx)"
231
- :disabled="$store.emailConfig.testing === idx || !$store.emailConfig.canTest(handler)">
232
- <span x-text="$store.emailConfig.testButtonLabel(handler, idx)"></span>
233
- </button>
234
- </div>
268
236
- <template
237
- x-if="$store.emailConfig.testResults && $store.emailConfig.testResultsFor === idx">
238
- <div class="email-results"
239
- :class="{ 'is-error': !$store.emailConfig.testResults.success }">
240
- <template x-for="result in $store.emailConfig.testResults.results"
241
- :key="result.test + result.message">
242
- <div class="email-result-row">
243
- <span class="email-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
244
- <div class="email-result-copy">
245
- <div class="email-result-title"
246
- x-text="$store.emailConfig.resultTitle(result)"></div>
247
- <div class="email-result-message"
248
- x-text="$store.emailConfig.resultMessage(result)"></div>
269
+ <template
270
+ x-if="$store.emailConfig.testResults && $store.emailConfig.testResultsFor === idx">
271
+ <div class="email-results"
272
+ :class="{ 'is-error': !$store.emailConfig.testResults.success }">
273
+ <template x-for="result in $store.emailConfig.testResults.results"
274
+ :key="result.test + result.message">
275
+ <div class="email-result-row">
276
+ <span class="email-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
277
+ <div class="email-result-copy">
278
+ <div class="email-result-title"
279
+ x-text="$store.emailConfig.resultTitle(result)"></div>
280
+ <div class="email-result-message"
281
+ x-text="$store.emailConfig.resultMessage(result)"></div>
282
+ </div>
283
</div>
250
- </div>
251
- </template>
252
- </div>
253
- </template>
284
+ </template>
285
+ </div>
286
+ </template>
287
+ </div>
288
255
- <details class="email-advanced">
289
+ <details class="email-advanced" x-show="$store.emailConfig.hasProvider(handler)">
290
<summary>
291
<span>Advanced</span>
292
<span class="material-symbols-outlined email-advanced-chevron"
@@ -590,6 +624,173 @@
624
transform: rotate(180deg);
625
}
626
627
+ .email-provider-panel {
628
+ margin: 1rem 0;
629
+ padding: 0.95rem;
630
+ border: 1px solid color-mix(in srgb, var(--color-border) 86%, white 14%);
631
+ border-radius: 0.5rem;
632
+ background: color-mix(in srgb, var(--color-background) 92%, white 8%);
633
+ }
634
+
635
+ .email-provider-panel.has-provider {
636
+ background: color-mix(in srgb, var(--color-background) 88%, white 12%);
637
+ }
638
+
639
+ .email-provider-topline {
640
+ display: flex;
641
+ align-items: flex-start;
642
+ justify-content: space-between;
643
+ gap: 1rem;
644
+ }
645
+
646
+ .email-provider-copy {
647
+ min-width: 0;
648
+ }
649
+
650
+ .email-provider-title {
651
+ font-size: 1rem;
652
+ font-weight: 800;
653
+ line-height: 1.35;
654
+ }
655
+
656
+ .email-provider-description {
657
+ margin-top: 0.35rem;
658
+ color: var(--color-text-secondary);
659
+ line-height: 1.5;
660
+ }
661
+
662
+ .email-change-provider {
663
+ flex: 0 0 auto;
664
+ }
665
+
666
+ .email-provider-grid {
667
+ display: grid;
668
+ grid-template-columns: repeat(3, minmax(0, 1fr));
669
+ gap: 0.7rem;
670
+ margin-top: 0.95rem;
671
+ }
672
+
673
+ .email-provider-option {
674
+ appearance: none;
675
+ width: 100%;
676
+ min-height: 5.5rem;
677
+ border: 1px solid color-mix(in srgb, var(--color-border) 84%, white 16%);
678
+ border-radius: 0.5rem;
679
+ padding: 0.8rem;
680
+ background: color-mix(in srgb, var(--color-background) 96%, white 4%);
681
+ color: var(--color-text);
682
+ display: grid;
683
+ grid-template-columns: auto minmax(0, 1fr);
684
+ grid-template-rows: auto auto;
685
+ align-items: center;
686
+ column-gap: 0.75rem;
687
+ row-gap: 0.15rem;
688
+ font: inherit;
689
+ text-align: left;
690
+ cursor: pointer;
691
+ transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
692
+ }
693
+
694
+ .email-provider-option:hover,
695
+ .email-provider-option:focus-visible {
696
+ border-color: color-mix(in srgb, var(--color-highlight) 58%, var(--color-border) 42%);
697
+ background: color-mix(in srgb, var(--color-background) 88%, white 12%);
698
+ transform: translateY(-1px);
699
+ outline: none;
700
+ }
701
+
702
+ .email-provider-option.is-selected {
703
+ border-color: color-mix(in srgb, var(--color-highlight) 78%, white 22%);
704
+ background: color-mix(in srgb, var(--color-highlight) 10%, var(--color-background) 90%);
705
+ }
706
+
707
+ .email-provider-logo {
708
+ grid-row: 1 / span 2;
709
+ width: 2.7rem;
710
+ height: 2.7rem;
711
+ border-radius: 0.5rem;
712
+ display: inline-flex;
713
+ align-items: center;
714
+ justify-content: center;
715
+ overflow: hidden;
716
+ font-size: 1.1rem;
717
+ font-weight: 900;
718
+ letter-spacing: 0;
719
+ }
720
+
721
+ .email-provider-logo .material-symbols-outlined {
722
+ font-size: 1.45rem;
723
+ }
724
+
725
+ .logo-gmail .email-provider-logo {
726
+ background: #ffffff;
727
+ color: #ea4335;
728
+ border: 1px solid rgba(234, 67, 53, 0.45);
729
+ }
730
+
731
+ .logo-icloud .email-provider-logo {
732
+ background: linear-gradient(135deg, #e0f2fe, #ffffff);
733
+ color: #2563eb;
734
+ }
735
+
736
+ .logo-yahoo .email-provider-logo {
737
+ background: #6001d2;
738
+ color: #ffffff;
739
+ }
740
+
741
+ .logo-exchange .email-provider-logo {
742
+ background: #0078d4;
743
+ color: #ffffff;
744
+ }
745
+
746
+ .logo-custom-imap .email-provider-logo {
747
+ background: color-mix(in srgb, var(--color-background) 72%, white 28%);
748
+ color: var(--color-text-secondary);
749
+ }
750
+
751
+ .email-microsoft-mark {
752
+ width: 1.55rem;
753
+ height: 1.55rem;
754
+ display: grid;
755
+ grid-template-columns: repeat(2, minmax(0, 1fr));
756
+ gap: 0.16rem;
757
+ }
758
+
759
+ .email-microsoft-mark span:nth-child(1) {
760
+ background: #f25022;
761
+ }
762
+
763
+ .email-microsoft-mark span:nth-child(2) {
764
+ background: #7fba00;
765
+ }
766
+
767
+ .email-microsoft-mark span:nth-child(3) {
768
+ background: #00a4ef;
769
+ }
770
+
771
+ .email-microsoft-mark span:nth-child(4) {
772
+ background: #ffb900;
773
+ }
774
+
775
+ .email-provider-name {
776
+ min-width: 0;
777
+ font-weight: 800;
778
+ line-height: 1.25;
779
+ overflow-wrap: anywhere;
780
+ }
781
+
782
+ .email-provider-hint {
783
+ min-width: 0;
784
+ color: var(--color-text-secondary);
785
+ font-size: var(--font-size-small);
786
+ line-height: 1.35;
787
+ overflow-wrap: anywhere;
788
+ }
789
+
790
+ .email-post-provider {
791
+ margin-top: 0.3rem;
792
+ }
793
+
794
.email-intro-copy {
795
margin: 1rem 0;
796
padding: 0.85rem 0.95rem;
@@ -764,6 +965,10 @@
965
966
@media (max-width: 900px) {
967
968
+ .email-provider-grid {
969
+ grid-template-columns: repeat(2, minmax(0, 1fr));
970
+ }
971
+
972
.email-guide-body,
973
.email-grid {
974
grid-template-columns: minmax(0, 1fr);
@@ -783,6 +988,20 @@
988
width: 100%;
989
}
990
991
+ .email-provider-topline {
992
+ flex-direction: column;
993
+ align-items: stretch;
994
+ }
995
+
996
+ .email-provider-grid,
997
+ .email-grid {
998
+ grid-template-columns: minmax(0, 1fr);
999
+ }
1000
+
1001
+ .email-change-provider {
1002
+ align-self: flex-start;
1003
+ }
1004
+
1005
.email-status-pill {
1006
min-width: 0;
1007
}
plugins/_email_integration/webui/email-config-store.js
+91
@@ -62,6 +62,51 @@ const PRESETS = {
62
},
63
};
64
65
+const PROVIDER_OPTIONS = [
66
+ {
67
+ value: "gmail",
68
+ label: "Gmail",
69
+ hint: "Google app password",
70
+ logo: "gmail",
71
+ logoText: "M",
72
+ },
73
+ {
74
+ value: "icloud",
75
+ label: "iCloud Mail",
76
+ hint: "Apple app-specific password",
77
+ logo: "icloud",
78
+ logoText: "cloud",
79
+ },
80
+ {
81
+ value: "microsoft365",
82
+ label: "Outlook / Microsoft 365",
83
+ hint: "Microsoft mailbox preset",
84
+ logo: "microsoft365",
85
+ logoText: "",
86
+ },
87
+ {
88
+ value: "yahoo",
89
+ label: "Yahoo Mail",
90
+ hint: "Yahoo app password",
91
+ logo: "yahoo",
92
+ logoText: "Y!",
93
+ },
94
+ {
95
+ value: "exchange",
96
+ label: "Exchange",
97
+ hint: "Organization Exchange server",
98
+ logo: "exchange",
99
+ logoText: "X",
100
+ },
101
+ {
102
+ value: "custom-imap",
103
+ label: "Custom IMAP",
104
+ hint: "Manual server settings",
105
+ logo: "custom-imap",
106
+ logoText: "dns",
107
+ },
108
+];
109
+
110
function ensureConfig(config) {
111
if (!config || typeof config !== "object") return;
112
if (!Array.isArray(config.handlers)) config.handlers = [];
@@ -74,18 +119,25 @@ export const store = createStore("emailConfig", {
119
testing: null,
120
testResults: null,
121
testResultsFor: null,
122
+ providerPickerOpen: null,
123
guideOpen: false,
124
didInit: false,
125
projects: [],
126
modelPresets: [],
127
presets: PRESETS,
128
129
+ get providerOptions() {
130
+ return PROVIDER_OPTIONS;
131
+ },
132
+
133
get handlers() {
134
ensureConfig(this.config);
135
return Array.isArray(this.config?.handlers) ? this.config.handlers : [];
136
},
137
138
async init(config, context = null) {
139
+ if (!config || typeof config !== "object") return;
140
+
141
this.config = config || null;
142
this.context = context;
143
this.didInit = false;
@@ -94,8 +146,12 @@ export const store = createStore("emailConfig", {
146
this.testing = null;
147
this.testResults = null;
148
this.testResultsFor = null;
149
+ this.providerPickerOpen = null;
150
this.guideOpen = this.handlers.length === 0 && window.innerWidth > 720;
151
if (this.handlers.length === 0) this._startInitialHandlerFlow();
152
+ if (this.editing !== null && !this.hasProvider(this.handlers[this.editing])) {
153
+ this.providerPickerOpen = this.editing;
154
+ }
155
this.didInit = true;
156
157
const [projectsResult, presetsResult] = await Promise.allSettled([
@@ -116,6 +172,7 @@ export const store = createStore("emailConfig", {
172
this.testing = null;
173
this.testResults = null;
174
this.testResultsFor = null;
175
+ this.providerPickerOpen = null;
176
this.guideOpen = false;
177
this.didInit = false;
178
},
@@ -124,6 +181,7 @@ export const store = createStore("emailConfig", {
181
return {
182
name: "",
183
enabled: false,
184
+ provider: "",
185
account_type: "imap",
186
imap_server: "",
187
imap_port: 993,
@@ -148,6 +206,7 @@ export const store = createStore("emailConfig", {
206
ensureConfig(this.config);
207
this.config.handlers.push(this.newHandler());
208
this.editing = this.config.handlers.length - 1;
209
+ this.providerPickerOpen = this.editing;
210
this.testResults = null;
211
this.testResultsFor = null;
212
},
@@ -156,6 +215,8 @@ export const store = createStore("emailConfig", {
215
this.handlers.splice(idx, 1);
216
if (this.editing === idx) this.editing = null;
217
if (this.editing !== null && this.editing > idx) this.editing -= 1;
218
+ if (this.providerPickerOpen === idx) this.providerPickerOpen = null;
219
+ if (this.providerPickerOpen !== null && this.providerPickerOpen > idx) this.providerPickerOpen -= 1;
220
if (this.testResultsFor === idx) {
221
this.testResults = null;
222
this.testResultsFor = null;
@@ -164,6 +225,11 @@ export const store = createStore("emailConfig", {
225
226
toggleEditing(idx) {
227
this.editing = this.editing === idx ? null : idx;
228
+ if (this.editing === null) {
229
+ this.providerPickerOpen = null;
230
+ } else if (!this.hasProvider(this.handlers[idx])) {
231
+ this.providerPickerOpen = idx;
232
+ }
233
if (this.testResultsFor !== idx) {
234
this.testResults = null;
235
this.testResultsFor = null;
@@ -171,6 +237,10 @@ export const store = createStore("emailConfig", {
237
},
238
239
providerValue(handler) {
240
+ if (!handler || typeof handler !== "object") return "";
241
+ const explicitProvider = String(handler.provider || "").trim();
242
+ if (explicitProvider && this.presets[explicitProvider]) return explicitProvider;
243
+
244
const incoming = String(handler.imap_server || "").trim().toLowerCase();
245
const outgoing = String(handler.smtp_server || "").trim().toLowerCase();
246
const accountType = handler.account_type || "imap";
@@ -184,16 +254,37 @@ export const store = createStore("emailConfig", {
254
return "custom-imap";
255
},
256
257
+ hasProvider(handler) {
258
+ return Boolean(this.providerValue(handler));
259
+ },
260
+
261
+ showProviderChoices(handler, idx) {
262
+ return !this.hasProvider(handler) || this.providerPickerOpen === idx;
263
+ },
264
+
265
+ changeProvider(idx) {
266
+ this.providerPickerOpen = this.providerPickerOpen === idx ? null : idx;
267
+ },
268
+
269
providerLabel(value) {
270
return this.presets[value]?.label || "Custom IMAP";
271
},
272
273
+ selectProvider(handler, value, idx = null) {
274
+ this.applyProvider(handler, value);
275
+ this.providerPickerOpen = null;
276
+ this.testResults = null;
277
+ this.testResultsFor = null;
278
+ if (idx !== null) this.editing = idx;
279
+ },
280
+
281
applyProvider(handler, value) {
282
const preset = this.presets[value];
283
if (!preset) return;
284
const previous = this.providerValue(handler);
285
286
handler.account_type = preset.account_type;
287
+ handler.provider = value;
288
289
if (value === "custom-imap") {
290
if (previous !== "custom-imap") {
plugins/_onboarding/webui/onboarding-store.js
+19
-1
@@ -8,6 +8,11 @@ export const store = createStore("onboarding", {
8
step: 1,
9
config: null,
10
loading: true,
11
+ steps: [
12
+ { step: 1, label: "Main Model" },
13
+ { step: 2, label: "Utility Model" },
14
+ { step: 3, label: "Ready" },
15
+ ],
16
17
async init() {
18
this.step = 1;
@@ -62,6 +67,19 @@ export const store = createStore("onboarding", {
67
}
68
},
69
70
+ nextButtonLabel() {
71
+ if (this.step === 1) return "Use Main Model";
72
+ if (this.step === 2) return "Use Utility Model";
73
+ return "Next";
74
+ },
75
+
76
+ providerLabel(modelKey) {
77
+ const provider = this.config?.[modelKey]?.provider || "";
78
+ const providers = modelConfigStore.getProviders(modelKey) || [];
79
+ const match = providers.find((item) => item.value === provider);
80
+ return match?.label || provider || "Provider";
81
+ },
82
+
83
async finish() {
84
this.loading = true;
85
try {
@@ -98,4 +116,4 @@ export const store = createStore("onboarding", {
116
const { store: pluginSettingsStore } = await import("/components/plugins/plugin-settings-store.js");
117
await pluginSettingsStore.openConfig("_model_config");
118
}
101
-});
\ No newline at end of file
119
+});
plugins/_onboarding/webui/onboarding.html
+125
-21
@@ -1,13 +1,22 @@
1
-<html>
1
+<html class="onboarding-modal">
2
<head>
3
<title>Welcome to Agent Zero</title>
4
<script type="module">
5
import { store } from "/plugins/_onboarding/webui/onboarding-store.js";
6
</script>
7
<style>
8
+ .modal-inner.onboarding-modal {
9
+ width: min(92vw, 980px);
10
+ }
11
+ .modal-inner.onboarding-modal .modal-scroll {
12
+ padding: 0;
13
+ }
14
+ .modal-inner.onboarding-modal .modal-bd.onboarding-body {
15
+ padding: 24px 32px 28px;
16
+ }
17
.onboarding-logo {
18
text-align: center;
10
- margin-bottom: 24px;
19
+ margin-bottom: 18px;
20
}
21
.onboarding-logo img {
22
width: 200px;
@@ -16,17 +25,78 @@
25
}
26
.onboarding-welcome-text {
27
text-align: center;
19
- margin: var(--spacing-md) 0 var(--spacing-lg) 0;
28
+ max-width: 78ch;
29
+ margin: var(--spacing-md) auto var(--spacing-lg);
30
color: var(--text-2);
21
- font-size: 1.1rem;
31
+ font-size: 1.02rem;
32
line-height: 1.5;
33
}
34
.onboarding-welcome-title {
35
color: var(--text-1);
26
- font-size: 1.5rem;
27
- font-weight: 600;
36
+ font-size: 1.42rem;
37
+ font-weight: 800;
38
margin-bottom: 12px;
39
}
40
+ .onboarding-progress {
41
+ display: grid;
42
+ grid-template-columns: repeat(3, minmax(0, 1fr));
43
+ gap: 6px;
44
+ margin: 0 0 24px;
45
+ padding: 7px;
46
+ border: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
47
+ border-radius: 8px;
48
+ background: color-mix(in srgb, var(--color-panel) 42%, transparent);
49
+ }
50
+ .onboarding-progress-step {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 8px;
54
+ min-width: 0;
55
+ min-height: 36px;
56
+ padding: 0 10px;
57
+ border: 1px solid transparent;
58
+ border-radius: 7px;
59
+ color: var(--color-text);
60
+ opacity: 0.64;
61
+ font-size: 0.84rem;
62
+ font-weight: 700;
63
+ }
64
+ .onboarding-progress-step.active {
65
+ opacity: 1;
66
+ border-color: color-mix(in srgb, var(--color-primary) 32%, var(--color-border));
67
+ background: color-mix(in srgb, var(--color-background-hover) 58%, transparent);
68
+ }
69
+ .onboarding-progress-step.done {
70
+ opacity: 0.88;
71
+ }
72
+ .onboarding-progress-dot {
73
+ display: grid;
74
+ place-items: center;
75
+ width: 20px;
76
+ height: 20px;
77
+ border-radius: 999px;
78
+ background: color-mix(in srgb, var(--color-border) 70%, transparent);
79
+ color: var(--color-background);
80
+ font-size: 0.72rem;
81
+ font-weight: 900;
82
+ flex: 0 0 auto;
83
+ }
84
+ .onboarding-progress-step.active .onboarding-progress-dot {
85
+ background: var(--color-primary);
86
+ }
87
+ .onboarding-progress-step.done .onboarding-progress-dot {
88
+ background: #63c98b;
89
+ }
90
+ .onboarding-progress-check {
91
+ font-size: 14px;
92
+ line-height: 1;
93
+ }
94
+ .onboarding-progress-label {
95
+ min-width: 0;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ white-space: nowrap;
99
+ }
100
.onboarding-success {
101
text-align: center;
102
padding: var(--spacing-md) 0;
@@ -61,12 +131,16 @@
131
}
132
/* Scoped overrides to make the fields look nice here */
133
.onboarding-body .model-section {
64
- background: var(--surface-2);
134
+ padding: 16px;
135
+ background: color-mix(in srgb, var(--color-panel) 58%, transparent);
136
border-radius: 8px;
66
- border: 1px solid var(--surface-3);
137
+ border: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
138
}
139
.onboarding-body .section-title { margin-bottom: 8px; }
140
.onboarding-body .section-description { margin-bottom: 24px; }
141
+ .onboarding-body .model-section .field:last-child {
142
+ margin-bottom: 0;
143
+ }
144
.onboarding-body .loading-container { height: 200px; }
145
.onboarding-body .input-with-icon { padding-right: 32px; }
146
.onboarding-body .relative-container { position: relative; }
@@ -160,6 +234,17 @@
234
.onboarding-body .model-search-item.disabled:hover {
235
background: transparent;
236
}
237
+ @media (max-width: 720px) {
238
+ .modal-inner.onboarding-modal .modal-bd.onboarding-body {
239
+ padding: 18px 16px 22px;
240
+ }
241
+ .onboarding-progress {
242
+ grid-template-columns: 1fr;
243
+ }
244
+ .onboarding-welcome-text {
245
+ font-size: 0.96rem;
246
+ }
247
+ }
248
</style>
249
</head>
250
@@ -181,22 +266,36 @@
266
267
<template x-if="!$store.onboarding.loading && $store.onboarding.config">
268
<div>
269
+ <nav class="onboarding-progress" aria-label="Onboarding progress">
270
+ <template x-for="item in $store.onboarding.steps" :key="item.step">
271
+ <div class="onboarding-progress-step"
272
+ :class="{ active: $store.onboarding.step === item.step, done: $store.onboarding.step > item.step }"
273
+ :aria-current="$store.onboarding.step === item.step ? 'step' : null">
274
+ <span class="onboarding-progress-dot">
275
+ <span x-show="$store.onboarding.step <= item.step" x-text="item.step"></span>
276
+ <span class="material-symbols-outlined onboarding-progress-check" x-show="$store.onboarding.step > item.step">check</span>
277
+ </span>
278
+ <span class="onboarding-progress-label" x-text="item.label"></span>
279
+ </div>
280
+ </template>
281
+ </nav>
282
283
<!-- Step 1: Main Model -->
284
<div x-show="$store.onboarding.step === 1">
285
<div class="onboarding-welcome-text">
286
<div class="onboarding-welcome-title">Welcome to Agent Zero</div>
189
- Let's get your models configured. The <b>Main Model</b> handles chat, tool calls, skills, and browser automation. We recommend a capable model like Claude Sonnet 4.6, GPT-5.4, Kimi 2.5, or similar.
287
+ First, choose the model Agent Zero will think with. The <b>Main Model</b> handles conversation, tool calls, skills, and browser work.
288
</div>
289
290
<div class="model-section">
291
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[0].title"></div>
194
- <div class="section-description" x-text="$store.modelConfig.MODEL_SECTIONS[0].desc"></div>
292
+ <div class="section-description">Choose a capable general model for conversation, reasoning, tools, skills, and browser automation.</div>
293
294
<!-- Provider -->
295
<div class="field">
296
<div class="field-label">
297
<div class="field-title">Provider</div>
298
+ <div class="field-description">Where Agent Zero will send Main Model requests.</div>
299
</div>
300
<div class="field-control">
301
<select x-model="$store.onboarding.config.chat_model.provider"
@@ -212,7 +311,7 @@
311
<div class="field">
312
<div class="field-label">
313
<div class="field-title">Model name</div>
215
- <div class="field-description">Model identifier. Click the search icon to browse available models.</div>
314
+ <div class="field-description">Use the suggested model or search for another capable general-purpose model.</div>
315
</div>
316
<div class="field-control relative-container"
317
x-data="{ results: [], open: false, searching: false,
@@ -243,7 +342,8 @@
342
<!-- API Key -->
343
<div class="field">
344
<div class="field-label">
246
- <div class="field-title">API key</div>
345
+ <div class="field-title" x-text="`${$store.onboarding.providerLabel('chat_model')} API key`"></div>
346
+ <div class="field-description">Saved keys stay hidden. Paste a new key only when you need to add or replace one.</div>
347
</div>
348
<div class="field-control relative-container" x-data="{ showKey: false }">
349
<input :type="showKey ? 'text' : 'password'"
@@ -269,18 +369,19 @@
369
<!-- Step 2: Utility Model -->
370
<div x-show="$store.onboarding.step === 2">
371
<div class="onboarding-welcome-text">
272
- <div class="onboarding-welcome-title">Almost there!</div>
273
- The <b>Utility Model</b> handles background tasks like summarization and memory updates. A fast, cheap model like GPT-5.4-mini, Gemini 3.1 Flash Lite, or similar works best here.
372
+ <div class="onboarding-welcome-title">Choose your Utility Model</div>
373
+ Agent Zero uses the <b>Utility Model</b> for quiet work: summaries, memory, and preparation. Pick something fast, reliable, and cost-conscious.
374
</div>
375
376
<div class="model-section">
377
<div class="section-title" x-text="$store.modelConfig.MODEL_SECTIONS[1].title"></div>
278
- <div class="section-description" x-text="$store.modelConfig.MODEL_SECTIONS[1].desc"></div>
378
+ <div class="section-description">Choose a fast, reliable model for summaries, memory, and prompt preparation.</div>
379
380
<!-- Provider -->
381
<div class="field">
382
<div class="field-label">
383
<div class="field-title">Provider</div>
384
+ <div class="field-description">Use the same provider as your Main Model, or choose a faster low-cost option.</div>
385
</div>
386
<div class="field-control">
387
<select x-model="$store.onboarding.config.utility_model.provider"
@@ -296,7 +397,7 @@
397
<div class="field">
398
<div class="field-label">
399
<div class="field-title">Model name</div>
299
- <div class="field-description">Model identifier. Click the search icon to browse available models.</div>
400
+ <div class="field-description">The suggested model is tuned for speed and cost. Search if your provider uses another name.</div>
401
</div>
402
<div class="field-control relative-container"
403
x-data="{ results: [], open: false, searching: false,
@@ -327,7 +428,8 @@
428
<!-- API Key -->
429
<div class="field">
430
<div class="field-label">
330
- <div class="field-title">API key</div>
431
+ <div class="field-title" x-text="`${$store.onboarding.providerLabel('utility_model')} API key`"></div>
432
+ <div class="field-description">Saved keys stay hidden. Paste a new key only when you need to add or replace one.</div>
433
</div>
434
<div class="field-control relative-container" x-data="{ showKey: false }">
435
<input :type="showKey ? 'text' : 'password'"
@@ -354,8 +456,9 @@
456
<div x-show="$store.onboarding.step === 3" class="onboarding-success">
457
<div class="material-symbols-outlined onboarding-success-icon">check_circle</div>
458
<div class="onboarding-welcome-text onboarding-success-text">
357
- Your models are configured. You can change these anytime in Settings.<br>
358
- While you're here, why not check out some integrations to extend what Agent Zero can do?
459
+ <div class="onboarding-welcome-title">Agent Zero is ready</div>
460
+ Your models are configured. You can change them anytime in Settings.<br>
461
+ You can also connect integrations now and give Agent Zero more places to work.
462
</div>
463
<x-extension id="onboarding-success-end"></x-extension>
464
</div>
@@ -381,7 +484,8 @@
484
</button>
485
486
<button class="btn btn-ok" x-show="$store.onboarding.step < 3" @click="$store.onboarding.next()" :disabled="$store.onboarding.loading">
384
- Next <span class="material-symbols-outlined onboarding-icon-right">arrow_forward</span>
487
+ <span x-text="$store.onboarding.nextButtonLabel()"></span>
488
+ <span class="material-symbols-outlined onboarding-icon-right">arrow_forward</span>
489
</button>
490
491
<button class="btn btn-ok" x-show="$store.onboarding.step === 3" @click="$store.onboarding.finish()" :disabled="$store.onboarding.loading">
@@ -394,4 +498,4 @@
498
</template>
499
</div>
500
</body>
397
-</html>
\ No newline at end of file
501
+</html>