camox 0.63.0 → 0.66.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 (60) hide show
  1. package/dist/core/createApp.d.ts +28 -28
  2. package/dist/core/createLayout.d.ts +1 -1
  3. package/dist/core/derivedRoutes.js +3 -30
  4. package/dist/core/editing/createEditableBlock.d.ts +604 -604
  5. package/dist/features/canvas/CamoxCanvas.js +195 -173
  6. package/dist/features/canvas/canvasCamera.js +11 -2
  7. package/dist/features/canvas/canvasFrameCapture.js +5 -12
  8. package/dist/features/canvas/canvasFrameDocument.js +90 -29
  9. package/dist/features/canvas/canvasItems.js +22 -6
  10. package/dist/features/canvas/sidebar/ContentSidebar.js +19 -21
  11. package/dist/features/canvas/sidebar/LayoutPageOverview.js +22 -49
  12. package/dist/features/canvas/sidebar/LayoutPageSeo.js +73 -76
  13. package/dist/features/canvas/sidebar/PageInspector.js +61 -50
  14. package/dist/features/canvas/sidebar/PageInspectorSkeleton.js +4 -4
  15. package/dist/features/canvas/useCanvasCamera.js +36 -15
  16. package/dist/features/page/CuratedPageContent.js +2 -1
  17. package/dist/features/page/UnplacedBlockContent.js +52 -0
  18. package/dist/features/preview/components/PageInfo.js +103 -100
  19. package/dist/features/preview/components/PreviewPanel.js +40 -12
  20. package/dist/features/preview/components/SidebarStack.js +78 -9
  21. package/dist/features/preview/strayCaretGuard.js +34 -0
  22. package/dist/features/provider/CoreCamoxProvider.js +5 -3
  23. package/dist/features/routes/pageRuntime.d.ts +2 -0
  24. package/dist/features/routes/pageRuntime.js +8 -5
  25. package/dist/features/runtime/RuntimeChrome.js +66 -40
  26. package/dist/features/runtime/pageApp.js +111 -74
  27. package/dist/features/runtime/pageClient.js +22 -2
  28. package/dist/features/runtime/runtime.d.ts +10 -0
  29. package/dist/features/runtime/runtime.js +314 -4
  30. package/dist/features/runtime/runtimeRoutes.d.ts +5 -0
  31. package/dist/features/runtime/runtimeRoutes.js +5 -0
  32. package/dist/features/studio/components/AgentSidebar.js +3 -3
  33. package/dist/features/studio/components/ChatWidget.js +106 -75
  34. package/dist/features/studio/components/ConnectAgent.js +313 -0
  35. package/dist/features/studio/components/DraftsControl.js +172 -159
  36. package/dist/features/studio/components/Patrol.js +605 -350
  37. package/dist/features/studio/components/ReviewDraftSheet.js +566 -430
  38. package/dist/features/studio/components/Squareling.js +74 -41
  39. package/dist/features/studio/components/ThreadComposer.js +192 -107
  40. package/dist/features/studio/components/ThreadMessages.js +95 -18
  41. package/dist/features/studio/components/UnnamedDraftBar.js +349 -0
  42. package/dist/features/studio/components/clientLogoPaths.js +10 -0
  43. package/dist/features/studio/findingSentence.js +37 -0
  44. package/dist/features/studio/patrollerBio.js +40 -0
  45. package/dist/features/studio/reportSentence.js +15 -0
  46. package/dist/features/studio/studioStore.js +36 -1
  47. package/dist/features/studio/workingSentence.js +10 -7
  48. package/dist/lib/api-client.d.ts +1 -0
  49. package/dist/lib/auth.js +34 -32
  50. package/dist/lib/queries.js +9 -1
  51. package/dist/lib/revisions.js +25 -3
  52. package/dist/lib/server-auth-cookie.js +17 -4
  53. package/dist/lib/studio-reads.d.ts +1 -1
  54. package/dist/lib/studio-reads.js +5 -1
  55. package/dist/lib/use-thread.js +1 -1
  56. package/dist/package.js +1 -1
  57. package/dist/studio.css +1 -1
  58. package/package.json +15 -4
  59. package/dist/features/canvas/sidebar/DerivedPageInspector.js +0 -49
  60. package/dist/features/canvas/sidebar/SingletonPageInspector.js +0 -39
@@ -2,7 +2,7 @@ import { serializeJsonForHtml } from "../../lib/html.js";
2
2
  import { createRequestContext } from "../../core/requestContext.js";
3
3
  import { UNNAMED_DEFAULT_LOCALE, localizedPath, parseLocalePath } from "../../lib/locale-path.js";
4
4
  import { draftNamedBy } from "../../lib/draft-scope.js";
