camox 0.68.0 → 0.71.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 (34) hide show
  1. package/dist/core/createBlock.js +14 -7
  2. package/dist/core/hooks/useArtifactHost.js +56 -46
  3. package/dist/core/lib/artifact.d.ts +4 -0
  4. package/dist/core/lib/artifactHost.js +40 -0
  5. package/dist/core/lib/artifactRuntime.js +20 -1
  6. package/dist/features/canvas/canvasFrameDocument.js +25 -2
  7. package/dist/features/canvas/useCanvasFrame.js +6 -2
  8. package/dist/features/preview/components/previewStyles.js +11 -2
  9. package/dist/features/provider/AuthenticatedCamoxProvider.js +33 -28
  10. package/dist/features/renderer/RendererApp.js +202 -154
  11. package/dist/features/renderer/RendererFrames.js +87 -55
  12. package/dist/features/renderer/definitionAnswers.js +1 -0
  13. package/dist/features/renderer/rendererHost.js +4 -0
  14. package/dist/features/renderer/studioConnection.js +9 -1
  15. package/dist/features/runtime/pageApp.js +7 -3
  16. package/dist/features/runtime/pageNavigation.js +93 -17
  17. package/dist/features/runtime/rendererClient.js +13 -6
  18. package/dist/features/studio/agentRunToast.js +71 -0
  19. package/dist/features/studio/components/ChatWidget.js +2 -0
  20. package/dist/features/studio/components/DraftBar.js +2 -3
  21. package/dist/features/studio/components/DraftsControl.js +181 -198
  22. package/dist/features/studio/components/LocalePicker.js +22 -4
  23. package/dist/features/studio/components/ReviewDraftSheet.js +345 -568
  24. package/dist/features/studio/draftLabel.js +1 -1
  25. package/dist/features/studio/studioStore.js +5 -17
  26. package/dist/features/studio/viewSwitch.js +62 -0
  27. package/dist/lib/auth.js +5 -1
  28. package/dist/lib/draft-scope.js +28 -15
  29. package/dist/lib/studio-reads.js +46 -13
  30. package/dist/lib/view-switch.js +64 -0
  31. package/dist/package.js +1 -1
  32. package/dist/studio.css +1 -1
  33. package/package.json +6 -4
  34. package/skills/camox/references/artifacts.md +85 -1
