@comty.io/core 0.1.6 → 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
@@ -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);
@@ -107,27 +111,34 @@ function getComtyStyle() {
107
111
  .comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
108
112
  .comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
109
113
  .comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
110
- .comty-agent-row-actions { position: absolute; top: 50%; right: .25rem; display: flex; gap: .25rem; border-radius: .25rem; background: var(--comty-hover-surface); opacity: 0; pointer-events: none; transform: translateY(-50%); }
111
- .comty-agent-row:hover .comty-agent-row-actions, .comty-agent-row-actions:has(.comty-agent-action:focus-visible) { opacity: 1; pointer-events: auto; }
112
- @media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
114
+ .comty-agent-row-actions { display: flex; flex-shrink: 0; gap: .25rem; }
113
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; }
114
116
  .comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
115
- .comty-agent-error-popover { position: fixed; z-index: 2147483647; width: min(20rem, calc(100vw - 1rem)); max-height: calc(100vh - 1rem); overflow: auto; padding: .75rem .875rem; border: 0; border-radius: .625rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; pointer-events: none; color-scheme: dark; }
116
- .comty-agent-error-popover strong { display: block; font-weight: 600; }
117
- .comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
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); } }
118
127
  .comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
119
128
  .comty-agent-error { margin-top: .75rem; }
120
129
  .comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
121
130
  .comty-agent-error p { margin-top: .25rem; }
122
131
  .comty-agent-banner { margin-bottom: 1rem; }
123
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); }
124
134
  .comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
125
- .comty-agent-panel .comty-agent-action { display: inline-flex; align-items: center; justify-content: center; gap: .375rem; min-height: 2rem; margin: 0; padding: .375rem .625rem; border: 0; border-radius: .5rem; font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; cursor: pointer; color: var(--comty-foreground); background: var(--comty-hover-surface); transition: background-color 150ms ease; }
126
- .comty-agent-panel .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
127
- .comty-agent-panel .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
128
- .comty-agent-panel .comty-agent-action:disabled { opacity: .5; cursor: wait; }
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; }
129
140
  .comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
130
- .comty-agent-panel :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
141
+ :is(.comty-agent-panel, .comty-agent-task-thread) :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
131
142
  .comty-agent-details { margin-top: .625rem; }
132
143
  .comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
