@comty.io/core 0.1.5 → 0.1.7

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;
@@ -50,6 +50,10 @@ function getComtyStyle() {
50
50
  --comty-toolbar-selected: #505457;
51
51
  --comty-toolbar-muted: ${toolbarMuted};
52
52
  --comty-measure: #ff3b30;
53
+ --comty-markup-yellow: #facc15;
54
+ --comty-markup-green: #22c55e;
55
+ --comty-color-black: #000;
56
+ --comty-color-spectrum: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
53
57
  --comty-font: "Inter", ui-sans-serif, system-ui, sans-serif;
54
58
  --comty-dialog-surface: var(--comty-surface);
55
59
  --comty-dialog-ink: var(--comty-foreground);
@@ -60,6 +64,8 @@ function getComtyStyle() {
60
64
  --comty-dialog-width: 32rem;
61
65
  --comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
62
66
  --comty-motion-duration: 220ms;
67
+ --comty-working-dot-size: .25em;
68
+ --comty-working-dot-gap: .125em;
63
69
  --comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
64
70
  --comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
65
71
  position: fixed;
@@ -71,13 +77,80 @@ function getComtyStyle() {
71
77
  font-family: var(--comty-font);
72
78
  color-scheme: dark;
73
79
  }
74
-
80
+ #comty-root :is(button, .comty-comment-avatar) { user-select: none; -webkit-user-select: none; }
81
+ #comty-root .comty-comment-avatar img { -webkit-user-drag: none; }
82
+ #comty-root .comty-pin .comty-comment-avatar { pointer-events: none; }
83
+ #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; }
84
+ #comty-root > .comty-agent-panel[data-side="left"] { left: 1rem; right: auto; }
85
+ #comty-root > .comty-agent-panel[data-closed] { transform: translateY(.5rem); opacity: 0; pointer-events: none; }
86
+ #comty-root > .comty-agent-panel[hidden] { display: none; }
87
+ .comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
88
+ .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); }
89
+ .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); }
90
+ .comty-agent-brand.comty-agent-avatar { width: 2.375rem; height: 2.375rem; border-radius: 0; background: transparent; }
91
+ .comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
92
+ .comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
93
+ .comty-agent-brand.comty-agent-avatar svg { width: 100%; height: 100%; }
94
+ .comty-agent-answer { display: flex; align-items: flex-start; gap: .5rem; padding: .375rem .5rem .875rem; }
95
+ .comty-agent-answer .comty-agent-brand { width: 1.5rem; height: 1.5rem; }
96
+ .comty-agent-answer p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--comty-foreground); }
97
+ .comty-agent-heading { flex: 1; min-width: 0; }
98
+ .comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
99
+ .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; }
100
+ .comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
101
+ .comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
102
+ .comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
103
+ .comty-agent-list { display: grid; gap: .125rem; }
104
+ .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); }
105
+ .comty-agent-row:hover { background: var(--comty-hover-surface); }
106
+ .comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
107
+ .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); }
108
+ .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; }
109
+ .comty-working-indicator > i[data-active] { opacity: 1; }
110
+ .comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
111
+ .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
112
+ .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
113
+ .comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
114
+ .comty-agent-row-actions { display: flex; flex-shrink: 0; gap: .25rem; }
115
+ .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; }
116
+ .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
117
+ #comty-root > .comty-agent-task-toast { position: fixed; right: 1rem; bottom: 1rem; z-index: 2147483647; width: min(20rem, calc(100vw - 2rem)); box-sizing: border-box; padding: .75rem 2rem .75rem .875rem; border: .0625rem solid var(--comty-border); border-radius: .75rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .75rem/1.5 ui-sans-serif, system-ui, sans-serif; pointer-events: auto; opacity: 1; transform: translateY(0); transition: opacity 200ms ease, transform 200ms var(--comty-ease-out); animation: comty-agent-toast-in 200ms var(--comty-ease-out); color-scheme: dark; }
118
+ #comty-root > .comty-agent-task-toast[data-toolbar] { right: auto; bottom: auto; }
119
+ #comty-root > .comty-agent-task-toast[data-leaving] { opacity: 0; transform: translateY(.25rem); pointer-events: none; }
120
+ .comty-agent-task-toast strong { display: block; font-size: .8125rem; font-weight: 600; }
121
+ .comty-agent-task-toast p { margin: .25rem 0 0; color: var(--comty-muted); overflow-wrap: anywhere; }
122
+ .comty-agent-task-toast button { position: absolute; top: .5rem; right: .375rem; display: flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem; padding: 0; border: 0; border-radius: .25rem; background: transparent; color: var(--comty-muted); cursor: pointer; }
123
+ .comty-agent-task-toast button:hover { background: var(--comty-hover-surface); color: var(--comty-foreground); }
124
+ .comty-agent-task-toast button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
125
+ .comty-agent-task-toast svg { width: .875rem; height: .875rem; }
126
+ @keyframes comty-agent-toast-in { from { opacity: 0; transform: translateY(.25rem); } to { opacity: 1; transform: translateY(0); } }
127
+ .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
128
+ .comty-agent-error { margin-top: .75rem; }
129
+ .comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
130
+ .comty-agent-error p { margin-top: .25rem; }
131
+ .comty-agent-banner { margin-bottom: 1rem; }
132
+ .comty-agent-banner .comty-agent-error { margin-top: 0; }
133
+ .comty-agent-task-actions { display: flex; justify-content: flex-end; gap: .375rem; padding: .375rem .625rem; border-top: .0625rem solid var(--comty-border); }
134
+ .comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
135
+ :is(.comty-agent-panel, .comty-agent-task-thread) .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; }
136
+ .comty-agent-task-thread .comty-agent-action { min-height: 1.5rem; padding: .125rem .5rem; border-radius: .375rem; font-size: .75rem; }
137
+ :is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
138
+ :is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
139
+ :is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action:disabled { opacity: .5; cursor: wait; }
140
+ .comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
141
+ :is(.comty-agent-panel, .comty-agent-task-thread) :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
142
+ .comty-agent-details { margin-top: .625rem; }
143
+ .comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
144
+ .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; }
145
+ .comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: 0; pointer-events: none; }
146
+ .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; }
147
+ .comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
75
148
  #comty-root { cursor: default; }
76
149
  #comty-root::backdrop { background: transparent; pointer-events: none; }
77
150
  html.comty-comment-placement,
78
151
  html.comty-comment-placement body,
79
152
  html.comty-comment-placement body *:not(#comty-root):not(#comty-root *) {
80
- cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 20 20, crosshair !important;
153
+ cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 16 16, crosshair !important;
81
154
  }
82
155
  #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
