@comty.io/core 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/comty-sdk.js +1350 -140
- package/package.json +17 -6
- package/src/acp.mjs +68 -0
- package/src/agent-process.mjs +89 -0
- package/src/anchors.js +58 -17
- package/src/browser.d.ts +3 -1
- package/src/browser.js +820 -88
- package/src/capture.js +10 -8
- package/src/claude-permission.mjs +37 -0
- package/src/claude.mjs +119 -0
- package/src/codex.mjs +64 -0
- package/src/comments.js +31 -11
- package/src/companion.mjs +201 -0
- package/src/cursor.mjs +49 -0
- package/src/dev.mjs +172 -0
- package/src/gemini.mjs +3 -0
- package/src/grok.mjs +3 -0
- package/src/inspect-markup.js +283 -0
- package/src/measurements.js +3 -3
- package/src/style.js +145 -13
- package/src/vite.d.ts +5 -0
- package/src/vite.mjs +69 -0
package/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;
|
|
@@ -50,6 +50,10 @@ function getComtyStyle() {
|
|
|
50
50
|
--comty-toolbar-selected: #505457;
|
|
51
51
|
--comty-toolbar-muted: ${toolbarMuted};
|
|
52
52
|
--comty-measure: #ff3b30;
|
|
53
|
+
--comty-markup-yellow: #facc15;
|
|
54
|
+
--comty-markup-green: #22c55e;
|
|
55
|
+
--comty-color-black: #000;
|
|
56
|
+
--comty-color-spectrum: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
|
|
53
57
|
--comty-font: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
54
58
|
--comty-dialog-surface: var(--comty-surface);
|
|
55
59
|
--comty-dialog-ink: var(--comty-foreground);
|
|
@@ -60,6 +64,8 @@ function getComtyStyle() {
|
|
|
60
64
|
--comty-dialog-width: 32rem;
|
|
61
65
|
--comty-dialog-shadow: 0 1.5rem 4rem rgb(0 0 0 / .16);
|
|
62
66
|
--comty-motion-duration: 220ms;
|
|
67
|
+
--comty-working-dot-size: .25em;
|
|
68
|
+
--comty-working-dot-gap: .125em;
|
|
63
69
|
--comty-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
64
70
|
--comty-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
65
71
|
position: fixed;
|
|
@@ -71,13 +77,80 @@ function getComtyStyle() {
|
|
|
71
77
|
font-family: var(--comty-font);
|
|
72
78
|
color-scheme: dark;
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
#comty-root :is(button, .comty-comment-avatar) { user-select: none; -webkit-user-select: none; }
|
|
81
|
+
#comty-root .comty-comment-avatar img { -webkit-user-drag: none; }
|
|
82
|
+
#comty-root .comty-pin .comty-comment-avatar { pointer-events: none; }
|
|
83
|
+
#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; }
|
|
84
|
+
#comty-root > .comty-agent-panel[data-side="left"] { left: 1rem; right: auto; }
|
|
85
|
+
#comty-root > .comty-agent-panel[data-closed] { transform: translateY(.5rem); opacity: 0; pointer-events: none; }
|
|
86
|
+
#comty-root > .comty-agent-panel[hidden] { display: none; }
|
|
87
|
+
.comty-agent-panel *, .comty-agent-panel *::before { box-sizing: border-box; }
|
|
88
|
+
.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); }
|
|
89
|
+
.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); }
|
|
90
|
+
.comty-agent-brand.comty-agent-avatar { width: 2.375rem; height: 2.375rem; border-radius: 0; background: transparent; }
|
|
91
|
+
.comty-agent-panel svg { display: block; width: 1rem; height: 1rem; flex-shrink: 0; }
|
|
92
|
+
.comty-agent-brand svg { width: 1.25rem; height: 1.25rem; }
|
|
93
|
+
.comty-agent-brand.comty-agent-avatar svg { width: 100%; height: 100%; }
|
|
94
|
+
.comty-agent-answer { display: flex; align-items: flex-start; gap: .5rem; padding: .375rem .5rem .875rem; }
|
|
95
|
+
.comty-agent-answer .comty-agent-brand { width: 1.5rem; height: 1.5rem; }
|
|
96
|
+
.comty-agent-answer p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--comty-foreground); }
|
|
97
|
+
.comty-agent-heading { flex: 1; min-width: 0; }
|
|
98
|
+
.comty-agent-heading strong { display: block; font-weight: 600; font-size: .875rem; }
|
|
99
|
+
.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; }
|
|
100
|
+
.comty-agent-item { padding: .375rem .25rem; overflow-wrap: anywhere; }
|
|
101
|
+
.comty-agent-item + .comty-agent-item { border-top: .0625rem solid var(--comty-hover-surface); }
|
|
102
|
+
.comty-agent-item h3 { margin: 0 0 .25rem; color: var(--comty-foreground); font: 500 .875rem/1.4 ui-sans-serif, system-ui, sans-serif; }
|
|
103
|
+
.comty-agent-list { display: grid; gap: .125rem; }
|
|
104
|
+
.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); }
|
|
105
|
+
.comty-agent-row:hover { background: var(--comty-hover-surface); }
|
|
106
|
+
.comty-agent-dot { width: .4375rem; height: .4375rem; flex: 0 0 auto; border-radius: 50%; background: var(--comty-muted); }
|
|
107
|
+
.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); }
|
|
108
|
+
.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; }
|
|
109
|
+
.comty-working-indicator > i[data-active] { opacity: 1; }
|
|
110
|
+
.comty-agent-dot[data-status="success"] { background: var(--comty-agent-success); }
|
|
111
|
+
.comty-agent-dot[data-status="error"] { background: var(--comty-agent-error); }
|
|
112
|
+
.comty-agent-row-name { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
113
|
+
.comty-agent-row-state { flex: 0 0 auto; color: var(--comty-muted); font-size: .875rem; }
|
|
114
|
+
.comty-agent-row-actions { display: flex; flex-shrink: 0; gap: .25rem; }
|
|
115
|
+
.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; }
|
|
116
|
+
.comty-agent-panel .comty-agent-row-actions .comty-agent-action:hover { color: var(--comty-foreground); background: var(--comty-surface); }
|
|
117
|
+
#comty-root > .comty-agent-task-toast { position: fixed; right: 1rem; bottom: 1rem; z-index: 2147483647; width: min(20rem, calc(100vw - 2rem)); box-sizing: border-box; padding: .75rem 2rem .75rem .875rem; border: .0625rem solid var(--comty-border); border-radius: .75rem; background: var(--comty-surface); box-shadow: var(--comty-floating-shadow); color: var(--comty-foreground); font: .75rem/1.5 ui-sans-serif, system-ui, sans-serif; pointer-events: auto; opacity: 1; transform: translateY(0); transition: opacity 200ms ease, transform 200ms var(--comty-ease-out); animation: comty-agent-toast-in 200ms var(--comty-ease-out); color-scheme: dark; }
|
|
118
|
+
#comty-root > .comty-agent-task-toast[data-toolbar] { right: auto; bottom: auto; }
|
|
119
|
+
#comty-root > .comty-agent-task-toast[data-leaving] { opacity: 0; transform: translateY(.25rem); pointer-events: none; }
|
|
120
|
+
.comty-agent-task-toast strong { display: block; font-size: .8125rem; font-weight: 600; }
|
|
121
|
+
.comty-agent-task-toast p { margin: .25rem 0 0; color: var(--comty-muted); overflow-wrap: anywhere; }
|
|
122
|
+
.comty-agent-task-toast button { position: absolute; top: .5rem; right: .375rem; display: flex; align-items: center; justify-content: center; width: 1.5rem; height: 1.5rem; padding: 0; border: 0; border-radius: .25rem; background: transparent; color: var(--comty-muted); cursor: pointer; }
|
|
123
|
+
.comty-agent-task-toast button:hover { background: var(--comty-hover-surface); color: var(--comty-foreground); }
|
|
124
|
+
.comty-agent-task-toast button:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
|
|
125
|
+
.comty-agent-task-toast svg { width: .875rem; height: .875rem; }
|
|
126
|
+
@keyframes comty-agent-toast-in { from { opacity: 0; transform: translateY(.25rem); } to { opacity: 1; transform: translateY(0); } }
|
|
127
|
+
.comty-agent-panel p { margin: .5rem 0 0; font: inherit; color: var(--comty-muted); }
|
|
128
|
+
.comty-agent-error { margin-top: .75rem; }
|
|
129
|
+
.comty-agent-error strong { display: block; font-weight: 500; color: var(--comty-foreground); }
|
|
130
|
+
.comty-agent-error p { margin-top: .25rem; }
|
|
131
|
+
.comty-agent-banner { margin-bottom: 1rem; }
|
|
132
|
+
.comty-agent-banner .comty-agent-error { margin-top: 0; }
|
|
133
|
+
.comty-agent-task-actions { display: flex; justify-content: flex-end; gap: .375rem; padding: .375rem .625rem; border-top: .0625rem solid var(--comty-border); }
|
|
134
|
+
.comty-agent-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .875rem; }
|
|
135
|
+
:is(.comty-agent-panel, .comty-agent-task-thread) .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; }
|
|
136
|
+
.comty-agent-task-thread .comty-agent-action { min-height: 1.5rem; padding: .125rem .5rem; border-radius: .375rem; font-size: .75rem; }
|
|
137
|
+
:is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action:hover { background: color-mix(in srgb, var(--comty-foreground) 12%, var(--comty-hover-surface)); }
|
|
138
|
+
:is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action[data-primary] { color: var(--comty-paper); background: var(--comty-accent); }
|
|
139
|
+
:is(.comty-agent-panel, .comty-agent-task-thread) .comty-agent-action:disabled { opacity: .5; cursor: wait; }
|
|
140
|
+
.comty-agent-panel .comty-agent-close { width: 1.5rem; min-height: 1.5rem; padding: 0; background: transparent; color: var(--comty-muted); }
|
|
141
|
+
:is(.comty-agent-panel, .comty-agent-task-thread) :is(button, summary):focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .1875rem; }
|
|
142
|
+
.comty-agent-details { margin-top: .625rem; }
|
|
143
|
+
.comty-agent-panel summary { cursor: pointer; color: var(--comty-muted); font-size: .875rem; }
|
|
144
|
+
.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; }
|
|
145
|
+
.comty-agent-working { position: fixed; z-index: 1; box-sizing: border-box; border: 2px solid var(--comty-accent); border-radius: 0; pointer-events: none; }
|
|
146
|
+
.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; }
|
|
147
|
+
.comty-agent-working[data-badge-below] .comty-agent-working-badge { bottom: auto; top: 100%; border-radius: 0 0 .375rem .375rem; }
|
|
75
148
|
#comty-root { cursor: default; }
|
|
76
149
|
#comty-root::backdrop { background: transparent; pointer-events: none; }
|
|
77
150
|
html.comty-comment-placement,
|
|
78
151
|
html.comty-comment-placement body,
|
|
79
152
|
html.comty-comment-placement body *:not(#comty-root):not(#comty-root *) {
|
|
80
|
-
cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}")
|
|
153
|
+
cursor: url("data:image/svg+xml,${encodeURIComponent(commentCursor)}") 16 16, crosshair !important;
|
|
81
154
|
}
|
|
82
155
|
#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
156
|
.comty-toolbar {
|
|
@@ -144,7 +217,7 @@ function getComtyStyle() {
|
|
|
144
217
|
#comty-root [hidden] { display: none !important; }
|
|
145
218
|
[data-comty-anchor-hidden] { visibility: hidden !important; pointer-events: none !important; }
|
|
146
219
|
@media (prefers-reduced-motion: reduce) {
|
|
147
|
-
.comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble { transition: none !important; animation: none !important; }
|
|
220
|
+
.comty-toolbar, .comty-control-button, .comty-sidepanel, .comty-pin, .comty-bubble, .comty-agent-panel, .comty-agent-action, .comty-agent-dot, .comty-working-indicator > i, #comty-root > .comty-agent-task-toast { transition: none !important; animation: none !important; }
|
|
148
221
|
}
|
|
149
222
|
|
|
150
223
|
.comty-sidepanel {
|
|
@@ -326,6 +399,16 @@ function getComtyStyle() {
|
|
|
326
399
|
white-space: nowrap;
|
|
327
400
|
}
|
|
328
401
|
|
|
402
|
+
.comty-anchor-badge {
|
|
403
|
+
display: inline-block;
|
|
404
|
+
margin-top: .375rem;
|
|
405
|
+
padding: .125rem .375rem;
|
|
406
|
+
border: 1px solid var(--comty-border);
|
|
407
|
+
border-radius: 999rem;
|
|
408
|
+
color: var(--comty-muted);
|
|
409
|
+
font: .6875rem/1.4 sans-serif;
|
|
410
|
+
}
|
|
411
|
+
|
|
329
412
|
.comty-pin {
|
|
330
413
|
position: fixed;
|
|
331
414
|
z-index: 2147483645;
|
|
@@ -607,6 +690,8 @@ function getComtyStyle() {
|
|
|
607
690
|
height: 100%;
|
|
608
691
|
object-fit: cover;
|
|
609
692
|
}
|
|
693
|
+
.comty-comment-avatar.comty-agent-avatar { overflow: visible; border-radius: 0; background: transparent; color: var(--comty-paper); }
|
|
694
|
+
.comty-comment-avatar.comty-agent-avatar svg { display: block; width: 100%; height: 100%; }
|
|
610
695
|
|
|
611
696
|
.comty-pin-avatar {
|
|
612
697
|
width: 24px;
|
|
@@ -721,12 +806,12 @@ function getComtyStyle() {
|
|
|
721
806
|
}
|
|
722
807
|
|
|
723
808
|
.comty-popover-item.comty-popover-item-destructive {
|
|
724
|
-
color: var(--comty-error);
|
|
809
|
+
color: var(--comty-agent-error);
|
|
725
810
|
}
|
|
726
811
|
|
|
727
812
|
.comty-popover-item.comty-popover-item-destructive:hover,
|
|
728
813
|
.comty-popover-item.comty-popover-item-destructive:focus-visible {
|
|
729
|
-
background: color-mix(in srgb, var(--comty-error) 12%, var(--comty-surface));
|
|
814
|
+
background: color-mix(in srgb, var(--comty-agent-error) 12%, var(--comty-surface));
|
|
730
815
|
outline: none;
|
|
731
816
|
}
|
|
732
817
|
|
|
@@ -855,6 +940,8 @@ function getComtyStyle() {
|
|
|
855
940
|
}
|
|
856
941
|
|
|
857
942
|
.comty-hover-bubble .comty-thread {
|
|
943
|
+
display: grid;
|
|
944
|
+
gap: 1rem;
|
|
858
945
|
padding: 12px;
|
|
859
946
|
}
|
|
860
947
|
|
|
@@ -1097,19 +1184,23 @@ function getComtyStyle() {
|
|
|
1097
1184
|
margin: 4px 0;
|
|
1098
1185
|
}
|
|
1099
1186
|
|
|
1100
|
-
html.comty-comments-active .comty-pin {
|
|
1187
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin {
|
|
1101
1188
|
display: grid;
|
|
1102
1189
|
}
|
|
1103
1190
|
|
|
1104
|
-
html.comty-comments-active .comty-pin.comty-filtered-out {
|
|
1191
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-pin.comty-filtered-out {
|
|
1105
1192
|
display: none;
|
|
1106
1193
|
}
|
|
1107
1194
|
|
|
1108
|
-
html.comty-comments-active .comty-bubble {
|
|
1195
|
+
html:is(.comty-comments-active, .comty-inspect-annotations) .comty-bubble {
|
|
1109
1196
|
display: block;
|
|
1110
1197
|
}
|
|
1111
1198
|
|
|
1112
1199
|
.comty-inspection-layer { position: relative; z-index: 2147483647; pointer-events: none; }
|
|
1200
|
+
.comty-markup-layer, .comty-markup-surface { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; }
|
|
1201
|
+
.comty-markup-surface { pointer-events: auto; cursor: crosshair; touch-action: none; user-select: none; }
|
|
1202
|
+
.comty-markup-surface[aria-busy="true"] { cursor: progress; }
|
|
1203
|
+
.comty-capture-selection { position: absolute; box-sizing: border-box; border: .0625rem solid var(--comty-accent); box-shadow: 0 0 0 100vmax var(--comty-dialog-backdrop); color: var(--comty-paper); text-shadow: 0 .0625rem .125rem var(--comty-ink); font: .6875rem/1.5 var(--comty-font); padding: .25rem; white-space: nowrap; }
|
|
1113
1204
|
.comty-overlay-tools, .comty-layout-guides, .comty-layout-guide { position: fixed; inset: 0; pointer-events: none; }
|
|
1114
1205
|
.comty-overlay-tools { overflow: hidden; }
|
|
1115
1206
|
.comty-layout-guide { display: grid; box-sizing: border-box; }
|
|
@@ -1135,6 +1226,38 @@ function getComtyStyle() {
|
|
|
1135
1226
|
.comty-guide-fields label { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
|
|
1136
1227
|
.comty-tool-panel input, .comty-tool-panel select { min-width: 0; width: 6rem; box-sizing: border-box; border: 0; border-radius: .25rem; padding: .25rem; background: var(--comty-surface); color: var(--comty-foreground); font: inherit; }
|
|
1137
1228
|
.comty-tool-panel input[type="color"] { height: 1.75rem; padding: .125rem; }
|
|
1229
|
+
.comty-tool-panel.comty-markup-panel { width: max-content; padding: .375rem; border-radius: .875rem; background: var(--comty-toolbar-group); }
|
|
1230
|
+
.comty-markup-controls, .comty-markup-options, .comty-markup-strokes, .comty-markup-colors { display: flex; align-items: center; gap: .125rem; }
|
|
1231
|
+
.comty-markup-controls { justify-content: space-between; }
|
|
1232
|
+
.comty-markup-options { margin-top: .375rem; padding-top: .375rem; border-top: .0625rem solid var(--comty-border); }
|
|
1233
|
+
.comty-markup-divider { flex: 0 0 .0625rem; height: 1rem; margin-inline: .25rem; background: var(--comty-border); }
|
|
1234
|
+
.comty-markup-panel button { flex: 0 0 auto; width: 2rem; height: 2rem; padding: 0; color: var(--comty-toolbar-muted); border-radius: .5rem; }
|
|
1235
|
+
.comty-markup-panel button svg { display: block; flex: 0 0 auto; width: 1rem; height: 1rem; }
|
|
1236
|
+
.comty-markup-panel button[aria-pressed="false"] { opacity: 1; }
|
|
1237
|
+
.comty-markup-panel button[aria-pressed="true"] { color: var(--comty-foreground); background: var(--comty-hover-surface); }
|
|
1238
|
+
.comty-markup-panel button:disabled { opacity: .35; cursor: default; }
|
|
1239
|
+
.comty-markup-panel button:disabled:hover { background: transparent; }
|
|
1240
|
+
.comty-markup-panel button:not(:disabled):active { transform: scale(.97); }
|
|
1241
|
+
.comty-markup-strokes button { width: 1.5rem; height: 1.5rem; }
|
|
1242
|
+
.comty-markup-colors button { width: 1.375rem; height: 1.5rem; background: transparent; }
|
|
1243
|
+
.comty-markup-colors button[aria-pressed="true"] { background: transparent; }
|
|
1244
|
+
.comty-markup-swatch { display: block; width: .875rem; height: .875rem; border-radius: 50%; background: var(--markup-swatch); box-shadow: inset 0 0 0 .0625rem color-mix(in srgb, var(--comty-paper) 15%, transparent); }
|
|
1245
|
+
.comty-markup-colors button[aria-pressed="true"] .comty-markup-swatch { outline: .0625rem solid var(--comty-foreground); outline-offset: .125rem; }
|
|
1246
|
+
.comty-markup-panel button.comty-markup-picker { width: 1rem; padding: 0; position: relative; flex: 0 0 1rem; height: 1rem; margin-inline: .25rem; border-radius: 50%; background: conic-gradient(var(--comty-measure), var(--comty-markup-yellow), var(--comty-markup-green), var(--comty-accent), var(--comty-error), var(--comty-measure)); }
|
|
1247
|
+
.comty-markup-picker[data-selected] { outline: .0625rem solid var(--comty-foreground); outline-offset: .125rem; }
|
|
1248
|
+
.comty-markup-picker:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
|
|
1249
|
+
.comty-markup-color { display: grid; gap: .625rem; margin-top: .375rem; padding: .625rem .25rem .25rem; border-top: .0625rem solid var(--comty-border); }
|
|
1250
|
+
.comty-markup-color-field { position: relative; height: 8rem; border-radius: .5rem; background: linear-gradient(to top, var(--comty-color-black), transparent), linear-gradient(to right, var(--comty-paper), transparent), var(--markup-hue); touch-action: none; cursor: crosshair; }
|
|
1251
|
+
.comty-markup-color-field:focus-visible { outline: .125rem solid var(--comty-accent); outline-offset: .125rem; }
|
|
1252
|
+
.comty-markup-color-field > span { position: absolute; width: .625rem; height: .625rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-color); box-shadow: 0 0 0 .0625rem var(--comty-ink); transform: translate(-50%, -50%); pointer-events: none; }
|
|
1253
|
+
.comty-markup-color input.comty-markup-hue { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 1rem; margin: 0; padding: 0; border-radius: 1rem; background: var(--comty-color-spectrum); cursor: pointer; }
|
|
1254
|
+
.comty-markup-hue::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: .875rem; height: .875rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-hue); box-shadow: 0 0 0 .0625rem var(--comty-ink); }
|
|
1255
|
+
.comty-markup-hue::-moz-range-thumb { width: .875rem; height: .875rem; box-sizing: border-box; border: .125rem solid var(--comty-paper); border-radius: 50%; background: var(--markup-hue); box-shadow: 0 0 0 .0625rem var(--comty-ink); }
|
|
1256
|
+
.comty-markup-hex { display: flex; align-items: center; gap: .5rem; padding: .25rem .5rem; border-radius: .5rem; background: var(--comty-surface); color: var(--comty-muted); font: .6875rem/1.5 var(--comty-font); }
|
|
1257
|
+
.comty-markup-color-preview { flex: 0 0 1rem; height: 1rem; border-radius: .25rem; background: var(--markup-color); box-shadow: inset 0 0 0 .0625rem var(--comty-border); }
|
|
1258
|
+
.comty-markup-hex input { flex: 1; width: 0; text-align: right; font-family: ui-monospace, monospace; }
|
|
1259
|
+
.comty-markup-hex input[aria-invalid="true"] { outline: .0625rem solid var(--comty-error); }
|
|
1260
|
+
|
|
1138
1261
|
.comty-ruler { position: fixed; background: var(--comty-surface); pointer-events: auto; touch-action: none; }
|
|
1139
1262
|
.comty-ruler-horizontal { left: 0; top: 0; width: 100vw; height: 1.125rem; border-bottom: .0625rem solid var(--comty-border); cursor: ns-resize; }
|
|
1140
1263
|
.comty-ruler-vertical { left: 0; top: 0; height: 100vh; width: 1.125rem; border-right: .0625rem solid var(--comty-border); cursor: ew-resize; }
|
|
@@ -1209,16 +1332,16 @@ function getComtyStyle() {
|
|
|
1209
1332
|
.comty-motion-panel button[aria-pressed="true"] { opacity: 1; color: var(--comty-paper); background: var(--comty-hover-surface); }
|
|
1210
1333
|
.comty-motion-panel button[aria-pressed="false"] { opacity: 1; }
|
|
1211
1334
|
.comty-motion-panel [data-motion-status] { margin: .5rem; font-size: .75rem; color: var(--comty-muted); }
|
|
1212
|
-
@media (max-width:
|
|
1335
|
+
@media (max-width: 38rem) { #comty-root { --comty-tool-size: 2rem; --comty-toolbar-padding: .25rem; } .comty-toolbar { right: .5rem; bottom: .5rem; max-width: calc(100vw - 1rem); flex-wrap: wrap; justify-content: center; row-gap: .25rem; } .comty-tool-divider { display: none; } }
|
|
1213
1336
|
|
|
1214
1337
|
.comty-inspection-box {
|
|
1215
1338
|
position: fixed;
|
|
1216
1339
|
box-sizing: border-box;
|
|
1217
1340
|
border: .0625rem solid var(--comty-outline);
|
|
1218
|
-
background:
|
|
1341
|
+
background: transparent;
|
|
1342
|
+
border-radius: 0;
|
|
1219
1343
|
pointer-events: none;
|
|
1220
1344
|
}
|
|
1221
|
-
.comty-inspection-box[data-selected] { background: color-mix(in oklch, var(--comty-outline) 5%, transparent); }
|
|
1222
1345
|
.comty-inspect-spacing { position: fixed; pointer-events: none; background: var(--comty-spacing-color); }
|
|
1223
1346
|
.comty-inspect-margin { --comty-spacing-color: var(--comty-margin-fill); }
|
|
1224
1347
|
.comty-inspect-padding { --comty-spacing-color: var(--comty-padding-fill); }
|
|
@@ -1272,6 +1395,15 @@ function getComtyStyle() {
|
|
|
1272
1395
|
height: 5.75rem;
|
|
1273
1396
|
border-radius: .75rem;
|
|
1274
1397
|
}
|
|
1398
|
+
.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; }
|
|
1399
|
+
.comty-create-expanded .comty-agent-scope { opacity: 1; visibility: visible; }
|
|
1400
|
+
.comty-agent-scope input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
|
|
1401
|
+
.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; }
|
|
1402
|
+
.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; }
|
|
1403
|
+
.comty-agent-scope input:checked + .comty-agent-scope-track { background: var(--comty-accent); }
|
|
1404
|
+
.comty-agent-scope input:checked + .comty-agent-scope-track::after { transform: translateX(.625rem); }
|
|
1405
|
+
.comty-agent-scope input:focus-visible + .comty-agent-scope-track { outline: .125rem solid var(--comty-comment-focus); outline-offset: .125rem; }
|
|
1406
|
+
.comty-agent-thread-error { grid-column: 2; margin: .25rem 0 0; color: var(--comty-agent-error); font: .6875rem/1.3 var(--comty-font); }
|
|
1275
1407
|
|
|
1276
1408
|
.comty-create-bubble.comty-create-has-attachment { height: 7.5rem; }
|
|
1277
1409
|
.comty-create-bubble.comty-bubble-left { transform-origin: top right; }
|
|
@@ -1502,6 +1634,14 @@ function createAnchorMatcher({
|
|
|
1502
1634
|
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1503
1635
|
}
|
|
1504
1636
|
|
|
1637
|
+
function similarText(a, b) {
|
|
1638
|
+
if (!a || !b) return false;
|
|
1639
|
+
const words = value => new Set(value.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1640
|
+
const left = words(a), right = words(b);
|
|
1641
|
+
const common = [...left].filter(word => right.has(word)).length;
|
|
1642
|
+
return common >= 2 && common / Math.max(left.size, right.size) >= 0.7;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1505
1645
|
function scoreFingerprint(el, fingerprint) {
|
|
1506
1646
|
if (!el || !fingerprint) return 0;
|
|
1507
1647
|
if (
|
|
@@ -1571,6 +1711,8 @@ function createAnchorMatcher({
|
|
|
1571
1711
|
score += 15;
|
|
1572
1712
|
}
|
|
1573
1713
|
|
|
1714
|
+
if (text !== fingerprint.text && similarText(text, fingerprint.text)) score += 24;
|
|
1715
|
+
|
|
1574
1716
|
if (typeof fingerprint.siblingIndex === "number" && el.parentElement) {
|
|
1575
1717
|
const siblings = [...el.parentElement.children].filter(
|
|
1576
1718
|
(child) => child.tagName === el.tagName,
|
|
@@ -1648,6 +1790,8 @@ function createAnchorMatcher({
|
|
|
1648
1790
|
if (ancestorScore > 0) {
|
|
1649
1791
|
score += Math.min(ancestorScore, 40) * (index === 0 ? 0.5 : 0.2);
|
|
1650
1792
|
}
|
|
1793
|
+
// Distinguish repeated cards by their section, even when dimensions match.
|
|
1794
|
+
if (index < 3 && ancestors[index].text && getElementTextSignature(parent) === ancestors[index].text) score += 18;
|
|
1651
1795
|
parent = parent.parentElement;
|
|
1652
1796
|
}
|
|
1653
1797
|
|
|
@@ -1662,46 +1806,75 @@ function createAnchorMatcher({
|
|
|
1662
1806
|
return score;
|
|
1663
1807
|
}
|
|
1664
1808
|
|
|
1665
|
-
function matchesAnchorIdentity(el, fingerprint) {
|
|
1809
|
+
function matchesAnchorIdentity(el, fingerprint, ignoreDescendantText = false, allowTextChanges = true) {
|
|
1666
1810
|
if (!el || !fingerprint || el.tagName.toLowerCase() !== fingerprint.tag) return false;
|
|
1667
1811
|
const attributes = { ariaLabel: "aria-label", name: "name", href: "href", src: "src", alt: "alt", title: "title", role: "role" };
|
|
1668
1812
|
for (const [key, attribute] of Object.entries(attributes)) {
|
|
1669
1813
|
if (fingerprint[key] && getAttributeSignature(el, attribute) !== fingerprint[key]) return false;
|
|
1670
1814
|
}
|
|
1671
|
-
if (fingerprint.
|
|
1815
|
+
if (fingerprint.id && el.id !== fingerprint.id) return false;
|
|
1816
|
+
if (fingerprint.testId && !["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId)) return false;
|
|
1817
|
+
const stableIdentity = (fingerprint.id && el.id === fingerprint.id) ||
|
|
1818
|
+
(fingerprint.testId && ["data-testid", "data-test", "data-cy"].some(name => getAttributeSignature(el, name) === fingerprint.testId));
|
|
1819
|
+
if (fingerprint.ownText && getElementOwnTextSignature(el) !== fingerprint.ownText && !(allowTextChanges && stableIdentity)) return false;
|
|
1672
1820
|
// Container text is useful for matching remounted drawers and dialogs.
|
|
1673
|
-
if (!fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text)
|
|
1821
|
+
if (!ignoreDescendantText && !fingerprint.ownText && fingerprint.text && getElementTextSignature(el) !== fingerprint.text && !(allowTextChanges && stableIdentity)) {
|
|
1822
|
+
if (!allowTextChanges || !el.children.length || !similarText(getElementTextSignature(el), fingerprint.text)) return false;
|
|
1823
|
+
}
|
|
1674
1824
|
return true;
|
|
1675
1825
|
}
|
|
1676
1826
|
|
|
1677
1827
|
function matchesAnchorContext(el, context) {
|
|
1678
1828
|
if (!context) return true;
|
|
1679
|
-
|
|
1829
|
+
// A unique page landmark keeps its identity when a child is edited.
|
|
1830
|
+
const pageContainer = !context.layer && (context.target?.tag === "main" || context.target?.role === "main") &&
|
|
1831
|
+
[...document.querySelectorAll('main, [role="main"]')].filter(node => !isComtyNode(node)).length === 1;
|
|
1832
|
+
if (!matchesAnchorIdentity(el, context.target, pageContainer)) return false;
|
|
1680
1833
|
const parentFingerprint = context.ancestors?.[0];
|
|
1681
1834
|
if (parentFingerprint?.text && parentFingerprint.text !== context.target.text &&
|
|
1682
|
-
!matchesAnchorIdentity(el.parentElement, parentFingerprint)) return false;
|
|
1835
|
+
!matchesAnchorIdentity(el.parentElement, parentFingerprint, pageContainer)) return false;
|
|
1683
1836
|
const layer = getPresentationLayer(el);
|
|
1684
|
-
if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer))) return false;
|
|
1837
|
+
if (context.layer && (!layer || !matchesAnchorIdentity(layer, context.layer, false, false))) return false;
|
|
1685
1838
|
if (context.version >= 2 && !context.layer && layer) return false;
|
|
1686
1839
|
return scoreAnchorCandidate(el, context) >= 50;
|
|
1687
1840
|
}
|
|
1688
1841
|
|
|
1689
|
-
function
|
|
1690
|
-
if (!context?.target) return
|
|
1842
|
+
function resolveAnchor(context, exact = null) {
|
|
1843
|
+
if (!context?.target) return { element: exact, status: exact ? "matched" : "missing" };
|
|
1691
1844
|
let candidates;
|
|
1692
1845
|
try {
|
|
1693
|
-
candidates = [...document.body.querySelectorAll(context.target.tag || "*")];
|
|
1846
|
+
candidates = [...document.body.querySelectorAll(context.target.tag || "*")].filter(el => !isComtyNode(el));
|
|
1694
1847
|
} catch {
|
|
1695
|
-
return null;
|
|
1848
|
+
return { element: null, status: "missing" };
|
|
1696
1849
|
}
|
|
1697
1850
|
const matches = candidates
|
|
1698
|
-
.filter(el =>
|
|
1699
|
-
.map(el => ({ el, score: scoreAnchorCandidate(el, context) }))
|
|
1851
|
+
.filter(el => matchesAnchorContext(el, context))
|
|
1852
|
+
.map(el => ({ el, score: scoreAnchorCandidate(el, context) + (context.version === 1 && el === exact ? 16 : 0) }))
|
|
1700
1853
|
.sort((a, b) => b.score - a.score);
|
|
1701
|
-
if (
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1854
|
+
if (context.candidateCount > 1) {
|
|
1855
|
+
const peers = candidates.filter(el => matchesAnchorIdentity(el, context.target));
|
|
1856
|
+
if (peers.length !== context.candidateCount) {
|
|
1857
|
+
// A removed identical sibling must not move its comment to the survivor.
|
|
1858
|
+
const distinctive = matches[0] && (context.target.id || context.target.testId);
|
|
1859
|
+
if (!distinctive) return { element: null, status: "uncertain" };
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (matches[0]?.score >= 58 && (!matches[1] || matches[0].score - matches[1].score >= 12)) {
|
|
1863
|
+
return { element: matches[0].el, status: "matched" };
|
|
1864
|
+
}
|
|
1865
|
+
// A closed/unmounted overlay is not evidence that its contents were deleted.
|
|
1866
|
+
if (context.layer && !candidates.some(el => {
|
|
1867
|
+
const layer = getPresentationLayer(el);
|
|
1868
|
+
return layer && matchesAnchorIdentity(layer, context.layer, false, false);
|
|
1869
|
+
})) return { element: null, status: "hidden" };
|
|
1870
|
+
const plausible = matches.length || candidates.some(el =>
|
|
1871
|
+
(context.target.id && el.id === context.target.id) ||
|
|
1872
|
+
(context.target.text && (getElementTextSignature(el) === context.target.text || similarText(getElementTextSignature(el), context.target.text))));
|
|
1873
|
+
return { element: null, status: plausible ? "uncertain" : "missing" };
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function resolveByAnchorContext(context) {
|
|
1877
|
+
return resolveAnchor(context).element;
|
|
1705
1878
|
}
|
|
1706
1879
|
|
|
1707
1880
|
function getViewportBounds() {
|
|
@@ -1873,7 +2046,7 @@ function createAnchorMatcher({
|
|
|
1873
2046
|
return { clientX, clientY };
|
|
1874
2047
|
}
|
|
1875
2048
|
|
|
1876
|
-
return { matchesAnchorContext, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
2049
|
+
return { matchesAnchorContext, resolveAnchor, resolveByAnchorContext, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint };
|
|
1877
2050
|
}
|
|
1878
2051
|
|
|
1879
2052
|
const captureEngine = (() => { // Generated from html-to-image 1.11.13 by scripts/capture-engine.mjs.
|
|
@@ -2017,8 +2190,10 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
|
|
|
2017
2190
|
finally { clearTimeout(timer); }
|
|
2018
2191
|
}
|
|
2019
2192
|
|
|
2020
|
-
async function captureViewportScreenshot({ context = captureContext(), annotation } = {}) {
|
|
2193
|
+
async function captureViewportScreenshot({ context = captureContext(), annotation, region } = {}) {
|
|
2021
2194
|
if (document.visibilityState === "hidden") return null;
|
|
2195
|
+
const area = region || { left: 0, top: 0, width: context.width, height: context.height };
|
|
2196
|
+
if (![area.left, area.top, area.width, area.height].every(Number.isFinite) || area.left < 0 || area.top < 0 || area.width <= 0 || area.height <= 0 || area.left + area.width > context.width || area.top + area.height > context.height) throw new Error("Invalid capture area");
|
|
2022
2197
|
const controller = new AbortController();
|
|
2023
2198
|
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
2024
2199
|
const { signal } = controller;
|
|
@@ -2032,7 +2207,7 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
|
|
|
2032
2207
|
let lastYield = performance.now();
|
|
2033
2208
|
const checkContext = () => {
|
|
2034
2209
|
signal.throwIfAborted();
|
|
2035
|
-
if (!frozen && (context.url !== location.href || context.width !== innerWidth || context.height !== innerHeight || context.scrollX !== scrollX || context.scrollY !== scrollY)) {
|
|
2210
|
+
if ((!frozen || region) && (context.url !== location.href || context.width !== innerWidth || context.height !== innerHeight || context.scrollX !== scrollX || context.scrollY !== scrollY)) {
|
|
2036
2211
|
throw new Error("Page moved before its screenshot was ready");
|
|
2037
2212
|
}
|
|
2038
2213
|
};
|
|
@@ -2231,21 +2406,21 @@ function createScreenshotCapture({ apiUrl, projectKey, getHeaders = () => ({}) }
|
|
|
2231
2406
|
image.src = svg;
|
|
2232
2407
|
await untilAborted(image.decode());
|
|
2233
2408
|
checkContext();
|
|
2234
|
-
const scale = Math.min(2, devicePixelRatio || 1, 2560 /
|
|
2409
|
+
const scale = Math.min(2, devicePixelRatio || 1, 2560 / area.width, 2560 / area.height);
|
|
2235
2410
|
const canvas = document.createElement("canvas");
|
|
2236
|
-
canvas.width = Math.round(
|
|
2237
|
-
canvas.height = Math.round(
|
|
2411
|
+
canvas.width = Math.max(1, Math.round(area.width * scale));
|
|
2412
|
+
canvas.height = Math.max(1, Math.round(area.height * scale));
|
|
2238
2413
|
const ctx = canvas.getContext("2d");
|
|
2239
2414
|
ctx.scale(scale, scale);
|
|
2240
|
-
ctx.drawImage(image,
|
|
2415
|
+
ctx.drawImage(image, -area.left, -area.top);
|
|
2241
2416
|
let blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/png")));
|
|
2242
2417
|
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2243
|
-
if (blob.size > 2800000) {
|
|
2418
|
+
if (!region && blob.size > 2800000) {
|
|
2244
2419
|
blob = await untilAborted(new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", 0.9)));
|
|
2245
2420
|
if (!blob) throw new Error("Screenshot encoding failed");
|
|
2246
2421
|
}
|
|
2247
2422
|
// CSS dimensions keep the eyedropper's coordinate system independent of DPR.
|
|
2248
|
-
return { blob, width: canvas.width, height: canvas.height, cssWidth:
|
|
2423
|
+
return { blob, width: canvas.width, height: canvas.height, cssWidth: area.width, cssHeight: area.height };
|
|
2249
2424
|
} finally {
|
|
2250
2425
|
clearTimeout(timeout);
|
|
2251
2426
|
controller.abort();
|
|
@@ -2371,7 +2546,7 @@ function createCommentThread(context) {
|
|
|
2371
2546
|
renderSidepanelComments, applyStatusFilters, createThreadItem, renderCommentContent,
|
|
2372
2547
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
2373
2548
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
2374
|
-
showAuthPrompt,
|
|
2549
|
+
showAuthPrompt, startThreadAgent,
|
|
2375
2550
|
} = context;
|
|
2376
2551
|
|
|
2377
2552
|
return function openCommentThread(
|
|
@@ -2434,16 +2609,16 @@ function createCommentThread(context) {
|
|
|
2434
2609
|
};
|
|
2435
2610
|
|
|
2436
2611
|
const saveStatus = async (nextStatus, control) => {
|
|
2437
|
-
if (!context.canManage) return false;
|
|
2612
|
+
if (!context.canManage || comment.local_agent) return false;
|
|
2438
2613
|
control.disabled = true;
|
|
2439
2614
|
const savedStatus = await updateCommentStatus(comment, nextStatus);
|
|
2440
2615
|
control.disabled = false;
|
|
2441
2616
|
if (!savedStatus) return false;
|
|
2442
2617
|
|
|
2443
2618
|
const savedStatusOption = getCommentStatus(savedStatus);
|
|
2444
|
-
pin.dataset.status = savedStatusOption.value;
|
|
2445
|
-
applyStatusDot(pinStatusDot, savedStatus);
|
|
2446
|
-
pin
|
|
2619
|
+
if (pin) pin.dataset.status = savedStatusOption.value;
|
|
2620
|
+
if (pinStatusDot) applyStatusDot(pinStatusDot, savedStatus);
|
|
2621
|
+
pin?.setAttribute(
|
|
2447
2622
|
"aria-label",
|
|
2448
2623
|
`Afficher le commentaire, statut ${savedStatusOption.label}`,
|
|
2449
2624
|
);
|
|
@@ -2459,7 +2634,7 @@ function createCommentThread(context) {
|
|
|
2459
2634
|
return true;
|
|
2460
2635
|
};
|
|
2461
2636
|
|
|
2462
|
-
statusMenuButton.disabled = !context.canManage;
|
|
2637
|
+
statusMenuButton.disabled = comment.local_agent || !context.canManage;
|
|
2463
2638
|
statusMenuButton.onclick = (event) => {
|
|
2464
2639
|
event.stopPropagation();
|
|
2465
2640
|
const existingPopover = document.querySelector(
|
|
@@ -2608,7 +2783,7 @@ function createCommentThread(context) {
|
|
|
2608
2783
|
);
|
|
2609
2784
|
};
|
|
2610
2785
|
|
|
2611
|
-
const mainItem = createThreadItem(comment, { showMore: isOwnCommentEntry(comment) });
|
|
2786
|
+
const mainItem = createThreadItem(comment, { showMore: !comment.local_agent && isOwnCommentEntry(comment) });
|
|
2612
2787
|
|
|
2613
2788
|
const startEditingEntry = (entry, item, persist) => {
|
|
2614
2789
|
if (!isOwnCommentEntry(entry)) return;
|
|
@@ -2704,7 +2879,7 @@ function createCommentThread(context) {
|
|
|
2704
2879
|
getCommentId(cachedComment) !== getCommentId(comment),
|
|
2705
2880
|
);
|
|
2706
2881
|
placedPins.delete(getCommentId(comment));
|
|
2707
|
-
pin
|
|
2882
|
+
pin?.remove();
|
|
2708
2883
|
closeBubbles();
|
|
2709
2884
|
renderSidepanelComments();
|
|
2710
2885
|
applyStatusFilters();
|
|
@@ -2751,11 +2926,19 @@ function createCommentThread(context) {
|
|
|
2751
2926
|
};
|
|
2752
2927
|
replies.forEach(appendReply);
|
|
2753
2928
|
thread.appendChild(threadList);
|
|
2929
|
+
bubble.addEventListener('comty-agent-reply', (event) => {
|
|
2930
|
+
const reply = event.detail;
|
|
2931
|
+
if (!reply || replies.some(item => item.id === reply.id)) return;
|
|
2932
|
+
replies.push(reply);
|
|
2933
|
+
comment.replies = replies;
|
|
2934
|
+
appendReply(reply);
|
|
2935
|
+
threadList.scrollTop = threadList.scrollHeight;
|
|
2936
|
+
});
|
|
2754
2937
|
|
|
2755
2938
|
const replyForm = document.createElement("div");
|
|
2756
2939
|
replyForm.className = "comty-reply-form";
|
|
2757
2940
|
|
|
2758
|
-
if (context.canWrite) {
|
|
2941
|
+
if (context.canWrite && !comment.local_agent) {
|
|
2759
2942
|
replyForm.appendChild(createCommentAvatar(context.currentUser || { name: "You" }));
|
|
2760
2943
|
const replyField = document.createElement("div");
|
|
2761
2944
|
replyField.className = "comty-reply-field";
|
|
@@ -2798,6 +2981,18 @@ function createCommentThread(context) {
|
|
|
2798
2981
|
appendReply(data.reply);
|
|
2799
2982
|
replyInput.value = "";
|
|
2800
2983
|
syncReplySize();
|
|
2984
|
+
if (/(^|\s)@agent\b/i.test(content)) {
|
|
2985
|
+
try {
|
|
2986
|
+
await startThreadAgent(comment, replies, content);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
const notice = document.createElement('p');
|
|
2989
|
+
notice.className = 'comty-agent-thread-error';
|
|
2990
|
+
notice.setAttribute('role', 'alert');
|
|
2991
|
+
notice.textContent = `Agent could not start: ${error.message}`;
|
|
2992
|
+
replyForm.querySelector('.comty-agent-thread-error')?.remove();
|
|
2993
|
+
replyForm.appendChild(notice);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2801
2996
|
requestAnimationFrame(() => {
|
|
2802
2997
|
threadList.scrollTop = threadList.scrollHeight;
|
|
2803
2998
|
});
|
|
@@ -2845,7 +3040,7 @@ function createCommentThread(context) {
|
|
|
2845
3040
|
replyField.appendChild(replyInput);
|
|
2846
3041
|
replyField.appendChild(replyButton);
|
|
2847
3042
|
replyForm.appendChild(replyField);
|
|
2848
|
-
} else {
|
|
3043
|
+
} else if (!comment.local_agent) {
|
|
2849
3044
|
replyForm.classList.add("comty-reply-form-guest");
|
|
2850
3045
|
const signInButton = document.createElement("button");
|
|
2851
3046
|
signInButton.className = "comty-sign-in-reply";
|
|
@@ -2858,7 +3053,7 @@ function createCommentThread(context) {
|
|
|
2858
3053
|
replyForm.appendChild(signInButton);
|
|
2859
3054
|
}
|
|
2860
3055
|
|
|
2861
|
-
thread.appendChild(replyForm);
|
|
3056
|
+
if (!comment.local_agent) thread.appendChild(replyForm);
|
|
2862
3057
|
bubble.appendChild(thread);
|
|
2863
3058
|
root.appendChild(bubble);
|
|
2864
3059
|
|
|
@@ -2879,6 +3074,290 @@ function createCommentThread(context) {
|
|
|
2879
3074
|
}
|
|
2880
3075
|
}
|
|
2881
3076
|
|
|
3077
|
+
// Page-coordinate drawings stay attached to the document when the viewport scrolls.
|
|
3078
|
+
function createInspectMarkup({ root, icon, onChange, capture, onError }) {
|
|
3079
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
3080
|
+
const layer = document.createElementNS(ns, 'svg');
|
|
3081
|
+
layer.classList.add('comty-markup-layer');
|
|
3082
|
+
layer.setAttribute('aria-hidden', 'true');
|
|
3083
|
+
const drawings = document.createElementNS(ns, 'g');
|
|
3084
|
+
layer.append(drawings);
|
|
3085
|
+
root.append(layer);
|
|
3086
|
+
const surface = document.createElement('div');
|
|
3087
|
+
surface.className = 'comty-markup-surface';
|
|
3088
|
+
surface.hidden = true;
|
|
3089
|
+
const selection = document.createElement('div');
|
|
3090
|
+
selection.className = 'comty-capture-selection';
|
|
3091
|
+
selection.hidden = true;
|
|
3092
|
+
surface.append(selection);
|
|
3093
|
+
root.append(surface);
|
|
3094
|
+
const panel = document.createElement('section');
|
|
3095
|
+
panel.className = 'comty-tool-panel comty-markup-panel';
|
|
3096
|
+
panel.id = 'comty-markup-panel';
|
|
3097
|
+
panel.setAttribute('aria-label', 'Annotations and screenshot');
|
|
3098
|
+
panel.hidden = true;
|
|
3099
|
+
root.append(panel);
|
|
3100
|
+
const shapes = {
|
|
3101
|
+
pencil: ['Pencil', '<path d="m16 3 5 5L8 21H3v-5ZM14 5l5 5"/>'],
|
|
3102
|
+
rectangle: ['Rectangle', '<rect x="4" y="5" width="16" height="14" rx="1"/>'],
|
|
3103
|
+
ellipse: ['Ellipse', '<ellipse cx="12" cy="12" rx="9" ry="7"/>'],
|
|
3104
|
+
arrow: ['Arrow', '<path d="M4 20 20 4M8 4h12v12"/>'],
|
|
3105
|
+
};
|
|
3106
|
+
const palette = [
|
|
3107
|
+
['Red', '--comty-measure'], ['Yellow', '--comty-markup-yellow'],
|
|
3108
|
+
['Green', '--comty-markup-green'], ['Blue', '--comty-accent'],
|
|
3109
|
+
['Black', '--comty-ink'], ['White', '--comty-paper'],
|
|
3110
|
+
];
|
|
3111
|
+
const tokenColor = token => {
|
|
3112
|
+
const value = getComputedStyle(root).getPropertyValue(token).trim();
|
|
3113
|
+
return /^#[\da-f]{3}$/i.test(value) ? '#' + [...value.slice(1)].map(char => char + char).join('') : value;
|
|
3114
|
+
};
|
|
3115
|
+
panel.setAttribute('aria-label', 'Annotation tools');
|
|
3116
|
+
panel.innerHTML = [
|
|
3117
|
+
'<div class="comty-markup-controls" role="group" aria-label="Drawing tools">',
|
|
3118
|
+
...Object.entries(shapes).map(([key, [label, paths]]) => `<button type="button" data-shape="${key}" aria-label="${label}" title="${label}" aria-pressed="false">${icon(paths)}</button>`),
|
|
3119
|
+
'<span class="comty-markup-divider" aria-hidden="true"></span>',
|
|
3120
|
+
`<button type="button" data-undo aria-label="Undo annotation" title="Undo · ⌘/Ctrl Z">${icon('<path d="m9 4-5 5 5 5M4 9h10a6 6 0 0 1 0 12"/>')}</button>`,
|
|
3121
|
+
`<button type="button" data-clear aria-label="Clear annotations" title="Clear annotations">${icon('<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/>')}</button></div>`,
|
|
3122
|
+
'<div class="comty-markup-options"><div class="comty-markup-strokes" role="group" aria-label="Stroke width">',
|
|
3123
|
+
...[[2, 'Thin'], [3, 'Medium'], [5, 'Thick']].map(([width, label]) => `<button type="button" data-stroke="${width}" aria-label="${label} stroke" title="${label}" aria-pressed="false"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m7 7 10 10" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg></button>`),
|
|
3124
|
+
'</div><span class="comty-markup-divider" aria-hidden="true"></span><div class="comty-markup-colors" role="group" aria-label="Annotation colors">',
|
|
3125
|
+
...palette.map(([name, token]) => `<button type="button" data-color="${token}" aria-label="${name} ink" title="${name}" aria-pressed="false"><span class="comty-markup-swatch" aria-hidden="true"></span></button>`),
|
|
3126
|
+
'<button type="button" class="comty-markup-picker" title="Custom color" aria-label="Custom annotation color" aria-expanded="false" aria-controls="comty-markup-color"></button>',
|
|
3127
|
+
'</div></div>',
|
|
3128
|
+
'<div class="comty-markup-color" id="comty-markup-color" role="group" aria-label="Custom color picker" hidden>',
|
|
3129
|
+
'<div class="comty-markup-color-field" tabindex="0" role="group" aria-label="Saturation and brightness" aria-description="Left and right arrows adjust saturation. Up and down arrows adjust brightness."><span aria-hidden="true"></span></div>',
|
|
3130
|
+
'<input class="comty-markup-hue" type="range" min="0" max="360" step="1" aria-label="Hue">',
|
|
3131
|
+
'<label class="comty-markup-hex"><span class="comty-markup-color-preview" aria-hidden="true"></span><span>HEX</span><input type="text" aria-label="Hex color" maxlength="7" spellcheck="false" autocomplete="off" autocapitalize="off"></label>',
|
|
3132
|
+
'</div>',
|
|
3133
|
+
].join('');
|
|
3134
|
+
const picker = panel.querySelector('.comty-markup-picker');
|
|
3135
|
+
const colorPanel = panel.querySelector('.comty-markup-color');
|
|
3136
|
+
const colorField = panel.querySelector('.comty-markup-color-field');
|
|
3137
|
+
const hueInput = panel.querySelector('.comty-markup-hue');
|
|
3138
|
+
const hexInput = panel.querySelector('[aria-label="Hex color"]');
|
|
3139
|
+
let pickerOpen = false, colorPointer = null;
|
|
3140
|
+
let hue = 0, saturation = 1, brightness = 1;
|
|
3141
|
+
let mode = null, shape = 'pencil', drag = null, visible = false, busy = false;
|
|
3142
|
+
let stroke = 3, color = tokenColor('--comty-measure');
|
|
3143
|
+
readColor(color);
|
|
3144
|
+
for (const button of panel.querySelectorAll('[data-color]')) button.style.setProperty('--markup-swatch', tokenColor(button.dataset.color));
|
|
3145
|
+
let revision = 0;
|
|
3146
|
+
const history = [];
|
|
3147
|
+
const clamp = (v, max) => Math.max(0, Math.min(max, v));
|
|
3148
|
+
function point(event) { return { x: clamp(event.clientX, innerWidth), y: clamp(event.clientY, innerHeight) }; }
|
|
3149
|
+
function sync() {
|
|
3150
|
+
layer.style.display = visible ? '' : 'none';
|
|
3151
|
+
surface.hidden = !visible || !mode;
|
|
3152
|
+
panel.hidden = !visible || mode !== 'annotate';
|
|
3153
|
+
for (const button of panel.querySelectorAll('[data-stroke]')) button.setAttribute('aria-pressed', String(Number(button.dataset.stroke) === stroke));
|
|
3154
|
+
for (const button of panel.querySelectorAll('[data-color]')) button.setAttribute('aria-pressed', String(tokenColor(button.dataset.color) === color));
|
|
3155
|
+
picker.toggleAttribute('data-selected', !palette.some(([, token]) => tokenColor(token) === color));
|
|
3156
|
+
syncPicker();
|
|
3157
|
+
for (const button of panel.querySelectorAll('[data-shape]')) button.setAttribute('aria-pressed', String(button.dataset.shape === shape));
|
|
3158
|
+
panel.querySelector('[data-undo]').disabled = !history.length;
|
|
3159
|
+
panel.querySelector('[data-clear]').disabled = !history.length;
|
|
3160
|
+
render();
|
|
3161
|
+
onChange();
|
|
3162
|
+
}
|
|
3163
|
+
function cancelDrag() {
|
|
3164
|
+
if (!drag) return;
|
|
3165
|
+
drag.node?.remove();
|
|
3166
|
+
const id = drag.id;
|
|
3167
|
+
drag = null;
|
|
3168
|
+
if (surface.hasPointerCapture(id)) surface.releasePointerCapture(id);
|
|
3169
|
+
selection.hidden = true;
|
|
3170
|
+
}
|
|
3171
|
+
function setMode(next) {
|
|
3172
|
+
if (busy && next) return;
|
|
3173
|
+
cancelDrag();
|
|
3174
|
+
if (!next) revision++;
|
|
3175
|
+
mode = next;
|
|
3176
|
+
if (next !== 'annotate') { pickerOpen = false; finishColorDrag(); }
|
|
3177
|
+
sync();
|
|
3178
|
+
}
|
|
3179
|
+
function undo() { history.pop()?.remove(); sync(); }
|
|
3180
|
+
function clear() { cancelDrag(); history.length = 0; drawings.replaceChildren(); sync(); }
|
|
3181
|
+
panel.querySelector('[data-undo]').onclick = undo;
|
|
3182
|
+
panel.querySelector('[data-clear]').onclick = clear;
|
|
3183
|
+
for (const button of panel.querySelectorAll('[data-shape]')) button.onclick = () => { shape = button.dataset.shape; sync(); };
|
|
3184
|
+
for (const button of panel.querySelectorAll('[data-stroke]')) button.onclick = () => { stroke = Number(button.dataset.stroke); sync(); };
|
|
3185
|
+
for (const button of panel.querySelectorAll('[data-color]')) button.onclick = () => {
|
|
3186
|
+
color = tokenColor(button.dataset.color); readColor(color); sync();
|
|
3187
|
+
};
|
|
3188
|
+
function readColor(value) {
|
|
3189
|
+
const [r, g, b] = value.slice(1).match(/../g).map(part => parseInt(part, 16) / 255);
|
|
3190
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
|
|
3191
|
+
if (delta) hue = ((max === r ? (g - b) / delta : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4) * 60 + 360) % 360;
|
|
3192
|
+
saturation = max ? delta / max : 0;
|
|
3193
|
+
brightness = max;
|
|
3194
|
+
}
|
|
3195
|
+
function writeColor() {
|
|
3196
|
+
const channel = offset => {
|
|
3197
|
+
const k = (offset + hue / 60) % 6;
|
|
3198
|
+
return Math.round(255 * brightness * (1 - saturation * Math.max(0, Math.min(k, 4 - k, 1)))).toString(16).padStart(2, '0');
|
|
3199
|
+
};
|
|
3200
|
+
color = '#' + [5, 3, 1].map(channel).join('');
|
|
3201
|
+
sync();
|
|
3202
|
+
}
|
|
3203
|
+
function syncPicker() {
|
|
3204
|
+
colorPanel.hidden = !pickerOpen;
|
|
3205
|
+
picker.setAttribute('aria-expanded', String(pickerOpen));
|
|
3206
|
+
colorPanel.style.setProperty('--markup-hue', `hsl(${hue} 100% 50%)`);
|
|
3207
|
+
colorPanel.style.setProperty('--markup-color', color);
|
|
3208
|
+
const marker = colorField.firstElementChild;
|
|
3209
|
+
marker.style.left = `${saturation * 100}%`;
|
|
3210
|
+
marker.style.top = `${(1 - brightness) * 100}%`;
|
|
3211
|
+
colorField.setAttribute('aria-description', `Saturation ${Math.round(saturation * 100)}%, brightness ${Math.round(brightness * 100)}%. Left and right arrows adjust saturation. Up and down arrows adjust brightness.`);
|
|
3212
|
+
hueInput.value = String(Math.round(hue));
|
|
3213
|
+
hueInput.setAttribute('aria-valuetext', `${Math.round(hue)} degrees`);
|
|
3214
|
+
if (document.activeElement !== hexInput) { hexInput.value = color.toUpperCase(); hexInput.removeAttribute('aria-invalid'); }
|
|
3215
|
+
}
|
|
3216
|
+
picker.onclick = () => { pickerOpen = !pickerOpen; sync(); };
|
|
3217
|
+
function moveColor(event) {
|
|
3218
|
+
const rect = colorField.getBoundingClientRect();
|
|
3219
|
+
saturation = clamp((event.clientX - rect.left) / rect.width, 1);
|
|
3220
|
+
brightness = 1 - clamp((event.clientY - rect.top) / rect.height, 1);
|
|
3221
|
+
writeColor();
|
|
3222
|
+
}
|
|
3223
|
+
function finishColorDrag() {
|
|
3224
|
+
const id = colorPointer;
|
|
3225
|
+
colorPointer = null;
|
|
3226
|
+
if (id !== null && colorField.hasPointerCapture(id)) colorField.releasePointerCapture(id);
|
|
3227
|
+
}
|
|
3228
|
+
colorField.onpointerdown = event => {
|
|
3229
|
+
if (event.button !== 0 || colorPointer !== null) return;
|
|
3230
|
+
event.preventDefault(); event.stopPropagation();
|
|
3231
|
+
colorField.focus({ preventScroll: true });
|
|
3232
|
+
colorPointer = event.pointerId;
|
|
3233
|
+
colorField.setPointerCapture(event.pointerId);
|
|
3234
|
+
moveColor(event);
|
|
3235
|
+
};
|
|
3236
|
+
colorField.onpointermove = event => { if (colorPointer === event.pointerId) moveColor(event); };
|
|
3237
|
+
colorField.onpointerup = event => { if (colorPointer === event.pointerId) { moveColor(event); finishColorDrag(); } };
|
|
3238
|
+
colorField.onpointercancel = finishColorDrag;
|
|
3239
|
+
colorField.onlostpointercapture = finishColorDrag;
|
|
3240
|
+
window.addEventListener('blur', finishColorDrag);
|
|
3241
|
+
colorField.onkeydown = event => {
|
|
3242
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
3243
|
+
event.preventDefault(); event.stopPropagation();
|
|
3244
|
+
const step = event.shiftKey ? .1 : .01;
|
|
3245
|
+
if (event.key === 'ArrowLeft') saturation = clamp(saturation - step, 1);
|
|
3246
|
+
if (event.key === 'ArrowRight') saturation = clamp(saturation + step, 1);
|
|
3247
|
+
if (event.key === 'ArrowUp') brightness = clamp(brightness + step, 1);
|
|
3248
|
+
if (event.key === 'ArrowDown') brightness = clamp(brightness - step, 1);
|
|
3249
|
+
writeColor();
|
|
3250
|
+
};
|
|
3251
|
+
hueInput.oninput = () => { hue = Number(hueInput.value); writeColor(); };
|
|
3252
|
+
function applyHex(commit = false) {
|
|
3253
|
+
const value = hexInput.value.trim().replace(/^#/, '');
|
|
3254
|
+
const valid = /^[\da-f]{6}$/i.test(value) || (commit && /^[\da-f]{3}$/i.test(value));
|
|
3255
|
+
if (valid) {
|
|
3256
|
+
color = '#' + (value.length === 3 ? [...value].map(char => char + char).join('') : value).toLowerCase();
|
|
3257
|
+
readColor(color); hexInput.removeAttribute('aria-invalid'); sync();
|
|
3258
|
+
} else hexInput.setAttribute('aria-invalid', 'true');
|
|
3259
|
+
if (commit) { hexInput.value = color.toUpperCase(); hexInput.removeAttribute('aria-invalid'); }
|
|
3260
|
+
}
|
|
3261
|
+
hexInput.oninput = () => applyHex();
|
|
3262
|
+
hexInput.onblur = () => applyHex(true);
|
|
3263
|
+
hexInput.onkeydown = event => { if (event.key === 'Enter') { event.preventDefault(); applyHex(true); } };
|
|
3264
|
+
function render() { drawings.setAttribute('transform', `translate(${-scrollX} ${-scrollY})`); }
|
|
3265
|
+
function update(event) {
|
|
3266
|
+
const p = point(event), start = drag.start;
|
|
3267
|
+
drag.end = p;
|
|
3268
|
+
if (mode === 'capture') {
|
|
3269
|
+
Object.assign(selection.style, { left: `${Math.min(start.x, p.x)}px`, top: `${Math.min(start.y, p.y)}px`, width: `${Math.abs(p.x - start.x)}px`, height: `${Math.abs(p.y - start.y)}px` });
|
|
3270
|
+
selection.textContent = `${Math.round(Math.abs(p.x - start.x))} × ${Math.round(Math.abs(p.y - start.y))}`;
|
|
3271
|
+
selection.hidden = false;
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
const a = drag.origin, b = { x: p.x + scrollX, y: p.y + scrollY };
|
|
3275
|
+
const left = Math.min(a.x, b.x), top = Math.min(a.y, b.y), w = Math.abs(b.x - a.x), h = Math.abs(b.y - a.y);
|
|
3276
|
+
let d;
|
|
3277
|
+
if (shape === 'pencil') {
|
|
3278
|
+
drag.points.push(b);
|
|
3279
|
+
d = drag.points.map((p, i) => `${i ? 'L' : 'M'}${p.x} ${p.y}`).join(' ');
|
|
3280
|
+
} else if (shape === 'rectangle') d = `M${left} ${top}h${w}v${h}h${-w}Z`;
|
|
3281
|
+
else if (shape === 'ellipse') d = `M${left} ${top + h/2}a${w/2} ${h/2} 0 1 0 ${w} 0a${w/2} ${h/2} 0 1 0 ${-w} 0`;
|
|
3282
|
+
else {
|
|
3283
|
+
const angle = Math.atan2(b.y-a.y, b.x-a.x), size = Math.min(16, Math.hypot(b.x-a.x,b.y-a.y)/3);
|
|
3284
|
+
d = `M${a.x} ${a.y}L${b.x} ${b.y}M${b.x-size*Math.cos(angle-.5)} ${b.y-size*Math.sin(angle-.5)}L${b.x} ${b.y}L${b.x-size*Math.cos(angle+.5)} ${b.y-size*Math.sin(angle+.5)}`;
|
|
3285
|
+
}
|
|
3286
|
+
drag.node.setAttribute('d', d);
|
|
3287
|
+
}
|
|
3288
|
+
surface.addEventListener('pointerdown', event => {
|
|
3289
|
+
if (event.button !== 0 || drag || busy) return;
|
|
3290
|
+
event.preventDefault(); event.stopPropagation();
|
|
3291
|
+
const start = point(event), origin = { x: start.x + scrollX, y: start.y + scrollY };
|
|
3292
|
+
drag = { id: event.pointerId, start, end: start, origin, points: [origin] };
|
|
3293
|
+
if (mode === 'annotate') {
|
|
3294
|
+
drag.node = document.createElementNS(ns, 'path');
|
|
3295
|
+
drag.node.setAttribute('fill', 'none');
|
|
3296
|
+
drag.node.setAttribute('stroke', color);
|
|
3297
|
+
drag.node.setAttribute('stroke-width', String(stroke));
|
|
3298
|
+
drag.node.setAttribute('stroke-linecap', 'round');
|
|
3299
|
+
drag.node.setAttribute('stroke-linejoin', 'round');
|
|
3300
|
+
drawings.append(drag.node);
|
|
3301
|
+
}
|
|
3302
|
+
surface.setPointerCapture(event.pointerId);
|
|
3303
|
+
update(event);
|
|
3304
|
+
});
|
|
3305
|
+
surface.addEventListener('pointermove', event => { if (drag?.id === event.pointerId) { event.preventDefault(); update(event); } });
|
|
3306
|
+
surface.addEventListener('pointerup', async event => {
|
|
3307
|
+
if (drag?.id !== event.pointerId) return;
|
|
3308
|
+
event.preventDefault(); event.stopPropagation();
|
|
3309
|
+
update(event);
|
|
3310
|
+
const finished = drag;
|
|
3311
|
+
drag = null;
|
|
3312
|
+
surface.releasePointerCapture(event.pointerId);
|
|
3313
|
+
selection.hidden = true;
|
|
3314
|
+
if (mode === 'annotate') { history.push(finished.node); sync(); return; }
|
|
3315
|
+
const region = { left: Math.floor(Math.min(finished.start.x, finished.end.x)), top: Math.floor(Math.min(finished.start.y, finished.end.y)), width: Math.round(Math.abs(finished.end.x-finished.start.x)), height: Math.round(Math.abs(finished.end.y-finished.start.y)) };
|
|
3316
|
+
if (region.width < 4 || region.height < 4) return;
|
|
3317
|
+
busy = true;
|
|
3318
|
+
const current = ++revision;
|
|
3319
|
+
surface.setAttribute('aria-busy', 'true');
|
|
3320
|
+
sync();
|
|
3321
|
+
try {
|
|
3322
|
+
const result = await capture(region, layer);
|
|
3323
|
+
if (current !== revision) return;
|
|
3324
|
+
if (!result) throw new Error('Page is not visible');
|
|
3325
|
+
const url = URL.createObjectURL(result.blob);
|
|
3326
|
+
const link = document.createElement('a');
|
|
3327
|
+
link.href = url; link.download = `comty-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
|
3328
|
+
link.click();
|
|
3329
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
if (current === revision) onError(`Capture failed. ${error.message}. Try again.`);
|
|
3332
|
+
} finally {
|
|
3333
|
+
busy = false; surface.removeAttribute('aria-busy'); sync();
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
surface.addEventListener('pointercancel', cancelDrag);
|
|
3337
|
+
surface.addEventListener('lostpointercapture', cancelDrag);
|
|
3338
|
+
surface.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); });
|
|
3339
|
+
surface.addEventListener('wheel', event => { if (drag || busy) event.preventDefault(); }, { passive: false });
|
|
3340
|
+
document.addEventListener('scroll', () => { cancelDrag(); render(); }, { passive: true, capture: true });
|
|
3341
|
+
window.addEventListener('resize', cancelDrag);
|
|
3342
|
+
window.addEventListener('blur', cancelDrag);
|
|
3343
|
+
return {
|
|
3344
|
+
panel, setMode, render, clear,
|
|
3345
|
+
get mode() { return mode; },
|
|
3346
|
+
get busy() { return busy; },
|
|
3347
|
+
setVisible(value) { visible = value; if (!value) { revision++; mode = null; pickerOpen = false; finishColorDrag(); cancelDrag(); } sync(); },
|
|
3348
|
+
keydown(event) {
|
|
3349
|
+
if (!mode) return false;
|
|
3350
|
+
if (event.key === 'Escape') {
|
|
3351
|
+
if (pickerOpen) { pickerOpen = false; finishColorDrag(); picker.focus({ preventScroll: true }); sync(); return true; }
|
|
3352
|
+
if (drag) cancelDrag(); else setMode(null);
|
|
3353
|
+
return true;
|
|
3354
|
+
}
|
|
3355
|
+
if (mode === 'annotate' && (event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === 'z') { cancelDrag(); undo(); return true; }
|
|
3356
|
+
return false;
|
|
3357
|
+
},
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
|
|
2882
3361
|
const edgeAxis = { top: "y", bottom: "y", left: "x", right: "x" };
|
|
2883
3362
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
2884
3363
|
|
|
@@ -2911,7 +3390,7 @@ function measurementPoint(anchor, rect, scroll = { x: 0, y: 0 }) {
|
|
|
2911
3390
|
}
|
|
2912
3391
|
|
|
2913
3392
|
/** Border measurements live for one activation of the existing ruler tool. */
|
|
2914
|
-
function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit }) {
|
|
3393
|
+
function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit, isInteractionBlocked = () => false }) {
|
|
2915
3394
|
let enabled = false;
|
|
2916
3395
|
let hover = null;
|
|
2917
3396
|
let drag = null;
|
|
@@ -3035,7 +3514,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
|
|
|
3035
3514
|
}
|
|
3036
3515
|
}
|
|
3037
3516
|
function isHostEvent(event) {
|
|
3038
|
-
if (!enabled || !(event.target instanceof Element)) return false;
|
|
3517
|
+
if (!enabled || isInteractionBlocked() || !(event.target instanceof Element)) return false;
|
|
3039
3518
|
if (!isComtyElement(event.target)) return true;
|
|
3040
3519
|
// The outer ruler strip must still let us anchor to the page's top/left edge.
|
|
3041
3520
|
return event.target.matches(".comty-ruler") && (event.clientX + scrollX <= 10 || event.clientY + scrollY <= 10);
|
|
@@ -3073,7 +3552,7 @@ function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit
|
|
|
3073
3552
|
if (isHostEvent(event)) { event.preventDefault(); event.stopImmediatePropagation(); }
|
|
3074
3553
|
}, true);
|
|
3075
3554
|
document.addEventListener("keydown", event => {
|
|
3076
|
-
if (!enabled || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
|
|
3555
|
+
if (!enabled || isInteractionBlocked() || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
|
|
3077
3556
|
event.preventDefault(); event.stopImmediatePropagation();
|
|
3078
3557
|
if (drag) cancel(); else onExit();
|
|
3079
3558
|
}, true);
|
|
@@ -3095,7 +3574,17 @@ const instanceKey = Symbol.for("comty.sdk.instance");
|
|
|
3095
3574
|
/** Initialize once per document; safe to import and call during server rendering. */
|
|
3096
3575
|
function initComty(options = {}) {
|
|
3097
3576
|
if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
|
|
3098
|
-
const
|
|
3577
|
+
const isLocalSite = ["localhost", "127.0.0.1", "[::1]"].includes(window.location.hostname);
|
|
3578
|
+
const agent = options.agent;
|
|
3579
|
+
if (agent && (!isLocalSite || typeof agent.token !== "string" || !/^[a-f0-9]{64}$/.test(agent.token))) {
|
|
3580
|
+
return Promise.reject(new TypeError("Comty: invalid local companion configuration"));
|
|
3581
|
+
}
|
|
3582
|
+
if (agent) {
|
|
3583
|
+
try {
|
|
3584
|
+
if (new URL(agent.url).origin !== window.location.origin) throw new Error();
|
|
3585
|
+
} catch { return Promise.reject(new TypeError("Comty: companion must share the page origin")); }
|
|
3586
|
+
}
|
|
3587
|
+
const projectKey = options.projectKey || (agent ? "local" : undefined);
|
|
3099
3588
|
const apiUrl = options.apiUrl || "http://localhost:4000";
|
|
3100
3589
|
if (typeof projectKey !== "string" || !projectKey.trim()) {
|
|
3101
3590
|
return Promise.reject(new TypeError("Comty: projectKey is required"));
|
|
@@ -3107,12 +3596,14 @@ function initComty(options = {}) {
|
|
|
3107
3596
|
if (!["http:", "https:"].includes(parsedApi.protocol)) {
|
|
3108
3597
|
return Promise.reject(new TypeError("Comty: apiUrl must be an absolute HTTP(S) URL"));
|
|
3109
3598
|
}
|
|
3110
|
-
const configuration = JSON.stringify([projectKey, apiUrl]);
|
|
3599
|
+
const configuration = JSON.stringify([projectKey, apiUrl, agent || null]);
|
|
3111
3600
|
const existing = window[instanceKey];
|
|
3112
3601
|
if (existing) {
|
|
3113
3602
|
return existing.configuration === configuration ? existing.ready
|
|
3114
3603
|
: Promise.reject(new Error("Comty: already initialized with different options"));
|
|
3115
3604
|
}
|
|
3605
|
+
const agentBridgeUrl = agent?.url;
|
|
3606
|
+
const localOnly = Boolean(agent && !options.projectKey);
|
|
3116
3607
|
const ready = new Promise((resolve, reject) => {
|
|
3117
3608
|
const mount = () => {
|
|
3118
3609
|
try { resolve(mountComty()); } catch (error) { reject(error); }
|
|
@@ -3126,10 +3617,24 @@ function initComty(options = {}) {
|
|
|
3126
3617
|
|
|
3127
3618
|
function mountComty() {
|
|
3128
3619
|
let projectId = null;
|
|
3620
|
+
let localTasks = [];
|
|
3621
|
+
const deliveringAgentReplies = new Set();
|
|
3622
|
+
let localApprovals = [];
|
|
3623
|
+
const seenAgentApprovals = new Set();
|
|
3624
|
+
// Only requests made in this document may interrupt it with agent notifications.
|
|
3625
|
+
const requestedAgentTasks = new Set();
|
|
3626
|
+
let agentPanelOpen = false;
|
|
3627
|
+
let agentPanelSignature = "";
|
|
3628
|
+
let agentPanelTransition = 0;
|
|
3629
|
+
let agentConnectionError = "";
|
|
3630
|
+
let agentActionError = "";
|
|
3631
|
+
let clearAgentToast = () => {};
|
|
3632
|
+
const agentPendingActions = new Set();
|
|
3129
3633
|
const session = {
|
|
3130
3634
|
mode: "off",
|
|
3131
|
-
get comments() { return this.mode === "browse" || this.mode === "edit"; },
|
|
3132
|
-
get edit() { return this.mode === "edit"; },
|
|
3635
|
+
get comments() { return this.mode === "browse" || this.mode === "edit" || this.mode === "agent"; },
|
|
3636
|
+
get edit() { return this.mode === "edit" || this.mode === "agent"; },
|
|
3637
|
+
get agent() { return this.mode === "agent"; },
|
|
3133
3638
|
get inspect() { return this.mode === "inspect"; },
|
|
3134
3639
|
};
|
|
3135
3640
|
const selectedStatusFilters = new Set();
|
|
@@ -3153,9 +3658,10 @@ function initComty(options = {}) {
|
|
|
3153
3658
|
const authReturnStorageKey = `comty:${projectKey}:auth-return`;
|
|
3154
3659
|
let sdkToken = "";
|
|
3155
3660
|
const activeHoverCommentIds = new Set();
|
|
3156
|
-
/** @type {null | 'edit' | 'browse'} */
|
|
3661
|
+
/** @type {null | 'edit' | 'browse' | 'agent'} */
|
|
3157
3662
|
let sessionEntry = null;
|
|
3158
3663
|
const placedPins = new Map();
|
|
3664
|
+
const workingOutlines = new Map();
|
|
3159
3665
|
const placementDiagnostics = new Map();
|
|
3160
3666
|
let inspectedElement = null;
|
|
3161
3667
|
let selectedElement = null;
|
|
@@ -3169,8 +3675,8 @@ function initComty(options = {}) {
|
|
|
3169
3675
|
const commentTargets = new Map();
|
|
3170
3676
|
|
|
3171
3677
|
const {
|
|
3172
|
-
matchesAnchorContext,
|
|
3173
|
-
isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
3678
|
+
matchesAnchorContext,
|
|
3679
|
+
resolveAnchor, isElementTreeHidden, getTopActiveModal, getVisibleAnchorPoint,
|
|
3174
3680
|
} = createAnchorMatcher({
|
|
3175
3681
|
getElementTextSignature,
|
|
3176
3682
|
getElementOwnTextSignature,
|
|
@@ -3248,10 +3754,27 @@ function initComty(options = {}) {
|
|
|
3248
3754
|
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"));
|
|
3249
3755
|
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"/>', () => {
|
|
3250
3756
|
if (!canWrite) showAuthPrompt();
|
|
3251
|
-
else if (session.inspect || !isSessionActive()) startEditSession();
|
|
3757
|
+
else if (session.agent || session.inspect || !isSessionActive()) startEditSession();
|
|
3252
3758
|
else if (!session.edit) setSession("edit", { entry: sessionEntry });
|
|
3253
3759
|
});
|
|
3254
3760
|
btn.id = "comty-toggle";
|
|
3761
|
+
const agentBtn = isLocalSite
|
|
3762
|
+
? toolButton("Agent task · localhost only", '<path d="M8 7 3 12l5 5m8-10 5 5-5 5M14 4l-4 16"/>', () => {
|
|
3763
|
+
if (session.agent) {
|
|
3764
|
+
const failedTask = [...localTasks].reverse().find(task => task.status === 'in_review' && !task.thread_comment_id);
|
|
3765
|
+
const failedPin = failedTask && placedPins.get(String(failedTask.id));
|
|
3766
|
+
if (failedPin && !failedPin.hasAttribute('data-comty-anchor-hidden')) { failedPin.click(); return; }
|
|
3767
|
+
if (localTasks.some(task => task.status === 'in_review' || (!task.thread_comment_id && task.status === 'done')) || localApprovals.length) {
|
|
3768
|
+
agentPanelOpen = true;
|
|
3769
|
+
renderAgentPanel();
|
|
3770
|
+
}
|
|
3771
|
+
} else startAgentSession();
|
|
3772
|
+
})
|
|
3773
|
+
: null;
|
|
3774
|
+
if (agentBtn) {
|
|
3775
|
+
agentBtn.title = "Agent · Shift A · localhost only";
|
|
3776
|
+
agentBtn.setAttribute("aria-keyshortcuts", "Shift+A");
|
|
3777
|
+
}
|
|
3255
3778
|
const divider = document.createElement("span");
|
|
3256
3779
|
divider.className = "comty-tool-divider";
|
|
3257
3780
|
toolbar.appendChild(divider);
|
|
@@ -3261,7 +3784,7 @@ function initComty(options = {}) {
|
|
|
3261
3784
|
const primaryTools = document.createElement("div");
|
|
3262
3785
|
primaryTools.className = "comty-tool-primary";
|
|
3263
3786
|
toolbar.insertBefore(primaryTools, browseBtn);
|
|
3264
|
-
for (const button of [browseBtn, inspectorBtn, btn]) primaryTools.appendChild(button);
|
|
3787
|
+
for (const button of [browseBtn, inspectorBtn, btn, agentBtn].filter(Boolean)) primaryTools.appendChild(button);
|
|
3265
3788
|
const inspectorTools = document.createElement("div");
|
|
3266
3789
|
inspectorTools.className = "comty-tool-context";
|
|
3267
3790
|
inspectorTools.hidden = true;
|
|
@@ -3291,6 +3814,7 @@ function initComty(options = {}) {
|
|
|
3291
3814
|
root,
|
|
3292
3815
|
isComtyElement,
|
|
3293
3816
|
isElementTreeHidden,
|
|
3817
|
+
isInteractionBlocked: () => Boolean(markup.mode),
|
|
3294
3818
|
onExit() { rulersEnabled = false; syncInspectorTools(); },
|
|
3295
3819
|
});
|
|
3296
3820
|
const gridLayer = document.createElement("div");
|
|
@@ -3317,6 +3841,7 @@ function initComty(options = {}) {
|
|
|
3317
3841
|
syncInspectorTools();
|
|
3318
3842
|
});
|
|
3319
3843
|
const rulersBtn = toolButton("Rulers · R", '<path d="M3 3h18v6H9v12H3Z"/><path d="M7 3v3m4-3v3m4-3v3M3 11h3m-3 4h3m-3 4h3"/>', () => {
|
|
3844
|
+
markup.setMode(null);
|
|
3320
3845
|
closePropertyPanel();
|
|
3321
3846
|
if (motion.enabled) setMotionEnabled(false);
|
|
3322
3847
|
rulersEnabled = !rulersEnabled;
|
|
@@ -3327,6 +3852,7 @@ function initComty(options = {}) {
|
|
|
3327
3852
|
syncInspectorTools();
|
|
3328
3853
|
});
|
|
3329
3854
|
const gridBtn = toolButton("Layout guides · G", '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 10h18M10 3v18"/>', () => {
|
|
3855
|
+
markup.setMode(null);
|
|
3330
3856
|
closePropertyPanel();
|
|
3331
3857
|
if (motion.enabled) setMotionEnabled(false);
|
|
3332
3858
|
gridPanelOpen = !gridPanelOpen;
|
|
@@ -3344,7 +3870,37 @@ function initComty(options = {}) {
|
|
|
3344
3870
|
}
|
|
3345
3871
|
const motionBtn = toolButton("Motion · M", '<circle cx="12" cy="14" r="8"/><path d="M9 2h6m-3 4V2m6 5 2-2m-8 5v4l3 2"/>', () => setMotionEnabled(!motion.enabled));
|
|
3346
3872
|
motionBtn.setAttribute("aria-expanded", "false");
|
|
3347
|
-
|
|
3873
|
+
const annotateBtn = toolButton("Annotate · D", '<path d="m16 3 5 5L8 21H3v-5ZM14 5l5 5"/>', () => toggleMarkup("annotate"));
|
|
3874
|
+
const screenshotBtn = toolButton("Capture area · S", '<path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3"/><rect x="7" y="8" width="10" height="8" rx="1"/><circle cx="12" cy="12" r="2"/>', () => toggleMarkup("capture"));
|
|
3875
|
+
const markup = createInspectMarkup({
|
|
3876
|
+
root, icon,
|
|
3877
|
+
onError: description => showNotice({ title: "Screenshot unavailable", description }),
|
|
3878
|
+
onChange() {
|
|
3879
|
+
annotateBtn.setAttribute("aria-pressed", String(markup.mode === "annotate"));
|
|
3880
|
+
annotateBtn.setAttribute("aria-expanded", String(markup.mode === "annotate"));
|
|
3881
|
+
screenshotBtn.setAttribute("aria-pressed", String(markup.mode === "capture"));
|
|
3882
|
+
screenshotBtn.setAttribute("aria-busy", String(markup.busy));
|
|
3883
|
+
positionToolbar();
|
|
3884
|
+
},
|
|
3885
|
+
capture: (region, layer) => captureViewportScreenshot({ region, annotation: () => ({
|
|
3886
|
+
elements: [overlayTools, root.querySelector('.comty-distance-layer'), layer].filter(node => node && !node.hidden),
|
|
3887
|
+
dispose() {},
|
|
3888
|
+
}) }),
|
|
3889
|
+
});
|
|
3890
|
+
for (const button of [annotateBtn]) {
|
|
3891
|
+
button.setAttribute("aria-controls", markup.panel.id);
|
|
3892
|
+
button.setAttribute("aria-expanded", "false");
|
|
3893
|
+
}
|
|
3894
|
+
function toggleMarkup(mode) {
|
|
3895
|
+
const next = markup.mode === mode ? null : mode;
|
|
3896
|
+
closePropertyPanel();
|
|
3897
|
+
if (motion.enabled) setMotionEnabled(false);
|
|
3898
|
+
gridPanelOpen = false; toolPanel.hidden = true; gridBtn.setAttribute("aria-expanded", "false");
|
|
3899
|
+
cancelRulerDrag();
|
|
3900
|
+
selectedElement = null; measuredRegion = null; inspectDrag = null; hideInspector();
|
|
3901
|
+
markup.setMode(next);
|
|
3902
|
+
}
|
|
3903
|
+
for (const button of [xrayBtn, rulersBtn, gridBtn, pipetteBtn, typographyBtn, motionBtn, annotateBtn, screenshotBtn]) inspectorTools.appendChild(button);
|
|
3348
3904
|
|
|
3349
3905
|
const motion = { enabled: false, speed: 1, paused: false, managed: new Map(), shadowRoots: [], nextRootScan: 0 };
|
|
3350
3906
|
const motionPanel = document.createElement('section');
|
|
@@ -3393,6 +3949,7 @@ function initComty(options = {}) {
|
|
|
3393
3949
|
motion.enabled = enabled; motionPanel.hidden = !enabled;
|
|
3394
3950
|
motionBtn.setAttribute('aria-pressed', String(enabled)); motionBtn.setAttribute('aria-expanded', String(enabled));
|
|
3395
3951
|
if (enabled) {
|
|
3952
|
+
markup.setMode(null);
|
|
3396
3953
|
closePropertyPanel();
|
|
3397
3954
|
rulersEnabled = false;
|
|
3398
3955
|
syncInspectorTools();
|
|
@@ -3651,6 +4208,7 @@ function initComty(options = {}) {
|
|
|
3651
4208
|
const visible = session.inspect;
|
|
3652
4209
|
measurements.setEnabled(visible && rulersEnabled && !motion.enabled);
|
|
3653
4210
|
overlayTools.hidden = !visible;
|
|
4211
|
+
markup.setVisible(visible);
|
|
3654
4212
|
xrayBtn.setAttribute("aria-pressed", String(visible && xrayEnabled));
|
|
3655
4213
|
rulersBtn.setAttribute("aria-pressed", String(visible && rulersEnabled));
|
|
3656
4214
|
gridBtn.setAttribute("aria-pressed", String(visible && layoutGuides.some(guide => guide.visible)));
|
|
@@ -3934,6 +4492,7 @@ function initComty(options = {}) {
|
|
|
3934
4492
|
}
|
|
3935
4493
|
|
|
3936
4494
|
function openPropertyPanel(kind, element, property, opener = null, sampledColor = null) {
|
|
4495
|
+
markup.setMode(null);
|
|
3937
4496
|
propertyPicking = null;
|
|
3938
4497
|
colorMagnifier.setEnabled(false);
|
|
3939
4498
|
delete document.documentElement.dataset.comtyPropertyPicking;
|
|
@@ -4114,6 +4673,50 @@ function initComty(options = {}) {
|
|
|
4114
4673
|
let toolbarMotionFrame = 0;
|
|
4115
4674
|
let toolbarAnimations = [];
|
|
4116
4675
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
4676
|
+
let workingIndicatorTimer = 0;
|
|
4677
|
+
function scheduleWorkingIndicators() {
|
|
4678
|
+
if (workingIndicatorTimer || reducedMotion.matches || document.hidden) return;
|
|
4679
|
+
workingIndicatorTimer = setTimeout(() => {
|
|
4680
|
+
workingIndicatorTimer = 0;
|
|
4681
|
+
if (!root.isConnected) return;
|
|
4682
|
+
const indicators = root.querySelectorAll('.comty-working-indicator');
|
|
4683
|
+
if (!indicators.length) return;
|
|
4684
|
+
for (const indicator of indicators) {
|
|
4685
|
+
const previous = Number(indicator.dataset.activeDots.split(',')[0]);
|
|
4686
|
+
const first = (previous + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
4687
|
+
const second = (first + 1 + Math.floor(Math.random() * 8)) % 9;
|
|
4688
|
+
indicator.dataset.activeDots = `${first},${second}`;
|
|
4689
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === first || index === second));
|
|
4690
|
+
}
|
|
4691
|
+
scheduleWorkingIndicators();
|
|
4692
|
+
}, 600);
|
|
4693
|
+
}
|
|
4694
|
+
function createWorkingIndicator() {
|
|
4695
|
+
const indicator = document.createElement('span');
|
|
4696
|
+
indicator.className = 'comty-working-indicator';
|
|
4697
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
4698
|
+
indicator.dataset.activeDots = '3,5';
|
|
4699
|
+
for (let index = 0; index < 9; index++) {
|
|
4700
|
+
const dot = document.createElement('i');
|
|
4701
|
+
dot.toggleAttribute('data-active', index === 3 || index === 5);
|
|
4702
|
+
indicator.appendChild(dot);
|
|
4703
|
+
}
|
|
4704
|
+
scheduleWorkingIndicators();
|
|
4705
|
+
return indicator;
|
|
4706
|
+
}
|
|
4707
|
+
function resetWorkingIndicators() {
|
|
4708
|
+
clearTimeout(workingIndicatorTimer);
|
|
4709
|
+
workingIndicatorTimer = 0;
|
|
4710
|
+
if (reducedMotion.matches) {
|
|
4711
|
+
for (const indicator of root.querySelectorAll('.comty-working-indicator')) {
|
|
4712
|
+
indicator.dataset.activeDots = '3,5';
|
|
4713
|
+
[...indicator.children].forEach((dot, index) => dot.toggleAttribute('data-active', index === 3 || index === 5));
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
if (root.querySelector('.comty-working-indicator')) scheduleWorkingIndicators();
|
|
4717
|
+
}
|
|
4718
|
+
reducedMotion.addEventListener('change', resetWorkingIndicators);
|
|
4719
|
+
document.addEventListener('visibilitychange', resetWorkingIndicators);
|
|
4117
4720
|
const clampToolbar = (value, size, viewport) => Math.max(8, Math.min(value, viewport - size - 8));
|
|
4118
4721
|
function rememberToolbarPosition(left, top) {
|
|
4119
4722
|
const width = toolbar.offsetWidth, height = toolbar.offsetHeight;
|
|
@@ -4147,7 +4750,7 @@ function initComty(options = {}) {
|
|
|
4147
4750
|
});
|
|
4148
4751
|
}
|
|
4149
4752
|
const rect = toolbar.getBoundingClientRect();
|
|
4150
|
-
for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel]) {
|
|
4753
|
+
for (const panel of [toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel]) {
|
|
4151
4754
|
if (panel.hidden || (panel === sidepanel && !sidepanelOpen)) continue;
|
|
4152
4755
|
const above = rect.top - 16;
|
|
4153
4756
|
const below = innerHeight - rect.bottom - 16;
|
|
@@ -4164,9 +4767,10 @@ function initComty(options = {}) {
|
|
|
4164
4767
|
panel.style.top = `${useAbove ? rect.top - panel.offsetHeight - 8 : rect.bottom + 8}px`;
|
|
4165
4768
|
panel.style.bottom = "auto";
|
|
4166
4769
|
}
|
|
4770
|
+
positionNotice();
|
|
4167
4771
|
}
|
|
4168
4772
|
const toolbarResize = new ResizeObserver(positionToolbar);
|
|
4169
|
-
[toolbar, toolPanel, propertyPanel, sidepanel, motionPanel].forEach(node => toolbarResize.observe(node));
|
|
4773
|
+
[toolbar, toolPanel, propertyPanel, sidepanel, motionPanel, markup.panel].forEach(node => toolbarResize.observe(node));
|
|
4170
4774
|
window.addEventListener("resize", positionToolbar);
|
|
4171
4775
|
|
|
4172
4776
|
toolbar.addEventListener("pointerdown", event => {
|
|
@@ -4226,7 +4830,7 @@ function initComty(options = {}) {
|
|
|
4226
4830
|
|
|
4227
4831
|
function prepareToolbarMotion() {
|
|
4228
4832
|
if (toolbarMotionFrame) return;
|
|
4229
|
-
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools]
|
|
4833
|
+
const nodes = [toolbarSurface, dragHandle, primaryTools, commentTools, inspectorTools];
|
|
4230
4834
|
// Move each group's background with its icons, including when interrupted.
|
|
4231
4835
|
const before = new Map(nodes.filter(node => !node.hidden).map(node => [node, {
|
|
4232
4836
|
rect: node.getBoundingClientRect(), opacity: getComputedStyle(node).opacity,
|
|
@@ -4450,7 +5054,8 @@ function initComty(options = {}) {
|
|
|
4450
5054
|
selectedElement = null;
|
|
4451
5055
|
sessionEntry = null;
|
|
4452
5056
|
session.mode = "off";
|
|
4453
|
-
|
|
5057
|
+
syncPlacementCursor();
|
|
5058
|
+
document.documentElement.classList.remove("comty-comments-active", "comty-inspect-annotations");
|
|
4454
5059
|
hideInspector();
|
|
4455
5060
|
setSidepanelOpen(false);
|
|
4456
5061
|
closeBubbles();
|
|
@@ -4464,25 +5069,29 @@ function initComty(options = {}) {
|
|
|
4464
5069
|
if (mode === "off") return;
|
|
4465
5070
|
if (mode === "inspect") {
|
|
4466
5071
|
session.mode = "inspect";
|
|
5072
|
+
document.documentElement.classList.add("comty-inspect-annotations");
|
|
4467
5073
|
syncToolbar();
|
|
5074
|
+
renderCachedComments();
|
|
4468
5075
|
ensureLiveTracking();
|
|
4469
5076
|
return;
|
|
4470
5077
|
}
|
|
4471
5078
|
|
|
4472
|
-
if (session.edit && mode === "browse") {
|
|
5079
|
+
if ((session.edit && mode === "browse") || (session.agent && mode === "edit")) {
|
|
4473
5080
|
closeBubbles();
|
|
4474
5081
|
}
|
|
4475
5082
|
const hadComments = session.comments;
|
|
4476
5083
|
session.mode = mode;
|
|
4477
5084
|
sessionEntry = entry;
|
|
5085
|
+
document.documentElement.classList.remove("comty-inspect-annotations");
|
|
4478
5086
|
document.documentElement.classList.toggle("comty-comments-active", session.comments);
|
|
4479
|
-
|
|
5087
|
+
syncPlacementCursor();
|
|
4480
5088
|
if (session.edit) {
|
|
4481
5089
|
setMotionEnabled(false);
|
|
4482
5090
|
selectedElement = null;
|
|
4483
5091
|
} else {
|
|
4484
5092
|
hideInspector();
|
|
4485
5093
|
}
|
|
5094
|
+
if (session.agent && sidepanelOpen) setSidepanelOpen(false);
|
|
4486
5095
|
syncToolbar();
|
|
4487
5096
|
ensureLiveTracking();
|
|
4488
5097
|
if (renderComments || !hadComments) renderCachedComments();
|
|
@@ -4495,6 +5104,10 @@ function initComty(options = {}) {
|
|
|
4495
5104
|
}
|
|
4496
5105
|
|
|
4497
5106
|
function showAuthPrompt(forbidden = false, onReady = startEditSession) {
|
|
5107
|
+
if (localOnly) {
|
|
5108
|
+
showNotice({ title: "Comments are not configured", description: "Connect this site to a Comty project to leave shared comments." });
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
4498
5111
|
const guestAllowed = allowGuestComments;
|
|
4499
5112
|
document.querySelector(".comty-auth-backdrop")?.remove();
|
|
4500
5113
|
const opener = document.activeElement;
|
|
@@ -4616,14 +5229,17 @@ function initComty(options = {}) {
|
|
|
4616
5229
|
}
|
|
4617
5230
|
|
|
4618
5231
|
function syncToolbar() {
|
|
4619
|
-
if (commentTools.hidden !== session.inspect || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
5232
|
+
if (commentTools.hidden !== (session.inspect || session.agent) || inspectorTools.hidden !== !session.inspect) prepareToolbarMotion();
|
|
4620
5233
|
root.classList.toggle("comty-active", session.comments || session.edit || sidepanelOpen || session.inspect);
|
|
4621
5234
|
browseBtn.setAttribute("aria-pressed", String(!session.edit && !session.inspect));
|
|
4622
5235
|
inspectorBtn.setAttribute("aria-pressed", String(session.inspect));
|
|
4623
|
-
btn.setAttribute("aria-pressed", String(session.edit));
|
|
4624
|
-
|
|
5236
|
+
btn.setAttribute("aria-pressed", String(session.edit && !session.agent));
|
|
5237
|
+
agentBtn?.setAttribute("aria-pressed", String(session.edit && session.agent));
|
|
5238
|
+
commentTools.hidden = session.inspect || session.agent;
|
|
4625
5239
|
inspectorTools.hidden = !session.inspect;
|
|
4626
5240
|
syncInspectorTools();
|
|
5241
|
+
renderAgentPanel();
|
|
5242
|
+
positionNotice();
|
|
4627
5243
|
}
|
|
4628
5244
|
|
|
4629
5245
|
const inspectionLayer = document.createElement("div");
|
|
@@ -4936,7 +5552,7 @@ function initComty(options = {}) {
|
|
|
4936
5552
|
});
|
|
4937
5553
|
return;
|
|
4938
5554
|
}
|
|
4939
|
-
if (propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
|
|
5555
|
+
if (markup.mode || propertyPicking || motion.enabled || (session.inspect && rulersEnabled)) {
|
|
4940
5556
|
if (inspectionLayer.childNodes.length) hideInspector();
|
|
4941
5557
|
return;
|
|
4942
5558
|
}
|
|
@@ -5080,10 +5696,20 @@ function initComty(options = {}) {
|
|
|
5080
5696
|
|
|
5081
5697
|
function startBrowseSession() {
|
|
5082
5698
|
setSession("browse", { renderComments: true });
|
|
5699
|
+
void loadComments({ force: true });
|
|
5083
5700
|
}
|
|
5084
5701
|
|
|
5085
5702
|
function startEditSession() {
|
|
5086
5703
|
setSession("edit", { renderComments: true });
|
|
5704
|
+
void loadComments({ force: true });
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
function startAgentSession() {
|
|
5708
|
+
if (!isLocalSite) return;
|
|
5709
|
+
if (session.agent) return;
|
|
5710
|
+
closeBubbles();
|
|
5711
|
+
setSession("agent", { renderComments: true });
|
|
5712
|
+
void loadComments({ force: true });
|
|
5087
5713
|
}
|
|
5088
5714
|
|
|
5089
5715
|
function toggleSidepanel() {
|
|
@@ -5109,7 +5735,7 @@ function initComty(options = {}) {
|
|
|
5109
5735
|
showAuthPrompt();
|
|
5110
5736
|
return;
|
|
5111
5737
|
}
|
|
5112
|
-
if (session.inspect || !isSessionActive()) {
|
|
5738
|
+
if (session.agent || session.inspect || !isSessionActive()) {
|
|
5113
5739
|
startEditSession();
|
|
5114
5740
|
return;
|
|
5115
5741
|
}
|
|
@@ -5397,6 +6023,24 @@ function initComty(options = {}) {
|
|
|
5397
6023
|
ancestors,
|
|
5398
6024
|
layer: layer ? getElementFingerprint(layer) : null,
|
|
5399
6025
|
};
|
|
6026
|
+
// React exposes this in development builds. Keep it as a hint: the coding
|
|
6027
|
+
// agent must confirm the component and variant in source before editing.
|
|
6028
|
+
const fiberKey = Object.keys(el).find(key => key.startsWith('__reactFiber$'));
|
|
6029
|
+
let fiber = fiberKey ? el[fiberKey] : null;
|
|
6030
|
+
const components = [];
|
|
6031
|
+
while (fiber && components.length < 8) {
|
|
6032
|
+
const component = fiber.type;
|
|
6033
|
+
const name = component?.displayName || component?.name;
|
|
6034
|
+
if (name && typeof name === 'string') {
|
|
6035
|
+
const props = fiber.memoizedProps || {};
|
|
6036
|
+
const variants = Object.fromEntries(['variant', 'size', 'tone', 'appearance'].filter(key =>
|
|
6037
|
+
['string', 'number', 'boolean'].includes(typeof props[key])).map(key => [key, props[key]]));
|
|
6038
|
+
const source = fiber._debugSource;
|
|
6039
|
+
components.push({ name, variants, ...(source?.fileName ? { file: source.fileName, line: source.lineNumber } : {}) });
|
|
6040
|
+
}
|
|
6041
|
+
fiber = fiber.return;
|
|
6042
|
+
}
|
|
6043
|
+
if (components.length) context.components = components;
|
|
5400
6044
|
context.candidateCount = [...document.querySelectorAll(context.target.tag)]
|
|
5401
6045
|
.filter(candidate => !isComtyNode(candidate) && matchesAnchorContext(candidate, context)).length;
|
|
5402
6046
|
return context;
|
|
@@ -5425,6 +6069,11 @@ function initComty(options = {}) {
|
|
|
5425
6069
|
};
|
|
5426
6070
|
}
|
|
5427
6071
|
|
|
6072
|
+
function getPagePath() {
|
|
6073
|
+
const { pathname, hash } = window.location;
|
|
6074
|
+
return pathname + (hash.startsWith("#/") || hash.startsWith("#!/") ? hash : "");
|
|
6075
|
+
}
|
|
6076
|
+
|
|
5428
6077
|
function resolveBySelector(selector) {
|
|
5429
6078
|
if (!selector) return null;
|
|
5430
6079
|
try {
|
|
@@ -5446,17 +6095,9 @@ function initComty(options = {}) {
|
|
|
5446
6095
|
}
|
|
5447
6096
|
const context = parseAnchorContext(comment);
|
|
5448
6097
|
const exact = resolveBySelector(comment.selector || "body");
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
if (count !== context.candidateCount) {
|
|
5453
|
-
commentTargets.set(key, { element: null, signature, revision: anchorRevision });
|
|
5454
|
-
return null;
|
|
5455
|
-
}
|
|
5456
|
-
}
|
|
5457
|
-
let element = exact && matchesAnchorContext(exact, context) ? getAnchorElement(exact) : null;
|
|
5458
|
-
if (!element) element = resolveByAnchorContext(context);
|
|
5459
|
-
commentTargets.set(key, { element, signature, revision: anchorRevision });
|
|
6098
|
+
const resolution = resolveAnchor(context, exact);
|
|
6099
|
+
const element = resolution.element ? getAnchorElement(resolution.element) : null;
|
|
6100
|
+
commentTargets.set(key, { ...resolution, element, signature, revision: anchorRevision });
|
|
5460
6101
|
return element;
|
|
5461
6102
|
}
|
|
5462
6103
|
|
|
@@ -5527,9 +6168,14 @@ function initComty(options = {}) {
|
|
|
5527
6168
|
return { deviceType: "desktop", deviceLabel: "Desktop" };
|
|
5528
6169
|
}
|
|
5529
6170
|
|
|
6171
|
+
function syncPlacementCursor() {
|
|
6172
|
+
document.documentElement.classList.toggle("comty-comment-placement", session.edit && !draftAnchor);
|
|
6173
|
+
}
|
|
6174
|
+
|
|
5530
6175
|
function closeBubbles() {
|
|
5531
6176
|
const hadDraft = Boolean(draftAnchor);
|
|
5532
6177
|
draftAnchor = null;
|
|
6178
|
+
syncPlacementCursor();
|
|
5533
6179
|
activeHoverCommentIds.clear();
|
|
5534
6180
|
if (!session.edit || hadDraft) hideInspector();
|
|
5535
6181
|
document
|
|
@@ -5574,7 +6220,7 @@ function initComty(options = {}) {
|
|
|
5574
6220
|
pending: commentId.startsWith("pending-"),
|
|
5575
6221
|
project_id: projectId,
|
|
5576
6222
|
domain: payload.host || window.location.host,
|
|
5577
|
-
page_path: payload.pagePath ||
|
|
6223
|
+
page_path: payload.pagePath || getPagePath(),
|
|
5578
6224
|
selector: payload.selector || "body",
|
|
5579
6225
|
rel_x: payload.relX,
|
|
5580
6226
|
rel_y: payload.relY,
|
|
@@ -5599,7 +6245,7 @@ function initComty(options = {}) {
|
|
|
5599
6245
|
replies: [],
|
|
5600
6246
|
};
|
|
5601
6247
|
|
|
5602
|
-
cachedPagePath =
|
|
6248
|
+
cachedPagePath = getPagePath();
|
|
5603
6249
|
cachedComments = [
|
|
5604
6250
|
...cachedComments.filter(
|
|
5605
6251
|
(cached) => getCommentId(cached) !== commentId,
|
|
@@ -5612,6 +6258,7 @@ function initComty(options = {}) {
|
|
|
5612
6258
|
}
|
|
5613
6259
|
|
|
5614
6260
|
function getCommentAuthorName(comment) {
|
|
6261
|
+
if (comment?.author_kind === 'agent') return 'Agent';
|
|
5615
6262
|
return comment?.author_name || comment?.name || "Anonymous";
|
|
5616
6263
|
}
|
|
5617
6264
|
|
|
@@ -5633,6 +6280,12 @@ function initComty(options = {}) {
|
|
|
5633
6280
|
function createCommentAvatar(comment, extraClass = "") {
|
|
5634
6281
|
const avatar = document.createElement("span");
|
|
5635
6282
|
avatar.className = `comty-comment-avatar${extraClass ? ` ${extraClass}` : ""}`;
|
|
6283
|
+
if (comment?.author_kind === 'agent') {
|
|
6284
|
+
avatar.classList.add('comty-agent-avatar');
|
|
6285
|
+
avatar.setAttribute('aria-label', 'Agent');
|
|
6286
|
+
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>';
|
|
6287
|
+
return avatar;
|
|
6288
|
+
}
|
|
5636
6289
|
const authorName = getCommentAuthorName(comment);
|
|
5637
6290
|
const avatarUrl = getCommentAuthorAvatarUrl(comment);
|
|
5638
6291
|
|
|
@@ -5640,6 +6293,7 @@ function initComty(options = {}) {
|
|
|
5640
6293
|
const image = document.createElement("img");
|
|
5641
6294
|
image.src = avatarUrl;
|
|
5642
6295
|
image.alt = "";
|
|
6296
|
+
image.draggable = false;
|
|
5643
6297
|
image.onerror = () => {
|
|
5644
6298
|
image.remove();
|
|
5645
6299
|
avatar.innerText = authorName.charAt(0).toUpperCase();
|
|
@@ -5793,6 +6447,7 @@ function initComty(options = {}) {
|
|
|
5793
6447
|
function createCommentPin(comment, x, y) {
|
|
5794
6448
|
const pin = document.createElement("button");
|
|
5795
6449
|
pin.className = "comty-pin";
|
|
6450
|
+
pin.toggleAttribute('data-agent-task', Boolean(comment.local_agent));
|
|
5796
6451
|
pin.type = "button";
|
|
5797
6452
|
pin.style.left = `${x}px`;
|
|
5798
6453
|
pin.style.top = `${y}px`;
|
|
@@ -5810,10 +6465,52 @@ function initComty(options = {}) {
|
|
|
5810
6465
|
const thread = document.createElement("div");
|
|
5811
6466
|
thread.className = "comty-thread";
|
|
5812
6467
|
thread.appendChild(createThreadItem(comment, { showMore: false, showLink: false }).item);
|
|
6468
|
+
if (comment.local_agent) {
|
|
6469
|
+
thread.appendChild(createThreadItem({ author_kind: 'agent', content: 'Modification failed. Open to retry or dismiss.' }, { showMore: false }).item);
|
|
6470
|
+
}
|
|
5813
6471
|
bubble.appendChild(thread);
|
|
5814
6472
|
return bubble;
|
|
5815
6473
|
}
|
|
5816
6474
|
|
|
6475
|
+
function openAgentTask(task, x, y) {
|
|
6476
|
+
closeBubbles();
|
|
6477
|
+
const bubble = document.createElement('div');
|
|
6478
|
+
bubble.className = 'comty-bubble comty-agent-task-thread';
|
|
6479
|
+
bubble.dataset.commentId = task.id;
|
|
6480
|
+
bubble.setAttribute('role', 'dialog');
|
|
6481
|
+
bubble.setAttribute('aria-label', 'Failed modification');
|
|
6482
|
+
const header = document.createElement('div'); header.className = 'comty-bubble-header';
|
|
6483
|
+
const title = document.createElement('strong'); title.textContent = 'Modification failed';
|
|
6484
|
+
const close = document.createElement('button'); close.type = 'button'; close.className = 'comty-icon-button';
|
|
6485
|
+
close.setAttribute('aria-label', 'Close failed modification');
|
|
6486
|
+
close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
6487
|
+
close.onclick = () => closeBubbles();
|
|
6488
|
+
header.append(title, close);
|
|
6489
|
+
const thread = document.createElement('div'); thread.className = 'comty-thread';
|
|
6490
|
+
const list = document.createElement('div'); list.className = 'comty-thread-list';
|
|
6491
|
+
const issue = task.error ? describeAgentError(task.error) : null;
|
|
6492
|
+
list.append(
|
|
6493
|
+
createThreadItem(task, { showMore: false }).item,
|
|
6494
|
+
createThreadItem({ author_kind: 'agent', content: task.result?.summary || issue?.message || issue?.description || 'The agent could not complete this request.' }, { showMore: false }).item,
|
|
6495
|
+
);
|
|
6496
|
+
thread.appendChild(list);
|
|
6497
|
+
const actions = document.createElement('div'); actions.className = 'comty-agent-task-actions';
|
|
6498
|
+
for (const action of ['retry', 'dismiss']) {
|
|
6499
|
+
const button = document.createElement('button'); button.type = 'button'; button.className = 'comty-agent-action';
|
|
6500
|
+
button.textContent = action === 'retry' ? 'Retry' : 'Dismiss';
|
|
6501
|
+
button.onclick = async () => {
|
|
6502
|
+
for (const control of actions.children) control.disabled = true;
|
|
6503
|
+
try { await runAgentAction(`tasks/${task.id}/${action}`); }
|
|
6504
|
+
catch (error) { showAgentBridgeError(error.message); }
|
|
6505
|
+
finally { for (const control of actions.children) control.disabled = false; }
|
|
6506
|
+
};
|
|
6507
|
+
actions.appendChild(button);
|
|
6508
|
+
}
|
|
6509
|
+
bubble.append(header, thread, actions);
|
|
6510
|
+
root.appendChild(bubble);
|
|
6511
|
+
positionCommentBubble(bubble, x, y);
|
|
6512
|
+
}
|
|
6513
|
+
|
|
5817
6514
|
function mountCaptureAnnotation(comment, { x, y }) {
|
|
5818
6515
|
const host = document.createElement("div");
|
|
5819
6516
|
host.dataset.comtyCaptureExclude = "";
|
|
@@ -5864,6 +6561,7 @@ function initComty(options = {}) {
|
|
|
5864
6561
|
payload,
|
|
5865
6562
|
preview: { rect: target.getBoundingClientRect(), rows: getInspectRows(target), selector: getSelector(target), spacing: getSpacingPreview(target) },
|
|
5866
6563
|
};
|
|
6564
|
+
syncPlacementCursor();
|
|
5867
6565
|
ensureLiveTracking();
|
|
5868
6566
|
|
|
5869
6567
|
const pin = document.createElement("div");
|
|
@@ -5881,10 +6579,21 @@ function initComty(options = {}) {
|
|
|
5881
6579
|
|
|
5882
6580
|
const textarea = document.createElement("textarea");
|
|
5883
6581
|
textarea.className = "comty-create-textarea";
|
|
5884
|
-
textarea.placeholder = "Add a comment";
|
|
6582
|
+
textarea.placeholder = payload.kind === "agent" ? "Ask a question or describe a change" : "Add a comment";
|
|
5885
6583
|
textarea.setAttribute("aria-label", textarea.placeholder);
|
|
5886
6584
|
textarea.rows = 1;
|
|
5887
6585
|
|
|
6586
|
+
const scopePicker = document.createElement('label');
|
|
6587
|
+
scopePicker.className = 'comty-agent-scope';
|
|
6588
|
+
if (payload.kind === 'agent') {
|
|
6589
|
+
const input = document.createElement('input');
|
|
6590
|
+
input.type = 'checkbox';
|
|
6591
|
+
input.setAttribute('aria-label', 'Apply to all instances of this variant');
|
|
6592
|
+
const track = document.createElement('span'); track.className = 'comty-agent-scope-track'; track.setAttribute('aria-hidden', 'true');
|
|
6593
|
+
scopePicker.title = 'Apply to all instances of this variant';
|
|
6594
|
+
scopePicker.append(input, track, document.createTextNode('Apply globally'));
|
|
6595
|
+
}
|
|
6596
|
+
|
|
5888
6597
|
const actions = document.createElement("div");
|
|
5889
6598
|
actions.className = "comty-create-actions";
|
|
5890
6599
|
|
|
@@ -5938,7 +6647,7 @@ function initComty(options = {}) {
|
|
|
5938
6647
|
const submitBtn = document.createElement("button");
|
|
5939
6648
|
submitBtn.type = "button";
|
|
5940
6649
|
submitBtn.className = "comty-btn-submit";
|
|
5941
|
-
submitBtn.setAttribute("aria-label", "Post comment");
|
|
6650
|
+
submitBtn.setAttribute("aria-label", payload.kind === "agent" ? "Send agent task" : "Post comment");
|
|
5942
6651
|
submitBtn.disabled = true;
|
|
5943
6652
|
submitBtn.innerHTML = `
|
|
5944
6653
|
<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">
|
|
@@ -5955,49 +6664,63 @@ function initComty(options = {}) {
|
|
|
5955
6664
|
textarea.disabled = true;
|
|
5956
6665
|
linkBtn.disabled = true;
|
|
5957
6666
|
|
|
6667
|
+
if (payload.kind === "agent" && !agent) {
|
|
6668
|
+
showAgentBridgeError(); submitBtn.disabled = false; textarea.disabled = false; return;
|
|
6669
|
+
}
|
|
5958
6670
|
payload.content = content;
|
|
6671
|
+
if (payload.kind === 'agent') payload.scope = scopePicker.querySelector('input').checked ? 'variant' : 'element';
|
|
5959
6672
|
payload.linkUrl = attachedLink || null;
|
|
5960
6673
|
textarea.textContent = content;
|
|
5961
6674
|
|
|
5962
|
-
const
|
|
5963
|
-
const
|
|
5964
|
-
const
|
|
5965
|
-
|
|
6675
|
+
const isAgentTask = payload.kind === "agent";
|
|
6676
|
+
const context = isAgentTask ? null : captureContext();
|
|
6677
|
+
const point = isAgentTask ? null : getPinPoint(pin);
|
|
6678
|
+
const pendingId = isAgentTask ? null : `pending-${crypto.randomUUID()}`;
|
|
5966
6679
|
bubble.remove();
|
|
5967
6680
|
pin.remove();
|
|
5968
6681
|
if (draftAnchor === editorDraft) draftAnchor = null;
|
|
6682
|
+
syncPlacementCursor();
|
|
5969
6683
|
hideInspector();
|
|
5970
|
-
addLocalComment(pendingId, payload);
|
|
6684
|
+
if (!isAgentTask) addLocalComment(pendingId, payload);
|
|
5971
6685
|
let post;
|
|
5972
6686
|
try {
|
|
5973
|
-
post = await fetch(`${apiUrl}/api/comments`, {
|
|
6687
|
+
post = await fetch(payload.kind === "agent" ? `${agentBridgeUrl}/tasks` : `${apiUrl}/api/comments`, {
|
|
5974
6688
|
method: "POST",
|
|
5975
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
5976
|
-
body: JSON.stringify(payload),
|
|
6689
|
+
headers: payload.kind === "agent" ? agentHeaders() : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
6690
|
+
body: JSON.stringify(payload.kind === "agent" ? { ...payload, projectKey } : payload),
|
|
5977
6691
|
});
|
|
5978
|
-
if (!post.ok) throw new Error(`
|
|
6692
|
+
if (!post.ok) throw new Error(`Save failed (${post.status})`);
|
|
5979
6693
|
const data = await post.json();
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
if (
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
6694
|
+
const id = isAgentTask ? data?.task?.id : data?.id;
|
|
6695
|
+
if (!id) throw new Error("Save returned no ID");
|
|
6696
|
+
if (isAgentTask) {
|
|
6697
|
+
requestedAgentTasks.add(id);
|
|
6698
|
+
void loadComments({ force: true });
|
|
6699
|
+
} else {
|
|
6700
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
6701
|
+
if (context.url === location.href) {
|
|
6702
|
+
const comment = addLocalComment(String(id), payload);
|
|
6703
|
+
void uploadCommentScreenshot(comment, context, point);
|
|
6704
|
+
} else void loadComments({ force: true });
|
|
6705
|
+
}
|
|
5987
6706
|
} catch (error) {
|
|
5988
6707
|
console.error("Comty: save failed", error);
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
6708
|
+
if (isAgentTask) showAgentBridgeError(error.message);
|
|
6709
|
+
else {
|
|
6710
|
+
cachedComments = cachedComments.filter(item => String(item.id) !== pendingId);
|
|
6711
|
+
renderSidepanelComments();
|
|
6712
|
+
void renderComments(cachedComments, { retry: false });
|
|
6713
|
+
}
|
|
5992
6714
|
// Keep the original editor and its text available for retry, including attachments.
|
|
5993
6715
|
if (!draftAnchor) draftAnchor = editorDraft;
|
|
6716
|
+
syncPlacementCursor();
|
|
5994
6717
|
root.append(pin, bubble);
|
|
5995
6718
|
submitBtn.disabled = false;
|
|
5996
6719
|
textarea.disabled = false;
|
|
5997
6720
|
linkBtn.disabled = false;
|
|
5998
6721
|
const errorMessage = document.createElement("p");
|
|
5999
6722
|
errorMessage.setAttribute("role", "alert");
|
|
6000
|
-
errorMessage.textContent = "Comment not saved. Your draft is ready to retry
|
|
6723
|
+
errorMessage.textContent = `${isAgentTask ? "Task" : "Comment"} not saved. Your draft is ready to retry.`;
|
|
6001
6724
|
bubble.querySelector('[role="alert"]')?.remove();
|
|
6002
6725
|
bubble.append(errorMessage);
|
|
6003
6726
|
if (bubble.isConnected) textarea.focus();
|
|
@@ -6086,7 +6809,8 @@ function initComty(options = {}) {
|
|
|
6086
6809
|
renderInspector();
|
|
6087
6810
|
};
|
|
6088
6811
|
|
|
6089
|
-
actions.append(
|
|
6812
|
+
if (payload.kind === 'agent') actions.append(scopePicker, submitBtn);
|
|
6813
|
+
else actions.append(linkBtn, submitBtn, linkEditor);
|
|
6090
6814
|
|
|
6091
6815
|
bubble.appendChild(textarea);
|
|
6092
6816
|
bubble.appendChild(attachment);
|
|
@@ -6099,6 +6823,249 @@ function initComty(options = {}) {
|
|
|
6099
6823
|
setTimeout(() => textarea.focus(), 10);
|
|
6100
6824
|
}
|
|
6101
6825
|
|
|
6826
|
+
function agentHeaders() { return { "Content-Type": "application/json", "X-Comty-Token": agent?.token || "" }; }
|
|
6827
|
+
|
|
6828
|
+
function showAgentBridgeError(message = "Start your app with comty dev to use the local agent. Check the terminal if it is already running.") {
|
|
6829
|
+
const issue = describeAgentError(message);
|
|
6830
|
+
showNotice({ title: 'Modification failed', description: issue.title === 'Task could not finish' ? 'The agent could not complete this request.' : issue.title + '.' });
|
|
6831
|
+
}
|
|
6832
|
+
|
|
6833
|
+
function positionNotice() {
|
|
6834
|
+
const toast = root.querySelector(".comty-agent-task-toast");
|
|
6835
|
+
if (!toast) return;
|
|
6836
|
+
toast.style.removeProperty("left");
|
|
6837
|
+
toast.style.removeProperty("top");
|
|
6838
|
+
toast.toggleAttribute("data-toolbar", isSessionActive());
|
|
6839
|
+
if (!isSessionActive()) return;
|
|
6840
|
+
const rect = toolbar.getBoundingClientRect();
|
|
6841
|
+
const above = rect.top - toast.offsetHeight - 8;
|
|
6842
|
+
toast.style.left = `${Math.max(8, Math.min(rect.right - toast.offsetWidth, innerWidth - toast.offsetWidth - 8))}px`;
|
|
6843
|
+
toast.style.top = `${Math.max(8, Math.min(above >= 8 ? above : rect.bottom + 8, innerHeight - toast.offsetHeight - 8))}px`;
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
function showNotice(issue) {
|
|
6847
|
+
clearAgentToast();
|
|
6848
|
+
const toast = document.createElement("div");
|
|
6849
|
+
toast.className = "comty-agent-task-toast";
|
|
6850
|
+
toast.setAttribute("role", "alert");
|
|
6851
|
+
const title = document.createElement("strong"); title.textContent = issue.title;
|
|
6852
|
+
const description = document.createElement("p"); description.textContent = issue.description;
|
|
6853
|
+
const close = document.createElement("button"); close.type = "button";
|
|
6854
|
+
close.setAttribute("aria-label", "Dismiss notification");
|
|
6855
|
+
close.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
6856
|
+
toast.append(title, description, close);
|
|
6857
|
+
let timer, leaving = false;
|
|
6858
|
+
const dismiss = () => {
|
|
6859
|
+
if (leaving) return;
|
|
6860
|
+
leaving = true;
|
|
6861
|
+
clearTimeout(timer);
|
|
6862
|
+
toast.dataset.leaving = "";
|
|
6863
|
+
timer = setTimeout(() => toast.remove(), reducedMotion.matches ? 0 : 200);
|
|
6864
|
+
};
|
|
6865
|
+
const schedule = () => { if (!leaving) { clearTimeout(timer); timer = setTimeout(dismiss, 6000); } };
|
|
6866
|
+
const pause = () => { if (!leaving) clearTimeout(timer); };
|
|
6867
|
+
clearAgentToast = () => { clearTimeout(timer); toast.remove(); };
|
|
6868
|
+
close.onclick = dismiss;
|
|
6869
|
+
toast.onmouseenter = pause;
|
|
6870
|
+
toast.onmouseleave = schedule;
|
|
6871
|
+
toast.onfocusin = pause;
|
|
6872
|
+
toast.onfocusout = schedule;
|
|
6873
|
+
root.appendChild(toast);
|
|
6874
|
+
positionNotice();
|
|
6875
|
+
schedule();
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
function describeAgentError(error) {
|
|
6879
|
+
const raw = typeof error === 'string' ? error : JSON.stringify(error, null, 2) || 'Unknown error';
|
|
6880
|
+
let value = error;
|
|
6881
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
6882
|
+
if (typeof value === 'string') {
|
|
6883
|
+
try { value = JSON.parse(value); continue; } catch {}
|
|
6884
|
+
const start = value.indexOf('{'), end = value.lastIndexOf('}');
|
|
6885
|
+
if (start >= 0 && end > start) { try { value = JSON.parse(value.slice(start, end + 1)); continue; } catch {} }
|
|
6886
|
+
break;
|
|
6887
|
+
}
|
|
6888
|
+
if (!value || typeof value !== 'object') break;
|
|
6889
|
+
value = value.error || value.message || value.cause || value.stderr || value;
|
|
6890
|
+
}
|
|
6891
|
+
const message = typeof value === 'string' ? value : '';
|
|
6892
|
+
const source = raw + ' ' + message;
|
|
6893
|
+
const agentName = /codex/i.test(source) ? 'Codex' : /claude/i.test(source) ? 'Claude Code' : /cursor/i.test(source) ? 'Cursor' : 'your coding agent';
|
|
6894
|
+
let title, description;
|
|
6895
|
+
if (/requires a newer version|upgrade.*latest|outdated|unsupported.*version/i.test(source)) {
|
|
6896
|
+
title = 'Agent update required'; description = `Update ${agentName}, restart your local development session, then retry this task.`;
|
|
6897
|
+
} else if (/insufficient_quota|quota exceeded|credit balance|billing|usage limit/i.test(source)) {
|
|
6898
|
+
title = 'Usage limit reached'; description = 'Check your agent account’s usage allowance or billing, then retry when capacity is available.';
|
|
6899
|
+
} else if (/rate.?limit|too many requests|\b429\b/i.test(source)) {
|
|
6900
|
+
title = 'Too many requests'; description = 'The agent is temporarily rate limited. Wait a moment before retrying.';
|
|
6901
|
+
} else if (/unauthorized|unauthenticated|authentication|not logged|sign.?in|log.?in|invalid.api.key|\b401\b/i.test(source)) {
|
|
6902
|
+
title = 'Sign in to your agent'; description = `Open ${agentName} and sign in again, then retry this task.`;
|
|
6903
|
+
} else if (/model.*(?:not found|does not exist|unavailable|not supported)|unsupported model|model_not_found/i.test(source)) {
|
|
6904
|
+
title = 'Model unavailable'; description = 'The local agent rejected this model. Check its CLI version and signed-in account, then retry.';
|
|
6905
|
+
} else if (/disconnected|ECONNREFUSED|ENOTFOUND|network|failed to fetch|development server stopped/i.test(source)) {
|
|
6906
|
+
title = 'Connection lost'; description = 'Check that your local development server and coding agent are running, then reload this page.';
|
|
6907
|
+
} else if (/timed? ?out|timeout|exceeded.*minutes/i.test(source)) {
|
|
6908
|
+
title = 'Task timed out'; description = 'Review any changes already made, then retry with a smaller, more specific request.';
|
|
6909
|
+
} else if (/permission|denied|forbidden|\b403\b/i.test(source)) {
|
|
6910
|
+
title = 'Permission needed'; description = 'Check the agent’s access to this project and any pending approval before retrying.';
|
|
6911
|
+
} else if (/ENOENT|command not found|not installed|executable.*not found/i.test(source)) {
|
|
6912
|
+
title = 'Agent not found'; description = `Install or configure ${agentName} in your local development environment, then restart and retry.`;
|
|
6913
|
+
} else if (/completion was not verified/i.test(source)) {
|
|
6914
|
+
title = 'Result needs checking'; description = 'The agent could not confirm completion. Review the changes before retrying.';
|
|
6915
|
+
} else {
|
|
6916
|
+
title = 'Task could not finish';
|
|
6917
|
+
description = message && !/[{}]|\[object Object\]/.test(message) ? message.slice(0, 160) + (message.length > 160 ? '…' : '') : 'The agent reported an unexpected error. Review the technical details and any changes before retrying.';
|
|
6918
|
+
}
|
|
6919
|
+
return { title, description, raw, message };
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
async function runAgentAction(path, input = {}) {
|
|
6923
|
+
const response = await fetch(`${agentBridgeUrl}/${path}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(input) });
|
|
6924
|
+
if (!response.ok) {
|
|
6925
|
+
const body = await response.json().catch(() => null);
|
|
6926
|
+
throw new Error(typeof body?.error === 'string' ? body.error : JSON.stringify(body?.error || { status: response.status, message: 'Request failed' }));
|
|
6927
|
+
}
|
|
6928
|
+
if (path.endsWith('/retry')) requestedAgentTasks.add(path.split('/')[1]);
|
|
6929
|
+
await loadComments({ force: true });
|
|
6930
|
+
}
|
|
6931
|
+
|
|
6932
|
+
function renderAgentPanel() {
|
|
6933
|
+
if (!agent) return;
|
|
6934
|
+
const visibleTasks = [];
|
|
6935
|
+
const latestResult = [...localTasks].reverse().find(task => ['done', 'in_review'].includes(task.status) && (!task.thread_comment_id || task.status === 'in_review'));
|
|
6936
|
+
if (latestResult) visibleTasks.push(latestResult);
|
|
6937
|
+
if (!visibleTasks.length && !localApprovals.length && !agentActionError) agentPanelOpen = false;
|
|
6938
|
+
let panel = root.querySelector(".comty-agent-panel");
|
|
6939
|
+
if (!panel && !agentPanelOpen) return;
|
|
6940
|
+
if (!panel) {
|
|
6941
|
+
panel = document.createElement("section");
|
|
6942
|
+
panel.className = "comty-agent-panel";
|
|
6943
|
+
panel.setAttribute("aria-label", "Local agent tasks");
|
|
6944
|
+
panel.hidden = true;
|
|
6945
|
+
// Keep the task card above inspection overlays and below the toolbar.
|
|
6946
|
+
root.insertBefore(panel, toolbar);
|
|
6947
|
+
}
|
|
6948
|
+
const open = Boolean(agent) && agentPanelOpen;
|
|
6949
|
+
if (open) {
|
|
6950
|
+
clearAgentToast();
|
|
6951
|
+
if (panel.hidden) {
|
|
6952
|
+
const rect = toolbar.getBoundingClientRect();
|
|
6953
|
+
panel.dataset.side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
|
|
6954
|
+
panel.dataset.closed = "";
|
|
6955
|
+
panel.hidden = false;
|
|
6956
|
+
panel.getBoundingClientRect();
|
|
6957
|
+
}
|
|
6958
|
+
if (panel.hasAttribute("data-closed")) { agentPanelTransition++; delete panel.dataset.closed; }
|
|
6959
|
+
} else if (!panel.hidden && !panel.hasAttribute("data-closed")) {
|
|
6960
|
+
const transition = ++agentPanelTransition;
|
|
6961
|
+
panel.dataset.closed = "";
|
|
6962
|
+
const finish = () => { if (transition === agentPanelTransition && panel.hasAttribute("data-closed")) panel.hidden = true; };
|
|
6963
|
+
if (reducedMotion.matches) finish();
|
|
6964
|
+
else setTimeout(finish, parseFloat(getComputedStyle(root).getPropertyValue("--comty-motion-duration")));
|
|
6965
|
+
}
|
|
6966
|
+
panel.inert = !open;
|
|
6967
|
+
panel.setAttribute("aria-hidden", String(!open));
|
|
6968
|
+
const signature = JSON.stringify([visibleTasks, localApprovals, agentConnectionError, agentActionError, agentPanelOpen]);
|
|
6969
|
+
if (signature === agentPanelSignature) return;
|
|
6970
|
+
agentPanelSignature = signature;
|
|
6971
|
+
const expanded = new Set([...panel.querySelectorAll('details[data-detail-key][open]')].map(node => node.dataset.detailKey));
|
|
6972
|
+
const focusKey = panel.contains(document.activeElement) ? document.activeElement.dataset.focusKey : null;
|
|
6973
|
+
const scrollTop = panel.querySelector('.comty-agent-panel-content')?.scrollTop || 0;
|
|
6974
|
+
const node = (tag, className, text) => {
|
|
6975
|
+
const element = document.createElement(tag);
|
|
6976
|
+
if (className) element.className = className;
|
|
6977
|
+
if (text != null) element.textContent = text;
|
|
6978
|
+
return element;
|
|
6979
|
+
};
|
|
6980
|
+
const brand = () => createCommentAvatar({ author_kind: 'agent' }, 'comty-agent-brand');
|
|
6981
|
+
panel.replaceChildren();
|
|
6982
|
+
const header = node('header', 'comty-agent-panel-header');
|
|
6983
|
+
const heading = node('div', 'comty-agent-heading');
|
|
6984
|
+
heading.append(node('strong', '', 'Agent'));
|
|
6985
|
+
const toggle = node('button', 'comty-agent-action comty-agent-close'); toggle.type = 'button';
|
|
6986
|
+
toggle.innerHTML = icon('<path d="m6 6 12 12M6 18 18 6"/>');
|
|
6987
|
+
toggle.setAttribute('aria-label', 'Close Agent task'); toggle.title = 'Close'; toggle.dataset.focusKey = 'close';
|
|
6988
|
+
toggle.onclick = () => { agentPanelOpen = false; renderAgentPanel(); agentBtn?.focus({ preventScroll: true }); };
|
|
6989
|
+
header.append(heading, toggle); panel.appendChild(header);
|
|
6990
|
+
const content = node('div', 'comty-agent-panel-content'); panel.appendChild(content);
|
|
6991
|
+
const technicalDetails = (parent, raw, key) => {
|
|
6992
|
+
const details = node('details', 'comty-agent-details'); details.dataset.detailKey = key; details.open = expanded.has(key);
|
|
6993
|
+
const summary = node('summary', '', 'Technical details'); summary.dataset.focusKey = 'details-' + key;
|
|
6994
|
+
details.append(summary, node('pre', '', raw)); parent.appendChild(details);
|
|
6995
|
+
};
|
|
6996
|
+
const errorMessage = (parent, error, key) => {
|
|
6997
|
+
const issue = describeAgentError(error);
|
|
6998
|
+
const message = node('div', 'comty-agent-error');
|
|
6999
|
+
message.append(node('strong', '', issue.title), node('p', '', issue.description));
|
|
7000
|
+
parent.appendChild(message); technicalDetails(parent, issue.raw, key);
|
|
7001
|
+
};
|
|
7002
|
+
if (agentConnectionError || agentActionError) {
|
|
7003
|
+
const banner = node('div', 'comty-agent-item comty-agent-banner'); banner.setAttribute('role', 'alert');
|
|
7004
|
+
errorMessage(banner, agentConnectionError || agentActionError, 'connection'); content.appendChild(banner);
|
|
7005
|
+
}
|
|
7006
|
+
const action = (parent, label, path, input = {}, primary = false) => {
|
|
7007
|
+
const button = node('button', 'comty-agent-action', label); button.type = 'button';
|
|
7008
|
+
const key = path + JSON.stringify(input); button.dataset.focusKey = key;
|
|
7009
|
+
button.disabled = agentPendingActions.has(key);
|
|
7010
|
+
if (primary) button.dataset.primary = '';
|
|
7011
|
+
button.onclick = async () => {
|
|
7012
|
+
if (agentPendingActions.has(key)) return;
|
|
7013
|
+
agentActionError = '';
|
|
7014
|
+
agentPendingActions.add(key); button.disabled = true; button.textContent = 'Please wait…';
|
|
7015
|
+
try {
|
|
7016
|
+
await runAgentAction(path, input);
|
|
7017
|
+
} catch (error) { agentActionError = error.message; }
|
|
7018
|
+
finally { agentPendingActions.delete(key); agentPanelSignature = ''; renderAgentPanel(); }
|
|
7019
|
+
};
|
|
7020
|
+
parent.appendChild(button);
|
|
7021
|
+
};
|
|
7022
|
+
for (const approval of localApprovals) {
|
|
7023
|
+
const section = node('div', 'comty-agent-item');
|
|
7024
|
+
section.append(node('h3', '', 'Approval required'));
|
|
7025
|
+
const task = localTasks.find(task => task.id === approval.taskId);
|
|
7026
|
+
if (task) section.appendChild(node('p', '', task.content));
|
|
7027
|
+
section.appendChild(node('p', '', 'Review this operation before allowing the agent to continue.'));
|
|
7028
|
+
const operation = approval.details?.command || approval.details?.file_path || approval.details?.path;
|
|
7029
|
+
if (typeof operation === 'string' || Array.isArray(operation)) section.appendChild(node('pre', '', Array.isArray(operation) ? operation.join(' ') : operation));
|
|
7030
|
+
technicalDetails(section, JSON.stringify(approval.details ?? {}, null, 2), 'approval-' + approval.id);
|
|
7031
|
+
const actions = node('div', 'comty-agent-actions');
|
|
7032
|
+
action(actions, 'Allow once', `approvals/${approval.id}`, { allow: true }, true);
|
|
7033
|
+
action(actions, 'Deny', `approvals/${approval.id}`, { allow: false });
|
|
7034
|
+
section.appendChild(actions); content.appendChild(section);
|
|
7035
|
+
}
|
|
7036
|
+
const list = node('div', 'comty-agent-list'); content.appendChild(list);
|
|
7037
|
+
for (const task of visibleTasks.reverse()) {
|
|
7038
|
+
const failed = Boolean(task.error) || task.status === 'in_review';
|
|
7039
|
+
const state = task.status === 'done' ? 'success' : failed ? 'error' : 'working';
|
|
7040
|
+
const row = node('div', 'comty-agent-row'); row.dataset.taskId = task.id;
|
|
7041
|
+
const dot = state === 'working' ? createWorkingIndicator() : node('span', 'comty-agent-dot');
|
|
7042
|
+
dot.dataset.status = state; dot.setAttribute('aria-hidden', 'true');
|
|
7043
|
+
const name = node('span', 'comty-agent-row-name', task.thread_comment_id ? `@Agent · ${task.content}` : task.content);
|
|
7044
|
+
row.append(dot, name);
|
|
7045
|
+
if (state === 'working') row.appendChild(node('span', 'comty-agent-row-state', task.status === 'todo' ? 'Starting' : 'Working'));
|
|
7046
|
+
const actions = node('div', 'comty-agent-row-actions');
|
|
7047
|
+
if (['in_review', 'cancel'].includes(task.status)) action(actions, 'Retry', `tasks/${task.id}/retry`);
|
|
7048
|
+
if (['todo', 'in_progress'].includes(task.status)) action(actions, 'Cancel', `tasks/${task.id}/cancel`);
|
|
7049
|
+
if (task.status === 'done') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
7050
|
+
if (task.status === 'in_review') action(actions, 'Dismiss', `tasks/${task.id}/dismiss`);
|
|
7051
|
+
if (actions.childElementCount) row.appendChild(actions);
|
|
7052
|
+
row.setAttribute('aria-label', `${task.content}, ${state === 'error' ? 'failed' : state === 'success' ? 'completed' : state === 'cancel' ? 'cancelled' : task.status === 'todo' ? 'starting' : 'working'}`);
|
|
7053
|
+
list.appendChild(row);
|
|
7054
|
+
if (task.error) {
|
|
7055
|
+
const details = node('div', 'comty-agent-item');
|
|
7056
|
+
errorMessage(details, task.error, task.id);
|
|
7057
|
+
list.appendChild(details);
|
|
7058
|
+
}
|
|
7059
|
+
if (task.result?.summary) {
|
|
7060
|
+
const answer = node('div', 'comty-agent-answer');
|
|
7061
|
+
answer.append(brand(), node('p', '', task.result.summary));
|
|
7062
|
+
list.appendChild(answer);
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
content.scrollTop = scrollTop;
|
|
7066
|
+
if (focusKey && open) [...panel.querySelectorAll('[data-focus-key]')].find(element => element.dataset.focusKey === focusKey)?.focus({ preventScroll: true });
|
|
7067
|
+
}
|
|
7068
|
+
|
|
6102
7069
|
function setSidepanelOpen(open) {
|
|
6103
7070
|
sidepanelOpen = open;
|
|
6104
7071
|
sidepanel.classList.toggle("comty-sidepanel-open", sidepanelOpen);
|
|
@@ -6134,12 +7101,52 @@ function initComty(options = {}) {
|
|
|
6134
7101
|
|
|
6135
7102
|
function openCommentFromPanel(comment) {
|
|
6136
7103
|
const target = resolveCommentElement(comment);
|
|
6137
|
-
if (!target || isElementTreeHidden(target))
|
|
7104
|
+
if (!target || isElementTreeHidden(target)) {
|
|
7105
|
+
openDetachedThread(comment);
|
|
7106
|
+
return;
|
|
7107
|
+
}
|
|
6138
7108
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
6139
7109
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
7110
|
+
const rect = target.getBoundingClientRect();
|
|
7111
|
+
const relX = Number(comment.rel_x ?? comment.relX);
|
|
7112
|
+
const relY = Number(comment.rel_y ?? comment.relY);
|
|
7113
|
+
// Large page containers must reveal the annotation point, not their center.
|
|
7114
|
+
window.scrollBy({
|
|
7115
|
+
left: rect.width > innerWidth && Number.isFinite(relX) ? rect.left + Math.max(0, Math.min(1, relX)) * rect.width - innerWidth / 2 : 0,
|
|
7116
|
+
top: rect.height > innerHeight && Number.isFinite(relY) ? rect.top + Math.max(0, Math.min(1, relY)) * rect.height - innerHeight / 2 : 0,
|
|
7117
|
+
behavior: "instant",
|
|
7118
|
+
});
|
|
6140
7119
|
syncCommentAnchors();
|
|
6141
7120
|
const pin = findPlacedPin(comment);
|
|
6142
7121
|
if (pin && !pin.hasAttribute("data-comty-anchor-hidden")) pin.click();
|
|
7122
|
+
else openDetachedThread(comment);
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7125
|
+
function openDetachedThread(comment) {
|
|
7126
|
+
openCommentThread(comment, null, null, innerWidth / 2, innerHeight / 3, () => {});
|
|
7127
|
+
const bubble = [...root.querySelectorAll(".comty-bubble")].find(node => node.dataset.commentId === String(comment.id));
|
|
7128
|
+
if (bubble) {
|
|
7129
|
+
bubble.dataset.detached = "";
|
|
7130
|
+
positionCommentBubble(bubble, innerWidth / 2, innerHeight / 3);
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
|
|
7134
|
+
function updateAnchorBadge(item, comment) {
|
|
7135
|
+
resolveCommentElement(comment);
|
|
7136
|
+
const result = commentTargets.get(String(comment.id));
|
|
7137
|
+
const legacy = getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname;
|
|
7138
|
+
const status = legacy && !result?.element ? "uncertain" : result?.status;
|
|
7139
|
+
const label = status === "missing" ? "Target missing" : status === "uncertain" ? "Target uncertain" : "";
|
|
7140
|
+
let badge = item.querySelector(".comty-anchor-badge");
|
|
7141
|
+
if (!label) { badge?.remove(); return; }
|
|
7142
|
+
if (!badge) {
|
|
7143
|
+
badge = document.createElement("span");
|
|
7144
|
+
badge.className = "comty-anchor-badge";
|
|
7145
|
+
item.querySelector(".comty-sidepanel-item-body").appendChild(badge);
|
|
7146
|
+
}
|
|
7147
|
+
if (badge.textContent !== label) badge.textContent = label;
|
|
7148
|
+
badge.title = legacy ? "This older comment has no saved hash route. Its target may be on another page." :
|
|
7149
|
+
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.";
|
|
6143
7150
|
}
|
|
6144
7151
|
|
|
6145
7152
|
let sidepanelFilters = { status: "", device: "", dateSort: "desc" };
|
|
@@ -6152,6 +7159,7 @@ function initComty(options = {}) {
|
|
|
6152
7159
|
? sidepanelSearch.value.trim().toLowerCase()
|
|
6153
7160
|
: "";
|
|
6154
7161
|
let list = cachedComments.filter((comment) => {
|
|
7162
|
+
if (comment.local_agent || comment.kind === "agent") return false;
|
|
6155
7163
|
if (
|
|
6156
7164
|
search &&
|
|
6157
7165
|
!`${comment.content ?? ""} ${getCommentLabel(comment)}`
|
|
@@ -6198,6 +7206,7 @@ function initComty(options = {}) {
|
|
|
6198
7206
|
const item = document.createElement("button");
|
|
6199
7207
|
item.type = "button";
|
|
6200
7208
|
item.className = "comty-sidepanel-item";
|
|
7209
|
+
item.dataset.commentId = String(comment.id);
|
|
6201
7210
|
item.disabled = Boolean(comment.pending);
|
|
6202
7211
|
item.innerHTML = `
|
|
6203
7212
|
<span class="comty-sidepanel-item-status" aria-hidden="true"></span>
|
|
@@ -6221,8 +7230,13 @@ function initComty(options = {}) {
|
|
|
6221
7230
|
if (metaEl instanceof HTMLElement) {
|
|
6222
7231
|
metaEl.innerText = `${comment.author_name || "Anonymous"} · ${comment.pending ? "Sending…" : getCommentLabel(comment)}`;
|
|
6223
7232
|
metaEl.title = getCommentLabel(comment);
|
|
7233
|
+
if (getPagePath() !== window.location.pathname && comment.page_path === window.location.pathname && !resolveCommentElement(comment)) {
|
|
7234
|
+
metaEl.innerText += " · Original page unknown";
|
|
7235
|
+
}
|
|
6224
7236
|
}
|
|
6225
7237
|
|
|
7238
|
+
updateAnchorBadge(item, comment);
|
|
7239
|
+
|
|
6226
7240
|
item.onclick = (event) => {
|
|
6227
7241
|
event.stopPropagation();
|
|
6228
7242
|
openCommentFromPanel(comment);
|
|
@@ -6292,7 +7306,7 @@ function initComty(options = {}) {
|
|
|
6292
7306
|
if (!(pin instanceof HTMLElement)) return;
|
|
6293
7307
|
const status = pin.dataset.status;
|
|
6294
7308
|
const shouldShow =
|
|
6295
|
-
!hasFilters || Boolean(status && selectedStatusFilters.has(status));
|
|
7309
|
+
pin.hasAttribute('data-agent-task') || !hasFilters || Boolean(status && selectedStatusFilters.has(status));
|
|
6296
7310
|
pin.classList.toggle("comty-filtered-out", !shouldShow);
|
|
6297
7311
|
});
|
|
6298
7312
|
}
|
|
@@ -6338,14 +7352,15 @@ function initComty(options = {}) {
|
|
|
6338
7352
|
createStatusFilters();
|
|
6339
7353
|
|
|
6340
7354
|
async function updateCommentStatus(comment, status) {
|
|
6341
|
-
if (!projectId || !comment?.id) return false;
|
|
7355
|
+
if (comment?.local_agent || !projectId || !comment?.id) return false;
|
|
6342
7356
|
|
|
6343
7357
|
let res;
|
|
6344
7358
|
try {
|
|
6345
|
-
|
|
7359
|
+
const local = comment.local_agent === true;
|
|
7360
|
+
res = await fetch(local ? `${agentBridgeUrl}/tasks/${encodeURIComponent(comment.id)}/status` : `${apiUrl}/api/comments/${comment.id}/status`, {
|
|
6346
7361
|
method: "POST",
|
|
6347
|
-
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
6348
|
-
body: JSON.stringify({
|
|
7362
|
+
headers: local ? { "Content-Type": "application/json" } : getSdkHeaders({ "Content-Type": "application/json" }),
|
|
7363
|
+
body: JSON.stringify(local ? { status } : {
|
|
6349
7364
|
projectKey,
|
|
6350
7365
|
host: window.location.host,
|
|
6351
7366
|
status,
|
|
@@ -6362,7 +7377,7 @@ function initComty(options = {}) {
|
|
|
6362
7377
|
}
|
|
6363
7378
|
|
|
6364
7379
|
const data = await res.json();
|
|
6365
|
-
comment.status = getCommentStatus(data.status).value;
|
|
7380
|
+
comment.status = getCommentStatus(data.task?.status ?? data.status).value;
|
|
6366
7381
|
return comment.status;
|
|
6367
7382
|
}
|
|
6368
7383
|
|
|
@@ -6554,6 +7569,27 @@ function initComty(options = {}) {
|
|
|
6554
7569
|
isOwnCommentEntry, updateCommentContent, deleteSdkComment, getCommentId,
|
|
6555
7570
|
updateReplyContent, deleteSdkReply, createCommentAvatar, getSdkHeaders,
|
|
6556
7571
|
showAuthPrompt,
|
|
7572
|
+
startThreadAgent: async (comment, replies, content) => {
|
|
7573
|
+
if (!agent) throw new Error('Start the local agent to reply with @Agent');
|
|
7574
|
+
if (!canManage) throw new Error('Sign in as a project editor to use @Agent');
|
|
7575
|
+
const transcript = [
|
|
7576
|
+
`${getCommentAuthorName(comment)}: ${comment.content}`,
|
|
7577
|
+
...replies.map(reply => `${getCommentAuthorName(reply)}: ${reply.content}`),
|
|
7578
|
+
].join('\n').slice(-24000);
|
|
7579
|
+
const response = await fetch(`${agentBridgeUrl}/tasks`, {
|
|
7580
|
+
method: 'POST', headers: agentHeaders(),
|
|
7581
|
+
body: JSON.stringify({ content, pagePath: comment.page_path || getPagePath(),
|
|
7582
|
+
selector: comment.selector || 'body', relX: comment.rel_x ?? 0.5, relY: comment.rel_y ?? 0.5,
|
|
7583
|
+
domElement: comment.dom_element || '', viewport: comment.viewport || '',
|
|
7584
|
+
anchorContext: comment.anchor_context || '', scope: 'element',
|
|
7585
|
+
thread: { commentId: String(comment.id), transcript } }),
|
|
7586
|
+
});
|
|
7587
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.error || 'Could not start Agent');
|
|
7588
|
+
const data = await response.json();
|
|
7589
|
+
if (!data.task?.id) throw new Error('Agent returned no task ID');
|
|
7590
|
+
requestedAgentTasks.add(data.task.id);
|
|
7591
|
+
void loadComments({ force: true });
|
|
7592
|
+
},
|
|
6557
7593
|
get canWrite() { return canWrite; },
|
|
6558
7594
|
get canManage() { return canManage; },
|
|
6559
7595
|
get allowGuestComments() { return allowGuestComments; },
|
|
@@ -6668,6 +7704,7 @@ function initComty(options = {}) {
|
|
|
6668
7704
|
|
|
6669
7705
|
pin.onclick = (event) => {
|
|
6670
7706
|
event.stopPropagation();
|
|
7707
|
+
if (comment.local_agent) { openAgentTask(comment, getPinPoint(pin).x, getPinPoint(pin).y); return; }
|
|
6671
7708
|
openCommentThread(
|
|
6672
7709
|
comment,
|
|
6673
7710
|
pin,
|
|
@@ -6754,42 +7791,122 @@ function initComty(options = {}) {
|
|
|
6754
7791
|
function reconcileComments(comments) {
|
|
6755
7792
|
const previous = new Map(cachedComments.map(comment => [String(comment.id), comment]));
|
|
6756
7793
|
cachedComments = comments
|
|
6757
|
-
.filter(comment => comment.kind !== "agent"
|
|
7794
|
+
.filter(comment => comment.kind !== "agent" || comment.status !== "done")
|
|
6758
7795
|
.map(comment => Object.assign(previous.get(String(comment.id)) || {}, comment));
|
|
6759
7796
|
for (const comment of previous.values()) {
|
|
6760
7797
|
if (comment.pending && !cachedComments.some(item => item.id === comment.id)) cachedComments.push(comment);
|
|
6761
7798
|
}
|
|
6762
7799
|
}
|
|
6763
7800
|
|
|
6764
|
-
async function
|
|
6765
|
-
if (
|
|
6766
|
-
|
|
7801
|
+
async function deliverThreadReply(task) {
|
|
7802
|
+
if (deliveringAgentReplies.has(task.id)) return;
|
|
7803
|
+
deliveringAgentReplies.add(task.id);
|
|
7804
|
+
try {
|
|
7805
|
+
const response = await fetch(`${apiUrl}/api/comments/${encodeURIComponent(task.thread_comment_id)}/replies`, {
|
|
7806
|
+
method: 'POST', headers: getSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
7807
|
+
body: JSON.stringify({ projectKey, content: task.result.summary, agentTaskId: task.id }),
|
|
7808
|
+
});
|
|
7809
|
+
if (!response.ok) throw new Error(`Could not save Agent reply (${response.status})`);
|
|
7810
|
+
const { reply } = await response.json();
|
|
7811
|
+
const local = cachedComments.find(comment => String(comment.id) === task.thread_comment_id);
|
|
7812
|
+
if (local && !local.replies?.some(item => item.id === reply.id)) {
|
|
7813
|
+
local.replies ||= [];
|
|
7814
|
+
local.replies.push(reply);
|
|
7815
|
+
}
|
|
7816
|
+
for (const bubble of root.querySelectorAll('.comty-bubble')) {
|
|
7817
|
+
if (bubble.dataset.commentId === task.thread_comment_id) bubble.dispatchEvent(new CustomEvent('comty-agent-reply', { detail: reply }));
|
|
7818
|
+
}
|
|
7819
|
+
const ack = await fetch(`${agentBridgeUrl}/tasks/${task.id}/dismiss`, { method: 'POST', headers: agentHeaders(), body: '{}' });
|
|
7820
|
+
if (!ack.ok) throw new Error('Agent reply saved, but local acknowledgement failed');
|
|
7821
|
+
localTasks = localTasks.filter(item => item.id !== task.id);
|
|
7822
|
+
} catch (error) {
|
|
7823
|
+
agentActionError = error.message;
|
|
7824
|
+
console.error('Comty: Agent reply delivery failed', error);
|
|
7825
|
+
} finally {
|
|
7826
|
+
deliveringAgentReplies.delete(task.id);
|
|
7827
|
+
agentPanelSignature = '';
|
|
7828
|
+
renderAgentPanel();
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7831
|
+
|
|
7832
|
+
async function loadComments({ force = false, tasksOnly = false } = {}) {
|
|
7833
|
+
if (!projectId && !agent) return;
|
|
7834
|
+
const pagePath = getPagePath();
|
|
6767
7835
|
if (!force && cachedPagePath === pagePath) { await renderComments(cachedComments); return; }
|
|
6768
7836
|
if (pendingCommentsLoad) {
|
|
6769
7837
|
await pendingCommentsLoad;
|
|
6770
|
-
if (!force) return;
|
|
7838
|
+
if (!force && cachedPagePath === pagePath) return;
|
|
6771
7839
|
}
|
|
6772
7840
|
pendingCommentsLoad = (async () => {
|
|
6773
|
-
let comments = cachedPagePath === pagePath ? cachedComments : [];
|
|
6774
|
-
if (projectId) {
|
|
7841
|
+
let comments = cachedPagePath === pagePath ? cachedComments.filter(comment => !comment.local_agent) : [];
|
|
7842
|
+
if (projectId && !tasksOnly) {
|
|
6775
7843
|
try {
|
|
6776
|
-
const
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
7844
|
+
const paths = [...new Set([pagePath, window.location.pathname])];
|
|
7845
|
+
const batches = await Promise.all(paths.map(async path => {
|
|
7846
|
+
const url = new URL(`${apiUrl}/api/comments`);
|
|
7847
|
+
url.search = new URLSearchParams({ projectKey, host: window.location.host, pagePath: path, ...(force ? { _t: String(Date.now()) } : {}) });
|
|
7848
|
+
const res = await fetch(url);
|
|
7849
|
+
if (!res.ok) throw new Error(await res.text());
|
|
7850
|
+
const data = await res.json();
|
|
7851
|
+
return Array.isArray(data.comments) ? data.comments : [];
|
|
7852
|
+
}));
|
|
7853
|
+
comments = [...new Map(batches.flat().map(comment => [String(comment.id), comment])).values()];
|
|
6782
7854
|
} catch (error) { console.error("Comty: list comments failed", error); }
|
|
6783
7855
|
}
|
|
7856
|
+
if (agent) {
|
|
7857
|
+
try {
|
|
7858
|
+
const res = await fetch(`${agentBridgeUrl}/tasks`, { headers: agentHeaders(), cache: "no-store" });
|
|
7859
|
+
if (!res.ok) throw new Error("Local companion unavailable");
|
|
7860
|
+
const data = await res.json();
|
|
7861
|
+
if (Array.isArray(data.tasks)) localTasks = data.tasks;
|
|
7862
|
+
const failedTasks = localTasks.filter(task => task.status === 'in_review');
|
|
7863
|
+
for (const task of failedTasks.filter(task => requestedAgentTasks.has(task.id))) {
|
|
7864
|
+
if (task.error) showAgentBridgeError(task.error);
|
|
7865
|
+
else showNotice({ title: 'Modification failed', description: 'The agent needs clarification.' });
|
|
7866
|
+
}
|
|
7867
|
+
if (Array.isArray(data.approvals)) {
|
|
7868
|
+
localApprovals = data.approvals;
|
|
7869
|
+
if (localApprovals.some(approval => requestedAgentTasks.has(approval.taskId) && !seenAgentApprovals.has(approval.id))) agentPanelOpen = true;
|
|
7870
|
+
seenAgentApprovals.clear();
|
|
7871
|
+
for (const approval of localApprovals) seenAgentApprovals.add(approval.id);
|
|
7872
|
+
}
|
|
7873
|
+
for (const id of requestedAgentTasks) {
|
|
7874
|
+
const task = localTasks.find(task => task.id === id);
|
|
7875
|
+
if (!task || ['done', 'cancel', 'in_review'].includes(task.status)) requestedAgentTasks.delete(id);
|
|
7876
|
+
}
|
|
7877
|
+
agentConnectionError = "";
|
|
7878
|
+
} catch (error) {
|
|
7879
|
+
agentConnectionError = "Local agent disconnected. Check the development server, then reload this page.";
|
|
7880
|
+
console.error("Comty: local tasks failed", error);
|
|
7881
|
+
}
|
|
7882
|
+
for (const task of localTasks.filter(task => task.thread_comment_id && task.status === 'done' && task.result?.summary)) {
|
|
7883
|
+
void deliverThreadReply(task);
|
|
7884
|
+
}
|
|
7885
|
+
comments.push(...localTasks.filter(task => task.page_path === pagePath && !task.thread_comment_id && task.status === 'in_review'));
|
|
7886
|
+
renderAgentPanel();
|
|
7887
|
+
}
|
|
7888
|
+
if (pagePath !== getPagePath()) return;
|
|
6784
7889
|
reconcileComments(comments);
|
|
6785
7890
|
cachedPagePath = pagePath;
|
|
6786
7891
|
renderSidepanelComments();
|
|
6787
7892
|
await renderComments(cachedComments);
|
|
7893
|
+
syncAgentWorkingOutlines();
|
|
6788
7894
|
ensureLiveTracking();
|
|
6789
7895
|
})();
|
|
6790
7896
|
try { await pendingCommentsLoad; } finally { pendingCommentsLoad = null; }
|
|
6791
7897
|
}
|
|
6792
7898
|
|
|
7899
|
+
if (agent) {
|
|
7900
|
+
setInterval(() => {
|
|
7901
|
+
const active = requestedAgentTasks.size || localTasks.some(task =>
|
|
7902
|
+
['todo', 'in_progress'].includes(task.status) || (task.thread_comment_id && task.status === 'done' && task.result?.summary));
|
|
7903
|
+
if (active && !pendingCommentsLoad && document.visibilityState === "visible") void loadComments({ force: true, tasksOnly: true });
|
|
7904
|
+
}, 2000);
|
|
7905
|
+
document.addEventListener('visibilitychange', () => {
|
|
7906
|
+
if (document.visibilityState === 'visible' && !pendingCommentsLoad) void loadComments({ force: true, tasksOnly: true });
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
|
|
6793
7910
|
function getPinPoint(pin) {
|
|
6794
7911
|
return { x: parseFloat(pin.style.left), y: parseFloat(pin.style.top) };
|
|
6795
7912
|
}
|
|
@@ -6817,7 +7934,7 @@ function initComty(options = {}) {
|
|
|
6817
7934
|
}
|
|
6818
7935
|
|
|
6819
7936
|
function syncCommentAnchors() {
|
|
6820
|
-
if (!session.comments) return;
|
|
7937
|
+
if (!session.comments && !session.inspect) return;
|
|
6821
7938
|
const modal = getTopActiveModal();
|
|
6822
7939
|
for (const comment of cachedComments) {
|
|
6823
7940
|
const id = String(comment.id);
|
|
@@ -6828,7 +7945,7 @@ function initComty(options = {}) {
|
|
|
6828
7945
|
placePin(comment, modal);
|
|
6829
7946
|
pin = placedPins.get(id);
|
|
6830
7947
|
}
|
|
6831
|
-
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => bubble.dataset.commentId === id || bubble.dataset.hoverId === id);
|
|
7948
|
+
const bubbles = [...root.querySelectorAll(".comty-bubble")].filter(bubble => (bubble.dataset.commentId === id || bubble.dataset.hoverId === id) && !bubble.hasAttribute("data-detached"));
|
|
6832
7949
|
// A pin can remove its target's :hover style as the pointer enters it.
|
|
6833
7950
|
if (!point && pin && target?.isConnected && !target.hasAttribute("hidden") && !isElementTreeHidden(target.parentElement) &&
|
|
6834
7951
|
(activeHoverCommentIds.has(id) || bubbles.some(bubble => bubble.dataset.commentId === id))) continue;
|
|
@@ -6843,6 +7960,10 @@ function initComty(options = {}) {
|
|
|
6843
7960
|
if (pin.style.top !== y) pin.style.top = y;
|
|
6844
7961
|
for (const bubble of bubbles) positionCommentBubble(bubble, point.clientX, point.clientY);
|
|
6845
7962
|
}
|
|
7963
|
+
if (sidepanelOpen) for (const item of sidepanelList.querySelectorAll(".comty-sidepanel-item")) {
|
|
7964
|
+
const comment = cachedComments.find(comment => String(comment.id) === item.dataset.commentId);
|
|
7965
|
+
if (comment) updateAnchorBadge(item, comment);
|
|
7966
|
+
}
|
|
6846
7967
|
if (draftAnchor) {
|
|
6847
7968
|
if (!draftAnchor.target?.isConnected) draftAnchor.target = resolveCommentElement(draftAnchor.payload);
|
|
6848
7969
|
const point = getVisibleAnchorPoint(draftAnchor.target, draftAnchor.payload.relX, draftAnchor.payload.relY, modal);
|
|
@@ -6856,16 +7977,50 @@ function initComty(options = {}) {
|
|
|
6856
7977
|
}
|
|
6857
7978
|
}
|
|
6858
7979
|
|
|
7980
|
+
function syncAgentWorkingOutlines() {
|
|
7981
|
+
const working = localTasks.filter(task => task.status === "in_progress" && task.page_path === getPagePath());
|
|
7982
|
+
const ids = new Set(working.map(task => String(task.id)));
|
|
7983
|
+
for (const [id, outline] of workingOutlines) {
|
|
7984
|
+
if (ids.has(id)) continue;
|
|
7985
|
+
outline.remove(); workingOutlines.delete(id);
|
|
7986
|
+
}
|
|
7987
|
+
const modal = getTopActiveModal();
|
|
7988
|
+
for (const task of working) {
|
|
7989
|
+
const id = String(task.id);
|
|
7990
|
+
let outline = workingOutlines.get(id);
|
|
7991
|
+
if (!outline) {
|
|
7992
|
+
outline = document.createElement("div");
|
|
7993
|
+
outline.className = "comty-agent-working";
|
|
7994
|
+
outline.setAttribute("aria-label", "Agent working on this element");
|
|
7995
|
+
const badge = document.createElement("span");
|
|
7996
|
+
badge.className = "comty-agent-working-badge";
|
|
7997
|
+
badge.append(createWorkingIndicator(), "Working");
|
|
7998
|
+
outline.appendChild(badge); root.appendChild(outline);
|
|
7999
|
+
workingOutlines.set(id, outline);
|
|
8000
|
+
}
|
|
8001
|
+
const target = resolveCommentElement(task);
|
|
8002
|
+
const point = target && getVisibleAnchorPoint(target, Number(task.rel_x), Number(task.rel_y), modal);
|
|
8003
|
+
outline.hidden = !point;
|
|
8004
|
+
if (!point) continue;
|
|
8005
|
+
const rect = target.getBoundingClientRect();
|
|
8006
|
+
setMeasuredRect(outline, rect);
|
|
8007
|
+
outline.toggleAttribute("data-badge-below", rect.top < 32);
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
|
|
6859
8011
|
function onFrame() {
|
|
6860
8012
|
if (motion.enabled) updateMotion();
|
|
6861
|
-
if (session.comments) syncCommentAnchors();
|
|
8013
|
+
if (session.comments || session.inspect) syncCommentAnchors();
|
|
8014
|
+
if (session.agent || workingOutlines.size || localTasks.some(task => task.status === "in_progress" && task.page_path === getPagePath())) {
|
|
8015
|
+
syncAgentWorkingOutlines();
|
|
8016
|
+
}
|
|
6862
8017
|
if (session.inspect || session.edit || inspectionLayer.childNodes.length) renderInspector();
|
|
6863
8018
|
if (session.inspect && rulersEnabled) renderRulers();
|
|
6864
8019
|
if (session.inspect && rulersEnabled) measurements.render();
|
|
6865
8020
|
}
|
|
6866
8021
|
|
|
6867
8022
|
function ensureLiveTracking() {
|
|
6868
|
-
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0);
|
|
8023
|
+
const active = motion.enabled || session.inspect || session.edit || (session.comments && cachedComments.length > 0) || workingOutlines.size > 0;
|
|
6869
8024
|
if (!active || document.hidden) {
|
|
6870
8025
|
if (liveFrame) cancelAnimationFrame(liveFrame);
|
|
6871
8026
|
liveFrame = null;
|
|
@@ -6885,13 +8040,14 @@ function initComty(options = {}) {
|
|
|
6885
8040
|
syncOverlayHost();
|
|
6886
8041
|
|
|
6887
8042
|
function renderCachedComments() {
|
|
6888
|
-
if (!session.comments && !session.inspect && !session.edit) return;
|
|
8043
|
+
if (!session.comments && !session.inspect && !session.edit && !workingOutlines.size) return;
|
|
6889
8044
|
if (resizeRenderFrame) {
|
|
6890
8045
|
window.cancelAnimationFrame(resizeRenderFrame);
|
|
6891
8046
|
}
|
|
6892
8047
|
resizeRenderFrame = window.requestAnimationFrame(() => {
|
|
6893
8048
|
resizeRenderFrame = null;
|
|
6894
8049
|
syncCommentAnchors();
|
|
8050
|
+
syncAgentWorkingOutlines();
|
|
6895
8051
|
renderInspector();
|
|
6896
8052
|
ensureLiveTracking();
|
|
6897
8053
|
});
|
|
@@ -6933,6 +8089,31 @@ function initComty(options = {}) {
|
|
|
6933
8089
|
characterData: true,
|
|
6934
8090
|
});
|
|
6935
8091
|
|
|
8092
|
+
let activePagePath = getPagePath();
|
|
8093
|
+
function handleRouteChange() {
|
|
8094
|
+
const next = getPagePath();
|
|
8095
|
+
if (next === activePagePath) return;
|
|
8096
|
+
activePagePath = next;
|
|
8097
|
+
markup.clear();
|
|
8098
|
+
markup.setMode(null);
|
|
8099
|
+
closeBubbles();
|
|
8100
|
+
commentTargets.clear();
|
|
8101
|
+
reconcileComments([]);
|
|
8102
|
+
void renderComments([], { retry: false });
|
|
8103
|
+
renderSidepanelComments();
|
|
8104
|
+
void loadComments({ force: true });
|
|
8105
|
+
}
|
|
8106
|
+
window.addEventListener("hashchange", handleRouteChange);
|
|
8107
|
+
window.addEventListener("popstate", handleRouteChange);
|
|
8108
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
8109
|
+
const original = history[method];
|
|
8110
|
+
history[method] = function (...args) {
|
|
8111
|
+
const result = original.apply(this, args);
|
|
8112
|
+
handleRouteChange();
|
|
8113
|
+
return result;
|
|
8114
|
+
};
|
|
8115
|
+
}
|
|
8116
|
+
|
|
6936
8117
|
document.addEventListener("scroll", handleHostLayoutEvent, {
|
|
6937
8118
|
capture: true,
|
|
6938
8119
|
passive: true,
|
|
@@ -6951,7 +8132,7 @@ function initComty(options = {}) {
|
|
|
6951
8132
|
|
|
6952
8133
|
document.addEventListener("pointermove", (event) => {
|
|
6953
8134
|
if (!session.edit && !session.inspect) return;
|
|
6954
|
-
if (motion.enabled) return;
|
|
8135
|
+
if (motion.enabled || markup.mode) return;
|
|
6955
8136
|
if (propertyPicking) {
|
|
6956
8137
|
const target = event.target;
|
|
6957
8138
|
if (propertyPicking === "font" && target instanceof Element && !isComtyElement(target) && getElementOwnTextSignature(target)) {
|
|
@@ -7070,13 +8251,14 @@ function initComty(options = {}) {
|
|
|
7070
8251
|
const y = e.clientY;
|
|
7071
8252
|
|
|
7072
8253
|
const anchorContext = getAnchorContext(el);
|
|
8254
|
+
if (session.agent && anchorContext) anchorContext.inspectedStyles = getInspectRows(el);
|
|
7073
8255
|
|
|
7074
8256
|
const payload = {
|
|
7075
8257
|
...getDeviceContext(),
|
|
7076
8258
|
projectId,
|
|
7077
|
-
kind: "comment",
|
|
8259
|
+
kind: session.agent ? "agent" : "comment",
|
|
7078
8260
|
host: window.location.host,
|
|
7079
|
-
pagePath:
|
|
8261
|
+
pagePath: getPagePath(),
|
|
7080
8262
|
selector: getSelector(el),
|
|
7081
8263
|
relX,
|
|
7082
8264
|
relY,
|
|
@@ -7097,6 +8279,12 @@ function initComty(options = {}) {
|
|
|
7097
8279
|
(target.isContentEditable ||
|
|
7098
8280
|
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
|
|
7099
8281
|
|
|
8282
|
+
const markupMode = markup.mode;
|
|
8283
|
+
if (session.inspect && (e.key === "Escape" || !isEditable) && markup.keydown(e)) {
|
|
8284
|
+
e.preventDefault();
|
|
8285
|
+
if (!markup.mode) (markupMode === "annotate" ? annotateBtn : screenshotBtn).focus({ preventScroll: true });
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
7100
8288
|
if (e.key === "Escape" && motion.enabled) {
|
|
7101
8289
|
e.preventDefault();
|
|
7102
8290
|
setMotionEnabled(false); motionBtn.focus({ preventScroll: true });
|
|
@@ -7111,7 +8299,7 @@ function initComty(options = {}) {
|
|
|
7111
8299
|
return;
|
|
7112
8300
|
}
|
|
7113
8301
|
if (session.inspect && !isEditable && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
|
7114
|
-
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn }[e.key.toLowerCase()];
|
|
8302
|
+
const button = { x: xrayBtn, r: rulersBtn, g: gridBtn, p: pipetteBtn, t: typographyBtn, m: motionBtn, d: annotateBtn, s: screenshotBtn }[e.key.toLowerCase()];
|
|
7115
8303
|
if (button) { e.preventDefault(); button.click(); return; }
|
|
7116
8304
|
}
|
|
7117
8305
|
|
|
@@ -7119,6 +8307,11 @@ function initComty(options = {}) {
|
|
|
7119
8307
|
measureAlt = true;
|
|
7120
8308
|
renderInspector();
|
|
7121
8309
|
}
|
|
8310
|
+
if (isLocalSite && !isEditable && !e.repeat && !e.isComposing && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "a") {
|
|
8311
|
+
e.preventDefault();
|
|
8312
|
+
startAgentSession();
|
|
8313
|
+
return;
|
|
8314
|
+
}
|
|
7122
8315
|
if (!isEditable && !e.repeat && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "i") {
|
|
7123
8316
|
e.preventDefault();
|
|
7124
8317
|
setSession(session.inspect ? "off" : "inspect");
|
|
@@ -7174,6 +8367,7 @@ function initComty(options = {}) {
|
|
|
7174
8367
|
window.addEventListener("blur", () => { measureAlt = false; renderInspector(); });
|
|
7175
8368
|
|
|
7176
8369
|
async function boot() {
|
|
8370
|
+
if (localOnly) { await loadComments(); return; }
|
|
7177
8371
|
const res = await fetch(`${apiUrl}/api/sdk/bootstrap`, {
|
|
7178
8372
|
method: "POST",
|
|
7179
8373
|
headers: getSdkHeaders({ "Content-Type": "application/json" }),
|
|
@@ -7186,6 +8380,7 @@ function initComty(options = {}) {
|
|
|
7186
8380
|
|
|
7187
8381
|
if (!res.ok) {
|
|
7188
8382
|
console.error("Comty: bootstrap failed", await res.text());
|
|
8383
|
+
if (agent) await loadComments();
|
|
7189
8384
|
return;
|
|
7190
8385
|
}
|
|
7191
8386
|
|
|
@@ -7230,16 +8425,30 @@ function initComty(options = {}) {
|
|
|
7230
8425
|
|
|
7231
8426
|
if (typeof options.onTestReady === "function") {
|
|
7232
8427
|
options.onTestReady({
|
|
7233
|
-
getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
8428
|
+
getPagePath, getSelector, getAnchorContext, resolveCommentElement, getVisibleAnchorPoint,
|
|
7234
8429
|
getDistanceSegments, getInspectRows, syncCommentAnchors,
|
|
7235
8430
|
setInspectorMode(enabled) { setSession(enabled ? "inspect" : "off"); },
|
|
7236
8431
|
startEditSession, startBrowseSession, endSession, showInspector,
|
|
7237
|
-
motion, setMotionEnabled, updateMotion,
|
|
8432
|
+
motion, setMotionEnabled, updateMotion, describeAgentError,
|
|
8433
|
+
previewAgent(tasks, approvals = [], error = "") {
|
|
8434
|
+
localTasks = tasks;
|
|
8435
|
+
localApprovals = approvals;
|
|
8436
|
+
agentConnectionError = error;
|
|
8437
|
+
startAgentSession();
|
|
8438
|
+
agentPanelOpen = true;
|
|
8439
|
+
agentPanelSignature = "";
|
|
8440
|
+
renderAgentPanel();
|
|
8441
|
+
},
|
|
7238
8442
|
seed(comments) {
|
|
7239
8443
|
reconcileComments(comments);
|
|
7240
8444
|
setSession(session.comments ? session.mode : "browse", { entry: sessionEntry, renderComments: true });
|
|
7241
8445
|
return renderComments(cachedComments, { retry: false });
|
|
7242
8446
|
},
|
|
8447
|
+
seedAgentTasks(tasks) {
|
|
8448
|
+
localTasks = tasks;
|
|
8449
|
+
syncAgentWorkingOutlines();
|
|
8450
|
+
ensureLiveTracking();
|
|
8451
|
+
},
|
|
7243
8452
|
setCanWrite(value) { canWrite = value; canManage = value; },
|
|
7244
8453
|
invalidate() { anchorRevision += 1; },
|
|
7245
8454
|
});
|
|
@@ -7247,6 +8456,7 @@ function initComty(options = {}) {
|
|
|
7247
8456
|
|
|
7248
8457
|
return boot().catch(error => {
|
|
7249
8458
|
console.error("Comty: bootstrap failed", error);
|
|
8459
|
+
if (agent) return loadComments();
|
|
7250
8460
|
});
|
|
7251
8461
|
}
|
|
7252
8462
|
}
|