camox 0.65.1 → 0.67.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.
Files changed (42) hide show
  1. package/dist/features/canvas/CamoxCanvas.js +195 -173
  2. package/dist/features/canvas/canvasFrameCapture.js +5 -12
  3. package/dist/features/canvas/canvasFrameDocument.js +90 -29
  4. package/dist/features/canvas/canvasItems.js +22 -6
  5. package/dist/features/canvas/sidebar/ContentSidebar.js +19 -21
  6. package/dist/features/canvas/sidebar/LayoutPageOverview.js +22 -49
  7. package/dist/features/canvas/sidebar/LayoutPageSeo.js +73 -76
  8. package/dist/features/canvas/sidebar/PageInspector.js +61 -50
  9. package/dist/features/canvas/sidebar/PageInspectorSkeleton.js +4 -4
  10. package/dist/features/canvas/useCanvasCamera.js +22 -5
  11. package/dist/features/content/components/MissingFromLocale.js +1 -1
  12. package/dist/features/page/UnplacedLayoutContent.js +141 -0
  13. package/dist/features/preview/components/ItemFieldsEditor.js +50 -44
  14. package/dist/features/preview/components/PageInfo.js +103 -100
  15. package/dist/features/preview/components/SelectionEditor.js +284 -231
  16. package/dist/features/preview/components/contentFieldSchema.js +5 -1
  17. package/dist/features/runtime/RuntimeChrome.js +66 -40
  18. package/dist/features/runtime/pageApp.js +24 -10
  19. package/dist/features/runtime/runtime.d.ts +13 -0
  20. package/dist/features/runtime/runtime.js +58 -2
  21. package/dist/features/studio/components/AgentSidebar.js +3 -3
  22. package/dist/features/studio/components/ChatWidget.js +34 -29
  23. package/dist/features/studio/components/ContentFindings.js +397 -0
  24. package/dist/features/studio/components/DraftsControl.js +172 -159
  25. package/dist/features/studio/components/Patrol.js +449 -426
  26. package/dist/features/studio/components/ReviewDraftSheet.js +231 -192
  27. package/dist/features/studio/components/Squareling.js +0 -9
  28. package/dist/features/studio/components/ThreadComposer.js +183 -121
  29. package/dist/features/studio/components/ThreadMessages.js +19 -11
  30. package/dist/features/studio/components/UnnamedDraftBar.js +349 -0
  31. package/dist/features/studio/findingSentence.js +3 -5
  32. package/dist/features/studio/patrollerBio.js +39 -0
  33. package/dist/features/studio/reportSentence.js +0 -5
  34. package/dist/features/studio/studioStore.js +13 -0
  35. package/dist/features/studio/workingSentence.js +2 -11
  36. package/dist/lib/revisions.js +25 -3
  37. package/dist/lib/use-thread.js +1 -1
  38. package/dist/package.js +1 -1
  39. package/dist/studio.css +1 -1
  40. package/package.json +4 -4
  41. package/dist/features/canvas/sidebar/DerivedPageInspector.js +0 -49
  42. package/dist/features/canvas/sidebar/SingletonPageInspector.js +0 -39