5
- import { buildClearServerAuthCookieHeader, buildServerAuthCookieHeader, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
5
+ import { buildClearServerAuthCookieHeader, buildServerAuthCookieHeader, getLookToken, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
6
6
  import { getCookie, getSetCookie } from "../../lib/stored-auth-cookies.js";
7
7
  import { ARTIFACTS_PATH_PREFIX, RUNTIME_ROUTE_PATHS, matchRuntimeOwnedRoute } from "./runtimeRoutes.js";
8
8
  import { reads } from "../../lib/studio-reads.js";
@@ -13,7 +13,7 @@ import { STUDIO_THEME_SCRIPT } from "../studio/studioTheme.js";
13
13
  import { DefaultPageImage, renderPageImage } from "../og/pageImage.js";
14
14
  import { discoverLayoutPages, matchDerivedLayout } from "../../core/derivedRoutes.js";
15
15
  import { buildLayoutContext, curatedLayoutContext } from "../../core/layoutContext.js";
16
- import { buildCamoxPageHead, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, declarePage, isNotFoundError, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readPageMarkdown, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
16
+ import { buildCamoxPageHead, buildCamoxPageLoaderData, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, declarePage, isAuthSessionError, isNotFoundError, loadCamoxPage, loadCamoxPageForRequest, parseMarkdownPath, prefersMarkdown, readDraftOrLive, readPageMarkdown, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter } from "../routes/pageRuntime.js";
17
17
  import { assetPrefix } from "./assetPrefix.js";
18
18
  import { documentGoogleFonts } from "../og/documentFonts.js";
19
19
  import { alternatesByPage, findAlternates, hreflangLinks } from "./alternates.js";
@@ -27,7 +27,7 @@ import { getPageBlockTypes } from "./pageBlockTypes.js";
27
27
  import { withPageUrls } from "./structuredData.js";
28
28
  import { curatedPageDescription, pageRecords, placedBlockNodes, withStructuredData } from "./pageGraph.js";
29
29
  import { createProjectLogoResponse, projectLogo } from "./projectLogo.js";
30
- import { LOCALE_HEADER } from "@camox/api-contract/drafts";
30
+ import { DRAFT_SEARCH_PARAM, LOCALE_HEADER, LOOK_BLOCK_DEFINITION_PARAM, LOOK_CONTENT_PARAM, LOOK_LOCALE_PARAM, LOOK_PATH_PARAM, LOOK_SETTINGS_PARAM, UNPLACED_BLOCK_ID } from "@camox/api-contract/drafts";
31
31
  import { QueryClient, dehydrate } from "@tanstack/react-query";
32
32
  import { createHead, renderSSRHead } from "@unhead/react/server";
33
33
 
@@ -503,6 +503,314 @@ function createRendererDocumentResponse(options) {
503
503
  "X-Robots-Tag": "noindex"
504
504
  } });
505
505
  }
