Merge pull request #1402 from 3clyp50/discovery

feat: add plugin discovery to dashboard and onboarding wizard
This commit is contained in:
Jan Tomášek 2026-04-02 16:21:05 +02:00 committed by GitHub
commit 01de1f7bca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1313 additions and 16 deletions

View file

@ -0,0 +1,77 @@
from helpers.extension import Extension
from helpers import plugins
class DiscoveryCardsExtension(Extension):
"""Injects discovery cards into the banners list."""
async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs):
# Optional logic: only show specific cards if plugins aren't already configured.
# Telegram, Email, Whatsapp are built-in, so we only need to check if they've been configured.
telegram_config = plugins.get_plugin_config("_telegram_integration") or {}
email_config = plugins.get_plugin_config("_email_integration") or {}
whatsapp_config = plugins.get_plugin_config("_whatsapp_integration") or {}
# 1. Plugin Hub Hero
banners.append({
"id": "discovery-plugin-hub",
"type": "hero",
"title": "Discover the Plugin Hub",
"description": "Extend Agent Zero with integrations, tools, and automations from the community.",
"thumbnail": "/plugins/_discovery/webui/assets/hero-plugin-hub.png",
"icon": "extension",
"cta_text": "Explore Plugins",
"cta_action": "open-plugin-hub",
"dismissible": True,
"priority": 100,
"show_in_onboarding": True
})
# 2. Telegram
if not telegram_config.get("bot_token"):
banners.append({
"id": "discovery-telegram",
"type": "feature",
"title": "Connect Telegram",
"description": "Chat with Agent Zero from Telegram wherever you are.",
"thumbnail": "/plugins/_discovery/webui/assets/thumb-telegram.png",
"icon": "send",
"cta_text": "Setup",
"cta_action": "open-plugin-config:_telegram_integration",
"dismissible": True,
"priority": 50,
"show_in_onboarding": True
})
# 3. Email
if not email_config.get("imap_username") and not email_config.get("smtp_username"):
banners.append({
"id": "discovery-email",
"type": "feature",
"title": "Setup Email",
"description": "Let Agent Zero read and send emails on your behalf.",
"thumbnail": "/plugins/_discovery/webui/assets/thumb-email.png",
"icon": "mail",
"cta_text": "Setup",
"cta_action": "open-plugin-config:_email_integration",
"dismissible": True,
"priority": 50,
"show_in_onboarding": True
})
# 4. WhatsApp
if not whatsapp_config.get("phone_number_id"):
banners.append({
"id": "discovery-whatsapp",
"type": "feature",
"title": "Connect WhatsApp",
"description": "Send and receive WhatsApp messages through A0.",
"thumbnail": "/plugins/_discovery/webui/assets/thumb-whatsapp.png",
"icon": "chat",
"cta_text": "Setup",
"cta_action": "open-plugin-config:_whatsapp_integration",
"dismissible": True,
"priority": 50,
"show_in_onboarding": True
})

View file

