fix: sidebar, welcome screen and UX
Alessandro committed
Nov 1, 2025 at 12:29 UTC
4999b4f8d0c8b5961213ced793b16a6731101f5c
8 files changed
+174
-111
webui/components/sidebar/bottom/preferences/preferences-panel.html
+7
-3
@@ -9,13 +9,17 @@
9
<template x-if="$store.preferences">
10
<div class="pref-section">
11
<span>
12
- <h3 class="pref-header" data-bs-toggle="collapse" data-bs-target="#pref-list">
12
+ <h3 class="pref-header"
13
+ data-bs-toggle="collapse"
14
+ @click="$store.sidebar.toggleSection('preferences')"
15
+ x-effect="!$store.sidebar.isSectionOpen('preferences') ? $el.classList.add('collapsed') : $el.classList.remove('collapsed')">
16
Preferences
17
<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
18
<path d="M8 4l8 8-8 8" />
19
</svg>
20
</h3>
18
- <ul class="config-list collapse show" id="pref-list">
21
+ <ul class="config-list collapse" id="preferences-collapse"
22
+ x-effect="(() => { const c = bootstrap.Collapse.getOrCreateInstance($el, { toggle: false }); $store.sidebar.isSectionOpen('preferences') ? c.show() : c.hide(); })()">
23
<li x-data>
24
<span>Autoscroll</span>
25
<label class="switch">
@@ -89,7 +93,7 @@
93
flex-direction: column;
94
gap: 1px;
95
}
92
- #pref-list li {
96
+ #preferences-collapse li {
97
opacity: 0.8;
98
}
99
.pref-section .switch-label,
webui/components/sidebar/sidebar-store.js
+40
@@ -5,17 +5,57 @@ const model = {
5
isOpen: true,
6
_initialized: false,
7
8
+ // Centralized collapse state for all sidebar sections (persisted in localStorage)
9
+ sectionStates: {
10
+ tasks: false, // default: collapsed
11
+ preferences: false // default: collapsed
12
+ },
13
+
14
// Initialize the store by setting up a resize listener
15
// Guard ensures this runs only once, even if called from multiple components
16
init() {
17
if (this._initialized) return;
18
this._initialized = true;
19
20
+ this.loadSectionStates();
21
this.handleResize();
22
this.resizeHandler = () => this.handleResize();
23
window.addEventListener("resize", this.resizeHandler);
24
},
25
26
+ // Load section collapse states from localStorage
27
+ loadSectionStates() {
28
+ try {
29
+ const stored = localStorage.getItem('sidebarSections');
30
+ if (stored) {
31
+ this.sectionStates = { ...this.sectionStates, ...JSON.parse(stored) };
32
+ }
33
+ } catch (e) {
34
+ console.error('Failed to load sidebar section states', e);
35
+ }
36
+ },
37
+
38
+ // Persist section states to localStorage
39
+ persistSectionStates() {
40
+ try {
41
+ localStorage.setItem('sidebarSections', JSON.stringify(this.sectionStates));
42
+ } catch (e) {
43
+ console.error('Failed to persist section states', e);
44
+ }
45
+ },
46
+
47
+ // Check if a section should be open (used by x-init in templates)
48
+ isSectionOpen(name) {
49
+ return this.sectionStates[name] === true;
50
+ },
51
+
52
+ // Toggle and persist a section's open state (drives Bootstrap programmatically via components)
53
+ toggleSection(name) {
54
+ if (!(name in this.sectionStates)) return;
55
+ this.sectionStates[name] = !this.sectionStates[name];
56
+ this.persistSectionStates();
57
+ },
58
+
59
// Cleanup method for lifecycle management
60
destroy() {
61
if (this.resizeHandler) {
webui/components/sidebar/tasks/tasks-list.html
+6
-2
@@ -8,13 +8,17 @@
8
<div class="tasks-list-root">
9
<template x-if="$store.tasks">
10
<div x-data class="tasks-list-inner">
11
- <h3 class="section-header section-header-collapsible" data-bs-toggle="collapse" data-bs-target="#tasks-collapse">
11
+ <h3 class="section-header section-header-collapsible"
12
+ data-bs-toggle="collapse"
13
+ @click="$store.sidebar.toggleSection('tasks')"
14
+ x-effect="!$store.sidebar.isSectionOpen('tasks') ? $el.classList.add('collapsed') : $el.classList.remove('collapsed')">
15
Tasks
16
<svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
17
<path d="M8 4l8 8-8 8" />
18
</svg>
19
</h3>
17
- <div class="collapse show" id="tasks-collapse">
20
+ <div class="collapse" id="tasks-collapse"
21
+ x-effect="(() => { const c = bootstrap.Collapse.getOrCreateInstance($el, { toggle: false }); $store.sidebar.isSectionOpen('tasks') ? c.show() : c.hide(); })()">
22
<div class="tasks-list-body">
23
<div class="tasks-list-container" x-data>
24
<ul class="config-list tasks-config-list" x-show="$store.tasks.tasks.length > 0">
webui/components/sidebar/top-section/header-icons.html
+1
-3
@@ -62,14 +62,12 @@
62
}
63
#logo-container a { color: inherit; text-decoration: none; }
64
#logo-container img { border-radius: var(--spacing-xs); width: auto; height: 2.6rem; -webkit-transition: filter 0.3s ease; transition: filter 0.3s ease; }
65
- #right-panel.expanded #logo-container { margin-left: 4.6rem; }
65
66
.light-mode #logo-container img { -webkit-filter: invert(100%) grayscale(100%); filter: invert(100%) grayscale(100%); }
67
68
@media (max-width: 768px) {
69
.toggle-sidebar-button { position: fixed; left: var(--spacing-md); z-index: 1004; }
71
- #logo-container { margin-left: 4.6rem; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; z-index: 1004; }
72
- #right-panel.expanded #logo-container { margin-left: 4.6rem; }
70
+ #logo-container { -webkit-transition: all 0.3s ease; transition: all 0.3s ease; z-index: 1004; }
71
}
72
</style>
73
</body>
webui/components/welcome/welcome-screen.html
+56
-78
@@ -11,7 +11,7 @@
11
<!-- Agent Zero Logo -->
12
<div class="welcome-logo-container">
13
<img
14
- src="/image_get?path=webui/public/lightSymbol.svg"
14
+ src="./public/darkSymbol.svg"
15
alt="Agent Zero Logo"
16
class="welcome-logo"
17
/>
@@ -78,22 +78,10 @@
78
</p>
79
</div>
80
</div>
81
-
82
- <!-- Footer Info -->
83
- <div class="welcome-footer">
84
- <p>Agent Zero Framework • Open Source AI Assistant</p>
85
- </div>
81
</div>
82
</template>
83
</div>
84
90
- <script>
91
- document.addEventListener("alpine:init", () => {
92
- const s = Alpine.store("welcomeStore");
93
- if (s && typeof s.init === "function") s.init();
94
- });
95
- </script>
96
-
85
<style>
86
/* Welcome Screen Styles */
87
.welcome-container {
@@ -101,38 +89,43 @@
89
flex-direction: column;
90
align-items: center;
91
justify-content: center;
104
- padding: 2rem;
92
+ padding: 3%;
93
text-align: center;
94
background: var(--color-background);
95
color: var(--color-text);
108
- min-height: 400px;
96
+ min-height: 100vh;
97
}
110
-
98
+ /* Welcome container styles */
99
.welcome-logo-container {
100
display: flex;
101
justify-content: center;
102
align-items: center;
115
- margin-bottom: 1.5rem;
103
}
104
105
.welcome-logo {
119
- width: 120px;
120
- height: 120px;
121
- filter: brightness(1);
106
+ width: 7rem;
107
+ height: 7rem;
108
}
109
124
- /* Dark mode adjustments for logo */
110
+ /* Themes adjustments for logo */
111
+ .light-mode .welcome-logo {
112
+ filter: invert(0.5);
113
+ }
114
.dark-mode .welcome-logo {
126
- filter: brightness(1.2);
115
+ filter: invert(0.8);
116
}
117
118
.welcome-title {
119
font-size: 2rem;
131
- font-weight: 300;
132
- margin-bottom: 0.8rem;
120
+ font-weight: 400;
121
+ margin: var(--spacing-xs) 0 0 0;
122
color: var(--color-text);
123
}
124
125
+ .welcome-action-title {
126
+ margin-bottom: 0;
127
+ }
128
+
129
.welcome-subtitle {
130
font-size: 1rem;
131
margin-bottom: 2rem;
@@ -163,32 +156,19 @@
156
flex-direction: column;
157
align-items: center;
158
text-align: center;
166
- min-height: 120px;
159
+ min-height: 10rem;
160
justify-content: center;
161
}
162
163
.welcome-action-card:hover {
171
- border-color: var(--color-accent);
164
+ border-color: var(--color-accent-dark);
165
background: var(--color-message-bg);
173
- transform: translateY(-2px);
174
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
175
- }
176
-
177
- .dark-mode .welcome-action-card:hover {
178
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
166
}
167
168
.welcome-action-icon {
169
font-size: 2rem;
183
- margin-bottom: 0.8rem;
184
- color: var(--color-accent);
185
- }
186
-
187
- .welcome-action-title {
188
- font-size: 1.1rem;
189
- font-weight: 500;
190
- margin-bottom: 0.4rem;
191
- color: var(--color-text);
170
+ margin-bottom: 0;
171
+ color: var(--color-accent-dark);
172
}
173
174
.welcome-action-description {
@@ -197,12 +177,6 @@
177
line-height: 1.3;
178
}
179
200
- .welcome-footer {
201
- margin-top: 1.5rem;
202
- opacity: 0.7;
203
- font-size: 0.8rem;
204
- }
205
-
180
/* Light mode adjustments */
181
.light-mode .welcome-title {
182
color: #2d2d2d;
@@ -226,37 +200,41 @@
200
201
/* Responsive design */
202
@media (max-width: 768px) {
229
- .welcome-container {
230
- padding: 1rem;
231
- }
232
-
233
- .welcome-title {
234
- font-size: 2rem;
235
- }
236
-
237
- .welcome-subtitle {
238
- font-size: 1rem;
239
- margin-bottom: 2rem;
240
- }
241
-
242
- .welcome-actions {
243
- grid-template-columns: 1fr;
244
- gap: 1rem;
245
- margin-bottom: 2rem;
246
- }
247
-
248
- .welcome-action-card {
249
- padding: 1.5rem;
250
- min-height: 120px;
251
- }
252
-
253
- .welcome-action-icon {
254
- font-size: 2rem;
255
- }
256
-
257
- .welcome-action-title {
258
- font-size: 1.1rem;
259
- }
203
+ .welcome-container {
204
+ padding: 1rem;
205
+ padding-top: 3rem;
206
+ }
207
+ .welcome-logo {
208
+ width: 5rem;
209
+ height: 5rem;
210
+ }
211
+ .welcome-logo-container {
212
+ margin-top: 2rem;
213
+ }
214
+ .welcome-title {
215
+ font-size: 1.5rem;
216
+ margin: 0 !important;
217
+ }
218
+ .welcome-subtitle {
219
+ font-size: 1rem;
220
+ margin-top: var(--spacing-xs);
221
+ margin-bottom: 0;
222
+ }
223
+ .welcome-actions {
224
+ grid-template-columns: 1fr;
225
+ gap: 1rem;
226
+ margin-bottom: 2rem;
227
+ }
228
+ .welcome-action-card {
229
+ padding: 1.5rem;
230
+ min-height: 120px;
231
+ }
232
+ .welcome-action-icon {
233
+ font-size: 2rem;
234
+ }
235
+ .welcome-action-title {
236
+ font-size: 1.1rem;
237
+ }
238
}
239
</style>
240
</body>
webui/index.css
+14
@@ -164,9 +164,23 @@ img {
164
transition: margin-left var(--transition-speed) ease-in-out;
165
}
166
167
+/* Scrollbar styles for right panel */
168
#right-panel.expanded {
169
margin-left: 0;
170
}
171
+div#right-panel::-webkit-scrollbar {
172
+ width: 6px;
173
+}
174
+div#right-panel::-webkit-scrollbar-track {
175
+ background: transparent;
176
+}
177
+div#right-panel::-webkit-scrollbar-thumb {
178
+ background-color: rgba(155, 155, 155, 0.3);
179
+ border-radius: 6px;
180
+}
181
+div#right-panel::-webkit-scrollbar-thumb:hover {
182
+ background-color: rgba(155, 155, 155, 0.5);
183
+}
184
185
#time-date-container {
186
z-index: 1000;
webui/index.html
+12
-10
@@ -128,8 +128,6 @@
128
129
<!-- Non-module scripts after Alpine.js -->
130
<script type="text/javascript" src="js/settings.js"></script>
131
- <script type="text/javascript" src="js/file_browser.js"></script>
132
- <script type="module" src="js/tunnel.js"></script>
131
<script>
132
// Expose git info for sidebar component
133
globalThis.gitinfo = { version: "{{version_no}}", commit_time: "{{version_time}}" };
@@ -139,10 +137,11 @@
137
<body class="dark-mode device-pointer" x-data>
138
<div class="container">
139
<!-- Sidebar Overlay -->
142
- <template x-if="$store.sidebar">
143
- <div id="sidebar-overlay" class="sidebar-overlay" x-data
144
- :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}"
145
- @click="$store.sidebar.close()"></div>
140
+ <div id="sidebar-overlay" x-data>
141
+ <template x-if="$store.sidebar">
142
+ <div class="sidebar-overlay" :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}" @click="$store.sidebar.close()"></div>
143
+ </template>
144
+ </div>
145
</template>
146
<!-- Left Sidebar (Header Icons, Quick Actions, Tabs, Chats, Tasks) -->
147
<x-component path="sidebar/left-sidebar.html"></x-component>
@@ -190,10 +189,13 @@
189
<button class="toast__copy" style="display: none;">Copy</button>
190
<button class="toast__close">Close</button>
191
</div>
193
- <!-- Progress Bar -->
194
- <x-component path="chat/input/progress.html"></x-component>
195
- <!-- Input Section -->
196
- <x-component path="chat/input/chat-bar.html"></x-component>
192
+ <!-- Chat Input Area -->
193
+ <div x-show="!$store.welcomeStore || !$store.welcomeStore.isVisible">
194
+ <!-- Progress Bar -->
195
+ <x-component path="chat/input/progress.html"></x-component>
196
+ <!-- Input Section -->
197
+ <x-component path="chat/input/chat-bar.html"></x-component>
198
+ </div>
199
</div>
200
</div>
201
<div id="settingsModal" x-data="settingsModalProxy">
webui/index.js
+38
-15
@@ -331,26 +331,37 @@ async function poll() {
331
// Check if this context exists in the chats list
332
const contextExists = chatsStore.contains(context);
333
334
- // If it doesn't exist in the chats list, try to select the first chat
335
- if (!contextExists && chatsStore.contexts.length > 0) {
336
- const firstChatId = chatsStore.firstId();
337
- if (firstChatId) {
338
- setContext(firstChatId);
339
- chatsStore.setSelected(firstChatId);
334
+ if (!contextExists) {
335
+ if (chatsStore.contexts.length > 0) {
336
+ // If it doesn't exist in the list but other contexts do, fall back to the first
337
+ const firstChatId = chatsStore.firstId();
338
+ if (firstChatId) {
339
+ setContext(firstChatId);
340
+ chatsStore.setSelected(firstChatId);
341
+ }
342
+ } else if (typeof deselectChat === "function") {
343
+ // No contexts remain – clear state so the welcome screen can surface
344
+ deselectChat();
345
}
346
+ } else {
347
+ tasksStore.setSelected(context);
348
}
342
-
343
- tasksStore.setSelected(context);
349
} else {
345
- // No context selected, try to select the first available item
346
- if (contexts.length > 0) {
347
- const firstChatId = chatsStore.firstId();
348
- if (firstChatId) {
349
- setContext(firstChatId);
350
- chatsStore.setSelected(firstChatId);
351
- }
350
+ const welcomeStore =
351
+ globalThis.Alpine && typeof globalThis.Alpine.store === "function"
352
+ ? globalThis.Alpine.store("welcomeStore")
353
+ : null;
354
+ const welcomeVisible = Boolean(welcomeStore && welcomeStore.isVisible);
355
+
356
+ // No context selected, try to select the first available item unless welcome screen is active
357
+ if (!welcomeVisible && contexts.length > 0) {
358
+ const firstChatId = chatsStore.firstId();
359
+ if (firstChatId) {
360
+ setContext(firstChatId);
361
+ chatsStore.setSelected(firstChatId);
362
}
363
}
364
+ }
365
366
lastLogVersion = response.log_version;
367
lastLogGuid = response.log_guid;
@@ -617,6 +628,18 @@ document.addEventListener("DOMContentLoaded", function () {
628
chatHistory.addEventListener("scroll", updateAfterScroll);
629
}
630
631
+ // Restore previously selected context (if any) before polling begins
632
+ let storedContext = null;
633
+ try {
634
+ storedContext = localStorage.getItem("lastSelectedChat");
635
+ } catch (_e) {
636
+ storedContext = null;
637
+ }
638
+
639
+ if (storedContext && storedContext !== "null" && storedContext !== "") {
640
+ setContext(storedContext);
641
+ }
642
+
643
// Start polling for updates
644
startPolling();
645
});