feat: add built-in plugin discovery cards to the welcome screen

Add the always-enabled `_discovery` plugin to turn the welcome screen into a discovery surface for the Plugin Hub and A0 integrations. Includes a hero card plus Telegram, Email, and WhatsApp feature cards, with persistent dismiss/restore state, CTA routing to plugin config screens, and self-contained placeholder artwork. Implemented entirely through the existing WebUI extension mechanism with no core welcome-screen changes. stores cleanup layout polish and onboarding integration Move feature card titles beside thumbnails for better space efficiency and visibility. Restructure card markup and styles to support a fluid grid layout and horizontal alignment. Integrate discovery cards into the final onboarding step via a new 'onboarding-success-end' extension point, ensuring new users see extension opportunities immediately after setup. Hide discovery cards on the dashboard while the missing API key onboarding banner is visible to reduce UI noise and user confusion during initial config. update discovery card initialization and loading logic Enhance the discovery store to fetch cards from the API, improving the dynamic loading of discovery cards based on user context. This change optimizes the user experience by ensuring relevant cards are displayed immediately after onboarding and when modals are closed. And on top of that, there's a proper backend for these new cards.

Alessandro committed Mar 31, 2026 at 18:28 UTC 0061b3a511cf54d491e6b96670f5b268c7218c1d
13 files changed +1217 -15
plugins/_discovery/extensions/python/banners/10_discovery_cards.py new
+77
@@ -0,0 +1,77 @@
1 +from helpers.extension import Extension
2 +from helpers import plugins
3 +
4 +class DiscoveryCardsExtension(Extension):
5 + """Injects discovery cards into the banners list."""
6 +
7 + async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
8 + # Optional logic: only show specific cards if plugins aren't already configured.
9 + # Telegram, Email, Whatsapp are built-in, so we only need to check if they've been configured.
10 +
11 + telegram_config = plugins.get_plugin_config("_telegram_integration") or {}
12 + email_config = plugins.get_plugin_config("_email_integration") or {}
13 + whatsapp_config = plugins.get_plugin_config("_whatsapp_integration") or {}
14 +
15 + # 1. Plugin Hub Hero
16 + banners.append({
17 + "id": "discovery-plugin-hub",
18 + "type": "hero",
19 + "title": "Discover the Plugin Hub",
20 + "description": "Extend Agent Zero with integrations, tools, and automations from the community.",
21 + "thumbnail": "/plugins/_discovery/webui/assets/hero-plugin-hub.png",
22 + "icon": "extension",
23 + "cta_text": "Explore Plugins",
24 + "cta_action": "open-plugin-hub",
25 + "dismissible": True,
26 + "priority": 100,
27 + "show_in_onboarding": True
28 + })
29 +
30 + # 2. Telegram
31 + if not telegram_config.get("bot_token"):
32 + banners.append({
33 + "id": "discovery-telegram",
34 + "type": "feature",
35 + "title": "Connect Telegram",
36 + "description": "Chat with Agent Zero from Telegram wherever you are.",
37 + "thumbnail": "/plugins/_discovery/webui/assets/thumb-telegram.png",
38 + "icon": "send",
39 + "cta_text": "Setup",
40 + "cta_action": "open-plugin-config:_telegram_integration",
41 + "dismissible": True,
42 + "priority": 50,
43 + "show_in_onboarding": True
44 + })
45 +
46 + # 3. Email
47 + if not email_config.get("imap_username") and not email_config.get("smtp_username"):
48 + banners.append({
49 + "id": "discovery-email",
50 + "type": "feature",
51 + "title": "Setup Email",
52 + "description": "Let Agent Zero read and send emails on your behalf.",
53 + "thumbnail": "/plugins/_discovery/webui/assets/thumb-email.png",
54 + "icon": "mail",
55 + "cta_text": "Setup",
56 + "cta_action": "open-plugin-config:_email_integration",
57 + "dismissible": True,
58 + "priority": 50,
59 + "show_in_onboarding": True
60 + })
61 +
62 + # 4. WhatsApp
63 + if not whatsapp_config.get("phone_number_id"):
64 + banners.append({
65 + "id": "discovery-whatsapp",
66 + "type": "feature",
67 + "title": "Connect WhatsApp",
68 + "description": "Send and receive WhatsApp messages through A0.",
69 + "thumbnail": "/plugins/_discovery/webui/assets/thumb-whatsapp.png",
70 + "icon": "chat",
71 + "cta_text": "Setup",
72 + "cta_action": "open-plugin-config:_whatsapp_integration",
73 + "dismissible": True,
74 + "priority": 50,
75 + "show_in_onboarding": True
76 + })
77 +
plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html new
+483
@@ -0,0 +1,483 @@
1 +<script type="module">
2 + import { store } from "/plugins/_discovery/webui/discovery-store.js";
3 +</script>
4 +
5 +<div x-data>
6 + <template x-if="$store.discoveryStore">
7 + <div class="discovery-slot"
8 + @modal-closed.window="$store.discoveryStore.refreshCards()"
9 + x-create="$store.discoveryStore.refreshCards()"
10 + x-show="$store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards">
11 +
12 + <div class="discovery-section" style="margin-top: 2rem;">
13 +
14 + <div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0" style="text-align: left;">
15 + <template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
16 + <article class="discovery-feature-card"
17 + role="button"
18 + tabindex="0"
19 + @click="$store.discoveryStore.executeCta(card.cta_action)"
20 + @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
21 + @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
22 + <button class="discovery-dismiss discovery-dismiss-small"
23 + type="button"
24 + x-show="card.dismissible"
25 + @click.stop="$store.discoveryStore.dismissCard(card.id)"
26 + :aria-label="`Dismiss ${card.title}`"
27 + title="Dismiss">
28 + <span class="material-symbols-outlined">close</span>
29 + </button>
30 +
31 + <div class="discovery-feature-head">
32 + <div class="discovery-feature-thumb">
33 + <template x-if="card.thumbnail">
34 + <img :src="card.thumbnail" :alt="card.title" loading="lazy">
35 + </template>
36 + <template x-if="!card.thumbnail && card.icon">
37 + <span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
38 + </template>
39 + </div>
40 + <h4 class="discovery-feature-title" x-text="card.title"></h4>
41 + </div>
42 +
43 + <p class="discovery-feature-desc" x-text="card.description"></p>
44 +
45 + <button class="btn btn primary"
46 + type="button"
47 + @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
48 + <span x-text="card.cta_text"></span>
49 + <span class="material-symbols-outlined">arrow_forward</span>
50 + </button>
51 + </article>
52 + </template>
53 + </div>
54 +
55 +
56 + <template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
57 + <article class="discovery-hero"
58 + role="button"
59 + tabindex="0"
60 + @click="$store.discoveryStore.executeCta(card.cta_action)"
61 + @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
62 + @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
63 + <button class="discovery-dismiss"
64 + type="button"
65 + x-show="card.dismissible"
66 + @click.stop="$store.discoveryStore.dismissCard(card.id)"
67 + :aria-label="`Dismiss ${card.title}`"
68 + title="Dismiss">
69 + <span class="material-symbols-outlined">close</span>
70 + </button>
71 +
72 + <div class="discovery-hero-content">
73 + <h3 class="discovery-hero-title" x-text="card.title"></h3>
74 + <p class="discovery-hero-desc" x-text="card.description"></p>
75 + <button class="btn btn-ok"
76 + type="button"
77 + @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
78 + <span x-text="card.cta_text"></span>
79 + <span class="material-symbols-outlined">arrow_forward</span>
80 + </button>
81 + </div>
82 +
83 + <div class="discovery-hero-thumb">
84 + <template x-if="card.thumbnail">
85 + <img :src="card.thumbnail" :alt="card.title" loading="lazy">
86 + </template>
87 + <template x-if="!card.thumbnail && card.icon">
88 + <span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
89 + </template>
90 + </div>
91 + </article>
92 + </template>
93 +
94 + <div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
95 + <button type="button"
96 + class="discovery-undismiss-btn"
97 + @click="$store.discoveryStore.undismissCards()">
98 + <span class="material-symbols-outlined">undo</span>
99 + <span>Show dismissed suggestions</span>
100 + </button>
101 + </div>
102 + </div>
103 + </div>
104 + </template>
105 +
106 + <style>
107 + .discovery-slot {
108 + grid-column: 1 / -1;
109 + width: 100%;
110 + }
111 +
112 + .discovery-section {
113 + display: flex;
114 + flex-direction: column;
115 + gap: 1rem;
116 + width: 100%;
117 + margin-top: 0.1rem;
118 + }
119 +
120 + .discovery-hero,
121 + .discovery-feature-card {
122 + position: relative;
123 + border: 1px solid var(--color-border);
124 + border-radius: 12px;
125 + background: var(--color-panel);
126 + text-align: left;
127 + transition:
128 + border-color 0.2s ease,
129 + background-color 0.2s ease,
130 + box-shadow 0.2s ease,
131 + transform 0.2s ease;
132 + }
133 +
134 + .discovery-hero:hover,
135 + .discovery-feature-card:hover,
136 + .discovery-hero:focus-visible,
137 + .discovery-feature-card:focus-visible {
138 + border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
139 + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
140 + outline: none;
141 + }
142 +
143 + .discovery-hero {
144 + display: flex;
145 + align-items: center;
146 + gap: 1rem;
147 + min-height: 152px;
148 + overflow: hidden;
149 + padding: 1.25rem 1.25rem 1.25rem 1.5rem;
150 + background:
151 + linear-gradient(
152 + 135deg,
153 + color-mix(in srgb, var(--color-primary) 10%, transparent),
154 + transparent 58%
155 + ),
156 + radial-gradient(
157 + circle at 92% 24%,
158 + color-mix(in srgb, var(--color-primary) 12%, transparent),
159 + transparent 42%
160 + ),
161 + var(--color-panel);
162 + }
163 +
164 + .discovery-hero-content,
165 + .discovery-hero-thumb {
166 + position: relative;
167 + z-index: 1;
168 + }
169 +
170 + .discovery-hero-content {
171 + flex: 1 1 auto;
172 + min-width: 0;
173 + }
174 +
175 + .discovery-hero-title {
176 + margin: 0 0 0.4rem;
177 + color: var(--color-text);
178 + font-size: 1.08rem;
179 + font-weight: 600;
180 + letter-spacing: 0.01em;
181 + }
182 +
183 + .discovery-hero-desc {
184 + margin: 0 0 1rem;
185 + color: var(--color-text);
186 + font-size: 0.86rem;
187 + line-height: 1.5;
188 + max-width: 38ch;
189 + }
190 +
191 + .discovery-cta-link,
192 + .discovery-undismiss-btn {
193 + display: inline-flex;
194 + align-items: center;
195 + justify-content: center;
196 + gap: 0.35rem;
197 + cursor: pointer;
198 + transition:
199 + background-color 0.2s ease,
200 + border-color 0.2s ease,
201 + color 0.2s ease,
202 + transform 0.2s ease;
203 + }
204 +
205 + .discovery-hero-thumb {
206 + flex: 0 0 132px;
207 + width: 132px;
208 + height: 108px;
209 + display: flex;
210 + align-items: center;
211 + justify-content: center;
212 + }
213 +
214 + .discovery-hero-thumb img {
215 + width: 100%;
216 + height: 100%;
217 + object-fit: contain;
218 + filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
219 + }
220 +
221 + .discovery-hero-icon {
222 + font-size: 3rem;
223 + color: var(--color-highlight-dark);
224 + }
225 +
226 + .discovery-features {
227 + display: grid;
228 + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
229 + gap: 1rem;
230 + }
231 +
232 + .discovery-feature-card {
233 + display: flex;
234 + flex-direction: column;
235 + align-items: flex-start;
236 + min-height: 188px;
237 + padding: 1rem;
238 + background:
239 + linear-gradient(
240 + 180deg,
241 + color-mix(in srgb, var(--color-primary) 4%, transparent),
242 + transparent 38%
243 + ),
244 + var(--color-panel);
245 + }
246 +
247 + .discovery-feature-head {
248 + display: flex;
249 + flex-direction: row;
250 + align-items: center;
251 + gap: 0.75rem;
252 + width: 100%;
253 + min-width: 0;
254 + }
255 +
256 + .discovery-feature-thumb {
257 + flex: 0 0 auto;
258 + width: 52px;
259 + height: 52px;
260 + display: flex;
261 + align-items: center;
262 + justify-content: center;
263 + border-radius: 14px;
264 + overflow: hidden;
265 + background: color-mix(in srgb, var(--color-primary) 10%, transparent);
266 + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
267 + }
268 +
269 + .discovery-feature-thumb img {
270 + width: 100%;
271 + height: 100%;
272 + object-fit: cover;
273 + }
274 +
275 + .discovery-feature-icon {
276 + font-size: 1.45rem;
277 + color: var(--color-highlight-dark);
278 + }
279 +
280 + .discovery-feature-title {
281 + margin: 0;
282 + flex: 1 1 auto;
283 + min-width: 0;
284 + color: var(--color-text);
285 + font-size: 0.92rem;
286 + font-weight: 600;
287 + line-height: 1.25;
288 + }
289 +
290 + .discovery-feature-desc {
291 + margin: 0.55rem 0 0;
292 + padding-bottom: var(--spacing-md);
293 + color: var(--color-text);
294 + font-size: 0.8rem;
295 + line-height: 1.45;
296 + }
297 +
298 + .discovery-feature-card > .btn {
299 + margin-top: auto;
300 + }
301 +
302 + .discovery-cta-link {
303 + margin-top: 0.95rem;
304 + border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
305 + border-radius: 8px;
306 + background: transparent;
307 + color: var(--color-primary);
308 + padding: 0.42rem 0.72rem;
309 + font-size: 0.79rem;
310 + font-weight: 600;
311 + }
312 +
313 + .discovery-cta-link:hover,
314 + .discovery-cta-link:focus-visible {
315 + background: var(--color-primary);
316 + border-color: var(--color-primary);
317 + color: #fff;
318 + outline: none;
319 + }
320 +
321 + .discovery-dismiss {
322 + position: absolute;
323 + top: 0.65rem;
324 + right: 0.65rem;
325 + display: flex;
326 + align-items: center;
327 + justify-content: center;
328 + width: 28px;
329 + height: 28px;
330 + border: none;
331 + border-radius: 8px;
332 + background: transparent;
333 + color: var(--color-secondary);
334 + cursor: pointer;
335 + opacity: 0;
336 + transition:
337 + opacity 0.2s ease,
338 + background-color 0.2s ease,
339 + color 0.2s ease;
340 + z-index: 2;
341 + }
342 +
343 + .discovery-dismiss-small {
344 + top: 0.55rem;
345 + right: 0.55rem;
346 + width: 26px;
347 + height: 26px;
348 + }
349 +
350 + .discovery-hero:hover .discovery-dismiss,
351 + .discovery-hero:focus-visible .discovery-dismiss,
352 + .discovery-feature-card:hover .discovery-dismiss,
353 + .discovery-feature-card:focus-visible .discovery-dismiss {
354 + opacity: 0.68;
355 + }
356 +
357 + .discovery-dismiss:hover,
358 + .discovery-dismiss:focus-visible {
359 + opacity: 1;
360 + color: var(--color-text);
361 + background: color-mix(in srgb, var(--color-border) 84%, transparent);
362 + outline: none;
363 + }
364 +
365 + .discovery-dismiss .material-symbols-outlined {
366 + font-size: 1rem;
367 + }
368 +
369 + .discovery-undismiss {
370 + display: flex;
371 + justify-content: flex-start;
372 + }
373 +
374 + .discovery-undismiss-btn {
375 + border: none;
376 + background: transparent;
377 + padding: 0;
378 + color: var(--color-secondary);
379 + font-size: 0.8rem;
380 + font-weight: 500;
381 + }
382 +
383 + .discovery-undismiss-btn:hover,
384 + .discovery-undismiss-btn:focus-visible {
385 + color: var(--color-primary);
386 + outline: none;
387 + }
388 +
389 + .discovery-cta-link .material-symbols-outlined,
390 + .discovery-undismiss-btn .material-symbols-outlined {
391 + font-size: 1rem;
392 + }
393 +
394 + @media (hover: none) {
395 + .discovery-dismiss {
396 + opacity: 0.68;
397 + }
398 + }
399 +
400 + @media (max-width: 768px) {
401 + .discovery-section {
402 + gap: 0.85rem;
403 + }
404 +
405 + .discovery-hero {
406 + min-height: 0;
407 + padding: 1rem;
408 + }
409 +
410 + .discovery-hero-thumb {
411 + flex-basis: 92px;
412 + width: 92px;
413 + height: 76px;
414 + }
415 +
416 + .discovery-features {
417 + grid-template-columns: 1fr;
418 + gap: 0.75rem;
419 + }
420 +
421 + .discovery-feature-card {
422 + display: grid;
423 + grid-template-columns: minmax(0, 1fr) auto;
424 + gap: 0.75rem;
425 + min-height: 0;
426 + align-items: center;
427 + padding: 0.85rem 1rem;
428 + }
429 +
430 + .discovery-feature-head {
431 + grid-column: 1;
432 + min-width: 0;
433 + }
434 +
435 + .discovery-feature-thumb {
436 + width: 46px;
437 + height: 46px;
438 + }
439 +
440 + .discovery-feature-desc {
441 + display: none;
442 + }
443 +
444 + .discovery-feature-card > .btn {
445 + grid-column: 2;
446 + grid-row: 1;
447 + align-self: center;
448 + margin-top: 0;
449 + }
450 +
451 + .discovery-cta-link {
452 + margin-top: 0;
453 + }
454 + }
455 +
456 + @media (max-width: 520px) {
457 + .discovery-slot {
458 + margin-top: 0.15rem;
459 + }
460 +
461 + .discovery-hero {
462 + padding-right: 0.95rem;
463 + }
464 +
465 + .discovery-hero-thumb {
466 + display: none;
467 + }
468 +
469 + .discovery-hero-title {
470 + font-size: 1rem;
471 + }
472 +
473 + .discovery-feature-card {
474 + grid-template-columns: minmax(0, 1fr) auto;
475 + padding-right: 3rem;
476 + }
477 +
478 + .discovery-feature-card > .btn {
479 + justify-self: end;
480 + }
481 + }
482 + </style>
483 +</div>
plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html new
+481
@@ -0,0 +1,481 @@
1 +<script type="module">
2 + import { store } from "/plugins/_discovery/webui/discovery-store.js";
3 +</script>
4 +
5 +<div x-data>
6 + <template x-if="$store.discoveryStore">
7 + <div class="discovery-slot"
8 + @modal-closed.window="$store.discoveryStore.refreshCards()"
9 + x-create="$store.discoveryStore.refreshCards()"
10 + x-show="!($store.welcomeStore?.banners || []).some(b => b.id === 'missing-api-key') && ($store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards)">
11 +
12 + <div class="discovery-section">
13 + <template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
14 + <article class="discovery-hero"
15 + role="button"
16 + tabindex="0"
17 + @click="$store.discoveryStore.executeCta(card.cta_action)"
18 + @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
19 + @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
20 + <button class="discovery-dismiss"
21 + type="button"
22 + x-show="card.dismissible"
23 + @click.stop="$store.discoveryStore.dismissCard(card.id)"
24 + :aria-label="`Dismiss ${card.title}`"
25 + title="Dismiss">
26 + <span class="material-symbols-outlined">close</span>
27 + </button>
28 +
29 + <div class="discovery-hero-content">
30 + <h3 class="discovery-hero-title" x-text="card.title"></h3>
31 + <p class="discovery-hero-desc" x-text="card.description"></p>
32 + <button class="btn btn-ok"
33 + type="button"
34 + @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
35 + <span x-text="card.cta_text"></span>
36 + <span class="material-symbols-outlined">arrow_forward</span>
37 + </button>
38 + </div>
39 +
40 + <div class="discovery-hero-thumb">
41 + <template x-if="card.thumbnail">
42 + <img :src="card.thumbnail" :alt="card.title" loading="lazy">
43 + </template>
44 + <template x-if="!card.thumbnail && card.icon">
45 + <span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
46 + </template>
47 + </div>
48 + </article>
49 + </template>
50 +
51 + <div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0">
52 + <template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
53 + <article class="discovery-feature-card"
54 + role="button"
55 + tabindex="0"
56 + @click="$store.discoveryStore.executeCta(card.cta_action)"
57 + @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
58 + @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
59 + <button class="discovery-dismiss discovery-dismiss-small"
60 + type="button"
61 + x-show="card.dismissible"
62 + @click.stop="$store.discoveryStore.dismissCard(card.id)"
63 + :aria-label="`Dismiss ${card.title}`"
64 + title="Dismiss">
65 + <span class="material-symbols-outlined">close</span>
66 + </button>
67 +
68 + <div class="discovery-feature-head">
69 + <div class="discovery-feature-thumb">
70 + <template x-if="card.thumbnail">
71 + <img :src="card.thumbnail" :alt="card.title" loading="lazy">
72 + </template>
73 + <template x-if="!card.thumbnail && card.icon">
74 + <span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
75 + </template>
76 + </div>
77 + <h4 class="discovery-feature-title" x-text="card.title"></h4>
78 + </div>
79 +
80 + <p class="discovery-feature-desc" x-text="card.description"></p>
81 +
82 + <button class="btn btn primary"
83 + type="button"
84 + @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
85 + <span x-text="card.cta_text"></span>
86 + <span class="material-symbols-outlined">arrow_forward</span>
87 + </button>
88 + </article>
89 + </template>
90 + </div>
91 +
92 + <div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
93 + <button type="button"
94 + class="discovery-undismiss-btn"
95 + @click="$store.discoveryStore.undismissCards()">
96 + <span class="material-symbols-outlined">undo</span>
97 + <span>Show dismissed suggestions</span>
98 + </button>
99 + </div>
100 + </div>
101 + </div>
102 + </template>
103 +
104 + <style>
105 + .discovery-slot {
106 + grid-column: 1 / -1;
107 + width: 100%;
108 + }
109 +
110 + .discovery-section {
111 + display: flex;
112 + flex-direction: column;
113 + gap: 1rem;
114 + width: 100%;
115 + margin-top: 0.1rem;
116 + }
117 +
118 + .discovery-hero,
119 + .discovery-feature-card {
120 + position: relative;
121 + border: 1px solid var(--color-border);
122 + border-radius: 12px;
123 + background: var(--color-panel);
124 + text-align: left;
125 + transition:
126 + border-color 0.2s ease,
127 + background-color 0.2s ease,
128 + box-shadow 0.2s ease,
129 + transform 0.2s ease;
130 + }
131 +
132 + .discovery-hero:hover,
133 + .discovery-feature-card:hover,
134 + .discovery-hero:focus-visible,
135 + .discovery-feature-card:focus-visible {
136 + border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
137 + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
138 + outline: none;
139 + }
140 +
141 + .discovery-hero {
142 + display: flex;
143 + align-items: center;
144 + gap: 1rem;
145 + min-height: 152px;
146 + overflow: hidden;
147 + padding: 1.25rem 1.25rem 1.25rem 1.5rem;
148 + background:
149 + linear-gradient(
150 + 135deg,
151 + color-mix(in srgb, var(--color-primary) 10%, transparent),
152 + transparent 58%
153 + ),
154 + radial-gradient(
155 + circle at 92% 24%,
156 + color-mix(in srgb, var(--color-primary) 12%, transparent),
157 + transparent 42%
158 + ),
159 + var(--color-panel);
160 + }
161 +
162 + .discovery-hero-content,
163 + .discovery-hero-thumb {
164 + position: relative;
165 + z-index: 1;
166 + }
167 +
168 + .discovery-hero-content {
169 + flex: 1 1 auto;
170 + min-width: 0;
171 + }
172 +
173 + .discovery-hero-title {
174 + margin: 0 0 0.4rem;
175 + color: var(--color-text);
176 + font-size: 1.08rem;
177 + font-weight: 600;
178 + letter-spacing: 0.01em;
179 + }
180 +
181 + .discovery-hero-desc {
182 + margin: 0 0 1rem;
183 + color: var(--color-text);
184 + font-size: 0.86rem;
185 + line-height: 1.5;
186 + max-width: 38ch;
187 + }
188 +
189 + .discovery-cta-link,
190 + .discovery-undismiss-btn {
191 + display: inline-flex;
192 + align-items: center;
193 + justify-content: center;
194 + gap: 0.35rem;
195 + cursor: pointer;
196 + transition:
197 + background-color 0.2s ease,
198 + border-color 0.2s ease,
199 + color 0.2s ease,
200 + transform 0.2s ease;
201 + }
202 +
203 + .discovery-hero-thumb {
204 + flex: 0 0 132px;
205 + width: 132px;
206 + height: 108px;
207 + display: flex;
208 + align-items: center;
209 + justify-content: center;
210 + }
211 +
212 + .discovery-hero-thumb img {
213 + width: 100%;
214 + height: 100%;
215 + object-fit: contain;
216 + filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
217 + }
218 +
219 + .discovery-hero-icon {
220 + font-size: 3rem;
221 + color: var(--color-highlight-dark);
222 + }
223 +
224 + .discovery-features {
225 + display: grid;
226 + grid-template-columns: repeat(3, minmax(0, 1fr));
227 + gap: 1rem;
228 + }
229 +
230 + .discovery-feature-card {
231 + display: flex;
232 + flex-direction: column;
233 + align-items: flex-start;
234 + min-height: 188px;
235 + padding: 1rem;
236 + background:
237 + linear-gradient(
238 + 180deg,
239 + color-mix(in srgb, var(--color-primary) 4%, transparent),
240 + transparent 38%
241 + ),
242 + var(--color-panel);
243 + }
244 +
245 + .discovery-feature-head {
246 + display: flex;
247 + flex-direction: row;
248 + align-items: center;
249 + gap: 0.75rem;
250 + width: 100%;
251 + min-width: 0;
252 + }
253 +
254 + .discovery-feature-thumb {
255 + flex: 0 0 auto;
256 + width: 52px;
257 + height: 52px;
258 + display: flex;
259 + align-items: center;
260 + justify-content: center;
261 + border-radius: 14px;
262 + overflow: hidden;
263 + background: color-mix(in srgb, var(--color-primary) 10%, transparent);
264 + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
265 + }
266 +
267 + .discovery-feature-thumb img {
268 + width: 100%;
269 + height: 100%;
270 + object-fit: cover;
271 + }
272 +
273 + .discovery-feature-icon {
274 + font-size: 1.45rem;
275 + color: var(--color-highlight-dark);
276 + }
277 +
278 + .discovery-feature-title {
279 + margin: 0;
280 + flex: 1 1 auto;
281 + min-width: 0;
282 + color: var(--color-text);
283 + font-size: 0.92rem;
284 + font-weight: 600;
285 + line-height: 1.25;
286 + }
287 +
288 + .discovery-feature-desc {
289 + margin: 0.55rem 0 0;
290 + padding-bottom: var(--spacing-md);
291 + color: var(--color-text);
292 + font-size: 0.8rem;
293 + line-height: 1.45;
294 + }
295 +
296 + .discovery-feature-card > .btn {
297 + margin-top: auto;
298 + }
299 +
300 + .discovery-cta-link {
301 + margin-top: 0.95rem;
302 + border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
303 + border-radius: 8px;
304 + background: transparent;
305 + color: var(--color-primary);
306 + padding: 0.42rem 0.72rem;
307 + font-size: 0.79rem;
308 + font-weight: 600;
309 + }
310 +
311 + .discovery-cta-link:hover,
312 + .discovery-cta-link:focus-visible {
313 + background: var(--color-primary);
314 + border-color: var(--color-primary);
315 + color: #fff;
316 + outline: none;
317 + }
318 +
319 + .discovery-dismiss {
320 + position: absolute;
321 + top: 0.65rem;
322 + right: 0.65rem;
323 + display: flex;
324 + align-items: center;
325 + justify-content: center;
326 + width: 28px;
327 + height: 28px;
328 + border: none;
329 + border-radius: 8px;
330 + background: transparent;
331 + color: var(--color-secondary);
332 + cursor: pointer;
333 + opacity: 0;
334 + transition:
335 + opacity 0.2s ease,
336 + background-color 0.2s ease,
337 + color 0.2s ease;
338 + z-index: 2;
339 + }
340 +
341 + .discovery-dismiss-small {
342 + top: 0.55rem;
343 + right: 0.55rem;
344 + width: 26px;
345 + height: 26px;
346 + }
347 +
348 + .discovery-hero:hover .discovery-dismiss,
349 + .discovery-hero:focus-visible .discovery-dismiss,
350 + .discovery-feature-card:hover .discovery-dismiss,
351 + .discovery-feature-card:focus-visible .discovery-dismiss {
352 + opacity: 0.68;
353 + }
354 +
355 + .discovery-dismiss:hover,
356 + .discovery-dismiss:focus-visible {
357 + opacity: 1;
358 + color: var(--color-text);
359 + background: color-mix(in srgb, var(--color-border) 84%, transparent);
360 + outline: none;
361 + }
362 +
363 + .discovery-dismiss .material-symbols-outlined {
364 + font-size: 1rem;
365 + }
366 +
367 + .discovery-undismiss {
368 + display: flex;
369 + justify-content: flex-start;
370 + }
371 +
372 + .discovery-undismiss-btn {
373 + border: none;
374 + background: transparent;
375 + padding: 0;
376 + color: var(--color-secondary);
377 + font-size: 0.8rem;
378 + font-weight: 500;
379 + }
380 +
381 + .discovery-undismiss-btn:hover,
382 + .discovery-undismiss-btn:focus-visible {
383 + color: var(--color-primary);
384 + outline: none;
385 + }
386 +
387 + .discovery-cta-link .material-symbols-outlined,
388 + .discovery-undismiss-btn .material-symbols-outlined {
389 + font-size: 1rem;
390 + }
391 +
392 + @media (hover: none) {
393 + .discovery-dismiss {
394 + opacity: 0.68;
395 + }
396 + }
397 +
398 + @media (max-width: 768px) {
399 + .discovery-section {
400 + gap: 0.85rem;
401 + }
402 +
403 + .discovery-hero {
404 + min-height: 0;
405 + padding: 1rem;
406 + }
407 +
408 + .discovery-hero-thumb {
409 + flex-basis: 92px;
410 + width: 92px;
411 + height: 76px;
412 + }
413 +
414 + .discovery-features {
415 + grid-template-columns: 1fr;
416 + gap: 0.75rem;
417 + }
418 +
419 + .discovery-feature-card {
420 + display: grid;
421 + grid-template-columns: minmax(0, 1fr) auto;
422 + gap: 0.75rem;
423 + min-height: 0;
424 + align-items: center;
425 + padding: 0.85rem 1rem;
426 + }
427 +
428 + .discovery-feature-head {
429 + grid-column: 1;
430 + min-width: 0;
431 + }
432 +
433 + .discovery-feature-thumb {
434 + width: 46px;
435 + height: 46px;
436 + }
437 +
438 + .discovery-feature-desc {
439 + display: none;
440 + }
441 +
442 + .discovery-feature-card > .btn {
443 + grid-column: 2;
444 + grid-row: 1;
445 + align-self: center;
446 + margin-top: 0;
447 + }
448 +
449 + .discovery-cta-link {
450 + margin-top: 0;
451 + }
452 + }
453 +
454 + @media (max-width: 520px) {
455 + .discovery-slot {
456 + margin-top: 0.15rem;
457 + }
458 +
459 + .discovery-hero {
460 + padding-right: 0.95rem;
461 + }
462 +
463 + .discovery-hero-thumb {
464 + display: none;
465 + }
466 +
467 + .discovery-hero-title {
468 + font-size: 1rem;
469 + }
470 +
471 + .discovery-feature-card {
472 + grid-template-columns: minmax(0, 1fr) auto;
473 + padding-right: 3rem;
474 + }
475 +
476 + .discovery-feature-card > .btn {
477 + justify-self: end;
478 + }
479 + }
480 + </style>
481 +</div>
plugins/_discovery/plugin.yaml new
+8
@@ -0,0 +1,8 @@
1 +name: _discovery
2 +title: Plugin Discovery
3 +description: Contextual discovery cards on the welcome screen promoting plugins and integrations.
4 +version: 1.0.0
5 +settings_sections: []
6 +always_enabled: true
7 +per_project_config: false
8 +per_agent_config: false
plugins/_discovery/webui/assets/hero-plugin-hub.png
Binary files /dev/null and b/plugins/_discovery/webui/assets/hero-plugin-hub.png differ
plugins/_discovery/webui/assets/thumb-email.png
Binary files /dev/null and b/plugins/_discovery/webui/assets/thumb-email.png differ
plugins/_discovery/webui/assets/thumb-telegram.png
Binary files /dev/null and b/plugins/_discovery/webui/assets/thumb-telegram.png differ
plugins/_discovery/webui/assets/thumb-whatsapp.png
Binary files /dev/null and b/plugins/_discovery/webui/assets/thumb-whatsapp.png differ
plugins/_discovery/webui/discovery-store.js new
+129
@@ -0,0 +1,129 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import { toastFrontendError } from "/components/notifications/notification-store.js";
3 +import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
4 +import * as API from "/js/api.js";
5 +
6 +const STORAGE_KEY = "dismissed_discovery_cards";
7 +
8 +const model = {
9 + // --- State ---
10 + /** @type {any[]} */
11 + cards: [],
12 + hasDismissedCards: false,
13 + _initialized: false,
14 + _isLoading: false,
15 +
16 + // --- Lifecycle ---
17 + init() {
18 + if (this._initialized) return;
19 + this._initialized = true;
20 + },
21 +
22 + // --- Actions ---
23 +
24 + async refreshCards() {
25 + if (this._isLoading) return;
26 + this._isLoading = true;
27 +
28 + try {
29 + const response = await API.callJsonApi("/banners", {
30 + banners: [],
31 + context: {
32 + is_onboarding: document.body.dataset.mode === "onboarding"
33 + },
34 + });
35 +
36 + const banners = response?.banners || [];
37 + const dismissed = this._getDismissedIds();
38 +
39 + // Filter out standard banners, keep only hero and feature
40 + // Also respect the onboarding filtering
41 + const is_onboarding = document.body.dataset.mode === "onboarding";
42 +
43 + this.cards = banners
44 + .filter((card) => card.type === "hero" || card.type === "feature")
45 + .filter((card) => !is_onboarding || card.show_in_onboarding === true)
46 + .filter((card) => !dismissed.has(card.id))
47 + .sort((left, right) => (right.priority || 0) - (left.priority || 0));
48 +
49 + this.hasDismissedCards = dismissed.size > 0;
50 + } catch (error) {
51 + console.error("Failed to fetch discovery cards:", error);
52 + } finally {
53 + this._isLoading = false;
54 + }
55 + },
56 +
57 + dismissCard(cardId) {
58 + const dismissed = this._getDismissedIds();
59 + dismissed.add(cardId);
60 + this._persistDismissedIds(dismissed);
61 +
62 + // Optimistically update UI
63 + this.cards = this.cards.filter(c => c.id !== cardId);
64 + this.hasDismissedCards = true;
65 + },
66 +
67 + undismissCards() {
68 + localStorage.removeItem(STORAGE_KEY);
69 + this.refreshCards();
70 + },
71 +
72 + async executeCta(action) {
73 + if (!action) return;
74 +
75 + try {
76 + if (action === "open-plugin-hub") {
77 + await pluginListStore.open("pluginHub");
78 + return;
79 + }
80 +
81 + if (action.startsWith("open-plugin-config:")) {
82 + const pluginName = action.split(":")[1];
83 + await pluginListStore.openPluginConfig(pluginName);
84 + return;
85 + }
86 +
87 + if (action.startsWith("open-url:")) {
88 + const url = action.slice("open-url:".length);
89 + if (url) {
90 + window.open(url, "_blank", "noopener,noreferrer");
91 + }
92 + return;
93 + }
94 + } catch (error) {
95 + console.error("Discovery action failed:", error);
96 + const message = error instanceof Error ? error.message : String(error);
97 + await toastFrontendError(message, "Discovery");
98 + }
99 + },
100 +
101 + // --- Helpers (Private-ish) ---
102 +
103 + _getDismissedIds() {
104 + try {
105 + const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
106 + if (!Array.isArray(raw)) return new Set();
107 + return new Set(raw);
108 + } catch {
109 + return new Set();
110 + }
111 + },
112 +
113 + _persistDismissedIds(ids) {
114 + localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(ids)));
115 + },
116 +
117 + // --- Computed ---
118 +
119 + get heroCards() {
120 + return this.cards.filter((card) => card.type === "hero");
121 + },
122 +
123 + get featureCards() {
124 + return this.cards.filter((card) => card.type === "feature");
125 + },
126 +};
127 +
128 +const store = createStore("discoveryStore", model);
129 +export { store };
plugins/_onboarding/webui/onboarding.html
+7 -6
@@ -16,7 +16,7 @@
16 }
17 .onboarding-welcome-text {
18 text-align: center;
19 - margin: var(--spacing-md) 0;
19 + margin: var(--spacing-md) 0 var(--spacing-lg) 0;
20 color: var(--text-2);
21 font-size: 1.1rem;
22 line-height: 1.5;
@@ -29,7 +29,7 @@
29 }
30 .onboarding-success {
31 text-align: center;
32 - padding: 40px 0;
32 + padding: var(--spacing-md) 0;
33 }
34 .onboarding-success-icon {
35 font-size: 64px;
@@ -186,7 +186,7 @@
186 <div x-show="$store.onboarding.step === 1">
187 <div class="onboarding-welcome-text">
188 <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.<br> We recommend a capable model like Claude Sonnet 4.6, GPT-5.4, Kimi 2.5, or similar.
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.
190 </div>
191
192 <div class="model-section">
@@ -270,7 +270,7 @@
270 <div x-show="$store.onboarding.step === 2">
271 <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.<br> A fast, cheap model like GPT-5.4-mini, Gemini 3.1 Flash Lite, or similar works best here.
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.
274 </div>
275
276 <div class="model-section">
@@ -353,10 +353,11 @@
353 <!-- Step 3: Success -->
354 <div x-show="$store.onboarding.step === 3" class="onboarding-success">
355 <div class="material-symbols-outlined onboarding-success-icon">check_circle</div>
356 - <div class="onboarding-welcome-title">Ready to chat!</div>
356 <div class="onboarding-welcome-text onboarding-success-text">
358 - Your models are configured. You can change these anytime in Settings.
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?
359 </div>
360 + <x-extension id="onboarding-success-end"></x-extension>
361 </div>
362
363 <div class="onboarding-advanced-link" x-show="$store.onboarding.step < 3">
webui/components/plugins/list/pluginListStore.js
+27 -6
@@ -12,6 +12,8 @@ import {
12 defaultPriority,
13 } from "/components/notifications/notification-store.js";
14
15 +const MODAL_PATH = "components/plugins/list/plugin-list.html";
16 +
17 // define the model object holding data and functions
18 const model = {
19 loading: false,
@@ -22,11 +24,22 @@ const model = {
24 readmeLoading: false,
25 readmeError: "",
26
27 + async open(tab = "custom") {
28 + await this.setTab(tab);
29 + window.openModal?.(MODAL_PATH);
30 + },
31 +
32 async init() {
33 this.loading = false;
27 - await this.setTab('custom');
28 - if (this.plugins.length === 0) {
29 - await this.setTab('builtin');
34 + // If a tab is already selected (e.g. via open()), use it.
35 + // Otherwise default to custom -> builtin fallback.
36 + if (this.activeTab && this.activeTab !== "custom") {
37 + await this.setTab(this.activeTab);
38 + } else {
39 + await this.setTab("custom");
40 + if (this.plugins.length === 0) {
41 + await this.setTab("builtin");
42 + }
43 }
44 },
45
@@ -81,13 +94,21 @@ const model = {
94 pluginExecuteStore.open(plugin);
95 },
96
84 - async openPluginConfig(plugin) {
85 - if (!plugin?.name || !plugin?.has_config_screen) return;
97 + async openPluginConfig(pluginOrName) {
98 + const pluginName =
99 + typeof pluginOrName === "string" ? pluginOrName : pluginOrName?.name;
100 + if (!pluginName) return;
101 +
102 + // If it's an object, we can check has_config_screen.
103 + // If it's a name, we just try to open it and let pluginSettingsStore handle errors.
104 + if (typeof pluginOrName === "object" && !pluginOrName.has_config_screen)
105 + return;
106 +
107 try {
108 if (!pluginSettingsStore?.openConfig) {
109 throw new Error("Plugin settings store is unavailable.");
110 }
90 - await pluginSettingsStore.openConfig(plugin.name);
111 + await pluginSettingsStore.openConfig(pluginName);
112 } catch (e) {
113 showErrorNotification(e, "Failed to open plugin config");
114 }
webui/components/welcome/welcome-store.js
+3 -3
@@ -141,9 +141,9 @@ const model = {
141 },
142
143 get sortedBanners() {
144 - return [...this.banners].sort(
145 - (a, b) => (b.priority || 0) - (a.priority || 0),
146 - );
144 + return [...this.banners]
145 + .filter((b) => b.type !== "hero" && b.type !== "feature")
146 + .sort((a, b) => (b.priority || 0) - (a.priority || 0));
147 },
148
149 /**
webui/css/modals.css
+2
@@ -256,6 +256,8 @@ some classes like modal-header are shared between the old and the new system */
256 background: #2196f3;
257 color: white;
258 width: fit-content;
259 + align-items: center;
260 + display: inline-flex;
261 }
262 .btn:disabled {
263 cursor: not-allowed;