@ -0,0 +1,483 @@
<script type="module">
import { store } from "/plugins/_discovery/webui/discovery-store.js";
</script>
<div x-data>
<template x-if="$store.discoveryStore">
<div class="discovery-slot"
@modal-closed.window="$store.discoveryStore.refreshCards()"
x-create="$store.discoveryStore.refreshCards()"
x-show="$store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards">
<div class="discovery-section" style="margin-top: 2rem;">
<div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0" style="text-align: left;">
<template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
<article class="discovery-feature-card"
role="button"
tabindex="0"
@click="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
<button class="discovery-dismiss discovery-dismiss-small"
type="button"
x-show="card.dismissible"
@click.stop="$store.discoveryStore.dismissCard(card.id)"
:aria-label="`Dismiss ${card.title}`"
title="Dismiss">
<span class="material-symbols-outlined">close</span>
</button>
<div class="discovery-feature-head">
<div class="discovery-feature-thumb">
<template x-if="card.thumbnail">
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
</template>
<template x-if="!card.thumbnail && card.icon">
<span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
</template>
</div>
<h4 class="discovery-feature-title" x-text="card.title"></h4>
</div>
<p class="discovery-feature-desc" x-text="card.description"></p>
<button class="btn btn primary"
type="button"
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</article>
</template>
</div>
<template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
<article class="discovery-hero"
role="button"
tabindex="0"
@click="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
<button class="discovery-dismiss"
type="button"
x-show="card.dismissible"
@click.stop="$store.discoveryStore.dismissCard(card.id)"
:aria-label="`Dismiss ${card.title}`"
title="Dismiss">
<span class="material-symbols-outlined">close</span>
</button>
<div class="discovery-hero-content">
<h3 class="discovery-hero-title" x-text="card.title"></h3>
<p class="discovery-hero-desc" x-text="card.description"></p>
<button class="btn btn-ok"
type="button"
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</div>
<div class="discovery-hero-thumb">
<template x-if="card.thumbnail">
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
</template>
<template x-if="!card.thumbnail && card.icon">
<span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
</template>
</div>
</article>
</template>
<div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
<button type="button"
class="discovery-undismiss-btn"
@click="$store.discoveryStore.undismissCards()">
<span class="material-symbols-outlined">undo</span>
<span>Show dismissed suggestions</span>
</button>
</div>
</div>
</div>
</template>
<style>
.discovery-slot {
grid-column: 1 / -1;
width: 100%;
}
.discovery-section {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
margin-top: 0.1rem;
}
.discovery-hero,
.discovery-feature-card {
position: relative;
border: 1px solid var(--color-border);
border-radius: 12px;
background: var(--color-panel);
text-align: left;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.discovery-hero:hover,
.discovery-feature-card:hover,
.discovery-hero:focus-visible,
.discovery-feature-card:focus-visible {
border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
outline: none;
}
.discovery-hero {
display: flex;
align-items: center;
gap: 1rem;
min-height: 152px;
overflow: hidden;
padding: 1.25rem 1.25rem 1.25rem 1.5rem;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--color-primary) 10%, transparent),
transparent 58%
),
radial-gradient(
circle at 92% 24%,
color-mix(in srgb, var(--color-primary) 12%, transparent),
transparent 42%
),
var(--color-panel);
}
.discovery-hero-content,
.discovery-hero-thumb {
position: relative;
z-index: 1;
}
.discovery-hero-content {
flex: 1 1 auto;
min-width: 0;
}
.discovery-hero-title {
margin: 0 0 0.4rem;
color: var(--color-text);
font-size: 1.08rem;
font-weight: 600;
letter-spacing: 0.01em;
}
.discovery-hero-desc {
margin: 0 0 1rem;
color: var(--color-text);
font-size: 0.86rem;
line-height: 1.5;
max-width: 38ch;
}
.discovery-cta-link,
.discovery-undismiss-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.discovery-hero-thumb {
flex: 0 0 132px;
width: 132px;
height: 108px;
display: flex;
align-items: center;
justify-content: center;
}
.discovery-hero-thumb img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
}
.discovery-hero-icon {
font-size: 3rem;
color: var(--color-highlight-dark);
}
.discovery-features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
}
.discovery-feature-card {
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 188px;
padding: 1rem;
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary) 4%, transparent),
transparent 38%
),
var(--color-panel);
}
.discovery-feature-head {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
width: 100%;
min-width: 0;
}
.discovery-feature-thumb {
flex: 0 0 auto;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
overflow: hidden;
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.discovery-feature-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.discovery-feature-icon {
font-size: 1.45rem;
color: var(--color-highlight-dark);
}
.discovery-feature-title {
margin: 0;
flex: 1 1 auto;
min-width: 0;
color: var(--color-text);
font-size: 0.92rem;
font-weight: 600;
line-height: 1.25;
}
.discovery-feature-desc {
margin: 0.55rem 0 0;
padding-bottom: var(--spacing-md);
color: var(--color-text);
font-size: 0.8rem;
line-height: 1.45;
}
.discovery-feature-card > .btn {
margin-top: auto;
}
.discovery-cta-link {
margin-top: 0.95rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
border-radius: 8px;
background: transparent;
color: var(--color-primary);
padding: 0.42rem 0.72rem;
font-size: 0.79rem;
font-weight: 600;
}
.discovery-cta-link:hover,
.discovery-cta-link:focus-visible {
background: var(--color-primary);
border-color: var(--color-primary);
color: #fff;
outline: none;
}
.discovery-dismiss {
position: absolute;
top: 0.65rem;
right: 0.65rem;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--color-secondary);
cursor: pointer;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease;
z-index: 2;
}
.discovery-dismiss-small {
top: 0.55rem;
right: 0.55rem;
width: 26px;
height: 26px;
}
.discovery-hero:hover .discovery-dismiss,
.discovery-hero:focus-visible .discovery-dismiss,
.discovery-feature-card:hover .discovery-dismiss,
.discovery-feature-card:focus-visible .discovery-dismiss {
opacity: 0.68;
}
.discovery-dismiss:hover,
.discovery-dismiss:focus-visible {
opacity: 1;
color: var(--color-text);
background: color-mix(in srgb, var(--color-border) 84%, transparent);
outline: none;
}
.discovery-dismiss .material-symbols-outlined {
font-size: 1rem;
}
.discovery-undismiss {
display: flex;
justify-content: flex-start;
}
.discovery-undismiss-btn {
border: none;
background: transparent;
padding: 0;
color: var(--color-secondary);
font-size: 0.8rem;
font-weight: 500;
}
.discovery-undismiss-btn:hover,
.discovery-undismiss-btn:focus-visible {
color: var(--color-primary);
outline: none;
}
.discovery-cta-link .material-symbols-outlined,
.discovery-undismiss-btn .material-symbols-outlined {
font-size: 1rem;
}
@media (hover: none) {
.discovery-dismiss {
opacity: 0.68;
}
}
@media (max-width: 768px) {
.discovery-section {
gap: 0.85rem;
}
.discovery-hero {
min-height: 0;
padding: 1rem;
}
.discovery-hero-thumb {
flex-basis: 92px;
width: 92px;
height: 76px;
}
.discovery-features {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.discovery-feature-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
min-height: 0;
align-items: center;
padding: 0.85rem 1rem;
}
.discovery-feature-head {
grid-column: 1;
min-width: 0;
}
.discovery-feature-thumb {
width: 46px;
height: 46px;
}
.discovery-feature-desc {
display: none;
}
.discovery-feature-card > .btn {
grid-column: 2;
grid-row: 1;
align-self: center;
margin-top: 0;
}
.discovery-cta-link {
margin-top: 0;
}
}
@media (max-width: 520px) {
.discovery-slot {
margin-top: 0.15rem;
}
.discovery-hero {
padding-right: 0.95rem;
}
.discovery-hero-thumb {
display: none;
}
.discovery-hero-title {
font-size: 1rem;
}
.discovery-feature-card {
grid-template-columns: minmax(0, 1fr) auto;
padding-right: 3rem;
}
.discovery-feature-card > .btn {
justify-self: end;
}
}
</style>
</div>