156
  .comty-toolbar {
@@ -144,7 +217,7 @@ function getComtyStyle() {
144
217
  #comty-root [hidden] { display: none !important; }
145
218
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
146
219
  @media (prefers-reduced-motion: reduce) {
147
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
220
+ .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i, #comty-root > .comty-agent-task-toast { transition: none !important; animation: none !important; }
148
221
  }
149
222
 
150
223
  .comty-sidepanel {
@@ -326,6 +399,16 @@ function getComtyStyle() {
326
399
  white-space: nowrap;
327
400
  }
328
401
 
402
+ .comty-anchor-badge {
403
+ display: inline-block;
404
+ margin-top: .375rem;
405
+ padding: .125rem .375rem;
406
+ border: 1px solid var(--comty-border);
407
+ border-radius: 999rem;
408
+ color: var(--comty-muted);
409
+ font: .6875rem/1.4 sans-serif;
410
+ }
411
+
329
412
  .comty-pin {
330
413
  position: fixed;
331
414
  z-index: 2147483645;
@@ -607,6 +690,8 @@ function getComtyStyle() {
607
690
  height: 100%;
608
691
  object-fit: cover;
609
692
  }
693
+ .comty-comment-avatar.comty-agent-avatar { overflow: visible; border-radius: 0; background: transparent; color: var(--comty-paper); }
694
+ .comty-comment-avatar.comty-agent-avatar svg { display: block; width: 100%; height: 100%; }
610
695
 
611
696
  .comty-pin-avatar {
612
697
  width: 24px;
@@ -721,12 +806,12 @@ function getComtyStyle() {
721
806
  }
722
807
 
723
808
  .comty-popover-item.comty-popover-item-destructive {
724
- color: var(--comty-error);
809
+ color: var(--comty-agent-error);
725
810
  }
726
811
 
727
812
  .comty-popover-item.comty-popover-item-destructive:hover,
728
813
  .comty-popover-item.comty-popover-item-destructive:focus-visible {
729
- background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
814
+ background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
730
815
  outline: none;
731
816
  }
732
817
 
@@ -855,6 +940,8 @@ function getComtyStyle() {
855
940
  }
856
941
 
857
942
  .comty-hover-bubble .comty-thread {
943
+ display: grid;
944
+ gap: 1rem;
858
945
  padding: 12px;
859
946
  }
860
947
 
@@ -1097,19 +1184,23 @@ function getComtyStyle() {
1097
1184
  margin: 4px 0;
1098
1185
  }
1099
1186
 
1100
- html.comty-comments-active .comty-pin {
1187
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin {
1101
1188
  display: grid;
1102
1189
  }
1103
1190
 
1104
- html.comty-comments-active .comty-pin.comty-filtered-out {
1191
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin.comty-filtered-out {
1105
1192
  display: none;
1106
1193
  }
1107
1194
 
1108
- html.comty-comments-active .comty-bubble {
1195
+ html:is(.comty-comments-active, .comty-inspect-annotations) .comty-bubble {
1109
1196
  display: block;
1110
1197
  }
1111
1198
 
1112
1199
  .comty-inspection-layer { position: relative; z-index: 2147483647; pointer-events: none; }
1200
+ .comty-markup-layer, .comty-markup-surface { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; }
1201
+ .comty-markup-surface { pointer-events: auto; cursor: crosshair; touch-action: none; user-select: none; }
1202
+ .comty-markup-surface[aria-busy="true"] { cursor: progress; }
1203
+ .comty-capture-selection { position: absolute; box-sizing: border-box; border: .0625rem solid var(--comty-accent); box-shadow: 0 0 0 100vmax var(--comty-dialog-backdrop); color: var(--comty-paper); text-shadow: 0 .0625rem .125rem var(--comty-ink); font: .6875rem/1.5 var(--comty-font); padding: .25rem; white-space: nowrap; }
1113
1204
  .comty-overlay-tools, .comty-layout-guides, .comty-layout-guide { position: fixed; inset: 0; pointer-events: none; }
1114
1205
  .comty-overlay-tools { overflow: hidden; }
1115
1206
  .comty-layout-guide { display: grid; box-sizing: border-box; }
@@ -1135,6 +1226,38 @@ function getComtyStyle() {
1135
1226
  .comty-guide-fields label { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
1136
1227
  .comty-tool-panel input, .comty-tool-panel select { min-width: 0; width: 6rem; box-sizing: border-box; border: 0; border-radius: .25rem; padding: .25rem; background: var(--comty-surface); color: var(--comty-foreground); font: inherit; }
1137
1228
  .comty-tool-panel input[type="color"] { height: 1.75rem; padding: .125rem; }
1229
+ .comty-tool-panel.comty-markup-panel { width: max-content; padding: .375rem; border-radius: .875rem; background: var(--comty-toolbar-group); }
1230
+ .comty-markup-controls, .comty-markup-options, .comty-markup-strokes, .comty-markup-colors { display: flex; align-items: center; gap: .125rem; }
1231
+ .comty-markup-controls { justify-content: space-between; }
1232
+ .comty-markup-options { margin-top: .375rem; padding-top: .375rem; border-top: .0625rem solid var(--comty-border); }
1233
+ .comty-markup-divider { flex: 0 0 .0625rem; height: 1rem; margin-inline: .25rem; background: var(--comty-border); }
1234
+ .comty-markup-panel button { flex: 0 0 auto; width: 2rem; height: 2rem; padding: 0; color: var(--comty-toolbar-muted); border-radius: .5rem; }
1235
+ .comty-markup-panel button svg { display: block; flex: 0 0 auto; width: 1rem; height: 1rem; }
1236
+ .comty-markup-panel button[aria-pressed="false"] { opacity: 1; }
1237
+ .comty-markup-panel button[aria-pressed="true"] { color: var(--comty-foreground); background: var(--comty-hover-surface); }
1238
+ .comty-markup-panel button:disabled { opacity: .35; cursor: default; }
1239
+ .comty-markup-panel button:disabled:hover { background: transparent; }
1240
+ .comty-markup-panel button:not(:disabled):active { transform: scale(.97); }
1241
+ .comty-markup-strokes button { width: 1.5rem; height: 1.5rem; }
1242
+ .comty-markup-colors button { width: 1.375rem; height: 1.5rem; background: transparent; }
1243
+ .comty-markup-colors button[aria-pressed="true"] { background: transparent; }
1244
+ .comty-markup-swatch { display: block; width: .875rem; height: .875rem; border-radius: 50%; background: var(--markup-swatch); box-shadow: inset 0 0 0 .0625rem color-mix(in srgb, var(--comty-paper) 15%, transparent); }
1245
+ .comty-markup-colors button[aria-pressed="true"] .comty-markup-swatch { outline: .0625rem solid var(--comty-foreground); outline-offset: .125rem; }
1246
+ .comty-markup-panel button.comty-markup-picker { width: 1rem; padding: 0; position: relative; flex: 0 0 1rem; height: 1rem; margin-inline: .25rem; border-radius: 50%; background: conic-gradient(var(--comty-measure), var(--comty-markup-yellow), var(--comty-markup-green), var(--comty-accent), var(--comty-error), var(--comty-measure)); }
1247
+ .comty-markup-picker[data-selected] { outline: .0625rem solid var(--comty-foreground); outline-offset: .125rem; }
1248
+ .comty-markup-picker:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
1249
+ .comty-markup-color { display: grid; gap: .625rem; margin-top: .375rem; padding: .625rem .25rem .25rem; border-top: .0625rem solid var(--comty-border); }
1250
+ .comty-markup-color-field { position: relative; height: 8rem; border-radius: .5rem; background: linear-gradient(to top, var(--comty-color-black), transparent), linear-gradient(to right, var(--comty-paper), transparent), var(--markup-hue); touch-action: none; cursor: crosshair; }
1251
+ .comty-markup-color-field:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
1252
+ .comty-markup-color-field > span { position: absolute; width: .625rem; height: .625rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-color); box-shadow: 0 0 0 .0625rem var(--comty-ink); transform: translate(-50%, -50%); pointer-events: none; }
1253
+ .comty-markup-color input.comty-markup-hue { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 1rem; margin: 0; padding: 0; border-radius: 1rem; background: var(--comty-color-spectrum); cursor: pointer; }
1254
+ .comty-markup-hue::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: .875rem; height: .875rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-hue); box-shadow: 0 0 0 .0625rem var(--comty-ink); }
1255
+ .comty-markup-hue::-moz-range-thumb { width: .875rem; height: .875rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-hue); box-shadow: 0 0 0 .0625rem var(--comty-ink); }
1256
+ .comty-markup-hex { display: flex; align-items: center; gap: .5rem; padding: .25rem .5rem; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .6875rem/1.5 var(--comty-font); }
1257
+ .comty-markup-color-preview { flex: 0 0 1rem; height: 1rem; border-radius: .25rem; background: var(--markup-color); box-shadow: inset 0 0 0 .0625rem var(--comty-border); }
1258
+ .comty-markup-hex input { flex: 1; width: 0; text-align: right; font-family: ui-monospace, monospace; }
1259
+ .comty-markup-hex input[aria-invalid="true"] { outline: .0625rem solid var(--comty-error); }
1260
+
1138
1261
  .comty-ruler { position: fixed; background: var(--comty-surface); pointer-events: auto; touch-action: none; }
1139
1262
  .comty-ruler-horizontal { left: 0; top: 0; width: 100vw; height: 1.125rem; border-bottom: .0625rem solid var(--comty-border); cursor: ns-resize; }
1140
1263
  .comty-ruler-vertical { left: 0; top: 0; height: 100vh; width: 1.125rem; border-right: .0625rem solid var(--comty-border); cursor: ew-resize; }
@@ -1209,16 +1332,16 @@ function getComtyStyle() {
1209
1332
  .comty-motion-panel button[aria-pressed="true"] { opacity: 1; color: var(--comty-paper); background: var(--comty-hover-surface); }
1210
1333
  .comty-motion-panel button[aria-pressed="false"] { opacity: 1; }
1211
1334
  .comty-motion-panel [data-motion-status] { margin: .5rem; font-size: .75rem; color: var(--comty-muted); }
1212
- @media (max-width: 25rem) { #comty-root { --comty-tool-size: 2rem; --comty-toolbar-padding: .25rem; } .comty-toolbar { right: .5rem; bottom: .5rem; max-width: calc(100vw - 1rem); flex-wrap: wrap; justify-content: center; row-gap: .25rem; } .comty-tool-divider { display: none; } }
1335
+ @media (max-width: 38rem) { #comty-root { --comty-tool-size: 2rem; --comty-toolbar-padding: .25rem; } .comty-toolbar { right: .5rem; bottom: .5rem; max-width: calc(100vw - 1rem); flex-wrap: wrap; justify-content: center; row-gap: .25rem; } .comty-tool-divider { display: none; } }
1213
1336
 
1214
1337
  .comty-inspection-box {
1215
1338
  position: fixed;
1216
1339
  box-sizing: border-box;
1217
1340
  border: .0625rem solid var(--comty-outline);
1218
- background: color-mix(in oklch, var(--comty-outline) 8%, transparent);
1341
+ background: transparent;
1342
+ border-radius: 0;
1219
1343
  pointer-events: none;
1220
1344
  }
1221
- .comty-inspection-box[data-selected] { background: color-mix(in oklch, var(--comty-outline) 5%, transparent); }
1222
1345
  .comty-inspect-spacing { position: fixed; pointer-events: none; background: var(--comty-spacing-color); }
1223
1346
  .comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
1224
1347
  .comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
@@ -1272,6 +1395,15 @@ function getComtyStyle() {
1272
1395
  height: 5.75rem;
1273
1396
  border-radius: .75rem;
1274
1397
  }
1398
+ .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; }
1399
+ .comty-create-expanded .comty-agent-scope { opacity: 1; visibility: visible; }
1400
+ .comty-agent-scope input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
1401
+ .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; }
1402
+ .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; }
1403
+ .comty-agent-scope input:checked + .comty-agent-scope-track { background: var(--comty-accent); }
1404
+ .comty-agent-scope input:checked + .comty-agent-scope-track::after { transform: translateX(.625rem); }
1405
+ .comty-agent-scope input:focus-visible + .comty-agent-scope-track { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
1406
+ .comty-agent-thread-error { grid-column: 2; margin: .25rem 0 0; color: var(--comty-agent-error); font: .6875rem/1.3 var(--comty-font); }
1275
1407
 
1276
1408
  .comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
1277
1409
  .comty-create-bubble.comty-bubble-left { transform-origin: top right; }
@@ -1502,6 +1634,14 @@ function createAnchorMatcher({
1502
1634
  return haystack.toLowerCase().includes(needle.toLowerCase());
1503
1635
  }
1504
1636
 
1637
+ function similarText(a, b) {
1638
+ if (!a || !b) return false;
1639
+ const words = value => new Set(value.toLowerCase().split(/\s+/).filter(Boolean));
1640
+ const left = words(a), right = words(b);
1641
+ const common = [...left].filter(word => right.has(word)).length;
1642
+ return common >= 2 && common / Math.max(left.size, right.size) >= 0.7;
1643
+ }
1644
+
1505
1645
  function scoreFingerprint(el, fingerprint) {
1506
1646
  if (!el || !fingerprint) return 0;
1507
1647
  if (
@@ -1571,6 +1711,8 @@ function createAnchorMatcher({
1571
1711
  score += 15;
1572
1712
  }
1573
1713
 
1714
+ if (text !== fingerprint.text && similarText(text, fingerprint.text)) score += 24;
1715
+
1574
1716
  if (typeof fingerprint.siblingIndex === "number" && el.parentElement) {
1575
1717
  const siblings = [...el.parentElement.children].filter(
1576
1718
  (child) => child.tagName === el.tagName,
@@ -1648,6 +1790,8 @@ function createAnchorMatcher({
1648
1790
  if (ancestorScore > 0) {
1649
1791
  score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
1650
1792
  }
1793
+ // Distinguish repeated cards by their section, even when dimensions match.
1794
+ if (index < 3 && ancestors[index].text && getElementTextSignature(parent) === ancestors[index].text) score += 18;
1651
1795
  parent = parent.parentElement;
1652
1796
  }
1653
1797
 
@@ -1662,46 +1806,75 @@ function createAnchorMatcher({
1662
1806
  return score;
1663
1807
  }
1664
1808
 
1665
- function matchesAnchorIdentity(el, fingerprint) {
1809
+ function matchesAnchorIdentity(el, fingerprint, ignoreDescendantText = false, allowTextChanges = true) {
1666
1810
  if (!el || !fingerprint || el.tagName.toLowerCase() !== fingerprint.tag) return false;
1667
1811
  const attributes = { ariaLabel: "aria-label", name: "name", href: "href", src: "src", alt: "alt", title: "title", role: "role" };
1668
1812
  for (const [key, attribute] of Object.entries(attributes)) {
1669
1813
  if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
1670
1814
  }
1671
- if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText) return false;
1815
+ if (fingerprint.id && el.id !== fingerprint.id) return false;
1816
+ if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
1817
+ const stableIdentity = (fingerprint.id && el.id === fingerprint.id) ||
1818
+ (fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
1819
+ if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
1672
1820
  // Container text is useful for matching remounted drawers and dialogs.
1673
- if (!fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text) return false;
1821
+ if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
1822
+ if (!allowTextChanges || !el.children.length || !similarText(getElementTextSignature(el), fingerprint.text)) return false;
1823
+ }
1674
1824
  return true;
1675
1825
  }
1676
1826
 
1677
1827
  function matchesAnchorContext(el, context) {
1678
1828
  if (!context) return true;
1679
- if (!matchesAnchorIdentity(el, context.target)) return false;
1829
+ // A unique page landmark keeps its identity when a child is edited.
1830
+ const pageContainer = !context.layer && (context.target?.tag === "main" || context.target?.role === "main") &&
1831
+ [...document.querySelectorAll('main, [role="main"]')].filter(node => !isComtyNode(node)).length === 1;
1832
+ if (!matchesAnchorIdentity(el, context.target, pageContainer)) return false;
1680
1833
  const parentFingerprint = context.ancestors?.[0];
1681
1834
  if (parentFingerprint?.text && parentFingerprint.text !== context.target.text &&
1682
- !matchesAnchorIdentity(el.parentElement, parentFingerprint)) return false;
1835
+ !matchesAnchorIdentity(el.parentElement, parentFingerprint, pageContainer)) return false;
1683
1836
  const layer = getPresentationLayer(el);
1684
- if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer))) return false;
1837
+ if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer, false, false))) return false;
1685
1838
  if (context.version >= 2 && !context.layer && layer) return false;
1686
1839
  return scoreAnchorCandidate(el, context) >= 50;
1687
1840
  }
1688
1841
 
1689
- function resolveByAnchorContext(context) {
1690
- if (!context?.target) return null;
1842
+ function resolveAnchor(context, exact = null) {
1843
+ if (!context?.target) return { element: exact, status: exact ? "matched" : "missing" };
1691
1844
  let candidates;
1692
1845
  try {
1693
- candidates = [...document.body.querySelectorAll(context.target.tag || "*")];
1846
+ candidates = [...document.body.querySelectorAll(context.target.tag || "*")].filter(el => !isComtyNode(el));
1694
1847
  } catch {
1695
- return null;
1848
+ return { element: null, status: "missing" };
1696
1849
  }
1697
1850
  const matches = candidates
1698
- .filter(el => !isComtyNode(el) && matchesAnchorContext(el, context))
1699
- .map(el => ({ el, score: scoreAnchorCandidate(el, context) }))
1851
+ .filter(el => matchesAnchorContext(el, context))
1852
+ .map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
1700
1853
  .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;
1854
+ if (context.candidateCount > 1) {
1855
+ const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
1856
+ if (peers.length !== context.candidateCount) {
1857
+ // A removed identical sibling must not move its comment to the survivor.
1858
+ const distinctive = matches[0] && (context.target.id || context.target.testId);
1859
+ if (!distinctive) return { element: null, status: "uncertain" };
1860
+ }
1861
+ }
1862
+ if (matches[0]?.score >= 58 && (!matches[1] || matches[0].score - matches[1].score >= 12)) {
1863
+ return { element: matches[0].el, status: "matched" };
1864
+ }
1865
+ // A closed/unmounted overlay is not evidence that its contents were deleted.
1866
+ if (context.layer && !candidates.some(el => {
1867
+ const layer = getPresentationLayer(el);
1868
+ return layer && matchesAnchorIdentity(layer, context.layer, false, false);
1869
+ })) return { element: null, status: "hidden" };
1870
+ const plausible = matches.length || candidates.some(el =>
1871
+ (context.target.id && el.id === context.target.id) ||
1872
+ (context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
1873
+ return { element: null, status: plausible ? "uncertain" : "missing" };
1874
+ }
1875
+
1876
+ function resolveByAnchorContext(context) {
1877
+ return resolveAnchor(context).element;
1705
1878
  }
1706
1879
 
1707
1880
  function getViewportBounds() {
@@ -1873,7 +2046,7 @@ function createAnchorMatcher({
1873
2046
  return { clientX, clientY };
1874
2047
  }
1875
2048
 
1876
- return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
2049
+ return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
1877
2050
  }
1878
2051
 
1879
2052
  const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
@@ -2017,8 +2190,10 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2017
2190
  finally { clearTimeout(timer); }
2018
2191
  }
2019
2192
 
2020
- async function captureViewportScreenshot({ context = captureContext(), annotation } = {}) {
2193
+ async function captureViewportScreenshot({ context = captureContext(), annotation, region } = {}) {
2021
2194
  if (document.visibilityState === "hidden") return null;
2195
+ const area = region || { left: 0, top: 0, width: context.width, height: context.height };
2196
+ if (![area.left, area.top, area.width, area.height].every(Number.isFinite) || area.left < 0 || area.top < 0 || area.width <= 0 || area.height <= 0 || area.left + area.width > context.width || area.top + area.height > context.height) throw new Error("Invalid capture area");
2022
2197
  const controller = new AbortController();
2023
2198
  const timeout = setTimeout(() => controller.abort(), 15000);
2024
2199
  const { signal } = controller;
@@ -2032,7 +2207,7 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2032
2207
  let lastYield = performance.now();
2033
2208
  const checkContext = () => {
2034
2209
  signal.throwIfAborted();
2035
- if (!frozen && (context.url !== location.href || context.width !== innerWidth || context.height !== innerHeight || context.scrollX !== scrollX || context.scrollY !== scrollY)) {
2210
+ if ((!frozen || region) && (context.url !== location.href || context.width !== innerWidth || context.height !== innerHeight || context.scrollX !== scrollX || context.scrollY !== scrollY)) {
2036
2211
  throw new Error("Page moved before its screenshot was ready");
2037
2212
  }
2038
2213
  };
@@ -2231,21 +2406,21 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2231
2406
  image.src = svg;
2232
2407
  await untilAborted(image.decode());
2233
2408
  checkContext();
2234
- const scale = Math.min(2, devicePixelRatio || 1, 2560 / context.width, 2560 / context.height);
2409
+ const scale = Math.min(2, devicePixelRatio || 1, 2560 / area.width, 2560 / area.height);
2235
2410
  const canvas = document.createElement("canvas");
2236
- canvas.width = Math.round(context.width * scale);
2237
- canvas.height = Math.round(context.height * scale);
2411
+ canvas.width = Math.max(1, Math.round(area.width * scale));
2412
+ canvas.height = Math.max(1, Math.round(area.height * scale));
2238
2413
  const ctx = canvas.getContext("2d");
2239
2414
  ctx.scale(scale, scale);
2240
- ctx.drawImage(image, 0, 0);
2415
+ ctx.drawImage(image, -area.left, -area.top);
2241
2416
  let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
2242
2417
  if (!blob) throw new Error("Screenshot encoding failed");
2243
- if (blob.size > 2800000) {
2418
+ if (!region && blob.size > 2800000) {
2244
2419
  blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
2245
2420
  if (!blob) throw new Error("Screenshot encoding failed");
2246
2421
  }
2247
2422
  // CSS dimensions keep the eyedropper's coordinate system independent of DPR.
2248
- return { blob, width: canvas.width, height: canvas.height, cssWidth: context.width, cssHeight: context.height };
2423
+ return { blob, width: canvas.width, height: canvas.height, cssWidth: area.width, cssHeight: area.height };
2249
2424
  } finally {
2250
2425
  clearTimeout(timeout);
2251
2426
  controller.abort();
@@ -2371,7 +2546,7 @@ function createCommentThread(context) {
2371
2546
  renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
2372
2547
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
2373
2548
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
2374
- showAuthPrompt,
2549
+ showAuthPrompt, startThreadAgent,
2375
2550
  } = context;
2376
2551
 
2377
2552
  return function openCommentThread(
@@ -2434,16 +2609,16 @@ function createCommentThread(context) {
2434
2609
  };
2435
2610
 
2436
2611
  const saveStatus = async (nextStatus, control) => {
2437
- if (!context.canManage) return false;
2612
+ if (!context.canManage || comment.local_agent) return false;
2438
2613
  control.disabled = true;
2439
2614
  const savedStatus = await updateCommentStatus(comment, nextStatus);
2440
2615
  control.disabled = false;
2441
2616
  if (!savedStatus) return false;
2442
2617
 
2443
2618
  const savedStatusOption = getCommentStatus(savedStatus);
2444
- pin.dataset.status = savedStatusOption.value;
2445
- applyStatusDot(pinStatusDot, savedStatus);
2446
- pin.setAttribute(
2619
+ if (pin) pin.dataset.status = savedStatusOption.value;
2620
+ if (pinStatusDot) applyStatusDot(pinStatusDot, savedStatus);
2621
+ pin?.setAttribute(
2447
2622
  "aria-label",
2448
2623
  `Afficher le commentaire, statut ${savedStatusOption.label}`,
2449
2624
  );
@@ -2459,7 +2634,7 @@ function createCommentThread(context) {
2459
2634
  return true;
2460
2635
  };
2461
2636
 
2462
- statusMenuButton.disabled = !context.canManage;
2637
+ statusMenuButton.disabled = comment.local_agent || !context.canManage;
2463
2638
  statusMenuButton.onclick = (event) => {
2464
2639
  event.stopPropagation();
2465
2640
  const existingPopover = document.querySelector(
@@ -2608,7 +2783,7 @@ function createCommentThread(context) {
2608
2783
  );
2609
2784
  };
2610
2785
 
2611
- const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
2786
+ const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
2612
2787
 
2613
2788
  const startEditingEntry = (entry, item, persist) => {
2614
2789
  if (!isOwnCommentEntry(entry)) return;
@@ -2704,7 +2879,7 @@ function createCommentThread(context) {
2704
2879
  getCommentId(cachedComment) !== getCommentId(comment),
2705
2880
  );
2706
2881
  placedPins.delete(getCommentId(comment));
2707
- pin.remove();
2882
+ pin?.remove();
2708
2883
  closeBubbles();
2709
2884
  renderSidepanelComments();
2710
2885
  applyStatusFilters();
@@ -2751,11 +2926,19 @@ function createCommentThread(context) {
2751
2926
  };
2752
2927
  replies.forEach(appendReply);
2753
2928
  thread.appendChild(threadList);
2929
+ bubble.addEventListener('comty-agent-reply', (event) => {
2930
+ const reply = event.detail;
2931
+ if (!reply || replies.some(item => item.id === reply.id)) return;
2932
+ replies.push(reply);
2933
+ comment.replies = replies;
2934
+ appendReply(reply);
2935
+ threadList.scrollTop = threadList.scrollHeight;
2936
+ });
2754
2937
 
2755
2938
  const replyForm = document.createElement("div");
2756
2939
  replyForm.className = "comty-reply-form";
2757
2940
 
2758
- if (context.canWrite) {
2941
+ if (context.canWrite && !comment.local_agent) {
2759
2942
  replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
2760
2943
  const replyField = document.createElement("div");
2761
2944
  replyField.className = "comty-reply-field";
@@ -2798,6 +2981,18 @@ function createCommentThread(context) {
2798
2981
  appendReply(data.reply);
2799
2982
  replyInput.value = "";
2800
2983
  syncReplySize();
2984
+ if (/(^|\s)@agent\b/i.test(content)) {
2985
+ try {
2986
+ await startThreadAgent(comment, replies, content);
2987
+ } catch (error) {
2988
+ const notice = document.createElement('p');
2989
+ notice.className = 'comty-agent-thread-error';
2990
+ notice.setAttribute('role', 'alert');
2991
+ notice.textContent = `Agent could not start: ${error.message}`;
2992
+ replyForm.querySelector('.comty-agent-thread-error')?.remove();
2993
+ replyForm.appendChild(notice);
2994
+ }
2995
+ }
2801
2996
  requestAnimationFrame(() => {
2802
2997
  threadList.scrollTop = threadList.scrollHeight;
2803
2998
  });
@@ -2845,7 +3040,7 @@ function createCommentThread(context) {
2845
3040
  replyField.appendChild(replyInput);
2846
3041
  replyField.appendChild(replyButton);
2847
3042
  replyForm.appendChild(replyField);
2848
- } else {
3043
+ } else if (!comment.local_agent) {
2849
3044
  replyForm.classList.add("comty-reply-form-guest");
2850
3045
  const signInButton = document.createElement("button");
2851
3046
  signInButton.className = "comty-sign-in-reply";
@@ -2858,7 +3053,7 @@ function createCommentThread(context) {
2858
3053
  replyForm.appendChild(signInButton);
2859
3054
  }
2860
3055
 
2861
- thread.appendChild(replyForm);
3056
+ if (!comment.local_agent) thread.appendChild(replyForm);
2862
3057
  bubble.appendChild(thread);
2863
3058
  root.appendChild(bubble);
2864
3059
 
@@ -2879,6 +3074,290 @@ function createCommentThread(context) {
2879
3074
  }
2880
3075
  }
2881
3076
 
3077
+ // Page-coordinate drawings stay attached to the document when the viewport scrolls.
3078
+ function createInspectMarkup({ root, icon, onChange, capture, onError }) {
3079
+ const ns = 'http://www.w3.org/2000/svg';
3080
+ const layer = document.createElementNS(ns, 'svg');
3081
+ layer.classList.add('comty-markup-layer');
3082
+ layer.setAttribute('aria-hidden', 'true');
3083
+ const drawings = document.createElementNS(ns, 'g');
3084
+ layer.append(drawings);
3085
+ root.append(layer);
3086
+ const surface = document.createElement('div');
3087
+ surface.className = 'comty-markup-surface';
3088
+ surface.hidden = true;
3089
+ const selection = document.createElement('div');
3090
+ selection.className = 'comty-capture-selection';
3091
+ selection.hidden = true;
3092
+ surface.append(selection);
3093
+ root.append(surface);
3094
+ const panel = document.createElement('section');
3095
+ panel.className = 'comty-tool-panel comty-markup-panel';
3096
+ panel.id = 'comty-markup-panel';
3097
+ panel.setAttribute('aria-label', 'Annotations and screenshot');
3098
+ panel.hidden = true;
3099
+ root.append(panel);
3100
+ const shapes = {
3101
+ pencil: ['Pencil', '<path d="m16 3 5 5L8 21H3v-5ZM14 5l5 5"/>'],
3102
+ rectangle: ['Rectangle', '<rect x="4" y="5" width="16" height="14" rx="1"/>'],
3103
+ ellipse: ['Ellipse', '<ellipse cx="12" cy="12" rx="9" ry="7"/>'],
3104
+ arrow: ['Arrow', '<path d="M4 20 20 4M8 4h12v12"/>'],
3105
+ };
3106
+ const palette = [
3107
+ ['Red', '--comty-measure'], ['Yellow', '--comty-markup-yellow'],
3108
+ ['Green', '--comty-markup-green'], ['Blue', '--comty-accent'],
3109
+ ['Black', '--comty-ink'], ['White', '--comty-paper'],
3110
+ ];
3111
+ const tokenColor = token => {
3112
+ const value = getComputedStyle(root).getPropertyValue(token).trim();
3113
+ return /^#[\da-f]{3}$/i.test(value) ? '#' + [...value.slice(1)].map(char => char + char).join('') : value;
3114
+ };
3115
+ panel.setAttribute('aria-label', 'Annotation tools');
3116
+ panel.innerHTML = [
3117
+ '<div class="comty-markup-controls" role="group" aria-label="Drawing tools">',
3118
+ ...Object.entries(shapes).map(([key, [label, paths]]) => `<button type="button" data-shape="${key}" aria-label="${label}" title="${label}" aria-pressed="false">${icon(paths)}</button>`),
3119
+ '<span class="comty-markup-divider" aria-hidden="true"></span>',
3120
+ `<button type="button" data-undo aria-label="Undo annotation" title="Undo · ⌘/Ctrl Z">${icon('<path d="m9 4-5 5 5 5M4 9h10a6 6 0 0 1 0 12"/>')}</button>`,
3121
+ `<button type="button" data-clear aria-label="Clear annotations" title="Clear annotations">${icon('<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/>')}</button></div>`,
3122
+ '<div class="comty-markup-options"><div class="comty-markup-strokes" role="group" aria-label="Stroke width">',
3123
+ ...[[2, 'Thin'], [3, 'Medium'], [5, 'Thick']].map(([width, label]) => `<button type="button" data-stroke="${width}" aria-label="${label} stroke" title="${label}" aria-pressed="false"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m7 7 10 10" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg></button>`),
3124
+ '</div><span class="comty-markup-divider" aria-hidden="true"></span><div class="comty-markup-colors" role="group" aria-label="Annotation colors">',
3125
+ ...palette.map(([name, token]) => `<button type="button" data-color="${token}" aria-label="${name} ink" title="${name}" aria-pressed="false"><span class="comty-markup-swatch" aria-hidden="true"></span></button>`),
3126
+ '<button type="button" class="comty-markup-picker" title="Custom color" aria-label="Custom annotation color" aria-expanded="false" aria-controls="comty-markup-color"></button>',
3127
+ '</div></div>',
3128
+ '<div class="comty-markup-color" id="comty-markup-color" role="group" aria-label="Custom color picker" hidden>',
3129
+ '<div class="comty-markup-color-field" tabindex="0" role="group" aria-label="Saturation and brightness" aria-description="Left and right arrows adjust saturation. Up and down arrows adjust brightness."><span aria-hidden="true"></span></div>',
3130
+ '<input class="comty-markup-hue" type="range" min="0" max="360" step="1" aria-label="Hue">',
3131
+ '<label class="comty-markup-hex"><span class="comty-markup-color-preview" aria-hidden="true"></span><span>HEX</span><input type="text" aria-label="Hex color" maxlength="7" spellcheck="false" autocomplete="off" autocapitalize="off"></label>',
3132
+ '</div>',
3133
+ ].join('');
3134
+ const picker = panel.querySelector('.comty-markup-picker');
3135
+ const colorPanel = panel.querySelector('.comty-markup-color');
3136
+ const colorField = panel.querySelector('.comty-markup-color-field');
3137
+ const hueInput = panel.querySelector('.comty-markup-hue');
3138
+ const hexInput = panel.querySelector('[aria-label="Hex color"]');
3139
+ let pickerOpen = false, colorPointer = null;
3140
+ let hue = 0, saturation = 1, brightness = 1;
3141
+ let mode = null, shape = 'pencil', drag = null, visible = false, busy = false;
3142
+ let stroke = 3, color = tokenColor('--comty-measure');
3143
+ readColor(color);
3144
+ for (const button of panel.querySelectorAll('[data-color]')) button.style.setProperty('--markup-swatch', tokenColor(button.dataset.color));
3145
+ let revision = 0;
3146
+ const history = [];
3147
+ const clamp = (v, max) => Math.max(0, Math.min(max, v));
3148
+ function point(event) { return { x: clamp(event.clientX, innerWidth), y: clamp(event.clientY, innerHeight) }; }
3149
+ function sync() {
3150
+ layer.style.display = visible ? '' : 'none';
3151
+ surface.hidden = !visible || !mode;
3152
+ panel.hidden = !visible || mode !== 'annotate';
3153
+ for (const button of panel.querySelectorAll('[data-stroke]')) button.setAttribute('aria-pressed', String(Number(button.dataset.stroke) === stroke));
3154
+ for (const button of panel.querySelectorAll('[data-color]')) button.setAttribute('aria-pressed', String(tokenColor(button.dataset.color) === color));
3155
+ picker.toggleAttribute('data-selected', !palette.some(([, token]) => tokenColor(token) === color));
3156
+ syncPicker();
3157
+ for (const button of panel.querySelectorAll('[data-shape]')) button.setAttribute('aria-pressed', String(button.dataset.shape === shape));
3158
+ panel.querySelector('[data-undo]').disabled = !history.length;
3159
+ panel.querySelector('[data-clear]').disabled = !history.length;
3160
+ render();
3161
+ onChange();
3162
+ }
3163
+ function cancelDrag() {
3164
+ if (!drag) return;
3165
+ drag.node?.remove();
3166
+ const id = drag.id;
3167
+ drag = null;
3168
+ if (surface.hasPointerCapture(id)) surface.releasePointerCapture(id);
3169
+ selection.hidden = true;
3170
+ }
3171
+ function setMode(next) {
3172
+ if (busy && next) return;
3173
+ cancelDrag();
3174
+ if (!next) revision++;
3175
+ mode = next;
3176
+ if (next !== 'annotate') { pickerOpen = false; finishColorDrag(); }
3177
+ sync();
3178
+ }
3179
+ function undo() { history.pop()?.remove(); sync(); }
3180
+ function clear() { cancelDrag(); history.length = 0; drawings.replaceChildren(); sync(); }
3181
+ panel.querySelector('[data-undo]').onclick = undo;
3182
+ panel.querySelector('[data-clear]').onclick = clear;
3183
+ for (const button of panel.querySelectorAll('[data-shape]')) button.onclick = () => { shape = button.dataset.shape; sync(); };
3184
+ for (const button of panel.querySelectorAll('[data-stroke]')) button.onclick = () => { stroke = Number(button.dataset.stroke); sync(); };
3185
+ for (const button of panel.querySelectorAll('[data-color]')) button.onclick = () => {
3186
+ color = tokenColor(button.dataset.color); readColor(color); sync();
3187
+ };
3188
+ function readColor(value) {
3189
+ const [r, g, b] = value.slice(1).match(/../g).map(part => parseInt(part, 16) / 255);
3190
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
3191
+ if (delta) hue = ((max === r ? (g - b) / delta : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4) * 60 + 360) % 360;
3192
+ saturation = max ? delta / max : 0;
3193
+ brightness = max;
3194
+ }
3195
+ function writeColor() {
3196
+ const channel = offset => {
3197
+ const k = (offset + hue / 60) % 6;
3198
+ return Math.round(255 * brightness * (1 - saturation * Math.max(0, Math.min(k, 4 - k, 1)))).toString(16).padStart(2, '0');
3199
+ };
3200
+ color = '#' + [5, 3, 1].map(channel).join('');
3201
+ sync();
3202
+ }
3203
+ function syncPicker() {
3204
+ colorPanel.hidden = !pickerOpen;
3205
+ picker.setAttribute('aria-expanded', String(pickerOpen));
3206
+ colorPanel.style.setProperty('--markup-hue', `hsl(${hue} 100% 50%)`);
3207
+ colorPanel.style.setProperty('--markup-color', color);
3208
+ const marker = colorField.firstElementChild;
3209
+ marker.style.left = `${saturation * 100}%`;
3210
+ marker.style.top = `${(1 - brightness) * 100}%`;
3211
+ colorField.setAttribute('aria-description', `Saturation ${Math.round(saturation * 100)}%, brightness ${Math.round(brightness * 100)}%. Left and right arrows adjust saturation. Up and down arrows adjust brightness.`);
3212
+ hueInput.value = String(Math.round(hue));
3213
+ hueInput.setAttribute('aria-valuetext', `${Math.round(hue)} degrees`);
3214
+ if (document.activeElement !== hexInput) { hexInput.value = color.toUpperCase(); hexInput.removeAttribute('aria-invalid'); }
3215
+ }
3216
+ picker.onclick = () => { pickerOpen = !pickerOpen; sync(); };
3217
+ function moveColor(event) {
3218
+ const rect = colorField.getBoundingClientRect();
3219
+ saturation = clamp((event.clientX - rect.left) / rect.width, 1);
3220
+ brightness = 1 - clamp((event.clientY - rect.top) / rect.height, 1);
3221
+ writeColor();
3222
+ }
3223
+ function finishColorDrag() {
3224
+ const id = colorPointer;
3225
+ colorPointer = null;
3226
+ if (id !== null && colorField.hasPointerCapture(id)) colorField.releasePointerCapture(id);
3227
+ }
3228
+ colorField.onpointerdown = event => {
3229
+ if (event.button !== 0 || colorPointer !== null) return;
3230
+ event.preventDefault(); event.stopPropagation();
3231
+ colorField.focus({ preventScroll: true });
3232
+ colorPointer = event.pointerId;
3233
+ colorField.setPointerCapture(event.pointerId);
3234
+ moveColor(event);
3235
+ };
3236
+ colorField.onpointermove = event => { if (colorPointer === event.pointerId) moveColor(event); };
3237
+ colorField.onpointerup = event => { if (colorPointer === event.pointerId) { moveColor(event); finishColorDrag(); } };
3238
+ colorField.onpointercancel = finishColorDrag;
3239
+ colorField.onlostpointercapture = finishColorDrag;
3240
+ window.addEventListener('blur', finishColorDrag);
3241
+ colorField.onkeydown = event => {
3242
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
3243
+ event.preventDefault(); event.stopPropagation();
3244
+ const step = event.shiftKey ? .1 : .01;
3245
+ if (event.key === 'ArrowLeft') saturation = clamp(saturation - step, 1);
3246
+ if (event.key === 'ArrowRight') saturation = clamp(saturation + step, 1);
3247
+ if (event.key === 'ArrowUp') brightness = clamp(brightness + step, 1);
3248
+ if (event.key === 'ArrowDown') brightness = clamp(brightness - step, 1);
3249
+ writeColor();
3250
+ };
3251
+ hueInput.oninput = () => { hue = Number(hueInput.value); writeColor(); };
3252
+ function applyHex(commit = false) {
3253
+ const value = hexInput.value.trim().replace(/^#/, '');
3254
+ const valid = /^[\da-f]{6}$/i.test(value) || (commit && /^[\da-f]{3}$/i.test(value));
3255
+ if (valid) {
3256
+ color = '#' + (value.length === 3 ? [...value].map(char => char + char).join('') : value).toLowerCase();
3257
+ readColor(color); hexInput.removeAttribute('aria-invalid'); sync();
3258
+ } else hexInput.setAttribute('aria-invalid', 'true');
3259
+ if (commit) { hexInput.value = color.toUpperCase(); hexInput.removeAttribute('aria-invalid'); }
3260
+ }
3261
+ hexInput.oninput = () => applyHex();
3262
+ hexInput.onblur = () => applyHex(true);
3263
+ hexInput.onkeydown = event => { if (event.key === 'Enter') { event.preventDefault(); applyHex(true); } };
3264
+ function render() { drawings.setAttribute('transform', `translate(${-scrollX} ${-scrollY})`); }
3265
+ function update(event) {
3266
+ const p = point(event), start = drag.start;
3267
+ drag.end = p;
3268
+ if (mode === 'capture') {
3269
+ Object.assign(selection.style, { left: `${Math.min(start.x, p.x)}px`, top: `${Math.min(start.y, p.y)}px`, width: `${Math.abs(p.x - start.x)}px`, height: `${Math.abs(p.y - start.y)}px` });
3270
+ selection.textContent = `${Math.round(Math.abs(p.x - start.x))} × ${Math.round(Math.abs(p.y - start.y))}`;
3271
+ selection.hidden = false;
3272
+ return;
3273
+ }
3274
+ const a = drag.origin, b = { x: p.x + scrollX, y: p.y + scrollY };
3275
+ const left = Math.min(a.x, b.x), top = Math.min(a.y, b.y), w = Math.abs(b.x - a.x), h = Math.abs(b.y - a.y);
3276
+ let d;
3277
+ if (shape === 'pencil') {
3278
+ drag.points.push(b);
3279
+ d = drag.points.map((p, i) => `${i ? 'L' : 'M'}${p.x} ${p.y}`).join(' ');
3280
+ } else if (shape === 'rectangle') d = `M${left} ${top}h${w}v${h}h${-w}Z`;
3281
+ else if (shape === 'ellipse') d = `M${left} ${top + h/2}a${w/2} ${h/2} 0 1 0 ${w} 0a${w/2} ${h/2} 0 1 0 ${-w} 0`;
3282
+ else {
3283
+ const angle = Math.atan2(b.y-a.y, b.x-a.x), size = Math.min(16, Math.hypot(b.x-a.x,b.y-a.y)/3);
3284
+ d = `M${a.x} ${a.y}L${b.x} ${b.y}M${b.x-size*Math.cos(angle-.5)} ${b.y-size*Math.sin(angle-.5)}L${b.x} ${b.y}L${b.x-size*Math.cos(angle+.5)} ${b.y-size*Math.sin(angle+.5)}`;
3285
+ }
3286
+ drag.node.setAttribute('d', d);
3287
+ }
3288
+ surface.addEventListener('pointerdown', event => {
3289
+ if (event.button !== 0 || drag || busy) return;
3290
+ event.preventDefault(); event.stopPropagation();
3291
+ const start = point(event), origin = { x: start.x + scrollX, y: start.y + scrollY };
3292
+ drag = { id: event.pointerId, start, end: start, origin, points: [origin] };
3293
+ if (mode === 'annotate') {
3294
+ drag.node = document.createElementNS(ns, 'path');
3295
+ drag.node.setAttribute('fill', 'none');
3296
+ drag.node.setAttribute('stroke', color);
3297
+ drag.node.setAttribute('stroke-width', String(stroke));
3298
+ drag.node.setAttribute('stroke-linecap', 'round');
3299
+ drag.node.setAttribute('stroke-linejoin', 'round');
3300
+ drawings.append(drag.node);
3301
+ }
3302
+ surface.setPointerCapture(event.pointerId);
3303
+ update(event);
3304
+ });
3305
+ surface.addEventListener('pointermove', event => { if (drag?.id === event.pointerId) { event.preventDefault(); update(event); } });
3306
+ surface.addEventListener('pointerup', async event => {
3307
+ if (drag?.id !== event.pointerId) return;
3308
+ event.preventDefault(); event.stopPropagation();
3309
+ update(event);
3310
+ const finished = drag;
3311
+ drag = null;
3312
+ surface.releasePointerCapture(event.pointerId);
3313
+ selection.hidden = true;
3314
+ if (mode === 'annotate') { history.push(finished.node); sync(); return; }
3315
+ const region = { left: Math.floor(Math.min(finished.start.x, finished.end.x)), top: Math.floor(Math.min(finished.start.y, finished.end.y)), width: Math.round(Math.abs(finished.end.x-finished.start.x)), height: Math.round(Math.abs(finished.end.y-finished.start.y)) };
3316
+ if (region.width < 4 || region.height < 4) return;
3317
+ busy = true;
3318
+ const current = ++revision;
3319
+ surface.setAttribute('aria-busy', 'true');
3320
+ sync();
3321
+ try {
3322
+ const result = await capture(region, layer);
3323
+ if (current !== revision) return;
3324
+ if (!result) throw new Error('Page is not visible');
3325
+ const url = URL.createObjectURL(result.blob);
3326
+ const link = document.createElement('a');
3327
+ link.href = url; link.download = `comty-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
3328
+ link.click();
3329
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
3330
+ } catch (error) {
3331
+ if (current === revision) onError(`Capture failed. ${error.message}. Try again.`);
3332
+ } finally {
3333
+ busy = false; surface.removeAttribute('aria-busy'); sync();
3334
+ }
3335
+ });
3336
+ surface.addEventListener('pointercancel', cancelDrag);
3337
+ surface.addEventListener('lostpointercapture', cancelDrag);
3338
+ surface.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); });
3339
+ surface.addEventListener('wheel', event => { if (drag || busy) event.preventDefault(); }, { passive: false });
3340
+ document.addEventListener('scroll', () => { cancelDrag(); render(); }, { passive: true, capture: true });
3341
+ window.addEventListener('resize', cancelDrag);
3342
+ window.addEventListener('blur', cancelDrag);
3343
+ return {
3344
+ panel, setMode, render, clear,
3345
+ get mode() { return mode; },
3346
+ get busy() { return busy; },
3347
+ setVisible(value) { visible = value; if (!value) { revision++; mode = null; pickerOpen = false; finishColorDrag(); cancelDrag(); } sync(); },
3348
+ keydown(event) {
3349
+ if (!mode) return false;
3350
+ if (event.key === 'Escape') {
3351
+ if (pickerOpen) { pickerOpen = false; finishColorDrag(); picker.focus({ preventScroll: true }); sync(); return true; }
3352
+ if (drag) cancelDrag(); else setMode(null);
3353
+ return true;
3354
+ }
3355
+ if (mode === 'annotate' && (event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === 'z') { cancelDrag(); undo(); return true; }
3356
+ return false;
3357
+ },
3358
+ };
3359
+ }
3360
+
2882
3361
  const edgeAxis = { top: "y", bottom: "y", left: "x", right: "x" };
2883
3362
  const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
2884
3363
 
@@ -2911,7 +3390,7 @@ function measurementPoint(anchor, rect, scroll = { x: 0, y: 0 }) {
2911
3390
  }
2912
3391
 
2913
3392
  /** Border measurements live for one activation of the existing ruler tool. */
2914
- function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit }) {
3393
+ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit, isInteractionBlocked = () => false }) {
2915
3394
  let enabled = false;
2916
3395
  let hover = null;
2917
3396
  let drag = null;
@@ -3035,7 +3514,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
3035
3514
  }
3036
3515
  }
3037
3516
  function isHostEvent(event) {
3038
- if (!enabled || !(event.target instanceof Element)) return false;
3517
+ if (!enabled || isInteractionBlocked() || !(event.target instanceof Element)) return false;
3039
3518
  if (!isComtyElement(event.target)) return true;
3040
3519
  // The outer ruler strip must still let us anchor to the page's top/left edge.
3041
3520
  return event.target.matches(".comty-ruler") && (event.clientX + scrollX <= 10 || event.clientY + scrollY <= 10);
@@ -3073,7 +3552,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
3073
3552
  if (isHostEvent(event)) { event.preventDefault(); event.stopImmediatePropagation(); }
3074
3553
  }, true);
3075
3554
  document.addEventListener("keydown", event => {
3076
- if (!enabled || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
3555
+ if (!enabled || isInteractionBlocked() || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
3077
3556
  event.preventDefault(); event.stopImmediatePropagation();
3078
3557
  if (drag) cancel(); else onExit();
3079
3558
  }, true);
@@ -3095,7 +3574,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
3095
3574
  /** Initialize once per document; safe to import and call during server rendering. */
3096
3575
  function initComty(options = {}) {
3097
3576
  if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
3098
- const projectKey = options.projectKey;
3577
+ const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
3578
+ const agent = options.agent;
3579
+ if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
3580
+ return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
3581
+ }
3582
+ if (agent) {
3583
+ try {
3584
+ if (new URL(agent.url).origin !== window.location.origin) throw new Error();
3585
+ } catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
3586
+ }
3587
+ const projectKey = options.projectKey || (agent ? "local" : undefined);
3099
3588
  const apiUrl = options.apiUrl || "http://localhost:4000";
3100
3589
  if (typeof projectKey !== "string" || !projectKey.trim()) {
3101
3590
  return Promise.reject(new TypeError("Comty: projectKey is required"));
@@ -3107,12 +3596,14 @@ function initComty(options = {}) {
3107
3596
  if (!["http:", "https:"].includes(parsedApi.protocol)) {
3108
3597
  return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
3109
3598
  }
3110
- const configuration = JSON.stringify([projectKey, apiUrl]);
3599
+ const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
3111
3600
  const existing = window[instanceKey];
3112
3601
  if (existing) {
3113
3602
  return existing.configuration === configuration ? existing.ready
3114
3603
  : Promise.reject(new Error("Comty: already initialized with different options"));
3115
3604
  }
3605
+ const agentBridgeUrl = agent?.url;
3606
+ const localOnly = Boolean(agent && !options.projectKey);
3116
3607
  const ready = new Promise((resolve, reject) => {
3117
3608
  const mount = () => {
3118
3609
  try { resolve(mountComty()); } catch (error) { reject(error); }
@@ -3126,10 +3617,24 @@ function initComty(options = {}) {
3126
3617
 
3127
3618
  function mountComty() {
3128
3619
  let projectId = null;
3620
+ let localTasks = [];
3621
+ const deliveringAgentReplies = new Set();
3622
+ let localApprovals = [];
3623
+ const seenAgentApprovals = new Set();
3624
+ // Only requests made in this document may interrupt it with agent notifications.
3625
+ const requestedAgentTasks = new Set();
3626
+ let agentPanelOpen = false;
3627
+ let agentPanelSignature = "";
3628
+ let agentPanelTransition = 0;
3629
+ let agentConnectionError = "";
3630
+ let agentActionError = "";
3631
+ let clearAgentToast = () => {};
3632
+ const agentPendingActions = new Set();
3129
3633
  const session = {
3130
3634
  mode: "off",
3131
- get comments() { return this.mode === "browse" || this.mode === "edit"; },
3132
- get edit() { return this.mode === "edit"; },
3635
+ get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
3636
+ get edit() { return this.mode === "edit" || this.mode === "agent"; },
3637
+ get agent() { return this.mode === "agent"; },
3133
3638
  get inspect() { return this.mode === "inspect"; },
3134
3639
  };
3135
3640
  const selectedStatusFilters = new Set();
@@ -3153,9 +3658,10 @@ function initComty(options = {}) {
3153
3658
  const authReturnStorageKey = `comty:${projectKey}:auth-return`;
3154
3659
  let sdkToken = "";
3155
3660
  const activeHoverCommentIds = new Set();
3156
- /** @type {null | 'edit' | 'browse'} */
3661
+ /** @type {null | 'edit' | 'browse' | 'agent'} */
3157
3662
  let sessionEntry = null;
3158
3663
  const placedPins = new Map();
3664
+ const workingOutlines = new Map();
3159
3665
  const placementDiagnostics = new Map();
3160
3666
  let inspectedElement = null;
3161
3667
  let selectedElement = null;
@@ -3169,8 +3675,8 @@ function initComty(options = {}) {
3169
3675
  const commentTargets = new Map();
3170
3676
 
3171
3677
  const {
3172
- matchesAnchorContext, resolveByAnchorContext,
3173
- isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3678
+ matchesAnchorContext,
3679
+ resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
3174
3680
  } = createAnchorMatcher({
3175
3681
  getElementTextSignature,
3176
3682
  getElementOwnTextSignature,
@@ -3248,10 +3754,27 @@ function initComty(options = {}) {
3248
3754
  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
3755
  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
3756
  if (!canWrite) showAuthPrompt();
3251
- else if (session.inspect || !isSessionActive()) startEditSession();
3757
+ else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
3252
3758
  else if (!session.edit) setSession("edit", { entry: sessionEntry });
3253
3759
  });
3254
3760
  btn.id = "comty-toggle";
3761
+ const agentBtn = isLocalSite
3762
+ ? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
3763
+ if (session.agent) {
3764
+ const failedTask = [...localTasks].reverse().find(task => task.status === 'in_review' && !task.thread_comment_id);
3765
+ const failedPin = failedTask && placedPins.get(String(failedTask.id));
3766
+ if (failedPin && !failedPin.hasAttribute('data-comty-anchor-hidden')) { failedPin.click(); return; }
3767
+ if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
3768
+ agentPanelOpen = true;
3769
+ renderAgentPanel();
3770
+ }
3771
+ } else startAgentSession();
3772
+ })
3773
+ : null;
3774
+ if (agentBtn) {
3775
+ agentBtn.title = "Agent · Shift A · localhost only";
3776
+ agentBtn.setAttribute("aria-keyshortcuts", "Shift+A");
3777
+ }
3255
3778
  const divider = document.createElement("span");
3256
3779
  divider.className = "comty-tool-divider";
3257
3780
  toolbar.appendChild(divider);
@@ -3261,7 +3784,7 @@ function initComty(options = {}) {
3261
3784
  const primaryTools = document.createElement("div");
3262
3785
  primaryTools.className = "comty-tool-primary";
3263
3786
  toolbar.insertBefore(primaryTools, browseBtn);
3264
- for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
3787
+ for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
3265
3788
  const inspectorTools = document.createElement("div");
3266
3789
  inspectorTools.className = "comty-tool-context";
3267
3790
  inspectorTools.hidden = true;
@@ -3291,6 +3814,7 @@ function initComty(options = {}) {
3291
3814
  root,
3292
3815
  isComtyElement,
3293
3816
  isElementTreeHidden,
3817
+ isInteractionBlocked: () => Boolean(markup.mode),
3294
3818
  onExit() { rulersEnabled = false; syncInspectorTools(); },
3295
3819
  });
3296
3820
  const gridLayer = document.createElement("div");
@@ -3317,6 +3841,7 @@ function initComty(options = {}) {
3317
3841
  syncInspectorTools();
3318
3842
  });
3319
3843
  const rulersBtn = toolButton("Rulers · R", '<path d="M3 3h18v6H9v12H3Z"/><path d="M7 3v3m4-3v3m4-3v3M3 11h3m-3 4h3m-3 4h3"/>', () => {
3844
+ markup.setMode(null);
3320
3845
  closePropertyPanel();
3321
3846
  if (motion.enabled) setMotionEnabled(false);
3322
3847
  rulersEnabled = !rulersEnabled;
@@ -3327,6 +3852,7 @@ function initComty(options = {}) {
3327
3852
  syncInspectorTools();
3328
3853
  });
3329
3854
  const gridBtn = toolButton("Layout guides · G", '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 10h18M10 3v18"/>', () => {
3855
+ markup.setMode(null);
3330
3856
  closePropertyPanel();
3331
3857
  if (motion.enabled) setMotionEnabled(false);
3332
3858
  gridPanelOpen = !gridPanelOpen;
@@ -3344,7 +3870,37 @@ function initComty(options = {}) {
3344
3870
  }
3345
3871
  const motionBtn = toolButton("Motion · M", '<circle cx="12" cy="14" r="8"/><path d="M9 2h6m-3 4V2m6 5 2-2m-8 5v4l3 2"/>', () => setMotionEnabled(!motion.enabled));
3346
3872
  motionBtn.setAttribute("aria-expanded", "false");
3347
- for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn]) inspectorTools.appendChild(button);
3873
+ const annotateBtn = toolButton("Annotate · D", '<path d="m16 3 5 5L8 21H3v-5ZM14 5l5 5"/>', () => toggleMarkup("annotate"));
3874
+ const screenshotBtn = toolButton("Capture area · S", '<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"/><rect x="7" y="8" width="10" height="8" rx="1"/><circle cx="12" cy="12" r="2"/>', () => toggleMarkup("capture"));
3875
+ const markup = createInspectMarkup({
3876
+ root, icon,
3877
+ onError: description => showNotice({ title: "Screenshot unavailable", description }),
3878
+ onChange() {
3879
+ annotateBtn.setAttribute("aria-pressed", String(markup.mode === "annotate"));
3880
+ annotateBtn.setAttribute("aria-expanded", String(markup.mode === "annotate"));
3881
+ screenshotBtn.setAttribute("aria-pressed", String(markup.mode === "capture"));
3882
+ screenshotBtn.setAttribute("aria-busy", String(markup.busy));
3883
+ positionToolbar();
3884
+ },
3885
+ capture: (region, layer) => captureViewportScreenshot({ region, annotation: () => ({
3886
+ elements: [overlayTools, root.querySelector('.comty-distance-layer'), layer].filter(node => node && !node.hidden),
3887
+ dispose() {},
3888
+ }) }),
3889
+ });
3890
+ for (const button of [annotateBtn]) {
3891
+ button.setAttribute("aria-controls", markup.panel.id);
3892
+ button.setAttribute("aria-expanded", "false");
3893
+ }
3894
+ function toggleMarkup(mode) {
3895
+ const next = markup.mode === mode ? null : mode;
3896
+ closePropertyPanel();
3897
+ if (motion.enabled) setMotionEnabled(false);
3898
+ gridPanelOpen = false; toolPanel.hidden = true; gridBtn.setAttribute("aria-expanded", "false");
3899
+ cancelRulerDrag();
3900
+ selectedElement = null; measuredRegion = null; inspectDrag = null; hideInspector();
3901
+ markup.setMode(next);
3902
+ }
3903
+ for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn, annotateBtn, screenshotBtn]) inspectorTools.appendChild(button);
3348
3904
 
3349
3905
  const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
3350
3906
  const motionPanel = document.createElement('section');
@@ -3393,6 +3949,7 @@ function initComty(options = {}) {
3393
3949
  motion.enabled = enabled; motionPanel.hidden = !enabled;
3394
3950
  motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
3395
3951
  if (enabled) {
3952
+ markup.setMode(null);
3396
3953
  closePropertyPanel();
3397
3954
  rulersEnabled = false;
3398
3955
  syncInspectorTools();
@@ -3651,6 +4208,7 @@ function initComty(options = {}) {
3651
4208
  const visible = session.inspect;
3652
4209
  measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
3653
4210
  overlayTools.hidden = !visible;
4211
+ markup.setVisible(visible);
3654
4212
  xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
3655
4213
  rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
3656
4214
  gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
@@ -3934,6 +4492,7 @@ function initComty(options = {}) {
3934
4492
  }
3935
4493
 
3936
4494
  function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
4495
+ markup.setMode(null);
3937
4496
  propertyPicking = null;
3938
4497
  colorMagnifier.setEnabled(false);
3939
4498
  delete document.documentElement.dataset.comtyPropertyPicking;
@@ -4114,6 +4673,50 @@ function initComty(options = {}) {
4114
4673
  let toolbarMotionFrame = 0;
4115
4674
  let toolbarAnimations = [];
4116
4675
  const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
4676
+ let workingIndicatorTimer = 0;
4677
+ function scheduleWorkingIndicators() {
4678
+ if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
4679
+ workingIndicatorTimer = setTimeout(() => {
4680
+ workingIndicatorTimer = 0;
4681
+ if (!root.isConnected) return;
4682
+ const indicators = root.querySelectorAll('.comty-working-indicator');
4683
+ if (!indicators.length) return;
4684
+ for (const indicator of indicators) {
4685
+ const previous = Number(indicator.dataset.activeDots.split(',')[0]);
4686
+ const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
4687
+ const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
4688
+ indicator.dataset.activeDots = `${first},${second}`;
4689
+ [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
4690
+ }
4691
+ scheduleWorkingIndicators();
4692
+ }, 600);
4693
+ }
4694
+ function createWorkingIndicator() {
4695
+ const indicator = document.createElement('span');
4696
+ indicator.className = 'comty-working-indicator';
4697
+ indicator.setAttribute('aria-hidden', 'true');
4698
+ indicator.dataset.activeDots = '3,5';
4699
+ for (let index = 0; index < 9; index++) {
4700
+ const dot = document.createElement('i');
4701
+ dot.toggleAttribute('data-active', index === 3 || index === 5);
4702
+ indicator.appendChild(dot);
4703
+ }
4704
+ scheduleWorkingIndicators();
4705
+ return indicator;
4706
+ }
4707
+ function resetWorkingIndicators() {
4708
+ clearTimeout(workingIndicatorTimer);
4709
+ workingIndicatorTimer = 0;
4710
+ if (reducedMotion.matches) {
4711
+ for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
4712
+ indicator.dataset.activeDots = '3,5';
4713
+ [...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
4714
+ }
4715
+ }
4716
+ if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
4717
+ }
4718
+ reducedMotion.addEventListener('change', resetWorkingIndicators);
4719
+ document.addEventListener('visibilitychange', resetWorkingIndicators);
4117
4720
  const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
4118
4721
  function rememberToolbarPosition(left, top) {
4119
4722
  const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
@@ -4147,7 +4750,7 @@ function initComty(options = {}) {
4147
4750
  });
4148
4751
  }
4149
4752
  const rect = toolbar.getBoundingClientRect();
4150
- for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
4753
+ for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel]) {
4151
4754
  if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
4152
4755
  const above = rect.top - 16;
4153
4756
  const below = innerHeight - rect.bottom - 16;
@@ -4164,9 +4767,10 @@ function initComty(options = {}) {
4164
4767
  panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
4165
4768
  panel.style.bottom = "auto";
4166
4769
  }
4770
+ positionNotice();
4167
4771
  }
4168
4772
  const toolbarResize = new ResizeObserver(positionToolbar);
4169
- [toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
4773
+ [toolbar, toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel].forEach(node => toolbarResize.observe(node));
4170
4774
  window.addEventListener("resize", positionToolbar);
4171
4775
 
4172
4776
  toolbar.addEventListener("pointerdown", event => {
@@ -4226,7 +4830,7 @@ function initComty(options = {}) {
4226
4830
 
4227
4831
  function prepareToolbarMotion() {
4228
4832
  if (toolbarMotionFrame) return;
4229
- const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools].filter(Boolean);
4833
+ const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
4230
4834
  // Move each group's background with its icons, including when interrupted.
4231
4835
  const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
4232
4836
  rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
@@ -4450,7 +5054,8 @@ function initComty(options = {}) {
4450
5054
  selectedElement = null;
4451
5055
  sessionEntry = null;
4452
5056
  session.mode = "off";
4453
- document.documentElement.classList.remove("comty-comments-active", "comty-comment-placement");
5057
+ syncPlacementCursor();
5058
+ document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
4454
5059
  hideInspector();
4455
5060
  setSidepanelOpen(false);
4456
5061
  closeBubbles();
@@ -4464,25 +5069,29 @@ function initComty(options = {}) {
4464
5069
  if (mode === "off") return;
4465
5070
  if (mode === "inspect") {
4466
5071
  session.mode = "inspect";
5072
+ document.documentElement.classList.add("comty-inspect-annotations");
4467
5073
  syncToolbar();
5074
+ renderCachedComments();
4468
5075
  ensureLiveTracking();
4469
5076
  return;
4470
5077
  }
4471
5078
 
4472
- if (session.edit && mode === "browse") {
5079
+ if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
4473
5080
  closeBubbles();
4474
5081
  }
4475
5082
  const hadComments = session.comments;
4476
5083
  session.mode = mode;
4477
5084
  sessionEntry = entry;
5085
+ document.documentElement.classList.remove("comty-inspect-annotations");
4478
5086
  document.documentElement.classList.toggle("comty-comments-active", session.comments);
4479
- document.documentElement.classList.toggle("comty-comment-placement", session.edit);
5087
+ syncPlacementCursor();
4480
5088
  if (session.edit) {
4481
5089
  setMotionEnabled(false);
4482
5090
  selectedElement = null;
4483
5091
  } else {
4484
5092
  hideInspector();
4485
5093
  }
5094
+ if (session.agent && sidepanelOpen) setSidepanelOpen(false);
4486
5095
  syncToolbar();
4487
5096
  ensureLiveTracking();
4488
5097
  if (renderComments || !hadComments) renderCachedComments();
@@ -4495,6 +5104,10 @@ function initComty(options = {}) {
4495
5104
  }
4496
5105
 
4497
5106
  function showAuthPrompt(forbidden = false, onReady = startEditSession) {
5107
+ if (localOnly) {
5108
+ showNotice({ title: "Comments are not configured", description: "Connect this site to a Comty project to leave shared comments." });
5109
+ return;
5110
+ }
4498
5111
  const guestAllowed = allowGuestComments;
4499
5112
  document.querySelector(".comty-auth-backdrop")?.remove();
4500
5113
  const opener = document.activeElement;
@@ -4616,14 +5229,17 @@ function initComty(options = {}) {
4616
5229
  }
4617
5230
 
4618
5231
  function syncToolbar() {
4619
- if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
5232
+ if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
4620
5233
  root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
4621
5234
  browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
4622
5235
  inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
4623
- btn.setAttribute("aria-pressed", String(session.edit));
4624
- commentTools.hidden = session.inspect;
5236
+ btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
5237
+ agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
5238
+ commentTools.hidden = session.inspect || session.agent;
4625
5239
  inspectorTools.hidden = !session.inspect;
4626
5240
  syncInspectorTools();
5241
+ renderAgentPanel();
5242
+ positionNotice();
4627
5243
  }
4628
5244
 
4629
5245
  const inspectionLayer = document.createElement("div");
@@ -4936,7 +5552,7 @@ function initComty(options = {}) {
4936
5552
  });
4937
5553
  return;
4938
5554
  }
4939
- if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
5555
+ if (markup.mode || propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
4940
5556
  if (inspectionLayer.childNodes.length) hideInspector();
4941
5557
  return;
4942
5558
  }
@@ -5080,10 +5696,20 @@ function initComty(options = {}) {
5080
5696
 
5081
5697
  function startBrowseSession() {
5082
5698
  setSession("browse", { renderComments: true });
5699
+ void loadComments({ force: true });
5083
5700
  }
5084
5701
 
5085
5702
  function startEditSession() {
5086
5703
  setSession("edit", { renderComments: true });
5704
+ void loadComments({ force: true });
5705
+ }
5706
+
5707
+ function startAgentSession() {
5708
+ if (!isLocalSite) return;
5709
+ if (session.agent) return;
5710
+ closeBubbles();
5711
+ setSession("agent", { renderComments: true });
5712
+ void loadComments({ force: true });
5087
5713
  }
5088
5714
 
5089
5715
  function toggleSidepanel() {
@@ -5109,7 +5735,7 @@ function initComty(options = {}) {
5109
5735
  showAuthPrompt();
5110
5736
  return;
5111
5737
  }
5112
- if (session.inspect || !isSessionActive()) {
5738
+ if (session.agent || session.inspect || !isSessionActive()) {
5113
5739
  startEditSession();
5114
5740
  return;
5115
5741
  }
@@ -5397,6 +6023,24 @@ function initComty(options = {}) {
5397
6023
  ancestors,
5398
6024
  layer: layer ? getElementFingerprint(layer) : null,
5399
6025
  };
6026
+ // React exposes this in development builds. Keep it as a hint: the coding
6027
+ // agent must confirm the component and variant in source before editing.
6028
+ const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
6029
+ let fiber = fiberKey ? el[fiberKey] : null;
6030
+ const components = [];
6031
+ while (fiber && components.length < 8) {
6032
+ const component = fiber.type;
6033
+ const name = component?.displayName || component?.name;
6034
+ if (name && typeof name === 'string') {
6035
+ const props = fiber.memoizedProps || {};
6036
+ const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
6037
+ ['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
6038
+ const source = fiber._debugSource;
6039
+ components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
6040
+ }
6041
+ fiber = fiber.return;
6042
+ }
6043
+ if (components.length) context.components = components;
5400
6044
  context.candidateCount = [...document.querySelectorAll(context.target.tag)]
5401
6045
  .filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
5402
6046
  return context;
@@ -5425,6 +6069,11 @@ function initComty(options = {}) {
5425
6069
  };
5426
6070
  }
5427
6071
 
6072
+ function getPagePath() {
6073
+ const { pathname, hash } = window.location;
6074
+ return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
6075
+ }
6076
+
5428
6077
  function resolveBySelector(selector) {
5429
6078
  if (!selector) return null;
5430
6079
  try {
@@ -5446,17 +6095,9 @@ function initComty(options = {}) {
5446
6095
  }
5447
6096
  const context = parseAnchorContext(comment);
5448
6097
  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 });
6098
+ const resolution = resolveAnchor(context, exact);
6099
+ const element = resolution.element ? getAnchorElement(resolution.element) : null;
6100
+ commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
5460
6101
  return element;
5461
6102
  }
5462
6103
 
@@ -5527,9 +6168,14 @@ function initComty(options = {}) {
5527
6168
  return { deviceType: "desktop", deviceLabel: "Desktop" };
5528
6169
  }
5529
6170
 
6171
+ function syncPlacementCursor() {
6172
+ document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
6173
+ }
6174
+
5530
6175
  function closeBubbles() {
5531
6176
  const hadDraft = Boolean(draftAnchor);
5532
6177
  draftAnchor = null;
6178
+ syncPlacementCursor();
5533
6179
  activeHoverCommentIds.clear();
5534
6180
  if (!session.edit || hadDraft) hideInspector();
5535
6181
  document
@@ -5574,7 +6220,7 @@ function initComty(options = {}) {
5574
6220
  pending: commentId.startsWith("pending-"),
5575
6221
  project_id: projectId,
5576
6222
  domain: payload.host || window.location.host,
5577
- page_path: payload.pagePath || window.location.pathname,
6223
+ page_path: payload.pagePath || getPagePath(),
5578
6224
  selector: payload.selector || "body",
5579
6225
  rel_x: payload.relX,
5580
6226
  rel_y: payload.relY,
@@ -5599,7 +6245,7 @@ function initComty(options = {}) {
5599
6245
  replies: [],
5600
6246
  };
5601
6247
 
5602
- cachedPagePath = window.location.pathname;
6248
+ cachedPagePath = getPagePath();
5603
6249
  cachedComments = [
5604
6250
  ...cachedComments.filter(
5605
6251
  (cached) => getCommentId(cached) !== commentId,
@@ -5612,6 +6258,7 @@ function initComty(options = {}) {
5612
6258
  }
5613
6259
 
5614
6260
  function getCommentAuthorName(comment) {
6261
+ if (comment?.author_kind === 'agent') return 'Agent';
5615
6262
  return comment?.author_name || comment?.name || "Anonymous";
5616
6263
  }
5617
6264
 
@@ -5633,6 +6280,12 @@ function initComty(options = {}) {
5633
6280
  function createCommentAvatar(comment, extraClass = "") {
5634
6281
  const avatar = document.createElement("span");
5635
6282
  avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
6283
+ if (comment?.author_kind === 'agent') {
6284
+ avatar.classList.add('comty-agent-avatar');
6285
+ avatar.setAttribute('aria-label', 'Agent');
6286
+ 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>';
6287
+ return avatar;
6288
+ }
5636
6289
  const authorName = getCommentAuthorName(comment);
5637
6290
  const avatarUrl = getCommentAuthorAvatarUrl(comment);
5638
6291
 
@@ -5640,6 +6293,7 @@ function initComty(options = {}) {
5640
6293
  const image = document.createElement("img");
5641
6294
  image.src = avatarUrl;
5642
6295
  image.alt = "";
6296
+ image.draggable = false;
5643
6297
  image.onerror = () => {
5644
6298
  image.remove();
5645
6299
  avatar.innerText = authorName.charAt(0).toUpperCase();
@@ -5793,6 +6447,7 @@ function initComty(options = {}) {
5793
6447
  function createCommentPin(comment, x, y) {
5794
6448
  const pin = document.createElement("button");
5795
6449
  pin.className = "comty-pin";
6450
+ pin.toggleAttribute('data-agent-task', Boolean(comment.local_agent));
5796
6451
  pin.type = "button";
5797
6452
  pin.style.left = `${x}px`;
5798
6453
  pin.style.top = `${y}px`;
@@ -5810,10 +6465,52 @@ function initComty(options = {}) {
5810
6465
  const thread = document.createElement("div");
5811
6466
  thread.className = "comty-thread";
5812
6467
  thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
6468
+ if (comment.local_agent) {
6469
+ thread.appendChild(createThreadItem({ author_kind: 'agent', content: 'Modification failed. Open to retry or dismiss.' }, { showMore: false }).item);
6470
+ }
5813
6471
  bubble.appendChild(thread);
5814
6472
  return bubble;
5815
6473
  }
5816
6474
 
6475
+ function openAgentTask(task, x, y) {
6476
+ closeBubbles();
6477
+ const bubble = document.createElement('div');
6478
+ bubble.className = 'comty-bubble comty-agent-task-thread';
6479
+ bubble.dataset.commentId = task.id;
6480
+ bubble.setAttribute('role', 'dialog');
6481
+ bubble.setAttribute('aria-label', 'Failed modification');
6482
+ const header = document.createElement('div'); header.className = 'comty-bubble-header';
6483
+ const title = document.createElement('strong'); title.textContent = 'Modification failed';
6484
+ const close = document.createElement('button'); close.type = 'button'; close.className = 'comty-icon-button';
6485
+ close.setAttribute('aria-label', 'Close failed modification');
6486
+ close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
6487
+ close.onclick = () => closeBubbles();
6488
+ header.append(title, close);
6489
+ const thread = document.createElement('div'); thread.className = 'comty-thread';
6490
+ const list = document.createElement('div'); list.className = 'comty-thread-list';
6491
+ const issue = task.error ? describeAgentError(task.error) : null;
6492
+ list.append(
6493
+ createThreadItem(task, { showMore: false }).item,
6494
+ createThreadItem({ author_kind: 'agent', content: task.result?.summary || issue?.message || issue?.description || 'The agent could not complete this request.' }, { showMore: false }).item,
6495
+ );
6496
+ thread.appendChild(list);
6497
+ const actions = document.createElement('div'); actions.className = 'comty-agent-task-actions';
6498
+ for (const action of ['retry', 'dismiss']) {
6499
+ const button = document.createElement('button'); button.type = 'button'; button.className = 'comty-agent-action';
6500
+ button.textContent = action === 'retry' ? 'Retry' : 'Dismiss';
6501
+ button.onclick = async () => {
6502
+ for (const control of actions.children) control.disabled = true;
6503
+ try { await runAgentAction(`tasks/${task.id}/${action}`); }
6504
+ catch (error) { showAgentBridgeError(error.message); }
6505
+ finally { for (const control of actions.children) control.disabled = false; }
6506
+ };
6507
+ actions.appendChild(button);
6508
+ }
6509
+ bubble.append(header, thread, actions);
6510
+ root.appendChild(bubble);
6511
+ positionCommentBubble(bubble, x, y);
6512
+ }
6513
+
5817
6514
  function mountCaptureAnnotation(comment, { x, y }) {
5818
6515
  const host = document.createElement("div");
5819
6516
  host.dataset.comtyCaptureExclude = "";
@@ -5864,6 +6561,7 @@ function initComty(options = {}) {
5864
6561
  payload,
5865
6562
  preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
5866
6563
  };
6564
+ syncPlacementCursor();
5867
6565
  ensureLiveTracking();
5868
6566
 
5869
6567
  const pin = document.createElement("div");
@@ -5881,10 +6579,21 @@ function initComty(options = {}) {
5881
6579
 
5882
6580
  const textarea = document.createElement("textarea");
5883
6581
  textarea.className = "comty-create-textarea";
5884
- textarea.placeholder = "Add a comment";
6582
+ textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
5885
6583
  textarea.setAttribute("aria-label", textarea.placeholder);
5886
6584
  textarea.rows = 1;
5887
6585
 
6586
+ const scopePicker = document.createElement('label');
6587
+ scopePicker.className = 'comty-agent-scope';
6588
+ if (payload.kind === 'agent') {
6589
+ const input = document.createElement('input');
6590
+ input.type = 'checkbox';
6591
+ input.setAttribute('aria-label', 'Apply to all instances of this variant');
6592
+ const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
6593
+ scopePicker.title = 'Apply to all instances of this variant';
6594
+ scopePicker.append(input, track, document.createTextNode('Apply globally'));
6595
+ }
6596
+
5888
6597
  const actions = document.createElement("div");
5889
6598
  actions.className = "comty-create-actions";
5890
6599
 
@@ -5938,7 +6647,7 @@ function initComty(options = {}) {
5938
6647
  const submitBtn = document.createElement("button");
5939
6648
  submitBtn.type = "button";
5940
6649
  submitBtn.className = "comty-btn-submit";
5941
- submitBtn.setAttribute("aria-label", "Post comment");
6650
+ submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
5942
6651
  submitBtn.disabled = true;
5943
6652
  submitBtn.innerHTML = `
5944
6653
  <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 +6664,63 @@ function initComty(options = {}) {
5955
6664
  textarea.disabled = true;
5956
6665
  linkBtn.disabled = true;
5957
6666
 
6667
+ if (payload.kind === "agent" && !agent) {
6668
+ showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
6669
+ }
5958
6670
  payload.content = content;
6671
+ if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
5959
6672
  payload.linkUrl = attachedLink || null;
5960
6673
  textarea.textContent = content;
5961
6674
 
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.
6675
+ const isAgentTask = payload.kind === "agent";
6676
+ const context = isAgentTask ? null : captureContext();
6677
+ const point = isAgentTask ? null : getPinPoint(pin);
6678
+ const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
5966
6679
  bubble.remove();
5967
6680
  pin.remove();
5968
6681
  if (draftAnchor === editorDraft) draftAnchor = null;
6682
+ syncPlacementCursor();
5969
6683
  hideInspector();
5970
- addLocalComment(pendingId, payload);
6684
+ if (!isAgentTask) addLocalComment(pendingId, payload);
5971
6685
  let post;
5972
6686
  try {
5973
- post = await fetch(`${apiUrl}/api/comments`, {
6687
+ post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
5974
6688
  method: "POST",
5975
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
5976
- body: JSON.stringify(payload),
6689
+ headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
6690
+ body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
5977
6691
  });
5978
- if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
6692
+ if (!post.ok) throw new Error(`Save failed (${post.status})`);
5979
6693
  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 });
6694
+ const id = isAgentTask ? data?.task?.id : data?.id;
6695
+ if (!id) throw new Error("Save returned no ID");
6696
+ if (isAgentTask) {
6697
+ requestedAgentTasks.add(id);
6698
+ void loadComments({ force: true });
6699
+ } else {
6700
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
6701
+ if (context.url === location.href) {
6702
+ const comment = addLocalComment(String(id), payload);
6703
+ void uploadCommentScreenshot(comment, context, point);
6704
+ } else void loadComments({ force: true });
6705
+ }
5987
6706
  } catch (error) {
5988
6707
  console.error("Comty: save failed", error);
5989
- cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
5990
- renderSidepanelComments();
5991
- void renderComments(cachedComments, { retry: false });
6708
+ if (isAgentTask) showAgentBridgeError(error.message);
6709
+ else {
6710
+ cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
6711
+ renderSidepanelComments();
6712
+ void renderComments(cachedComments, { retry: false });
6713
+ }
5992
6714
  // Keep the original editor and its text available for retry, including attachments.
5993
6715
  if (!draftAnchor) draftAnchor = editorDraft;
6716
+ syncPlacementCursor();
5994
6717
  root.append(pin, bubble);
5995
6718
  submitBtn.disabled = false;
5996
6719
  textarea.disabled = false;
5997
6720
  linkBtn.disabled = false;
5998
6721
  const errorMessage = document.createElement("p");
5999
6722
  errorMessage.setAttribute("role", "alert");
6000
- errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
6723
+ errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
6001
6724
  bubble.querySelector('[role="alert"]')?.remove();
6002
6725
  bubble.append(errorMessage);
6003
6726
  if (bubble.isConnected) textarea.focus();
@@ -6086,7 +6809,8 @@ function initComty(options = {}) {
6086
6809
  renderInspector();
6087
6810
  };
6088
6811
 
6089
- actions.append(linkBtn, submitBtn, linkEditor);
6812
+ if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
6813
+ else actions.append(linkBtn, submitBtn, linkEditor);
6090
6814
 
6091
6815
  bubble.appendChild(textarea);
6092
6816
  bubble.appendChild(attachment);
@@ -6099,6 +6823,249 @@ function initComty(options = {}) {
6099
6823
  setTimeout(() => textarea.focus(), 10);
6100
6824
  }
6101
6825
 
6826
+ function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
6827
+
6828
+ function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
6829
+ const issue = describeAgentError(message);
6830
+ showNotice({ title: 'Modification failed', description: issue.title === 'Task could not finish' ? 'The agent could not complete this request.' : issue.title + '.' });
6831
+ }
6832
+
6833
+ function positionNotice() {
6834
+ const toast = root.querySelector(".comty-agent-task-toast");
6835
+ if (!toast) return;
6836
+ toast.style.removeProperty("left");
6837
+ toast.style.removeProperty("top");
6838
+ toast.toggleAttribute("data-toolbar", isSessionActive());
6839
+ if (!isSessionActive()) return;
6840
+ const rect = toolbar.getBoundingClientRect();
6841
+ const above = rect.top - toast.offsetHeight - 8;
6842
+ toast.style.left = `${Math.max(8, Math.min(rect.right - toast.offsetWidth, innerWidth - toast.offsetWidth - 8))}px`;
6843
+ toast.style.top = `${Math.max(8, Math.min(above >= 8 ? above : rect.bottom + 8, innerHeight - toast.offsetHeight - 8))}px`;
6844
+ }
6845
+
6846
+ function showNotice(issue) {
6847
+ clearAgentToast();
6848
+ const toast = document.createElement("div");
6849
+ toast.className = "comty-agent-task-toast";
6850
+ toast.setAttribute("role", "alert");
6851
+ const title = document.createElement("strong"); title.textContent = issue.title;
6852
+ const description = document.createElement("p"); description.textContent = issue.description;
6853
+ const close = document.createElement("button"); close.type = "button";
6854
+ close.setAttribute("aria-label", "Dismiss notification");
6855
+ close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
6856
+ toast.append(title, description, close);
6857
+ let timer, leaving = false;
6858
+ const dismiss = () => {
6859
+ if (leaving) return;
6860
+ leaving = true;
6861
+ clearTimeout(timer);
6862
+ toast.dataset.leaving = "";
6863
+ timer = setTimeout(() => toast.remove(), reducedMotion.matches ? 0 : 200);
6864
+ };
6865
+ const schedule = () => { if (!leaving) { clearTimeout(timer); timer = setTimeout(dismiss, 6000); } };
6866
+ const pause = () => { if (!leaving) clearTimeout(timer); };
6867
+ clearAgentToast = () => { clearTimeout(timer); toast.remove(); };
6868
+ close.onclick = dismiss;
6869
+ toast.onmouseenter = pause;
6870
+ toast.onmouseleave = schedule;
6871
+ toast.onfocusin = pause;
6872
+ toast.onfocusout = schedule;
6873
+ root.appendChild(toast);
6874
+ positionNotice();
6875
+ schedule();
6876
+ }
6877
+
6878
+ function describeAgentError(error) {
6879
+ const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
6880
+ let value = error;
6881
+ for (let depth = 0; depth < 8; depth++) {
6882
+ if (typeof value === 'string') {
6883
+ try { value = JSON.parse(value); continue; } catch {}
6884
+ const start = value.indexOf('{'), end = value.lastIndexOf('}');
6885
+ if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
6886
+ break;
6887
+ }
6888
+ if (!value || typeof value !== 'object') break;
6889
+ value = value.error || value.message || value.cause || value.stderr || value;
6890
+ }
6891
+ const message = typeof value === 'string' ? value : '';
6892
+ const source = raw + ' ' + message;
6893
+ const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
6894
+ let title, description;
6895
+ if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
6896
+ title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
6897
+ } else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
6898
+ title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
6899
+ } else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
6900
+ title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
6901
+ } else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
6902
+ title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
6903
+ } else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
6904
+ title = 'Model unavailable'; description = 'The local agent rejected this model. Check its CLI version and signed-in account, then retry.';
6905
+ } else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
6906
+ title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
6907
+ } else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
6908
+ title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
6909
+ } else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
6910
+ title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
6911
+ } else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
6912
+ title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
6913
+ } else if (/completion was not verified/i.test(source)) {
6914
+ title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
6915
+ } else {
6916
+ title = 'Task could not finish';
6917
+ description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 160) + (message.length > 160 ? '…' : '') : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
6918
+ }
6919
+ return { title, description, raw, message };
6920
+ }
6921
+
6922
+ async function runAgentAction(path, input = {}) {
6923
+ const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
6924
+ if (!response.ok) {
6925
+ const body = await response.json().catch(() => null);
6926
+ throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
6927
+ }
6928
+ if (path.endsWith('/retry')) requestedAgentTasks.add(path.split('/')[1]);
6929
+ await loadComments({ force: true });
6930
+ }
6931
+
6932
+ function renderAgentPanel() {
6933
+ if (!agent) return;
6934
+ const visibleTasks = [];
6935
+ const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
6936
+ if (latestResult) visibleTasks.push(latestResult);
6937
+ if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
6938
+ let panel = root.querySelector(".comty-agent-panel");
6939
+ if (!panel && !agentPanelOpen) return;
6940
+ if (!panel) {
6941
+ panel = document.createElement("section");
6942
+ panel.className = "comty-agent-panel";
6943
+ panel.setAttribute("aria-label", "Local agent tasks");
6944
+ panel.hidden = true;
6945
+ // Keep the task card above inspection overlays and below the toolbar.
6946
+ root.insertBefore(panel, toolbar);
6947
+ }
6948
+ const open = Boolean(agent) && agentPanelOpen;
6949
+ if (open) {
6950
+ clearAgentToast();
6951
+ if (panel.hidden) {
6952
+ const rect = toolbar.getBoundingClientRect();
6953
+ panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
6954
+ panel.dataset.closed = "";
6955
+ panel.hidden = false;
6956
+ panel.getBoundingClientRect();
6957
+ }
6958
+ if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
6959
+ } else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
6960
+ const transition = ++agentPanelTransition;
6961
+ panel.dataset.closed = "";
6962
+ const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
6963
+ if (reducedMotion.matches) finish();
6964
+ else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
6965
+ }
6966
+ panel.inert = !open;
6967
+ panel.setAttribute("aria-hidden", String(!open));
6968
+ const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
6969
+ if (signature === agentPanelSignature) return;
6970
+ agentPanelSignature = signature;
6971
+ const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
6972
+ const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
6973
+ const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
6974
+ const node = (tag, className, text) => {
6975
+ const element = document.createElement(tag);
6976
+ if (className) element.className = className;
6977
+ if (text != null) element.textContent = text;
6978
+ return element;
6979
+ };
6980
+ const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
6981
+ panel.replaceChildren();
6982
+ const header = node('header', 'comty-agent-panel-header');
6983
+ const heading = node('div', 'comty-agent-heading');
6984
+ heading.append(node('strong', '', 'Agent'));
6985
+ const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
6986
+ toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
6987
+ toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
6988
+ toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
6989
+ header.append(heading, toggle); panel.appendChild(header);
6990
+ const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
6991
+ const technicalDetails = (parent, raw, key) => {
6992
+ const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
6993
+ const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
6994
+ details.append(summary, node('pre', '', raw)); parent.appendChild(details);
6995
+ };
6996
+ const errorMessage = (parent, error, key) => {
6997
+ const issue = describeAgentError(error);
6998
+ const message = node('div', 'comty-agent-error');
6999
+ message.append(node('strong', '', issue.title), node('p', '', issue.description));
7000
+ parent.appendChild(message); technicalDetails(parent, issue.raw, key);
7001
+ };
7002
+ if (agentConnectionError || agentActionError) {
7003
+ const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
7004
+ errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
7005
+ }
7006
+ const action = (parent, label, path, input = {}, primary = false) => {
7007
+ const button = node('button', 'comty-agent-action', label); button.type = 'button';
7008
+ const key = path + JSON.stringify(input); button.dataset.focusKey = key;
7009
+ button.disabled = agentPendingActions.has(key);
7010
+ if (primary) button.dataset.primary = '';
7011
+ button.onclick = async () => {
7012
+ if (agentPendingActions.has(key)) return;
7013
+ agentActionError = '';
7014
+ agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
7015
+ try {
7016
+ await runAgentAction(path, input);
7017
+ } catch (error) { agentActionError = error.message; }
7018
+ finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
7019
+ };
7020
+ parent.appendChild(button);
7021
+ };
7022
+ for (const approval of localApprovals) {
7023
+ const section = node('div', 'comty-agent-item');
7024
+ section.append(node('h3', '', 'Approval required'));
7025
+ const task = localTasks.find(task => task.id === approval.taskId);
7026
+ if (task) section.appendChild(node('p', '', task.content));
7027
+ section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
7028
+ const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
7029
+ if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
7030
+ technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
7031
+ const actions = node('div', 'comty-agent-actions');
7032
+ action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
7033
+ action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
7034
+ section.appendChild(actions); content.appendChild(section);
7035
+ }
7036
+ const list = node('div', 'comty-agent-list'); content.appendChild(list);
7037
+ for (const task of visibleTasks.reverse()) {
7038
+ const failed = Boolean(task.error) || task.status === 'in_review';
7039
+ const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
7040
+ const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
7041
+ const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
7042
+ dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
7043
+ const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
7044
+ row.append(dot, name);
7045
+ if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
7046
+ const actions = node('div', 'comty-agent-row-actions');
7047
+ if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
7048
+ if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
7049
+ if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
7050
+ if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
7051
+ if (actions.childElementCount) row.appendChild(actions);
7052
+ row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
7053
+ list.appendChild(row);
7054
+ if (task.error) {
7055
+ const details = node('div', 'comty-agent-item');
7056
+ errorMessage(details, task.error, task.id);
7057
+ list.appendChild(details);
7058
+ }
7059
+ if (task.result?.summary) {
7060
+ const answer = node('div', 'comty-agent-answer');
7061
+ answer.append(brand(), node('p', '', task.result.summary));
7062
+ list.appendChild(answer);
7063
+ }
7064
+ }
7065
+ content.scrollTop = scrollTop;
7066
+ if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
7067
+ }
7068
+
6102
7069
  function setSidepanelOpen(open) {
6103
7070
  sidepanelOpen = open;
6104
7071
  sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
@@ -6134,12 +7101,52 @@ function initComty(options = {}) {
6134
7101
 
6135
7102
  function openCommentFromPanel(comment) {
6136
7103
  const target = resolveCommentElement(comment);
6137
- if (!target || isElementTreeHidden(target)) return;
7104
+ if (!target || isElementTreeHidden(target)) {
7105
+ openDetachedThread(comment);
7106
+ return;
7107
+ }
6138
7108
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
6139
7109
  target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
7110
+ const rect = target.getBoundingClientRect();
7111
+ const relX = Number(comment.rel_x ?? comment.relX);
7112
+ const relY = Number(comment.rel_y ?? comment.relY);
7113
+ // Large page containers must reveal the annotation point, not their center.
7114
+ window.scrollBy({
7115
+ left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
7116
+ top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
7117
+ behavior: "instant",
7118
+ });
6140
7119
  syncCommentAnchors();
6141
7120
  const pin = findPlacedPin(comment);
6142
7121
  if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
7122
+ else openDetachedThread(comment);
7123
+ }
7124
+
7125
+ function openDetachedThread(comment) {
7126
+ openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
7127
+ const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
7128
+ if (bubble) {
7129
+ bubble.dataset.detached = "";
7130
+ positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
7131
+ }
7132
+ }
7133
+
7134
+ function updateAnchorBadge(item, comment) {
7135
+ resolveCommentElement(comment);
7136
+ const result = commentTargets.get(String(comment.id));
7137
+ const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
7138
+ const status = legacy && !result?.element ? "uncertain" : result?.status;
7139
+ const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
7140
+ let badge = item.querySelector(".comty-anchor-badge");
7141
+ if (!label) { badge?.remove(); return; }
7142
+ if (!badge) {
7143
+ badge = document.createElement("span");
7144
+ badge.className = "comty-anchor-badge";
7145
+ item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
7146
+ }
7147
+ if (badge.textContent !== label) badge.textContent = label;
7148
+ badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
7149
+ 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
7150
  }
6144
7151
 
6145
7152
  let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
@@ -6152,6 +7159,7 @@ function initComty(options = {}) {
6152
7159
  ? sidepanelSearch.value.trim().toLowerCase()
6153
7160
  : "";
6154
7161
  let list = cachedComments.filter((comment) => {
7162
+ if (comment.local_agent || comment.kind === "agent") return false;
6155
7163
  if (
6156
7164
  search &&
6157
7165
  !`${comment.content ?? ""} ${getCommentLabel(comment)}`
@@ -6198,6 +7206,7 @@ function initComty(options = {}) {
6198
7206
  const item = document.createElement("button");
6199
7207
  item.type = "button";
6200
7208
  item.className = "comty-sidepanel-item";
7209
+ item.dataset.commentId = String(comment.id);
6201
7210
  item.disabled = Boolean(comment.pending);
6202
7211
  item.innerHTML = `
6203
7212
  <span class="comty-sidepanel-item-status" aria-hidden="true"></span>
@@ -6221,8 +7230,13 @@ function initComty(options = {}) {
6221
7230
  if (metaEl instanceof HTMLElement) {
6222
7231
  metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
6223
7232
  metaEl.title = getCommentLabel(comment);
7233
+ if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
7234
+ metaEl.innerText += " · Original page unknown";
7235
+ }
6224
7236
  }
6225
7237
 
7238
+ updateAnchorBadge(item, comment);
7239
+
6226
7240
  item.onclick = (event) => {
6227
7241
  event.stopPropagation();
6228
7242
  openCommentFromPanel(comment);
@@ -6292,7 +7306,7 @@ function initComty(options = {}) {
6292
7306
  if (!(pin instanceof HTMLElement)) return;
6293
7307
  const status = pin.dataset.status;
6294
7308
  const shouldShow =
6295
- !hasFilters || Boolean(status && selectedStatusFilters.has(status));
7309
+ pin.hasAttribute('data-agent-task') || !hasFilters || Boolean(status && selectedStatusFilters.has(status));
6296
7310
  pin.classList.toggle("comty-filtered-out", !shouldShow);
6297
7311
  });
6298
7312
  }
@@ -6338,14 +7352,15 @@ function initComty(options = {}) {
6338
7352
  createStatusFilters();
6339
7353
 
6340
7354
  async function updateCommentStatus(comment, status) {
6341
- if (!projectId || !comment?.id) return false;
7355
+ if (comment?.local_agent || !projectId || !comment?.id) return false;
6342
7356
 
6343
7357
  let res;
6344
7358
  try {
6345
- res = await fetch(`${apiUrl}/api/comments/${comment.id}/status`, {
7359
+ const local = comment.local_agent === true;
7360
+ res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
6346
7361
  method: "POST",
6347
- headers: getSdkHeaders({ "Content-Type": "application/json" }),
6348
- body: JSON.stringify({
7362
+ headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
7363
+ body: JSON.stringify(local ? { status } : {
6349
7364
  projectKey,
6350
7365
  host: window.location.host,
6351
7366
  status,
@@ -6362,7 +7377,7 @@ function initComty(options = {}) {
6362
7377
  }
6363
7378
 
6364
7379
  const data = await res.json();
6365
- comment.status = getCommentStatus(data.status).value;
7380
+ comment.status = getCommentStatus(data.task?.status ?? data.status).value;
6366
7381
  return comment.status;
6367
7382
  }
6368
7383
 
@@ -6554,6 +7569,27 @@ function initComty(options = {}) {
6554
7569
  isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
6555
7570
  updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
6556
7571
  showAuthPrompt,
7572
+ startThreadAgent: async (comment, replies, content) => {
7573
+ if (!agent) throw new Error('Start the local agent to reply with @Agent');
7574
+ if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
7575
+ const transcript = [
7576
+ `${getCommentAuthorName(comment)}: ${comment.content}`,
7577
+ ...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
7578
+ ].join('\n').slice(-24000);
7579
+ const response = await fetch(`${agentBridgeUrl}/tasks`, {
7580
+ method: 'POST', headers: agentHeaders(),
7581
+ body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
7582
+ selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
7583
+ domElement: comment.dom_element || '', viewport: comment.viewport || '',
7584
+ anchorContext: comment.anchor_context || '', scope: 'element',
7585
+ thread: { commentId: String(comment.id), transcript } }),
7586
+ });
7587
+ if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
7588
+ const data = await response.json();
7589
+ if (!data.task?.id) throw new Error('Agent returned no task ID');
7590
+ requestedAgentTasks.add(data.task.id);
7591
+ void loadComments({ force: true });
7592
+ },
6557
7593
  get canWrite() { return canWrite; },
6558
7594
  get canManage() { return canManage; },
6559
7595
  get allowGuestComments() { return allowGuestComments; },
@@ -6668,6 +7704,7 @@ function initComty(options = {}) {
6668
7704
 
6669
7705
  pin.onclick = (event) => {
6670
7706
  event.stopPropagation();
7707
+ if (comment.local_agent) { openAgentTask(comment, getPinPoint(pin).x, getPinPoint(pin).y); return; }
6671
7708
  openCommentThread(
6672
7709
  comment,
6673
7710
  pin,
@@ -6754,42 +7791,122 @@ function initComty(options = {}) {
6754
7791
  function reconcileComments(comments) {
6755
7792
  const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
6756
7793
  cachedComments = comments
6757
- .filter(comment => comment.kind !== "agent" && !comment.local_agent)
7794
+ .filter(comment => comment.kind !== "agent" || comment.status !== "done")
6758
7795
  .map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
6759
7796
  for (const comment of previous.values()) {
6760
7797
  if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
6761
7798
  }
6762
7799
  }
6763
7800
 
6764
- async function loadComments({ force = false } = {}) {
6765
- if (!projectId) return;
6766
- const pagePath = window.location.pathname;
7801
+ async function deliverThreadReply(task) {
7802
+ if (deliveringAgentReplies.has(task.id)) return;
7803
+ deliveringAgentReplies.add(task.id);
7804
+ try {
7805
+ const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
7806
+ method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
7807
+ body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
7808
+ });
7809
+ if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
7810
+ const { reply } = await response.json();
7811
+ const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
7812
+ if (local && !local.replies?.some(item => item.id === reply.id)) {
7813
+ local.replies ||= [];
7814
+ local.replies.push(reply);
7815
+ }
7816
+ for (const bubble of root.querySelectorAll('.comty-bubble')) {
7817
+ if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
7818
+ }
7819
+ const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
7820
+ if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
7821
+ localTasks = localTasks.filter(item => item.id !== task.id);
7822
+ } catch (error) {
7823
+ agentActionError = error.message;
7824
+ console.error('Comty: Agent reply delivery failed', error);
7825
+ } finally {
7826
+ deliveringAgentReplies.delete(task.id);
7827
+ agentPanelSignature = '';
7828
+ renderAgentPanel();
7829
+ }
7830
+ }
7831
+
7832
+ async function loadComments({ force = false, tasksOnly = false } = {}) {
7833
+ if (!projectId && !agent) return;
7834
+ const pagePath = getPagePath();
6767
7835
  if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
6768
7836
  if (pendingCommentsLoad) {
6769
7837
  await pendingCommentsLoad;
6770
- if (!force) return;
7838
+ if (!force && cachedPagePath === pagePath) return;
6771
7839
  }
6772
7840
  pendingCommentsLoad = (async () => {
6773
- let comments = cachedPagePath === pagePath ? cachedComments : [];
6774
- if (projectId) {
7841
+ let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
7842
+ if (projectId && !tasksOnly) {
6775
7843
  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;
7844
+ const paths = [...new Set([pagePath, window.location.pathname])];
7845
+ const batches = await Promise.all(paths.map(async path => {
7846
+ const url = new URL(`${apiUrl}/api/comments`);
7847
+ url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
7848
+ const res = await fetch(url);
7849
+ if (!res.ok) throw new Error(await res.text());
7850
+ const data = await res.json();
7851
+ return Array.isArray(data.comments) ? data.comments : [];
7852
+ }));
7853
+ comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
6782
7854
  } catch (error) { console.error("Comty: list comments failed", error); }
6783
7855
  }
7856
+ if (agent) {
7857
+ try {
7858
+ const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
7859
+ if (!res.ok) throw new Error("Local companion unavailable");
7860
+ const data = await res.json();
7861
+ if (Array.isArray(data.tasks)) localTasks = data.tasks;
7862
+ const failedTasks = localTasks.filter(task => task.status === 'in_review');
7863
+ for (const task of failedTasks.filter(task => requestedAgentTasks.has(task.id))) {
7864
+ if (task.error) showAgentBridgeError(task.error);
7865
+ else showNotice({ title: 'Modification failed', description: 'The agent needs clarification.' });
7866
+ }
7867
+ if (Array.isArray(data.approvals)) {
7868
+ localApprovals = data.approvals;
7869
+ if (localApprovals.some(approval => requestedAgentTasks.has(approval.taskId) && !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
7870
+ seenAgentApprovals.clear();
7871
+ for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
7872
+ }
7873
+ for (const id of requestedAgentTasks) {
7874
+ const task = localTasks.find(task => task.id === id);
7875
+ if (!task || ['done', 'cancel', 'in_review'].includes(task.status)) requestedAgentTasks.delete(id);
7876
+ }
7877
+ agentConnectionError = "";
7878
+ } catch (error) {
7879
+ agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
7880
+ console.error("Comty: local tasks failed", error);
7881
+ }
7882
+ for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
7883
+ void deliverThreadReply(task);
7884
+ }
7885
+ comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && task.status === 'in_review'));
7886
+ renderAgentPanel();
7887
+ }
7888
+ if (pagePath !== getPagePath()) return;
6784
7889
  reconcileComments(comments);
6785
7890
  cachedPagePath = pagePath;
6786
7891
  renderSidepanelComments();
6787
7892
  await renderComments(cachedComments);
7893
+ syncAgentWorkingOutlines();
6788
7894
  ensureLiveTracking();
6789
7895
  })();
6790
7896
  try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
6791
7897
  }
6792
7898
 
7899
+ if (agent) {
7900
+ setInterval(() => {
7901
+ const active = requestedAgentTasks.size || localTasks.some(task =>
7902
+ ['todo', 'in_progress'].includes(task.status) || (task.thread_comment_id && task.status === 'done' && task.result?.summary));
7903
+ if (active && !pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true, tasksOnly: true });
7904
+ }, 2000);
7905
+ document.addEventListener('visibilitychange', () => {
7906
+ if (document.visibilityState === 'visible' && !pendingCommentsLoad) void loadComments({ force: true, tasksOnly: true });
7907
+ });
7908
+ }
7909
+
6793
7910
  function getPinPoint(pin) {
6794
7911
  return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
6795
7912
  }
@@ -6817,7 +7934,7 @@ function initComty(options = {}) {
6817
7934
  }
6818
7935
 
6819
7936
  function syncCommentAnchors() {
6820
- if (!session.comments) return;
7937
+ if (!session.comments && !session.inspect) return;
6821
7938
  const modal = getTopActiveModal();
6822
7939
  for (const comment of cachedComments) {
6823
7940
  const id = String(comment.id);
@@ -6828,7 +7945,7 @@ function initComty(options = {}) {
6828
7945
  placePin(comment, modal);
6829
7946
  pin = placedPins.get(id);
6830
7947
  }
6831
- const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
7948
+ const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
6832
7949
  // A pin can remove its target's :hover style as the pointer enters it.
6833
7950
  if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
6834
7951
  (activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
@@ -6843,6 +7960,10 @@ function initComty(options = {}) {
6843
7960
  if (pin.style.top !== y) pin.style.top = y;
6844
7961
  for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
6845
7962
  }
7963
+ if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
7964
+ const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
7965
+ if (comment) updateAnchorBadge(item, comment);
7966
+ }
6846
7967
  if (draftAnchor) {
6847
7968
  if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
6848
7969
  const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
@@ -6856,16 +7977,50 @@ function initComty(options = {}) {
6856
7977
  }
6857
7978
  }
6858
7979
 
7980
+ function syncAgentWorkingOutlines() {
7981
+ const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
7982
+ const ids = new Set(working.map(task => String(task.id)));
7983
+ for (const [id, outline] of workingOutlines) {
7984
+ if (ids.has(id)) continue;
7985
+ outline.remove(); workingOutlines.delete(id);
7986
+ }
7987
+ const modal = getTopActiveModal();
7988
+ for (const task of working) {
7989
+ const id = String(task.id);
7990
+ let outline = workingOutlines.get(id);
7991
+ if (!outline) {
7992
+ outline = document.createElement("div");
7993
+ outline.className = "comty-agent-working";
7994
+ outline.setAttribute("aria-label", "Agent working on this element");
7995
+ const badge = document.createElement("span");
7996
+ badge.className = "comty-agent-working-badge";
7997
+ badge.append(createWorkingIndicator(), "Working");
7998
+ outline.appendChild(badge); root.appendChild(outline);
7999
+ workingOutlines.set(id, outline);
8000
+ }
8001
+ const target = resolveCommentElement(task);
8002
+ const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
8003
+ outline.hidden = !point;
8004
+ if (!point) continue;
8005
+ const rect = target.getBoundingClientRect();
8006
+ setMeasuredRect(outline, rect);
8007
+ outline.toggleAttribute("data-badge-below", rect.top < 32);
8008
+ }
8009
+ }
8010
+
6859
8011
  function onFrame() {
6860
8012
  if (motion.enabled) updateMotion();
6861
- if (session.comments) syncCommentAnchors();
8013
+ if (session.comments || session.inspect) syncCommentAnchors();
8014
+ if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
8015
+ syncAgentWorkingOutlines();
8016
+ }
6862
8017
  if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
6863
8018
  if (session.inspect && rulersEnabled) renderRulers();
6864
8019
  if (session.inspect && rulersEnabled) measurements.render();
6865
8020
  }
6866
8021
 
6867
8022
  function ensureLiveTracking() {
6868
- const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
8023
+ const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
6869
8024
  if (!active || document.hidden) {
6870
8025
  if (liveFrame) cancelAnimationFrame(liveFrame);
6871
8026
  liveFrame = null;
@@ -6885,13 +8040,14 @@ function initComty(options = {}) {
6885
8040
  syncOverlayHost();
6886
8041
 
6887
8042
  function renderCachedComments() {
6888
- if (!session.comments && !session.inspect && !session.edit) return;
8043
+ if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
6889
8044
  if (resizeRenderFrame) {
6890
8045
  window.cancelAnimationFrame(resizeRenderFrame);
6891
8046
  }
6892
8047
  resizeRenderFrame = window.requestAnimationFrame(() => {
6893
8048
  resizeRenderFrame = null;
6894
8049
  syncCommentAnchors();
8050
+ syncAgentWorkingOutlines();
6895
8051
  renderInspector();
6896
8052
  ensureLiveTracking();
6897
8053
  });
@@ -6933,6 +8089,31 @@ function initComty(options = {}) {
6933
8089
  characterData: true,
6934
8090
  });
6935
8091
 
8092
+ let activePagePath = getPagePath();
8093
+ function handleRouteChange() {
8094
+ const next = getPagePath();
8095
+ if (next === activePagePath) return;
8096
+ activePagePath = next;
8097
+ markup.clear();
8098
+ markup.setMode(null);
8099
+ closeBubbles();
8100
+ commentTargets.clear();
8101
+ reconcileComments([]);
8102
+ void renderComments([], { retry: false });
8103
+ renderSidepanelComments();
8104
+ void loadComments({ force: true });
8105
+ }
8106
+ window.addEventListener("hashchange", handleRouteChange);
8107
+ window.addEventListener("popstate", handleRouteChange);
8108
+ for (const method of ["pushState", "replaceState"]) {
8109
+ const original = history[method];
8110
+ history[method] = function (...args) {
8111
+ const result = original.apply(this, args);
8112
+ handleRouteChange();
8113
+ return result;
8114
+ };
8115
+ }
8116
+
6936
8117
  document.addEventListener("scroll", handleHostLayoutEvent, {
6937
8118
  capture: true,
6938
8119
  passive: true,
@@ -6951,7 +8132,7 @@ function initComty(options = {}) {
6951
8132
 
6952
8133
  document.addEventListener("pointermove", (event) => {
6953
8134
  if (!session.edit && !session.inspect) return;
6954
- if (motion.enabled) return;
8135
+ if (motion.enabled || markup.mode) return;
6955
8136
  if (propertyPicking) {
6956
8137
  const target = event.target;
6957
8138
  if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
@@ -7070,13 +8251,14 @@ function initComty(options = {}) {
7070
8251
  const y = e.clientY;
7071
8252
 
7072
8253
  const anchorContext = getAnchorContext(el);
8254
+ if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
7073
8255
 
7074
8256
  const payload = {
7075
8257
  ...getDeviceContext(),
7076
8258
  projectId,
7077
- kind: "comment",
8259
+ kind: session.agent ? "agent" : "comment",
7078
8260
  host: window.location.host,
7079
- pagePath: window.location.pathname,
8261
+ pagePath: getPagePath(),
7080
8262
  selector: getSelector(el),
7081
8263
  relX,
7082
8264
  relY,
@@ -7097,6 +8279,12 @@ function initComty(options = {}) {
7097
8279
  (target.isContentEditable ||
7098
8280
  ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
7099
8281
 
8282
+ const markupMode = markup.mode;
8283
+ if (session.inspect && (e.key === "Escape" || !isEditable) && markup.keydown(e)) {
8284
+ e.preventDefault();
8285
+ if (!markup.mode) (markupMode === "annotate" ? annotateBtn : screenshotBtn).focus({ preventScroll: true });
8286
+ return;
8287
+ }
7100
8288
  if (e.key === "Escape" && motion.enabled) {
7101
8289
  e.preventDefault();
7102
8290
  setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
@@ -7111,7 +8299,7 @@ function initComty(options = {}) {
7111
8299
  return;
7112
8300
  }
7113
8301
  if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
7114
- const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn }[e.key.toLowerCase()];
8302
+ const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn, d: annotateBtn, s: screenshotBtn }[e.key.toLowerCase()];
7115
8303
  if (button) { e.preventDefault(); button.click(); return; }
7116
8304
  }
7117
8305
 
@@ -7119,6 +8307,11 @@ function initComty(options = {}) {
7119
8307
  measureAlt = true;
7120
8308
  renderInspector();
7121
8309
  }
8310
+ if (isLocalSite && !isEditable && !e.repeat && !e.isComposing && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "a") {
8311
+ e.preventDefault();
8312
+ startAgentSession();
8313
+ return;
8314
+ }
7122
8315
  if (!isEditable && !e.repeat && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "i") {
7123
8316
  e.preventDefault();
7124
8317
  setSession(session.inspect ? "off" : "inspect");
@@ -7174,6 +8367,7 @@ function initComty(options = {}) {
7174
8367
  window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
7175
8368
 
7176
8369
  async function boot() {
8370
+ if (localOnly) { await loadComments(); return; }
7177
8371
  const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
7178
8372
  method: "POST",
7179
8373
  headers: getSdkHeaders({ "Content-Type": "application/json" }),
@@ -7186,6 +8380,7 @@ function initComty(options = {}) {
7186
8380
 
7187
8381
  if (!res.ok) {
7188
8382
  console.error("Comty: bootstrap failed", await res.text());
8383
+ if (agent) await loadComments();
7189
8384
  return;
7190
8385
  }
7191
8386
 
@@ -7230,16 +8425,30 @@ function initComty(options = {}) {
7230
8425
 
7231
8426
  if (typeof options.onTestReady === "function") {
7232
8427
  options.onTestReady({
7233
- getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
8428
+ getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
7234
8429
  getDistanceSegments, getInspectRows, syncCommentAnchors,
7235
8430
  setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
7236
8431
  startEditSession, startBrowseSession, endSession, showInspector,
7237
- motion, setMotionEnabled, updateMotion,
8432
+ motion, setMotionEnabled, updateMotion, describeAgentError,
8433
+ previewAgent(tasks, approvals = [], error = "") {
8434
+ localTasks = tasks;
8435
+ localApprovals = approvals;
8436
+ agentConnectionError = error;
8437
+ startAgentSession();
8438
+ agentPanelOpen = true;
8439
+ agentPanelSignature = "";
8440
+ renderAgentPanel();
8441
+ },
7238
8442
  seed(comments) {
7239
8443
  reconcileComments(comments);
7240
8444
  setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
7241
8445
  return renderComments(cachedComments, { retry: false });
7242
8446
  },
8447
+ seedAgentTasks(tasks) {
8448
+ localTasks = tasks;
8449
+ syncAgentWorkingOutlines();
8450
+ ensureLiveTracking();
8451
+ },
7243
8452
  setCanWrite(value) { canWrite = value; canManage = value; },
7244
8453
  invalidate() { anchorRevision += 1; },
7245
8454
  });
@@ -7247,6 +8456,7 @@ function initComty(options = {}) {
7247
8456
 
7248
8457
  return boot().catch(error => {
7249
8458
  console.error("Comty: bootstrap failed", error);
8459
+ if (agent) return loadComments();
7250
8460
  });
7251
8461
  }
7252
8462
  }