Refresh welcome screen and composer

Rework the welcome screen around the shared new-chat composer, renewed quick actions, discovery cards, OAuth accounts, and dedicated system resources panel. Restore static button colors and neutral surfaces, move microphone state feedback onto the icon, and keep Rubik/code-font composer behavior. Add focused static regressions for the welcome composer, OAuth discovery panel, and speech button contract.

Alessandro committed Jun 22, 2026 at 15:12 UTC 0e262f7b8782bc9b2060ffa6ef645b8a9994cd41
23 files changed +1194 -940
extensions/python/banners/_30_system_resources.py
+23 -21
@@ -69,34 +69,37 @@ class SystemResourcesCheck(Extension):
69 "priority": 10,
70 "title": "System Resources",
71 "html": (
72 - "<div style=\"display:flex;flex-direction:column;gap:14px;padding-top:6px;\">"
73 - "<div style=\"display:flex;flex-direction:column;gap:12px;\">"
74 - "<div style=\"display:flex;flex-wrap:wrap;column-gap:12px;row-gap:10px;align-items:center;\">"
75 - "<div style=\"flex:0 1 140px;min-width:110px;display:flex;flex-direction:column;gap:6px;\">"
76 - "<div style=\"font-size:12px;letter-spacing:.10em;text-transform:uppercase;opacity:.65;line-height:1.1\">CPU</div>"
77 - f"<div style=\"font-weight:750;font-variant-numeric:tabular-nums;letter-spacing:.02em;line-height:1.1\">{cpu_value}</div>"
72 + "<div class=\"system-resource-body\">"
73 + "<div class=\"system-resource-meter\">"
74 + "<div class=\"system-resource-label\">"
75 + "<div class=\"system-resource-name\">CPU</div>"
76 + f"<div class=\"system-resource-value\">{cpu_value}</div>"
77 "</div>"
78 f"{cpu_bar}"
79 "</div>"
81 - "<div style=\"display:flex;flex-wrap:wrap;column-gap:12px;row-gap:10px;align-items:center;\">"
82 - "<div style=\"flex:0 1 140px;min-width:110px;display:flex;flex-direction:column;gap:6px;\">"
83 - "<div style=\"font-size:12px;letter-spacing:.10em;text-transform:uppercase;opacity:.65;line-height:1.1\">RAM</div>"
84 - f"<div style=\"font-weight:750;font-variant-numeric:tabular-nums;letter-spacing:.02em;line-height:1.1\">{ram_value}</div>"
80 + "<div class=\"system-resource-meter\">"
81 + "<div class=\"system-resource-label\">"
82 + "<div class=\"system-resource-name\">RAM</div>"
83 + f"<div class=\"system-resource-value\">{ram_value}</div>"
84 "</div>"
85 f"{ram_bar}"
86 "</div>"
88 - "<div style=\"display:flex;flex-wrap:wrap;column-gap:12px;row-gap:10px;align-items:center;\">"
89 - "<div style=\"flex:0 1 140px;min-width:110px;display:flex;flex-direction:column;gap:6px;\">"
90 - "<div style=\"font-size:12px;letter-spacing:.10em;text-transform:uppercase;opacity:.65;line-height:1.1\">Disk</div>"
91 - f"<div style=\"font-weight:750;font-variant-numeric:tabular-nums;letter-spacing:.02em;line-height:1.1\">{disk_value}</div>"
87 + "<div class=\"system-resource-meter\">"
88 + "<div class=\"system-resource-label\">"
89 + "<div class=\"system-resource-name\">Disk</div>"
90 + f"<div class=\"system-resource-value\">{disk_value}</div>"
91 "</div>"
92 f"{disk_bar}"
93 "</div>"
94 + "<div class=\"system-resource-footer\">"
95 + "<div class=\"system-resource-detail\">"
96 + "<div class=\"system-resource-detail-title\">Load (1/5/15)</div>"
97 + f"<div class=\"system-resource-detail-value\">{load_value}</div>"
98 + "</div>"
99 + "<div class=\"system-resource-detail\">"
100 + "<div class=\"system-resource-detail-title\">Net (since boot)</div>"
101 + f"<div class=\"system-resource-detail-value\">{net_sent} sent / {net_recv} recv</div>"
102 "</div>"
96 - "<div style=\"height:1px;background:rgba(255,255,255,.08);\"></div>"
97 - "<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;\">"
98 - f"<div><div style=\"font-size:12px;letter-spacing:.10em;text-transform:uppercase;opacity:.65;margin-bottom:6px;\">Load (1/5/15)</div><div style=\"font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;opacity:.85;font-variant-numeric:tabular-nums\">{load_value}</div></div>"
99 - f"<div><div style=\"font-size:12px;letter-spacing:.10em;text-transform:uppercase;opacity:.65;margin-bottom:6px;\">Net (since boot)</div><div style=\"font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;opacity:.85;font-variant-numeric:tabular-nums\">{net_sent} sent / {net_recv} recv</div></div>"
103 "</div>"
104 "</div>"
105 ),
@@ -117,9 +120,8 @@ class SystemResourcesCheck(Extension):
120 color = "#22c55e"
121
122 return (
120 - "<div style=\"flex:1 1 220px;width:100%;max-width:260px;height:10px;border-radius:999px;"
121 - "background:rgba(255,255,255,.10);overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);\">"
122 - f"<div style=\"height:100%;width:{p:.0f}%;background:{color};border-radius:999px;\"></div>"
123 + "<div class=\"system-resource-track\">"
124 + f"<span class=\"system-resource-fill\" style=\"--resource-value:{p:.0f}%;--resource-color:{color};\"></span>"
125 "</div>"
126 )
127
plugins/_discovery/AGENTS.md
+1
@@ -15,6 +15,7 @@
15 - Discovery cards must be accurate, dismissible where appropriate, and avoid advertising already-complete setup.
16 - CTA actions must match supported welcome-screen action contracts.
17 - Keep card IDs unique and plugin-prefixed.
18 +- The Welcome screen `welcome-actions-end` surface renders feature channel cards plus the compact OAuth account-provider card; other hero discovery cards stay out of the lower welcome grid.
19
20 ## Work Guidance
21
plugins/_discovery/extensions/python/banners/10_discovery_cards.py
+7 -7
@@ -87,11 +87,11 @@ class DiscoveryCardsExtension(Extension):
87 banners.append({
88 "id": "discovery-telegram",
89 "type": "feature",
90 - "title": "Connect Telegram",
91 - "description": "Chat with Agent Zero from Telegram wherever you are.",
90 + "title": "Telegram",
91 + "description": "Chat on Telegram wherever you are.",
92 "thumbnail": "/plugins/_discovery/webui/assets/thumb-telegram.png",
93 "icon": "send",
94 - "cta_text": "Setup",
94 + "cta_text": "Connect",
95 "cta_action": "open-plugin-config:_telegram_integration",
96 "dismissible": True,
97 "priority": 50,
@@ -105,11 +105,11 @@ class DiscoveryCardsExtension(Extension):
105 banners.append({
106 "id": "discovery-email",
107 "type": "feature",
108 - "title": "Setup Email",
108 + "title": "Email",
109 "description": "Let Agent Zero read and send emails on your behalf.",
110 "thumbnail": "/plugins/_discovery/webui/assets/thumb-email.png",
111 "icon": "mail",
112 - "cta_text": "Open Setup",
112 + "cta_text": "Connect",
113 "cta_action": "open-plugin-config:_email_integration",
114 "dismissible": True,
115 "priority": 50,
@@ -121,11 +121,11 @@ class DiscoveryCardsExtension(Extension):
121 banners.append({
122 "id": "discovery-whatsapp",
123 "type": "feature",
124 - "title": "Connect WhatsApp",
124 + "title": "WhatsApp",
125 "description": "Send and receive WhatsApp messages through A0.",
126 "thumbnail": "/plugins/_discovery/webui/assets/thumb-whatsapp.png",
127 "icon": "chat",
128 - "cta_text": "Setup",
128 + "cta_text": "Connect",
129 "cta_action": "open-plugin-config:_whatsapp_integration",
130 "dismissible": True,
131 "priority": 50,
plugins/_discovery/extensions/webui/onboarding-success-end/discovery-cards.html
+2 -10
@@ -19,14 +19,6 @@
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>
22
23 <div class="discovery-feature-head">
24 <div class="discovery-feature-thumb">
@@ -40,8 +32,6 @@
32 <h4 class="discovery-feature-title" x-text="card.title"></h4>
33 </div>
34
43 - <p class="discovery-feature-desc" x-text="card.description"></p>
44 -
35 <button class="btn btn primary"
36 type="button"
37 @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
@@ -281,6 +271,7 @@
271 gap: 0.62rem;
272 width: 100%;
273 min-width: 0;
274 + margin-bottom: 1rem;
275 }
276
277 .discovery-feature-thumb {
@@ -575,6 +566,7 @@
566 .discovery-feature-head {
567 grid-column: 1;
568 min-width: 0;
569 + margin-bottom: 0;
570 }
571
572 .discovery-feature-thumb {
plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html
+342 -418
@@ -7,73 +7,15 @@
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.topHeroCards" :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-show="card.description" x-text="card.description"></p>
32 - <div class="discovery-account-chips" x-show="(card.account_chips || []).length">
33 - <template x-for="chip in card.account_chips" :key="`${card.id}-${chip.provider_id}`">
34 - <span class="discovery-account-chip" :class="{ connected: chip.connected }">
35 - <span x-text="chip.label"></span>
36 - <small x-show="chip.detail" x-text="chip.detail"></small>
37 - </span>
38 - </template>
39 - </div>
40 - <div class="discovery-usage" x-show="(card.usage_windows || []).length">
41 - <template x-for="window in card.usage_windows" :key="`${card.id}-${window.key}`">
42 - <div class="discovery-usage-window">
43 - <div class="discovery-usage-head">
44 - <span>
45 - <span x-text="window.title"></span>
46 - <small x-show="window.label" x-text="window.label"></small>
47 - </span>
48 - <strong x-text="$store.discoveryStore.formatRemainingPercent(window)"></strong>
49 - </div>
50 - <div class="discovery-usage-bar" aria-hidden="true">
51 - <i :style="{ width: $store.discoveryStore.usageWidth(window) }"></i>
52 - </div>
53 - <p x-show="$store.discoveryStore.formatReset(window)" x-text="`Resets in ${$store.discoveryStore.formatReset(window)}`"></p>
54 - </div>
55 - </template>
56 - </div>
57 - <button class="btn btn-ok"
58 - type="button"
59 - @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
60 - <span x-text="card.cta_text"></span>
61 - <span class="material-symbols-outlined">arrow_forward</span>
62 - </button>
63 - </div>
10 + x-show="!($store.welcomeStore?.banners || []).some(b => b.id === 'missing-api-key') && ($store.discoveryStore.featureCards.length > 0 || $store.discoveryStore.oauthAccountCards.length > 0 || $store.discoveryStore.hasDismissedCards)">
11
65 - <div class="discovery-hero-thumb" x-show="card.thumbnail || card.icon">
66 - <template x-if="card.thumbnail">
67 - <img :src="card.thumbnail" :alt="card.title" loading="lazy">
68 - </template>
69 - <template x-if="!card.thumbnail && card.icon">
70 - <span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
71 - </template>
72 - </div>
73 - </article>
74 - </template>
12 + <section class="discovery-features-panel" x-show="$store.discoveryStore.featureCards.length > 0">
13 + <header class="discovery-panel-header">
14 + <h2>Connect Channels</h2>
15 + <p>Extend Agent Zero across your favorite platforms.</p>
16 + </header>
17
76 - <div class="discovery-features" x-show="$store.discoveryStore.featureCards.length > 0">
18 + <div class="discovery-features">
19 <template x-for="card in $store.discoveryStore.featureCards" :key="card.id">
20 <article class="discovery-feature-card"
21 role="button"
@@ -81,14 +23,6 @@
23 @click="$store.discoveryStore.executeCta(card.cta_action)"
24 @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
25 @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
84 - <button class="discovery-dismiss discovery-dismiss-small"
85 - type="button"
86 - x-show="card.dismissible"
87 - @click.stop="$store.discoveryStore.dismissCard(card.id)"
88 - :aria-label="`Dismiss ${card.title}`"
89 - title="Dismiss">
90 - <span class="material-symbols-outlined">close</span>
91 - </button>
26
27 <div class="discovery-feature-head">
28 <div class="discovery-feature-thumb">
@@ -102,339 +36,141 @@
36 <h4 class="discovery-feature-title" x-text="card.title"></h4>
37 </div>
38
105 - <p class="discovery-feature-desc" x-text="card.description"></p>
106 -
39 <button class="btn btn primary"
40 type="button"
41 @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
42 <span x-text="card.cta_text"></span>
111 - <span class="material-symbols-outlined">arrow_forward</span>
43 </button>
44 </article>
45 </template>
46 </div>
47 + </section>
48 +
49 + <template x-for="card in $store.discoveryStore.oauthAccountCards" :key="card.id">
50 + <article class="discovery-account-card">
51 + <button class="discovery-dismiss"
52 + type="button"
53 + x-show="card.dismissible"
54 + @click.stop="$store.discoveryStore.dismissCard(card.id)"
55 + :aria-label="`Dismiss ${card.title}`"
56 + title="Dismiss">
57 + <span class="material-symbols-outlined">close</span>
58 + </button>
59
117 - <template x-for="card in $store.discoveryStore.bottomHeroCards" :key="card.id">
118 - <article class="discovery-hero"
119 - role="button"
120 - tabindex="0"
121 - @click="$store.discoveryStore.executeCta(card.cta_action)"
122 - @keydown.enter.prevent="$store.discoveryStore.executeCta(card.cta_action)"
123 - @keydown.space.prevent="$store.discoveryStore.executeCta(card.cta_action)">
124 - <button class="discovery-dismiss"
125 - type="button"
126 - x-show="card.dismissible"
127 - @click.stop="$store.discoveryStore.dismissCard(card.id)"
128 - :aria-label="`Dismiss ${card.title}`"
129 - title="Dismiss">
130 - <span class="material-symbols-outlined">close</span>
131 - </button>
132 -
133 - <div class="discovery-hero-content">
134 - <h3 class="discovery-hero-title" x-text="card.title"></h3>
135 - <p class="discovery-hero-desc" x-show="card.description" x-text="card.description"></p>
136 - <div class="discovery-account-chips" x-show="(card.account_chips || []).length">
137 - <template x-for="chip in card.account_chips" :key="`${card.id}-${chip.provider_id}`">
138 - <span class="discovery-account-chip" :class="{ connected: chip.connected }">
139 - <span x-text="chip.label"></span>
140 - <small x-show="chip.detail" x-text="chip.detail"></small>
60 + <header class="discovery-account-header">
61 + <span class="material-symbols-outlined discovery-account-icon">key</span>
62 + <div class="discovery-account-copy">
63 + <h2 x-text="card.title"></h2>
64 + <p x-show="card.description" x-text="card.description"></p>
65 + </div>
66 + </header>
67 +
68 + <div class="discovery-account-chips" x-show="(card.account_chips || []).length > 0">
69 + <template x-for="chip in card.account_chips || []" :key="chip.provider_id || chip.label">
70 + <span class="discovery-account-chip" :class="{ 'is-connected': chip.connected }">
71 + <span class="discovery-account-chip-dot" aria-hidden="true"></span>
72 + <span class="discovery-account-chip-label" x-text="chip.label"></span>
73 + <span class="discovery-account-chip-detail" x-show="chip.detail" x-text="chip.detail"></span>
74 + </span>
75 + </template>
76 + </div>
77 +
78 + <div class="discovery-account-usage" x-show="(card.usage_windows || []).length > 0">
79 + <template x-for="window in card.usage_windows || []" :key="window.key || window.title">
80 + <div class="discovery-account-usage-row">
81 + <div class="discovery-account-usage-label">
82 + <span x-text="window.title"></span>
83 + <small x-show="$store.discoveryStore.formatReset(window)"
84 + x-text="`Resets in ${$store.discoveryStore.formatReset(window)}`"></small>
85 + </div>
86 + <div class="discovery-account-usage-meter">
87 + <span class="discovery-account-usage-track">
88 + <span class="discovery-account-usage-fill"
89 + :style="`width: ${$store.discoveryStore.usageWidth(window)}`"></span>
90 </span>
142 - </template>
143 - </div>
144 - <div class="discovery-usage" x-show="(card.usage_windows || []).length">
145 - <template x-for="window in card.usage_windows" :key="`${card.id}-${window.key}`">
146 - <div class="discovery-usage-window">
147 - <div class="discovery-usage-head">
148 - <span>
149 - <span x-text="window.title"></span>
150 - <small x-show="window.label" x-text="window.label"></small>
151 - </span>
152 - <strong x-text="$store.discoveryStore.formatRemainingPercent(window)"></strong>
153 - </div>
154 - <div class="discovery-usage-bar" aria-hidden="true">
155 - <i :style="{ width: $store.discoveryStore.usageWidth(window) }"></i>
156 - </div>
157 - <p x-show="$store.discoveryStore.formatReset(window)" x-text="`Resets in ${$store.discoveryStore.formatReset(window)}`"></p>
158 - </div>
159 - </template>
91 + <span class="discovery-account-usage-value"
92 + x-text="$store.discoveryStore.formatRemainingPercent(window)"></span>
93 + </div>
94 </div>
161 - <button class="btn btn-ok"
162 - type="button"
163 - @click.stop="$store.discoveryStore.executeCta(card.cta_action)">
164 - <span x-text="card.cta_text"></span>
165 - <span class="material-symbols-outlined">arrow_forward</span>
166 - </button>
167 - </div>
168 -
169 - <div class="discovery-hero-thumb" x-show="card.thumbnail || card.icon">
170 - <template x-if="card.thumbnail">
171 - <img :src="card.thumbnail" :alt="card.title" loading="lazy">
172 - </template>
173 - <template x-if="!card.thumbnail && card.icon">
174 - <span class="material-symbols-outlined discovery-hero-icon" x-text="card.icon"></span>
175 - </template>
176 - </div>
177 - </article>
178 - </template>
179 -
180 - <div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
181 - <button type="button"
182 - class="discovery-undismiss-btn"
183 - @click="$store.discoveryStore.undismissCards()">
184 - <span class="material-symbols-outlined">undo</span>
185 - <span>Show dismissed suggestions</span>
95 + </template>
96 + </div>
97 +
98 + <button class="btn btn primary discovery-account-cta"
99 + type="button"
100 + @click="$store.discoveryStore.executeCta(card.cta_action)">
101 + <span x-text="card.cta_text"></span>
102 + <span class="material-symbols-outlined">arrow_forward</span>
103 </button>
187 - </div>
104 + </article>
105 + </template>
106 +
107 + <div class="discovery-undismiss" x-show="$store.discoveryStore.hasDismissedCards">
108 + <button type="button"
109 + class="discovery-undismiss-btn"
110 + @click="$store.discoveryStore.undismissCards()">
111 + <span class="material-symbols-outlined">undo</span>
112 + <span>Show dismissed suggestions</span>
113 + </button>
114 </div>
115 </div>
116 </template>
117
118 <style>
119 .discovery-slot {
194 - grid-column: 1 / -1;
195 - width: 100%;
196 - }
197 -
198 - .discovery-section {
199 - display: flex;
200 - flex-direction: column;
120 + display: grid;
121 gap: 1rem;
122 + min-width: 0;
123 width: 100%;
203 - margin-top: 0.1rem;
124 }
125
206 - .discovery-hero,
207 - .discovery-feature-card {
208 - position: relative;
209 - border: 1px solid var(--color-border);
210 - border-radius: 12px;
211 - background: var(--color-panel);
126 + .discovery-features-panel,
127 + .discovery-account-card {
128 + border: 1px solid color-mix(in srgb, var(--color-border) 66%, transparent);
129 + border-radius: 8px;
130 + background: color-mix(in srgb, var(--color-panel) 78%, var(--color-background) 22%);
131 + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.026);
132 + backdrop-filter: blur(18px);
133 text-align: left;
213 - transition:
214 - border-color 0.2s ease,
215 - background-color 0.2s ease,
216 - box-shadow 0.2s ease,
217 - transform 0.2s ease;
134 }
135
220 - .discovery-hero:hover,
221 - .discovery-feature-card:hover,
222 - .discovery-hero:focus-visible,
223 - .discovery-feature-card:focus-visible {
224 - border-color: color-mix(in srgb, var(--color-primary) 72%, white 6%);
225 - box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
226 - outline: none;
227 - }
228 -
229 - .discovery-hero {
230 - display: flex;
231 - align-items: center;
232 - gap: 1rem;
233 - min-height: 152px;
234 - overflow: hidden;
235 - padding: 1.25rem 1.25rem 1.25rem 1.5rem;
236 - background:
237 - linear-gradient(
238 - 135deg,
239 - color-mix(in srgb, var(--color-primary) 10%, transparent),
240 - transparent 58%
241 - ),
242 - radial-gradient(
243 - circle at 92% 24%,
244 - color-mix(in srgb, var(--color-primary) 12%, transparent),
245 - transparent 42%
246 - ),
247 - var(--color-panel);
248 - }
249 -
250 - .discovery-hero-content,
251 - .discovery-hero-thumb {
252 - position: relative;
253 - z-index: 1;
254 - }
255 -
256 - .discovery-hero-content {
257 - flex: 1 1 auto;
258 - min-width: 0;
259 - }
260 -
261 - .discovery-hero-title {
262 - margin: 0 0 var(--spacing-sm);
263 - color: var(--color-text);
264 - font-size: 1.08rem;
265 - font-weight: 600;
266 - letter-spacing: 0.01em;
267 - }
268 -
269 - .discovery-hero-desc {
270 - margin: 0 0 1rem;
271 - color: var(--color-text);
272 - font-size: 0.86rem;
273 - line-height: 1.5;
274 - max-width: 38ch;
136 + .discovery-features-panel {
137 + min-height: 100%;
138 + padding: 1.7rem 1.55rem 1.55rem;
139 }
140
277 - .discovery-usage {
141 + .discovery-panel-header {
142 display: grid;
279 - grid-template-columns: repeat(2, minmax(0, 1fr));
280 - gap: 0.6rem;
281 - max-width: 520px;
282 - margin: -0.15rem 0 1rem;
283 - }
284 -
285 - .discovery-account-chips {
286 - display: flex;
287 - flex-wrap: wrap;
288 - gap: 0.45rem;
289 - max-width: 560px;
290 - margin: -0.25rem 0 0.95rem;
291 - }
292 -
293 - .discovery-account-chip {
294 - display: inline-flex;
295 - min-width: 0;
296 - max-width: 100%;
297 - align-items: center;
298 - gap: 0.4rem;
299 - padding: 0.28rem 0.5rem;
300 - border: 1px solid color-mix(in srgb, var(--color-border) 74%, transparent);
301 - border-radius: 999px;
302 - background: color-mix(in srgb, var(--color-border) 28%, transparent);
303 - color: var(--color-text);
304 - font-size: 0.72rem;
305 - font-weight: 750;
306 - line-height: 1.15;
307 - }
308 -
309 - .discovery-account-chip.connected {
310 - border-color: color-mix(in srgb, #35d07f 44%, var(--color-border));
311 - background: color-mix(in srgb, #35d07f 13%, transparent);
312 - }
313 -
314 - .discovery-account-chip small {
315 - overflow: hidden;
316 - max-width: 18ch;
317 - color: var(--color-secondary);
318 - font-size: 0.68rem;
319 - font-weight: 700;
320 - text-overflow: ellipsis;
321 - white-space: nowrap;
322 - }
323 -
324 - .discovery-usage-window {
325 - display: grid;
326 - min-width: 0;
143 gap: 0.42rem;
144 + margin-bottom: 1.28rem;
145 }
146
330 - .discovery-usage-head {
331 - display: flex;
332 - align-items: center;
333 - justify-content: space-between;
334 - gap: 0.65rem;
335 - }
336 -
337 - .discovery-usage-head span {
338 - display: inline-flex;
339 - min-width: 0;
340 - align-items: center;
341 - gap: 0.35rem;
342 - color: var(--color-secondary);
343 - font-size: 0.75rem;
344 - font-weight: 700;
345 - }
346 -
347 - .discovery-usage-head small {
348 - padding: 0.12rem 0.34rem;
349 - border-radius: 999px;
350 - background: color-mix(in srgb, var(--color-border) 56%, transparent);
351 - color: var(--color-secondary);
352 - font-size: 0.68rem;
353 - line-height: 1;
354 - }
355 -
356 - .discovery-usage-head strong {
147 + .discovery-panel-header h2 {
148 + margin: 0;
149 color: var(--color-text);
358 - font-size: 0.78rem;
359 - white-space: nowrap;
360 - }
361 -
362 - .discovery-usage-bar {
363 - overflow: hidden;
364 - height: 0.48rem;
365 - border-radius: 999px;
366 - background: color-mix(in srgb, var(--color-border) 54%, transparent);
367 - }
368 -
369 - .discovery-usage-bar i {
370 - display: block;
371 - width: 0;
372 - height: 100%;
373 - border-radius: inherit;
374 - background: #35d07f;
375 - transition: width 0.22s ease;
150 + font-size: 1.05rem;
151 + font-weight: 520;
152 + line-height: 1.2;
153 }
154
378 - .discovery-usage-window p {
155 + .discovery-panel-header p {
156 margin: 0;
380 - color: var(--color-secondary);
381 - font-size: 0.7rem;
382 - }
383 -
384 - .discovery-cta-link,
385 - .discovery-undismiss-btn {
386 - display: inline-flex;
387 - align-items: center;
388 - justify-content: center;
389 - gap: 0.35rem;
390 - cursor: pointer;
391 - transition:
392 - background-color 0.2s ease,
393 - border-color 0.2s ease,
394 - color 0.2s ease,
395 - transform 0.2s ease;
396 - }
397 -
398 - .discovery-hero-thumb {
399 - flex: 0 0 132px;
400 - width: 132px;
401 - height: 108px;
402 - display: flex;
403 - align-items: center;
404 - justify-content: center;
405 - }
406 -
407 - .discovery-hero-thumb img {
408 - width: 100%;
409 - height: 100%;
410 - object-fit: contain;
411 - filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.12));
412 - }
413 -
414 - .discovery-hero-icon {
415 - font-size: 3rem;
416 - color: var(--color-highlight-dark);
157 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
158 + font-size: 0.92rem;
159 + line-height: 1.35;
160 }
161
162 .discovery-features {
163 display: grid;
164 grid-template-columns: repeat(3, minmax(0, 1fr));
422 - gap: 1rem;
165 + gap: 0.7rem;
166 }
167
168 .discovery-feature-card {
169 + position: relative;
170 display: flex;
171 flex-direction: column;
172 align-items: flex-start;
429 - min-height: 188px;
430 - padding: 1rem;
431 - background:
432 - linear-gradient(
433 - 180deg,
434 - color-mix(in srgb, var(--color-primary) 4%, transparent),
435 - transparent 38%
436 - ),
437 - var(--color-panel);
173 + text-align: left;
174 }
175
176 .discovery-feature-head {
@@ -444,6 +180,7 @@
180 gap: 0.75rem;
181 width: 100%;
182 min-width: 0;
183 + margin-bottom: 1rem;
184 }
185
186 .discovery-feature-thumb {
@@ -453,7 +190,7 @@
190 display: flex;
191 align-items: center;
192 justify-content: center;
456 - border-radius: 14px;
193 + border-radius: 999px;
194 overflow: hidden;
195 background: color-mix(in srgb, var(--color-primary) 10%, transparent);
196 box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 10%, transparent);
@@ -475,39 +212,34 @@
212 flex: 1 1 auto;
213 min-width: 0;
214 color: var(--color-text);
478 - font-size: 0.92rem;
479 - font-weight: 600;
215 + font-size: 0.96rem;
216 + font-weight: 520;
217 line-height: 1.25;
218 }
219
220 .discovery-feature-desc {
484 - margin: 0.55rem 0 0;
221 + margin: 1.12rem 0 0;
222 padding-bottom: var(--spacing-md);
486 - color: var(--color-text);
487 - font-size: 0.8rem;
488 - line-height: 1.45;
223 + color: color-mix(in srgb, var(--color-text) 72%, transparent);
224 + font-size: 0.86rem;
225 + line-height: 1.48;
226 }
227
228 .discovery-feature-card > .btn {
229 margin-top: auto;
493 - }
494 -
495 - .discovery-cta-link {
496 - margin-top: 0.95rem;
497 - border: 1px solid color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
230 + min-width: 6.1rem;
231 + min-height: 2.35rem;
232 + border: 0;
233 border-radius: 8px;
499 - background: transparent;
500 - color: var(--color-primary);
501 - padding: 0.42rem 0.72rem;
502 - font-size: 0.79rem;
503 - font-weight: 600;
234 + color: #fff;
235 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
236 + font-size: 0.88rem;
237 + font-weight: 460;
238 }
239
506 - .discovery-cta-link:hover,
507 - .discovery-cta-link:focus-visible {
508 - background: var(--color-primary);
509 - border-color: var(--color-primary);
510 - color: #fff;
240 + .discovery-feature-card > .btn:hover,
241 + .discovery-feature-card > .btn:focus-visible {
242 + filter: brightness(1.06);
243 outline: none;
244 }
245
@@ -540,10 +272,10 @@
272 height: 26px;
273 }
274
543 - .discovery-hero:hover .discovery-dismiss,
544 - .discovery-hero:focus-visible .discovery-dismiss,
275 .discovery-feature-card:hover .discovery-dismiss,
546 - .discovery-feature-card:focus-visible .discovery-dismiss {
276 + .discovery-feature-card:focus-visible .discovery-dismiss,
277 + .discovery-account-card:hover .discovery-dismiss,
278 + .discovery-account-card:focus-within .discovery-dismiss {
279 opacity: 0.68;
280 }
281
@@ -559,18 +291,226 @@
291 font-size: 1rem;
292 }
293
294 + .discovery-account-card {
295 + position: relative;
296 + display: grid;
297 + gap: 1.1rem;
298 + padding: 1.35rem 1.45rem 1.4rem;
299 + }
300 +
301 + .discovery-account-header {
302 + display: grid;
303 + grid-template-columns: auto minmax(0, 1fr);
304 + gap: 0.95rem;
305 + align-items: center;
306 + min-width: 0;
307 + padding-right: 2.1rem;
308 + }
309 +
310 + .discovery-account-icon {
311 + display: inline-flex;
312 + align-items: center;
313 + justify-content: center;
314 + width: 3rem;
315 + height: 3rem;
316 + border-radius: 10px;
317 + color: #2f6bff;
318 + background: color-mix(in srgb, var(--color-primary) 10%, transparent);
319 + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 12%, transparent);
320 + font-size: 1.7rem;
321 + }
322 +
323 + .discovery-account-copy {
324 + display: grid;
325 + gap: 0.3rem;
326 + min-width: 0;
327 + }
328 +
329 + .discovery-account-copy h2 {
330 + margin: 0;
331 + color: var(--color-text);
332 + font-size: 1.05rem;
333 + font-weight: 520;
334 + line-height: 1.2;
335 + }
336 +
337 + .discovery-account-copy p {
338 + margin: 0;
339 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
340 + font-size: 0.9rem;
341 + line-height: 1.42;
342 + }
343 +
344 + .discovery-account-chips {
345 + display: flex;
346 + flex-wrap: wrap;
347 + gap: 0.5rem;
348 + min-width: 0;
349 + }
350 +
351 + .discovery-account-chip {
352 + display: inline-grid;
353 + grid-template-columns: auto auto minmax(0, auto);
354 + align-items: center;
355 + gap: 0.4rem;
356 + max-width: 100%;
357 + min-height: 2rem;
358 + padding: 0.32rem 0.58rem;
359 + border: 1px solid color-mix(in srgb, var(--color-border) 52%, transparent);
360 + border-radius: 999px;
361 + background: color-mix(in srgb, var(--color-background) 50%, transparent);
362 + color: color-mix(in srgb, var(--color-text) 74%, transparent);
363 + font-size: 0.8rem;
364 + line-height: 1.2;
365 + }
366 +
367 + .discovery-account-chip-dot {
368 + width: 0.44rem;
369 + height: 0.44rem;
370 + border-radius: 999px;
371 + background: color-mix(in srgb, var(--color-text) 34%, transparent);
372 + }
373 +
374 + .discovery-account-chip.is-connected .discovery-account-chip-dot {
375 + background: #22c55e;
376 + }
377 +
378 + .discovery-account-chip-label,
379 + .discovery-account-chip-detail {
380 + min-width: 0;
381 + overflow: hidden;
382 + text-overflow: ellipsis;
383 + white-space: nowrap;
384 + }
385 +
386 + .discovery-account-chip-label {
387 + color: var(--color-text);
388 + font-weight: 500;
389 + }
390 +
391 + .discovery-account-chip-detail {
392 + color: color-mix(in srgb, var(--color-text) 52%, transparent);
393 + }
394 +
395 + .discovery-account-usage {
396 + display: grid;
397 + gap: 0.74rem;
398 + padding-top: 0.12rem;
399 + }
400 +
401 + .discovery-account-usage-row {
402 + display: grid;
403 + grid-template-columns: minmax(0, 1fr) minmax(8rem, 0.82fr);
404 + gap: 1rem;
405 + align-items: center;
406 + min-width: 0;
407 + }
408 +
409 + .discovery-account-usage-label {
410 + display: grid;
411 + gap: 0.22rem;
412 + min-width: 0;
413 + }
414 +
415 + .discovery-account-usage-label span,
416 + .discovery-account-usage-label small {
417 + min-width: 0;
418 + overflow: hidden;
419 + text-overflow: ellipsis;
420 + white-space: nowrap;
421 + }
422 +
423 + .discovery-account-usage-label span {
424 + color: color-mix(in srgb, var(--color-text) 74%, transparent);
425 + font-size: 0.84rem;
426 + line-height: 1.25;
427 + }
428 +
429 + .discovery-account-usage-label small {
430 + color: color-mix(in srgb, var(--color-text) 44%, transparent);
431 + font-size: 0.72rem;
432 + line-height: 1.2;
433 + }
434 +
435 + .discovery-account-usage-meter {
436 + display: grid;
437 + grid-template-columns: minmax(0, 1fr) auto;
438 + gap: 0.58rem;
439 + align-items: center;
440 + min-width: 0;
441 + }
442 +
443 + .discovery-account-usage-track {
444 + height: 0.52rem;
445 + overflow: hidden;
446 + border-radius: 999px;
447 + background: color-mix(in srgb, var(--color-border) 70%, transparent);
448 + }
449 +
450 + .discovery-account-usage-fill {
451 + display: block;
452 + height: 100%;
453 + border-radius: inherit;
454 + background: #22c55e;
455 + transition: width 0.22s ease;
456 + }
457 +
458 + .discovery-account-usage-value {
459 + color: color-mix(in srgb, var(--color-text) 70%, transparent);
460 + font-family: var(--font-family-code, "Roboto Mono", monospace);
461 + font-size: 0.74rem;
462 + line-height: 1;
463 + white-space: nowrap;
464 + }
465 +
466 + .discovery-account-cta {
467 + justify-self: start;
468 + display: inline-flex;
469 + align-items: center;
470 + justify-content: center;
471 + gap: 0.36rem;
472 + min-height: 2.35rem;
473 + min-width: 8.65rem;
474 + border: 0;
475 + border-radius: 8px;
476 + padding: 0.55rem 0.9rem;
477 + background-color: #4248f1;
478 + color: #fff;
479 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
480 + font-size: 0.88rem;
481 + font-weight: 460;
482 + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
483 + }
484 +
485 + .discovery-account-cta:hover,
486 + .discovery-account-cta:focus-visible {
487 + background-color: #353bc5;
488 + filter: none;
489 + outline: none;
490 + }
491 +
492 + .discovery-account-cta .material-symbols-outlined {
493 + font-size: 1.05rem;
494 + }
495 +
496 .discovery-undismiss {
497 display: flex;
498 justify-content: flex-start;
499 }
500
501 .discovery-undismiss-btn {
502 + display: inline-flex;
503 + align-items: center;
504 + justify-content: center;
505 + gap: 0.35rem;
506 border: none;
507 background: transparent;
508 padding: 0;
509 color: var(--color-secondary);
510 + cursor: pointer;
511 font-size: 0.8rem;
512 font-weight: 500;
513 + transition: color 0.2s ease;
514 }
515
516 .discovery-undismiss-btn:hover,
@@ -579,7 +519,6 @@
519 outline: none;
520 }
521
582 - .discovery-cta-link .material-symbols-outlined,
522 .discovery-undismiss-btn .material-symbols-outlined {
523 font-size: 1rem;
524 }
@@ -591,21 +530,6 @@
530 }
531
532 @media (max-width: 768px) {
594 - .discovery-section {
595 - gap: 0.85rem;
596 - }
597 -
598 - .discovery-hero {
599 - min-height: 0;
600 - padding: 1rem;
601 - }
602 -
603 - .discovery-hero-thumb {
604 - flex-basis: 92px;
605 - width: 92px;
606 - height: 76px;
607 - }
608 -
533 .discovery-features {
534 grid-template-columns: 1fr;
535 gap: 0.75rem;
@@ -617,12 +541,12 @@
541 gap: 0.75rem;
542 min-height: 0;
543 align-items: center;
620 - padding: 0.85rem 1rem;
544 }
545
546 .discovery-feature-head {
547 grid-column: 1;
548 min-width: 0;
549 + margin-bottom: 0;
550 }
551
552 .discovery-feature-thumb {
@@ -641,8 +565,9 @@
565 margin-top: 0;
566 }
567
644 - .discovery-cta-link {
645 - margin-top: 0;
568 + .discovery-account-usage-row {
569 + grid-template-columns: 1fr;
570 + gap: 0.45rem;
571 }
572 }
573
@@ -651,29 +576,28 @@
576 margin-top: 0.15rem;
577 }
578
654 - .discovery-hero {
655 - padding-right: 0.95rem;
579 + .discovery-feature-card {
580 + grid-template-columns: minmax(0, 1fr) auto;
581 + padding-right: 3rem;
582 }
583
658 - .discovery-hero-thumb {
659 - display: none;
584 + .discovery-feature-card > .btn {
585 + justify-self: end;
586 }
587
662 - .discovery-hero-title {
663 - font-size: 1rem;
588 + .discovery-account-card {
589 + padding: 1.05rem;
590 }
591
666 - .discovery-usage {
592 + .discovery-account-header {
593 grid-template-columns: 1fr;
594 + padding-right: 2.2rem;
595 }
596
670 - .discovery-feature-card {
671 - grid-template-columns: minmax(0, 1fr) auto;
672 - padding-right: 3rem;
673 - }
674 -
675 - .discovery-feature-card > .btn {
676 - justify-self: end;
597 + .discovery-account-icon {
598 + width: 2.55rem;
599 + height: 2.55rem;
600 + font-size: 1.45rem;
601 }
602 }
603 </style>
plugins/_discovery/webui/discovery-store.js
+4
@@ -154,6 +154,10 @@ const model = {
154 return this.cards.filter((card) => card.type === "hero" && card.placement === "after-features");
155 },
156
157 + get oauthAccountCards() {
158 + return this.bottomHeroCards.filter((card) => card.id === "discovery-oauth-accounts");
159 + },
160 +
161 get heroCards() {
162 return [...this.topHeroCards, ...this.bottomHeroCards];
163 },
plugins/_whisper_stt/extensions/webui/chat-input-box-end/microphone-button.html
+33 -16
@@ -1,58 +1,75 @@
1 <div x-data>
2 <style>
3 #microphone-button {
4 - color: var(--color-background);
4 + order: 1;
5 + border-color: transparent;
6 + border-radius: 12px;
7 + background-color: transparent;
8 + color: grey;
9 transition:
10 background-color 0.2s ease,
11 + border-color 0.2s ease,
12 box-shadow 0.12s ease-in-out,
8 - filter 0.12s ease-in-out,
13 + color 0.2s ease,
14 opacity 0.12s ease-in-out;
15 }
16
17 + #microphone-button svg {
18 + transform-origin: center;
19 + transition: color 0.2s ease, transform 0.12s ease-in-out;
20 + }
21 +
22 #microphone-button.mic-disabled {
13 - background-color: #5f6368;
23 + color: #5f6368;
24 cursor: not-allowed;
25 opacity: 0.58;
26 }
27
28 #microphone-button.mic-inactive {
19 - background-color: grey;
29 + color: grey;
30 }
31
32 #microphone-button.mic-activating {
23 - background-color: silver;
24 - animation: whisper-stt-mic-pulse 0.8s infinite;
33 + color: silver;
34 }
35
36 #microphone-button.mic-listening {
28 - background-color: red;
37 + color: red;
38 }
39
40 #microphone-button.mic-recording {
32 - background-color: green;
41 + color: green;
42 }
43
44 #microphone-button.mic-waiting {
36 - background-color: teal;
45 + color: teal;
46 }
47
48 #microphone-button.mic-processing {
40 - background-color: darkcyan;
49 + color: darkcyan;
50 + }
51 +
52 + #microphone-button.mic-activating svg,
53 + #microphone-button.mic-processing svg {
54 animation: whisper-stt-mic-pulse 0.8s infinite;
42 - transform-origin: center;
55 }
56
57 @media (hover: hover) {
58 #microphone-button:not(.mic-disabled):hover {
47 - filter: brightness(1.08);
48 - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08),
49 - 0 6px 14px rgba(0, 0, 0, 0.18);
59 + box-shadow: none;
60 + }
61 +
62 + #microphone-button:not(.mic-disabled):hover svg {
63 + transform: scale(1.08);
64 }
65 }
66
67 #microphone-button:not(.mic-disabled):active {
54 - filter: brightness(0.92);
55 - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
68 + box-shadow: none;
69 + }
70 +
71 + #microphone-button:not(.mic-disabled):active svg {
72 + transform: scale(0.92);
73 }
74
75 @keyframes whisper-stt-mic-pulse {
plugins/_whisper_stt/webui/whisper-stt.css
+33 -16
@@ -1,56 +1,73 @@
1 #microphone-button {
2 - color: var(--color-background);
2 + order: 1;
3 + border-color: transparent;
4 + border-radius: 12px;
5 + background-color: transparent;
6 + color: grey;
7 transition:
8 background-color 0.2s ease,
9 + border-color 0.2s ease,
10 box-shadow 0.12s ease-in-out,
6 - filter 0.12s ease-in-out,
11 + color 0.2s ease,
12 opacity 0.12s ease-in-out;
13 }
14
15 +#microphone-button svg {
16 + transform-origin: center;
17 + transition: color 0.2s ease, transform 0.12s ease-in-out;
18 +}
19 +
20 #microphone-button.mic-disabled {
11 - background-color: #5f6368;
21 + color: #5f6368;
22 cursor: not-allowed;
23 opacity: 0.58;
24 }
25
26 #microphone-button.mic-inactive {
17 - background-color: grey;
27 + color: grey;
28 }
29
30 #microphone-button.mic-activating {
21 - background-color: silver;
22 - animation: whisper-stt-mic-pulse 0.8s infinite;
31 + color: silver;
32 }
33
34 #microphone-button.mic-listening {
26 - background-color: red;
35 + color: red;
36 }
37
38 #microphone-button.mic-recording {
30 - background-color: green;
39 + color: green;
40 }
41
42 #microphone-button.mic-waiting {
34 - background-color: teal;
43 + color: teal;
44 }
45
46 #microphone-button.mic-processing {
38 - background-color: darkcyan;
47 + color: darkcyan;
48 +}
49 +
50 +#microphone-button.mic-activating svg,
51 +#microphone-button.mic-processing svg {
52 animation: whisper-stt-mic-pulse 0.8s infinite;
40 - transform-origin: center;
53 }
54
55 @media (hover: hover) {
56 #microphone-button:not(.mic-disabled):hover {
45 - filter: brightness(1.08);
46 - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08),
47 - 0 6px 14px rgba(0, 0, 0, 0.18);
57 + box-shadow: none;
58 + }
59 +
60 + #microphone-button:not(.mic-disabled):hover svg {
61 + transform: scale(1.08);
62 }
63 }
64
65 #microphone-button:not(.mic-disabled):active {
52 - filter: brightness(0.92);
53 - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
66 + box-shadow: none;
67 +}
68 +
69 +#microphone-button:not(.mic-disabled):active svg {
70 + transform: scale(0.92);
71 }
72
73 @keyframes whisper-stt-mic-pulse {
tests/test_oauth_static.py
+8 -5
@@ -166,7 +166,7 @@ def test_oauth_model_wrappers_do_not_add_box_borders_or_lateral_padding():
166 assert "'is-codex'" not in config_html
167
168
169 -def test_connected_codex_welcome_card_renders_usage_limit_bars():
169 +def test_oauth_discovery_card_renders_in_welcome_account_panel():
170 discovery_cards = (
171 PROJECT_ROOT
172 / "plugins/_discovery/extensions/python/banners/10_discovery_cards.py"
@@ -186,8 +186,11 @@ def test_connected_codex_welcome_card_renders_usage_limit_bars():
186 assert '"icon": "account_circle"' not in discovery_cards
187 assert "usage_windows" in discovery_cards
188 assert "account_chips" in discovery_cards
189 + assert "discovery-account-card" in welcome_cards
190 assert "discovery-account-chip" in welcome_cards
190 - assert 'x-show="card.thumbnail || card.icon"' in welcome_cards
191 - assert "discovery-usage" in welcome_cards
192 - assert "discovery-usage-bar" in welcome_cards
193 - assert "formatRemainingPercent(window)" in welcome_cards + discovery_store
191 + assert "discovery-account-usage" in welcome_cards
192 + assert "formatRemainingPercent(window)" in welcome_cards
193 + assert "formatRemainingPercent(window)" in discovery_store
194 + assert "usageWidth(window)" in discovery_store
195 + assert "oauthAccountCards" in discovery_store
196 + assert 'card.id === "discovery-oauth-accounts"' in discovery_store
tests/test_speech_plugin_split.py
+14 -4
@@ -215,9 +215,19 @@ def test_chat_bar_keeps_existing_send_and_mic_icon_contract() -> None:
215 assert f"#microphone-button.mic-{state}" in whisper_css
216 assert f"#microphone-button.mic-{state}" in mic_extension
217
218 - assert "background-color: red;" in whisper_css
219 - assert "background-color: green;" in whisper_css
220 - assert "background-color: teal;" in whisper_css
221 - assert "background-color: grey;" in mic_extension
218 + assert "border-radius: 12px;" in whisper_css
219 + assert "background-color: transparent;" in whisper_css
220 + assert "#microphone-button.mic-inactive {\n color: grey;" in whisper_css
221 + assert "background-color: grey;" not in whisper_css
222 + assert "#microphone-button.mic-listening {\n color: red;" in whisper_css
223 + assert "#microphone-button.mic-recording {\n color: green;" in whisper_css
224 + assert "#microphone-button.mic-waiting {\n color: teal;" in whisper_css
225 + assert "#microphone-button.mic-processing {\n color: darkcyan;" in whisper_css
226 + assert "#microphone-button.mic-activating svg" in whisper_css
227 + assert "#microphone-button.mic-processing svg" in whisper_css
228 + assert "#microphone-button:not(.mic-disabled):hover svg" in whisper_css
229 + assert "#microphone-button:not(.mic-disabled):active svg" in whisper_css
230 + assert "box-shadow: none;" in whisper_css
231 + assert "order: 1;" in mic_extension
232 assert "whisper-stt-mic-pulse 0.8s infinite" in mic_extension
233 assert "whisper-stt-mic-pulse 0.8s infinite" in whisper_css
tests/test_welcome_composer_static.py new
+93
@@ -0,0 +1,93 @@
1 +from pathlib import Path
2 +
3 +
4 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
5 +
6 +
7 +def _read(relative_path: str) -> str:
8 + return (PROJECT_ROOT / relative_path).read_text(encoding="utf-8")
9 +
10 +
11 +def test_welcome_screen_embeds_shared_new_chat_composer() -> None:
12 + welcome = _read("webui/components/welcome/welcome-screen.html")
13 + index = _read("webui/index.html")
14 + discovery_cards = _read(
15 + "plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html"
16 + )
17 +
18 + assert 'class="welcome-composer"' in welcome
19 + assert 'path="chat/attachments/inputPreview.html"' in welcome
20 + assert 'path="chat/input/chat-bar-input.html"' in welcome
21 + assert '<h2>Quick Actions</h2>' in welcome
22 + assert 'class="welcome-lower-grid"' in welcome
23 + assert 'x-extension id="welcome-actions-end"' in welcome
24 +
25 + assert "x-if=\"$store.welcomeStore && $store.welcomeStore.isVisible\"" in index
26 + assert "x-if=\"!$store.welcomeStore || !$store.welcomeStore.isVisible\"" in index
27 + assert "Connect Channels" in discovery_cards
28 + assert "oauthAccountCards" in discovery_cards
29 + assert "discovery-account-card" in discovery_cards
30 + assert "topHeroCards" not in discovery_cards
31 + assert "bottomHeroCards" not in discovery_cards
32 + assert "background: var(--color-background);" in welcome
33 + assert "radial-gradient" not in welcome
34 +
35 +
36 +def test_welcome_composer_can_create_a_chat_before_sending() -> None:
37 + input_store = _read("webui/components/chat/input/input-store.js")
38 + chats_store = _read("webui/components/sidebar/chats/chats-store.js")
39 +
40 + assert 'return "Ask anything to start a new chat";' in input_store
41 + assert "if (!chatsStore.selected" in input_store
42 + assert "await chatsStore.newChat()" in input_store
43 + assert "return response.ctxid;" in chats_store
44 + assert 'return "arrow_forward";' in input_store
45 +
46 +
47 +def test_welcome_composer_buttons_keep_target_geometry_without_glow() -> None:
48 + chat_bar = _read("webui/components/chat/input/chat-bar-input.html")
49 + mic_css = _read("plugins/_whisper_stt/webui/whisper-stt.css")
50 +
51 + assert "#send-button {\n order: 2;\n border-radius: 12px;" in chat_bar
52 + assert "background-color: #4248f1;" in chat_bar
53 + assert "background-color: #e67e22;" in chat_bar
54 + assert "linear-gradient" not in chat_bar
55 + assert "#microphone-button {\n order: 1;" in mic_css
56 + assert "border-radius: 12px;" in mic_css
57 + assert "background-color: transparent;" in mic_css
58 + assert "#microphone-button.mic-inactive {\n color: grey;" in mic_css
59 + assert "background-color: grey;" not in mic_css
60 + assert "#microphone-button.mic-listening {\n color: red;" in mic_css
61 + assert "#microphone-button.mic-recording {\n color: green;" in mic_css
62 + assert "#microphone-button.mic-activating svg" in mic_css
63 + assert "rgba(34, 68, 255" not in chat_bar
64 + assert "rgba(217, 119, 6" not in chat_bar
65 +
66 +
67 +def test_welcome_hover_and_focus_borders_use_neutral_tokens() -> None:
68 + surfaces = "\n".join(
69 + [
70 + _read("webui/components/welcome/welcome-screen.html"),
71 + _read("webui/components/chat/input/chat-bar-input.html"),
72 + _read("plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html"),
73 + ]
74 + )
75 +
76 + assert "#315cf6" not in surfaces
77 + assert "rgba(34, 68, 255" not in surfaces
78 + assert "linear-gradient" not in surfaces
79 + assert "border-color: color-mix(in srgb, var(--color-primary" not in surfaces
80 + assert "border-color: var(--color-primary" not in surfaces
81 + assert "border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%)" in surfaces
82 +
83 +
84 +def test_composer_uses_rubik_until_caret_enters_fenced_code() -> None:
85 + chat_bar = _read("webui/components/chat/input/chat-bar-input.html")
86 + input_store = _read("webui/components/chat/input/input-store.js")
87 +
88 + assert "font-family: var(--font-family-main" in chat_bar
89 + assert "#chat-input.is-code-context" in chat_bar
90 + assert "font-family: var(--font-family-code" in chat_bar
91 + assert "'is-code-context': $store.chatInput.isCodeContext" in chat_bar
92 + assert "_isCaretInsideFence(text, index)" in input_store
93 + assert "matches.length % 2 === 1" in input_store
webui/AGENTS.md
+1
@@ -26,6 +26,7 @@
26 - Do not bypass WebSocket origin/auth/CSRF assumptions from frontend code.
27 - Avoid editing vendored files unless intentionally updating the vendor asset.
28 - Rubik (`--font-family-main`) is the default WebUI text and control font; use the code/mono font tokens only for code, logs, paths, and fixed-width data.
29 +- Hover, focus, and active border treatments should follow existing neutral border/background patterns; avoid hard-coded blue border highlights unless matching an established specialized surface.
30
31 ## Work Guidance
32
webui/components/chat/AGENTS.md
+2
@@ -17,6 +17,8 @@
17 - Preserve Store Gating for all store-backed chat components.
18 - Use shared API, WebSocket, notification, and attachment helpers where available.
19 - Do not bypass CSRF or WebSocket state-sync expectations.
20 +- The shared composer can be mounted on the Welcome screen with no selected chat; sending from that state must create and select a chat context before dispatch.
21 +- Composer text uses the main UI font by default and switches to the code font while the caret is inside an open triple-backtick fenced block.
22
23 ## Work Guidance
24
webui/components/chat/attachments/attachmentsStore.js
+4 -1
@@ -136,7 +136,10 @@ const model = {
136 console.log("Setting up paste handler...");
137 document.addEventListener("paste", (e) => {
138 // console.log("Paste event detected, target:", e.target.id);
139 - if(e.target.id != "chat-input" && e.target.id != "full-screen-input") return;
139 + if (
140 + e.target.id !== "chat-input" &&
141 + e.target.id !== "full-screen-input"
142 + ) return;
143
144 const items = e.clipboardData.items;
145 let imageFound = false;
webui/components/chat/input/bottom-actions.html
+6 -4
@@ -14,7 +14,7 @@
14 <div class="chat-bottom-actions-menu">
15 <input
16 type="file"
17 - id="chat-file-input"
17 + x-ref="chatFileInput"
18 class="chat-bottom-menu-file-input"
19 accept="*"
20 multiple
@@ -23,7 +23,7 @@
23 <button
24 type="button"
25 class="chat-bottom-menu-item"
26 - @click.stop="document.getElementById('chat-file-input')?.click()"
26 + @click.stop="$refs.chatFileInput?.click()"
27 >
28 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
29 <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
@@ -33,7 +33,7 @@
33
34 <input
35 type="file"
36 - id="chat-folder-input"
36 + x-ref="chatFolderInput"
37 class="chat-bottom-menu-file-input"
38 webkitdirectory
39 directory
@@ -43,7 +43,7 @@
43 <button
44 type="button"
45 class="chat-bottom-menu-item"
46 - @click.stop="document.getElementById('chat-folder-input')?.click()"
46 + @click.stop="$refs.chatFolderInput?.click()"
47 >
48 <span class="material-symbols-outlined" aria-hidden="true">drive_folder_upload</span>
49 <span>Attach folder</span>
@@ -73,6 +73,7 @@
73 class="chat-bottom-menu-item"
74 id="history_inspect"
75 @click="$store.history.open(); $store.chatInput.closeChatMoreMenu()"
76 + :disabled="!$store.chats.selected"
77 >
78 <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85" width="18" height="18" aria-hidden="true">
79 <path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
@@ -85,6 +86,7 @@
86 class="chat-bottom-menu-item"
87 id="ctx_window"
88 @click="$store.context.open(); $store.chatInput.closeChatMoreMenu()"
89 + :disabled="!$store.chats.selected"
90 >
91 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" width="18" height="18" fill="currentColor" aria-hidden="true">
92 <path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
webui/components/chat/input/chat-bar-input.html
+111 -57
@@ -25,11 +25,16 @@
25 </div>
26
27 <!-- Container for textarea and expand button -->
28 - <div id="chat-input-container" style="position: relative;">
28 + <div id="chat-input-container">
29 <textarea id="chat-input" :placeholder="$store.chatInput.inputPlaceholder" rows="1"
30 @keydown.enter="if (!$event.shiftKey && !$event.isComposing && $event.keyCode !== 229) { $event.preventDefault(); $store.chatInput.sendMessage(); }"
31 @keydown.up="$store.chatInput.historyPrev($event)" @keydown.down="$store.chatInput.historyNext($event)"
32 - @input="$store.chatInput.adjustTextareaHeight()" x-model="$store.chatInput.message"></textarea>
32 + @input="$store.chatInput.handleInput($event)"
33 + @keyup="$store.chatInput._syncCaretFromEvent($event)"
34 + @click="$store.chatInput._syncCaretFromEvent($event)"
35 + @select="$store.chatInput._syncCaretFromEvent($event)"
36 + :class="{ 'progress-active': $store.chatInput.progressActive, 'is-code-context': $store.chatInput.isCodeContext }"
37 + x-model="$store.chatInput.message"></textarea>
38 <div id="chat-input-progress-placeholder" x-show="$store.chatInput.showProgressPlaceholder"
39 x-html="$store.chatInput.progressPlaceholderHtml"
40 :class="{ 'progress-active': $store.chatInput.progressActive }" aria-hidden="true" style="display: none;">
@@ -59,9 +64,25 @@
64 .input-row {
65 display: flex;
66 align-items: center;
62 - gap: var(--spacing-xs);
67 + gap: 0.72rem;
68 width: 100%;
69 + min-height: 4.25rem;
70 + padding: 0.42rem 0.52rem 0.42rem 0.72rem;
71 + border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
72 + border-radius: 16px;
73 + background: color-mix(in srgb, var(--color-panel) 72%, var(--color-background) 28%);
74 + box-shadow:
75 + inset 0 1px 0 rgba(255, 255, 255, 0.035),
76 + 0 18px 46px rgba(0, 0, 0, 0.18);
77 white-space: nowrap;
78 + transition: border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
79 + }
80 +
81 + .input-row:focus-within {
82 + border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
83 + box-shadow:
84 + inset 0 1px 0 rgba(255, 255, 255, 0.045),
85 + 0 22px 54px rgba(0, 0, 0, 0.22);
86 }
87
88 .attachment-wrapper {
@@ -77,21 +98,22 @@
98 .chat-more-trigger {
99 cursor: pointer;
100 color: var(--color-text);
80 - opacity: 0.75;
81 - transition: opacity 0.2s ease;
101 + opacity: 0.82;
102 + transition: opacity 0.16s ease, background-color 0.16s ease, color 0.16s ease;
103 display: flex;
104 align-items: center;
105 justify-content: center;
85 - width: 2.25rem;
86 - height: 2.25rem;
106 + width: 2.5rem;
107 + height: 2.5rem;
108 padding: 0;
109 border: none;
89 - border-radius: 50%;
110 + border-radius: 10px;
111 background: transparent;
112 }
113
114 .chat-more-trigger:hover {
115 opacity: 1;
116 + background: color-mix(in srgb, var(--color-border) 36%, transparent);
117 }
118
119 .chat-more-trigger:active {
@@ -99,7 +121,7 @@
121 }
122
123 .chat-more-trigger .material-symbols-outlined {
102 - font-size: 1.5rem;
124 + font-size: 1.7rem;
125 line-height: 1;
126 }
127
@@ -113,12 +135,12 @@
135 max-height: min(70vh, 20rem);
136 overflow-x: hidden;
137 overflow-y: auto;
116 - background-color: var(--color-panel);
138 + background-color: color-mix(in srgb, var(--color-panel) 90%, black 10%);
139 border: 1px solid var(--color-border);
118 - border-radius: 0.4rem;
119 - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
140 + border-radius: 12px;
141 + box-shadow: 0 16px 38px rgba(0, 0, 0, 0.3);
142 z-index: 1050;
121 - padding: 0.2rem 0;
143 + padding: 0.3rem;
144 }
145
146 .light-mode .chat-more-panel {
@@ -129,32 +151,41 @@
151 #chat-input-container {
152 position: relative;
153 width: 100%;
154 + min-width: 0;
155 + display: flex;
156 + align-items: center;
157 }
158
159 #chat-input {
135 - background-color: var(--color-input);
136 - border: 1px solid var(--color-border);
137 - border-radius: 8px;
160 + background-color: transparent;
161 + border: 0;
162 + border-radius: 0;
163 color: var(--color-text);
164 flex-grow: 1;
140 - font-family: "Roboto Mono", monospace;
165 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
166 font-optical-sizing: auto;
167 -webkit-font-optical-sizing: auto;
143 - font-size: 0.9rem;
168 + font-size: 1rem;
169 + line-height: 1.45;
170 max-height: 30vh;
145 - min-height: 3.05rem;
171 + min-height: 2.8rem;
172 width: 100%;
147 - padding: 0.48rem 40px var(--spacing-sm) var(--spacing-sm);
148 - margin-right: var(--spacing-xs);
173 + padding: 0.68rem 2.2rem 0.68rem 0.1rem;
174 + margin: 0;
175 overflow-y: auto;
176 scroll-behavior: smooth;
177 resize: none;
152 - align-content: start;
178 + align-content: center;
179 background-clip: border-box;
154 - border: 6px solid transparent;
155 - outline: 1px solid var(--color-border);
156 - transition: all 0.3s ease;
157 - -webkit-transition: all 0.3s ease;
180 + outline: none;
181 + transition: color 0.16s ease, background-color 0.16s ease;
182 + -webkit-transition: color 0.16s ease, background-color 0.16s ease;
183 + }
184 +
185 + #chat-input.is-code-context {
186 + font-family: var(--font-family-code, "Roboto Mono", monospace);
187 + font-size: 0.94rem;
188 + align-content: start;
189 }
190
191 #chat-input::-webkit-scrollbar {
@@ -180,16 +211,14 @@
211 }
212
213 #chat-input:focus {
183 - outline: 0.05rem solid rgba(155, 155, 155, 0.5);
184 - font-size: 0.955rem;
185 - padding-top: 0.45rem;
186 - background-color: var(--color-input-focus);
214 + outline: none;
215 + background-color: transparent;
216 }
217
218 #chat-input::placeholder {
190 - color: var(--color-text-muted);
191 - opacity: 0.7;
192 - transition: color 0.3s ease;
219 + color: color-mix(in srgb, var(--color-text) 62%, transparent);
220 + opacity: 0.78;
221 + transition: color 0.16s ease;
222 }
223
224 /* Progress ghost text animation — gentle pulse on placeholder */
@@ -216,12 +245,12 @@
245
246 #chat-input-progress-placeholder {
247 position: absolute;
219 - top: calc(6px + 0.48rem);
220 - left: calc(6px + var(--spacing-sm));
248 + top: 0.74rem;
249 + left: 0.12rem;
250 right: 46px;
251 color: var(--color-text-muted);
252 opacity: 0.7;
224 - font-family: "Roboto Mono", monospace;
253 + font-family: var(--font-family-code, "Roboto Mono", monospace);
254 font-size: 0.9rem;
255 line-height: 1.45;
256 white-space: nowrap;
@@ -251,7 +280,7 @@
280 #expand-button {
281 position: absolute;
282 top: 12px;
254 - right: 6px;
283 + right: 0;
284 background: transparent;
285 border: none;
286 cursor: pointer;
@@ -278,52 +307,60 @@
307
308 /* Chat buttons (Send/Mic) */
309 #chat-buttons-wrapper {
281 - gap: var(--spacing-xs);
282 - padding-left: var(--spacing-xs);
283 - line-height: 0.5rem;
310 + gap: 0.58rem;
311 + padding-left: 0;
312 + line-height: 1;
313 display: -webkit-flex;
314 display: flex;
315 + align-items: center;
316 }
317
318 .chat-button {
289 - border: none;
319 + border: 1px solid transparent;
320 border-radius: 50%;
291 - color: var(--color-background);
321 + color: #fff;
322 cursor: pointer;
323 font-size: var(--font-size-normal);
294 - height: 2.525rem;
295 - width: 2.525rem;
296 - margin: 0 0.18rem 0 0 var(--spacing-xs);
324 + height: 3.15rem;
325 + width: 3.15rem;
326 + margin: 0;
327 display: -webkit-flex;
328 display: flex;
329 align-items: center;
330 justify-content: center;
331 flex-shrink: 0;
332 flex-grow: 0;
303 - min-width: 2.525rem;
304 - -webkit-transition: background-color var(--transition-speed), box-shadow 0.12s ease-in-out, filter 0.12s ease-in-out;
305 - transition: background-color var(--transition-speed), box-shadow 0.12s ease-in-out, filter 0.12s ease-in-out;
333 + min-width: 3.15rem;
334 + -webkit-transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
335 + transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
336 }
337
338 #send-button {
339 + order: 2;
340 + border-radius: 12px;
341 background-color: #4248f1;
342 + box-shadow: none;
343 }
344
345 #send-button.send-queue {
346 background-color: #e67e22;
347 + box-shadow: none;
348 }
349
350 #send-button:hover {
351 background-color: #353bc5;
318 - box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 6px 14px rgba(0, 0, 0, 0.2);
352 + filter: none;
353 + box-shadow: none;
354 }
355
356 #send-button.send-queue:hover {
357 background-color: #d35400;
358 + filter: none;
359 }
360
361 #send-button:active {
362 background-color: #2b309c;
363 + transform: translateY(1px) scale(0.98);
364 filter: brightness(0.96);
365 }
366
@@ -333,25 +370,42 @@
370 }
371
372 #send-button .material-symbols-outlined {
336 - font-size: 1.5rem;
373 + font-size: 1.78rem;
374 }
375
376 /* Responsive tweaks */
377 @media (max-width: 640px) {
378 #chat-input {
342 - min-height: 5.3rem;
343 - align-content: start;
379 + min-height: 4.2rem;
380 + align-content: center;
381 }
382
383 #chat-buttons-wrapper {
384 display: flex;
348 - gap: var(--spacing-xs);
349 - padding: 0 0 0 var(--spacing-sm) !important;
350 - width: 3.5rem;
351 - flex-wrap: wrap;
385 + gap: 0.42rem;
386 + padding: 0 !important;
387 + width: auto;
388 + flex-wrap: nowrap;
389 -webkit-transition: all 0.3s ease;
390 transition: all 0.3s ease;
391 }
392 +
393 + .input-row {
394 + gap: 0.42rem;
395 + min-height: 4rem;
396 + padding: 0.36rem 0.42rem 0.36rem 0.48rem;
397 + }
398 +
399 + .chat-more-trigger {
400 + width: 2.15rem;
401 + height: 2.15rem;
402 + }
403 +
404 + .chat-button {
405 + width: 2.75rem;
406 + height: 2.75rem;
407 + min-width: 2.75rem;
408 + }
409 }
410
411 @media (min-width: 768px) {
@@ -364,4 +418,4 @@
418 </style>
419 </body>
420
367 -</html>
\ No newline at end of file
421 +</html>
webui/components/chat/input/input-store.js
+62 -9
@@ -56,6 +56,7 @@ const model = {
56 chatMoreMenuOpen: false,
57 progressText: "",
58 progressActive: false,
59 + _caretIndex: 0,
60
61 toggleChatMoreMenu() {
62 this.chatMoreMenuOpen = !this.chatMoreMenuOpen;
@@ -76,12 +77,17 @@ const model = {
77 },
78
79 get inputPlaceholder() {
80 + if (!chatsStore.selected) return "Ask anything to start a new chat";
81 const state = this._getSendState();
82 if (state === "all") return "Press Enter to send queued messages";
83 if (this.showProgressPlaceholder) return "";
84 return "Type your message here...";
85 },
86
87 + get isCodeContext() {
88 + return this._isCaretInsideFence(this.message, this._caretIndex);
89 + },
90 +
91 get showProgressPlaceholder() {
92 return (
93 this._getSendState() !== "all" &&
@@ -102,7 +108,7 @@ const model = {
108 const state = this._getSendState();
109 if (state === "all") return "send_and_archive";
110 if (state === "queue") return "schedule_send";
105 - return "send";
111 + return "arrow_forward";
112 },
113
114 // Computed: send button CSS class
@@ -129,15 +135,59 @@ const model = {
135 async sendMessage() {
136 // Capture sent prompt to per-chat history (bash-style)
137 try { this._pushHistory(this.message); } catch (_e) { /* ignore */ }
138 +
139 + if (!chatsStore.selected && (this.message.trim() || attachmentsStore?.attachments?.length > 0)) {
140 + const ctxid = await chatsStore.newChat();
141 + if (!ctxid && !chatsStore.selected) return;
142 + }
143 +
144 // Delegate to the global function
145 if (globalThis.sendMessage) {
146 await globalThis.sendMessage();
147 }
148 },
149
138 - adjustTextareaHeight() {
139 - const chatInput = document.getElementById("chat-input");
140 - if (chatInput) {
150 + _composerTextareas(target = null) {
151 + if (target?.tagName === "TEXTAREA") return [target];
152 + return ["chat-input"]
153 + .map((id) => document.getElementById(id))
154 + .filter(Boolean);
155 + },
156 +
157 + _activeTextarea() {
158 + const active = document.activeElement;
159 + if (active?.id === "chat-input") {
160 + return active;
161 + }
162 + return document.getElementById("chat-input");
163 + },
164 +
165 + _syncCaretFromEvent($event = null) {
166 + const ta = ($event && $event.target?.tagName === "TEXTAREA")
167 + ? $event.target
168 + : this._activeTextarea();
169 + if (!ta) {
170 + this._caretIndex = this.message.length;
171 + return;
172 + }
173 + this._caretIndex = Number.isFinite(ta.selectionStart) ? ta.selectionStart : this.message.length;
174 + },
175 +
176 + _isCaretInsideFence(text, index) {
177 + const source = String(text || "");
178 + const caret = Math.max(0, Math.min(source.length, Number(index) || 0));
179 + const matches = source.slice(0, caret).match(/```/g);
180 + return Boolean(matches && matches.length % 2 === 1);
181 + },
182 +
183 + handleInput($event) {
184 + this._syncCaretFromEvent($event);
185 + this.adjustTextareaHeight($event);
186 + },
187 +
188 + adjustTextareaHeight($event = null) {
189 + const target = $event?.target || null;
190 + for (const chatInput of this._composerTextareas(target)) {
191 if (!this.message) chatInput.value = "";
192 chatInput.style.height = "auto";
193 chatInput.style.height = chatInput.scrollHeight + "px";
@@ -284,7 +334,7 @@ const model = {
334 },
335
336 focus() {
287 - const chatInput = document.getElementById("chat-input");
337 + const chatInput = this._activeTextarea();
338 if (chatInput) {
339 chatInput.focus();
340 }
@@ -379,10 +429,11 @@ const model = {
429
430 _setCaretStart() {
431 queueMicrotask(() => {
382 - const ta = document.getElementById("chat-input");
432 + const ta = this._activeTextarea();
433 if (ta) {
434 try { ta.setSelectionRange(0, 0); } catch (_e) { /* ignore */ }
435 try { ta.scrollTop = 0; } catch (_e) { /* ignore */ }
436 + this._syncCaretFromEvent({ target: ta });
437 }
438 this.adjustTextareaHeight();
439 });
@@ -390,11 +441,12 @@ const model = {
441
442 _setCaretEnd() {
443 queueMicrotask(() => {
393 - const ta = document.getElementById("chat-input");
444 + const ta = this._activeTextarea();
445 if (ta) {
446 const end = ta.value.length;
447 try { ta.setSelectionRange(end, end); } catch (_e) { /* ignore */ }
448 try { ta.scrollTop = ta.scrollHeight; } catch (_e) { /* ignore */ }
449 + this._syncCaretFromEvent({ target: ta });
450 }
451 this.adjustTextareaHeight();
452 });
@@ -402,7 +454,7 @@ const model = {
454
455 historyPrev($event) {
456 if ($event && ($event.isComposing || $event.keyCode === 229)) return;
405 - const ta = ($event && $event.target) ? $event.target : document.getElementById("chat-input");
457 + const ta = ($event && $event.target) ? $event.target : this._activeTextarea();
458 if (!ta) return;
459 const start = ta.selectionStart;
460 const end = ta.selectionEnd;
@@ -424,7 +476,7 @@ const model = {
476
477 historyNext($event) {
478 if ($event && ($event.isComposing || $event.keyCode === 229)) return;
427 - const ta = ($event && $event.target) ? $event.target : document.getElementById("chat-input");
479 + const ta = ($event && $event.target) ? $event.target : this._activeTextarea();
480 if (!ta) return;
481 const value = ta.value;
482 const start = ta.selectionStart;
@@ -447,6 +499,7 @@ const model = {
499 this.message = "";
500 attachmentsStore.clearAttachments();
501 this.chatMoreMenuOpen = false;
502 + this._caretIndex = 0;
503 this._historyIndex = null;
504 this._draft = "";
505 this.adjustTextareaHeight();
webui/components/sidebar/chats/chats-store.js
+2 -1
@@ -210,12 +210,13 @@ const model = {
210 if (response.ok) {
211 await this.selectChat(response.ctxid);
212 document.dispatchEvent(new CustomEvent("chat-created", { detail: { ctxid: response.ctxid } }));
213 - return;
213 + return response.ctxid;
214 }
215
216 } catch (e) {
217 toastFetchError("Error creating new chat", e);
218 }
219 + return null;
220 },
221
222 deselectChat(){
webui/components/welcome/AGENTS.md
+3
@@ -14,10 +14,13 @@
14 - Keep banner and discovery card behavior compatible with Python `banners` extensions.
15 - Supported banner/card CTA actions must stay synchronized with plugin discovery contracts.
16 - Do not show setup prompts for already configured plugins when backend status can prevent it.
17 +- The welcome screen mounts the shared chat composer to start a new chat; keep it mutually exclusive with the normal chat input DOM.
18 +- Render `system-resources` as the dedicated System Resources panel, not as a generic alert banner.
19
20 ## Work Guidance
21
22 - Coordinate visual card or CTA changes with plugin discovery and onboarding surfaces.
23 +- Keep the lower welcome grid focused on Connect Channels, OAuth accounts, and System Resources unless the target design changes.
24
25 ## Verification
26
webui/components/welcome/welcome-screen.html
+419 -355
@@ -12,534 +12,598 @@
12 <div x-init="$store.welcomeStore.init(); if ($store.welcomeStore.isVisible) { $store.welcomeStore.onCreate(); } $watch('$store.welcomeStore.isVisible', (visible) => { if (visible) { $store.welcomeStore.onCreate(); } });"
13 class="welcome-container">
14 <x-extension id="welcome-screen-start"></x-extension>
15 - <div class="welcome-content">
16 - <!-- Action Cards -->
17 - <div class="welcome-actions">
18 - <x-extension id="welcome-actions-start"></x-extension>
19 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('new-chat')">
20 - <span class="material-symbols-outlined welcome-action-icon">add_circle</span>
21 - <h3 class="welcome-action-title">New Chat</h3>
22 - </div>
15
24 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('projects')">
25 - <span class="material-symbols-outlined welcome-action-icon">folder</span>
26 - <h3 class="welcome-action-title">Projects</h3>
27 - </div>
28 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('memory')">
29 - <span class="material-symbols-outlined welcome-action-icon">memory</span>
30 - <h3 class="welcome-action-title">Memory</h3>
16 + <main class="welcome-shell">
17 + <header class="welcome-hero">
18 + <h1>Welcome back <span aria-hidden="true">&#128075;</span></h1>
19 + <p>Agent Zero is ready to help.</p>
20 + </header>
21 +
22 + <section class="welcome-composer" aria-label="Start a new chat">
23 + <x-component path="chat/attachments/inputPreview.html"></x-component>
24 + <x-component path="chat/input/chat-bar-input.html"></x-component>
25 + </section>
26 +
27 + <section class="welcome-alerts"
28 + x-show="$store.welcomeStore.sortedBanners.length > 0 || $store.welcomeStore.hasDismissedBanners">
29 + <div class="welcome-banners" x-show="$store.welcomeStore.sortedBanners.length > 0">
30 + <x-extension id="welcome-banners-start"></x-extension>
31 + <template x-for="banner in $store.welcomeStore.sortedBanners" :key="banner.id">
32 + <article class="welcome-banner" :class="$store.welcomeStore.getBannerClass(banner.type)">
33 + <div class="welcome-banner-icon">
34 + <span class="material-symbols-outlined" x-text="$store.welcomeStore.getBannerIcon(banner.type)"></span>
35 + </div>
36 + <div class="welcome-banner-content">
37 + <div class="welcome-banner-title" x-text="banner.title"></div>
38 + <div class="welcome-banner-html" x-html="banner.html"></div>
39 + </div>
40 + <button class="welcome-banner-dismiss"
41 + type="button"
42 + x-show="banner.dismissible !== false"
43 + @click.stop="$store.welcomeStore.dismissBanner(banner.id)"
44 + title="Dismiss"
45 + aria-label="Dismiss">
46 + <span class="material-symbols-outlined">close</span>
47 + </button>
48 + </article>
49 + </template>
50 + <x-extension id="welcome-banners-end"></x-extension>
51 </div>
52
33 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('scheduler')">
34 - <span class="material-symbols-outlined welcome-action-icon">schedule</span>
35 - <h3 class="welcome-action-title">Tasks</h3>
36 - </div>
37 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('settings')">
38 - <span class="material-symbols-outlined welcome-action-icon">settings</span>
39 - <h3 class="welcome-action-title">Settings</h3>
40 - </div>
41 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('plugins')">
42 - <span class="material-symbols-outlined welcome-action-icon">extension</span>
43 - <h3 class="welcome-action-title">Plugins</h3>
44 - </div>
45 -
46 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('files')">
47 - <span class="material-symbols-outlined welcome-action-icon">folder_open</span>
48 - <h3 class="welcome-action-title">Files</h3>
49 - </div>
50 - <div class="welcome-action-card" @click="$store.welcomeStore.executeAction('website')">
51 - <span class="material-symbols-outlined welcome-action-icon">language</span>
52 - <h3 class="welcome-action-title">Visit Website</h3>
53 - </div>
54 - <x-extension id="welcome-actions-end"></x-extension>
55 - </div>
56 -
57 - <div class="welcome-actions-footer">
53 <button type="button"
59 - class="welcome-refresh-button"
60 - @click="$store.welcomeStore.refreshBanners(true)"
61 - :disabled="$store.welcomeStore.bannersLoading"
62 - title="Refresh banners">
63 - <span class="material-symbols-outlined" :class="{ 'welcome-spin': $store.welcomeStore.bannersLoading }">refresh</span>
64 - </button>
65 -
66 - <button type="button"
67 - class="welcome-refresh-button"
68 - @click="$store.welcomeStore.undismissBanners()"
54 + class="welcome-undismiss-button"
55 x-show="$store.welcomeStore.hasDismissedBanners"
56 :disabled="$store.welcomeStore.bannersLoading"
71 - title="Undismiss all banners">
57 + @click="$store.welcomeStore.undismissBanners()">
58 <span class="material-symbols-outlined">undo</span>
59 + <span>Show dismissed notices</span>
60 </button>
74 - </div>
75 -
76 - <!-- Banner Section -->
77 - <div class="welcome-banners" x-show="$store.welcomeStore.banners && $store.welcomeStore.banners.length > 0">
78 - <x-extension id="welcome-banners-start"></x-extension>
79 - <template x-for="banner in $store.welcomeStore.sortedBanners" :key="banner.id">
80 - <div class="welcome-banner" :class="$store.welcomeStore.getBannerClass(banner.type)">
81 - <!-- Banner Icon -->
82 - <div class="welcome-banner-icon">
83 - <span class="material-symbols-outlined" x-text="$store.welcomeStore.getBannerIcon(banner.type)"></span>
84 - </div>
85 -
86 - <!-- Banner Content -->
87 - <div class="welcome-banner-content">
88 - <div class="welcome-banner-title" x-text="banner.title"></div>
89 - <div class="welcome-banner-html" x-html="banner.html"></div>
90 - </div>
91 -
92 - <!-- Dismiss Button -->
93 - <button class="welcome-banner-dismiss"
94 - x-show="banner.dismissible !== false"
95 - @click.stop="$store.welcomeStore.dismissBanner(banner.id)"
96 - title="Dismiss">
97 - <span class="material-symbols-outlined">close</span>
98 - </button>
99 - </div>
100 - </template>
101 - <x-extension id="welcome-banners-end"></x-extension>
102 - </div>
103 - </div>
61 + </section>
62 +
63 + <section class="welcome-quick-section">
64 + <h2>Quick Actions</h2>
65 + <div class="welcome-actions">
66 + <x-extension id="welcome-actions-start"></x-extension>
67 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('projects')">
68 + <span class="material-symbols-outlined welcome-action-icon">folder</span>
69 + <span class="welcome-action-copy">
70 + <span class="welcome-action-title">Projects</span>
71 + <span class="welcome-action-description">Organize and track your work.</span>
72 + </span>
73 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
74 + </button>
75 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('memory')">
76 + <span class="material-symbols-outlined welcome-action-icon">memory</span>
77 + <span class="welcome-action-copy">
78 + <span class="welcome-action-title">Memory</span>
79 + <span class="welcome-action-description">Access saved context and knowledge.</span>
80 + </span>
81 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
82 + </button>
83 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('scheduler')">
84 + <span class="material-symbols-outlined welcome-action-icon">schedule</span>
85 + <span class="welcome-action-copy">
86 + <span class="welcome-action-title">Tasks</span>
87 + <span class="welcome-action-description">View and manage your tasks.</span>
88 + </span>
89 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
90 + </button>
91 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('files')">
92 + <span class="material-symbols-outlined welcome-action-icon">folder_open</span>
93 + <span class="welcome-action-copy">
94 + <span class="welcome-action-title">Files</span>
95 + <span class="welcome-action-description">Search and manage your files.</span>
96 + </span>
97 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
98 + </button>
99 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('settings')">
100 + <span class="material-symbols-outlined welcome-action-icon">settings</span>
101 + <span class="welcome-action-copy">
102 + <span class="welcome-action-title">Settings</span>
103 + <span class="welcome-action-description">Configure Agent Zero to your preferences.</span>
104 + </span>
105 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
106 + </button>
107 + <button type="button" class="welcome-action-card" @click="$store.welcomeStore.executeAction('plugins')">
108 + <span class="material-symbols-outlined welcome-action-icon">extension</span>
109 + <span class="welcome-action-copy">
110 + <span class="welcome-action-title">Plugins</span>
111 + <span class="welcome-action-description">Extend Agent Zero with integrations.</span>
112 + </span>
113 + <span class="material-symbols-outlined welcome-action-arrow">chevron_right</span>
114 + </button>
115 + <button type="button" class="welcome-action-card is-wide" @click="$store.welcomeStore.executeAction('website')">
116 + <span class="material-symbols-outlined welcome-action-icon">language</span>
117 + <span class="welcome-action-copy">
118 + <span class="welcome-action-title">Visit Website</span>
119 + <span class="welcome-action-description">Explore Agent Zero online and resources.</span>
120 + </span>
121 + <span class="material-symbols-outlined welcome-action-arrow">open_in_new</span>
122 + </button>
123 + </div>
124 + </section>
125 +
126 + <section class="welcome-lower-grid">
127 + <x-extension id="welcome-actions-end"></x-extension>
128 +
129 + <article class="welcome-system-card"
130 + x-show="$store.welcomeStore.systemResourceBanner"
131 + x-transition.opacity>
132 + <header class="welcome-panel-header">
133 + <h2 x-text="$store.welcomeStore.systemResourceBanner?.title || 'System Resources'"></h2>
134 + <span class="material-symbols-outlined">monitoring</span>
135 + </header>
136 + <div class="welcome-system-body"
137 + x-html="$store.welcomeStore.systemResourceBanner?.html || ''"></div>
138 + </article>
139 + </section>
140 + </main>
141 +
142 <x-extension id="welcome-screen-end"></x-extension>
143 </div>
144 </template>
145 </div>
146
147 <style>
110 - /* Welcome Screen Styles */
148 .welcome-container {
112 - display: flex;
113 - flex-direction: column;
114 - align-items: center;
115 - justify-content: center;
116 - padding: 3%;
117 - text-align: center;
118 - background: var(--color-background);
119 - color: var(--color-text);
120 - min-height: 100vh;
149 position: relative;
150 + min-height: 100vh;
151 overflow: hidden;
152 + padding: clamp(4.8rem, 8vh, 6.4rem) clamp(1.25rem, 5.4vw, 6.5rem) 3.5rem;
153 + background: var(--color-background);
154 + color: var(--color-text);
155 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
156 }
157 +
158 .welcome-container > x-extension,
159 .welcome-actions > x-extension,
126 - .welcome-banners > x-extension {
160 + .welcome-banners > x-extension,
161 + .welcome-lower-grid > x-extension,
162 + .welcome-lower-grid > x-extension > x-component,
163 + .welcome-lower-grid > x-extension > x-component > div[x-data] {
164 display: contents;
165 }
166
167 .welcome-container::before {
168 content: "";
169 position: absolute;
133 - inset: 0;
170 + inset: -6% -8% -10% 10%;
171 background-image: url("./public/darkSymbol.svg");
172 background-repeat: no-repeat;
136 - background-position: center;
137 - background-size: 80% auto;
138 - opacity: 0.07;
173 + background-position: center top;
174 + background-size: min(78vw, 980px) auto;
175 + opacity: 0.105;
176 pointer-events: none;
177 }
178
179 .light-mode .welcome-container::before {
180 filter: invert(0.55);
144 - opacity: 0.06;
145 - }
146 -
147 - .dark-mode .welcome-container::before {
148 - filter: invert(0.85);
149 - opacity: 0.08;
181 + opacity: 0.055;
182 }
183
152 - .welcome-content {
184 + .welcome-shell {
185 position: relative;
186 z-index: 1;
155 - width: 100%;
187 display: flex;
188 flex-direction: column;
158 - align-items: center;
189 + gap: 1.55rem;
190 + width: min(100%, 1220px);
191 + margin: 0 auto;
192 }
193
161 - /* Welcome container styles */
162 - .welcome-logo-container {
163 - display: flex;
164 - justify-content: center;
165 - align-items: center;
194 + .welcome-hero h1 {
195 + margin: 0;
196 + color: var(--color-text);
197 + font-size: clamp(2rem, 3.4vw, 2.75rem);
198 + font-weight: 440;
199 + letter-spacing: 0;
200 + line-height: 1.08;
201 }
202
168 - .welcome-logo {
169 - width: 7rem;
170 - height: 7rem;
203 + .welcome-hero p {
204 + margin: 0.5rem 0 0;
205 + color: color-mix(in srgb, var(--color-text) 66%, transparent);
206 + font-size: clamp(1.08rem, 1.8vw, 1.35rem);
207 + line-height: 1.35;
208 }
209
173 - /* Themes adjustments for logo */
174 - .light-mode .welcome-logo {
175 - filter: invert(0.5);
210 + .welcome-composer {
211 + width: 100%;
212 }
213
178 - .dark-mode .welcome-logo {
179 - filter: invert(0.8);
214 + .welcome-composer > x-component,
215 + .welcome-composer > x-component > div[x-data],
216 + .welcome-composer > x-component > div[x-data] > template + div {
217 + display: contents;
218 }
219
182 - .welcome-title {
183 - font-size: 2rem;
184 - font-weight: 400;
185 - margin: var(--spacing-xs) 0 0 0;
186 - color: var(--color-text);
220 + .welcome-composer .input-row {
221 + min-height: 4.9rem;
222 + padding: 0.58rem 0.7rem 0.58rem 0.86rem;
223 + border-radius: 16px;
224 + background: color-mix(in srgb, var(--color-panel) 64%, var(--color-background) 36%);
225 }
226
189 - .welcome-action-title {
227 + .welcome-composer #chat-input {
228 + min-height: 3.2rem;
229 + font-size: 1.03rem;
230 + }
231 +
232 + .welcome-composer #expand-button {
233 + display: none;
234 + }
235 +
236 + .welcome-composer .chat-more-panel {
237 + top: calc(100% + 0.55rem);
238 + bottom: auto;
239 + margin-top: 0;
240 margin-bottom: 0;
241 }
242
193 - .welcome-subtitle {
194 - font-size: 1rem;
195 - margin-bottom: 2rem;
196 - color: var(--color-secondary);
197 - max-width: 600px;
198 - line-height: 1.5;
243 + .welcome-alerts {
244 + display: grid;
245 + gap: 0.7rem;
246 }
247
201 - .welcome-actions {
248 + .welcome-banners {
249 display: grid;
203 - grid-template-columns: repeat(4, 1fr);
204 - gap: 1rem;
205 - max-width: 640px;
206 - width: 100%;
207 - margin: 1.5rem 0;
250 + gap: 0.75rem;
251 }
252
210 - .welcome-actions-footer {
211 - max-width: 640px;
212 - width: 100%;
253 + .welcome-banner,
254 + .welcome-action-card,
255 + .welcome-panel-card,
256 + .welcome-system-card {
257 + border: 1px solid color-mix(in srgb, var(--color-border) 66%, transparent);
258 + background: color-mix(in srgb, var(--color-panel) 78%, var(--color-background) 22%);
259 + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.026);
260 + backdrop-filter: blur(18px);
261 + }
262 +
263 + .welcome-banner {
264 + position: relative;
265 display: flex;
214 - justify-content: flex-start;
215 - gap: 0.5rem;
216 - margin-top: -0.75rem;
266 + align-items: flex-start;
267 + gap: 0.78rem;
268 + padding: 0.9rem 1rem;
269 + border-radius: 12px;
270 + text-align: left;
271 }
272
219 - .welcome-refresh-button {
220 - width: 38px;
221 - height: 38px;
273 + .welcome-banner.banner-info {
274 + border-left: 4px solid #2f6bff;
275 + }
276 +
277 + .welcome-banner.banner-warning {
278 + border-left: 4px solid #f59e0b;
279 + }
280 +
281 + .welcome-banner.banner-error {
282 + border-left: 4px solid #ef4444;
283 + }
284 +
285 + .welcome-banner-icon {
286 display: flex;
287 + width: 1.6rem;
288 + height: 1.6rem;
289 align-items: center;
290 justify-content: center;
225 - background: transparent;
226 - border: 1px solid var(--color-border);
227 - border-radius: 10px;
228 - color: var(--color-secondary);
229 - cursor: pointer;
230 - transition: all 0.2s ease;
291 + color: #2f6bff;
292 }
293
233 - .welcome-refresh-button:hover {
234 - border-color: var(--color-primary);
235 - background: var(--color-message-bg);
236 - color: var(--color-text);
294 + .banner-warning .welcome-banner-icon {
295 + color: #f59e0b;
296 }
297
239 - .welcome-refresh-button:disabled {
240 - opacity: 0.6;
241 - cursor: default;
298 + .banner-error .welcome-banner-icon {
299 + color: #ef4444;
300 }
301
244 - .welcome-refresh-button .material-symbols-outlined {
302 + .welcome-banner-icon .material-symbols-outlined {
303 font-size: 1.25rem;
304 }
305
248 - .welcome-spin {
249 - animation: welcome-spin 0.9s linear infinite;
250 - }
251 -
252 - @keyframes welcome-spin {
253 - from { transform: rotate(0deg); }
254 - to { transform: rotate(360deg); }
306 + .welcome-banner-content {
307 + flex: 1;
308 + min-width: 0;
309 }
310
257 - .welcome-action-card {
258 - background: var(--color-panel);
259 - border: 1px solid var(--color-border);
260 - border-radius: 12px;
261 - padding: 0.6rem;
262 - transition: all 0.3s ease;
263 - cursor: pointer;
264 - text-decoration: none;
265 - color: inherit;
266 - display: flex;
267 - flex-direction: column;
268 - align-items: center;
269 - text-align: center;
270 - justify-content: center;
271 - aspect-ratio: 1 / 1;
311 + .welcome-banner-title {
312 + margin-bottom: 0.25rem;
313 + color: var(--color-text);
314 + font-size: 0.92rem;
315 + font-weight: 650;
316 + line-height: 1.25;
317 }
318
274 - .welcome-action-card:hover {
275 - border-color: var(--color-primary);
276 - background: var(--color-message-bg);
319 + .welcome-banner-html {
320 + color: color-mix(in srgb, var(--color-text) 68%, transparent);
321 + font-size: 0.84rem;
322 + line-height: 1.45;
323 }
324
279 - .welcome-action-icon {
280 - font-size: 2.1rem;
281 - margin-bottom: 0.25rem;
282 - color: var(--color-highlight-dark);
325 + .welcome-banner-html a {
326 + color: #5f83ff;
327 + text-decoration: none;
328 }
329
285 - .welcome-action-title {
286 - font-size: 0.9rem;
330 + .welcome-banner-html a:hover {
331 + text-decoration: underline;
332 }
333
289 - /* Banner Styles */
290 - .welcome-banners {
291 - display: flex;
292 - flex-direction: column;
293 - gap: 0.75rem;
294 - max-width: 680px;
295 - width: 100%;
296 - margin-top: 1.5rem;
334 + .welcome-banner-dismiss,
335 + .welcome-undismiss-button {
336 + display: inline-flex;
337 + align-items: center;
338 + justify-content: center;
339 + gap: 0.4rem;
340 + border: 0;
341 + background: transparent;
342 + color: color-mix(in srgb, var(--color-text) 64%, transparent);
343 + cursor: pointer;
344 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
345 }
346
299 - .welcome-banner {
300 - display: flex;
301 - align-items: flex-start;
302 - gap: 12px;
303 - padding: 14px 16px;
304 - background: var(--color-panel);
305 - border: 1px solid var(--color-border);
347 + .welcome-banner-dismiss {
348 + width: 2rem;
349 + height: 2rem;
350 border-radius: 8px;
307 - text-align: left;
308 - position: relative;
309 - transition: all 0.2s ease;
351 }
352
312 - .welcome-banner:hover {
313 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
353 + .welcome-banner-dismiss:hover,
354 + .welcome-undismiss-button:hover {
355 + color: var(--color-text);
356 + background: color-mix(in srgb, var(--color-border) 45%, transparent);
357 }
358
316 - /* Banner Type Styles */
317 - .welcome-banner.banner-info {
318 - border-left: 4px solid #2196F3;
359 + .welcome-undismiss-button {
360 + justify-self: start;
361 + border-radius: 9px;
362 + padding: 0.45rem 0.55rem;
363 + font-size: 0.82rem;
364 }
365
321 - .welcome-banner.banner-warning {
322 - border-left: 4px solid #FF9800;
366 + .welcome-quick-section {
367 + display: grid;
368 + gap: 0.82rem;
369 }
370
325 - .welcome-banner.banner-error {
326 - border-left: 4px solid #F44336;
371 + .welcome-quick-section h2,
372 + .welcome-panel-header h2 {
373 + margin: 0;
374 + color: var(--color-text);
375 + font-size: 1.05rem;
376 + font-weight: 520;
377 + line-height: 1.2;
378 }
379
329 - /* Banner Icon */
330 - .welcome-banner-icon {
331 - flex-shrink: 0;
332 - display: flex;
333 - align-items: center;
334 - justify-content: center;
335 - width: 24px;
336 - height: 24px;
337 - margin-top: 2px;
380 + .welcome-actions {
381 + display: grid;
382 + grid-template-columns: repeat(4, minmax(0, 1fr));
383 + gap: 1rem;
384 + width: 100%;
385 }
386
340 - .welcome-banner-icon .material-symbols-outlined {
341 - font-size: 1.25rem;
387 + .welcome-action-card {
388 + display: grid;
389 + grid-template-columns: auto minmax(0, 1fr) auto;
390 + align-items: center;
391 + gap: 1rem;
392 + min-height: 6.1rem;
393 + border-radius: 8px;
394 + padding: 1.05rem 1rem;
395 + color: inherit;
396 + cursor: pointer;
397 + font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
398 + text-align: left;
399 + transition:
400 + border-color 0.18s ease,
401 + background-color 0.18s ease,
402 + box-shadow 0.18s ease,
403 + transform 0.18s ease;
404 }
405
344 - .banner-info .welcome-banner-icon {
345 - color: #2196F3;
406 + .welcome-action-card.is-wide {
407 + grid-column: span 2;
408 }
409
348 - .banner-warning .welcome-banner-icon {
349 - color: #FF9800;
410 + .welcome-action-card:hover,
411 + .welcome-action-card:focus-visible {
412 + border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
413 + background: color-mix(in srgb, var(--color-panel) 86%, var(--color-background) 14%);
414 + box-shadow:
415 + inset 0 1px 0 rgba(255, 255, 255, 0.036),
416 + 0 16px 34px rgba(0, 0, 0, 0.18);
417 + outline: none;
418 + transform: translateY(-1px);
419 }
420
352 - .banner-error .welcome-banner-icon {
353 - color: #F44336;
421 + .welcome-action-icon {
422 + color: #2f6bff;
423 + font-size: 2.35rem;
424 + line-height: 1;
425 }
426
356 - /* Banner Content */
357 - .welcome-banner-content {
358 - flex: 1;
427 + .welcome-action-copy {
428 + display: grid;
429 min-width: 0;
430 + gap: 0.28rem;
431 }
432
362 - .welcome-banner-title {
363 - font-weight: 600;
364 - font-size: 0.9rem;
433 + .welcome-action-title {
434 color: var(--color-text);
366 - margin-bottom: 4px;
367 - line-height: 1.3;
435 + font-size: 1rem;
436 + font-weight: 520;
437 + line-height: 1.25;
438 }
439
370 - .welcome-banner-html {
371 - font-size: 0.85rem;
372 - color: var(--color-secondary);
373 - line-height: 1.5;
440 + .welcome-action-description {
441 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
442 + font-size: 0.92rem;
443 + line-height: 1.42;
444 }
445
376 - .welcome-banner-html a {
377 - color: var(--color-primary);
378 - text-decoration: underline;
379 - cursor: pointer;
446 + .welcome-action-arrow {
447 + color: var(--color-text);
448 + font-size: 1.35rem;
449 + opacity: 0.8;
450 }
451
382 - .welcome-banner-html a:hover {
383 - color: var(--color-highlight-dark);
452 + .welcome-lower-grid {
453 + display: grid;
454 + grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.94fr);
455 + gap: 1rem;
456 + align-items: stretch;
457 }
458
386 - .welcome-banner-html strong {
387 - color: var(--color-text);
388 - font-weight: 600;
459 + .welcome-lower-grid .discovery-slot {
460 + grid-column: auto;
461 + min-width: 0;
462 + width: 100%;
463 }
464
391 - /* Banner Dismiss Button */
392 - .welcome-banner-dismiss {
393 - flex-shrink: 0;
394 - background: transparent;
395 - border: none;
396 - border-radius: 4px;
397 - color: var(--color-secondary);
398 - cursor: pointer;
399 - padding: 4px;
465 + .welcome-system-card {
466 + min-width: 0;
467 + border-radius: 8px;
468 + padding: 1.6rem 1.7rem;
469 + text-align: left;
470 + }
471 +
472 + .welcome-panel-header {
473 display: flex;
474 align-items: center;
402 - justify-content: center;
403 - transition: all 0.2s ease;
404 - opacity: 0.6;
475 + justify-content: space-between;
476 + gap: 1rem;
477 + margin-bottom: 1.5rem;
478 }
479
407 - .welcome-banner-dismiss:hover {
408 - background: var(--color-border);
480 + .welcome-panel-header .material-symbols-outlined {
481 color: var(--color-text);
410 - opacity: 1;
482 + font-size: 1.35rem;
483 + opacity: 0.86;
484 }
485
413 - .welcome-banner-dismiss .material-symbols-outlined {
414 - font-size: 1.1rem;
486 + .system-resource-body {
487 + display: grid;
488 + gap: 1.22rem;
489 }
490
417 - /* Light mode adjustments */
418 - .light-mode .welcome-title {
419 - color: #2d2d2d;
491 + .system-resource-meter {
492 + display: grid;
493 + grid-template-columns: 8.2rem minmax(0, 1fr);
494 + gap: 1rem;
495 + align-items: center;
496 }
497
422 - .light-mode .welcome-subtitle {
423 - color: #666;
498 + .system-resource-label {
499 + display: grid;
500 + gap: 0.38rem;
501 + min-width: 0;
502 }
503
426 - .light-mode .welcome-action-title {
427 - color: #2d2d2d;
504 + .system-resource-name,
505 + .system-resource-detail-title {
506 + color: color-mix(in srgb, var(--color-text) 58%, transparent);
507 + font-size: 0.82rem;
508 + font-weight: 430;
509 + letter-spacing: 0.02em;
510 }
511
430 - .light-mode .welcome-action-description {
431 - color: #666;
512 + .system-resource-value,
513 + .system-resource-detail-value {
514 + color: color-mix(in srgb, var(--color-text) 76%, transparent);
515 + font-size: 0.96rem;
516 + font-variant-numeric: tabular-nums;
517 + line-height: 1.2;
518 + white-space: nowrap;
519 }
520
434 - .light-mode .welcome-footer {
435 - color: #666;
521 + .system-resource-track {
522 + height: 0.64rem;
523 + overflow: hidden;
524 + border-radius: 999px;
525 + background: color-mix(in srgb, var(--color-border) 70%, transparent);
526 }
527
438 - .light-mode .welcome-banner {
439 - background: var(--color-panel);
528 + .system-resource-fill {
529 + display: block;
530 + width: var(--resource-value, 0%);
531 + height: 100%;
532 + border-radius: inherit;
533 + background: var(--resource-color, #22c55e);
534 + transition: width 0.22s ease;
535 }
536
442 - .light-mode .welcome-banner-title {
443 - color: var(--color-text);
537 + .system-resource-footer {
538 + display: grid;
539 + grid-template-columns: 1fr 1fr;
540 + gap: 1rem 1.5rem;
541 + margin-top: 0.15rem;
542 + padding-top: 1.02rem;
543 + border-top: 1px solid color-mix(in srgb, var(--color-border) 55%, transparent);
544 }
545
446 - .light-mode .welcome-banner-html {
447 - color: var(--color-primary);
546 + .system-resource-detail-value {
547 + margin-top: 0.48rem;
548 + font-family: var(--font-family-code, "Roboto Mono", monospace);
549 + font-size: 0.88rem;
550 }
551
450 - /* Responsive design */
451 - @media (max-width: 768px) {
552 + @media (max-width: 1180px) {
553 .welcome-container {
453 - padding: 1rem;
454 - padding-top: 3rem;
554 + padding-inline: clamp(1.25rem, 4vw, 3rem);
555 }
556
457 - .welcome-logo {
458 - width: 5rem;
459 - height: 5rem;
557 + .welcome-actions {
558 + grid-template-columns: repeat(2, minmax(0, 1fr));
559 }
560
462 - .welcome-logo-container {
463 - margin-top: 2rem;
561 + .welcome-lower-grid {
562 + grid-template-columns: 1fr;
563 }
564 + }
565
466 - .welcome-title {
467 - font-size: 1.5rem;
468 - margin: 0 !important;
566 + @media (max-width: 720px) {
567 + .welcome-container {
568 + min-height: 100%;
569 + padding: 4.5rem 1rem 2rem;
570 }
571
471 - .welcome-subtitle {
472 - font-size: 1rem;
473 - margin-top: var(--spacing-xs);
474 - margin-bottom: 0;
572 + .welcome-shell {
573 + gap: 1.2rem;
574 }
575
576 .welcome-actions {
478 - grid-template-columns: repeat(4, 1fr);
577 + grid-template-columns: 1fr;
578 gap: 0.75rem;
480 - margin-bottom: 1.5rem;
579 }
580
483 - .welcome-action-card {
484 - aspect-ratio: 1 / 1;
485 - padding: 0.5rem;
581 + .welcome-action-card,
582 + .welcome-action-card.is-wide {
583 + grid-column: auto;
584 + min-height: 5.25rem;
585 + padding: 0.85rem;
586 }
587
588 .welcome-action-icon {
489 - font-size: 1.6rem;
490 - }
491 -
492 - .welcome-action-title {
493 - font-size: 0.85rem;
589 + font-size: 2rem;
590 }
591
496 - .welcome-banners {
497 - max-width: 100%;
592 + .welcome-composer .input-row {
593 + min-height: 4.35rem;
594 }
595
500 - .welcome-banner {
501 - padding: 12px 14px;
596 + .welcome-system-card {
597 + padding: 1.15rem;
598 }
599
504 - .welcome-banner-title {
505 - font-size: 0.85rem;
506 - }
507 -
508 - .welcome-banner-html {
509 - font-size: 0.8rem;
510 - }
511 - }
512 -
513 - @media (max-width: 520px) {
514 - .welcome-actions {
600 + .system-resource-meter {
601 grid-template-columns: 1fr;
516 - gap: 0.5rem;
602 + gap: 0.55rem;
603 }
604
519 - .welcome-action-card {
520 - aspect-ratio: auto;
521 - padding: 0.5rem 0.75rem;
522 - flex-direction: row;
523 - justify-content: flex-start;
524 - align-items: center;
525 - text-align: left;
526 - border-radius: 10px;
527 - min-height: 44px;
528 - }
529 -
530 - .welcome-action-icon {
531 - margin-bottom: 0;
532 - margin-right: 0.5rem;
533 - font-size: 1.35rem;
534 - line-height: 1;
535 - display: flex;
536 - align-items: center;
537 - }
538 -
539 - .welcome-action-title {
540 - font-size: 0.95rem;
541 - margin: 0;
542 - line-height: 1.2;
605 + .system-resource-footer {
606 + grid-template-columns: 1fr;
607 }
608 }
609 </style>
webui/components/welcome/welcome-store.js
+5 -1
@@ -1,5 +1,4 @@
1 import { createStore } from "/js/AlpineStore.js";
2 -import { getContext } from "/index.js";
2 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
3 import { store as memoryStore } from "/plugins/_memory/webui/memory-dashboard-store.js";
4 import { store as projectsStore } from "/components/projects/projects-store.js";
@@ -143,10 +142,15 @@ const model = {
142
143 get sortedBanners() {
144 return [...this.banners]
145 + .filter((b) => b.id !== "system-resources")
146 .filter((b) => b.type !== "hero" && b.type !== "feature")
147 .sort((a, b) => (b.priority || 0) - (a.priority || 0));
148 },
149
150 + get systemResourceBanner() {
151 + return this.banners.find((b) => b.id === "system-resources") || null;
152 + },
153 +
154 /**
155 * Dismiss a banner by ID.
156 *
webui/index.html
+15 -7
@@ -96,8 +96,12 @@
96 <x-component path="chat/top-section/chat-top.html"></x-component>
97
98 <!-- Welcome Screen Component -->
99 - <div x-data x-show="$store.welcomeStore && $store.welcomeStore.isVisible">
100 - <x-component path="welcome/welcome-screen.html"></x-component>
99 + <div x-data>
100 + <template x-if="$store.welcomeStore && $store.welcomeStore.isVisible">
101 + <div class="welcome-host">
102 + <x-component path="welcome/welcome-screen.html"></x-component>
103 + </div>
104 + </template>
105 </div>
106 <!-- Message History (actual messages) -->
107 <div id="chat-area-wrapper" x-data x-show="!$store.welcomeStore || !$store.welcomeStore.isVisible">
@@ -135,11 +139,15 @@
139 <button class="toast__close">Close</button>
140 </div>
141 <!-- Chat Input Area -->
138 - <div x-show="!$store.welcomeStore || !$store.welcomeStore.isVisible">
139 - <!-- Progress Bar -->
140 - <x-component path="chat/input/progress.html"></x-component>
141 - <!-- Input Section -->
142 - <x-component path="chat/input/chat-bar.html"></x-component>
142 + <div x-data>
143 + <template x-if="!$store.welcomeStore || !$store.welcomeStore.isVisible">
144 + <div>
145 + <!-- Progress Bar -->
146 + <x-component path="chat/input/progress.html"></x-component>
147 + <!-- Input Section -->
148 + <x-component path="chat/input/chat-bar.html"></x-component>
149 + </div>
150 + </template>
151 </div>
152 </div>
153 <x-component path="canvas/right-canvas.html"></x-component>
webui/index.js
+4 -8
@@ -14,6 +14,7 @@ import { store as chatTopStore } from "/components/chat/top-section/chat-top-sto
14 import { store as _tooltipsStore } from "/components/tooltips/tooltip-store.js";
15 import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
16 import { store as syncStore } from "/components/sync/sync-store.js"
17 +import { store as welcomeStore } from "/components/welcome/welcome-store.js";
18 import { getUserHour12, getUserTimezone } from "/js/time-utils.js";
19
20 globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
@@ -58,7 +59,7 @@ export async function sendMessage() {
59
60 if (message || hasAttachments) {
61 // Check if agent is busy - queue instead of sending
61 - if (chatsStore.selectedContext.running || messageQueueStore.hasQueue) {
62 + if (chatsStore.selectedContext?.running || messageQueueStore.hasQueue) {
63 const success = messageQueueStore.addToQueue(message, attachmentsWithUrls);
64 // no await for the queue
65 // if (success) {
@@ -231,12 +232,7 @@ globalThis.loadKnowledge = async function () {
232 };
233
234 function adjustTextareaHeight() {
234 - const chatInputEl = document.getElementById("chat-input");
235 - if (chatInputEl) {
236 - if (!inputStore.message) chatInputEl.value = "";
237 - chatInputEl.style.height = "auto";
238 - chatInputEl.style.height = chatInputEl.scrollHeight + "px";
239 - }
235 + inputStore.adjustTextareaHeight();
236 }
237
238 export const sendJsonData = async function (url, data) {
@@ -593,7 +589,7 @@ export const deselectChat = function () {
589 sessionStorage.removeItem("lastSelectedTask");
590
591 // Clear the chat history
596 - chatHistory.innerHTML = "";
592 + if (chatHistory) chatHistory.innerHTML = "";
593 };
594 globalThis.deselectChat = deselectChat;
595