@@ -0,0 +1,349 @@
1
+ import { reads, useReadQuery } from "../../../lib/studio-reads.js";
2
+ import { cn } from "../../../lib/utils.js";
3
+ import { studioStore } from "../studioStore.js";
4
+ import { plural } from "../draftLabel.js";
5
+ import { useRenameDraft } from "./ReviewDraftSheet.js";
6
+ import { useDrafts } from "./DraftsControl.js";
7
+ import { c } from "react/compiler-runtime";
8
+ import * as React from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { useSelector } from "@xstate/store-react";
11
+ import { Button } from "@camox/ui/button";
12
+ import { Input } from "@camox/ui/input";
13
+ import { toast } from "@camox/ui/toaster";
14
+ import { Spinner } from "@camox/ui/spinner";
15
+ import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@camox/ui/popover";
16
+ import { FloatingToolbar } from "@camox/ui/floating-toolbar";
17
+
18
+ //#region src/features/studio/components/UnnamedDraftBar.tsx
19
+ /**
20
+ * How long a draft goes without a change before the bar shows, so it doesn't pop up mid-edit.
21
+ */
22
+ const SETTLE_MS = 1500;
23
+ /**
24
+ * Asks what to do with an unnamed draft, which only editing on Live starts: discard its
25
+ * changes, keep it as a draft by naming it, or publish it. It stays up until one of them ends
26
+ * the draft or names it, and stands in for the navbar's pending changes until then.
27
+ */
28
+ function UnnamedDraftBar(t0) {
29
+ const $ = c(23);
30
+ const { stacked } = t0;
31
+ const { projectId, current } = useDrafts();
32
+ const unnamed = current?.name === null ? current : null;
33
+ const keeping = useSelector(studioStore, _temp);
34
+ const settled = useSettled(unnamed?.id ?? null, unnamed?.lastChangedAt ?? null);
35
+ if (!projectId || !unnamed || !(settled || keeping)) return null;
36
+ const empty = unnamed.changeCount === 0;
37
+ const t1 = stacked ? "bottom-16" : "bottom-2";
38
+ let t2;
39
+ if ($[0] !== t1) {
40
+ t2 = cn("animate-in fade-in slide-in-from-bottom-4 fixed w-max gap-6 duration-300", t1);
41
+ $[0] = t1;
42
+ $[1] = t2;
43
+ } else t2 = $[1];
44
+ let t3;
45
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
46
+ t3 = /* @__PURE__ */ jsx("span", {
47
+ "aria-hidden": true,
48
+ className: "size-2 shrink-0 rounded-full bg-blue-500"
49
+ });
50
+ $[2] = t3;
51
+ } else t3 = $[2];
52
+ let t4;
53
+ if ($[3] !== empty || $[4] !== unnamed.changeCount) {
54
+ t4 = empty ? "No changes yet" : plural(unnamed.changeCount, "pending change");
55
+ $[3] = empty;
56
+ $[4] = unnamed.changeCount;
57
+ $[5] = t4;
58
+ } else t4 = $[5];
59
+ let t5;
60
+ if ($[6] !== t4) {
61
+ t5 = /* @__PURE__ */ jsxs("span", {
62
+ className: "flex items-center gap-2 px-2 text-sm",
63
+ children: [t3, t4]
64
+ });
65
+ $[6] = t4;
66
+ $[7] = t5;
67
+ } else t5 = $[7];
68
+ let t6;
69
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
70
+ t6 = /* @__PURE__ */ jsx(Button, {
71
+ type: "button",
72
+ variant: "outline",
73
+ onClick: _temp2,
74
+ children: "Discard"
75
+ });
76
+ $[8] = t6;
77
+ } else t6 = $[8];
78
+ let t7;
79
+ if ($[9] !== empty || $[10] !== keeping || $[11] !== projectId || $[12] !== unnamed) {
80
+ t7 = /* @__PURE__ */ jsx(KeepDraft, {
81
+ projectId,
82
+ draft: unnamed,
83
+ open: keeping,
84
+ disabled: empty
85
+ });
86
+ $[9] = empty;
87
+ $[10] = keeping;
88
+ $[11] = projectId;
89
+ $[12] = unnamed;
90
+ $[13] = t7;
91
+ } else t7 = $[13];
92
+ let t8;
93
+ if ($[14] !== empty) {
94
+ t8 = /* @__PURE__ */ jsx(Button, {
95
+ type: "button",
96
+ disabled: empty,
97
+ onClick: _temp3,
98
+ children: "Publish…"
99
+ });
100
+ $[14] = empty;
101
+ $[15] = t8;
102
+ } else t8 = $[15];
103
+ let t9;
104
+ if ($[16] !== t7 || $[17] !== t8) {
105
+ t9 = /* @__PURE__ */ jsxs("div", {
106
+ className: "flex items-center gap-2",
107
+ children: [
108
+ t6,
109
+ t7,
110
+ t8
111
+ ]
112
+ });
113
+ $[16] = t7;
114
+ $[17] = t8;
115
+ $[18] = t9;
116
+ } else t9 = $[18];
117
+ let t10;
118
+ if ($[19] !== t2 || $[20] !== t5 || $[21] !== t9) {
119
+ t10 = /* @__PURE__ */ jsxs(FloatingToolbar, {
120
+ "aria-label": "Unnamed draft",
121
+ className: t2,
122
+ children: [t5, t9]
123
+ });
124
+ $[19] = t2;
125
+ $[20] = t5;
126
+ $[21] = t9;
127
+ $[22] = t10;
128
+ } else t10 = $[22];
129
+ return t10;
130
+ }
131
+ function _temp3() {
132
+ return studioStore.send({ type: "openReviewDraftSheet" });
133
+ }
134
+ function _temp2() {
135
+ return studioStore.send({ type: "openDeleteDraftDialog" });
136
+ }
137
+ function _temp(state) {
138
+ return state.context.isKeepDraftOpen;
139
+ }
140
+ /**
141
+ * Whether the draft went without a change for `SETTLE_MS`. Once settled it stays so for that
142
+ * draft, so the bar doesn't hide again on the next edit.
143
+ */
144
+ function useSettled(draftId, lastChangedAt) {
145
+ const $ = c(5);
146
+ const [settledDraft, setSettledDraft] = React.useState(null);
147
+ let t0;
148
+ let t1;
149
+ if ($[0] !== draftId || $[1] !== lastChangedAt || $[2] !== settledDraft) {
150
+ t0 = () => {
151
+ if (!draftId || settledDraft === draftId) return;
152
+ const wait = Math.max(0, (lastChangedAt ?? 0) + SETTLE_MS - Date.now());
153
+ const timer = setTimeout(() => setSettledDraft(draftId), wait);
154
+ return () => clearTimeout(timer);
155
+ };
156
+ t1 = [
157
+ draftId,
158
+ lastChangedAt,
159
+ settledDraft
160
+ ];
161
+ $[0] = draftId;
162
+ $[1] = lastChangedAt;
163
+ $[2] = settledDraft;
164
+ $[3] = t0;
165
+ $[4] = t1;
166
+ } else {
167
+ t0 = $[3];
168
+ t1 = $[4];
169
+ }
170
+ React.useEffect(t0, t1);
171
+ return !!draftId && settledDraft === draftId;
172
+ }
173
+ /** "Keep as draft": names the draft, which keeps it and ends the bar. */
174
+ function KeepDraft(t0) {
175
+ const $ = c(15);
176
+ const { projectId, draft, open, disabled } = t0;
177
+ let t1;
178
+ if ($[0] !== disabled) {
179
+ t1 = /* @__PURE__ */ jsx(Button, {
180
+ type: "button",
181
+ variant: "outline",
182
+ disabled
183
+ });
184
+ $[0] = disabled;
185
+ $[1] = t1;
186
+ } else t1 = $[1];
187
+ let t2;
188
+ if ($[2] !== disabled || $[3] !== t1) {
189
+ t2 = /* @__PURE__ */ jsx(PopoverTrigger, {
190
+ disabled,
191
+ render: t1,
192
+ children: "Keep as draft"
193
+ });
194
+ $[2] = disabled;
195
+ $[3] = t1;
196
+ $[4] = t2;
197
+ } else t2 = $[4];
198
+ let t3;
199
+ if ($[5] !== draft || $[6] !== open || $[7] !== projectId) {
200
+ t3 = open && /* @__PURE__ */ jsx(KeepDraftForm, {
201
+ projectId,
202
+ draft
203
+ });
204
+ $[5] = draft;
205
+ $[6] = open;
206
+ $[7] = projectId;
207
+ $[8] = t3;
208
+ } else t3 = $[8];
209
+ let t4;
210
+ if ($[9] !== t3) {
211
+ t4 = /* @__PURE__ */ jsx(PopoverContent, {
212
+ side: "top",
213
+ sideOffset: 16,
214
+ className: "w-80",
215
+ children: t3
216
+ });
217
+ $[9] = t3;
218
+ $[10] = t4;
219
+ } else t4 = $[10];
220
+ let t5;
221
+ if ($[11] !== open || $[12] !== t2 || $[13] !== t4) {
222
+ t5 = /* @__PURE__ */ jsxs(Popover, {
223
+ open,
224
+ onOpenChange: _temp4,
225
+ children: [t2, t4]
226
+ });
227
+ $[11] = open;
228
+ $[12] = t2;
229
+ $[13] = t4;
230
+ $[14] = t5;
231
+ } else t5 = $[14];
232
+ return t5;
233
+ }
234
+ function _temp4(next) {
235
+ return studioStore.send({ type: next ? "openKeepDraft" : "closeKeepDraft" });
236
+ }
237
+ /**
238
+ * The name to keep the draft under, filled with the short name its Draft Overview suggests until
239
+ * the editor types: AI names a few changes well, and better than a name typed before making them.
240
+ */
241
+ function KeepDraftForm(t0) {
242
+ const $ = c(24);
243
+ const { projectId, draft } = t0;
244
+ let t1;
245
+ if ($[0] !== draft.id || $[1] !== projectId) {
246
+ t1 = reads.publishPreview({
247
+ projectId,
248
+ draftId: draft.id
249
+ });
250
+ $[0] = draft.id;
251
+ $[1] = projectId;
252
+ $[2] = t1;
253
+ } else t1 = $[2];
254
+ const preview = useReadQuery(t1);
255
+ const overview = preview.data?.overview;
256
+ const suggested = overview?.status === "ready" ? overview.name : null;
257
+ const summarizing = !suggested && (preview.isPending || overview?.status === "pending");
258
+ const [typed, setTyped] = React.useState(null);
259
+ const name = typed ?? suggested ?? "";
260
+ const rename = useRenameDraft(projectId);
261
+ let t2;
262
+ if ($[3] !== draft.id || $[4] !== name || $[5] !== rename) {
263
+ t2 = (event) => {
264
+ event.preventDefault();
265
+ const kept = name.trim();
266
+ if (!kept) return;
267
+ rename.mutate({
268
+ draftId: draft.id,
269
+ name: kept
270
+ }, { onSuccess: () => {
271
+ studioStore.send({ type: "closeKeepDraft" });
272
+ toast.success(`Kept as "${kept}"`);
273
+ } });
274
+ };
275
+ $[3] = draft.id;
276
+ $[4] = name;
277
+ $[5] = rename;
278
+ $[6] = t2;
279
+ } else t2 = $[6];
280
+ let t3;
281
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
282
+ t3 = /* @__PURE__ */ jsxs(PopoverHeader, { children: [/* @__PURE__ */ jsx(PopoverTitle, { children: "Keep as draft" }), /* @__PURE__ */ jsx(PopoverDescription, { children: "Name it for what it changes, so you and your team can find it later." })] });
283
+ $[7] = t3;
284
+ } else t3 = $[7];
285
+ const t4 = summarizing ? "Summarizing your changes…" : "What does this draft change?";
286
+ let t5;
287
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
288
+ t5 = (event_0) => setTyped(event_0.target.value);
289
+ $[8] = t5;
290
+ } else t5 = $[8];
291
+ let t6;
292
+ if ($[9] !== name || $[10] !== t4) {
293
+ t6 = /* @__PURE__ */ jsx(Input, {
294
+ autoFocus: true,
295
+ "aria-label": "Draft name",
296
+ value: name,
297
+ maxLength: 200,
298
+ placeholder: t4,
299
+ onChange: t5
300
+ });
301
+ $[9] = name;
302
+ $[10] = t4;
303
+ $[11] = t6;
304
+ } else t6 = $[11];
305
+ let t7;
306
+ if ($[12] !== name || $[13] !== rename.isPending) {
307
+ t7 = !name.trim() || rename.isPending;
308
+ $[12] = name;
309
+ $[13] = rename.isPending;
310
+ $[14] = t7;
311
+ } else t7 = $[14];
312
+ let t8;
313
+ if ($[15] !== rename.isPending) {
314
+ t8 = rename.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : null;
315
+ $[15] = rename.isPending;
316
+ $[16] = t8;
317
+ } else t8 = $[16];
318
+ let t9;
319
+ if ($[17] !== t7 || $[18] !== t8) {
320
+ t9 = /* @__PURE__ */ jsxs(Button, {
321
+ type: "submit",
322
+ disabled: t7,
323
+ children: [t8, "Keep draft"]
324
+ });
325
+ $[17] = t7;
326
+ $[18] = t8;
327
+ $[19] = t9;
328
+ } else t9 = $[19];
329
+ let t10;
330
+ if ($[20] !== t2 || $[21] !== t6 || $[22] !== t9) {
331
+ t10 = /* @__PURE__ */ jsxs("form", {
332
+ className: "flex flex-col gap-3",
333
+ onSubmit: t2,
334
+ children: [
335
+ t3,
336
+ t6,
337
+ t9
338
+ ]
339
+ });
340
+ $[20] = t2;
341
+ $[21] = t6;
342
+ $[22] = t9;
343
+ $[23] = t10;
344
+ } else t10 = $[23];
345
+ return t10;
346
+ }
347
+
348
+ //#endregion
349
+ export { SETTLE_MS, UnnamedDraftBar };
@@ -1,15 +1,14 @@
1
1
  import { localeName } from "./localeOptions.js";