133
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; }
@@ -206,7 +217,7 @@ function getComtyStyle() {
206
217
  #comty-root [hidden] { display: none !important; }
207
218
  [data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
208
219
  @media (prefers-reduced-motion: reduce) {
209
- .comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i { transition: none !important; animation: none !important; }
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; }
210
221
  }
211
222
 
212
223
  .comty-sidepanel {
@@ -929,6 +940,8 @@ function getComtyStyle() {
929
940
  }
930
941
 
931
942
  .comty-hover-bubble .comty-thread {
943
+ display: grid;
944
+ gap: 1rem;
932
945
  padding: 12px;
933
946
  }
934
947
 
@@ -1184,6 +1197,10 @@ function getComtyStyle() {
1184
1197
  }
1185
1198
 
1186
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; }
1187
1204
  .comty-overlay-tools, .comty-layout-guides, .comty-layout-guide { position: fixed; inset: 0; pointer-events: none; }
1188
1205
  .comty-overlay-tools { overflow: hidden; }
1189
1206
  .comty-layout-guide { display: grid; box-sizing: border-box; }
@@ -1209,6 +1226,38 @@ function getComtyStyle() {
1209
1226
  .comty-guide-fields label { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
1210
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; }
1211
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
+
1212
1261
  .comty-ruler { position: fixed; background: var(--comty-surface); pointer-events: auto; touch-action: none; }
1213
1262
  .comty-ruler-horizontal { left: 0; top: 0; width: 100vw; height: 1.125rem; border-bottom: .0625rem solid var(--comty-border); cursor: ns-resize; }
1214
1263
  .comty-ruler-vertical { left: 0; top: 0; height: 100vh; width: 1.125rem; border-right: .0625rem solid var(--comty-border); cursor: ew-resize; }
@@ -1283,7 +1332,7 @@ function getComtyStyle() {
1283
1332
  .comty-motion-panel button[aria-pressed="true"] { opacity: 1; color: var(--comty-paper); background: var(--comty-hover-surface); }
1284
1333
  .comty-motion-panel button[aria-pressed="false"] { opacity: 1; }
1285
1334
  .comty-motion-panel [data-motion-status] { margin: .5rem; font-size: .75rem; color: var(--comty-muted); }
1286
- @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; } }
1287
1336
 
1288
1337
  .comty-inspection-box {
1289
1338
  position: fixed;
@@ -2141,8 +2190,10 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2141
2190
  finally { clearTimeout(timer); }
2142
2191
  }
2143
2192
 
2144
- async function captureViewportScreenshot({ context = captureContext(), annotation } = {}) {
2193
+ async function captureViewportScreenshot({ context = captureContext(), annotation, region } = {}) {
2145
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");
2146
2197
  const controller = new AbortController();
2147
2198
  const timeout = setTimeout(() => controller.abort(), 15000);
2148
2199
  const { signal } = controller;
@@ -2156,7 +2207,7 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2156
2207
  let lastYield = performance.now();
2157
2208
  const checkContext = () => {
2158
2209
  signal.throwIfAborted();
2159
- 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)) {
2160
2211
  throw new Error("Page moved before its screenshot was ready");
2161
2212
  }
2162
2213
  };
@@ -2355,21 +2406,21 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
2355
2406
  image.src = svg;
2356
2407
  await untilAborted(image.decode());
2357
2408
  checkContext();
2358
- 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);
2359
2410
  const canvas = document.createElement("canvas");
2360
- canvas.width = Math.round(context.width * scale);
2361
- 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));
2362
2413
  const ctx = canvas.getContext("2d");
2363
2414
  ctx.scale(scale, scale);
2364
- ctx.drawImage(image, 0, 0);
2415
+ ctx.drawImage(image, -area.left, -area.top);
2365
2416
  let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
2366
2417
  if (!blob) throw new Error("Screenshot encoding failed");
2367
- if (blob.size > 2800000) {
2418
+ if (!region && blob.size > 2800000) {
2368
2419
  blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
2369
2420
  if (!blob) throw new Error("Screenshot encoding failed");
2370
2421
  }
2371
2422
  // CSS dimensions keep the eyedropper's coordinate system independent of DPR.
2372
- 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 };
2373
2424
  } finally {
2374
2425
  clearTimeout(timeout);
2375
2426
  controller.abort();
@@ -3023,6 +3074,290 @@ function createCommentThread(context) {
3023
3074
  }
3024
3075
  }
3025
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
+
3026
3361
  const edgeAxis = { top: "y", bottom: "y", left: "x", right: "x" };
3027
3362
  const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
3028
3363
 
@@ -3055,7 +3390,7 @@ function measurementPoint(anchor, rect, scroll = { x: 0, y: 0 }) {
3055
3390
  }
3056
3391
 
3057
3392
  /** Border measurements live for one activation of the existing ruler tool. */
3058
- function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit }) {
3393
+ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit, isInteractionBlocked = () => false }) {
3059
3394
  let enabled = false;
3060
3395
  let hover = null;
3061
3396
  let drag = null;
@@ -3179,7 +3514,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
3179
3514
  }
3180
3515
  }
3181
3516
  function isHostEvent(event) {
3182
- if (!enabled || !(event.target instanceof Element)) return false;
3517
+ if (!enabled || isInteractionBlocked() || !(event.target instanceof Element)) return false;
3183
3518
  if (!isComtyElement(event.target)) return true;
3184
3519
  // The outer ruler strip must still let us anchor to the page's top/left edge.
3185
3520
  return event.target.matches(".comty-ruler") && (event.clientX + scrollX <= 10 || event.clientY + scrollY <= 10);
@@ -3217,7 +3552,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
3217
3552
  if (isHostEvent(event)) { event.preventDefault(); event.stopImmediatePropagation(); }
3218
3553
  }, true);
