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