agentation-vue 0.2.6 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +94 -0
  2. package/dist/AgentationVue.vue +270 -60
  3. package/dist/components/AgentationToolbar.d.vue.ts +2 -0
  4. package/dist/components/AgentationToolbar.vue +12 -3
  5. package/dist/components/AgentationToolbar.vue.d.ts +2 -0
  6. package/dist/components/AnnotationInput.d.vue.ts +2 -0
  7. package/dist/components/AnnotationInput.vue +88 -16
  8. package/dist/components/AnnotationInput.vue.d.ts +2 -0
  9. package/dist/components/AnnotationMarker.d.vue.ts +1 -0
  10. package/dist/components/AnnotationMarker.vue +12 -3
  11. package/dist/components/AnnotationMarker.vue.d.ts +1 -0
  12. package/dist/components/MentionDropdown.d.vue.ts +16 -0
  13. package/dist/components/MentionDropdown.vue +41 -0
  14. package/dist/components/MentionDropdown.vue.d.ts +16 -0
  15. package/dist/components/SettingsPanel.vue +26 -3
  16. package/dist/components/SettingsPopover.vue +3 -5
  17. package/dist/composables/useAnnotations.d.ts +2 -0
  18. package/dist/composables/useAnnotations.mjs +6 -1
  19. package/dist/composables/useKeyboardShortcuts.mjs +22 -8
  20. package/dist/composables/useMentionDropdown.d.ts +21 -0
  21. package/dist/composables/useMentionDropdown.mjs +162 -0
  22. package/dist/composables/useOutputFormatter.mjs +2 -1
  23. package/dist/composables/usePeekMode.d.ts +14 -0
  24. package/dist/composables/usePeekMode.mjs +119 -0
  25. package/dist/composables/useSettings.d.ts +1 -0
  26. package/dist/composables/useSettings.mjs +2 -1
  27. package/dist/constants.d.ts +2 -0
  28. package/dist/constants.mjs +3 -0
  29. package/dist/styles/agentation.css +1 -1
  30. package/dist/types.d.ts +1 -0
  31. package/dist/utils/mention.d.ts +13 -0
  32. package/dist/utils/mention.mjs +46 -0
  33. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # agentation-vue
