Refine settings and remote access UX

Restyle Settings and standard modals around a streamlined left-rail layout, clearer section hierarchy, advanced settings disclosures, and stronger update states. Add persistent update visibility with quieter once-daily update notifications, plus Remote Link and Space Agent actions in the canvas rail. Refresh the tunnel experience as a normal Remote Link modal with clearer copy, QR/mobile affordances, and safer state handling.

Alessandro committed Apr 27, 2026 at 02:48 UTC a1c12e92470b8f7b590224764d1fd94e5ab2a3c8
33 files changed +1835 -1245
extensions/python/user_message_ui/_10_update_check.py
+45 -4
@@ -1,20 +1,51 @@
1 from helpers import notification
2 from helpers.extension import Extension
3 from agent import LoopData
4 -from helpers import settings, update_check
4 +from helpers import files, settings, update_check
5 import datetime
6 +import json
7
8
9 # check for newer versions of A0 available and send notification
10 # check after user message is sent from UI, not API, MCP etc. (user is active and can see the notification)
11 # do not check too often, use cooldown
11 -# do not notify too often unless there's a different notification
12 +# do not notify too often
13
14 last_check = datetime.datetime.fromtimestamp(0)
15 check_cooldown_seconds = 60
16 last_notification_id = None
17 last_notification_time = datetime.datetime.fromtimestamp(0)
18 notification_cooldown_seconds = 60 * 60 * 24
19 +notification_state_file = "usr/update-check-state.json"
20 +
21 +
22 +def _load_notification_state() -> dict:
23 + try:
24 + return json.loads(files.read_file(notification_state_file))
25 + except Exception:
26 + return {}
27 +
28 +
29 +def _parse_timestamp(value: str | None) -> datetime.datetime | None:
30 + if not value:
31 + return None
32 + try:
33 + parsed = datetime.datetime.fromisoformat(value)
34 + except ValueError:
35 + return None
36 + if parsed.tzinfo:
37 + return parsed.astimezone(datetime.timezone.utc).replace(tzinfo=None)
38 + return parsed
39 +
40 +
41 +def _remember_notification(notif: dict, now: datetime.datetime):
42 + state = {
43 + "last_notification_at": now.replace(tzinfo=datetime.timezone.utc).isoformat(),
44 + "last_notification_id": notif.get("id") or "",
45 + "last_notification_group": notif.get("group", "update_check"),
46 + }
47 + files.write_file(notification_state_file, json.dumps(state, indent=2))
48 +
49
50 class UpdateCheck(Extension):
51
@@ -40,9 +71,18 @@ class UpdateCheck(Extension):
71
72 # if the user should update, send notification
73 if notif := version.get("notification"):
43 - if notif.get("id") != last_notification_id or (datetime.datetime.now() - last_notification_time).total_seconds() > notification_cooldown_seconds:
74 + now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
75 + stored_state = _load_notification_state()
76 + stored_notification_time = _parse_timestamp(stored_state.get("last_notification_at"))
77 + effective_notification_time = stored_notification_time or last_notification_time
78 +
79 + if (now - effective_notification_time).total_seconds() > notification_cooldown_seconds:
80 last_notification_id = notif.get("id")
45 - last_notification_time = datetime.datetime.now()
81 + last_notification_time = now
82 + try:
83 + _remember_notification(notif, now)
84 + except Exception:
85 + pass
86 self.send_notification(notif)
87 except Exception as e:
88 pass # no need to log if the update server is inaccessible
@@ -61,4 +101,5 @@ class UpdateCheck(Extension):
101 display_time=notif.get("display_time", 10),
102 group=notif.get("group", "update_check"),
103 priority=notif.get("priority", notification.NotificationPriority.NORMAL),
104 + id=notif.get("id", "update_check_available"),
105 )
extensions/webui/right_canvas_register_surfaces/register-remote-link.js new
+18
@@ -0,0 +1,18 @@
1 +const REMOTE_LINK_MODAL_PATH = "settings/tunnel/remote-link.html";
2 +
3 +export default async function registerRemoteLinkAction(canvas) {
4 + canvas.registerSurface({
5 + id: "remote-link",
6 + title: "Remote Link",
7 + icon: "share",
8 + order: 31,
9 + actionOnly: true,
10 + async open() {
11 + if (typeof globalThis.ensureModalOpen === "function") {
12 + await globalThis.ensureModalOpen(REMOTE_LINK_MODAL_PATH);
13 + return;
14 + }
15 + await globalThis.openModal?.(REMOTE_LINK_MODAL_PATH);
16 + },
17 + });
18 +}
extensions/webui/right_canvas_register_surfaces/register-space-agent.js new
+14
@@ -0,0 +1,14 @@
1 +const SPACE_AGENT_URL = "https://space-agent.ai/";
2 +
3 +export default async function registerSpaceAgentAction(canvas) {
4 + canvas.registerSurface({
5 + id: "space-agent",
6 + title: "Space Agent",
7 + image: "/public/space-agent-icon-512.webp",
8 + order: 32,
9 + actionOnly: true,
10 + open() {
11 + globalThis.open(SPACE_AGENT_URL, "_blank", "noopener,noreferrer");
12 + },
13 + });
14 +}
webui/components/canvas/right-canvas-store.js
+26 -6
@@ -64,11 +64,13 @@ const model = {
64 const normalized = {
65 title: surface.id,
66 icon: "web_asset",
67 + image: "",
68 order: 100,
69 canOpen: () => true,
70 open: () => {},
71 close: () => {},
72 modalPath: "",
73 + actionOnly: false,
74 ...surface,
75 };
76
@@ -83,23 +85,33 @@ const model = {
85 },
86
87 ensureActiveSurface() {
86 - if (!this.surfaces.length) {
88 + const panelSurfaces = this.panelSurfaces;
89 + if (!panelSurfaces.length) {
90 this.activeSurfaceId = "";
91 return;
92 }
90 - if (!this.surfaces.some((surface) => surface.id === this.activeSurfaceId)) {
91 - this.activeSurfaceId = this.surfaces[0].id;
93 + if (!panelSurfaces.some((surface) => surface.id === this.activeSurfaceId)) {
94 + this.activeSurfaceId = panelSurfaces[0].id;
95 }
96 },
97
98 async open(surfaceId = "", payload = {}) {
96 - const targetId = surfaceId || this.activeSurfaceId || this.surfaces[0]?.id || "";
99 + const targetId = surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "";
100 const surface = this.getSurface(targetId);
101 if (!surface) return false;
102 if (typeof surface.canOpen === "function" && surface.canOpen(payload) === false) {
103 return false;
104 }
105
106 + if (surface.actionOnly) {
107 + try {
108 + await surface.open?.(payload || {});
109 + } catch (error) {
110 + console.error(`Canvas action ${targetId} failed`, error);
111 + }
112 + return true;
113 + }
114 +
115 this.activeSurfaceId = targetId;
116 this.isOpen = true;
117 this._lastPayloadBySurface[targetId] = payload || {};
@@ -167,7 +179,7 @@ const model = {
179 },
180
181 async toggle(surfaceId = "", payload = {}) {
170 - const targetId = surfaceId || this.activeSurfaceId || this.surfaces[0]?.id || "";
182 + const targetId = surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "";
183 if (this.isOpen && targetId === this.activeSurfaceId) {
184 await this.close();
185 return false;
@@ -180,7 +192,7 @@ const model = {
192 await this.close();
193 return false;
194 }
183 - return await this.open(this.activeSurfaceId || this.surfaces[0]?.id || "");
195 + return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
196 },
197
198 setWidth(px, options = {}) {
@@ -276,6 +288,14 @@ const model = {
288 return this.surfaces.find((surface) => surface.id === id) || null;
289 },
290
291 + get railSurfaces() {
292 + return this.surfaces;
293 + },
294 +
295 + get panelSurfaces() {
296 + return this.surfaces.filter((surface) => !surface.actionOnly);
297 + },
298 +
299 currentSurface() {
300 return this.getSurface(this.activeSurfaceId);
301 },
webui/components/canvas/right-canvas.css
+16 -1
@@ -40,7 +40,7 @@ body.right-canvas-resizing {
40
41 .right-canvas-rail {
42 position: absolute;
43 - top: 50%;
43 + top: 24%;
44 right: 100%;
45 z-index: 4;
46 display: flex;
@@ -125,6 +125,14 @@ body.right-canvas-resizing {
125 font-size: 19px;
126 }
127
128 +.right-canvas-surface-image {
129 + display: block;
130 + width: 22px;
131 + height: 22px;
132 + border-radius: 6px;
133 + object-fit: cover;
134 +}
135 +
136 .right-canvas-shell {
137 display: flex;
138 flex: 1 1 auto;
@@ -194,6 +202,13 @@ body.right-canvas-resizing {
202 font-size: 18px;
203 }
204
205 +.right-canvas-tab .right-canvas-surface-image {
206 + flex: 0 0 auto;
207 + width: 18px;
208 + height: 18px;
209 + border-radius: 5px;
210 +}
211 +
212 .right-canvas-tab-label {
213 min-width: 0;
214 overflow: hidden;
webui/components/canvas/right-canvas.html
+15 -5
@@ -38,7 +38,7 @@
38
39 <div class="right-canvas-rail-separator" aria-hidden="true"></div>
40
41 - <template x-for="surface in $store.rightCanvas.surfaces" :key="surface.id">
41 + <template x-for="surface in $store.rightCanvas.railSurfaces" :key="surface.id">
42 <button
43 type="button"
44 class="right-canvas-rail-button"
@@ -47,7 +47,12 @@
47 :aria-label="surface.title"
48 @click="$store.rightCanvas.open(surface.id)"
49 >
50 - <span class="material-symbols-outlined" x-text="surface.icon"></span>
50 + <template x-if="surface.image">
51 + <img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
52 + </template>
53 + <template x-if="!surface.image">
54 + <span class="material-symbols-outlined" x-text="surface.icon"></span>
55 + </template>
56 </button>
57 </template>
58 </nav>
@@ -58,7 +63,7 @@
63 <header class="right-canvas-header">
64 <div class="right-canvas-tabs" role="tablist" aria-label="Canvas tabs">
65 <x-extension id="right-canvas-tabs-start"></x-extension>
61 - <template x-for="surface in $store.rightCanvas.surfaces" :key="surface.id">
66 + <template x-for="surface in $store.rightCanvas.panelSurfaces" :key="surface.id">
67 <button
68 type="button"
69 class="right-canvas-tab"
@@ -68,7 +73,12 @@
73 :class="{ 'is-active': $store.rightCanvas.isSurfaceActive(surface.id) }"
74 @click="$store.rightCanvas.open(surface.id)"
75 >
71 - <span class="material-symbols-outlined" aria-hidden="true" x-text="surface.icon"></span>
76 + <template x-if="surface.image">
77 + <img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
78 + </template>
79 + <template x-if="!surface.image">
80 + <span class="material-symbols-outlined" aria-hidden="true" x-text="surface.icon"></span>
81 + </template>
82 <span class="right-canvas-tab-label" x-text="surface.title"></span>
83 </button>
84 </template>
@@ -93,7 +103,7 @@
103
104 <main class="right-canvas-panels">
105 <x-extension id="right-canvas-panels"></x-extension>
96 - <template x-if="$store.rightCanvas.surfaces.length === 0">
106 + <template x-if="$store.rightCanvas.panelSurfaces.length === 0">
107 <div class="right-canvas-empty-state">
108 <x-extension id="right-canvas-empty-state"></x-extension>
109 <span class="material-symbols-outlined">space_dashboard</span>
webui/components/settings/a2a/a2a-server.html
+2 -2
@@ -9,14 +9,14 @@
9 <div>
10 <div class="section-title">A0 A2A Server</div>
11 <div class="section-description">
12 - Agent Zero can be exposed as an A2A server. See <a href="javascript:openModal('settings/a2a/a2a-connection.html')">connection example</a>.
12 + Expose this Agent Zero instance to other agents over A2A. See <a href="javascript:openModal('settings/a2a/a2a-connection.html')">connection example</a>.
13 </div>
14
15 <div class="field">
16 <div class="field-label">
17 <div class="field-title">Enable A2A server</div>
18 <div class="field-description">
19 - Expose Agent Zero as A2A server. This allows other agents to connect to A0 via A2A protocol.
19 + Let trusted agents connect to A0 through the A2A protocol.
20 </div>
21 </div>
22 <div class="field-control">
webui/components/settings/agent/agent.html
+37 -27
@@ -9,14 +9,14 @@
9 <div>
10 <div class="section-title">Agent Config</div>
11 <div class="section-description">
12 - Agent parameters.
12 + Choose the default personality and operating profile for new chats.
13 </div>
14
15 <div class="field">
16 <div class="field-label">
17 <div class="field-title">Default agent profile</div>
18 <div class="field-description">
19 - Profile used for new top-level chats. Existing chats keep their own active profile; use the chat composer selector to change the selected chat.
19 + New top-level chats start here. Existing chats keep their own selected profile.
20 </div>
21 </div>
22 <div class="field-control">
@@ -28,35 +28,45 @@
28 </div>
29 </div>
30
31 - <div class="field">
32 - <div class="field-label">
33 - <div class="field-title">Knowledge subdirectory</div>
34 - <div class="field-description">
35 - Subdirectory of /knowledge folder to use for agent knowledge import. 'default' subfolder is always imported and contains framework knowledge.
31 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
32 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
33 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
34 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
35 + <span>Advanced Settings</span>
36 + </button>
37 +
38 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
39 + <div class="field">
40 + <div class="field-label">
41 + <div class="field-title">Knowledge subdirectory</div>
42 + <div class="field-description">
43 + Add a specialized knowledge folder on top of the framework defaults.
44 + </div>
45 + </div>
46 + <div class="field-control">
47 + <select x-model="$store.settings.settings.agent_knowledge_subdir">
48 + <template x-for="option in $store.settings.additional?.knowledge_subdirs" :key="option.value">
49 + <option :value="option.value" :selected="option.value === $store.settings.settings.agent_knowledge_subdir" x-text="option.label"></option>
50 + </template>
51 + </select>
52 + </div>
53 </div>
37 - </div>
38 - <div class="field-control">
39 - <select x-model="$store.settings.settings.agent_knowledge_subdir">
40 - <template x-for="option in $store.settings.additional?.knowledge_subdirs" :key="option.value">
41 - <option :value="option.value" :selected="option.value === $store.settings.settings.agent_knowledge_subdir" x-text="option.label"></option>
42 - </template>
43 - </select>
44 - </div>
45 - </div>
54
47 - <div class="field">
48 - <div class="field-label">
49 - <div class="field-title">Inherit active project</div>
50 - <div class="field-description">
51 - When creating a new chat, automatically inherit the active project from the current chat.
55 + <div class="field">
56 + <div class="field-label">
57 + <div class="field-title">Inherit active project</div>
58 + <div class="field-description">
59 + Start new chats in the same project context as the current chat.
60 + </div>
61 + </div>
62 + <div class="field-control">
63 + <label class="toggle">
64 + <input type="checkbox" x-model="$store.settings.settings.chat_inherit_project">
65 + <span class="toggler"> </span>
66 + </label>
67 + </div>
68 </div>
69 </div>
54 - <div class="field-control">
55 - <label class="toggle">
56 - <input type="checkbox" x-model="$store.settings.settings.chat_inherit_project">
57 - <span class="toggler"> </span>
58 - </label>
59 - </div>
70 </div>
71 </div>
72 </template>
webui/components/settings/agent/speech.html
+55 -45
@@ -9,13 +9,13 @@
9 <div>
10 <div class="section-title">Speech</div>
11 <div class="section-description">
12 - Voice transcription and speech synthesis settings.
12 + Pick the microphone, transcription model, and voice output behavior.
13 </div>
14
15 <div class="field field-full">
16 <div class="field-label">
17 <div class="field-title">Microphone device</div>
18 - <div class="field-description">Select the microphone device to use for speech-to-text.</div>
18 + <div class="field-description">Choose the input device Agent Zero listens to.</div>
19 </div>
20 <div class="field-control">
21 <x-component path="settings/speech/microphone.html"></x-component>
@@ -25,7 +25,7 @@
25 <div class="field">
26 <div class="field-label">
27 <div class="field-title">Speech-to-text model size</div>
28 - <div class="field-description">Select the speech-to-text model size</div>
28 + <div class="field-description">Larger models can hear more accurately, but need more time and memory.</div>
29 </div>
30 <div class="field-control">
31 <select x-model="$store.settings.settings.stt_model_size">
@@ -36,52 +36,11 @@
36 </div>
37 </div>
38
39 - <div class="field">
40 - <div class="field-label">
41 - <div class="field-title">Speech-to-text language code</div>
42 - <div class="field-description">Language code (e.g. en, fr, it)</div>
43 - </div>
44 - <div class="field-control">
45 - <input type="text" x-model="$store.settings.settings.stt_language" />
46 - </div>
47 - </div>
48 -
49 - <div class="field">
50 - <div class="field-label">
51 - <div class="field-title">Microphone silence threshold</div>
52 - <div class="field-description">Silence detection threshold. Lower values are more sensitive to noise.</div>
53 - </div>
54 - <div class="field-control">
55 - <input type="range" min="0" max="1" step="0.01" x-model.number="$store.settings.settings.stt_silence_threshold" />
56 - <span class="range-value" x-text="$store.settings.settings.stt_silence_threshold"></span>
57 - </div>
58 - </div>
59 -
60 - <div class="field">
61 - <div class="field-label">
62 - <div class="field-title">Microphone silence duration (ms)</div>
63 - <div class="field-description">Duration of silence before the system considers speaking to have ended.</div>
64 - </div>
65 - <div class="field-control">
66 - <input type="number" x-model.number="$store.settings.settings.stt_silence_duration" />
67 - </div>
68 - </div>
69 -
70 - <div class="field">
71 - <div class="field-label">
72 - <div class="field-title">Microphone waiting timeout (ms)</div>
73 - <div class="field-description">Duration of silence before the system closes the microphone.</div>
74 - </div>
75 - <div class="field-control">
76 - <input type="number" x-model.number="$store.settings.settings.stt_waiting_timeout" />
77 - </div>
78 - </div>
79 -
39 <div class="field">
40 <div class="field-label">
41 <div class="field-title">Enable Kokoro TTS</div>
42 <div class="field-description">
84 - Enable higher quality server-side AI (Kokoro) instead of browser-based text-to-speech.
43 + Use higher-quality server-side speech instead of the browser voice.
44 </div>
45 </div>
46 <div class="field-control">
@@ -91,6 +50,57 @@
50 </label>
51 </div>
52 </div>
53 +
54 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
55 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
56 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
57 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
58 + <span>Advanced Settings</span>
59 + </button>
60 +
61 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
62 + <div class="field">
63 + <div class="field-label">
64 + <div class="field-title">Language hint</div>
65 + <div class="field-description">Use a short language code such as en, fr, or it when automatic detection needs guidance.</div>
66 + </div>
67 + <div class="field-control">
68 + <input type="text" x-model="$store.settings.settings.stt_language" />
69 + </div>
70 + </div>
71 +
72 + <div class="field">
73 + <div class="field-label">
74 + <div class="field-title">Silence threshold</div>
75 + <div class="field-description">Lower values catch softer speech; higher values ignore more room noise.</div>
76 + </div>
77 + <div class="field-control">
78 + <input type="range" min="0" max="1" step="0.01" x-model.number="$store.settings.settings.stt_silence_threshold" />
79 + <span class="range-value" x-text="$store.settings.settings.stt_silence_threshold"></span>
80 + </div>
81 + </div>
82 +
83 + <div class="field">
84 + <div class="field-label">
85 + <div class="field-title">End-of-speech delay</div>
86 + <div class="field-description">How long silence must last before Agent Zero treats your sentence as complete.</div>
87 + </div>
88 + <div class="field-control">
89 + <input type="number" x-model.number="$store.settings.settings.stt_silence_duration" />
90 + </div>
91 + </div>
92 +
93 + <div class="field">
94 + <div class="field-label">
95 + <div class="field-title">Microphone close delay</div>
96 + <div class="field-description">How long Agent Zero waits before closing the microphone after speech stops.</div>
97 + </div>
98 + <div class="field-control">
99 + <input type="number" x-model.number="$store.settings.settings.stt_waiting_timeout" />
100 + </div>
101 + </div>
102 + </div>
103 + </div>
104 </div>
105 </template>
106 </div>
webui/components/settings/agent/workdir.html
+60 -52
@@ -14,16 +14,17 @@
14 <div>
15 <div class="section-title">Default working directory</div>
16 <div class="section-description">
17 - When project is not selected, agent is instructed to use this directory as it's workplace. New terminal sessions will be spawned with this CWD.
17 + The fallback workspace for chats that are not attached to a project.
18 </div>
19
20 <div class="field">
21 <div class="field-label">
22 <div class="field-title">Workdir path</div>
23 - <div class="field-description">Full path to the working directory inside the linux container. Default is /a0/usr/workdir.</div>
23 + <div class="field-description">Use a Linux path inside the container, for example <code>/a0/usr/workdir</code>.</div>
24 </div>
25 <div class="field-control">
26 <input type="text" x-model="$store.settings.settings.workdir_path" />
27 + <button class="btn btn-field settings-inline-button" @click="$store.fileBrowser.open($store.settings.settings.workdir_path)">Browse</button>
28 </div>
29 </div>
30
@@ -31,7 +32,7 @@
32 <div class="field-label">
33 <div class="field-title">Show workdir structure to the agent</div>
34 <div class="field-description">
34 - When turned on, the workdir file structure will be injected into the context window of the agent.
35 + Give the agent a compact map of the workspace at the start of a task.
36 </div>
37 </div>
38 <div class="field-control">
@@ -43,65 +44,72 @@
44 </div>
45
46 <template x-if="$store.settings.settings.workdir_show">
46 - <div>
47 - <div class="field">
48 - <div class="field-label">
49 - <div class="field-title">Max Depth</div>
50 - <div class="field-description">Set the maximum depth for the file structure (0 = unlimited).</div>
51 - </div>
52 - <div class="field-control">
53 - <input type="number" x-model.number="$store.settings.settings.workdir_max_depth" min="0" step="1" />
54 - </div>
55 - </div>
47 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
48 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
49 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
50 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
51 + <span>Advanced Settings</span>
52 + </button>
53
57 - <div class="field">
58 - <div class="field-label">
59 - <div class="field-title">Max Lines</div>
60 - <div class="field-description">Maximum total lines outputted for the agent (0 = unlimited).</div>
54 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
55 + <div class="field">
56 + <div class="field-label">
57 + <div class="field-title">Depth limit</div>
58 + <div class="field-description">How many folder levels to include. Use 0 for no depth limit.</div>
59 + </div>
60 + <div class="field-control">
61 + <input type="number" x-model.number="$store.settings.settings.workdir_max_depth" min="0" step="1" />
62 + </div>
63 </div>
62 - <div class="field-control">
63 - <input type="number" x-model.number="$store.settings.settings.workdir_max_lines" min="0" step="1" />
64 - </div>
65 - </div>
64
67 - <div class="field">
68 - <div class="field-label">
69 - <div class="field-title">Max Folders</div>
70 - <div class="field-description">Maximum number of subfolders to display under one folder (0 = unlimited).</div>
71 - </div>
72 - <div class="field-control">
73 - <input type="number" x-model.number="$store.settings.settings.workdir_max_folders" min="0" step="1" />
65 + <div class="field">
66 + <div class="field-label">
67 + <div class="field-title">Line budget</div>
68 + <div class="field-description">Maximum lines shown to the agent. Use 0 for no line limit.</div>
69 + </div>
70 + <div class="field-control">
71 + <input type="number" x-model.number="$store.settings.settings.workdir_max_lines" min="0" step="1" />
72 + </div>
73 </div>
75 - </div>
74
77 - <div class="field">
78 - <div class="field-label">
79 - <div class="field-title">Max Files</div>
80 - <div class="field-description">Maximum number of files to display under one folder (0 = unlimited).</div>
81 - </div>
82 - <div class="field-control">
83 - <input type="number" x-model.number="$store.settings.settings.workdir_max_files" min="0" step="1" />
75 + <div class="field">
76 + <div class="field-label">
77 + <div class="field-title">Folder limit</div>
78 + <div class="field-description">Maximum subfolders shown per folder. Use 0 for no folder limit.</div>
79 + </div>
80 + <div class="field-control">
81 + <input type="number" x-model.number="$store.settings.settings.workdir_max_folders" min="0" step="1" />
82 + </div>
83 </div>
85 - </div>
84
87 - <div class="field field-full">
88 - <div class="field-label">
89 - <div class="field-title">Ignored files / folders</div>
90 - <div class="field-description">Specify patterns in gitignore format.</div>
85 + <div class="field">
86 + <div class="field-label">
87 + <div class="field-title">File limit</div>
88 + <div class="field-description">Maximum files shown per folder. Use 0 for no file limit.</div>
89 + </div>
90 + <div class="field-control">
91 + <input type="number" x-model.number="$store.settings.settings.workdir_max_files" min="0" step="1" />
92 + </div>
93 </div>
92 - <div class="field-control">
93 - <textarea x-model="$store.settings.settings.workdir_gitignore" rows="5" placeholder="Enter gitignore patterns (e.g., node_modules/, .git/, *.log)"></textarea>
94 - </div>
95 - </div>
94
97 - <div class="field">
98 - <div class="field-label">
99 - <div class="field-title">Test output</div>
100 - <div class="field-description">Preview the current workdir structure output.</div>
95 + <div class="field field-full">
96 + <div class="field-label">
97 + <div class="field-title">Ignored files and folders</div>
98 + <div class="field-description">Gitignore-style patterns that should stay out of the agent’s workspace map.</div>
99 + </div>
100 + <div class="field-control">
101 + <textarea x-model="$store.settings.settings.workdir_gitignore" rows="5" placeholder="node_modules/&#10;.git/&#10;*.log"></textarea>
102 + </div>
103 </div>
102 - <div class="field-control">
103 - <button class="btn btn-field" @click="$store.settings.testWorkdirFileStructure()">Test output</button>
104 - <button class="btn btn-field" style="margin-left: 0.5em;" @click="$store.fileBrowser.open($store.settings.settings.workdir_path)">Browse</button>
104 +
105 + <div class="field">
106 + <div class="field-label">
107 + <div class="field-title">Preview workspace map</div>
108 + <div class="field-description">See exactly what the agent will receive before saving the limits.</div>
109 + </div>
110 + <div class="field-control">
111 + <button class="btn btn-field" @click="$store.settings.testWorkdirFileStructure()">Preview</button>
112 + </div>
113 </div>
114 </div>
115 </div>
webui/components/settings/backup/backup-settings.html
+15 -5
@@ -1,6 +1,6 @@
1 <html>
2 <head>
3 - <title>Update</title>
3 + <title>Check for updates</title>
4 </head>
5
6 <body>
@@ -16,9 +16,9 @@
16 </a>
17 </li>
18 <li>
19 - <a href="#section-backup-restore">
19 + <a href="#section-update-advanced">
20 <img src="/public/backup_restore.svg" alt="Backup & Restore" />
21 - <span>Backup & Restore</span>
21 + <span>Advanced Settings</span>
22 </a>
23 </li>
24 </ul>
@@ -28,8 +28,18 @@
28 <x-component path="settings/backup/self-update.html"></x-component>
29 </div>
30
31 - <div id="section-backup-restore" class="section">
32 - <x-component path="settings/backup/backup_restore.html"></x-component>
31 + <div id="section-update-advanced" class="section" x-data="{ advOpen: false }">
32 + <div class="settings-advanced-section">
33 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
34 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
35 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
36 + <span>Advanced Settings</span>
37 + </button>
38 +
39 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
40 + <x-component path="settings/backup/backup_restore.html"></x-component>
41 + </div>
42 + </div>
43 </div>
44 </div>
45 </template>
webui/components/settings/backup/backup_restore.html
+3 -3
@@ -8,14 +8,14 @@
8 <div>
9 <div class="section-title">Backup &amp; Restore</div>
10 <div class="section-description">
11 - Backup and restore Agent Zero data and configurations using glob pattern-based file selection.
11 + Preserve or restore Agent Zero data with pattern-based file selection.
12 </div>
13
14 <div class="field">
15 <div class="field-label">
16 <div class="field-title">Create Backup</div>
17 <div class="field-description">
18 - Create a backup archive of selected files and configurations using customizable patterns.
18 + Package selected files and configuration into a backup archive.
19 </div>
20 </div>
21 <div class="field-control">
@@ -32,7 +32,7 @@
32 <div class="field-label">
33 <div class="field-title">Restore from Backup</div>
34 <div class="field-description">
35 - Restore files and configurations from a backup archive with pattern-based selection.
35 + Bring selected files back from an existing backup archive.
36 </div>
37 </div>
38 <div class="field-control">
webui/components/settings/backup/self-update.html
+25 -13
@@ -10,36 +10,48 @@
10 <div x-data="{ get settings() { return $store.settings.settings } }">
11 <template x-if="settings">
12 <div>
13 - <div class="section-title">Self Update</div>
13 + <div class="section-title"
14 + :class="{'settings-update-title-attention': $store.settings.hasUpdateAttention}">
15 + Self Update
16 + </div>
17 + <div class="section-description">
18 + Update Agent Zero deliberately, with a backup path available before anything changes.
19 + </div>
20
15 - <div class="field">
16 - <div class="field-label">
17 - <div class="field-title">Open Self Update</div>
18 - <div class="field-description">
19 - Choose the target branch and tag, decide whether to zip <code>/a0/usr</code> first, and review the durable trigger, status, and log file locations used across restarts.
20 - </div>
21 + <div class="settings-update-card"
22 + :class="{'settings-update-card-attention': $store.settings.hasUpdateAttention}">
23 + <div class="settings-update-card-icon" aria-hidden="true">
24 + <span class="material-symbols-outlined"
25 + x-text="$store.settings.hasUpdateAttention ? 'release_alert' : 'system_update_alt'"></span>
26 + </div>
27 + <div class="settings-update-card-copy">
28 + <div class="settings-update-card-title" x-text="$store.settings.updateAttentionTitle"></div>
29 + <div class="settings-update-card-message" x-text="$store.settings.updateAttentionMessage"></div>
30 <template x-if="!$store.settings.additional?.is_dockerized">
22 - <div class="field-description">
23 - This action is currently available only in dockerized installs.
31 + <div class="settings-update-card-message">
32 + Self-update is currently available only in dockerized installs.
33 </div>
34 </template>
35 + <div class="settings-update-card-meta">
36 + <span class="settings-update-card-badge" x-text="$store.settings.updateAttentionLabel"></span>
37 + </div>
38 </div>
27 - <div class="field-control">
39 + <div class="settings-update-card-actions">
40 <button
41 class="btn btn-field"
42 @click="$store.selfUpdateStore.openModal()"
43 :disabled="!$store.settings.additional?.is_dockerized"
44 >
33 - Open Self Update
45 + Review Update
46 </button>
47 </div>
48 </div>
49
50 <div class="field">
51 <div class="field-label">
40 - <div class="field-title">Enable Update Checker</div>
52 + <div class="field-title">Update notifications</div>
53 <div class="field-description">
42 - Periodically check for newer Agent Zero releases and show a notification when an update is recommended.
54 + Check for newer releases quietly. Toasts are intentionally rate-limited; the update status here stays visible.
55 </div>
56 </div>
57 <div class="field-control">
webui/components/settings/developer/dev.html
+78 -69
@@ -9,16 +9,16 @@
9 <div>
10 <div class="section-title">Development</div>
11 <div class="section-description">
12 - 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.
12 + Low-level controls for local framework development and transport diagnostics.
13 </div>
14
15 <template x-if="!$store.settings.additional?.is_dockerized">
16 <div>
17 <div class="field">
18 <div class="field-label">
19 - <div class="field-title">RFC Destination URL</div>
19 + <div class="field-title">RFC destination URL</div>
20 <div class="field-description">
21 - URL of dockerized A0 instance for remote function calls. Do not specify port here.
21 + Base URL of the Dockerized Agent Zero instance used for remote function calls.
22 </div>
23 </div>
24 <div class="field-control">
@@ -30,9 +30,9 @@
30
31 <div class="field">
32 <div class="field-label">
33 - <div class="field-title">RFC Password</div>
33 + <div class="field-title">RFC password</div>
34 <div class="field-description">
35 - Password for remote function calls. Passwords must match on both instances. RFCs can not be used with empty password.
35 + Shared password required by both instances before remote calls are accepted.
36 </div>
37 </div>
38 <div class="field-control">
@@ -45,97 +45,106 @@
45 <div class="field">
46 <div class="field-label">
47 <div class="field-title">RFC HTTP port</div>
48 - <div class="field-description">HTTP port for dockerized instance of A0.</div>
48 + <div class="field-description">HTTP port exposed by the Dockerized Agent Zero instance.</div>
49 </div>
50 <div class="field-control">
51 <input type="number" x-model.number="$store.settings.settings.rfc_port_http" />
52 </div>
53 </div>
54 -
54 </div>
55 </template>
56
58 - <div class="field">
59 - <div class="field-label">
60 - <div class="field-title">Broadcast server restart event</div>
61 - <div class="field-description">
62 - Emit a fire-and-forget <code>server_restart</code> broadcast to clients after the server starts.
63 - </div>
64 - </div>
65 - <div class="field-control">
66 - <label class="toggle">
67 - <input type="checkbox" x-model="$store.settings.settings.websocket_server_restart_enabled" />
68 - <span class="toggler"></span>
69 - </label>
70 - </div>
71 - </div>
72 -
73 - <div class="field">
74 - <div class="field-label">
75 - <div class="field-title">Enable uvicorn access logs</div>
76 - <div class="field-description">
77 - Temporarily enable uvicorn access logs for debugging WebSocket transport issues (default off).
78 - </div>
79 - <template
80 - x-if="$store.settings.additional?.runtime_settings &&
81 - $store.settings.settings?.uvicorn_access_logs_enabled !==
82 - $store.settings.additional.runtime_settings.uvicorn_access_logs_enabled"
83 - >
84 - <div class="field-description">
85 - Applies after backend restart.
86 - </div>
87 - </template>
88 - </div>
89 - <div class="field-control">
90 - <label class="toggle">
91 - <input type="checkbox" x-model="$store.settings.settings.uvicorn_access_logs_enabled" />
92 - <span class="toggler"></span>
93 - </label>
94 - </div>
95 - </div>
96 -
97 - <template x-if="!$store.settings.additional?.is_dockerized">
98 - <div>
99 - <div class="section-title">Testing</div>
100 - <div class="section-description">
101 - Utilities for validating WebSocket infrastructure in development environments.
102 - </div>
57 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
58 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
59 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
60 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
61 + <span>Advanced Settings</span>
62 + </button>
63
64 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
65 <div class="field">
66 <div class="field-label">
106 - <div class="field-title">WebSocket Test Harness</div>
67 + <div class="field-title">Broadcast restart events</div>
68 <div class="field-description">
108 - Open the developer harness to run automated and manual WebSocket validation suites.
69 + Tell connected clients when the server has started again.
70 </div>
71 </div>
72 <div class="field-control">
112 - <button
113 - class="btn btn-field"
114 - @click="openModal('settings/developer/websocket-tester.html');"
115 - >
116 - Open Harness
117 - </button>
73 + <label class="toggle">
74 + <input type="checkbox" x-model="$store.settings.settings.websocket_server_restart_enabled" />
75 + <span class="toggler"></span>
76 + </label>
77 </div>
78 </div>
79
80 <div class="field">
81 <div class="field-label">
123 - <div class="field-title">WebSocket Event Console</div>
82 + <div class="field-title">Uvicorn access logs</div>
83 <div class="field-description">
125 - Inspect inbound and outbound envelopes and lifecycle events in real time (development only).
84 + Temporarily log HTTP traffic while debugging transport issues.
85 </div>
86 + <template
87 + x-if="$store.settings.additional?.runtime_settings &&
88 + $store.settings.settings?.uvicorn_access_logs_enabled !==
89 + $store.settings.additional.runtime_settings.uvicorn_access_logs_enabled"
90 + >
91 + <div class="field-description">
92 + Applies after backend restart.
93 + </div>
94 + </template>
95 </div>
96 <div class="field-control">
129 - <button
130 - class="btn btn-field"
131 - @click="openModal('settings/developer/websocket-event-console.html');"
132 - >
133 - Open Console
134 - </button>
97 + <label class="toggle">
98 + <input type="checkbox" x-model="$store.settings.settings.uvicorn_access_logs_enabled" />
99 + <span class="toggler"></span>
100 + </label>
101 </div>
102 </div>
103 +
104 + <template x-if="!$store.settings.additional?.is_dockerized">
105 + <div>
106 + <div class="settings-subsection-title">Testing</div>
107 + <div class="settings-subsection-description">
108 + Utilities for validating WebSocket infrastructure in development environments.
109 + </div>
110 +
111 + <div class="field">
112 + <div class="field-label">
113 + <div class="field-title">WebSocket test harness</div>
114 + <div class="field-description">
115 + Run automated and manual WebSocket validation suites.
116 + </div>
117 + </div>
118 + <div class="field-control">
119 + <button
120 + class="btn btn-field"
121 + @click="openModal('settings/developer/websocket-tester.html');"
122 + >
123 + Open Harness
124 + </button>
125 + </div>
126 + </div>
127 +
128 + <div class="field">
129 + <div class="field-label">
130 + <div class="field-title">WebSocket event console</div>
131 + <div class="field-description">
132 + Inspect inbound and outbound envelopes and lifecycle events in real time.
133 + </div>
134 + </div>
135 + <div class="field-control">
136 + <button
137 + class="btn btn-field"
138 + @click="openModal('settings/developer/websocket-event-console.html');"
139 + >
140 + Open Console
141 + </button>
142 + </div>
143 + </div>
144 + </div>
145 + </template>
146 </div>
138 - </template>
147 + </div>
148 </div>
149 </template>
150 </div>
webui/components/settings/external/api_keys.html
+1 -2
@@ -9,8 +9,7 @@
9 <div>
10 <div class="section-title">API Keys</div>
11 <div class="section-description">
12 - API keys for model providers and services used by Agent Zero. You can set multiple API keys separated by a comma (,). They will be used in round-robin fashion.<br>
13 - For more information about Agent Zero Venice provider, see <a href="http://agent-zero.ai/?community/api-dashboard/about" target="_blank">Agent Zero Venice</a>.
12 + Store provider credentials used by Agent Zero. Add multiple keys with commas when you want round-robin rotation.
13 </div>
14
15 <template x-for="provider in $store.settings.apiKeyProviders" :key="provider.value">
webui/components/settings/external/auth.html
+24 -14
@@ -14,13 +14,13 @@
14 <div>
15 <div class="section-title">Authentication</div>
16 <div class="section-description">
17 - Settings for authentication to use Agent Zero Web UI.
17 + Protect the web UI with credentials you control.
18 </div>
19
20 <div class="field">
21 <div class="field-label">
22 <div class="field-title">UI Login</div>
23 - <div class="field-description">Set user name for web UI</div>
23 + <div class="field-description">The username requested when the web UI asks you to sign in.</div>
24 </div>
25 <div class="field-control">
26 <input type="text" x-model="settings.auth_login" />
@@ -30,26 +30,36 @@
30 <div class="field">
31 <div class="field-label">
32 <div class="field-title">UI Password</div>
33 - <div class="field-description">Set user password for web UI</div>
33 + <div class="field-description">The password for browser access to this Agent Zero instance.</div>
34 </div>
35 <div class="field-control">
36 <input type="password" autocomplete="off" x-model="settings.auth_password" />
37 </div>
38 </div>
39
40 - <template x-if="additional.is_dockerized">
41 - <div class="field">
42 - <div class="field-label">
43 - <div class="field-title">root Password</div>
44 - <div class="field-description">
45 - Change linux root password in docker container. This password can be used for SSH access. Original password was randomly generated during setup.
40 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
41 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
42 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
43 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
44 + <span>Advanced Settings</span>
45 + </button>
46 +
47 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
48 + <template x-if="additional.is_dockerized">
49 + <div class="field">
50 + <div class="field-label">
51 + <div class="field-title">Container root password</div>
52 + <div class="field-description">
53 + Change the Linux root password used for SSH access to the Docker container.
54 + </div>
55 + </div>
56 + <div class="field-control">
57 + <input type="password" autocomplete="off" x-model="settings.root_password" />
58 + </div>
59 </div>
47 - </div>
48 - <div class="field-control">
49 - <input type="password" autocomplete="off" x-model="settings.root_password" />
50 - </div>
60 + </template>
61 </div>
52 - </template>
62 + </div>
63 </div>
64 </template>
65 </div>
webui/components/settings/external/external-settings.html
+2 -2
@@ -41,8 +41,8 @@
41 </li>
42 <li>
43 <a href="#section-tunnel">
44 - <img src="/public/tunnel.svg" alt="Tunnel" />
45 - <span>Flare Tunnel</span>
44 + <img src="/public/tunnel.svg" alt="Remote Link" />
45 + <span>Remote Link</span>
46 </a>
47 </li>
48 </ul>
webui/components/settings/external/external_api.html
+2 -2
@@ -8,14 +8,14 @@
8 <div>
9 <div class="section-title">External API</div>
10 <div class="section-description">
11 - Agent Zero provides external API endpoints for integration with other applications. These endpoints use API key authentication and support text messages and file attachments.
11 + Let external applications send messages and attachments to Agent Zero through authenticated API endpoints.
12 </div>
13
14 <div class="field">
15 <div class="field-label">
16 <div class="field-title">API Examples</div>
17 <div class="field-description">
18 - View examples for using Agent Zero's external API endpoints with API key authentication.
18 + Open ready-to-adapt request examples for the external API.
19 </div>
20 </div>
21 <div class="field-control">
webui/components/settings/external/litellm.html
+19 -9
@@ -9,19 +9,29 @@
9 <div>
10 <div class="section-title">LiteLLM Global Settings</div>
11 <div class="section-description">
12 - Configure global parameters passed to LiteLLM for all providers.
12 + Provider-wide LiteLLM overrides. Leave this closed unless every model call needs the same extra parameter.
13 </div>
14
15 - <div class="field field-full">
16 - <div class="field-label">
17 - <div class="field-title">LiteLLM global parameters</div>
18 - <div class="field-description">
19 - Global LiteLLM params (e.g. timeout, stream_timeout) in .env format: one KEY=VALUE per line. Example: <code>stream_timeout=30</code>. Applied to all LiteLLM calls unless overridden. See <a href='https://docs.litellm.ai/docs/set_keys' target='_blank'>LiteLLM</a> and <a href='https://docs.litellm.ai/docs/proxy/timeout' target='_blank'>timeouts</a>.
15 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
16 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
17 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
18 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
19 + <span>Advanced Settings</span>
20 + </button>
21 +
22 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
23 + <div class="field field-full">
24 + <div class="field-label">
25 + <div class="field-title">Global LiteLLM parameters</div>
26 + <div class="field-description">
27 + One <code>KEY=VALUE</code> pair per line, for example <code>stream_timeout=30</code>. These values apply to all LiteLLM calls unless a model overrides them.
28 + </div>
29 + </div>
30 + <div class="field-control">
31 + <textarea style="height: 12em" x-model="settings.litellm_global_kwargs"></textarea>
32 + </div>
33 </div>
34 </div>
22 - <div class="field-control">
23 - <textarea style="height: 12em" x-model="settings.litellm_global_kwargs"></textarea>
24 - </div>
35 </div>
36 </div>
37 </template>
webui/components/settings/external/secrets.html
+29 -19
@@ -9,31 +9,41 @@
9 <div>
10 <div class="section-title">Secrets Management</div>
11 <div class="section-description">
12 - Manage secrets and credentials that agents can use without exposing values to LLMs, chat history or logs. Placeholders are automatically replaced with values just before tool calls. If bare passwords occur in tool results, they are masked back to placeholders.
12 + Keep credentials out of prompts and logs while still letting tools receive them when needed.
13 </div>
14
15 - <div class="field field-full">
16 - <div class="field-label">
17 - <div class="field-title">Variables Store</div>
18 - <div class="field-description">
19 - Store non-sensitive variables in .env format e.g. EMAIL_IMAP_SERVER="imap.gmail.com", one item per line. You can use comments starting with # to add descriptions for the agent. See <a href="javascript:openModal('settings/secrets/example-vars.html')">example</a>.<br>These variables are visible to LLMs and in chat history, they are not being masked.
15 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
16 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
17 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
18 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
19 + <span>Advanced Settings</span>
20 + </button>
21 +
22 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
23 + <div class="field field-full">
24 + <div class="field-label">
25 + <div class="field-title">Variables</div>
26 + <div class="field-description">
27 + Non-sensitive <code>.env</code> values the agent may see, such as service hosts or usernames. See <a href="javascript:openModal('settings/secrets/example-vars.html')">example</a>.
28 + </div>
29 + </div>
30 + <div class="field-control">
31 + <textarea style="height: 16em" x-model="settings.variables"></textarea>
32 + </div>
33 </div>
21 - </div>
22 - <div class="field-control">
23 - <textarea style="height: 20em" x-model="settings.variables"></textarea>
24 - </div>
25 - </div>
34
27 - <div class="field field-full">
28 - <div class="field-label">
29 - <div class="field-title">Secrets Store</div>
30 - <div class="field-description">
31 - Store secrets and credentials in .env format e.g. EMAIL_PASSWORD="s3cret-p4$$w0rd", one item per line. You can use comments starting with # to add descriptions for the agent. See <a href="javascript:openModal('settings/secrets/example-secrets.html')">example</a>.<br>These variables are not visile to LLMs and in chat history, they are being masked. ⚠️ only values with length >= 4 are being masked to prevent false positives.
35 + <div class="field field-full">
36 + <div class="field-label">
37 + <div class="field-title">Secrets</div>
38 + <div class="field-description">
39 + Sensitive <code>.env</code> values that are masked from the agent, chat history, and logs. Values shorter than 4 characters are not masked. See <a href="javascript:openModal('settings/secrets/example-secrets.html')">example</a>.
40 + </div>
41 + </div>
42 + <div class="field-control">
43 + <textarea style="height: 16em" x-model="settings.secrets"></textarea>
44 + </div>
45 </div>
46 </div>
34 - <div class="field-control">
35 - <textarea style="height: 20em" x-model="settings.secrets"></textarea>
36 - </div>
47 </div>
48 </div>
49 </template>
webui/components/settings/external/self-update-store.js
+1 -1
@@ -218,7 +218,7 @@ const model = {
218 return `This instance is significantly behind the latest release currently available on main. Restart Agent Zero to move to ${this.mainBranchLatestVersion}.`;
219 }
220 if (this.quickUpdateComparison === 0) {
221 - return "This instance already matches the latest version currently available on main.";
221 + return "You already have the latest version of Agent Zero main branch";
222 }
223 return "This checkout already reports a newer tagged version than main.";
224 },
webui/components/settings/mcp/mcp_client.html
+30 -20
@@ -9,13 +9,13 @@
9 <div>
10 <div class="section-title">External MCP Servers</div>
11 <div class="section-description">
12 - Agent Zero can use external MCP servers, local or remote as tools.
12 + Connect local or remote MCP servers so their tools become available to the agent.
13 </div>
14
15 <div class="field">
16 <div class="field-label">
17 <div class="field-title">MCP Servers Configuration</div>
18 - <div class="field-description">External MCP servers can be configured here.</div>
18 + <div class="field-description">Add servers, inspect status, and review their exposed tools.</div>
19 </div>
20 <div class="field-control">
21 <button
@@ -27,28 +27,38 @@
27 </div>
28 </div>
29
30 - <div class="field">
31 - <div class="field-label">
32 - <div class="field-title">MCP Client Init Timeout</div>
33 - <div class="field-description">
34 - Timeout for MCP client initialization (in seconds). Higher values might be required for complex MCPs, but might also slowdown system startup.
30 + <div class="settings-advanced-section" x-data="{ advOpen: false }">
31 + <button type="button" class="settings-advanced-toggle" @click="advOpen = !advOpen">
32 + <span class="material-symbols-outlined settings-advanced-toggle-icon"
33 + :style="advOpen ? 'transform:rotate(90deg)' : ''">chevron_right</span>
34 + <span>Advanced Settings</span>
35 + </button>
36 +
37 + <div class="settings-advanced-body" x-show="advOpen" x-transition.opacity>
38 + <div class="field">
39 + <div class="field-label">
40 + <div class="field-title">Startup timeout</div>
41 + <div class="field-description">
42 + How long Agent Zero waits for an MCP server to initialize before marking it unavailable.
43 + </div>
44 + </div>
45 + <div class="field-control">
46 + <input type="number" x-model.number="settings.mcp_client_init_timeout" />
47 + </div>
48 </div>
36 - </div>
37 - <div class="field-control">
38 - <input type="number" x-model.number="settings.mcp_client_init_timeout" />
39 - </div>
40 - </div>
49
42 - <div class="field">
43 - <div class="field-label">
44 - <div class="field-title">MCP Client Tool Timeout</div>
45 - <div class="field-description">
46 - Timeout for MCP client tool execution. Higher values might be required for complex tools, but might also result in long responses with failing tools.
50 + <div class="field">
51 + <div class="field-label">
52 + <div class="field-title">Tool call timeout</div>
53 + <div class="field-description">
54 + Maximum time a single MCP tool call may run before it is treated as failed.
55 + </div>
56 + </div>
57 + <div class="field-control">
58 + <input type="number" x-model.number="settings.mcp_client_tool_timeout" />
59 + </div>
60 </div>
61 </div>
49 - <div class="field-control">
50 - <input type="number" x-model.number="settings.mcp_client_tool_timeout" />
51 - </div>
62 </div>
63 </div>
64 </template>
webui/components/settings/mcp/mcp_server.html
+2 -2
@@ -9,14 +9,14 @@
9 <div>
10 <div class="section-title">A0 MCP Server</div>
11 <div class="section-description">
12 - Agent Zero can be exposed as an SSE MCP server. See <a href="javascript:openModal('settings/mcp/server/example.html')">connection example</a>.
12 + Expose this Agent Zero instance as an MCP server for trusted clients. See <a href="javascript:openModal('settings/mcp/server/example.html')">connection example</a>.
13 </div>
14
15 <div class="field">
16 <div class="field-label">
17 <div class="field-title">Enable A0 MCP Server</div>
18 <div class="field-description">
19 - Expose Agent Zero as an SSE/HTTP MCP server. This will make this A0 instance available to MCP clients.
19 + Make Agent Zero available over SSE/HTTP to MCP-compatible tools.
20 </div>
21 </div>
22 <div class="field-control">
webui/components/settings/settings-store.js
+231 -1
@@ -5,6 +5,71 @@ import { store as notificationStore } from "/components/notifications/notificati
5 // Constants
6 const VIEW_MODE_STORAGE_KEY = "settingsActiveTab";
7 const DEFAULT_TAB = "agent";
8 +const UPDATE_STATUS_REFRESH_COOLDOWN_MS = 60 * 1000;
9 +
10 +const TAB_ITEMS = Object.freeze([
11 + {
12 + id: "agent",
13 + label: "Agent Settings",
14 + icon: "smart_toy",
15 + sections: [
16 + { id: "section-agent-config", label: "Agent Config", icon: "settings" },
17 + { id: "section-models-summary", label: "Models", icon: "forum" },
18 + { id: "section-speech", label: "Speech", icon: "mic" },
19 + { id: "section-workdir", label: "Workdir", icon: "folder" },
20 + { id: "section-agent-plugins", label: "Plugins", icon: "extension" },
21 + ],
22 + },
23 + {
24 + id: "skills",
25 + label: "Skills",
26 + icon: "school",
27 + sections: [
28 + { id: "section-skills-list", label: "List Skills", icon: "view_list" },
29 + { id: "section-skills-import", label: "Import Skills", icon: "upload_file" },
30 + ],
31 + },
32 + {
33 + id: "external",
34 + label: "External Services",
35 + icon: "cloud_sync",
36 + sections: [
37 + { id: "section-api-keys", label: "API Keys", icon: "key" },
38 + { id: "section-litellm", label: "LiteLLM", icon: "tune" },
39 + { id: "section-secrets", label: "Secrets", icon: "lock" },
40 + { id: "section-auth", label: "Authentication", icon: "passkey" },
41 + { id: "section-external-api", label: "External API", icon: "api" },
42 + { id: "section-tunnel", label: "Remote Link", icon: "share" },
43 + ],
44 + },
45 + {
46 + id: "mcp",
47 + label: "MCP/A2A",
48 + icon: "hub",
49 + sections: [
50 + { id: "section-mcp-client", label: "External MCP Servers", icon: "hub" },
51 + { id: "section-mcp-server", label: "A0 MCP Server", icon: "settings_input_antenna" },
52 + { id: "section-a2a-server", label: "A0 A2A Server", icon: "conversion_path" },
53 + ],
54 + },
55 + {
56 + id: "developer",
57 + label: "Developer",
58 + icon: "code",
59 + sections: [
60 + { id: "section-dev", label: "Development", icon: "terminal" },
61 + ],
62 + },
63 + {
64 + id: "backup",
65 + label: "Check for updates",
66 + icon: "system_update_alt",
67 + sections: [
68 + { id: "section-self-update", label: "Self Update", icon: "system_update_alt" },
69 + { id: "section-update-advanced", label: "Advanced Settings", icon: "tune" },
70 + ],
71 + },
72 +]);
73
74 // Field button actions (field id -> modal path)
75 const FIELD_BUTTON_MODAL_BY_ID = Object.freeze({
@@ -28,6 +93,8 @@ const model = {
93 settings: null,
94 additional: null,
95 workdirFileStructureTestOutput: "",
96 + navMode: "categories",
97 + _updateStatusRefreshedAt: 0,
98
99 // Tab state
100 _activeTab: DEFAULT_TAB,
@@ -69,6 +136,8 @@ const model = {
136 this.isLoading = false;
137 }
138
139 + this.refreshUpdateStatus();
140 +
141 // Trigger tab activation for current tab
142 this.applyActiveTab(null, this._activeTab);
143 },
@@ -78,6 +147,7 @@ const model = {
147 this.additional = null;
148 this.error = null;
149 this.isLoading = false;
150 + this.navMode = "categories";
151 },
152
153 // Tab management
@@ -92,6 +162,167 @@ const model = {
162 this.activeTab = tabName;
163 },
164
165 + get navItems() {
166 + return TAB_ITEMS;
167 + },
168 +
169 + get activeTabItem() {
170 + return TAB_ITEMS.find((item) => item.id === this.activeTab) || TAB_ITEMS[0];
171 + },
172 +
173 + get sectionItems() {
174 + return this.activeTabItem?.sections || [];
175 + },
176 +
177 + enterTab(tabName) {
178 + this.activeTab = tabName;
179 + this.navMode = "sections";
180 + this.resetPaneScroll();
181 + if (tabName === "backup") this.refreshUpdateStatus();
182 + },
183 +
184 + backToCategories() {
185 + this.navMode = "categories";
186 + },
187 +
188 + resetPaneScroll() {
189 + requestAnimationFrame(() => {
190 + const pane = this.getSettingsPane();
191 + if (pane) pane.scrollTop = 0;
192 + });
193 + },
194 +
195 + getSettingsPane() {
196 + return document.querySelector(".modal-inner.settings-modal .settings-pane");
197 + },
198 +
199 + get selfUpdate() {
200 + return globalThis.Alpine?.store?.("selfUpdateStore") || null;
201 + },
202 +
203 + getSectionTarget(sectionId, pane = this.getSettingsPane()) {
204 + if (!sectionId) return null;
205 + const escapedId = window.CSS?.escape ? window.CSS.escape(sectionId) : sectionId;
206 + const selector = `#${escapedId}`;
207 + const activePanel = pane?.querySelector(`.settings-tab-panel[data-settings-tab="${this.activeTab}"]`);
208 + return activePanel?.querySelector(selector) || pane?.querySelector(selector) || document.getElementById(sectionId);
209 + },
210 +
211 + scrollToSection(sectionId, event = null) {
212 + event?.preventDefault?.();
213 + this.navMode = "sections";
214 +
215 + const performScroll = () => {
216 + const pane = this.getSettingsPane();
217 + const target = this.getSectionTarget(sectionId, pane);
218 + if (!target) {
219 + history.replaceState(null, "", `#${sectionId}`);
220 + return;
221 + }
222 + if (!pane) {
223 + target.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
224 + history.replaceState(null, "", `#${sectionId}`);
225 + return;
226 + }
227 + const paneRect = pane.getBoundingClientRect();
228 + const targetRect = target.getBoundingClientRect();
229 + pane.scrollTo({
230 + top: Math.max(0, pane.scrollTop + targetRect.top - paneRect.top - 12),
231 + behavior: "smooth",
232 + });
233 + history.replaceState(null, "", `#${sectionId}`);
234 + };
235 +
236 + requestAnimationFrame(() => requestAnimationFrame(performScroll));
237 + },
238 +
239 + refreshUpdateStatus(force = false) {
240 + const selfUpdate = this.selfUpdate;
241 + if (typeof selfUpdate?.refresh !== "function") return;
242 + const now = Date.now();
243 + if (!force && now - this._updateStatusRefreshedAt < UPDATE_STATUS_REFRESH_COOLDOWN_MS) {
244 + return;
245 + }
246 + this._updateStatusRefreshedAt = now;
247 + selfUpdate.refresh().catch((error) => {
248 + console.warn("Failed to refresh self-update status:", error);
249 + });
250 + },
251 +
252 + isUpdateNotification(notification) {
253 + if (!notification) return false;
254 + const group = String(notification.group || "").toLowerCase();
255 + const id = String(notification.id || "").toLowerCase();
256 + return (
257 + group === "update_check" ||
258 + group.startsWith("self-update") ||
259 + id.startsWith("update_check") ||
260 + id.includes("self-update")
261 + );
262 + },
263 +
264 + get latestUpdateNotification() {
265 + return notificationStore.notifications.find((item) => this.isUpdateNotification(item)) || null;
266 + },
267 +
268 + get hasUpdateNotification() {
269 + return Boolean(this.latestUpdateNotification);
270 + },
271 +
272 + get hasUpdateAttention() {
273 + const selfUpdate = this.selfUpdate;
274 + return Boolean(
275 + selfUpdate?.info?.pending ||
276 + selfUpdate?.quickUpdateAvailable ||
277 + selfUpdate?.hasMajorUpgrade ||
278 + this.hasUpdateNotification
279 + );
280 + },
281 +
282 + get updateAttentionLabel() {
283 + const selfUpdate = this.selfUpdate;
284 + if (selfUpdate?.info?.pending) return "Scheduled";
285 + if (selfUpdate?.quickUpdateAvailable) return "Update available";
286 + if (selfUpdate?.hasMajorUpgrade) return "New release line";
287 + if (this.hasUpdateNotification) return "Update notice";
288 + return selfUpdate?.quickStatusLabel || "Ready";
289 + },
290 +
291 + get updateAttentionTitle() {
292 + const selfUpdate = this.selfUpdate;
293 + if (selfUpdate?.info?.pending) return "Update scheduled";
294 + if (selfUpdate?.quickUpdateAvailable) return "Update available";
295 + if (selfUpdate?.hasMajorUpgrade) return "New release line available";
296 + if (this.hasUpdateNotification) return "Update notice";
297 + return "Self Update";
298 + },
299 +
300 + get updateAttentionMessage() {
301 + const notification = this.latestUpdateNotification;
302 + if (notification?.message) {
303 + return this.toPlainText(notification.message);
304 + }
305 + const selfUpdate = this.selfUpdate;
306 + if (selfUpdate?.info?.pending) {
307 + return "Agent Zero has a self-update request ready for the next restart.";
308 + }
309 + return selfUpdate?.quickStatusMessage || "Review versions, backups, and update readiness in one place.";
310 + },
311 +
312 + navItemHasAttention(item) {
313 + return item?.id === "backup" && this.hasUpdateAttention;
314 + },
315 +
316 + sectionItemHasAttention(item) {
317 + return item?.id === "section-self-update" && this.hasUpdateAttention;
318 + },
319 +
320 + toPlainText(value) {
321 + const container = document.createElement("div");
322 + container.innerHTML = String(value || "");
323 + return (container.textContent || container.innerText || "").trim();
324 + },
325 +
326
327
328 get apiKeyProviders() {
@@ -191,4 +422,3 @@ const model = {
422 const store = createStore("settings", model);
423
424 export { store };
194 -
webui/components/settings/settings.html
+51 -42
@@ -1,4 +1,4 @@
1 -<html>
1 +<html class="settings-modal">
2 <head>
3 <title>Settings</title>
4 </head>
@@ -28,56 +28,71 @@
28 <!-- Settings content -->
29 <div x-show="$store.settings.settings" class="settings-content">
30 <!-- Tab Navigation -->
31 - <div class="settings-tabs-container">
32 - <div class="settings-tabs no-scrollbar">
33 - <div class="settings-tab"
34 - :class="{'active': $store.settings.activeTab === 'agent'}"
35 - @click="$store.settings.switchTab('agent')"
36 - title="Agent Settings">Agent Settings</div>
37 - <div class="settings-tab"
38 - :class="{'active': $store.settings.activeTab === 'skills'}"
39 - @click="$store.settings.switchTab('skills')"
40 - title="Skills">Skills</div>
41 - <div class="settings-tab"
42 - :class="{'active': $store.settings.activeTab === 'external'}"
43 - @click="$store.settings.switchTab('external')"
44 - title="External Services">External Services</div>
45 - <div class="settings-tab"
46 - :class="{'active': $store.settings.activeTab === 'mcp'}"
47 - @click="$store.settings.switchTab('mcp')"
48 - title="MCP">MCP/A2A</div>
49 - <div class="settings-tab"
50 - :class="{'active': $store.settings.activeTab === 'developer'}"
51 - @click="$store.settings.switchTab('developer')"
52 - title="Developer">Developer</div>
53 - <div class="settings-tab"
54 - :class="{'active': $store.settings.activeTab === 'backup'}"
55 - @click="$store.settings.switchTab('backup')"
56 - title="Update">Update</div>
31 + <aside class="settings-tabs-container" aria-label="Settings categories">
32 + <div class="settings-tabs no-scrollbar" role="tablist" aria-orientation="vertical" x-show="$store.settings.navMode === 'categories'">
33 + <template x-for="item in $store.settings.navItems" :key="item.id">
34 + <button type="button"
35 + class="settings-tab"
36 + role="tab"
37 + :aria-selected="$store.settings.activeTab === item.id"
38 + :class="{
39 + 'active': $store.settings.activeTab === item.id,
40 + 'settings-tab-attention': $store.settings.navItemHasAttention(item)
41 + }"
42 + @click="$store.settings.enterTab(item.id)">
43 + <span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
44 + <span class="settings-tab-label" x-text="item.label"></span>
45 + <span class="settings-attention-dot"
46 + x-show="$store.settings.navItemHasAttention(item)"
47 + aria-hidden="true"></span>
48 + </button>
49 + </template>
50 </div>
58 - </div>
51 +
52 + <div class="settings-tabs no-scrollbar settings-section-tabs" x-show="$store.settings.navMode === 'sections'" style="display: none;">
53 + <button type="button"
54 + class="settings-tab settings-back-tab"
55 + @click="$store.settings.backToCategories()">
56 + <span class="material-symbols-outlined" aria-hidden="true">arrow_back</span>
57 + <span class="settings-tab-label">&lt; Back</span>
58 + </button>
59 + <div class="settings-tabs-heading" x-text="$store.settings.activeTabItem?.label || 'Settings'"></div>
60 + <template x-for="item in $store.settings.sectionItems" :key="item.id">
61 + <a class="settings-tab settings-section-link"
62 + :class="{'settings-tab-attention': $store.settings.sectionItemHasAttention(item)}"
63 + :href="`#${item.id}`"
64 + @click="$store.settings.scrollToSection(item.id, $event)">
65 + <span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
66 + <span class="settings-tab-label" x-text="item.label"></span>
67 + <span class="settings-attention-dot"
68 + x-show="$store.settings.sectionItemHasAttention(item)"
69 + aria-hidden="true"></span>
70 + </a>
71 + </template>
72 + </div>
73 + </aside>
74
75 <!-- Settings sections for agent, external, developer, mcp, backup tabs -->
61 - <div id="settings-sections">
62 - <div x-show="$store.settings.activeTab === 'agent'">
76 + <main id="settings-sections" class="settings-pane">
77 + <div class="settings-tab-panel" data-settings-tab="agent" x-show="$store.settings.activeTab === 'agent'">
78 <x-component path="settings/agent/agent-settings.html"></x-component>
79 </div>
65 - <div x-show="$store.settings.activeTab === 'external'">
80 + <div class="settings-tab-panel" data-settings-tab="external" x-show="$store.settings.activeTab === 'external'">
81 <x-component path="settings/external/external-settings.html"></x-component>
82 </div>
68 - <div x-show="$store.settings.activeTab === 'mcp'">
83 + <div class="settings-tab-panel" data-settings-tab="mcp" x-show="$store.settings.activeTab === 'mcp'">
84 <x-component path="settings/mcp/mcp-settings.html"></x-component>
85 </div>
71 - <div x-show="$store.settings.activeTab === 'developer'">
86 + <div class="settings-tab-panel" data-settings-tab="developer" x-show="$store.settings.activeTab === 'developer'">
87 <x-component path="settings/developer/developer-settings.html"></x-component>
88 </div>
74 - <div x-show="$store.settings.activeTab === 'backup'">
89 + <div class="settings-tab-panel" data-settings-tab="backup" x-show="$store.settings.activeTab === 'backup'">
90 <x-component path="settings/backup/backup-settings.html"></x-component>
91 </div>
77 - <div x-show="$store.settings.activeTab === 'skills'">
92 + <div class="settings-tab-panel" data-settings-tab="skills" x-show="$store.settings.activeTab === 'skills'">
93 <x-component path="settings/skills/skills-settings.html"></x-component>
94 </div>
80 - </div>
95 + </main>
96 </div>
97
98 <!-- Footer -->
@@ -124,12 +139,6 @@
139 margin-top: 1rem;
140 }
141
127 -.settings-content {
128 - display: flex;
129 - flex-direction: column;
130 - gap: 1rem;
131 -}
132 -
142 .spinning {
143 animation: spin 1s linear infinite;
144 }
webui/components/settings/skills/import.html
+4 -2
@@ -10,7 +10,10 @@
10 <template x-if="$store.skillsImportStore">
11 <div x-init="$store.skillsImportStore.init()" x-destroy="$store.skillsImportStore.onClose()">
12
13 - <h3>Import Skills (SKILL.md)</h3>
13 + <div class="section-title">Import Skills</div>
14 + <div class="section-description">
15 + Add a zipped skill pack that contains one or more <code>SKILL.md</code> folders.
16 + </div>
17
18 <div class="upload-section">
19 <label for="skills-file" class="upload-label">
@@ -231,4 +234,3 @@
234 </style>
235 </body>
236 </html>
234 -
webui/components/settings/tunnel/remote-link.html new
+9
@@ -0,0 +1,9 @@
1 +<html class="remote-link-modal">
2 + <head>
3 + <title>Remote Link</title>
4 + </head>
5 +
6 + <body>
7 + <x-component path="settings/tunnel/tunnel-section.html"></x-component>
8 + </body>
9 +</html>
webui/components/settings/tunnel/tunnel-section.html
+499 -589
@@ -1,602 +1,512 @@
1 <html>
2 -
3 -<head>
4 - <title></title>
5 -
6 - <!-- Import the alpine store -->
2 + <head>
3 + <title>Remote Link</title>
4 <script type="module">
8 - import { store } from "/components/settings/tunnel/tunnel-store.js";
5 + import { store } from "/components/settings/tunnel/tunnel-store.js";
6 </script>
10 -</head>
11 -
12 -<body>
7 + </head>
8
14 - <!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
9 + <body>
10 <div x-data>
16 - <template x-if="$store.tunnelStore">
17 -
18 - <!-- Keep in mind that <template> can have only one root element inside -->
19 - <div id="tunnel-settings-section">
20 - <div class="section-title">Flare Tunnel</div>
21 - <div class="section-description">Create a secure public URL to access your Agent Zero instance anytime,
22 - anywhere.</div>
23 - <!-- Tunnel content UI -->
24 - <div class="tunnel-container">
25 - <div class="field">
26 - <div class="field-label">
27 - <div class="field-title">Tunnel provider</div>
28 - <div class="field-description">Select provider for public tunnel</div>
29 - </div>
30 - <div class="field-control">
31 - <select id="tunnel-provider" x-model="$store.tunnelStore.provider"
32 - :disabled="$store.tunnelStore.isLoading">
33 - <option value="cloudflared">Cloudflare</option>
34 - <option value="microsoft">Microsoft Dev Tunnels</option>
35 - <option value="serveo">Serveo</option>
36 - </select>
37 - </div>
38 - </div>
39 - <!-- Loading spinner for tunnel operations -->
40 - <div class="tunnel-loading-status" x-show="$store.tunnelStore.isLoading">
41 - <span class="icon material-symbols-outlined spin">progress_activity</span>
42 - <span x-text="$store.tunnelStore.loadingText || 'Processing tunnel request...'"></span>
43 - </div>
44 - <!-- Microsoft login code display -->
45 - <div class="microsoft-login-box" x-show="$store.tunnelStore.microsoftLoginCode" x-transition>
46 - <div class="microsoft-login-header">
47 - <span class="microsoft-login-icon material-symbols-outlined">key</span>
48 - <div class="microsoft-login-title">Microsoft Login Required</div>
49 - </div>
50 - <div class="microsoft-login-steps">
51 - <div class="microsoft-login-step">
52 - <span class="step-number">1</span>
53 - <span class="step-text">Open the authentication page:</span>
54 - </div>
55 - <a class="microsoft-login-link" :href="$store.tunnelStore.microsoftLoginUrl" target="_blank">
56 - <span class="icon material-symbols-outlined">open_in_new</span>
57 - <span x-text="$store.tunnelStore.microsoftLoginUrl"></span>
58 - </a>
59 - <div class="microsoft-login-step">
60 - <span class="step-number">2</span>
61 - <span class="step-text">Enter this code:</span>
62 - </div>
63 - </div>
64 - <div class="microsoft-login-code-container">
65 - <div class="microsoft-login-code" x-text="$store.tunnelStore.microsoftLoginCode"></div>
66 - <button class="btn btn-copy-code" @click="$store.tunnelStore.copyLoginCode()" :class="$store.tunnelStore.codeCopied ? 'copied' : ''">
67 - <span class="icon material-symbols-outlined" x-text="$store.tunnelStore.codeCopied ? 'check' : 'content_copy'"></span>
68 - <span x-text="$store.tunnelStore.codeCopied ? 'Copied!' : 'Copy Code'"></span>
69 - </button>
70 - </div>
71 - <div class="microsoft-login-footer">
72 - <span class="icon material-symbols-outlined spin-slow">sync</span>
73 - <span>Waiting for authentication to complete...</span>
74 - </div>
75 - </div>
76 -
77 - <!-- Tunnel content when not loading -->
78 - <div x-show="!$store.tunnelStore.isLoading">
79 - <!-- Tunnel link display when generated -->
80 - <div class="tunnel-link-container" x-show="$store.tunnelStore.linkGenerated">
81 - <div class="tunnel-link-field">
82 - <input type="text" class="tunnel-link-input" :value="$store.tunnelStore.tunnelLink"
83 - readonly>
84 - <div class="buttons-row">
85 - <button class="copy-link-button" @click="$store.tunnelStore.copyToClipboard()"
86 - title="Copy to clipboard">
87 - <span class="icon material-symbols-outlined">content_copy</span> Copy
88 - </button>
89 - <button class="refresh-link-button" @click="$store.tunnelStore.refreshLink()"
90 - title="Generate new URL">
91 - <span class="icon material-symbols-outlined">refresh</span> Refresh
92 - </button>
93 - </div>
94 - </div>
95 - <div class="tunnel-qr-container">
96 - <div class="tunnel-qr-code" id="qrcode-tunnel">
97 - <!-- QR code will be generated here -->
98 - </div>
99 - <div class="tunnel-qr-label">Scan with mobile device</div>
100 - </div>
101 - <div class="tunnel-link-info">
102 - Share this URL to allow others to access your Agent Zero instance.
103 - </div>
104 - <div class="tunnel-link-persistence">
105 - This URL will persist until you stop the tunnel or restart the Docker container.
106 - </div>
107 - <div class="stop-tunnel-container">
108 - <button class="btn btn-danger" @click="$store.tunnelStore.stopTunnel()">
109 - <span class="icon material-symbols-outlined">stop_circle</span> Stop Tunnel
110 - </button>
111 - </div>
112 - </div>
113 - <!-- Generate tunnel button when no link exists -->
114 - <div class="tunnel-actions" x-show="!$store.tunnelStore.linkGenerated">
115 - <button class="btn btn-ok" @click="$store.tunnelStore.generateLink()">
116 - <span class="icon material-symbols-outlined">play_circle</span> Create Tunnel
117 - </button>
118 - </div>
11 + <template x-if="$store.tunnelStore">
12 + <div
13 + id="tunnel-settings-section"
14 + class="remote-link-surface"
15 + x-init="$store.tunnelStore.init()"
16 + x-destroy="$store.tunnelStore.cleanup()"
17 + >
18 + <header class="remote-link-hero">
19 + <div class="remote-link-hero-icon" aria-hidden="true">
20 + <span class="material-symbols-outlined">share</span>
21 + </div>
22 + <div>
23 + <div class="section-title">Remote Link</div>
24 + <div class="section-description">
25 + Create a temporary HTTPS link for this Agent Zero instance, then open it from another browser or scan it from your phone.
26 + </div>
27 + </div>
28 + </header>
29 +
30 + <div class="remote-link-safety-note">
31 + <span class="material-symbols-outlined" aria-hidden="true">lock</span>
32 + <span>Use sign-in before sharing a remote link. The URL can reach your instance while the link is active.</span>
33 + </div>
34 +
35 + <div class="field remote-link-provider-field">
36 + <div class="field-label">
37 + <div class="field-title">Link provider</div>
38 + <div class="field-description">
39 + Cloudflare is the quickest path for a shareable URL. Microsoft Dev Tunnels may ask you to sign in.
40 + </div>
41 + </div>
42 + <div class="field-control">
43 + <select
44 + id="tunnel-provider"
45 + x-model="$store.tunnelStore.provider"
46 + :disabled="$store.tunnelStore.isLoading"
47 + >
48 + <option value="cloudflared">Cloudflare</option>
49 + <option value="microsoft">Microsoft Dev Tunnels</option>
50 + <option value="serveo">Serveo</option>
51 + </select>
52 + </div>
53 + </div>
54 +
55 + <div class="remote-link-loading" x-show="$store.tunnelStore.isLoading" style="display: none;">
56 + <span class="material-symbols-outlined spin" aria-hidden="true">progress_activity</span>
57 + <span x-text="$store.tunnelStore.loadingText || 'Preparing remote link...'"></span>
58 + </div>
59 +
60 + <div class="microsoft-login-box" x-show="$store.tunnelStore.microsoftLoginCode" x-transition style="display: none;">
61 + <div class="microsoft-login-header">
62 + <span class="material-symbols-outlined" aria-hidden="true">key</span>
63 + <div>
64 + <div class="microsoft-login-title">Microsoft sign-in</div>
65 + <div class="microsoft-login-copy">Approve the tunnel request, then Agent Zero will finish creating the link.</div>
66 + </div>
67 + </div>
68 + <a class="microsoft-login-link" :href="$store.tunnelStore.microsoftLoginUrl" target="_blank" rel="noopener noreferrer">
69 + <span class="material-symbols-outlined" aria-hidden="true">open_in_new</span>
70 + <span x-text="$store.tunnelStore.microsoftLoginUrl"></span>
71 + </a>
72 + <div class="microsoft-login-code-row">
73 + <div class="microsoft-login-code" x-text="$store.tunnelStore.microsoftLoginCode"></div>
74 + <button
75 + type="button"
76 + class="btn btn-field btn-copy-code"
77 + @click="$store.tunnelStore.copyLoginCode()"
78 + :class="$store.tunnelStore.codeCopied ? 'copied' : ''"
79 + >
80 + <span class="material-symbols-outlined" aria-hidden="true" x-text="$store.tunnelStore.codeCopied ? 'check' : 'content_copy'"></span>
81 + <span x-text="$store.tunnelStore.codeCopied ? 'Copied' : 'Copy code'"></span>
82 + </button>
83 + </div>
84 + </div>
85 +
86 + <div x-show="!$store.tunnelStore.isLoading">
87 + <div class="remote-link-live" x-show="$store.tunnelStore.linkGenerated" style="display: none;">
88 + <div class="remote-link-url-row">
89 + <input
90 + type="text"
91 + class="remote-link-input"
92 + :value="$store.tunnelStore.tunnelLink"
93 + readonly
94 + aria-label="Remote link URL"
95 + >
96 + <button
97 + type="button"
98 + class="remote-link-action copy-link-button"
99 + @click="$store.tunnelStore.copyToClipboard()"
100 + :class="{
101 + 'copy-success': $store.tunnelStore.copyState === 'success',
102 + 'copy-error': $store.tunnelStore.copyState === 'error'
103 + }"
104 + >
105 + <span class="material-symbols-outlined" aria-hidden="true" x-text="$store.tunnelStore.copyLinkIcon"></span>
106 + <span x-text="$store.tunnelStore.copyLinkLabel"></span>
107 + </button>
108 + <button
109 + type="button"
110 + class="remote-link-action refresh-link-button"
111 + @click="$store.tunnelStore.refreshLink()"
112 + :disabled="$store.tunnelStore.isLoading"
113 + >
114 + <span class="material-symbols-outlined" aria-hidden="true">refresh</span>
115 + <span>New link</span>
116 + </button>
117 + </div>
118 +
119 + <div class="remote-link-share-layout">
120 + <div class="remote-link-qr-panel">
121 + <div class="tunnel-qr-code" id="qrcode-tunnel"></div>
122 + <div>
123 + <div class="remote-link-qr-title">Open on mobile</div>
124 + <div class="remote-link-qr-copy">Scan the code with your phone to connect without retyping the URL.</div>
125 + </div>
126 + </div>
127 + <div class="remote-link-status-panel">
128 + <span class="material-symbols-outlined" aria-hidden="true">verified</span>
129 + <div>
130 + <div class="remote-link-status-title">Link is active</div>
131 + <div class="remote-link-status-copy">
132 + It stays available until you stop it or the container restarts.
133 </div>
134 + </div>
135 </div>
136 + </div>
137 +
138 + <div class="remote-link-footer-actions">
139 + <button
140 + type="button"
141 + class="btn btn-cancel"
142 + @click="$store.tunnelStore.stopTunnel()"
143 + :disabled="$store.tunnelStore.isLoading"
144 + >
145 + <span class="material-symbols-outlined" aria-hidden="true">stop_circle</span>
146 + <span>Stop link</span>
147 + </button>
148 + </div>
149 </div>
150
123 - </template>
151 + <div class="remote-link-empty" x-show="!$store.tunnelStore.linkGenerated">
152 + <span class="material-symbols-outlined" aria-hidden="true">link</span>
153 + <strong>No remote link yet</strong>
154 + <span>Create one when you want to reach Agent Zero from another device or share access temporarily.</span>
155 + <button
156 + type="button"
157 + class="btn btn-ok remote-link-primary"
158 + @click="$store.tunnelStore.generateLink()"
159 + :disabled="$store.tunnelStore.isLoading"
160 + >
161 + <span class="material-symbols-outlined" aria-hidden="true">share</span>
162 + <span>Create remote link</span>
163 + </button>
164 + </div>
165 + </div>
166 + </div>
167 + </template>
168 </div>
169
126 - <!-- Optional style for the component -->
170 <style>
128 - /* Tunnel Modal Styles */
129 - .tunnel-container {
130 - padding: 1rem;
131 - width: 100%;
132 - }
133 -
134 - .tunnel-description {
135 - margin-bottom: 1.5rem;
136 - text-align: center;
137 - }
138 -
139 - .tunnel-actions {
140 - display: flex;
141 - justify-content: center;
142 - margin-top: 2rem;
143 - margin-bottom: 2rem;
144 - }
145 -
146 - .tunnel-link-container {
147 - margin-top: 1rem;
148 - display: flex;
149 - flex-direction: column;
150 - gap: 1rem;
151 - }
152 -
153 - .tunnel-link-field {
154 - display: flex;
155 - align-items: center;
156 - margin-bottom: 0.5rem;
157 - background-color: var(--bg-color-secondary);
158 - border: 1px solid var(--border-color);
159 - border-radius: 4px;
160 - }
161 -
162 - .tunnel-link-input {
163 - flex: 1;
164 - padding: 0.75rem;
165 - background-color: transparent;
166 - border: none;
167 - color: var(--text-color);
168 - font-size: 0.9rem;
169 - outline: none;
170 - }
171 -
172 - .copy-link-button {
173 - padding: 0.5rem 0.75rem;
174 - background: none;
175 - border: none;
176 - border-left: 1px solid var(--border-color);
177 - color: var(--text-color);
178 - cursor: pointer;
179 - transition: background-color 0.2s;
180 - }
181 -
182 - .copy-link-button:hover {
183 - background-color: var(--bg-color-tertiary);
184 - }
185 -
186 - .copy-link-button i,
187 - .refresh-link-button i,
188 - .btn i {
189 - margin-right: 6px;
190 - }
191 -
192 - .tunnel-link-info {
193 - margin-top: 1rem;
194 - font-size: 1rem;
195 - color: var(--text-color);
196 - text-align: center;
197 - line-height: 1.5;
198 - }
199 -
200 - .tunnel-loading-status {
201 - display: flex;
202 - justify-content: center;
203 - align-items: center;
204 - margin-top: 2rem;
205 - margin-bottom: 2rem;
206 - min-height: 38px;
207 - font-style: italic;
208 - color: var(--text-color-secondary);
209 - }
210 -
211 - .tunnel-loading-status .icon {
212 - font-size: 1.5rem;
213 - margin-right: 10px;
214 - color: var(--accent-color);
215 - }
216 -
217 - .refresh-link-button {
218 - padding: 0.5rem 0.75rem;
219 - background: none;
220 - border: none;
221 - border-left: 1px solid var(--border-color);
222 - color: var(--text-color);
223 - cursor: pointer;
224 - transition: background-color 0.2s;
225 - }
226 -
227 - .refresh-link-button:hover {
228 - background-color: var(--bg-color-tertiary);
229 - color: var(--accent-color);
230 - }
231 -
232 - .tunnel-link-persistence {
233 - margin-top: 0.75rem;
234 - font-size: 0.95rem;
235 - color: rgba(255, 255, 255, 0.7);
236 - text-align: center;
237 - font-style: italic;
238 - }
239 -
240 - .btn-danger {
241 - background-color: #dc3545;
242 - color: white;
243 - border: none;
244 - }
245 -
246 - .btn-danger:hover {
247 - background-color: #bd2130;
248 - }
249 -
250 - .stop-tunnel-container {
251 - margin-top: 20px;
252 - display: flex;
253 - justify-content: center;
254 - }
255 -
256 - /* Section title icon styling */
257 - .section-title i {
258 - margin-right: 8px;
259 - }
260 -
261 - /* Copy button states */
262 - .copy-success {
263 - background-color: rgba(40, 167, 69, 0.15) !important;
264 - color: #28a745 !important;
265 - border-left: 1px solid rgba(40, 167, 69, 0.5) !important;
266 - transition: all 0.3s ease-in-out;
267 - }
268 -
269 - .copy-error {
270 - background-color: rgba(220, 53, 69, 0.15) !important;
271 - color: #dc3545 !important;
272 - border-left: 1px solid rgba(220, 53, 69, 0.5) !important;
273 - transition: all 0.3s ease-in-out;
274 - }
275 -
276 - /* Animation for copy button */
277 - @keyframes pulse {
278 - 0% {
279 - transform: scale(1);
280 - }
281 -
282 - 50% {
283 - transform: scale(1.05);
284 - }
285 -
286 - 100% {
287 - transform: scale(1);
288 - }
289 - }
290 -
291 - .copy-success,
292 - .copy-error {
293 - animation: pulse 0.5s;
294 - }
295 -
296 - /* Refresh button state */
297 - .refreshing {
298 - opacity: 0.7;
299 - pointer-events: none;
300 - background-color: rgba(108, 117, 125, 0.15) !important;
301 - border-left: 1px solid rgba(108, 117, 125, 0.5) !important;
302 - }
303 -
304 - /* Create and Stop button states */
305 - .creating,
306 - .stopping {
307 - opacity: 0.8;
308 - pointer-events: none;
309 - cursor: not-allowed;
310 - }
311 -
312 - .creating {
313 - background-color: rgba(0, 123, 255, 0.7) !important;
314 - }
315 -
316 - .stopping {
317 - background-color: rgba(220, 53, 69, 0.7) !important;
318 - }
319 -
320 - /* QR Code Styles */
321 - .tunnel-qr-container {
322 - display: flex;
323 - align-items: center;
324 - justify-content: flex-start;
325 - gap: 1rem;
326 - margin: 0.5rem 0;
327 - padding: 1rem;
328 - background-color: var(--bg-color-secondary);
329 - border: 1px solid var(--border-color);
330 - border-radius: 8px;
331 - }
332 -
333 - .tunnel-qr-code {
334 - flex-shrink: 0;
335 - display: flex;
336 - align-items: center;
337 - justify-content: center;
338 - min-width: 128px;
339 - min-height: 128px;
340 - background-color: white;
341 - border-radius: 8px;
342 - padding: 8px;
343 - }
344 -
345 - .tunnel-qr-code canvas,
346 - .tunnel-qr-code img {
347 - border-radius: 4px;
348 - max-width: 100%;
349 - max-height: 100%;
350 - }
351 -
352 - .tunnel-qr-label {
353 - font-size: 0.9rem;
354 - color: var(--text-color-secondary);
355 - text-align: center;
356 - line-height: 1.4;
357 - flex: 1;
358 - }
359 -
360 - .qr-error {
361 - color: var(--error-color, #dc3545);
362 - font-size: 0.8rem;
363 - text-align: center;
364 - padding: 1rem;
365 - background-color: rgba(220, 53, 69, 0.1);
366 - border-radius: 4px;
367 - border: 1px solid rgba(220, 53, 69, 0.2);
368 - }
369 -
370 - /* Responsive design for QR code container */
371 - @media (max-width: 640px) {
372 - .tunnel-qr-container {
373 - flex-direction: column;
374 - text-align: center;
375 - gap: 0.75rem;
376 - padding: 0.75rem;
377 - }
378 -
379 - .tunnel-qr-code {
380 - min-width: 100px;
381 - min-height: 100px;
382 - align-self: center;
383 - }
384 -
385 - .tunnel-qr-label {
386 - text-align: center;
387 - }
388 - }
389 -
390 - /* Light mode adjustments */
391 - .light-mode .tunnel-qr-code {
392 - background-color: #ffffff;
393 - border: 1px solid #e0e0e0;
394 - }
395 -
396 - .light-mode .qr-error {
397 - background-color: rgba(220, 53, 69, 0.05);
398 - color: #dc3545;
399 - }
400 -
401 - /* Microsoft Login Box */
402 - .microsoft-login-box {
403 - margin: 1.5rem 0;
404 - padding: 1.5rem;
405 - background: linear-gradient(135deg, rgba(0, 120, 212, 0.12), rgba(0, 120, 212, 0.04));
406 - border: 1px solid rgba(0, 120, 212, 0.35);
407 - border-radius: 12px;
408 - text-align: center;
409 - box-shadow: 0 4px 20px rgba(0, 120, 212, 0.1);
410 - }
411 -
412 - .microsoft-login-header {
413 - display: flex;
414 - align-items: center;
415 - justify-content: center;
416 - gap: 0.5rem;
417 - margin-bottom: 1.25rem;
418 - }
419 -
420 - .microsoft-login-icon {
421 - font-size: 1.5rem;
422 - color: #0078d4;
423 - background: rgba(0, 120, 212, 0.15);
424 - padding: 0.5rem;
425 - border-radius: 50%;
426 - }
427 -
428 - .microsoft-login-title {
429 - font-size: 1.1rem;
430 - font-weight: 600;
431 - color: #0078d4;
432 - }
433 -
434 - .microsoft-login-steps {
435 - text-align: left;
436 - margin-bottom: 1rem;
437 - }
438 -
439 - .microsoft-login-step {
440 - display: flex;
441 - align-items: center;
442 - gap: 0.75rem;
443 - margin: 0.75rem 0 0.5rem 0;
444 - font-size: 0.9rem;
445 - color: var(--text-color);
446 - }
447 -
448 - .step-number {
449 - display: flex;
450 - align-items: center;
451 - justify-content: center;
452 - width: 24px;
453 - height: 24px;
454 - background-color: #0078d4;
455 - color: white;
456 - border-radius: 50%;
457 - font-size: 0.8rem;
458 - font-weight: 600;
459 - flex-shrink: 0;
460 - }
461 -
462 - .step-text {
463 - color: var(--text-color-secondary);
464 - }
465 -
466 - .microsoft-login-link {
467 - display: flex;
468 - align-items: center;
469 - justify-content: center;
470 - gap: 0.5rem;
471 - padding: 0.75rem 1rem;
472 - margin: 0.5rem 0 0.75rem 2.25rem;
473 - background-color: rgba(0, 120, 212, 0.1);
474 - border: 1px solid rgba(0, 120, 212, 0.3);
475 - border-radius: 8px;
476 - color: #0078d4;
477 - text-decoration: none;
478 - font-size: 0.85rem;
479 - transition: background-color 0.2s ease, border-color 0.2s ease,
480 - box-shadow 0.2s ease;
481 - word-break: break-all;
482 - }
483 -
484 - .microsoft-login-link:hover {
485 - background-color: rgba(0, 120, 212, 0.2);
486 - border-color: rgba(0, 120, 212, 0.5);
487 - box-shadow: 0 4px 12px rgba(0, 120, 212, 0.15);
488 - }
489 -
490 - .microsoft-login-link .icon {
491 - font-size: 1.1rem;
492 - flex-shrink: 0;
493 - }
494 -
495 - .microsoft-login-code-container {
496 - display: flex;
497 - flex-direction: column;
498 - align-items: center;
499 - gap: 0.75rem;
500 - margin: 1rem 0;
501 - padding: 1rem;
502 - background-color: var(--bg-color-secondary);
503 - border-radius: 10px;
504 - border: 1px solid var(--border-color);
505 - }
506 -
507 - .microsoft-login-code {
508 - font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
509 - font-size: 2rem;
510 - font-weight: 700;
511 - letter-spacing: 0.2em;
512 - color: var(--text-color);
513 - background: linear-gradient(135deg, rgba(0, 120, 212, 0.1), transparent);
514 - padding: 0.75rem 1.5rem;
515 - border-radius: 8px;
516 - border: 2px dashed rgba(0, 120, 212, 0.4);
517 - user-select: all;
518 - cursor: text;
519 - }
520 -
521 - .btn-copy-code {
522 - background-color: #0078d4;
523 - color: white;
524 - border: none;
525 - padding: 0.6rem 1.25rem;
526 - border-radius: 6px;
527 - cursor: pointer;
528 - font-size: 0.9rem;
529 - font-weight: 500;
530 - display: inline-flex;
531 - align-items: center;
532 - gap: 0.4rem;
533 - transition: background-color 0.2s ease, box-shadow 0.2s ease,
534 - filter 0.2s ease;
535 - }
536 -
537 - .btn-copy-code:hover {
538 - background-color: #106ebe;
539 - box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
540 - }
541 -
542 - .btn-copy-code.copied {
543 - background-color: #28a745;
544 - }
545 -
546 - .btn-copy-code.copied:hover {
547 - background-color: #218838;
548 - }
549 -
550 - .microsoft-login-footer {
551 - display: flex;
552 - align-items: center;
553 - justify-content: center;
554 - gap: 0.5rem;
555 - margin-top: 1rem;
556 - padding-top: 1rem;
557 - border-top: 1px solid rgba(0, 120, 212, 0.2);
558 - font-size: 0.85rem;
559 - color: var(--text-color-secondary);
560 - font-style: italic;
561 - }
562 -
563 - .microsoft-login-footer .icon {
564 - font-size: 1.1rem;
565 - color: #0078d4;
566 - }
567 -
568 - @keyframes spin-slow {
569 - from { transform: rotate(0deg); }
570 - to { transform: rotate(360deg); }
571 - }
572 -
573 - .spin-slow {
574 - animation: spin-slow 2s linear infinite;
575 - }
576 -
577 - /* Responsive adjustments for Microsoft login */
578 - @media (max-width: 480px) {
579 - .microsoft-login-box {
580 - padding: 1rem;
581 - }
582 -
583 - .microsoft-login-code {
584 - font-size: 1.5rem;
585 - letter-spacing: 0.15em;
586 - padding: 0.5rem 1rem;
587 - }
588 -
589 - .microsoft-login-link {
590 - margin-left: 0;
591 - font-size: 0.8rem;
592 - }
593 -
594 - .microsoft-login-step {
595 - font-size: 0.85rem;
596 - }
597 - }
171 + .modal-inner.remote-link-modal {
172 + width: min(92vw, 760px);
173 + max-height: min(88vh, 820px);
174 + }
175 +
176 + .modal-inner.remote-link-modal .modal-scroll {
177 + padding: 0;
178 + }
179 +
180 + .modal-inner.remote-link-modal .modal-bd {
181 + padding: 22px 24px 26px;
182 + }
183 +
184 + .remote-link-surface {
185 + width: 100%;
186 + margin: 0 auto;
187 + }
188 +
189 + .remote-link-hero {
190 + display: grid;
191 + grid-template-columns: auto minmax(0, 1fr);
192 + gap: 14px;
193 + align-items: start;
194 + margin-bottom: 16px;
195 + }
196 +
197 + .remote-link-hero-icon {
198 + display: grid;
199 + place-items: center;
200 + width: 42px;
201 + height: 42px;
202 + border-radius: 8px;
203 + background: color-mix(in srgb, var(--color-primary) 16%, transparent);
204 + color: color-mix(in srgb, var(--color-primary) 82%, var(--color-text));
205 + }
206 +
207 + .remote-link-hero-icon .material-symbols-outlined {
208 + font-size: 24px;
209 + }
210 +
211 + .remote-link-safety-note,
212 + .remote-link-loading,
213 + .remote-link-status-panel,
214 + .remote-link-qr-panel {
215 + display: flex;
216 + align-items: center;
217 + gap: 10px;
218 + border: 1px solid color-mix(in srgb, var(--color-border) 68%, transparent);
219 + border-radius: 8px;
220 + background: color-mix(in srgb, var(--color-panel) 58%, transparent);
221 + }
222 +
223 + .remote-link-safety-note {
224 + margin: 10px 0 14px;
225 + padding: 10px 12px;
226 + color: var(--color-text);
227 + font-size: 0.86rem;
228 + line-height: 1.35;
229 + }
230 +
231 + .remote-link-safety-note .material-symbols-outlined {
232 + color: #f2bf4b;
233 + font-size: 20px;
234 + }
235 +
236 + .remote-link-provider-field {
237 + margin-bottom: 12px;
238 + }
239 +
240 + .remote-link-loading {
241 + justify-content: center;
242 + min-height: 46px;
243 + margin: 16px 0;
244 + padding: 10px 12px;
245 + color: var(--color-text);
246 + opacity: 0.86;
247 + }
248 +
249 + .remote-link-url-row {
250 + display: grid;
251 + grid-template-columns: minmax(0, 1fr) auto auto;
252 + gap: 8px;
253 + align-items: center;
254 + margin: 16px 0 12px;
255 + }
256 +
257 + .remote-link-input {
258 + min-height: 38px;
259 + font-family: "Roboto Mono", monospace;
260 + font-size: 0.82rem;
261 + }
262 +
263 + .remote-link-action,
264 + .remote-link-primary,
265 + .btn-copy-code,
266 + .remote-link-footer-actions .btn {
267 + display: inline-flex;
268 + align-items: center;
269 + justify-content: center;
270 + gap: 6px;
271 + white-space: nowrap;
272 + }
273 +
274 + .remote-link-action {
275 + min-height: 38px;
276 + padding: 0 11px;
277 + border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
278 + border-radius: 7px;
279 + background: color-mix(in srgb, var(--color-background) 72%, transparent);
280 + color: var(--color-text);
281 + cursor: pointer;
282 + font: inherit;
283 + font-size: 0.82rem;
284 + font-weight: 700;
285 + }
286 +
287 + .remote-link-action:hover:not(:disabled) {
288 + border-color: color-mix(in srgb, var(--color-primary) 34%, var(--color-border));
289 + background: color-mix(in srgb, var(--color-background-hover) 74%, transparent);
290 + }
291 +
292 + .remote-link-action:disabled {
293 + cursor: not-allowed;
294 + opacity: 0.52;
295 + }
296 +
297 + .remote-link-share-layout {
298 + display: grid;
299 + grid-template-columns: minmax(0, 1.35fr) minmax(220px, 0.8fr);
300 + gap: 10px;
301 + margin-top: 10px;
302 + }
303 +
304 + .remote-link-qr-panel,
305 + .remote-link-status-panel {
306 + padding: 12px;
307 + align-items: center;
308 + }
309 +
310 + .remote-link-status-panel {
311 + align-items: flex-start;
312 + }
313 +
314 + .remote-link-status-panel > .material-symbols-outlined {
315 + color: #63c98b;
316 + font-size: 22px;
317 + }
318 +
319 + .tunnel-qr-code {
320 + display: grid;
321 + place-items: center;
322 + width: 116px;
323 + min-width: 116px;
324 + height: 116px;
325 + padding: 8px;
326 + border-radius: 8px;
327 + background: #fff;
328 + }
329 +
330 + .tunnel-qr-code canvas,
331 + .tunnel-qr-code img {
332 + max-width: 100%;
333 + max-height: 100%;
334 + border-radius: 4px;
335 + }
336 +
337 + .remote-link-qr-title,
338 + .remote-link-status-title,
339 + .microsoft-login-title {
340 + color: color-mix(in srgb, var(--color-text) 88%, var(--color-primary));
341 + font-weight: 800;
342 + }
343 +
344 + .remote-link-qr-copy,
345 + .remote-link-status-copy,
346 + .microsoft-login-copy {
347 + margin-top: 4px;
348 + color: var(--color-text);
349 + font-size: 0.84rem;
350 + line-height: 1.35;
351 + opacity: 0.76;
352 + }
353 +
354 + .remote-link-footer-actions {
355 + display: flex;
356 + justify-content: flex-end;
357 + margin-top: 14px;
358 + }
359 +
360 + .remote-link-empty {
361 + display: flex;
362 + flex-direction: column;
363 + align-items: center;
364 + justify-content: center;
365 + gap: 8px;
366 + min-height: 190px;
367 + margin-top: 14px;
368 + padding: 22px;
369 + border: 1px dashed color-mix(in srgb, var(--color-border) 78%, transparent);
370 + border-radius: 8px;
371 + text-align: center;
372 + color: var(--color-text);
373 + }
374 +
375 + .remote-link-empty > .material-symbols-outlined {
376 + color: var(--color-primary);
377 + font-size: 30px;
378 + }
379 +
380 + .remote-link-empty strong {
381 + font-size: 1rem;
382 + }
383 +
384 + .remote-link-empty > span:not(.material-symbols-outlined) {
385 + max-width: 46ch;
386 + opacity: 0.74;
387 + line-height: 1.35;
388 + }
389 +
390 + .remote-link-primary {
391 + margin-top: 6px;
392 + }
393 +
394 + .copy-success {
395 + border-color: color-mix(in srgb, #63c98b 58%, var(--color-border)) !important;
396 + color: #63c98b !important;
397 + background: color-mix(in srgb, #63c98b 12%, transparent) !important;
398 + }
399 +
400 + .copy-error {
401 + border-color: color-mix(in srgb, #ff6b7a 56%, var(--color-border)) !important;
402 + color: #ff6b7a !important;
403 + background: color-mix(in srgb, #ff6b7a 12%, transparent) !important;
404 + }
405 +
406 + .microsoft-login-box {
407 + margin: 14px 0;
408 + padding: 14px;
409 + border: 1px solid color-mix(in srgb, #4da3ff 36%, var(--color-border));
410 + border-radius: 8px;
411 + background:
412 + linear-gradient(120deg, color-mix(in srgb, #4da3ff 12%, transparent), transparent 62%),
413 + color-mix(in srgb, var(--color-panel) 68%, transparent);
414 + }
415 +
416 + .microsoft-login-header {
417 + display: flex;
418 + align-items: flex-start;
419 + gap: 10px;
420 + margin-bottom: 12px;
421 + }
422 +
423 + .microsoft-login-header .material-symbols-outlined {
424 + color: #4da3ff;
425 + font-size: 22px;
426 + }
427 +
428 + .microsoft-login-link {
429 + display: flex;
430 + align-items: center;
431 + gap: 6px;
432 + min-width: 0;
433 + margin-bottom: 12px;
434 + padding: 9px 10px;
435 + border: 1px solid color-mix(in srgb, #4da3ff 34%, var(--color-border));
436 + border-radius: 7px;
437 + color: color-mix(in srgb, #4da3ff 82%, var(--color-text));
438 + background: color-mix(in srgb, #4da3ff 8%, transparent);
439 + text-decoration: none;
440 + word-break: break-all;
441 + }
442 +
443 + .microsoft-login-code-row {
444 + display: flex;
445 + flex-wrap: wrap;
446 + align-items: center;
447 + gap: 10px;
448 + }
449 +
450 + .microsoft-login-code {
451 + min-height: 38px;
452 + padding: 7px 12px;
453 + border: 1px dashed color-mix(in srgb, #4da3ff 44%, var(--color-border));
454 + border-radius: 7px;
455 + font-family: "Roboto Mono", monospace;
456 + font-size: 1.1rem;
457 + font-weight: 800;
458 + color: var(--color-text);
459 + background: color-mix(in srgb, var(--color-background) 56%, transparent);
460 + user-select: all;
461 + }
462 +
463 + .spin {
464 + animation: remote-link-spin 0.8s linear infinite;
465 + }
466 +
467 + @keyframes remote-link-spin {
468 + from { transform: rotate(0deg); }
469 + to { transform: rotate(360deg); }
470 + }
471 +
472 + @media (max-width: 720px) {
473 + .modal-inner.remote-link-modal .modal-bd {
474 + padding: 16px;
475 + }
476 +
477 + .remote-link-url-row,
478 + .remote-link-share-layout {
479 + grid-template-columns: 1fr;
480 + }
481 +
482 + .remote-link-action {
483 + width: 100%;
484 + }
485 +
486 + .remote-link-qr-panel {
487 + align-items: center;
488 + }
489 + }
490 +
491 + @media (max-width: 520px) {
492 + .remote-link-hero {
493 + grid-template-columns: 1fr;
494 + }
495 +
496 + .remote-link-hero-icon {
497 + width: 38px;
498 + height: 38px;
499 + }
500 +
501 + .remote-link-qr-panel {
502 + flex-direction: column;
503 + text-align: center;
504 + }
505 +
506 + .remote-link-footer-actions .btn {
507 + width: 100%;
508 + }
509 + }
510 </style>
599 -
600 -</body>
601 -
602 -</html>
\ No newline at end of file
511 + </body>
512 +</html>
webui/components/settings/tunnel/tunnel-store.js
+40 -76
@@ -12,6 +12,7 @@ const model = {
12 microsoftLoginCode: "",
13 microsoftLoginUrl: "",
14 codeCopied: false,
15 + copyState: "",
16 notificationPollInterval: null,
17 hasError: false,
18
@@ -19,6 +20,22 @@ const model = {
20 this.checkTunnelStatus();
21 },
22
23 + cleanup() {
24 + this.stopNotificationPolling();
25 + },
26 +
27 + get copyLinkIcon() {
28 + if (this.copyState === "success") return "check";
29 + if (this.copyState === "error") return "close";
30 + return "content_copy";
31 + },
32 +
33 + get copyLinkLabel() {
34 + if (this.copyState === "success") return "Copied";
35 + if (this.copyState === "error") return "Copy failed";
36 + return "Copy link";
37 + },
38 +
39 clearMicrosoftLogin() {
40 this.microsoftLoginCode = "";
41 this.microsoftLoginUrl = "";
@@ -74,13 +91,14 @@ const model = {
91 break;
92 case "error":
93 this.hasError = true;
77 - window.toastFrontendError(n.message, "Tunnel Error");
94 + window.toastFrontendError(n.message, "Remote Link");
95 this.stopNotificationPolling();
96 break;
97 case "tunnel_url":
98 if (n.data && n.data.url) {
99 this.tunnelLink = n.data.url;
100 this.linkGenerated = true;
101 + Sleep.Skip().then(() => this.generateQRCode());
102 }
103 break;
104 case "tunnel_stopped":
@@ -108,6 +126,7 @@ const model = {
126 if (data.tunnel_url && data.is_running) {
127 this.tunnelLink = data.tunnel_url;
128 this.linkGenerated = true;
129 + Sleep.Skip().then(() => this.generateQRCode());
130 this.stopNotificationPolling();
131 }
132 } catch (error) {
@@ -214,7 +233,7 @@ const model = {
233 // Call generate but with a confirmation first
234 if (
235 confirm(
217 - "Are you sure you want to generate a new tunnel URL? The old URL will no longer work."
236 + "Create a new remote link? The current URL will stop working."
237 )
238 ) {
239
@@ -223,14 +242,6 @@ const model = {
242 this.clearMicrosoftLogin();
243 this.loadingText = "Refreshing tunnel...";
244
226 - // Change refresh button appearance
227 - const refreshButton = document.querySelector("#tunnel-settings-section .refresh-link-button");
228 - const originalContent = refreshButton.innerHTML;
229 - refreshButton.innerHTML =
230 - '<span class="icon material-symbols-outlined spin">progress_activity</span> Refreshing...';
231 - refreshButton.disabled = true;
232 - refreshButton.classList.add("refreshing");
233 -
245 try {
246 // First stop any existing tunnel
247 const stopResponse = await fetchApi("/tunnel_proxy", {
@@ -252,14 +263,9 @@ const model = {
263 await this.generateLink();
264 } catch (error) {
265 console.error("Error refreshing tunnel:", error);
255 - window.toastFrontendError("Error refreshing tunnel", "Tunnel Error");
266 + window.toastFrontendError("Error refreshing remote link", "Remote Link");
267 this.isLoading = false;
268 this.loadingText = "";
258 - } finally {
259 - // Reset refresh button
260 - refreshButton.innerHTML = originalContent;
261 - refreshButton.disabled = false;
262 - refreshButton.classList.remove("refreshing");
269 }
270 }
271 },
@@ -281,12 +287,9 @@ const model = {
287 // If no authentication is set, warn the user
288 if (!hasAuth) {
289 const proceed = confirm(
284 - "WARNING: No authentication is configured for your Agent Zero instance.\n\n" +
285 - "Creating a public tunnel without authentication means anyone with the URL " +
286 - "can access your Agent Zero instance.\n\n" +
287 - "It is recommended to set up authentication in the Settings > Authentication section " +
288 - "before creating a public tunnel.\n\n" +
289 - "Do you want to proceed anyway?"
290 + "Remote Link works best with sign-in enabled.\n\n" +
291 + "Without a login, anyone with the URL can reach this Agent Zero instance.\n\n" +
292 + "Turn on authentication in Settings before sharing this link. Continue anyway?"
293 );
294
295 if (!proceed) {
@@ -303,15 +306,6 @@ const model = {
306 this.clearMicrosoftLogin();
307 this.loadingText = "Starting tunnel...";
308
306 - // Change create button appearance
307 - const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok");
308 - if (createButton) {
309 - createButton.innerHTML =
310 - '<span class="icon material-symbols-outlined spin">progress_activity</span> Creating...';
311 - createButton.disabled = true;
312 - createButton.classList.add("creating");
313 - }
314 -
309 // Start polling for notifications
310 this.startNotificationPolling();
311
@@ -338,7 +332,7 @@ const model = {
332 // Check for error
333 if (!data.success && data.message) {
334 this.hasError = true;
341 - window.toastFrontendError(data.message, "Tunnel Error");
335 + window.toastFrontendError(data.message, "Remote Link");
336 console.error("Tunnel creation failed:", data);
337 this.stopNotificationPolling();
338 return;
@@ -357,12 +351,12 @@ const model = {
351
352 // Show success message to confirm creation
353 window.toastFrontendInfo(
360 - "Tunnel created successfully",
361 - "Tunnel Status"
354 + "Remote link is ready",
355 + "Remote Link"
356 );
357 }
358 } catch (error) {
365 - window.toastFrontendError("Error creating tunnel", "Tunnel Error");
359 + window.toastFrontendError("Error creating remote link", "Remote Link");
360 console.error("Error creating tunnel:", error);
361 } finally {
362 this.isLoading = false;
@@ -370,21 +364,13 @@ const model = {
364 this.stopNotificationPolling();
365 this.clearMicrosoftLogin();
366
373 - // Reset create button if it's still in the DOM
374 - const createButton = document.querySelector("#tunnel-settings-section .tunnel-actions .btn-ok");
375 - if (createButton) {
376 - createButton.innerHTML =
377 - '<span class="icon material-symbols-outlined">play_circle</span> Create Tunnel';
378 - createButton.disabled = false;
379 - createButton.classList.remove("creating");
380 - }
367 }
368 },
369
370 async stopTunnel() {
371 if (
372 confirm(
387 - "Are you sure you want to stop the tunnel? The URL will no longer be accessible."
373 + "Stop this remote link? The current URL will no longer be accessible."
374 )
375 ) {
376 this.isLoading = true;
@@ -418,25 +404,15 @@ const model = {
404 this.linkGenerated = false;
405
406 window.toastFrontendInfo(
421 - "Tunnel stopped successfully",
422 - "Tunnel Status"
407 + "Remote link stopped",
408 + "Remote Link"
409 );
410 } else {
425 - window.toastFrontendError("Failed to stop tunnel", "Tunnel Error");
426 -
427 - // Reset stop button
428 - stopButton.innerHTML = originalStopContent;
429 - stopButton.disabled = false;
430 - stopButton.classList.remove("stopping");
411 + window.toastFrontendError("Failed to stop remote link", "Remote Link");
412 }
413 } catch (error) {
433 - window.toastFrontendError("Error stopping tunnel", "Tunnel Error");
414 + window.toastFrontendError("Error stopping remote link", "Remote Link");
415 console.error("Error stopping tunnel:", error);
435 -
436 - // Reset stop button
437 - stopButton.innerHTML = originalStopContent;
438 - stopButton.disabled = false;
439 - stopButton.classList.remove("stopping");
416 } finally {
417 this.isLoading = false;
418 this.loadingText = "";
@@ -447,45 +423,33 @@ const model = {
423 copyToClipboard() {
424 if (!this.tunnelLink) return;
425
450 - const copyButton = document.querySelector("#tunnel-settings-section .copy-link-button");
451 - const originalContent = copyButton.innerHTML;
452 -
426 navigator.clipboard
427 .writeText(this.tunnelLink)
428 .then(() => {
456 - // Update button to show success state
457 - copyButton.innerHTML =
458 - '<span class="icon material-symbols-outlined">check</span> Copied!';
459 - copyButton.classList.add("copy-success");
429 + this.copyState = "success";
430
431 // Show toast notification
432 window.toastFrontendInfo(
463 - "Tunnel URL copied to clipboard!",
433 + "Remote link copied",
434 "Clipboard"
435 );
436
437 // Reset button after 2 seconds
438 setTimeout(() => {
469 - copyButton.innerHTML = originalContent;
470 - copyButton.classList.remove("copy-success");
439 + this.copyState = "";
440 }, 2000);
441 })
442 .catch((err) => {
443 console.error("Failed to copy URL: ", err);
444 + this.copyState = "error";
445 window.toastFrontendError(
476 - "Failed to copy tunnel URL",
446 + "Failed to copy remote link",
447 "Clipboard Error"
448 );
449
480 - // Show error state
481 - copyButton.innerHTML =
482 - '<span class="icon material-symbols-outlined">close</span> Failed';
483 - copyButton.classList.add("copy-error");
484 -
450 // Reset button after 2 seconds
451 setTimeout(() => {
487 - copyButton.innerHTML = originalContent;
488 - copyButton.classList.remove("copy-error");
452 + this.copyState = "";
453 }, 2000);
454 });
455 },
webui/css/modals.css
+76 -41
@@ -1,10 +1,9 @@
1 /* Modal Styles */
2 /* Until the full transition of A0 modals is complete,
3 -consider these classes as part of the old modal system that will be deprecated soon
4 -(currently used only by Settings modal). .modal-content, .modal-container, .modal-overlay
3 +consider .modal-content, .modal-container, and .modal-overlay part of the legacy modal system.
4
6 -Do not maintain these classes, but be careful:
7 -some classes like modal-header are shared between the old and the new system */
5 +Keep changes conservative because some classes like modal-header are shared between
6 +the old and the new system. */
7
8 .modal {
9 display: none;
@@ -27,12 +26,13 @@ some classes like modal-header are shared between the old and the new system */
26 top: 50%;
27 left: 50%;
28 transform: translate(-50%, -50%);
30 - background-color: var(--color-background);
31 - width: 90%;
32 - max-width: 960px;
33 - max-height: 90vh;
34 - border-radius: 8px;
35 - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
29 + width: min(92vw, 980px);
30 + max-width: calc(100vw - 24px);
31 + max-height: min(88vh, 900px);
32 + border: 1px solid color-mix(in srgb, var(--color-border) 68%, transparent);
33 + border-radius: 7px;
34 + background: color-mix(in srgb, var(--color-background) 94%, #000 6%);
35 + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32);
36 z-index: 2;
37 }
38
@@ -43,7 +43,9 @@ some classes like modal-header are shared between the old and the new system */
43 left: 0;
44 right: 0;
45 bottom: 0;
46 - background: rgba(0, 0, 0, 0.5);
46 + background: rgba(0, 0, 0, 0.48);
47 + backdrop-filter: blur(8px) saturate(112%);
48 + -webkit-backdrop-filter: blur(8px) saturate(112%);
49 display: flex;
50 align-items: center;
51 justify-content: center;
@@ -57,21 +59,24 @@ some classes like modal-header are shared between the old and the new system */
59 left: 0;
60 width: 100%;
61 height: 100%;
60 - background-color: rgba(0, 0, 0, 0.5);
62 + background-color: rgba(0, 0, 0, 0.48);
63 + backdrop-filter: blur(8px) saturate(112%);
64 + -webkit-backdrop-filter: blur(8px) saturate(112%);
65 z-index: 1;
66 cursor: pointer;
67 }
68
69 /* Modal Container (old system) */
70 .modal-container {
67 - background-color: var(--color-panel);
68 - border-radius: 12px;
71 + background: color-mix(in srgb, var(--color-background) 94%, #000 6%);
72 + border: 1px solid color-mix(in srgb, var(--color-border) 68%, transparent);
73 + border-radius: 7px;
74 width: 1100px; /* Reduced from 1200px to 1100px */
75 max-height: 90vh;
76 display: flex;
77 flex-direction: column;
78 overflow: hidden;
74 - box-shadow: 0 4px 23px rgba(0, 0, 0, 0.2);
79 + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32);
80 box-sizing: border-box;
81 }
82
@@ -82,10 +87,11 @@ some classes like modal-header are shared between the old and the new system */
87 align-items: center;
88 justify-content: space-between;
89 gap: 0.5rem;
85 - padding: 0.5rem 1.5rem 0.5rem 2rem;
86 - background-color: var(--color-background);
87 - color: var(--color-primary);
88 - border-bottom: 1px solid var(--color-border);
90 + min-height: 42px;
91 + padding: 0.45rem 0.75rem 0.45rem 1rem;
92 + background: color-mix(in srgb, var(--color-background) 92%, #000 8%);
93 + color: var(--color-text);
94 + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
95 }
96 .modal-header h2 {
97 margin: 0;
@@ -96,7 +102,8 @@ some classes like modal-header are shared between the old and the new system */
102 }
103
104 .modal-title {
99 - font-size: 0.95rem;
105 + font-size: 0.92rem;
106 + font-weight: 750;
107 letter-spacing: 0;
108 }
109
@@ -110,18 +117,29 @@ some classes like modal-header are shared between the old and the new system */
117
118 /* Modal Close Button */
119 .modal-close {
113 - background: none;
114 - border: none;
115 - font-size: xx-large;
120 + display: inline-flex;
121 + align-items: center;
122 + justify-content: center;
123 + width: 34px;
124 + height: 34px;
125 + min-width: 34px;
126 + min-height: 34px;
127 + background: transparent;
128 + border: 1px solid transparent;
129 + border-radius: 7px;
130 + font-size: 1.45rem;
131 + line-height: 1;
132 color: var(--color-text);
117 - opacity: 0.7;
133 + opacity: 0.72;
134 cursor: pointer;
135 padding: 0;
120 - transition: opacity 0.2s;
136 + transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
137 }
138
139 .modal-close:hover {
140 opacity: 1;
141 + border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
142 + background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
143 }
144
145 .modal-dock-button {
@@ -171,10 +189,16 @@ some classes like modal-header are shared between the old and the new system */
189 box-sizing: border-box;
190 }
191
192 +.modal-bd {
193 + min-width: 0;
194 +}
195 +
196 .modal-scroll {
175 - max-height: 90vh;
197 + min-height: 0;
198 + max-height: min(88vh, 900px);
199 overflow-y: auto;
177 - padding: 1rem;
200 + padding: 0.9rem 1rem 1rem;
201 + background: color-mix(in srgb, var(--color-background) 95%, #000 5%);
202 }
203
204 .modal-x {
@@ -214,10 +238,11 @@ some classes like modal-header are shared between the old and the new system */
238 display: flex;
239 justify-content: flex-end;
240 align-items: center;
217 - padding: var(--spacing-sm) var(--spacing-lg);
218 - border-top: 1px solid var(--color-border);
219 - background: var(--color-background);
220 - gap: 1rem;
241 + min-height: 54px;
242 + padding: 0.55rem 1rem;
243 + border-top: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
244 + background: color-mix(in srgb, var(--color-background) 92%, #000 8%);
245 + gap: 0.75rem;
246 }
247 .modal-inner.modal-with-footer {
248 display: flex;
@@ -230,8 +255,12 @@ some classes like modal-header are shared between the old and the new system */
255 }
256 .modal-footer-slot {
257 flex-shrink: 0;
233 - border-top: 1px solid var(--color-border);
234 - background: var(--color-background);
258 + border-top: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
259 + background: color-mix(in srgb, var(--color-background) 92%, #000 8%);
260 +}
261 +
262 +.modal-footer-slot .modal-footer {
263 + border-top: 0;
264 }
265
266 /* Section Styles */
@@ -280,18 +309,21 @@ some classes like modal-header are shared between the old and the new system */
309 /* Button Styles */
310 .btn {
311 font-weight: 500;
283 - padding: 0.5rem 1.5rem;
284 - border-radius: 0.25rem;
312 + min-height: 32px;
313 + padding: 0.42rem 1rem;
314 + border-radius: 7px;
315 cursor: pointer;
286 - border: none;
287 - font-size: 0.875rem;
316 + border: 1px solid transparent;
317 + font-size: 0.84rem;
318 font-family: "Rubik", Arial, Helvetica, sans-serif;
319 + transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, opacity 0.16s ease;
320 }
321 .btn.slim {
322 padding: 0.2em 0.4em;
323 }
324 .btn.primary {
294 - background: #2196f3;
325 + border-color: color-mix(in srgb, var(--color-primary) 42%, var(--color-border));
326 + background: color-mix(in srgb, #2196f3 78%, var(--color-panel));
327 color: white;
328 width: fit-content;
329 align-items: center;
@@ -301,7 +333,8 @@ some classes like modal-header are shared between the old and the new system */
333 cursor: not-allowed;
334 }
335 .btn-ok {
304 - background: #4248f1;
336 + border-color: color-mix(in srgb, #4248f1 56%, var(--color-border));
337 + background: color-mix(in srgb, #4248f1 86%, var(--color-panel));
338 color: white;
339 display: inline-flex;
340 align-items: center;
@@ -312,6 +345,7 @@ some classes like modal-header are shared between the old and the new system */
345 max-width: 20px;
346 }
347 .btn-ok:hover {
348 + border-color: color-mix(in srgb, #656bff 70%, var(--color-border));
349 background: #353bc5;
350 }
351 .btn-ok:active {
@@ -320,7 +354,7 @@ some classes like modal-header are shared between the old and the new system */
354 .btn-cancel {
355 background: transparent;
356 color: var(--color-accent);
323 - border: 0.15rem solid var(--color-accent);
357 + border: 1px solid color-mix(in srgb, var(--color-accent) 82%, var(--color-border));
358 transition: background 0.3s ease-in-out, color 0.3s ease-in-out;
359 }
360 .btn-cancel:hover {
@@ -340,7 +374,8 @@ some classes like modal-header are shared between the old and the new system */
374 color: var(--color-background);
375 }
376 .btn-field {
343 - background: #2196f3;
377 + border-color: color-mix(in srgb, #2196f3 48%, var(--color-border));
378 + background: color-mix(in srgb, #2196f3 78%, var(--color-panel));
379 color: white;
380 width: fit-content;
381 }
webui/css/settings.css
+404 -189
@@ -148,271 +148,486 @@ select:disabled {
148 cursor: not-allowed;
149 }
150
151 -/* Navigation Links */
152 -#settings-sections {
153 - padding-bottom: 1rem;
151 +/* Settings modal shell */
152 +.modal-inner.settings-modal {
153 + width: min(92vw, 1180px);
154 + height: min(88vh, 900px);
155 }
156
156 -nav ul {
157 - list-style: none;
157 +.modal-inner.settings-modal .modal-scroll,
158 +.modal-inner.settings-modal .modal-bd,
159 +.modal-inner.settings-modal .modal-bd > div[x-data],
160 +.modal-inner.settings-modal .modal-bd > div[x-data] > div {
161 + display: flex;
162 + flex: 1 1 auto;
163 + width: 100%;
164 + min-width: 0;
165 + min-height: 0;
166 + overflow: hidden;
167 +}
168 +
169 +.modal-inner.settings-modal .modal-scroll {
170 padding: 0;
159 - margin: 0;
171 +}
172 +
173 +.modal-inner.settings-modal .modal-header {
174 + min-height: 48px;
175 +}
176 +
177 +.settings-content {
178 display: grid;
161 - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
162 - gap: 1rem;
179 + grid-template-columns: 224px minmax(0, 1fr);
180 + flex: 1 1 auto;
181 + width: 100%;
182 + min-width: 0;
183 + min-height: 0;
184 + overflow: hidden;
185 }
186
165 -nav ul li {
166 - display: flex;
167 - font-weight: 500;
187 +.settings-tabs-container {
188 + position: sticky;
189 + top: 0;
190 + align-self: stretch;
191 + z-index: 2;
192 + min-width: 0;
193 + min-height: 0;
194 + height: 100%;
195 + padding: 10px 8px;
196 + overflow: hidden;
197 + border-right: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
198 + background: color-mix(in srgb, var(--color-panel) 44%, transparent);
199 }
200
170 -nav ul li a {
201 +.settings-tabs {
202 display: flex;
203 flex-direction: column;
204 + gap: 4px;
205 + width: 100%;
206 + min-width: 0;
207 + min-height: 0;
208 + max-height: 100%;
209 + overflow-y: auto;
210 +}
211 +
212 +.settings-tab {
213 + display: grid;
214 + grid-template-columns: 22px minmax(0, 1fr) auto;
215 align-items: center;
174 - text-align: center;
175 - text-decoration: none;
176 - color: var(--color-text);
177 - opacity: 0.8;
178 - background-color: var(--color-panel);
179 - border: 1px solid var(--color-border);
180 - border-radius: 8px;
181 - padding: 1rem;
216 + gap: 8px;
217 width: 100%;
183 - transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out,
184 - box-shadow 0.2s ease-in-out, opacity 0.2s ease-in-out;
218 + min-height: 38px;
219 + padding: 0 10px;
220 + border: 1px solid transparent;
221 + border-radius: 7px;
222 + appearance: none;
223 + background: transparent;
224 + color: var(--color-text);
225 + cursor: pointer;
226 + font: inherit;
227 + font-size: 0.83rem;
228 + font-weight: 560;
229 + line-height: 1.1;
230 + text-align: left;
231 + text-decoration: none;
232 + transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
233 }
234
187 -nav ul li a:hover {
188 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
189 - background-color: var(--color-secondary);
190 - border-color: var(--color-primary);
235 +.settings-tab-label {
236 + min-width: 0;
237 + overflow: hidden;
238 + text-overflow: ellipsis;
239 + white-space: nowrap;
240 }
241
193 -nav ul li a img {
194 - width: 50px;
195 - height: 50px;
196 - margin-bottom: 0.5rem;
197 - filter: var(--svg-filter);
242 +.settings-tab .material-symbols-outlined {
243 + color: var(--color-text-muted);
244 + font-size: 18px;
245 }
246
200 -.light-mode nav ul li a {
201 - background-color: var(--color-panel-light);
247 +.settings-tab:hover,
248 +.settings-tab.active {
249 + border-color: color-mix(in srgb, var(--color-primary) 25%, var(--color-border));
250 + background: color-mix(in srgb, var(--color-background-hover) 56%, transparent);
251 + opacity: 1;
252 }
253
204 -.light-mode nav ul li a:hover {
205 - background-color: var(--color-secondary-light);
254 +.settings-tab.active {
255 + color: var(--color-text);
256 + font-weight: 750;
257 }
258
208 -/* Responsive Design */
209 -@media (max-width: 768px) {
210 - .field-control {
211 - width: 100%;
212 - }
259 +.settings-tab.active .material-symbols-outlined {
260 + color: var(--color-primary);
261 +}
262
214 - .field-description {
215 - padding-bottom: var(--spacing-sm);
216 - }
263 +.settings-tab-attention {
264 + border-color: color-mix(in srgb, #f2bf4b 42%, var(--color-border));
265 + background: linear-gradient(
266 + 90deg,
267 + color-mix(in srgb, #f2bf4b 13%, transparent),
268 + transparent 62%
269 + );
270 +}
271
218 - .field {
219 - display: block;
220 - align-items: center;
221 - padding: var(--spacing-xs) 0;
222 - }
272 +.settings-tab-attention .material-symbols-outlined {
273 + color: #f2bf4b;
274 }
275
225 -@media (max-width: 640px) {
226 - nav ul {
227 - grid-template-columns: repeat(2, 1.2fr);
228 - }
276 +.settings-attention-dot {
277 + width: 7px;
278 + height: 7px;
279 + border-radius: 999px;
280 + background: #f2bf4b;
281 + box-shadow: 0 0 0 3px color-mix(in srgb, #f2bf4b 18%, transparent);
282 }
283
231 -@media (max-width: 480px) {
232 - nav ul {
233 - grid-template-columns: 1fr;
234 - }
284 +.settings-back-tab {
285 + margin-bottom: 4px;
286 + color: var(--color-text-muted);
287 +}
288
236 - nav ul li a {
237 - flex-direction: row;
238 - justify-content: flex-start;
239 - gap: 1rem;
240 - padding: 0.75rem 1rem;
241 - }
289 +.settings-tabs-heading {
290 + margin: 8px 8px 6px;
291 + color: var(--color-primary);
292 + font-size: 0.72rem;
293 + font-weight: 750;
294 + letter-spacing: 0;
295 + text-transform: uppercase;
296 +}
297
243 - nav ul li a img {
244 - margin-bottom: 0;
245 - width: 30px;
246 - height: 30px;
247 - }
298 +.settings-section-link {
299 + color: var(--color-text);
300 }
301
250 -/* Settings Tab Styles */
251 -.settings-tabs-container {
302 +.settings-pane {
303 + min-width: 0;
304 + min-height: 0;
305 + overflow: auto;
306 + padding: 18px 22px 28px;
307 + scroll-behavior: smooth;
308 +}
309 +
310 +.settings-tab-panel,
311 +.settings-tab-panel > x-component,
312 +.settings-tab-panel > x-component > div[x-data] {
313 + min-width: 0;
314 +}
315 +
316 +.settings-pane nav {
317 + display: none;
318 +}
319 +
320 +.settings-pane .section {
321 + display: block;
322 width: 100%;
253 - margin-bottom: 8px;
254 - padding: 0;
255 - position: relative;
323 + min-width: 0;
324 + margin: 0 0 10px;
325 + padding: 26px 0 30px;
326 + scroll-margin-top: 12px;
327 overflow: visible;
328 + border: 0;
329 + border-top: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
330 + border-radius: 0;
331 + box-sizing: border-box;
332 }
333
259 -.settings-tabs {
260 - display: flex;
261 - width: 100%;
262 - position: relative;
263 - gap: 5px;
264 - border-bottom: 3px solid var(--color-border);
265 - justify-content: flex-start;
266 - padding-left: 20px;
267 - padding-top: 8px;
268 - overflow-x: auto;
269 - overflow-y: hidden;
270 - white-space: nowrap;
271 - -webkit-overflow-scrolling: touch;
334 +.settings-tab-panel > x-component > div[x-data] > div > .section:first-of-type,
335 +.settings-tab-panel > x-component > div[x-data] > div > nav + .section {
336 + padding-top: 0;
337 + border-top: 0;
338 }
339
274 -.settings-tabs::before,
275 -.settings-tabs::after {
276 - content: '';
277 - position: absolute;
278 - top: 2px;
279 - bottom: 3px;
280 - width: 20px;
281 - pointer-events: none;
282 - z-index: 2;
283 - opacity: 0.7;
340 +.settings-pane .section-title {
341 + margin: 0 0 6px;
342 + padding: 0;
343 + border: 0;
344 + color: color-mix(in srgb, var(--color-text) 88%, var(--color-primary));
345 + font-size: 1.05rem;
346 + font-weight: 800;
347 }
348
286 -.settings-tabs::before {
287 - left: 0;
349 +.settings-pane .section-title.settings-update-title-attention {
350 + color: #f2bf4b;
351 }
352
290 -.settings-tabs::after {
291 - right: 0;
353 +.settings-pane .section-description {
354 + max-width: 72ch;
355 + margin: 0 0 18px;
356 + color: var(--color-text);
357 + font-size: 0.92rem;
358 + opacity: 0.78;
359 + line-height: 1.42;
360 }
361
294 -.settings-tab {
295 - padding: 8px 16px;
296 - cursor: pointer;
297 - position: relative;
362 +.modal-inner.settings-modal .field {
363 + grid-template-columns: minmax(220px, 0.9fr) minmax(260px, 1fr);
364 + gap: 20px;
365 + margin-block: 0;
366 + padding: 12px 0;
367 +}
368 +
369 +.modal-inner.settings-modal .field + .field {
370 + border-top: 1px solid color-mix(in srgb, var(--color-border) 24%, transparent);
371 +}
372 +
373 +.modal-inner.settings-modal .field-title {
374 + color: color-mix(in srgb, var(--color-text) 78%, var(--color-primary));
375 + font-weight: 800;
376 +}
377 +
378 +.modal-inner.settings-modal .field-description {
379 + margin-bottom: 0;
380 color: var(--color-text);
299 - border: 2px solid var(--color-border);
300 - border-bottom: none;
301 - border-radius: 8px 8px 0 0;
302 - transition: all 0.3s ease;
303 - background-color: var(--color-panel);
304 - margin-bottom: -3px;
305 - z-index: 1;
306 - min-width: min-content;
307 - width: auto;
308 - overflow: hidden;
309 - text-overflow: ellipsis;
310 - white-space: nowrap;
311 - flex-shrink: 0;
381 + opacity: 0.78;
382 + line-height: 1.28;
383 }
384
314 -.settings-tab:not(.active) {
315 - opacity: 0.8;
316 - border-bottom: 3px solid var(--color-border);
317 - background-color: var(--color-background);
385 +.modal-inner.settings-modal .field-control {
386 + gap: 8px;
387 }
388
320 -.settings-tab.active {
321 - border-color: var(--color-border);
322 - /* box-shadow:
323 - 0 -4px 8px -2px var(--color-border),
324 - 4px 0 8px -2px var(--color-border),
325 - -4px 0 8px -2px var(--color-border); */
326 - font-weight: bold;
327 - background-color: var(--color-panel);
328 - min-width: min-content;
329 - max-width: 150px;
389 +.modal-inner.settings-modal input[type="text"],
390 +.modal-inner.settings-modal input[type="password"],
391 +.modal-inner.settings-modal input[type="number"],
392 +.modal-inner.settings-modal textarea,
393 +.modal-inner.settings-modal select {
394 + border-color: color-mix(in srgb, var(--color-border) 70%, transparent);
395 + border-radius: 7px;
396 + background: color-mix(in srgb, var(--color-background) 72%, transparent);
397 }
398
399 +.settings-advanced-section {
400 + margin-top: 8px;
401 +}
402
333 -/* Responsive Design for Settings Tabs */
334 -@media (max-width: 640px) {
335 - .settings-tabs {
336 - gap: 2px;
337 - padding-left: 10px;
338 - }
403 +.settings-advanced-toggle {
404 + display: inline-flex;
405 + align-items: center;
406 + gap: 5px;
407 + min-height: 30px;
408 + padding: 3px 0;
409 + border: 0;
410 + background: transparent;
411 + color: var(--color-text);
412 + opacity: 0.68;
413 + cursor: pointer;
414 + font: inherit;
415 + font-size: 0.79rem;
416 + font-weight: 650;
417 + text-align: left;
418 +}
419
340 - .settings-tab {
341 - padding: 6px 12px;
342 - font-size: 0.9rem;
343 - }
420 +.settings-advanced-toggle:hover {
421 + opacity: 1;
422 }
423
346 -@media (max-width: 480px) {
347 - .settings-tabs {
348 - padding-left: 5px;
349 - }
424 +.settings-advanced-toggle-icon {
425 + font-size: 17px;
426 + transition: transform 0.15s ease;
427 +}
428
351 - .settings-tab {
352 - flex: 0 0 auto;
353 - text-align: center;
354 - min-width: min-content;
355 - max-width: 150px;
356 - }
357 - .settings-tab.active {
358 - min-width: min-content;
359 - max-width: 150px;
360 - }
429 +.settings-advanced-body {
430 + margin-top: 6px;
431 + padding-top: 6px;
432 }
433
363 -/* Section Styles */
364 -.section {
365 - margin-bottom: 2rem;
366 - padding: 1rem;
367 - padding-bottom: 0;
368 - border: 1px solid var(--color-border);
369 - border-radius: 0.5rem;
370 - overflow-x: visible;
371 - width: 100%;
372 - min-width: min-content;
373 - display: block;
374 - box-sizing: border-box;
434 +.settings-inline-button {
435 + flex: 0 0 auto;
436 }
437
377 -.section-title {
378 - font-size: 1.25rem;
379 - font-weight: bold;
438 +.settings-subsection-title {
439 + margin: 18px 0 4px;
440 color: var(--color-primary);
381 - margin-bottom: 0.5rem;
441 + font-size: 0.94rem;
442 + font-weight: 750;
443 }
444
384 -.section-description {
445 +.settings-subsection-description {
446 + margin: 0 0 8px;
447 color: var(--color-text);
386 - margin-bottom: 1rem;
448 + font-size: 0.85rem;
449 + line-height: 1.35;
450 + opacity: 0.72;
451 +}
452 +
453 +.settings-update-card {
454 + display: grid;
455 + grid-template-columns: auto minmax(0, 1fr) auto;
456 + align-items: center;
457 + gap: 14px;
458 + margin: 4px 0 16px;
459 + padding: 14px;
460 + border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
461 + border-radius: 7px;
462 + background: color-mix(in srgb, var(--color-panel) 60%, transparent);
463 +}
464 +
465 +.settings-update-card-attention {
466 + border-color: color-mix(in srgb, #f2bf4b 44%, var(--color-border));
467 + background:
468 + linear-gradient(
469 + 120deg,
470 + color-mix(in srgb, #f2bf4b 16%, transparent),
471 + transparent 58%
472 + ),
473 + color-mix(in srgb, var(--color-panel) 72%, transparent);
474 +}
475 +
476 +.settings-update-card-icon {
477 + display: grid;
478 + place-items: center;
479 + width: 38px;
480 + height: 38px;
481 + border-radius: 7px;
482 + background: color-mix(in srgb, var(--color-primary) 15%, transparent);
483 + color: color-mix(in srgb, var(--color-text) 82%, var(--color-primary));
484 }
485
389 -/* Scrollbar styling for sections */
390 -.section::-webkit-scrollbar {
391 - height: 10px;
392 - background-color: rgba(0, 0, 0, 0.1);
486 +.settings-update-card-attention .settings-update-card-icon {
487 + background: color-mix(in srgb, #f2bf4b 18%, transparent);
488 + color: #f2bf4b;
489 }
490
395 -.section::-webkit-scrollbar-thumb {
491 +.settings-update-card-copy {
492 + min-width: 0;
493 +}
494 +
495 +.settings-update-card-title {
496 + margin-bottom: 4px;
497 + color: color-mix(in srgb, var(--color-text) 88%, var(--color-primary));
498 + font-size: 0.96rem;
499 + font-weight: 800;
500 +}
501 +
502 +.settings-update-card-attention .settings-update-card-title {
503 + color: #f2bf4b;
504 +}
505 +
506 +.settings-update-card-message {
507 + color: var(--color-text);
508 + font-size: 0.86rem;
509 + line-height: 1.35;
510 + opacity: 0.78;
511 +}
512 +
513 +.settings-update-card-meta {
514 + display: flex;
515 + align-items: center;
516 + flex-wrap: wrap;
517 + gap: 6px;
518 + margin-top: 10px;
519 +}
520 +
521 +.settings-update-card-actions {
522 + display: flex;
523 + align-items: center;
524 + gap: 8px;
525 +}
526 +
527 +.settings-update-card-badge {
528 + display: inline-flex;
529 + align-items: center;
530 + min-height: 24px;
531 + padding: 0 9px;
532 + border-radius: 999px;
533 + border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
534 + color: var(--color-text);
535 + background: color-mix(in srgb, var(--color-background) 70%, transparent);
536 + font-size: 0.7rem;
537 + font-weight: 800;
538 + line-height: 1;
539 + text-transform: uppercase;
540 + white-space: nowrap;
541 +}
542 +
543 +.settings-update-card-attention .settings-update-card-badge {
544 + border-color: color-mix(in srgb, #f2bf4b 48%, var(--color-border));
545 + color: color-mix(in srgb, #f2bf4b 84%, var(--color-text));
546 + background: color-mix(in srgb, #f2bf4b 12%, transparent);
547 +}
548 +
549 +.settings-pane::-webkit-scrollbar {
550 + width: 6px;
551 + height: 6px;
552 +}
553 +
554 +.settings-pane::-webkit-scrollbar-track {
555 + background: transparent;
556 + margin: 8px 0;
557 +}
558 +
559 +.settings-pane::-webkit-scrollbar-thumb {
560 + border-radius: 6px;
561 + background-color: rgba(155, 155, 155, 0.5);
562 +}
563 +
564 +.settings-pane::-webkit-scrollbar-thumb:hover {
565 background-color: rgba(155, 155, 155, 0.7);
397 - border-radius: 5px;
566 }
567
400 -.section::-webkit-scrollbar-thumb:hover {
401 - background-color: rgba(155, 155, 155, 0.9);
568 +/* Responsive Design */
569 +@media (max-width: 768px) {
570 + .modal-inner.settings-modal .field-control {
571 + width: 100%;
572 + }
573 +
574 + .modal-inner.settings-modal .field-description {
575 + padding-bottom: var(--spacing-sm);
576 + }
577 +
578 + .modal-inner.settings-modal .field {
579 + display: block;
580 + align-items: center;
581 + padding: 12px 0;
582 + }
583 }
584
404 -@media (max-width: 1280px) {
405 - .section::-webkit-scrollbar {
406 - height: 10px;
407 - background-color: rgba(0, 0, 0, 0.1);
585 +@media (max-width: 720px) {
586 + .modal-inner.settings-modal {
587 + width: calc(100vw - 16px);
588 + height: calc(100vh - 16px);
589 + max-height: calc(100vh - 16px);
590 + }
591 +
592 + .settings-content {
593 + grid-template-columns: 1fr;
594 + grid-template-rows: auto minmax(0, 1fr);
595 + }
596 +
597 + .settings-tabs-container {
598 + position: sticky;
599 + top: 0;
600 + z-index: 3;
601 + height: auto;
602 + padding: 7px 8px;
603 + overflow-x: auto;
604 + border-right: 0;
605 + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
606 + }
607 +
608 + .settings-tabs {
609 + flex-direction: row;
610 + width: max-content;
611 + min-width: 100%;
612 + overflow-y: hidden;
613 }
614
410 - .section::-webkit-scrollbar-thumb {
411 - background-color: rgba(155, 155, 155, 0.7);
412 - border-radius: 5px;
615 + .settings-tab {
616 + width: auto;
617 + min-width: max-content;
618 + padding: 0 9px;
619 }
620
415 - .section::-webkit-scrollbar-thumb:hover {
416 - background-color: rgba(155, 155, 155, 0.9);
621 + .settings-pane {
622 + padding: 14px 16px 24px;
623 + }
624 +
625 + .settings-update-card {
626 + grid-template-columns: auto minmax(0, 1fr);
627 + }
628 +
629 + .settings-update-card-actions {
630 + grid-column: 1 / -1;
631 + justify-content: flex-start;
632 }
633 }
webui/js/modals.js
+2 -2
@@ -49,7 +49,7 @@ function restoreModalScrollSnapshot(modal) {
49 const backdrop = document.createElement("div");
50 backdrop.className = "modal-backdrop";
51 backdrop.style.display = "none";
52 -backdrop.style.backdropFilter = "blur(5px)";
52 +backdrop.style.backdropFilter = "blur(8px) saturate(112%)";
53 document.body.appendChild(backdrop);
54
55 function modalSuppressesBackdrop(modal) {
@@ -86,7 +86,7 @@ function updateModalZIndexes() {
86 }
87
88 backdrop.style.display = "block";
89 - backdrop.style.backdropFilter = "blur(5px)";
89 + backdrop.style.backdropFilter = "blur(8px) saturate(112%)";
90 backdrop.style.backgroundColor = "";
91
92 if (backdropModalStack.length === modalStack.length && modalStack.length > 1) {
webui/public/space-agent-icon-512.webp
Binary files /dev/null and b/webui/public/space-agent-icon-512.webp differ