agent-canvas 0.1.38 → 0.1.39

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.
@@ -58,6 +58,7 @@ function getTextNodesInRange(range: Range): Text[] {
58
58
  // Skip line numbers and existing marks
59
59
  const parent = node.parentElement;
60
60
  if (parent?.classList.contains("select-none")) continue;
61
+ if (parent?.closest("[data-annotation-id]")) continue;
61
62
 
62
63
  if (range.intersectsNode(node)) {
63
64
  nodes.push(node);
@@ -108,27 +109,18 @@ export function restoreMarks(
108
109
  container: HTMLElement,
109
110
  annotations: { id: string; snippet: string; filePath?: string; context?: { before: string; after: string; hierarchy: string[] } }[]
110
111
  ) {
111
- // Skip annotations that already have marks in the DOM
112
- const toRestore = annotations.filter(
113
- (a) => !document.querySelector(`[data-annotation-id="${a.id}"]`)
114
- );
115
- if (!toRestore.length) return;
112
+ for (const ann of annotations) {
113
+ // A canvas can be rendered more than once (overview, compare, hidden
114
+ // source panes), so restore decisions must be scoped to this container.
115
+ if (container.querySelector(`[data-annotation-id="${ann.id}"]`)) continue;
116
116
 
117
- // Build full text content + mapping from text offset to (textNode, offsetInNode)
118
- const textNodes: { node: Text; start: number }[] = [];
119
- let fullText = "";
120
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
121
- let tn: Text | null;
122
- while ((tn = walker.nextNode() as Text | null)) {
123
- if (tn.parentElement?.classList.contains("select-none")) continue;
124
- textNodes.push({ node: tn, start: fullText.length });
125
- fullText += tn.textContent || "";
126
- }
127
-
128
- for (const ann of toRestore) {
129
117
  const snippet = ann.snippet;
130
118
  if (!snippet) continue;
131
119
 
120
+ // Rebuild after every inserted mark. Wrapping text splits and moves text
121
+ // nodes, so a cached offset map can point at stale DOM after the first hit.
122
+ const { fullText, textNodes } = buildTextIndex(container);
123
+
132
124
  // Find all occurrences of the snippet in the full text
133
125
  const occurrences: number[] = [];
134
126
  let searchFrom = 0;
@@ -181,6 +173,24 @@ export function restoreMarks(
181
173
  }
182
174
  }
183
175
 
176
+ function buildTextIndex(container: HTMLElement): {
177
+ fullText: string;
178
+ textNodes: { node: Text; start: number }[];
179
+ } {
180
+ const textNodes: { node: Text; start: number }[] = [];
181
+ let fullText = "";
182
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
183
+ let tn: Text | null;
184
+ while ((tn = walker.nextNode() as Text | null)) {
185
+ const parent = tn.parentElement;
186
+ if (parent?.classList.contains("select-none")) continue;
187
+ if (parent?.closest("[data-annotation-id]")) continue;
188
+ textNodes.push({ node: tn, start: fullText.length });
189
+ fullText += tn.textContent || "";
190
+ }
191
+ return { fullText, textNodes };
192
+ }
193
+
184
194
  function offsetToNode(
185
195
  textNodes: { node: Text; start: number }[],
186
196
  offset: number