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 });