@erdemtuna/doc-review 0.12.0 → 0.13.0
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 +23 -25
- package/lib/SKILL.md +65 -202
- package/lib/agent-handoff.js +24 -0
- package/lib/agent-output.js +287 -0
- package/lib/anchor-text.js +31 -19
- package/lib/chrome-api.js +23 -4
- package/lib/chrome.html +0 -25
- package/lib/cli.js +282 -104
- package/lib/comment-target.js +4 -0
- package/lib/contracts/agent.js +122 -0
- package/lib/contracts/feedback.js +369 -1
- package/lib/contracts/frame.js +81 -0
- package/lib/contracts/history.js +189 -1
- package/lib/contracts/index.js +7 -2
- package/lib/contracts/page-boundary.js +187 -0
- package/lib/contracts/validation.js +200 -0
- package/lib/conversation-anchor-controller.js +84 -0
- package/lib/conversation-capture.js +81 -0
- package/lib/conversation-controller.js +798 -0
- package/lib/conversation-save.js +175 -0
- package/lib/conversation-server.js +184 -0
- package/lib/conversation-shell.js +1013 -0
- package/lib/conversation-store.js +809 -0
- package/lib/frame-controller.js +8 -8
- package/lib/frame-policy.js +1 -0
- package/lib/history-policy.js +4 -15
- package/lib/history-server.js +42 -326
- package/lib/html-transform.js +19 -4
- package/lib/icons.js +323 -0
- package/lib/new-message-target.js +27 -0
- package/lib/paths.js +2 -2
- package/lib/poll-transport.js +109 -26
- package/lib/positioning.js +51 -0
- package/lib/references/context-and-recovery.md +130 -0
- package/lib/references/response-contract.md +108 -0
- package/lib/references/source-edits.md +47 -0
- package/lib/revision-store.js +1 -1
- package/lib/save-controller.js +28 -14
- package/lib/sdk.js +357 -35
- package/lib/server.js +73 -595
- package/lib/setup.js +17 -48
- package/lib/state.js +21 -7
- package/lib/thread-anchor-controller.js +56 -0
- package/lib/toolbar-controller.js +3 -3
- package/lib/ui/THIRD_PARTY_NOTICES.md +127 -8
- package/lib/ui/chrome.css +1079 -2782
- package/lib/ui/chrome.js +81 -17
- package/package.json +2 -2
- package/lib/chrome-client.js +0 -1706
package/lib/icons.js
CHANGED
|
@@ -30,6 +30,203 @@ export const ICON_NODES = Object.freeze({
|
|
|
30
30
|
}
|
|
31
31
|
]
|
|
32
32
|
],
|
|
33
|
+
"filePenLine": [
|
|
34
|
+
[
|
|
35
|
+
"path",
|
|
36
|
+
{
|
|
37
|
+
"d": "M14.364 13.634a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506l4.013-4.009a1 1 0 0 0-3.004-3.004z"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"path",
|
|
42
|
+
{
|
|
43
|
+
"d": "M14.487 7.858A1 1 0 0 1 14 7V2"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"path",
|
|
48
|
+
{
|
|
49
|
+
"d": "M20 19.645V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l2.516 2.516"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"path",
|
|
54
|
+
{
|
|
55
|
+
"d": "M8 18h1"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
],
|
|
59
|
+
"arrowRight": [
|
|
60
|
+
[
|
|
61
|
+
"path",
|
|
62
|
+
{
|
|
63
|
+
"d": "M5 12h14"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"path",
|
|
68
|
+
{
|
|
69
|
+
"d": "m12 5 7 7-7 7"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
],
|
|
73
|
+
"messageSquareDiff": [
|
|
74
|
+
[
|
|
75
|
+
"path",
|
|
76
|
+
{
|
|
77
|
+
"d": "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"path",
|
|
82
|
+
{
|
|
83
|
+
"d": "M10 15h4"
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"path",
|
|
88
|
+
{
|
|
89
|
+
"d": "M10 9h4"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"path",
|
|
94
|
+
{
|
|
95
|
+
"d": "M12 7v4"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
],
|
|
99
|
+
"messageSquareCheck": [
|
|
100
|
+
[
|
|
101
|
+
"path",
|
|
102
|
+
{
|
|
103
|
+
"d": "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.7.7 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
"path",
|
|
108
|
+
{
|
|
109
|
+
"d": "m9 11 2 2 4-4"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
],
|
|
113
|
+
"messageCircleQuestion": [
|
|
114
|
+
[
|
|
115
|
+
"path",
|
|
116
|
+
{
|
|
117
|
+
"d": "M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
"path",
|
|
122
|
+
{
|
|
123
|
+
"d": "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"path",
|
|
128
|
+
{
|
|
129
|
+
"d": "M12 17h.01"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
],
|
|
133
|
+
"circleDashed": [
|
|
134
|
+
[
|
|
135
|
+
"path",
|
|
136
|
+
{
|
|
137
|
+
"d": "M10.1 2.182a10 10 0 0 1 3.8 0"
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"path",
|
|
142
|
+
{
|
|
143
|
+
"d": "M13.9 21.818a10 10 0 0 1-3.8 0"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"path",
|
|
148
|
+
{
|
|
149
|
+
"d": "M17.609 3.721a10 10 0 0 1 2.69 2.7"
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
"path",
|
|
154
|
+
{
|
|
155
|
+
"d": "M2.182 13.9a10 10 0 0 1 0-3.8"
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"path",
|
|
160
|
+
{
|
|
161
|
+
"d": "M20.279 17.609a10 10 0 0 1-2.7 2.69"
|
|
162
|
+
}
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"path",
|
|
166
|
+
{
|
|
167
|
+
"d": "M21.818 10.1a10 10 0 0 1 0 3.8"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"path",
|
|
172
|
+
{
|
|
173
|
+
"d": "M3.721 6.391a10 10 0 0 1 2.7-2.69"
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"path",
|
|
178
|
+
{
|
|
179
|
+
"d": "M6.391 20.279a10 10 0 0 1-2.69-2.7"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
],
|
|
183
|
+
"circlePause": [
|
|
184
|
+
[
|
|
185
|
+
"circle",
|
|
186
|
+
{
|
|
187
|
+
"cx": "12",
|
|
188
|
+
"cy": "12",
|
|
189
|
+
"r": "10"
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
"line",
|
|
194
|
+
{
|
|
195
|
+
"x1": "10",
|
|
196
|
+
"x2": "10",
|
|
197
|
+
"y1": "15",
|
|
198
|
+
"y2": "9"
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
"line",
|
|
203
|
+
{
|
|
204
|
+
"x1": "14",
|
|
205
|
+
"x2": "14",
|
|
206
|
+
"y1": "15",
|
|
207
|
+
"y2": "9"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
],
|
|
211
|
+
"copy": [
|
|
212
|
+
[
|
|
213
|
+
"rect",
|
|
214
|
+
{
|
|
215
|
+
"width": "14",
|
|
216
|
+
"height": "14",
|
|
217
|
+
"x": "8",
|
|
218
|
+
"y": "8",
|
|
219
|
+
"rx": "2",
|
|
220
|
+
"ry": "2"
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
"path",
|
|
225
|
+
{
|
|
226
|
+
"d": "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
],
|
|
33
230
|
"plus": [
|
|
34
231
|
[
|
|
35
232
|
"path",
|
|
@@ -106,6 +303,14 @@ export const ICON_NODES = Object.freeze({
|
|
|
106
303
|
}
|
|
107
304
|
]
|
|
108
305
|
],
|
|
306
|
+
"chevronLeft": [
|
|
307
|
+
[
|
|
308
|
+
"path",
|
|
309
|
+
{
|
|
310
|
+
"d": "m15 18-6-6 6-6"
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
],
|
|
109
314
|
"chevronRight": [
|
|
110
315
|
[
|
|
111
316
|
"path",
|
|
@@ -242,6 +447,124 @@ export const ICON_NODES = Object.freeze({
|
|
|
242
447
|
}
|
|
243
448
|
]
|
|
244
449
|
],
|
|
450
|
+
"checkCheck": [
|
|
451
|
+
[
|
|
452
|
+
"path",
|
|
453
|
+
{
|
|
454
|
+
"d": "M18 6 7 17l-5-5"
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
[
|
|
458
|
+
"path",
|
|
459
|
+
{
|
|
460
|
+
"d": "m22 10-7.5 7.5L13 16"
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
],
|
|
464
|
+
"circleCheck": [
|
|
465
|
+
[
|
|
466
|
+
"circle",
|
|
467
|
+
{
|
|
468
|
+
"cx": "12",
|
|
469
|
+
"cy": "12",
|
|
470
|
+
"r": "10"
|
|
471
|
+
}
|
|
472
|
+
],
|
|
473
|
+
[
|
|
474
|
+
"path",
|
|
475
|
+
{
|
|
476
|
+
"d": "m16 9-5.5 5.5L8 12"
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
],
|
|
480
|
+
"rotateCcw": [
|
|
481
|
+
[
|
|
482
|
+
"path",
|
|
483
|
+
{
|
|
484
|
+
"d": "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
|
485
|
+
}
|
|
486
|
+
],
|
|
487
|
+
[
|
|
488
|
+
"path",
|
|
489
|
+
{
|
|
490
|
+
"d": "M3 3v5h5"
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
],
|
|
494
|
+
"clock": [
|
|
495
|
+
[
|
|
496
|
+
"circle",
|
|
497
|
+
{
|
|
498
|
+
"cx": "12",
|
|
499
|
+
"cy": "12",
|
|
500
|
+
"r": "10"
|
|
501
|
+
}
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
"path",
|
|
505
|
+
{
|
|
506
|
+
"d": "M12 6v6l4 2"
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
],
|
|
510
|
+
"inbox": [
|
|
511
|
+
[
|
|
512
|
+
"polyline",
|
|
513
|
+
{
|
|
514
|
+
"points": "22 12 16 12 14 15 10 15 8 12 2 12"
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
[
|
|
518
|
+
"path",
|
|
519
|
+
{
|
|
520
|
+
"d": "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
],
|
|
524
|
+
"circleX": [
|
|
525
|
+
[
|
|
526
|
+
"circle",
|
|
527
|
+
{
|
|
528
|
+
"cx": "12",
|
|
529
|
+
"cy": "12",
|
|
530
|
+
"r": "10"
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
[
|
|
534
|
+
"path",
|
|
535
|
+
{
|
|
536
|
+
"d": "m15 9-6 6"
|
|
537
|
+
}
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
"path",
|
|
541
|
+
{
|
|
542
|
+
"d": "m9 9 6 6"
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
],
|
|
546
|
+
"circleHelp": [
|
|
547
|
+
[
|
|
548
|
+
"circle",
|
|
549
|
+
{
|
|
550
|
+
"cx": "12",
|
|
551
|
+
"cy": "12",
|
|
552
|
+
"r": "10"
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
[
|
|
556
|
+
"path",
|
|
557
|
+
{
|
|
558
|
+
"d": "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
[
|
|
562
|
+
"path",
|
|
563
|
+
{
|
|
564
|
+
"d": "M12 17h.01"
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
],
|
|
245
568
|
"sun": [
|
|
246
569
|
[
|
|
247
570
|
"circle",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { conversationTargetSchema } from "./contracts/feedback.js";
|
|
2
|
+
import { reject } from "./contracts/validation.js";
|
|
3
|
+
import { sanitizeClientRects, sanitizeClipRect } from "./comment-target.js";
|
|
4
|
+
/** Existing SDK payload: anchors are required to save; geometry is optional presentation. */
|
|
5
|
+
export function readNewMessageTarget(input) {
|
|
6
|
+
const target = conversationTargetSchema.parse({ kind: input.kind, anchor: input.anchor });
|
|
7
|
+
if (!Number.isSafeInteger(input.targetGeneration) || Number(input.targetGeneration) < 1) {
|
|
8
|
+
reject("INVALID_INPUT", "Invalid new-comment target generation.");
|
|
9
|
+
}
|
|
10
|
+
const viewport = input.viewport;
|
|
11
|
+
const relation = input.relation;
|
|
12
|
+
const clip = viewport && Number.isFinite(viewport.width) && viewport.width > 0 &&
|
|
13
|
+
Number.isFinite(viewport.height) && viewport.height > 0 ? sanitizeClipRect(input.clip, viewport) : null;
|
|
14
|
+
const rects = clip ? sanitizeClientRects(input.rects, viewport).flatMap((rect) => {
|
|
15
|
+
if (!rect)
|
|
16
|
+
return [];
|
|
17
|
+
const left = Math.max(rect.left, clip.left), top = Math.max(rect.top, clip.top);
|
|
18
|
+
const right = Math.min(rect.right, clip.right), bottom = Math.min(rect.bottom, clip.bottom);
|
|
19
|
+
return right > left && bottom > top ? [{ left, top, right, bottom, width: right - left, height: bottom - top }] : [];
|
|
20
|
+
}) : [];
|
|
21
|
+
const geometry = clip && ["visible", "above", "below", "unavailable"].includes(String(relation)) &&
|
|
22
|
+
(relation !== "visible" || rects.length) ? {
|
|
23
|
+
rects, clip, viewport: viewport, relation: relation,
|
|
24
|
+
horizontal: typeof input.horizontal === "number" && Number.isFinite(input.horizontal) ? input.horizontal : null,
|
|
25
|
+
} : null;
|
|
26
|
+
return { target, generation: Number(input.targetGeneration), geometry };
|
|
27
|
+
}
|
package/lib/paths.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
// Bump for incompatible CLI/server or shell/SDK contracts, including readiness.
|
|
6
6
|
// A new CLI must not silently reuse an older background server.
|
|
7
|
-
export const SERVER_PROTOCOL =
|
|
7
|
+
export const SERVER_PROTOCOL = 25;
|
|
8
8
|
export function serverProtocolMatches(protocol) {
|
|
9
9
|
return Number(protocol) === SERVER_PROTOCOL;
|
|
10
10
|
}
|
|
@@ -13,7 +13,7 @@ export function stateDir() {
|
|
|
13
13
|
return override ? path.resolve(override) : path.join(homedir(), ".doc-review");
|
|
14
14
|
}
|
|
15
15
|
export function statePath() {
|
|
16
|
-
return path.join(stateDir(), "state.json");
|
|
16
|
+
return path.join(stateDir(), "conversation-state.json");
|
|
17
17
|
}
|
|
18
18
|
export function serverPath() {
|
|
19
19
|
return path.join(stateDir(), "server.json");
|
package/lib/poll-transport.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { acceptedMutationSchema } from "./contracts/feedback.js";
|
|
4
|
+
import { pollResponseSchema } from "./contracts/page-boundary.js";
|
|
5
|
+
import { ContractError, failureSchema } from "./contracts/validation.js";
|
|
3
6
|
export const DEFAULT_POLL_SECONDS = 12 * 60 * 60;
|
|
4
7
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
5
8
|
const clock = {
|
|
@@ -162,48 +165,62 @@ export function parseServerResponse(response) {
|
|
|
162
165
|
throw codedError("Malformed response from the doc-review server. End the review and restart the server.", "SERVER_RESPONSE_INVALID");
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
export async function
|
|
168
|
+
export async function conversationOnce(server, body, decoder, deadline, route = "/api/conversation") {
|
|
166
169
|
deadline.check();
|
|
167
|
-
const query = `target=${encodeURIComponent(target)}${ackId ? `&ack=${encodeURIComponent(ackId)}` : ""}`;
|
|
168
170
|
const response = await requestRaw(server, {
|
|
169
|
-
method: "
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
timeoutMs: deadline.remaining(),
|
|
171
|
+
method: "POST", path: route, headers: { "content-type": "application/json" },
|
|
172
|
+
}, body, {
|
|
173
|
+
timeoutMs: Math.min(15000, deadline.remaining()),
|
|
173
174
|
time: deadline.time,
|
|
174
|
-
timeoutCode: "POLL_DEADLINE",
|
|
175
175
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(response.raw);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!(error instanceof SyntaxError))
|
|
182
|
+
throw error;
|
|
183
|
+
throw codedError("Malformed conversation response.", "SERVER_RESPONSE_INVALID");
|
|
184
|
+
}
|
|
185
|
+
if (response.status !== 200) {
|
|
186
|
+
let failure;
|
|
187
|
+
try {
|
|
188
|
+
failure = failureSchema.parse(parsed);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw codedError(`Invalid error response (HTTP ${response.status}).`, "SERVER_RESPONSE_INVALID");
|
|
192
|
+
}
|
|
193
|
+
if (failure.error.status !== response.status)
|
|
194
|
+
throw codedError("HTTP and error status disagree.", "SERVER_RESPONSE_INVALID");
|
|
195
|
+
throw new ContractError(failure.error.code, failure.error.message);
|
|
180
196
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
try {
|
|
198
|
+
return decoder.parse(parsed);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw codedError("Conversation response does not match its schema.", "SERVER_RESPONSE_INVALID");
|
|
185
202
|
}
|
|
186
|
-
return batch;
|
|
187
203
|
}
|
|
188
|
-
export
|
|
204
|
+
export const pollOnce = (server, reference, deadline) => conversationOnce(server, { operation: "poll", ...reference }, pollResponseSchema, deadline);
|
|
205
|
+
export async function pollUntilDeadline({ reference, deadline = createDeadline(), discover, poll = pollOnce, diagnostic = () => { }, }) {
|
|
189
206
|
let failures = 0;
|
|
190
207
|
for (;;) {
|
|
191
208
|
try {
|
|
192
209
|
deadline.check();
|
|
193
210
|
const server = await discover(deadline);
|
|
194
211
|
deadline.check();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
const result = await poll(server, reference, deadline);
|
|
213
|
+
if (result.review.reviewId !== reference.reviewId || result.review.entryKey !== reference.entryKey) {
|
|
214
|
+
throw codedError("Polling response belongs to another review.", "SERVER_RESPONSE_INVALID");
|
|
215
|
+
}
|
|
216
|
+
if (result.state !== "waiting")
|
|
217
|
+
return result;
|
|
218
|
+
failures = 0;
|
|
219
|
+
await deadline.sleep(500);
|
|
199
220
|
}
|
|
200
221
|
catch (err) {
|
|
201
222
|
if (err.code === "POLL_DEADLINE" || deadline.remaining() <= 0) {
|
|
202
|
-
return {
|
|
203
|
-
status: "timeout",
|
|
204
|
-
waited_seconds: deadline.seconds,
|
|
205
|
-
next_step: "No feedback yet. Run the same poll command again to keep waiting, or `doc-review status <target>` to check without blocking.",
|
|
206
|
-
};
|
|
223
|
+
return { state: "timeout", ...reference };
|
|
207
224
|
}
|
|
208
225
|
if (!isRecoverableTransportError(err))
|
|
209
226
|
throw err;
|
|
@@ -218,3 +235,69 @@ export async function pollUntilDeadline({ target, ackId = "", deadline = createD
|
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
237
|
}
|
|
238
|
+
/** Retry transport only. This function never opens or edits a reviewed source. */
|
|
239
|
+
export async function mutationUntilDeadline({ body, deadline = createDeadline(60), discover, send = conversationOnce, diagnostic = () => { }, }) {
|
|
240
|
+
const request = structuredClone(body);
|
|
241
|
+
let failures = 0;
|
|
242
|
+
let reason = "unavailable";
|
|
243
|
+
let attempted = false;
|
|
244
|
+
const unknown = (detail = "") => Object.assign(new Error(`Acceptance is unknown. Reuse the identical request; do not repeat source edits.${detail ? ` ${detail}` : ""}`), { code: "TRANSPORT_UNKNOWN", outcome: { state: "unknown", requestId: request.requestId, reason } });
|
|
245
|
+
for (;;) {
|
|
246
|
+
try {
|
|
247
|
+
deadline.check();
|
|
248
|
+
const server = await discover(deadline);
|
|
249
|
+
deadline.check();
|
|
250
|
+
attempted = true;
|
|
251
|
+
const accepted = await send(server, request, acceptedMutationSchema, deadline);
|
|
252
|
+
const receipt = acceptedMutationSchema.parse(accepted).receipt;
|
|
253
|
+
if (receipt.requestId !== request.requestId || receipt.operation !== request.operation ||
|
|
254
|
+
(request.reviewId !== undefined && receipt.reviewId !== request.reviewId) ||
|
|
255
|
+
(request.entryKey !== undefined && receipt.entryKey !== request.entryKey) ||
|
|
256
|
+
(request.submissionId !== undefined && receipt.value.submissionId !== request.submissionId)) {
|
|
257
|
+
throw codedError("Receipt does not match the submitted request.", "SERVER_RESPONSE_INVALID");
|
|
258
|
+
}
|
|
259
|
+
return accepted;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof ContractError && !["STATE_PERSIST_FAILED", "INTERNAL_ERROR"].includes(error.code))
|
|
263
|
+
throw error;
|
|
264
|
+
const recoverable = isRecoverableTransportError(error) || ["SERVER_RESPONSE_INVALID", "STATE_PERSIST_FAILED"].includes(error.code);
|
|
265
|
+
if (!recoverable && error.code !== "POLL_DEADLINE") {
|
|
266
|
+
if (attempted)
|
|
267
|
+
throw unknown(error.message);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (error.code !== "POLL_DEADLINE")
|
|
271
|
+
reason = error.code === "SERVER_RESPONSE_INVALID" ? "invalid-response" :
|
|
272
|
+
error.code === "ETIMEDOUT" ? "timeout" : error.code === "STATE_PERSIST_FAILED" ? "unavailable" : "disconnected";
|
|
273
|
+
if (error.code === "POLL_DEADLINE" || deadline.remaining() <= 0) {
|
|
274
|
+
throw unknown();
|
|
275
|
+
}
|
|
276
|
+
diagnostic(`Response uncertain (${error.message}); retrying the identical request.\n`);
|
|
277
|
+
try {
|
|
278
|
+
await deadline.sleep(Math.min(250 * 2 ** Math.min(failures++, 5), 5000));
|
|
279
|
+
}
|
|
280
|
+
catch (sleepError) {
|
|
281
|
+
if (sleepError.code !== "POLL_DEADLINE")
|
|
282
|
+
throw sleepError;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export async function readUntilDeadline({ body, decoder, deadline = createDeadline(60), discover, diagnostic = () => { }, route }) {
|
|
288
|
+
let failures = 0;
|
|
289
|
+
for (;;) {
|
|
290
|
+
try {
|
|
291
|
+
deadline.check();
|
|
292
|
+
return await conversationOnce(await discover(deadline), body, decoder, deadline, route);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code === "POLL_DEADLINE" || deadline.remaining() <= 0)
|
|
296
|
+
throw error;
|
|
297
|
+
if (!isRecoverableTransportError(error))
|
|
298
|
+
throw error;
|
|
299
|
+
diagnostic(`Read interrupted (${error.message}); retrying.\n`);
|
|
300
|
+
await deadline.sleep(Math.min(250 * 2 ** Math.min(failures++, 5), 5000));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
package/lib/positioning.js
CHANGED
|
@@ -95,3 +95,54 @@ export function alignedCardPosition(rects, { frameRect, viewport, width = 300, h
|
|
|
95
95
|
top: Math.max(viewport.top + 12, Math.min(viewport.top + viewport.height - height - 12, top)),
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
/** Popovers may cover unselected prose, never any visible part of their target. */
|
|
99
|
+
function placeLocalSurface(geometry, { frameRect, viewport, surfaceWidth, surfaceHeight, minHeight = surfaceHeight, toolbarHeight = 48 }) {
|
|
100
|
+
if (!geometry || geometry.relation === "unavailable")
|
|
101
|
+
return null;
|
|
102
|
+
const bounds = { left: viewport.left + GAP, right: viewport.left + viewport.width - GAP,
|
|
103
|
+
top: viewport.top + toolbarHeight + GAP, bottom: viewport.top + viewport.height - GAP };
|
|
104
|
+
const width = Math.min(surfaceWidth, bounds.right - bounds.left);
|
|
105
|
+
if (width < 240 || bounds.bottom - bounds.top < minHeight)
|
|
106
|
+
return null;
|
|
107
|
+
const height = Math.min(surfaceHeight, bounds.bottom - bounds.top);
|
|
108
|
+
const clip = geometry.clip ? frameRectToChrome(geometry.clip, frameRect) : bounds;
|
|
109
|
+
const rects = geometry.rects.map(rect => frameRectToChrome(rect, frameRect)).map(rect => ({
|
|
110
|
+
left: Math.max(rect.left, clip.left, bounds.left - GAP), right: Math.min(rect.right, clip.right, bounds.right + GAP),
|
|
111
|
+
top: Math.max(rect.top, clip.top, bounds.top - GAP), bottom: Math.min(rect.bottom, clip.bottom, bounds.bottom + GAP),
|
|
112
|
+
})).filter(rect => rect.right > rect.left && rect.bottom > rect.top);
|
|
113
|
+
if (!rects.length) {
|
|
114
|
+
if (!["above", "below"].includes(geometry.relation))
|
|
115
|
+
return null;
|
|
116
|
+
const horizontal = Number.isFinite(geometry.horizontal) ? frameRect.left + geometry.horizontal : clip.left;
|
|
117
|
+
return { kind: geometry.relation === "above" ? "edge-top" : "edge-bottom",
|
|
118
|
+
left: clamp(horizontal, bounds.left, bounds.right - width),
|
|
119
|
+
top: clamp(geometry.relation === "above" ? clip.top + GAP : clip.bottom - height - GAP, bounds.top, bounds.bottom - height),
|
|
120
|
+
width, height };
|
|
121
|
+
}
|
|
122
|
+
const target = { left: Math.min(...rects.map(rect => rect.left)), right: Math.max(...rects.map(rect => rect.right)),
|
|
123
|
+
top: Math.min(...rects.map(rect => rect.top)), bottom: Math.max(...rects.map(rect => rect.bottom)) };
|
|
124
|
+
const x = clamp(target.left, bounds.left, bounds.right - width);
|
|
125
|
+
const y = clamp(target.top, bounds.top, bounds.bottom - height);
|
|
126
|
+
const below = Math.min(height, bounds.bottom - target.bottom - GAP);
|
|
127
|
+
const above = Math.min(height, target.top - GAP - bounds.top);
|
|
128
|
+
const candidates = [
|
|
129
|
+
{ left: target.right + GAP, top: y, height },
|
|
130
|
+
{ left: target.left - width - GAP, top: y, height },
|
|
131
|
+
{ left: x, top: target.bottom + GAP, height: below },
|
|
132
|
+
{ left: x, top: target.top - GAP - above, height: above },
|
|
133
|
+
];
|
|
134
|
+
const fit = candidates.find(candidate => candidate.height >= minHeight &&
|
|
135
|
+
candidate.left >= bounds.left && candidate.left + width <= bounds.right &&
|
|
136
|
+
candidate.top >= bounds.top && candidate.top + candidate.height <= bounds.bottom &&
|
|
137
|
+
rects.every(rect => candidate.left >= rect.right + GAP || candidate.left + width <= rect.left - GAP ||
|
|
138
|
+
candidate.top >= rect.bottom + GAP || candidate.top + candidate.height <= rect.top - GAP));
|
|
139
|
+
return fit ? { kind: "attached", ...fit, width } : null;
|
|
140
|
+
}
|
|
141
|
+
export function placeNewMessageSurface(geometry, options) {
|
|
142
|
+
return placeLocalSurface(geometry, options);
|
|
143
|
+
}
|
|
144
|
+
export function placeConversationSurface(state, { frameRect, viewport, toolbarHeight = 48, width = 360, height = 260, minHeight = 160 }) {
|
|
145
|
+
if (state?.state !== "found")
|
|
146
|
+
return null;
|
|
147
|
+
return placeLocalSurface(state, { frameRect, viewport, toolbarHeight, surfaceWidth: width, surfaceHeight: height, minHeight });
|
|
148
|
+
}
|