@comty.io/core 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -46
- package/comty-sdk.js +1242 -1423
- package/package.json +8 -12
- package/src/browser.d.ts +1 -3
- package/src/browser.js +461 -693
- package/src/capture-engine.js +26 -0
- package/src/capture-resources.js +94 -0
- package/src/capture.js +249 -459
- package/src/comments.js +45 -32
- package/src/style.js +365 -236
- package/src/agent-process.mjs +0 -89
- package/src/claude-permission.mjs +0 -37
- package/src/claude.mjs +0 -120
- package/src/codex.mjs +0 -66
- package/src/companion.mjs +0 -207
- package/src/cursor.mjs +0 -49
- package/src/dev.mjs +0 -169
- package/src/vite.d.ts +0 -5
- package/src/vite.mjs +0 -66
package/src/browser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getComtyStyle } from "./style.js";
|
|
2
2
|
import { createAnchorMatcher } from "./anchors.js";
|
|
3
|
-
import { createScreenshotCapture, createColorMagnifier } from "./capture.js";
|
|
3
|
+
import { createScreenshotCapture, createColorMagnifier, captureContext } from "./capture.js";
|
|
4
4
|
import { createCommentThread } from "./comments.js";
|
|
5
5
|
import { createMeasurements } from "./measurements.js";
|
|
6
6
|
// Shared browser runtime. Both npm and the standalone CDN script use this file.
|
|
@@ -9,17 +9,7 @@ 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
|
|
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);
|
|
12
|
+
const projectKey = options.projectKey;
|
|
23
13
|
const apiUrl = options.apiUrl || "http://localhost:4000";
|
|
24
14
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
25
15
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
@@ -31,14 +21,12 @@ export function initComty(options = {}) {
|
|
|
31
21
|
if (!["http:", "https:"].includes(parsedApi.protocol)) {
|
|
32
22
|
return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
|
|
33
23
|
}
|
|
34
|
-
const configuration = JSON.stringify([projectKey, apiUrl
|
|
24
|
+
const configuration = JSON.stringify([projectKey, apiUrl]);
|
|
35
25
|
const existing = window[instanceKey];
|
|
36
26
|
if (existing) {
|
|
37
27
|
return existing.configuration === configuration ? existing.ready
|
|
38
28
|
: Promise.reject(new Error("Comty: already initialized with different options"));
|
|
39
29
|
}
|
|
40
|
-
const agentBridgeUrl = agent?.url;
|
|
41
|
-
const localOnly = Boolean(agent && !options.projectKey);
|
|
42
30
|
const ready = new Promise((resolve, reject) => {
|
|
43
31
|
const mount = () => {
|
|
44
32
|
try { resolve(mountComty()); } catch (error) { reject(error); }
|
|
@@ -52,19 +40,10 @@ export function initComty(options = {}) {
|
|
|
52
40
|
|
|
53
41
|
function mountComty() {
|
|
54
42
|
let projectId = null;
|
|
55
|
-
let localTasks = [];
|
|
56
|
-
let localApprovals = [];
|
|
57
|
-
let agentPanelOpen = false;
|
|
58
|
-
let agentPanelSignature = "";
|
|
59
|
-
let agentPanelTransition = 0;
|
|
60
|
-
let agentConnectionError = "";
|
|
61
|
-
let agentActionError = "";
|
|
62
|
-
const agentPendingActions = new Set();
|
|
63
43
|
const session = {
|
|
64
44
|
mode: "off",
|
|
65
|
-
get comments() { return this.mode === "browse" || this.mode === "edit"
|
|
66
|
-
get edit() { return this.mode === "edit"
|
|
67
|
-
get agent() { return this.mode === "agent"; },
|
|
45
|
+
get comments() { return this.mode === "browse" || this.mode === "edit"; },
|
|
46
|
+
get edit() { return this.mode === "edit"; },
|
|
68
47
|
get inspect() { return this.mode === "inspect"; },
|
|
69
48
|
};
|
|
70
49
|
const selectedStatusFilters = new Set();
|
|
@@ -78,21 +57,22 @@ export function initComty(options = {}) {
|
|
|
78
57
|
const observedCommentTargets = new WeakSet();
|
|
79
58
|
let sidepanelOpen = false;
|
|
80
59
|
let canWrite = false;
|
|
60
|
+
let canManage = false;
|
|
61
|
+
let allowGuestComments = false;
|
|
81
62
|
let loginUrl = "";
|
|
82
63
|
let currentUser = null;
|
|
83
64
|
let authStatus = "";
|
|
84
65
|
let didCompleteSignIn = false;
|
|
85
66
|
const tokenStorageKey = `comty:${projectKey}:token`;
|
|
67
|
+
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
86
68
|
let sdkToken = "";
|
|
87
69
|
const activeHoverCommentIds = new Set();
|
|
88
|
-
/** @type {null | 'edit' | 'browse'
|
|
70
|
+
/** @type {null | 'edit' | 'browse'} */
|
|
89
71
|
let sessionEntry = null;
|
|
90
72
|
const placedPins = new Map();
|
|
91
|
-
const workingOutlines = new Map();
|
|
92
73
|
const placementDiagnostics = new Map();
|
|
93
74
|
let inspectedElement = null;
|
|
94
75
|
let selectedElement = null;
|
|
95
|
-
let inspectExpanded = false;
|
|
96
76
|
let measureAlt = false;
|
|
97
77
|
let inspectDrag = null;
|
|
98
78
|
let measuredRegion = null;
|
|
@@ -135,7 +115,6 @@ export function initComty(options = {}) {
|
|
|
135
115
|
const root = document.createElement("div");
|
|
136
116
|
root.id = "comty-root";
|
|
137
117
|
document.body.appendChild(root);
|
|
138
|
-
if (agent) document.documentElement.classList.add("comty-agent-enabled");
|
|
139
118
|
|
|
140
119
|
const style = document.createElement("style");
|
|
141
120
|
style.innerHTML = getComtyStyle();
|
|
@@ -183,40 +162,20 @@ export function initComty(options = {}) {
|
|
|
183
162
|
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"));
|
|
184
163
|
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"/>', () => {
|
|
185
164
|
if (!canWrite) showAuthPrompt();
|
|
186
|
-
else if (session.
|
|
165
|
+
else if (session.inspect || !isSessionActive()) startEditSession();
|
|
187
166
|
else if (!session.edit) setSession("edit", { entry: sessionEntry });
|
|
188
167
|
});
|
|
189
168
|
btn.id = "comty-toggle";
|
|
190
|
-
const agentBtn = isLocalSite
|
|
191
|
-
? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
|
|
192
|
-
startAgentSession();
|
|
193
|
-
})
|
|
194
|
-
: null;
|
|
195
169
|
const divider = document.createElement("span");
|
|
196
170
|
divider.className = "comty-tool-divider";
|
|
197
171
|
toolbar.appendChild(divider);
|
|
198
172
|
const commentTools = document.createElement("div");
|
|
199
173
|
commentTools.className = "comty-tool-context";
|
|
200
174
|
toolbar.appendChild(commentTools);
|
|
201
|
-
const agentTools = agentBtn ? document.createElement("div") : null;
|
|
202
|
-
if (agentTools) {
|
|
203
|
-
agentTools.className = "comty-tool-context";
|
|
204
|
-
agentTools.hidden = true;
|
|
205
|
-
toolbar.appendChild(agentTools);
|
|
206
|
-
}
|
|
207
|
-
const agentHistoryBtn = agentTools ? toolButton("Local agent history", '<path d="M3 12a9 9 0 1 0 3-6.7M3 4v4h4"/><path d="M12 7v5l3 2"/>', () => {
|
|
208
|
-
if (!agent) { showAgentBridgeError(); return; }
|
|
209
|
-
agentPanelOpen = !agentPanelOpen;
|
|
210
|
-
renderAgentPanel();
|
|
211
|
-
}) : null;
|
|
212
|
-
if (agentHistoryBtn) {
|
|
213
|
-
agentHistoryBtn.setAttribute("aria-expanded", "false");
|
|
214
|
-
agentTools.appendChild(agentHistoryBtn);
|
|
215
|
-
}
|
|
216
175
|
const primaryTools = document.createElement("div");
|
|
217
176
|
primaryTools.className = "comty-tool-primary";
|
|
218
177
|
toolbar.insertBefore(primaryTools, browseBtn);
|
|
219
|
-
for (const button of [browseBtn, inspectorBtn, btn
|
|
178
|
+
for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
|
|
220
179
|
const inspectorTools = document.createElement("div");
|
|
221
180
|
inspectorTools.className = "comty-tool-context";
|
|
222
181
|
inspectorTools.hidden = true;
|
|
@@ -795,6 +754,21 @@ export function initComty(options = {}) {
|
|
|
795
754
|
const copyIcon = icon('<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>');
|
|
796
755
|
const checkIcon = icon('<path d="m5 12 4 4 10-10"/>');
|
|
797
756
|
const copyFeedbackTimers = new WeakMap();
|
|
757
|
+
async function copyPropertyValue(button, feedback) {
|
|
758
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
759
|
+
try {
|
|
760
|
+
await navigator.clipboard.writeText(button.dataset.copy);
|
|
761
|
+
if (!button.isConnected) return;
|
|
762
|
+
button.dataset.copied = "";
|
|
763
|
+
feedback.textContent = "Copied";
|
|
764
|
+
clearTimeout(copyFeedbackTimers.get(button));
|
|
765
|
+
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
766
|
+
} catch {
|
|
767
|
+
delete button.dataset.copied;
|
|
768
|
+
if (button.isConnected) feedback.textContent = "Clipboard unavailable — select the value to copy";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
798
772
|
function propertyRow(label, value, display = value) {
|
|
799
773
|
const unavailable = value === "—";
|
|
800
774
|
return `<div class="comty-property-row"><span>${label}</span><button type="button" data-copy="${escapeInspectorText(value)}" aria-label="Copy ${label}: ${escapeInspectorText(display)}" ${unavailable ? "disabled" : ""}><span>${escapeInspectorText(display)}</span>${unavailable ? "" : `<i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i>`}</button></div>`;
|
|
@@ -821,7 +795,7 @@ export function initComty(options = {}) {
|
|
|
821
795
|
propertyPanel.setAttribute("aria-label", propertyKind === "color" ? "Recent colours" : "Recent typography");
|
|
822
796
|
const recent = propertyKind === "color" ? recentColors : recentFonts;
|
|
823
797
|
const details = propertyKind === "color" && expanded
|
|
824
|
-
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ?
|
|
798
|
+
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("HEX", propertySelection.hex)}${propertyRow("RGB", propertySelection.rgb)}${propertyRow("HSL", propertySelection.hsl)}${propertyRow("OKLCH", propertySelection.oklch)}${propertyRow("Token", propertySelection.token ? propertySelection.token.replace(/^--/, "") : "—", propertySelection.token || "—")}</div></div>`
|
|
825
799
|
: propertyKind === "font" && expanded
|
|
826
800
|
? `<div class="comty-property-detail"><div class="comty-property-rows">${propertyRow("Family", propertySelection.family)}${propertyRow("Size", propertySelection.size)}${propertyRow("Weight", propertySelection.weight)}${propertyRow("Leading", propertySelection.leading)}${propertyRow("Tracking", propertySelection.tracking)}${propertyRow("Colour", propertySelection.color?.hex || "—")}${propertyRow("CSS", propertySelection.css, "All of the above")}</div></div>`
|
|
827
801
|
: "";
|
|
@@ -937,18 +911,7 @@ export function initComty(options = {}) {
|
|
|
937
911
|
renderPropertyPanel(true);
|
|
938
912
|
propertyPanel.querySelector(`button[data-recent="${button.dataset.recent}"]`)?.focus({ preventScroll: true });
|
|
939
913
|
} else if (button.dataset.copy != null && button.dataset.copy !== "—") {
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
await navigator.clipboard.writeText(button.dataset.copy);
|
|
943
|
-
if (!button.isConnected || propertyPanel.hidden) return;
|
|
944
|
-
button.dataset.copied = "";
|
|
945
|
-
propertyPanel.querySelector(".comty-property-feedback").textContent = `${button.getAttribute("aria-label").slice(5)} copied`;
|
|
946
|
-
clearTimeout(copyFeedbackTimers.get(button));
|
|
947
|
-
copyFeedbackTimers.set(button, setTimeout(() => { delete button.dataset.copied; }, 1200));
|
|
948
|
-
} catch {
|
|
949
|
-
delete button.dataset.copied;
|
|
950
|
-
if (button.isConnected && !propertyPanel.hidden) propertyPanel.querySelector(".comty-property-feedback").textContent = "Clipboard unavailable";
|
|
951
|
-
}
|
|
914
|
+
await copyPropertyValue(button, propertyPanel.querySelector(".comty-property-feedback"));
|
|
952
915
|
}
|
|
953
916
|
};
|
|
954
917
|
|
|
@@ -1065,50 +1028,6 @@ export function initComty(options = {}) {
|
|
|
1065
1028
|
let toolbarMotionFrame = 0;
|
|
1066
1029
|
let toolbarAnimations = [];
|
|
1067
1030
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1068
|
-
let workingIndicatorTimer = 0;
|
|
1069
|
-
function scheduleWorkingIndicators() {
|
|
1070
|
-
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
1071
|
-
workingIndicatorTimer = setTimeout(() => {
|
|
1072
|
-
workingIndicatorTimer = 0;
|
|
1073
|
-
if (!root.isConnected) return;
|
|
1074
|
-
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
1075
|
-
if (!indicators.length) return;
|
|
1076
|
-
for (const indicator of indicators) {
|
|
1077
|
-
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
1078
|
-
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1079
|
-
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
1080
|
-
indicator.dataset.activeDots = `${first},${second}`;
|
|
1081
|
-
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
1082
|
-
}
|
|
1083
|
-
scheduleWorkingIndicators();
|
|
1084
|
-
}, 600);
|
|
1085
|
-
}
|
|
1086
|
-
function createWorkingIndicator() {
|
|
1087
|
-
const indicator = document.createElement('span');
|
|
1088
|
-
indicator.className = 'comty-working-indicator';
|
|
1089
|
-
indicator.setAttribute('aria-hidden', 'true');
|
|
1090
|
-
indicator.dataset.activeDots = '3,5';
|
|
1091
|
-
for (let index = 0; index < 9; index++) {
|
|
1092
|
-
const dot = document.createElement('i');
|
|
1093
|
-
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
1094
|
-
indicator.appendChild(dot);
|
|
1095
|
-
}
|
|
1096
|
-
scheduleWorkingIndicators();
|
|
1097
|
-
return indicator;
|
|
1098
|
-
}
|
|
1099
|
-
function resetWorkingIndicators() {
|
|
1100
|
-
clearTimeout(workingIndicatorTimer);
|
|
1101
|
-
workingIndicatorTimer = 0;
|
|
1102
|
-
if (reducedMotion.matches) {
|
|
1103
|
-
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
1104
|
-
indicator.dataset.activeDots = '3,5';
|
|
1105
|
-
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
1109
|
-
}
|
|
1110
|
-
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
1111
|
-
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
1112
1031
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
1113
1032
|
function rememberToolbarPosition(left, top) {
|
|
1114
1033
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
@@ -1221,7 +1140,7 @@ export function initComty(options = {}) {
|
|
|
1221
1140
|
|
|
1222
1141
|
function prepareToolbarMotion() {
|
|
1223
1142
|
if (toolbarMotionFrame) return;
|
|
1224
|
-
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools,
|
|
1143
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools].filter(Boolean);
|
|
1225
1144
|
// Move each group's background with its icons, including when interrupted.
|
|
1226
1145
|
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
1227
1146
|
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
@@ -1464,7 +1383,7 @@ export function initComty(options = {}) {
|
|
|
1464
1383
|
return;
|
|
1465
1384
|
}
|
|
1466
1385
|
|
|
1467
|
-
if (
|
|
1386
|
+
if (session.edit && mode === "browse") {
|
|
1468
1387
|
closeBubbles();
|
|
1469
1388
|
}
|
|
1470
1389
|
const hadComments = session.comments;
|
|
@@ -1478,7 +1397,6 @@ export function initComty(options = {}) {
|
|
|
1478
1397
|
} else {
|
|
1479
1398
|
hideInspector();
|
|
1480
1399
|
}
|
|
1481
|
-
if (session.agent && sidepanelOpen) setSidepanelOpen(false);
|
|
1482
1400
|
syncToolbar();
|
|
1483
1401
|
ensureLiveTracking();
|
|
1484
1402
|
if (renderComments || !hadComments) renderCachedComments();
|
|
@@ -1490,11 +1408,8 @@ export function initComty(options = {}) {
|
|
|
1490
1408
|
: headers;
|
|
1491
1409
|
}
|
|
1492
1410
|
|
|
1493
|
-
function showAuthPrompt(forbidden = false) {
|
|
1494
|
-
|
|
1495
|
-
showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1411
|
+
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
1412
|
+
const guestAllowed = allowGuestComments;
|
|
1498
1413
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
1499
1414
|
const opener = document.activeElement;
|
|
1500
1415
|
const backdrop = document.createElement("div");
|
|
@@ -1505,12 +1420,23 @@ export function initComty(options = {}) {
|
|
|
1505
1420
|
<span class="comty-auth-icon">${icon('<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.5 8.5 0 1 1 16.1-3.8Z"/>')}</span>
|
|
1506
1421
|
<button type="button" class="comty-auth-close" data-comty-close aria-label="Close sign-in dialog">${icon('<path d="m6 6 12 12M6 18 18 6"/>')}</button>
|
|
1507
1422
|
</div>
|
|
1508
|
-
<h2 id="comty-auth-title">${forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
1509
|
-
<p id="comty-auth-description">${forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Sign in to share feedback with your team and keep the conversation in one place."}</p>
|
|
1510
|
-
|
|
1511
|
-
<
|
|
1512
|
-
|
|
1513
|
-
|
|
1423
|
+
<h2 id="comty-auth-title">${guestAllowed ? "Leave a comment" : forbidden ? "Project access required" : "Sign in to comment"}</h2>
|
|
1424
|
+
<p id="comty-auth-description">${guestAllowed ? "Continue with your name or sign in with Google." : forbidden ? "This account is not a member of this project. Ask the project owner for an invite." : "Sign in to share feedback with your team and keep the conversation in one place."}</p>
|
|
1425
|
+
${guestAllowed ? `
|
|
1426
|
+
<form class="comty-guest-form">
|
|
1427
|
+
<label for="comty-guest-name">Your name</label>
|
|
1428
|
+
<input id="comty-guest-name" name="name" autocomplete="name" placeholder="Your name" maxlength="80" required aria-describedby="comty-guest-error" />
|
|
1429
|
+
<p id="comty-guest-error" role="alert" hidden></p>
|
|
1430
|
+
<div class="comty-auth-actions comty-guest-actions">
|
|
1431
|
+
<button type="submit" class="comty-auth-primary" data-comty-guest>Continue</button>
|
|
1432
|
+
<span class="comty-auth-or">or</span>
|
|
1433
|
+
<button type="button" data-comty-login>Continue with Google</button>
|
|
1434
|
+
</div>
|
|
1435
|
+
</form>` : `
|
|
1436
|
+
<div class="comty-auth-actions">
|
|
1437
|
+
<button type="button" data-comty-close>${forbidden ? "Close" : "Cancel"}</button>
|
|
1438
|
+
${forbidden ? "" : '<button type="button" class="comty-auth-primary" data-comty-login>Continue to sign in</button>'}
|
|
1439
|
+
</div>`}
|
|
1514
1440
|
</div>`;
|
|
1515
1441
|
const close = () => {
|
|
1516
1442
|
backdrop.remove();
|
|
@@ -1521,11 +1447,12 @@ export function initComty(options = {}) {
|
|
|
1521
1447
|
event.stopPropagation();
|
|
1522
1448
|
if (event.key === "Escape") { event.preventDefault(); close(); }
|
|
1523
1449
|
if (event.key === "Tab") {
|
|
1524
|
-
const buttons = [...backdrop.querySelectorAll("button:not(:disabled)")];
|
|
1450
|
+
const buttons = [...backdrop.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
|
|
1525
1451
|
if (!buttons.length) { event.preventDefault(); return; }
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1452
|
+
event.preventDefault();
|
|
1453
|
+
const index = buttons.indexOf(document.activeElement);
|
|
1454
|
+
const next = (index + (event.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
|
|
1455
|
+
buttons[next].focus();
|
|
1529
1456
|
}
|
|
1530
1457
|
});
|
|
1531
1458
|
const loginButton = backdrop.querySelector("[data-comty-login]");
|
|
@@ -1533,15 +1460,62 @@ export function initComty(options = {}) {
|
|
|
1533
1460
|
loginButton.onclick = () => {
|
|
1534
1461
|
loginButton.disabled = true;
|
|
1535
1462
|
loginButton.innerText = "Connecting…";
|
|
1536
|
-
backdrop.querySelectorAll("
|
|
1537
|
-
|
|
1463
|
+
backdrop.querySelectorAll("button, input").forEach(control => { control.disabled = true; });
|
|
1464
|
+
try {
|
|
1465
|
+
sessionStorage.setItem(authReturnStorageKey, JSON.stringify({
|
|
1466
|
+
mode: onReady === startEditSession || session.mode === "off" ? "edit" : session.mode,
|
|
1467
|
+
entry: sessionEntry,
|
|
1468
|
+
sidepanelOpen,
|
|
1469
|
+
}));
|
|
1470
|
+
} catch { /* Resume in comment mode if browser storage is unavailable. */ }
|
|
1471
|
+
const destination = new URL("/api/auth/sign-in/google", apiUrl);
|
|
1472
|
+
destination.searchParams.set("callbackURL", loginUrl);
|
|
1473
|
+
setTimeout(() => window.location.assign(guestAllowed ? destination.toString() : loginUrl), 50);
|
|
1538
1474
|
};
|
|
1539
1475
|
}
|
|
1476
|
+
const guestForm = backdrop.querySelector(".comty-guest-form");
|
|
1477
|
+
if (guestForm) guestForm.onsubmit = async event => {
|
|
1478
|
+
event.preventDefault();
|
|
1479
|
+
const nameInput = guestForm.querySelector("input");
|
|
1480
|
+
const error = guestForm.querySelector("[role=alert]");
|
|
1481
|
+
const name = nameInput.value.trim();
|
|
1482
|
+
if (!name) { nameInput.focus(); return; }
|
|
1483
|
+
const controls = [...backdrop.querySelectorAll("button, input")];
|
|
1484
|
+
controls.forEach(control => { control.disabled = true; });
|
|
1485
|
+
error.hidden = true;
|
|
1486
|
+
const continueButton = guestForm.querySelector("[data-comty-guest]");
|
|
1487
|
+
continueButton.textContent = "Connecting…";
|
|
1488
|
+
try {
|
|
1489
|
+
const response = await fetch(`${apiUrl}/api/sdk/guest`, {
|
|
1490
|
+
method: "POST",
|
|
1491
|
+
headers: { "Content-Type": "application/json" },
|
|
1492
|
+
body: JSON.stringify({ projectKey, name }),
|
|
1493
|
+
});
|
|
1494
|
+
const data = await response.json();
|
|
1495
|
+
if (!response.ok) throw new Error(data.error || "Unable to continue. Please try again.");
|
|
1496
|
+
sdkToken = data.token;
|
|
1497
|
+
try { sessionStorage.setItem(tokenStorageKey, sdkToken); } catch { /* Keep the session in memory when browser storage is unavailable. */ }
|
|
1498
|
+
currentUser = data.user;
|
|
1499
|
+
canWrite = true;
|
|
1500
|
+
canManage = false;
|
|
1501
|
+
if (backdrop.isConnected) {
|
|
1502
|
+
close();
|
|
1503
|
+
onReady();
|
|
1504
|
+
}
|
|
1505
|
+
} catch (failure) {
|
|
1506
|
+
if (!backdrop.isConnected) return;
|
|
1507
|
+
error.textContent = failure.message || "Unable to connect. Please try again.";
|
|
1508
|
+
error.hidden = false;
|
|
1509
|
+
controls.forEach(control => { control.disabled = false; });
|
|
1510
|
+
continueButton.textContent = "Continue";
|
|
1511
|
+
nameInput.focus();
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1540
1514
|
backdrop.onclick = (event) => {
|
|
1541
1515
|
if (event.target === backdrop) close();
|
|
1542
1516
|
};
|
|
1543
1517
|
root.appendChild(backdrop);
|
|
1544
|
-
(loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
1518
|
+
(backdrop.querySelector("#comty-guest-name") || loginButton || backdrop.querySelector("[data-comty-close]")).focus({ preventScroll: true });
|
|
1545
1519
|
}
|
|
1546
1520
|
|
|
1547
1521
|
function formatBoxValue(value) {
|
|
@@ -1556,17 +1530,14 @@ export function initComty(options = {}) {
|
|
|
1556
1530
|
}
|
|
1557
1531
|
|
|
1558
1532
|
function syncToolbar() {
|
|
1559
|
-
if (commentTools.hidden !==
|
|
1533
|
+
if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
1560
1534
|
root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
|
|
1561
1535
|
browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
|
|
1562
1536
|
inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
|
|
1563
|
-
btn.setAttribute("aria-pressed", String(session.edit
|
|
1564
|
-
|
|
1565
|
-
commentTools.hidden = session.inspect || session.agent;
|
|
1566
|
-
if (agentTools) agentTools.hidden = !session.agent;
|
|
1537
|
+
btn.setAttribute("aria-pressed", String(session.edit));
|
|
1538
|
+
commentTools.hidden = session.inspect;
|
|
1567
1539
|
inspectorTools.hidden = !session.inspect;
|
|
1568
1540
|
syncInspectorTools();
|
|
1569
|
-
renderAgentPanel();
|
|
1570
1541
|
}
|
|
1571
1542
|
|
|
1572
1543
|
const inspectionLayer = document.createElement("div");
|
|
@@ -1741,7 +1712,8 @@ export function initComty(options = {}) {
|
|
|
1741
1712
|
add("background", css.backgroundColor, (tokens.background.length > 0 || !["rgba(0, 0, 0, 0)", "rgb(255, 255, 255)"].includes(css.backgroundColor)));
|
|
1742
1713
|
add("color", css.color);
|
|
1743
1714
|
if (getElementOwnTextSignature(el)) {
|
|
1744
|
-
add("font",
|
|
1715
|
+
add("font-size", css.fontSize);
|
|
1716
|
+
add("font-weight", css.fontWeight);
|
|
1745
1717
|
add("font-family", css.fontFamily);
|
|
1746
1718
|
add("line-height", css.lineHeight);
|
|
1747
1719
|
add("letter-spacing", css.letterSpacing, css.letterSpacing !== "normal");
|
|
@@ -1925,7 +1897,7 @@ export function initComty(options = {}) {
|
|
|
1925
1897
|
height: draftBubble.offsetHeight,
|
|
1926
1898
|
};
|
|
1927
1899
|
const pinBounds = draftPin?.getBoundingClientRect();
|
|
1928
|
-
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected,
|
|
1900
|
+
const signature = JSON.stringify([selector, rect, hoverRect, rows, !!selected, measureAlt, draftBounds, pinBounds, spacing, innerWidth, innerHeight]);
|
|
1929
1901
|
if (signature === inspectionSignature) return;
|
|
1930
1902
|
inspectionSignature = signature;
|
|
1931
1903
|
inspectionLayer.replaceChildren();
|
|
@@ -1951,36 +1923,34 @@ export function initComty(options = {}) {
|
|
|
1951
1923
|
const panel = document.createElement("div");
|
|
1952
1924
|
panel.className = "comty-inspector";
|
|
1953
1925
|
panel.setAttribute("aria-label", "Element properties");
|
|
1954
|
-
if (selected) panel.dataset.selected = "";
|
|
1955
|
-
const
|
|
1956
|
-
const
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1926
|
+
if (selected || draftPreview) panel.dataset.selected = "";
|
|
1927
|
+
const priority = ["background", "color", "font-family", "font-size", "font-weight", "margin", "line-height", "letter-spacing"];
|
|
1928
|
+
const orderedRows = [...rows.filter(([key]) => priority.includes(key)).sort((a, b) => priority.indexOf(a[0]) - priority.indexOf(b[0])), ...rows.filter(([key]) => !priority.includes(key))];
|
|
1929
|
+
const labels = { "font-family": "Font", "font-size": "Size", "font-weight": "Weight", "line-height": "Line height", "letter-spacing": "Spacing", "background": "Background", "color": "Color", "margin": "Margin" };
|
|
1930
|
+
const cssProperty = key => ({ radius: "border-radius", background: "background-color" })[key] || key;
|
|
1931
|
+
const copyValue = (value, tokens) => tokens?.length ? `var(${tokens[0]})` : value;
|
|
1932
|
+
const allProperties = `${selector} {\n${rows.map(([key, value, tokens]) => ` ${cssProperty(key)}: ${copyValue(value, tokens)};`).join("\n")}\n}`;
|
|
1933
|
+
const copyButton = (value, label) => `<button type="button" class="comty-inspector-copy" data-copy="${escapeInspectorText(value)}" aria-label="${escapeInspectorText(label)}" title="${escapeInspectorText(label)}"><i class="comty-property-copy-icon" aria-hidden="true">${copyIcon}${checkIcon}</i></button>`;
|
|
1934
|
+
panel.innerHTML = `<div class="comty-inspector-head"><span class="comty-inspector-selector" dir="rtl" title="${escapeInspectorText(selector)}"><bdi dir="ltr">${escapeInspectorText(selector)}</bdi></span>${copyButton(allProperties, "Copy all properties")}</div><div class="comty-inspector-details">${orderedRows.map(([key, value, tokens]) => {
|
|
1935
|
+
const label = labels[key] || key.replaceAll("-", " ").replace(/^./, c => c.toUpperCase());
|
|
1936
|
+
const isColor = key === "background" || key === "color";
|
|
1937
|
+
const picker = isColor ? "color" : key.startsWith("font-") || ["line-height", "letter-spacing"].includes(key) ? "font" : "";
|
|
1938
|
+
const displayValue = isColor ? colorDetails(value)?.hex || value : value;
|
|
1939
|
+
const valueMarkup = `${isColor ? '<span class="comty-inspector-swatch" aria-hidden="true"></span>' : ""}<span class="comty-inspector-text">${tokens?.length ? `<span class="comty-inspector-tokens">${tokens.map(escapeInspectorText).join(", ")}</span><span class="comty-inspector-secondary">${escapeInspectorText(displayValue)}</span>` : escapeInspectorText(displayValue)}</span>`;
|
|
1940
|
+
const valueElement = picker && selected
|
|
1941
|
+
? `<button type="button" class="comty-inspector-value comty-inspector-picker" data-picker="${picker}" data-property="${key}" aria-label="Open ${isColor ? key + " colour" : "typography"} details">${valueMarkup}</button>`
|
|
1942
|
+
: `<span class="comty-inspector-value" data-property="${key}">${valueMarkup}</span>`;
|
|
1943
|
+
return `<div class="comty-inspector-row"><span class="comty-inspector-label">${label}</span>${valueElement}${copyButton(tokens?.length ? tokens[0].replace(/^--/, "") : value, `Copy ${label}`)}</div>`;
|
|
1944
|
+
}).join("")}</div><span class="comty-property-feedback" role="status"></span>`;
|
|
1945
|
+
for (const swatch of panel.querySelectorAll('.comty-inspector-swatch')) {
|
|
1946
|
+
const key = swatch.parentElement.dataset.property;
|
|
1947
|
+
swatch.style.backgroundColor = rows.find(([name]) => name === key)?.[1];
|
|
1967
1948
|
}
|
|
1968
1949
|
panel.onclick = async (event) => {
|
|
1969
1950
|
const button = event.target.closest("button");
|
|
1970
1951
|
if (!button) return;
|
|
1971
|
-
if (button.
|
|
1972
|
-
|
|
1973
|
-
renderInspector();
|
|
1974
|
-
} else if (button.dataset.picker) {
|
|
1975
|
-
openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
1976
|
-
} else {
|
|
1977
|
-
try {
|
|
1978
|
-
await navigator.clipboard.writeText(button.dataset.copy);
|
|
1979
|
-
button.title = "Copied";
|
|
1980
|
-
} catch {
|
|
1981
|
-
button.title = "Clipboard unavailable — select the value to copy";
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1952
|
+
if (button.dataset.picker) openPropertyPanel(button.dataset.picker, target, button.dataset.property, button);
|
|
1953
|
+
else await copyPropertyValue(button, panel.querySelector(".comty-property-feedback"));
|
|
1984
1954
|
};
|
|
1985
1955
|
inspectionLayer.appendChild(panel);
|
|
1986
1956
|
const bounds = panel.getBoundingClientRect();
|
|
@@ -1998,8 +1968,24 @@ export function initComty(options = {}) {
|
|
|
1998
1968
|
}
|
|
1999
1969
|
return;
|
|
2000
1970
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
1971
|
+
const inset = 8, gap = 12;
|
|
1972
|
+
const clampX = x => Math.max(inset, Math.min(x, innerWidth - bounds.width - inset));
|
|
1973
|
+
const clampY = y => Math.max(inset, Math.min(y, innerHeight - bounds.height - inset));
|
|
1974
|
+
const x = clampX(rect.left), y = clampY(rect.top);
|
|
1975
|
+
const candidates = [
|
|
1976
|
+
{ left: x, top: rect.bottom + gap },
|
|
1977
|
+
{ left: rect.right + gap, top: y },
|
|
1978
|
+
{ left: rect.left - bounds.width - gap, top: y },
|
|
1979
|
+
{ left: x, top: rect.top - bounds.height - gap },
|
|
1980
|
+
];
|
|
1981
|
+
let position = candidates.find(({ left, top }) => left >= inset && top >= inset && left + bounds.width <= innerWidth - inset && top + bounds.height <= innerHeight - inset);
|
|
1982
|
+
if (!position) {
|
|
1983
|
+
// A viewport-sized target leaves no outside space; minimize the covered area.
|
|
1984
|
+
const overlap = ({ left, top }) => Math.max(0, Math.min(left + bounds.width, rect.right) - Math.max(left, rect.left)) * Math.max(0, Math.min(top + bounds.height, rect.bottom) - Math.max(top, rect.top));
|
|
1985
|
+
position = candidates.map(({ left, top }) => ({ left: clampX(left), top: clampY(top) })).sort((a, b) => overlap(a) - overlap(b))[0];
|
|
1986
|
+
}
|
|
1987
|
+
panel.style.left = `${position.left}px`;
|
|
1988
|
+
panel.style.top = `${position.top}px`;
|
|
2003
1989
|
}
|
|
2004
1990
|
|
|
2005
1991
|
function endSession() {
|
|
@@ -2014,13 +2000,6 @@ export function initComty(options = {}) {
|
|
|
2014
2000
|
setSession("edit", { renderComments: true });
|
|
2015
2001
|
}
|
|
2016
2002
|
|
|
2017
|
-
function startAgentSession() {
|
|
2018
|
-
if (!isLocalSite) return;
|
|
2019
|
-
if (session.agent) return;
|
|
2020
|
-
closeBubbles();
|
|
2021
|
-
setSession("agent", { renderComments: true });
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
2003
|
function toggleSidepanel() {
|
|
2025
2004
|
if (sidepanelOpen) {
|
|
2026
2005
|
setSidepanelOpen(false);
|
|
@@ -2044,7 +2023,7 @@ export function initComty(options = {}) {
|
|
|
2044
2023
|
showAuthPrompt();
|
|
2045
2024
|
return;
|
|
2046
2025
|
}
|
|
2047
|
-
if (session.
|
|
2026
|
+
if (session.inspect || !isSessionActive()) {
|
|
2048
2027
|
startEditSession();
|
|
2049
2028
|
return;
|
|
2050
2029
|
}
|
|
@@ -2395,15 +2374,6 @@ export function initComty(options = {}) {
|
|
|
2395
2374
|
return element;
|
|
2396
2375
|
}
|
|
2397
2376
|
|
|
2398
|
-
async function waitForCommentTargetReady(comment) {
|
|
2399
|
-
const el = resolveCommentElement(comment);
|
|
2400
|
-
if (!el) return false;
|
|
2401
|
-
|
|
2402
|
-
await waitForCustomElements(el);
|
|
2403
|
-
await waitForPaint();
|
|
2404
|
-
return true;
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2377
|
function getDeviceContext() {
|
|
2408
2378
|
const width = window.innerWidth;
|
|
2409
2379
|
const height = window.innerHeight;
|
|
@@ -2478,125 +2448,44 @@ export function initComty(options = {}) {
|
|
|
2478
2448
|
if (!session.edit || hadDraft) hideInspector();
|
|
2479
2449
|
document
|
|
2480
2450
|
.querySelectorAll(
|
|
2481
|
-
".comty-bubble, .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
2451
|
+
".comty-bubble:not([data-comty-capture-keep]), .comty-create-bubble, .comty-temp-pin, .comty-popover",
|
|
2482
2452
|
)
|
|
2483
2453
|
.forEach((x) => x.remove());
|
|
2484
2454
|
}
|
|
2485
2455
|
|
|
2486
|
-
const {
|
|
2487
|
-
|
|
2488
|
-
async function buildCommentScreenshotFormData(payload) {
|
|
2489
|
-
const formData = new FormData();
|
|
2490
|
-
|
|
2491
|
-
Object.entries(payload).forEach(([key, value]) => {
|
|
2492
|
-
if (value === undefined || value === null) return;
|
|
2493
|
-
formData.append(key, String(value));
|
|
2494
|
-
});
|
|
2495
|
-
|
|
2496
|
-
try {
|
|
2497
|
-
await waitForCommentTargetReady(payload);
|
|
2498
|
-
const screenshot = await captureViewportScreenshot();
|
|
2499
|
-
if (screenshot) {
|
|
2500
|
-
const extension =
|
|
2501
|
-
screenshot.blob.type === "image/svg+xml"
|
|
2502
|
-
? "svg"
|
|
2503
|
-
: screenshot.blob.type === "image/jpeg"
|
|
2504
|
-
? "jpg"
|
|
2505
|
-
: screenshot.blob.type === "image/png"
|
|
2506
|
-
? "png"
|
|
2507
|
-
: "webp";
|
|
2508
|
-
formData.append(
|
|
2509
|
-
"screenshot",
|
|
2510
|
-
screenshot.blob,
|
|
2511
|
-
`comment.${extension}`,
|
|
2512
|
-
);
|
|
2513
|
-
formData.append("screenshotWidth", String(screenshot.width));
|
|
2514
|
-
formData.append("screenshotHeight", String(screenshot.height));
|
|
2515
|
-
}
|
|
2516
|
-
} catch (error) {
|
|
2517
|
-
console.error("Comty: screenshot capture failed", error);
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
return formData;
|
|
2521
|
-
}
|
|
2456
|
+
const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
|
|
2522
2457
|
|
|
2523
|
-
async function uploadCommentScreenshot(
|
|
2524
|
-
commentId,
|
|
2525
|
-
payload,
|
|
2526
|
-
captureNodes = [],
|
|
2527
|
-
) {
|
|
2458
|
+
async function uploadCommentScreenshot(comment, context, point) {
|
|
2528
2459
|
try {
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
if (!formData.has("screenshot")) return;
|
|
2538
|
-
|
|
2460
|
+
const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
|
|
2461
|
+
if (!screenshot) return;
|
|
2462
|
+
const formData = new FormData();
|
|
2463
|
+
formData.append("projectKey", projectKey);
|
|
2464
|
+
formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
2465
|
+
formData.append("screenshotWidth", String(screenshot.width));
|
|
2466
|
+
formData.append("screenshotHeight", String(screenshot.height));
|
|
2539
2467
|
const response = await fetch(
|
|
2540
|
-
`${apiUrl}/api/comments/${encodeURIComponent(
|
|
2541
|
-
{
|
|
2542
|
-
method: "POST",
|
|
2543
|
-
headers: getSdkHeaders(),
|
|
2544
|
-
body: formData,
|
|
2545
|
-
},
|
|
2468
|
+
`${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
|
|
2469
|
+
{ method: "POST", headers: getSdkHeaders(), body: formData },
|
|
2546
2470
|
);
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
await loadComments({ force: true });
|
|
2471
|
+
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
2472
|
+
// Updating metadata must not redraw the page or interrupt a new draft.
|
|
2473
|
+
const { screenshotUrl } = await response.json();
|
|
2474
|
+
const cached = cachedComments.find(item => String(item.id) === String(comment.id));
|
|
2475
|
+
if (cached) Object.assign(cached, {
|
|
2476
|
+
screenshot_url: screenshotUrl,
|
|
2477
|
+
screenshot_width: screenshot.width,
|
|
2478
|
+
screenshot_height: screenshot.height,
|
|
2479
|
+
});
|
|
2557
2480
|
} catch (error) {
|
|
2558
|
-
console.
|
|
2559
|
-
captureNodes.forEach((node) => node.remove());
|
|
2481
|
+
console.warn("Comty: comment saved, screenshot unavailable", error);
|
|
2560
2482
|
}
|
|
2561
2483
|
}
|
|
2562
2484
|
|
|
2563
|
-
function createCaptureAnnotation(pin, bubble, content) {
|
|
2564
|
-
const capturePin = pin.cloneNode(false);
|
|
2565
|
-
capturePin.dataset.comtyCaptureKeep = "true";
|
|
2566
|
-
capturePin.classList.add("comty-capture-annotation");
|
|
2567
|
-
capturePin.classList.remove("comty-temp-pin");
|
|
2568
|
-
capturePin.style.display = "grid";
|
|
2569
|
-
capturePin.style.pointerEvents = "none";
|
|
2570
|
-
capturePin.style.animation = "none";
|
|
2571
|
-
capturePin.style.opacity = "1";
|
|
2572
|
-
capturePin.style.transform = "translate(-50%, -50%)";
|
|
2573
|
-
setPinIcon(capturePin, currentUser || { name: "You" });
|
|
2574
|
-
|
|
2575
|
-
const captureBubble = document.createElement("div");
|
|
2576
|
-
captureBubble.className =
|
|
2577
|
-
"comty-bubble comty-hover-bubble comty-capture-annotation";
|
|
2578
|
-
captureBubble.dataset.comtyCaptureKeep = "true";
|
|
2579
|
-
captureBubble.style.left = bubble.style.left;
|
|
2580
|
-
captureBubble.style.top = bubble.style.top;
|
|
2581
|
-
captureBubble.style.display = "block";
|
|
2582
|
-
captureBubble.style.pointerEvents = "none";
|
|
2583
|
-
captureBubble.style.animation = "none";
|
|
2584
|
-
captureBubble.style.opacity = "1";
|
|
2585
|
-
captureBubble.style.transform = "none";
|
|
2586
|
-
|
|
2587
|
-
const contentEl = document.createElement("div");
|
|
2588
|
-
contentEl.className = "comty-bubble-content";
|
|
2589
|
-
contentEl.innerText = content;
|
|
2590
|
-
captureBubble.appendChild(contentEl);
|
|
2591
|
-
|
|
2592
|
-
document.body.appendChild(capturePin);
|
|
2593
|
-
document.body.appendChild(captureBubble);
|
|
2594
|
-
return [capturePin, captureBubble];
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
2485
|
function addLocalComment(commentId, payload) {
|
|
2598
2486
|
const comment = {
|
|
2599
2487
|
id: commentId,
|
|
2488
|
+
pending: commentId.startsWith("pending-"),
|
|
2600
2489
|
project_id: projectId,
|
|
2601
2490
|
domain: payload.host || window.location.host,
|
|
2602
2491
|
page_path: payload.pagePath || window.location.pathname,
|
|
@@ -2604,6 +2493,7 @@ export function initComty(options = {}) {
|
|
|
2604
2493
|
rel_x: payload.relX,
|
|
2605
2494
|
rel_y: payload.relY,
|
|
2606
2495
|
content: payload.content || "",
|
|
2496
|
+
link_url: payload.linkUrl || null,
|
|
2607
2497
|
created_at: new Date().toISOString(),
|
|
2608
2498
|
dom_element: payload.domElement || null,
|
|
2609
2499
|
viewport: payload.viewport || null,
|
|
@@ -2615,7 +2505,9 @@ export function initComty(options = {}) {
|
|
|
2615
2505
|
screenshot_width: null,
|
|
2616
2506
|
screenshot_height: null,
|
|
2617
2507
|
anchor_context: payload.anchorContext || null,
|
|
2618
|
-
author_user_id: currentUser?.id || null,
|
|
2508
|
+
author_user_id: currentUser?.isGuest ? null : currentUser?.id || null,
|
|
2509
|
+
guest_author_id: currentUser?.isGuest ? currentUser.id : null,
|
|
2510
|
+
guest_author_name: currentUser?.isGuest ? currentUser.name : null,
|
|
2619
2511
|
author_name: currentUser?.name || null,
|
|
2620
2512
|
author_avatar_url: currentUser?.avatarUrl || null,
|
|
2621
2513
|
replies: [],
|
|
@@ -2630,6 +2522,7 @@ export function initComty(options = {}) {
|
|
|
2630
2522
|
];
|
|
2631
2523
|
renderSidepanelComments();
|
|
2632
2524
|
void renderComments(cachedComments, { retry: false });
|
|
2525
|
+
return comment;
|
|
2633
2526
|
}
|
|
2634
2527
|
|
|
2635
2528
|
function getCommentAuthorName(comment) {
|
|
@@ -2639,8 +2532,7 @@ export function initComty(options = {}) {
|
|
|
2639
2532
|
function isOwnCommentEntry(entry) {
|
|
2640
2533
|
return Boolean(
|
|
2641
2534
|
currentUser?.id &&
|
|
2642
|
-
entry?.author_user_id
|
|
2643
|
-
entry.author_user_id === currentUser.id,
|
|
2535
|
+
(currentUser.isGuest ? entry?.guest_author_id : entry?.author_user_id) === currentUser.id,
|
|
2644
2536
|
);
|
|
2645
2537
|
}
|
|
2646
2538
|
|
|
@@ -2716,7 +2608,7 @@ export function initComty(options = {}) {
|
|
|
2716
2608
|
|
|
2717
2609
|
function createThreadItem(
|
|
2718
2610
|
entry,
|
|
2719
|
-
{ showMore = isOwnCommentEntry(entry) } = {},
|
|
2611
|
+
{ showMore = isOwnCommentEntry(entry), showLink = true } = {},
|
|
2720
2612
|
) {
|
|
2721
2613
|
const item = document.createElement("div");
|
|
2722
2614
|
item.className = "comty-thread-item";
|
|
@@ -2751,15 +2643,51 @@ export function initComty(options = {}) {
|
|
|
2751
2643
|
|
|
2752
2644
|
const content = document.createElement("div");
|
|
2753
2645
|
content.className = "comty-bubble-content";
|
|
2754
|
-
content
|
|
2646
|
+
renderCommentContent(content, entry?.content || "");
|
|
2755
2647
|
|
|
2756
2648
|
body.appendChild(author);
|
|
2757
2649
|
body.appendChild(content);
|
|
2650
|
+
if (showLink && entry?.link_url) body.appendChild(createCommentLink(entry.link_url));
|
|
2758
2651
|
item.appendChild(body);
|
|
2759
2652
|
|
|
2760
2653
|
return { item, body, content, moreButton };
|
|
2761
2654
|
}
|
|
2762
2655
|
|
|
2656
|
+
function renderCommentContent(element, value) {
|
|
2657
|
+
const content = String(value ?? "");
|
|
2658
|
+
const nodes = [];
|
|
2659
|
+
let previousEnd = 0;
|
|
2660
|
+
for (const match of content.matchAll(/https?:\/\/[^\s<>"']+/gi)) {
|
|
2661
|
+
const url = match[0].replace(/[.,!?;:]+$/, "");
|
|
2662
|
+
if (!url) continue;
|
|
2663
|
+
let parsed;
|
|
2664
|
+
try { parsed = new URL(url); } catch { continue; }
|
|
2665
|
+
if (!["http:", "https:"].includes(parsed.protocol) || !parsed.hostname) continue;
|
|
2666
|
+
nodes.push(document.createTextNode(content.slice(previousEnd, match.index)));
|
|
2667
|
+
const link = document.createElement("a");
|
|
2668
|
+
link.href = url;
|
|
2669
|
+
link.target = "_blank";
|
|
2670
|
+
link.rel = "noopener noreferrer";
|
|
2671
|
+
link.textContent = url;
|
|
2672
|
+
nodes.push(link);
|
|
2673
|
+
previousEnd = match.index + url.length;
|
|
2674
|
+
}
|
|
2675
|
+
nodes.push(document.createTextNode(content.slice(previousEnd)));
|
|
2676
|
+
element.replaceChildren(...nodes);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function createCommentLink(url) {
|
|
2680
|
+
const link = document.createElement("a");
|
|
2681
|
+
link.className = "comty-comment-link";
|
|
2682
|
+
link.href = url;
|
|
2683
|
+
link.target = "_blank";
|
|
2684
|
+
link.rel = "noopener noreferrer";
|
|
2685
|
+
link.title = url;
|
|
2686
|
+
link.setAttribute("aria-label", `Open attached link: ${url}`);
|
|
2687
|
+
link.textContent = url;
|
|
2688
|
+
return link;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2763
2691
|
function setPinIcon(pin, comment = null) {
|
|
2764
2692
|
if (comment) {
|
|
2765
2693
|
pin.classList.add("comty-pin-with-avatar");
|
|
@@ -2776,6 +2704,54 @@ export function initComty(options = {}) {
|
|
|
2776
2704
|
`;
|
|
2777
2705
|
}
|
|
2778
2706
|
|
|
2707
|
+
function createCommentPin(comment, x, y) {
|
|
2708
|
+
const pin = document.createElement("button");
|
|
2709
|
+
pin.className = "comty-pin";
|
|
2710
|
+
pin.type = "button";
|
|
2711
|
+
pin.style.left = `${x}px`;
|
|
2712
|
+
pin.style.top = `${y}px`;
|
|
2713
|
+
setPinIcon(pin, comment);
|
|
2714
|
+
const statusDot = document.createElement("span");
|
|
2715
|
+
statusDot.className = "comty-pin-status-dot";
|
|
2716
|
+
applyStatusDot(statusDot, comment.status);
|
|
2717
|
+
pin.appendChild(statusDot);
|
|
2718
|
+
return pin;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function createCommentPreview(comment) {
|
|
2722
|
+
const bubble = document.createElement("div");
|
|
2723
|
+
bubble.className = "comty-bubble comty-hover-bubble";
|
|
2724
|
+
const thread = document.createElement("div");
|
|
2725
|
+
thread.className = "comty-thread";
|
|
2726
|
+
thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
|
|
2727
|
+
bubble.appendChild(thread);
|
|
2728
|
+
return bubble;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function mountCaptureAnnotation(comment, { x, y }) {
|
|
2732
|
+
const host = document.createElement("div");
|
|
2733
|
+
host.dataset.comtyCaptureExclude = "";
|
|
2734
|
+
host.setAttribute("aria-hidden", "true");
|
|
2735
|
+
host.inert = true;
|
|
2736
|
+
// Ancestor opacity hides measurement without changing the components'
|
|
2737
|
+
// computed opacity. Nothing flashes or intercepts input during cloning.
|
|
2738
|
+
Object.assign(host.style, { position: "fixed", inset: "0", opacity: "0", pointerEvents: "none" });
|
|
2739
|
+
const pin = createCommentPin(comment, x, y);
|
|
2740
|
+
const bubble = createCommentPreview(comment);
|
|
2741
|
+
for (const element of [pin, bubble]) {
|
|
2742
|
+
element.dataset.comtyCaptureKeep = "";
|
|
2743
|
+
element.style.setProperty("animation", "none", "important");
|
|
2744
|
+
element.style.setProperty("transition", "none", "important");
|
|
2745
|
+
element.style.setProperty("pointer-events", "none", "important");
|
|
2746
|
+
}
|
|
2747
|
+
pin.style.display = "grid";
|
|
2748
|
+
bubble.style.display = "block";
|
|
2749
|
+
host.append(pin, bubble);
|
|
2750
|
+
root.appendChild(host);
|
|
2751
|
+
positionCommentBubble(bubble, x, y);
|
|
2752
|
+
return { elements: [pin, bubble], dispose: () => host.remove() };
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2779
2755
|
function createCommentAuthor(comment) {
|
|
2780
2756
|
const author = document.createElement("div");
|
|
2781
2757
|
author.className = "comty-comment-author";
|
|
@@ -2819,74 +2795,126 @@ export function initComty(options = {}) {
|
|
|
2819
2795
|
|
|
2820
2796
|
const textarea = document.createElement("textarea");
|
|
2821
2797
|
textarea.className = "comty-create-textarea";
|
|
2822
|
-
textarea.placeholder =
|
|
2798
|
+
textarea.placeholder = "Add a comment";
|
|
2823
2799
|
textarea.setAttribute("aria-label", textarea.placeholder);
|
|
2824
2800
|
textarea.rows = 1;
|
|
2825
2801
|
|
|
2826
2802
|
const actions = document.createElement("div");
|
|
2827
2803
|
actions.className = "comty-create-actions";
|
|
2828
2804
|
|
|
2805
|
+
const linkBtn = document.createElement("button");
|
|
2806
|
+
linkBtn.type = "button";
|
|
2807
|
+
linkBtn.className = "comty-create-link-button";
|
|
2808
|
+
linkBtn.setAttribute("aria-label", "Add link");
|
|
2809
|
+
linkBtn.innerHTML = `
|
|
2810
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
2811
|
+
<path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
2812
|
+
<path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
2813
|
+
</svg>
|
|
2814
|
+
`;
|
|
2815
|
+
|
|
2816
|
+
const linkEditor = document.createElement("div");
|
|
2817
|
+
linkEditor.className = "comty-create-link-editor";
|
|
2818
|
+
linkEditor.inert = true;
|
|
2819
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
2820
|
+
const linkInput = document.createElement("input");
|
|
2821
|
+
linkInput.type = "url";
|
|
2822
|
+
linkInput.placeholder = "Paste a link";
|
|
2823
|
+
linkInput.setAttribute("aria-label", "Link URL");
|
|
2824
|
+
const linkInsert = document.createElement("button");
|
|
2825
|
+
linkInsert.type = "button";
|
|
2826
|
+
linkInsert.textContent = "Add";
|
|
2827
|
+
const linkCancel = document.createElement("button");
|
|
2828
|
+
linkCancel.type = "button";
|
|
2829
|
+
linkCancel.setAttribute("aria-label", "Cancel link");
|
|
2830
|
+
linkCancel.textContent = "×";
|
|
2831
|
+
const linkField = document.createElement("div");
|
|
2832
|
+
linkField.className = "comty-create-link-field";
|
|
2833
|
+
linkField.append(linkInput, linkCancel);
|
|
2834
|
+
linkEditor.append(linkField, linkInsert);
|
|
2835
|
+
|
|
2836
|
+
const attachment = document.createElement("div");
|
|
2837
|
+
attachment.className = "comty-create-attachment";
|
|
2838
|
+
const attachmentLink = document.createElement("a");
|
|
2839
|
+
attachmentLink.className = "comty-comment-link";
|
|
2840
|
+
attachmentLink.target = "_blank";
|
|
2841
|
+
attachmentLink.rel = "noopener noreferrer";
|
|
2842
|
+
const removeAttachment = document.createElement("button");
|
|
2843
|
+
removeAttachment.type = "button";
|
|
2844
|
+
removeAttachment.setAttribute("aria-label", "Remove attached link");
|
|
2845
|
+
removeAttachment.textContent = "×";
|
|
2846
|
+
const attachmentChip = document.createElement("div");
|
|
2847
|
+
attachmentChip.className = "comty-create-attachment-chip";
|
|
2848
|
+
attachmentChip.append(attachmentLink, removeAttachment);
|
|
2849
|
+
attachment.appendChild(attachmentChip);
|
|
2850
|
+
let attachedLink = "";
|
|
2851
|
+
|
|
2829
2852
|
const submitBtn = document.createElement("button");
|
|
2853
|
+
submitBtn.type = "button";
|
|
2830
2854
|
submitBtn.className = "comty-btn-submit";
|
|
2831
|
-
submitBtn.setAttribute("aria-label",
|
|
2855
|
+
submitBtn.setAttribute("aria-label", "Post comment");
|
|
2856
|
+
submitBtn.disabled = true;
|
|
2832
2857
|
submitBtn.innerHTML = `
|
|
2833
2858
|
<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">
|
|
2834
2859
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
2835
2860
|
</svg>
|
|
2836
2861
|
`;
|
|
2837
2862
|
|
|
2863
|
+
const editorDraft = draftAnchor;
|
|
2838
2864
|
const submit = async () => {
|
|
2839
2865
|
const content = textarea.value.trim();
|
|
2840
|
-
if (!content) return;
|
|
2866
|
+
if (!content || textarea.disabled) return;
|
|
2841
2867
|
|
|
2842
2868
|
submitBtn.disabled = true;
|
|
2843
2869
|
textarea.disabled = true;
|
|
2870
|
+
linkBtn.disabled = true;
|
|
2844
2871
|
|
|
2845
|
-
if (payload.kind === "agent" && !agent) {
|
|
2846
|
-
showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
|
|
2847
|
-
}
|
|
2848
2872
|
payload.content = content;
|
|
2873
|
+
payload.linkUrl = attachedLink || null;
|
|
2849
2874
|
textarea.textContent = content;
|
|
2850
2875
|
|
|
2876
|
+
const context = captureContext();
|
|
2877
|
+
const point = getPinPoint(pin);
|
|
2878
|
+
const pendingId = `pending-${crypto.randomUUID()}`;
|
|
2879
|
+
// Paint the comment immediately. The network save never waits for capture work.
|
|
2880
|
+
bubble.remove();
|
|
2881
|
+
pin.remove();
|
|
2882
|
+
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
2883
|
+
hideInspector();
|
|
2884
|
+
addLocalComment(pendingId, payload);
|
|
2851
2885
|
let post;
|
|
2852
2886
|
try {
|
|
2853
|
-
post = await fetch(
|
|
2887
|
+
post = await fetch(`${apiUrl}/api/comments`, {
|
|
2854
2888
|
method: "POST",
|
|
2855
|
-
headers:
|
|
2856
|
-
body: JSON.stringify(payload
|
|
2889
|
+
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
2890
|
+
body: JSON.stringify(payload),
|
|
2857
2891
|
});
|
|
2892
|
+
if (!post.ok) throw new Error(`Comment save failed (${post.status})`);
|
|
2893
|
+
const data = await post.json();
|
|
2894
|
+
if (!data?.id) throw new Error("Comment save returned no ID");
|
|
2895
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
2896
|
+
if (context.url === location.href) {
|
|
2897
|
+
const comment = addLocalComment(String(data.id), payload);
|
|
2898
|
+
// Start only after the save and immediate UI update; the capture yields to input.
|
|
2899
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
2900
|
+
} else void loadComments({ force: true });
|
|
2858
2901
|
} catch (error) {
|
|
2859
2902
|
console.error("Comty: save failed", error);
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
if (!post.ok) {
|
|
2867
|
-
console.error("Comty: save failed", await post.text());
|
|
2868
|
-
if (payload.kind === "agent") showAgentBridgeError();
|
|
2903
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
2904
|
+
renderSidepanelComments();
|
|
2905
|
+
void renderComments(cachedComments, { retry: false });
|
|
2906
|
+
// Keep the original editor and its text available for retry, including attachments.
|
|
2907
|
+
if (!draftAnchor) draftAnchor = editorDraft;
|
|
2908
|
+
root.append(pin, bubble);
|
|
2869
2909
|
submitBtn.disabled = false;
|
|
2870
2910
|
textarea.disabled = false;
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
: [];
|
|
2879
|
-
bubble.remove();
|
|
2880
|
-
pin.remove();
|
|
2881
|
-
draftAnchor = null;
|
|
2882
|
-
hideInspector();
|
|
2883
|
-
if (commentId) {
|
|
2884
|
-
if (payload.kind === "agent") showAgentTaskToast(String(commentId));
|
|
2885
|
-
else addLocalComment(String(commentId), payload);
|
|
2886
|
-
void loadComments({ force: true });
|
|
2887
|
-
if (payload.kind !== "agent") void uploadCommentScreenshot(String(commentId), payload, captureNodes);
|
|
2888
|
-
} else {
|
|
2889
|
-
void loadComments({ force: true });
|
|
2911
|
+
linkBtn.disabled = false;
|
|
2912
|
+
const errorMessage = document.createElement("p");
|
|
2913
|
+
errorMessage.setAttribute("role", "alert");
|
|
2914
|
+
errorMessage.textContent = "Comment not saved. Your draft is ready to retry.";
|
|
2915
|
+
bubble.querySelector('[role="alert"]')?.remove();
|
|
2916
|
+
bubble.append(errorMessage);
|
|
2917
|
+
if (bubble.isConnected) textarea.focus();
|
|
2890
2918
|
}
|
|
2891
2919
|
};
|
|
2892
2920
|
|
|
@@ -2895,8 +2923,63 @@ export function initComty(options = {}) {
|
|
|
2895
2923
|
submit();
|
|
2896
2924
|
};
|
|
2897
2925
|
|
|
2926
|
+
const closeLinkEditor = () => {
|
|
2927
|
+
bubble.classList.remove("comty-create-link-open");
|
|
2928
|
+
linkEditor.inert = true;
|
|
2929
|
+
linkEditor.setAttribute("aria-hidden", "true");
|
|
2930
|
+
linkInput.value = "";
|
|
2931
|
+
linkInput.removeAttribute("aria-invalid");
|
|
2932
|
+
linkInput.setCustomValidity("");
|
|
2933
|
+
textarea.focus();
|
|
2934
|
+
};
|
|
2935
|
+
linkBtn.onclick = () => {
|
|
2936
|
+
if (!bubble.classList.contains("comty-create-expanded")) return;
|
|
2937
|
+
bubble.classList.add("comty-create-link-open");
|
|
2938
|
+
linkEditor.inert = false;
|
|
2939
|
+
linkEditor.setAttribute("aria-hidden", "false");
|
|
2940
|
+
linkInput.focus();
|
|
2941
|
+
};
|
|
2942
|
+
linkCancel.onclick = closeLinkEditor;
|
|
2943
|
+
linkInput.oninput = () => {
|
|
2944
|
+
linkInput.removeAttribute("aria-invalid");
|
|
2945
|
+
linkInput.setCustomValidity("");
|
|
2946
|
+
};
|
|
2947
|
+
linkInput.onkeydown = (event) => {
|
|
2948
|
+
if (event.key === "Enter") { event.preventDefault(); linkInsert.click(); }
|
|
2949
|
+
if (event.key === "Escape") { event.preventDefault(); closeLinkEditor(); }
|
|
2950
|
+
};
|
|
2951
|
+
linkInsert.onclick = () => {
|
|
2952
|
+
const value = linkInput.value.trim();
|
|
2953
|
+
const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`;
|
|
2954
|
+
let url;
|
|
2955
|
+
try { url = new URL(candidate); } catch { /* Report below. */ }
|
|
2956
|
+
if (!url || !["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || /\s/.test(value) || String(url).length > 2048) {
|
|
2957
|
+
linkInput.setAttribute("aria-invalid", "true");
|
|
2958
|
+
linkInput.setCustomValidity("Enter a valid HTTP(S) URL.");
|
|
2959
|
+
linkInput.reportValidity();
|
|
2960
|
+
linkInput.focus();
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
attachedLink = String(url);
|
|
2964
|
+
attachmentLink.href = attachedLink;
|
|
2965
|
+
attachmentLink.title = attachedLink;
|
|
2966
|
+
attachmentLink.textContent = attachedLink;
|
|
2967
|
+
bubble.classList.add("comty-create-has-attachment");
|
|
2968
|
+
closeLinkEditor();
|
|
2969
|
+
const point = getPinPoint(pin);
|
|
2970
|
+
positionCommentBubble(bubble, point.x, point.y);
|
|
2971
|
+
renderInspector();
|
|
2972
|
+
};
|
|
2973
|
+
|
|
2974
|
+
removeAttachment.onclick = () => {
|
|
2975
|
+
attachedLink = "";
|
|
2976
|
+
bubble.classList.remove("comty-create-has-attachment");
|
|
2977
|
+
textarea.focus();
|
|
2978
|
+
textarea.oninput();
|
|
2979
|
+
};
|
|
2980
|
+
|
|
2898
2981
|
textarea.onkeydown = (e) => {
|
|
2899
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
2982
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
2900
2983
|
e.preventDefault();
|
|
2901
2984
|
submit();
|
|
2902
2985
|
}
|
|
@@ -2907,31 +2990,20 @@ export function initComty(options = {}) {
|
|
|
2907
2990
|
};
|
|
2908
2991
|
|
|
2909
2992
|
textarea.oninput = () => {
|
|
2910
|
-
textarea.
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
bubble.style.borderRadius = "16px";
|
|
2916
|
-
bubble.style.alignItems = "flex-end";
|
|
2917
|
-
} else {
|
|
2918
|
-
bubble.style.borderRadius = "999px";
|
|
2919
|
-
bubble.style.alignItems = "center";
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
if (textarea.value.trim().length > 0) {
|
|
2923
|
-
submitBtn.classList.add("comty-btn-active");
|
|
2924
|
-
} else {
|
|
2925
|
-
submitBtn.classList.remove("comty-btn-active");
|
|
2926
|
-
}
|
|
2993
|
+
const expanded = textarea.value.length > 0 || Boolean(attachedLink);
|
|
2994
|
+
bubble.classList.toggle("comty-create-expanded", expanded);
|
|
2995
|
+
if (!expanded && bubble.classList.contains("comty-create-link-open")) closeLinkEditor();
|
|
2996
|
+
submitBtn.disabled = !textarea.value.trim();
|
|
2997
|
+
submitBtn.classList.toggle("comty-btn-active", !submitBtn.disabled);
|
|
2927
2998
|
const point = getPinPoint(pin);
|
|
2928
2999
|
positionCommentBubble(bubble, point.x, point.y);
|
|
2929
3000
|
renderInspector();
|
|
2930
3001
|
};
|
|
2931
3002
|
|
|
2932
|
-
actions.
|
|
3003
|
+
actions.append(linkBtn, submitBtn, linkEditor);
|
|
2933
3004
|
|
|
2934
3005
|
bubble.appendChild(textarea);
|
|
3006
|
+
bubble.appendChild(attachment);
|
|
2935
3007
|
bubble.appendChild(actions);
|
|
2936
3008
|
|
|
2937
3009
|
root.appendChild(bubble);
|
|
@@ -2941,223 +3013,6 @@ export function initComty(options = {}) {
|
|
|
2941
3013
|
setTimeout(() => textarea.focus(), 10);
|
|
2942
3014
|
}
|
|
2943
3015
|
|
|
2944
|
-
function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
|
|
2945
|
-
|
|
2946
|
-
function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
|
|
2947
|
-
root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
|
|
2948
|
-
const toast = document.createElement("div");
|
|
2949
|
-
toast.className = "comty-auth-toast comty-agent-task-toast";
|
|
2950
|
-
toast.setAttribute("role", "alert");
|
|
2951
|
-
const issue = describeAgentError(message);
|
|
2952
|
-
toast.textContent = issue.title + ". " + issue.description;
|
|
2953
|
-
root.appendChild(toast);
|
|
2954
|
-
setTimeout(() => toast.remove(), 5000);
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
function showAgentTaskToast() {
|
|
2958
|
-
agentPanelOpen = true;
|
|
2959
|
-
agentPanelSignature = "";
|
|
2960
|
-
renderAgentPanel();
|
|
2961
|
-
}
|
|
2962
|
-
|
|
2963
|
-
function describeAgentError(error) {
|
|
2964
|
-
const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
|
|
2965
|
-
let value = error;
|
|
2966
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
2967
|
-
if (typeof value === 'string') {
|
|
2968
|
-
try { value = JSON.parse(value); continue; } catch {}
|
|
2969
|
-
const start = value.indexOf('{'), end = value.lastIndexOf('}');
|
|
2970
|
-
if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
|
|
2971
|
-
break;
|
|
2972
|
-
}
|
|
2973
|
-
if (!value || typeof value !== 'object') break;
|
|
2974
|
-
value = value.error || value.message || value.cause || value.stderr || value;
|
|
2975
|
-
}
|
|
2976
|
-
const message = typeof value === 'string' ? value : '';
|
|
2977
|
-
const source = raw + ' ' + message;
|
|
2978
|
-
const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
|
|
2979
|
-
let title, description;
|
|
2980
|
-
if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
|
|
2981
|
-
title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
|
|
2982
|
-
} else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
|
|
2983
|
-
title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
|
|
2984
|
-
} else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
|
|
2985
|
-
title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
|
|
2986
|
-
} else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
|
|
2987
|
-
title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
|
|
2988
|
-
} else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
|
|
2989
|
-
title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
|
|
2990
|
-
} else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
|
|
2991
|
-
title = 'Connection lost'; description = 'Check that your local development server and coding agent are running. Reconnect before retrying; your task history is kept.';
|
|
2992
|
-
} else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
|
|
2993
|
-
title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
|
|
2994
|
-
} else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
|
|
2995
|
-
title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
|
|
2996
|
-
} else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
|
|
2997
|
-
title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
|
|
2998
|
-
} else if (/completion was not verified/i.test(source)) {
|
|
2999
|
-
title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
|
|
3000
|
-
} else {
|
|
3001
|
-
title = 'Task could not finish';
|
|
3002
|
-
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.';
|
|
3003
|
-
}
|
|
3004
|
-
return { title, description, raw, message };
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
function renderAgentPanel() {
|
|
3008
|
-
if (!agent) return;
|
|
3009
|
-
let panel = root.querySelector(".comty-agent-panel");
|
|
3010
|
-
if (!panel) {
|
|
3011
|
-
panel = document.createElement("section");
|
|
3012
|
-
panel.className = "comty-agent-panel";
|
|
3013
|
-
panel.setAttribute("aria-label", "Local agent tasks");
|
|
3014
|
-
panel.hidden = true;
|
|
3015
|
-
// Keep the dock above inspection overlays and below the toolbar.
|
|
3016
|
-
root.insertBefore(panel, toolbar);
|
|
3017
|
-
}
|
|
3018
|
-
const open = session.agent && agentPanelOpen;
|
|
3019
|
-
if (open) {
|
|
3020
|
-
if (panel.hidden) {
|
|
3021
|
-
const rect = toolbar.getBoundingClientRect();
|
|
3022
|
-
panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
|
|
3023
|
-
document.documentElement.dataset.comtyAgentSide = panel.dataset.side;
|
|
3024
|
-
panel.dataset.closed = "";
|
|
3025
|
-
panel.hidden = false;
|
|
3026
|
-
panel.getBoundingClientRect();
|
|
3027
|
-
}
|
|
3028
|
-
if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
|
|
3029
|
-
} else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
|
|
3030
|
-
const transition = ++agentPanelTransition;
|
|
3031
|
-
panel.dataset.closed = "";
|
|
3032
|
-
const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
|
|
3033
|
-
if (reducedMotion.matches) finish();
|
|
3034
|
-
else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
|
|
3035
|
-
}
|
|
3036
|
-
panel.inert = !open;
|
|
3037
|
-
panel.setAttribute("aria-hidden", String(!open));
|
|
3038
|
-
document.documentElement.classList.toggle("comty-agent-docked", open);
|
|
3039
|
-
agentHistoryBtn?.setAttribute("aria-expanded", String(open));
|
|
3040
|
-
agentHistoryBtn?.setAttribute("aria-pressed", String(open));
|
|
3041
|
-
const signature = JSON.stringify([localTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
|
|
3042
|
-
if (signature === agentPanelSignature) return;
|
|
3043
|
-
agentPanelSignature = signature;
|
|
3044
|
-
root.querySelector('.comty-agent-error-popover')?.remove();
|
|
3045
|
-
const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
|
|
3046
|
-
const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
|
|
3047
|
-
const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
|
|
3048
|
-
const node = (tag, className, text) => {
|
|
3049
|
-
const element = document.createElement(tag);
|
|
3050
|
-
if (className) element.className = className;
|
|
3051
|
-
if (text != null) element.textContent = text;
|
|
3052
|
-
return element;
|
|
3053
|
-
};
|
|
3054
|
-
const codeIcon = '<path d="m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18"/>';
|
|
3055
|
-
const brand = () => { const element = node('span', 'comty-agent-brand'); element.innerHTML = icon(codeIcon); element.setAttribute('aria-hidden', 'true'); return element; };
|
|
3056
|
-
panel.replaceChildren();
|
|
3057
|
-
const header = node('header', 'comty-agent-panel-header');
|
|
3058
|
-
const heading = node('div', 'comty-agent-heading');
|
|
3059
|
-
heading.append(node('strong', '', 'Modifications'));
|
|
3060
|
-
const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
|
|
3061
|
-
toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
3062
|
-
toggle.setAttribute('aria-label', 'Close local agent sidebar'); toggle.title = 'Close sidebar'; toggle.dataset.focusKey = 'close';
|
|
3063
|
-
toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentHistoryBtn?.focus({ preventScroll: true }); };
|
|
3064
|
-
header.append(heading, toggle); panel.appendChild(header);
|
|
3065
|
-
const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
|
|
3066
|
-
const technicalDetails = (parent, raw, key) => {
|
|
3067
|
-
const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
|
|
3068
|
-
const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
|
|
3069
|
-
details.append(summary, node('pre', '', raw)); parent.appendChild(details);
|
|
3070
|
-
};
|
|
3071
|
-
const errorMessage = (parent, error, key) => {
|
|
3072
|
-
const issue = describeAgentError(error);
|
|
3073
|
-
const message = node('div', 'comty-agent-error');
|
|
3074
|
-
message.append(node('strong', '', issue.title), node('p', '', issue.description));
|
|
3075
|
-
parent.appendChild(message); technicalDetails(parent, issue.raw, key);
|
|
3076
|
-
};
|
|
3077
|
-
if (agentConnectionError || agentActionError) {
|
|
3078
|
-
const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
|
|
3079
|
-
errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
|
|
3080
|
-
}
|
|
3081
|
-
const action = (parent, label, path, input = {}, primary = false) => {
|
|
3082
|
-
const button = node('button', 'comty-agent-action', label); button.type = 'button';
|
|
3083
|
-
const key = path + JSON.stringify(input); button.dataset.focusKey = key;
|
|
3084
|
-
button.disabled = agentPendingActions.has(key);
|
|
3085
|
-
if (primary) button.dataset.primary = '';
|
|
3086
|
-
button.onclick = async () => {
|
|
3087
|
-
if (agentPendingActions.has(key)) return;
|
|
3088
|
-
agentActionError = '';
|
|
3089
|
-
agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
|
|
3090
|
-
try {
|
|
3091
|
-
const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
|
|
3092
|
-
if (!response.ok) {
|
|
3093
|
-
const body = await response.json().catch(() => null);
|
|
3094
|
-
throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
|
|
3095
|
-
}
|
|
3096
|
-
await loadComments({ force: true });
|
|
3097
|
-
} catch (error) { agentActionError = error.message; }
|
|
3098
|
-
finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
|
|
3099
|
-
};
|
|
3100
|
-
parent.appendChild(button);
|
|
3101
|
-
};
|
|
3102
|
-
for (const approval of localApprovals) {
|
|
3103
|
-
const section = node('div', 'comty-agent-item');
|
|
3104
|
-
section.append(node('h3', '', 'Approval required'));
|
|
3105
|
-
const task = localTasks.find(task => task.id === approval.taskId);
|
|
3106
|
-
if (task) section.appendChild(node('p', '', task.content));
|
|
3107
|
-
section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
|
|
3108
|
-
const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
|
|
3109
|
-
if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
|
|
3110
|
-
technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
|
|
3111
|
-
const actions = node('div', 'comty-agent-actions');
|
|
3112
|
-
action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
|
|
3113
|
-
action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
|
|
3114
|
-
section.appendChild(actions); content.appendChild(section);
|
|
3115
|
-
}
|
|
3116
|
-
const list = node('div', 'comty-agent-list'); content.appendChild(list);
|
|
3117
|
-
const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
|
|
3118
|
-
content.addEventListener('scroll', hideErrorPopover, { passive: true });
|
|
3119
|
-
for (const [index, task] of [...localTasks].reverse().entries()) {
|
|
3120
|
-
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
3121
|
-
const state = failed ? 'error' : task.status === 'done' ? 'success' : task.status === 'cancel' ? 'cancel' : 'working';
|
|
3122
|
-
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
3123
|
-
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
3124
|
-
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
3125
|
-
const name = node('span', 'comty-agent-row-name', task.content);
|
|
3126
|
-
row.append(dot, name);
|
|
3127
|
-
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
3128
|
-
const actions = node('div', 'comty-agent-row-actions');
|
|
3129
|
-
if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
|
|
3130
|
-
if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
|
|
3131
|
-
if (actions.childElementCount) row.appendChild(actions);
|
|
3132
|
-
row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
|
|
3133
|
-
if (failed) {
|
|
3134
|
-
const tooltipId = `comty-agent-error-${index}`;
|
|
3135
|
-
const showErrorPopover = () => {
|
|
3136
|
-
hideErrorPopover();
|
|
3137
|
-
const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
|
|
3138
|
-
const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
|
|
3139
|
-
popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
|
|
3140
|
-
root.appendChild(popover);
|
|
3141
|
-
const rowRect = row.getBoundingClientRect();
|
|
3142
|
-
const popoverRect = popover.getBoundingClientRect();
|
|
3143
|
-
const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
|
|
3144
|
-
popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
|
|
3145
|
-
popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
|
|
3146
|
-
};
|
|
3147
|
-
row.addEventListener('mouseenter', showErrorPopover);
|
|
3148
|
-
row.addEventListener('mouseleave', hideErrorPopover);
|
|
3149
|
-
}
|
|
3150
|
-
list.appendChild(row);
|
|
3151
|
-
}
|
|
3152
|
-
if (!localTasks.length && !localApprovals.length) {
|
|
3153
|
-
const empty = node('div', 'comty-agent-empty');
|
|
3154
|
-
empty.append(brand(), node('strong', '', localTasks.length ? 'All caught up' : 'What shall we change?'), node('p', '', 'Point at an element on the page and describe the change you want.'));
|
|
3155
|
-
content.appendChild(empty);
|
|
3156
|
-
}
|
|
3157
|
-
content.scrollTop = scrollTop;
|
|
3158
|
-
if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
3016
|
function setSidepanelOpen(open) {
|
|
3162
3017
|
sidepanelOpen = open;
|
|
3163
3018
|
sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
|
|
@@ -3211,7 +3066,6 @@ export function initComty(options = {}) {
|
|
|
3211
3066
|
? sidepanelSearch.value.trim().toLowerCase()
|
|
3212
3067
|
: "";
|
|
3213
3068
|
let list = cachedComments.filter((comment) => {
|
|
3214
|
-
if (comment.local_agent || comment.kind === "agent") return false;
|
|
3215
3069
|
if (
|
|
3216
3070
|
search &&
|
|
3217
3071
|
!`${comment.content ?? ""} ${getCommentLabel(comment)}`
|
|
@@ -3258,6 +3112,7 @@ export function initComty(options = {}) {
|
|
|
3258
3112
|
const item = document.createElement("button");
|
|
3259
3113
|
item.type = "button";
|
|
3260
3114
|
item.className = "comty-sidepanel-item";
|
|
3115
|
+
item.disabled = Boolean(comment.pending);
|
|
3261
3116
|
item.innerHTML = `
|
|
3262
3117
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
3263
3118
|
<span class="comty-sidepanel-item-body">
|
|
@@ -3278,7 +3133,7 @@ export function initComty(options = {}) {
|
|
|
3278
3133
|
|
|
3279
3134
|
const metaEl = item.querySelector(".comty-sidepanel-item-meta");
|
|
3280
3135
|
if (metaEl instanceof HTMLElement) {
|
|
3281
|
-
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${getCommentLabel(comment)}`;
|
|
3136
|
+
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
3282
3137
|
metaEl.title = getCommentLabel(comment);
|
|
3283
3138
|
}
|
|
3284
3139
|
|
|
@@ -3397,15 +3252,14 @@ export function initComty(options = {}) {
|
|
|
3397
3252
|
createStatusFilters();
|
|
3398
3253
|
|
|
3399
3254
|
async function updateCommentStatus(comment, status) {
|
|
3400
|
-
if (
|
|
3255
|
+
if (!projectId || !comment?.id) return false;
|
|
3401
3256
|
|
|
3402
3257
|
let res;
|
|
3403
3258
|
try {
|
|
3404
|
-
|
|
3405
|
-
res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
|
|
3259
|
+
res = await fetch(`${apiUrl}/api/comments/${comment.id}/status`, {
|
|
3406
3260
|
method: "POST",
|
|
3407
|
-
headers:
|
|
3408
|
-
body: JSON.stringify(
|
|
3261
|
+
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
3262
|
+
body: JSON.stringify({
|
|
3409
3263
|
projectKey,
|
|
3410
3264
|
host: window.location.host,
|
|
3411
3265
|
status,
|
|
@@ -3422,7 +3276,7 @@ export function initComty(options = {}) {
|
|
|
3422
3276
|
}
|
|
3423
3277
|
|
|
3424
3278
|
const data = await res.json();
|
|
3425
|
-
comment.status = getCommentStatus(data.
|
|
3279
|
+
comment.status = getCommentStatus(data.status).value;
|
|
3426
3280
|
return comment.status;
|
|
3427
3281
|
}
|
|
3428
3282
|
|
|
@@ -3610,11 +3464,13 @@ export function initComty(options = {}) {
|
|
|
3610
3464
|
const openCommentThread = createCommentThread({
|
|
3611
3465
|
root, apiUrl, projectKey, commentStatuses, selectedStatusFilters, placedPins,
|
|
3612
3466
|
closeBubbles, getCommentStatus, updateCommentStatus, applyStatusDot,
|
|
3613
|
-
renderSidepanelComments, applyStatusFilters,
|
|
3467
|
+
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
3614
3468
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
3615
3469
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
3616
3470
|
showAuthPrompt,
|
|
3617
3471
|
get canWrite() { return canWrite; },
|
|
3472
|
+
get canManage() { return canManage; },
|
|
3473
|
+
get allowGuestComments() { return allowGuestComments; },
|
|
3618
3474
|
get currentUser() { return currentUser; },
|
|
3619
3475
|
get cachedComments() { return cachedComments; },
|
|
3620
3476
|
set cachedComments(value) { cachedComments = value; },
|
|
@@ -3664,20 +3520,11 @@ export function initComty(options = {}) {
|
|
|
3664
3520
|
const x = visibility.clientX;
|
|
3665
3521
|
const y = visibility.clientY;
|
|
3666
3522
|
|
|
3667
|
-
const pin =
|
|
3668
|
-
pin.
|
|
3669
|
-
pin.type = "button";
|
|
3523
|
+
const pin = createCommentPin(comment, x, y);
|
|
3524
|
+
pin.disabled = Boolean(comment.pending);
|
|
3670
3525
|
pin.dataset.status = getCommentStatus(comment.status).value;
|
|
3671
|
-
if (comment.id)
|
|
3672
|
-
|
|
3673
|
-
}
|
|
3674
|
-
pin.style.left = `${x}px`;
|
|
3675
|
-
pin.style.top = `${y}px`;
|
|
3676
|
-
setPinIcon(pin, comment);
|
|
3677
|
-
const pinStatusDot = document.createElement("span");
|
|
3678
|
-
pinStatusDot.className = "comty-pin-status-dot";
|
|
3679
|
-
applyStatusDot(pinStatusDot, comment.status);
|
|
3680
|
-
pin.appendChild(pinStatusDot);
|
|
3526
|
+
if (comment.id) pin.dataset.commentId = String(comment.id);
|
|
3527
|
+
const pinStatusDot = pin.querySelector(".comty-pin-status-dot");
|
|
3681
3528
|
pin.setAttribute(
|
|
3682
3529
|
"aria-label",
|
|
3683
3530
|
`Afficher le commentaire, statut ${getCommentStatus(comment.status).label}`,
|
|
@@ -3719,32 +3566,14 @@ export function initComty(options = {}) {
|
|
|
3719
3566
|
activeHoverCommentIds.add(commentId);
|
|
3720
3567
|
}
|
|
3721
3568
|
|
|
3722
|
-
const hoverBubble =
|
|
3723
|
-
hoverBubble.className = "comty-bubble comty-hover-bubble";
|
|
3569
|
+
const hoverBubble = createCommentPreview(comment);
|
|
3724
3570
|
hoverBubble.style.left = `${x + 32}px`;
|
|
3725
3571
|
hoverBubble.style.top = `${y - 17}px`;
|
|
3726
|
-
hoverBubble.style.pointerEvents = "none";
|
|
3727
3572
|
hoverBubble.dataset.hoverId = commentId;
|
|
3728
3573
|
|
|
3729
|
-
const hoverThread = document.createElement("div");
|
|
3730
|
-
hoverThread.className = "comty-thread";
|
|
3731
|
-
const hoverItem = createThreadItem(comment, { showMore: false });
|
|
3732
|
-
hoverThread.appendChild(hoverItem.item);
|
|
3733
|
-
hoverBubble.appendChild(hoverThread);
|
|
3734
|
-
|
|
3735
3574
|
root.appendChild(hoverBubble);
|
|
3736
3575
|
|
|
3737
|
-
requestAnimationFrame(() =>
|
|
3738
|
-
if (x + 16 + hoverBubble.offsetWidth > window.innerWidth) {
|
|
3739
|
-
hoverBubble.style.left = `${x - hoverBubble.offsetWidth - 24}px`;
|
|
3740
|
-
hoverBubble.classList.add("comty-bubble-left");
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
const rect = hoverBubble.getBoundingClientRect();
|
|
3744
|
-
if (rect.bottom > window.innerHeight) {
|
|
3745
|
-
hoverBubble.style.top = `${y - rect.height - 16}px`;
|
|
3746
|
-
}
|
|
3747
|
-
});
|
|
3576
|
+
requestAnimationFrame(() => positionCommentBubble(hoverBubble, x, y));
|
|
3748
3577
|
};
|
|
3749
3578
|
|
|
3750
3579
|
pin.onmouseleave = () => {
|
|
@@ -3839,12 +3668,15 @@ export function initComty(options = {}) {
|
|
|
3839
3668
|
function reconcileComments(comments) {
|
|
3840
3669
|
const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
|
|
3841
3670
|
cachedComments = comments
|
|
3842
|
-
.filter(comment => comment.kind !== "agent"
|
|
3671
|
+
.filter(comment => comment.kind !== "agent" && !comment.local_agent)
|
|
3843
3672
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
3673
|
+
for (const comment of previous.values()) {
|
|
3674
|
+
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
3675
|
+
}
|
|
3844
3676
|
}
|
|
3845
3677
|
|
|
3846
3678
|
async function loadComments({ force = false } = {}) {
|
|
3847
|
-
if (!projectId
|
|
3679
|
+
if (!projectId) return;
|
|
3848
3680
|
const pagePath = window.location.pathname;
|
|
3849
3681
|
if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
|
|
3850
3682
|
if (pendingCommentsLoad) {
|
|
@@ -3852,7 +3684,7 @@ export function initComty(options = {}) {
|
|
|
3852
3684
|
if (!force) return;
|
|
3853
3685
|
}
|
|
3854
3686
|
pendingCommentsLoad = (async () => {
|
|
3855
|
-
let comments = cachedPagePath === pagePath ? cachedComments
|
|
3687
|
+
let comments = cachedPagePath === pagePath ? cachedComments : [];
|
|
3856
3688
|
if (projectId) {
|
|
3857
3689
|
try {
|
|
3858
3690
|
const url = new URL(`${apiUrl}/api/comments`);
|
|
@@ -3863,37 +3695,15 @@ export function initComty(options = {}) {
|
|
|
3863
3695
|
if (Array.isArray(data.comments)) comments = data.comments;
|
|
3864
3696
|
} catch (error) { console.error("Comty: list comments failed", error); }
|
|
3865
3697
|
}
|
|
3866
|
-
if (agent) {
|
|
3867
|
-
try {
|
|
3868
|
-
const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
|
|
3869
|
-
if (!res.ok) throw new Error("Local companion unavailable");
|
|
3870
|
-
const data = await res.json();
|
|
3871
|
-
if (Array.isArray(data.tasks)) localTasks = data.tasks;
|
|
3872
|
-
if (Array.isArray(data.approvals)) localApprovals = data.approvals;
|
|
3873
|
-
agentConnectionError = "";
|
|
3874
|
-
} catch (error) {
|
|
3875
|
-
agentConnectionError = "Local agent disconnected. Check the development server, then reload this page. Your task history is kept.";
|
|
3876
|
-
console.error("Comty: local tasks failed", error);
|
|
3877
|
-
}
|
|
3878
|
-
comments.push(...localTasks.filter(task => task.page_path === pagePath && !["done", "cancel"].includes(task.status)));
|
|
3879
|
-
renderAgentPanel();
|
|
3880
|
-
}
|
|
3881
3698
|
reconcileComments(comments);
|
|
3882
3699
|
cachedPagePath = pagePath;
|
|
3883
3700
|
renderSidepanelComments();
|
|
3884
3701
|
await renderComments(cachedComments);
|
|
3885
|
-
syncAgentWorkingOutlines();
|
|
3886
3702
|
ensureLiveTracking();
|
|
3887
3703
|
})();
|
|
3888
3704
|
try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
|
|
3889
3705
|
}
|
|
3890
3706
|
|
|
3891
|
-
if (agent) {
|
|
3892
|
-
setInterval(() => {
|
|
3893
|
-
if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
|
|
3894
|
-
}, 2000);
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
3707
|
function getPinPoint(pin) {
|
|
3898
3708
|
return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
|
|
3899
3709
|
}
|
|
@@ -3960,50 +3770,16 @@ export function initComty(options = {}) {
|
|
|
3960
3770
|
}
|
|
3961
3771
|
}
|
|
3962
3772
|
|
|
3963
|
-
function syncAgentWorkingOutlines() {
|
|
3964
|
-
const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === window.location.pathname);
|
|
3965
|
-
const ids = new Set(working.map(task => String(task.id)));
|
|
3966
|
-
for (const [id, outline] of workingOutlines) {
|
|
3967
|
-
if (ids.has(id)) continue;
|
|
3968
|
-
outline.remove(); workingOutlines.delete(id);
|
|
3969
|
-
}
|
|
3970
|
-
const modal = getTopActiveModal();
|
|
3971
|
-
for (const task of working) {
|
|
3972
|
-
const id = String(task.id);
|
|
3973
|
-
let outline = workingOutlines.get(id);
|
|
3974
|
-
if (!outline) {
|
|
3975
|
-
outline = document.createElement("div");
|
|
3976
|
-
outline.className = "comty-agent-working";
|
|
3977
|
-
outline.setAttribute("aria-label", "Agent working on this element");
|
|
3978
|
-
const badge = document.createElement("span");
|
|
3979
|
-
badge.className = "comty-agent-working-badge";
|
|
3980
|
-
badge.append(createWorkingIndicator(), "Working");
|
|
3981
|
-
outline.appendChild(badge); root.appendChild(outline);
|
|
3982
|
-
workingOutlines.set(id, outline);
|
|
3983
|
-
}
|
|
3984
|
-
const target = resolveCommentElement(task);
|
|
3985
|
-
const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
|
|
3986
|
-
outline.hidden = !point;
|
|
3987
|
-
if (!point) continue;
|
|
3988
|
-
const rect = target.getBoundingClientRect();
|
|
3989
|
-
setMeasuredRect(outline, rect);
|
|
3990
|
-
outline.toggleAttribute("data-badge-below", rect.top < 32);
|
|
3991
|
-
}
|
|
3992
|
-
}
|
|
3993
|
-
|
|
3994
3773
|
function onFrame() {
|
|
3995
3774
|
if (motion.enabled) updateMotion();
|
|
3996
3775
|
if (session.comments) syncCommentAnchors();
|
|
3997
|
-
if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === window.location.pathname)) {
|
|
3998
|
-
syncAgentWorkingOutlines();
|
|
3999
|
-
}
|
|
4000
3776
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
4001
3777
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
4002
3778
|
if (session.inspect && rulersEnabled) measurements.render();
|
|
4003
3779
|
}
|
|
4004
3780
|
|
|
4005
3781
|
function ensureLiveTracking() {
|
|
4006
|
-
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0)
|
|
3782
|
+
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
|
|
4007
3783
|
if (!active || document.hidden) {
|
|
4008
3784
|
if (liveFrame) cancelAnimationFrame(liveFrame);
|
|
4009
3785
|
liveFrame = null;
|
|
@@ -4023,14 +3799,13 @@ export function initComty(options = {}) {
|
|
|
4023
3799
|
syncOverlayHost();
|
|
4024
3800
|
|
|
4025
3801
|
function renderCachedComments() {
|
|
4026
|
-
if (!session.comments && !session.inspect && !session.edit
|
|
3802
|
+
if (!session.comments && !session.inspect && !session.edit) return;
|
|
4027
3803
|
if (resizeRenderFrame) {
|
|
4028
3804
|
window.cancelAnimationFrame(resizeRenderFrame);
|
|
4029
3805
|
}
|
|
4030
3806
|
resizeRenderFrame = window.requestAnimationFrame(() => {
|
|
4031
3807
|
resizeRenderFrame = null;
|
|
4032
3808
|
syncCommentAnchors();
|
|
4033
|
-
syncAgentWorkingOutlines();
|
|
4034
3809
|
renderInspector();
|
|
4035
3810
|
ensureLiveTracking();
|
|
4036
3811
|
});
|
|
@@ -4118,7 +3893,6 @@ export function initComty(options = {}) {
|
|
|
4118
3893
|
hideInspector();
|
|
4119
3894
|
return;
|
|
4120
3895
|
}
|
|
4121
|
-
if (inspectedElement !== target) inspectExpanded = selectedElement ? inspectExpanded : false;
|
|
4122
3896
|
showInspector(target);
|
|
4123
3897
|
}, true);
|
|
4124
3898
|
document.documentElement.addEventListener("pointerleave", () => {
|
|
@@ -4175,7 +3949,6 @@ export function initComty(options = {}) {
|
|
|
4175
3949
|
}
|
|
4176
3950
|
measuredRegion = null;
|
|
4177
3951
|
selectedElement = getAnchorElement(target);
|
|
4178
|
-
inspectExpanded = false;
|
|
4179
3952
|
showInspector(target);
|
|
4180
3953
|
return;
|
|
4181
3954
|
}
|
|
@@ -4211,12 +3984,11 @@ export function initComty(options = {}) {
|
|
|
4211
3984
|
const y = e.clientY;
|
|
4212
3985
|
|
|
4213
3986
|
const anchorContext = getAnchorContext(el);
|
|
4214
|
-
if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
|
|
4215
3987
|
|
|
4216
3988
|
const payload = {
|
|
4217
3989
|
...getDeviceContext(),
|
|
4218
3990
|
projectId,
|
|
4219
|
-
kind:
|
|
3991
|
+
kind: "comment",
|
|
4220
3992
|
host: window.location.host,
|
|
4221
3993
|
pagePath: window.location.pathname,
|
|
4222
3994
|
selector: getSelector(el),
|
|
@@ -4316,7 +4088,6 @@ export function initComty(options = {}) {
|
|
|
4316
4088
|
window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
|
|
4317
4089
|
|
|
4318
4090
|
async function boot() {
|
|
4319
|
-
if (localOnly) { await loadComments(); return; }
|
|
4320
4091
|
const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
|
|
4321
4092
|
method: "POST",
|
|
4322
4093
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
@@ -4329,20 +4100,32 @@ export function initComty(options = {}) {
|
|
|
4329
4100
|
|
|
4330
4101
|
if (!res.ok) {
|
|
4331
4102
|
console.error("Comty: bootstrap failed", await res.text());
|
|
4332
|
-
if (agent) await loadComments();
|
|
4333
4103
|
return;
|
|
4334
4104
|
}
|
|
4335
4105
|
|
|
4336
4106
|
const data = await res.json();
|
|
4337
4107
|
projectId = data.projectId;
|
|
4338
4108
|
canWrite = Boolean(data.canWrite);
|
|
4109
|
+
canManage = Boolean(data.canManage ?? data.canWrite);
|
|
4110
|
+
allowGuestComments = Boolean(data.allowGuestComments);
|
|
4339
4111
|
currentUser = data.user || null;
|
|
4340
4112
|
loginUrl = data.loginUrl || "";
|
|
4341
4113
|
|
|
4342
4114
|
if (!canWrite && sdkToken) {
|
|
4343
|
-
sessionStorage.removeItem(tokenStorageKey);
|
|
4115
|
+
try { sessionStorage.removeItem(tokenStorageKey); } catch { /* Storage may be unavailable. */ }
|
|
4344
4116
|
sdkToken = "";
|
|
4345
4117
|
}
|
|
4118
|
+
if (didCompleteSignIn || authStatus === "forbidden") {
|
|
4119
|
+
let previousSession;
|
|
4120
|
+
try {
|
|
4121
|
+
previousSession = JSON.parse(sessionStorage.getItem(authReturnStorageKey) || "null");
|
|
4122
|
+
sessionStorage.removeItem(authReturnStorageKey);
|
|
4123
|
+
} catch { /* Fall back to the requested comment mode. */ }
|
|
4124
|
+
const mode = canWrite && ["browse", "edit", "inspect"].includes(previousSession?.mode)
|
|
4125
|
+
? previousSession.mode : canWrite ? "edit" : "browse";
|
|
4126
|
+
setSession(mode, { entry: previousSession?.entry || mode });
|
|
4127
|
+
if (previousSession?.sidepanelOpen) setSidepanelOpen(true);
|
|
4128
|
+
}
|
|
4346
4129
|
if (authStatus === "forbidden") showAuthPrompt(true);
|
|
4347
4130
|
if (canWrite && didCompleteSignIn) {
|
|
4348
4131
|
const toast = document.createElement("div");
|
|
@@ -4365,34 +4148,19 @@ export function initComty(options = {}) {
|
|
|
4365
4148
|
getDistanceSegments, getInspectRows, syncCommentAnchors,
|
|
4366
4149
|
setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
|
|
4367
4150
|
startEditSession, startBrowseSession, endSession, showInspector,
|
|
4368
|
-
motion, setMotionEnabled, updateMotion,
|
|
4369
|
-
previewAgent(tasks, approvals = [], error = "") {
|
|
4370
|
-
localTasks = tasks;
|
|
4371
|
-
localApprovals = approvals;
|
|
4372
|
-
agentConnectionError = error;
|
|
4373
|
-
startAgentSession();
|
|
4374
|
-
agentPanelOpen = true;
|
|
4375
|
-
agentPanelSignature = "";
|
|
4376
|
-
renderAgentPanel();
|
|
4377
|
-
},
|
|
4151
|
+
motion, setMotionEnabled, updateMotion,
|
|
4378
4152
|
seed(comments) {
|
|
4379
4153
|
reconcileComments(comments);
|
|
4380
4154
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
4381
4155
|
return renderComments(cachedComments, { retry: false });
|
|
4382
4156
|
},
|
|
4383
|
-
|
|
4384
|
-
localTasks = tasks;
|
|
4385
|
-
syncAgentWorkingOutlines();
|
|
4386
|
-
ensureLiveTracking();
|
|
4387
|
-
},
|
|
4388
|
-
setCanWrite(value) { canWrite = value; },
|
|
4157
|
+
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
4389
4158
|
invalidate() { anchorRevision += 1; },
|
|
4390
4159
|
});
|
|
4391
4160
|
}
|
|
4392
4161
|
|
|
4393
4162
|
return boot().catch(error => {
|
|
4394
4163
|
console.error("Comty: bootstrap failed", error);
|
|
4395
|
-
if (agent) return loadComments();
|
|
4396
4164
|
});
|
|
4397
4165
|
}
|
|
4398
4166
|
}
|