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>