3219
3554
  document.addEventListener("keydown", event => {
3220
- 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;
3221
3556
  event.preventDefault(); event.stopImmediatePropagation();
3222
3557
  if (drag) cancel(); else onExit();
3223
3558
  }, true);
@@ -3286,12 +3621,14 @@ function initComty(options = {}) {
3286
3621
  const deliveringAgentReplies = new Set();
3287
3622
  let localApprovals = [];
3288
3623
  const seenAgentApprovals = new Set();
3289
- const seenFailedAgentTasks = new Set();
3624
+ // Only requests made in this document may interrupt it with agent notifications.
3625
+ const requestedAgentTasks = new Set();
3290
3626
  let agentPanelOpen = false;
3291
3627
  let agentPanelSignature = "";
3292
3628
  let agentPanelTransition = 0;
3293
3629
  let agentConnectionError = "";
3294
3630
  let agentActionError = "";
3631
+ let clearAgentToast = () => {};
3295
3632
  const agentPendingActions = new Set();
3296
3633
  const session = {
3297
3634
  mode: "off",
@@ -3424,6 +3761,9 @@ function initComty(options = {}) {
3424
3761
  const agentBtn = isLocalSite
3425
3762
  ? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
3426
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; }
3427
3767
  if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
3428
3768
  agentPanelOpen = true;
3429
3769
  renderAgentPanel();
@@ -3431,6 +3771,10 @@ function initComty(options = {}) {
3431
3771
  } else startAgentSession();
3432
3772
  })
3433
3773
  : null;
3774
+ if (agentBtn) {
3775
+ agentBtn.title = "Agent · Shift A · localhost only";
3776
+ agentBtn.setAttribute("aria-keyshortcuts", "Shift+A");
3777
+ }
3434
3778
  const divider = document.createElement("span");
3435
3779
  divider.className = "comty-tool-divider";
3436
3780
  toolbar.appendChild(divider);
@@ -3470,6 +3814,7 @@ function initComty(options = {}) {
3470
3814
  root,
3471
3815
  isComtyElement,
3472
3816
  isElementTreeHidden,
3817
+ isInteractionBlocked: () => Boolean(markup.mode),
3473
3818
  onExit() { rulersEnabled = false; syncInspectorTools(); },
3474
3819
  });
3475
3820
  const gridLayer = document.createElement("div");
@@ -3496,6 +3841,7 @@ function initComty(options = {}) {
3496
3841
  syncInspectorTools();
3497
3842
  });
3498
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);
3499
3845
  closePropertyPanel();
3500
3846
  if (motion.enabled) setMotionEnabled(false);
3501
3847
  rulersEnabled = !rulersEnabled;
@@ -3506,6 +3852,7 @@ function initComty(options = {}) {
3506
3852
  syncInspectorTools();
3507
3853
  });
3508
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);
3509
3856
  closePropertyPanel();
3510
3857
  if (motion.enabled) setMotionEnabled(false);
3511
3858
  gridPanelOpen = !gridPanelOpen;
@@ -3523,7 +3870,37 @@ function initComty(options = {}) {
3523
3870
  }
3524
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));
3525
3872
  motionBtn.setAttribute("aria-expanded", "false");
3526
- 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);
3527
3904
 
3528
3905
  const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
3529
3906
  const motionPanel = document.createElement('section');
