@comty.io/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -3
- package/comty-sdk.js +849 -124
- package/package.json +16 -5
- 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 +658 -79
- package/src/capture-engine.js +1 -1
- package/src/capture.js +6 -4
- package/src/claude-permission.mjs +37 -0
- package/src/claude.mjs +120 -0
- package/src/codex.mjs +66 -0
- package/src/comments.js +31 -11
- package/src/companion.mjs +201 -0
- package/src/cursor.mjs +49 -0
- package/src/dev.mjs +171 -0
- package/src/gemini.mjs +3 -0
- package/src/grok.mjs +3 -0
- package/src/style.js +95 -12
- package/src/vite.d.ts +5 -0
- package/src/vite.mjs +68 -0
package/src/browser.js
CHANGED
|
@@ -9,7 +9,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
|
|
|
9
9
|
/** Initialize once per document; safe to import and call during server rendering. */
|
|
10
10
|
export function initComty(options = {}) {
|
|
11
11
|
if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
|
|
12
|
-
const
|
|
12
|
+
const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
|
|
13
|
+
const agent = options.agent;
|
|
14
|
+
if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
|
|
15
|
+
return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
|
|
16
|
+
}
|
|
17
|
+
if (agent) {
|
|
18
|
+
try {
|
|
19
|
+
if (new URL(agent.url).origin !== window.location.origin) throw new Error();
|
|
20
|
+
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
21
|
+
}
|
|
22
|
+
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
13
23
|
const apiUrl = options.apiUrl || "http://localhost:4000";
|
|
14
24
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
15
25
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
@@ -21,12 +31,14 @@ export function initComty(options = {}) {
|
|
|
21
31
|
if (!["http:", "https:"].includes(parsedApi.protocol)) {
|
|
22
32
|
return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
|
|
23
33
|
}
|
|
24
|
-
const configuration = JSON.stringify([projectKey, apiUrl]);
|
|
34
|
+
const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
|
|
25
35
|
const existing = window[instanceKey];
|
|
26
36
|
if (existing) {
|
|
27
37
|
return existing.configuration === configuration ? existing.ready
|
|
28
38
|
: Promise.reject(new Error("Comty: already initialized with different options"));
|
|
29
39
|
}
|
|
40
|
+
const agentBridgeUrl = agent?.url;
|
|
41
|
+
const localOnly = Boolean(agent && !options.projectKey);
|
|
30
42
|
const ready = new Promise((resolve, reject) => {
|
|
31
43
|
const mount = () => {
|
|
32
44
|
try { resolve(mountComty()); } catch (error) { reject(error); }
|
|
@@ -40,10 +52,22 @@ export function initComty(options = {}) {
|
|
|
40
52
|
|
|
41
53
|
function mountComty() {
|
|
42
54
|
let projectId = null;
|
|
55
|
+
let localTasks = [];
|
|
56
|
+
const deliveringAgentReplies = new Set();
|
|
57
|
+
let localApprovals = [];
|
|
58
|
+
const seenAgentApprovals = new Set();
|
|
59
|
+
const seenFailedAgentTasks = new Set();
|
|
60
|
+
let agentPanelOpen = false;
|
|
61
|
+
let agentPanelSignature = "";
|
|
62
|
+
let agentPanelTransition = 0;
|
|
63
|
+
let agentConnectionError = "";
|
|
64
|
+
let agentActionError = "";
|
|
65
|
+
const agentPendingActions = new Set();
|
|
43
66
|
const session = {
|
|
44
67
|
mode: "off",
|
|
45
|
-
get comments() { return this.mode === "browse" || this.mode === "edit"; },
|
|
46
|
-
get edit() { return this.mode === "edit"; },
|
|
68
|
+
get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
|
|
69
|
+
get edit() { return this.mode === "edit" || this.mode === "agent"; },
|
|
70
|
+
get agent() { return this.mode === "agent"; },
|
|
47
71
|
get inspect() { return this.mode === "inspect"; },
|
|
48
72
|
};
|
|
49
73
|
const selectedStatusFilters = new Set();
|
|
@@ -67,9 +91,10 @@ export function initComty(options = {}) {
|
|
|
67
91
|
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
68
92
|
let sdkToken = "";
|
|
69
93
|
const activeHoverCommentIds = new Set();
|
|
70
|
-
/** @type {null | 'edit' | 'browse'} */
|
|
94
|
+
/** @type {null | 'edit' | 'browse' | 'agent'} */
|
|
71
95
|
let sessionEntry = null;
|
|
72
96
|
const placedPins = new Map();
|
|
97
|
+
const workingOutlines = new Map();
|
|
73
98
|
const placementDiagnostics = new Map();
|
|
74
99
|
let inspectedElement = null;
|
|
75
100
|
let selectedElement = null;
|
|
@@ -83,8 +108,8 @@ export function initComty(options = {}) {
|
|
|
83
108
|
const commentTargets = new Map();
|
|
84
109
|
|
|
85
110
|
const {
|
|
86
|
-
matchesAnchorContext,
|
|
87
|
-
isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
111
|
+
matchesAnchorContext,
|
|
112
|
+
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
88
113
|
} = createAnchorMatcher({
|
|
89
114
|
getElementTextSignature,
|
|
90
115
|
getElementOwnTextSignature,
|
|
@@ -162,10 +187,20 @@ export function initComty(options = {}) {
|
|
|
162
187
|
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
188
|
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
189
|
if (!canWrite) showAuthPrompt();
|
|
165
|
-
else if (session.inspect || !isSessionActive()) startEditSession();
|
|
190
|
+
else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
|
|
166
191
|
else if (!session.edit) setSession("edit", { entry: sessionEntry });
|
|
167
192
|
});
|
|
168
193
|
btn.id = "comty-toggle";
|
|
194
|
+
const agentBtn = isLocalSite
|
|
195
|
+
? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
|
|
196
|
+
if (session.agent) {
|
|
197
|
+
if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
|
|
198
|
+
agentPanelOpen = true;
|
|
199
|
+
renderAgentPanel();
|
|
200
|
+
}
|
|
201
|
+
} else startAgentSession();
|
|
202
|
+
})
|
|
203
|
+
: null;
|
|
169
204
|
const divider = document.createElement("span");
|
|
170
205
|
divider.className = "comty-tool-divider";
|
|
171
206
|
toolbar.appendChild(divider);
|
|
@@ -175,7 +210,7 @@ export function initComty(options = {}) {
|
|
|
175
210
|
const primaryTools = document.createElement("div");
|
|
176
211
|
primaryTools.className = "comty-tool-primary";
|
|
177
212
|
toolbar.insertBefore(primaryTools, browseBtn);
|
|
178
|
-
for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
|
|
213
|
+
for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
|
|
179
214
|
const inspectorTools = document.createElement("div");
|
|
180
215
|
inspectorTools.className = "comty-tool-context";
|
|
181
216
|
inspectorTools.hidden = true;
|
|
@@ -1028,6 +1063,50 @@ export function initComty(options = {}) {
|
|
|
1028
1063
|
let toolbarMotionFrame = 0;
|
|
1029
1064
|
let toolbarAnimations = [];
|
|
1030
1065
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1066
|
+
let workingIndicatorTimer = 0;
|
|
1067
|
+
function scheduleWorkingIndicators() {
|
|
1068
|
+
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
1069
|
+
workingIndicatorTimer = setTimeout(() => {
|
|
1070
|
+
workingIndicatorTimer = 0;
|
|
1071
|
+
if (!root.isConnected) return;
|
|
1072
|
+
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
1073
|
+
if (!indicators.length) return;
|
|
1074
|
+
for (const indicator of indicators) {
|
|
1075
|
+
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
1076
|
+
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1077
|
+
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1078
|
+
indicator.dataset.activeDots = `${first},${second}`;
|
|
1079
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
1080
|
+
}
|
|
1081
|
+
scheduleWorkingIndicators();
|
|
1082
|
+
}, 600);
|
|
1083
|
+
}
|
|
1084
|
+
function createWorkingIndicator() {
|
|
1085
|
+
const indicator = document.createElement('span');
|
|
1086
|
+
indicator.className = 'comty-working-indicator';
|
|
1087
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
1088
|
+
indicator.dataset.activeDots = '3,5';
|
|
1089
|
+
for (let index = 0; index < 9; index++) {
|
|
1090
|
+
const dot = document.createElement('i');
|
|
1091
|
+
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
1092
|
+
indicator.appendChild(dot);
|
|
1093
|
+
}
|
|
1094
|
+
scheduleWorkingIndicators();
|
|
1095
|
+
return indicator;
|
|
1096
|
+
}
|
|
1097
|
+
function resetWorkingIndicators() {
|
|
1098
|
+
clearTimeout(workingIndicatorTimer);
|
|
1099
|
+
workingIndicatorTimer = 0;
|
|
1100
|
+
if (reducedMotion.matches) {
|
|
1101
|
+
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
1102
|
+
indicator.dataset.activeDots = '3,5';
|
|
1103
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
1107
|
+
}
|
|
1108
|
+
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
1109
|
+
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
1031
1110
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
1032
1111
|
function rememberToolbarPosition(left, top) {
|
|
1033
1112
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
@@ -1140,7 +1219,7 @@ export function initComty(options = {}) {
|
|
|
1140
1219
|
|
|
1141
1220
|
function prepareToolbarMotion() {
|
|
1142
1221
|
if (toolbarMotionFrame) return;
|
|
1143
|
-
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools]
|
|
1222
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
|
|
1144
1223
|
// Move each group's background with its icons, including when interrupted.
|
|
1145
1224
|
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
1146
1225
|
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
@@ -1364,7 +1443,8 @@ export function initComty(options = {}) {
|
|
|
1364
1443
|
selectedElement = null;
|
|
1365
1444
|
sessionEntry = null;
|
|
1366
1445
|
session.mode = "off";
|
|
1367
|
-
|
|
1446
|
+
syncPlacementCursor();
|
|
1447
|
+
document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
|
|
1368
1448
|
hideInspector();
|
|
1369
1449
|
setSidepanelOpen(false);
|
|
1370
1450
|
closeBubbles();
|
|
@@ -1378,25 +1458,29 @@ export function initComty(options = {}) {
|
|
|
1378
1458
|
if (mode === "off") return;
|
|
1379
1459
|
if (mode === "inspect") {
|
|
1380
1460
|
session.mode = "inspect";
|
|
1461
|
+
document.documentElement.classList.add("comty-inspect-annotations");
|
|
1381
1462
|
syncToolbar();
|
|
1463
|
+
renderCachedComments();
|
|
1382
1464
|
ensureLiveTracking();
|
|
1383
1465
|
return;
|
|
1384
1466
|
}
|
|
1385
1467
|
|
|
1386
|
-
if (session.edit && mode === "browse") {
|
|
1468
|
+
if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
|
|
1387
1469
|
closeBubbles();
|
|
1388
1470
|
}
|
|
1389
1471
|
const hadComments = session.comments;
|
|
1390
1472
|
session.mode = mode;
|
|
1391
1473
|
sessionEntry = entry;
|
|
1474
|
+
document.documentElement.classList.remove("comty-inspect-annotations");
|
|
1392
1475
|
document.documentElement.classList.toggle("comty-comments-active", session.comments);
|
|
1393
|
-
|
|
1476
|
+
syncPlacementCursor();
|
|
1394
1477
|
if (session.edit) {
|
|
1395
1478
|
setMotionEnabled(false);
|
|
1396
1479
|
selectedElement = null;
|
|
1397
1480
|
} else {
|
|
1398
1481
|
hideInspector();
|
|
1399
1482
|
}
|
|
1483
|
+
if (session.agent && sidepanelOpen) setSidepanelOpen(false);
|
|
1400
1484
|
syncToolbar();
|
|
1401
1485
|
ensureLiveTracking();
|
|
1402
1486
|
if (renderComments || !hadComments) renderCachedComments();
|
|
@@ -1409,6 +1493,10 @@ export function initComty(options = {}) {
|
|
|
1409
1493
|
}
|
|
1410
1494
|
|
|
1411
1495
|
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
1496
|
+
if (localOnly) {
|
|
1497
|
+
showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1412
1500
|
const guestAllowed = allowGuestComments;
|
|
1413
1501
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
1414
1502
|
const opener = document.activeElement;
|
|
@@ -1530,14 +1618,16 @@ export function initComty(options = {}) {
|
|
|
1530
1618
|
}
|
|
1531
1619
|
|
|
1532
1620
|
function syncToolbar() {
|
|
1533
|
-
if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
1621
|
+
if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
1534
1622
|
root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
|
|
1535
1623
|
browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
|
|
1536
1624
|
inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
|
|
1537
|
-
btn.setAttribute("aria-pressed", String(session.edit));
|
|
1538
|
-
|
|
1625
|
+
btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
|
|
1626
|
+
agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
|
|
1627
|
+
commentTools.hidden = session.inspect || session.agent;
|
|
1539
1628
|
inspectorTools.hidden = !session.inspect;
|
|
1540
1629
|
syncInspectorTools();
|
|
1630
|
+
renderAgentPanel();
|
|
1541
1631
|
}
|
|
1542
1632
|
|
|
1543
1633
|
const inspectionLayer = document.createElement("div");
|
|
@@ -2000,6 +2090,13 @@ export function initComty(options = {}) {
|
|
|
2000
2090
|
setSession("edit", { renderComments: true });
|
|
2001
2091
|
}
|
|
2002
2092
|
|
|
2093
|
+
function startAgentSession() {
|
|
2094
|
+
if (!isLocalSite) return;
|
|
2095
|
+
if (session.agent) return;
|
|
2096
|
+
closeBubbles();
|
|
2097
|
+
setSession("agent", { renderComments: true });
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2003
2100
|
function toggleSidepanel() {
|
|
2004
2101
|
if (sidepanelOpen) {
|
|
2005
2102
|
setSidepanelOpen(false);
|
|
@@ -2023,7 +2120,7 @@ export function initComty(options = {}) {
|
|
|
2023
2120
|
showAuthPrompt();
|
|
2024
2121
|
return;
|
|
2025
2122
|
}
|
|
2026
|
-
if (session.inspect || !isSessionActive()) {
|
|
2123
|
+
if (session.agent || session.inspect || !isSessionActive()) {
|
|
2027
2124
|
startEditSession();
|
|
2028
2125
|
return;
|
|
2029
2126
|
}
|
|
@@ -2311,6 +2408,24 @@ export function initComty(options = {}) {
|
|
|
2311
2408
|
ancestors,
|
|
2312
2409
|
layer: layer ? getElementFingerprint(layer) : null,
|
|
2313
2410
|
};
|
|
2411
|
+
// React exposes this in development builds. Keep it as a hint: the coding
|
|
2412
|
+
// agent must confirm the component and variant in source before editing.
|
|
2413
|
+
const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
|
|
2414
|
+
let fiber = fiberKey ? el[fiberKey] : null;
|
|
2415
|
+
const components = [];
|
|
2416
|
+
while (fiber && components.length < 8) {
|
|
2417
|
+
const component = fiber.type;
|
|
2418
|
+
const name = component?.displayName || component?.name;
|
|
2419
|
+
if (name && typeof name === 'string') {
|
|
2420
|
+
const props = fiber.memoizedProps || {};
|
|
2421
|
+
const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
|
|
2422
|
+
['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
|
|
2423
|
+
const source = fiber._debugSource;
|
|
2424
|
+
components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
|
|
2425
|
+
}
|
|
2426
|
+
fiber = fiber.return;
|
|
2427
|
+
}
|
|
2428
|
+
if (components.length) context.components = components;
|
|
2314
2429
|
context.candidateCount = [...document.querySelectorAll(context.target.tag)]
|
|
2315
2430
|
.filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
|
|
2316
2431
|
return context;
|
|
@@ -2339,6 +2454,11 @@ export function initComty(options = {}) {
|
|
|
2339
2454
|
};
|
|
2340
2455
|
}
|
|
2341
2456
|
|
|
2457
|
+
function getPagePath() {
|
|
2458
|
+
const { pathname, hash } = window.location;
|
|
2459
|
+
return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2342
2462
|
function resolveBySelector(selector) {
|
|
2343
2463
|
if (!selector) return null;
|
|
2344
2464
|
try {
|
|
@@ -2360,17 +2480,9 @@ export function initComty(options = {}) {
|
|
|
2360
2480
|
}
|
|
2361
2481
|
const context = parseAnchorContext(comment);
|
|
2362
2482
|
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 });
|
|
2483
|
+
const resolution = resolveAnchor(context, exact);
|
|
2484
|
+
const element = resolution.element ? getAnchorElement(resolution.element) : null;
|
|
2485
|
+
commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
|
|
2374
2486
|
return element;
|
|
2375
2487
|
}
|
|
2376
2488
|
|
|
@@ -2441,9 +2553,14 @@ export function initComty(options = {}) {
|
|
|
2441
2553
|
return { deviceType: "desktop", deviceLabel: "Desktop" };
|
|
2442
2554
|
}
|
|
2443
2555
|
|
|
2556
|
+
function syncPlacementCursor() {
|
|
2557
|
+
document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2444
2560
|
function closeBubbles() {
|
|
2445
2561
|
const hadDraft = Boolean(draftAnchor);
|
|
2446
2562
|
draftAnchor = null;
|
|
2563
|
+
syncPlacementCursor();
|
|
2447
2564
|
activeHoverCommentIds.clear();
|
|
2448
2565
|
if (!session.edit || hadDraft) hideInspector();
|
|
2449
2566
|
document
|
|
@@ -2488,7 +2605,7 @@ export function initComty(options = {}) {
|
|
|
2488
2605
|
pending: commentId.startsWith("pending-"),
|
|
2489
2606
|
project_id: projectId,
|
|
2490
2607
|
domain: payload.host || window.location.host,
|
|
2491
|
-
page_path: payload.pagePath ||
|
|
2608
|
+
page_path: payload.pagePath || getPagePath(),
|
|
2492
2609
|
selector: payload.selector || "body",
|
|
2493
2610
|
rel_x: payload.relX,
|
|
2494
2611
|
rel_y: payload.relY,
|
|
@@ -2513,7 +2630,7 @@ export function initComty(options = {}) {
|
|
|
2513
2630
|
replies: [],
|
|
2514
2631
|
};
|
|
2515
2632
|
|
|
2516
|
-
cachedPagePath =
|
|
2633
|
+
cachedPagePath = getPagePath();
|
|
2517
2634
|
cachedComments = [
|
|
2518
2635
|
...cachedComments.filter(
|
|
2519
2636
|
(cached) => getCommentId(cached) !== commentId,
|
|
@@ -2526,6 +2643,7 @@ export function initComty(options = {}) {
|
|
|
2526
2643
|
}
|
|
2527
2644
|
|
|
2528
2645
|
function getCommentAuthorName(comment) {
|
|
2646
|
+
if (comment?.author_kind === 'agent') return 'Agent';
|
|
2529
2647
|
return comment?.author_name || comment?.name || "Anonymous";
|
|
2530
2648
|
}
|
|
2531
2649
|
|
|
@@ -2547,6 +2665,12 @@ export function initComty(options = {}) {
|
|
|
2547
2665
|
function createCommentAvatar(comment, extraClass = "") {
|
|
2548
2666
|
const avatar = document.createElement("span");
|
|
2549
2667
|
avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
|
|
2668
|
+
if (comment?.author_kind === 'agent') {
|
|
2669
|
+
avatar.classList.add('comty-agent-avatar');
|
|
2670
|
+
avatar.setAttribute('aria-label', 'Agent');
|
|
2671
|
+
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>';
|
|
2672
|
+
return avatar;
|
|
2673
|
+
}
|
|
2550
2674
|
const authorName = getCommentAuthorName(comment);
|
|
2551
2675
|
const avatarUrl = getCommentAuthorAvatarUrl(comment);
|
|
2552
2676
|
|
|
@@ -2554,6 +2678,7 @@ export function initComty(options = {}) {
|
|
|
2554
2678
|
const image = document.createElement("img");
|
|
2555
2679
|
image.src = avatarUrl;
|
|
2556
2680
|
image.alt = "";
|
|
2681
|
+
image.draggable = false;
|
|
2557
2682
|
image.onerror = () => {
|
|
2558
2683
|
image.remove();
|
|
2559
2684
|
avatar.innerText = authorName.charAt(0).toUpperCase();
|
|
@@ -2778,6 +2903,7 @@ export function initComty(options = {}) {
|
|
|
2778
2903
|
payload,
|
|
2779
2904
|
preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
|
|
2780
2905
|
};
|
|
2906
|
+
syncPlacementCursor();
|
|
2781
2907
|
ensureLiveTracking();
|
|
2782
2908
|
|
|
2783
2909
|
const pin = document.createElement("div");
|
|
@@ -2795,10 +2921,21 @@ export function initComty(options = {}) {
|
|
|
2795
2921
|
|
|
2796
2922
|
const textarea = document.createElement("textarea");
|
|
2797
2923
|
textarea.className = "comty-create-textarea";
|
|
2798
|
-
textarea.placeholder = "Add a comment";
|
|
2924
|
+
textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
|
|
2799
2925
|
textarea.setAttribute("aria-label", textarea.placeholder);
|
|
2800
2926
|
textarea.rows = 1;
|
|
2801
2927
|
|
|
2928
|
+
const scopePicker = document.createElement('label');
|
|
2929
|
+
scopePicker.className = 'comty-agent-scope';
|
|
2930
|
+
if (payload.kind === 'agent') {
|
|
2931
|
+
const input = document.createElement('input');
|
|
2932
|
+
input.type = 'checkbox';
|
|
2933
|
+
input.setAttribute('aria-label', 'Apply to all instances of this variant');
|
|
2934
|
+
const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
|
|
2935
|
+
scopePicker.title = 'Apply to all instances of this variant';
|
|
2936
|
+
scopePicker.append(input, track, document.createTextNode('Apply globally'));
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2802
2939
|
const actions = document.createElement("div");
|
|
2803
2940
|
actions.className = "comty-create-actions";
|
|
2804
2941
|
|
|
@@ -2852,7 +2989,7 @@ export function initComty(options = {}) {
|
|
|
2852
2989
|
const submitBtn = document.createElement("button");
|
|
2853
2990
|
submitBtn.type = "button";
|
|
2854
2991
|
submitBtn.className = "comty-btn-submit";
|
|
2855
|
-
submitBtn.setAttribute("aria-label", "Post comment");
|
|
2992
|
+
submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
|
|
2856
2993
|
submitBtn.disabled = true;
|
|
2857
2994
|
submitBtn.innerHTML = `
|
|
2858
2995
|
<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 +3006,62 @@ export function initComty(options = {}) {
|
|
|
2869
3006
|
textarea.disabled = true;
|
|
2870
3007
|
linkBtn.disabled = true;
|
|
2871
3008
|
|
|
3009
|
+
if (payload.kind === "agent" && !agent) {
|
|
3010
|
+
showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
|
|
3011
|
+
}
|
|
2872
3012
|
payload.content = content;
|
|
3013
|
+
if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
|
|
2873
3014
|
payload.linkUrl = attachedLink || null;
|
|
2874
3015
|
textarea.textContent = content;
|
|
2875
3016
|
|
|
2876
|
-
const
|
|
2877
|
-
const
|
|
2878
|
-
const
|
|
2879
|
-
|
|
3017
|
+
const isAgentTask = payload.kind === "agent";
|
|
3018
|
+
const context = isAgentTask ? null : captureContext();
|
|
3019
|
+
const point = isAgentTask ? null : getPinPoint(pin);
|
|
3020
|
+
const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
|
|
2880
3021
|
bubble.remove();
|
|
2881
3022
|
pin.remove();
|
|
2882
3023
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
3024
|
+
syncPlacementCursor();
|
|
2883
3025
|
hideInspector();
|
|
2884
|
-
addLocalComment(pendingId, payload);
|
|
3026
|
+
if (!isAgentTask) addLocalComment(pendingId, payload);
|
|
2885
3027
|
let post;
|
|
2886
3028
|
try {
|
|
2887
|
-
post = await fetch(`${apiUrl}/api/comments`, {
|
|
3029
|
+
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
2888
3030
|
method: "POST",
|
|
2889
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
2890
|
-
body: JSON.stringify(payload),
|
|
3031
|
+
headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3032
|
+
body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
|
|
2891
3033
|
});
|
|
2892
|
-
if (!post.ok) throw new Error(`
|
|
3034
|
+
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
2893
3035
|
const data = await post.json();
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3036
|
+
const id = isAgentTask ? data?.task?.id : data?.id;
|
|
3037
|
+
if (!id) throw new Error("Save returned no ID");
|
|
3038
|
+
if (isAgentTask) {
|
|
3039
|
+
void loadComments({ force: true });
|
|
3040
|
+
} else {
|
|
3041
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
3042
|
+
if (context.url === location.href) {
|
|
3043
|
+
const comment = addLocalComment(String(id), payload);
|
|
3044
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
3045
|
+
} else void loadComments({ force: true });
|
|
3046
|
+
}
|
|
2901
3047
|
} catch (error) {
|
|
2902
3048
|
console.error("Comty: save failed", error);
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3049
|
+
if (isAgentTask) showAgentBridgeError(error.message);
|
|
3050
|
+
else {
|
|
3051
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
3052
|
+
renderSidepanelComments();
|
|
3053
|
+
void renderComments(cachedComments, { retry: false });
|
|
3054
|
+
}
|
|
2906
3055
|
// Keep the original editor and its text available for retry, including attachments.
|
|
2907
3056
|
if (!draftAnchor) draftAnchor = editorDraft;
|
|
3057
|
+
syncPlacementCursor();
|
|
2908
3058
|
root.append(pin, bubble);
|
|
2909
3059
|
submitBtn.disabled = false;
|
|
2910
3060
|
textarea.disabled = false;
|
|
2911
3061
|
linkBtn.disabled = false;
|
|
2912
3062
|
const errorMessage = document.createElement("p");
|
|
2913
3063
|
errorMessage.setAttribute("role", "alert");
|
|
2914
|
-
errorMessage.textContent = "Comment not saved. Your draft is ready to retry
|
|
3064
|
+
errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
|
|
2915
3065
|
bubble.querySelector('[role="alert"]')?.remove();
|
|
2916
3066
|
bubble.append(errorMessage);
|
|
2917
3067
|
if (bubble.isConnected) textarea.focus();
|
|
@@ -3000,7 +3150,8 @@ export function initComty(options = {}) {
|
|
|
3000
3150
|
renderInspector();
|
|
3001
3151
|
};
|
|
3002
3152
|
|
|
3003
|
-
actions.append(
|
|
3153
|
+
if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
|
|
3154
|
+
else actions.append(linkBtn, submitBtn, linkEditor);
|
|
3004
3155
|
|
|
3005
3156
|
bubble.appendChild(textarea);
|
|
3006
3157
|
bubble.appendChild(attachment);
|
|
@@ -3013,6 +3164,219 @@ export function initComty(options = {}) {
|
|
|
3013
3164
|
setTimeout(() => textarea.focus(), 10);
|
|
3014
3165
|
}
|
|
3015
3166
|
|
|
3167
|
+
function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
|
|
3168
|
+
|
|
3169
|
+
function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
|
|
3170
|
+
root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
|
|
3171
|
+
const toast = document.createElement("div");
|
|
3172
|
+
toast.className = "comty-auth-toast comty-agent-task-toast";
|
|
3173
|
+
toast.setAttribute("role", "alert");
|
|
3174
|
+
const issue = describeAgentError(message);
|
|
3175
|
+
toast.textContent = issue.title + ". " + issue.description;
|
|
3176
|
+
root.appendChild(toast);
|
|
3177
|
+
setTimeout(() => toast.remove(), 5000);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function describeAgentError(error) {
|
|
3181
|
+
const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
|
|
3182
|
+
let value = error;
|
|
3183
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
3184
|
+
if (typeof value === 'string') {
|
|
3185
|
+
try { value = JSON.parse(value); continue; } catch {}
|
|
3186
|
+
const start = value.indexOf('{'), end = value.lastIndexOf('}');
|
|
3187
|
+
if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
|
|
3188
|
+
break;
|
|
3189
|
+
}
|
|
3190
|
+
if (!value || typeof value !== 'object') break;
|
|
3191
|
+
value = value.error || value.message || value.cause || value.stderr || value;
|
|
3192
|
+
}
|
|
3193
|
+
const message = typeof value === 'string' ? value : '';
|
|
3194
|
+
const source = raw + ' ' + message;
|
|
3195
|
+
const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
|
|
3196
|
+
let title, description;
|
|
3197
|
+
if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
|
|
3198
|
+
title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
|
|
3199
|
+
} else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
|
|
3200
|
+
title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
|
|
3201
|
+
} else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
|
|
3202
|
+
title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
|
|
3203
|
+
} else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
|
|
3204
|
+
title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
|
|
3205
|
+
} else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
|
|
3206
|
+
title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
|
|
3207
|
+
} else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
|
|
3208
|
+
title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
|
|
3209
|
+
} else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
|
|
3210
|
+
title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
|
|
3211
|
+
} else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
|
|
3212
|
+
title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
|
|
3213
|
+
} else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
|
|
3214
|
+
title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
|
|
3215
|
+
} else if (/completion was not verified/i.test(source)) {
|
|
3216
|
+
title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
|
|
3217
|
+
} else {
|
|
3218
|
+
title = 'Task could not finish';
|
|
3219
|
+
description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 300) : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
|
|
3220
|
+
}
|
|
3221
|
+
return { title, description, raw, message };
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
function renderAgentPanel() {
|
|
3225
|
+
if (!agent) return;
|
|
3226
|
+
const visibleTasks = [];
|
|
3227
|
+
const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
|
|
3228
|
+
if (latestResult) visibleTasks.push(latestResult);
|
|
3229
|
+
if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
|
|
3230
|
+
let panel = root.querySelector(".comty-agent-panel");
|
|
3231
|
+
if (!panel && !agentPanelOpen) return;
|
|
3232
|
+
if (!panel) {
|
|
3233
|
+
panel = document.createElement("section");
|
|
3234
|
+
panel.className = "comty-agent-panel";
|
|
3235
|
+
panel.setAttribute("aria-label", "Local agent tasks");
|
|
3236
|
+
panel.hidden = true;
|
|
3237
|
+
// Keep the task card above inspection overlays and below the toolbar.
|
|
3238
|
+
root.insertBefore(panel, toolbar);
|
|
3239
|
+
}
|
|
3240
|
+
const open = Boolean(agent) && agentPanelOpen;
|
|
3241
|
+
if (open) {
|
|
3242
|
+
if (panel.hidden) {
|
|
3243
|
+
const rect = toolbar.getBoundingClientRect();
|
|
3244
|
+
panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
|
|
3245
|
+
panel.dataset.closed = "";
|
|
3246
|
+
panel.hidden = false;
|
|
3247
|
+
panel.getBoundingClientRect();
|
|
3248
|
+
}
|
|
3249
|
+
if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
|
|
3250
|
+
} else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
|
|
3251
|
+
const transition = ++agentPanelTransition;
|
|
3252
|
+
panel.dataset.closed = "";
|
|
3253
|
+
const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
|
|
3254
|
+
if (reducedMotion.matches) finish();
|
|
3255
|
+
else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
|
|
3256
|
+
}
|
|
3257
|
+
panel.inert = !open;
|
|
3258
|
+
panel.setAttribute("aria-hidden", String(!open));
|
|
3259
|
+
const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
|
|
3260
|
+
if (signature === agentPanelSignature) return;
|
|
3261
|
+
agentPanelSignature = signature;
|
|
3262
|
+
root.querySelector('.comty-agent-error-popover')?.remove();
|
|
3263
|
+
const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
|
|
3264
|
+
const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
|
|
3265
|
+
const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
|
|
3266
|
+
const node = (tag, className, text) => {
|
|
3267
|
+
const element = document.createElement(tag);
|
|
3268
|
+
if (className) element.className = className;
|
|
3269
|
+
if (text != null) element.textContent = text;
|
|
3270
|
+
return element;
|
|
3271
|
+
};
|
|
3272
|
+
const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
|
|
3273
|
+
panel.replaceChildren();
|
|
3274
|
+
const header = node('header', 'comty-agent-panel-header');
|
|
3275
|
+
const heading = node('div', 'comty-agent-heading');
|
|
3276
|
+
heading.append(node('strong', '', 'Agent'));
|
|
3277
|
+
const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
|
|
3278
|
+
toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
3279
|
+
toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
|
|
3280
|
+
toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
|
|
3281
|
+
header.append(heading, toggle); panel.appendChild(header);
|
|
3282
|
+
const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
|
|
3283
|
+
const technicalDetails = (parent, raw, key) => {
|
|
3284
|
+
const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
|
|
3285
|
+
const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
|
|
3286
|
+
details.append(summary, node('pre', '', raw)); parent.appendChild(details);
|
|
3287
|
+
};
|
|
3288
|
+
const errorMessage = (parent, error, key) => {
|
|
3289
|
+
const issue = describeAgentError(error);
|
|
3290
|
+
const message = node('div', 'comty-agent-error');
|
|
3291
|
+
message.append(node('strong', '', issue.title), node('p', '', issue.description));
|
|
3292
|
+
parent.appendChild(message); technicalDetails(parent, issue.raw, key);
|
|
3293
|
+
};
|
|
3294
|
+
if (agentConnectionError || agentActionError) {
|
|
3295
|
+
const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
|
|
3296
|
+
errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
|
|
3297
|
+
}
|
|
3298
|
+
const action = (parent, label, path, input = {}, primary = false) => {
|
|
3299
|
+
const button = node('button', 'comty-agent-action', label); button.type = 'button';
|
|
3300
|
+
const key = path + JSON.stringify(input); button.dataset.focusKey = key;
|
|
3301
|
+
button.disabled = agentPendingActions.has(key);
|
|
3302
|
+
if (primary) button.dataset.primary = '';
|
|
3303
|
+
button.onclick = async () => {
|
|
3304
|
+
if (agentPendingActions.has(key)) return;
|
|
3305
|
+
agentActionError = '';
|
|
3306
|
+
agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
|
|
3307
|
+
try {
|
|
3308
|
+
const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
|
|
3309
|
+
if (!response.ok) {
|
|
3310
|
+
const body = await response.json().catch(() => null);
|
|
3311
|
+
throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
|
|
3312
|
+
}
|
|
3313
|
+
await loadComments({ force: true });
|
|
3314
|
+
} catch (error) { agentActionError = error.message; }
|
|
3315
|
+
finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
|
|
3316
|
+
};
|
|
3317
|
+
parent.appendChild(button);
|
|
3318
|
+
};
|
|
3319
|
+
for (const approval of localApprovals) {
|
|
3320
|
+
const section = node('div', 'comty-agent-item');
|
|
3321
|
+
section.append(node('h3', '', 'Approval required'));
|
|
3322
|
+
const task = localTasks.find(task => task.id === approval.taskId);
|
|
3323
|
+
if (task) section.appendChild(node('p', '', task.content));
|
|
3324
|
+
section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
|
|
3325
|
+
const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
|
|
3326
|
+
if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
|
|
3327
|
+
technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
|
|
3328
|
+
const actions = node('div', 'comty-agent-actions');
|
|
3329
|
+
action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
|
|
3330
|
+
action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
|
|
3331
|
+
section.appendChild(actions); content.appendChild(section);
|
|
3332
|
+
}
|
|
3333
|
+
const list = node('div', 'comty-agent-list'); content.appendChild(list);
|
|
3334
|
+
const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
|
|
3335
|
+
content.addEventListener('scroll', hideErrorPopover, { passive: true });
|
|
3336
|
+
for (const [index, task] of visibleTasks.reverse().entries()) {
|
|
3337
|
+
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
3338
|
+
const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
|
|
3339
|
+
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
3340
|
+
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
3341
|
+
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
3342
|
+
const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
|
|
3343
|
+
row.append(dot, name);
|
|
3344
|
+
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
3345
|
+
const actions = node('div', 'comty-agent-row-actions');
|
|
3346
|
+
if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
|
|
3347
|
+
if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
|
|
3348
|
+
if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
3349
|
+
if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
3350
|
+
if (actions.childElementCount) row.appendChild(actions);
|
|
3351
|
+
row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
|
|
3352
|
+
if (failed) {
|
|
3353
|
+
const tooltipId = `comty-agent-error-${index}`;
|
|
3354
|
+
const showErrorPopover = () => {
|
|
3355
|
+
hideErrorPopover();
|
|
3356
|
+
const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
|
|
3357
|
+
const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
|
|
3358
|
+
popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
|
|
3359
|
+
root.appendChild(popover);
|
|
3360
|
+
const rowRect = row.getBoundingClientRect();
|
|
3361
|
+
const popoverRect = popover.getBoundingClientRect();
|
|
3362
|
+
const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
|
|
3363
|
+
popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
|
|
3364
|
+
popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
|
|
3365
|
+
};
|
|
3366
|
+
row.addEventListener('mouseenter', showErrorPopover);
|
|
3367
|
+
row.addEventListener('mouseleave', hideErrorPopover);
|
|
3368
|
+
}
|
|
3369
|
+
list.appendChild(row);
|
|
3370
|
+
if (task.result?.summary) {
|
|
3371
|
+
const answer = node('div', 'comty-agent-answer');
|
|
3372
|
+
answer.append(brand(), node('p', '', task.result.summary));
|
|
3373
|
+
list.appendChild(answer);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
content.scrollTop = scrollTop;
|
|
3377
|
+
if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3016
3380
|
function setSidepanelOpen(open) {
|
|
3017
3381
|
sidepanelOpen = open;
|
|
3018
3382
|
sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
|
|
@@ -3048,12 +3412,52 @@ export function initComty(options = {}) {
|
|
|
3048
3412
|
|
|
3049
3413
|
function openCommentFromPanel(comment) {
|
|
3050
3414
|
const target = resolveCommentElement(comment);
|
|
3051
|
-
if (!target || isElementTreeHidden(target))
|
|
3415
|
+
if (!target || isElementTreeHidden(target)) {
|
|
3416
|
+
openDetachedThread(comment);
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3052
3419
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
3053
3420
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
3421
|
+
const rect = target.getBoundingClientRect();
|
|
3422
|
+
const relX = Number(comment.rel_x ?? comment.relX);
|
|
3423
|
+
const relY = Number(comment.rel_y ?? comment.relY);
|
|
3424
|
+
// Large page containers must reveal the annotation point, not their center.
|
|
3425
|
+
window.scrollBy({
|
|
3426
|
+
left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
|
|
3427
|
+
top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
|
|
3428
|
+
behavior: "instant",
|
|
3429
|
+
});
|
|
3054
3430
|
syncCommentAnchors();
|
|
3055
3431
|
const pin = findPlacedPin(comment);
|
|
3056
3432
|
if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
|
|
3433
|
+
else openDetachedThread(comment);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
function openDetachedThread(comment) {
|
|
3437
|
+
openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
|
|
3438
|
+
const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
|
|
3439
|
+
if (bubble) {
|
|
3440
|
+
bubble.dataset.detached = "";
|
|
3441
|
+
positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function updateAnchorBadge(item, comment) {
|
|
3446
|
+
resolveCommentElement(comment);
|
|
3447
|
+
const result = commentTargets.get(String(comment.id));
|
|
3448
|
+
const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
|
|
3449
|
+
const status = legacy && !result?.element ? "uncertain" : result?.status;
|
|
3450
|
+
const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
|
|
3451
|
+
let badge = item.querySelector(".comty-anchor-badge");
|
|
3452
|
+
if (!label) { badge?.remove(); return; }
|
|
3453
|
+
if (!badge) {
|
|
3454
|
+
badge = document.createElement("span");
|
|
3455
|
+
badge.className = "comty-anchor-badge";
|
|
3456
|
+
item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
|
|
3457
|
+
}
|
|
3458
|
+
if (badge.textContent !== label) badge.textContent = label;
|
|
3459
|
+
badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
|
|
3460
|
+
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
3461
|
}
|
|
3058
3462
|
|
|
3059
3463
|
let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
|
|
@@ -3066,6 +3470,7 @@ export function initComty(options = {}) {
|
|
|
3066
3470
|
? sidepanelSearch.value.trim().toLowerCase()
|
|
3067
3471
|
: "";
|
|
3068
3472
|
let list = cachedComments.filter((comment) => {
|
|
3473
|
+
if (comment.local_agent || comment.kind === "agent") return false;
|
|
3069
3474
|
if (
|
|
3070
3475
|
search &&
|
|
3071
3476
|
!`${comment.content ?? ""} ${getCommentLabel(comment)}`
|
|
@@ -3112,6 +3517,7 @@ export function initComty(options = {}) {
|
|
|
3112
3517
|
const item = document.createElement("button");
|
|
3113
3518
|
item.type = "button";
|
|
3114
3519
|
item.className = "comty-sidepanel-item";
|
|
3520
|
+
item.dataset.commentId = String(comment.id);
|
|
3115
3521
|
item.disabled = Boolean(comment.pending);
|
|
3116
3522
|
item.innerHTML = `
|
|
3117
3523
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
@@ -3135,8 +3541,13 @@ export function initComty(options = {}) {
|
|
|
3135
3541
|
if (metaEl instanceof HTMLElement) {
|
|
3136
3542
|
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
3137
3543
|
metaEl.title = getCommentLabel(comment);
|
|
3544
|
+
if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
|
|
3545
|
+
metaEl.innerText += " · Original page unknown";
|
|
3546
|
+
}
|
|
3138
3547
|
}
|
|
3139
3548
|
|
|
3549
|
+
updateAnchorBadge(item, comment);
|
|
3550
|
+
|
|
3140
3551
|
item.onclick = (event) => {
|
|
3141
3552
|
event.stopPropagation();
|
|
3142
3553
|
openCommentFromPanel(comment);
|
|
@@ -3252,14 +3663,15 @@ export function initComty(options = {}) {
|
|
|
3252
3663
|
createStatusFilters();
|
|
3253
3664
|
|
|
3254
3665
|
async function updateCommentStatus(comment, status) {
|
|
3255
|
-
if (!projectId || !comment?.id) return false;
|
|
3666
|
+
if (comment?.local_agent || !projectId || !comment?.id) return false;
|
|
3256
3667
|
|
|
3257
3668
|
let res;
|
|
3258
3669
|
try {
|
|
3259
|
-
|
|
3670
|
+
const local = comment.local_agent === true;
|
|
3671
|
+
res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
|
|
3260
3672
|
method: "POST",
|
|
3261
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3262
|
-
body: JSON.stringify({
|
|
3673
|
+
headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3674
|
+
body: JSON.stringify(local ? { status } : {
|
|
3263
3675
|
projectKey,
|
|
3264
3676
|
host: window.location.host,
|
|
3265
3677
|
status,
|
|
@@ -3276,7 +3688,7 @@ export function initComty(options = {}) {
|
|
|
3276
3688
|
}
|
|
3277
3689
|
|
|
3278
3690
|
const data = await res.json();
|
|
3279
|
-
comment.status = getCommentStatus(data.status).value;
|
|
3691
|
+
comment.status = getCommentStatus(data.task?.status ?? data.status).value;
|
|
3280
3692
|
return comment.status;
|
|
3281
3693
|
}
|
|
3282
3694
|
|
|
@@ -3468,6 +3880,23 @@ export function initComty(options = {}) {
|
|
|
3468
3880
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
3469
3881
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
3470
3882
|
showAuthPrompt,
|
|
3883
|
+
startThreadAgent: async (comment, replies, content) => {
|
|
3884
|
+
if (!agent) throw new Error('Start the local agent to reply with @Agent');
|
|
3885
|
+
if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
|
|
3886
|
+
const transcript = [
|
|
3887
|
+
`${getCommentAuthorName(comment)}: ${comment.content}`,
|
|
3888
|
+
...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
|
|
3889
|
+
].join('\n').slice(-24000);
|
|
3890
|
+
const response = await fetch(`${agentBridgeUrl}/tasks`, {
|
|
3891
|
+
method: 'POST', headers: agentHeaders(),
|
|
3892
|
+
body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
|
|
3893
|
+
selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
|
|
3894
|
+
domElement: comment.dom_element || '', viewport: comment.viewport || '',
|
|
3895
|
+
anchorContext: comment.anchor_context || '', scope: 'element',
|
|
3896
|
+
thread: { commentId: String(comment.id), transcript } }),
|
|
3897
|
+
});
|
|
3898
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
|
|
3899
|
+
},
|
|
3471
3900
|
get canWrite() { return canWrite; },
|
|
3472
3901
|
get canManage() { return canManage; },
|
|
3473
3902
|
get allowGuestComments() { return allowGuestComments; },
|
|
@@ -3668,42 +4097,112 @@ export function initComty(options = {}) {
|
|
|
3668
4097
|
function reconcileComments(comments) {
|
|
3669
4098
|
const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
|
|
3670
4099
|
cachedComments = comments
|
|
3671
|
-
.filter(comment => comment.kind !== "agent"
|
|
4100
|
+
.filter(comment => comment.kind !== "agent" || comment.status !== "done")
|
|
3672
4101
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
3673
4102
|
for (const comment of previous.values()) {
|
|
3674
4103
|
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
3675
4104
|
}
|
|
3676
4105
|
}
|
|
3677
4106
|
|
|
4107
|
+
async function deliverThreadReply(task) {
|
|
4108
|
+
if (deliveringAgentReplies.has(task.id)) return;
|
|
4109
|
+
deliveringAgentReplies.add(task.id);
|
|
4110
|
+
try {
|
|
4111
|
+
const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
|
|
4112
|
+
method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
4113
|
+
body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
|
|
4114
|
+
});
|
|
4115
|
+
if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
|
|
4116
|
+
const { reply } = await response.json();
|
|
4117
|
+
const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
|
|
4118
|
+
if (local && !local.replies?.some(item => item.id === reply.id)) {
|
|
4119
|
+
local.replies ||= [];
|
|
4120
|
+
local.replies.push(reply);
|
|
4121
|
+
}
|
|
4122
|
+
for (const bubble of root.querySelectorAll('.comty-bubble')) {
|
|
4123
|
+
if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
|
|
4124
|
+
}
|
|
4125
|
+
const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
|
|
4126
|
+
if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
|
|
4127
|
+
localTasks = localTasks.filter(item => item.id !== task.id);
|
|
4128
|
+
} catch (error) {
|
|
4129
|
+
agentActionError = error.message;
|
|
4130
|
+
console.error('Comty: Agent reply delivery failed', error);
|
|
4131
|
+
} finally {
|
|
4132
|
+
deliveringAgentReplies.delete(task.id);
|
|
4133
|
+
agentPanelSignature = '';
|
|
4134
|
+
renderAgentPanel();
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
|
|
3678
4138
|
async function loadComments({ force = false } = {}) {
|
|
3679
|
-
if (!projectId) return;
|
|
3680
|
-
const pagePath =
|
|
4139
|
+
if (!projectId && !agent) return;
|
|
4140
|
+
const pagePath = getPagePath();
|
|
3681
4141
|
if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
|
|
3682
4142
|
if (pendingCommentsLoad) {
|
|
3683
4143
|
await pendingCommentsLoad;
|
|
3684
|
-
if (!force) return;
|
|
4144
|
+
if (!force && cachedPagePath === pagePath) return;
|
|
3685
4145
|
}
|
|
3686
4146
|
pendingCommentsLoad = (async () => {
|
|
3687
|
-
let comments = cachedPagePath === pagePath ? cachedComments : [];
|
|
4147
|
+
let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
|
|
3688
4148
|
if (projectId) {
|
|
3689
4149
|
try {
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
4150
|
+
const paths = [...new Set([pagePath, window.location.pathname])];
|
|
4151
|
+
const batches = await Promise.all(paths.map(async path => {
|
|
4152
|
+
const url = new URL(`${apiUrl}/api/comments`);
|
|
4153
|
+
url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
|
|
4154
|
+
const res = await fetch(url);
|
|
4155
|
+
if (!res.ok) throw new Error(await res.text());
|
|
4156
|
+
const data = await res.json();
|
|
4157
|
+
return Array.isArray(data.comments) ? data.comments : [];
|
|
4158
|
+
}));
|
|
4159
|
+
comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
|
|
3696
4160
|
} catch (error) { console.error("Comty: list comments failed", error); }
|
|
3697
4161
|
}
|
|
4162
|
+
if (agent) {
|
|
4163
|
+
try {
|
|
4164
|
+
const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
|
|
4165
|
+
if (!res.ok) throw new Error("Local companion unavailable");
|
|
4166
|
+
const data = await res.json();
|
|
4167
|
+
if (Array.isArray(data.tasks)) localTasks = data.tasks;
|
|
4168
|
+
const failedTasks = localTasks.filter(task => task.status === 'in_review');
|
|
4169
|
+
if (failedTasks.some(task => !seenFailedAgentTasks.has(task.id))) agentPanelOpen = true;
|
|
4170
|
+
seenFailedAgentTasks.clear();
|
|
4171
|
+
for (const task of failedTasks) seenFailedAgentTasks.add(task.id);
|
|
4172
|
+
if (Array.isArray(data.approvals)) {
|
|
4173
|
+
localApprovals = data.approvals;
|
|
4174
|
+
if (localApprovals.some(approval => !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
|
|
4175
|
+
seenAgentApprovals.clear();
|
|
4176
|
+
for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
|
|
4177
|
+
}
|
|
4178
|
+
agentConnectionError = "";
|
|
4179
|
+
} catch (error) {
|
|
4180
|
+
agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
|
|
4181
|
+
console.error("Comty: local tasks failed", error);
|
|
4182
|
+
}
|
|
4183
|
+
for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
|
|
4184
|
+
void deliverThreadReply(task);
|
|
4185
|
+
}
|
|
4186
|
+
comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && !["done", "cancel"].includes(task.status)));
|
|
4187
|
+
renderAgentPanel();
|
|
4188
|
+
}
|
|
4189
|
+
if (pagePath !== getPagePath()) return;
|
|
3698
4190
|
reconcileComments(comments);
|
|
3699
4191
|
cachedPagePath = pagePath;
|
|
3700
4192
|
renderSidepanelComments();
|
|
3701
4193
|
await renderComments(cachedComments);
|
|
4194
|
+
syncAgentWorkingOutlines();
|
|
3702
4195
|
ensureLiveTracking();
|
|
3703
4196
|
})();
|
|
3704
4197
|
try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
|
|
3705
4198
|
}
|
|
3706
4199
|
|
|
4200
|
+
if (agent) {
|
|
4201
|
+
setInterval(() => {
|
|
4202
|
+
if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
|
|
4203
|
+
}, 2000);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
3707
4206
|
function getPinPoint(pin) {
|
|
3708
4207
|
return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
|
|
3709
4208
|
}
|
|
@@ -3731,7 +4230,7 @@ export function initComty(options = {}) {
|
|
|
3731
4230
|
}
|
|
3732
4231
|
|
|
3733
4232
|
function syncCommentAnchors() {
|
|
3734
|
-
if (!session.comments) return;
|
|
4233
|
+
if (!session.comments && !session.inspect) return;
|
|
3735
4234
|
const modal = getTopActiveModal();
|
|
3736
4235
|
for (const comment of cachedComments) {
|
|
3737
4236
|
const id = String(comment.id);
|
|
@@ -3742,7 +4241,7 @@ export function initComty(options = {}) {
|
|
|
3742
4241
|
placePin(comment, modal);
|
|
3743
4242
|
pin = placedPins.get(id);
|
|
3744
4243
|
}
|
|
3745
|
-
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
|
|
4244
|
+
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
|
|
3746
4245
|
// A pin can remove its target's :hover style as the pointer enters it.
|
|
3747
4246
|
if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
|
|
3748
4247
|
(activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
|
|
@@ -3757,6 +4256,10 @@ export function initComty(options = {}) {
|
|
|
3757
4256
|
if (pin.style.top !== y) pin.style.top = y;
|
|
3758
4257
|
for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
|
|
3759
4258
|
}
|
|
4259
|
+
if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
|
|
4260
|
+
const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
|
|
4261
|
+
if (comment) updateAnchorBadge(item, comment);
|
|
4262
|
+
}
|
|
3760
4263
|
if (draftAnchor) {
|
|
3761
4264
|
if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
|
|
3762
4265
|
const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
|
|
@@ -3770,16 +4273,50 @@ export function initComty(options = {}) {
|
|
|
3770
4273
|
}
|
|
3771
4274
|
}
|
|
3772
4275
|
|
|
4276
|
+
function syncAgentWorkingOutlines() {
|
|
4277
|
+
const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
|
|
4278
|
+
const ids = new Set(working.map(task => String(task.id)));
|
|
4279
|
+
for (const [id, outline] of workingOutlines) {
|
|
4280
|
+
if (ids.has(id)) continue;
|
|
4281
|
+
outline.remove(); workingOutlines.delete(id);
|
|
4282
|
+
}
|
|
4283
|
+
const modal = getTopActiveModal();
|
|
4284
|
+
for (const task of working) {
|
|
4285
|
+
const id = String(task.id);
|
|
4286
|
+
let outline = workingOutlines.get(id);
|
|
4287
|
+
if (!outline) {
|
|
4288
|
+
outline = document.createElement("div");
|
|
4289
|
+
outline.className = "comty-agent-working";
|
|
4290
|
+
outline.setAttribute("aria-label", "Agent working on this element");
|
|
4291
|
+
const badge = document.createElement("span");
|
|
4292
|
+
badge.className = "comty-agent-working-badge";
|
|
4293
|
+
badge.append(createWorkingIndicator(), "Working");
|
|
4294
|
+
outline.appendChild(badge); root.appendChild(outline);
|
|
4295
|
+
workingOutlines.set(id, outline);
|
|
4296
|
+
}
|
|
4297
|
+
const target = resolveCommentElement(task);
|
|
4298
|
+
const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
|
|
4299
|
+
outline.hidden = !point;
|
|
4300
|
+
if (!point) continue;
|
|
4301
|
+
const rect = target.getBoundingClientRect();
|
|
4302
|
+
setMeasuredRect(outline, rect);
|
|
4303
|
+
outline.toggleAttribute("data-badge-below", rect.top < 32);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
|
|
3773
4307
|
function onFrame() {
|
|
3774
4308
|
if (motion.enabled) updateMotion();
|
|
3775
|
-
if (session.comments) syncCommentAnchors();
|
|
4309
|
+
if (session.comments || session.inspect) syncCommentAnchors();
|
|
4310
|
+
if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
|
|
4311
|
+
syncAgentWorkingOutlines();
|
|
4312
|
+
}
|
|
3776
4313
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
3777
4314
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
3778
4315
|
if (session.inspect && rulersEnabled) measurements.render();
|
|
3779
4316
|
}
|
|
3780
4317
|
|
|
3781
4318
|
function ensureLiveTracking() {
|
|
3782
|
-
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
|
|
4319
|
+
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
|
|
3783
4320
|
if (!active || document.hidden) {
|
|
3784
4321
|
if (liveFrame) cancelAnimationFrame(liveFrame);
|
|
3785
4322
|
liveFrame = null;
|
|
@@ -3799,13 +4336,14 @@ export function initComty(options = {}) {
|
|
|
3799
4336
|
syncOverlayHost();
|
|
3800
4337
|
|
|
3801
4338
|
function renderCachedComments() {
|
|
3802
|
-
if (!session.comments && !session.inspect && !session.edit) return;
|
|
4339
|
+
if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
|
|
3803
4340
|
if (resizeRenderFrame) {
|
|
3804
4341
|
window.cancelAnimationFrame(resizeRenderFrame);
|
|
3805
4342
|
}
|
|
3806
4343
|
resizeRenderFrame = window.requestAnimationFrame(() => {
|
|
3807
4344
|
resizeRenderFrame = null;
|
|
3808
4345
|
syncCommentAnchors();
|
|
4346
|
+
syncAgentWorkingOutlines();
|
|
3809
4347
|
renderInspector();
|
|
3810
4348
|
ensureLiveTracking();
|
|
3811
4349
|
});
|
|
@@ -3847,6 +4385,29 @@ export function initComty(options = {}) {
|
|
|
3847
4385
|
characterData: true,
|
|
3848
4386
|
});
|
|
3849
4387
|
|
|
4388
|
+
let activePagePath = getPagePath();
|
|
4389
|
+
function handleRouteChange() {
|
|
4390
|
+
const next = getPagePath();
|
|
4391
|
+
if (next === activePagePath) return;
|
|
4392
|
+
activePagePath = next;
|
|
4393
|
+
closeBubbles();
|
|
4394
|
+
commentTargets.clear();
|
|
4395
|
+
reconcileComments([]);
|
|
4396
|
+
void renderComments([], { retry: false });
|
|
4397
|
+
renderSidepanelComments();
|
|
4398
|
+
void loadComments({ force: true });
|
|
4399
|
+
}
|
|
4400
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
4401
|
+
window.addEventListener("popstate", handleRouteChange);
|
|
4402
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
4403
|
+
const original = history[method];
|
|
4404
|
+
history[method] = function (...args) {
|
|
4405
|
+
const result = original.apply(this, args);
|
|
4406
|
+
handleRouteChange();
|
|
4407
|
+
return result;
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
|
|
3850
4411
|
document.addEventListener("scroll", handleHostLayoutEvent, {
|
|
3851
4412
|
capture: true,
|
|
3852
4413
|
passive: true,
|
|
@@ -3984,13 +4545,14 @@ export function initComty(options = {}) {
|
|
|
3984
4545
|
const y = e.clientY;
|
|
3985
4546
|
|
|
3986
4547
|
const anchorContext = getAnchorContext(el);
|
|
4548
|
+
if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
|
|
3987
4549
|
|
|
3988
4550
|
const payload = {
|
|
3989
4551
|
...getDeviceContext(),
|
|
3990
4552
|
projectId,
|
|
3991
|
-
kind: "comment",
|
|
4553
|
+
kind: session.agent ? "agent" : "comment",
|
|
3992
4554
|
host: window.location.host,
|
|
3993
|
-
pagePath:
|
|
4555
|
+
pagePath: getPagePath(),
|
|
3994
4556
|
selector: getSelector(el),
|
|
3995
4557
|
relX,
|
|
3996
4558
|
relY,
|
|
@@ -4088,6 +4650,7 @@ export function initComty(options = {}) {
|
|
|
4088
4650
|
window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
|
|
4089
4651
|
|
|
4090
4652
|
async function boot() {
|
|
4653
|
+
if (localOnly) { await loadComments(); return; }
|
|
4091
4654
|
const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
|
|
4092
4655
|
method: "POST",
|
|
4093
4656
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
@@ -4100,6 +4663,7 @@ export function initComty(options = {}) {
|
|
|
4100
4663
|
|
|
4101
4664
|
if (!res.ok) {
|
|
4102
4665
|
console.error("Comty: bootstrap failed", await res.text());
|
|
4666
|
+
if (agent) await loadComments();
|
|
4103
4667
|
return;
|
|
4104
4668
|
}
|
|
4105
4669
|
|
|
@@ -4144,16 +4708,30 @@ export function initComty(options = {}) {
|
|
|
4144
4708
|
|
|
4145
4709
|
if (typeof options.onTestReady === "function") {
|
|
4146
4710
|
options.onTestReady({
|
|
4147
|
-
getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
4711
|
+
getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
4148
4712
|
getDistanceSegments, getInspectRows, syncCommentAnchors,
|
|
4149
4713
|
setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
|
|
4150
4714
|
startEditSession, startBrowseSession, endSession, showInspector,
|
|
4151
|
-
motion, setMotionEnabled, updateMotion,
|
|
4715
|
+
motion, setMotionEnabled, updateMotion, describeAgentError,
|
|
4716
|
+
previewAgent(tasks, approvals = [], error = "") {
|
|
4717
|
+
localTasks = tasks;
|
|
4718
|
+
localApprovals = approvals;
|
|
4719
|
+
agentConnectionError = error;
|
|
4720
|
+
startAgentSession();
|
|
4721
|
+
agentPanelOpen = true;
|
|
4722
|
+
agentPanelSignature = "";
|
|
4723
|
+
renderAgentPanel();
|
|
4724
|
+
},
|
|
4152
4725
|
seed(comments) {
|
|
4153
4726
|
reconcileComments(comments);
|
|
4154
4727
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
4155
4728
|
return renderComments(cachedComments, { retry: false });
|
|
4156
4729
|
},
|
|
4730
|
+
seedAgentTasks(tasks) {
|
|
4731
|
+
localTasks = tasks;
|
|
4732
|
+
syncAgentWorkingOutlines();
|
|
4733
|
+
ensureLiveTracking();
|
|
4734
|
+
},
|
|
4157
4735
|
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
4158
4736
|
invalidate() { anchorRevision += 1; },
|
|
4159
4737
|
});
|
|
@@ -4161,6 +4739,7 @@ export function initComty(options = {}) {
|
|
|
4161
4739
|
|
|
4162
4740
|
return boot().catch(error => {
|
|
4163
4741
|
console.error("Comty: bootstrap failed", error);
|
|
4742
|
+
if (agent) return loadComments();
|
|
4164
4743
|
});
|
|
4165
4744
|
}
|
|
4166
4745
|
}
|