@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.
@@ -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
- const readyIdentity = currentRender();
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__">
@@ -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 &&
@@ -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";
@@ -1,5 +1,6 @@
1
1
  import { record } from "./chrome-api.js";
2
- export function createFrameController({ sessionId, host, request, suspended, transitioning = () => { }, failed, now = Date.now, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, }) {
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 replacementTimer = null;
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 suspend() {
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 = replacementTimer = null;
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
- suspend();
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(replacementTimer);
87
- replacementTimer = null;
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.phase = { kind: "ready" };
185
- host.ready(state.execution);
186
- publish();
187
- return state.execution;
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(replacementTimer);
333
+ cancelTimer(configurationTimer);
202
334
  const started = identity();
203
- replacementTimer = later(() => {
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
- if (host.previous)
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 this when the CLI and detached server no longer share the same request
6
- // contract. A new CLI must not silently reuse an older background server.
7
- export const SERVER_PROTOCOL = 17;
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
+ };