@@ -3572,6 +3949,7 @@ function initComty(options = {}) {
3572
3949
  motion.enabled = enabled; motionPanel.hidden = !enabled;
3573
3950
  motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
3574
3951
  if (enabled) {
3952
+ markup.setMode(null);
3575
3953
  closePropertyPanel();
3576
3954
  rulersEnabled = false;
3577
3955
  syncInspectorTools();
@@ -3830,6 +4208,7 @@ function initComty(options = {}) {
3830
4208
  const visible = session.inspect;
3831
4209
  measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
3832
4210
  overlayTools.hidden = !visible;
4211
+ markup.setVisible(visible);
3833
4212
  xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
3834
4213
  rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
3835
4214
  gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
@@ -4113,6 +4492,7 @@ function initComty(options = {}) {
4113
4492
  }
4114
4493
 
4115
4494
  function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
4495
+ markup.setMode(null);
4116
4496
  propertyPicking = null;
4117
4497
  colorMagnifier.setEnabled(false);
4118
4498
  delete document.documentElement.dataset.comtyPropertyPicking;
@@ -4370,7 +4750,7 @@ function initComty(options = {}) {
4370
4750
  });
4371
4751
  }
4372
4752
  const rect = toolbar.getBoundingClientRect();
4373
- for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
4753
+ for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel]) {
4374
4754
  if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
4375
4755
  const above = rect.top - 16;
4376
4756
  const below = innerHeight - rect.bottom - 16;
@@ -4387,9 +4767,10 @@ function initComty(options = {}) {
4387
4767
  panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
4388
4768
  panel.style.bottom = "auto";
4389
4769
  }
4770
+ positionNotice();
4390
4771
  }
4391
4772
  const toolbarResize = new ResizeObserver(positionToolbar);