2
+
3
+ [![npm version](https://img.shields.io/npm/v/agentation-vue)](https://www.npmjs.com/package/agentation-vue)
4
+ [![npm downloads](https://img.shields.io/npm/dm/agentation-vue)](https://www.npmjs.com/package/agentation-vue)
5
+
6
+ Visual feedback tool for AI coding agents. Drop a single component into your Vue app and let your AI agent see and annotate UI elements.
7
+
8
+ Compatible with **Vue 3.3+** and **Vue 2.7**.
9
+
10
+ > **Note:** This is an unofficial, community-maintained Vue adaptation of [Agentation](https://github.com/benjitaylor/agentation). It is not affiliated with or endorsed by the original project.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install agentation-vue
16
+ ```
17
+
18
+ ## Setup — Vue 3
19
+
20
+ ```ts
21
+ import { AgentationVuePlugin } from 'agentation-vue'
22
+ // main.ts
23
+ import { createApp } from 'vue'
24
+ import App from './App.vue'
25
+ import 'agentation-vue/style.css'
26
+
27
+ createApp(App)
28
+ .use(AgentationVuePlugin)
29
+ .mount('#app')
30
+ ```
31
+
32
+ Then place the component once in your root `App.vue`:
33
+
34
+ ```vue
35
+ <template>
36
+ <router-view />
37
+ <agentation-vue />
38
+ </template>
39
+ ```
40
+
41
+ ## Setup — Vue 2.7
42
+
43
+ ```ts
44
+ import { AgentationVuePlugin } from 'agentation-vue'
45
+ // main.ts
46
+ import Vue from 'vue'
47
+ import App from './App.vue'
48
+ import 'agentation-vue/style.css'
49
+
50
+ Vue.use(AgentationVuePlugin)
51
+
52
+ new Vue({
53
+ render: h => h(App),
54
+ }).$mount('#app')
55
+ ```
56
+
57
+ Then place the component once in your root `App.vue`:
58
+
59
+ ```vue
60
+ <template>
61
+ <div>
62
+ <router-view />
63
+ <agentation-vue />
64
+ </div>
65
+ </template>
66
+ ```
67
+
68
+ > Vue 2 templates require a single root element — wrap your content in a `<div>`.
69
+
70
+ ## Chrome extension
71
+
72
+ This repo also contains a Chrome extension for injecting Agentation without modifying the target app code.
73
+
74
+ ## Features
75
+
76
+ - **Click-to-annotate** — click any element to pin a numbered marker and leave a comment
77
+ - **Multi-select** — `Shift+drag` to rubber-band select multiple elements at once
78
+ - **Area select** — `Alt+drag` to annotate a screen region
79
+ - **Text selection** — highlight text to annotate it
80
+ - **Vue component tree** — automatically detects and reports the Vue component hierarchy for each annotated element
81
+ - **Markdown output** — copies all annotations as structured Markdown, ready to paste into an AI chat
82
+ - **Forensic mode** — captures bounding boxes, computed styles, CSS classes, and accessibility attributes
83
+ - **Themes** — light, dark, or auto (follows system preference)
84
+ - **Auto-hide launcher** — optionally tucks the collapsed floating button near screen edges and reveals it on approach
85
+ - **Session persistence** — annotations survive refreshes and are scoped per page URL via `sessionStorage`
86
+ - **Custom tooltip directive** — `v-va-tooltip` with optional keyboard shortcut badge
87
+
88
+ ## Acknowledgments
89
+
90
+ This project is inspired by and based on [Agentation](https://github.com/benjitaylor/agentation) by [Ben Taylor](https://github.com/benjitaylor). Thank you for the original concept and implementation.
91
+
92
+ ## License
93
+
94
+ [PolyForm Shield 1.0.0](./LICENSE) — Copyright (c) 2026 Dorian Becker
@@ -1,5 +1,13 @@
1
1
  <script setup>
2
- import { isVue2 as _isVue2, computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from "vue-demi";
2
+ import {
3
+ isVue2 as _isVue2,
4
+ computed,
5
+ defineComponent,
6
+ onBeforeUnmount,
7
+ onMounted,
8
+ ref,
9
+ watch
10
+ } from "vue-demi";
3
11
  import AgentationToolbar from "./components/AgentationToolbar.vue";
4
12
  import AnnotationInput from "./components/AnnotationInput.vue";
5
13
  import AnnotationMarker from "./components/AnnotationMarker.vue";
@@ -10,15 +18,28 @@ import { useAnnotations } from "./composables/useAnnotations.mjs";
10
18
  import { useAreaSelect } from "./composables/useAreaSelect.mjs";
11
19
  import { useElementDetection } from "./composables/useElementDetection.mjs";
12
20
  import { useInteractionMode } from "./composables/useInteractionMode.mjs";
13
- import { DEFAULT_SHORTCUT_CONFIG, useKeyboardShortcuts } from "./composables/useKeyboardShortcuts.mjs";
21
+ import {
22
+ DEFAULT_SHORTCUT_CONFIG,
23
+ useKeyboardShortcuts
24
+ } from "./composables/useKeyboardShortcuts.mjs";
14
25
  import { useMarkerPositions } from "./composables/useMarkerPositions.mjs";
15
26
  import { useMultiSelect } from "./composables/useMultiSelect.mjs";
16
27
  import { useOutputFormatter } from "./composables/useOutputFormatter.mjs";
28
+ import { usePeekMode } from "./composables/usePeekMode.mjs";
17
29
  import { useSettings } from "./composables/useSettings.mjs";
18
30
  import { useTextSelection } from "./composables/useTextSelection.mjs";
31
+ import { PEEK_HOLD_DURATION_MS } from "./constants.mjs";
19
32
  import { isInsideAgentationTree } from "./utils/agentation-tree.mjs";
20
33
  import { copyToClipboard } from "./utils/clipboard.mjs";
21
- import { isFixed as checkIsFixed, detectVueComponents, getAccessibilityInfo, getComputedStylesSummary, getNearbyElements, getNearbyText, getRelevantComputedStyles } from "./utils/dom-inspector.mjs";
34
+ import {
35
+ isFixed as checkIsFixed,
36
+ detectVueComponents,
37
+ getAccessibilityInfo,
38
+ getComputedStylesSummary,
39
+ getNearbyElements,
40
+ getNearbyText,
41
+ getRelevantComputedStyles
42
+ } from "./utils/dom-inspector.mjs";
22
43
  import { createPortalContainer, destroyPortalContainer } from "./utils/portal.mjs";
23
44
  import { getElementName, getElementPath } from "./utils/selectors.mjs";
24
45
  import { boundingBoxToStyle } from "./utils/style.mjs";
@@ -60,14 +81,49 @@ const toolbarRef = ref(null);
60
81
  const currentUrl = ref(props.pageUrl || getCurrentUrl());
61
82
  const { settings } = useSettings();
62
83
  const { mode, transition } = useInteractionMode();
63
- const { annotations, addAnnotation, removeAnnotation, updateAnnotation, clearAnnotations, setScopeUrl } = useAnnotations(currentUrl.value);
64
- const { hoveredRect, hoveredName, hoveredComponentChain, onMouseMove, clearHighlight, getElementUnderOverlay, cleanup: cleanupDetection } = useElementDetection(overlayEl, () => settings.showComponentTree);
84
+ const {
85
+ annotations,
86
+ addAnnotation,
87
+ removeAnnotation,
88
+ updateAnnotation,
89
+ clearAnnotations,
90
+ restoreAnnotations,
91
+ setScopeUrl
92
+ } = useAnnotations(currentUrl.value);
93
+ const {
94
+ hoveredRect,
95
+ hoveredName,
96
+ hoveredComponentChain,
97
+ onMouseMove,
98
+ clearHighlight,
99
+ getElementUnderOverlay,
100
+ cleanup: cleanupDetection
101
+ } = useElementDetection(overlayEl, () => settings.showComponentTree);
65
102
  const textSelection = useTextSelection(mode);
66
103
  const multiSelect = useMultiSelect(mode, transition);
67
104
  const areaSelect = useAreaSelect(mode, transition);
68
105
  const animPause = useAnimationPause();
69
106
  const { recalculatePositions: _recalculatePositions } = useMarkerPositions(annotations);
70
107
  const { formatAnnotations } = useOutputFormatter();
108
+ const peekActive = ref(false);
109
+ const peekMode = usePeekMode({
110
+ peekKey: () => settings.peekKey,
111
+ enabled: () => mode.value === "idle" && (!toolbarRef.value || !toolbarRef.value.expanded),
112
+ isInputOpen: () => mode.value === "input-open",
113
+ onActivate() {
114
+ peekActive.value = true;
115
+ transition("inspect");
116
+ },
117
+ onDeactivate() {
118
+ peekActive.value = false;
119
+ if (mode.value === "input-open")
120
+ return;
121
+ if (mode.value !== "idle") {
122
+ transition("idle");
123
+ clearHighlight();
124
+ }
125
+ }
126
+ });
71
127
  const pendingPosition = ref(null);
72
128
  const pendingElementName = ref("");
73
129
  const pendingTarget = ref(null);
@@ -78,11 +134,17 @@ const editingAnnotation = ref(null);
78
134
  const settingsOpen = ref(false);
79
135
  const settingsAnchorEl = ref(null);
80
136
  const copyFeedback = ref(false);
137
+ const undoFeedback = ref(false);
138
+ const undoSnapshot = ref([]);
139
+ let undoTimer = null;
140
+ const UNDO_TIMEOUT_MS = 5e3;
81
141
  const toolbarDragging = ref(false);
82
142
  const DRAG_END_SUPPRESSION_MS = 500;
83
143
  const SETTINGS_CLOSE_SUPPRESSION_MS = 220;
84
144
  let suppressInteractionsUntil = 0;
85
- const effectiveBlockPageInteractions = computed(() => props.blockPageInteractions ?? settings.blockPageInteractions);
145
+ const effectiveBlockPageInteractions = computed(
146
+ () => props.blockPageInteractions ?? settings.blockPageInteractions
147
+ );
86
148
  const rootStyle = computed(() => {
87
149
  const hex = settings.markerColor;
88
150
  if (!hex)
@@ -106,7 +168,16 @@ const pendingMarkerY = computed(() => {
106
168
  return 0;
107
169
  return pendingPosition.value.y + (window.scrollY || document.documentElement.scrollTop);
108
170
  });
109
- const pendingIsSelection = computed(() => mode.value === "input-open" && !editingAnnotation.value && (multiSelect.selectedElements.value.length > 0 || !!areaSelect.areaRect.value));
171
+ const pendingIsSelection = computed(
172
+ () => mode.value === "input-open" && !editingAnnotation.value && (multiSelect.selectedElements.value.length > 0 || !!areaSelect.areaRect.value)
173
+ );
174
+ const mentionCandidates = computed(
175
+ () => annotations.value.map((ann, i) => ({
176
+ id: ann.id,
177
+ displayNumber: i + 1,
178
+ commentPreview: ann.comment.replace(/@\[\d+\]/g, "@\u2026").slice(0, 40) + (ann.comment.length > 40 ? "\u2026" : "")
179
+ })).filter((c) => !editingAnnotation.value || c.id !== editingAnnotation.value.id)
180
+ );
110
181
  let portalContainer = null;
111
182
  const isVue2 = _isVue2;
112
183
  const PassThrough = defineComponent({
@@ -115,8 +186,12 @@ const PassThrough = defineComponent({
115
186
  return (typeof slot === "function" ? slot() : slot?.[0]) || null;
116
187
  }
117
188
  });
118
- const portalWrapper = computed(() => props.disablePortal || isVue2 ? PassThrough : "Teleport");
119
- const portalProps = computed(() => props.disablePortal || isVue2 ? {} : { to: "body" });
189
+ const portalWrapper = computed(
190
+ () => props.disablePortal || isVue2 ? PassThrough : "Teleport"
191
+ );
192
+ const portalProps = computed(
193
+ () => props.disablePortal || isVue2 ? {} : { to: "body" }
194
+ );
120
195
  onMounted(() => {
121
196
  if (!props.disablePortal && isVue2 && rootEl.value) {
122
197
  portalContainer = createPortalContainer();
@@ -124,39 +199,79 @@ onMounted(() => {
124
199
  }
125
200
  });
126
201
  onBeforeUnmount(() => {
202
+ dismissUndo();
127
203
  animPause.cleanup();
128
204
  cleanupDetection();
129
205
  if (portalContainer) {
130
206
  destroyPortalContainer(portalContainer);
131
207
  }
132
208
  });
133
- watch(() => props.outputDetail, (v) => {
134
- if (v)
135
- settings.outputDetail = v;
136
- }, { immediate: true });
137
- watch(() => props.markerColor, (v) => {
138
- if (v)
139
- settings.markerColor = v;
140
- }, { immediate: true });
141
- watch(() => props.theme, (v) => {
142
- if (v)
143
- settings.theme = v;
144
- }, { immediate: true });
145
- watch(() => props.blockPageInteractions, (v) => {
146
- if (v)
147
- settings.blockPageInteractions = v;
148
- }, { immediate: true });
149
- watch(() => props.autoHideToolbar, (v) => {
150
- if (v)
151
- settings.autoHideToolbar = v;
152
- }, { immediate: true });
153
- watch(() => props.pageUrl, (url) => {
154
- syncUrlScope(url || getCurrentUrl());
155
- }, { immediate: true });
156
- watch(() => props.activationKey, (v) => {
157
- if (v !== void 0)
158
- settings.activationKey = v;
159
- }, { immediate: true });
209
+ watch(
210
+ () => props.outputDetail,
211
+ (v) => {
212
+ if (v)
213
+ settings.outputDetail = v;
214
+ },
215
+ { immediate: true }
216
+ );
217
+ watch(
218
+ () => props.markerColor,
219
+ (v) => {
220
+ if (v)
221
+ settings.markerColor = v;
222
+ },
223
+ { immediate: true }
224
+ );
225
+ watch(
226
+ () => props.theme,
227
+ (v) => {
228
+ if (v)
229
+ settings.theme = v;
230
+ },
231
+ { immediate: true }
232
+ );
233
+ watch(
234
+ () => props.blockPageInteractions,
235
+ (v) => {
236
+ if (v)
237
+ settings.blockPageInteractions = v;
238
+ },
239
+ { immediate: true }
240
+ );
241
+ watch(
242
+ () => props.autoHideToolbar,
243
+ (v) => {
244
+ if (v)
245
+ settings.autoHideToolbar = v;
246
+ },
247
+ { immediate: true }
248
+ );
249
+ watch(
250
+ () => props.pageUrl,
251
+ (url) => {
252
+ syncUrlScope(url || getCurrentUrl());
253
+ },
254
+ { immediate: true }
255
+ );
256
+ watch(
257
+ () => props.activationKey,
258
+ (v) => {
259
+ if (v !== void 0)
260
+ settings.activationKey = v;
261
+ },
262
+ { immediate: true }
263
+ );
264
+ let crosshairStyle = null;
265
+ watch(mode, (current, previous) => {
266
+ if (current !== "idle" && previous === "idle") {
267
+ crosshairStyle = document.createElement("style");
268
+ crosshairStyle.textContent = "* { cursor: crosshair !important; }";
269
+ document.head.appendChild(crosshairStyle);
270
+ } else if (current === "idle" && previous !== "idle") {
271
+ crosshairStyle?.remove();
272
+ crosshairStyle = null;
273
+ }
274
+ });
160
275
  function onActivate() {
161
276
  transition("inspect");
162
277
  }
@@ -179,8 +294,6 @@ function onOverlayMouseMove(e) {
179
294
  function onOverlayMouseDown(e) {
180
295
  if (isInteractionLocked())
181
296
  return;
182
- if (multiSelect.onMouseDown(e))
183
- return;
184
297
  areaSelect.onMouseDown(e);
185
298
  }
186
299
  function onOverlayMouseUp(e) {
@@ -207,7 +320,10 @@ function onOverlayMouseUp(e) {
207
320
  areaSelect.onMouseUp();
208
321
  const rect = areaSelect.areaRect.value;
209
322
  if (rect && rect.width > 10 && rect.height > 10) {
210
- pendingPosition.value = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
323
+ pendingPosition.value = {
324
+ x: rect.x + rect.width / 2,
325
+ y: rect.y + rect.height / 2
326
+ };
211
327
  pendingElementName.value = "Area selection";
212
328
  pendingComponentChain.value = void 0;
213
329
  pendingComputedStyles.value = void 0;
@@ -229,7 +345,9 @@ function onOverlayMouseUp(e) {
229
345
  };
230
346
  pendingElementName.value = `"${textResult.selectedText.slice(0, 30)}"`;
231
347
  pendingComponentChain.value = getVueComponents(textResult.anchorElement);
232
- pendingComputedStyles.value = getRelevantComputedStyles(textResult.anchorElement);
348
+ pendingComputedStyles.value = getRelevantComputedStyles(
349
+ textResult.anchorElement
350
+ );
233
351
  pendingTarget.value = textResult.anchorElement;
234
352
  pendingTextSelection.value = {
235
353
  text: textResult.selectedText,
@@ -282,7 +400,10 @@ function shouldUseDocumentFallbackEvents() {
282
400
  return mode.value === "inspect" && !effectiveBlockPageInteractions.value && !isInteractionLocked();
283
401
  }
284
402
  function lockInteractionsTemporarily(durationMs) {
285
- suppressInteractionsUntil = Math.max(suppressInteractionsUntil, Date.now() + durationMs);
403
+ suppressInteractionsUntil = Math.max(
404
+ suppressInteractionsUntil,
405
+ Date.now() + durationMs
406
+ );
286
407
  }
287
408
  function isInteractionLocked() {
288
409
  return settingsOpen.value || toolbarDragging.value || Date.now() < suppressInteractionsUntil;
@@ -297,6 +418,7 @@ function closeSettings(lockInteractions = true) {
297
418
  function syncUrlScope(nextUrl) {
298
419
  if (!nextUrl || currentUrl.value === nextUrl)
299
420
  return;
421
+ dismissUndo();
300
422
  currentUrl.value = nextUrl;
301
423
  setScopeUrl(nextUrl);
302
424
  }
@@ -395,7 +517,9 @@ function onInputAdd(comment) {
395
517
  comment,
396
518
  url,
397
519
  element: "multi",
398
- elementPath: `region at (${Math.round(boundingBox.x)}, ${Math.round(boundingBox.y)})`,
520
+ elementPath: `region at (${Math.round(boundingBox.x)}, ${Math.round(
521
+ boundingBox.y
522
+ )})`,
399
523
  isMultiSelect: true,
400
524
  elements,
401
525
  boundingBox,
@@ -471,7 +595,12 @@ function onInputAdd(comment) {
471
595
  vueComponents: getVueComponents(el),
472
596
  nearbyElements: getNearbyElements(el),
473
597
  nearbyText: getNearbyText(el),
474
- boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
598
+ boundingBox: {
599
+ x: rect.x,
600
+ y: rect.y,
601
+ width: rect.width,
602
+ height: rect.height
603
+ },
475
604
  cssClasses: detail === "forensic" ? Array.from(el.classList).join(" ") : void 0,
476
605
  fullPath: detail === "forensic" ? getElementPath(el) : void 0,
477
606
  computedStyles: detail === "forensic" ? getComputedStylesSummary(el) : void 0,
@@ -480,16 +609,31 @@ function onInputAdd(comment) {
480
609
  emit("annotation-add", ann);
481
610
  }
482
611
  resetPendingState();
483
- transition("inspect");
612
+ clearHighlight();
613
+ if (peekActive.value) {
614
+ transition("inspect");
615
+ peekMode.scheduleExit();
616
+ } else {
617
+ transition("inspect");
618
+ }
484
619
  }
485
620
  function onInputCancel() {
486
621
  resetPendingState();
487
- multiSelect.reset();
488
622
  areaSelect.reset();
489
- transition("inspect");
623
+ clearHighlight();
624
+ if (peekActive.value) {
625
+ transition("idle");
626
+ peekMode.deactivate();
627
+ } else {
628
+ transition("inspect");
629
+ }
490
630
  }
491
631
  async function onCopy() {
492
- const markdown = formatAnnotations(annotations.value, settings.outputDetail, resolvedUrl.value);
632
+ const markdown = formatAnnotations(
633
+ annotations.value,
634
+ settings.outputDetail,
635
+ resolvedUrl.value
636
+ );
493
637
  if (props.copyToClipboard !== false) {
494
638
  const success = await copyToClipboard(markdown);
495
639
  if (!success)
@@ -501,13 +645,37 @@ async function onCopy() {
501
645
  }
502
646
  emit("copy", markdown);
503
647
  if (settings.clearAfterCopy) {
504
- const cleared = clearAnnotations();
505
- emit("annotations-clear", cleared);
648
+ onClear();
506
649
  }
507
650
  }
651
+ function dismissUndo() {
652
+ undoFeedback.value = false;
653
+ undoSnapshot.value = [];
654
+ if (undoTimer) {
655
+ clearTimeout(undoTimer);
656
+ undoTimer = null;
657
+ }
658
+ }
659
+ function startUndoTimer() {
660
+ if (undoTimer)
661
+ clearTimeout(undoTimer);
662
+ undoTimer = setTimeout(() => dismissUndo(), UNDO_TIMEOUT_MS);
663
+ }
664
+ function onUndo() {
665
+ const snapshot = undoSnapshot.value;
666
+ dismissUndo();
667
+ if (snapshot.length > 0)
668
+ restoreAnnotations(snapshot);
669
+ }
508
670
  function onClear() {
671
+ dismissUndo();
509
672
  const cleared = clearAnnotations();
673
+ if (cleared.length === 0)
674
+ return;
510
675
  emit("annotations-clear", cleared);
676
+ undoSnapshot.value = cleared;
677
+ undoFeedback.value = true;
678
+ startUndoTimer();
511
679
  }
512
680
  function onMarkerClick(ann) {
513
681
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
@@ -515,12 +683,16 @@ function onMarkerClick(ann) {
515
683
  const markerY = ann.isFixed ? ann.y : ann.y - scrollTop;
516
684
  editingAnnotation.value = ann;
517
685
  pendingPosition.value = { x: markerX, y: markerY };
518
- pendingElementName.value = getElementName(ann._targetRef?.deref() || document.createElement(ann.element));
686
+ pendingElementName.value = getElementName(
687
+ ann._targetRef?.deref() || document.createElement(ann.element)
688
+ );
519
689
  pendingComponentChain.value = ann.vueComponents;
520
- pendingComputedStyles.value = ann.computedStyles ? Object.fromEntries(ann.computedStyles.split("\n").filter(Boolean).map((line) => {
521
- const idx = line.indexOf(":");
522
- return idx > -1 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, ""];
523
- })) : ann._targetRef?.deref() ? getRelevantComputedStyles(ann._targetRef.deref()) : void 0;
690
+ pendingComputedStyles.value = ann.computedStyles ? Object.fromEntries(
691
+ ann.computedStyles.split("\n").filter(Boolean).map((line) => {
692
+ const idx = line.indexOf(":");
693
+ return idx > -1 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, ""];
694
+ })
695
+ ) : ann._targetRef?.deref() ? getRelevantComputedStyles(ann._targetRef.deref()) : void 0;
524
696
  pendingTextSelection.value = null;
525
697
  pendingTarget.value = ann._targetRef?.deref() || null;
526
698
  transition("input-open");
@@ -532,7 +704,13 @@ function onInputDelete() {
532
704
  if (removed)
533
705
  emit("annotation-delete", removed);
534
706
  resetPendingState();
535
- transition("inspect");
707
+ clearHighlight();
708
+ if (peekActive.value) {
709
+ transition("idle");
710
+ peekMode.deactivate();
711
+ } else {
712
+ transition("inspect");
713
+ }
536
714
  }
537
715
  function onInputSave(comment) {
538
716
  if (!editingAnnotation.value)
@@ -541,7 +719,13 @@ function onInputSave(comment) {
541
719
  if (updated)
542
720
  emit("annotation-update", updated);
543
721
  resetPendingState();
544
- transition("inspect");
722
+ clearHighlight();
723
+ if (peekActive.value) {
724
+ transition("inspect");
725
+ peekMode.scheduleExit();
726
+ } else {
727
+ transition("inspect");
728
+ }
545
729
  }
546
730
  function onToggleArea(value) {
547
731
  areaSelect.isAreaMode.value = value;
@@ -596,7 +780,10 @@ onMounted(() => {
596
780
  document.addEventListener("mousemove", onDocumentMouseMove, true);
597
781
  document.addEventListener("mousedown", onDocumentMouseDown, true);
598
782
  document.addEventListener("mouseup", onDocumentMouseUp, true);
599
- document.addEventListener("wheel", onDocumentWheel, { passive: true, capture: true });
783
+ document.addEventListener("wheel", onDocumentWheel, {
784
+ passive: true,
785
+ capture: true
786
+ });
600
787
  document.addEventListener("click", onDocumentClick, true);
601
788
  });
602
789
  onBeforeUnmount(() => {
@@ -613,14 +800,21 @@ onBeforeUnmount(() => {
613
800
 
614
801
  <template>
615
802
  <component :is="portalWrapper" v-bind="portalProps">
616
- <div ref="rootEl" data-agentation-vue :data-va-theme="settings.theme !== 'auto' ? settings.theme : void 0" :style="rootStyle">
803
+ <div
804
+ ref="rootEl"
805
+ data-agentation-vue
806
+ :data-va-theme="settings.theme !== 'auto' ? settings.theme : void 0"
807
+ :style="rootStyle"
808
+ >
617
809
  <!-- Intercept overlay -->
618
810
  <div
619
811
  v-if="mode !== 'idle'"
620
812
  ref="overlayEl"
621
813
  class="__va-intercept"
622
814
  :class="{ '__va-intercept--input-open': mode === 'input-open' }"
623
- :style="mode === 'inspect' && !effectiveBlockPageInteractions ? { pointerEvents: 'none' } : void 0"
815
+ :style="
816
+ mode === 'inspect' && !effectiveBlockPageInteractions ? { pointerEvents: 'none' } : void 0
817
+ "
624
818
  @mousemove="onOverlayMouseMove"
625
819
  @mousedown="onOverlayMouseDown"
626
820
  @mouseup="onOverlayMouseUp"
@@ -647,10 +841,11 @@ onBeforeUnmount(() => {
647
841
  :style="boundingBoxToStyle(areaSelect.areaRect.value)"
648
842
  />
649
843
 
650
- <!-- Annotation markers -->
844
+ <!-- Annotation markers (hidden when toolbar is collapsed) -->
651
845
  <AnnotationMarker
652
846
  v-for="(ann, i) in annotations"
653
847
  :key="ann.id"
848
+ :hidden="mode === 'idle'"
654
849
  :number="i + 1"
655
850
  :x="ann.x"
656
851
  :y="ann.y"
@@ -679,6 +874,7 @@ onBeforeUnmount(() => {
679
874
  :computed-styles="pendingComputedStyles"
680
875
  :initial-comment="editingAnnotation?.comment"
681
876
  :is-editing="!!editingAnnotation"
877
+ :mention-candidates="mentionCandidates"
682
878
  @add="onInputAdd"
683
879
  @cancel="onInputCancel"
684
880
  @delete="onInputDelete"
@@ -698,6 +894,18 @@ onBeforeUnmount(() => {
698
894
  Copied!
699
895
  </div>
700
896
 
897
+ <!-- Undo clear feedback -->
898
+ <div
899
+ v-if="undoFeedback"
900
+ class="__va-undo-feedback"
901
+ :class="{ '__va-undo-feedback--shifted': copyFeedback }"
902
+ >
903
+ <span>Annotations cleared</span>
904
+ <button type="button" class="__va-undo-btn" @click="onUndo">
905
+ Undo
906
+ </button>
907
+ </div>
908
+
701
909
  <!-- Toolbar -->
702
910
  <AgentationToolbar
703
911
  ref="toolbarRef"
@@ -707,6 +915,8 @@ onBeforeUnmount(() => {
707
915
  :is-area-mode="areaSelect.isAreaMode.value"
708
916
  :auto-hide-enabled="settings.autoHideToolbar"
709
917
  :placement="settings.toolbarPlacement"
918
+ :is-peek-charging="peekMode.isCharging.value"
919
+ :peek-duration-ms="PEEK_HOLD_DURATION_MS"
710
920
  @activate="onActivate"
711
921
  @deactivate="onDeactivate"
712
922
  @copy="onCopy"
@@ -6,6 +6,8 @@ type __VLS_Props = {
6
6
  isAreaMode: boolean;
7
7
  autoHideEnabled: boolean;
8
8
  placement: ToolbarAnchor;
9
+ isPeekCharging: boolean;
10
+ peekDurationMs: number;
9
11
  };
10
12
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {
11
13
  expanded: import("vue-demi").Ref<boolean, boolean>;
@@ -10,7 +10,9 @@ const props = defineProps({
10
10
  isPaused: { type: Boolean, required: true },
11
11
  isAreaMode: { type: Boolean, required: true },
12
12
  autoHideEnabled: { type: Boolean, required: true },
13
- placement: { type: String, required: true }
13
+ placement: { type: String, required: true },
14
+ isPeekCharging: { type: Boolean, required: true },
15
+ peekDurationMs: { type: Number, required: true }
14
16
  });
15
17
  const emit = defineEmits(["activate", "deactivate", "copy", "clear", "toggle-pause", "toggle-area", "update:placement", "open-settings", "drag-start", "drag-end"]);
16
18
  const expanded = ref(false);
@@ -77,8 +79,7 @@ function onDeactivate() {
77
79
  emit("deactivate");
78
80
  }
79
81
  function onClear() {
80
- if (confirm("Clear all annotations?"))
81
- emit("clear");
82
+ emit("clear");
82
83
  }
83
84
  function onOpenSettings(e) {
84
85
  const anchorEl = e.currentTarget instanceof HTMLElement ? e.currentTarget : null;
@@ -134,6 +135,14 @@ defineExpose({ expanded, placement });
134
135
  >
135
136
  <VaIcon name="cursor" />
136
137
  <span v-if="annotationCount > 0" class="__va-toolbar-badge">{{ annotationCount }}</span>
138
+ <svg
139
+ v-if="isPeekCharging"
140
+ class="__va-peek-ring"
141
+ viewBox="0 0 46 46"
142
+ aria-hidden="true"
143
+ >
144
+ <circle cx="23" cy="23" r="21" :style="{ animationDuration: `${peekDurationMs}ms` }" />
145
+ </svg>
137
146
  </button>
138
147
  </div>
139
148
 
@@ -6,6 +6,8 @@ type __VLS_Props = {
6
6
  isAreaMode: boolean;
7
7
  autoHideEnabled: boolean;
8
8
  placement: ToolbarAnchor;
9
+ isPeekCharging: boolean;
10
+ peekDurationMs: number;
9
11
  };
10
12
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {
11
13
  expanded: import("vue-demi").Ref<boolean, boolean>;