View file

@ -0,0 +1,481 @@
<script type="module">
import { store } from "/plugins/_discovery/webui/discovery-store.js";
</script>
<div x-data>
<template x-if="$store.discoveryStore">
<div class="discovery-slot"
@modal-closed.window="$store.discoveryStore.refreshCards()"
x-create="$store.discoveryStore.refreshCards()"
x-show="!($store.welcomeStore?.banners || []).some(b => b.id === 'missing-api-key') && ($store.discoveryStore.cards.length > 0 || $store.discoveryStore.hasDismissedCards)">
<div class="discovery-section">
<template x-for="card in $store.discoveryStore.heroCards" :key="card.id">
<article class="discovery-hero"
role="button"
tabindex="0"
@click="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
<button class="discovery-dismiss"
type="button"
x-show="card.dismissible"
@click.stop="$store.discoveryStore.dismissCard(card.id)"
:aria-label="`Dismiss ${card.title}`"
title="Dismiss">
<span class="material-symbols-outlined">close</span>
</button>
<div class="discovery-hero-content">
<h3 class="discovery-hero-title" x-text="card.title"></h3>
<p class="discovery-hero-desc" x-text="card.description"></p>
<button class="btn btn-ok"
type="button"
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</div>
<div class="discovery-hero-thumb">
<template x-if="card.thumbnail">
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
</template>
<template x-if="!card.thumbnail && card.icon">
<span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
</template>
</div>
</article>
</template>
<div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0">
<template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
<article class="discovery-feature-card"
role="button"
tabindex="0"
@click="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
@keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
<button class="discovery-dismiss discovery-dismiss-small"
type="button"
x-show="card.dismissible"
@click.stop="$store.discoveryStore.dismissCard(card.id)"
:aria-label="`Dismiss ${card.title}`"
title="Dismiss">
<span class="material-symbols-outlined">close</span>
</button>
<div class="discovery-feature-head">
<div class="discovery-feature-thumb">
<template x-if="card.thumbnail">
<img :src="card.thumbnail" :alt="card.title" loading="lazy">
</template>
<template x-if="!card.thumbnail && card.icon">
<span class="material-symbols-outlined discovery-feature-icon" x-text="card.icon"></span>
</template>
</div>
<h4 class="discovery-feature-title" x-text="card.title"></h4>
</div>
<p class="discovery-feature-desc" x-text="card.description"></p>
<button class="btn btn primary"
type="button"
@click.stop="$store.discoveryStore.executeCta(card.cta_action)">
<span x-text="card.cta_text"></span>
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</article>
</template>
</div>
<div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
<button type="button"
class="discovery-undismiss-btn"
@click="$store.discoveryStore.undismissCards()">
<span class="material-symbols-outlined">undo</span>
<span>Show dismissed suggestions</span>
</button>
</div>
</div>
</div>
</template>
<style>
.discovery-slot {
grid-column: 1 / -1;
width: 100%;
}
.discovery-section {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
margin-top: 0.1rem;
}
.discovery-hero,
.discovery-feature-card {
position: relative;
border: 1px solid var(--color-border);
border-radius: 12px;
background: var(--color-panel);
text-align: left;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.discovery-hero:hover,
.discovery-feature-card:hover,
.discovery-hero:focus-visible,
.discovery-feature-card:focus-visible {
border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
outline: none;
}
.discovery-hero {
display: flex;
align-items: center;
gap: 1rem;
min-height: 152px;
overflow: hidden;
padding: 1.25rem 1.25rem 1.25rem 1.5rem;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--color-primary) 10%, transparent),
transparent 58%
),
radial-gradient(
circle at 92% 24%,
color-mix(in srgb, var(--color-primary) 12%, transparent),
transparent 42%
),
var(--color-panel);
}
.discovery-hero-content,
.discovery-hero-thumb {
position: relative;
z-index: 1;
}
.discovery-hero-content {
flex: 1 1 auto;
min-width: 0;
}
.discovery-hero-title {
margin: 0 0 0.4rem;
color: var(--color-text);
font-size: 1.08rem;
font-weight: 600;
letter-spacing: 0.01em;
}
.discovery-hero-desc {
margin: 0 0 1rem;
color: var(--color-text);
font-size: 0.86rem;
line-height: 1.5;
max-width: 38ch;
}
.discovery-cta-link,
.discovery-undismiss-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.discovery-hero-thumb {
flex: 0 0 132px;
width: 132px;
height: 108px;
display: flex;
align-items: center;
justify-content: center;
}
.discovery-hero-thumb img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
}
.discovery-hero-icon {
font-size: 3rem;
color: var(--color-highlight-dark);
}
.discovery-features {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.discovery-feature-card {
display: flex;
flex-direction: column;
align-items: flex-start;
min-height: 188px;
padding: 1rem;
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary) 4%, transparent),
transparent 38%
),
var(--color-panel);
}
.discovery-feature-head {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
width: 100%;
min-width: 0;
}
.discovery-feature-thumb {
flex: 0 0 auto;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
overflow: hidden;
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.discovery-feature-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.discovery-feature-icon {
font-size: 1.45rem;
color: var(--color-highlight-dark);
}
.discovery-feature-title {
margin: 0;
flex: 1 1 auto;
min-width: 0;
color: var(--color-text);
font-size: 0.92rem;
font-weight: 600;
line-height: 1.25;
}
.discovery-feature-desc {
margin: 0.55rem 0 0;
padding-bottom: var(--spacing-md);
color: var(--color-text);
font-size: 0.8rem;
line-height: 1.45;
}
.discovery-feature-card > .btn {
margin-top: auto;
}
.discovery-cta-link {
margin-top: 0.95rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
border-radius: 8px;
background: transparent;
color: var(--color-primary);
padding: 0.42rem 0.72rem;
font-size: 0.79rem;
font-weight: 600;
}
.discovery-cta-link:hover,
.discovery-cta-link:focus-visible {
background: var(--color-primary);
border-color: var(--color-primary);
color: #fff;
outline: none;
}
.discovery-dismiss {
position: absolute;
top: 0.65rem;
right: 0.65rem;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--color-secondary);
cursor: pointer;
opacity: 0;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease;
z-index: 2;
}
.discovery-dismiss-small {
top: 0.55rem;
right: 0.55rem;
width: 26px;
height: 26px;
}
.discovery-hero:hover .discovery-dismiss,
.discovery-hero:focus-visible .discovery-dismiss,
.discovery-feature-card:hover .discovery-dismiss,
.discovery-feature-card:focus-visible .discovery-dismiss {
opacity: 0.68;
}
.discovery-dismiss:hover,
.discovery-dismiss:focus-visible {
opacity: 1;
color: var(--color-text);
background: color-mix(in srgb, var(--color-border) 84%, transparent);
outline: none;
}
.discovery-dismiss .material-symbols-outlined {
font-size: 1rem;
}
.discovery-undismiss {
display: flex;
justify-content: flex-start;
}
.discovery-undismiss-btn {
border: none;
background: transparent;
padding: 0;
color: var(--color-secondary);
font-size: 0.8rem;
font-weight: 500;
}
.discovery-undismiss-btn:hover,
.discovery-undismiss-btn:focus-visible {
color: var(--color-primary);
outline: none;
}
.discovery-cta-link .material-symbols-outlined,
.discovery-undismiss-btn .material-symbols-outlined {
font-size: 1rem;
}
@media (hover: none) {
.discovery-dismiss {
opacity: 0.68;
}
}
@media (max-width: 768px) {
.discovery-section {
gap: 0.85rem;
}
.discovery-hero {
min-height: 0;
padding: 1rem;
}
.discovery-hero-thumb {
flex-basis: 92px;
width: 92px;
height: 76px;
}
.discovery-features {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.discovery-feature-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
min-height: 0;
align-items: center;
padding: 0.85rem 1rem;
}
.discovery-feature-head {
grid-column: 1;
min-width: 0;
}
.discovery-feature-thumb {
width: 46px;
height: 46px;
}
.discovery-feature-desc {
display: none;
}
.discovery-feature-card > .btn {
grid-column: 2;
grid-row: 1;
align-self: center;
margin-top: 0;
}
.discovery-cta-link {
margin-top: 0;
}
}
@media (max-width: 520px) {
.discovery-slot {
margin-top: 0.15rem;
}
.discovery-hero {
padding-right: 0.95rem;
}
.discovery-hero-thumb {
display: none;
}
.discovery-hero-title {
font-size: 1rem;
}
.discovery-feature-card {
grid-template-columns: minmax(0, 1fr) auto;
padding-right: 3rem;
}
.discovery-feature-card > .btn {
justify-self: end;
}
}
</style>
</div>