4392
- [toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
4773
+ [toolbar, toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel].forEach(node => toolbarResize.observe(node));
4393
4774
  window.addEventListener("resize", positionToolbar);
4394
4775
 
4395
4776
  toolbar.addEventListener("pointerdown", event => {
@@ -4724,7 +5105,7 @@ function initComty(options = {}) {
4724
5105
 
4725
5106
  function showAuthPrompt(forbidden = false, onReady = startEditSession) {
4726
5107
  if (localOnly) {
4727
- showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
5108
+ showNotice({ title: "Comments are not configured", description: "Connect this site to a Comty project to leave shared comments." });
4728
5109
  return;
4729
5110
  }
4730
5111
  const guestAllowed = allowGuestComments;
@@ -4858,6 +5239,7 @@ function initComty(options = {}) {
4858
5239
  inspectorTools.hidden = !session.inspect;
4859
5240
  syncInspectorTools();
4860
5241
  renderAgentPanel();
5242
+ positionNotice();
4861
5243
  }
4862
5244
 
4863
5245
  const inspectionLayer = document.createElement("div");
@@ -5170,7 +5552,7 @@ function initComty(options = {}) {
5170
5552
  });
5171
5553
  return;
5172
5554
  }
5173
- if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
5555
+ if (markup.mode || propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
5174
5556
  if (inspectionLayer.childNodes.length) hideInspector();
5175
5557
  return;
5176
5558
  }
@@ -5314,10 +5696,12 @@ function initComty(options = {}) {
5314
5696
 
5315
5697
  function startBrowseSession() {
5316
5698
  setSession("browse", { renderComments: true });
5699
+ void loadComments({ force: true });
5317
5700
  }
5318
5701
 
5319
5702
  function startEditSession() {
5320
5703
  setSession("edit", { renderComments: true });
5704
+ void loadComments({ force: true });
5321
5705
  }
5322
5706
 
5323
5707
  function startAgentSession() {
@@ -5325,6 +5709,7 @@ function initComty(options = {}) {
5325
5709
  if (session.agent) return;
5326
5710
  closeBubbles();
5327
5711
  setSession("agent", { renderComments: true });
5712
+ void loadComments({ force: true });
5328
5713
  }
5329
5714
 
5330
5715
  function toggleSidepanel() {
@@ -6062,6 +6447,7 @@ function initComty(options = {}) {
6062
6447
  function createCommentPin(comment, x, y) {
6063
6448
  const pin = document.createElement("button");
6064
6449
  pin.className = "comty-pin";
6450
+ pin.toggleAttribute('data-agent-task', Boolean(comment.local_agent));
6065
6451
  pin.type = "button";
6066
6452
  pin.style.left = `${x}px`;
6067
6453
  pin.style.top = `${y}px`;
@@ -6079,10 +6465,52 @@ function initComty(options = {}) {
6079
6465
  const thread = document.createElement("div");
6080
6466
  thread.className = "comty-thread";
6081
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
+ }
6082
6471
  bubble.appendChild(thread);
6083
6472
  return bubble;
6084
6473
  }
6085
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
+
6086
6514
  function mountCaptureAnnotation(comment, { x, y }) {
6087
6515
  const host = document.createElement("div");
6088
6516
  host.dataset.comtyCaptureExclude = "";
@@ -6266,6 +6694,7 @@ function initComty(options = {}) {
6266
6694
  const id = isAgentTask ? data?.task?.id : data?.id;
6267
6695
  if (!id) throw new Error("Save returned no ID");
6268
6696
  if (isAgentTask) {
6697
+ requestedAgentTasks.add(id);
6269
6698
  void loadComments({ force: true });
6270
6699
  } else {
6271
6700
  cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
@@ -6397,14 +6826,53 @@ function initComty(options = {}) {
6397
6826
  function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
6398
6827
 
6399
6828
  function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
6400
- root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
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();
6401
6848
  const toast = document.createElement("div");
6402
- toast.className = "comty-auth-toast comty-agent-task-toast";
6849
+ toast.className = "comty-agent-task-toast";
6403
6850
  toast.setAttribute("role", "alert");
6404
- const issue = describeAgentError(message);
6405
- toast.textContent = issue.title + ". " + issue.description;
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;
6406
6873
  root.appendChild(toast);
6407
- setTimeout(() => toast.remove(), 5000);
6874
+ positionNotice();
6875
+ schedule();
6408
6876
  }
6409
6877
 
6410
6878
  function describeAgentError(error) {
@@ -6433,7 +6901,7 @@ function initComty(options = {}) {
6433
6901
  } else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
6434
6902
  title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
6435
6903
  } else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
6436
- title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
6904
+ title = 'Model unavailable'; description = 'The local agent rejected this model. Check its CLI version and signed-in account, then retry.';
6437
6905
  } else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
6438
6906
  title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
6439
6907
  } else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
@@ -6446,11 +6914,21 @@ function initComty(options = {}) {
6446
6914
  title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
6447
6915
  } else {
6448
6916
  title = 'Task could not finish';
6449
- description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 300) : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
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.';
6450
6918
  }
6451
6919
  return { title, description, raw, message };
6452
6920
  }
6453
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
+
6454
6932
  function renderAgentPanel() {
6455
6933
  if (!agent) return;
6456
6934
  const visibleTasks = [];
@@ -6469,6 +6947,7 @@ function initComty(options = {}) {
6469
6947
  }
6470
6948
  const open = Boolean(agent) && agentPanelOpen;
6471
6949
  if (open) {
6950
+ clearAgentToast();
6472
6951
  if (panel.hidden) {
6473
6952
  const rect = toolbar.getBoundingClientRect();
6474
6953
  panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
@@ -6489,7 +6968,6 @@ function initComty(options = {}) {
6489
6968
  const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
6490
6969
  if (signature === agentPanelSignature) return;
6491
6970
  agentPanelSignature = signature;
6492
- root.querySelector('.comty-agent-error-popover')?.remove();
6493
6971
  const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
6494
6972
  const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
6495
6973
  const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
@@ -6535,12 +7013,7 @@ function initComty(options = {}) {
6535
7013
  agentActionError = '';
6536
7014
  agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
6537
7015
  try {
6538
- const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
6539
- if (!response.ok) {
6540
- const body = await response.json().catch(() => null);
6541
- throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
6542
- }
6543
- await loadComments({ force: true });
7016
+ await runAgentAction(path, input);
6544
7017
  } catch (error) { agentActionError = error.message; }
6545
7018
  finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
6546
7019
  };
@@ -6561,9 +7034,7 @@ function initComty(options = {}) {
6561
7034
  section.appendChild(actions); content.appendChild(section);
6562
7035
  }
6563
7036
  const list = node('div', 'comty-agent-list'); content.appendChild(list);
6564
- const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
6565
- content.addEventListener('scroll', hideErrorPopover, { passive: true });
6566
- for (const [index, task] of visibleTasks.reverse().entries()) {
7037
+ for (const task of visibleTasks.reverse()) {
6567
7038
  const failed = Boolean(task.error) || task.status === 'in_review';
6568
7039
  const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
6569
7040
  const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
@@ -6579,24 +7050,12 @@ function initComty(options = {}) {
6579
7050
  if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
6580
7051
  if (actions.childElementCount) row.appendChild(actions);
6581
7052
  row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
6582
- if (failed) {
6583
- const tooltipId = `comty-agent-error-${index}`;
6584
- const showErrorPopover = () => {
6585
- hideErrorPopover();
6586
- const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
6587
- const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
6588
- popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
6589
- root.appendChild(popover);
6590
- const rowRect = row.getBoundingClientRect();
6591
- const popoverRect = popover.getBoundingClientRect();
6592
- const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
6593
- popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
6594
- popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
6595
- };
6596
- row.addEventListener('mouseenter', showErrorPopover);
6597
- row.addEventListener('mouseleave', hideErrorPopover);
6598
- }
6599
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
+ }
6600
7059
  if (task.result?.summary) {
6601
7060
  const answer = node('div', 'comty-agent-answer');
6602
7061
  answer.append(brand(), node('p', '', task.result.summary));
@@ -6847,7 +7306,7 @@ function initComty(options = {}) {
6847
7306
  if (!(pin instanceof HTMLElement)) return;
6848
7307
  const status = pin.dataset.status;
6849
7308
  const shouldShow =
6850
- !hasFilters || Boolean(status && selectedStatusFilters.has(status));
7309
+ pin.hasAttribute('data-agent-task') || !hasFilters || Boolean(status && selectedStatusFilters.has(status));
6851
7310
  pin.classList.toggle("comty-filtered-out", !shouldShow);
6852
7311
  });
6853
7312
  }
@@ -7126,6 +7585,10 @@ function initComty(options = {}) {
7126
7585
  thread: { commentId: String(comment.id), transcript } }),
7127
7586
  });
7128
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 });
7129
7592
  },
7130
7593
  get canWrite() { return canWrite; },
7131
7594
  get canManage() { return canManage; },
@@ -7241,6 +7704,7 @@ function initComty(options = {}) {
7241
7704
 
7242
7705
  pin.onclick = (event) => {
7243
7706
  event.stopPropagation();
7707
+ if (comment.local_agent) { openAgentTask(comment, getPinPoint(pin).x, getPinPoint(pin).y); return; }
7244
7708
  openCommentThread(
7245
7709
  comment,
7246
7710
  pin,
@@ -7365,7 +7829,7 @@ function initComty(options = {}) {
7365
7829
  }
7366
7830
  }
7367
7831
 
7368
- async function loadComments({ force = false } = {}) {
7832
+ async function loadComments({ force = false, tasksOnly = false } = {}) {
7369
7833
  if (!projectId && !agent) return;
7370
7834
  const pagePath = getPagePath();
7371
7835
  if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
@@ -7375,7 +7839,7 @@ function initComty(options = {}) {
7375
7839
  }
7376
7840
  pendingCommentsLoad = (async () => {
7377
7841
  let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
7378
- if (projectId) {
7842
+ if (projectId && !tasksOnly) {
7379
7843
  try {
7380
7844
  const paths = [...new Set([pagePath, window.location.pathname])];
7381
7845
  const batches = await Promise.all(paths.map(async path => {
@@ -7396,15 +7860,20 @@ function initComty(options = {}) {
7396
7860
  const data = await res.json();
7397
7861
  if (Array.isArray(data.tasks)) localTasks = data.tasks;
7398
7862
  const failedTasks = localTasks.filter(task => task.status === 'in_review');
7399
- if (failedTasks.some(task => !seenFailedAgentTasks.has(task.id))) agentPanelOpen = true;
7400
- seenFailedAgentTasks.clear();
7401
- for (const task of failedTasks) seenFailedAgentTasks.add(task.id);
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
+ }
7402
7867
  if (Array.isArray(data.approvals)) {
7403
7868
  localApprovals = data.approvals;
7404
- if (localApprovals.some(approval => !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
7869
+ if (localApprovals.some(approval => requestedAgentTasks.has(approval.taskId) && !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
7405
7870
  seenAgentApprovals.clear();
7406
7871
  for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
7407
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
+ }
7408
7877
  agentConnectionError = "";
7409
7878
  } catch (error) {
7410
7879
  agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
@@ -7413,7 +7882,7 @@ function initComty(options = {}) {
7413
7882
  for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
7414
7883
  void deliverThreadReply(task);
7415
7884
  }
7416
- comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && !["done", "cancel"].includes(task.status)));
7885
+ comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && task.status === 'in_review'));
7417
7886
  renderAgentPanel();
7418
7887
  }
7419
7888
  if (pagePath !== getPagePath()) return;
@@ -7429,8 +7898,13 @@ function initComty(options = {}) {
7429
7898
 
7430
7899
  if (agent) {
7431
7900
  setInterval(() => {
7432
- if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
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 });
7433
7904
  }, 2000);
7905
+ document.addEventListener('visibilitychange', () => {
7906
+ if (document.visibilityState === 'visible' && !pendingCommentsLoad) void loadComments({ force: true, tasksOnly: true });
7907
+ });
7434
7908
  }
7435
7909
 
7436
7910
  function getPinPoint(pin) {
@@ -7620,6 +8094,8 @@ function initComty(options = {}) {
7620
8094
  const next = getPagePath();
7621
8095
  if (next === activePagePath) return;
7622
8096
  activePagePath = next;
8097
+ markup.clear();
8098
+ markup.setMode(null);
7623
8099
  closeBubbles();
7624
8100
  commentTargets.clear();
7625
8101
  reconcileComments([]);
@@ -7656,7 +8132,7 @@ function initComty(options = {}) {
7656
8132
 
7657
8133
  document.addEventListener("pointermove", (event) => {
7658
8134
  if (!session.edit && !session.inspect) return;
7659
- if (motion.enabled) return;
8135
+ if (motion.enabled || markup.mode) return;
7660
8136
  if (propertyPicking) {
7661
8137
  const target = event.target;
7662
8138
  if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
@@ -7803,6 +8279,12 @@ function initComty(options = {}) {
7803
8279
  (target.isContentEditable ||
7804
8280
  ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
7805
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
+ }
7806
8288
  if (e.key === "Escape" && motion.enabled) {
7807
8289
  e.preventDefault();
7808
8290
  setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
@@ -7817,7 +8299,7 @@ function initComty(options = {}) {
7817
8299
  return;
7818
8300
  }
7819
8301
  if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
7820
- 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()];
7821
8303
  if (button) { e.preventDefault(); button.click(); return; }
7822
8304
  }
7823
8305
 
@@ -7825,6 +8307,11 @@ function initComty(options = {}) {
7825
8307
  measureAlt = true;
7826
8308
  renderInspector();
7827
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
+ }
7828
8315
  if (!isEditable && !e.repeat && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "i") {
7829
8316
  e.preventDefault();
7830
8317
  setSession(session.inspect ? "off" : "inspect");