Group mobile browser toolbar controls
Move Annotate and Browser settings into the Browser toolbar so narrow views keep navigation and controls on one row above a full-width address bar. Make the Browser panel itself the container-query boundary so mobile modals and narrow right-canvas panels share the same polished toolbar layout, with regression and DOX coverage.
Alessandro committed
Jun 23, 2026 at 14:17 UTC
6ccd48e81f4efdff30fbc3be55ddd18fec13164a
3 files changed
+139
-126
plugins/_browser/AGENTS.md
+1
-1
@@ -19,7 +19,7 @@
19
- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility across regular host browsers and Electron WebContentsView embedding.
20
- Keep the WebUI Browser inside its own modal/canvas affordance; do not replace it with page-level navigation.
21
- Default the visible WebUI Browser to live CDP screencast for responsiveness. Keep lightweight CDP/DOM state snapshots as the fallback transport.
22
-- Keep narrow WebUI Browser controls usable by placing navigation buttons above a full-width address bar.
22
+- Keep narrow WebUI Browser controls usable by grouping navigation with Annotate/settings above a full-width address bar.
23
- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
24
- Do not hardcode user-specific browser paths or secrets.
25
plugins/_browser/webui/browser-panel.html
+126
-122
@@ -42,118 +42,6 @@
42
</button>
43
</div>
44
45
- <div class="browser-session-controls">
46
- <button type="button" class="btn browser-annotate-toggle" title="Annotate" aria-label="Annotate"
47
- :aria-pressed="$store.browserPage.annotating.toString()"
48
- :class="{ 'is-active': $store.browserPage.annotating }"
49
- :disabled="!$store.browserPage.canAnnotate() && !$store.browserPage.annotating"
50
- @click="$store.browserPage.toggleAnnotationMode()">
51
- <span class="material-symbols-outlined" aria-hidden="true"
52
- x-text="$store.browserPage.annotating ? 'rate_review' : 'edit_note'"></span>
53
- <span x-text="$store.browserPage.annotating ? 'Annotating' : 'Annotate'"></span>
54
- </button>
55
- <div class="browser-extension-menu" @click.outside="$store.browserPage.closeExtensionsMenu()"
56
- @keydown.escape.window="$store.browserPage.closeExtensionsMenu()">
57
- <button type="button" class="btn btn-icon-action browser-extensions" title="Browser settings"
58
- aria-label="Browser settings" @click.stop="$store.browserPage.toggleExtensionsMenu()"
59
- :aria-expanded="$store.browserPage.extensionMenuOpen.toString()"
60
- :class="{ 'is-active': $store.browserPage.status?.extensions?.active }">
61
- <span class="material-symbols-outlined">tune</span>
62
- </button>
63
- <div class="browser-extension-dropdown" x-show="$store.browserPage.extensionMenuOpen" x-transition
64
- style="display: none;">
65
- <div class="browser-extension-preset">
66
- <label for="browser-model-preset">Browser LLM Preset</label>
67
- <select id="browser-model-preset" x-model="$store.browserPage.modelPreset"
68
- @change="$store.browserPage.setBrowserModelPreset($event.target.value)"
69
- :disabled="$store.browserPage.modelPresetSaving">
70
- <option value="">Default Main Model</option>
71
- <template x-for="preset in $store.browserPage.modelPresetOptions" :key="preset.name">
72
- <option :value="preset.name" x-text="preset.label"></option>
73
- </template>
74
- </select>
75
- <div class="browser-extension-preset-summary" x-text="$store.browserPage.modelPresetSummary()">
76
- </div>
77
- </div>
78
- <div class="browser-extension-section-title">Chrome Extensions</div>
79
- <button type="button" class="dropdown-item" @click="$store.browserPage.createExtensionWithAgent()">
80
- <span class="material-symbols-outlined">add_circle</span>
81
- <span>Create New Extension with A0</span>
82
- </button>
83
- <div class="browser-extension-url">
84
- <label for="browser-extension-url">Input a Chrome Web Store URL</label>
85
- <input id="browser-extension-url" type="url" x-model="$store.browserPage.extensionInstallUrl"
86
- @keydown.enter.prevent="$store.browserPage.installExtensionFromUrl()"
87
- placeholder="https://chromewebstore.google.com/detail/..." />
88
- <div class="browser-extension-url-actions">
89
- <button type="button" class="btn btn-ok" @click="$store.browserPage.installExtensionFromUrl()"
90
- :disabled="$store.browserPage.extensionActionLoading">
91
- <span class="material-symbols-outlined"
92
- :class="{ spinning: $store.browserPage.extensionActionLoading }"
93
- x-text="$store.browserPage.extensionActionLoading ? 'progress_activity' : 'download'"></span>
94
- <span>Install URL</span>
95
- </button>
96
- <button type="button" class="btn btn-field"
97
- @click="$store.browserPage.askAgentInstallExtension()">
98
- <span class="material-symbols-outlined">psychology_alt</span>
99
- <span x-text="$store.browserPage.extensionAssistantActionLabel()">Scan with A0</span>
100
- </button>
101
- </div>
102
- <div class="browser-extension-warning" x-show="$store.browserPage.hasExtensionInstallUrl()"
103
- x-transition style="display: none;">
104
- <span class="material-symbols-outlined">warning</span>
105
- <span>
106
- Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
107
- damage that environment. Review what you install.
108
- </span>
109
- </div>
110
- </div>
111
- <div class="browser-extension-list"
112
- x-show="$store.browserPage.extensionsListLoading || $store.browserPage.extensionsList.length"
113
- style="display: none;">
114
- <div class="browser-extension-list-header">
115
- <span>Installed extensions</span>
116
- <span class="material-symbols-outlined spinning"
117
- x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
118
- </div>
119
- <template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
120
- <div class="browser-extension-row" :title="extension.path">
121
- <span class="browser-extension-row-text">
122
- <span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
123
- <span class="browser-extension-meta"
124
- x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
125
- </span>
126
- <span class="browser-extension-row-actions">
127
- <button type="button" class="browser-extension-open"
128
- x-show="$store.browserPage.extensionHasOpenUi(extension)"
129
- :title="$store.browserPage.extensionOpenTitle(extension)"
130
- :aria-label="$store.browserPage.extensionOpenTitle(extension)"
131
- :disabled="!extension.enabled || $store.browserPage.isBusy()"
132
- @click.stop="$store.browserPage.openExtensionUi(extension)">
133
- <span class="material-symbols-outlined">open_in_new</span>
134
- <span>Open</span>
135
- </button>
136
- <label class="browser-extension-toggle">
137
- <input type="checkbox" :checked="extension.enabled"
138
- :disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
139
- @change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
140
- <span class="browser-extension-switch"></span>
141
- </label>
142
- </span>
143
- </div>
144
- </template>
145
- </div>
146
- <button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
147
- <span class="material-symbols-outlined">tune</span>
148
- <span>Settings</span>
149
- </button>
150
- <div class="browser-extension-message" x-show="$store.browserPage.extensionActionMessage"
151
- x-text="$store.browserPage.extensionActionMessage"></div>
152
- <div class="browser-extension-error" x-show="$store.browserPage.extensionActionError"
153
- x-text="$store.browserPage.extensionActionError"></div>
154
- </div>
155
- </div>
156
- </div>
45
</div>
46
</div>
47
@@ -173,6 +61,119 @@
61
</button>
62
</div>
63
64
+ <div class="browser-session-controls">
65
+ <button type="button" class="btn browser-annotate-toggle" title="Annotate" aria-label="Annotate"
66
+ :aria-pressed="$store.browserPage.annotating.toString()"
67
+ :class="{ 'is-active': $store.browserPage.annotating }"
68
+ :disabled="!$store.browserPage.canAnnotate() && !$store.browserPage.annotating"
69
+ @click="$store.browserPage.toggleAnnotationMode()">
70
+ <span class="material-symbols-outlined" aria-hidden="true"
71
+ x-text="$store.browserPage.annotating ? 'rate_review' : 'edit_note'"></span>
72
+ <span x-text="$store.browserPage.annotating ? 'Annotating' : 'Annotate'"></span>
73
+ </button>
74
+ <div class="browser-extension-menu" @click.outside="$store.browserPage.closeExtensionsMenu()"
75
+ @keydown.escape.window="$store.browserPage.closeExtensionsMenu()">
76
+ <button type="button" class="btn btn-icon-action browser-extensions" title="Browser settings"
77
+ aria-label="Browser settings" @click.stop="$store.browserPage.toggleExtensionsMenu()"
78
+ :aria-expanded="$store.browserPage.extensionMenuOpen.toString()"
79
+ :class="{ 'is-active': $store.browserPage.status?.extensions?.active }">
80
+ <span class="material-symbols-outlined">tune</span>
81
+ </button>
82
+ <div class="browser-extension-dropdown" x-show="$store.browserPage.extensionMenuOpen" x-transition
83
+ style="display: none;">
84
+ <div class="browser-extension-preset">
85
+ <label for="browser-model-preset">Browser LLM Preset</label>
86
+ <select id="browser-model-preset" x-model="$store.browserPage.modelPreset"
87
+ @change="$store.browserPage.setBrowserModelPreset($event.target.value)"
88
+ :disabled="$store.browserPage.modelPresetSaving">
89
+ <option value="">Default Main Model</option>
90
+ <template x-for="preset in $store.browserPage.modelPresetOptions" :key="preset.name">
91
+ <option :value="preset.name" x-text="preset.label"></option>
92
+ </template>
93
+ </select>
94
+ <div class="browser-extension-preset-summary" x-text="$store.browserPage.modelPresetSummary()">
95
+ </div>
96
+ </div>
97
+ <div class="browser-extension-section-title">Chrome Extensions</div>
98
+ <button type="button" class="dropdown-item" @click="$store.browserPage.createExtensionWithAgent()">
99
+ <span class="material-symbols-outlined">add_circle</span>
100
+ <span>Create New Extension with A0</span>
101
+ </button>
102
+ <div class="browser-extension-url">
103
+ <label for="browser-extension-url">Input a Chrome Web Store URL</label>
104
+ <input id="browser-extension-url" type="url" x-model="$store.browserPage.extensionInstallUrl"
105
+ @keydown.enter.prevent="$store.browserPage.installExtensionFromUrl()"
106
+ placeholder="https://chromewebstore.google.com/detail/..." />
107
+ <div class="browser-extension-url-actions">
108
+ <button type="button" class="btn btn-ok" @click="$store.browserPage.installExtensionFromUrl()"
109
+ :disabled="$store.browserPage.extensionActionLoading">
110
+ <span class="material-symbols-outlined"
111
+ :class="{ spinning: $store.browserPage.extensionActionLoading }"
112
+ x-text="$store.browserPage.extensionActionLoading ? 'progress_activity' : 'download'"></span>
113
+ <span>Install URL</span>
114
+ </button>
115
+ <button type="button" class="btn btn-field"
116
+ @click="$store.browserPage.askAgentInstallExtension()">
117
+ <span class="material-symbols-outlined">psychology_alt</span>
118
+ <span x-text="$store.browserPage.extensionAssistantActionLabel()">Scan with A0</span>
119
+ </button>
120
+ </div>
121
+ <div class="browser-extension-warning" x-show="$store.browserPage.hasExtensionInstallUrl()"
122
+ x-transition style="display: none;">
123
+ <span class="material-symbols-outlined">warning</span>
124
+ <span>
125
+ Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
126
+ damage that environment. Review what you install.
127
+ </span>
128
+ </div>
129
+ </div>
130
+ <div class="browser-extension-list"
131
+ x-show="$store.browserPage.extensionsListLoading || $store.browserPage.extensionsList.length"
132
+ style="display: none;">
133
+ <div class="browser-extension-list-header">
134
+ <span>Installed extensions</span>
135
+ <span class="material-symbols-outlined spinning"
136
+ x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
137
+ </div>
138
+ <template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
139
+ <div class="browser-extension-row" :title="extension.path">
140
+ <span class="browser-extension-row-text">
141
+ <span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
142
+ <span class="browser-extension-meta"
143
+ x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
144
+ </span>
145
+ <span class="browser-extension-row-actions">
146
+ <button type="button" class="browser-extension-open"
147
+ x-show="$store.browserPage.extensionHasOpenUi(extension)"
148
+ :title="$store.browserPage.extensionOpenTitle(extension)"
149
+ :aria-label="$store.browserPage.extensionOpenTitle(extension)"
150
+ :disabled="!extension.enabled || $store.browserPage.isBusy()"
151
+ @click.stop="$store.browserPage.openExtensionUi(extension)">
152
+ <span class="material-symbols-outlined">open_in_new</span>
153
+ <span>Open</span>
154
+ </button>
155
+ <label class="browser-extension-toggle">
156
+ <input type="checkbox" :checked="extension.enabled"
157
+ :disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
158
+ @change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
159
+ <span class="browser-extension-switch"></span>
160
+ </label>
161
+ </span>
162
+ </div>
163
+ </template>
164
+ </div>
165
+ <button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
166
+ <span class="material-symbols-outlined">tune</span>
167
+ <span>Settings</span>
168
+ </button>
169
+ <div class="browser-extension-message" x-show="$store.browserPage.extensionActionMessage"
170
+ x-text="$store.browserPage.extensionActionMessage"></div>
171
+ <div class="browser-extension-error" x-show="$store.browserPage.extensionActionError"
172
+ x-text="$store.browserPage.extensionActionError"></div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
<form class="browser-address-form" aria-label="Browser navigation" @submit.prevent="$store.browserPage.go()">
178
<span class="material-symbols-outlined browser-address-icon">language</span>
179
<input class="browser-address" aria-label="Browser address" name="browser_address" x-model="$store.browserPage.address"
@@ -384,6 +385,7 @@
385
--browser-tab-close-size: 32px;
386
--browser-control-radius: 0.55rem;
387
box-sizing: border-box;
388
+ container-type: inline-size;
389
display: flex;
390
flex: 1 1 auto;
391
flex-direction: column;
@@ -395,8 +397,8 @@
397
398
.browser-toolbar {
399
display: grid;
398
- grid-template-columns: auto minmax(0, 1fr);
399
- grid-template-areas: "nav address";
400
+ grid-template-columns: auto minmax(0, 1fr) auto;
401
+ grid-template-areas: "nav address controls";
402
gap: 8px;
403
align-items: center;
404
padding: 8px 10px 9px;
@@ -475,7 +477,7 @@
477
478
.browser-meta-top {
479
display: grid;
478
- grid-template-columns: minmax(0, 1fr) auto;
480
+ grid-template-columns: minmax(0, 1fr);
481
gap: 10px;
482
align-items: end;
483
}
@@ -647,11 +649,12 @@
649
}
650
651
.browser-session-controls {
652
+ grid-area: controls;
653
display: flex;
651
- align-items: end;
654
+ align-items: center;
655
+ justify-content: flex-end;
656
gap: 6px;
657
min-width: 0;
654
- padding-bottom: 7px;
658
}
659
660
.browser-annotate-toggle {
@@ -1279,11 +1282,11 @@
1282
1283
@container (max-width: 460px) {
1284
.browser-toolbar {
1282
- grid-template-columns: minmax(0, 1fr);
1285
+ grid-template-columns: auto minmax(0, 1fr) auto;
1286
grid-template-areas:
1284
- "nav"
1285
- "address";
1286
- align-items: stretch;
1287
+ "nav . controls"
1288
+ "address address address";
1289
+ align-items: center;
1290
gap: 7px;
1291
}
1292
@@ -1300,7 +1303,7 @@
1303
}
1304
1305
.browser-session-controls {
1303
- width: 100%;
1306
+ width: auto;
1307
justify-content: flex-end;
1308
}
1309
@@ -1332,6 +1335,7 @@
1335
.browser-extension-dropdown {
1336
right: 0;
1337
left: auto;
1338
+ top: calc(var(--browser-control-size) + var(--browser-address-height) + 13px);
1339
width: min(296px, calc(100vw - 72px));
1340
}
1341
tests/test_browser_agent_regressions.py
+12
-3
@@ -1354,14 +1354,23 @@ def test_browser_panel_exposes_agent_friendly_address_input():
1354
assert 'class="browser-address" aria-label="Browser address" name="browser_address"' in panel
1355
1356
1357
-def test_browser_panel_gives_address_bar_full_mobile_row():
1357
+def test_browser_panel_groups_mobile_toolbar_controls_above_address():
1358
panel = (
1359
PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-panel.html"
1360
).read_text(encoding="utf-8")
1361
1362
+ toolbar_index = panel.index('<div class="browser-toolbar">')
1363
+ nav_index = panel.index('<div class="browser-navigation">', toolbar_index)
1364
+ controls_index = panel.index('<div class="browser-session-controls">', toolbar_index)
1365
+ address_index = panel.index('<form class="browser-address-form"', toolbar_index)
1366
+
1367
+ assert nav_index < controls_index < address_index
1368
+ assert ".browser-panel {\n --browser-chrome-surface:" in panel
1369
+ assert "container-type: inline-size;" in panel
1370
assert "@container (max-width: 460px)" in panel
1363
- assert ".browser-toolbar {\n grid-template-columns: minmax(0, 1fr);" in panel
1364
- assert ' "nav"\n "address";' in panel
1371
+ assert ".browser-toolbar {\n grid-template-columns: auto minmax(0, 1fr) auto;" in panel
1372
+ assert ' "nav . controls"\n "address address address";' in panel
1373
+ assert ".browser-session-controls {\n width: auto;" in panel
1374
assert ".browser-address-form {\n width: 100%;" in panel
1375
1376