@@ -26,7 +26,7 @@ const mirror = (message) => mirrors[message.store]?.send({
26
26
  * its cache follows the studio's (see `protocol.ts`). The studio's questions are answered from
27
27
  * `camoxApp`, the draft's code; without it, none can be.
28
28
  */
29
- function connectToStudio(handle, { queryClient, studioCache, draftScope, camoxApp }) {
29
+ function connectToStudio(handle, { queryClient, studioCache, draftScope, camoxApp, prepareFrame }) {
30
30
  let welcomed = false;
31
31
  let frames = [];
32
32
  const listeners = /* @__PURE__ */ new Set();
@@ -109,6 +109,14 @@ function connectToStudio(handle, { queryClient, studioCache, draftScope, camoxAp
109
109
  return;
110
110
  case "ask": {
111
111
  const { askId, question } = message;
112
+ if (question.type === "prepareFrame") {
113
+ (prepareFrame?.(question) ?? Promise.resolve()).catch(() => {}).then(() => studio({
114
+ type: "answer",
115
+ askId,
116
+ value: null
117
+ }));
118
+ return;
119
+ }
112
120
  if (!camoxApp) {
113
121
  studio({
114
122
  type: "answer",
@@ -1,5 +1,6 @@
1
1
  import { DraftScopeContext } from "../../lib/draft-scope.js";
2
2
  import { useAuthState, useSignInRedirect } from "../../lib/auth.js";
3
+ import { ArtifactCaptureContext } from "../../core/hooks/useArtifactHost.js";
3
4
  import { useSignInShortcut } from "../provider/signInShortcut.js";
4
5
  import { PreviewDocumentContext } from "./PreviewDocumentContext.js";
5
6
  import { PreviewActivation } from "../preview/components/PreviewActivation.js";
@@ -36,9 +37,12 @@ function PageApp(t0) {
36
37
  sessionless: input.look,
37
38
  children: /* @__PURE__ */ jsx(PreviewDocumentContext.Provider, {
38
39
  value: currentInput.previewDocument,
39
- children: input.previewHandoff ? /* @__PURE__ */ jsx(PreviewSignInPage, {}) : input.look ? /* @__PURE__ */ jsx(PageContent, {
40
- camoxApp,
41
- input: currentInput
40
+ children: input.previewHandoff ? /* @__PURE__ */ jsx(PreviewSignInPage, {}) : input.look ? /* @__PURE__ */ jsx(ArtifactCaptureContext, {
41
+ value: true,
42
+ children: /* @__PURE__ */ jsx(PageContent, {
43
+ camoxApp,
44
+ input: currentInput
45
+ })
42
46
  }) : /* @__PURE__ */ jsx(PageExperience, {
43
47
  camoxApp,
44
48
  input: currentInput,
@@ -1,7 +1,8 @@
1
- import { DraftScopeContext, draftNamedBy } from "../../lib/draft-scope.js";
1
+ import { DraftScopeContext, ShownDraftContext, draftNamedBy } from "../../lib/draft-scope.js";
2
2
  import { PageAlternatesContext, ViewLocaleContext } from "../../lib/view-locale-context.js";
3
3
  import { NavigationProvider } from "../navigation/navigation.js";
4
4
  import { fetchRuntimeJson, getNavigationTarget } from "./navigationTarget.js";
5
+ import { SWITCH_WAIT_MS, ViewSwitchDeadlineContext, ViewSwitchesContext } from "../../lib/view-switch.js";
5
6
  import { reads } from "../../lib/studio-reads.js";
6
7
  import { createPrefetchCache, listenForLinkIntent } from "./linkPrefetch.js";
7
8
  import { PageViewportContext } from "./pageViewport.js";
@@ -13,7 +14,7 @@ import { createNavigationRequestTracker } from "./navigationRequest.js";
13
14
  import { createScrollRestoration } from "./scrollRestoration.js";
14
15
  import { c } from "react/compiler-runtime";
15
16
  import * as React from "react";
16
- import { jsx } from "react/jsx-runtime";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
17
18
  import { hydrate } from "@tanstack/react-query";
18
19
  import { createHead, renderDOMHead } from "@unhead/react/client";
19
20
 
@@ -63,13 +64,24 @@ function prefetchKey(pathname, draft) {
63
64
  function PageNavigationProvider({ camoxApp, children, initialInput, queryClient }) {
64
65
  const draftScope = React.useContext(DraftScopeContext);
65
66
  if (!draftScope) throw new Error("PageNavigationProvider needs a DraftScopeContext provider");
66
- const [{ input, source, generation }, setLocation] = React.useState(() => {
67
+ const [{ input, source, switched, navigation }, setLocation] = React.useState(() => {
67
68
  draftScope.adopt(initialInput);
68
69
  return {
69
70
  input: initialInput,
70
- generation: 0
71
+ navigation: 0
71
72
  };
72
73
  });
74
+ const [preparers] = React.useState(() => /* @__PURE__ */ new Set());
75
+ const [commits] = React.useState(() => /* @__PURE__ */ new Map());
76
+ const navigations = React.useRef(0);
77
+ const lastShown = React.useRef(Promise.resolve());
78
+ const switches = React.useMemo(() => ({
79
+ prepare(preparer) {
80
+ preparers.add(preparer);
81
+ return () => void preparers.delete(preparer);
82
+ },
83
+ shown: () => lastShown.current
84
+ }), [preparers]);
73
85
  const current = React.useRef(initialInput);
74
86
  const [requests] = React.useState(createNavigationRequestTracker);
75
87
  const ownsWindow = initialInput.presentation !== "studio";
@@ -88,6 +100,20 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
88
100
  }, [restoration]);
89
101
  const pendingAnnouncement = React.useRef(null);
90
102
  const announcer = React.useRef(null);
103
+ const prepareSwitch = React.useCallback((next) => {
104
+ const view = {
105
+ pathname: next.pathname,
106
+ ...next.derived && { derivedLayoutId: next.derived.layoutId },
107
+ draftId: next.draftId ?? null,
108
+ locale: next.locale
109
+ };
110
+ const deadline = Date.now() + SWITCH_WAIT_MS;
111
+ const prepared = Promise.all([...preparers].map((prepare) => Promise.resolve(prepare(view)).catch(() => {})));
112
+ return {
113
+ deadline,
114
+ ready: Promise.race([prepared.then(() => {}), new Promise((resolve) => setTimeout(resolve, SWITCH_WAIT_MS))])
115
+ };
116
+ }, [preparers]);
91
117
  const loadPage = React.useCallback(async (pathname, draft, signal) => {
92
118
  const response = await fetchRuntimeJson(draftScope, "/_camox/data", initialInput.runtimeBasePath, { path: pathname }, draft, signal);
93
119
  if (!response.ok) throw new Error(`Navigation failed: ${response.status}`);
@@ -106,6 +132,7 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
106
132
  const { analytics } = initialInput;
107
133
  const navigate = React.useCallback(async ({ to, replace, source, draft, pop = false, refresh = false }) => {
108
134
  const request = requests.begin();
135
+ const id = ++navigations.current;
109
136
  const href = pop ? to : draftScope.hrefFor(to, {
110
137
  from: window.location.href,
111
138
  draft
@@ -122,12 +149,20 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
122
149
  if (target.url.hash && viewport) position(viewport, "fragment", target.url.href);
123
150
  return;
124
151
  }
152
+ lastShown.current = new Promise((resolve) => commits.set(id, resolve));
153
+ const settle = () => {
154
+ for (const [waiting, resolve] of commits) {
155
+ if (waiting > id) continue;
156
+ commits.delete(waiting);
157
+ resolve();
158
+ }
159
+ };
125
160
  try {
126
161
  let next = current.current;
127
162
  const targetDraft = draftNamedBy(target.url.href);
128
163
  const fetched = target.pathname !== next.pathname || refresh || targetDraft !== draftNamedBy(next.href);
129
164
  if (fetched) next = await (!refresh && prefetched.take(prefetchKey(target.pathname, targetDraft)) || loadPage(target.pathname, targetDraft, request.signal));
130
- if (!request.isCurrent()) return;
165
+ if (!request.isCurrent()) return settle();
131
166
  const switching = (next.draftId ?? null) !== (current.current.draftId ?? null) || !sameLocale(next.locale, current.current.locale);
132
167
  const positions = restoration();
133
168
  if (!pop) {
@@ -145,6 +180,7 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
145
180
  };
146
181
  pendingAnnouncement.current = fetched && !refresh ? next.pathname : null;
147
182
  if (fetched) draftScope.adopt(next);
183
+ const switched = switching ? prepareSwitch(next) : void 0;
148
184
  hydrate(queryClient, switching ? next.dehydratedState : preserveStableLayoutBlockCaches(next.dehydratedState, current.current.layoutIdentity, next.layoutIdentity));
149
185
  next = {
150
186
  ...next,
@@ -162,12 +198,14 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
162
198
  renderDOMHead(manager.head);
163
199
  if (ownsWindow && analytics && next.pathname !== previous.pathname) sendPageView(analytics, window);
164
200
  if (ownsWindow) document.documentElement.lang = next.locale.tag;
165
- React.startTransition(() => setLocation((location) => ({
201
+ React.startTransition(() => setLocation({
166
202
  input: next,
167
203
  source: pop ? void 0 : source,
168
- generation: location.generation + (switching ? 1 : 0)
169
- })));
204
+ switched,
205
+ navigation: id
206
+ }));
170
207
  } catch {
208
+ settle();
171
209
  if (!request.isCurrent()) return;
172
210
  if (pop) window.location.reload();
173
211
  else if (replace) window.location.replace(target.url.href);
@@ -175,12 +213,14 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
175
213
  }
176
214
  }, [
177
215
  analytics,
216
+ commits,
178
217
  draftScope,
179
218
  initialInput.runtimeBasePath,
180
219
  loadPage,
181
220
  ownsWindow,
182
221
  position,
183
222
  prefetched,
223
+ prepareSwitch,
184
224
  queryClient,
185
225
  requests,
186
226
  restoration
@@ -250,6 +290,21 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
250
290
  prefetched.clear();
251
291
  };
252
292
  }, [draftScope, prefetched]);
293
+ React.useLayoutEffect(() => {
294
+ for (const [waiting, resolve] of commits) {
295
+ if (waiting > navigation) continue;
296
+ commits.delete(waiting);
297
+ resolve();
298
+ }
299
+ if (switched) setLocation((location) => ({
300
+ ...location,
301
+ switched: void 0
302
+ }));
303
+ }, [
304
+ commits,
305
+ navigation,
306
+ switched
307
+ ]);
253
308
  React.useLayoutEffect(() => {
254
309
  const intent = takeScroll(input.href);
255
310
  const viewport = scroll.current?.viewport();
@@ -310,13 +365,14 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
310
365
  initialInput.runtimeBasePath,
311
366
  requests
312
367
  ]);
368
+ const hydrated = React.useSyncExternalStore(noSubscription, isClient, isServer);
313
369
  const url = new URL(input.href);
314
370
  const viewLocale = useSameJson({
315
371
  locale: input.locale,
316
372
  locales: input.locales
317
373
  });
318
374
  const alternates = useSameJson(input.alternates ?? null);
319
- return /* @__PURE__ */ jsx(NavigationProvider, {
375
+ return /* @__PURE__ */ jsxs(NavigationProvider, {
320
376
  location: {
321
377
  href: input.href,
322
378
  pathname: input.pathname,
@@ -325,18 +381,38 @@ function PageNavigationProvider({ camoxApp, children, initialInput, queryClient
325
381
  source
326
382
  },
327
383
  navigate,
328
- children: /* @__PURE__ */ jsx(ViewLocaleContext, {
329
- value: viewLocale,
330
- children: /* @__PURE__ */ jsx(PageAlternatesContext, {
331
- value: alternates,
332
- children: /* @__PURE__ */ jsx(PageViewportContext, {
333
- value: pageViewport,
334
- children: /* @__PURE__ */ jsx(React.Fragment, { children: children(input) }, generation)
384
+ children: [/* @__PURE__ */ jsx(ViewSwitchesContext, {
385
+ value: switches,
386
+ children: /* @__PURE__ */ jsx(ViewSwitchDeadlineContext, {
387
+ value: switched?.deadline ?? null,
388
+ children: /* @__PURE__ */ jsx(ShownDraftContext, {
389
+ value: input.draftId ?? null,
390
+ children: /* @__PURE__ */ jsx(ViewLocaleContext, {
391
+ value: viewLocale,
392
+ children: /* @__PURE__ */ jsx(PageAlternatesContext, {
393
+ value: alternates,
394
+ children: /* @__PURE__ */ jsx(PageViewportContext, {
395
+ value: pageViewport,
396
+ children: children(input)
397
+ })
398
+ })
399
+ })
335
400
  })
336
401
  })
337
- })
402
+ }), hydrated && /* @__PURE__ */ jsx(React.Suspense, {
403
+ fallback: null,
404
+ children: /* @__PURE__ */ jsx(SwitchGate, { ready: switched?.ready })
405
+ })]
338
406
  });
339
407
  }
408
+ const noSubscription = () => () => {};
409
+ const isClient = () => true;
410
+ const isServer = () => false;
411
+ /** Suspends the switch's render until the next view is ready. */
412
+ function SwitchGate({ ready }) {
413
+ if (ready) React.use(ready);
414
+ return null;
415
+ }
340
416
 
341
417
  //#endregion
342
418
  export { PageNavigationProvider };
@@ -1,5 +1,5 @@
1
1
  import { DraftScopeContext, createDraftScope, followUrl } from "../../lib/draft-scope.js";
2
- import { StudioCacheContext, createStudioCache } from "../../lib/studio-reads.js";
2
+ import { StudioCacheContext, createStudioCache, reads } from "../../lib/studio-reads.js";
3
3
  import { getDraftReadClient } from "../../lib/api-client.js";
4
4
  import { STUDIO_HANDLE_KEY } from "../renderer/protocol.js";
5
5
  import { CoreCamoxProvider } from "../provider/CoreCamoxProvider.js";
@@ -23,7 +23,8 @@ async function startRendererWithApp(camoxApp) {
23
23
  return;
24
24
  }
25
25
  const input = readHydrationData();
26
- if (!input.apiUrl || !input.authenticationUrl || !input.projectUuid) {
26
+ const { projectUuid, runtimeBasePath } = input;
27
+ if (!input.apiUrl || !input.authenticationUrl || !projectUuid) {
27
28
  console.error("Camox: the renderer document is missing its API or project.");
28
29
  return;
29
30
  }
@@ -44,7 +45,13 @@ async function startRendererWithApp(camoxApp) {
44
45
  queryClient,
45
46
  studioCache,
46
47
  draftScope,
47
- camoxApp
48
+ camoxApp,
49
+ prepareFrame: ({ pathname, derivedLayoutId, locale }) => Promise.all([studioCache.ensure(reads.canvasFrame({
50
+ projectUuid,
51
+ runtimeBasePath,
52
+ pathname,
53
+ ...derivedLayoutId && { derivedLayoutId }
54
+ }), { locale }), codeLoaded])
48
55
  });
49
56
  const container = document.createElement("div");
50
57
  document.body.append(container);
@@ -58,14 +65,14 @@ async function startRendererWithApp(camoxApp) {
58
65
  apiUrl: input.apiUrl,
59
66
  authenticationUrl: input.authenticationUrl,
60
67
  camoxApp,
61
- projectUuid: input.projectUuid,
68
+ projectUuid,
62
69
  initialAuthenticated: true,
63
70
  children: /* @__PURE__ */ jsx(CompleteBlockEditingRuntimeProvider, { children: /* @__PURE__ */ jsx(RendererApp, {
64
71
  connection,
65
72
  camoxApp,
66
73
  codeLoaded,
67
- projectUuid: input.projectUuid,
68
- runtimeBasePath: input.runtimeBasePath
74
+ projectUuid,
75
+ runtimeBasePath
69
76
  }) })
70
77
  })
71
78
  })
@@ -0,0 +1,71 @@
1
+ import { c } from "react/compiler-runtime";
2
+ import * as React from "react";
3
+ import { toast } from "@camox/ui/toaster";
4
+
5
+ //#region src/features/studio/agentRunToast.ts
6
+ /**
7
+ * Says the agent is working in a toast while a run goes in the draft's thread, anyone's, so the
8
+ * editor knows wherever they are in the studio, then how it ended. Live's runs are too quick to
9
+ * need one. Leaving the thread takes the toast away.
10
+ */
11
+ function useAgentRunToast(view, draftId) {
12
+ const $ = c(13);
13
+ const id = React.useId();
14
+ const running = draftId !== null && view.running;
15
+ const ran = React.useRef(false);
16
+ let t0;
17
+ if ($[0] !== id || $[1] !== view) {
18
+ t0 = () => {
19
+ const last = view.messages.at(-1)?.from;
20
+ if (last === "failed") toast.error("Agent couldn't finish", { id });
21
+ else if (last === "stopped") toast.dismiss(id);
22
+ else toast.success("Agent is done", { id });
23
+ };
24
+ $[0] = id;
25
+ $[1] = view;
26
+ $[2] = t0;
27
+ } else t0 = $[2];
28
+ const ended = React.useEffectEvent(t0);
29
+ let t1;
30
+ if ($[3] !== ended || $[4] !== id || $[5] !== running) {
31
+ t1 = () => {
32
+ if (running) {
33
+ ran.current = true;
34
+ toast.loading("Agent is working…", { id });
35
+ } else if (ran.current) {
36
+ ran.current = false;
37
+ ended();
38
+ }
39
+ };
40
+ $[3] = ended;
41
+ $[4] = id;
42
+ $[5] = running;
43
+ $[6] = t1;
44
+ } else t1 = $[6];
45
+ let t2;
46
+ if ($[7] !== id || $[8] !== running) {
47
+ t2 = [running, id];
48
+ $[7] = id;
49
+ $[8] = running;
50
+ $[9] = t2;
51
+ } else t2 = $[9];
52
+ React.useEffect(t1, t2);
53
+ let t3;
54
+ let t4;
55
+ if ($[10] !== id) {
56
+ t3 = () => () => {
57
+ if (ran.current) toast.dismiss(id);
58
+ };
59
+ t4 = [id];
60
+ $[10] = id;
61
+ $[11] = t3;
62
+ $[12] = t4;
63
+ } else {
64
+ t3 = $[11];
65
+ t4 = $[12];
66
+ }
67
+ React.useEffect(t3, t4);
68
+ }
69
+
70
+ //#endregion
71
+ export { useAgentRunToast };
@@ -7,6 +7,7 @@ import { BOTTOM_CARD_EDGES, CARD_BODY_EDGES } from "../../preview/components/Sid
7
7
  import { studioStore } from "../studioStore.js";
8
8
  import { FollowAgentContext } from "../followAgentContext.js";
9
9
  import { useThread } from "../../../lib/use-thread.js";
10
+ import { useAgentRunToast } from "../agentRunToast.js";
10
11
  import { EditorWhereContext } from "../editorWhereContext.js";
11
12
  import { useEditorSelection } from "../useEditorSelection.js";
12
13
  import { ThreadComposer, useComposerCarrier } from "./ThreadComposer.js";
@@ -141,6 +142,7 @@ function Thread(t0) {
141
142
  $[1] = t1;
142
143
  } else t1 = $[1];
143
144
  const { view, sending, send, stop, error, touched } = useThread(apiUrl, projectId, draftId, t1);
145
+ useAgentRunToast(view, draftId);
144
146
  const follow = React.use(FollowAgentContext);
145
147
  let t2;
146
148
  if ($[2] !== follow) {
@@ -14,8 +14,7 @@ import { FloatingToolbar } from "@camox/ui/floating-toolbar";
14
14
  */
15
15
  const SETTLE_MS = 1500;
16
16
  /**
17
- * Asks what to do with the draft being viewed, once its edits settle: discard its changes or
18
- * publish them. `stacked` places it in the Preview toolbar's column, just above the toolbar;
17
+ * Asks what to do with the draft being viewed, once its edits settle: delete it or publish it. `stacked` places it in the Preview toolbar's column, just above the toolbar;
19
18
  * otherwise it floats at the bottom of the screen on its own.
20
19
  */
21
20
  function DraftBar(t0) {
@@ -63,7 +62,7 @@ function DraftBar(t0) {
63
62
  type: "button",
64
63
  variant: "outline",
65
64
  onClick: _temp,
66
- children: "Discard"
65
+ children: "Delete"
67
66
  });
68
67
  $[8] = t7;
69
68
  } else t7 = $[8];