Make browser annotation tray draggable
Fix annotation panel stacking so draft popovers render above the annotations recap.\n\nAllow the annotations recap tray to float within the browser stage by dragging its header, with bounded positioning and cleanup when annotations are cleared or the browser surface unmounts.
Alessandro committed
May 2, 2026 at 16:55 UTC
39a96012f998a5222763d69c4d48be0896705dbf
3 files changed
+117
-3
plugins/_browser/webui/browser-panel.html
+20
-3
@@ -220,9 +220,15 @@
220
</template>
221
<div class="browser-annotation-tray"
222
x-show="$store.browserPage.visibleAnnotations().length"
223
+ :class="{ 'is-floating': $store.browserPage.annotationTrayPosition, 'is-dragging': $store.browserPage.annotationTrayDragging }"
224
+ :style="$store.browserPage.annotationTrayStyle()"
225
x-transition style="display: none;"
224
- @click.stop @pointerdown.stop @keydown.stop>
225
- <div class="browser-annotation-tray-header">
226
+ @click.stop @pointerdown.stop @keydown.stop
227
+ @pointermove.window="$store.browserPage.moveAnnotationTrayDrag($event)"
228
+ @pointerup.window="$store.browserPage.finishAnnotationTrayDrag($event)"
229
+ @pointercancel.window="$store.browserPage.finishAnnotationTrayDrag($event)">
230
+ <div class="browser-annotation-tray-header"
231
+ @pointerdown.stop="$store.browserPage.startAnnotationTrayDrag($event)">
232
<span>Annotations</span>
233
<button type="button" class="browser-annotation-clear" title="Clear annotations"
234
aria-label="Clear annotations" @click="$store.browserPage.clearVisibleAnnotations()">
@@ -990,7 +996,6 @@
996
.browser-annotation-popover,
997
.browser-annotation-tray {
998
position: absolute;
993
- z-index: 18;
999
border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
1000
border-radius: 7px;
1001
background: color-mix(in srgb, var(--color-background) 96%, #000 4%);
@@ -999,6 +1004,7 @@
1004
}
1005
1006
.browser-annotation-popover {
1007
+ z-index: 24;
1008
display: flex;
1009
flex-direction: column;
1010
gap: 8px;
@@ -1044,6 +1050,7 @@
1050
}
1051
1052
.browser-annotation-tray {
1053
+ z-index: 20;
1054
right: 10px;
1055
bottom: 10px;
1056
display: flex;
@@ -1054,8 +1061,18 @@
1061
padding: 10px;
1062
}
1063
1064
+ .browser-annotation-tray.is-dragging {
1065
+ user-select: none;
1066
+ }
1067
+
1068
.browser-annotation-tray-header {
1069
justify-content: space-between;
1070
+ cursor: grab;
1071
+ user-select: none;
1072
+ }
1073
+
1074
+ .browser-annotation-tray.is-dragging .browser-annotation-tray-header {
1075
+ cursor: grabbing;
1076
}
1077
1078
.browser-annotation-clear,
plugins/_browser/webui/browser-store.js
+93
@@ -23,6 +23,7 @@ const FRAME_REJECT_SYNC_COOLDOWN_MS = 600;
23
const ANNOTATION_DRAG_THRESHOLD = 6;
24
const ANNOTATION_MAX_COMMENTS = 24;
25
const ANNOTATION_DOM_LIMIT = 1200;
26
+const ANNOTATION_TRAY_MARGIN = 10;
27
28
function makeViewerToken() {
29
return globalThis.crypto?.randomUUID?.()
@@ -106,6 +107,8 @@ const model = {
107
annotationDragRect: null,
108
annotationBusy: false,
109
annotationError: "",
110
+ annotationTrayPosition: null,
111
+ annotationTrayDragging: false,
112
connected: false,
113
switchingBrowserId: null,
114
commandInFlight: false,
@@ -127,6 +130,7 @@ const model = {
130
_lastViewportKey: "",
131
_lastViewport: null,
132
_annotationPointer: null,
133
+ _annotationTrayDrag: null,
134
_annotationSequence: 0,
135
_mode: "",
136
_surfaceMounted: false,
@@ -1588,8 +1592,93 @@ const model = {
1592
return this.visibleAnnotations().length + 1;
1593
},
1594
1595
+ annotationTrayStyle() {
1596
+ if (!this.annotationTrayPosition) return {};
1597
+ const position = this.clampAnnotationTrayPosition(this.annotationTrayPosition);
1598
+ return {
1599
+ left: `${position.x}px`,
1600
+ top: `${position.y}px`,
1601
+ right: "auto",
1602
+ bottom: "auto",
1603
+ };
1604
+ },
1605
+
1606
+ clampAnnotationTrayPosition(position = {}) {
1607
+ const stageRect = this._stageElement?.getBoundingClientRect?.();
1608
+ const stageWidth = Math.max(1, Number(stageRect?.width || 0));
1609
+ const stageHeight = Math.max(1, Number(stageRect?.height || 0));
1610
+ const width = Math.max(180, Number(position.width || 0));
1611
+ const height = Math.max(90, Number(position.height || 0));
1612
+ const maxX = Math.max(ANNOTATION_TRAY_MARGIN, stageWidth - width - ANNOTATION_TRAY_MARGIN);
1613
+ const maxY = Math.max(ANNOTATION_TRAY_MARGIN, stageHeight - height - ANNOTATION_TRAY_MARGIN);
1614
+ return {
1615
+ x: Math.min(Math.max(ANNOTATION_TRAY_MARGIN, Number(position.x || 0)), maxX),
1616
+ y: Math.min(Math.max(ANNOTATION_TRAY_MARGIN, Number(position.y || 0)), maxY),
1617
+ width,
1618
+ height,
1619
+ };
1620
+ },
1621
+
1622
+ startAnnotationTrayDrag(event) {
1623
+ if (event.button !== 0) return;
1624
+ if (event.target?.closest?.("button, input, select, textarea, a")) return;
1625
+ const tray = event.currentTarget?.closest?.(".browser-annotation-tray");
1626
+ const stageRect = this._stageElement?.getBoundingClientRect?.();
1627
+ const trayRect = tray?.getBoundingClientRect?.();
1628
+ if (!tray || !stageRect || !trayRect) return;
1629
+
1630
+ const position = this.clampAnnotationTrayPosition({
1631
+ x: trayRect.left - stageRect.left,
1632
+ y: trayRect.top - stageRect.top,
1633
+ width: trayRect.width,
1634
+ height: trayRect.height,
1635
+ });
1636
+ this.annotationTrayPosition = position;
1637
+ this.annotationTrayDragging = true;
1638
+ this._annotationTrayDrag = {
1639
+ id: event.pointerId,
1640
+ target: event.currentTarget,
1641
+ x: event.clientX,
1642
+ y: event.clientY,
1643
+ startX: position.x,
1644
+ startY: position.y,
1645
+ width: position.width,
1646
+ height: position.height,
1647
+ };
1648
+ event.currentTarget?.setPointerCapture?.(event.pointerId);
1649
+ event.preventDefault();
1650
+ },
1651
+
1652
+ moveAnnotationTrayDrag(event) {
1653
+ const drag = this._annotationTrayDrag;
1654
+ if (!drag || event.pointerId !== drag.id) return;
1655
+ this.annotationTrayPosition = this.clampAnnotationTrayPosition({
1656
+ x: drag.startX + event.clientX - drag.x,
1657
+ y: drag.startY + event.clientY - drag.y,
1658
+ width: drag.width,
1659
+ height: drag.height,
1660
+ });
1661
+ event.preventDefault();
1662
+ },
1663
+
1664
+ finishAnnotationTrayDrag(event = null) {
1665
+ const drag = this._annotationTrayDrag;
1666
+ if (!drag || (event?.pointerId && event.pointerId !== drag.id)) return;
1667
+ try {
1668
+ drag.target?.releasePointerCapture?.(drag.id);
1669
+ } catch {}
1670
+ this._annotationTrayDrag = null;
1671
+ this.annotationTrayDragging = false;
1672
+ },
1673
+
1674
+ resetAnnotationTrayPosition() {
1675
+ this.finishAnnotationTrayDrag();
1676
+ this.annotationTrayPosition = null;
1677
+ },
1678
+
1679
clearVisibleAnnotations() {
1680
this.clearAnnotationsForBrowser(this.activeBrowserId, this.activeAnnotationUrl(), this.activeBrowserContextId);
1681
+ this.resetAnnotationTrayPosition();
1682
},
1683
1684
clearAnnotationsForBrowser(browserId, url = null, contextId = "") {
@@ -1821,6 +1910,9 @@ const model = {
1910
1911
removeAnnotationComment(annotationId) {
1912
this.annotationComments = this.annotationComments.filter((annotation) => annotation.id !== annotationId);
1913
+ if (!this.visibleAnnotations().length) {
1914
+ this.resetAnnotationTrayPosition();
1915
+ }
1916
},
1917
1918
annotationChipLabel(annotation) {
@@ -2114,6 +2206,7 @@ const model = {
2206
this.annotationError = "";
2207
this.cancelAnnotationDraft();
2208
this.cancelAnnotationSelection();
2209
+ this.resetAnnotationTrayPosition();
2210
if (this.contextId) {
2211
try {
2212
await websocket.emit("browser_viewer_unsubscribe", { context_id: this.contextId });
tests/test_browser_agent_regressions.py
+4
@@ -788,11 +788,15 @@ def test_browser_annotate_mode_ui_and_prompt_hooks():
788
assert "Annotating" in panel_html
789
assert "browser-annotation-layer" in panel_html
790
assert "browser-annotation-tray" in panel_html
791
+ assert "annotationTrayStyle()" in panel_html
792
+ assert "startAnnotationTrayDrag($event)" in panel_html
793
assert "Draft to chat" in panel_html
794
assert "Send now" in panel_html
795
assert "@pointerdown.stop.prevent=\"$store.browserPage.startAnnotationSelection($event)\"" in panel_html
796
assert "@keydown.window=\"$store.browserPage.handleKeydown($event)\"" in panel_html
797
assert "annotationComments: []" in browser_store
798
+ assert "annotationTrayPosition: null" in browser_store
799
+ assert "clampAnnotationTrayPosition" in browser_store
800
assert '"browser_viewer_annotation"' in browser_store
801
assert 'event?.key === "." && (event.metaKey || event.ctrlKey)' in browser_store
802
assert "Browser annotations" in browser_store