@comty.io/core 0.1.5 → 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.
@@ -2371,7 +2495,7 @@ function createCommentThread(context) {
2371
2495
  renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
2372
2496
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
2373
2497
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
2374
- showAuthPrompt,
2498
+ showAuthPrompt, startThreadAgent,
2375
2499
  } = context;
2376
2500
 
2377
2501
  return function openCommentThread(
@@ -2434,16 +2558,16 @@ function createCommentThread(context) {
2434
2558
  };
2435
2559
 
2436
2560
  const saveStatus = async (nextStatus, control) => {
2437
- if (!context.canManage) return false;
2561
+ if (!context.canManage || comment.local_agent) return false;
2438
2562
  control.disabled = true;
2439
2563
  const savedStatus = await updateCommentStatus(comment, nextStatus);
2440
2564
  control.disabled = false;
2441
2565
  if (!savedStatus) return false;
2442
2566
 
2443
2567
  const savedStatusOption = getCommentStatus(savedStatus);
2444
- pin.dataset.status = savedStatusOption.value;
2445
- applyStatusDot(pinStatusDot, savedStatus);
2446
- pin.setAttribute(
2568
+ if (pin) pin.dataset.status = savedStatusOption.value;
2569
+ if (pinStatusDot) applyStatusDot(pinStatusDot, savedStatus);
2570
+ pin?.setAttribute(
2447
2571
  "aria-label",
2448
2572
  `Afficher le commentaire, statut ${savedStatusOption.label}`,
2449
2573
  );
@@ -2459,7 +2583,7 @@ function createCommentThread(context) {
2459
2583
  return true;
2460
2584
  };
2461
2585
 
2462
- statusMenuButton.disabled = !context.canManage;
2586
+ statusMenuButton.disabled = comment.local_agent || !context.canManage;
2463
2587
  statusMenuButton.onclick = (event) => {
2464
2588
  event.stopPropagation();
2465
2589
  const existingPopover = document.querySelector(
@@ -2608,7 +2732,7 @@ function createCommentThread(context) {
2608
2732
  );
2609
2733
  };
2610
2734
 
2611
- const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
2735
+ const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
2612
2736
 
2613
2737
  const startEditingEntry = (entry, item, persist) => {
2614
2738
  if (!isOwnCommentEntry(entry)) return;
@@ -2704,7 +2828,7 @@ function createCommentThread(context) {
2704
2828
  getCommentId(cachedComment) !== getCommentId(comment),
2705
2829
  );
2706
2830
  placedPins.delete(getCommentId(comment));
2707
- pin.remove();
2831
+ pin?.remove();
2708
2832
  closeBubbles();
2709
2833
  renderSidepanelComments();
2710
2834
  applyStatusFilters();
@@ -2751,11 +2875,19 @@ function createCommentThread(context) {
2751
2875
  };
2752
2876
  replies.forEach(appendReply);
2753
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
+ });
2754
2886
 
2755
2887
  const replyForm = document.createElement("div");
2756
2888
  replyForm.className = "comty-reply-form";
2757
2889
 
2758
- if (context.canWrite) {
2890
+ if (context.canWrite && !comment.local_agent) {
2759
2891
  replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
2760
2892
  const replyField = document.createElement("div");
2761
2893
  replyField.className = "comty-reply-field";
@@ -2798,6 +2930,18 @@ function createCommentThread(context) {
2798
2930
  appendReply(data.reply);
2799
2931
  replyInput.value = "";
2800
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
+ }
2801
2945
  requestAnimationFrame(() => {
2802
2946
  threadList.scrollTop = threadList.scrollHeight;
2803
2947
  });
@@ -2845,7 +2989,7 @@ function createCommentThread(context) {
2845
2989
  replyField.appendChild(replyInput);
2846
2990
  replyField.appendChild(replyButton);
2847
2991
  replyForm.appendChild(replyField);
2848
- } else {
2992
+ } else if (!comment.local_agent) {
2849
2993
  replyForm.classList.add("comty-reply-form-guest");
2850
2994
  const signInButton = document.createElement("button");
2851
2995
  signInButton.className = "comty-sign-in-reply";
@@ -2858,7 +3002,7 @@ function createCommentThread(context) {
2858
3002
  replyForm.appendChild(signInButton);
2859
3003
  }
2860
3004
 
2861
- thread.appendChild(replyForm);
3005
+ if (!comment.local_agent) thread.appendChild(replyForm);
2862
3006
  bubble.appendChild(thread);
2863
3007
  root.appendChild(bubble);
2864
3008
 
@@ -3095,7 +3239,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
3095
3239
  /** Initialize once per document; safe to import and call during server rendering. */
3096
3240
  function initComty(options = {}) {
3097
3241
  if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
3098
- 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);
3099
3253
  const apiUrl = options.apiUrl || "http://localhost:4000";
3100
3254
  if (typeof projectKey !== "string" || !projectKey.trim()) {
3101
3255
  return Promise.reject(new TypeError("Comty: projectKey is required"));
@@ -3107,12 +3261,14 @@ function initComty(options = {}) {
3107
3261
  if (!["http:", "https:"].includes(parsedApi.protocol)) {
3108
3262
  return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
3109
3263
  }
3110
- const configuration = JSON.stringify([projectKey, apiUrl]);
3264
+ const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
3111
3265
  const existing = window[instanceKey];
3112
3266
  if (existing) {
3113
3267
  return existing.configuration === configuration ? existing.ready
3114
3268
  : Promise.reject(new Error("Comty: already initialized with different options"));
3115
3269
  }
3270
+ const agentBridgeUrl = agent?.url;
3271
+ const localOnly = Boolean(agent && !options.projectKey);
3116
3272
  const ready = new Promise((resolve, reject) => {
3117
3273
  const mount = () => {
3118
3274
  try { resolve(mountComty()); } catch (error) { reject(error); }
@@ -3126,10 +3282,22 @@ function initComty(options = {}) {
3126
3282
 
3127
3283
  function mountComty() {
3128
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();
3129
3296
  const session = {
3130
3297
  mode: "off",
3131
- get comments() { return this.mode === "browse" || this.mode === "edit"; },
3132
- 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"; },
3133
3301
  get inspect() { return this.mode === "inspect"; },
3134
3302
  };
3135
3303
  const selectedStatusFilters = new Set();
@@ -3153,9 +3321,10 @@ function initComty(options = {}) {
3153
3321
  const authReturnStorageKey = `comty:${projectKey}:auth-return`;
3154
3322
  let sdkToken = "";
3155
3323
  const activeHoverCommentIds = new Set();
3156
- /** @type {null | 'edit' | 'browse'} */
3324
+ /** @type {null | 'edit' | 'browse' | 'agent'} */
3157
3325
  let sessionEntry = null;
3158
3326
  const placedPins = new Map();
3327
+ const workingOutlines = new Map();
3159
3328
  const placementDiagnostics = new Map();
3160
3329
  let inspectedElement = null;
3161
3330
  let selectedElement = null;
@@ -3169,8 +3338,8 @@ function initComty(options = {}) {
3169
3338
  const commentTargets = new Map();
3170
3339
 
3171
3340
  const {
3172
- matchesAnchorContext, resolveByAnchorContext,
3173
- isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3341
+ matchesAnchorContext,
3342
+ resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3174
3343
  } = createAnchorMatcher({
3175
3344
  getElementTextSignature,
3176
3345
  getElementOwnTextSignature,
@@ -3248,10 +3417,20 @@ function initComty(options = {}) {
3248
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"));
3249
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"/>', () => {
3250
3419
  if (!canWrite) showAuthPrompt();
3251
- else if (session.inspect || !isSessionActive()) startEditSession();
3420
+ else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
3252
3421
  else if (!session.edit) setSession("edit", { entry: sessionEntry });
3253
3422
  });
3254
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;
3255
3434
  const divider = document.createElement("span");
3256
3435
  divider.className = "comty-tool-divider";
3257
3436
  toolbar.appendChild(divider);
@@ -3261,7 +3440,7 @@ function initComty(options = {}) {
3261
3440
  const primaryTools = document.createElement("div");
3262
3441
  primaryTools.className = "comty-tool-primary";
3263
3442
  toolbar.insertBefore(primaryTools, browseBtn);
3264
- for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
3443
+ for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
3265
3444
  const inspectorTools = document.createElement("div");
3266
3445
  inspectorTools.className = "comty-tool-context";
3267
3446
  inspectorTools.hidden = true;
@@ -4114,6 +4293,50 @@ function initComty(options = {}) {
4114
4293
  let toolbarMotionFrame = 0;
4115
4294
  let toolbarAnimations = [];
4116
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);
4117
4340
  const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
4118
4341
  function rememberToolbarPosition(left, top) {
4119
4342
  const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
@@ -4226,7 +4449,7 @@ function initComty(options = {}) {
4226
4449
 
4227
4450
  function prepareToolbarMotion() {
4228
4451
  if (toolbarMotionFrame) return;
4229
- const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools].filter(Boolean);
4452
+ const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
4230
4453
  // Move each group's background with its icons, including when interrupted.
4231
4454
  const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
4232
4455
  rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
@@ -4450,7 +4673,8 @@ function initComty(options = {}) {
4450
4673
  selectedElement = null;
4451
4674
  sessionEntry = null;
4452
4675
  session.mode = "off";
4453
- document.documentElement.classList.remove("comty-comments-active", "comty-comment-placement");
4676
+ syncPlacementCursor();
4677
+ document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
4454
4678
  hideInspector();
4455
4679
  setSidepanelOpen(false);
4456
4680
  closeBubbles();
@@ -4464,25 +4688,29 @@ function initComty(options = {}) {
4464
4688
  if (mode === "off") return;
4465
4689
  if (mode === "inspect") {
4466
4690
  session.mode = "inspect";
4691
+ document.documentElement.classList.add("comty-inspect-annotations");
4467
4692
  syncToolbar();
4693
+ renderCachedComments();
4468
4694
  ensureLiveTracking();
4469
4695
  return;
4470
4696
  }
4471
4697
 
4472
- if (session.edit && mode === "browse") {
4698
+ if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
4473
4699
  closeBubbles();
4474
4700
  }
4475
4701
  const hadComments = session.comments;
4476
4702
  session.mode = mode;
4477
4703
  sessionEntry = entry;
4704
+ document.documentElement.classList.remove("comty-inspect-annotations");
4478
4705
  document.documentElement.classList.toggle("comty-comments-active", session.comments);
4479
- document.documentElement.classList.toggle("comty-comment-placement", session.edit);
4706
+ syncPlacementCursor();
4480
4707
  if (session.edit) {
4481
4708
  setMotionEnabled(false);
4482
4709
  selectedElement = null;
4483
4710
  } else {
4484
4711
  hideInspector();
4485
4712
  }
4713
+ if (session.agent && sidepanelOpen) setSidepanelOpen(false);
4486
4714
  syncToolbar();
4487
4715
  ensureLiveTracking();
4488
4716
  if (renderComments || !hadComments) renderCachedComments();
@@ -4495,6 +4723,10 @@ function initComty(options = {}) {
4495
4723
  }
4496
4724
 
4497
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
+ }
4498
4730
  const guestAllowed = allowGuestComments;
4499
4731
  document.querySelector(".comty-auth-backdrop")?.remove();
4500
4732
  const opener = document.activeElement;
@@ -4616,14 +4848,16 @@ function initComty(options = {}) {
4616
4848
  }
4617
4849
 
4618
4850
  function syncToolbar() {
4619
- if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4851
+ if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4620
4852
  root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
4621
4853
  browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
4622
4854
  inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
4623
- btn.setAttribute("aria-pressed", String(session.edit));
4624
- 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;
4625
4858
  inspectorTools.hidden = !session.inspect;
4626
4859
  syncInspectorTools();
4860
+ renderAgentPanel();
4627
4861
  }
4628
4862
 
4629
4863
  const inspectionLayer = document.createElement("div");
@@ -5086,6 +5320,13 @@ function initComty(options = {}) {
5086
5320
  setSession("edit", { renderComments: true });
5087
5321
  }
5088
5322
 
5323
+ function startAgentSession() {
5324
+ if (!isLocalSite) return;
5325
+ if (session.agent) return;
5326
+ closeBubbles();
5327
+ setSession("agent", { renderComments: true });
5328
+ }
5329
+
5089
5330
  function toggleSidepanel() {
5090
5331
  if (sidepanelOpen) {
5091
5332
  setSidepanelOpen(false);
@@ -5109,7 +5350,7 @@ function initComty(options = {}) {
5109
5350
  showAuthPrompt();
5110
5351
  return;
5111
5352
  }
5112
- if (session.inspect || !isSessionActive()) {
5353
+ if (session.agent || session.inspect || !isSessionActive()) {
5113
5354
  startEditSession();
5114
5355
  return;
5115
5356
  }
@@ -5397,6 +5638,24 @@ function initComty(options = {}) {
5397
5638
  ancestors,
5398
5639
  layer: layer ? getElementFingerprint(layer) : null,
5399
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;
5400
5659
  context.candidateCount = [...document.querySelectorAll(context.target.tag)]
5401
5660
  .filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
5402
5661
  return context;
@@ -5425,6 +5684,11 @@ function initComty(options = {}) {
5425
5684
  };
5426
5685
  }
5427
5686
 
5687
+ function getPagePath() {
5688
+ const { pathname, hash } = window.location;
5689
+ return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
5690
+ }
5691
+
5428
5692
  function resolveBySelector(selector) {
5429
5693
  if (!selector) return null;
5430
5694
  try {
@@ -5446,17 +5710,9 @@ function initComty(options = {}) {
5446
5710
  }
5447
5711
  const context = parseAnchorContext(comment);
5448
5712
  const exact = resolveBySelector(comment.selector || "body");
5449
- if (context?.candidateCount > 1) {
5450
- const count = [...document.querySelectorAll("*")].filter(candidate => candidate.tagName.toLowerCase() === context.target.tag)
5451
- .filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
5452
- if (count !== context.candidateCount) {
5453
- commentTargets.set(key, { element: null, signature, revision: anchorRevision });
5454
- return null;
5455
- }
5456
- }
5457
- let element = exact && matchesAnchorContext(exact, context) ? getAnchorElement(exact) : null;
5458
- if (!element) element = resolveByAnchorContext(context);
5459
- 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 });
5460
5716
  return element;
5461
5717
  }
5462
5718
 
@@ -5527,9 +5783,14 @@ function initComty(options = {}) {
5527
5783
  return { deviceType: "desktop", deviceLabel: "Desktop" };
5528
5784
  }
5529
5785
 
5786
+ function syncPlacementCursor() {
5787
+ document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
5788
+ }
5789
+
5530
5790
  function closeBubbles() {
5531
5791
  const hadDraft = Boolean(draftAnchor);
5532
5792
  draftAnchor = null;
5793
+ syncPlacementCursor();
5533
5794
  activeHoverCommentIds.clear();
5534
5795
  if (!session.edit || hadDraft) hideInspector();
5535
5796
  document
@@ -5574,7 +5835,7 @@ function initComty(options = {}) {
5574
5835
  pending: commentId.startsWith("pending-"),
5575
5836
  project_id: projectId,
5576
5837
  domain: payload.host || window.location.host,
5577
- page_path: payload.pagePath || window.location.pathname,
5838
+ page_path: payload.pagePath || getPagePath(),
5578
5839
  selector: payload.selector || "body",
5579
5840
  rel_x: payload.relX,
5580
5841
  rel_y: payload.relY,
@@ -5599,7 +5860,7 @@ function initComty(options = {}) {
5599
5860
  replies: [],
5600
5861
  };
5601
5862
 
5602
- cachedPagePath = window.location.pathname;
5863
+ cachedPagePath = getPagePath();
5603
5864
  cachedComments = [
5604
5865
  ...cachedComments.filter(
5605
5866
  (cached) => getCommentId(cached) !== commentId,
@@ -5612,6 +5873,7 @@ function initComty(options = {}) {
5612
5873
  }
5613
5874
 
5614
5875
  function getCommentAuthorName(comment) {
5876
+ if (comment?.author_kind === 'agent') return 'Agent';
5615
5877
  return comment?.author_name || comment?.name || "Anonymous";
5616
5878
  }
5617
5879
 
@@ -5633,6 +5895,12 @@ function initComty(options = {}) {
5633
5895
  function createCommentAvatar(comment, extraClass = "") {
5634
5896
  const avatar = document.createElement("span");
5635
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
+ }
5636
5904
  const authorName = getCommentAuthorName(comment);
5637
5905
  const avatarUrl = getCommentAuthorAvatarUrl(comment);
5638
5906
 
@@ -5640,6 +5908,7 @@ function initComty(options = {}) {
5640
5908
  const image = document.createElement("img");
5641
5909
  image.src = avatarUrl;
5642
5910
  image.alt = "";
5911
+ image.draggable = false;
5643
5912
  image.onerror = () => {
5644
5913
  image.remove();
5645
5914
  avatar.innerText = authorName.charAt(0).toUpperCase();
@@ -5864,6 +6133,7 @@ function initComty(options = {}) {
5864
6133
  payload,
5865
6134
  preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
5866
6135
  };
6136
+ syncPlacementCursor();
5867
6137
  ensureLiveTracking();
5868
6138
 
5869
6139
  const pin = document.createElement("div");
@@ -5881,10 +6151,21 @@ function initComty(options = {}) {
5881
6151
 
5882
6152
  const textarea = document.createElement("textarea");
5883
6153
  textarea.className = "comty-create-textarea";
5884
- textarea.placeholder = "Add a comment";
6154
+ textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
5885
6155
  textarea.setAttribute("aria-label", textarea.placeholder);
5886
6156
  textarea.rows = 1;
5887
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
+
5888
6169
  const actions = document.createElement("div");
5889
6170
  actions.className = "comty-create-actions";
5890
6171
 
@@ -5938,7 +6219,7 @@ function initComty(options = {}) {
5938
6219
  const submitBtn = document.createElement("button");
5939
6220
  submitBtn.type = "button";
5940
6221
  submitBtn.className = "comty-btn-submit";
5941
- submitBtn.setAttribute("aria-label", "Post comment");
6222
+ submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
5942
6223
  submitBtn.disabled = true;
5943
6224
  submitBtn.innerHTML = `
5944
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">
@@ -5955,49 +6236,62 @@ function initComty(options = {}) {
5955
6236
  textarea.disabled = true;
5956
6237
  linkBtn.disabled = true;
5957
6238
 
6239
+ if (payload.kind === "agent" && !agent) {
6240
+ showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
6241
+ }
5958
6242
  payload.content = content;
6243
+ if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
5959
6244
  payload.linkUrl = attachedLink || null;
5960
6245
  textarea.textContent = content;
5961
6246
 
5962
- const context = captureContext();
5963
- const point = getPinPoint(pin);
5964
- const pendingId = `pending-${crypto.randomUUID()}`;
5965
- // 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()}`;
5966
6251
  bubble.remove();
5967
6252
  pin.remove();
5968
6253
  if (draftAnchor === editorDraft) draftAnchor = null;
6254
+ syncPlacementCursor();
5969
6255
  hideInspector();
5970
- addLocalComment(pendingId, payload);
6256
+ if (!isAgentTask) addLocalComment(pendingId, payload);
5971
6257
  let post;
5972
6258
  try {
5973
- post = await fetch(`${apiUrl}/api/comments`, {
6259
+ post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
5974
6260
  method: "POST",
5975
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
5976
- 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),
5977
6263
  });
5978
- if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
6264
+ if (!post.ok) throw new Error(`Save failed (${post.status})`);
5979
6265
  const data = await post.json();
5980
- if (!data?.id) throw new Error("Comment save returned no ID");
5981
- cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5982
- if (context.url === location.href) {
5983
- const comment = addLocalComment(String(data.id), payload);
5984
- // Start only after the save and immediate UI update; the capture yields to input.
5985
- void uploadCommentScreenshot(comment, context, point);
5986
- } 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
+ }
5987
6277
  } catch (error) {
5988
6278
  console.error("Comty: save failed", error);
5989
- cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5990
- renderSidepanelComments();
5991
- 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
+ }
5992
6285
  // Keep the original editor and its text available for retry, including attachments.
5993
6286
  if (!draftAnchor) draftAnchor = editorDraft;
6287
+ syncPlacementCursor();
5994
6288
  root.append(pin, bubble);
5995
6289
  submitBtn.disabled = false;
5996
6290
  textarea.disabled = false;
5997
6291
  linkBtn.disabled = false;
5998
6292
  const errorMessage = document.createElement("p");
5999
6293
  errorMessage.setAttribute("role", "alert");
6000
- 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.`;
6001
6295
  bubble.querySelector('[role="alert"]')?.remove();
6002
6296
  bubble.append(errorMessage);
6003
6297
  if (bubble.isConnected) textarea.focus();
@@ -6086,7 +6380,8 @@ function initComty(options = {}) {
6086
6380
  renderInspector();
6087
6381
  };
6088
6382
 
6089
- actions.append(linkBtn, submitBtn, linkEditor);
6383
+ if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
6384
+ else actions.append(linkBtn, submitBtn, linkEditor);
6090
6385
 
6091
6386
  bubble.appendChild(textarea);
6092
6387
  bubble.appendChild(attachment);
@@ -6099,6 +6394,219 @@ function initComty(options = {}) {
6099
6394
  setTimeout(() => textarea.focus(), 10);
6100
6395
  }
6101
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
+
6102
6610
  function setSidepanelOpen(open) {
6103
6611
  sidepanelOpen = open;
6104
6612
  sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
@@ -6134,12 +6642,52 @@ function initComty(options = {}) {
6134
6642
 
6135
6643
  function openCommentFromPanel(comment) {
6136
6644
  const target = resolveCommentElement(comment);
6137
- if (!target || isElementTreeHidden(target)) return;
6645
+ if (!target || isElementTreeHidden(target)) {
6646
+ openDetachedThread(comment);
6647
+ return;
6648
+ }
6138
6649
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
6139
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
+ });
6140
6660
  syncCommentAnchors();
6141
6661
  const pin = findPlacedPin(comment);
6142
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.";
6143
6691
  }
6144
6692
 
6145
6693
  let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
@@ -6152,6 +6700,7 @@ function initComty(options = {}) {
6152
6700
  ? sidepanelSearch.value.trim().toLowerCase()
6153
6701
  : "";
6154
6702
  let list = cachedComments.filter((comment) => {
6703
+ if (comment.local_agent || comment.kind === "agent") return false;
6155
6704
  if (
6156
6705
  search &&
6157
6706
  !`${comment.content ?? ""} ${getCommentLabel(comment)}`
@@ -6198,6 +6747,7 @@ function initComty(options = {}) {
6198
6747
  const item = document.createElement("button");
6199
6748
  item.type = "button";
6200
6749
  item.className = "comty-sidepanel-item";
6750
+ item.dataset.commentId = String(comment.id);
6201
6751
  item.disabled = Boolean(comment.pending);
6202
6752
  item.innerHTML = `
6203
6753
  <span class="comty-sidepanel-item-status" aria-hidden="true"></span>
@@ -6221,8 +6771,13 @@ function initComty(options = {}) {
6221
6771
  if (metaEl instanceof HTMLElement) {
6222
6772
  metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
6223
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
+ }
6224
6777
  }
6225
6778
 
6779
+ updateAnchorBadge(item, comment);
6780
+
6226
6781
  item.onclick = (event) => {
6227
6782
  event.stopPropagation();
6228
6783
  openCommentFromPanel(comment);
@@ -6338,14 +6893,15 @@ function initComty(options = {}) {
6338
6893
  createStatusFilters();
6339
6894
 
6340
6895
  async function updateCommentStatus(comment, status) {
6341
- if (!projectId || !comment?.id) return false;
6896
+ if (comment?.local_agent || !projectId || !comment?.id) return false;
6342
6897
 
6343
6898
  let res;
6344
6899
  try {
6345
- 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`, {
6346
6902
  method: "POST",
6347
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
6348
- body: JSON.stringify({
6903
+ headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
6904
+ body: JSON.stringify(local ? { status } : {
6349
6905
  projectKey,
6350
6906
  host: window.location.host,
6351
6907
  status,
@@ -6362,7 +6918,7 @@ function initComty(options = {}) {
6362
6918
  }
6363
6919
 
6364
6920
  const data = await res.json();
6365
- comment.status = getCommentStatus(data.status).value;
6921
+ comment.status = getCommentStatus(data.task?.status ?? data.status).value;
6366
6922
  return comment.status;
6367
6923
  }
6368
6924
 
@@ -6554,6 +7110,23 @@ function initComty(options = {}) {
6554
7110
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
6555
7111
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
6556
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
+ },
6557
7130
  get canWrite() { return canWrite; },
6558
7131
  get canManage() { return canManage; },
6559
7132
  get allowGuestComments() { return allowGuestComments; },
@@ -6754,42 +7327,112 @@ function initComty(options = {}) {
6754
7327
  function reconcileComments(comments) {
6755
7328
  const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
6756
7329
  cachedComments = comments
6757
- .filter(comment => comment.kind !== "agent" && !comment.local_agent)
7330
+ .filter(comment => comment.kind !== "agent" || comment.status !== "done")
6758
7331
  .map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
6759
7332
  for (const comment of previous.values()) {
6760
7333
  if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
6761
7334
  }
6762
7335
  }
6763
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
+
6764
7368
  async function loadComments({ force = false } = {}) {
6765
- if (!projectId) return;
6766
- const pagePath = window.location.pathname;
7369
+ if (!projectId && !agent) return;
7370
+ const pagePath = getPagePath();
6767
7371
  if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
6768
7372
  if (pendingCommentsLoad) {
6769
7373
  await pendingCommentsLoad;
6770
- if (!force) return;
7374
+ if (!force && cachedPagePath === pagePath) return;
6771
7375
  }
6772
7376
  pendingCommentsLoad = (async () => {
6773
- let comments = cachedPagePath === pagePath ? cachedComments : [];
7377
+ let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
6774
7378
  if (projectId) {
6775
7379
  try {
6776
- const url = new URL(`${apiUrl}/api/comments`);
6777
- url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath, ...(force ? { _t: String(Date.now()) } : {}) });
6778
- const res = await fetch(url);
6779
- if (!res.ok) throw new Error(await res.text());
6780
- const data = await res.json();
6781
- 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()];
6782
7390
  } catch (error) { console.error("Comty: list comments failed", error); }
6783
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;
6784
7420
  reconcileComments(comments);
6785
7421
  cachedPagePath = pagePath;
6786
7422
  renderSidepanelComments();
6787
7423
  await renderComments(cachedComments);
7424
+ syncAgentWorkingOutlines();
6788
7425
  ensureLiveTracking();
6789
7426
  })();
6790
7427
  try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
6791
7428
  }
6792
7429
 
7430
+ if (agent) {
7431
+ setInterval(() => {
7432
+ if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
7433
+ }, 2000);
7434
+ }
7435
+
6793
7436
  function getPinPoint(pin) {
6794
7437
  return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
6795
7438
  }
@@ -6817,7 +7460,7 @@ function initComty(options = {}) {
6817
7460
  }
6818
7461
 
6819
7462
  function syncCommentAnchors() {
6820
- if (!session.comments) return;
7463
+ if (!session.comments && !session.inspect) return;
6821
7464
  const modal = getTopActiveModal();
6822
7465
  for (const comment of cachedComments) {
6823
7466
  const id = String(comment.id);
@@ -6828,7 +7471,7 @@ function initComty(options = {}) {
6828
7471
  placePin(comment, modal);
6829
7472
  pin = placedPins.get(id);
6830
7473
  }
6831
- 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"));
6832
7475
  // A pin can remove its target's :hover style as the pointer enters it.
6833
7476
  if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
6834
7477
  (activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
@@ -6843,6 +7486,10 @@ function initComty(options = {}) {
6843
7486
  if (pin.style.top !== y) pin.style.top = y;
6844
7487
  for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
6845
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
+ }
6846
7493
  if (draftAnchor) {
6847
7494
  if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
6848
7495
  const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
@@ -6856,16 +7503,50 @@ function initComty(options = {}) {
6856
7503
  }
6857
7504
  }
6858
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
+
6859
7537
  function onFrame() {
6860
7538
  if (motion.enabled) updateMotion();
6861
- 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
+ }
6862
7543
  if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
6863
7544
  if (session.inspect && rulersEnabled) renderRulers();
6864
7545
  if (session.inspect && rulersEnabled) measurements.render();
6865
7546
  }
6866
7547
 
6867
7548
  function ensureLiveTracking() {
6868
- 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;
6869
7550
  if (!active || document.hidden) {
6870
7551
  if (liveFrame) cancelAnimationFrame(liveFrame);
6871
7552
  liveFrame = null;
@@ -6885,13 +7566,14 @@ function initComty(options = {}) {
6885
7566
  syncOverlayHost();
6886
7567
 
6887
7568
  function renderCachedComments() {
6888
- if (!session.comments && !session.inspect && !session.edit) return;
7569
+ if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
6889
7570
  if (resizeRenderFrame) {
6890
7571
  window.cancelAnimationFrame(resizeRenderFrame);
6891
7572
  }
6892
7573
  resizeRenderFrame = window.requestAnimationFrame(() => {
6893
7574
  resizeRenderFrame = null;
6894
7575
  syncCommentAnchors();
7576
+ syncAgentWorkingOutlines();
6895
7577
  renderInspector();
6896
7578
  ensureLiveTracking();
6897
7579
  });
@@ -6933,6 +7615,29 @@ function initComty(options = {}) {
6933
7615
  characterData: true,
6934
7616
  });
6935
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
+
6936
7641
  document.addEventListener("scroll", handleHostLayoutEvent, {
6937
7642
  capture: true,
6938
7643
  passive: true,
@@ -7070,13 +7775,14 @@ function initComty(options = {}) {
7070
7775
  const y = e.clientY;
7071
7776
 
7072
7777
  const anchorContext = getAnchorContext(el);
7778
+ if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
7073
7779
 
7074
7780
  const payload = {
7075
7781
  ...getDeviceContext(),
7076
7782
  projectId,
7077
- kind: "comment",
7783
+ kind: session.agent ? "agent" : "comment",
7078
7784
  host: window.location.host,
7079
- pagePath: window.location.pathname,
7785
+ pagePath: getPagePath(),
7080
7786
  selector: getSelector(el),
7081
7787
  relX,
7082
7788
  relY,
@@ -7174,6 +7880,7 @@ function initComty(options = {}) {
7174
7880
  window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
7175
7881
 
7176
7882
  async function boot() {
7883
+ if (localOnly) { await loadComments(); return; }
7177
7884
  const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
7178
7885
  method: "POST",
7179
7886
  headers: getSdkHeaders({ "Content-Type": "application/json" }),
@@ -7186,6 +7893,7 @@ function initComty(options = {}) {
7186
7893
 
7187
7894
  if (!res.ok) {
7188
7895
  console.error("Comty: bootstrap failed", await res.text());
7896
+ if (agent) await loadComments();
7189
7897
  return;
7190
7898
  }
7191
7899
 
@@ -7230,16 +7938,30 @@ function initComty(options = {}) {
7230
7938
 
7231
7939
  if (typeof options.onTestReady === "function") {
7232
7940
  options.onTestReady({
7233
- getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
7941
+ getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
7234
7942
  getDistanceSegments, getInspectRows, syncCommentAnchors,
7235
7943
  setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
7236
7944
  startEditSession, startBrowseSession, endSession, showInspector,
7237
- 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
+ },
7238
7955
  seed(comments) {
7239
7956
  reconcileComments(comments);
7240
7957
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
7241
7958
  return renderComments(cachedComments, { retry: false });
7242
7959
  },
7960
+ seedAgentTasks(tasks) {
7961
+ localTasks = tasks;
7962
+ syncAgentWorkingOutlines();
7963
+ ensureLiveTracking();
7964
+ },
7243
7965
  setCanWrite(value) { canWrite = value; canManage = value; },
7244
7966
  invalidate() { anchorRevision += 1; },
7245
7967
  });
@@ -7247,6 +7969,7 @@ function initComty(options = {}) {
7247
7969
 
7248
7970
  return boot().catch(error => {
7249
7971
  console.error("Comty: bootstrap failed", error);
7972
+ if (agent) return loadComments();
7250
7973
  });
7251
7974
  }
7252
7975
  }