@comty.io/core 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/comty-sdk.js +1350 -140
- package/package.json +17 -6
- package/src/acp.mjs +68 -0
- package/src/agent-process.mjs +89 -0
- package/src/anchors.js +58 -17
- package/src/browser.d.ts +3 -1
- package/src/browser.js +820 -88
- package/src/capture.js +10 -8
- package/src/claude-permission.mjs +37 -0
- package/src/claude.mjs +119 -0
- package/src/codex.mjs +64 -0
- package/src/comments.js +31 -11
- package/src/companion.mjs +201 -0
- package/src/cursor.mjs +49 -0
- package/src/dev.mjs +172 -0
- package/src/gemini.mjs +3 -0
- package/src/grok.mjs +3 -0
- package/src/inspect-markup.js +283 -0
- package/src/measurements.js +3 -3
- package/src/style.js +145 -13
- package/src/vite.d.ts +5 -0
- package/src/vite.mjs +69 -0
package/src/browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createInspectMarkup } from "./inspect-markup.js";
|
|
1
2
|
import { getComtyStyle } from "./style.js";
|
|
2
3
|
import { createAnchorMatcher } from "./anchors.js";
|
|
3
4
|
import { createScreenshotCapture, createColorMagnifier, captureContext } from "./capture.js";
|
|
@@ -9,7 +10,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
|
|
|
9
10
|
/** Initialize once per document; safe to import and call during server rendering. */
|
|
10
11
|
export function initComty(options = {}) {
|
|
11
12
|
if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
|
|
12
|
-
const
|
|
13
|
+
const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
|
|
14
|
+
const agent = options.agent;
|
|
15
|
+
if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
|
|
16
|
+
return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
|
|
17
|
+
}
|
|
18
|
+
if (agent) {
|
|
19
|
+
try {
|
|
20
|
+
if (new URL(agent.url).origin !== window.location.origin) throw new Error();
|
|
21
|
+
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
22
|
+
}
|
|
23
|
+
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
13
24
|
const apiUrl = options.apiUrl || "http://localhost:4000";
|
|
14
25
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
15
26
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
@@ -21,12 +32,14 @@ export function initComty(options = {}) {
|
|
|
21
32
|
if (!["http:", "https:"].includes(parsedApi.protocol)) {
|
|
22
33
|
return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
|
|
23
34
|
}
|
|
24
|
-
const configuration = JSON.stringify([projectKey, apiUrl]);
|
|
35
|
+
const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
|
|
25
36
|
const existing = window[instanceKey];
|
|
26
37
|
if (existing) {
|
|
27
38
|
return existing.configuration === configuration ? existing.ready
|
|
28
39
|
: Promise.reject(new Error("Comty: already initialized with different options"));
|
|
29
40
|
}
|
|
41
|
+
const agentBridgeUrl = agent?.url;
|
|
42
|
+
const localOnly = Boolean(agent && !options.projectKey);
|
|
30
43
|
const ready = new Promise((resolve, reject) => {
|
|
31
44
|
const mount = () => {
|
|
32
45
|
try { resolve(mountComty()); } catch (error) { reject(error); }
|
|
@@ -40,10 +53,24 @@ export function initComty(options = {}) {
|
|
|
40
53
|
|
|
41
54
|
function mountComty() {
|
|
42
55
|
let projectId = null;
|
|
56
|
+
let localTasks = [];
|
|
57
|
+
const deliveringAgentReplies = new Set();
|
|
58
|
+
let localApprovals = [];
|
|
59
|
+
const seenAgentApprovals = new Set();
|
|
60
|
+
// Only requests made in this document may interrupt it with agent notifications.
|
|
61
|
+
const requestedAgentTasks = new Set();
|
|
62
|
+
let agentPanelOpen = false;
|
|
63
|
+
let agentPanelSignature = "";
|
|
64
|
+
let agentPanelTransition = 0;
|
|
65
|
+
let agentConnectionError = "";
|
|
66
|
+
let agentActionError = "";
|
|
67
|
+
let clearAgentToast = () => {};
|
|
68
|
+
const agentPendingActions = new Set();
|
|
43
69
|
const session = {
|
|
44
70
|
mode: "off",
|
|
45
|
-
get comments() { return this.mode === "browse" || this.mode === "edit"; },
|
|
46
|
-
get edit() { return this.mode === "edit"; },
|
|
71
|
+
get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
|
|
72
|
+
get edit() { return this.mode === "edit" || this.mode === "agent"; },
|
|
73
|
+
get agent() { return this.mode === "agent"; },
|
|
47
74
|
get inspect() { return this.mode === "inspect"; },
|
|
48
75
|
};
|
|
49
76
|
const selectedStatusFilters = new Set();
|
|
@@ -67,9 +94,10 @@ export function initComty(options = {}) {
|
|
|
67
94
|
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
68
95
|
let sdkToken = "";
|
|
69
96
|
const activeHoverCommentIds = new Set();
|
|
70
|
-
/** @type {null | 'edit' | 'browse'} */
|
|
97
|
+
/** @type {null | 'edit' | 'browse' | 'agent'} */
|
|
71
98
|
let sessionEntry = null;
|
|
72
99
|
const placedPins = new Map();
|
|
100
|
+
const workingOutlines = new Map();
|
|
73
101
|
const placementDiagnostics = new Map();
|
|
74
102
|
let inspectedElement = null;
|
|
75
103
|
let selectedElement = null;
|
|
@@ -83,8 +111,8 @@ export function initComty(options = {}) {
|
|
|
83
111
|
const commentTargets = new Map();
|
|
84
112
|
|
|
85
113
|
const {
|
|
86
|
-
matchesAnchorContext,
|
|
87
|
-
isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
114
|
+
matchesAnchorContext,
|
|
115
|
+
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
88
116
|
} = createAnchorMatcher({
|
|
89
117
|
getElementTextSignature,
|
|
90
118
|
getElementOwnTextSignature,
|
|
@@ -162,10 +190,27 @@ export function initComty(options = {}) {
|
|
|
162
190
|
const inspectorBtn = toolButton("Inspect · Shift I", '<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3"/><path d="m9 9 8 4-4 1-1 4Z"/>', () => setSession("inspect"));
|
|
163
191
|
const btn = toolButton("Comment · Shift C", '<path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8v.5Z"/>', () => {
|
|
164
192
|
if (!canWrite) showAuthPrompt();
|
|
165
|
-
else if (session.inspect || !isSessionActive()) startEditSession();
|
|
193
|
+
else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
|
|
166
194
|
else if (!session.edit) setSession("edit", { entry: sessionEntry });
|
|
167
195
|
});
|
|
168
196
|
btn.id = "comty-toggle";
|
|
197
|
+
const agentBtn = isLocalSite
|
|
198
|
+
? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
|
|
199
|
+
if (session.agent) {
|
|
200
|
+
const failedTask = [...localTasks].reverse().find(task => task.status === 'in_review' && !task.thread_comment_id);
|
|
201
|
+
const failedPin = failedTask && placedPins.get(String(failedTask.id));
|
|
202
|
+
if (failedPin && !failedPin.hasAttribute('data-comty-anchor-hidden')) { failedPin.click(); return; }
|
|
203
|
+
if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
|
|
204
|
+
agentPanelOpen = true;
|
|
205
|
+
renderAgentPanel();
|
|
206
|
+
}
|
|
207
|
+
} else startAgentSession();
|
|
208
|
+
})
|
|
209
|
+
: null;
|
|
210
|
+
if (agentBtn) {
|
|
211
|
+
agentBtn.title = "Agent · Shift A · localhost only";
|
|
212
|
+
agentBtn.setAttribute("aria-keyshortcuts", "Shift+A");
|
|
213
|
+
}
|
|
169
214
|
const divider = document.createElement("span");
|
|
170
215
|
divider.className = "comty-tool-divider";
|
|
171
216
|
toolbar.appendChild(divider);
|
|
@@ -175,7 +220,7 @@ export function initComty(options = {}) {
|
|
|
175
220
|
const primaryTools = document.createElement("div");
|
|
176
221
|
primaryTools.className = "comty-tool-primary";
|
|
177
222
|
toolbar.insertBefore(primaryTools, browseBtn);
|
|
178
|
-
for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
|
|
223
|
+
for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
|
|
179
224
|
const inspectorTools = document.createElement("div");
|
|
180
225
|
inspectorTools.className = "comty-tool-context";
|
|
181
226
|
inspectorTools.hidden = true;
|
|
@@ -205,6 +250,7 @@ export function initComty(options = {}) {
|
|
|
205
250
|
root,
|
|
206
251
|
isComtyElement,
|
|
207
252
|
isElementTreeHidden,
|
|
253
|
+
isInteractionBlocked: () => Boolean(markup.mode),
|
|
208
254
|
onExit() { rulersEnabled = false; syncInspectorTools(); },
|
|
209
255
|
});
|
|
210
256
|
const gridLayer = document.createElement("div");
|
|
@@ -231,6 +277,7 @@ export function initComty(options = {}) {
|
|
|
231
277
|
syncInspectorTools();
|
|
232
278
|
});
|
|
233
279
|
const rulersBtn = toolButton("Rulers · R", '<path d="M3 3h18v6H9v12H3Z"/><path d="M7 3v3m4-3v3m4-3v3M3 11h3m-3 4h3m-3 4h3"/>', () => {
|
|
280
|
+
markup.setMode(null);
|
|
234
281
|
closePropertyPanel();
|
|
235
282
|
if (motion.enabled) setMotionEnabled(false);
|
|
236
283
|
rulersEnabled = !rulersEnabled;
|
|
@@ -241,6 +288,7 @@ export function initComty(options = {}) {
|
|
|
241
288
|
syncInspectorTools();
|
|
242
289
|
});
|
|
243
290
|
const gridBtn = toolButton("Layout guides · G", '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 10h18M10 3v18"/>', () => {
|
|
291
|
+
markup.setMode(null);
|
|
244
292
|
closePropertyPanel();
|
|
245
293
|
if (motion.enabled) setMotionEnabled(false);
|
|
246
294
|
gridPanelOpen = !gridPanelOpen;
|
|
@@ -258,7 +306,37 @@ export function initComty(options = {}) {
|
|
|
258
306
|
}
|
|
259
307
|
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));
|
|
260
308
|
motionBtn.setAttribute("aria-expanded", "false");
|
|
261
|
-
|
|
309
|
+
const annotateBtn = toolButton("Annotate · D", '<path d="m16 3 5 5L8 21H3v-5ZM14 5l5 5"/>', () => toggleMarkup("annotate"));
|
|
310
|
+
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"));
|
|
311
|
+
const markup = createInspectMarkup({
|
|
312
|
+
root, icon,
|
|
313
|
+
onError: description => showNotice({ title: "Screenshot unavailable", description }),
|
|
314
|
+
onChange() {
|
|
315
|
+
annotateBtn.setAttribute("aria-pressed", String(markup.mode === "annotate"));
|
|
316
|
+
annotateBtn.setAttribute("aria-expanded", String(markup.mode === "annotate"));
|
|
317
|
+
screenshotBtn.setAttribute("aria-pressed", String(markup.mode === "capture"));
|
|
318
|
+
screenshotBtn.setAttribute("aria-busy", String(markup.busy));
|
|
319
|
+
positionToolbar();
|
|
320
|
+
},
|
|
321
|
+
capture: (region, layer) => captureViewportScreenshot({ region, annotation: () => ({
|
|
322
|
+
elements: [overlayTools, root.querySelector('.comty-distance-layer'), layer].filter(node => node && !node.hidden),
|
|
323
|
+
dispose() {},
|
|
324
|
+
}) }),
|
|
325
|
+
});
|
|
326
|
+
for (const button of [annotateBtn]) {
|
|
327
|
+
button.setAttribute("aria-controls", markup.panel.id);
|
|
328
|
+
button.setAttribute("aria-expanded", "false");
|
|
329
|
+
}
|
|
330
|
+
function toggleMarkup(mode) {
|
|
331
|
+
const next = markup.mode === mode ? null : mode;
|
|
332
|
+
closePropertyPanel();
|
|
333
|
+
if (motion.enabled) setMotionEnabled(false);
|
|
334
|
+
gridPanelOpen = false; toolPanel.hidden = true; gridBtn.setAttribute("aria-expanded", "false");
|
|
335
|
+
cancelRulerDrag();
|
|
336
|
+
selectedElement = null; measuredRegion = null; inspectDrag = null; hideInspector();
|
|
337
|
+
markup.setMode(next);
|
|
338
|
+
}
|
|
339
|
+
for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn, annotateBtn, screenshotBtn]) inspectorTools.appendChild(button);
|
|
262
340
|
|
|
263
341
|
const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
|
|
264
342
|
const motionPanel = document.createElement('section');
|
|
@@ -307,6 +385,7 @@ export function initComty(options = {}) {
|
|
|
307
385
|
motion.enabled = enabled; motionPanel.hidden = !enabled;
|
|
308
386
|
motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
|
|
309
387
|
if (enabled) {
|
|
388
|
+
markup.setMode(null);
|
|
310
389
|
closePropertyPanel();
|
|
311
390
|
rulersEnabled = false;
|
|
312
391
|
syncInspectorTools();
|
|
@@ -565,6 +644,7 @@ export function initComty(options = {}) {
|
|
|
565
644
|
const visible = session.inspect;
|
|
566
645
|
measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
|
|
567
646
|
overlayTools.hidden = !visible;
|
|
647
|
+
markup.setVisible(visible);
|
|
568
648
|
xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
|
|
569
649
|
rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
|
|
570
650
|
gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
|
|
@@ -848,6 +928,7 @@ export function initComty(options = {}) {
|
|
|
848
928
|
}
|
|
849
929
|
|
|
850
930
|
function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
|
|
931
|
+
markup.setMode(null);
|
|
851
932
|
propertyPicking = null;
|
|
852
933
|
colorMagnifier.setEnabled(false);
|
|
853
934
|
delete document.documentElement.dataset.comtyPropertyPicking;
|
|
@@ -1028,6 +1109,50 @@ export function initComty(options = {}) {
|
|
|
1028
1109
|
let toolbarMotionFrame = 0;
|
|
1029
1110
|
let toolbarAnimations = [];
|
|
1030
1111
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1112
|
+
let workingIndicatorTimer = 0;
|
|
1113
|
+
function scheduleWorkingIndicators() {
|
|
1114
|
+
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
1115
|
+
workingIndicatorTimer = setTimeout(() => {
|
|
1116
|
+
workingIndicatorTimer = 0;
|
|
1117
|
+
if (!root.isConnected) return;
|
|
1118
|
+
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
1119
|
+
if (!indicators.length) return;
|
|
1120
|
+
for (const indicator of indicators) {
|
|
1121
|
+
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
1122
|
+
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1123
|
+
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1124
|
+
indicator.dataset.activeDots = `${first},${second}`;
|
|
1125
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
1126
|
+
}
|
|
1127
|
+
scheduleWorkingIndicators();
|
|
1128
|
+
}, 600);
|
|
1129
|
+
}
|
|
1130
|
+
function createWorkingIndicator() {
|
|
1131
|
+
const indicator = document.createElement('span');
|
|
1132
|
+
indicator.className = 'comty-working-indicator';
|
|
1133
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
1134
|
+
indicator.dataset.activeDots = '3,5';
|
|
1135
|
+
for (let index = 0; index < 9; index++) {
|
|
1136
|
+
const dot = document.createElement('i');
|
|
1137
|
+
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
1138
|
+
indicator.appendChild(dot);
|
|
1139
|
+
}
|
|
1140
|
+
scheduleWorkingIndicators();
|
|
1141
|
+
return indicator;
|
|
1142
|
+
}
|
|
1143
|
+
function resetWorkingIndicators() {
|
|
1144
|
+
clearTimeout(workingIndicatorTimer);
|
|
1145
|
+
workingIndicatorTimer = 0;
|
|
1146
|
+
if (reducedMotion.matches) {
|
|
1147
|
+
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
1148
|
+
indicator.dataset.activeDots = '3,5';
|
|
1149
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
1153
|
+
}
|
|
1154
|
+
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
1155
|
+
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
1031
1156
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
1032
1157
|
function rememberToolbarPosition(left, top) {
|
|
1033
1158
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
@@ -1061,7 +1186,7 @@ export function initComty(options = {}) {
|
|
|
1061
1186
|
});
|
|
1062
1187
|
}
|
|
1063
1188
|
const rect = toolbar.getBoundingClientRect();
|
|
1064
|
-
for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
|
|
1189
|
+
for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel]) {
|
|
1065
1190
|
if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
|
|
1066
1191
|
const above = rect.top - 16;
|
|
1067
1192
|
const below = innerHeight - rect.bottom - 16;
|
|
@@ -1078,9 +1203,10 @@ export function initComty(options = {}) {
|
|
|
1078
1203
|
panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
|
|
1079
1204
|
panel.style.bottom = "auto";
|
|
1080
1205
|
}
|
|
1206
|
+
positionNotice();
|
|
1081
1207
|
}
|
|
1082
1208
|
const toolbarResize = new ResizeObserver(positionToolbar);
|
|
1083
|
-
[toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
|
|
1209
|
+
[toolbar, toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel].forEach(node => toolbarResize.observe(node));
|
|
1084
1210
|
window.addEventListener("resize", positionToolbar);
|
|
1085
1211
|
|
|
1086
1212
|
toolbar.addEventListener("pointerdown", event => {
|
|
@@ -1140,7 +1266,7 @@ export function initComty(options = {}) {
|
|
|
1140
1266
|
|
|
1141
1267
|
function prepareToolbarMotion() {
|
|
1142
1268
|
if (toolbarMotionFrame) return;
|
|
1143
|
-
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools]
|
|
1269
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
|
|
1144
1270
|
// Move each group's background with its icons, including when interrupted.
|
|
1145
1271
|
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
1146
1272
|
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
@@ -1364,7 +1490,8 @@ export function initComty(options = {}) {
|
|
|
1364
1490
|
selectedElement = null;
|
|
1365
1491
|
sessionEntry = null;
|
|
1366
1492
|
session.mode = "off";
|
|
1367
|
-
|
|
1493
|
+
syncPlacementCursor();
|
|
1494
|
+
document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
|
|
1368
1495
|
hideInspector();
|
|
1369
1496
|
setSidepanelOpen(false);
|
|
1370
1497
|
closeBubbles();
|
|
@@ -1378,25 +1505,29 @@ export function initComty(options = {}) {
|
|
|
1378
1505
|
if (mode === "off") return;
|
|
1379
1506
|
if (mode === "inspect") {
|
|
1380
1507
|
session.mode = "inspect";
|
|
1508
|
+
document.documentElement.classList.add("comty-inspect-annotations");
|
|
1381
1509
|
syncToolbar();
|
|
1510
|
+
renderCachedComments();
|
|
1382
1511
|
ensureLiveTracking();
|
|
1383
1512
|
return;
|
|
1384
1513
|
}
|
|
1385
1514
|
|
|
1386
|
-
if (session.edit && mode === "browse") {
|
|
1515
|
+
if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
|
|
1387
1516
|
closeBubbles();
|
|
1388
1517
|
}
|
|
1389
1518
|
const hadComments = session.comments;
|
|
1390
1519
|
session.mode = mode;
|
|
1391
1520
|
sessionEntry = entry;
|
|
1521
|
+
document.documentElement.classList.remove("comty-inspect-annotations");
|
|
1392
1522
|
document.documentElement.classList.toggle("comty-comments-active", session.comments);
|
|
1393
|
-
|
|
1523
|
+
syncPlacementCursor();
|
|
1394
1524
|
if (session.edit) {
|
|
1395
1525
|
setMotionEnabled(false);
|
|
1396
1526
|
selectedElement = null;
|
|
1397
1527
|
} else {
|
|
1398
1528
|
hideInspector();
|
|
1399
1529
|
}
|
|
1530
|
+
if (session.agent && sidepanelOpen) setSidepanelOpen(false);
|
|
1400
1531
|
syncToolbar();
|
|
1401
1532
|
ensureLiveTracking();
|
|
1402
1533
|
if (renderComments || !hadComments) renderCachedComments();
|
|
@@ -1409,6 +1540,10 @@ export function initComty(options = {}) {
|
|
|
1409
1540
|
}
|
|
1410
1541
|
|
|
1411
1542
|
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
1543
|
+
if (localOnly) {
|
|
1544
|
+
showNotice({ title: "Comments are not configured", description: "Connect this site to a Comty project to leave shared comments." });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1412
1547
|
const guestAllowed = allowGuestComments;
|
|
1413
1548
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
1414
1549
|
const opener = document.activeElement;
|
|
@@ -1530,14 +1665,17 @@ export function initComty(options = {}) {
|
|
|
1530
1665
|
}
|
|
1531
1666
|
|
|
1532
1667
|
function syncToolbar() {
|
|
1533
|
-
if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
1668
|
+
if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
1534
1669
|
root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
|
|
1535
1670
|
browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
|
|
1536
1671
|
inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
|
|
1537
|
-
btn.setAttribute("aria-pressed", String(session.edit));
|
|
1538
|
-
|
|
1672
|
+
btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
|
|
1673
|
+
agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
|
|
1674
|
+
commentTools.hidden = session.inspect || session.agent;
|
|
1539
1675
|
inspectorTools.hidden = !session.inspect;
|
|
1540
1676
|
syncInspectorTools();
|
|
1677
|
+
renderAgentPanel();
|
|
1678
|
+
positionNotice();
|
|
1541
1679
|
}
|
|
1542
1680
|
|
|
1543
1681
|
const inspectionLayer = document.createElement("div");
|
|
@@ -1850,7 +1988,7 @@ export function initComty(options = {}) {
|
|
|
1850
1988
|
});
|
|
1851
1989
|
return;
|
|
1852
1990
|
}
|
|
1853
|
-
if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
|
|
1991
|
+
if (markup.mode || propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
|
|
1854
1992
|
if (inspectionLayer.childNodes.length) hideInspector();
|
|
1855
1993
|
return;
|
|
1856
1994
|
}
|
|
@@ -1994,10 +2132,20 @@ export function initComty(options = {}) {
|
|
|
1994
2132
|
|
|
1995
2133
|
function startBrowseSession() {
|
|
1996
2134
|
setSession("browse", { renderComments: true });
|
|
2135
|
+
void loadComments({ force: true });
|
|
1997
2136
|
}
|
|
1998
2137
|
|
|
1999
2138
|
function startEditSession() {
|
|
2000
2139
|
setSession("edit", { renderComments: true });
|
|
2140
|
+
void loadComments({ force: true });
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function startAgentSession() {
|
|
2144
|
+
if (!isLocalSite) return;
|
|
2145
|
+
if (session.agent) return;
|
|
2146
|
+
closeBubbles();
|
|
2147
|
+
setSession("agent", { renderComments: true });
|
|
2148
|
+
void loadComments({ force: true });
|
|
2001
2149
|
}
|
|
2002
2150
|
|
|
2003
2151
|
function toggleSidepanel() {
|
|
@@ -2023,7 +2171,7 @@ export function initComty(options = {}) {
|
|
|
2023
2171
|
showAuthPrompt();
|
|
2024
2172
|
return;
|
|
2025
2173
|
}
|
|
2026
|
-
if (session.inspect || !isSessionActive()) {
|
|
2174
|
+
if (session.agent || session.inspect || !isSessionActive()) {
|
|
2027
2175
|
startEditSession();
|
|
2028
2176
|
return;
|
|
2029
2177
|
}
|
|
@@ -2311,6 +2459,24 @@ export function initComty(options = {}) {
|
|
|
2311
2459
|
ancestors,
|
|
2312
2460
|
layer: layer ? getElementFingerprint(layer) : null,
|
|
2313
2461
|
};
|
|
2462
|
+
// React exposes this in development builds. Keep it as a hint: the coding
|
|
2463
|
+
// agent must confirm the component and variant in source before editing.
|
|
2464
|
+
const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
|
|
2465
|
+
let fiber = fiberKey ? el[fiberKey] : null;
|
|
2466
|
+
const components = [];
|
|
2467
|
+
while (fiber && components.length < 8) {
|
|
2468
|
+
const component = fiber.type;
|
|
2469
|
+
const name = component?.displayName || component?.name;
|
|
2470
|
+
if (name && typeof name === 'string') {
|
|
2471
|
+
const props = fiber.memoizedProps || {};
|
|
2472
|
+
const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
|
|
2473
|
+
['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
|
|
2474
|
+
const source = fiber._debugSource;
|
|
2475
|
+
components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
|
|
2476
|
+
}
|
|
2477
|
+
fiber = fiber.return;
|
|
2478
|
+
}
|
|
2479
|
+
if (components.length) context.components = components;
|
|
2314
2480
|
context.candidateCount = [...document.querySelectorAll(context.target.tag)]
|
|
2315
2481
|
.filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
|
|
2316
2482
|
return context;
|
|
@@ -2339,6 +2505,11 @@ export function initComty(options = {}) {
|
|
|
2339
2505
|
};
|
|
2340
2506
|
}
|
|
2341
2507
|
|
|
2508
|
+
function getPagePath() {
|
|
2509
|
+
const { pathname, hash } = window.location;
|
|
2510
|
+
return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2342
2513
|
function resolveBySelector(selector) {
|
|
2343
2514
|
if (!selector) return null;
|
|
2344
2515
|
try {
|
|
@@ -2360,17 +2531,9 @@ export function initComty(options = {}) {
|
|
|
2360
2531
|
}
|
|
2361
2532
|
const context = parseAnchorContext(comment);
|
|
2362
2533
|
const exact = resolveBySelector(comment.selector || "body");
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
if (count !== context.candidateCount) {
|
|
2367
|
-
commentTargets.set(key, { element: null, signature, revision: anchorRevision });
|
|
2368
|
-
return null;
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
let element = exact && matchesAnchorContext(exact, context) ? getAnchorElement(exact) : null;
|
|
2372
|
-
if (!element) element = resolveByAnchorContext(context);
|
|
2373
|
-
commentTargets.set(key, { element, signature, revision: anchorRevision });
|
|
2534
|
+
const resolution = resolveAnchor(context, exact);
|
|
2535
|
+
const element = resolution.element ? getAnchorElement(resolution.element) : null;
|
|
2536
|
+
commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
|
|
2374
2537
|
return element;
|
|
2375
2538
|
}
|
|
2376
2539
|
|
|
@@ -2441,9 +2604,14 @@ export function initComty(options = {}) {
|
|
|
2441
2604
|
return { deviceType: "desktop", deviceLabel: "Desktop" };
|
|
2442
2605
|
}
|
|
2443
2606
|
|
|
2607
|
+
function syncPlacementCursor() {
|
|
2608
|
+
document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2444
2611
|
function closeBubbles() {
|
|
2445
2612
|
const hadDraft = Boolean(draftAnchor);
|
|
2446
2613
|
draftAnchor = null;
|
|
2614
|
+
syncPlacementCursor();
|
|
2447
2615
|
activeHoverCommentIds.clear();
|
|
2448
2616
|
if (!session.edit || hadDraft) hideInspector();
|
|
2449
2617
|
document
|
|
@@ -2488,7 +2656,7 @@ export function initComty(options = {}) {
|
|
|
2488
2656
|
pending: commentId.startsWith("pending-"),
|
|
2489
2657
|
project_id: projectId,
|
|
2490
2658
|
domain: payload.host || window.location.host,
|
|
2491
|
-
page_path: payload.pagePath ||
|
|
2659
|
+
page_path: payload.pagePath || getPagePath(),
|
|
2492
2660
|
selector: payload.selector || "body",
|
|
2493
2661
|
rel_x: payload.relX,
|
|
2494
2662
|
rel_y: payload.relY,
|
|
@@ -2513,7 +2681,7 @@ export function initComty(options = {}) {
|
|
|
2513
2681
|
replies: [],
|
|
2514
2682
|
};
|
|
2515
2683
|
|
|
2516
|
-
cachedPagePath =
|
|
2684
|
+
cachedPagePath = getPagePath();
|
|
2517
2685
|
cachedComments = [
|
|
2518
2686
|
...cachedComments.filter(
|
|
2519
2687
|
(cached) => getCommentId(cached) !== commentId,
|
|
@@ -2526,6 +2694,7 @@ export function initComty(options = {}) {
|
|
|
2526
2694
|
}
|
|
2527
2695
|
|
|
2528
2696
|
function getCommentAuthorName(comment) {
|
|
2697
|
+
if (comment?.author_kind === 'agent') return 'Agent';
|
|
2529
2698
|
return comment?.author_name || comment?.name || "Anonymous";
|
|
2530
2699
|
}
|
|
2531
2700
|
|
|
@@ -2547,6 +2716,12 @@ export function initComty(options = {}) {
|
|
|
2547
2716
|
function createCommentAvatar(comment, extraClass = "") {
|
|
2548
2717
|
const avatar = document.createElement("span");
|
|
2549
2718
|
avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
|
|
2719
|
+
if (comment?.author_kind === 'agent') {
|
|
2720
|
+
avatar.classList.add('comty-agent-avatar');
|
|
2721
|
+
avatar.setAttribute('aria-label', 'Agent');
|
|
2722
|
+
avatar.innerHTML = '<svg viewBox="0 0 48 48" aria-hidden="true"><path d="M24 2C26.62 2 28.47 5.29 31.27 6.45C34.07 7.61 37.71 6.59 39.56 8.44C41.41 10.29 40.39 13.93 41.55 16.73C42.71 19.53 46 21.38 46 24C46 26.62 42.71 28.47 41.55 31.27C40.39 34.07 41.41 37.71 39.56 39.56C37.71 41.41 34.07 40.39 31.27 41.55C28.47 42.71 26.62 46 24 46C21.38 46 19.53 42.71 16.73 41.55C13.93 40.39 10.29 41.41 8.44 39.56C6.59 37.71 7.61 34.07 6.45 31.27C5.29 28.47 2 26.62 2 24C2 21.38 5.29 19.53 6.45 16.73C7.61 13.93 6.59 10.29 8.44 8.44C10.29 6.59 13.93 7.61 16.73 6.45C19.53 5.29 21.38 2 24 2Z" fill="currentColor"/><path fill="var(--comty-surface)" fill-rule="evenodd" d="M16 33 22.3 15h3.4L32 33h-3.3l-1.3-4h-6.8l-1.3 4H16Zm5.5-6.8h5L24 18.4l-2.5 7.8Z"/></svg>';
|
|
2723
|
+
return avatar;
|
|
2724
|
+
}
|
|
2550
2725
|
const authorName = getCommentAuthorName(comment);
|
|
2551
2726
|
const avatarUrl = getCommentAuthorAvatarUrl(comment);
|
|
2552
2727
|
|
|
@@ -2554,6 +2729,7 @@ export function initComty(options = {}) {
|
|
|
2554
2729
|
const image = document.createElement("img");
|
|
2555
2730
|
image.src = avatarUrl;
|
|
2556
2731
|
image.alt = "";
|
|
2732
|
+
image.draggable = false;
|
|
2557
2733
|
image.onerror = () => {
|
|
2558
2734
|
image.remove();
|
|
2559
2735
|
avatar.innerText = authorName.charAt(0).toUpperCase();
|
|
@@ -2707,6 +2883,7 @@ export function initComty(options = {}) {
|
|
|
2707
2883
|
function createCommentPin(comment, x, y) {
|
|
2708
2884
|
const pin = document.createElement("button");
|
|
2709
2885
|
pin.className = "comty-pin";
|
|
2886
|
+
pin.toggleAttribute('data-agent-task', Boolean(comment.local_agent));
|
|
2710
2887
|
pin.type = "button";
|
|
2711
2888
|
pin.style.left = `${x}px`;
|
|
2712
2889
|
pin.style.top = `${y}px`;
|
|
@@ -2724,10 +2901,52 @@ export function initComty(options = {}) {
|
|
|
2724
2901
|
const thread = document.createElement("div");
|
|
2725
2902
|
thread.className = "comty-thread";
|
|
2726
2903
|
thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
|
|
2904
|
+
if (comment.local_agent) {
|
|
2905
|
+
thread.appendChild(createThreadItem({ author_kind: 'agent', content: 'Modification failed. Open to retry or dismiss.' }, { showMore: false }).item);
|
|
2906
|
+
}
|
|
2727
2907
|
bubble.appendChild(thread);
|
|
2728
2908
|
return bubble;
|
|
2729
2909
|
}
|
|
2730
2910
|
|
|
2911
|
+
function openAgentTask(task, x, y) {
|
|
2912
|
+
closeBubbles();
|
|
2913
|
+
const bubble = document.createElement('div');
|
|
2914
|
+
bubble.className = 'comty-bubble comty-agent-task-thread';
|
|
2915
|
+
bubble.dataset.commentId = task.id;
|
|
2916
|
+
bubble.setAttribute('role', 'dialog');
|
|
2917
|
+
bubble.setAttribute('aria-label', 'Failed modification');
|
|
2918
|
+
const header = document.createElement('div'); header.className = 'comty-bubble-header';
|
|
2919
|
+
const title = document.createElement('strong'); title.textContent = 'Modification failed';
|
|
2920
|
+
const close = document.createElement('button'); close.type = 'button'; close.className = 'comty-icon-button';
|
|
2921
|
+
close.setAttribute('aria-label', 'Close failed modification');
|
|
2922
|
+
close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
2923
|
+
close.onclick = () => closeBubbles();
|
|
2924
|
+
header.append(title, close);
|
|
2925
|
+
const thread = document.createElement('div'); thread.className = 'comty-thread';
|
|
2926
|
+
const list = document.createElement('div'); list.className = 'comty-thread-list';
|
|
2927
|
+
const issue = task.error ? describeAgentError(task.error) : null;
|
|
2928
|
+
list.append(
|
|
2929
|
+
createThreadItem(task, { showMore: false }).item,
|
|
2930
|
+
createThreadItem({ author_kind: 'agent', content: task.result?.summary || issue?.message || issue?.description || 'The agent could not complete this request.' }, { showMore: false }).item,
|
|
2931
|
+
);
|
|
2932
|
+
thread.appendChild(list);
|
|
2933
|
+
const actions = document.createElement('div'); actions.className = 'comty-agent-task-actions';
|
|
2934
|
+
for (const action of ['retry', 'dismiss']) {
|
|
2935
|
+
const button = document.createElement('button'); button.type = 'button'; button.className = 'comty-agent-action';
|
|
2936
|
+
button.textContent = action === 'retry' ? 'Retry' : 'Dismiss';
|
|
2937
|
+
button.onclick = async () => {
|
|
2938
|
+
for (const control of actions.children) control.disabled = true;
|
|
2939
|
+
try { await runAgentAction(`tasks/${task.id}/${action}`); }
|
|
2940
|
+
catch (error) { showAgentBridgeError(error.message); }
|
|
2941
|
+
finally { for (const control of actions.children) control.disabled = false; }
|
|
2942
|
+
};
|
|
2943
|
+
actions.appendChild(button);
|
|
2944
|
+
}
|
|
2945
|
+
bubble.append(header, thread, actions);
|
|
2946
|
+
root.appendChild(bubble);
|
|
2947
|
+
positionCommentBubble(bubble, x, y);
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2731
2950
|
function mountCaptureAnnotation(comment, { x, y }) {
|
|
2732
2951
|
const host = document.createElement("div");
|
|
2733
2952
|
host.dataset.comtyCaptureExclude = "";
|
|
@@ -2778,6 +2997,7 @@ export function initComty(options = {}) {
|
|
|
2778
2997
|
payload,
|
|
2779
2998
|
preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
|
|
2780
2999
|
};
|
|
3000
|
+
syncPlacementCursor();
|
|
2781
3001
|
ensureLiveTracking();
|
|
2782
3002
|
|
|
2783
3003
|
const pin = document.createElement("div");
|
|
@@ -2795,10 +3015,21 @@ export function initComty(options = {}) {
|
|
|
2795
3015
|
|
|
2796
3016
|
const textarea = document.createElement("textarea");
|
|
2797
3017
|
textarea.className = "comty-create-textarea";
|
|
2798
|
-
textarea.placeholder = "Add a comment";
|
|
3018
|
+
textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
|
|
2799
3019
|
textarea.setAttribute("aria-label", textarea.placeholder);
|
|
2800
3020
|
textarea.rows = 1;
|
|
2801
3021
|
|
|
3022
|
+
const scopePicker = document.createElement('label');
|
|
3023
|
+
scopePicker.className = 'comty-agent-scope';
|
|
3024
|
+
if (payload.kind === 'agent') {
|
|
3025
|
+
const input = document.createElement('input');
|
|
3026
|
+
input.type = 'checkbox';
|
|
3027
|
+
input.setAttribute('aria-label', 'Apply to all instances of this variant');
|
|
3028
|
+
const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
|
|
3029
|
+
scopePicker.title = 'Apply to all instances of this variant';
|
|
3030
|
+
scopePicker.append(input, track, document.createTextNode('Apply globally'));
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2802
3033
|
const actions = document.createElement("div");
|
|
2803
3034
|
actions.className = "comty-create-actions";
|
|
2804
3035
|
|
|
@@ -2852,7 +3083,7 @@ export function initComty(options = {}) {
|
|
|
2852
3083
|
const submitBtn = document.createElement("button");
|
|
2853
3084
|
submitBtn.type = "button";
|
|
2854
3085
|
submitBtn.className = "comty-btn-submit";
|
|
2855
|
-
submitBtn.setAttribute("aria-label", "Post comment");
|
|
3086
|
+
submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
|
|
2856
3087
|
submitBtn.disabled = true;
|
|
2857
3088
|
submitBtn.innerHTML = `
|
|
2858
3089
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -2869,49 +3100,63 @@ export function initComty(options = {}) {
|
|
|
2869
3100
|
textarea.disabled = true;
|
|
2870
3101
|
linkBtn.disabled = true;
|
|
2871
3102
|
|
|
3103
|
+
if (payload.kind === "agent" && !agent) {
|
|
3104
|
+
showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
|
|
3105
|
+
}
|
|
2872
3106
|
payload.content = content;
|
|
3107
|
+
if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
|
|
2873
3108
|
payload.linkUrl = attachedLink || null;
|
|
2874
3109
|
textarea.textContent = content;
|
|
2875
3110
|
|
|
2876
|
-
const
|
|
2877
|
-
const
|
|
2878
|
-
const
|
|
2879
|
-
|
|
3111
|
+
const isAgentTask = payload.kind === "agent";
|
|
3112
|
+
const context = isAgentTask ? null : captureContext();
|
|
3113
|
+
const point = isAgentTask ? null : getPinPoint(pin);
|
|
3114
|
+
const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
|
|
2880
3115
|
bubble.remove();
|
|
2881
3116
|
pin.remove();
|
|
2882
3117
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
3118
|
+
syncPlacementCursor();
|
|
2883
3119
|
hideInspector();
|
|
2884
|
-
addLocalComment(pendingId, payload);
|
|
3120
|
+
if (!isAgentTask) addLocalComment(pendingId, payload);
|
|
2885
3121
|
let post;
|
|
2886
3122
|
try {
|
|
2887
|
-
post = await fetch(`${apiUrl}/api/comments`, {
|
|
3123
|
+
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
2888
3124
|
method: "POST",
|
|
2889
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
2890
|
-
body: JSON.stringify(payload),
|
|
3125
|
+
headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3126
|
+
body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
|
|
2891
3127
|
});
|
|
2892
|
-
if (!post.ok) throw new Error(`
|
|
3128
|
+
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
2893
3129
|
const data = await post.json();
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3130
|
+
const id = isAgentTask ? data?.task?.id : data?.id;
|
|
3131
|
+
if (!id) throw new Error("Save returned no ID");
|
|
3132
|
+
if (isAgentTask) {
|
|
3133
|
+
requestedAgentTasks.add(id);
|
|
3134
|
+
void loadComments({ force: true });
|
|
3135
|
+
} else {
|
|
3136
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
3137
|
+
if (context.url === location.href) {
|
|
3138
|
+
const comment = addLocalComment(String(id), payload);
|
|
3139
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
3140
|
+
} else void loadComments({ force: true });
|
|
3141
|
+
}
|
|
2901
3142
|
} catch (error) {
|
|
2902
3143
|
console.error("Comty: save failed", error);
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3144
|
+
if (isAgentTask) showAgentBridgeError(error.message);
|
|
3145
|
+
else {
|
|
3146
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
3147
|
+
renderSidepanelComments();
|
|
3148
|
+
void renderComments(cachedComments, { retry: false });
|
|
3149
|
+
}
|
|
2906
3150
|
// Keep the original editor and its text available for retry, including attachments.
|
|
2907
3151
|
if (!draftAnchor) draftAnchor = editorDraft;
|
|
3152
|
+
syncPlacementCursor();
|
|
2908
3153
|
root.append(pin, bubble);
|
|
2909
3154
|
submitBtn.disabled = false;
|
|
2910
3155
|
textarea.disabled = false;
|
|
2911
3156
|
linkBtn.disabled = false;
|
|
2912
3157
|
const errorMessage = document.createElement("p");
|
|
2913
3158
|
errorMessage.setAttribute("role", "alert");
|
|
2914
|
-
errorMessage.textContent = "Comment not saved. Your draft is ready to retry
|
|
3159
|
+
errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
|
|
2915
3160
|
bubble.querySelector('[role="alert"]')?.remove();
|
|
2916
3161
|
bubble.append(errorMessage);
|
|
2917
3162
|
if (bubble.isConnected) textarea.focus();
|
|
@@ -3000,7 +3245,8 @@ export function initComty(options = {}) {
|
|
|
3000
3245
|
renderInspector();
|
|
3001
3246
|
};
|
|
3002
3247
|
|
|
3003
|
-
actions.append(
|
|
3248
|
+
if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
|
|
3249
|
+
else actions.append(linkBtn, submitBtn, linkEditor);
|
|
3004
3250
|
|
|
3005
3251
|
bubble.appendChild(textarea);
|
|
3006
3252
|
bubble.appendChild(attachment);
|
|
@@ -3013,6 +3259,249 @@ export function initComty(options = {}) {
|
|
|
3013
3259
|
setTimeout(() => textarea.focus(), 10);
|
|
3014
3260
|
}
|
|
3015
3261
|
|
|
3262
|
+
function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
|
|
3263
|
+
|
|
3264
|
+
function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
|
|
3265
|
+
const issue = describeAgentError(message);
|
|
3266
|
+
showNotice({ title: 'Modification failed', description: issue.title === 'Task could not finish' ? 'The agent could not complete this request.' : issue.title + '.' });
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function positionNotice() {
|
|
3270
|
+
const toast = root.querySelector(".comty-agent-task-toast");
|
|
3271
|
+
if (!toast) return;
|
|
3272
|
+
toast.style.removeProperty("left");
|
|
3273
|
+
toast.style.removeProperty("top");
|
|
3274
|
+
toast.toggleAttribute("data-toolbar", isSessionActive());
|
|
3275
|
+
if (!isSessionActive()) return;
|
|
3276
|
+
const rect = toolbar.getBoundingClientRect();
|
|
3277
|
+
const above = rect.top - toast.offsetHeight - 8;
|
|
3278
|
+
toast.style.left = `${Math.max(8, Math.min(rect.right - toast.offsetWidth, innerWidth - toast.offsetWidth - 8))}px`;
|
|
3279
|
+
toast.style.top = `${Math.max(8, Math.min(above >= 8 ? above : rect.bottom + 8, innerHeight - toast.offsetHeight - 8))}px`;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function showNotice(issue) {
|
|
3283
|
+
clearAgentToast();
|
|
3284
|
+
const toast = document.createElement("div");
|
|
3285
|
+
toast.className = "comty-agent-task-toast";
|
|
3286
|
+
toast.setAttribute("role", "alert");
|
|
3287
|
+
const title = document.createElement("strong"); title.textContent = issue.title;
|
|
3288
|
+
const description = document.createElement("p"); description.textContent = issue.description;
|
|
3289
|
+
const close = document.createElement("button"); close.type = "button";
|
|
3290
|
+
close.setAttribute("aria-label", "Dismiss notification");
|
|
3291
|
+
close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
3292
|
+
toast.append(title, description, close);
|
|
3293
|
+
let timer, leaving = false;
|
|
3294
|
+
const dismiss = () => {
|
|
3295
|
+
if (leaving) return;
|
|
3296
|
+
leaving = true;
|
|
3297
|
+
clearTimeout(timer);
|
|
3298
|
+
toast.dataset.leaving = "";
|
|
3299
|
+
timer = setTimeout(() => toast.remove(), reducedMotion.matches ? 0 : 200);
|
|
3300
|
+
};
|
|
3301
|
+
const schedule = () => { if (!leaving) { clearTimeout(timer); timer = setTimeout(dismiss, 6000); } };
|
|
3302
|
+
const pause = () => { if (!leaving) clearTimeout(timer); };
|
|
3303
|
+
clearAgentToast = () => { clearTimeout(timer); toast.remove(); };
|
|
3304
|
+
close.onclick = dismiss;
|
|
3305
|
+
toast.onmouseenter = pause;
|
|
3306
|
+
toast.onmouseleave = schedule;
|
|
3307
|
+
toast.onfocusin = pause;
|
|
3308
|
+
toast.onfocusout = schedule;
|
|
3309
|
+
root.appendChild(toast);
|
|
3310
|
+
positionNotice();
|
|
3311
|
+
schedule();
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function describeAgentError(error) {
|
|
3315
|
+
const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
|
|
3316
|
+
let value = error;
|
|
3317
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
3318
|
+
if (typeof value === 'string') {
|
|
3319
|
+
try { value = JSON.parse(value); continue; } catch {}
|
|
3320
|
+
const start = value.indexOf('{'), end = value.lastIndexOf('}');
|
|
3321
|
+
if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
|
|
3322
|
+
break;
|
|
3323
|
+
}
|
|
3324
|
+
if (!value || typeof value !== 'object') break;
|
|
3325
|
+
value = value.error || value.message || value.cause || value.stderr || value;
|
|
3326
|
+
}
|
|
3327
|
+
const message = typeof value === 'string' ? value : '';
|
|
3328
|
+
const source = raw + ' ' + message;
|
|
3329
|
+
const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
|
|
3330
|
+
let title, description;
|
|
3331
|
+
if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
|
|
3332
|
+
title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
|
|
3333
|
+
} else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
|
|
3334
|
+
title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
|
|
3335
|
+
} else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
|
|
3336
|
+
title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
|
|
3337
|
+
} else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
|
|
3338
|
+
title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
|
|
3339
|
+
} else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
|
|
3340
|
+
title = 'Model unavailable'; description = 'The local agent rejected this model. Check its CLI version and signed-in account, then retry.';
|
|
3341
|
+
} else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
|
|
3342
|
+
title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
|
|
3343
|
+
} else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
|
|
3344
|
+
title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
|
|
3345
|
+
} else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
|
|
3346
|
+
title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
|
|
3347
|
+
} else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
|
|
3348
|
+
title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
|
|
3349
|
+
} else if (/completion was not verified/i.test(source)) {
|
|
3350
|
+
title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
|
|
3351
|
+
} else {
|
|
3352
|
+
title = 'Task could not finish';
|
|
3353
|
+
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.';
|
|
3354
|
+
}
|
|
3355
|
+
return { title, description, raw, message };
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
async function runAgentAction(path, input = {}) {
|
|
3359
|
+
const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
|
|
3360
|
+
if (!response.ok) {
|
|
3361
|
+
const body = await response.json().catch(() => null);
|
|
3362
|
+
throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
|
|
3363
|
+
}
|
|
3364
|
+
if (path.endsWith('/retry')) requestedAgentTasks.add(path.split('/')[1]);
|
|
3365
|
+
await loadComments({ force: true });
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
function renderAgentPanel() {
|
|
3369
|
+
if (!agent) return;
|
|
3370
|
+
const visibleTasks = [];
|
|
3371
|
+
const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
|
|
3372
|
+
if (latestResult) visibleTasks.push(latestResult);
|
|
3373
|
+
if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
|
|
3374
|
+
let panel = root.querySelector(".comty-agent-panel");
|
|
3375
|
+
if (!panel && !agentPanelOpen) return;
|
|
3376
|
+
if (!panel) {
|
|
3377
|
+
panel = document.createElement("section");
|
|
3378
|
+
panel.className = "comty-agent-panel";
|
|
3379
|
+
panel.setAttribute("aria-label", "Local agent tasks");
|
|
3380
|
+
panel.hidden = true;
|
|
3381
|
+
// Keep the task card above inspection overlays and below the toolbar.
|
|
3382
|
+
root.insertBefore(panel, toolbar);
|
|
3383
|
+
}
|
|
3384
|
+
const open = Boolean(agent) && agentPanelOpen;
|
|
3385
|
+
if (open) {
|
|
3386
|
+
clearAgentToast();
|
|
3387
|
+
if (panel.hidden) {
|
|
3388
|
+
const rect = toolbar.getBoundingClientRect();
|
|
3389
|
+
panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
|
|
3390
|
+
panel.dataset.closed = "";
|
|
3391
|
+
panel.hidden = false;
|
|
3392
|
+
panel.getBoundingClientRect();
|
|
3393
|
+
}
|
|
3394
|
+
if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
|
|
3395
|
+
} else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
|
|
3396
|
+
const transition = ++agentPanelTransition;
|
|
3397
|
+
panel.dataset.closed = "";
|
|
3398
|
+
const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
|
|
3399
|
+
if (reducedMotion.matches) finish();
|
|
3400
|
+
else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
|
|
3401
|
+
}
|
|
3402
|
+
panel.inert = !open;
|
|
3403
|
+
panel.setAttribute("aria-hidden", String(!open));
|
|
3404
|
+
const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
|
|
3405
|
+
if (signature === agentPanelSignature) return;
|
|
3406
|
+
agentPanelSignature = signature;
|
|
3407
|
+
const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
|
|
3408
|
+
const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
|
|
3409
|
+
const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
|
|
3410
|
+
const node = (tag, className, text) => {
|
|
3411
|
+
const element = document.createElement(tag);
|
|
3412
|
+
if (className) element.className = className;
|
|
3413
|
+
if (text != null) element.textContent = text;
|
|
3414
|
+
return element;
|
|
3415
|
+
};
|
|
3416
|
+
const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
|
|
3417
|
+
panel.replaceChildren();
|
|
3418
|
+
const header = node('header', 'comty-agent-panel-header');
|
|
3419
|
+
const heading = node('div', 'comty-agent-heading');
|
|
3420
|
+
heading.append(node('strong', '', 'Agent'));
|
|
3421
|
+
const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
|
|
3422
|
+
toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
3423
|
+
toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
|
|
3424
|
+
toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
|
|
3425
|
+
header.append(heading, toggle); panel.appendChild(header);
|
|
3426
|
+
const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
|
|
3427
|
+
const technicalDetails = (parent, raw, key) => {
|
|
3428
|
+
const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
|
|
3429
|
+
const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
|
|
3430
|
+
details.append(summary, node('pre', '', raw)); parent.appendChild(details);
|
|
3431
|
+
};
|
|
3432
|
+
const errorMessage = (parent, error, key) => {
|
|
3433
|
+
const issue = describeAgentError(error);
|
|
3434
|
+
const message = node('div', 'comty-agent-error');
|
|
3435
|
+
message.append(node('strong', '', issue.title), node('p', '', issue.description));
|
|
3436
|
+
parent.appendChild(message); technicalDetails(parent, issue.raw, key);
|
|
3437
|
+
};
|
|
3438
|
+
if (agentConnectionError || agentActionError) {
|
|
3439
|
+
const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
|
|
3440
|
+
errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
|
|
3441
|
+
}
|
|
3442
|
+
const action = (parent, label, path, input = {}, primary = false) => {
|
|
3443
|
+
const button = node('button', 'comty-agent-action', label); button.type = 'button';
|
|
3444
|
+
const key = path + JSON.stringify(input); button.dataset.focusKey = key;
|
|
3445
|
+
button.disabled = agentPendingActions.has(key);
|
|
3446
|
+
if (primary) button.dataset.primary = '';
|
|
3447
|
+
button.onclick = async () => {
|
|
3448
|
+
if (agentPendingActions.has(key)) return;
|
|
3449
|
+
agentActionError = '';
|
|
3450
|
+
agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
|
|
3451
|
+
try {
|
|
3452
|
+
await runAgentAction(path, input);
|
|
3453
|
+
} catch (error) { agentActionError = error.message; }
|
|
3454
|
+
finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
|
|
3455
|
+
};
|
|
3456
|
+
parent.appendChild(button);
|
|
3457
|
+
};
|
|
3458
|
+
for (const approval of localApprovals) {
|
|
3459
|
+
const section = node('div', 'comty-agent-item');
|
|
3460
|
+
section.append(node('h3', '', 'Approval required'));
|
|
3461
|
+
const task = localTasks.find(task => task.id === approval.taskId);
|
|
3462
|
+
if (task) section.appendChild(node('p', '', task.content));
|
|
3463
|
+
section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
|
|
3464
|
+
const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
|
|
3465
|
+
if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
|
|
3466
|
+
technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
|
|
3467
|
+
const actions = node('div', 'comty-agent-actions');
|
|
3468
|
+
action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
|
|
3469
|
+
action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
|
|
3470
|
+
section.appendChild(actions); content.appendChild(section);
|
|
3471
|
+
}
|
|
3472
|
+
const list = node('div', 'comty-agent-list'); content.appendChild(list);
|
|
3473
|
+
for (const task of visibleTasks.reverse()) {
|
|
3474
|
+
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
3475
|
+
const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
|
|
3476
|
+
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
3477
|
+
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
3478
|
+
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
3479
|
+
const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
|
|
3480
|
+
row.append(dot, name);
|
|
3481
|
+
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
3482
|
+
const actions = node('div', 'comty-agent-row-actions');
|
|
3483
|
+
if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
|
|
3484
|
+
if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
|
|
3485
|
+
if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
3486
|
+
if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
3487
|
+
if (actions.childElementCount) row.appendChild(actions);
|
|
3488
|
+
row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
|
|
3489
|
+
list.appendChild(row);
|
|
3490
|
+
if (task.error) {
|
|
3491
|
+
const details = node('div', 'comty-agent-item');
|
|
3492
|
+
errorMessage(details, task.error, task.id);
|
|
3493
|
+
list.appendChild(details);
|
|
3494
|
+
}
|
|
3495
|
+
if (task.result?.summary) {
|
|
3496
|
+
const answer = node('div', 'comty-agent-answer');
|
|
3497
|
+
answer.append(brand(), node('p', '', task.result.summary));
|
|
3498
|
+
list.appendChild(answer);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
content.scrollTop = scrollTop;
|
|
3502
|
+
if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3016
3505
|
function setSidepanelOpen(open) {
|
|
3017
3506
|
sidepanelOpen = open;
|
|
3018
3507
|
sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
|
|
@@ -3048,12 +3537,52 @@ export function initComty(options = {}) {
|
|
|
3048
3537
|
|
|
3049
3538
|
function openCommentFromPanel(comment) {
|
|
3050
3539
|
const target = resolveCommentElement(comment);
|
|
3051
|
-
if (!target || isElementTreeHidden(target))
|
|
3540
|
+
if (!target || isElementTreeHidden(target)) {
|
|
3541
|
+
openDetachedThread(comment);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3052
3544
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
3053
3545
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
3546
|
+
const rect = target.getBoundingClientRect();
|
|
3547
|
+
const relX = Number(comment.rel_x ?? comment.relX);
|
|
3548
|
+
const relY = Number(comment.rel_y ?? comment.relY);
|
|
3549
|
+
// Large page containers must reveal the annotation point, not their center.
|
|
3550
|
+
window.scrollBy({
|
|
3551
|
+
left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
|
|
3552
|
+
top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
|
|
3553
|
+
behavior: "instant",
|
|
3554
|
+
});
|
|
3054
3555
|
syncCommentAnchors();
|
|
3055
3556
|
const pin = findPlacedPin(comment);
|
|
3056
3557
|
if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
|
|
3558
|
+
else openDetachedThread(comment);
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
function openDetachedThread(comment) {
|
|
3562
|
+
openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
|
|
3563
|
+
const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
|
|
3564
|
+
if (bubble) {
|
|
3565
|
+
bubble.dataset.detached = "";
|
|
3566
|
+
positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
function updateAnchorBadge(item, comment) {
|
|
3571
|
+
resolveCommentElement(comment);
|
|
3572
|
+
const result = commentTargets.get(String(comment.id));
|
|
3573
|
+
const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
|
|
3574
|
+
const status = legacy && !result?.element ? "uncertain" : result?.status;
|
|
3575
|
+
const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
|
|
3576
|
+
let badge = item.querySelector(".comty-anchor-badge");
|
|
3577
|
+
if (!label) { badge?.remove(); return; }
|
|
3578
|
+
if (!badge) {
|
|
3579
|
+
badge = document.createElement("span");
|
|
3580
|
+
badge.className = "comty-anchor-badge";
|
|
3581
|
+
item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
|
|
3582
|
+
}
|
|
3583
|
+
if (badge.textContent !== label) badge.textContent = label;
|
|
3584
|
+
badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
|
|
3585
|
+
status === "missing" ? "The target is not present on this page. The comment is still available." : "The target cannot be identified reliably. No pin is placed.";
|
|
3057
3586
|
}
|
|
3058
3587
|
|
|
3059
3588
|
let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
|
|
@@ -3066,6 +3595,7 @@ export function initComty(options = {}) {
|
|
|
3066
3595
|
? sidepanelSearch.value.trim().toLowerCase()
|
|
3067
3596
|
: "";
|
|
3068
3597
|
let list = cachedComments.filter((comment) => {
|
|
3598
|
+
if (comment.local_agent || comment.kind === "agent") return false;
|
|
3069
3599
|
if (
|
|
3070
3600
|
search &&
|
|
3071
3601
|
!`${comment.content ?? ""} ${getCommentLabel(comment)}`
|
|
@@ -3112,6 +3642,7 @@ export function initComty(options = {}) {
|
|
|
3112
3642
|
const item = document.createElement("button");
|
|
3113
3643
|
item.type = "button";
|
|
3114
3644
|
item.className = "comty-sidepanel-item";
|
|
3645
|
+
item.dataset.commentId = String(comment.id);
|
|
3115
3646
|
item.disabled = Boolean(comment.pending);
|
|
3116
3647
|
item.innerHTML = `
|
|
3117
3648
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
@@ -3135,8 +3666,13 @@ export function initComty(options = {}) {
|
|
|
3135
3666
|
if (metaEl instanceof HTMLElement) {
|
|
3136
3667
|
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
3137
3668
|
metaEl.title = getCommentLabel(comment);
|
|
3669
|
+
if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
|
|
3670
|
+
metaEl.innerText += " · Original page unknown";
|
|
3671
|
+
}
|
|
3138
3672
|
}
|
|
3139
3673
|
|
|
3674
|
+
updateAnchorBadge(item, comment);
|
|
3675
|
+
|
|
3140
3676
|
item.onclick = (event) => {
|
|
3141
3677
|
event.stopPropagation();
|
|
3142
3678
|
openCommentFromPanel(comment);
|
|
@@ -3206,7 +3742,7 @@ export function initComty(options = {}) {
|
|
|
3206
3742
|
if (!(pin instanceof HTMLElement)) return;
|
|
3207
3743
|
const status = pin.dataset.status;
|
|
3208
3744
|
const shouldShow =
|
|
3209
|
-
!hasFilters || Boolean(status && selectedStatusFilters.has(status));
|
|
3745
|
+
pin.hasAttribute('data-agent-task') || !hasFilters || Boolean(status && selectedStatusFilters.has(status));
|
|
3210
3746
|
pin.classList.toggle("comty-filtered-out", !shouldShow);
|
|
3211
3747
|
});
|
|
3212
3748
|
}
|
|
@@ -3252,14 +3788,15 @@ export function initComty(options = {}) {
|
|
|
3252
3788
|
createStatusFilters();
|
|
3253
3789
|
|
|
3254
3790
|
async function updateCommentStatus(comment, status) {
|
|
3255
|
-
if (!projectId || !comment?.id) return false;
|
|
3791
|
+
if (comment?.local_agent || !projectId || !comment?.id) return false;
|
|
3256
3792
|
|
|
3257
3793
|
let res;
|
|
3258
3794
|
try {
|
|
3259
|
-
|
|
3795
|
+
const local = comment.local_agent === true;
|
|
3796
|
+
res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
|
|
3260
3797
|
method: "POST",
|
|
3261
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3262
|
-
body: JSON.stringify({
|
|
3798
|
+
headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3799
|
+
body: JSON.stringify(local ? { status } : {
|
|
3263
3800
|
projectKey,
|
|
3264
3801
|
host: window.location.host,
|
|
3265
3802
|
status,
|
|
@@ -3276,7 +3813,7 @@ export function initComty(options = {}) {
|
|
|
3276
3813
|
}
|
|
3277
3814
|
|
|
3278
3815
|
const data = await res.json();
|
|
3279
|
-
comment.status = getCommentStatus(data.status).value;
|
|
3816
|
+
comment.status = getCommentStatus(data.task?.status ?? data.status).value;
|
|
3280
3817
|
return comment.status;
|
|
3281
3818
|
}
|
|
3282
3819
|
|
|
@@ -3468,6 +4005,27 @@ export function initComty(options = {}) {
|
|
|
3468
4005
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
3469
4006
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
3470
4007
|
showAuthPrompt,
|
|
4008
|
+
startThreadAgent: async (comment, replies, content) => {
|
|
4009
|
+
if (!agent) throw new Error('Start the local agent to reply with @Agent');
|
|
4010
|
+
if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
|
|
4011
|
+
const transcript = [
|
|
4012
|
+
`${getCommentAuthorName(comment)}: ${comment.content}`,
|
|
4013
|
+
...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
|
|
4014
|
+
].join('\n').slice(-24000);
|
|
4015
|
+
const response = await fetch(`${agentBridgeUrl}/tasks`, {
|
|
4016
|
+
method: 'POST', headers: agentHeaders(),
|
|
4017
|
+
body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
|
|
4018
|
+
selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
|
|
4019
|
+
domElement: comment.dom_element || '', viewport: comment.viewport || '',
|
|
4020
|
+
anchorContext: comment.anchor_context || '', scope: 'element',
|
|
4021
|
+
thread: { commentId: String(comment.id), transcript } }),
|
|
4022
|
+
});
|
|
4023
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
|
|
4024
|
+
const data = await response.json();
|
|
4025
|
+
if (!data.task?.id) throw new Error('Agent returned no task ID');
|
|
4026
|
+
requestedAgentTasks.add(data.task.id);
|
|
4027
|
+
void loadComments({ force: true });
|
|
4028
|
+
},
|
|
3471
4029
|
get canWrite() { return canWrite; },
|
|
3472
4030
|
get canManage() { return canManage; },
|
|
3473
4031
|
get allowGuestComments() { return allowGuestComments; },
|
|
@@ -3582,6 +4140,7 @@ export function initComty(options = {}) {
|
|
|
3582
4140
|
|
|
3583
4141
|
pin.onclick = (event) => {
|
|
3584
4142
|
event.stopPropagation();
|
|
4143
|
+
if (comment.local_agent) { openAgentTask(comment, getPinPoint(pin).x, getPinPoint(pin).y); return; }
|
|
3585
4144
|
openCommentThread(
|
|
3586
4145
|
comment,
|
|
3587
4146
|
pin,
|
|
@@ -3668,42 +4227,122 @@ export function initComty(options = {}) {
|
|
|
3668
4227
|
function reconcileComments(comments) {
|
|
3669
4228
|
const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
|
|
3670
4229
|
cachedComments = comments
|
|
3671
|
-
.filter(comment => comment.kind !== "agent"
|
|
4230
|
+
.filter(comment => comment.kind !== "agent" || comment.status !== "done")
|
|
3672
4231
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
3673
4232
|
for (const comment of previous.values()) {
|
|
3674
4233
|
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
3675
4234
|
}
|
|
3676
4235
|
}
|
|
3677
4236
|
|
|
3678
|
-
async function
|
|
3679
|
-
if (
|
|
3680
|
-
|
|
4237
|
+
async function deliverThreadReply(task) {
|
|
4238
|
+
if (deliveringAgentReplies.has(task.id)) return;
|
|
4239
|
+
deliveringAgentReplies.add(task.id);
|
|
4240
|
+
try {
|
|
4241
|
+
const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
|
|
4242
|
+
method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
4243
|
+
body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
|
|
4244
|
+
});
|
|
4245
|
+
if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
|
|
4246
|
+
const { reply } = await response.json();
|
|
4247
|
+
const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
|
|
4248
|
+
if (local && !local.replies?.some(item => item.id === reply.id)) {
|
|
4249
|
+
local.replies ||= [];
|
|
4250
|
+
local.replies.push(reply);
|
|
4251
|
+
}
|
|
4252
|
+
for (const bubble of root.querySelectorAll('.comty-bubble')) {
|
|
4253
|
+
if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
|
|
4254
|
+
}
|
|
4255
|
+
const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
|
|
4256
|
+
if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
|
|
4257
|
+
localTasks = localTasks.filter(item => item.id !== task.id);
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
agentActionError = error.message;
|
|
4260
|
+
console.error('Comty: Agent reply delivery failed', error);
|
|
4261
|
+
} finally {
|
|
4262
|
+
deliveringAgentReplies.delete(task.id);
|
|
4263
|
+
agentPanelSignature = '';
|
|
4264
|
+
renderAgentPanel();
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
async function loadComments({ force = false, tasksOnly = false } = {}) {
|
|
4269
|
+
if (!projectId && !agent) return;
|
|
4270
|
+
const pagePath = getPagePath();
|
|
3681
4271
|
if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
|
|
3682
4272
|
if (pendingCommentsLoad) {
|
|
3683
4273
|
await pendingCommentsLoad;
|
|
3684
|
-
if (!force) return;
|
|
4274
|
+
if (!force && cachedPagePath === pagePath) return;
|
|
3685
4275
|
}
|
|
3686
4276
|
pendingCommentsLoad = (async () => {
|
|
3687
|
-
let comments = cachedPagePath === pagePath ? cachedComments : [];
|
|
3688
|
-
if (projectId) {
|
|
4277
|
+
let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
|
|
4278
|
+
if (projectId && !tasksOnly) {
|
|
3689
4279
|
try {
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
4280
|
+
const paths = [...new Set([pagePath, window.location.pathname])];
|
|
4281
|
+
const batches = await Promise.all(paths.map(async path => {
|
|
4282
|
+
const url = new URL(`${apiUrl}/api/comments`);
|
|
4283
|
+
url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
|
|
4284
|
+
const res = await fetch(url);
|
|
4285
|
+
if (!res.ok) throw new Error(await res.text());
|
|
4286
|
+
const data = await res.json();
|
|
4287
|
+
return Array.isArray(data.comments) ? data.comments : [];
|
|
4288
|
+
}));
|
|
4289
|
+
comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
|
|
3696
4290
|
} catch (error) { console.error("Comty: list comments failed", error); }
|
|
3697
4291
|
}
|
|
4292
|
+
if (agent) {
|
|
4293
|
+
try {
|
|
4294
|
+
const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
|
|
4295
|
+
if (!res.ok) throw new Error("Local companion unavailable");
|
|
4296
|
+
const data = await res.json();
|
|
4297
|
+
if (Array.isArray(data.tasks)) localTasks = data.tasks;
|
|
4298
|
+
const failedTasks = localTasks.filter(task => task.status === 'in_review');
|
|
4299
|
+
for (const task of failedTasks.filter(task => requestedAgentTasks.has(task.id))) {
|
|
4300
|
+
if (task.error) showAgentBridgeError(task.error);
|
|
4301
|
+
else showNotice({ title: 'Modification failed', description: 'The agent needs clarification.' });
|
|
4302
|
+
}
|
|
4303
|
+
if (Array.isArray(data.approvals)) {
|
|
4304
|
+
localApprovals = data.approvals;
|
|
4305
|
+
if (localApprovals.some(approval => requestedAgentTasks.has(approval.taskId) && !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
|
|
4306
|
+
seenAgentApprovals.clear();
|
|
4307
|
+
for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
|
|
4308
|
+
}
|
|
4309
|
+
for (const id of requestedAgentTasks) {
|
|
4310
|
+
const task = localTasks.find(task => task.id === id);
|
|
4311
|
+
if (!task || ['done', 'cancel', 'in_review'].includes(task.status)) requestedAgentTasks.delete(id);
|
|
4312
|
+
}
|
|
4313
|
+
agentConnectionError = "";
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
|
|
4316
|
+
console.error("Comty: local tasks failed", error);
|
|
4317
|
+
}
|
|
4318
|
+
for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
|
|
4319
|
+
void deliverThreadReply(task);
|
|
4320
|
+
}
|
|
4321
|
+
comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && task.status === 'in_review'));
|
|
4322
|
+
renderAgentPanel();
|
|
4323
|
+
}
|
|
4324
|
+
if (pagePath !== getPagePath()) return;
|
|
3698
4325
|
reconcileComments(comments);
|
|
3699
4326
|
cachedPagePath = pagePath;
|
|
3700
4327
|
renderSidepanelComments();
|
|
3701
4328
|
await renderComments(cachedComments);
|
|
4329
|
+
syncAgentWorkingOutlines();
|
|
3702
4330
|
ensureLiveTracking();
|
|
3703
4331
|
})();
|
|
3704
4332
|
try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
|
|
3705
4333
|
}
|
|
3706
4334
|
|
|
4335
|
+
if (agent) {
|
|
4336
|
+
setInterval(() => {
|
|
4337
|
+
const active = requestedAgentTasks.size || localTasks.some(task =>
|
|
4338
|
+
['todo', 'in_progress'].includes(task.status) || (task.thread_comment_id && task.status === 'done' && task.result?.summary));
|
|
4339
|
+
if (active && !pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true, tasksOnly: true });
|
|
4340
|
+
}, 2000);
|
|
4341
|
+
document.addEventListener('visibilitychange', () => {
|
|
4342
|
+
if (document.visibilityState === 'visible' && !pendingCommentsLoad) void loadComments({ force: true, tasksOnly: true });
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
|
|
3707
4346
|
function getPinPoint(pin) {
|
|
3708
4347
|
return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
|
|
3709
4348
|
}
|
|
@@ -3731,7 +4370,7 @@ export function initComty(options = {}) {
|
|
|
3731
4370
|
}
|
|
3732
4371
|
|
|
3733
4372
|
function syncCommentAnchors() {
|
|
3734
|
-
if (!session.comments) return;
|
|
4373
|
+
if (!session.comments && !session.inspect) return;
|
|
3735
4374
|
const modal = getTopActiveModal();
|
|
3736
4375
|
for (const comment of cachedComments) {
|
|
3737
4376
|
const id = String(comment.id);
|
|
@@ -3742,7 +4381,7 @@ export function initComty(options = {}) {
|
|
|
3742
4381
|
placePin(comment, modal);
|
|
3743
4382
|
pin = placedPins.get(id);
|
|
3744
4383
|
}
|
|
3745
|
-
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
|
|
4384
|
+
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
|
|
3746
4385
|
// A pin can remove its target's :hover style as the pointer enters it.
|
|
3747
4386
|
if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
|
|
3748
4387
|
(activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
|
|
@@ -3757,6 +4396,10 @@ export function initComty(options = {}) {
|
|
|
3757
4396
|
if (pin.style.top !== y) pin.style.top = y;
|
|
3758
4397
|
for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
|
|
3759
4398
|
}
|
|
4399
|
+
if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
|
|
4400
|
+
const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
|
|
4401
|
+
if (comment) updateAnchorBadge(item, comment);
|
|
4402
|
+
}
|
|
3760
4403
|
if (draftAnchor) {
|
|
3761
4404
|
if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
|
|
3762
4405
|
const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
|
|
@@ -3770,16 +4413,50 @@ export function initComty(options = {}) {
|
|
|
3770
4413
|
}
|
|
3771
4414
|
}
|
|
3772
4415
|
|
|
4416
|
+
function syncAgentWorkingOutlines() {
|
|
4417
|
+
const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
|
|
4418
|
+
const ids = new Set(working.map(task => String(task.id)));
|
|
4419
|
+
for (const [id, outline] of workingOutlines) {
|
|
4420
|
+
if (ids.has(id)) continue;
|
|
4421
|
+
outline.remove(); workingOutlines.delete(id);
|
|
4422
|
+
}
|
|
4423
|
+
const modal = getTopActiveModal();
|
|
4424
|
+
for (const task of working) {
|
|
4425
|
+
const id = String(task.id);
|
|
4426
|
+
let outline = workingOutlines.get(id);
|
|
4427
|
+
if (!outline) {
|
|
4428
|
+
outline = document.createElement("div");
|
|
4429
|
+
outline.className = "comty-agent-working";
|
|
4430
|
+
outline.setAttribute("aria-label", "Agent working on this element");
|
|
4431
|
+
const badge = document.createElement("span");
|
|
4432
|
+
badge.className = "comty-agent-working-badge";
|
|
4433
|
+
badge.append(createWorkingIndicator(), "Working");
|
|
4434
|
+
outline.appendChild(badge); root.appendChild(outline);
|
|
4435
|
+
workingOutlines.set(id, outline);
|
|
4436
|
+
}
|
|
4437
|
+
const target = resolveCommentElement(task);
|
|
4438
|
+
const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
|
|
4439
|
+
outline.hidden = !point;
|
|
4440
|
+
if (!point) continue;
|
|
4441
|
+
const rect = target.getBoundingClientRect();
|
|
4442
|
+
setMeasuredRect(outline, rect);
|
|
4443
|
+
outline.toggleAttribute("data-badge-below", rect.top < 32);
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
|
|
3773
4447
|
function onFrame() {
|
|
3774
4448
|
if (motion.enabled) updateMotion();
|
|
3775
|
-
if (session.comments) syncCommentAnchors();
|
|
4449
|
+
if (session.comments || session.inspect) syncCommentAnchors();
|
|
4450
|
+
if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
|
|
4451
|
+
syncAgentWorkingOutlines();
|
|
4452
|
+
}
|
|
3776
4453
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
3777
4454
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
3778
4455
|
if (session.inspect && rulersEnabled) measurements.render();
|
|
3779
4456
|
}
|
|
3780
4457
|
|
|
3781
4458
|
function ensureLiveTracking() {
|
|
3782
|
-
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
|
|
4459
|
+
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
|
|
3783
4460
|
if (!active || document.hidden) {
|
|
3784
4461
|
if (liveFrame) cancelAnimationFrame(liveFrame);
|
|
3785
4462
|
liveFrame = null;
|
|
@@ -3799,13 +4476,14 @@ export function initComty(options = {}) {
|
|
|
3799
4476
|
syncOverlayHost();
|
|
3800
4477
|
|
|
3801
4478
|
function renderCachedComments() {
|
|
3802
|
-
if (!session.comments && !session.inspect && !session.edit) return;
|
|
4479
|
+
if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
|
|
3803
4480
|
if (resizeRenderFrame) {
|
|
3804
4481
|
window.cancelAnimationFrame(resizeRenderFrame);
|
|
3805
4482
|
}
|
|
3806
4483
|
resizeRenderFrame = window.requestAnimationFrame(() => {
|
|
3807
4484
|
resizeRenderFrame = null;
|
|
3808
4485
|
syncCommentAnchors();
|
|
4486
|
+
syncAgentWorkingOutlines();
|
|
3809
4487
|
renderInspector();
|
|
3810
4488
|
ensureLiveTracking();
|
|
3811
4489
|
});
|
|
@@ -3847,6 +4525,31 @@ export function initComty(options = {}) {
|
|
|
3847
4525
|
characterData: true,
|
|
3848
4526
|
});
|
|
3849
4527
|
|
|
4528
|
+
let activePagePath = getPagePath();
|
|
4529
|
+
function handleRouteChange() {
|
|
4530
|
+
const next = getPagePath();
|
|
4531
|
+
if (next === activePagePath) return;
|
|
4532
|
+
activePagePath = next;
|
|
4533
|
+
markup.clear();
|
|
4534
|
+
markup.setMode(null);
|
|
4535
|
+
closeBubbles();
|
|
4536
|
+
commentTargets.clear();
|
|
4537
|
+
reconcileComments([]);
|
|
4538
|
+
void renderComments([], { retry: false });
|
|
4539
|
+
renderSidepanelComments();
|
|
4540
|
+
void loadComments({ force: true });
|
|
4541
|
+
}
|
|
4542
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
4543
|
+
window.addEventListener("popstate", handleRouteChange);
|
|
4544
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
4545
|
+
const original = history[method];
|
|
4546
|
+
history[method] = function (...args) {
|
|
4547
|
+
const result = original.apply(this, args);
|
|
4548
|
+
handleRouteChange();
|
|
4549
|
+
return result;
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
4552
|
+
|
|
3850
4553
|
document.addEventListener("scroll", handleHostLayoutEvent, {
|
|
3851
4554
|
capture: true,
|
|
3852
4555
|
passive: true,
|
|
@@ -3865,7 +4568,7 @@ export function initComty(options = {}) {
|
|
|
3865
4568
|
|
|
3866
4569
|
document.addEventListener("pointermove", (event) => {
|
|
3867
4570
|
if (!session.edit && !session.inspect) return;
|
|
3868
|
-
if (motion.enabled) return;
|
|
4571
|
+
if (motion.enabled || markup.mode) return;
|
|
3869
4572
|
if (propertyPicking) {
|
|
3870
4573
|
const target = event.target;
|
|
3871
4574
|
if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
|
|
@@ -3984,13 +4687,14 @@ export function initComty(options = {}) {
|
|
|
3984
4687
|
const y = e.clientY;
|
|
3985
4688
|
|
|
3986
4689
|
const anchorContext = getAnchorContext(el);
|
|
4690
|
+
if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
|
|
3987
4691
|
|
|
3988
4692
|
const payload = {
|
|
3989
4693
|
...getDeviceContext(),
|
|
3990
4694
|
projectId,
|
|
3991
|
-
kind: "comment",
|
|
4695
|
+
kind: session.agent ? "agent" : "comment",
|
|
3992
4696
|
host: window.location.host,
|
|
3993
|
-
pagePath:
|
|
4697
|
+
pagePath: getPagePath(),
|
|
3994
4698
|
selector: getSelector(el),
|
|
3995
4699
|
relX,
|
|
3996
4700
|
relY,
|
|
@@ -4011,6 +4715,12 @@ export function initComty(options = {}) {
|
|
|
4011
4715
|
(target.isContentEditable ||
|
|
4012
4716
|
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
|
|
4013
4717
|
|
|
4718
|
+
const markupMode = markup.mode;
|
|
4719
|
+
if (session.inspect && (e.key === "Escape" || !isEditable) && markup.keydown(e)) {
|
|
4720
|
+
e.preventDefault();
|
|
4721
|
+
if (!markup.mode) (markupMode === "annotate" ? annotateBtn : screenshotBtn).focus({ preventScroll: true });
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4014
4724
|
if (e.key === "Escape" && motion.enabled) {
|
|
4015
4725
|
e.preventDefault();
|
|
4016
4726
|
setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
|
|
@@ -4025,7 +4735,7 @@ export function initComty(options = {}) {
|
|
|
4025
4735
|
return;
|
|
4026
4736
|
}
|
|
4027
4737
|
if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
|
4028
|
-
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn }[e.key.toLowerCase()];
|
|
4738
|
+
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn, d: annotateBtn, s: screenshotBtn }[e.key.toLowerCase()];
|
|
4029
4739
|
if (button) { e.preventDefault(); button.click(); return; }
|
|
4030
4740
|
}
|
|
4031
4741
|
|
|
@@ -4033,6 +4743,11 @@ export function initComty(options = {}) {
|
|
|
4033
4743
|
measureAlt = true;
|
|
4034
4744
|
renderInspector();
|
|
4035
4745
|
}
|
|
4746
|
+
if (isLocalSite && !isEditable && !e.repeat && !e.isComposing && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "a") {
|
|
4747
|
+
e.preventDefault();
|
|
4748
|
+
startAgentSession();
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4036
4751
|
if (!isEditable && !e.repeat && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "i") {
|
|
4037
4752
|
e.preventDefault();
|
|
4038
4753
|
setSession(session.inspect ? "off" : "inspect");
|
|
@@ -4088,6 +4803,7 @@ export function initComty(options = {}) {
|
|
|
4088
4803
|
window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
|
|
4089
4804
|
|
|
4090
4805
|
async function boot() {
|
|
4806
|
+
if (localOnly) { await loadComments(); return; }
|
|
4091
4807
|
const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
|
|
4092
4808
|
method: "POST",
|
|
4093
4809
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
@@ -4100,6 +4816,7 @@ export function initComty(options = {}) {
|
|
|
4100
4816
|
|
|
4101
4817
|
if (!res.ok) {
|
|
4102
4818
|
console.error("Comty: bootstrap failed", await res.text());
|
|
4819
|
+
if (agent) await loadComments();
|
|
4103
4820
|
return;
|
|
4104
4821
|
}
|
|
4105
4822
|
|
|
@@ -4144,16 +4861,30 @@ export function initComty(options = {}) {
|
|
|
4144
4861
|
|
|
4145
4862
|
if (typeof options.onTestReady === "function") {
|
|
4146
4863
|
options.onTestReady({
|
|
4147
|
-
getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
4864
|
+
getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
4148
4865
|
getDistanceSegments, getInspectRows, syncCommentAnchors,
|
|
4149
4866
|
setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
|
|
4150
4867
|
startEditSession, startBrowseSession, endSession, showInspector,
|
|
4151
|
-
motion, setMotionEnabled, updateMotion,
|
|
4868
|
+
motion, setMotionEnabled, updateMotion, describeAgentError,
|
|
4869
|
+
previewAgent(tasks, approvals = [], error = "") {
|
|
4870
|
+
localTasks = tasks;
|
|
4871
|
+
localApprovals = approvals;
|
|
4872
|
+
agentConnectionError = error;
|
|
4873
|
+
startAgentSession();
|
|
4874
|
+
agentPanelOpen = true;
|
|
4875
|
+
agentPanelSignature = "";
|
|
4876
|
+
renderAgentPanel();
|
|
4877
|
+
},
|
|
4152
4878
|
seed(comments) {
|
|
4153
4879
|
reconcileComments(comments);
|
|
4154
4880
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
4155
4881
|
return renderComments(cachedComments, { retry: false });
|
|
4156
4882
|
},
|
|
4883
|
+
seedAgentTasks(tasks) {
|
|
4884
|
+
localTasks = tasks;
|
|
4885
|
+
syncAgentWorkingOutlines();
|
|
4886
|
+
ensureLiveTracking();
|
|
4887
|
+
},
|
|
4157
4888
|
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
4158
4889
|
invalidate() { anchorRevision += 1; },
|
|
4159
4890
|
});
|
|
@@ -4161,6 +4892,7 @@ export function initComty(options = {}) {
|
|
|
4161
4892
|
|
|
4162
4893
|
return boot().catch(error => {
|
|
4163
4894
|
console.error("Comty: bootstrap failed", error);
|
|
4895
|
+
if (agent) return loadComments();
|
|
4164
4896
|
});
|
|
4165
4897
|
}
|
|
4166
4898
|
}
|