506
+ /**
507
+ * Marks the look document ready (`data-camox-ready` on its root element) for the browser to wait
508
+ * on, once the page's client hydrated it (`camox:hydrated`), the window loaded, and every frame
509
+ * on the page, its Artifacts' included, loaded or failed to, then a frame later, once painted.
510
+ * Artifacts' frames load lazily, and their documents may fetch libraries as they start, so the
511
+ * window's `load` alone comes before them: the frames are made eager once hydrated, so those
512
+ * below the first screen load too, and each frame's own `load` is awaited. A frame that never
513
+ * loads leaves the document unready, and the capture waiting on it times out.
514
+ */
515
+ const LOOK_READY_SCRIPT = `(()=>{const loaded=new WeakSet();let hydrated=false,marked=false;const mark=()=>{if(marked||!hydrated||document.readyState!=="complete")return;const frames=[...document.querySelectorAll("iframe")];for(const frame of frames)if(frame.getAttribute("loading")==="lazy")frame.setAttribute("loading","eager");if(!frames.every((frame)=>loaded.has(frame)))return;marked=true;requestAnimationFrame(()=>requestAnimationFrame(()=>document.documentElement.setAttribute("data-camox-ready","")))};const settle=(event)=>{if(event.target&&event.target.tagName==="IFRAME")loaded.add(event.target);mark()};document.addEventListener("load",settle,true);document.addEventListener("error",settle,true);addEventListener("load",mark,{once:true});addEventListener("camox:hydrated",()=>{hydrated=true;mark()},{once:true})})()`;
516
+ /** The headers every look document answer carries: private, never cached, never indexed. */
517
+ const LOOK_HEADERS = {
518
+ "Cache-Control": "private, no-store",
519
+ "X-Robots-Tag": "noindex"
520
+ };
521
+ /** What the look document answers when it shows nothing: no token, or one the API refused. */
522
+ function lookNotFound() {
523
+ return new Response("Not found", {
524
+ status: 404,
525
+ headers: LOOK_HEADERS
526
+ });
527
+ }
528
+ /** What the look document answers when the API refuses what it was asked to show, and why. */
529
+ function lookRefused(message) {
530
+ return new Response(message, {
531
+ status: 422,
532
+ headers: {
533
+ ...LOOK_HEADERS,
534
+ "Content-Type": "text/plain; charset=utf-8"
535
+ }
536
+ });
537
+ }
538
+ function overrideParam(searchParams, name) {
539
+ const raw = searchParams.get(name);
540
+ if (raw === null) return {
541
+ ok: true,
542
+ value: void 0
543
+ };
544
+ try {
545
+ return {
546
+ ok: true,
547
+ value: JSON.parse(raw)
548
+ };
549
+ } catch {
550
+ return {
551
+ ok: false,
552
+ error: `The ${name} override isn't JSON.`
553
+ };
554
+ }
555
+ }
556
+ /**
557
+ * The lightweight document the agent's `look` tool captures (ADR 0032), for the draft
558
+ * `camox-draft` names, read with the look token the request's cookie carries, in the Locale
559
+ * `locale` names by its tag (the Default Locale without one). It shows the page at `path` as a
560
+ * visitor would, or in its Block Definition mode the block of the Block Definition
561
+ * `blockDefinition` names, placed nowhere, alone in the site's shell, with the `content` and
562
+ * `settings` overrides merged on. Either way it has the site's head and stylesheets and the
563
+ * page's client to hydrate its blocks, but nothing of the studio. It answers nothing without a
564
+ * token the API accepts.
565
+ */
566
+ async function createLookDocumentResponse(request, options) {
567
+ const { apiUrl, authenticationUrl, projectUuid, renderPage } = options;
568
+ const { searchParams } = new URL(request.url);
569
+ const lookToken = getLookToken(request.headers);
570
+ const draftId = searchParams.get(DRAFT_SEARCH_PARAM) ?? void 0;
571
+ if (!lookToken || !draftId || !apiUrl || !authenticationUrl || !projectUuid || !renderPage) return lookNotFound();
572
+ const tag = searchParams.get(LOOK_LOCALE_PARAM)?.toLowerCase();
573
+ const blockDefinition = searchParams.get(LOOK_BLOCK_DEFINITION_PARAM);
574
+ const queryClient = new QueryClient();
575
+ let locales;
576
+ let locale;
577
+ let shown;
578
+ try {
579
+ locales = await createServerApiClient(apiUrl, {
580
+ lookToken,
581
+ draftId
582
+ }).locales.list({ projectUuid });
583
+ locale = locales.find((each) => tag ? each.tag.toLowerCase() === tag : each.isDefault);
584
+ if (!locale) return lookNotFound();
585
+ const reading = {
586
+ apiUrl,
587
+ lookToken,
588
+ draftId,
589
+ locale,
590
+ projectUuid,
591
+ queryClient
592
+ };
593
+ shown = blockDefinition ? await lookAtBlockDefinition(request, options, reading, blockDefinition) : await lookAtPage(request, options, reading);
594
+ } catch (error) {
595
+ if (isAuthSessionError(error) || isNotFoundError(error)) return lookNotFound();
596
+ throw error;
597
+ }
598
+ if (shown instanceof Response) return shown;
599
+ const document = await options.getDocument?.() ?? {};
600
+ const dehydratedState = dehydrate(queryClient);
601
+ const input = {
602
+ ...shown,
603
+ look: true,
604
+ presentation: "public",
605
+ apiUrl,
606
+ authenticationUrl,
607
+ dehydratedState,
608
+ draftId,
609
+ projectUuid,
610
+ runtimeBasePath: normalizeRuntimeBasePath(options.runtimeBasePath),
611
+ locale,
612
+ locales
613
+ };
614
+ const appHtml = await renderPage(input);
615
+ const renderedHead = renderHead(document, createPageHeadInput(shown.head), options.stylesheetUrl, void 0, locale);
616
+ return new Response(`<!doctype html>
617
+ <html${renderedHead.htmlAttrs}>
618
+ <head>
619
+ ${renderedHead.headTags}
620
+ <meta name="robots" content="noindex">
621
+ <script>${LOOK_READY_SCRIPT}<\/script>
622
+ ${renderBlockPreloads(options, dehydratedState)}
623
+ <script id="__CAMOX_DATA__" type="application/json">${serializeJsonForHtml(hydrationInputFor(input, appHtml))}<\/script>
624
+ </head>
625
+ <body${renderedHead.bodyAttrs}>
626
+ ${renderedHead.bodyTagsOpen}
627
+ <div id="root">${appHtml}</div>
628
+ ${renderedHead.bodyTags}
629
+ ${renderClientEntryScript(options.pageClientEntryUrl)}
630
+ </body>
631
+ </html>`, { headers: {
632
+ ...LOOK_HEADERS,
633
+ "Content-Type": "text/html; charset=utf-8",
634
+ "Referrer-Policy": "no-referrer"
635
+ } });
636
+ }
637
+ /** Where the look document's client believes it is: `path` of the draft, in its Locale. */
638
+ function lookHref(request, options, pathname, draftId) {
639
+ const href = new URL(withRuntimeBasePath(pathname, options.runtimeBasePath), request.url);
640
+ href.searchParams.set(DRAFT_SEARCH_PARAM, draftId);
641
+ return href;
642
+ }
643
+ /**
644
+ * The look document's page at `path`, with its head, as a visitor would see it. A path is
645
+ * matched as the website serves it: a singleton layout's route first, then a curated page, then a
646
+ * derived layout's route.
647
+ */
648
+ async function lookAtPage(request, options, reading) {
649
+ const { apiUrl, draftId, locale } = reading;
650
+ const path = normalizePagePath(new URL(request.url).searchParams.get(LOOK_PATH_PARAM));
651
+ const camoxApp = await options.getCamoxApp?.();
652
+ const match = camoxApp && matchDerivedLayout(camoxApp.getLayouts(), path);
653
+ if (match?.layout._internal.kind === "singleton") return lookAtLayoutPage(request, options, reading, path, match);
654
+ let page;
655
+ try {
656
+ page = await loadCamoxPage({
657
+ ...reading,
658
+ pathname: path
659
+ });
660
+ } catch (error) {
661
+ if (match && isNotFoundError(error)) return lookAtLayoutPage(request, options, reading, path, match);
662
+ throw error;
663
+ }
664
+ const pathname = localizedPath(path, locale);
665
+ const href = lookHref(request, options, pathname, draftId);
666
+ const loaderData = buildCamoxPageLoaderData(apiUrl, page, href.origin);
667
+ return {
668
+ head: camoxApp ? buildCamoxPageHead(camoxApp, loaderData, locale) : {},
669
+ href: href.href,
670
+ layoutIdentity: getLayoutIdentity(page.layout),
671
+ loaderData,
672
+ pathname
673
+ };
674
+ }
675
+ /**
676
+ * The look document's page of a singleton or derived layout at `path`: the layout's blocks, read
677
+ * with the look token, around what its loader returns, its records read from the draft. A loader
678
+ * that finds nothing answers why.
679
+ */
680
+ async function lookAtLayoutPage(request, options, reading, path, match) {
681
+ const { apiUrl, lookToken, draftId, locale, projectUuid, queryClient } = reading;
682
+ const app = await options.getCamoxApp?.();
683
+ const layout = match.layout._internal;
684
+ const pathname = localizedPath(path, locale);
685
+ let result;
686
+ let shared;
687
+ try {
688
+ [result, shared] = await Promise.all([loadLayoutResult(app, match, createLayoutRequestContext(options, "draft", {
689
+ lookToken,
690
+ draftId,
691
+ locale
692
+ }), locale), createServerApiClient(apiUrl, {
693
+ lookToken,
694
+ draftId,
695
+ locale
696
+ }).layouts.get({
697
+ projectUuid,
698
+ layoutId: layout.id
699
+ })]);
700
+ } catch (error) {
701
+ if (!isNotFoundError(error)) throw error;
702
+ return new Response(`The draft has no page at ${path}: the ${layout.id} layout's loader found nothing to show.`, {
703
+ status: 404,
704
+ headers: {
705
+ ...LOOK_HEADERS,
706
+ "Content-Type": "text/plain; charset=utf-8"
707
+ }
708
+ });
709
+ }
710
+ const studioCache = createServerStudioCache(queryClient, {
711
+ apiUrl,
712
+ lookToken,
713
+ locale
714
+ });
715
+ const inDraft = { in: draftId };
716
+ const runtimeBasePath = normalizeRuntimeBasePath(options.runtimeBasePath);
717
+ studioCache.land(reads.layoutStructure({
718
+ projectUuid,
719
+ layoutId: layout.id
720
+ }), shared, inDraft);
721
+ studioCache.set(reads.derivedPageData({
722
+ projectUuid,
723
+ runtimeBasePath,
724
+ pathname
725
+ }), result ?? null, inDraft);
726
+ const href = lookHref(request, options, pathname, draftId);
727
+ const record = asCollectionItem(result);
728
+ const collection = record && app?.getCollectionById(record.collectionId);
729
+ const context = buildLayoutContext(layout, {
730
+ path: pathname,
731
+ locale: locale.tag,
732
+ projectName: shared.projectName,
733
+ data: result?.data,
734
+ params: match.params,
735
+ record: record && collection ? recordPageValues(collection, record) : void 0
736
+ });
737
+ return {
738
+ head: buildPageHead({
739
+ layout,
740
+ context,
741
+ origin: href.origin,
742
+ faviconUrl: buildFaviconUrl(apiUrl, shared.project),
743
+ hasMarkdown: layout.toMarkdown !== void 0,
744
+ recordType: record?.structuredData?.node?.["@type"]
745
+ }),
746
+ href: href.href,
747
+ layoutIdentity: getLayoutIdentity(shared.layout),
748
+ loaderData: null,
749
+ pathname,
750
+ derived: {
751
+ layoutId: layout.id,
752
+ page: {
753
+ params: match.params,
754
+ path: pathname,
755
+ projectName: shared.projectName
756
+ },
757
+ result,
758
+ layout: shared.layout
759
+ }
760
+ };
761
+ }
762
+ /**
763
+ * The look document's Block Definition mode: the block of the Block Definition `type` as the
764
+ * draft would show it placed, with its default content and Block Settings and the overrides the
765
+ * URL gives merged on, by the API, which checks them against its schemas. An override it refuses
766
+ * answers its message. The block is read as a page reads its blocks, so it renders and hydrates
767
+ * as a page's block does; its page is the Locale's home, which links resolve against.
768
+ */
769
+ async function lookAtBlockDefinition(request, options, reading, type) {
770
+ const { apiUrl, lookToken, draftId, locale, projectUuid, queryClient } = reading;
771
+ const { searchParams } = new URL(request.url);
772
+ const content = overrideParam(searchParams, LOOK_CONTENT_PARAM);
773
+ const settings = overrideParam(searchParams, LOOK_SETTINGS_PARAM);
774
+ if (!content.ok) return lookRefused(content.error);
775
+ if (!settings.ok) return lookRefused(settings.error);
776
+ const studioCache = createServerStudioCache(queryClient, {
777
+ apiUrl,
778
+ lookToken,
779
+ locale
780
+ });
781
+ const inDraft = { in: draftId };
782
+ let bundle;
783
+ try {
784
+ [bundle] = await Promise.all([createServerApiClient(apiUrl, {
785
+ lookToken,
786
+ draftId,
787
+ locale
788
+ }).blocks.getUnplaced({
789
+ projectUuid,
790
+ type,
791
+ content: content.value,
792
+ settings: settings.value
793
+ }), studioCache.ensure(reads.pageList({ projectUuid }), inDraft).catch(() => null)]);
794
+ } catch (error) {
795
+ if (isBadRequestError(error)) return lookRefused(error.message);
796
+ throw error;
797
+ }
798
+ studioCache.set(reads.blockBundle(UNPLACED_BLOCK_ID), bundle, inDraft);
799
+ const pathname = localizedPath("/", locale);
800
+ return {
801
+ head: {},
802
+ href: lookHref(request, options, pathname, draftId).href,
803
+ layoutIdentity: null,
804
+ loaderData: null,
805
+ pathname,
806
+ lookBlock: UNPLACED_BLOCK_ID
807
+ };
808
+ }
809
+ /** Whether an API error refused the request's input, such as an override its schema refuses. */
810
+ function isBadRequestError(error) {
811
+ const { code, status } = error ?? {};
812
+ return code === "BAD_REQUEST" || status === 400;
813
+ }
506
814
  function renderClientEntryScript(src) {
507
815
  if (!src) return "";
508
816
  return `<script type="module" src="${escapeXml(src)}"><\/script>`;
@@ -959,11 +1267,12 @@ function createLayoutRequestContext(options, source, reading) {
959
1267
  * Reads the records `loader` and `routes` ask for, in `locale` or the default locale; none
960
1268
  * without an API.
961
1269
  */
962
- function createRecordLookup(options, source, { authCookieHeader, draftId, locale } = {}) {
1270
+ function createRecordLookup(options, source, { authCookieHeader, lookToken, draftId, locale } = {}) {
963
1271
  const { apiUrl, projectUuid } = options;
964
1272
  if (!apiUrl || !projectUuid) return void 0;
965
1273
  const api = createServerApiClient(apiUrl, {
966
1274
  authCookieHeader: source === "draft" ? authCookieHeader : void 0,
1275
+ lookToken: source === "draft" ? lookToken : void 0,
967
1276
  draftId,
968
1277
  locale
969
1278
  });
@@ -1460,6 +1769,7 @@ async function handleCamoxRequest(request, options = {}) {
1460
1769
  if (match.kind === "artifact") return createArtifactDocumentResponse(pathname, options);
1461
1770
  if (match.kind === "og") return createOgResponse(request, options);
1462
1771
  if (match.kind === "renderer") return createRendererDocumentResponse(options);
1772
+ if (match.kind === "look") return createLookDocumentResponse(request, options);
1463
1773
  if (match.kind === "logo") return createProjectLogoResponse(request, options);
1464
1774
  if (match.kind === "sitemap") return createSitemapResponse(request, options);
1465
1775
  if (match.kind === "llms-txt") return createLlmsTxtResponse(request, options);
@@ -10,6 +10,11 @@
10
10
  export declare const RUNTIME_ROUTE_PATHS: {
11
11
  readonly data: "/_camox/data";
12
12
  readonly health: "/_camox/health";
13
+ /**
14
+ * The lightweight document the agent's `look` tool captures (ADR 0032): one page of a draft,
15
+ * read with a look token, without the studio.
16
+ */
17
+ readonly look: "/_camox/look";
13
18
  readonly registry: "/_camox/registry";
14
19
  /** The renderer frame's document (ADR 0023), which the studio loads to render its pages. */
15
20
  readonly renderer: "/_camox/renderer";
@@ -10,6 +10,11 @@
10
10
  const RUNTIME_ROUTE_PATHS = {
11
11
  data: "/_camox/data",
12
12
  health: "/_camox/health",
13
+ /**
14
+ * The lightweight document the agent's `look` tool captures (ADR 0032): one page of a draft,
15
+ * read with a look token, without the studio.
16
+ */
17
+ look: "/_camox/look",
13
18
  registry: "/_camox/registry",
14
19
  /** The renderer frame's document (ADR 0023), which the studio loads to render its pages. */
15
20
  renderer: "/_camox/renderer",
@@ -7,8 +7,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
7
7
 
8
8
  //#region src/features/studio/components/AgentSidebar.tsx
9
9
  /**
10
- * The sidebar on the Canvas's right side, where editors work with Camox's agent: the Patrol
11
- * over the Chat widget, which gets the larger share of the height. Laid over the Canvas,
10
+ * The sidebar on the Canvas's right side, where editors work with Camox's agent: the Patrol, as
11
+ * tall as its patrollers, over the Chat widget, which gets the rest of the height. Laid over the Canvas,
12
12
  * as the Content Sidebar is over its left: only the cards have a background, and the Canvas shows
13
13
  * around them. Elsewhere in the studio it is only hidden, so the chat keeps its thread and the
14
14
  * Preview and Studio pages follow the editor's runs too.
@@ -24,7 +24,7 @@ function AgentSidebar(t0) {
24
24
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
25
  t2 = cn("absolute inset-y-0 right-0 z-10 flex flex-col gap-2 py-2 max-md:hidden", SIDEBAR_GUTTER);
26
26
  t3 = { width: 265 };
27
- t4 = /* @__PURE__ */ jsx(Patrol, { className: "min-h-0 flex-[3_1_0]" });
27
+ t4 = /* @__PURE__ */ jsx(Patrol, { className: "min-h-0 flex-[0_1_auto]" });
28
28
  t5 = /* @__PURE__ */ jsx(ChatWidget, { className: "min-h-0 flex-[7_1_0]" });
29
29
  $[0] = t2;
30
30
  $[1] = t3;
@@ -4,15 +4,18 @@ import { AuthContext } from "../../../lib/auth.js";
4
4
  import { reads, useReadQuery } from "../../../lib/studio-reads.js";
5
5
  import { cn } from "../../../lib/utils.js";
6
6
  import { BOTTOM_CARD_EDGES, CARD_BODY_EDGES } from "../../preview/components/SidebarStack.js";
7
+ import { studioStore } from "../studioStore.js";
7
8
  import { FollowAgentContext } from "../followAgentContext.js";
8
9
  import { useThread } from "../../../lib/use-thread.js";
9
10
  import { EditorWhereContext } from "../editorWhereContext.js";
10
11
  import { useEditorSelection } from "../useEditorSelection.js";
11
- import { ThreadComposer } from "./ThreadComposer.js";
12
+ import { ConnectAgent } from "./ConnectAgent.js";
13
+ import { ThreadComposer, useComposerCarrier } from "./ThreadComposer.js";
12
14
  import { ThreadMessages } from "./ThreadMessages.js";
13
15
  import { c } from "react/compiler-runtime";
14
16
  import * as React from "react";
15
17
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
18
+ import { useSelector } from "@xstate/store-react";
16
19
  import { EMPTY_THREAD_VIEW } from "@camox/api-contract/threads";
17
20
 
18
21
  //#region src/features/studio/components/ChatWidget.tsx
@@ -23,7 +26,8 @@ import { EMPTY_THREAD_VIEW } from "@camox/api-contract/threads";
23
26
  * When the agent on Live hands the editor's request off to a draft, the studio that asked enters
24
27
  * it, if it still shows Live; the card the Live turn ends on opens it too. Each message tells the
25
28
  * agent where the editor is: the page they look at, with what they selected on it unless they
26
- * cleared it from the composer, or the Canvas with no page selected.
29
+ * cleared it from the composer, or the Canvas with no page selected. "Ask the agent" elsewhere in
30
+ * the studio fills its composer. Under the composer, it invites the editor to connect their own AI.
27
31
  */
28
32
  function ChatWidget(t0) {
29
33
  const $ = c(6);
@@ -44,9 +48,9 @@ function ChatWidget(t0) {
44
48
  children: "Agent"
45
49
  })
46
50
  });
47
- t3 = /* @__PURE__ */ jsx("div", {
51
+ t3 = /* @__PURE__ */ jsxs("div", {
48
52
  className: cn("bg-background flex min-h-0 flex-1 flex-col", CARD_BODY_EDGES),
49
- children: /* @__PURE__ */ jsx(CurrentThread, {})
53
+ children: [/* @__PURE__ */ jsx(CurrentThread, {}), /* @__PURE__ */ jsx(ConnectAgent, {})]
50
54
  });
51
55
  $[2] = t2;
52
56
  $[3] = t3;
@@ -68,7 +72,7 @@ function ChatWidget(t0) {
68
72
  return t4;
69
73
  }
70
74
  function CurrentThread() {
71
- const $ = c(14);
75
+ const $ = c(15);
72
76
  const { apiUrl, projectUuid } = React.useContext(AuthContext);
73
77
  let t0;
74
78
  if ($[0] !== projectUuid) {
@@ -80,6 +84,7 @@ function CurrentThread() {
80
84
  const draftId = useCurrentDraftId();
81
85
  const draftScope = useDraftScope();
82
86
  const navigate = useNavigate();
87
+ const composerCarry = useComposerCarrier();
83
88
  let t1;
84
89
  if ($[2] !== draftScope) {
85
90
  t1 = (started) => draftScope.enterStarted(started);
@@ -107,27 +112,29 @@ function CurrentThread() {
107
112
  }
108
113
  const t3 = `${project.id}:${draftId ?? "live"}`;
109
114
  let t4;
110
- if ($[7] !== apiUrl || $[8] !== draftId || $[9] !== enterDraft || $[10] !== openDraft || $[11] !== project.id || $[12] !== t3) {
115
+ if ($[7] !== apiUrl || $[8] !== composerCarry || $[9] !== draftId || $[10] !== enterDraft || $[11] !== openDraft || $[12] !== project.id || $[13] !== t3) {
111
116
  t4 = /* @__PURE__ */ jsx(Thread, {
112
117
  apiUrl,
113
118
  projectId: project.id,
114
119
  draftId,
115
120
  onEnterDraft: enterDraft,
116
- onOpenDraft: openDraft
121
+ onOpenDraft: openDraft,
122
+ composerCarry
117
123
  }, t3);
118
124
  $[7] = apiUrl;
119
- $[8] = draftId;
120
- $[9] = enterDraft;
121
- $[10] = openDraft;
122
- $[11] = project.id;
123
- $[12] = t3;
124
- $[13] = t4;
125
- } else t4 = $[13];
125
+ $[8] = composerCarry;
126
+ $[9] = draftId;
127
+ $[10] = enterDraft;
128
+ $[11] = openDraft;
129
+ $[12] = project.id;
130
+ $[13] = t3;
131
+ $[14] = t4;
132
+ } else t4 = $[14];
126
133
  return t4;
127
134
  }
128
135
  function Thread(t0) {
129
- const $ = c(36);
130
- const { apiUrl, projectId, draftId, onEnterDraft, onOpenDraft } = t0;
136
+ const $ = c(41);
137
+ const { apiUrl, projectId, draftId, onEnterDraft, onOpenDraft, composerCarry } = t0;
131
138
  let t1;
132
139
  if ($[0] !== onEnterDraft) {
133
140
  t1 = { onEnterDraft };
@@ -176,85 +183,109 @@ function Thread(t0) {
176
183
  const [cleared, setCleared] = React.useState(null);
177
184
  if (cleared !== null && selectionKey !== cleared) setCleared(null);
178
185
  const carried = selection && selectionKey !== cleared ? selection : void 0;
186
+ const asked = useSelector(studioStore, _temp);
179
187
  let t6;
180
- if ($[11] !== carried || $[12] !== follow || $[13] !== send || $[14] !== where) {
181
- t6 = (text) => {
188
+ let t7;
189
+ if ($[11] !== asked) {
190
+ t6 = () => {
191
+ if (asked) studioStore.trigger.takeAgentAsk();
192
+ };
193
+ t7 = [asked];
194
+ $[11] = asked;
195
+ $[12] = t6;
196
+ $[13] = t7;
197
+ } else {
198
+ t6 = $[12];
199
+ t7 = $[13];
200
+ }
201
+ React.useEffect(t6, t7);
202
+ let t8;
203
+ if ($[14] !== carried || $[15] !== follow || $[16] !== send || $[17] !== where) {
204
+ t8 = (text) => {
182
205
  follow?.resume();
183
206
  send(text, where?.on === "page" && carried ? {
184
207
  ...where,
185
208
  selection: carried
186
209
  } : where);
187
210
  };
188
- $[11] = carried;
189
- $[12] = follow;
190
- $[13] = send;
191
- $[14] = where;
192
- $[15] = t6;
193
- } else t6 = $[15];
194
- const sendFollowing = t6;
195
- let t7;
196
- if ($[16] !== onOpenDraft || $[17] !== sending || $[18] !== view) {
197
- t7 = /* @__PURE__ */ jsx(ThreadMessages, {
211
+ $[14] = carried;
212
+ $[15] = follow;
213
+ $[16] = send;
214
+ $[17] = where;
215
+ $[18] = t8;
216
+ } else t8 = $[18];
217
+ const sendFollowing = t8;
218
+ let t9;
219
+ if ($[19] !== onOpenDraft || $[20] !== sending || $[21] !== view) {
220
+ t9 = /* @__PURE__ */ jsx(ThreadMessages, {
198
221
  view,
199
222
  sending,
200
223
  onOpenDraft
201
224
  });
202
- $[16] = onOpenDraft;
203
- $[17] = sending;
204
- $[18] = view;
205
- $[19] = t7;
206
- } else t7 = $[19];
207
- let t8;
208
- if ($[20] !== error) {
209
- t8 = error && /* @__PURE__ */ jsx("p", {
225
+ $[19] = onOpenDraft;
226
+ $[20] = sending;
227
+ $[21] = view;
228
+ $[22] = t9;
229
+ } else t9 = $[22];
230
+ let t10;
231
+ if ($[23] !== error) {
232
+ t10 = error && /* @__PURE__ */ jsx("p", {
210
233
  role: "alert",
211
234
  className: "text-destructive px-2 pt-2 text-sm",
212
235
  children: error
213
236
  });
214
- $[20] = error;
215
- $[21] = t8;
216
- } else t8 = $[21];
217
- let t9;
218
- if ($[22] !== carried?.path) {
219
- t9 = carried?.path.at(-1);
220
- $[22] = carried?.path;
221
- $[23] = t9;
222
- } else t9 = $[23];
223
- let t10;
224
- if ($[24] !== selectionKey) {
225
- t10 = () => selectionKey !== null && setCleared(selectionKey);
226
- $[24] = selectionKey;
227
- $[25] = t10;
228
- } else t10 = $[25];
237
+ $[23] = error;
238
+ $[24] = t10;
239
+ } else t10 = $[24];
229
240
  let t11;
230
- if ($[26] !== sendFollowing || $[27] !== stop || $[28] !== t10 || $[29] !== t9 || $[30] !== view.running) {
231
- t11 = /* @__PURE__ */ jsx(ThreadComposer, {
241
+ if ($[25] !== carried?.path) {
242
+ t11 = carried?.path.at(-1);
243
+ $[25] = carried?.path;
244
+ $[26] = t11;
245
+ } else t11 = $[26];
246
+ let t12;
247
+ if ($[27] !== selectionKey) {
248
+ t12 = () => selectionKey !== null && setCleared(selectionKey);
249
+ $[27] = selectionKey;
250
+ $[28] = t12;
251
+ } else t12 = $[28];
252
+ const t13 = asked ?? void 0;
253
+ let t14;
254
+ if ($[29] !== composerCarry || $[30] !== sendFollowing || $[31] !== stop || $[32] !== t11 || $[33] !== t12 || $[34] !== t13 || $[35] !== view.running) {
255
+ t14 = /* @__PURE__ */ jsx(ThreadComposer, {
232
256
  onSend: sendFollowing,
233
257
  onStop: stop,
234
- selection: t9,
235
- onClearSelection: t10,
236
- running: view.running
258
+ selection: t11,
259
+ onClearSelection: t12,
260
+ running: view.running,
261
+ filled: t13,
262
+ carry: composerCarry
237
263
  });
238
- $[26] = sendFollowing;
239
- $[27] = stop;
240
- $[28] = t10;
241
- $[29] = t9;
242
- $[30] = view.running;
243
- $[31] = t11;
244
- } else t11 = $[31];
245
- let t12;
246
- if ($[32] !== t11 || $[33] !== t7 || $[34] !== t8) {
247
- t12 = /* @__PURE__ */ jsxs(Fragment, { children: [
248
- t7,
249
- t8,
250
- t11
251
- ] });
264
+ $[29] = composerCarry;
265
+ $[30] = sendFollowing;
266
+ $[31] = stop;
252
267
  $[32] = t11;
253
- $[33] = t7;
254
- $[34] = t8;
255
- $[35] = t12;
256
- } else t12 = $[35];
257
- return t12;
268
+ $[33] = t12;
269
+ $[34] = t13;
270
+ $[35] = view.running;
271
+ $[36] = t14;
272
+ } else t14 = $[36];
273
+ let t15;
274
+ if ($[37] !== t10 || $[38] !== t14 || $[39] !== t9) {
275
+ t15 = /* @__PURE__ */ jsxs(Fragment, { children: [
276
+ t9,
277
+ t10,
278
+ t14
279
+ ] });
280
+ $[37] = t10;
281
+ $[38] = t14;
282
+ $[39] = t9;
283
+ $[40] = t15;
284
+ } else t15 = $[40];
285
+ return t15;
286
+ }
287
+ function _temp(state) {
288
+ return state.context.agentAsk;
258
289
  }
259
290
 
260
291
  //#endregion