View file

@ -0,0 +1,8 @@
name: _discovery
title: Plugin Discovery
description: Contextual discovery cards on the welcome screen promoting plugins and integrations.
version: 1.0.0
settings_sections: []
always_enabled: true
per_project_config: false
per_agent_config: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,129 @@
import { createStore } from "/js/AlpineStore.js";
import { toastFrontendError } from "/components/notifications/notification-store.js";
import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
import * as API from "/js/api.js";
const STORAGE_KEY = "dismissed_discovery_cards";
const model = {
// --- State ---
/** @type {any[]} */
cards: [],
hasDismissedCards: false,
_initialized: false,
_isLoading: false,
// --- Lifecycle ---
init() {
if (this._initialized) return;
this._initialized = true;
},
// --- Actions ---
async refreshCards() {
if (this._isLoading) return;
this._isLoading = true;
try {
const response = await API.callJsonApi("/banners", {
banners: [],
context: {
is_onboarding: document.body.dataset.mode === "onboarding"
},
});
const banners = response?.banners || [];
const dismissed = this._getDismissedIds();
// Filter out standard banners, keep only hero and feature
// Also respect the onboarding filtering
const is_onboarding = document.body.dataset.mode === "onboarding";
this.cards = banners
.filter((card) => card.type === "hero" || card.type === "feature")
.filter((card) => !is_onboarding || card.show_in_onboarding === true)
.filter((card) => !dismissed.has(card.id))
.sort((left, right) => (right.priority || 0) - (left.priority || 0));
this.hasDismissedCards = dismissed.size > 0;
} catch (error) {
console.error("Failed to fetch discovery cards:", error);
} finally {
this._isLoading = false;
}
},
dismissCard(cardId) {
const dismissed = this._getDismissedIds();
dismissed.add(cardId);
this._persistDismissedIds(dismissed);
// Optimistically update UI
this.cards = this.cards.filter(c => c.id !== cardId);
this.hasDismissedCards = true;
},
undismissCards() {
localStorage.removeItem(STORAGE_KEY);
this.refreshCards();
},
async executeCta(action) {
if (!action) return;
try {
if (action === "open-plugin-hub") {
await pluginListStore.open("pluginHub");
return;
}
if (action.startsWith("open-plugin-config:")) {
const pluginName = action.split(":")[1];
await pluginListStore.openPluginConfig(pluginName);
return;
}
if (action.startsWith("open-url:")) {
const url = action.slice("open-url:".length);
if (url) {
window.open(url, "_blank", "noopener,noreferrer");
}
return;
}
} catch (error) {
console.error("Discovery action failed:", error);
const message = error instanceof Error ? error.message : String(error);
await toastFrontendError(message, "Discovery");
}
},
// --- Helpers (Private-ish) ---
_getDismissedIds() {
try {
const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
if (!Array.isArray(raw)) return new Set();
return new Set(raw);
} catch {
return new Set();
}
},
_persistDismissedIds(ids) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(ids)));
},
// --- Computed ---
get heroCards() {
return this.cards.filter((card) => card.type === "hero");
},
get featureCards() {
return this.cards.filter((card) => card.type === "feature");
},
};
const store = createStore("discoveryStore", model);
export { store };

