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.
This commit is contained in:
Alessandro 2026-06-23 14:17:13 +02:00
parent 0028473ec9
commit 6ccd48e81f
3 changed files with 139 additions and 126 deletions

View file

@ -19,7 +19,7 @@
- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility across regular host browsers and Electron WebContentsView embedding.
- Keep the WebUI Browser inside its own modal/canvas affordance; do not replace it with page-level navigation.
- Default the visible WebUI Browser to live CDP screencast for responsiveness. Keep lightweight CDP/DOM state snapshots as the fallback transport.
- Keep narrow WebUI Browser controls usable by placing navigation buttons above a full-width address bar.
- Keep narrow WebUI Browser controls usable by grouping navigation with Annotate/settings above a full-width address bar.
- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
- Do not hardcode user-specific browser paths or secrets.

View file

@ -42,118 +42,6 @@
</button>
</div>
<div class="browser-session-controls">
<button type="button" class="btn browser-annotate-toggle" title="Annotate" aria-label="Annotate"
:aria-pressed="$store.browserPage.annotating.toString()"
:class="{ 'is-active': $store.browserPage.annotating }"
:disabled="!$store.browserPage.canAnnotate() && !$store.browserPage.annotating"
@click="$store.browserPage.toggleAnnotationMode()">
<span class="material-symbols-outlined" aria-hidden="true"
x-text="$store.browserPage.annotating ? 'rate_review' : 'edit_note'"></span>
<span x-text="$store.browserPage.annotating ? 'Annotating' : 'Annotate'"></span>
</button>
<div class="browser-extension-menu" @click.outside="$store.browserPage.closeExtensionsMenu()"
@keydown.escape.window="$store.browserPage.closeExtensionsMenu()">
<button type="button" class="btn btn-icon-action browser-extensions" title="Browser settings"
aria-label="Browser settings" @click.stop="$store.browserPage.toggleExtensionsMenu()"
:aria-expanded="$store.browserPage.extensionMenuOpen.toString()"
:class="{ 'is-active': $store.browserPage.status?.extensions?.active }">
<span class="material-symbols-outlined">tune</span>
</button>
<div class="browser-extension-dropdown" x-show="$store.browserPage.extensionMenuOpen" x-transition
style="display: none;">
<div class="browser-extension-preset">
<label for="browser-model-preset">Browser LLM Preset</label>
<select id="browser-model-preset" x-model="$store.browserPage.modelPreset"
@change="$store.browserPage.setBrowserModelPreset($event.target.value)"
:disabled="$store.browserPage.modelPresetSaving">
<option value="">Default Main Model</option>
<template x-for="preset in $store.browserPage.modelPresetOptions" :key="preset.name">
<option :value="preset.name" x-text="preset.label"></option>
</template>
</select>
<div class="browser-extension-preset-summary" x-text="$store.browserPage.modelPresetSummary()">
</div>
</div>
<div class="browser-extension-section-title">Chrome Extensions</div>
<button type="button" class="dropdown-item" @click="$store.browserPage.createExtensionWithAgent()">
<span class="material-symbols-outlined">add_circle</span>
<span>Create New Extension with A0</span>
</button>
<div class="browser-extension-url">
<label for="browser-extension-url">Input a Chrome Web Store URL</label>
<input id="browser-extension-url" type="url" x-model="$store.browserPage.extensionInstallUrl"
@keydown.enter.prevent="$store.browserPage.installExtensionFromUrl()"
placeholder="https://chromewebstore.google.com/detail/..." />
<div class="browser-extension-url-actions">
<button type="button" class="btn btn-ok" @click="$store.browserPage.installExtensionFromUrl()"
:disabled="$store.browserPage.extensionActionLoading">
<span class="material-symbols-outlined"
:class="{ spinning: $store.browserPage.extensionActionLoading }"
x-text="$store.browserPage.extensionActionLoading ? 'progress_activity' : 'download'"></span>
<span>Install URL</span>
</button>
<button type="button" class="btn btn-field"
@click="$store.browserPage.askAgentInstallExtension()">
<span class="material-symbols-outlined">psychology_alt</span>
<span x-text="$store.browserPage.extensionAssistantActionLabel()">Scan with A0</span>
</button>
</div>
<div class="browser-extension-warning" x-show="$store.browserPage.hasExtensionInstallUrl()"
x-transition style="display: none;">
<span class="material-symbols-outlined">warning</span>
<span>
Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
damage that environment. Review what you install.
</span>
</div>
</div>
<div class="browser-extension-list"
x-show="$store.browserPage.extensionsListLoading || $store.browserPage.extensionsList.length"
style="display: none;">
<div class="browser-extension-list-header">
<span>Installed extensions</span>
<span class="material-symbols-outlined spinning"
x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
</div>
<template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
<div class="browser-extension-row" :title="extension.path">
<span class="browser-extension-row-text">
<span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
<span class="browser-extension-meta"
x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
</span>
<span class="browser-extension-row-actions">
<button type="button" class="browser-extension-open"
x-show="$store.browserPage.extensionHasOpenUi(extension)"
:title="$store.browserPage.extensionOpenTitle(extension)"
:aria-label="$store.browserPage.extensionOpenTitle(extension)"
:disabled="!extension.enabled || $store.browserPage.isBusy()"
@click.stop="$store.browserPage.openExtensionUi(extension)">
<span class="material-symbols-outlined">open_in_new</span>
<span>Open</span>
</button>
<label class="browser-extension-toggle">
<input type="checkbox" :checked="extension.enabled"
:disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
@change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
<span class="browser-extension-switch"></span>
</label>
</span>
</div>
</template>
</div>
<button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
<span class="material-symbols-outlined">tune</span>
<span>Settings</span>
</button>
<div class="browser-extension-message" x-show="$store.browserPage.extensionActionMessage"
x-text="$store.browserPage.extensionActionMessage"></div>
<div class="browser-extension-error" x-show="$store.browserPage.extensionActionError"
x-text="$store.browserPage.extensionActionError"></div>
</div>
</div>
</div>
</div>
</div>
@ -173,6 +61,119 @@
</button>
</div>
<div class="browser-session-controls">
<button type="button" class="btn browser-annotate-toggle" title="Annotate" aria-label="Annotate"
:aria-pressed="$store.browserPage.annotating.toString()"
:class="{ 'is-active': $store.browserPage.annotating }"
:disabled="!$store.browserPage.canAnnotate() && !$store.browserPage.annotating"
@click="$store.browserPage.toggleAnnotationMode()">
<span class="material-symbols-outlined" aria-hidden="true"
x-text="$store.browserPage.annotating ? 'rate_review' : 'edit_note'"></span>
<span x-text="$store.browserPage.annotating ? 'Annotating' : 'Annotate'"></span>
</button>
<div class="browser-extension-menu" @click.outside="$store.browserPage.closeExtensionsMenu()"
@keydown.escape.window="$store.browserPage.closeExtensionsMenu()">
<button type="button" class="btn btn-icon-action browser-extensions" title="Browser settings"
aria-label="Browser settings" @click.stop="$store.browserPage.toggleExtensionsMenu()"
:aria-expanded="$store.browserPage.extensionMenuOpen.toString()"
:class="{ 'is-active': $store.browserPage.status?.extensions?.active }">
<span class="material-symbols-outlined">tune</span>
</button>
<div class="browser-extension-dropdown" x-show="$store.browserPage.extensionMenuOpen" x-transition
style="display: none;">
<div class="browser-extension-preset">
<label for="browser-model-preset">Browser LLM Preset</label>
<select id="browser-model-preset" x-model="$store.browserPage.modelPreset"
@change="$store.browserPage.setBrowserModelPreset($event.target.value)"
:disabled="$store.browserPage.modelPresetSaving">
<option value="">Default Main Model</option>
<template x-for="preset in $store.browserPage.modelPresetOptions" :key="preset.name">
<option :value="preset.name" x-text="preset.label"></option>
</template>
</select>
<div class="browser-extension-preset-summary" x-text="$store.browserPage.modelPresetSummary()">
</div>
</div>
<div class="browser-extension-section-title">Chrome Extensions</div>
<button type="button" class="dropdown-item" @click="$store.browserPage.createExtensionWithAgent()">
<span class="material-symbols-outlined">add_circle</span>
<span>Create New Extension with A0</span>
</button>
<div class="browser-extension-url">
<label for="browser-extension-url">Input a Chrome Web Store URL</label>
<input id="browser-extension-url" type="url" x-model="$store.browserPage.extensionInstallUrl"
@keydown.enter.prevent="$store.browserPage.installExtensionFromUrl()"
placeholder="https://chromewebstore.google.com/detail/..." />
<div class="browser-extension-url-actions">
<button type="button" class="btn btn-ok" @click="$store.browserPage.installExtensionFromUrl()"
:disabled="$store.browserPage.extensionActionLoading">
<span class="material-symbols-outlined"
:class="{ spinning: $store.browserPage.extensionActionLoading }"
x-text="$store.browserPage.extensionActionLoading ? 'progress_activity' : 'download'"></span>
<span>Install URL</span>
</button>
<button type="button" class="btn btn-field"
@click="$store.browserPage.askAgentInstallExtension()">
<span class="material-symbols-outlined">psychology_alt</span>
<span x-text="$store.browserPage.extensionAssistantActionLabel()">Scan with A0</span>
</button>
</div>
<div class="browser-extension-warning" x-show="$store.browserPage.hasExtensionInstallUrl()"
x-transition style="display: none;">
<span class="material-symbols-outlined">warning</span>
<span>
Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
damage that environment. Review what you install.
</span>
</div>
</div>
<div class="browser-extension-list"
x-show="$store.browserPage.extensionsListLoading || $store.browserPage.extensionsList.length"
style="display: none;">
<div class="browser-extension-list-header">
<span>Installed extensions</span>
<span class="material-symbols-outlined spinning"
x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
</div>
<template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
<div class="browser-extension-row" :title="extension.path">
<span class="browser-extension-row-text">
<span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
<span class="browser-extension-meta"
x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
</span>
<span class="browser-extension-row-actions">
<button type="button" class="browser-extension-open"
x-show="$store.browserPage.extensionHasOpenUi(extension)"
:title="$store.browserPage.extensionOpenTitle(extension)"
:aria-label="$store.browserPage.extensionOpenTitle(extension)"
:disabled="!extension.enabled || $store.browserPage.isBusy()"
@click.stop="$store.browserPage.openExtensionUi(extension)">
<span class="material-symbols-outlined">open_in_new</span>
<span>Open</span>
</button>
<label class="browser-extension-toggle">
<input type="checkbox" :checked="extension.enabled"
:disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
@change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
<span class="browser-extension-switch"></span>
</label>
</span>
</div>
</template>
</div>
<button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
<span class="material-symbols-outlined">tune</span>
<span>Settings</span>
</button>
<div class="browser-extension-message" x-show="$store.browserPage.extensionActionMessage"
x-text="$store.browserPage.extensionActionMessage"></div>
<div class="browser-extension-error" x-show="$store.browserPage.extensionActionError"
x-text="$store.browserPage.extensionActionError"></div>
</div>
</div>
</div>
<form class="browser-address-form" aria-label="Browser navigation" @submit.prevent="$store.browserPage.go()">
<span class="material-symbols-outlined browser-address-icon">language</span>
<input class="browser-address" aria-label="Browser address" name="browser_address" x-model="$store.browserPage.address"
@ -384,6 +385,7 @@
--browser-tab-close-size: 32px;
--browser-control-radius: 0.55rem;
box-sizing: border-box;
container-type: inline-size;
display: flex;
flex: 1 1 auto;
flex-direction: column;
@ -395,8 +397,8 @@
.browser-toolbar {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-areas: "nav address";
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas: "nav address controls";
gap: 8px;
align-items: center;
padding: 8px 10px 9px;
@ -475,7 +477,7 @@
.browser-meta-top {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
align-items: end;
}
@ -647,11 +649,12 @@
}
.browser-session-controls {
grid-area: controls;
display: flex;
align-items: end;
align-items: center;
justify-content: flex-end;
gap: 6px;
min-width: 0;
padding-bottom: 7px;
}
.browser-annotate-toggle {
@ -1279,11 +1282,11 @@
@container (max-width: 460px) {
.browser-toolbar {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas:
"nav"
"address";
align-items: stretch;
"nav . controls"
"address address address";
align-items: center;
gap: 7px;
}
@ -1300,7 +1303,7 @@
}
.browser-session-controls {
width: 100%;
width: auto;
justify-content: flex-end;
}
@ -1332,6 +1335,7 @@
.browser-extension-dropdown {
right: 0;
left: auto;
top: calc(var(--browser-control-size) + var(--browser-address-height) + 13px);
width: min(296px, calc(100vw - 72px));
}