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">👋</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