@comty.io/core 0.1.4 → 0.1.6

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.
package/comty-sdk.js CHANGED
@@ -3,7 +3,7 @@
3
3
  function getComtyStyle() {
4
4
  const toolbarSurface = "#2b2d2f";
5
5
  const toolbarMuted = "#b8babb";
6
- const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
6
+ const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
7
7
  const typographyCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="8" width="23" height="23" rx="7" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width=".75"/><path d="m14 25 5.5-12L25 25m-8.5-4h6M13 25h4m5 0h4" fill="none" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m2 2 2 15 4-5 6-3Z" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
8
8
  return `
9
9
  #comty-root {
@@ -13,7 +13,7 @@ function getComtyStyle() {
13
13
  --comty-muted: var(--comty-toolbar-muted);
14
14
  --comty-accent: #55aaf5;
15
15
  --comty-error: #ef7676;
16
- --comty-outline: var(--comty-toolbar-muted);
16
+ --comty-outline: var(--comty-accent);
17
17
  --comty-comment-focus: var(--comty-toolbar-muted);
18
18
  --comty-paper: #fff;
19
19
  --comty-ink: #171717;
@@ -60,6 +60,8 @@ function getComtyStyle() {
60
60
  --comty-dialog-width: 32rem;
61
61
  --comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
62
62
  --comty-motion-duration: 220ms;
63
+ --comty-working-dot-size: .25em;
64
+ --comty-working-dot-gap: .125em;
63
65
  --comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
64
66
  --comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
65
67
  position: fixed;
@@ -71,13 +73,73 @@ function getComtyStyle() {
71
73
  font-family: var(--comty-font);
72
74
  color-scheme: dark;
73
75
  }
74
-
76
+ #comty-root :is(button, .comty-comment-avatar) { user-select: none; -webkit-user-select: none; }
77
+ #comty-root .comty-comment-avatar img { -webkit-user-drag: none; }
78
+ #comty-root .comty-pin .comty-comment-avatar { pointer-events: none; }
79
+ #comty-root > .comty-agent-panel { position: fixed; right: 1rem; bottom: 5rem; z-index: 2147483647; width: min(22rem, calc(100vw - 2rem)); max-height: min(32rem, calc(100vh - 6rem)); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; pointer-events: auto; border: .0625rem solid var(--comty-border); border-radius: 1rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; transform: translateY(0); opacity: 1; transition: transform var(--comty-motion-duration) var(--comty-ease-out), opacity var(--comty-motion-duration) ease; color-scheme: dark; }
80
+ #comty-root > .comty-agent-panel[data-side="left"] { left: 1rem; right: auto; }
81
+ #comty-root > .comty-agent-panel[data-closed] { transform: translateY(.5rem); opacity: 0; pointer-events: none; }
82
+ #comty-root > .comty-agent-panel[hidden] { display: none; }
83
+ .comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
84
+ .comty-agent-panel-header { display: flex; flex: 0 0 auto; align-items: center; gap: .75rem; padding: .5rem .75rem; border-bottom: .0625rem solid var(--comty-hover-surface); }
85
+ .comty-agent-brand { display: grid; place-items: center; width: 2.375rem; height: 2.375rem; flex-shrink: 0; border-radius: .6875rem; color: var(--comty-accent); background: var(--comty-hover-surface); }
86
+ .comty-agent-brand.comty-agent-avatar { width: 2.375rem; height: 2.375rem; border-radius: 0; background: transparent; }
87
+ .comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
88
+ .comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
89
+ .comty-agent-brand.comty-agent-avatar svg { width: 100%; height: 100%; }
90
+ .comty-agent-answer { display: flex; align-items: flex-start; gap: .5rem; padding: .375rem .5rem .875rem; }
91
+ .comty-agent-answer .comty-agent-brand { width: 1.5rem; height: 1.5rem; }
92
+ .comty-agent-answer p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--comty-foreground); }
93
+ .comty-agent-heading { flex: 1; min-width: 0; }
94
+ .comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
95
+ .comty-agent-panel-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--comty-hover-surface) transparent; padding: .375rem .5rem 2rem; }
96
+ .comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
97
+ .comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
98
+ .comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
99
+ .comty-agent-list { display: grid; gap: .125rem; }
100
+ .comty-agent-row { position: relative; display: flex; align-items: center; gap: .625rem; min-height: 2rem; padding: .25rem .5rem; border-radius: .5rem; color: var(--comty-foreground); }
101
+ .comty-agent-row:hover { background: var(--comty-hover-surface); }
102
+ .comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
103
+ .comty-working-indicator { display: inline-grid; grid-template-columns: repeat(3, var(--comty-working-dot-size)); gap: var(--comty-working-dot-gap); flex: 0 0 auto; color: var(--comty-foreground); }
104
+ .comty-working-indicator > i { display: block; width: var(--comty-working-dot-size); height: var(--comty-working-dot-size); border-radius: 50%; background: currentColor; opacity: .25; transition: opacity 150ms linear; }
105
+ .comty-working-indicator > i[data-active] { opacity: 1; }
106
+ .comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
107
+ .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
108
+ .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
109
+ .comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
110
+ .comty-agent-row-actions { position: absolute; top: 50%; right: .25rem; display: flex; gap: .25rem; border-radius: .25rem; background: var(--comty-hover-surface); opacity: 0; pointer-events: none; transform: translateY(-50%); }
111
+ .comty-agent-row:hover .comty-agent-row-actions, .comty-agent-row-actions:has(.comty-agent-action:focus-visible) { opacity: 1; pointer-events: auto; }
112
+ @media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
113
+ .comty-agent-panel .comty-agent-row-actions .comty-agent-action { min-height: 1.5rem; padding: .125rem .375rem; border-radius: .25rem; background: transparent; color: var(--comty-muted); font-size: .875rem; }
114
+ .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
115
+ .comty-agent-error-popover { position: fixed; z-index: 2147483647; width: min(20rem, calc(100vw - 1rem)); max-height: calc(100vh - 1rem); overflow: auto; padding: .75rem .875rem; border: 0; border-radius: .625rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; pointer-events: none; color-scheme: dark; }
116
+ .comty-agent-error-popover strong { display: block; font-weight: 600; }
117
+ .comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
118
+ .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
119
+ .comty-agent-error { margin-top: .75rem; }
120
+ .comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
121
+ .comty-agent-error p { margin-top: .25rem; }
122
+ .comty-agent-banner { margin-bottom: 1rem; }
123
+ .comty-agent-banner .comty-agent-error { margin-top: 0; }
124
+ .comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
125
+ .comty-agent-panel .comty-agent-action { display: inline-flex; align-items: center; justify-content: center; gap: .375rem; min-height: 2rem; margin: 0; padding: .375rem .625rem; border: 0; border-radius: .5rem; font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; cursor: pointer; color: var(--comty-foreground); background: var(--comty-hover-surface); transition: background-color 150ms ease; }
126
+ .comty-agent-panel .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
127
+ .comty-agent-panel .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
128
+ .comty-agent-panel .comty-agent-action:disabled { opacity: .5; cursor: wait; }
129
+ .comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
130
+ .comty-agent-panel :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
131
+ .comty-agent-details { margin-top: .625rem; }
132
+ .comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
133
+ .comty-agent-panel pre { margin: .5rem 0 0; padding: .625rem; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 10rem; overflow: auto; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .875rem/1.4 ui-monospace, monospace; }
134
+ .comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: 0; pointer-events: none; }
135
+ .comty-agent-working-badge { position: absolute; left: -.125rem; bottom: 100%; display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .5rem; border-radius: .375rem .375rem 0 0; background: var(--comty-surface); color: var(--comty-foreground); font: 600 .6875rem/1.4 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
136
+ .comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
75
137
  #comty-root { cursor: default; }
76
138
  #comty-root::backdrop { background: transparent; pointer-events: none; }
77
139
  html.comty-comment-placement,
78
140
  html.comty-comment-placement body,
79
141
  html.comty-comment-placement body *:not(#comty-root):not(#comty-root *) {
80
- cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 20 20, crosshair !important;
142
+ cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 16 16, crosshair !important;
81
143
  }
82
144
  #comty-root > .comty-sidepanel-open, #comty-root > .comty-pin, #comty-root > .comty-bubble, #comty-root > .comty-create-bubble, #comty-root > .comty-popover, #comty-root > .comty-auth-backdrop, #comty-root > .comty-auth-toast { pointer-events: auto; }
83
145
  .comty-toolbar {
@@ -144,7 +206,7 @@ function getComtyStyle() {
144
206
  #comty-root [hidden] { display: none !important; }
145
207
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
146
208
  @media (prefers-reduced-motion: reduce) {
147
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
209
+ .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i { transition: none !important; animation: none !important; }
148
210
  }
149
211
 
150
212
  .comty-sidepanel {
@@ -326,6 +388,16 @@ function getComtyStyle() {
326
388
  white-space: nowrap;
327
389
  }
328
390
 
391
+ .comty-anchor-badge {
392
+ display: inline-block;
393
+ margin-top: .375rem;
394
+ padding: .125rem .375rem;
395
+ border: 1px solid var(--comty-border);
396
+ border-radius: 999rem;
397
+ color: var(--comty-muted);
398
+ font: .6875rem/1.4 sans-serif;
399
+ }
400
+
329
401
  .comty-pin {
330
402
  position: fixed;
331
403
  z-index: 2147483645;
@@ -607,6 +679,8 @@ function getComtyStyle() {
607
679
  height: 100%;
608
680
  object-fit: cover;
609
681
  }
682
+ .comty-comment-avatar.comty-agent-avatar { overflow: visible; border-radius: 0; background: transparent; color: var(--comty-paper); }
683
+ .comty-comment-avatar.comty-agent-avatar svg { display: block; width: 100%; height: 100%; }
610
684
 
611
685
  .comty-pin-avatar {
612
686
  width: 24px;
@@ -721,12 +795,12 @@ function getComtyStyle() {
721
795
  }
722
796
 
723
797
  .comty-popover-item.comty-popover-item-destructive {
724
- color: var(--comty-error);
798
+ color: var(--comty-agent-error);
725
799
  }
726
800
 
727
801
  .comty-popover-item.comty-popover-item-destructive:hover,
728
802
  .comty-popover-item.comty-popover-item-destructive:focus-visible {
729
- background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
803
+ background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
730
804
  outline: none;
731
805
  }
732
806
 
@@ -1097,15 +1171,15 @@ function getComtyStyle() {
1097
1171
  margin: 4px 0;
1098
1172
  }
1099
1173
 
1100
- html.comty-comments-active .comty-pin {
1174
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin {
1101
1175
  display: grid;
1102
1176
  }
1103
1177
 
1104
- html.comty-comments-active .comty-pin.comty-filtered-out {
1178
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin.comty-filtered-out {
1105
1179
  display: none;
1106
1180
  }
1107
1181
 
1108
- html.comty-comments-active .comty-bubble {
1182
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-bubble {
1109
1183
  display: block;
1110
1184
  }
1111
1185
 
@@ -1215,10 +1289,10 @@ function getComtyStyle() {
1215
1289
  position: fixed;
1216
1290
  box-sizing: border-box;
1217
1291
  border: .0625rem solid var(--comty-outline);
1218
- background: color-mix(in oklch, var(--comty-outline) 8%, transparent);
1292
+ background: transparent;
1293
+ border-radius: 0;
1219
1294
  pointer-events: none;
1220
1295
  }
1221
- .comty-inspection-box[data-selected] { background: color-mix(in oklch, var(--comty-outline) 5%, transparent); }
1222
1296
  .comty-inspect-spacing { position: fixed; pointer-events: none; background: var(--comty-spacing-color); }
1223
1297
  .comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
1224
1298
  .comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
@@ -1272,6 +1346,15 @@ function getComtyStyle() {
1272
1346
  height: 5.75rem;
1273
1347
  border-radius: .75rem;
1274
1348
  }
1349
+ .comty-agent-scope { display: inline-flex; align-items: center; gap: .375rem; color: var(--comty-muted); font: .6875rem/1.25 var(--comty-font); white-space: nowrap; cursor: pointer; pointer-events: auto; opacity: 0; visibility: hidden; }
1350
+ .comty-create-expanded .comty-agent-scope { opacity: 1; visibility: visible; }
1351
+ .comty-agent-scope input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
1352
+ .comty-agent-scope-track { position: relative; width: 1.5rem; height: .875rem; flex: 0 0 auto; border-radius: 999rem; background: var(--comty-border); transition: background-color 160ms ease; }
1353
+ .comty-agent-scope-track::after { content: ""; position: absolute; top: .125rem; left: .125rem; width: .625rem; height: .625rem; border-radius: 50%; background: var(--comty-paper); transition: transform 160ms ease; }
1354
+ .comty-agent-scope input:checked + .comty-agent-scope-track { background: var(--comty-accent); }
1355
+ .comty-agent-scope input:checked + .comty-agent-scope-track::after { transform: translateX(.625rem); }
1356
+ .comty-agent-scope input:focus-visible + .comty-agent-scope-track { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
1357
+ .comty-agent-thread-error { grid-column: 2; margin: .25rem 0 0; color: var(--comty-agent-error); font: .6875rem/1.3 var(--comty-font); }
1275
1358
 
1276
1359
  .comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
1277
1360
  .comty-create-bubble.comty-bubble-left { transform-origin: top right; }
@@ -1502,6 +1585,14 @@ function createAnchorMatcher({
1502
1585
  return haystack.toLowerCase().includes(needle.toLowerCase());
1503
1586
  }
1504
1587
 
1588
+ function similarText(a, b) {
1589
+ if (!a || !b) return false;
1590
+ const words = value => new Set(value.toLowerCase().split(/\s+/).filter(Boolean));
1591
+ const left = words(a), right = words(b);
1592
+ const common = [...left].filter(word => right.has(word)).length;
1593
+ return common >= 2 && common / Math.max(left.size, right.size) >= 0.7;
1594
+ }
1595
+
1505
1596
  function scoreFingerprint(el, fingerprint) {
1506
1597
  if (!el || !fingerprint) return 0;
1507
1598
  if (
@@ -1571,6 +1662,8 @@ function createAnchorMatcher({
1571
1662
  score += 15;
1572
1663
  }
1573
1664
 
1665
+ if (text !== fingerprint.text && similarText(text, fingerprint.text)) score += 24;
1666
+
1574
1667
  if (typeof fingerprint.siblingIndex === "number" && el.parentElement) {
1575
1668
  const siblings = [...el.parentElement.children].filter(
1576
1669
  (child) => child.tagName === el.tagName,
@@ -1648,6 +1741,8 @@ function createAnchorMatcher({
1648
1741
  if (ancestorScore > 0) {
1649
1742
  score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
1650
1743
  }
1744
+ // Distinguish repeated cards by their section, even when dimensions match.
1745
+ if (index < 3 && ancestors[index].text && getElementTextSignature(parent) === ancestors[index].text) score += 18;
1651
1746
  parent = parent.parentElement;
1652
1747
  }
1653
1748
 
@@ -1662,46 +1757,75 @@ function createAnchorMatcher({
1662
1757
  return score;
1663
1758
  }
1664
1759
 
1665
- function matchesAnchorIdentity(el, fingerprint) {
1760
+ function matchesAnchorIdentity(el, fingerprint, ignoreDescendantText = false, allowTextChanges = true) {
1666
1761
  if (!el || !fingerprint || el.tagName.toLowerCase() !== fingerprint.tag) return false;
1667
1762
  const attributes = { ariaLabel: "aria-label", name: "name", href: "href", src: "src", alt: "alt", title: "title", role: "role" };
1668
1763
  for (const [key, attribute] of Object.entries(attributes)) {
1669
1764
  if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
1670
1765
  }
1671
- if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText) return false;
1766
+ if (fingerprint.id && el.id !== fingerprint.id) return false;
1767
+ if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
1768
+ const stableIdentity = (fingerprint.id && el.id === fingerprint.id) ||
1769
+ (fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
1770
+ if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
1672
1771
  // Container text is useful for matching remounted drawers and dialogs.
1673
- if (!fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text) return false;
1772
+ if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
1773
+ if (!allowTextChanges || !el.children.length || !similarText(getElementTextSignature(el), fingerprint.text)) return false;
1774
+ }
1674
1775
  return true;
1675
1776
  }
1676
1777
 
1677
1778
  function matchesAnchorContext(el, context) {
1678
1779
  if (!context) return true;
1679
- if (!matchesAnchorIdentity(el, context.target)) return false;
1780
+ // A unique page landmark keeps its identity when a child is edited.
1781
+ const pageContainer = !context.layer && (context.target?.tag === "main" || context.target?.role === "main") &&
1782
+ [...document.querySelectorAll('main, [role="main"]')].filter(node => !isComtyNode(node)).length === 1;
1783
+ if (!matchesAnchorIdentity(el, context.target, pageContainer)) return false;
1680
1784
  const parentFingerprint = context.ancestors?.[0];
1681
1785
  if (parentFingerprint?.text && parentFingerprint.text !== context.target.text &&
1682
- !matchesAnchorIdentity(el.parentElement, parentFingerprint)) return false;
1786
+ !matchesAnchorIdentity(el.parentElement, parentFingerprint, pageContainer)) return false;
1683
1787
  const layer = getPresentationLayer(el);
1684
- if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer))) return false;
1788
+ if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer, false, false))) return false;
1685
1789
  if (context.version >= 2 && !context.layer && layer) return false;
1686
1790
  return scoreAnchorCandidate(el, context) >= 50;
1687
1791
  }
1688
1792
 
1689
- function resolveByAnchorContext(context) {
1690
- if (!context?.target) return null;
1793
+ function resolveAnchor(context, exact = null) {
1794
+ if (!context?.target) return { element: exact, status: exact ? "matched" : "missing" };
1691
1795
  let candidates;
1692
1796
  try {
1693
- candidates = [...document.body.querySelectorAll(context.target.tag || "*")];
1797
+ candidates = [...document.body.querySelectorAll(context.target.tag || "*")].filter(el => !isComtyNode(el));
1694
1798
  } catch {
1695
- return null;
1799
+ return { element: null, status: "missing" };
1696
1800
  }
1697
1801
  const matches = candidates
1698
- .filter(el => !isComtyNode(el) && matchesAnchorContext(el, context))
1699
- .map(el => ({ el, score: scoreAnchorCandidate(el, context) }))
1802
+ .filter(el => matchesAnchorContext(el, context))
1803
+ .map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
1700
1804
  .sort((a, b) => b.score - a.score);
1701
- if (!matches.length || matches[0].score < 58) return null;
1702
- // ponytail: ambiguous remounts stay hidden; explicit reattachment can handle identical twins later.
1703
- if (matches[1] && matches[0].score - matches[1].score < 12) return null;
1704
- return matches[0].el;
1805
+ if (context.candidateCount > 1) {
1806
+ const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
1807
+ if (peers.length !== context.candidateCount) {
1808
+ // A removed identical sibling must not move its comment to the survivor.
1809
+ const distinctive = matches[0] && (context.target.id || context.target.testId);
1810
+ if (!distinctive) return { element: null, status: "uncertain" };
1811
+ }
1812
+ }
1813
+ if (matches[0]?.score >= 58 && (!matches[1] || matches[0].score - matches[1].score >= 12)) {
1814
+ return { element: matches[0].el, status: "matched" };
1815
+ }
1816
+ // A closed/unmounted overlay is not evidence that its contents were deleted.
1817
+ if (context.layer && !candidates.some(el => {
1818
+ const layer = getPresentationLayer(el);
1819
+ return layer && matchesAnchorIdentity(layer, context.layer, false, false);
1820
+ })) return { element: null, status: "hidden" };
1821
+ const plausible = matches.length || candidates.some(el =>
1822
+ (context.target.id && el.id === context.target.id) ||
1823
+ (context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
1824
+ return { element: null, status: plausible ? "uncertain" : "missing" };
1825
+ }
1826
+
1827
+ function resolveByAnchorContext(context) {
1828
+ return resolveAnchor(context).element;
1705
1829
  }
1706
1830
 
1707
1831
  function getViewportBounds() {
@@ -1873,7 +1997,7 @@ function createAnchorMatcher({
1873
1997
  return { clientX, clientY };
1874
1998
  }
1875
1999
 
1876
- return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
2000
+ return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
1877
2001
  }
1878
2002
 
1879
2003
  const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
@@ -1901,7 +2025,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1901
2025
  SOFTWARE.
1902
2026
 
1903
2027
  */
1904
- function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function D(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function I(t,e,r){let n="http://www.w3.org/2000/svg",i=document.createElementNS(n,"svg"),o=document.createElementNS(n,"foreignObject");return i.setAttribute("width",`${e}`),i.setAttribute("height",`${r}`),i.setAttribute("viewBox",`0 0 ${e} ${r}`),o.setAttribute("width","100%"),o.setAttribute("height","100%"),o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("externalResourcesRequired","true"),i.appendChild(o),o.appendChild(t),D(i)}var l=(t,e)=>{if(t instanceof e)return!0;let r=Object.getPrototypeOf(t);return r===null?!1:r.constructor.name===e.name||l(r,e)};function U(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function V(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function M(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?U(r):V(r,n);return document.createTextNode(`${i}{${o}}`)}function R(t,e,r,n){let i=window.getComputedStyle(t,r),o=i.getPropertyValue("content");if(o===""||o==="none")return;let a=P();try{e.className=`${e.className} ${a}`}catch{return}let c=document.createElement("style");c.appendChild(M(a,r,i,n)),e.appendChild(c)}function C(t,e,r){R(t,e,":before",r),R(t,e,":after",r)}var T="application/font-woff",$="image/jpeg",F={woff:T,woff2:T,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:$,jpeg:$,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml",webp:"image/webp"};function H(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=H(t).toLowerCase();return F[e]||""}function O(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function k(t,e,r){let n=await(e?.captureFetch||fetch)(t,e);if(!n.ok)throw new Error(`Resource "${n.url}" not found`);let i=await n.blob();return new Promise((o,a)=>{let c=new FileReader;c.onerror=a,c.onloadend=()=>{try{o(r({res:n,result:c.result}))}catch(s){a(s)}},c.readAsDataURL(i)})}async function h(t,e,r){let n=r.resourceCache||{},i=t;if(n[i]!=null)return n[i];r.cacheBust&&(t+=(/\?/.test(t)?"&":"?")+new Date().getTime());let o;try{let a=await k(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),O(s)));o=x(a,e)}catch(a){o=r.imagePlaceholder||"";let c=`Failed to fetch resource: ${t}`;a&&(c=typeof a=="string"?a:a.message),c&&console.warn(c)}return n[i]=o,o}async function q(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function _(t,e){if(t.currentSrc){let o=document.createElement("canvas"),a=o.getContext("2d");o.width=t.clientWidth,o.height=t.clientHeight,a?.drawImage(t,0,0,o.width,o.height);let c=o.toDataURL();return w(c)}let r=t.poster,n=m(r),i=await h(r,n,e);return w(i)}async function v(t,e){var r;try{if(!((r=t?.contentDocument)===null||r===void 0)&&r.body)return await b(t.contentDocument.body,e,!0)}catch{}return t.cloneNode(!1)}async function B(t,e){return l(t,HTMLCanvasElement)?q(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?_(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?v(t,e):t.cloneNode(!1)}var G=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function j(t,e,r){var n,i;let o=[];return G(t)&&t.assignedNodes?o=u(t.assignedNodes()):l(t,HTMLIFrameElement)&&(!((n=t.contentDocument)===null||n===void 0)&&n.body)?o=u(t.contentDocument.body.childNodes):o=u(((i=t.shadowRoot)!==null&&i!==void 0?i:t).childNodes),o.length===0||l(t,HTMLVideoElement)||await o.reduce((a,c)=>a.then(()=>b(c,r)).then(s=>{s&&e.appendChild(s)}),Promise.resolve()),e}function z(t,e,r){let n=e.style;if(!n)return;let i=window.getComputedStyle(t);i.cssText?(n.cssText=i.cssText,n.transformOrigin=i.transformOrigin):y(r).forEach(o=>{let a=i.getPropertyValue(o);o==="font-size"&&a.endsWith("px")&&(a=`${parseFloat(a.substring(0,a.length-2))}px`),l(t,HTMLIFrameElement)&&o==="display"&&a==="inline"&&(a="block"),o==="d"&&e.getAttribute("d")&&(a=`path(${e.getAttribute("d")})`),n.setProperty(o,a,i.getPropertyPriority(o))})}function W(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function X(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function K(t,e,r){return l(e,Element)&&(z(t,e,r),C(t,e,r),W(t,e),X(t,e)),e}async function J(t,e){let r=t.querySelectorAll?t.querySelectorAll("use"):[];if(r.length===0)return t;let n={};for(let o=0;o<r.length;o++){let a=r[o],c=a.getAttribute("href")||a.getAttribute("xlink:href");if(c){let s=t.querySelector(c),d=document.querySelector(c);!s&&d&&!n[c]&&(n[c]=await b(d,e,!0))}}let i=Object.values(n);if(i.length){let o="http://www.w3.org/1999/xhtml",a=document.createElementNS(o,"svg");a.setAttribute("xmlns",o),a.style.position="absolute",a.style.width="0",a.style.height="0",a.style.overflow="hidden",a.style.display="none";let c=document.createElementNS(o,"defs");a.appendChild(c);for(let s=0;s<i.length;s++)c.appendChild(i[s]);t.appendChild(a)}return t}async function b(t,e,r){if(await e.beforeClone?.(t),!r&&e.filter&&!e.filter(t))return null;let n=e.placeholder?.(t);return n||Promise.resolve(t).then(i=>B(i,e)).then(i=>j(t,i,e)).then(async i=>(await e.beforeClone?.(t),K(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>J(i,e))}var L=/url\((['"]?)([^'"]+?)\1\)/g,Q=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,Y=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Z(t){let e=t.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`,"g")}function N(t){let e=[];return t.replace(L,(r,n,i)=>(e.push(i),r)),e.filter(r=>!p(r)&&!r.startsWith("#"))}async function tt(t,e,r,n,i){try{let o=r?S(e,r):e,a=m(e),c;if(i){let s=await i(o);c=x(s,a)}else c=await h(o,a,n);return t.replace(Z(e),`$1${c}$3`)}catch{}return t}function et(t,{preferredFontFormat:e}){return e?t.replace(Y,r=>{for(;;){let[n,,i]=Q.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(L)!==-1}async function E(t,e,r){if(!rt(t))return t;let n=et(t,r);return N(n).reduce((o,a)=>o.then(c=>tt(c,a,e,r)),Promise.resolve(n))}async function g(t,e,r){var n;let i=(n=e.style)===null||n===void 0?void 0:n.getPropertyValue(t);if(i){let o=await E(i,null,r);return e.style.setProperty(t,o,e.style.getPropertyPriority(t)),!0}return!1}async function nt(t,e){await g("background",t,e)||await g("background-image",t,e),await g("mask",t,e)||await g("-webkit-mask",t,e)||await g("mask-image",t,e)||await g("-webkit-mask-image",t,e)}async function it(t,e){let r=l(t,HTMLImageElement);if(!(r&&!p(t.src))&&!(l(t,SVGImageElement)&&!p(t.href.baseVal)))return;let n=r?t.src:t.href.baseVal,i=await h(n,m(n),e);await new Promise((o,a)=>{t.onload=o,t.onerror=e.onImageErrorHandler?(...s)=>{try{o(e.onImageErrorHandler(...s))}catch(d){a(d)}}:a;let c=t;c.decode&&(c.decode=o),c.loading==="lazy"&&(c.loading="eager"),r?(t.srcset="",t.src=i):t.href.baseVal=i})}async function ot(t,e){let n=u(t.childNodes).map(i=>A(i,e));await Promise.all(n).then(()=>t)}async function A(t,e){await e.beforeClone?.(t),l(t,Element)&&(await nt(t,e),await it(t,e),await ot(t,e))} return {cloneNode:b,embedImages:A,embedResources:E,nodeToDataURL:I}; })();
2028
+ function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function I(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function U(t,e,r){let n="http://www.w3.org/2000/svg",i=document.createElementNS(n,"svg"),o=document.createElementNS(n,"foreignObject");return i.setAttribute("width",`${e}`),i.setAttribute("height",`${r}`),i.setAttribute("viewBox",`0 0 ${e} ${r}`),o.setAttribute("width","100%"),o.setAttribute("height","100%"),o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("externalResourcesRequired","true"),i.appendChild(o),o.appendChild(t),I(i)}var l=(t,e)=>{if(t instanceof e)return!0;let r=Object.getPrototypeOf(t);return r===null?!1:r.constructor.name===e.name||l(r,e)};function V(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function M(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function F(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?V(r):M(r,n);return document.createTextNode(`${i}{${o}}`)}function R(t,e,r,n){let i=window.getComputedStyle(t,r),o=i.getPropertyValue("content");if(o===""||o==="none")return;let a=P();try{e.className=`${e.className} ${a}`}catch{return}let c=document.createElement("style");c.appendChild(F(a,r,i,n)),e.appendChild(c)}function C(t,e,r){R(t,e,":before",r),R(t,e,":after",r)}var T="application/font-woff",$="image/jpeg",H={woff:T,woff2:T,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:$,jpeg:$,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml",webp:"image/webp"};function O(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=O(t).toLowerCase();return H[e]||""}function k(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function q(t,e,r){let n=await(e?.captureFetch||fetch)(t,e);if(!n.ok)throw new Error(`Resource "${n.url}" not found`);let i=await n.blob();return new Promise((o,a)=>{let c=new FileReader;c.onerror=a,c.onloadend=()=>{try{o(r({res:n,result:c.result}))}catch(s){a(s)}},c.readAsDataURL(i)})}async function h(t,e,r){let n=r.resourceCache||{},i=t;if(n[i]!=null)return n[i];r.cacheBust&&(t+=(/\?/.test(t)?"&":"?")+new Date().getTime());let o;try{let a=await q(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),k(s)));o=x(a,e)}catch(a){o=r.imagePlaceholder||"";let c=`Failed to fetch resource: ${t}`;a&&(c=typeof a=="string"?a:a.message),c&&console.warn(c)}return n[i]=o,o}async function _(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function v(t,e){if(t.currentSrc){let o=document.createElement("canvas"),a=o.getContext("2d");o.width=t.clientWidth,o.height=t.clientHeight,a?.drawImage(t,0,0,o.width,o.height);let c=o.toDataURL();return w(c)}let r=t.poster,n=m(r),i=await h(r,n,e);return w(i)}async function B(t,e){var r;try{if(!((r=t?.contentDocument)===null||r===void 0)&&r.body)return await b(t.contentDocument.body,e,!0)}catch{}return t.cloneNode(!1)}async function G(t,e){return l(t,HTMLCanvasElement)?_(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?v(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?B(t,e):t.cloneNode(!1)}var j=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function z(t,e,r){var n,i;let o=[];return j(t)&&t.assignedNodes?o=u(t.assignedNodes()):l(t,HTMLIFrameElement)&&(!((n=t.contentDocument)===null||n===void 0)&&n.body)?o=u(t.contentDocument.body.childNodes):o=u(((i=t.shadowRoot)!==null&&i!==void 0?i:t).childNodes),o.length===0||l(t,HTMLVideoElement)||await o.reduce((a,c)=>a.then(()=>b(c,r)).then(s=>{s&&e.appendChild(s)}),Promise.resolve()),e}function W(t,e,r){let n=e.style;if(!n)return;let i=window.getComputedStyle(t);i.cssText?(n.cssText=i.cssText,n.transformOrigin=i.transformOrigin):y(r).forEach(o=>{let a=i.getPropertyValue(o);o==="font-size"&&a.endsWith("px")&&(a=`${parseFloat(a.substring(0,a.length-2))}px`),l(t,HTMLIFrameElement)&&o==="display"&&a==="inline"&&(a="block"),o==="d"&&e.getAttribute("d")&&(a=`path(${e.getAttribute("d")})`),n.setProperty(o,a,i.getPropertyPriority(o))})}function X(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function K(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function J(t,e,r){return l(e,Element)&&(W(t,e,r),C(t,e,r),X(t,e),K(t,e)),e}async function Q(t,e){let r=t.querySelectorAll?t.querySelectorAll("use"):[];if(r.length===0)return t;let n={};for(let o=0;o<r.length;o++){let a=r[o],c=a.getAttribute("href")||a.getAttribute("xlink:href");if(c){let s=t.querySelector(c),d=document.querySelector(c);!s&&d&&!n[c]&&(n[c]=await b(d,e,!0))}}let i=Object.values(n);if(i.length){let o="http://www.w3.org/1999/xhtml",a=document.createElementNS(o,"svg");a.setAttribute("xmlns",o),a.style.position="absolute",a.style.width="0",a.style.height="0",a.style.overflow="hidden",a.style.display="none";let c=document.createElementNS(o,"defs");a.appendChild(c);for(let s=0;s<i.length;s++)c.appendChild(i[s]);t.appendChild(a)}return t}async function b(t,e,r){if(await e.beforeClone?.(t),!r&&e.filter&&!e.filter(t))return null;let n=e.placeholder?.(t);return n||Promise.resolve(t).then(i=>G(i,e)).then(i=>z(t,i,e)).then(async i=>(await e.beforeClone?.(t),J(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>Q(i,e))}var A=/url\((['"]?)([^'"]+?)\1\)/g,L=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,Y=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Z(t){let e=t.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`,"g")}function N(t){let e=[];return t.replace(A,(r,n,i)=>(e.push(i),r)),e.filter(r=>!p(r)&&!r.startsWith("#"))}async function tt(t,e,r,n,i){try{let o=r?S(e,r):e,a=m(e),c;if(i){let s=await i(o);c=x(s,a)}else c=await h(o,a,n);return t.replace(Z(e),`$1${c}$3`)}catch{}return t}function et(t,{preferredFontFormat:e}){return e?t.replace(Y,r=>{for(L.lastIndex=0;;){let[n,,i]=L.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(A)!==-1}async function E(t,e,r){if(!rt(t))return t;let n=et(t,r);return N(n).reduce((o,a)=>o.then(c=>tt(c,a,e,r)),Promise.resolve(n))}async function g(t,e,r){var n;let i=(n=e.style)===null||n===void 0?void 0:n.getPropertyValue(t);if(i){let o=await E(i,null,r);return e.style.setProperty(t,o,e.style.getPropertyPriority(t)),!0}return!1}async function nt(t,e){await g("background",t,e)||await g("background-image",t,e),await g("mask",t,e)||await g("-webkit-mask",t,e)||await g("mask-image",t,e)||await g("-webkit-mask-image",t,e)}async function it(t,e){let r=l(t,HTMLImageElement);if(!(r&&!p(t.src))&&!(l(t,SVGImageElement)&&!p(t.href.baseVal)))return;let n=r?t.src:t.href.baseVal,i=await h(n,m(n),e);await new Promise((o,a)=>{t.onload=o,t.onerror=e.onImageErrorHandler?(...s)=>{try{o(e.onImageErrorHandler(...s))}catch(d){a(d)}}:a;let c=t;c.decode&&(c.decode=o),c.loading==="lazy"&&(c.loading="eager"),r?(t.srcset="",t.src=i):t.href.baseVal=i})}async function ot(t,e){let n=u(t.childNodes).map(i=>D(i,e));await Promise.all(n).then(()=>t)}async function D(t,e){await e.beforeClone?.(t),l(t,Element)&&(await nt(t,e),await it(t,e),await ot(t,e))} return {cloneNode:b,embedImages:D,embedResources:E,nodeToDataURL:U}; })();
1905
2029
  // Fetches belong to one capture. Never forward SDK credentials to a resource host.
1906
2030
  function createCaptureResources({ apiUrl, projectKey, getHeaders, signal }) {
1907
2031
  const cache = new Map();
@@ -2221,7 +2345,12 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2221
2345
  await untilAborted(captureEngine.embedImages(stage, options));
2222
2346
  checkContext();
2223
2347
  await yieldCapture();
2224
- const svg = await captureEngine.nodeToDataURL(stage, context.width, context.height);
2348
+ // Transparent page backgrounds reveal the browser canvas, not the image viewer.
2349
+ const viewport = document.createElement("div");
2350
+ viewport.style.backgroundColor = "Canvas";
2351
+ viewport.style.colorScheme = rootStyle.colorScheme;
2352
+ viewport.append(stage);
2353
+ const svg = await captureEngine.nodeToDataURL(viewport, context.width, context.height);
2225
2354
  const image = new Image();
2226
2355
  image.src = svg;
2227
2356
  await untilAborted(image.decode());
@@ -2236,9 +2365,6 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2236
2365
  let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
2237
2366
  if (!blob) throw new Error("Screenshot encoding failed");
2238
2367
  if (blob.size > 2800000) {
2239
- ctx.globalCompositeOperation = "destination-over";
2240
- ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("background-color");
2241
- ctx.fillRect(0, 0, context.width, context.height);
2242
2368
  blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
2243
2369
  if (!blob) throw new Error("Screenshot encoding failed");
2244
2370
  }
@@ -2369,7 +2495,7 @@ function createCommentThread(context) {
2369
2495
  renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
2370
2496
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
2371
2497
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
2372
- showAuthPrompt,
2498
+ showAuthPrompt, startThreadAgent,
2373
2499
  } = context;
2374
2500
 
2375
2501
  return function openCommentThread(
@@ -2432,16 +2558,16 @@ function createCommentThread(context) {
2432
2558
  };
2433
2559
 
2434
2560
  const saveStatus = async (nextStatus, control) => {
2435
- if (!context.canManage) return false;
2561
+ if (!context.canManage || comment.local_agent) return false;
2436
2562
  control.disabled = true;
2437
2563
  const savedStatus = await updateCommentStatus(comment, nextStatus);
2438
2564
  control.disabled = false;
2439
2565
  if (!savedStatus) return false;
2440
2566
 
2441
2567
  const savedStatusOption = getCommentStatus(savedStatus);
2442
- pin.dataset.status = savedStatusOption.value;
2443
- applyStatusDot(pinStatusDot, savedStatus);
2444
- pin.setAttribute(
2568
+ if (pin) pin.dataset.status = savedStatusOption.value;
2569
+ if (pinStatusDot) applyStatusDot(pinStatusDot, savedStatus);
2570
+ pin?.setAttribute(
2445
2571
  "aria-label",
2446
2572
  `Afficher le commentaire, statut ${savedStatusOption.label}`,
2447
2573
  );
@@ -2457,7 +2583,7 @@ function createCommentThread(context) {
2457
2583
  return true;
2458
2584
  };
2459
2585
 
2460
- statusMenuButton.disabled = !context.canManage;
2586
+ statusMenuButton.disabled = comment.local_agent || !context.canManage;
2461
2587
  statusMenuButton.onclick = (event) => {
2462
2588
  event.stopPropagation();
2463
2589
  const existingPopover = document.querySelector(
@@ -2606,7 +2732,7 @@ function createCommentThread(context) {
2606
2732
  );
2607
2733
  };
2608
2734
 
2609
- const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
2735
+ const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
2610
2736
 
2611
2737
  const startEditingEntry = (entry, item, persist) => {
2612
2738
  if (!isOwnCommentEntry(entry)) return;
@@ -2702,7 +2828,7 @@ function createCommentThread(context) {
2702
2828
  getCommentId(cachedComment) !== getCommentId(comment),
2703
2829
  );
2704
2830
  placedPins.delete(getCommentId(comment));
2705
- pin.remove();
2831
+ pin?.remove();
2706
2832
  closeBubbles();
2707
2833
  renderSidepanelComments();
2708
2834
  applyStatusFilters();
@@ -2749,11 +2875,19 @@ function createCommentThread(context) {
2749
2875
  };
2750
2876
  replies.forEach(appendReply);
2751
2877
  thread.appendChild(threadList);
2878
+ bubble.addEventListener('comty-agent-reply', (event) => {
2879
+ const reply = event.detail;
2880
+ if (!reply || replies.some(item => item.id === reply.id)) return;
2881
+ replies.push(reply);
2882
+ comment.replies = replies;
2883
+ appendReply(reply);
2884
+ threadList.scrollTop = threadList.scrollHeight;
2885
+ });
2752
2886
 
2753
2887
  const replyForm = document.createElement("div");
2754
2888
  replyForm.className = "comty-reply-form";
2755
2889
 
2756
- if (context.canWrite) {
2890
+ if (context.canWrite && !comment.local_agent) {
2757
2891
  replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
2758
2892
  const replyField = document.createElement("div");
2759
2893
  replyField.className = "comty-reply-field";
@@ -2796,6 +2930,18 @@ function createCommentThread(context) {
2796
2930
  appendReply(data.reply);
2797
2931
  replyInput.value = "";
2798
2932
  syncReplySize();
2933
+ if (/(^|\s)@agent\b/i.test(content)) {
2934
+ try {
2935
+ await startThreadAgent(comment, replies, content);
2936
+ } catch (error) {
2937
+ const notice = document.createElement('p');
2938
+ notice.className = 'comty-agent-thread-error';
2939
+ notice.setAttribute('role', 'alert');
2940
+ notice.textContent = `Agent could not start: ${error.message}`;
2941
+ replyForm.querySelector('.comty-agent-thread-error')?.remove();
2942
+ replyForm.appendChild(notice);
2943
+ }
2944
+ }
2799
2945
  requestAnimationFrame(() => {
2800
2946
  threadList.scrollTop = threadList.scrollHeight;
2801
2947
  });
@@ -2843,7 +2989,7 @@ function createCommentThread(context) {
2843
2989
  replyField.appendChild(replyInput);
2844
2990
  replyField.appendChild(replyButton);
2845
2991
  replyForm.appendChild(replyField);
2846
- } else {
2992
+ } else if (!comment.local_agent) {
2847
2993
  replyForm.classList.add("comty-reply-form-guest");
2848
2994
  const signInButton = document.createElement("button");
2849
2995
  signInButton.className = "comty-sign-in-reply";
@@ -2856,7 +3002,7 @@ function createCommentThread(context) {
2856
3002
  replyForm.appendChild(signInButton);
2857
3003
  }
2858
3004
 
2859
- thread.appendChild(replyForm);
3005
+ if (!comment.local_agent) thread.appendChild(replyForm);
2860
3006
  bubble.appendChild(thread);
2861
3007
  root.appendChild(bubble);
2862
3008
 
@@ -3093,7 +3239,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
3093
3239
  /** Initialize once per document; safe to import and call during server rendering. */
3094
3240
  function initComty(options = {}) {
3095
3241
  if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
3096
- const projectKey = options.projectKey;
3242
+ const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
3243
+ const agent = options.agent;
3244
+ if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
3245
+ return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
3246
+ }
3247
+ if (agent) {
3248
+ try {
3249
+ if (new URL(agent.url).origin !== window.location.origin) throw new Error();
3250
+ } catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
3251
+ }
3252
+ const projectKey = options.projectKey || (agent ? "local" : undefined);
3097
3253
  const apiUrl = options.apiUrl || "http://localhost:4000";
3098
3254
  if (typeof projectKey !== "string" || !projectKey.trim()) {
3099
3255
  return Promise.reject(new TypeError("Comty: projectKey is required"));
@@ -3105,12 +3261,14 @@ function initComty(options = {}) {
3105
3261
  if (!["http:", "https:"].includes(parsedApi.protocol)) {
3106
3262
  return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
3107
3263
  }
3108
- const configuration = JSON.stringify([projectKey, apiUrl]);
3264
+ const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
3109
3265
  const existing = window[instanceKey];
3110
3266
  if (existing) {
3111
3267
  return existing.configuration === configuration ? existing.ready
3112
3268
  : Promise.reject(new Error("Comty: already initialized with different options"));
3113
3269
  }
3270
+ const agentBridgeUrl = agent?.url;
3271
+ const localOnly = Boolean(agent && !options.projectKey);
3114
3272
  const ready = new Promise((resolve, reject) => {
3115
3273
  const mount = () => {
3116
3274
  try { resolve(mountComty()); } catch (error) { reject(error); }
@@ -3124,10 +3282,22 @@ function initComty(options = {}) {
3124
3282
 
3125
3283
  function mountComty() {
3126
3284
  let projectId = null;
3285
+ let localTasks = [];
3286
+ const deliveringAgentReplies = new Set();
3287
+ let localApprovals = [];
3288
+ const seenAgentApprovals = new Set();
3289
+ const seenFailedAgentTasks = new Set();
3290
+ let agentPanelOpen = false;
3291
+ let agentPanelSignature = "";
3292
+ let agentPanelTransition = 0;
3293
+ let agentConnectionError = "";
3294
+ let agentActionError = "";
3295
+ const agentPendingActions = new Set();
3127
3296
  const session = {
3128
3297
  mode: "off",
3129
- get comments() { return this.mode === "browse" || this.mode === "edit"; },
3130
- get edit() { return this.mode === "edit"; },
3298
+ get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
3299
+ get edit() { return this.mode === "edit" || this.mode === "agent"; },
3300
+ get agent() { return this.mode === "agent"; },
3131
3301
  get inspect() { return this.mode === "inspect"; },
3132
3302
  };
3133
3303
  const selectedStatusFilters = new Set();
@@ -3151,9 +3321,10 @@ function initComty(options = {}) {
3151
3321
  const authReturnStorageKey = `comty:${projectKey}:auth-return`;
3152
3322
  let sdkToken = "";
3153
3323
  const activeHoverCommentIds = new Set();
3154
- /** @type {null | 'edit' | 'browse'} */
3324
+ /** @type {null | 'edit' | 'browse' | 'agent'} */
3155
3325
  let sessionEntry = null;
3156
3326
  const placedPins = new Map();
3327
+ const workingOutlines = new Map();
3157
3328
  const placementDiagnostics = new Map();
3158
3329
  let inspectedElement = null;
3159
3330
  let selectedElement = null;
@@ -3167,8 +3338,8 @@ function initComty(options = {}) {
3167
3338
  const commentTargets = new Map();
3168
3339
 
3169
3340
  const {
3170
- matchesAnchorContext, resolveByAnchorContext,
3171
- isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3341
+ matchesAnchorContext,
3342
+ resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3172
3343
  } = createAnchorMatcher({
3173
3344
  getElementTextSignature,
3174
3345
  getElementOwnTextSignature,
@@ -3246,10 +3417,20 @@ function initComty(options = {}) {
3246
3417
  const inspectorBtn = toolButton("Inspect · Shift I", '<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3"/><path d="m9 9 8 4-4 1-1 4Z"/>', () => setSession("inspect"));
3247
3418
  const btn = toolButton("Comment · Shift C", '<path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8v.5Z"/>', () => {
3248
3419
  if (!canWrite) showAuthPrompt();
3249
- else if (session.inspect || !isSessionActive()) startEditSession();
3420
+ else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
3250
3421
  else if (!session.edit) setSession("edit", { entry: sessionEntry });
3251
3422
  });
3252
3423
  btn.id = "comty-toggle";
3424
+ const agentBtn = isLocalSite
3425
+ ? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
3426
+ if (session.agent) {
3427
+ if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
3428
+ agentPanelOpen = true;
3429
+ renderAgentPanel();
3430
+ }
3431
+ } else startAgentSession();
3432
+ })
3433
+ : null;
3253
3434
  const divider = document.createElement("span");
3254
3435
  divider.className = "comty-tool-divider";
3255
3436
  toolbar.appendChild(divider);
@@ -3259,7 +3440,7 @@ function initComty(options = {}) {
3259
3440
  const primaryTools = document.createElement("div");
3260
3441
  primaryTools.className = "comty-tool-primary";
3261
3442
  toolbar.insertBefore(primaryTools, browseBtn);
3262
- for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
3443
+ for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
3263
3444
  const inspectorTools = document.createElement("div");
3264
3445
  inspectorTools.className = "comty-tool-context";
3265
3446
  inspectorTools.hidden = true;
@@ -4112,6 +4293,50 @@ function initComty(options = {}) {
4112
4293
  let toolbarMotionFrame = 0;
4113
4294
  let toolbarAnimations = [];
4114
4295
  const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
4296
+ let workingIndicatorTimer = 0;
4297
+ function scheduleWorkingIndicators() {
4298
+ if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
4299
+ workingIndicatorTimer = setTimeout(() => {
4300
+ workingIndicatorTimer = 0;
4301
+ if (!root.isConnected) return;
4302
+ const indicators = root.querySelectorAll('.comty-working-indicator');
4303
+ if (!indicators.length) return;
4304
+ for (const indicator of indicators) {
4305
+ const previous = Number(indicator.dataset.activeDots.split(',')[0]);
4306
+ const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
4307
+ const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
4308
+ indicator.dataset.activeDots = `${first},${second}`;
4309
+ [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
4310
+ }
4311
+ scheduleWorkingIndicators();
4312
+ }, 600);
4313
+ }
4314
+ function createWorkingIndicator() {
4315
+ const indicator = document.createElement('span');
4316
+ indicator.className = 'comty-working-indicator';
4317
+ indicator.setAttribute('aria-hidden', 'true');
4318
+ indicator.dataset.activeDots = '3,5';
4319
+ for (let index = 0; index < 9; index++) {
4320
+ const dot = document.createElement('i');
4321
+ dot.toggleAttribute('data-active', index === 3 || index === 5);
4322
+ indicator.appendChild(dot);
4323
+ }
4324
+ scheduleWorkingIndicators();
4325
+ return indicator;
4326
+ }
4327
+ function resetWorkingIndicators() {
4328
+ clearTimeout(workingIndicatorTimer);
4329
+ workingIndicatorTimer = 0;
4330
+ if (reducedMotion.matches) {
4331
+ for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
4332
+ indicator.dataset.activeDots = '3,5';
4333
+ [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
4334
+ }
4335
+ }
4336
+ if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
4337
+ }
4338
+ reducedMotion.addEventListener('change', resetWorkingIndicators);
4339
+ document.addEventListener('visibilitychange', resetWorkingIndicators);
4115
4340
  const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
4116
4341
  function rememberToolbarPosition(left, top) {
4117
4342
  const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
@@ -4224,7 +4449,7 @@ function initComty(options = {}) {
4224
4449
 
4225
4450
  function prepareToolbarMotion() {
4226
4451
  if (toolbarMotionFrame) return;
4227
- const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools].filter(Boolean);
4452
+ const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
4228
4453
  // Move each group's background with its icons, including when interrupted.
4229
4454
  const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
4230
4455
  rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
@@ -4448,7 +4673,8 @@ function initComty(options = {}) {
4448
4673
  selectedElement = null;
4449
4674
  sessionEntry = null;
4450
4675
  session.mode = "off";
4451
- document.documentElement.classList.remove("comty-comments-active", "comty-comment-placement");
4676
+ syncPlacementCursor();
4677
+ document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
4452
4678
  hideInspector();
4453
4679
  setSidepanelOpen(false);
4454
4680
  closeBubbles();
@@ -4462,25 +4688,29 @@ function initComty(options = {}) {
4462
4688
  if (mode === "off") return;
4463
4689
  if (mode === "inspect") {
4464
4690
  session.mode = "inspect";
4691
+ document.documentElement.classList.add("comty-inspect-annotations");
4465
4692
  syncToolbar();
4693
+ renderCachedComments();
4466
4694
  ensureLiveTracking();
4467
4695
  return;
4468
4696
  }
4469
4697
 
4470
- if (session.edit && mode === "browse") {
4698
+ if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
4471
4699
  closeBubbles();
4472
4700
  }
4473
4701
  const hadComments = session.comments;
4474
4702
  session.mode = mode;
4475
4703
  sessionEntry = entry;
4704
+ document.documentElement.classList.remove("comty-inspect-annotations");
4476
4705
  document.documentElement.classList.toggle("comty-comments-active", session.comments);
4477
- document.documentElement.classList.toggle("comty-comment-placement", session.edit);
4706
+ syncPlacementCursor();
4478
4707
  if (session.edit) {
4479
4708
  setMotionEnabled(false);
4480
4709
  selectedElement = null;
4481
4710
  } else {
4482
4711
  hideInspector();
4483
4712
  }
4713
+ if (session.agent && sidepanelOpen) setSidepanelOpen(false);
4484
4714
  syncToolbar();
4485
4715
  ensureLiveTracking();
4486
4716
  if (renderComments || !hadComments) renderCachedComments();
@@ -4493,6 +4723,10 @@ function initComty(options = {}) {
4493
4723
  }
4494
4724
 
4495
4725
  function showAuthPrompt(forbidden = false, onReady = startEditSession) {
4726
+ if (localOnly) {
4727
+ showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
4728
+ return;
4729
+ }
4496
4730
  const guestAllowed = allowGuestComments;
4497
4731
  document.querySelector(".comty-auth-backdrop")?.remove();
4498
4732
  const opener = document.activeElement;
@@ -4614,14 +4848,16 @@ function initComty(options = {}) {
4614
4848
  }
4615
4849
 
4616
4850
  function syncToolbar() {
4617
- if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4851
+ if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4618
4852
  root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
4619
4853
  browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
4620
4854
  inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
4621
- btn.setAttribute("aria-pressed", String(session.edit));
4622
- commentTools.hidden = session.inspect;
4855
+ btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
4856
+ agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
4857
+ commentTools.hidden = session.inspect || session.agent;
4623
4858
  inspectorTools.hidden = !session.inspect;
4624
4859
  syncInspectorTools();
4860
+ renderAgentPanel();
4625
4861
  }
4626
4862
 
4627
4863
  const inspectionLayer = document.createElement("div");
@@ -5084,6 +5320,13 @@ function initComty(options = {}) {
5084
5320
  setSession("edit", { renderComments: true });
5085
5321
  }
5086
5322
 
5323
+ function startAgentSession() {
5324
+ if (!isLocalSite) return;
5325
+ if (session.agent) return;
5326
+ closeBubbles();
5327
+ setSession("agent", { renderComments: true });
5328
+ }
5329
+
5087
5330
  function toggleSidepanel() {
5088
5331
  if (sidepanelOpen) {
5089
5332
  setSidepanelOpen(false);
@@ -5107,7 +5350,7 @@ function initComty(options = {}) {
5107
5350
  showAuthPrompt();
5108
5351
  return;
5109
5352
  }
5110
- if (session.inspect || !isSessionActive()) {
5353
+ if (session.agent || session.inspect || !isSessionActive()) {
5111
5354
  startEditSession();
5112
5355
  return;
5113
5356
  }
@@ -5395,6 +5638,24 @@ function initComty(options = {}) {
5395
5638
  ancestors,
5396
5639
  layer: layer ? getElementFingerprint(layer) : null,
5397
5640
  };
5641
+ // React exposes this in development builds. Keep it as a hint: the coding
5642
+ // agent must confirm the component and variant in source before editing.
5643
+ const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
5644
+ let fiber = fiberKey ? el[fiberKey] : null;
5645
+ const components = [];
5646
+ while (fiber && components.length < 8) {
5647
+ const component = fiber.type;
5648
+ const name = component?.displayName || component?.name;
5649
+ if (name && typeof name === 'string') {
5650
+ const props = fiber.memoizedProps || {};
5651
+ const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
5652
+ ['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
5653
+ const source = fiber._debugSource;
5654
+ components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
5655
+ }
5656
+ fiber = fiber.return;
5657
+ }
5658
+ if (components.length) context.components = components;
5398
5659
  context.candidateCount = [...document.querySelectorAll(context.target.tag)]
5399
5660
  .filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
5400
5661
  return context;
@@ -5423,6 +5684,11 @@ function initComty(options = {}) {
5423
5684
  };
5424
5685
  }
5425
5686
 
5687
+ function getPagePath() {
5688
+ const { pathname, hash } = window.location;
5689
+ return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
5690
+ }
5691
+
5426
5692
  function resolveBySelector(selector) {
5427
5693
  if (!selector) return null;
5428
5694
  try {
@@ -5444,17 +5710,9 @@ function initComty(options = {}) {
5444
5710
  }
5445
5711
  const context = parseAnchorContext(comment);
5446
5712
  const exact = resolveBySelector(comment.selector || "body");
5447
- if (context?.candidateCount > 1) {
5448
- const count = [...document.querySelectorAll("*")].filter(candidate => candidate.tagName.toLowerCase() === context.target.tag)
5449
- .filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
5450
- if (count !== context.candidateCount) {
5451
- commentTargets.set(key, { element: null, signature, revision: anchorRevision });
5452
- return null;
5453
- }
5454
- }
5455
- let element = exact && matchesAnchorContext(exact, context) ? getAnchorElement(exact) : null;
5456
- if (!element) element = resolveByAnchorContext(context);
5457
- commentTargets.set(key, { element, signature, revision: anchorRevision });
5713
+ const resolution = resolveAnchor(context, exact);
5714
+ const element = resolution.element ? getAnchorElement(resolution.element) : null;
5715
+ commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
5458
5716
  return element;
5459
5717
  }
5460
5718
 
@@ -5525,9 +5783,14 @@ function initComty(options = {}) {
5525
5783
  return { deviceType: "desktop", deviceLabel: "Desktop" };
5526
5784
  }
5527
5785
 
5786
+ function syncPlacementCursor() {
5787
+ document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
5788
+ }
5789
+
5528
5790
  function closeBubbles() {
5529
5791
  const hadDraft = Boolean(draftAnchor);
5530
5792
  draftAnchor = null;
5793
+ syncPlacementCursor();
5531
5794
  activeHoverCommentIds.clear();
5532
5795
  if (!session.edit || hadDraft) hideInspector();
5533
5796
  document
@@ -5572,7 +5835,7 @@ function initComty(options = {}) {
5572
5835
  pending: commentId.startsWith("pending-"),
5573
5836
  project_id: projectId,
5574
5837
  domain: payload.host || window.location.host,
5575
- page_path: payload.pagePath || window.location.pathname,
5838
+ page_path: payload.pagePath || getPagePath(),
5576
5839
  selector: payload.selector || "body",
5577
5840
  rel_x: payload.relX,
5578
5841
  rel_y: payload.relY,
@@ -5597,7 +5860,7 @@ function initComty(options = {}) {
5597
5860
  replies: [],
5598
5861
  };
5599
5862
 
5600
- cachedPagePath = window.location.pathname;
5863
+ cachedPagePath = getPagePath();
5601
5864
  cachedComments = [
5602
5865
  ...cachedComments.filter(
5603
5866
  (cached) => getCommentId(cached) !== commentId,
@@ -5610,6 +5873,7 @@ function initComty(options = {}) {
5610
5873
  }
5611
5874
 
5612
5875
  function getCommentAuthorName(comment) {
5876
+ if (comment?.author_kind === 'agent') return 'Agent';
5613
5877
  return comment?.author_name || comment?.name || "Anonymous";
5614
5878
  }
5615
5879
 
@@ -5631,6 +5895,12 @@ function initComty(options = {}) {
5631
5895
  function createCommentAvatar(comment, extraClass = "") {
5632
5896
  const avatar = document.createElement("span");
5633
5897
  avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
5898
+ if (comment?.author_kind === 'agent') {
5899
+ avatar.classList.add('comty-agent-avatar');
5900
+ avatar.setAttribute('aria-label', 'Agent');
5901
+ avatar.innerHTML = '<svg viewBox="0 0 48 48" aria-hidden="true"><path d="M24 2C26.62 2 28.47 5.29 31.27 6.45C34.07 7.61 37.71 6.59 39.56 8.44C41.41 10.29 40.39 13.93 41.55 16.73C42.71 19.53 46 21.38 46 24C46 26.62 42.71 28.47 41.55 31.27C40.39 34.07 41.41 37.71 39.56 39.56C37.71 41.41 34.07 40.39 31.27 41.55C28.47 42.71 26.62 46 24 46C21.38 46 19.53 42.71 16.73 41.55C13.93 40.39 10.29 41.41 8.44 39.56C6.59 37.71 7.61 34.07 6.45 31.27C5.29 28.47 2 26.62 2 24C2 21.38 5.29 19.53 6.45 16.73C7.61 13.93 6.59 10.29 8.44 8.44C10.29 6.59 13.93 7.61 16.73 6.45C19.53 5.29 21.38 2 24 2Z" fill="currentColor"/><path fill="var(--comty-surface)" fill-rule="evenodd" d="M16 33 22.3 15h3.4L32 33h-3.3l-1.3-4h-6.8l-1.3 4H16Zm5.5-6.8h5L24 18.4l-2.5 7.8Z"/></svg>';
5902
+ return avatar;
5903
+ }
5634
5904
  const authorName = getCommentAuthorName(comment);
5635
5905
  const avatarUrl = getCommentAuthorAvatarUrl(comment);
5636
5906
 
@@ -5638,6 +5908,7 @@ function initComty(options = {}) {
5638
5908
  const image = document.createElement("img");
5639
5909
  image.src = avatarUrl;
5640
5910
  image.alt = "";
5911
+ image.draggable = false;
5641
5912
  image.onerror = () => {
5642
5913
  image.remove();
5643
5914
  avatar.innerText = authorName.charAt(0).toUpperCase();
@@ -5862,6 +6133,7 @@ function initComty(options = {}) {
5862
6133
  payload,
5863
6134
  preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
5864
6135
  };
6136
+ syncPlacementCursor();
5865
6137
  ensureLiveTracking();
5866
6138
 
5867
6139
  const pin = document.createElement("div");
@@ -5879,10 +6151,21 @@ function initComty(options = {}) {
5879
6151
 
5880
6152
  const textarea = document.createElement("textarea");
5881
6153
  textarea.className = "comty-create-textarea";
5882
- textarea.placeholder = "Add a comment";
6154
+ textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
5883
6155
  textarea.setAttribute("aria-label", textarea.placeholder);
5884
6156
  textarea.rows = 1;
5885
6157
 
6158
+ const scopePicker = document.createElement('label');
6159
+ scopePicker.className = 'comty-agent-scope';
6160
+ if (payload.kind === 'agent') {
6161
+ const input = document.createElement('input');
6162
+ input.type = 'checkbox';
6163
+ input.setAttribute('aria-label', 'Apply to all instances of this variant');
6164
+ const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
6165
+ scopePicker.title = 'Apply to all instances of this variant';
6166
+ scopePicker.append(input, track, document.createTextNode('Apply globally'));
6167
+ }
6168
+
5886
6169
  const actions = document.createElement("div");
5887
6170
  actions.className = "comty-create-actions";
5888
6171
 
@@ -5936,7 +6219,7 @@ function initComty(options = {}) {
5936
6219
  const submitBtn = document.createElement("button");
5937
6220
  submitBtn.type = "button";
5938
6221
  submitBtn.className = "comty-btn-submit";
5939
- submitBtn.setAttribute("aria-label", "Post comment");
6222
+ submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
5940
6223
  submitBtn.disabled = true;
5941
6224
  submitBtn.innerHTML = `
5942
6225
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -5953,49 +6236,62 @@ function initComty(options = {}) {
5953
6236
  textarea.disabled = true;
5954
6237
  linkBtn.disabled = true;
5955
6238
 
6239
+ if (payload.kind === "agent" && !agent) {
6240
+ showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
6241
+ }
5956
6242
  payload.content = content;
6243
+ if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
5957
6244
  payload.linkUrl = attachedLink || null;
5958
6245
  textarea.textContent = content;
5959
6246
 
5960
- const context = captureContext();
5961
- const point = getPinPoint(pin);
5962
- const pendingId = `pending-${crypto.randomUUID()}`;
5963
- // Paint the comment immediately. The network save never waits for capture work.
6247
+ const isAgentTask = payload.kind === "agent";
6248
+ const context = isAgentTask ? null : captureContext();
6249
+ const point = isAgentTask ? null : getPinPoint(pin);
6250
+ const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
5964
6251
  bubble.remove();
5965
6252
  pin.remove();
5966
6253
  if (draftAnchor === editorDraft) draftAnchor = null;
6254
+ syncPlacementCursor();
5967
6255
  hideInspector();
5968
- addLocalComment(pendingId, payload);
6256
+ if (!isAgentTask) addLocalComment(pendingId, payload);
5969
6257
  let post;
5970
6258
  try {
5971
- post = await fetch(`${apiUrl}/api/comments`, {
6259
+ post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
5972
6260
  method: "POST",
5973
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
5974
- body: JSON.stringify(payload),
6261
+ headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
6262
+ body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
5975
6263
  });
5976
- if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
6264
+ if (!post.ok) throw new Error(`Save failed (${post.status})`);
5977
6265
  const data = await post.json();
5978
- if (!data?.id) throw new Error("Comment save returned no ID");
5979
- cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5980
- if (context.url === location.href) {
5981
- const comment = addLocalComment(String(data.id), payload);
5982
- // Start only after the save and immediate UI update; the capture yields to input.
5983
- void uploadCommentScreenshot(comment, context, point);
5984
- } else void loadComments({ force: true });
6266
+ const id = isAgentTask ? data?.task?.id : data?.id;
6267
+ if (!id) throw new Error("Save returned no ID");
6268
+ if (isAgentTask) {
6269
+ void loadComments({ force: true });
6270
+ } else {
6271
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
6272
+ if (context.url === location.href) {
6273
+ const comment = addLocalComment(String(id), payload);
6274
+ void uploadCommentScreenshot(comment, context, point);
6275
+ } else void loadComments({ force: true });
6276
+ }
5985
6277
  } catch (error) {
5986
6278
  console.error("Comty: save failed", error);
5987
- cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5988
- renderSidepanelComments();
5989
- void renderComments(cachedComments, { retry: false });
6279
+ if (isAgentTask) showAgentBridgeError(error.message);
6280
+ else {
6281
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
6282
+ renderSidepanelComments();
6283
+ void renderComments(cachedComments, { retry: false });
6284
+ }
5990
6285
  // Keep the original editor and its text available for retry, including attachments.
5991
6286
  if (!draftAnchor) draftAnchor = editorDraft;
6287
+ syncPlacementCursor();
5992
6288
  root.append(pin, bubble);
5993
6289
  submitBtn.disabled = false;
5994
6290
  textarea.disabled = false;
5995
6291
  linkBtn.disabled = false;
5996
6292
  const errorMessage = document.createElement("p");
5997
6293
  errorMessage.setAttribute("role", "alert");
5998
- errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
6294
+ errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
5999
6295
  bubble.querySelector('[role="alert"]')?.remove();
6000
6296
  bubble.append(errorMessage);
6001
6297
  if (bubble.isConnected) textarea.focus();
@@ -6084,7 +6380,8 @@ function initComty(options = {}) {
6084
6380
  renderInspector();
6085
6381
  };
6086
6382
 
6087
- actions.append(linkBtn, submitBtn, linkEditor);
6383
+ if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
6384
+ else actions.append(linkBtn, submitBtn, linkEditor);
6088
6385
 
6089
6386
  bubble.appendChild(textarea);
6090
6387
  bubble.appendChild(attachment);
@@ -6097,6 +6394,219 @@ function initComty(options = {}) {
6097
6394
  setTimeout(() => textarea.focus(), 10);
6098
6395
  }
6099
6396
 
6397
+ function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
6398
+
6399
+ function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
6400
+ root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
6401
+ const toast = document.createElement("div");
6402
+ toast.className = "comty-auth-toast comty-agent-task-toast";
6403
+ toast.setAttribute("role", "alert");
6404
+ const issue = describeAgentError(message);
6405
+ toast.textContent = issue.title + ". " + issue.description;
6406
+ root.appendChild(toast);
6407
+ setTimeout(() => toast.remove(), 5000);
6408
+ }
6409
+
6410
+ function describeAgentError(error) {
6411
+ const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
6412
+ let value = error;
6413
+ for (let depth = 0; depth < 8; depth++) {
6414
+ if (typeof value === 'string') {
6415
+ try { value = JSON.parse(value); continue; } catch {}
6416
+ const start = value.indexOf('{'), end = value.lastIndexOf('}');
6417
+ if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
6418
+ break;
6419
+ }
6420
+ if (!value || typeof value !== 'object') break;
6421
+ value = value.error || value.message || value.cause || value.stderr || value;
6422
+ }
6423
+ const message = typeof value === 'string' ? value : '';
6424
+ const source = raw + ' ' + message;
6425
+ const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
6426
+ let title, description;
6427
+ if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
6428
+ title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
6429
+ } else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
6430
+ title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
6431
+ } else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
6432
+ title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
6433
+ } else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
6434
+ title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
6435
+ } else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
6436
+ title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
6437
+ } else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
6438
+ title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
6439
+ } else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
6440
+ title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
6441
+ } else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
6442
+ title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
6443
+ } else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
6444
+ title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
6445
+ } else if (/completion was not verified/i.test(source)) {
6446
+ title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
6447
+ } else {
6448
+ title = 'Task could not finish';
6449
+ description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 300) : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
6450
+ }
6451
+ return { title, description, raw, message };
6452
+ }
6453
+
6454
+ function renderAgentPanel() {
6455
+ if (!agent) return;
6456
+ const visibleTasks = [];
6457
+ const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
6458
+ if (latestResult) visibleTasks.push(latestResult);
6459
+ if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
6460
+ let panel = root.querySelector(".comty-agent-panel");
6461
+ if (!panel && !agentPanelOpen) return;
6462
+ if (!panel) {
6463
+ panel = document.createElement("section");
6464
+ panel.className = "comty-agent-panel";
6465
+ panel.setAttribute("aria-label", "Local agent tasks");
6466
+ panel.hidden = true;
6467
+ // Keep the task card above inspection overlays and below the toolbar.
6468
+ root.insertBefore(panel, toolbar);
6469
+ }
6470
+ const open = Boolean(agent) && agentPanelOpen;
6471
+ if (open) {
6472
+ if (panel.hidden) {
6473
+ const rect = toolbar.getBoundingClientRect();
6474
+ panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
6475
+ panel.dataset.closed = "";
6476
+ panel.hidden = false;
6477
+ panel.getBoundingClientRect();
6478
+ }
6479
+ if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
6480
+ } else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
6481
+ const transition = ++agentPanelTransition;
6482
+ panel.dataset.closed = "";
6483
+ const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
6484
+ if (reducedMotion.matches) finish();
6485
+ else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
6486
+ }
6487
+ panel.inert = !open;
6488
+ panel.setAttribute("aria-hidden", String(!open));
6489
+ const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
6490
+ if (signature === agentPanelSignature) return;
6491
+ agentPanelSignature = signature;
6492
+ root.querySelector('.comty-agent-error-popover')?.remove();
6493
+ const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
6494
+ const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
6495
+ const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
6496
+ const node = (tag, className, text) => {
6497
+ const element = document.createElement(tag);
6498
+ if (className) element.className = className;
6499
+ if (text != null) element.textContent = text;
6500
+ return element;
6501
+ };
6502
+ const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
6503
+ panel.replaceChildren();
6504
+ const header = node('header', 'comty-agent-panel-header');
6505
+ const heading = node('div', 'comty-agent-heading');
6506
+ heading.append(node('strong', '', 'Agent'));
6507
+ const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
6508
+ toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
6509
+ toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
6510
+ toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
6511
+ header.append(heading, toggle); panel.appendChild(header);
6512
+ const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
6513
+ const technicalDetails = (parent, raw, key) => {
6514
+ const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
6515
+ const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
6516
+ details.append(summary, node('pre', '', raw)); parent.appendChild(details);
6517
+ };
6518
+ const errorMessage = (parent, error, key) => {
6519
+ const issue = describeAgentError(error);
6520
+ const message = node('div', 'comty-agent-error');
6521
+ message.append(node('strong', '', issue.title), node('p', '', issue.description));
6522
+ parent.appendChild(message); technicalDetails(parent, issue.raw, key);
6523
+ };
6524
+ if (agentConnectionError || agentActionError) {
6525
+ const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
6526
+ errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
6527
+ }
6528
+ const action = (parent, label, path, input = {}, primary = false) => {
6529
+ const button = node('button', 'comty-agent-action', label); button.type = 'button';
6530
+ const key = path + JSON.stringify(input); button.dataset.focusKey = key;
6531
+ button.disabled = agentPendingActions.has(key);
6532
+ if (primary) button.dataset.primary = '';
6533
+ button.onclick = async () => {
6534
+ if (agentPendingActions.has(key)) return;
6535
+ agentActionError = '';
6536
+ agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
6537
+ try {
6538
+ const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
6539
+ if (!response.ok) {
6540
+ const body = await response.json().catch(() => null);
6541
+ throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
6542
+ }
6543
+ await loadComments({ force: true });
6544
+ } catch (error) { agentActionError = error.message; }
6545
+ finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
6546
+ };
6547
+ parent.appendChild(button);
6548
+ };
6549
+ for (const approval of localApprovals) {
6550
+ const section = node('div', 'comty-agent-item');
6551
+ section.append(node('h3', '', 'Approval required'));
6552
+ const task = localTasks.find(task => task.id === approval.taskId);
6553
+ if (task) section.appendChild(node('p', '', task.content));
6554
+ section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
6555
+ const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
6556
+ if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
6557
+ technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
6558
+ const actions = node('div', 'comty-agent-actions');
6559
+ action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
6560
+ action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
6561
+ section.appendChild(actions); content.appendChild(section);
6562
+ }
6563
+ const list = node('div', 'comty-agent-list'); content.appendChild(list);
6564
+ const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
6565
+ content.addEventListener('scroll', hideErrorPopover, { passive: true });
6566
+ for (const [index, task] of visibleTasks.reverse().entries()) {
6567
+ const failed = Boolean(task.error) || task.status === 'in_review';
6568
+ const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
6569
+ const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
6570
+ const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
6571
+ dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
6572
+ const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
6573
+ row.append(dot, name);
6574
+ if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
6575
+ const actions = node('div', 'comty-agent-row-actions');
6576
+ if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
6577
+ if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
6578
+ if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
6579
+ if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
6580
+ if (actions.childElementCount) row.appendChild(actions);
6581
+ row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
6582
+ if (failed) {
6583
+ const tooltipId = `comty-agent-error-${index}`;
6584
+ const showErrorPopover = () => {
6585
+ hideErrorPopover();
6586
+ const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
6587
+ const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
6588
+ popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
6589
+ root.appendChild(popover);
6590
+ const rowRect = row.getBoundingClientRect();
6591
+ const popoverRect = popover.getBoundingClientRect();
6592
+ const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
6593
+ popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
6594
+ popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
6595
+ };
6596
+ row.addEventListener('mouseenter', showErrorPopover);
6597
+ row.addEventListener('mouseleave', hideErrorPopover);
6598
+ }
6599
+ list.appendChild(row);
6600
+ if (task.result?.summary) {
6601
+ const answer = node('div', 'comty-agent-answer');
6602
+ answer.append(brand(), node('p', '', task.result.summary));
6603
+ list.appendChild(answer);
6604
+ }
6605
+ }
6606
+ content.scrollTop = scrollTop;
6607
+ if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
6608
+ }
6609
+
6100
6610
  function setSidepanelOpen(open) {
6101
6611
  sidepanelOpen = open;
6102
6612
  sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
@@ -6132,12 +6642,52 @@ function initComty(options = {}) {
6132
6642
 
6133
6643
  function openCommentFromPanel(comment) {
6134
6644
  const target = resolveCommentElement(comment);
6135
- if (!target || isElementTreeHidden(target)) return;
6645
+ if (!target || isElementTreeHidden(target)) {
6646
+ openDetachedThread(comment);
6647
+ return;
6648
+ }
6136
6649
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
6137
6650
  target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
6651
+ const rect = target.getBoundingClientRect();
6652
+ const relX = Number(comment.rel_x ?? comment.relX);
6653
+ const relY = Number(comment.rel_y ?? comment.relY);
6654
+ // Large page containers must reveal the annotation point, not their center.
6655
+ window.scrollBy({
6656
+ left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
6657
+ top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
6658
+ behavior: "instant",
6659
+ });
6138
6660
  syncCommentAnchors();
6139
6661
  const pin = findPlacedPin(comment);
6140
6662
  if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
6663
+ else openDetachedThread(comment);
6664
+ }
6665
+
6666
+ function openDetachedThread(comment) {
6667
+ openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
6668
+ const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
6669
+ if (bubble) {
6670
+ bubble.dataset.detached = "";
6671
+ positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
6672
+ }
6673
+ }
6674
+
6675
+ function updateAnchorBadge(item, comment) {
6676
+ resolveCommentElement(comment);
6677
+ const result = commentTargets.get(String(comment.id));
6678
+ const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
6679
+ const status = legacy && !result?.element ? "uncertain" : result?.status;
6680
+ const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
6681
+ let badge = item.querySelector(".comty-anchor-badge");
6682
+ if (!label) { badge?.remove(); return; }
6683
+ if (!badge) {
6684
+ badge = document.createElement("span");
6685
+ badge.className = "comty-anchor-badge";
6686
+ item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
6687
+ }
6688
+ if (badge.textContent !== label) badge.textContent = label;
6689
+ badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
6690
+ status === "missing" ? "The target is not present on this page. The comment is still available." : "The target cannot be identified reliably. No pin is placed.";
6141
6691
  }
6142
6692
 
6143
6693
  let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
@@ -6150,6 +6700,7 @@ function initComty(options = {}) {
6150
6700
  ? sidepanelSearch.value.trim().toLowerCase()
6151
6701
  : "";
6152
6702
  let list = cachedComments.filter((comment) => {
6703
+ if (comment.local_agent || comment.kind === "agent") return false;
6153
6704
  if (
6154
6705
  search &&
6155
6706
  !`${comment.content ?? ""} ${getCommentLabel(comment)}`
@@ -6196,6 +6747,7 @@ function initComty(options = {}) {
6196
6747
  const item = document.createElement("button");
6197
6748
  item.type = "button";
6198
6749
  item.className = "comty-sidepanel-item";
6750
+ item.dataset.commentId = String(comment.id);
6199
6751
  item.disabled = Boolean(comment.pending);
6200
6752
  item.innerHTML = `
6201
6753
  <span class="comty-sidepanel-item-status" aria-hidden="true"></span>
@@ -6219,8 +6771,13 @@ function initComty(options = {}) {
6219
6771
  if (metaEl instanceof HTMLElement) {
6220
6772
  metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
6221
6773
  metaEl.title = getCommentLabel(comment);
6774
+ if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
6775
+ metaEl.innerText += " · Original page unknown";
6776
+ }
6222
6777
  }
6223
6778
 
6779
+ updateAnchorBadge(item, comment);
6780
+
6224
6781
  item.onclick = (event) => {
6225
6782
  event.stopPropagation();
6226
6783
  openCommentFromPanel(comment);
@@ -6336,14 +6893,15 @@ function initComty(options = {}) {
6336
6893
  createStatusFilters();
6337
6894
 
6338
6895
  async function updateCommentStatus(comment, status) {
6339
- if (!projectId || !comment?.id) return false;
6896
+ if (comment?.local_agent || !projectId || !comment?.id) return false;
6340
6897
 
6341
6898
  let res;
6342
6899
  try {
6343
- res = await fetch(`${apiUrl}/api/comments/${comment.id}/status`, {
6900
+ const local = comment.local_agent === true;
6901
+ res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
6344
6902
  method: "POST",
6345
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
6346
- body: JSON.stringify({
6903
+ headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
6904
+ body: JSON.stringify(local ? { status } : {
6347
6905
  projectKey,
6348
6906
  host: window.location.host,
6349
6907
  status,
@@ -6360,7 +6918,7 @@ function initComty(options = {}) {
6360
6918
  }
6361
6919
 
6362
6920
  const data = await res.json();
6363
- comment.status = getCommentStatus(data.status).value;
6921
+ comment.status = getCommentStatus(data.task?.status ?? data.status).value;
6364
6922
  return comment.status;
6365
6923
  }
6366
6924
 
@@ -6552,6 +7110,23 @@ function initComty(options = {}) {
6552
7110
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
6553
7111
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
6554
7112
  showAuthPrompt,
7113
+ startThreadAgent: async (comment, replies, content) => {
7114
+ if (!agent) throw new Error('Start the local agent to reply with @Agent');
7115
+ if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
7116
+ const transcript = [
7117
+ `${getCommentAuthorName(comment)}: ${comment.content}`,
7118
+ ...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
7119
+ ].join('\n').slice(-24000);
7120
+ const response = await fetch(`${agentBridgeUrl}/tasks`, {
7121
+ method: 'POST', headers: agentHeaders(),
7122
+ body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
7123
+ selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
7124
+ domElement: comment.dom_element || '', viewport: comment.viewport || '',
7125
+ anchorContext: comment.anchor_context || '', scope: 'element',
7126
+ thread: { commentId: String(comment.id), transcript } }),
7127
+ });
7128
+ if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
7129
+ },
6555
7130
  get canWrite() { return canWrite; },
6556
7131
  get canManage() { return canManage; },
6557
7132
  get allowGuestComments() { return allowGuestComments; },
@@ -6752,42 +7327,112 @@ function initComty(options = {}) {
6752
7327
  function reconcileComments(comments) {
6753
7328
  const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
6754
7329
  cachedComments = comments
6755
- .filter(comment => comment.kind !== "agent" && !comment.local_agent)
7330
+ .filter(comment => comment.kind !== "agent" || comment.status !== "done")
6756
7331
  .map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
6757
7332
  for (const comment of previous.values()) {
6758
7333
  if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
6759
7334
  }
6760
7335
  }
6761
7336
 
7337
+ async function deliverThreadReply(task) {
7338
+ if (deliveringAgentReplies.has(task.id)) return;
7339
+ deliveringAgentReplies.add(task.id);
7340
+ try {
7341
+ const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
7342
+ method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
7343
+ body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
7344
+ });
7345
+ if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
7346
+ const { reply } = await response.json();
7347
+ const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
7348
+ if (local && !local.replies?.some(item => item.id === reply.id)) {
7349
+ local.replies ||= [];
7350
+ local.replies.push(reply);
7351
+ }
7352
+ for (const bubble of root.querySelectorAll('.comty-bubble')) {
7353
+ if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
7354
+ }
7355
+ const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
7356
+ if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
7357
+ localTasks = localTasks.filter(item => item.id !== task.id);
7358
+ } catch (error) {
7359
+ agentActionError = error.message;
7360
+ console.error('Comty: Agent reply delivery failed', error);
7361
+ } finally {
7362
+ deliveringAgentReplies.delete(task.id);
7363
+ agentPanelSignature = '';
7364
+ renderAgentPanel();
7365
+ }
7366
+ }
7367
+
6762
7368
  async function loadComments({ force = false } = {}) {
6763
- if (!projectId) return;
6764
- const pagePath = window.location.pathname;
7369
+ if (!projectId && !agent) return;
7370
+ const pagePath = getPagePath();
6765
7371
  if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
6766
7372
  if (pendingCommentsLoad) {
6767
7373
  await pendingCommentsLoad;
6768
- if (!force) return;
7374
+ if (!force && cachedPagePath === pagePath) return;
6769
7375
  }
6770
7376
  pendingCommentsLoad = (async () => {
6771
- let comments = cachedPagePath === pagePath ? cachedComments : [];
7377
+ let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
6772
7378
  if (projectId) {
6773
7379
  try {
6774
- const url = new URL(`${apiUrl}/api/comments`);
6775
- url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath, ...(force ? { _t: String(Date.now()) } : {}) });
6776
- const res = await fetch(url);
6777
- if (!res.ok) throw new Error(await res.text());
6778
- const data = await res.json();
6779
- if (Array.isArray(data.comments)) comments = data.comments;
7380
+ const paths = [...new Set([pagePath, window.location.pathname])];
7381
+ const batches = await Promise.all(paths.map(async path => {
7382
+ const url = new URL(`${apiUrl}/api/comments`);
7383
+ url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
7384
+ const res = await fetch(url);
7385
+ if (!res.ok) throw new Error(await res.text());
7386
+ const data = await res.json();
7387
+ return Array.isArray(data.comments) ? data.comments : [];
7388
+ }));
7389
+ comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
6780
7390
  } catch (error) { console.error("Comty: list comments failed", error); }
6781
7391
  }
7392
+ if (agent) {
7393
+ try {
7394
+ const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
7395
+ if (!res.ok) throw new Error("Local companion unavailable");
7396
+ const data = await res.json();
7397
+ if (Array.isArray(data.tasks)) localTasks = data.tasks;
7398
+ const failedTasks = localTasks.filter(task => task.status === 'in_review');
7399
+ if (failedTasks.some(task => !seenFailedAgentTasks.has(task.id))) agentPanelOpen = true;
7400
+ seenFailedAgentTasks.clear();
7401
+ for (const task of failedTasks) seenFailedAgentTasks.add(task.id);
7402
+ if (Array.isArray(data.approvals)) {
7403
+ localApprovals = data.approvals;
7404
+ if (localApprovals.some(approval => !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
7405
+ seenAgentApprovals.clear();
7406
+ for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
7407
+ }
7408
+ agentConnectionError = "";
7409
+ } catch (error) {
7410
+ agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
7411
+ console.error("Comty: local tasks failed", error);
7412
+ }
7413
+ for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
7414
+ void deliverThreadReply(task);
7415
+ }
7416
+ comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && !["done", "cancel"].includes(task.status)));
7417
+ renderAgentPanel();
7418
+ }
7419
+ if (pagePath !== getPagePath()) return;
6782
7420
  reconcileComments(comments);
6783
7421
  cachedPagePath = pagePath;
6784
7422
  renderSidepanelComments();
6785
7423
  await renderComments(cachedComments);
7424
+ syncAgentWorkingOutlines();
6786
7425
  ensureLiveTracking();
6787
7426
  })();
6788
7427
  try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
6789
7428
  }
6790
7429
 
7430
+ if (agent) {
7431
+ setInterval(() => {
7432
+ if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
7433
+ }, 2000);
7434
+ }
7435
+
6791
7436
  function getPinPoint(pin) {
6792
7437
  return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
6793
7438
  }
@@ -6815,7 +7460,7 @@ function initComty(options = {}) {
6815
7460
  }
6816
7461
 
6817
7462
  function syncCommentAnchors() {
6818
- if (!session.comments) return;
7463
+ if (!session.comments && !session.inspect) return;
6819
7464
  const modal = getTopActiveModal();
6820
7465
  for (const comment of cachedComments) {
6821
7466
  const id = String(comment.id);
@@ -6826,7 +7471,7 @@ function initComty(options = {}) {
6826
7471
  placePin(comment, modal);
6827
7472
  pin = placedPins.get(id);
6828
7473
  }
6829
- const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
7474
+ const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
6830
7475
  // A pin can remove its target's :hover style as the pointer enters it.
6831
7476
  if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
6832
7477
  (activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
@@ -6841,6 +7486,10 @@ function initComty(options = {}) {
6841
7486
  if (pin.style.top !== y) pin.style.top = y;
6842
7487
  for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
6843
7488
  }
7489
+ if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
7490
+ const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
7491
+ if (comment) updateAnchorBadge(item, comment);
7492
+ }
6844
7493
  if (draftAnchor) {
6845
7494
  if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
6846
7495
  const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
@@ -6854,16 +7503,50 @@ function initComty(options = {}) {
6854
7503
  }
6855
7504
  }
6856
7505
 
7506
+ function syncAgentWorkingOutlines() {
7507
+ const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
7508
+ const ids = new Set(working.map(task => String(task.id)));
7509
+ for (const [id, outline] of workingOutlines) {
7510
+ if (ids.has(id)) continue;
7511
+ outline.remove(); workingOutlines.delete(id);
7512
+ }
7513
+ const modal = getTopActiveModal();
7514
+ for (const task of working) {
7515
+ const id = String(task.id);
7516
+ let outline = workingOutlines.get(id);
7517
+ if (!outline) {
7518
+ outline = document.createElement("div");
7519
+ outline.className = "comty-agent-working";
7520
+ outline.setAttribute("aria-label", "Agent working on this element");
7521
+ const badge = document.createElement("span");
7522
+ badge.className = "comty-agent-working-badge";
7523
+ badge.append(createWorkingIndicator(), "Working");
7524
+ outline.appendChild(badge); root.appendChild(outline);
7525
+ workingOutlines.set(id, outline);
7526
+ }
7527
+ const target = resolveCommentElement(task);
7528
+ const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
7529
+ outline.hidden = !point;
7530
+ if (!point) continue;
7531
+ const rect = target.getBoundingClientRect();
7532
+ setMeasuredRect(outline, rect);
7533
+ outline.toggleAttribute("data-badge-below", rect.top < 32);
7534
+ }
7535
+ }
7536
+
6857
7537
  function onFrame() {
6858
7538
  if (motion.enabled) updateMotion();
6859
- if (session.comments) syncCommentAnchors();
7539
+ if (session.comments || session.inspect) syncCommentAnchors();
7540
+ if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
7541
+ syncAgentWorkingOutlines();
7542
+ }
6860
7543
  if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
6861
7544
  if (session.inspect && rulersEnabled) renderRulers();
6862
7545
  if (session.inspect && rulersEnabled) measurements.render();
6863
7546
  }
6864
7547
 
6865
7548
  function ensureLiveTracking() {
6866
- const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
7549
+ const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
6867
7550
  if (!active || document.hidden) {
6868
7551
  if (liveFrame) cancelAnimationFrame(liveFrame);
6869
7552
  liveFrame = null;
@@ -6883,13 +7566,14 @@ function initComty(options = {}) {
6883
7566
  syncOverlayHost();
6884
7567
 
6885
7568
  function renderCachedComments() {
6886
- if (!session.comments && !session.inspect && !session.edit) return;
7569
+ if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
6887
7570
  if (resizeRenderFrame) {
6888
7571
  window.cancelAnimationFrame(resizeRenderFrame);
6889
7572
  }
6890
7573
  resizeRenderFrame = window.requestAnimationFrame(() => {
6891
7574
  resizeRenderFrame = null;
6892
7575
  syncCommentAnchors();
7576
+ syncAgentWorkingOutlines();
6893
7577
  renderInspector();
6894
7578
  ensureLiveTracking();
6895
7579
  });
@@ -6931,6 +7615,29 @@ function initComty(options = {}) {
6931
7615
  characterData: true,
6932
7616
  });
6933
7617
 
7618
+ let activePagePath = getPagePath();
7619
+ function handleRouteChange() {
7620
+ const next = getPagePath();
7621
+ if (next === activePagePath) return;
7622
+ activePagePath = next;
7623
+ closeBubbles();
7624
+ commentTargets.clear();
7625
+ reconcileComments([]);
7626
+ void renderComments([], { retry: false });
7627
+ renderSidepanelComments();
7628
+ void loadComments({ force: true });
7629
+ }
7630
+ window.addEventListener("hashchange", handleRouteChange);
7631
+ window.addEventListener("popstate", handleRouteChange);
7632
+ for (const method of ["pushState", "replaceState"]) {
7633
+ const original = history[method];
7634
+ history[method] = function (...args) {
7635
+ const result = original.apply(this, args);
7636
+ handleRouteChange();
7637
+ return result;
7638
+ };
7639
+ }
7640
+
6934
7641
  document.addEventListener("scroll", handleHostLayoutEvent, {
6935
7642
  capture: true,
6936
7643
  passive: true,
@@ -7068,13 +7775,14 @@ function initComty(options = {}) {
7068
7775
  const y = e.clientY;
7069
7776
 
7070
7777
  const anchorContext = getAnchorContext(el);
7778
+ if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
7071
7779
 
7072
7780
  const payload = {
7073
7781
  ...getDeviceContext(),
7074
7782
  projectId,
7075
- kind: "comment",
7783
+ kind: session.agent ? "agent" : "comment",
7076
7784
  host: window.location.host,
7077
- pagePath: window.location.pathname,
7785
+ pagePath: getPagePath(),
7078
7786
  selector: getSelector(el),
7079
7787
  relX,
7080
7788
  relY,
@@ -7172,6 +7880,7 @@ function initComty(options = {}) {
7172
7880
  window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
7173
7881
 
7174
7882
  async function boot() {
7883
+ if (localOnly) { await loadComments(); return; }
7175
7884
  const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
7176
7885
  method: "POST",
7177
7886
  headers: getSdkHeaders({ "Content-Type": "application/json" }),
@@ -7184,6 +7893,7 @@ function initComty(options = {}) {
7184
7893
 
7185
7894
  if (!res.ok) {
7186
7895
  console.error("Comty: bootstrap failed", await res.text());
7896
+ if (agent) await loadComments();
7187
7897
  return;
7188
7898
  }
7189
7899
 
@@ -7228,16 +7938,30 @@ function initComty(options = {}) {
7228
7938
 
7229
7939
  if (typeof options.onTestReady === "function") {
7230
7940
  options.onTestReady({
7231
- getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
7941
+ getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
7232
7942
  getDistanceSegments, getInspectRows, syncCommentAnchors,
7233
7943
  setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
7234
7944
  startEditSession, startBrowseSession, endSession, showInspector,
7235
- motion, setMotionEnabled, updateMotion,
7945
+ motion, setMotionEnabled, updateMotion, describeAgentError,
7946
+ previewAgent(tasks, approvals = [], error = "") {
7947
+ localTasks = tasks;
7948
+ localApprovals = approvals;
7949
+ agentConnectionError = error;
7950
+ startAgentSession();
7951
+ agentPanelOpen = true;
7952
+ agentPanelSignature = "";
7953
+ renderAgentPanel();
7954
+ },
7236
7955
  seed(comments) {
7237
7956
  reconcileComments(comments);
7238
7957
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
7239
7958
  return renderComments(cachedComments, { retry: false });
7240
7959
  },
7960
+ seedAgentTasks(tasks) {
7961
+ localTasks = tasks;
7962
+ syncAgentWorkingOutlines();
7963
+ ensureLiveTracking();
7964
+ },
7241
7965
  setCanWrite(value) { canWrite = value; canManage = value; },
7242
7966
  invalidate() { anchorRevision += 1; },
7243
7967
  });
@@ -7245,6 +7969,7 @@ function initComty(options = {}) {
7245
7969
 
7246
7970
  return boot().catch(error => {
7247
7971
  console.error("Comty: bootstrap failed", error);
7972
+ if (agent) return loadComments();
7248
7973
  });
7249
7974
  }
7250
7975
  }