feat: Tabbed Settings - implement support and move existing settings to 3 tab categories
Rafael Uzarowski committed
Mar 22, 2025 at 18:09 UTC
1eb6e735a45b4452998fbc453c285fceadfc92c5
5 files changed
+205
-13
python/helpers/settings.py
+10
-1
@@ -1,4 +1,3 @@
1
-import asyncio
1
import json
2
import os
3
import re
@@ -90,6 +89,7 @@ class SettingsSection(TypedDict, total=False):
89
title: str
90
description: str
91
fields: list[SettingsField]
92
+ tab: str # Indicates which tab this section belongs to
93
94
95
class SettingsOutput(TypedDict):
@@ -206,6 +206,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
206
"title": "Chat Model",
207
"description": "Selection and settings for main chat model used by Agent Zero",
208
"fields": chat_model_fields,
209
+ "tab": "agent",
210
}
211
212
# main model section
@@ -275,6 +276,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
276
"title": "Utility model",
277
"description": "Smaller, cheaper, faster model for handling utility tasks like organizing memory, preparing prompts, summarizing.",
278
"fields": util_model_fields,
279
+ "tab": "agent",
280
}
281
282
# embedding model section
@@ -334,6 +336,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
336
"title": "Embedding Model",
337
"description": "Settings for the embedding model used by Agent Zero.",
338
"fields": embed_model_fields,
339
+ "tab": "agent",
340
}
341
342
# embedding model section
@@ -383,6 +386,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
386
"title": "Web Browser Model",
387
"description": "Settings for the web browser model. Agent Zero uses <a href='https://github.com/browser-use/browser-use' target='_blank'>browser-use</a> agentic framework to handle web interactions.",
388
"fields": browser_model_fields,
389
+ "tab": "agent",
390
}
391
392
# # Memory settings section
@@ -447,6 +451,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
451
"title": "Authentication",
452
"description": "Settings for authentication to use Agent Zero Web UI.",
453
"fields": auth_fields,
454
+ "tab": "external",
455
}
456
457
# api keys model section
@@ -476,6 +481,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
481
"title": "API Keys",
482
"description": "API keys for model providers and services used by Agent Zero.",
483
"fields": api_keys_fields,
484
+ "tab": "external",
485
}
486
487
# Agent config section
@@ -528,6 +534,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
534
"title": "Agent Config",
535
"description": "Agent parameters.",
536
"fields": agent_fields,
537
+ "tab": "agent",
538
}
539
540
dev_fields: list[SettingsField] = []
@@ -593,6 +600,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
600
"title": "Development",
601
"description": "Parameters for A0 framework development. RFCs (remote function calls) are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker. This is crucial for development as A0 needs to run in standardized environment to support all features.",
602
"fields": dev_fields,
603
+ "tab": "developer",
604
}
605
606
# Speech to text section
@@ -664,6 +672,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
672
"title": "Speech to Text",
673
"description": "Voice transcription preferences and server turn detection settings.",
674
"fields": stt_fields,
675
+ "tab": "agent",
676
}
677
678
# Add the section to the result
python/helpers/task_scheduler.py
+5
-5
@@ -320,15 +320,15 @@ class TaskScheduler:
320
321
async def _run_task_wrapper(task: Union[ScheduledTask, AdHocTask]):
322
if task.state != "idle":
323
- self._printer.print(f"Scheduler Task {task.name} state is '{task.state}', skipping")
323
+ self._printer.print(f"Scheduler Task '{task.name}' state is '{task.state}', skipping")
324
return
325
326
if task.state == "running":
327
- self._printer.print(f"Scheduler Task {task.name} already running, skipping")
327
+ self._printer.print(f"Scheduler Task '{task.name}' already running, skipping")
328
return
329
330
try:
331
- self._printer.print(f"Scheduler Task {task.name} started")
331
+ self._printer.print(f"Scheduler Task '{task.name}' started")
332
333
task.update(state="running")
334
await self._tasks.save()
@@ -374,7 +374,7 @@ class TaskScheduler:
374
await self._persist_chat(task, context)
375
376
except Exception as e:
377
- self._printer.print(f"Scheduler Task {task.name} failed: {e}")
377
+ self._printer.print(f"Scheduler Task '{task.name}' failed: {e}")
378
task.update(last_result=f"ERROR: {str(e)}")
379
if agent:
380
agent.handle_critical_exception(e)
@@ -387,7 +387,7 @@ class TaskScheduler:
387
)
388
await self._tasks.save()
389
except Exception as e:
390
- self._printer.print(f"Scheduler Task {task.name} failed to save: {e}")
390
+ self._printer.print(f"Scheduler Task '{task.name}' failed to save: {e}")
391
392
deferred_task = DeferredTask(thread_name=self.__class__.__name__)
393
deferred_task.start_task(_run_task_wrapper, task)
webui/css/settings.css
+141
-3
@@ -214,7 +214,7 @@ nav ul li a img {
214
}
215
}
216
217
-@media (max-width: 640px) {
217
+@media (max-width: 640px) {
218
nav ul {
219
grid-template-columns: repeat(2, 1.2fr);
220
}
@@ -224,17 +224,155 @@ nav ul li a img {
224
nav ul {
225
grid-template-columns: 1fr;
226
}
227
-
227
+
228
nav ul li a {
229
flex-direction: row;
230
justify-content: flex-start;
231
gap: 1rem;
232
padding: 0.75rem 1rem;
233
}
234
-
234
+
235
nav ul li a img {
236
margin-bottom: 0;
237
width: 30px;
238
height: 30px;
239
}
240
}
241
+
242
+/* Settings Tab Styles */
243
+.settings-tabs-container {
244
+ width: 100%;
245
+ margin-bottom: 8px;
246
+ padding: 0;
247
+ margin-top: 20px;
248
+ position: relative;
249
+ overflow: visible;
250
+}
251
+
252
+.settings-tabs {
253
+ display: flex;
254
+ width: 100%;
255
+ position: relative;
256
+ gap: 5px;
257
+ border-bottom: 3px solid var(--color-border);
258
+ justify-content: flex-start;
259
+ padding-left: 20px;
260
+ padding-top: 8px;
261
+ overflow-x: auto;
262
+ overflow-y: hidden;
263
+ scrollbar-width: none;
264
+ -ms-overflow-style: none;
265
+ white-space: nowrap;
266
+ -webkit-overflow-scrolling: touch;
267
+}
268
+
269
+.settings-tabs::-webkit-scrollbar {
270
+ display: none;
271
+}
272
+
273
+.settings-tabs::before,
274
+.settings-tabs::after {
275
+ content: '';
276
+ position: absolute;
277
+ top: 2px;
278
+ bottom: 3px;
279
+ width: 20px;
280
+ pointer-events: none;
281
+ z-index: 2;
282
+ opacity: 0.7;
283
+}
284
+
285
+.settings-tabs::before {
286
+ left: 0;
287
+ background: linear-gradient(to right, var(--color-panel), transparent);
288
+}
289
+
290
+.settings-tabs::after {
291
+ right: 0;
292
+ background: linear-gradient(to left, var(--color-panel), transparent);
293
+}
294
+
295
+.settings-tab {
296
+ padding: 8px 16px;
297
+ cursor: pointer;
298
+ position: relative;
299
+ color: var(--color-text);
300
+ border: 2px solid var(--color-border);
301
+ border-bottom: none;
302
+ border-radius: 8px 8px 0 0;
303
+ transition: all 0.3s ease;
304
+ background-color: var(--color-panel);
305
+ margin-bottom: -3px;
306
+ z-index: 1;
307
+ min-width: min-content;
308
+ width: auto;
309
+ max-width: 100px;
310
+ overflow: hidden;
311
+ text-overflow: ellipsis;
312
+ white-space: nowrap;
313
+ flex-shrink: 0;
314
+}
315
+
316
+.settings-tab:not(.active) {
317
+ opacity: 0.8;
318
+ border-bottom: 3px solid var(--color-border);
319
+ background-color: rgba(255, 255, 255, 0.03);
320
+}
321
+
322
+.settings-tab.active {
323
+ color: var(--color-border);
324
+ border-color: var(--color-border);
325
+ box-shadow:
326
+ 0 -4px 8px -2px var(--color-border) 0.5,
327
+ 4px 0 8px -2px var(--color-border) 0.5,
328
+ -4px 0 8px -2px var(--color-border) 0.5;
329
+ font-weight: bold;
330
+ background-color: var(--color-panel);
331
+}
332
+
333
+/* Light mode overrides */
334
+.light-mode .settings-tab.active {
335
+ color: var(--color-border);
336
+ box-shadow:
337
+ 0 -4px 8px -2px var(--color-border) 0.5,
338
+ 4px 0 8px -2px var(--color-border) 0.5,
339
+ -4px 0 8px -2px var(--color-border) 0.5;
340
+}
341
+
342
+.light-mode .settings-tab:not(.active) {
343
+ background-color: rgba(0, 0, 0, 0.03);
344
+}
345
+
346
+.light-mode .settings-tabs::before {
347
+ background: linear-gradient(to right, var(--color-panel-light), transparent);
348
+}
349
+
350
+.light-mode .settings-tabs::after {
351
+ background: linear-gradient(to left, var(--color-panel-light), transparent);
352
+}
353
+
354
+/* Responsive Design for Settings Tabs */
355
+@media (max-width: 640px) {
356
+ .settings-tabs {
357
+ gap: 2px;
358
+ padding-left: 10px;
359
+ }
360
+
361
+ .settings-tab {
362
+ padding: 6px 12px;
363
+ font-size: 0.9rem;
364
+ }
365
+}
366
+
367
+@media (max-width: 480px) {
368
+ .settings-tabs {
369
+ padding-left: 5px;
370
+ }
371
+
372
+ .settings-tab {
373
+ flex: 0 0 auto;
374
+ text-align: center;
375
+ min-width: 60px;
376
+ max-width: 80px;
377
+ }
378
+}
webui/index.html
+20
-2
@@ -458,10 +458,28 @@
458
</div>
459
460
<div class="modal-content">
461
+ <!-- Tab Navigation -->
462
+ <div class="settings-tabs-container">
463
+ <div class="settings-tabs">
464
+ <div class="settings-tab"
465
+ :class="{'active': activeTab === 'agent'}"
466
+ @click="switchTab('agent')"
467
+ title="Agent Settings">Agent Settings</div>
468
+ <div class="settings-tab"
469
+ :class="{'active': activeTab === 'external'}"
470
+ @click="switchTab('external')"
471
+ title="External Services">External Services</div>
472
+ <div class="settings-tab"
473
+ :class="{'active': activeTab === 'developer'}"
474
+ @click="switchTab('developer')"
475
+ title="Developer">Developer</div>
476
+ </div>
477
+ </div>
478
+
479
<div id="settings-sections">
480
<nav>
481
<ul>
464
- <template x-for="(section, index) in settings.sections" :key="section.title">
482
+ <template x-for="(section, index) in filteredSections" :key="section.title">
483
<li>
484
<a :href="'#section' + (index + 1)">
485
<img :src="'/public/' + section.id +'.svg'"
@@ -473,7 +491,7 @@
491
</ul>
492
</nav>
493
</div>
476
- <template x-for="(section, sectionIndex) in settings.sections" :key="sectionIndex">
494
+ <template x-for="(section, sectionIndex) in filteredSections" :key="sectionIndex">
495
<div :id="'section' + (sectionIndex + 1)" class="section">
496
<div class="section-title" x-text="section.title"></div>
497
<div class="section-description" x-html="section.description"></div>
webui/js/settings.js
+29
-2
@@ -2,7 +2,34 @@ const settingsModalProxy = {
2
isOpen: false,
3
settings: {},
4
resolvePromise: null,
5
+ activeTab: 'agent', // Default tab
6
7
+ // Computed property for filtered sections
8
+ get filteredSections() {
9
+ if (!this.settings || !this.settings.sections) return [];
10
+ const filteredSections = this.settings.sections.filter(section => section.tab === this.activeTab);
11
+
12
+ // If no sections match the current tab (or all tabs are missing), show all sections
13
+ if (filteredSections.length === 0) {
14
+ return this.settings.sections;
15
+ }
16
+
17
+ return filteredSections;
18
+ },
19
+
20
+ // Switch tab method
21
+ switchTab(tabName) {
22
+ this.activeTab = tabName;
23
+ localStorage.setItem('settingsActiveTab', tabName);
24
+
25
+ // Auto-scroll active tab into view (added after a small delay to ensure DOM updates)
26
+ setTimeout(() => {
27
+ const activeTab = document.querySelector('.settings-tab.active');
28
+ if (activeTab) {
29
+ activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
30
+ }
31
+ }, 10);
32
+ },
33
34
async openModal() {
35
@@ -13,6 +40,8 @@ const settingsModalProxy = {
40
try {
41
const set = await sendJsonData("/settings_get", null);
42
43
+ // Restore active tab from localStorage or use default
44
+ this.activeTab = localStorage.getItem('settingsActiveTab') || 'agent';
45
46
const settings = {
47
"title": "Settings",
@@ -35,8 +64,6 @@ const settingsModalProxy = {
64
modalAD.isOpen = true; // Update directly
65
modalAD.settings = settings; // Update directly
66
38
-
39
-
67
return new Promise(resolve => {
68
this.resolvePromise = resolve;
69
});