2
- import { findingSentence } from "@camox/api-contract/patrol";
2
+ import { findingSentence, findingSentenceInPlace } from "@camox/api-contract/patrol";
3
3
 
4
4
  //#region src/features/studio/findingSentence.ts
5
5
  /** Quoted as editors read it. */
6
6
  const quoted = (text) => `“${text}”`;
7
- /** What a dismissal dismissed, briefly: "Links to About", or the text it was about. */
7
+ /** What a dismissal dismissed, briefly: "Links to About". */
8
8
  function dismissedSentence(about) {
9
9
  switch (about.kind) {
10
10
  case "missingPage": return `Links to ${about.page ?? "a missing page"}`;
11
11
  case "brokenLink": return `Links to ${about.url}`;
12
- default: return quoted(about.text);
13
12
  }
14
13
  }
15
14
  /** What dismissing a finding says, as its button says it: a broken link's is "This link is fine". */
@@ -20,7 +19,6 @@ function dismissLabel(about) {
20
19
  function fixLabel(fix) {
21
20
  switch (fix.kind) {
22
21
  case "removeLink": return "Remove the link";
23
- case "replaceText": return `Replace with ${quoted(fix.replacement)}`;
24
22
  }
25
23
  }
26
24
  /**
@@ -34,4 +32,4 @@ function askAgentMessage(finding, role) {
34
32
  }
35
33
 
36
34
  //#endregion
37
- export { askAgentMessage, dismissLabel, dismissedSentence, findingSentence, fixLabel };
35
+ export { askAgentMessage, dismissLabel, dismissedSentence, findingSentence, findingSentenceInPlace, fixLabel };
@@ -0,0 +1,39 @@
1
+ //#region src/features/studio/patrollerBio.ts
2
+ /** What each patroller does, wherever it works. */
3
+ const ABOUT = {
4
+ seoManager: "Writes what search engines read: each page's SEO, and each file's alt text and filename.",
5
+ translator: "Translates your website from its default language into every other one, so that each stays complete.",
6
+ linkChecker: "Looks for links that lead nowhere, to your own pages or elsewhere, and suggests a fix when it can."
7
+ };
8
+ /** What a patroller does, as its profile card introduces it. */
9
+ function aboutSentence(id) {
10
+ return ABOUT[id];
11
+ }
12
+ /**
13
+ * When a patroller works where the editor is, by its duty there, in a draft or on Live, as its
14
+ * card's tag says it, and whether it runs there at all. In a project with a single locale, the
15
+ * Translator waits for a second one.
16
+ */
17
+ function whenTag(id, duty, inDraft, singleLocale) {
18
+ const runs = (label) => ({
19
+ label,
20
+ runs: true
21
+ });
22
+ if (id === "translator" && singleLocale) return {
23
+ label: "Runs once you add a language",
24
+ runs: false
25
+ };
26
+ if (duty === "atPublish") return runs("Runs when you publish");
27
+ if (duty === "asYouGo") {
28
+ if (inDraft) return runs("Runs as you edit");
29
+ if (id === "linkChecker") return runs("Runs every 6 hours");
30
+ return runs("Runs while a release is translated");
31
+ }
32
+ return {
33
+ label: "Doesn't run on Live",
34
+ runs: false
35
+ };
36
+ }
37
+
38
+ //#endregion
39
+ export { aboutSentence, whenTag };
@@ -36,11 +36,6 @@ const PHRASES = {
36
36
  done: "Fixed a link in",
37
37
  failed: "Couldn't fix a link in",
38
38
  locale: "in"
39
- },
40
- fixedMistake: {
41
- done: "Fixed a typo in",
42
- failed: "Couldn't fix a typo in",
43
- locale: "in"
44
39
  }
45
40
  };