View file

@ -16,7 +16,7 @@
}
.onboarding-welcome-text {
text-align: center;
margin: var(--spacing-md) 0;
margin: var(--spacing-md) 0 var(--spacing-lg) 0;
color: var(--text-2);
font-size: 1.1rem;
line-height: 1.5;
@ -29,7 +29,7 @@
}
.onboarding-success {
text-align: center;
padding: 40px 0;
padding: var(--spacing-md) 0;
}
.onboarding-success-icon {
font-size: 64px;
@ -186,7 +186,7 @@
<div x-show="$store.onboarding.step === 1">
<div class="onboarding-welcome-text">
<div class="onboarding-welcome-title">Welcome to Agent Zero</div>
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.
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.
</div>
<div class="model-section">
@ -270,7 +270,7 @@
<div x-show="$store.onboarding.step === 2">
<div class="onboarding-welcome-text">
<div class="onboarding-welcome-title">Almost there!</div>
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.
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.
</div>
<div class="model-section">
@ -353,10 +353,11 @@
<!-- Step 3: Success -->
<div x-show="$store.onboarding.step === 3" class="onboarding-success">
<div class="material-symbols-outlined onboarding-success-icon">check_circle</div>
<div class="onboarding-welcome-title">Ready to chat!</div>
<div class="onboarding-welcome-text onboarding-success-text">
Your models are configured. You can change these anytime in Settings.
Your models are configured. You can change these anytime in Settings.<br>
While you're here, why not check out some integrations to extend what Agent Zero can do?
</div>
<x-extension id="onboarding-success-end"></x-extension>
</div>
<div class="onboarding-advanced-link" x-show="$store.onboarding.step < 3">