@comty.io/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -3
- package/comty-sdk.js +849 -124
- package/package.json +16 -5
- package/src/acp.mjs +68 -0
- package/src/agent-process.mjs +89 -0
- package/src/anchors.js +58 -17
- package/src/browser.d.ts +3 -1
- package/src/browser.js +658 -79
- package/src/capture-engine.js +1 -1
- package/src/capture.js +6 -4
- package/src/claude-permission.mjs +37 -0
- package/src/claude.mjs +120 -0
- package/src/codex.mjs +66 -0
- package/src/comments.js +31 -11
- package/src/companion.mjs +201 -0
- package/src/cursor.mjs +49 -0
- package/src/dev.mjs +171 -0
- package/src/gemini.mjs +3 -0
- package/src/grok.mjs +3 -0
- package/src/style.js +95 -12
- package/src/vite.d.ts +5 -0
- package/src/vite.mjs +68 -0
package/comty-sdk.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
function getComtyStyle() {
|
|
4
4
|
const toolbarSurface = "#2b2d2f";
|
|
5
5
|
const toolbarMuted = "#b8babb";
|
|
6
|
-
const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
6
|
+
const commentCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 40 40"><path d="M20 2C30 2 38 10 38 20S30 38 20 38H3V28C2 25 2 23 2 20 2 10 10 2 20 2Z" fill="white" stroke="#d6dbe2"/><circle cx="20" cy="20" r="14" fill="#e6e6e6"/><path d="M20 14v12m-6-6h12" fill="none" stroke="#404040" stroke-width="2.5" stroke-linecap="round"/></svg>`;
|
|
7
7
|
const typographyCursor = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="8" width="23" height="23" rx="7" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width=".75"/><path d="m14 25 5.5-12L25 25m-8.5-4h6M13 25h4m5 0h4" fill="none" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m2 2 2 15 4-5 6-3Z" fill="${toolbarSurface}" stroke="${toolbarMuted}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
|
8
8
|
return `
|
|
9
9
|
#comty-root {
|
|
@@ -13,7 +13,7 @@ function getComtyStyle() {
|
|
|
13
13
|
--comty-muted: var(--comty-toolbar-muted);
|
|
14
14
|
--comty-accent: #55aaf5;
|
|
15
15
|
--comty-error: #ef7676;
|
|
16
|
-
--comty-outline: var(--comty-
|
|
16
|
+
--comty-outline: var(--comty-accent);
|
|
17
17
|
--comty-comment-focus: var(--comty-toolbar-muted);
|
|
18
18
|
--comty-paper: #fff;
|
|
19
19
|
--comty-ink: #171717;
|
|
@@ -60,6 +60,8 @@ function getComtyStyle() {
|
|
|
60
60
|
--comty-dialog-width: 32rem;
|
|
61
61
|
--comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
|
|
62
62
|
--comty-motion-duration: 220ms;
|
|
63
|
+
--comty-working-dot-size: .25em;
|
|
64
|
+
--comty-working-dot-gap: .125em;
|
|
63
65
|
--comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
64
66
|
--comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
65
67
|
position: fixed;
|
|
@@ -71,13 +73,73 @@ function getComtyStyle() {
|
|
|
71
73
|
font-family: var(--comty-font);
|
|
72
74
|
color-scheme: dark;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
#comty-root :is(button, .comty-comment-avatar) { user-select: none; -webkit-user-select: none; }
|
|
77
|
+
#comty-root .comty-comment-avatar img { -webkit-user-drag: none; }
|
|
78
|
+
#comty-root .comty-pin .comty-comment-avatar { pointer-events: none; }
|
|
79
|
+
#comty-root > .comty-agent-panel { position: fixed; right: 1rem; bottom: 5rem; z-index: 2147483647; width: min(22rem, calc(100vw - 2rem)); max-height: min(32rem, calc(100vh - 6rem)); box-sizing: border-box; display: flex; flex-direction: column; overflow: hidden; pointer-events: auto; border: .0625rem solid var(--comty-border); border-radius: 1rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; transform: translateY(0); opacity: 1; transition: transform var(--comty-motion-duration) var(--comty-ease-out), opacity var(--comty-motion-duration) ease; color-scheme: dark; }
|
|
80
|
+
#comty-root > .comty-agent-panel[data-side="left"] { left: 1rem; right: auto; }
|
|
81
|
+
#comty-root > .comty-agent-panel[data-closed] { transform: translateY(.5rem); opacity: 0; pointer-events: none; }
|
|
82
|
+
#comty-root > .comty-agent-panel[hidden] { display: none; }
|
|
83
|
+
.comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
|
|
84
|
+
.comty-agent-panel-header { display: flex; flex: 0 0 auto; align-items: center; gap: .75rem; padding: .5rem .75rem; border-bottom: .0625rem solid var(--comty-hover-surface); }
|
|
85
|
+
.comty-agent-brand { display: grid; place-items: center; width: 2.375rem; height: 2.375rem; flex-shrink: 0; border-radius: .6875rem; color: var(--comty-accent); background: var(--comty-hover-surface); }
|
|
86
|
+
.comty-agent-brand.comty-agent-avatar { width: 2.375rem; height: 2.375rem; border-radius: 0; background: transparent; }
|
|
87
|
+
.comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
|
|
88
|
+
.comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
|
|
89
|
+
.comty-agent-brand.comty-agent-avatar svg { width: 100%; height: 100%; }
|
|
90
|
+
.comty-agent-answer { display: flex; align-items: flex-start; gap: .5rem; padding: .375rem .5rem .875rem; }
|
|
91
|
+
.comty-agent-answer .comty-agent-brand { width: 1.5rem; height: 1.5rem; }
|
|
92
|
+
.comty-agent-answer p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--comty-foreground); }
|
|
93
|
+
.comty-agent-heading { flex: 1; min-width: 0; }
|
|
94
|
+
.comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
|
|
95
|
+
.comty-agent-panel-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--comty-hover-surface) transparent; padding: .375rem .5rem 2rem; }
|
|
96
|
+
.comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
|
|
97
|
+
.comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
|
|
98
|
+
.comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
|
|
99
|
+
.comty-agent-list { display: grid; gap: .125rem; }
|
|
100
|
+
.comty-agent-row { position: relative; display: flex; align-items: center; gap: .625rem; min-height: 2rem; padding: .25rem .5rem; border-radius: .5rem; color: var(--comty-foreground); }
|
|
101
|
+
.comty-agent-row:hover { background: var(--comty-hover-surface); }
|
|
102
|
+
.comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
|
|
103
|
+
.comty-working-indicator { display: inline-grid; grid-template-columns: repeat(3, var(--comty-working-dot-size)); gap: var(--comty-working-dot-gap); flex: 0 0 auto; color: var(--comty-foreground); }
|
|
104
|
+
.comty-working-indicator > i { display: block; width: var(--comty-working-dot-size); height: var(--comty-working-dot-size); border-radius: 50%; background: currentColor; opacity: .25; transition: opacity 150ms linear; }
|
|
105
|
+
.comty-working-indicator > i[data-active] { opacity: 1; }
|
|
106
|
+
.comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
|
|
107
|
+
.comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
|
|
108
|
+
.comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
109
|
+
.comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
|
|
110
|
+
.comty-agent-row-actions { position: absolute; top: 50%; right: .25rem; display: flex; gap: .25rem; border-radius: .25rem; background: var(--comty-hover-surface); opacity: 0; pointer-events: none; transform: translateY(-50%); }
|
|
111
|
+
.comty-agent-row:hover .comty-agent-row-actions, .comty-agent-row-actions:has(.comty-agent-action:focus-visible) { opacity: 1; pointer-events: auto; }
|
|
112
|
+
@media (hover: none) { .comty-agent-row-actions { position: static; opacity: 1; pointer-events: auto; transform: none; background: transparent; } }
|
|
113
|
+
.comty-agent-panel .comty-agent-row-actions .comty-agent-action { min-height: 1.5rem; padding: .125rem .375rem; border-radius: .25rem; background: transparent; color: var(--comty-muted); font-size: .875rem; }
|
|
114
|
+
.comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
|
|
115
|
+
.comty-agent-error-popover { position: fixed; z-index: 2147483647; width: min(20rem, calc(100vw - 1rem)); max-height: calc(100vh - 1rem); overflow: auto; padding: .75rem .875rem; border: 0; border-radius: .625rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .875rem/1.4 ui-sans-serif, system-ui, sans-serif; pointer-events: none; color-scheme: dark; }
|
|
116
|
+
.comty-agent-error-popover strong { display: block; font-weight: 600; }
|
|
117
|
+
.comty-agent-error-popover p { margin: .375rem 0 0; color: var(--comty-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
118
|
+
.comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
|
|
119
|
+
.comty-agent-error { margin-top: .75rem; }
|
|
120
|
+
.comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
|
|
121
|
+
.comty-agent-error p { margin-top: .25rem; }
|
|
122
|
+
.comty-agent-banner { margin-bottom: 1rem; }
|
|
123
|
+
.comty-agent-banner .comty-agent-error { margin-top: 0; }
|
|
124
|
+
.comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
|
|
125
|
+
.comty-agent-panel .comty-agent-action { display: inline-flex; align-items: center; justify-content: center; gap: .375rem; min-height: 2rem; margin: 0; padding: .375rem .625rem; border: 0; border-radius: .5rem; font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; cursor: pointer; color: var(--comty-foreground); background: var(--comty-hover-surface); transition: background-color 150ms ease; }
|
|
126
|
+
.comty-agent-panel .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
|
|
127
|
+
.comty-agent-panel .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
|
|
128
|
+
.comty-agent-panel .comty-agent-action:disabled { opacity: .5; cursor: wait; }
|
|
129
|
+
.comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
|
|
130
|
+
.comty-agent-panel :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
|
|
131
|
+
.comty-agent-details { margin-top: .625rem; }
|
|
132
|
+
.comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
|
|
133
|
+
.comty-agent-panel pre { margin: .5rem 0 0; padding: .625rem; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 10rem; overflow: auto; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .875rem/1.4 ui-monospace, monospace; }
|
|
134
|
+
.comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: 0; pointer-events: none; }
|
|
135
|
+
.comty-agent-working-badge { position: absolute; left: -.125rem; bottom: 100%; display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .5rem; border-radius: .375rem .375rem 0 0; background: var(--comty-surface); color: var(--comty-foreground); font: 600 .6875rem/1.4 ui-sans-serif, system-ui, sans-serif; white-space: nowrap; }
|
|
136
|
+
.comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
|
|
75
137
|
#comty-root { cursor: default; }
|
|
76
138
|
#comty-root::backdrop { background: transparent; pointer-events: none; }
|
|
77
139
|
html.comty-comment-placement,
|
|
78
140
|
html.comty-comment-placement body,
|
|
79
141
|
html.comty-comment-placement body *:not(#comty-root):not(#comty-root *) {
|
|
80
|
-
cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}")
|
|
142
|
+
cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 16 16, crosshair !important;
|
|
81
143
|
}
|
|
82
144
|
#comty-root > .comty-sidepanel-open, #comty-root > .comty-pin, #comty-root > .comty-bubble, #comty-root > .comty-create-bubble, #comty-root > .comty-popover, #comty-root > .comty-auth-backdrop, #comty-root > .comty-auth-toast { pointer-events: auto; }
|
|
83
145
|
.comty-toolbar {
|
|
@@ -144,7 +206,7 @@ function getComtyStyle() {
|
|
|
144
206
|
#comty-root [hidden] { display: none !important; }
|
|
145
207
|
[data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
|
|
146
208
|
@media (prefers-reduced-motion: reduce) {
|
|
147
|
-
.comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
|
|
209
|
+
.comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i { transition: none !important; animation: none !important; }
|
|
148
210
|
}
|
|
149
211
|
|
|
150
212
|
.comty-sidepanel {
|
|
@@ -326,6 +388,16 @@ function getComtyStyle() {
|
|
|
326
388
|
white-space: nowrap;
|
|
327
389
|
}
|
|
328
390
|
|
|
391
|
+
.comty-anchor-badge {
|
|
392
|
+
display: inline-block;
|
|
393
|
+
margin-top: .375rem;
|
|
394
|
+
padding: .125rem .375rem;
|
|
395
|
+
border: 1px solid var(--comty-border);
|
|
396
|
+
border-radius: 999rem;
|
|
397
|
+
color: var(--comty-muted);
|
|
398
|
+
font: .6875rem/1.4 sans-serif;
|
|
399
|
+
}
|
|
400
|
+
|
|
329
401
|
.comty-pin {
|
|
330
402
|
position: fixed;
|
|
331
403
|
z-index: 2147483645;
|
|
@@ -607,6 +679,8 @@ function getComtyStyle() {
|
|
|
607
679
|
height: 100%;
|
|
608
680
|
object-fit: cover;
|
|
609
681
|
}
|
|
682
|
+
.comty-comment-avatar.comty-agent-avatar { overflow: visible; border-radius: 0; background: transparent; color: var(--comty-paper); }
|
|
683
|
+
.comty-comment-avatar.comty-agent-avatar svg { display: block; width: 100%; height: 100%; }
|
|
610
684
|
|
|
611
685
|
.comty-pin-avatar {
|
|
612
686
|
width: 24px;
|
|
@@ -721,12 +795,12 @@ function getComtyStyle() {
|
|
|
721
795
|
}
|
|
722
796
|
|
|
723
797
|
.comty-popover-item.comty-popover-item-destructive {
|
|
724
|
-
color: var(--comty-error);
|
|
798
|
+
color: var(--comty-agent-error);
|
|
725
799
|
}
|
|
726
800
|
|
|
727
801
|
.comty-popover-item.comty-popover-item-destructive:hover,
|
|
728
802
|
.comty-popover-item.comty-popover-item-destructive:focus-visible {
|
|
729
|
-
background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
|
|
803
|
+
background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
|
|
730
804
|
outline: none;
|
|
731
805
|
}
|
|
732
806
|
|
|
@@ -1097,15 +1171,15 @@ function getComtyStyle() {
|
|
|
1097
1171
|
margin: 4px 0;
|
|
1098
1172
|
}
|
|
1099
1173
|
|
|
1100
|
-
html.comty-comments-active .comty-pin {
|
|
1174
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin {
|
|
1101
1175
|
display: grid;
|
|
1102
1176
|
}
|
|
1103
1177
|
|
|
1104
|
-
html.comty-comments-active .comty-pin.comty-filtered-out {
|
|
1178
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin.comty-filtered-out {
|
|
1105
1179
|
display: none;
|
|
1106
1180
|
}
|
|
1107
1181
|
|
|
1108
|
-
html.comty-comments-active .comty-bubble {
|
|
1182
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-bubble {
|
|
1109
1183
|
display: block;
|
|
1110
1184
|
}
|
|
1111
1185
|
|
|
@@ -1215,10 +1289,10 @@ function getComtyStyle() {
|
|
|
1215
1289
|
position: fixed;
|
|
1216
1290
|
box-sizing: border-box;
|
|
1217
1291
|
border: .0625rem solid var(--comty-outline);
|
|
1218
|
-
background:
|
|
1292
|
+
background: transparent;
|
|
1293
|
+
border-radius: 0;
|
|
1219
1294
|
pointer-events: none;
|
|
1220
1295
|
}
|
|
1221
|
-
.comty-inspection-box[data-selected] { background: color-mix(in oklch, var(--comty-outline) 5%, transparent); }
|
|
1222
1296
|
.comty-inspect-spacing { position: fixed; pointer-events: none; background: var(--comty-spacing-color); }
|
|
1223
1297
|
.comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
|
|
1224
1298
|
.comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
|
|
@@ -1272,6 +1346,15 @@ function getComtyStyle() {
|
|
|
1272
1346
|
height: 5.75rem;
|
|
1273
1347
|
border-radius: .75rem;
|
|
1274
1348
|
}
|
|
1349
|
+
.comty-agent-scope { display: inline-flex; align-items: center; gap: .375rem; color: var(--comty-muted); font: .6875rem/1.25 var(--comty-font); white-space: nowrap; cursor: pointer; pointer-events: auto; opacity: 0; visibility: hidden; }
|
|
1350
|
+
.comty-create-expanded .comty-agent-scope { opacity: 1; visibility: visible; }
|
|
1351
|
+
.comty-agent-scope input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
|
|
1352
|
+
.comty-agent-scope-track { position: relative; width: 1.5rem; height: .875rem; flex: 0 0 auto; border-radius: 999rem; background: var(--comty-border); transition: background-color 160ms ease; }
|
|
1353
|
+
.comty-agent-scope-track::after { content: ""; position: absolute; top: .125rem; left: .125rem; width: .625rem; height: .625rem; border-radius: 50%; background: var(--comty-paper); transition: transform 160ms ease; }
|
|
1354
|
+
.comty-agent-scope input:checked + .comty-agent-scope-track { background: var(--comty-accent); }
|
|
1355
|
+
.comty-agent-scope input:checked + .comty-agent-scope-track::after { transform: translateX(.625rem); }
|
|
1356
|
+
.comty-agent-scope input:focus-visible + .comty-agent-scope-track { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
|
|
1357
|
+
.comty-agent-thread-error { grid-column: 2; margin: .25rem 0 0; color: var(--comty-agent-error); font: .6875rem/1.3 var(--comty-font); }
|
|
1275
1358
|
|
|
1276
1359
|
.comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
|
|
1277
1360
|
.comty-create-bubble.comty-bubble-left { transform-origin: top right; }
|
|
@@ -1502,6 +1585,14 @@ function createAnchorMatcher({
|
|
|
1502
1585
|
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1503
1586
|
}
|
|
1504
1587
|
|
|
1588
|
+
function similarText(a, b) {
|
|
1589
|
+
if (!a || !b) return false;
|
|
1590
|
+
const words = value => new Set(value.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1591
|
+
const left = words(a), right = words(b);
|
|
1592
|
+
const common = [...left].filter(word => right.has(word)).length;
|
|
1593
|
+
return common >= 2 && common / Math.max(left.size, right.size) >= 0.7;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1505
1596
|
function scoreFingerprint(el, fingerprint) {
|
|
1506
1597
|
if (!el || !fingerprint) return 0;
|
|
1507
1598
|
if (
|
|
@@ -1571,6 +1662,8 @@ function createAnchorMatcher({
|
|
|
1571
1662
|
score += 15;
|
|
1572
1663
|
}
|
|
1573
1664
|
|
|
1665
|
+
if (text !== fingerprint.text && similarText(text, fingerprint.text)) score += 24;
|
|
1666
|
+
|
|
1574
1667
|
if (typeof fingerprint.siblingIndex === "number" && el.parentElement) {
|
|
1575
1668
|
const siblings = [...el.parentElement.children].filter(
|
|
1576
1669
|
(child) => child.tagName === el.tagName,
|
|
@@ -1648,6 +1741,8 @@ function createAnchorMatcher({
|
|
|
1648
1741
|
if (ancestorScore > 0) {
|
|
1649
1742
|
score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
|
|
1650
1743
|
}
|
|
1744
|
+
// Distinguish repeated cards by their section, even when dimensions match.
|
|
1745
|
+
if (index < 3 && ancestors[index].text && getElementTextSignature(parent) === ancestors[index].text) score += 18;
|
|
1651
1746
|
parent = parent.parentElement;
|
|
1652
1747
|
}
|
|
1653
1748
|
|
|
@@ -1662,46 +1757,75 @@ function createAnchorMatcher({
|
|
|
1662
1757
|
return score;
|
|
1663
1758
|
}
|
|
1664
1759
|
|
|
1665
|
-
function matchesAnchorIdentity(el, fingerprint) {
|
|
1760
|
+
function matchesAnchorIdentity(el, fingerprint, ignoreDescendantText = false, allowTextChanges = true) {
|
|
1666
1761
|
if (!el || !fingerprint || el.tagName.toLowerCase() !== fingerprint.tag) return false;
|
|
1667
1762
|
const attributes = { ariaLabel: "aria-label", name: "name", href: "href", src: "src", alt: "alt", title: "title", role: "role" };
|
|
1668
1763
|
for (const [key, attribute] of Object.entries(attributes)) {
|
|
1669
1764
|
if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
|
|
1670
1765
|
}
|
|
1671
|
-
if (fingerprint.
|
|
1766
|
+
if (fingerprint.id && el.id !== fingerprint.id) return false;
|
|
1767
|
+
if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
|
|
1768
|
+
const stableIdentity = (fingerprint.id && el.id === fingerprint.id) ||
|
|
1769
|
+
(fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
|
|
1770
|
+
if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
|
|
1672
1771
|
// Container text is useful for matching remounted drawers and dialogs.
|
|
1673
|
-
if (!fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text)
|
|
1772
|
+
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
|
|
1773
|
+
if (!allowTextChanges || !el.children.length || !similarText(getElementTextSignature(el), fingerprint.text)) return false;
|
|
1774
|
+
}
|
|
1674
1775
|
return true;
|
|
1675
1776
|
}
|
|
1676
1777
|
|
|
1677
1778
|
function matchesAnchorContext(el, context) {
|
|
1678
1779
|
if (!context) return true;
|
|
1679
|
-
|
|
1780
|
+
// A unique page landmark keeps its identity when a child is edited.
|
|
1781
|
+
const pageContainer = !context.layer && (context.target?.tag === "main" || context.target?.role === "main") &&
|
|
1782
|
+
[...document.querySelectorAll('main, [role="main"]')].filter(node => !isComtyNode(node)).length === 1;
|
|
1783
|
+
if (!matchesAnchorIdentity(el, context.target, pageContainer)) return false;
|
|
1680
1784
|
const parentFingerprint = context.ancestors?.[0];
|
|
1681
1785
|
if (parentFingerprint?.text && parentFingerprint.text !== context.target.text &&
|
|
1682
|
-
!matchesAnchorIdentity(el.parentElement, parentFingerprint)) return false;
|
|
1786
|
+
!matchesAnchorIdentity(el.parentElement, parentFingerprint, pageContainer)) return false;
|
|
1683
1787
|
const layer = getPresentationLayer(el);
|
|
1684
|
-
if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer))) return false;
|
|
1788
|
+
if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer, false, false))) return false;
|
|
1685
1789
|
if (context.version >= 2 && !context.layer && layer) return false;
|
|
1686
1790
|
return scoreAnchorCandidate(el, context) >= 50;
|
|
1687
1791
|
}
|
|
1688
1792
|
|
|
1689
|
-
function
|
|
1690
|
-
if (!context?.target) return
|
|
1793
|
+
function resolveAnchor(context, exact = null) {
|
|
1794
|
+
if (!context?.target) return { element: exact, status: exact ? "matched" : "missing" };
|
|
1691
1795
|
let candidates;
|
|
1692
1796
|
try {
|
|
1693
|
-
candidates = [...document.body.querySelectorAll(context.target.tag || "*")];
|
|
1797
|
+
candidates = [...document.body.querySelectorAll(context.target.tag || "*")].filter(el => !isComtyNode(el));
|
|
1694
1798
|
} catch {
|
|
1695
|
-
return null;
|
|
1799
|
+
return { element: null, status: "missing" };
|
|
1696
1800
|
}
|
|
1697
1801
|
const matches = candidates
|
|
1698
|
-
.filter(el =>
|
|
1699
|
-
.map(el => ({ el, score: scoreAnchorCandidate(el, context) }))
|
|
1802
|
+
.filter(el => matchesAnchorContext(el, context))
|
|
1803
|
+
.map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
|
|
1700
1804
|
.sort((a, b) => b.score - a.score);
|
|
1701
|
-
if (
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1805
|
+
if (context.candidateCount > 1) {
|
|
1806
|
+
const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
|
|
1807
|
+
if (peers.length !== context.candidateCount) {
|
|
1808
|
+
// A removed identical sibling must not move its comment to the survivor.
|
|
1809
|
+
const distinctive = matches[0] && (context.target.id || context.target.testId);
|
|
1810
|
+
if (!distinctive) return { element: null, status: "uncertain" };
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (matches[0]?.score >= 58 && (!matches[1] || matches[0].score - matches[1].score >= 12)) {
|
|
1814
|
+
return { element: matches[0].el, status: "matched" };
|
|
1815
|
+
}
|
|
1816
|
+
// A closed/unmounted overlay is not evidence that its contents were deleted.
|
|
1817
|
+
if (context.layer && !candidates.some(el => {
|
|
1818
|
+
const layer = getPresentationLayer(el);
|
|
1819
|
+
return layer && matchesAnchorIdentity(layer, context.layer, false, false);
|
|
1820
|
+
})) return { element: null, status: "hidden" };
|
|
1821
|
+
const plausible = matches.length || candidates.some(el =>
|
|
1822
|
+
(context.target.id && el.id === context.target.id) ||
|
|
1823
|
+
(context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
|
|
1824
|
+
return { element: null, status: plausible ? "uncertain" : "missing" };
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function resolveByAnchorContext(context) {
|
|
1828
|
+
return resolveAnchor(context).element;
|
|
1705
1829
|
}
|
|
1706
1830
|
|
|
1707
1831
|
function getViewportBounds() {
|
|
@@ -1873,7 +1997,7 @@ function createAnchorMatcher({
|
|
|
1873
1997
|
return { clientX, clientY };
|
|
1874
1998
|
}
|
|
1875
1999
|
|
|
1876
|
-
return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
2000
|
+
return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
1877
2001
|
}
|
|
1878
2002
|
|
|
1879
2003
|
const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
|
|
@@ -1901,7 +2025,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
1901
2025
|
SOFTWARE.
|
|
1902
2026
|
|
|
1903
2027
|
*/
|
|
1904
|
-
function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function
|
|
2028
|
+
function S(t,e){if(t.match(/^[a-z]+:\/\//i))return t;if(t.match(/^\/\//))return window.location.protocol+t;if(t.match(/^[a-z]+:/i))return t;let r=document.implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),e&&(n.href=e),i.href=t,i.href}var P=(()=>{let t=0,e=()=>`0000${(Math.random()*36**4<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${e()}${t}`)})();function u(t){let e=[];for(let r=0,n=t.length;r<n;r++)e.push(t[r]);return e}var f=null;function y(t={}){return f||(t.includeStyleProperties?(f=t.includeStyleProperties,f):(f=u(window.getComputedStyle(document.documentElement)),f))}function w(t){return new Promise((e,r)=>{let n=new Image;n.onload=()=>{n.decode().then(()=>{requestAnimationFrame(()=>e(n))})},n.onerror=r,n.crossOrigin="anonymous",n.decoding="async",n.src=t})}async function I(t){return Promise.resolve().then(()=>new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then(e=>`data:image/svg+xml;charset=utf-8,${e}`)}async function U(t,e,r){let n="http://www.w3.org/2000/svg",i=document.createElementNS(n,"svg"),o=document.createElementNS(n,"foreignObject");return i.setAttribute("width",`${e}`),i.setAttribute("height",`${r}`),i.setAttribute("viewBox",`0 0 ${e} ${r}`),o.setAttribute("width","100%"),o.setAttribute("height","100%"),o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("externalResourcesRequired","true"),i.appendChild(o),o.appendChild(t),I(i)}var l=(t,e)=>{if(t instanceof e)return!0;let r=Object.getPrototypeOf(t);return r===null?!1:r.constructor.name===e.name||l(r,e)};function V(t){let e=t.getPropertyValue("content");return`${t.cssText} content: '${e.replace(/'|"/g,"")}';`}function M(t,e){return y(e).map(r=>{let n=t.getPropertyValue(r),i=t.getPropertyPriority(r);return`${r}: ${n}${i?" !important":""};`}).join(" ")}function F(t,e,r,n){let i=`.${t}:${e}`,o=r.cssText?V(r):M(r,n);return document.createTextNode(`${i}{${o}}`)}function R(t,e,r,n){let i=window.getComputedStyle(t,r),o=i.getPropertyValue("content");if(o===""||o==="none")return;let a=P();try{e.className=`${e.className} ${a}`}catch{return}let c=document.createElement("style");c.appendChild(F(a,r,i,n)),e.appendChild(c)}function C(t,e,r){R(t,e,":before",r),R(t,e,":after",r)}var T="application/font-woff",$="image/jpeg",H={woff:T,woff2:T,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:$,jpeg:$,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml",webp:"image/webp"};function O(t){let e=/\.([^./]*?)$/g.exec(t);return e?e[1]:""}function m(t){let e=O(t).toLowerCase();return H[e]||""}function k(t){return t.split(/,/)[1]}function p(t){return t.search(/^(data:)/)!==-1}function x(t,e){return`data:${e};base64,${t}`}async function q(t,e,r){let n=await(e?.captureFetch||fetch)(t,e);if(!n.ok)throw new Error(`Resource "${n.url}" not found`);let i=await n.blob();return new Promise((o,a)=>{let c=new FileReader;c.onerror=a,c.onloadend=()=>{try{o(r({res:n,result:c.result}))}catch(s){a(s)}},c.readAsDataURL(i)})}async function h(t,e,r){let n=r.resourceCache||{},i=t;if(n[i]!=null)return n[i];r.cacheBust&&(t+=(/\?/.test(t)?"&":"?")+new Date().getTime());let o;try{let a=await q(t,r.fetchRequestInit,({res:c,result:s})=>(e||(e=c.headers.get("Content-Type")||""),k(s)));o=x(a,e)}catch(a){o=r.imagePlaceholder||"";let c=`Failed to fetch resource: ${t}`;a&&(c=typeof a=="string"?a:a.message),c&&console.warn(c)}return n[i]=o,o}async function _(t){let e=t.toDataURL();return e==="data:,"?t.cloneNode(!1):w(e)}async function v(t,e){if(t.currentSrc){let o=document.createElement("canvas"),a=o.getContext("2d");o.width=t.clientWidth,o.height=t.clientHeight,a?.drawImage(t,0,0,o.width,o.height);let c=o.toDataURL();return w(c)}let r=t.poster,n=m(r),i=await h(r,n,e);return w(i)}async function B(t,e){var r;try{if(!((r=t?.contentDocument)===null||r===void 0)&&r.body)return await b(t.contentDocument.body,e,!0)}catch{}return t.cloneNode(!1)}async function G(t,e){return l(t,HTMLCanvasElement)?_(t).catch(()=>t.cloneNode(!1)):l(t,HTMLVideoElement)?v(t,e).catch(()=>document.createElement("div")):l(t,HTMLIFrameElement)?B(t,e):t.cloneNode(!1)}var j=t=>t.tagName!=null&&t.tagName.toUpperCase()==="SLOT";async function z(t,e,r){var n,i;let o=[];return j(t)&&t.assignedNodes?o=u(t.assignedNodes()):l(t,HTMLIFrameElement)&&(!((n=t.contentDocument)===null||n===void 0)&&n.body)?o=u(t.contentDocument.body.childNodes):o=u(((i=t.shadowRoot)!==null&&i!==void 0?i:t).childNodes),o.length===0||l(t,HTMLVideoElement)||await o.reduce((a,c)=>a.then(()=>b(c,r)).then(s=>{s&&e.appendChild(s)}),Promise.resolve()),e}function W(t,e,r){let n=e.style;if(!n)return;let i=window.getComputedStyle(t);i.cssText?(n.cssText=i.cssText,n.transformOrigin=i.transformOrigin):y(r).forEach(o=>{let a=i.getPropertyValue(o);o==="font-size"&&a.endsWith("px")&&(a=`${parseFloat(a.substring(0,a.length-2))}px`),l(t,HTMLIFrameElement)&&o==="display"&&a==="inline"&&(a="block"),o==="d"&&e.getAttribute("d")&&(a=`path(${e.getAttribute("d")})`),n.setProperty(o,a,i.getPropertyPriority(o))})}function X(t,e){l(t,HTMLTextAreaElement)&&(e.textContent=t.value),l(t,HTMLInputElement)&&e.setAttribute("value",t.value)}function K(t,e){if(l(t,HTMLSelectElement)){let n=Array.from(e.children).find(i=>t.value===i.getAttribute("value"));n&&n.setAttribute("selected","")}}function J(t,e,r){return l(e,Element)&&(W(t,e,r),C(t,e,r),X(t,e),K(t,e)),e}async function Q(t,e){let r=t.querySelectorAll?t.querySelectorAll("use"):[];if(r.length===0)return t;let n={};for(let o=0;o<r.length;o++){let a=r[o],c=a.getAttribute("href")||a.getAttribute("xlink:href");if(c){let s=t.querySelector(c),d=document.querySelector(c);!s&&d&&!n[c]&&(n[c]=await b(d,e,!0))}}let i=Object.values(n);if(i.length){let o="http://www.w3.org/1999/xhtml",a=document.createElementNS(o,"svg");a.setAttribute("xmlns",o),a.style.position="absolute",a.style.width="0",a.style.height="0",a.style.overflow="hidden",a.style.display="none";let c=document.createElementNS(o,"defs");a.appendChild(c);for(let s=0;s<i.length;s++)c.appendChild(i[s]);t.appendChild(a)}return t}async function b(t,e,r){if(await e.beforeClone?.(t),!r&&e.filter&&!e.filter(t))return null;let n=e.placeholder?.(t);return n||Promise.resolve(t).then(i=>G(i,e)).then(i=>z(t,i,e)).then(async i=>(await e.beforeClone?.(t),J(t,i,e))).then(i=>e.afterClone?.(t,i)??i).then(i=>Q(i,e))}var A=/url\((['"]?)([^'"]+?)\1\)/g,L=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,Y=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Z(t){let e=t.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`,"g")}function N(t){let e=[];return t.replace(A,(r,n,i)=>(e.push(i),r)),e.filter(r=>!p(r)&&!r.startsWith("#"))}async function tt(t,e,r,n,i){try{let o=r?S(e,r):e,a=m(e),c;if(i){let s=await i(o);c=x(s,a)}else c=await h(o,a,n);return t.replace(Z(e),`$1${c}$3`)}catch{}return t}function et(t,{preferredFontFormat:e}){return e?t.replace(Y,r=>{for(L.lastIndex=0;;){let[n,,i]=L.exec(r)||[];if(!i)return"";if(i===e)return`src: ${n};`}}):t}function rt(t){return t.search(A)!==-1}async function E(t,e,r){if(!rt(t))return t;let n=et(t,r);return N(n).reduce((o,a)=>o.then(c=>tt(c,a,e,r)),Promise.resolve(n))}async function g(t,e,r){var n;let i=(n=e.style)===null||n===void 0?void 0:n.getPropertyValue(t);if(i){let o=await E(i,null,r);return e.style.setProperty(t,o,e.style.getPropertyPriority(t)),!0}return!1}async function nt(t,e){await g("background",t,e)||await g("background-image",t,e),await g("mask",t,e)||await g("-webkit-mask",t,e)||await g("mask-image",t,e)||await g("-webkit-mask-image",t,e)}async function it(t,e){let r=l(t,HTMLImageElement);if(!(r&&!p(t.src))&&!(l(t,SVGImageElement)&&!p(t.href.baseVal)))return;let n=r?t.src:t.href.baseVal,i=await h(n,m(n),e);await new Promise((o,a)=>{t.onload=o,t.onerror=e.onImageErrorHandler?(...s)=>{try{o(e.onImageErrorHandler(...s))}catch(d){a(d)}}:a;let c=t;c.decode&&(c.decode=o),c.loading==="lazy"&&(c.loading="eager"),r?(t.srcset="",t.src=i):t.href.baseVal=i})}async function ot(t,e){let n=u(t.childNodes).map(i=>D(i,e));await Promise.all(n).then(()=>t)}async function D(t,e){await e.beforeClone?.(t),l(t,Element)&&(await nt(t,e),await it(t,e),await ot(t,e))} return {cloneNode:b,embedImages:D,embedResources:E,nodeToDataURL:U}; })();
|
|
1905
2029
|
// Fetches belong to one capture. Never forward SDK credentials to a resource host.
|
|
1906
2030
|
function createCaptureResources({ apiUrl, projectKey, getHeaders, signal }) {
|
|
1907
2031
|
const cache = new Map();
|
|
@@ -2221,7 +2345,12 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
|
|
|
2221
2345
|
await untilAborted(captureEngine.embedImages(stage, options));
|
|
2222
2346
|
checkContext();
|
|
2223
2347
|
await yieldCapture();
|
|
2224
|
-
|
|
2348
|
+
// Transparent page backgrounds reveal the browser canvas, not the image viewer.
|
|
2349
|
+
const viewport = document.createElement("div");
|
|
2350
|
+
viewport.style.backgroundColor = "Canvas";
|
|
2351
|
+
viewport.style.colorScheme = rootStyle.colorScheme;
|
|
2352
|
+
viewport.append(stage);
|
|
2353
|
+
const svg = await captureEngine.nodeToDataURL(viewport, context.width, context.height);
|
|
2225
2354
|
const image = new Image();
|
|
2226
2355
|
image.src = svg;
|
|
2227
2356
|
await untilAborted(image.decode());
|
|
@@ -2236,9 +2365,6 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
|
|
|
2236
2365
|
let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
|
|
2237
2366
|
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2238
2367
|
if (blob.size > 2800000) {
|
|
2239
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
2240
|
-
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("background-color");
|
|
2241
|
-
ctx.fillRect(0, 0, context.width, context.height);
|
|
2242
2368
|
blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
|
|
2243
2369
|
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2244
2370
|
}
|
|
@@ -2369,7 +2495,7 @@ function createCommentThread(context) {
|
|
|
2369
2495
|
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
2370
2496
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
2371
2497
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
2372
|
-
showAuthPrompt,
|
|
2498
|
+
showAuthPrompt, startThreadAgent,
|
|
2373
2499
|
} = context;
|
|
2374
2500
|
|
|
2375
2501
|
return function openCommentThread(
|
|
@@ -2432,16 +2558,16 @@ function createCommentThread(context) {
|
|
|
2432
2558
|
};
|
|
2433
2559
|
|
|
2434
2560
|
const saveStatus = async (nextStatus, control) => {
|
|
2435
|
-
if (!context.canManage) return false;
|
|
2561
|
+
if (!context.canManage || comment.local_agent) return false;
|
|
2436
2562
|
control.disabled = true;
|
|
2437
2563
|
const savedStatus = await updateCommentStatus(comment, nextStatus);
|
|
2438
2564
|
control.disabled = false;
|
|
2439
2565
|
if (!savedStatus) return false;
|
|
2440
2566
|
|
|
2441
2567
|
const savedStatusOption = getCommentStatus(savedStatus);
|
|
2442
|
-
pin.dataset.status = savedStatusOption.value;
|
|
2443
|
-
applyStatusDot(pinStatusDot, savedStatus);
|
|
2444
|
-
pin
|
|
2568
|
+
if (pin) pin.dataset.status = savedStatusOption.value;
|
|
2569
|
+
if (pinStatusDot) applyStatusDot(pinStatusDot, savedStatus);
|
|
2570
|
+
pin?.setAttribute(
|
|
2445
2571
|
"aria-label",
|
|
2446
2572
|
`Afficher le commentaire, statut ${savedStatusOption.label}`,
|
|
2447
2573
|
);
|
|
@@ -2457,7 +2583,7 @@ function createCommentThread(context) {
|
|
|
2457
2583
|
return true;
|
|
2458
2584
|
};
|
|
2459
2585
|
|
|
2460
|
-
statusMenuButton.disabled = !context.canManage;
|
|
2586
|
+
statusMenuButton.disabled = comment.local_agent || !context.canManage;
|
|
2461
2587
|
statusMenuButton.onclick = (event) => {
|
|
2462
2588
|
event.stopPropagation();
|
|
2463
2589
|
const existingPopover = document.querySelector(
|
|
@@ -2606,7 +2732,7 @@ function createCommentThread(context) {
|
|
|
2606
2732
|
);
|
|
2607
2733
|
};
|
|
2608
2734
|
|
|
2609
|
-
const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
|
|
2735
|
+
const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
|
|
2610
2736
|
|
|
2611
2737
|
const startEditingEntry = (entry, item, persist) => {
|
|
2612
2738
|
if (!isOwnCommentEntry(entry)) return;
|
|
@@ -2702,7 +2828,7 @@ function createCommentThread(context) {
|
|
|
2702
2828
|
getCommentId(cachedComment) !== getCommentId(comment),
|
|
2703
2829
|
);
|
|
2704
2830
|
placedPins.delete(getCommentId(comment));
|
|
2705
|
-
pin
|
|
2831
|
+
pin?.remove();
|
|
2706
2832
|
closeBubbles();
|
|
2707
2833
|
renderSidepanelComments();
|
|
2708
2834
|
applyStatusFilters();
|
|
@@ -2749,11 +2875,19 @@ function createCommentThread(context) {
|
|
|
2749
2875
|
};
|
|
2750
2876
|
replies.forEach(appendReply);
|
|
2751
2877
|
thread.appendChild(threadList);
|
|
2878
|
+
bubble.addEventListener('comty-agent-reply', (event) => {
|
|
2879
|
+
const reply = event.detail;
|
|
2880
|
+
if (!reply || replies.some(item => item.id === reply.id)) return;
|
|
2881
|
+
replies.push(reply);
|
|
2882
|
+
comment.replies = replies;
|
|
2883
|
+
appendReply(reply);
|
|
2884
|
+
threadList.scrollTop = threadList.scrollHeight;
|
|
2885
|
+
});
|
|
2752
2886
|
|
|
2753
2887
|
const replyForm = document.createElement("div");
|
|
2754
2888
|
replyForm.className = "comty-reply-form";
|
|
2755
2889
|
|
|
2756
|
-
if (context.canWrite) {
|
|
2890
|
+
if (context.canWrite && !comment.local_agent) {
|
|
2757
2891
|
replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
|
|
2758
2892
|
const replyField = document.createElement("div");
|
|
2759
2893
|
replyField.className = "comty-reply-field";
|
|
@@ -2796,6 +2930,18 @@ function createCommentThread(context) {
|
|
|
2796
2930
|
appendReply(data.reply);
|
|
2797
2931
|
replyInput.value = "";
|
|
2798
2932
|
syncReplySize();
|
|
2933
|
+
if (/(^|\s)@agent\b/i.test(content)) {
|
|
2934
|
+
try {
|
|
2935
|
+
await startThreadAgent(comment, replies, content);
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
const notice = document.createElement('p');
|
|
2938
|
+
notice.className = 'comty-agent-thread-error';
|
|
2939
|
+
notice.setAttribute('role', 'alert');
|
|
2940
|
+
notice.textContent = `Agent could not start: ${error.message}`;
|
|
2941
|
+
replyForm.querySelector('.comty-agent-thread-error')?.remove();
|
|
2942
|
+
replyForm.appendChild(notice);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2799
2945
|
requestAnimationFrame(() => {
|
|
2800
2946
|
threadList.scrollTop = threadList.scrollHeight;
|
|
2801
2947
|
});
|
|
@@ -2843,7 +2989,7 @@ function createCommentThread(context) {
|
|
|
2843
2989
|
replyField.appendChild(replyInput);
|
|
2844
2990
|
replyField.appendChild(replyButton);
|
|
2845
2991
|
replyForm.appendChild(replyField);
|
|
2846
|
-
} else {
|
|
2992
|
+
} else if (!comment.local_agent) {
|
|
2847
2993
|
replyForm.classList.add("comty-reply-form-guest");
|
|
2848
2994
|
const signInButton = document.createElement("button");
|
|
2849
2995
|
signInButton.className = "comty-sign-in-reply";
|
|
@@ -2856,7 +3002,7 @@ function createCommentThread(context) {
|
|
|
2856
3002
|
replyForm.appendChild(signInButton);
|
|
2857
3003
|
}
|
|
2858
3004
|
|
|
2859
|
-
thread.appendChild(replyForm);
|
|
3005
|
+
if (!comment.local_agent) thread.appendChild(replyForm);
|
|
2860
3006
|
bubble.appendChild(thread);
|
|
2861
3007
|
root.appendChild(bubble);
|
|
2862
3008
|
|
|
@@ -3093,7 +3239,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
|
|
|
3093
3239
|
/** Initialize once per document; safe to import and call during server rendering. */
|
|
3094
3240
|
function initComty(options = {}) {
|
|
3095
3241
|
if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
|
|
3096
|
-
const
|
|
3242
|
+
const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
|
|
3243
|
+
const agent = options.agent;
|
|
3244
|
+
if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
|
|
3245
|
+
return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
|
|
3246
|
+
}
|
|
3247
|
+
if (agent) {
|
|
3248
|
+
try {
|
|
3249
|
+
if (new URL(agent.url).origin !== window.location.origin) throw new Error();
|
|
3250
|
+
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
3251
|
+
}
|
|
3252
|
+
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
3097
3253
|
const apiUrl = options.apiUrl || "http://localhost:4000";
|
|
3098
3254
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
3099
3255
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
@@ -3105,12 +3261,14 @@ function initComty(options = {}) {
|
|
|
3105
3261
|
if (!["http:", "https:"].includes(parsedApi.protocol)) {
|
|
3106
3262
|
return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
|
|
3107
3263
|
}
|
|
3108
|
-
const configuration = JSON.stringify([projectKey, apiUrl]);
|
|
3264
|
+
const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
|
|
3109
3265
|
const existing = window[instanceKey];
|
|
3110
3266
|
if (existing) {
|
|
3111
3267
|
return existing.configuration === configuration ? existing.ready
|
|
3112
3268
|
: Promise.reject(new Error("Comty: already initialized with different options"));
|
|
3113
3269
|
}
|
|
3270
|
+
const agentBridgeUrl = agent?.url;
|
|
3271
|
+
const localOnly = Boolean(agent && !options.projectKey);
|
|
3114
3272
|
const ready = new Promise((resolve, reject) => {
|
|
3115
3273
|
const mount = () => {
|
|
3116
3274
|
try { resolve(mountComty()); } catch (error) { reject(error); }
|
|
@@ -3124,10 +3282,22 @@ function initComty(options = {}) {
|
|
|
3124
3282
|
|
|
3125
3283
|
function mountComty() {
|
|
3126
3284
|
let projectId = null;
|
|
3285
|
+
let localTasks = [];
|
|
3286
|
+
const deliveringAgentReplies = new Set();
|
|
3287
|
+
let localApprovals = [];
|
|
3288
|
+
const seenAgentApprovals = new Set();
|
|
3289
|
+
const seenFailedAgentTasks = new Set();
|
|
3290
|
+
let agentPanelOpen = false;
|
|
3291
|
+
let agentPanelSignature = "";
|
|
3292
|
+
let agentPanelTransition = 0;
|
|
3293
|
+
let agentConnectionError = "";
|
|
3294
|
+
let agentActionError = "";
|
|
3295
|
+
const agentPendingActions = new Set();
|
|
3127
3296
|
const session = {
|
|
3128
3297
|
mode: "off",
|
|
3129
|
-
get comments() { return this.mode === "browse" || this.mode === "edit"; },
|
|
3130
|
-
get edit() { return this.mode === "edit"; },
|
|
3298
|
+
get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
|
|
3299
|
+
get edit() { return this.mode === "edit" || this.mode === "agent"; },
|
|
3300
|
+
get agent() { return this.mode === "agent"; },
|
|
3131
3301
|
get inspect() { return this.mode === "inspect"; },
|
|
3132
3302
|
};
|
|
3133
3303
|
const selectedStatusFilters = new Set();
|
|
@@ -3151,9 +3321,10 @@ function initComty(options = {}) {
|
|
|
3151
3321
|
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
3152
3322
|
let sdkToken = "";
|
|
3153
3323
|
const activeHoverCommentIds = new Set();
|
|
3154
|
-
/** @type {null | 'edit' | 'browse'} */
|
|
3324
|
+
/** @type {null | 'edit' | 'browse' | 'agent'} */
|
|
3155
3325
|
let sessionEntry = null;
|
|
3156
3326
|
const placedPins = new Map();
|
|
3327
|
+
const workingOutlines = new Map();
|
|
3157
3328
|
const placementDiagnostics = new Map();
|
|
3158
3329
|
let inspectedElement = null;
|
|
3159
3330
|
let selectedElement = null;
|
|
@@ -3167,8 +3338,8 @@ function initComty(options = {}) {
|
|
|
3167
3338
|
const commentTargets = new Map();
|
|
3168
3339
|
|
|
3169
3340
|
const {
|
|
3170
|
-
matchesAnchorContext,
|
|
3171
|
-
isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
3341
|
+
matchesAnchorContext,
|
|
3342
|
+
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
3172
3343
|
} = createAnchorMatcher({
|
|
3173
3344
|
getElementTextSignature,
|
|
3174
3345
|
getElementOwnTextSignature,
|
|
@@ -3246,10 +3417,20 @@ function initComty(options = {}) {
|
|
|
3246
3417
|
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"));
|
|
3247
3418
|
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"/>', () => {
|
|
3248
3419
|
if (!canWrite) showAuthPrompt();
|
|
3249
|
-
else if (session.inspect || !isSessionActive()) startEditSession();
|
|
3420
|
+
else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
|
|
3250
3421
|
else if (!session.edit) setSession("edit", { entry: sessionEntry });
|
|
3251
3422
|
});
|
|
3252
3423
|
btn.id = "comty-toggle";
|
|
3424
|
+
const agentBtn = isLocalSite
|
|
3425
|
+
? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
|
|
3426
|
+
if (session.agent) {
|
|
3427
|
+
if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
|
|
3428
|
+
agentPanelOpen = true;
|
|
3429
|
+
renderAgentPanel();
|
|
3430
|
+
}
|
|
3431
|
+
} else startAgentSession();
|
|
3432
|
+
})
|
|
3433
|
+
: null;
|
|
3253
3434
|
const divider = document.createElement("span");
|
|
3254
3435
|
divider.className = "comty-tool-divider";
|
|
3255
3436
|
toolbar.appendChild(divider);
|
|
@@ -3259,7 +3440,7 @@ function initComty(options = {}) {
|
|
|
3259
3440
|
const primaryTools = document.createElement("div");
|
|
3260
3441
|
primaryTools.className = "comty-tool-primary";
|
|
3261
3442
|
toolbar.insertBefore(primaryTools, browseBtn);
|
|
3262
|
-
for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
|
|
3443
|
+
for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
|
|
3263
3444
|
const inspectorTools = document.createElement("div");
|
|
3264
3445
|
inspectorTools.className = "comty-tool-context";
|
|
3265
3446
|
inspectorTools.hidden = true;
|
|
@@ -4112,6 +4293,50 @@ function initComty(options = {}) {
|
|
|
4112
4293
|
let toolbarMotionFrame = 0;
|
|
4113
4294
|
let toolbarAnimations = [];
|
|
4114
4295
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
4296
|
+
let workingIndicatorTimer = 0;
|
|
4297
|
+
function scheduleWorkingIndicators() {
|
|
4298
|
+
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
4299
|
+
workingIndicatorTimer = setTimeout(() => {
|
|
4300
|
+
workingIndicatorTimer = 0;
|
|
4301
|
+
if (!root.isConnected) return;
|
|
4302
|
+
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
4303
|
+
if (!indicators.length) return;
|
|
4304
|
+
for (const indicator of indicators) {
|
|
4305
|
+
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
4306
|
+
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
4307
|
+
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
4308
|
+
indicator.dataset.activeDots = `${first},${second}`;
|
|
4309
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
4310
|
+
}
|
|
4311
|
+
scheduleWorkingIndicators();
|
|
4312
|
+
}, 600);
|
|
4313
|
+
}
|
|
4314
|
+
function createWorkingIndicator() {
|
|
4315
|
+
const indicator = document.createElement('span');
|
|
4316
|
+
indicator.className = 'comty-working-indicator';
|
|
4317
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
4318
|
+
indicator.dataset.activeDots = '3,5';
|
|
4319
|
+
for (let index = 0; index < 9; index++) {
|
|
4320
|
+
const dot = document.createElement('i');
|
|
4321
|
+
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
4322
|
+
indicator.appendChild(dot);
|
|
4323
|
+
}
|
|
4324
|
+
scheduleWorkingIndicators();
|
|
4325
|
+
return indicator;
|
|
4326
|
+
}
|
|
4327
|
+
function resetWorkingIndicators() {
|
|
4328
|
+
clearTimeout(workingIndicatorTimer);
|
|
4329
|
+
workingIndicatorTimer = 0;
|
|
4330
|
+
if (reducedMotion.matches) {
|
|
4331
|
+
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
4332
|
+
indicator.dataset.activeDots = '3,5';
|
|
4333
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
4337
|
+
}
|
|
4338
|
+
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
4339
|
+
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
4115
4340
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
4116
4341
|
function rememberToolbarPosition(left, top) {
|
|
4117
4342
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
@@ -4224,7 +4449,7 @@ function initComty(options = {}) {
|
|
|
4224
4449
|
|
|
4225
4450
|
function prepareToolbarMotion() {
|
|
4226
4451
|
if (toolbarMotionFrame) return;
|
|
4227
|
-
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools]
|
|
4452
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
|
|
4228
4453
|
// Move each group's background with its icons, including when interrupted.
|
|
4229
4454
|
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
4230
4455
|
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
@@ -4448,7 +4673,8 @@ function initComty(options = {}) {
|
|
|
4448
4673
|
selectedElement = null;
|
|
4449
4674
|
sessionEntry = null;
|
|
4450
4675
|
session.mode = "off";
|
|
4451
|
-
|
|
4676
|
+
syncPlacementCursor();
|
|
4677
|
+
document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
|
|
4452
4678
|
hideInspector();
|
|
4453
4679
|
setSidepanelOpen(false);
|
|
4454
4680
|
closeBubbles();
|
|
@@ -4462,25 +4688,29 @@ function initComty(options = {}) {
|
|
|
4462
4688
|
if (mode === "off") return;
|
|
4463
4689
|
if (mode === "inspect") {
|
|
4464
4690
|
session.mode = "inspect";
|
|
4691
|
+
document.documentElement.classList.add("comty-inspect-annotations");
|
|
4465
4692
|
syncToolbar();
|
|
4693
|
+
renderCachedComments();
|
|
4466
4694
|
ensureLiveTracking();
|
|
4467
4695
|
return;
|
|
4468
4696
|
}
|
|
4469
4697
|
|
|
4470
|
-
if (session.edit && mode === "browse") {
|
|
4698
|
+
if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
|
|
4471
4699
|
closeBubbles();
|
|
4472
4700
|
}
|
|
4473
4701
|
const hadComments = session.comments;
|
|
4474
4702
|
session.mode = mode;
|
|
4475
4703
|
sessionEntry = entry;
|
|
4704
|
+
document.documentElement.classList.remove("comty-inspect-annotations");
|
|
4476
4705
|
document.documentElement.classList.toggle("comty-comments-active", session.comments);
|
|
4477
|
-
|
|
4706
|
+
syncPlacementCursor();
|
|
4478
4707
|
if (session.edit) {
|
|
4479
4708
|
setMotionEnabled(false);
|
|
4480
4709
|
selectedElement = null;
|
|
4481
4710
|
} else {
|
|
4482
4711
|
hideInspector();
|
|
4483
4712
|
}
|
|
4713
|
+
if (session.agent && sidepanelOpen) setSidepanelOpen(false);
|
|
4484
4714
|
syncToolbar();
|
|
4485
4715
|
ensureLiveTracking();
|
|
4486
4716
|
if (renderComments || !hadComments) renderCachedComments();
|
|
@@ -4493,6 +4723,10 @@ function initComty(options = {}) {
|
|
|
4493
4723
|
}
|
|
4494
4724
|
|
|
4495
4725
|
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
4726
|
+
if (localOnly) {
|
|
4727
|
+
showAgentBridgeError("Shared comments need a Comty project key. Use the Agent tool for local tasks without signing in.");
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4496
4730
|
const guestAllowed = allowGuestComments;
|
|
4497
4731
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
4498
4732
|
const opener = document.activeElement;
|
|
@@ -4614,14 +4848,16 @@ function initComty(options = {}) {
|
|
|
4614
4848
|
}
|
|
4615
4849
|
|
|
4616
4850
|
function syncToolbar() {
|
|
4617
|
-
if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
4851
|
+
if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
4618
4852
|
root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
|
|
4619
4853
|
browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
|
|
4620
4854
|
inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
|
|
4621
|
-
btn.setAttribute("aria-pressed", String(session.edit));
|
|
4622
|
-
|
|
4855
|
+
btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
|
|
4856
|
+
agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
|
|
4857
|
+
commentTools.hidden = session.inspect || session.agent;
|
|
4623
4858
|
inspectorTools.hidden = !session.inspect;
|
|
4624
4859
|
syncInspectorTools();
|
|
4860
|
+
renderAgentPanel();
|
|
4625
4861
|
}
|
|
4626
4862
|
|
|
4627
4863
|
const inspectionLayer = document.createElement("div");
|
|
@@ -5084,6 +5320,13 @@ function initComty(options = {}) {
|
|
|
5084
5320
|
setSession("edit", { renderComments: true });
|
|
5085
5321
|
}
|
|
5086
5322
|
|
|
5323
|
+
function startAgentSession() {
|
|
5324
|
+
if (!isLocalSite) return;
|
|
5325
|
+
if (session.agent) return;
|
|
5326
|
+
closeBubbles();
|
|
5327
|
+
setSession("agent", { renderComments: true });
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5087
5330
|
function toggleSidepanel() {
|
|
5088
5331
|
if (sidepanelOpen) {
|
|
5089
5332
|
setSidepanelOpen(false);
|
|
@@ -5107,7 +5350,7 @@ function initComty(options = {}) {
|
|
|
5107
5350
|
showAuthPrompt();
|
|
5108
5351
|
return;
|
|
5109
5352
|
}
|
|
5110
|
-
if (session.inspect || !isSessionActive()) {
|
|
5353
|
+
if (session.agent || session.inspect || !isSessionActive()) {
|
|
5111
5354
|
startEditSession();
|
|
5112
5355
|
return;
|
|
5113
5356
|
}
|
|
@@ -5395,6 +5638,24 @@ function initComty(options = {}) {
|
|
|
5395
5638
|
ancestors,
|
|
5396
5639
|
layer: layer ? getElementFingerprint(layer) : null,
|
|
5397
5640
|
};
|
|
5641
|
+
// React exposes this in development builds. Keep it as a hint: the coding
|
|
5642
|
+
// agent must confirm the component and variant in source before editing.
|
|
5643
|
+
const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
|
|
5644
|
+
let fiber = fiberKey ? el[fiberKey] : null;
|
|
5645
|
+
const components = [];
|
|
5646
|
+
while (fiber && components.length < 8) {
|
|
5647
|
+
const component = fiber.type;
|
|
5648
|
+
const name = component?.displayName || component?.name;
|
|
5649
|
+
if (name && typeof name === 'string') {
|
|
5650
|
+
const props = fiber.memoizedProps || {};
|
|
5651
|
+
const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
|
|
5652
|
+
['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
|
|
5653
|
+
const source = fiber._debugSource;
|
|
5654
|
+
components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
|
|
5655
|
+
}
|
|
5656
|
+
fiber = fiber.return;
|
|
5657
|
+
}
|
|
5658
|
+
if (components.length) context.components = components;
|
|
5398
5659
|
context.candidateCount = [...document.querySelectorAll(context.target.tag)]
|
|
5399
5660
|
.filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
|
|
5400
5661
|
return context;
|
|
@@ -5423,6 +5684,11 @@ function initComty(options = {}) {
|
|
|
5423
5684
|
};
|
|
5424
5685
|
}
|
|
5425
5686
|
|
|
5687
|
+
function getPagePath() {
|
|
5688
|
+
const { pathname, hash } = window.location;
|
|
5689
|
+
return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5426
5692
|
function resolveBySelector(selector) {
|
|
5427
5693
|
if (!selector) return null;
|
|
5428
5694
|
try {
|
|
@@ -5444,17 +5710,9 @@ function initComty(options = {}) {
|
|
|
5444
5710
|
}
|
|
5445
5711
|
const context = parseAnchorContext(comment);
|
|
5446
5712
|
const exact = resolveBySelector(comment.selector || "body");
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
if (count !== context.candidateCount) {
|
|
5451
|
-
commentTargets.set(key, { element: null, signature, revision: anchorRevision });
|
|
5452
|
-
return null;
|
|
5453
|
-
}
|
|
5454
|
-
}
|
|
5455
|
-
let element = exact && matchesAnchorContext(exact, context) ? getAnchorElement(exact) : null;
|
|
5456
|
-
if (!element) element = resolveByAnchorContext(context);
|
|
5457
|
-
commentTargets.set(key, { element, signature, revision: anchorRevision });
|
|
5713
|
+
const resolution = resolveAnchor(context, exact);
|
|
5714
|
+
const element = resolution.element ? getAnchorElement(resolution.element) : null;
|
|
5715
|
+
commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
|
|
5458
5716
|
return element;
|
|
5459
5717
|
}
|
|
5460
5718
|
|
|
@@ -5525,9 +5783,14 @@ function initComty(options = {}) {
|
|
|
5525
5783
|
return { deviceType: "desktop", deviceLabel: "Desktop" };
|
|
5526
5784
|
}
|
|
5527
5785
|
|
|
5786
|
+
function syncPlacementCursor() {
|
|
5787
|
+
document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5528
5790
|
function closeBubbles() {
|
|
5529
5791
|
const hadDraft = Boolean(draftAnchor);
|
|
5530
5792
|
draftAnchor = null;
|
|
5793
|
+
syncPlacementCursor();
|
|
5531
5794
|
activeHoverCommentIds.clear();
|
|
5532
5795
|
if (!session.edit || hadDraft) hideInspector();
|
|
5533
5796
|
document
|
|
@@ -5572,7 +5835,7 @@ function initComty(options = {}) {
|
|
|
5572
5835
|
pending: commentId.startsWith("pending-"),
|
|
5573
5836
|
project_id: projectId,
|
|
5574
5837
|
domain: payload.host || window.location.host,
|
|
5575
|
-
page_path: payload.pagePath ||
|
|
5838
|
+
page_path: payload.pagePath || getPagePath(),
|
|
5576
5839
|
selector: payload.selector || "body",
|
|
5577
5840
|
rel_x: payload.relX,
|
|
5578
5841
|
rel_y: payload.relY,
|
|
@@ -5597,7 +5860,7 @@ function initComty(options = {}) {
|
|
|
5597
5860
|
replies: [],
|
|
5598
5861
|
};
|
|
5599
5862
|
|
|
5600
|
-
cachedPagePath =
|
|
5863
|
+
cachedPagePath = getPagePath();
|
|
5601
5864
|
cachedComments = [
|
|
5602
5865
|
...cachedComments.filter(
|
|
5603
5866
|
(cached) => getCommentId(cached) !== commentId,
|
|
@@ -5610,6 +5873,7 @@ function initComty(options = {}) {
|
|
|
5610
5873
|
}
|
|
5611
5874
|
|
|
5612
5875
|
function getCommentAuthorName(comment) {
|
|
5876
|
+
if (comment?.author_kind === 'agent') return 'Agent';
|
|
5613
5877
|
return comment?.author_name || comment?.name || "Anonymous";
|
|
5614
5878
|
}
|
|
5615
5879
|
|
|
@@ -5631,6 +5895,12 @@ function initComty(options = {}) {
|
|
|
5631
5895
|
function createCommentAvatar(comment, extraClass = "") {
|
|
5632
5896
|
const avatar = document.createElement("span");
|
|
5633
5897
|
avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
|
|
5898
|
+
if (comment?.author_kind === 'agent') {
|
|
5899
|
+
avatar.classList.add('comty-agent-avatar');
|
|
5900
|
+
avatar.setAttribute('aria-label', 'Agent');
|
|
5901
|
+
avatar.innerHTML = '<svg viewBox="0 0 48 48" aria-hidden="true"><path d="M24 2C26.62 2 28.47 5.29 31.27 6.45C34.07 7.61 37.71 6.59 39.56 8.44C41.41 10.29 40.39 13.93 41.55 16.73C42.71 19.53 46 21.38 46 24C46 26.62 42.71 28.47 41.55 31.27C40.39 34.07 41.41 37.71 39.56 39.56C37.71 41.41 34.07 40.39 31.27 41.55C28.47 42.71 26.62 46 24 46C21.38 46 19.53 42.71 16.73 41.55C13.93 40.39 10.29 41.41 8.44 39.56C6.59 37.71 7.61 34.07 6.45 31.27C5.29 28.47 2 26.62 2 24C2 21.38 5.29 19.53 6.45 16.73C7.61 13.93 6.59 10.29 8.44 8.44C10.29 6.59 13.93 7.61 16.73 6.45C19.53 5.29 21.38 2 24 2Z" fill="currentColor"/><path fill="var(--comty-surface)" fill-rule="evenodd" d="M16 33 22.3 15h3.4L32 33h-3.3l-1.3-4h-6.8l-1.3 4H16Zm5.5-6.8h5L24 18.4l-2.5 7.8Z"/></svg>';
|
|
5902
|
+
return avatar;
|
|
5903
|
+
}
|
|
5634
5904
|
const authorName = getCommentAuthorName(comment);
|
|
5635
5905
|
const avatarUrl = getCommentAuthorAvatarUrl(comment);
|
|
5636
5906
|
|
|
@@ -5638,6 +5908,7 @@ function initComty(options = {}) {
|
|
|
5638
5908
|
const image = document.createElement("img");
|
|
5639
5909
|
image.src = avatarUrl;
|
|
5640
5910
|
image.alt = "";
|
|
5911
|
+
image.draggable = false;
|
|
5641
5912
|
image.onerror = () => {
|
|
5642
5913
|
image.remove();
|
|
5643
5914
|
avatar.innerText = authorName.charAt(0).toUpperCase();
|
|
@@ -5862,6 +6133,7 @@ function initComty(options = {}) {
|
|
|
5862
6133
|
payload,
|
|
5863
6134
|
preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
|
|
5864
6135
|
};
|
|
6136
|
+
syncPlacementCursor();
|
|
5865
6137
|
ensureLiveTracking();
|
|
5866
6138
|
|
|
5867
6139
|
const pin = document.createElement("div");
|
|
@@ -5879,10 +6151,21 @@ function initComty(options = {}) {
|
|
|
5879
6151
|
|
|
5880
6152
|
const textarea = document.createElement("textarea");
|
|
5881
6153
|
textarea.className = "comty-create-textarea";
|
|
5882
|
-
textarea.placeholder = "Add a comment";
|
|
6154
|
+
textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
|
|
5883
6155
|
textarea.setAttribute("aria-label", textarea.placeholder);
|
|
5884
6156
|
textarea.rows = 1;
|
|
5885
6157
|
|
|
6158
|
+
const scopePicker = document.createElement('label');
|
|
6159
|
+
scopePicker.className = 'comty-agent-scope';
|
|
6160
|
+
if (payload.kind === 'agent') {
|
|
6161
|
+
const input = document.createElement('input');
|
|
6162
|
+
input.type = 'checkbox';
|
|
6163
|
+
input.setAttribute('aria-label', 'Apply to all instances of this variant');
|
|
6164
|
+
const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
|
|
6165
|
+
scopePicker.title = 'Apply to all instances of this variant';
|
|
6166
|
+
scopePicker.append(input, track, document.createTextNode('Apply globally'));
|
|
6167
|
+
}
|
|
6168
|
+
|
|
5886
6169
|
const actions = document.createElement("div");
|
|
5887
6170
|
actions.className = "comty-create-actions";
|
|
5888
6171
|
|
|
@@ -5936,7 +6219,7 @@ function initComty(options = {}) {
|
|
|
5936
6219
|
const submitBtn = document.createElement("button");
|
|
5937
6220
|
submitBtn.type = "button";
|
|
5938
6221
|
submitBtn.className = "comty-btn-submit";
|
|
5939
|
-
submitBtn.setAttribute("aria-label", "Post comment");
|
|
6222
|
+
submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
|
|
5940
6223
|
submitBtn.disabled = true;
|
|
5941
6224
|
submitBtn.innerHTML = `
|
|
5942
6225
|
<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">
|
|
@@ -5953,49 +6236,62 @@ function initComty(options = {}) {
|
|
|
5953
6236
|
textarea.disabled = true;
|
|
5954
6237
|
linkBtn.disabled = true;
|
|
5955
6238
|
|
|
6239
|
+
if (payload.kind === "agent" && !agent) {
|
|
6240
|
+
showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
|
|
6241
|
+
}
|
|
5956
6242
|
payload.content = content;
|
|
6243
|
+
if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
|
|
5957
6244
|
payload.linkUrl = attachedLink || null;
|
|
5958
6245
|
textarea.textContent = content;
|
|
5959
6246
|
|
|
5960
|
-
const
|
|
5961
|
-
const
|
|
5962
|
-
const
|
|
5963
|
-
|
|
6247
|
+
const isAgentTask = payload.kind === "agent";
|
|
6248
|
+
const context = isAgentTask ? null : captureContext();
|
|
6249
|
+
const point = isAgentTask ? null : getPinPoint(pin);
|
|
6250
|
+
const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
|
|
5964
6251
|
bubble.remove();
|
|
5965
6252
|
pin.remove();
|
|
5966
6253
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
6254
|
+
syncPlacementCursor();
|
|
5967
6255
|
hideInspector();
|
|
5968
|
-
addLocalComment(pendingId, payload);
|
|
6256
|
+
if (!isAgentTask) addLocalComment(pendingId, payload);
|
|
5969
6257
|
let post;
|
|
5970
6258
|
try {
|
|
5971
|
-
post = await fetch(`${apiUrl}/api/comments`, {
|
|
6259
|
+
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
5972
6260
|
method: "POST",
|
|
5973
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
5974
|
-
body: JSON.stringify(payload),
|
|
6261
|
+
headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
6262
|
+
body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
|
|
5975
6263
|
});
|
|
5976
|
-
if (!post.ok) throw new Error(`
|
|
6264
|
+
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
5977
6265
|
const data = await post.json();
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
if (
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
6266
|
+
const id = isAgentTask ? data?.task?.id : data?.id;
|
|
6267
|
+
if (!id) throw new Error("Save returned no ID");
|
|
6268
|
+
if (isAgentTask) {
|
|
6269
|
+
void loadComments({ force: true });
|
|
6270
|
+
} else {
|
|
6271
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
6272
|
+
if (context.url === location.href) {
|
|
6273
|
+
const comment = addLocalComment(String(id), payload);
|
|
6274
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
6275
|
+
} else void loadComments({ force: true });
|
|
6276
|
+
}
|
|
5985
6277
|
} catch (error) {
|
|
5986
6278
|
console.error("Comty: save failed", error);
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
6279
|
+
if (isAgentTask) showAgentBridgeError(error.message);
|
|
6280
|
+
else {
|
|
6281
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
6282
|
+
renderSidepanelComments();
|
|
6283
|
+
void renderComments(cachedComments, { retry: false });
|
|
6284
|
+
}
|
|
5990
6285
|
// Keep the original editor and its text available for retry, including attachments.
|
|
5991
6286
|
if (!draftAnchor) draftAnchor = editorDraft;
|
|
6287
|
+
syncPlacementCursor();
|
|
5992
6288
|
root.append(pin, bubble);
|
|
5993
6289
|
submitBtn.disabled = false;
|
|
5994
6290
|
textarea.disabled = false;
|
|
5995
6291
|
linkBtn.disabled = false;
|
|
5996
6292
|
const errorMessage = document.createElement("p");
|
|
5997
6293
|
errorMessage.setAttribute("role", "alert");
|
|
5998
|
-
errorMessage.textContent = "Comment not saved. Your draft is ready to retry
|
|
6294
|
+
errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
|
|
5999
6295
|
bubble.querySelector('[role="alert"]')?.remove();
|
|
6000
6296
|
bubble.append(errorMessage);
|
|
6001
6297
|
if (bubble.isConnected) textarea.focus();
|
|
@@ -6084,7 +6380,8 @@ function initComty(options = {}) {
|
|
|
6084
6380
|
renderInspector();
|
|
6085
6381
|
};
|
|
6086
6382
|
|
|
6087
|
-
actions.append(
|
|
6383
|
+
if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
|
|
6384
|
+
else actions.append(linkBtn, submitBtn, linkEditor);
|
|
6088
6385
|
|
|
6089
6386
|
bubble.appendChild(textarea);
|
|
6090
6387
|
bubble.appendChild(attachment);
|
|
@@ -6097,6 +6394,219 @@ function initComty(options = {}) {
|
|
|
6097
6394
|
setTimeout(() => textarea.focus(), 10);
|
|
6098
6395
|
}
|
|
6099
6396
|
|
|
6397
|
+
function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
|
|
6398
|
+
|
|
6399
|
+
function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
|
|
6400
|
+
root.querySelectorAll(".comty-agent-task-toast").forEach((node) => node.remove());
|
|
6401
|
+
const toast = document.createElement("div");
|
|
6402
|
+
toast.className = "comty-auth-toast comty-agent-task-toast";
|
|
6403
|
+
toast.setAttribute("role", "alert");
|
|
6404
|
+
const issue = describeAgentError(message);
|
|
6405
|
+
toast.textContent = issue.title + ". " + issue.description;
|
|
6406
|
+
root.appendChild(toast);
|
|
6407
|
+
setTimeout(() => toast.remove(), 5000);
|
|
6408
|
+
}
|
|
6409
|
+
|
|
6410
|
+
function describeAgentError(error) {
|
|
6411
|
+
const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
|
|
6412
|
+
let value = error;
|
|
6413
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
6414
|
+
if (typeof value === 'string') {
|
|
6415
|
+
try { value = JSON.parse(value); continue; } catch {}
|
|
6416
|
+
const start = value.indexOf('{'), end = value.lastIndexOf('}');
|
|
6417
|
+
if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
if (!value || typeof value !== 'object') break;
|
|
6421
|
+
value = value.error || value.message || value.cause || value.stderr || value;
|
|
6422
|
+
}
|
|
6423
|
+
const message = typeof value === 'string' ? value : '';
|
|
6424
|
+
const source = raw + ' ' + message;
|
|
6425
|
+
const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
|
|
6426
|
+
let title, description;
|
|
6427
|
+
if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
|
|
6428
|
+
title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
|
|
6429
|
+
} else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
|
|
6430
|
+
title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
|
|
6431
|
+
} else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
|
|
6432
|
+
title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
|
|
6433
|
+
} else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
|
|
6434
|
+
title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
|
|
6435
|
+
} else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
|
|
6436
|
+
title = 'Model unavailable'; description = 'Choose a model available to your agent account in the local Comty configuration, then restart and retry.';
|
|
6437
|
+
} else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
|
|
6438
|
+
title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
|
|
6439
|
+
} else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
|
|
6440
|
+
title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
|
|
6441
|
+
} else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
|
|
6442
|
+
title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
|
|
6443
|
+
} else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
|
|
6444
|
+
title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
|
|
6445
|
+
} else if (/completion was not verified/i.test(source)) {
|
|
6446
|
+
title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
|
|
6447
|
+
} else {
|
|
6448
|
+
title = 'Task could not finish';
|
|
6449
|
+
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.';
|
|
6450
|
+
}
|
|
6451
|
+
return { title, description, raw, message };
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6454
|
+
function renderAgentPanel() {
|
|
6455
|
+
if (!agent) return;
|
|
6456
|
+
const visibleTasks = [];
|
|
6457
|
+
const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
|
|
6458
|
+
if (latestResult) visibleTasks.push(latestResult);
|
|
6459
|
+
if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
|
|
6460
|
+
let panel = root.querySelector(".comty-agent-panel");
|
|
6461
|
+
if (!panel && !agentPanelOpen) return;
|
|
6462
|
+
if (!panel) {
|
|
6463
|
+
panel = document.createElement("section");
|
|
6464
|
+
panel.className = "comty-agent-panel";
|
|
6465
|
+
panel.setAttribute("aria-label", "Local agent tasks");
|
|
6466
|
+
panel.hidden = true;
|
|
6467
|
+
// Keep the task card above inspection overlays and below the toolbar.
|
|
6468
|
+
root.insertBefore(panel, toolbar);
|
|
6469
|
+
}
|
|
6470
|
+
const open = Boolean(agent) && agentPanelOpen;
|
|
6471
|
+
if (open) {
|
|
6472
|
+
if (panel.hidden) {
|
|
6473
|
+
const rect = toolbar.getBoundingClientRect();
|
|
6474
|
+
panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
|
|
6475
|
+
panel.dataset.closed = "";
|
|
6476
|
+
panel.hidden = false;
|
|
6477
|
+
panel.getBoundingClientRect();
|
|
6478
|
+
}
|
|
6479
|
+
if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
|
|
6480
|
+
} else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
|
|
6481
|
+
const transition = ++agentPanelTransition;
|
|
6482
|
+
panel.dataset.closed = "";
|
|
6483
|
+
const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
|
|
6484
|
+
if (reducedMotion.matches) finish();
|
|
6485
|
+
else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
|
|
6486
|
+
}
|
|
6487
|
+
panel.inert = !open;
|
|
6488
|
+
panel.setAttribute("aria-hidden", String(!open));
|
|
6489
|
+
const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
|
|
6490
|
+
if (signature === agentPanelSignature) return;
|
|
6491
|
+
agentPanelSignature = signature;
|
|
6492
|
+
root.querySelector('.comty-agent-error-popover')?.remove();
|
|
6493
|
+
const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
|
|
6494
|
+
const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
|
|
6495
|
+
const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
|
|
6496
|
+
const node = (tag, className, text) => {
|
|
6497
|
+
const element = document.createElement(tag);
|
|
6498
|
+
if (className) element.className = className;
|
|
6499
|
+
if (text != null) element.textContent = text;
|
|
6500
|
+
return element;
|
|
6501
|
+
};
|
|
6502
|
+
const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
|
|
6503
|
+
panel.replaceChildren();
|
|
6504
|
+
const header = node('header', 'comty-agent-panel-header');
|
|
6505
|
+
const heading = node('div', 'comty-agent-heading');
|
|
6506
|
+
heading.append(node('strong', '', 'Agent'));
|
|
6507
|
+
const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
|
|
6508
|
+
toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
6509
|
+
toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
|
|
6510
|
+
toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
|
|
6511
|
+
header.append(heading, toggle); panel.appendChild(header);
|
|
6512
|
+
const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
|
|
6513
|
+
const technicalDetails = (parent, raw, key) => {
|
|
6514
|
+
const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
|
|
6515
|
+
const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
|
|
6516
|
+
details.append(summary, node('pre', '', raw)); parent.appendChild(details);
|
|
6517
|
+
};
|
|
6518
|
+
const errorMessage = (parent, error, key) => {
|
|
6519
|
+
const issue = describeAgentError(error);
|
|
6520
|
+
const message = node('div', 'comty-agent-error');
|
|
6521
|
+
message.append(node('strong', '', issue.title), node('p', '', issue.description));
|
|
6522
|
+
parent.appendChild(message); technicalDetails(parent, issue.raw, key);
|
|
6523
|
+
};
|
|
6524
|
+
if (agentConnectionError || agentActionError) {
|
|
6525
|
+
const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
|
|
6526
|
+
errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
|
|
6527
|
+
}
|
|
6528
|
+
const action = (parent, label, path, input = {}, primary = false) => {
|
|
6529
|
+
const button = node('button', 'comty-agent-action', label); button.type = 'button';
|
|
6530
|
+
const key = path + JSON.stringify(input); button.dataset.focusKey = key;
|
|
6531
|
+
button.disabled = agentPendingActions.has(key);
|
|
6532
|
+
if (primary) button.dataset.primary = '';
|
|
6533
|
+
button.onclick = async () => {
|
|
6534
|
+
if (agentPendingActions.has(key)) return;
|
|
6535
|
+
agentActionError = '';
|
|
6536
|
+
agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
|
|
6537
|
+
try {
|
|
6538
|
+
const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
|
|
6539
|
+
if (!response.ok) {
|
|
6540
|
+
const body = await response.json().catch(() => null);
|
|
6541
|
+
throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
|
|
6542
|
+
}
|
|
6543
|
+
await loadComments({ force: true });
|
|
6544
|
+
} catch (error) { agentActionError = error.message; }
|
|
6545
|
+
finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
|
|
6546
|
+
};
|
|
6547
|
+
parent.appendChild(button);
|
|
6548
|
+
};
|
|
6549
|
+
for (const approval of localApprovals) {
|
|
6550
|
+
const section = node('div', 'comty-agent-item');
|
|
6551
|
+
section.append(node('h3', '', 'Approval required'));
|
|
6552
|
+
const task = localTasks.find(task => task.id === approval.taskId);
|
|
6553
|
+
if (task) section.appendChild(node('p', '', task.content));
|
|
6554
|
+
section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
|
|
6555
|
+
const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
|
|
6556
|
+
if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
|
|
6557
|
+
technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
|
|
6558
|
+
const actions = node('div', 'comty-agent-actions');
|
|
6559
|
+
action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
|
|
6560
|
+
action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
|
|
6561
|
+
section.appendChild(actions); content.appendChild(section);
|
|
6562
|
+
}
|
|
6563
|
+
const list = node('div', 'comty-agent-list'); content.appendChild(list);
|
|
6564
|
+
const hideErrorPopover = () => root.querySelector('.comty-agent-error-popover')?.remove();
|
|
6565
|
+
content.addEventListener('scroll', hideErrorPopover, { passive: true });
|
|
6566
|
+
for (const [index, task] of visibleTasks.reverse().entries()) {
|
|
6567
|
+
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
6568
|
+
const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
|
|
6569
|
+
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
6570
|
+
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
6571
|
+
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
6572
|
+
const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
|
|
6573
|
+
row.append(dot, name);
|
|
6574
|
+
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
6575
|
+
const actions = node('div', 'comty-agent-row-actions');
|
|
6576
|
+
if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
|
|
6577
|
+
if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
|
|
6578
|
+
if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
6579
|
+
if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
6580
|
+
if (actions.childElementCount) row.appendChild(actions);
|
|
6581
|
+
row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
|
|
6582
|
+
if (failed) {
|
|
6583
|
+
const tooltipId = `comty-agent-error-${index}`;
|
|
6584
|
+
const showErrorPopover = () => {
|
|
6585
|
+
hideErrorPopover();
|
|
6586
|
+
const issue = describeAgentError(task.error || task.result?.summary || task.progress || 'Completion was not verified');
|
|
6587
|
+
const popover = node('div', 'comty-agent-error-popover'); popover.id = tooltipId; popover.setAttribute('role', 'tooltip');
|
|
6588
|
+
popover.append(node('strong', '', task.content), node('p', '', issue.message || issue.description));
|
|
6589
|
+
root.appendChild(popover);
|
|
6590
|
+
const rowRect = row.getBoundingClientRect();
|
|
6591
|
+
const popoverRect = popover.getBoundingClientRect();
|
|
6592
|
+
const left = panel.dataset.side === 'right' ? rowRect.left - popoverRect.width - 8 : rowRect.right + 8;
|
|
6593
|
+
popover.style.left = `${Math.max(8, Math.min(left, innerWidth - popoverRect.width - 8))}px`;
|
|
6594
|
+
popover.style.top = `${Math.max(8, Math.min(rowRect.top - 4, innerHeight - popoverRect.height - 8))}px`;
|
|
6595
|
+
};
|
|
6596
|
+
row.addEventListener('mouseenter', showErrorPopover);
|
|
6597
|
+
row.addEventListener('mouseleave', hideErrorPopover);
|
|
6598
|
+
}
|
|
6599
|
+
list.appendChild(row);
|
|
6600
|
+
if (task.result?.summary) {
|
|
6601
|
+
const answer = node('div', 'comty-agent-answer');
|
|
6602
|
+
answer.append(brand(), node('p', '', task.result.summary));
|
|
6603
|
+
list.appendChild(answer);
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
content.scrollTop = scrollTop;
|
|
6607
|
+
if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6100
6610
|
function setSidepanelOpen(open) {
|
|
6101
6611
|
sidepanelOpen = open;
|
|
6102
6612
|
sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
|
|
@@ -6132,12 +6642,52 @@ function initComty(options = {}) {
|
|
|
6132
6642
|
|
|
6133
6643
|
function openCommentFromPanel(comment) {
|
|
6134
6644
|
const target = resolveCommentElement(comment);
|
|
6135
|
-
if (!target || isElementTreeHidden(target))
|
|
6645
|
+
if (!target || isElementTreeHidden(target)) {
|
|
6646
|
+
openDetachedThread(comment);
|
|
6647
|
+
return;
|
|
6648
|
+
}
|
|
6136
6649
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
6137
6650
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
6651
|
+
const rect = target.getBoundingClientRect();
|
|
6652
|
+
const relX = Number(comment.rel_x ?? comment.relX);
|
|
6653
|
+
const relY = Number(comment.rel_y ?? comment.relY);
|
|
6654
|
+
// Large page containers must reveal the annotation point, not their center.
|
|
6655
|
+
window.scrollBy({
|
|
6656
|
+
left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
|
|
6657
|
+
top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
|
|
6658
|
+
behavior: "instant",
|
|
6659
|
+
});
|
|
6138
6660
|
syncCommentAnchors();
|
|
6139
6661
|
const pin = findPlacedPin(comment);
|
|
6140
6662
|
if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
|
|
6663
|
+
else openDetachedThread(comment);
|
|
6664
|
+
}
|
|
6665
|
+
|
|
6666
|
+
function openDetachedThread(comment) {
|
|
6667
|
+
openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
|
|
6668
|
+
const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
|
|
6669
|
+
if (bubble) {
|
|
6670
|
+
bubble.dataset.detached = "";
|
|
6671
|
+
positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
function updateAnchorBadge(item, comment) {
|
|
6676
|
+
resolveCommentElement(comment);
|
|
6677
|
+
const result = commentTargets.get(String(comment.id));
|
|
6678
|
+
const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
|
|
6679
|
+
const status = legacy && !result?.element ? "uncertain" : result?.status;
|
|
6680
|
+
const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
|
|
6681
|
+
let badge = item.querySelector(".comty-anchor-badge");
|
|
6682
|
+
if (!label) { badge?.remove(); return; }
|
|
6683
|
+
if (!badge) {
|
|
6684
|
+
badge = document.createElement("span");
|
|
6685
|
+
badge.className = "comty-anchor-badge";
|
|
6686
|
+
item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
|
|
6687
|
+
}
|
|
6688
|
+
if (badge.textContent !== label) badge.textContent = label;
|
|
6689
|
+
badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
|
|
6690
|
+
status === "missing" ? "The target is not present on this page. The comment is still available." : "The target cannot be identified reliably. No pin is placed.";
|
|
6141
6691
|
}
|
|
6142
6692
|
|
|
6143
6693
|
let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
|
|
@@ -6150,6 +6700,7 @@ function initComty(options = {}) {
|
|
|
6150
6700
|
? sidepanelSearch.value.trim().toLowerCase()
|
|
6151
6701
|
: "";
|
|
6152
6702
|
let list = cachedComments.filter((comment) => {
|
|
6703
|
+
if (comment.local_agent || comment.kind === "agent") return false;
|
|
6153
6704
|
if (
|
|
6154
6705
|
search &&
|
|
6155
6706
|
!`${comment.content ?? ""} ${getCommentLabel(comment)}`
|
|
@@ -6196,6 +6747,7 @@ function initComty(options = {}) {
|
|
|
6196
6747
|
const item = document.createElement("button");
|
|
6197
6748
|
item.type = "button";
|
|
6198
6749
|
item.className = "comty-sidepanel-item";
|
|
6750
|
+
item.dataset.commentId = String(comment.id);
|
|
6199
6751
|
item.disabled = Boolean(comment.pending);
|
|
6200
6752
|
item.innerHTML = `
|
|
6201
6753
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
@@ -6219,8 +6771,13 @@ function initComty(options = {}) {
|
|
|
6219
6771
|
if (metaEl instanceof HTMLElement) {
|
|
6220
6772
|
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
6221
6773
|
metaEl.title = getCommentLabel(comment);
|
|
6774
|
+
if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
|
|
6775
|
+
metaEl.innerText += " · Original page unknown";
|
|
6776
|
+
}
|
|
6222
6777
|
}
|
|
6223
6778
|
|
|
6779
|
+
updateAnchorBadge(item, comment);
|
|
6780
|
+
|
|
6224
6781
|
item.onclick = (event) => {
|
|
6225
6782
|
event.stopPropagation();
|
|
6226
6783
|
openCommentFromPanel(comment);
|
|
@@ -6336,14 +6893,15 @@ function initComty(options = {}) {
|
|
|
6336
6893
|
createStatusFilters();
|
|
6337
6894
|
|
|
6338
6895
|
async function updateCommentStatus(comment, status) {
|
|
6339
|
-
if (!projectId || !comment?.id) return false;
|
|
6896
|
+
if (comment?.local_agent || !projectId || !comment?.id) return false;
|
|
6340
6897
|
|
|
6341
6898
|
let res;
|
|
6342
6899
|
try {
|
|
6343
|
-
|
|
6900
|
+
const local = comment.local_agent === true;
|
|
6901
|
+
res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
|
|
6344
6902
|
method: "POST",
|
|
6345
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
6346
|
-
body: JSON.stringify({
|
|
6903
|
+
headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
6904
|
+
body: JSON.stringify(local ? { status } : {
|
|
6347
6905
|
projectKey,
|
|
6348
6906
|
host: window.location.host,
|
|
6349
6907
|
status,
|
|
@@ -6360,7 +6918,7 @@ function initComty(options = {}) {
|
|
|
6360
6918
|
}
|
|
6361
6919
|
|
|
6362
6920
|
const data = await res.json();
|
|
6363
|
-
comment.status = getCommentStatus(data.status).value;
|
|
6921
|
+
comment.status = getCommentStatus(data.task?.status ?? data.status).value;
|
|
6364
6922
|
return comment.status;
|
|
6365
6923
|
}
|
|
6366
6924
|
|
|
@@ -6552,6 +7110,23 @@ function initComty(options = {}) {
|
|
|
6552
7110
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
6553
7111
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
6554
7112
|
showAuthPrompt,
|
|
7113
|
+
startThreadAgent: async (comment, replies, content) => {
|
|
7114
|
+
if (!agent) throw new Error('Start the local agent to reply with @Agent');
|
|
7115
|
+
if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
|
|
7116
|
+
const transcript = [
|
|
7117
|
+
`${getCommentAuthorName(comment)}: ${comment.content}`,
|
|
7118
|
+
...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
|
|
7119
|
+
].join('\n').slice(-24000);
|
|
7120
|
+
const response = await fetch(`${agentBridgeUrl}/tasks`, {
|
|
7121
|
+
method: 'POST', headers: agentHeaders(),
|
|
7122
|
+
body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
|
|
7123
|
+
selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
|
|
7124
|
+
domElement: comment.dom_element || '', viewport: comment.viewport || '',
|
|
7125
|
+
anchorContext: comment.anchor_context || '', scope: 'element',
|
|
7126
|
+
thread: { commentId: String(comment.id), transcript } }),
|
|
7127
|
+
});
|
|
7128
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
|
|
7129
|
+
},
|
|
6555
7130
|
get canWrite() { return canWrite; },
|
|
6556
7131
|
get canManage() { return canManage; },
|
|
6557
7132
|
get allowGuestComments() { return allowGuestComments; },
|
|
@@ -6752,42 +7327,112 @@ function initComty(options = {}) {
|
|
|
6752
7327
|
function reconcileComments(comments) {
|
|
6753
7328
|
const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
|
|
6754
7329
|
cachedComments = comments
|
|
6755
|
-
.filter(comment => comment.kind !== "agent"
|
|
7330
|
+
.filter(comment => comment.kind !== "agent" || comment.status !== "done")
|
|
6756
7331
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
6757
7332
|
for (const comment of previous.values()) {
|
|
6758
7333
|
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
6759
7334
|
}
|
|
6760
7335
|
}
|
|
6761
7336
|
|
|
7337
|
+
async function deliverThreadReply(task) {
|
|
7338
|
+
if (deliveringAgentReplies.has(task.id)) return;
|
|
7339
|
+
deliveringAgentReplies.add(task.id);
|
|
7340
|
+
try {
|
|
7341
|
+
const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
|
|
7342
|
+
method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
7343
|
+
body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
|
|
7344
|
+
});
|
|
7345
|
+
if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
|
|
7346
|
+
const { reply } = await response.json();
|
|
7347
|
+
const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
|
|
7348
|
+
if (local && !local.replies?.some(item => item.id === reply.id)) {
|
|
7349
|
+
local.replies ||= [];
|
|
7350
|
+
local.replies.push(reply);
|
|
7351
|
+
}
|
|
7352
|
+
for (const bubble of root.querySelectorAll('.comty-bubble')) {
|
|
7353
|
+
if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
|
|
7354
|
+
}
|
|
7355
|
+
const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
|
|
7356
|
+
if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
|
|
7357
|
+
localTasks = localTasks.filter(item => item.id !== task.id);
|
|
7358
|
+
} catch (error) {
|
|
7359
|
+
agentActionError = error.message;
|
|
7360
|
+
console.error('Comty: Agent reply delivery failed', error);
|
|
7361
|
+
} finally {
|
|
7362
|
+
deliveringAgentReplies.delete(task.id);
|
|
7363
|
+
agentPanelSignature = '';
|
|
7364
|
+
renderAgentPanel();
|
|
7365
|
+
}
|
|
7366
|
+
}
|
|
7367
|
+
|
|
6762
7368
|
async function loadComments({ force = false } = {}) {
|
|
6763
|
-
if (!projectId) return;
|
|
6764
|
-
const pagePath =
|
|
7369
|
+
if (!projectId && !agent) return;
|
|
7370
|
+
const pagePath = getPagePath();
|
|
6765
7371
|
if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
|
|
6766
7372
|
if (pendingCommentsLoad) {
|
|
6767
7373
|
await pendingCommentsLoad;
|
|
6768
|
-
if (!force) return;
|
|
7374
|
+
if (!force && cachedPagePath === pagePath) return;
|
|
6769
7375
|
}
|
|
6770
7376
|
pendingCommentsLoad = (async () => {
|
|
6771
|
-
let comments = cachedPagePath === pagePath ? cachedComments : [];
|
|
7377
|
+
let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
|
|
6772
7378
|
if (projectId) {
|
|
6773
7379
|
try {
|
|
6774
|
-
const
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
7380
|
+
const paths = [...new Set([pagePath, window.location.pathname])];
|
|
7381
|
+
const batches = await Promise.all(paths.map(async path => {
|
|
7382
|
+
const url = new URL(`${apiUrl}/api/comments`);
|
|
7383
|
+
url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
|
|
7384
|
+
const res = await fetch(url);
|
|
7385
|
+
if (!res.ok) throw new Error(await res.text());
|
|
7386
|
+
const data = await res.json();
|
|
7387
|
+
return Array.isArray(data.comments) ? data.comments : [];
|
|
7388
|
+
}));
|
|
7389
|
+
comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
|
|
6780
7390
|
} catch (error) { console.error("Comty: list comments failed", error); }
|
|
6781
7391
|
}
|
|
7392
|
+
if (agent) {
|
|
7393
|
+
try {
|
|
7394
|
+
const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
|
|
7395
|
+
if (!res.ok) throw new Error("Local companion unavailable");
|
|
7396
|
+
const data = await res.json();
|
|
7397
|
+
if (Array.isArray(data.tasks)) localTasks = data.tasks;
|
|
7398
|
+
const failedTasks = localTasks.filter(task => task.status === 'in_review');
|
|
7399
|
+
if (failedTasks.some(task => !seenFailedAgentTasks.has(task.id))) agentPanelOpen = true;
|
|
7400
|
+
seenFailedAgentTasks.clear();
|
|
7401
|
+
for (const task of failedTasks) seenFailedAgentTasks.add(task.id);
|
|
7402
|
+
if (Array.isArray(data.approvals)) {
|
|
7403
|
+
localApprovals = data.approvals;
|
|
7404
|
+
if (localApprovals.some(approval => !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
|
|
7405
|
+
seenAgentApprovals.clear();
|
|
7406
|
+
for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
|
|
7407
|
+
}
|
|
7408
|
+
agentConnectionError = "";
|
|
7409
|
+
} catch (error) {
|
|
7410
|
+
agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
|
|
7411
|
+
console.error("Comty: local tasks failed", error);
|
|
7412
|
+
}
|
|
7413
|
+
for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
|
|
7414
|
+
void deliverThreadReply(task);
|
|
7415
|
+
}
|
|
7416
|
+
comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && !["done", "cancel"].includes(task.status)));
|
|
7417
|
+
renderAgentPanel();
|
|
7418
|
+
}
|
|
7419
|
+
if (pagePath !== getPagePath()) return;
|
|
6782
7420
|
reconcileComments(comments);
|
|
6783
7421
|
cachedPagePath = pagePath;
|
|
6784
7422
|
renderSidepanelComments();
|
|
6785
7423
|
await renderComments(cachedComments);
|
|
7424
|
+
syncAgentWorkingOutlines();
|
|
6786
7425
|
ensureLiveTracking();
|
|
6787
7426
|
})();
|
|
6788
7427
|
try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
|
|
6789
7428
|
}
|
|
6790
7429
|
|
|
7430
|
+
if (agent) {
|
|
7431
|
+
setInterval(() => {
|
|
7432
|
+
if (!pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true });
|
|
7433
|
+
}, 2000);
|
|
7434
|
+
}
|
|
7435
|
+
|
|
6791
7436
|
function getPinPoint(pin) {
|
|
6792
7437
|
return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
|
|
6793
7438
|
}
|
|
@@ -6815,7 +7460,7 @@ function initComty(options = {}) {
|
|
|
6815
7460
|
}
|
|
6816
7461
|
|
|
6817
7462
|
function syncCommentAnchors() {
|
|
6818
|
-
if (!session.comments) return;
|
|
7463
|
+
if (!session.comments && !session.inspect) return;
|
|
6819
7464
|
const modal = getTopActiveModal();
|
|
6820
7465
|
for (const comment of cachedComments) {
|
|
6821
7466
|
const id = String(comment.id);
|
|
@@ -6826,7 +7471,7 @@ function initComty(options = {}) {
|
|
|
6826
7471
|
placePin(comment, modal);
|
|
6827
7472
|
pin = placedPins.get(id);
|
|
6828
7473
|
}
|
|
6829
|
-
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
|
|
7474
|
+
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
|
|
6830
7475
|
// A pin can remove its target's :hover style as the pointer enters it.
|
|
6831
7476
|
if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
|
|
6832
7477
|
(activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
|
|
@@ -6841,6 +7486,10 @@ function initComty(options = {}) {
|
|
|
6841
7486
|
if (pin.style.top !== y) pin.style.top = y;
|
|
6842
7487
|
for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
|
|
6843
7488
|
}
|
|
7489
|
+
if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
|
|
7490
|
+
const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
|
|
7491
|
+
if (comment) updateAnchorBadge(item, comment);
|
|
7492
|
+
}
|
|
6844
7493
|
if (draftAnchor) {
|
|
6845
7494
|
if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
|
|
6846
7495
|
const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
|
|
@@ -6854,16 +7503,50 @@ function initComty(options = {}) {
|
|
|
6854
7503
|
}
|
|
6855
7504
|
}
|
|
6856
7505
|
|
|
7506
|
+
function syncAgentWorkingOutlines() {
|
|
7507
|
+
const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
|
|
7508
|
+
const ids = new Set(working.map(task => String(task.id)));
|
|
7509
|
+
for (const [id, outline] of workingOutlines) {
|
|
7510
|
+
if (ids.has(id)) continue;
|
|
7511
|
+
outline.remove(); workingOutlines.delete(id);
|
|
7512
|
+
}
|
|
7513
|
+
const modal = getTopActiveModal();
|
|
7514
|
+
for (const task of working) {
|
|
7515
|
+
const id = String(task.id);
|
|
7516
|
+
let outline = workingOutlines.get(id);
|
|
7517
|
+
if (!outline) {
|
|
7518
|
+
outline = document.createElement("div");
|
|
7519
|
+
outline.className = "comty-agent-working";
|
|
7520
|
+
outline.setAttribute("aria-label", "Agent working on this element");
|
|
7521
|
+
const badge = document.createElement("span");
|
|
7522
|
+
badge.className = "comty-agent-working-badge";
|
|
7523
|
+
badge.append(createWorkingIndicator(), "Working");
|
|
7524
|
+
outline.appendChild(badge); root.appendChild(outline);
|
|
7525
|
+
workingOutlines.set(id, outline);
|
|
7526
|
+
}
|
|
7527
|
+
const target = resolveCommentElement(task);
|
|
7528
|
+
const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
|
|
7529
|
+
outline.hidden = !point;
|
|
7530
|
+
if (!point) continue;
|
|
7531
|
+
const rect = target.getBoundingClientRect();
|
|
7532
|
+
setMeasuredRect(outline, rect);
|
|
7533
|
+
outline.toggleAttribute("data-badge-below", rect.top < 32);
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7536
|
+
|
|
6857
7537
|
function onFrame() {
|
|
6858
7538
|
if (motion.enabled) updateMotion();
|
|
6859
|
-
if (session.comments) syncCommentAnchors();
|
|
7539
|
+
if (session.comments || session.inspect) syncCommentAnchors();
|
|
7540
|
+
if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
|
|
7541
|
+
syncAgentWorkingOutlines();
|
|
7542
|
+
}
|
|
6860
7543
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
6861
7544
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
6862
7545
|
if (session.inspect && rulersEnabled) measurements.render();
|
|
6863
7546
|
}
|
|
6864
7547
|
|
|
6865
7548
|
function ensureLiveTracking() {
|
|
6866
|
-
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
|
|
7549
|
+
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
|
|
6867
7550
|
if (!active || document.hidden) {
|
|
6868
7551
|
if (liveFrame) cancelAnimationFrame(liveFrame);
|
|
6869
7552
|
liveFrame = null;
|
|
@@ -6883,13 +7566,14 @@ function initComty(options = {}) {
|
|
|
6883
7566
|
syncOverlayHost();
|
|
6884
7567
|
|
|
6885
7568
|
function renderCachedComments() {
|
|
6886
|
-
if (!session.comments && !session.inspect && !session.edit) return;
|
|
7569
|
+
if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
|
|
6887
7570
|
if (resizeRenderFrame) {
|
|
6888
7571
|
window.cancelAnimationFrame(resizeRenderFrame);
|
|
6889
7572
|
}
|
|
6890
7573
|
resizeRenderFrame = window.requestAnimationFrame(() => {
|
|
6891
7574
|
resizeRenderFrame = null;
|
|
6892
7575
|
syncCommentAnchors();
|
|
7576
|
+
syncAgentWorkingOutlines();
|
|
6893
7577
|
renderInspector();
|
|
6894
7578
|
ensureLiveTracking();
|
|
6895
7579
|
});
|
|
@@ -6931,6 +7615,29 @@ function initComty(options = {}) {
|
|
|
6931
7615
|
characterData: true,
|
|
6932
7616
|
});
|
|
6933
7617
|
|
|
7618
|
+
let activePagePath = getPagePath();
|
|
7619
|
+
function handleRouteChange() {
|
|
7620
|
+
const next = getPagePath();
|
|
7621
|
+
if (next === activePagePath) return;
|
|
7622
|
+
activePagePath = next;
|
|
7623
|
+
closeBubbles();
|
|
7624
|
+
commentTargets.clear();
|
|
7625
|
+
reconcileComments([]);
|
|
7626
|
+
void renderComments([], { retry: false });
|
|
7627
|
+
renderSidepanelComments();
|
|
7628
|
+
void loadComments({ force: true });
|
|
7629
|
+
}
|
|
7630
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
7631
|
+
window.addEventListener("popstate", handleRouteChange);
|
|
7632
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
7633
|
+
const original = history[method];
|
|
7634
|
+
history[method] = function (...args) {
|
|
7635
|
+
const result = original.apply(this, args);
|
|
7636
|
+
handleRouteChange();
|
|
7637
|
+
return result;
|
|
7638
|
+
};
|
|
7639
|
+
}
|
|
7640
|
+
|
|
6934
7641
|
document.addEventListener("scroll", handleHostLayoutEvent, {
|
|
6935
7642
|
capture: true,
|
|
6936
7643
|
passive: true,
|
|
@@ -7068,13 +7775,14 @@ function initComty(options = {}) {
|
|
|
7068
7775
|
const y = e.clientY;
|
|
7069
7776
|
|
|
7070
7777
|
const anchorContext = getAnchorContext(el);
|
|
7778
|
+
if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
|
|
7071
7779
|
|
|
7072
7780
|
const payload = {
|
|
7073
7781
|
...getDeviceContext(),
|
|
7074
7782
|
projectId,
|
|
7075
|
-
kind: "comment",
|
|
7783
|
+
kind: session.agent ? "agent" : "comment",
|
|
7076
7784
|
host: window.location.host,
|
|
7077
|
-
pagePath:
|
|
7785
|
+
pagePath: getPagePath(),
|
|
7078
7786
|
selector: getSelector(el),
|
|
7079
7787
|
relX,
|
|
7080
7788
|
relY,
|
|
@@ -7172,6 +7880,7 @@ function initComty(options = {}) {
|
|
|
7172
7880
|
window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
|
|
7173
7881
|
|
|
7174
7882
|
async function boot() {
|
|
7883
|
+
if (localOnly) { await loadComments(); return; }
|
|
7175
7884
|
const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
|
|
7176
7885
|
method: "POST",
|
|
7177
7886
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
@@ -7184,6 +7893,7 @@ function initComty(options = {}) {
|
|
|
7184
7893
|
|
|
7185
7894
|
if (!res.ok) {
|
|
7186
7895
|
console.error("Comty: bootstrap failed", await res.text());
|
|
7896
|
+
if (agent) await loadComments();
|
|
7187
7897
|
return;
|
|
7188
7898
|
}
|
|
7189
7899
|
|
|
@@ -7228,16 +7938,30 @@ function initComty(options = {}) {
|
|
|
7228
7938
|
|
|
7229
7939
|
if (typeof options.onTestReady === "function") {
|
|
7230
7940
|
options.onTestReady({
|
|
7231
|
-
getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
7941
|
+
getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
7232
7942
|
getDistanceSegments, getInspectRows, syncCommentAnchors,
|
|
7233
7943
|
setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
|
|
7234
7944
|
startEditSession, startBrowseSession, endSession, showInspector,
|
|
7235
|
-
motion, setMotionEnabled, updateMotion,
|
|
7945
|
+
motion, setMotionEnabled, updateMotion, describeAgentError,
|
|
7946
|
+
previewAgent(tasks, approvals = [], error = "") {
|
|
7947
|
+
localTasks = tasks;
|
|
7948
|
+
localApprovals = approvals;
|
|
7949
|
+
agentConnectionError = error;
|
|
7950
|
+
startAgentSession();
|
|
7951
|
+
agentPanelOpen = true;
|
|
7952
|
+
agentPanelSignature = "";
|
|
7953
|
+
renderAgentPanel();
|
|
7954
|
+
},
|
|
7236
7955
|
seed(comments) {
|
|
7237
7956
|
reconcileComments(comments);
|
|
7238
7957
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
7239
7958
|
return renderComments(cachedComments, { retry: false });
|
|
7240
7959
|
},
|
|
7960
|
+
seedAgentTasks(tasks) {
|
|
7961
|
+
localTasks = tasks;
|
|
7962
|
+
syncAgentWorkingOutlines();
|
|
7963
|
+
ensureLiveTracking();
|
|
7964
|
+
},
|
|
7241
7965
|
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
7242
7966
|
invalidate() { anchorRevision += 1; },
|
|
7243
7967
|
});
|
|
@@ -7245,6 +7969,7 @@ function initComty(options = {}) {
|
|
|
7245
7969
|
|
|
7246
7970
|
return boot().catch(error => {
|
|
7247
7971
|
console.error("Comty: bootstrap failed", error);
|
|
7972
|
+
if (agent) return loadComments();
|
|
7248
7973
|
});
|
|
7249
7974
|
}
|
|
7250
7975
|
}
|