@comty.io/core 0.1.7 → 0.1.9
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 +9 -4
- package/comty-sdk.js +72 -64
- package/package.json +2 -1
- package/src/anchors.js +26 -16
- package/src/browser.d.ts +1 -1
- package/src/browser.js +46 -48
package/README.md
CHANGED
|
@@ -10,17 +10,19 @@ Comments use the pathname and hash routes starting with `#/` or `#!/`. Plain fra
|
|
|
10
10
|
|
|
11
11
|
Target matching combines content, attributes and surrounding structure. Small changes inside a container can preserve its comments; ambiguous matches do not place a pin. The sidebar shows **Target missing** when no target is found and **Target uncertain** when placement cannot be determined reliably. Both still open the full thread. Closed or unmounted known overlays do not count as deleted elements, and their comments never attach to matching content outside that overlay.
|
|
12
12
|
|
|
13
|
+
New anchors exclude editable content from inputs, textareas, selects and rich-text editors, including when comparing their surrounding containers. Repeated fields use their attributes and question context to distinguish them. React-generated IDs are not treated as permanent identities. Existing comments are resolved again on reload without rewriting their stored anchors.
|
|
14
|
+
|
|
13
15
|
Run `npm run test:anchors` for route, reload, ambiguity and overlay regressions.
|
|
14
16
|
|
|
15
17
|
## Comment screenshots
|
|
16
18
|
|
|
17
|
-
Posting immediately displays a pending comment
|
|
19
|
+
Posting immediately displays a pending comment, captures the viewport, then sends the comment and screenshot in one multipart request. The API stores the screenshot before creating the comment. Capture or save failures restore the draft for retry instead of silently creating a comment without an image. Screenshot annotations reuse the real Comty pin and hover card (avatar, author, date and content). They are measured in an invisible, inert host, then cloned with their computed styles and embedded resources into the screenshot. The temporary host is removed after cloning and never appears on screen.
|
|
18
20
|
|
|
19
21
|
The bundled capture engine is based on `html-to-image` 1.11.13 (MIT), not SnapDOM. It embeds web fonts, responsive images, CSS backgrounds and pseudo-elements, preserves scroll offsets, and yields during DOM traversal. Each font face keeps its source when selecting WOFF2, including consecutive subset and weight rules. Transparent page backgrounds are composited over the browser's light or dark canvas so the image viewer cannot tint the screenshot. Offscreen leaf boxes keep their layout dimensions without their rendering cost. PNG is preferred for text; large images fall back to JPEG. Captures are capped at 2× resolution / 2560 pixels per side and have a 15-second processing deadline.
|
|
20
22
|
|
|
21
23
|
Resources are fetched directly first. Public cross-origin assets blocked by CORS can use `/api/sdk/capture-resource`, authenticated with the SDK token and restricted to the project's allowed domain. The API validates and pins public DNS addresses on every redirect, limits bytes/concurrency/time, and never forwards credentials to resource hosts. Deploy the API alongside the SDK to enable this fallback.
|
|
22
24
|
|
|
23
|
-
The capture never locks or restyles the live page. If the URL, viewport or page scroll changes before cloning finishes,
|
|
25
|
+
The capture never locks or restyles the live page. If the URL, viewport or page scroll changes before cloning finishes, posting fails and restores the draft for retry rather than attaching a mismatched image. Browser restrictions still apply to private resources, closed shadow roots and embedded frames; inaccessible frames retain an empty layout box. The final SVG serialization/rasterization still uses browser APIs on the main thread, so zero processing cost on every possible site cannot be guaranteed.
|
|
24
26
|
|
|
25
27
|
`npm run build` reproducibly bundles the pinned library with small checked hooks for cooperative scheduling, resource fetching, cloning and exact font sizes; its MIT notice is included. No runtime package install is needed by SDK consumers. `npm run test:capture` runs Chrome/Chromium visual and posting tests; install Chromium with `npx playwright install chromium` on machines without Chrome. Comparison PNGs are written to `test-results/`.
|
|
26
28
|
|
|
@@ -37,12 +39,15 @@ import { initComty } from '@comty.io/core';
|
|
|
37
39
|
|
|
38
40
|
await initComty({
|
|
39
41
|
projectKey: 'YOUR_PROJECT_KEY',
|
|
40
|
-
apiUrl: 'https://YOUR_COMTY_API',
|
|
41
42
|
});
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Call once from your application entry. Server rendering is safe: it does nothing until called in a browser. Repeated calls with the same options reuse the document's instance. Reload the page to change its project or API URL. Register the site's domain in Comty to use shared comments.
|
|
45
46
|
|
|
47
|
+
The project key is the only required option for hosted Comty. The SDK connects to `https://api.comty.io` by default, including when your application runs on localhost. You do not need an API URL environment variable or your own Comty server. Set `apiUrl` only when using a different Comty API, such as `http://localhost:4000` for developing Comty itself or the URL of a self-hosted installation.
|
|
48
|
+
|
|
49
|
+
The browser entry also works in production. If Comty should only appear in development or preview, conditionally call `initComty` in your application. To include production, do not guard initialization with a development-only flag. The Vite companion below is a separate development-only integration.
|
|
50
|
+
|
|
46
51
|
## Color tokens
|
|
47
52
|
|
|
48
53
|
Element inspection shows a copyable CSS custom-property name above foreground/background colors when the winning declaration can be identified. It handles ordinary selectors, `:where()`, inline styles, inheritance, `!important`, active media/supports rules and named top-level cascade layers. Semantic aliases retain their declared name. No extra line is rendered for literal colors or uncertain provenance (including inaccessible stylesheets, shadow/scoped or nested cascades, and unsupported functional selectors).
|
|
@@ -81,7 +86,7 @@ export default defineConfig({
|
|
|
81
86
|
});
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
Keep your existing plugins. Run your normal dev command and open `http://localhost:<vite-port>`. The plugin injects the SDK and starts the companion automatically. It is disabled for production builds. Choose the installed coding agent with `agent: 'codex' | 'claude' | 'cursor' | 'gemini' | 'grok'`. The optional `model` setting accepts a model ID from the selected agent; omit it to use that agent's default. A project key is optional in local Agent mode; set `projectKey`
|
|
89
|
+
Keep your existing plugins. Run your normal dev command and open `http://localhost:<vite-port>`. The plugin injects the SDK and starts the companion automatically. It is disabled for production builds. Choose the installed coding agent with `agent: 'codex' | 'claude' | 'cursor' | 'gemini' | 'grok'`. The optional `model` setting accepts a model ID from the selected agent; omit it to use that agent's default. A project key is optional in local Agent mode; set `projectKey` in the plugin options if you also want shared Comty comments. Set `apiUrl` only for a local or self-hosted Comty API.
|
|
85
90
|
|
|
86
91
|
## Other development servers
|
|
87
92
|
|
package/comty-sdk.js
CHANGED
|
@@ -1629,6 +1629,11 @@ function createAnchorMatcher({
|
|
|
1629
1629
|
getPresentationLayer,
|
|
1630
1630
|
isComtyNode,
|
|
1631
1631
|
}) {
|
|
1632
|
+
function getStableAnchorId(id) {
|
|
1633
|
+
// React useId values change with mount order, including Base UI and Radix IDs.
|
|
1634
|
+
return /^(?:(?:radix|base-ui)-)?(?:_r_[\w-]+_|:r[\w-]+:)$/i.test(id || "") ? "" : (id || "");
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1632
1637
|
function includesText(haystack, needle) {
|
|
1633
1638
|
if (!haystack || !needle) return false;
|
|
1634
1639
|
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
@@ -1652,12 +1657,13 @@ function createAnchorMatcher({
|
|
|
1652
1657
|
}
|
|
1653
1658
|
|
|
1654
1659
|
let score = 0;
|
|
1655
|
-
const text = getElementTextSignature(el);
|
|
1656
|
-
const ownText = getElementOwnTextSignature(el);
|
|
1660
|
+
const text = getElementTextSignature(el, fingerprint.textMode === "static");
|
|
1661
|
+
const ownText = getElementOwnTextSignature(el, fingerprint.textMode === "static");
|
|
1657
1662
|
|
|
1658
1663
|
score += 20;
|
|
1659
|
-
|
|
1660
|
-
|
|
1664
|
+
const stableId = getStableAnchorId(fingerprint.id);
|
|
1665
|
+
if (stableId) {
|
|
1666
|
+
score += el.id.slice(0, 160) === stableId ? 70 : -30;
|
|
1661
1667
|
}
|
|
1662
1668
|
const exactAttributes = [
|
|
1663
1669
|
["role", "role", 24],
|
|
@@ -1752,7 +1758,7 @@ function createAnchorMatcher({
|
|
|
1752
1758
|
|
|
1753
1759
|
function scoreSibling(el, fingerprint) {
|
|
1754
1760
|
if (!fingerprint) return 0;
|
|
1755
|
-
const sibling = getSiblingFingerprint(el);
|
|
1761
|
+
const sibling = getSiblingFingerprint(el, fingerprint.textMode === "static");
|
|
1756
1762
|
if (!sibling || sibling.tag !== fingerprint.tag) return -4;
|
|
1757
1763
|
let score = 4;
|
|
1758
1764
|
if (fingerprint.ariaLabel && sibling.ariaLabel === fingerprint.ariaLabel) {
|
|
@@ -1791,7 +1797,7 @@ function createAnchorMatcher({
|
|
|
1791
1797
|
score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
|
|
1792
1798
|
}
|
|
1793
1799
|
// Distinguish repeated cards by their section, even when dimensions match.
|
|
1794
|
-
if (
|
|
1800
|
+
if (ancestors[index].text && getElementTextSignature(parent, ancestors[index].textMode === "static") === ancestors[index].text) score += 18;
|
|
1795
1801
|
parent = parent.parentElement;
|
|
1796
1802
|
}
|
|
1797
1803
|
|
|
@@ -1812,14 +1818,17 @@ function createAnchorMatcher({
|
|
|
1812
1818
|
for (const [key, attribute] of Object.entries(attributes)) {
|
|
1813
1819
|
if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
|
|
1814
1820
|
}
|
|
1815
|
-
|
|
1821
|
+
const stableId = getStableAnchorId(fingerprint.id);
|
|
1822
|
+
if (stableId && el.id !== stableId) return false;
|
|
1816
1823
|
if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
|
|
1817
|
-
const stableIdentity = (
|
|
1824
|
+
const stableIdentity = (stableId && el.id === stableId) ||
|
|
1818
1825
|
(fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
|
|
1819
|
-
|
|
1826
|
+
const staticText = fingerprint.textMode === "static";
|
|
1827
|
+
const text = getElementTextSignature(el, staticText);
|
|
1828
|
+
if (fingerprint.ownText && getElementOwnTextSignature(el, staticText) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
|
|
1820
1829
|
// Container text is useful for matching remounted drawers and dialogs.
|
|
1821
|
-
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text &&
|
|
1822
|
-
if (!allowTextChanges || !el.children.length || !similarText(
|
|
1830
|
+
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && text !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
|
|
1831
|
+
if (!allowTextChanges || !el.children.length || !similarText(text, fingerprint.text)) return false;
|
|
1823
1832
|
}
|
|
1824
1833
|
return true;
|
|
1825
1834
|
}
|
|
@@ -1852,10 +1861,11 @@ function createAnchorMatcher({
|
|
|
1852
1861
|
.map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
|
|
1853
1862
|
.sort((a, b) => b.score - a.score);
|
|
1854
1863
|
if (context.candidateCount > 1) {
|
|
1864
|
+
// Creation counts full context matches, not every element with the same text.
|
|
1855
1865
|
const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
|
|
1856
|
-
if (peers.length !== context.candidateCount) {
|
|
1866
|
+
if (matches.length !== context.candidateCount && peers.length !== context.candidateCount) {
|
|
1857
1867
|
// A removed identical sibling must not move its comment to the survivor.
|
|
1858
|
-
const distinctive = matches[0] && (context.target.id || context.target.testId);
|
|
1868
|
+
const distinctive = matches[0] && (getStableAnchorId(context.target.id) || context.target.testId);
|
|
1859
1869
|
if (!distinctive) return { element: null, status: "uncertain" };
|
|
1860
1870
|
}
|
|
1861
1871
|
}
|
|
@@ -1868,8 +1878,8 @@ function createAnchorMatcher({
|
|
|
1868
1878
|
return layer && matchesAnchorIdentity(layer, context.layer, false, false);
|
|
1869
1879
|
})) return { element: null, status: "hidden" };
|
|
1870
1880
|
const plausible = matches.length || candidates.some(el =>
|
|
1871
|
-
(context.target.id && el.id === context.target.id) ||
|
|
1872
|
-
(context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
|
|
1881
|
+
(getStableAnchorId(context.target.id) && el.id === context.target.id) ||
|
|
1882
|
+
(context.target.text && (getElementTextSignature(el, context.target.textMode === "static") === context.target.text || similarText(getElementTextSignature(el, context.target.textMode === "static"), context.target.text))));
|
|
1873
1883
|
return { element: null, status: plausible ? "uncertain" : "missing" };
|
|
1874
1884
|
}
|
|
1875
1885
|
|
|
@@ -2046,7 +2056,7 @@ function createAnchorMatcher({
|
|
|
2046
2056
|
return { clientX, clientY };
|
|
2047
2057
|
}
|
|
2048
2058
|
|
|
2049
|
-
return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
2059
|
+
return { getStableAnchorId, matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
2050
2060
|
}
|
|
2051
2061
|
|
|
2052
2062
|
const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
|
|
@@ -3585,7 +3595,7 @@ function initComty(options = {}) {
|
|
|
3585
3595
|
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
3586
3596
|
}
|
|
3587
3597
|
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
3588
|
-
const apiUrl = options.apiUrl || "
|
|
3598
|
+
const apiUrl = options.apiUrl || "https://api.comty.io";
|
|
3589
3599
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
3590
3600
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
3591
3601
|
}
|
|
@@ -3675,7 +3685,7 @@ function initComty(options = {}) {
|
|
|
3675
3685
|
const commentTargets = new Map();
|
|
3676
3686
|
|
|
3677
3687
|
const {
|
|
3678
|
-
matchesAnchorContext,
|
|
3688
|
+
getStableAnchorId, matchesAnchorContext,
|
|
3679
3689
|
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
3680
3690
|
} = createAnchorMatcher({
|
|
3681
3691
|
getElementTextSignature,
|
|
@@ -5168,8 +5178,11 @@ function initComty(options = {}) {
|
|
|
5168
5178
|
}));
|
|
5169
5179
|
} catch { /* Resume in comment mode if browser storage is unavailable. */ }
|
|
5170
5180
|
const destination = new URL("/api/auth/sign-in/google", apiUrl);
|
|
5171
|
-
|
|
5172
|
-
|
|
5181
|
+
const authorizeUrl = new URL(loginUrl);
|
|
5182
|
+
// Bootstrap may predate navigation within a single-page application.
|
|
5183
|
+
authorizeUrl.searchParams.set("returnTo", window.location.href);
|
|
5184
|
+
destination.searchParams.set("callbackURL", authorizeUrl.toString());
|
|
5185
|
+
setTimeout(() => window.location.assign(destination.toString()), 50);
|
|
5173
5186
|
};
|
|
5174
5187
|
}
|
|
5175
5188
|
const guestForm = backdrop.querySelector(".comty-guest-form");
|
|
@@ -5777,9 +5790,10 @@ function initComty(options = {}) {
|
|
|
5777
5790
|
) {
|
|
5778
5791
|
let selector = currentEl.nodeName.toLowerCase();
|
|
5779
5792
|
|
|
5780
|
-
|
|
5793
|
+
const stableId = getStableAnchorId(currentEl.id);
|
|
5794
|
+
if (stableId && stableId.length <= 160) {
|
|
5781
5795
|
try {
|
|
5782
|
-
selector += `#${CSS.escape(
|
|
5796
|
+
selector += `#${CSS.escape(stableId)}`;
|
|
5783
5797
|
} catch {
|
|
5784
5798
|
// fallback
|
|
5785
5799
|
}
|
|
@@ -5787,7 +5801,7 @@ function initComty(options = {}) {
|
|
|
5787
5801
|
|
|
5788
5802
|
const stableAttributeSelector = getStableAttributeSelector(currentEl);
|
|
5789
5803
|
if (
|
|
5790
|
-
(!
|
|
5804
|
+
(!stableId || stableId.length > 160) &&
|
|
5791
5805
|
stableAttributeSelector
|
|
5792
5806
|
) {
|
|
5793
5807
|
selector += stableAttributeSelector;
|
|
@@ -5884,12 +5898,16 @@ function initComty(options = {}) {
|
|
|
5884
5898
|
return el;
|
|
5885
5899
|
}
|
|
5886
5900
|
|
|
5887
|
-
function
|
|
5901
|
+
function isEditableContent(el) {
|
|
5902
|
+
return Boolean(el?.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])'));
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
function getElementTextSignature(el, staticText = false) {
|
|
5888
5906
|
if (!el) return "";
|
|
5889
5907
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
5890
5908
|
let text = "", node;
|
|
5891
5909
|
while ((node = walker.nextNode()) && text.length < 480) {
|
|
5892
|
-
if (!isComtyNode(node.parentElement)) text += ` ${node.textContent || ""}`;
|
|
5910
|
+
if (!isComtyNode(node.parentElement) && !(staticText && isEditableContent(node.parentElement))) text += ` ${node.textContent || ""}`;
|
|
5893
5911
|
}
|
|
5894
5912
|
return text.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
5895
5913
|
}
|
|
@@ -5907,8 +5925,8 @@ function initComty(options = {}) {
|
|
|
5907
5925
|
.slice(0, 8);
|
|
5908
5926
|
}
|
|
5909
5927
|
|
|
5910
|
-
function getElementOwnTextSignature(el) {
|
|
5911
|
-
if (!el) return "";
|
|
5928
|
+
function getElementOwnTextSignature(el, staticText = false) {
|
|
5929
|
+
if (!el || (staticText && isEditableContent(el))) return "";
|
|
5912
5930
|
return [...el.childNodes]
|
|
5913
5931
|
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
|
5914
5932
|
.map((node) => node.textContent || "")
|
|
@@ -5922,11 +5940,12 @@ function initComty(options = {}) {
|
|
|
5922
5940
|
return (el.getAttribute(attribute) || "").trim().slice(0, 240);
|
|
5923
5941
|
}
|
|
5924
5942
|
|
|
5925
|
-
function getSiblingFingerprint(el) {
|
|
5943
|
+
function getSiblingFingerprint(el, staticText = false) {
|
|
5926
5944
|
if (!(el instanceof Element)) return null;
|
|
5927
5945
|
return {
|
|
5928
5946
|
tag: el.tagName.toLowerCase(),
|
|
5929
|
-
|
|
5947
|
+
...(staticText ? { textMode: "static" } : {}),
|
|
5948
|
+
text: getElementOwnTextSignature(el, staticText) || getElementTextSignature(el, staticText),
|
|
5930
5949
|
ariaLabel: getAttributeSignature(el, "aria-label"),
|
|
5931
5950
|
role: getAttributeSignature(el, "role"),
|
|
5932
5951
|
};
|
|
@@ -5941,10 +5960,12 @@ function initComty(options = {}) {
|
|
|
5941
5960
|
|
|
5942
5961
|
const fingerprint = {
|
|
5943
5962
|
tag: el.tagName.toLowerCase(),
|
|
5944
|
-
id: (el.id
|
|
5963
|
+
id: getStableAnchorId(el.id).slice(0, 160),
|
|
5945
5964
|
classes: getElementClassList(el),
|
|
5946
|
-
|
|
5947
|
-
|
|
5965
|
+
// Keep editable values out of the field and every ancestor's identity.
|
|
5966
|
+
textMode: "static",
|
|
5967
|
+
text: getElementTextSignature(el, true),
|
|
5968
|
+
ownText: getElementOwnTextSignature(el, true),
|
|
5948
5969
|
role: getAttributeSignature(el, "role"),
|
|
5949
5970
|
ariaLabel: getAttributeSignature(el, "aria-label"),
|
|
5950
5971
|
name: getAttributeSignature(el, "name"),
|
|
@@ -5967,8 +5988,9 @@ function initComty(options = {}) {
|
|
|
5967
5988
|
if (includeSiblings) {
|
|
5968
5989
|
fingerprint.previousSibling = getSiblingFingerprint(
|
|
5969
5990
|
el.previousElementSibling,
|
|
5991
|
+
true,
|
|
5970
5992
|
);
|
|
5971
|
-
fingerprint.nextSibling = getSiblingFingerprint(el.nextElementSibling);
|
|
5993
|
+
fingerprint.nextSibling = getSiblingFingerprint(el.nextElementSibling, true);
|
|
5972
5994
|
}
|
|
5973
5995
|
|
|
5974
5996
|
return fingerprint;
|
|
@@ -6187,33 +6209,6 @@ function initComty(options = {}) {
|
|
|
6187
6209
|
|
|
6188
6210
|
const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
|
|
6189
6211
|
|
|
6190
|
-
async function uploadCommentScreenshot(comment, context, point) {
|
|
6191
|
-
try {
|
|
6192
|
-
const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
|
|
6193
|
-
if (!screenshot) return;
|
|
6194
|
-
const formData = new FormData();
|
|
6195
|
-
formData.append("projectKey", projectKey);
|
|
6196
|
-
formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
6197
|
-
formData.append("screenshotWidth", String(screenshot.width));
|
|
6198
|
-
formData.append("screenshotHeight", String(screenshot.height));
|
|
6199
|
-
const response = await fetch(
|
|
6200
|
-
`${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
|
|
6201
|
-
{ method: "POST", headers: getSdkHeaders(), body: formData },
|
|
6202
|
-
);
|
|
6203
|
-
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
6204
|
-
// Updating metadata must not redraw the page or interrupt a new draft.
|
|
6205
|
-
const { screenshotUrl } = await response.json();
|
|
6206
|
-
const cached = cachedComments.find(item => String(item.id) === String(comment.id));
|
|
6207
|
-
if (cached) Object.assign(cached, {
|
|
6208
|
-
screenshot_url: screenshotUrl,
|
|
6209
|
-
screenshot_width: screenshot.width,
|
|
6210
|
-
screenshot_height: screenshot.height,
|
|
6211
|
-
});
|
|
6212
|
-
} catch (error) {
|
|
6213
|
-
console.warn("Comty: comment saved, screenshot unavailable", error);
|
|
6214
|
-
}
|
|
6215
|
-
}
|
|
6216
|
-
|
|
6217
6212
|
function addLocalComment(commentId, payload) {
|
|
6218
6213
|
const comment = {
|
|
6219
6214
|
id: commentId,
|
|
@@ -6681,13 +6676,26 @@ function initComty(options = {}) {
|
|
|
6681
6676
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
6682
6677
|
syncPlacementCursor();
|
|
6683
6678
|
hideInspector();
|
|
6684
|
-
|
|
6679
|
+
const pendingComment = isAgentTask ? null : addLocalComment(pendingId, payload);
|
|
6685
6680
|
let post;
|
|
6686
6681
|
try {
|
|
6682
|
+
let body = JSON.stringify({ ...payload, projectKey });
|
|
6683
|
+
let screenshot;
|
|
6684
|
+
if (!isAgentTask) {
|
|
6685
|
+
screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(pendingComment, point) });
|
|
6686
|
+
if (!screenshot) throw new Error("Screenshot unavailable. Keep this page visible and retry.");
|
|
6687
|
+
body = new FormData();
|
|
6688
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
6689
|
+
if (value != null) body.append(key, String(value));
|
|
6690
|
+
}
|
|
6691
|
+
body.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
6692
|
+
body.append("screenshotWidth", String(screenshot.width));
|
|
6693
|
+
body.append("screenshotHeight", String(screenshot.height));
|
|
6694
|
+
}
|
|
6687
6695
|
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
6688
6696
|
method: "POST",
|
|
6689
|
-
headers:
|
|
6690
|
-
body
|
|
6697
|
+
headers: isAgentTask ? agentHeaders() : getSdkHeaders(),
|
|
6698
|
+
body,
|
|
6691
6699
|
});
|
|
6692
6700
|
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
6693
6701
|
const data = await post.json();
|
|
@@ -6700,7 +6708,7 @@ function initComty(options = {}) {
|
|
|
6700
6708
|
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
6701
6709
|
if (context.url === location.href) {
|
|
6702
6710
|
const comment = addLocalComment(String(id), payload);
|
|
6703
|
-
|
|
6711
|
+
Object.assign(comment, { screenshot_url: data.screenshotUrl, screenshot_width: screenshot.width, screenshot_height: screenshot.height });
|
|
6704
6712
|
} else void loadComments({ force: true });
|
|
6705
6713
|
}
|
|
6706
6714
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comty.io/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Comty visual comments, inspector and local agent tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"test:inspector": "node --test tests/inspector.browser.test.mjs tests/inspect-markup.browser.test.mjs tests/idle-network.browser.test.mjs",
|
|
38
38
|
"test:placement": "node --test tests/placement.browser.test.mjs",
|
|
39
39
|
"test:anchors": "node --test tests/anchors.test.mjs tests/anchor-reload.browser.test.mjs",
|
|
40
|
+
"test:auth": "node --test tests/auth-return.browser.test.mjs",
|
|
40
41
|
"test:vite": "node --test tests/vite.test.mjs",
|
|
41
42
|
"prepack": "npm run build"
|
|
42
43
|
},
|
package/src/anchors.js
CHANGED
|
@@ -6,6 +6,11 @@ export function createAnchorMatcher({
|
|
|
6
6
|
getPresentationLayer,
|
|
7
7
|
isComtyNode,
|
|
8
8
|
}) {
|
|
9
|
+
function getStableAnchorId(id) {
|
|
10
|
+
// React useId values change with mount order, including Base UI and Radix IDs.
|
|
11
|
+
return /^(?:(?:radix|base-ui)-)?(?:_r_[\w-]+_|:r[\w-]+:)$/i.test(id || "") ? "" : (id || "");
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
function includesText(haystack, needle) {
|
|
10
15
|
if (!haystack || !needle) return false;
|
|
11
16
|
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
@@ -29,12 +34,13 @@ export function createAnchorMatcher({
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
let score = 0;
|
|
32
|
-
const text = getElementTextSignature(el);
|
|
33
|
-
const ownText = getElementOwnTextSignature(el);
|
|
37
|
+
const text = getElementTextSignature(el, fingerprint.textMode === "static");
|
|
38
|
+
const ownText = getElementOwnTextSignature(el, fingerprint.textMode === "static");
|
|
34
39
|
|
|
35
40
|
score += 20;
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const stableId = getStableAnchorId(fingerprint.id);
|
|
42
|
+
if (stableId) {
|
|
43
|
+
score += el.id.slice(0, 160) === stableId ? 70 : -30;
|
|
38
44
|
}
|
|
39
45
|
const exactAttributes = [
|
|
40
46
|
["role", "role", 24],
|
|
@@ -129,7 +135,7 @@ export function createAnchorMatcher({
|
|
|
129
135
|
|
|
130
136
|
function scoreSibling(el, fingerprint) {
|
|
131
137
|
if (!fingerprint) return 0;
|
|
132
|
-
const sibling = getSiblingFingerprint(el);
|
|
138
|
+
const sibling = getSiblingFingerprint(el, fingerprint.textMode === "static");
|
|
133
139
|
if (!sibling || sibling.tag !== fingerprint.tag) return -4;
|
|
134
140
|
let score = 4;
|
|
135
141
|
if (fingerprint.ariaLabel && sibling.ariaLabel === fingerprint.ariaLabel) {
|
|
@@ -168,7 +174,7 @@ export function createAnchorMatcher({
|
|
|
168
174
|
score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
|
|
169
175
|
}
|
|
170
176
|
// Distinguish repeated cards by their section, even when dimensions match.
|
|
171
|
-
if (
|
|
177
|
+
if (ancestors[index].text && getElementTextSignature(parent, ancestors[index].textMode === "static") === ancestors[index].text) score += 18;
|
|
172
178
|
parent = parent.parentElement;
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -189,14 +195,17 @@ export function createAnchorMatcher({
|
|
|
189
195
|
for (const [key, attribute] of Object.entries(attributes)) {
|
|
190
196
|
if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
|
|
191
197
|
}
|
|
192
|
-
|
|
198
|
+
const stableId = getStableAnchorId(fingerprint.id);
|
|
199
|
+
if (stableId && el.id !== stableId) return false;
|
|
193
200
|
if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
|
|
194
|
-
const stableIdentity = (
|
|
201
|
+
const stableIdentity = (stableId && el.id === stableId) ||
|
|
195
202
|
(fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
|
|
196
|
-
|
|
203
|
+
const staticText = fingerprint.textMode === "static";
|
|
204
|
+
const text = getElementTextSignature(el, staticText);
|
|
205
|
+
if (fingerprint.ownText && getElementOwnTextSignature(el, staticText) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
|
|
197
206
|
// Container text is useful for matching remounted drawers and dialogs.
|
|
198
|
-
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text &&
|
|
199
|
-
if (!allowTextChanges || !el.children.length || !similarText(
|
|
207
|
+
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && text !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
|
|
208
|
+
if (!allowTextChanges || !el.children.length || !similarText(text, fingerprint.text)) return false;
|
|
200
209
|
}
|
|
201
210
|
return true;
|
|
202
211
|
}
|
|
@@ -229,10 +238,11 @@ export function createAnchorMatcher({
|
|
|
229
238
|
.map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
|
|
230
239
|
.sort((a, b) => b.score - a.score);
|
|
231
240
|
if (context.candidateCount > 1) {
|
|
241
|
+
// Creation counts full context matches, not every element with the same text.
|
|
232
242
|
const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
|
|
233
|
-
if (peers.length !== context.candidateCount) {
|
|
243
|
+
if (matches.length !== context.candidateCount && peers.length !== context.candidateCount) {
|
|
234
244
|
// A removed identical sibling must not move its comment to the survivor.
|
|
235
|
-
const distinctive = matches[0] && (context.target.id || context.target.testId);
|
|
245
|
+
const distinctive = matches[0] && (getStableAnchorId(context.target.id) || context.target.testId);
|
|
236
246
|
if (!distinctive) return { element: null, status: "uncertain" };
|
|
237
247
|
}
|
|
238
248
|
}
|
|
@@ -245,8 +255,8 @@ export function createAnchorMatcher({
|
|
|
245
255
|
return layer && matchesAnchorIdentity(layer, context.layer, false, false);
|
|
246
256
|
})) return { element: null, status: "hidden" };
|
|
247
257
|
const plausible = matches.length || candidates.some(el =>
|
|
248
|
-
(context.target.id && el.id === context.target.id) ||
|
|
249
|
-
(context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
|
|
258
|
+
(getStableAnchorId(context.target.id) && el.id === context.target.id) ||
|
|
259
|
+
(context.target.text && (getElementTextSignature(el, context.target.textMode === "static") === context.target.text || similarText(getElementTextSignature(el, context.target.textMode === "static"), context.target.text))));
|
|
250
260
|
return { element: null, status: plausible ? "uncertain" : "missing" };
|
|
251
261
|
}
|
|
252
262
|
|
|
@@ -423,5 +433,5 @@ export function createAnchorMatcher({
|
|
|
423
433
|
return { clientX, clientY };
|
|
424
434
|
}
|
|
425
435
|
|
|
426
|
-
return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
436
|
+
return { getStableAnchorId, matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
427
437
|
}
|
package/src/browser.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ComtyOptions {
|
|
2
2
|
/** Project key from your Comty dashboard. */
|
|
3
3
|
projectKey?: string;
|
|
4
|
-
/**
|
|
4
|
+
/** Defaults to https://api.comty.io. Override for a local or self-hosted Comty API. */
|
|
5
5
|
apiUrl?: string;
|
|
6
6
|
/** Set automatically by comty dev; not needed in application code. */
|
|
7
7
|
agent?: { url: string; token: string };
|
package/src/browser.js
CHANGED
|
@@ -21,7 +21,7 @@ export function initComty(options = {}) {
|
|
|
21
21
|
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
22
22
|
}
|
|
23
23
|
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
24
|
-
const apiUrl = options.apiUrl || "
|
|
24
|
+
const apiUrl = options.apiUrl || "https://api.comty.io";
|
|
25
25
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
26
26
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
27
27
|
}
|
|
@@ -111,7 +111,7 @@ export function initComty(options = {}) {
|
|
|
111
111
|
const commentTargets = new Map();
|
|
112
112
|
|
|
113
113
|
const {
|
|
114
|
-
matchesAnchorContext,
|
|
114
|
+
getStableAnchorId, matchesAnchorContext,
|
|
115
115
|
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
116
116
|
} = createAnchorMatcher({
|
|
117
117
|
getElementTextSignature,
|
|
@@ -1604,8 +1604,11 @@ export function initComty(options = {}) {
|
|
|
1604
1604
|
}));
|
|
1605
1605
|
} catch { /* Resume in comment mode if browser storage is unavailable. */ }
|
|
1606
1606
|
const destination = new URL("/api/auth/sign-in/google", apiUrl);
|
|
1607
|
-
|
|
1608
|
-
|
|
1607
|
+
const authorizeUrl = new URL(loginUrl);
|
|
1608
|
+
// Bootstrap may predate navigation within a single-page application.
|
|
1609
|
+
authorizeUrl.searchParams.set("returnTo", window.location.href);
|
|
1610
|
+
destination.searchParams.set("callbackURL", authorizeUrl.toString());
|
|
1611
|
+
setTimeout(() => window.location.assign(destination.toString()), 50);
|
|
1609
1612
|
};
|
|
1610
1613
|
}
|
|
1611
1614
|
const guestForm = backdrop.querySelector(".comty-guest-form");
|
|
@@ -2213,9 +2216,10 @@ export function initComty(options = {}) {
|
|
|
2213
2216
|
) {
|
|
2214
2217
|
let selector = currentEl.nodeName.toLowerCase();
|
|
2215
2218
|
|
|
2216
|
-
|
|
2219
|
+
const stableId = getStableAnchorId(currentEl.id);
|
|
2220
|
+
if (stableId && stableId.length <= 160) {
|
|
2217
2221
|
try {
|
|
2218
|
-
selector += `#${CSS.escape(
|
|
2222
|
+
selector += `#${CSS.escape(stableId)}`;
|
|
2219
2223
|
} catch {
|
|
2220
2224
|
// fallback
|
|
2221
2225
|
}
|
|
@@ -2223,7 +2227,7 @@ export function initComty(options = {}) {
|
|
|
2223
2227
|
|
|
2224
2228
|
const stableAttributeSelector = getStableAttributeSelector(currentEl);
|
|
2225
2229
|
if (
|
|
2226
|
-
(!
|
|
2230
|
+
(!stableId || stableId.length > 160) &&
|
|
2227
2231
|
stableAttributeSelector
|
|
2228
2232
|
) {
|
|
2229
2233
|
selector += stableAttributeSelector;
|
|
@@ -2320,12 +2324,16 @@ export function initComty(options = {}) {
|
|
|
2320
2324
|
return el;
|
|
2321
2325
|
}
|
|
2322
2326
|
|
|
2323
|
-
function
|
|
2327
|
+
function isEditableContent(el) {
|
|
2328
|
+
return Boolean(el?.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])'));
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function getElementTextSignature(el, staticText = false) {
|
|
2324
2332
|
if (!el) return "";
|
|
2325
2333
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
2326
2334
|
let text = "", node;
|
|
2327
2335
|
while ((node = walker.nextNode()) && text.length < 480) {
|
|
2328
|
-
if (!isComtyNode(node.parentElement)) text += ` ${node.textContent || ""}`;
|
|
2336
|
+
if (!isComtyNode(node.parentElement) && !(staticText && isEditableContent(node.parentElement))) text += ` ${node.textContent || ""}`;
|
|
2329
2337
|
}
|
|
2330
2338
|
return text.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
2331
2339
|
}
|
|
@@ -2343,8 +2351,8 @@ export function initComty(options = {}) {
|
|
|
2343
2351
|
.slice(0, 8);
|
|
2344
2352
|
}
|
|
2345
2353
|
|
|
2346
|
-
function getElementOwnTextSignature(el) {
|
|
2347
|
-
if (!el) return "";
|
|
2354
|
+
function getElementOwnTextSignature(el, staticText = false) {
|
|
2355
|
+
if (!el || (staticText && isEditableContent(el))) return "";
|
|
2348
2356
|
return [...el.childNodes]
|
|
2349
2357
|
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
|
2350
2358
|
.map((node) => node.textContent || "")
|
|
@@ -2358,11 +2366,12 @@ export function initComty(options = {}) {
|
|
|
2358
2366
|
return (el.getAttribute(attribute) || "").trim().slice(0, 240);
|
|
2359
2367
|
}
|
|
2360
2368
|
|
|
2361
|
-
function getSiblingFingerprint(el) {
|
|
2369
|
+
function getSiblingFingerprint(el, staticText = false) {
|
|
2362
2370
|
if (!(el instanceof Element)) return null;
|
|
2363
2371
|
return {
|
|
2364
2372
|
tag: el.tagName.toLowerCase(),
|
|
2365
|
-
|
|
2373
|
+
...(staticText ? { textMode: "static" } : {}),
|
|
2374
|
+
text: getElementOwnTextSignature(el, staticText) || getElementTextSignature(el, staticText),
|
|
2366
2375
|
ariaLabel: getAttributeSignature(el, "aria-label"),
|
|
2367
2376
|
role: getAttributeSignature(el, "role"),
|
|
2368
2377
|
};
|
|
@@ -2377,10 +2386,12 @@ export function initComty(options = {}) {
|
|
|
2377
2386
|
|
|
2378
2387
|
const fingerprint = {
|
|
2379
2388
|
tag: el.tagName.toLowerCase(),
|
|
2380
|
-
id: (el.id
|
|
2389
|
+
id: getStableAnchorId(el.id).slice(0, 160),
|
|
2381
2390
|
classes: getElementClassList(el),
|
|
2382
|
-
|
|
2383
|
-
|
|
2391
|
+
// Keep editable values out of the field and every ancestor's identity.
|
|
2392
|
+
textMode: "static",
|
|
2393
|
+
text: getElementTextSignature(el, true),
|
|
2394
|
+
ownText: getElementOwnTextSignature(el, true),
|
|
2384
2395
|
role: getAttributeSignature(el, "role"),
|
|
2385
2396
|
ariaLabel: getAttributeSignature(el, "aria-label"),
|
|
2386
2397
|
name: getAttributeSignature(el, "name"),
|
|
@@ -2403,8 +2414,9 @@ export function initComty(options = {}) {
|
|
|
2403
2414
|
if (includeSiblings) {
|
|
2404
2415
|
fingerprint.previousSibling = getSiblingFingerprint(
|
|
2405
2416
|
el.previousElementSibling,
|
|
2417
|
+
true,
|
|
2406
2418
|
);
|
|
2407
|
-
fingerprint.nextSibling = getSiblingFingerprint(el.nextElementSibling);
|
|
2419
|
+
fingerprint.nextSibling = getSiblingFingerprint(el.nextElementSibling, true);
|
|
2408
2420
|
}
|
|
2409
2421
|
|
|
2410
2422
|
return fingerprint;
|
|
@@ -2623,33 +2635,6 @@ export function initComty(options = {}) {
|
|
|
2623
2635
|
|
|
2624
2636
|
const { captureViewportScreenshot } = createScreenshotCapture({ apiUrl, projectKey, getHeaders: getSdkHeaders });
|
|
2625
2637
|
|
|
2626
|
-
async function uploadCommentScreenshot(comment, context, point) {
|
|
2627
|
-
try {
|
|
2628
|
-
const screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(comment, point) });
|
|
2629
|
-
if (!screenshot) return;
|
|
2630
|
-
const formData = new FormData();
|
|
2631
|
-
formData.append("projectKey", projectKey);
|
|
2632
|
-
formData.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
2633
|
-
formData.append("screenshotWidth", String(screenshot.width));
|
|
2634
|
-
formData.append("screenshotHeight", String(screenshot.height));
|
|
2635
|
-
const response = await fetch(
|
|
2636
|
-
`${apiUrl}/api/comments/${encodeURIComponent(comment.id)}/screenshot`,
|
|
2637
|
-
{ method: "POST", headers: getSdkHeaders(), body: formData },
|
|
2638
|
-
);
|
|
2639
|
-
if (!response.ok) throw new Error(`Screenshot upload failed (${response.status})`);
|
|
2640
|
-
// Updating metadata must not redraw the page or interrupt a new draft.
|
|
2641
|
-
const { screenshotUrl } = await response.json();
|
|
2642
|
-
const cached = cachedComments.find(item => String(item.id) === String(comment.id));
|
|
2643
|
-
if (cached) Object.assign(cached, {
|
|
2644
|
-
screenshot_url: screenshotUrl,
|
|
2645
|
-
screenshot_width: screenshot.width,
|
|
2646
|
-
screenshot_height: screenshot.height,
|
|
2647
|
-
});
|
|
2648
|
-
} catch (error) {
|
|
2649
|
-
console.warn("Comty: comment saved, screenshot unavailable", error);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
2638
|
function addLocalComment(commentId, payload) {
|
|
2654
2639
|
const comment = {
|
|
2655
2640
|
id: commentId,
|
|
@@ -3117,13 +3102,26 @@ export function initComty(options = {}) {
|
|
|
3117
3102
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
3118
3103
|
syncPlacementCursor();
|
|
3119
3104
|
hideInspector();
|
|
3120
|
-
|
|
3105
|
+
const pendingComment = isAgentTask ? null : addLocalComment(pendingId, payload);
|
|
3121
3106
|
let post;
|
|
3122
3107
|
try {
|
|
3108
|
+
let body = JSON.stringify({ ...payload, projectKey });
|
|
3109
|
+
let screenshot;
|
|
3110
|
+
if (!isAgentTask) {
|
|
3111
|
+
screenshot = await captureViewportScreenshot({ context, annotation: () => mountCaptureAnnotation(pendingComment, point) });
|
|
3112
|
+
if (!screenshot) throw new Error("Screenshot unavailable. Keep this page visible and retry.");
|
|
3113
|
+
body = new FormData();
|
|
3114
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
3115
|
+
if (value != null) body.append(key, String(value));
|
|
3116
|
+
}
|
|
3117
|
+
body.append("screenshot", screenshot.blob, screenshot.blob.type === "image/jpeg" ? "comment.jpg" : "comment.png");
|
|
3118
|
+
body.append("screenshotWidth", String(screenshot.width));
|
|
3119
|
+
body.append("screenshotHeight", String(screenshot.height));
|
|
3120
|
+
}
|
|
3123
3121
|
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
3124
3122
|
method: "POST",
|
|
3125
|
-
headers:
|
|
3126
|
-
body
|
|
3123
|
+
headers: isAgentTask ? agentHeaders() : getSdkHeaders(),
|
|
3124
|
+
body,
|
|
3127
3125
|
});
|
|
3128
3126
|
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
3129
3127
|
const data = await post.json();
|
|
@@ -3136,7 +3134,7 @@ export function initComty(options = {}) {
|
|
|
3136
3134
|
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
3137
3135
|
if (context.url === location.href) {
|
|
3138
3136
|
const comment = addLocalComment(String(id), payload);
|
|
3139
|
-
|
|
3137
|
+
Object.assign(comment, { screenshot_url: data.screenshotUrl, screenshot_width: screenshot.width, screenshot_height: screenshot.height });
|
|
3140
3138
|
} else void loadComments({ force: true });
|
|
3141
3139
|
}
|
|
3142
3140
|
} catch (error) {
|