@erdemtuna/doc-review 0.11.0 → 0.12.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/lib/chrome-client.js +41 -35
- package/lib/chrome.html +3 -0
- package/lib/contracts/frame.js +6 -0
- package/lib/contracts/index.js +1 -1
- package/lib/frame-controller.js +189 -17
- package/lib/frame-host.js +10 -0
- package/lib/paths.js +3 -3
- package/lib/recovery-controller.js +9 -0
- package/lib/review-palette.js +97 -0
- package/lib/sdk.js +214 -60
- package/lib/ui/chrome.css +356 -162
- package/lib/ui/chrome.js +17 -17
- package/package.json +1 -1
package/lib/chrome-client.js
CHANGED
|
@@ -260,6 +260,8 @@ const transport = createReviewApi({ token: state.token });
|
|
|
260
260
|
const api = transport.request;
|
|
261
261
|
const frameController = createFrameController({
|
|
262
262
|
sessionId: state.sessionId, host: frameHost, request: api,
|
|
263
|
+
activated: completeFrameReady,
|
|
264
|
+
diagnostic: (code) => diagnostic(code, {}),
|
|
263
265
|
suspended() {
|
|
264
266
|
captures.cancel();
|
|
265
267
|
captureCoordinator.reset();
|
|
@@ -353,6 +355,7 @@ export const recoveryRuntime = createRecoveryController({
|
|
|
353
355
|
comparing: state.comparing, ended: state.ended, loading: frameController.loading,
|
|
354
356
|
pendingReload: frameController.state.pendingReload || !!frameHost.previous,
|
|
355
357
|
frameError: frameController.state.phase.kind === "failed" ? frameController.state.phase.message : null,
|
|
358
|
+
themeSync: frameController.themeSync,
|
|
356
359
|
reload: {
|
|
357
360
|
visible: state.reloadVisible, message: state.reloadMessage,
|
|
358
361
|
error: state.reloadFailed || saveController.state.conflict,
|
|
@@ -378,6 +381,7 @@ export const recoveryRuntime = createRecoveryController({
|
|
|
378
381
|
toFrame({ type: "eh:modeMenuState", open });
|
|
379
382
|
},
|
|
380
383
|
reload: () => reloadLatest({ explicit: true }),
|
|
384
|
+
retryTheme: () => frameController.retryTheme(),
|
|
381
385
|
keepCurrent() {
|
|
382
386
|
state.reloadVisible = false;
|
|
383
387
|
announce("Keeping the current page. Return to Review to reload when ready.");
|
|
@@ -1280,47 +1284,48 @@ function targetPayload(msg) {
|
|
|
1280
1284
|
horizontal: Number.isFinite(msg.horizontal) ? Number(msg.horizontal) : null,
|
|
1281
1285
|
};
|
|
1282
1286
|
}
|
|
1287
|
+
function completeFrameReady(readyState) {
|
|
1288
|
+
state.page = { ...state.page, ...readyState };
|
|
1289
|
+
toFrame({ type: "eh:anchors", comments: state.page ? state.page.comments : [] });
|
|
1290
|
+
if (frameController.state.reloading) {
|
|
1291
|
+
toFrame({ type: "eh:restoreScroll", x: state.scroll.x, y: state.scroll.y });
|
|
1292
|
+
frameController.restoredScroll();
|
|
1293
|
+
}
|
|
1294
|
+
const configuration = reviewConfiguration(state.page, state.reviewMode);
|
|
1295
|
+
state.savePolicy = configuration.savePolicy;
|
|
1296
|
+
void frameController.configure(configuration.mode, configuration.savePolicy, false);
|
|
1297
|
+
render();
|
|
1298
|
+
if (state.page?.kind !== "url") {
|
|
1299
|
+
const rawIdentity = currentRender();
|
|
1300
|
+
void api(`/api/page/${frameController.state.key}/raw`).then((raw) => {
|
|
1301
|
+
if (!sameRender(rawIdentity, currentRender()) || frameController.state.pendingReload)
|
|
1302
|
+
return;
|
|
1303
|
+
if (frameController.state.sourceHash && frameController.state.sourceHash !== raw.hash) {
|
|
1304
|
+
saveController.markConflict();
|
|
1305
|
+
holdReload();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
saveController.baseline(raw.hash || null);
|
|
1309
|
+
if (configuration.savePolicy === "writable")
|
|
1310
|
+
toFrame({ type: "eh:raw", html: raw.html });
|
|
1311
|
+
scheduleResultCapture();
|
|
1312
|
+
}).catch((error) => {
|
|
1313
|
+
if (sameRender(rawIdentity, currentRender()) && !state.ended) {
|
|
1314
|
+
toast(`Source could not be loaded: ${error.message}. Reload before saving.`);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
void refreshHistory();
|
|
1319
|
+
}
|
|
1283
1320
|
listen(window, "message", async (event) => {
|
|
1321
|
+
if (frameController.handleThemeMessage(event))
|
|
1322
|
+
return;
|
|
1284
1323
|
if (!frameController.accepts(event))
|
|
1285
1324
|
return;
|
|
1286
1325
|
const msg = event.data;
|
|
1287
1326
|
switch (msg.type) {
|
|
1288
1327
|
case "eh:ready": {
|
|
1289
|
-
|
|
1290
|
-
const readyState = await frameController.ready();
|
|
1291
|
-
if (!readyState || !sameRender(readyIdentity, currentRender()))
|
|
1292
|
-
return;
|
|
1293
|
-
state.page = { ...state.page, ...readyState };
|
|
1294
|
-
toFrame({ type: "eh:anchors", comments: state.page ? state.page.comments : [] });
|
|
1295
|
-
if (frameController.state.reloading) {
|
|
1296
|
-
toFrame({ type: "eh:restoreScroll", x: state.scroll.x, y: state.scroll.y });
|
|
1297
|
-
frameController.restoredScroll();
|
|
1298
|
-
}
|
|
1299
|
-
const configuration = reviewConfiguration(state.page, state.reviewMode);
|
|
1300
|
-
state.savePolicy = configuration.savePolicy;
|
|
1301
|
-
void frameController.configure(configuration.mode, configuration.savePolicy, false);
|
|
1302
|
-
render();
|
|
1303
|
-
if (state.page?.kind !== "url") {
|
|
1304
|
-
const rawIdentity = currentRender();
|
|
1305
|
-
void api(`/api/page/${frameController.state.key}/raw`).then((raw) => {
|
|
1306
|
-
if (!sameRender(rawIdentity, currentRender()) || frameController.state.pendingReload)
|
|
1307
|
-
return;
|
|
1308
|
-
if (frameController.state.sourceHash && frameController.state.sourceHash !== raw.hash) {
|
|
1309
|
-
saveController.markConflict();
|
|
1310
|
-
holdReload();
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
saveController.baseline(raw.hash || null);
|
|
1314
|
-
if (configuration.savePolicy === "writable")
|
|
1315
|
-
toFrame({ type: "eh:raw", html: raw.html });
|
|
1316
|
-
scheduleResultCapture();
|
|
1317
|
-
}).catch((error) => {
|
|
1318
|
-
if (sameRender(rawIdentity, currentRender()) && !state.ended) {
|
|
1319
|
-
toast(`Source could not be loaded: ${error.message}. Reload before saving.`);
|
|
1320
|
-
}
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
void refreshHistory();
|
|
1328
|
+
await frameController.ready();
|
|
1324
1329
|
break;
|
|
1325
1330
|
}
|
|
1326
1331
|
case "eh:configurationApplied":
|
|
@@ -1555,6 +1560,7 @@ function toggleTheme() {
|
|
|
1555
1560
|
function applyTheme(dark) {
|
|
1556
1561
|
document.documentElement.dataset.theme = dark ? "dark" : "light";
|
|
1557
1562
|
state.theme = dark ? "dark" : "light";
|
|
1563
|
+
frameController.setTheme(state.theme);
|
|
1558
1564
|
toolbarRuntime.publish();
|
|
1559
1565
|
}
|
|
1560
1566
|
listen(document, "keydown", (event) => {
|
package/lib/chrome.html
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>doc-review</title>
|
|
7
|
+
<!-- brand:start -->
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2264%22%20height%3D%2264%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%2317685F%22%2F%3E%3Cpath%20d%3D%22M16%2012H48Q54%2012%2054%2018V40Q54%2046%2048%2046H30L16%2054V46Q10%2046%2010%2040V18Q10%2012%2016%2012Z%20M20%2022H36V18L43%2024L36%2030V26H20Z%20M44%2034H28V30L21%2036L28%2042V38H44Z%22%20fill%3D%22%23FFFDF7%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E">
|
|
9
|
+
<!-- brand:end -->
|
|
7
10
|
<link rel="stylesheet" href="/chrome.css">
|
|
8
11
|
</head>
|
|
9
12
|
<body data-session="__SESSION_ID__" data-token="__TOKEN__">
|
package/lib/contracts/frame.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export function isThemePayload(value) {
|
|
2
|
+
return typeof value === "object" && value !== null &&
|
|
3
|
+
"theme" in value && (value.theme === "light" || value.theme === "dark") &&
|
|
4
|
+
"themeRevision" in value && typeof value.themeRevision === "number" &&
|
|
5
|
+
Number.isSafeInteger(value.themeRevision) && value.themeRevision > 0;
|
|
6
|
+
}
|
|
1
7
|
/** Correlation alone does not validate a message type or its payload. */
|
|
2
8
|
export function isFrameIdentity(value) {
|
|
3
9
|
return typeof value === "object" && value !== null &&
|
package/lib/contracts/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { isFrameIdentity } from "./frame.js";
|
|
1
|
+
export { isFrameIdentity, isThemePayload } from "./frame.js";
|
|
2
2
|
export { isExecutionPolicy, isPageResponse, isRenderExecution } from "./page.js";
|
package/lib/frame-controller.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { record } from "./chrome-api.js";
|
|
2
|
-
|
|
2
|
+
import { isThemePayload } from "./contracts/frame.js";
|
|
3
|
+
export function createFrameController({ sessionId, host, request, suspended, transitioning = () => { }, failed, now = Date.now, activated = () => { }, diagnostic = () => { }, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, }) {
|
|
3
4
|
const state = {
|
|
4
5
|
key: null, generation: 0, renderId: null, capability: null, sourceHash: null,
|
|
5
6
|
execution: null, policy: null, phase: { kind: "loading" }, readyAt: 0,
|
|
@@ -9,7 +10,7 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
9
10
|
const timers = new Set();
|
|
10
11
|
const flushes = new Map();
|
|
11
12
|
let readinessTimer = null;
|
|
12
|
-
let
|
|
13
|
+
let configurationTimer = null;
|
|
13
14
|
let readinessRetries = 0;
|
|
14
15
|
let initialLoad = false;
|
|
15
16
|
let readyGeneration = null;
|
|
@@ -17,6 +18,10 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
17
18
|
let flushSequence = 0;
|
|
18
19
|
let configuration = null;
|
|
19
20
|
let expectedConfiguration = null;
|
|
21
|
+
let desired = { theme: "light", themeRevision: 1 };
|
|
22
|
+
let currentTheme = null;
|
|
23
|
+
let previousTheme = null;
|
|
24
|
+
let retainedCandidate = null;
|
|
20
25
|
const alive = () => state.phase.kind !== "disposed";
|
|
21
26
|
const loading = () => state.phase.kind !== "ready";
|
|
22
27
|
const publish = () => { if (alive())
|
|
@@ -49,7 +54,114 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
49
54
|
...message, capability: state.capability, generation: state.generation, pageKey: state.key,
|
|
50
55
|
}, state.policy?.targetOrigin || "*");
|
|
51
56
|
}
|
|
52
|
-
function
|
|
57
|
+
function cancelTheme(channel) {
|
|
58
|
+
if (!channel)
|
|
59
|
+
return;
|
|
60
|
+
cancelTimer(channel.timer);
|
|
61
|
+
channel.timer = null;
|
|
62
|
+
channel.pending = null;
|
|
63
|
+
channel.failed = false;
|
|
64
|
+
channel.settle?.(false);
|
|
65
|
+
channel.settle = null;
|
|
66
|
+
}
|
|
67
|
+
function clearPreviousTheme() {
|
|
68
|
+
cancelTheme(previousTheme);
|
|
69
|
+
previousTheme = null;
|
|
70
|
+
publish();
|
|
71
|
+
}
|
|
72
|
+
const stopRemoval = host.onPreviousRemoved(clearPreviousTheme);
|
|
73
|
+
const themeApplied = (channel) => channel?.applied?.themeRevision === desired.themeRevision && channel.applied.theme === desired.theme &&
|
|
74
|
+
!channel.pending && !channel.failed;
|
|
75
|
+
function handoff() {
|
|
76
|
+
if (!host.previous || loading() || !expectedConfiguration ||
|
|
77
|
+
state.configurationGeneration !== state.generation || !themeApplied(currentTheme))
|
|
78
|
+
return;
|
|
79
|
+
const started = identity();
|
|
80
|
+
const appliedConfiguration = expectedConfiguration;
|
|
81
|
+
const revision = desired.themeRevision;
|
|
82
|
+
host.afterPaint(() => {
|
|
83
|
+
if (matches(started) && !loading() && expectedConfiguration === appliedConfiguration &&
|
|
84
|
+
state.configurationGeneration === state.generation && desired.themeRevision === revision &&
|
|
85
|
+
themeApplied(currentTheme))
|
|
86
|
+
finishReplacement();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function exposeReady() {
|
|
90
|
+
if (state.phase.kind !== "confirming" || !state.execution || !themeApplied(currentTheme))
|
|
91
|
+
return;
|
|
92
|
+
state.phase = { kind: "ready" };
|
|
93
|
+
host.ready(state.execution);
|
|
94
|
+
publish();
|
|
95
|
+
activated(state.execution);
|
|
96
|
+
}
|
|
97
|
+
function requestTheme(channel) {
|
|
98
|
+
cancelTheme(channel);
|
|
99
|
+
channel.pending = { ...desired };
|
|
100
|
+
const pending = channel.pending;
|
|
101
|
+
const promise = new Promise((resolve) => { channel.settle = resolve; });
|
|
102
|
+
channel.timer = later(() => {
|
|
103
|
+
if (channel.pending !== pending)
|
|
104
|
+
return;
|
|
105
|
+
channel.timer = null;
|
|
106
|
+
channel.pending = null;
|
|
107
|
+
channel.failed = true;
|
|
108
|
+
channel.settle?.(false);
|
|
109
|
+
channel.settle = null;
|
|
110
|
+
publish();
|
|
111
|
+
}, 3000);
|
|
112
|
+
const message = {
|
|
113
|
+
type: "eh:setTheme", ...pending, capability: channel.capability,
|
|
114
|
+
generation: channel.generation, pageKey: channel.pageKey,
|
|
115
|
+
};
|
|
116
|
+
if (channel === currentTheme)
|
|
117
|
+
host.send(message, channel.policy.targetOrigin);
|
|
118
|
+
else if (channel.source && channel.source === host.previousWindow) {
|
|
119
|
+
channel.source.postMessage(message, channel.policy.targetOrigin);
|
|
120
|
+
}
|
|
121
|
+
publish();
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
function handleThemeMessage(event) {
|
|
125
|
+
const value = event.data;
|
|
126
|
+
if (!alive() || !value || typeof value !== "object" || !("type" in value) ||
|
|
127
|
+
(value.type !== "eh:themeApplied" && value.type !== "eh:setTheme"))
|
|
128
|
+
return false;
|
|
129
|
+
const channel = [currentTheme, previousTheme].find((candidate) => candidate && candidate.source &&
|
|
130
|
+
candidate.source === event.source && candidate.policy.incomingOrigin === event.origin &&
|
|
131
|
+
"capability" in value && value.capability === candidate.capability &&
|
|
132
|
+
"generation" in value && value.generation === candidate.generation &&
|
|
133
|
+
"pageKey" in value && value.pageKey === candidate.pageKey);
|
|
134
|
+
if (!channel)
|
|
135
|
+
return true;
|
|
136
|
+
if (channel === previousTheme && channel.source !== host.previousWindow) {
|
|
137
|
+
clearPreviousTheme();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (value.type !== "eh:themeApplied" || !isThemePayload(value)) {
|
|
141
|
+
if (!channel.diagnosed) {
|
|
142
|
+
channel.diagnosed = true;
|
|
143
|
+
diagnostic("invalid-theme-acknowledgment");
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (!channel.pending || channel.pending.themeRevision !== value.themeRevision ||
|
|
148
|
+
channel.pending.theme !== value.theme)
|
|
149
|
+
return true;
|
|
150
|
+
channel.applied = { theme: value.theme, themeRevision: value.themeRevision };
|
|
151
|
+
cancelTimer(channel.timer);
|
|
152
|
+
channel.timer = null;
|
|
153
|
+
channel.pending = null;
|
|
154
|
+
channel.failed = false;
|
|
155
|
+
channel.settle?.(true);
|
|
156
|
+
channel.settle = null;
|
|
157
|
+
if (channel === currentTheme) {
|
|
158
|
+
exposeReady();
|
|
159
|
+
handoff();
|
|
160
|
+
}
|
|
161
|
+
publish();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function suspend(preserveTheme = false) {
|
|
53
165
|
if (!alive())
|
|
54
166
|
return;
|
|
55
167
|
suspended();
|
|
@@ -57,10 +169,17 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
57
169
|
flush.settle(false);
|
|
58
170
|
configuration?.settle(false);
|
|
59
171
|
expectedConfiguration = null;
|
|
172
|
+
cancelTheme(currentTheme);
|
|
173
|
+
cancelTheme(previousTheme);
|
|
174
|
+
if (!preserveTheme) {
|
|
175
|
+
previousTheme = null;
|
|
176
|
+
retainedCandidate = null;
|
|
177
|
+
}
|
|
178
|
+
currentTheme = null;
|
|
60
179
|
for (const timer of timers)
|
|
61
180
|
clearTimer(timer);
|
|
62
181
|
timers.clear();
|
|
63
|
-
readinessTimer =
|
|
182
|
+
readinessTimer = configurationTimer = null;
|
|
64
183
|
state.capability = null;
|
|
65
184
|
state.renderId = null;
|
|
66
185
|
state.execution = null;
|
|
@@ -73,7 +192,8 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
73
192
|
if (!alive())
|
|
74
193
|
throw new Error("Review ended");
|
|
75
194
|
transitioning();
|
|
76
|
-
|
|
195
|
+
retainedCandidate = currentTheme || retainedCandidate;
|
|
196
|
+
suspend(true);
|
|
77
197
|
state.key = key;
|
|
78
198
|
state.generation++;
|
|
79
199
|
state.sourceHash = null;
|
|
@@ -83,8 +203,8 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
83
203
|
return state.generation;
|
|
84
204
|
}
|
|
85
205
|
function finishReplacement() {
|
|
86
|
-
cancelTimer(
|
|
87
|
-
|
|
206
|
+
cancelTimer(configurationTimer);
|
|
207
|
+
configurationTimer = null;
|
|
88
208
|
host.finishReplacement();
|
|
89
209
|
publish();
|
|
90
210
|
}
|
|
@@ -117,6 +237,13 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
117
237
|
state.phase = { kind: "loading" };
|
|
118
238
|
initialLoad = true;
|
|
119
239
|
host.navigate(value.path, state.reloading);
|
|
240
|
+
if (previousTheme?.source !== host.previousWindow)
|
|
241
|
+
clearPreviousTheme();
|
|
242
|
+
if (!previousTheme && retainedCandidate?.source === host.previousWindow)
|
|
243
|
+
previousTheme = retainedCandidate;
|
|
244
|
+
retainedCandidate = null;
|
|
245
|
+
if (previousTheme && !themeApplied(previousTheme))
|
|
246
|
+
void requestTheme(previousTheme);
|
|
120
247
|
publish();
|
|
121
248
|
readinessTimer = later(() => {
|
|
122
249
|
if (state.key !== key || state.generation !== generation || !loading())
|
|
@@ -181,10 +308,15 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
181
308
|
feedbackOnly: value.savePolicy === "feedback-only",
|
|
182
309
|
executionNotice: typeof value.executionNotice === "string" ? value.executionNotice : null,
|
|
183
310
|
};
|
|
184
|
-
state.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
311
|
+
if (!state.policy || !state.key)
|
|
312
|
+
throw new Error("Missing frame policy.");
|
|
313
|
+
currentTheme = {
|
|
314
|
+
source: host.currentWindow, capability: state.capability, generation: state.generation,
|
|
315
|
+
pageKey: state.key, policy: { ...state.policy }, applied: null, pending: null,
|
|
316
|
+
failed: false, timer: null, settle: null, diagnosed: false,
|
|
317
|
+
};
|
|
318
|
+
await requestTheme(currentTheme);
|
|
319
|
+
return matches(started) && !loading() ? state.execution : null;
|
|
188
320
|
}
|
|
189
321
|
catch (error) {
|
|
190
322
|
if (matches(started)) {
|
|
@@ -198,9 +330,9 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
198
330
|
function configurationTimeout() {
|
|
199
331
|
if (!host.previous)
|
|
200
332
|
return;
|
|
201
|
-
cancelTimer(
|
|
333
|
+
cancelTimer(configurationTimer);
|
|
202
334
|
const started = identity();
|
|
203
|
-
|
|
335
|
+
configurationTimer = later(() => {
|
|
204
336
|
if (matches(started) && host.previous) {
|
|
205
337
|
fail("The page did not confirm its review settings. Reload before continuing.");
|
|
206
338
|
}
|
|
@@ -211,6 +343,7 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
211
343
|
return Promise.resolve(false);
|
|
212
344
|
configuration?.settle(false);
|
|
213
345
|
expectedConfiguration = { mode, savePolicy };
|
|
346
|
+
state.configurationGeneration = null;
|
|
214
347
|
configurationTimeout();
|
|
215
348
|
if (!wait) {
|
|
216
349
|
send({ type: "eh:configureReview", mode, savePolicy });
|
|
@@ -252,6 +385,40 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
252
385
|
return {
|
|
253
386
|
get state() { return state; },
|
|
254
387
|
get loading() { return loading(); },
|
|
388
|
+
get themeSync() {
|
|
389
|
+
const channels = [currentTheme, previousTheme].filter((channel) => channel !== null);
|
|
390
|
+
const failure = channels.some((channel) => channel.failed);
|
|
391
|
+
return {
|
|
392
|
+
desired: { ...desired },
|
|
393
|
+
status: failure ? "failed" : channels.some((channel) => channel.pending) ? "pending" :
|
|
394
|
+
themeApplied(currentTheme) ? "applied" : "idle",
|
|
395
|
+
appliedRevision: currentTheme?.applied?.themeRevision ?? null,
|
|
396
|
+
message: failure ? "Annotation theme synchronization was not confirmed. Your page and drafts are unchanged. Retry theme without reloading." : null,
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
setTheme(theme) {
|
|
400
|
+
if (!alive() || (theme !== "light" && theme !== "dark") || desired.theme === theme)
|
|
401
|
+
return;
|
|
402
|
+
if (desired.themeRevision === Number.MAX_SAFE_INTEGER)
|
|
403
|
+
throw new Error("Theme revision limit reached.");
|
|
404
|
+
desired = { theme, themeRevision: desired.themeRevision + 1 };
|
|
405
|
+
if (currentTheme)
|
|
406
|
+
void requestTheme(currentTheme);
|
|
407
|
+
if (previousTheme && previousTheme.source === host.previousWindow)
|
|
408
|
+
void requestTheme(previousTheme);
|
|
409
|
+
publish();
|
|
410
|
+
},
|
|
411
|
+
async retryTheme() {
|
|
412
|
+
if (!alive())
|
|
413
|
+
return false;
|
|
414
|
+
const pending = [];
|
|
415
|
+
if (currentTheme)
|
|
416
|
+
pending.push(requestTheme(currentTheme));
|
|
417
|
+
if (previousTheme && previousTheme.source === host.previousWindow)
|
|
418
|
+
pending.push(requestTheme(previousTheme));
|
|
419
|
+
return pending.length > 0 && (await Promise.all(pending)).every(Boolean);
|
|
420
|
+
},
|
|
421
|
+
handleThemeMessage,
|
|
255
422
|
identity, matches, begin, register, suspend, ready, configure, flush, send, fail,
|
|
256
423
|
subscribe(listener) {
|
|
257
424
|
listeners.add(listener);
|
|
@@ -277,16 +444,20 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
277
444
|
return false;
|
|
278
445
|
return value.capability === state.capability && value.generation === state.generation &&
|
|
279
446
|
value.pageKey === state.key && typeof value.type === "string" &&
|
|
280
|
-
(!loading() || value.type === "eh:ready"
|
|
447
|
+
(!loading() || value.type === "eh:ready" ||
|
|
448
|
+
(state.phase.kind === "confirming" && value.type === "eh:themeApplied" &&
|
|
449
|
+
isThemePayload(value) && value.theme === currentTheme?.pending?.theme &&
|
|
450
|
+
value.themeRevision === currentTheme.pending.themeRevision));
|
|
281
451
|
},
|
|
282
452
|
configured(mode, savePolicy) {
|
|
283
453
|
if (!expectedConfiguration || expectedConfiguration.mode !== mode ||
|
|
284
454
|
expectedConfiguration.savePolicy !== savePolicy || loading())
|
|
285
455
|
return false;
|
|
456
|
+
// Confirmation is complete even when background tabs pause the visual handoff.
|
|
457
|
+
cancelTimer(configurationTimer);
|
|
458
|
+
configurationTimer = null;
|
|
286
459
|
state.configurationGeneration = state.generation;
|
|
287
|
-
|
|
288
|
-
host.afterPaint(() => { if (!loading())
|
|
289
|
-
finishReplacement(); });
|
|
460
|
+
handoff();
|
|
290
461
|
if (configuration && configuration.mode === mode && configuration.savePolicy === savePolicy) {
|
|
291
462
|
configuration.settle(true);
|
|
292
463
|
}
|
|
@@ -306,6 +477,7 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
306
477
|
suspend();
|
|
307
478
|
state.phase = { kind: "disposed" };
|
|
308
479
|
stopLoad();
|
|
480
|
+
stopRemoval();
|
|
309
481
|
host.dispose();
|
|
310
482
|
listeners.clear();
|
|
311
483
|
},
|
package/lib/frame-host.js
CHANGED
|
@@ -4,6 +4,7 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
4
4
|
let disposed = false;
|
|
5
5
|
const loadListeners = new Set();
|
|
6
6
|
const focusListeners = new Set();
|
|
7
|
+
const removalListeners = new Set();
|
|
7
8
|
const detach = new Map();
|
|
8
9
|
const paintFrames = new Set();
|
|
9
10
|
function paint(callback) {
|
|
@@ -41,6 +42,12 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
41
42
|
return {
|
|
42
43
|
get current() { return current; },
|
|
43
44
|
get previous() { return previous; },
|
|
45
|
+
get currentWindow() { return current.contentWindow; },
|
|
46
|
+
get previousWindow() { return previous?.contentWindow || null; },
|
|
47
|
+
onPreviousRemoved(listener) {
|
|
48
|
+
removalListeners.add(listener);
|
|
49
|
+
return () => { removalListeners.delete(listener); };
|
|
50
|
+
},
|
|
44
51
|
get visibleExecution() {
|
|
45
52
|
const visible = previous || current;
|
|
46
53
|
const savePolicy = visible.dataset.savePolicy;
|
|
@@ -98,6 +105,8 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
98
105
|
if (previous)
|
|
99
106
|
remove(previous);
|
|
100
107
|
previous = null;
|
|
108
|
+
for (const listener of removalListeners)
|
|
109
|
+
listener();
|
|
101
110
|
delete current.dataset.replacing;
|
|
102
111
|
},
|
|
103
112
|
suspend() { current.removeAttribute("data-sdk-ready"); },
|
|
@@ -131,6 +140,7 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
131
140
|
paintFrames.clear();
|
|
132
141
|
loadListeners.clear();
|
|
133
142
|
focusListeners.clear();
|
|
143
|
+
removalListeners.clear();
|
|
134
144
|
},
|
|
135
145
|
};
|
|
136
146
|
}
|
package/lib/paths.js
CHANGED
|
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
-
// Bump
|
|
6
|
-
//
|
|
7
|
-
export const SERVER_PROTOCOL =
|
|
5
|
+
// Bump for incompatible CLI/server or shell/SDK contracts, including readiness.
|
|
6
|
+
// A new CLI must not silently reuse an older background server.
|
|
7
|
+
export const SERVER_PROTOCOL = 18;
|
|
8
8
|
export function serverProtocolMatches(protocol) {
|
|
9
9
|
return Number(protocol) === SERVER_PROTOCOL;
|
|
10
10
|
}
|
|
@@ -26,6 +26,9 @@ export function createRecoveryController(options) {
|
|
|
26
26
|
reloadMessage: context.reload.message, reloadError: context.reload.error,
|
|
27
27
|
canReload: !disposed && visible && context.reload.visible && !!context.identity.key && (!context.loading || !!context.frameError) && !reloadBusy,
|
|
28
28
|
reloadBusy,
|
|
29
|
+
themeVisible: visible && context.themeSync?.status === "failed",
|
|
30
|
+
themeMessage: context.themeSync?.message || "",
|
|
31
|
+
canRetryTheme: !disposed && visible && context.themeSync?.status === "failed" && !!options.retryTheme,
|
|
29
32
|
status: visible && !context.reload.visible ? (context.frameError || error || view.status || (busy ? "Updating page recovery settings…" : "")) : "",
|
|
30
33
|
statusError: !!context.frameError || !!error,
|
|
31
34
|
};
|
|
@@ -105,6 +108,12 @@ export function createRecoveryController(options) {
|
|
|
105
108
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, publish,
|
|
106
109
|
commands: {
|
|
107
110
|
setMenuOpen, recover, reload,
|
|
111
|
+
async retryTheme() {
|
|
112
|
+
if (!project().canRetryTheme)
|
|
113
|
+
return;
|
|
114
|
+
await options.retryTheme?.();
|
|
115
|
+
publish();
|
|
116
|
+
},
|
|
108
117
|
keepCurrent() {
|
|
109
118
|
if (disposed || !project().reloadVisible || reloadBusy)
|
|
110
119
|
return;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Build-time only. Browser styles are generated, never imported from this module.
|
|
2
|
+
export const REVIEW_PALETTE = {
|
|
3
|
+
light: {
|
|
4
|
+
background: "#F5F2EA",
|
|
5
|
+
foreground: "#292E2B",
|
|
6
|
+
card: "#FFFDF7",
|
|
7
|
+
"card-foreground": "#292E2B",
|
|
8
|
+
popover: "#FFFDF7",
|
|
9
|
+
"popover-foreground": "#292E2B",
|
|
10
|
+
primary: "#17685F",
|
|
11
|
+
"primary-foreground": "#FFFDF7",
|
|
12
|
+
secondary: "#ECEDE5",
|
|
13
|
+
"secondary-foreground": "#626B65",
|
|
14
|
+
muted: "#ECEDE5",
|
|
15
|
+
"muted-foreground": "#626B65",
|
|
16
|
+
accent: "#E1EEEA",
|
|
17
|
+
"accent-foreground": "#17685F",
|
|
18
|
+
destructive: "#B43C48",
|
|
19
|
+
"destructive-foreground": "#FFFDF7",
|
|
20
|
+
border: "#D8D8CC",
|
|
21
|
+
input: "#7C877D",
|
|
22
|
+
ring: "#17685F",
|
|
23
|
+
"review-added": "#EFF3E2",
|
|
24
|
+
"review-added-foreground": "#4D6B24",
|
|
25
|
+
"review-added-border": "#82945E",
|
|
26
|
+
"review-removed": "#FCECEF",
|
|
27
|
+
"review-removed-foreground": "#B43C48",
|
|
28
|
+
"review-removed-border": "#C88791",
|
|
29
|
+
"review-modified": "#FFF1D9",
|
|
30
|
+
"review-modified-foreground": "#915B13",
|
|
31
|
+
"review-modified-border": "#B99157",
|
|
32
|
+
"review-count-added": "#4D6B24",
|
|
33
|
+
"review-count-removed": "#B43C48",
|
|
34
|
+
"review-count-modified": "#915B13",
|
|
35
|
+
"review-insert": "#DCE7C5",
|
|
36
|
+
"review-delete": "#FAE2E7",
|
|
37
|
+
"review-overlay": "#191F1D80",
|
|
38
|
+
"review-shadow-color": "#191F1D26",
|
|
39
|
+
"annotation-foreground": "#75470E",
|
|
40
|
+
"annotation-background": "#FFF1D9",
|
|
41
|
+
"annotation-active": "#F5D6A3",
|
|
42
|
+
"annotation-border": "#915B13",
|
|
43
|
+
"annotation-tint": "#915B131A",
|
|
44
|
+
"annotation-active-tint": "#915B132E",
|
|
45
|
+
"halo-light": "#FFFDF7",
|
|
46
|
+
"halo-dark": "#191F1D",
|
|
47
|
+
},
|
|
48
|
+
dark: {
|
|
49
|
+
background: "#191F1D",
|
|
50
|
+
foreground: "#EEEFE6",
|
|
51
|
+
card: "#222A26",
|
|
52
|
+
"card-foreground": "#EEEFE6",
|
|
53
|
+
popover: "#222A26",
|
|
54
|
+
"popover-foreground": "#EEEFE6",
|
|
55
|
+
primary: "#85C7B8",
|
|
56
|
+
"primary-foreground": "#102F29",
|
|
57
|
+
secondary: "#303A33",
|
|
58
|
+
"secondary-foreground": "#AFB9AF",
|
|
59
|
+
muted: "#303A33",
|
|
60
|
+
"muted-foreground": "#AFB9AF",
|
|
61
|
+
accent: "#293F37",
|
|
62
|
+
"accent-foreground": "#85C7B8",
|
|
63
|
+
destructive: "#F2A2AB",
|
|
64
|
+
"destructive-foreground": "#40272C",
|
|
65
|
+
border: "#3C4841",
|
|
66
|
+
input: "#77877B",
|
|
67
|
+
ring: "#85C7B8",
|
|
68
|
+
"review-added": "#2B3522",
|
|
69
|
+
"review-added-foreground": "#BBCD87",
|
|
70
|
+
"review-added-border": "#6F8347",
|
|
71
|
+
"review-removed": "#40272C",
|
|
72
|
+
"review-removed-foreground": "#F2A2AB",
|
|
73
|
+
"review-removed-border": "#A96572",
|
|
74
|
+
"review-modified": "#3D3020",
|
|
75
|
+
"review-modified-foreground": "#E7BB72",
|
|
76
|
+
"review-modified-border": "#9C7A46",
|
|
77
|
+
"review-count-added": "#BBCD87",
|
|
78
|
+
"review-count-removed": "#F2A2AB",
|
|
79
|
+
"review-count-modified": "#E7BB72",
|
|
80
|
+
"review-insert": "#3E4D2D",
|
|
81
|
+
"review-delete": "#633944",
|
|
82
|
+
"review-overlay": "#191F1DB3",
|
|
83
|
+
"review-shadow-color": "#00000066",
|
|
84
|
+
"annotation-foreground": "#F1CC8E",
|
|
85
|
+
"annotation-background": "#3D3020",
|
|
86
|
+
"annotation-active": "#594325",
|
|
87
|
+
"annotation-border": "#E7BB72",
|
|
88
|
+
"annotation-tint": "#E7BB721A",
|
|
89
|
+
"annotation-active-tint": "#E7BB722E",
|
|
90
|
+
"halo-light": "#FFFDF7",
|
|
91
|
+
"halo-dark": "#191F1D",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
export const BRAND_COLORS = {
|
|
95
|
+
tile: REVIEW_PALETTE.light.primary,
|
|
96
|
+
bubble: REVIEW_PALETTE.light.card,
|
|
97
|
+
};
|