@comty.io/core 0.1.6 → 0.1.8
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/README.md +18 -7
- package/comty-sdk.js +563 -76
- package/package.json +2 -2
- package/src/browser.d.ts +1 -1
- package/src/browser.js +205 -52
- package/src/capture.js +10 -8
- package/src/claude.mjs +2 -3
- package/src/codex.mjs +2 -4
- package/src/dev.mjs +3 -2
- package/src/inspect-markup.js +283 -0
- package/src/measurements.js +3 -3
- package/src/style.js +62 -13
- package/src/vite.mjs +3 -2
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 {
|
|
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-
|
|
116
|
-
.comty-agent-
|
|
117
|
-
.comty-agent-
|
|
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-
|
|
127
|
-
.comty-agent-panel .comty-agent-
|
|
128
|
-
.comty-agent-panel .comty-agent-action
|
|
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:
|
|
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 /
|
|
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(
|
|
2361
|
-
canvas.height = Math.round(
|
|
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,
|
|
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:
|
|
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);
|
|
@@ -3250,7 +3585,7 @@ function initComty(options = {}) {
|
|
|
3250
3585
|
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
3251
3586
|
}
|
|
3252
3587
|
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
3253
|
-
const apiUrl = options.apiUrl || "
|
|
3588
|
+
const apiUrl = options.apiUrl || "https://api.comty.io";
|
|
3254
3589
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
3255
3590
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
3256
3591
|
}
|
|
@@ -3286,12 +3621,14 @@ function initComty(options = {}) {
|
|
|
3286
3621
|
const deliveringAgentReplies = new Set();
|
|
3287
3622
|
let localApprovals = [];
|
|
3288
3623
|
const seenAgentApprovals = new Set();
|
|
3289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
6849
|
+
toast.className = "comty-agent-task-toast";
|
|
6403
6850
|
toast.setAttribute("role", "alert");
|
|
6404
|
-
const
|
|
6405
|
-
|
|
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
|
-
|
|
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 = '
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
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 &&
|
|
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
|
-
|
|
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");
|