46
41
  /** A report as a sentence: "Wrote SEO for Pricing in German", or the text an AI wrote. */
@@ -10,6 +10,7 @@ const studioStore = createStore({
10
10
  startRenaming: false
11
11
  },
12
12
  isDeleteDraftDialogOpen: false,
13
+ isKeepDraftOpen: false,
13
14
  localeDialog: null,
14
15
  websiteLocation: null,
15
16
  agentAsk: null,
@@ -78,6 +79,18 @@ const studioStore = createStore({
78
79
  isDeleteDraftDialogOpen: false
79
80
  };
80
81
  },
82
+ openKeepDraft: (context) => {
83
+ return {
84
+ ...context,
85
+ isKeepDraftOpen: true
86
+ };
87
+ },
88
+ closeKeepDraft: (context) => {
89
+ return {
90
+ ...context,
91
+ isKeepDraftOpen: false
92
+ };
93
+ },
81
94
  openLocaleDialog: (context, event) => {
82
95
  return {
83
96
  ...context,
@@ -4,8 +4,7 @@ import { localeName } from "./localeOptions.js";
4
4
  const PHRASES = {
5
5
  translator: {
6
6
  verb: "Translating",
7
- locale: "into",
8
- singleLocale: "Add a language to translate into"
7
+ locale: "into"
9
8
  },
10
9
  seoManager: {
11
10
  verb: "Writing SEO for",
@@ -15,10 +14,6 @@ const PHRASES = {
15
14
  linkChecker: {
16
15
  verb: "Checking links in",
17
16
  locale: "in"
18
- },
19
- proofreader: {
20
- verb: "Proofreading",
21
- locale: "in"
22
17
  }
23
18
  };
24
19
  /** The most locales a burst names; past it, the first are named and the rest counted. */
@@ -54,10 +49,6 @@ function workingSentence(id, working) {
54
49
  inLocales(locales, phrase.locale)
55
50
  ].filter(Boolean).join(" ");
56
51
  }
57
- /** What a patroller says while idle in a project with a single locale, if anything. */
58
- function singleLocaleSentence(id) {
59
- return PHRASES[id].singleLocale;
60
- }
61
52
 
62
53
  //#endregion
63
- export { singleLocaleSentence, workingSentence };
54
+ export { workingSentence };
@@ -1,20 +1,42 @@
1
+ import { c } from "react/compiler-runtime";
1
2
  import * as React from "react";
2
3
 
3
4
  //#region src/lib/revisions.ts
4
5
  function createRevisions() {
5
6
  const listeners = /* @__PURE__ */ new Set();
7
+ const latest = /* @__PURE__ */ new Map();
6
8
  return {
7
9
  announce(revision) {
10
+ latest.set(revision.draftId, revision.revisionId);
8
11
  for (const listener of listeners) listener(revision);
9
12
  },
10
- subscribe(listener) {
13
+ subscribe: (listener) => {
11
14
  listeners.add(listener);
12
15
  return () => listeners.delete(listener);
13
- }
16
+ },
17
+ latest: (draftId) => latest.get(draftId) ?? null
14
18
  };
15
19
  }
16
20
  /** The studio's announced revisions. The signed-in studio provides it. */
17
21
  const RevisionsContext = React.createContext(null);
22
+ const noSubscription = () => () => {};
23
+ /**
24
+ * The last revision announced for `draftId` since the studio opened, or null, following each new
25
+ * one: what a URL rendered by the draft's code names, so it is fetched again once a new one built.
26
+ */
27
+ function useLatestRevision(draftId) {
28
+ const $ = c(3);
29
+ const revisions = React.use(RevisionsContext);
30
+ let t0;
31
+ if ($[0] !== draftId || $[1] !== revisions) {
32
+ t0 = () => revisions && draftId ? revisions.latest(draftId) : null;
33
+ $[0] = draftId;
34
+ $[1] = revisions;
35
+ $[2] = t0;
36
+ } else t0 = $[2];
37
+ const latest = t0;
38
+ return React.useSyncExternalStore(revisions?.subscribe ?? noSubscription, latest, latest);
39
+ }
18
40
 
19
41
  //#endregion
20
- export { RevisionsContext, createRevisions };
42
+ export { RevisionsContext, createRevisions, useLatestRevision };
@@ -18,7 +18,7 @@ const OPEN = 1;
18
18
  *
19
19
  * When the agent on Live hands a request off to a draft it started, the studio that sent a
20
20
  * message of that run hears it (`onEnterDraft`), to enter the draft. The editor's Live thread
21
- * stays as it is, ending that turn on the draft's card, which every studio on Live shows.
21
+ * then starts afresh from the draft's card, which every studio on Live shows alone.
22
22
  *
23
23
  * It reports what each writing script touched (`touched`), for runs one of its messages was part
24
24
  * of only, so that the studio follows the agent; teammates' runs leave it where it is.
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.65.1";
2
+ var version = "0.67.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };