@agent-native/core 0.161.7 → 0.161.9

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 (65) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  3. package/corpus/templates/content/server/plugins/feature-flags.ts +5 -0
  4. package/corpus/templates/content/shared/feature-flags.ts +15 -0
  5. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +180 -37
  6. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -3
  7. package/corpus/templates/design/app/components/layout/Layout.tsx +4 -1
  8. package/corpus/templates/design/app/hooks/use-navigation-state.ts +13 -2
  9. package/corpus/templates/design/app/i18n-data.ts +11 -0
  10. package/corpus/templates/design/app/lib/agent-chat.ts +30 -0
  11. package/corpus/templates/design/app/lib/builder-host-chat.ts +34 -0
  12. package/corpus/templates/design/app/lib/builder-host-origin.ts +31 -0
  13. package/corpus/templates/design/app/lib/embed-chrome.ts +70 -0
  14. package/corpus/templates/design/app/lib/shell-design.ts +113 -0
  15. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +89 -0
  16. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +85 -12
  17. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +43 -19
  18. package/corpus/templates/design/app/pages/design-editor/screen-command-utils.ts +9 -2
  19. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +13 -0
  20. package/corpus/templates/design/app/root.tsx +23 -1
  21. package/corpus/templates/design/server/lib/fusion-screens.ts +17 -1
  22. package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +37 -0
  23. package/corpus/templates/design/server/routes/[...page].get.ts +1 -0
  24. package/corpus/templates/design/shared/builder-preview-url.ts +113 -0
  25. package/corpus/templates/design/shared/full-app.ts +19 -0
  26. package/corpus/templates/design/shared/shell-screens.ts +139 -0
  27. package/corpus/templates/design/shared/source-mode.ts +10 -0
  28. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +9 -0
  29. package/corpus/templates/slides/actions/patch-deck.ts +8 -0
  30. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +188 -66
  31. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +102 -22
  32. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +84 -2
  33. package/corpus/templates/slides/app/context/DeckContext.tsx +94 -3
  34. package/corpus/templates/slides/app/i18n/en-US.ts +5 -0
  35. package/corpus/templates/slides/app/pages/DeckEditor.tsx +122 -8
  36. package/corpus/templates/videos/package.json +9 -0
  37. package/dist/a2a/correlation.d.ts +6 -6
  38. package/dist/a2a/correlation.js +8 -6
  39. package/dist/a2a/types.d.ts +7 -5
  40. package/dist/client/RuntimeConfigNotice.js +3 -0
  41. package/dist/client/api-surface.d.ts +19 -0
  42. package/dist/client/api-surface.js +32 -0
  43. package/dist/client/application-state.js +4 -0
  44. package/dist/client/builder-frame.d.ts +6 -0
  45. package/dist/client/builder-frame.js +1 -1
  46. package/dist/client/client-status-requests.js +5 -0
  47. package/dist/client/host/index.d.ts +1 -0
  48. package/dist/client/host/index.js +1 -0
  49. package/dist/client/use-action.d.ts +1 -1
  50. package/dist/client/use-action.js +17 -0
  51. package/dist/client/use-session.js +5 -0
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/observability/routes.d.ts +5 -5
  54. package/dist/progress/routes.d.ts +1 -1
  55. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  56. package/dist/scripts/call-agent.js +6 -5
  57. package/dist/secrets/routes.d.ts +9 -9
  58. package/dist/server/agent-chat/action-filters-a2a.d.ts +6 -1
  59. package/dist/server/agent-chat/action-filters-a2a.js +29 -2
  60. package/dist/server/agent-chat/plugin-options.d.ts +6 -0
  61. package/dist/server/agent-chat-plugin.d.ts +2 -1
  62. package/dist/server/agent-chat-plugin.js +20 -4
  63. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  64. package/package.json +1 -1
  65. /package/corpus/templates/design/app/routes/{visual-edit.$id.tsx → visual-edit_.$id.tsx} +0 -0
@@ -112,6 +112,8 @@ export interface Slide {
112
112
  animations?: SlideAnimation[];
113
113
  /** @deprecated Use animations instead */
114
114
  splitByParagraph?: boolean;
115
+ /** Excluded from Present/Presenter mode playback, but stays in the deck. */
116
+ skipped?: boolean;
115
117
  }
116
118
 
117
119
  export type AnimationType = "appear" | "fade" | "slide-up" | "zoom";
@@ -237,6 +239,14 @@ interface DeckContextType {
237
239
  ) => void;
238
240
  deleteSlide: (deckId: string, slideId: string) => void;
239
241
  duplicateSlide: (deckId: string, slideId: string) => string | undefined;
242
+ /** Inserts a copy of arbitrary slide data after `afterSlideId`. Used for
243
+ * slide cut/paste, where the original may already be deleted so there is
244
+ * no live slide id left to duplicate from. */
245
+ pasteSlide: (
246
+ deckId: string,
247
+ afterSlideId: string,
248
+ slideFields: Omit<Slide, "id">,
249
+ ) => string | undefined;
240
250
  reorderSlides: (deckId: string, oldIndex: number, newIndex: number) => void;
241
251
  setDeckSlides: (deckId: string, slides: Slide[]) => void;
242
252
  /**
@@ -287,13 +297,54 @@ type DuplicateDeckActionResult = {
287
297
  url?: string;
288
298
  };
289
299
 
300
+ /** Per-slide fields `get-deck` computes for the agent (slide position/hash
301
+ * hints) that aren't part of the client's own `Slide` shape. Left on the
302
+ * fetched deck, they make every server refetch look "changed" relative to
303
+ * the client's slim optimistic copy — see `normalizeActionDeck`. */
304
+ const GET_DECK_ONLY_SLIDE_FIELDS = [
305
+ "slideNumber",
306
+ "zeroBasedIndex",
307
+ "contentHash",
308
+ ] as const;
309
+
310
+ /** Deck-level fields `get-deck` computes for the agent (counts, deep links,
311
+ * the currently-selected slide) that aren't part of the client's own `Deck`
312
+ * shape. See `normalizeActionDeck`. */
313
+ const GET_DECK_ONLY_DECK_FIELDS = [
314
+ "slideCount",
315
+ "slideNumbering",
316
+ "deepLink",
317
+ "selectedSlideId",
318
+ ] as const;
319
+
290
320
  function normalizeActionDeck(value: unknown): Deck | null {
291
321
  if (!value || typeof value !== "object") return null;
292
322
  const deck = value as Partial<Deck>;
293
323
  if (typeof deck.id !== "string") return null;
294
324
 
325
+ const deckRecord = deck as unknown as Record<string, unknown>;
326
+ const cleanedDeck = { ...deckRecord };
327
+ for (const field of GET_DECK_ONLY_DECK_FIELDS) delete cleanedDeck[field];
328
+
329
+ // Strip the same decorative fields from every slide, so a deck fetched from
330
+ // `get-deck` is structurally identical to one built by local mutations —
331
+ // otherwise `deckContentSignature` sees a "change" on every refetch of the
332
+ // open deck and spams the undo stack with no-op `replace-deck` entries.
333
+ const slides = Array.isArray(deck.slides)
334
+ ? deck.slides.map((slide) => {
335
+ if (!slide || typeof slide !== "object") return slide;
336
+ const cleanedSlide = {
337
+ ...(slide as unknown as Record<string, unknown>),
338
+ };
339
+ for (const field of GET_DECK_ONLY_SLIDE_FIELDS) {
340
+ delete cleanedSlide[field];
341
+ }
342
+ return cleanedSlide as unknown as Slide;
343
+ })
344
+ : [];
345
+
295
346
  return {
296
- ...deck,
347
+ ...cleanedDeck,
297
348
  id: deck.id,
298
349
  title: typeof deck.title === "string" ? deck.title : "Untitled",
299
350
  createdAt:
@@ -304,7 +355,7 @@ function normalizeActionDeck(value: unknown): Deck | null {
304
355
  typeof deck.updatedAt === "string"
305
356
  ? deck.updatedAt
306
357
  : deck.createdAt || "",
307
- slides: Array.isArray(deck.slides) ? deck.slides : [],
358
+ slides,
308
359
  } as Deck;
309
360
  }
310
361
 
@@ -873,7 +924,14 @@ export function deriveInverseOp(
873
924
  // restores exactly what changed (including clearing fields back to
874
925
  // undefined).
875
926
  if (!equalDeckValue(prior[key], op.fields[key])) {
876
- (priorFields as Record<string, unknown>)[key] = prior[key];
927
+ let priorValue: unknown = prior[key];
928
+ // `skipped` is undefined on a slide that was never skipped, but
929
+ // `undefined` doesn't survive JSON transport to the server — its
930
+ // `patch-slide` handler treats an absent field as "don't touch",
931
+ // so the persisted deck would stay skipped after undo. `false` is
932
+ // equivalent for this boolean field and does survive.
933
+ if (key === "skipped" && priorValue === undefined) priorValue = false;
934
+ (priorFields as Record<string, unknown>)[key] = priorValue;
877
935
  }
878
936
  }
879
937
  if (Object.keys(priorFields).length === 0) return null;
@@ -2346,6 +2404,38 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2346
2404
  [markDeckDirty, recordUndo, setDecksLocal],
2347
2405
  );
2348
2406
 
2407
+ const pasteSlide = useCallback(
2408
+ (deckId: string, afterSlideId: string, slideFields: Omit<Slide, "id">) => {
2409
+ const before = decksRef.current.find((d) => d.id === deckId);
2410
+ if (!before) return undefined;
2411
+
2412
+ markDeckDirty(deckId);
2413
+ const newSlide: Slide = { ...slideFields, id: nanoid(8) };
2414
+ setDecksLocal((prev) =>
2415
+ prev.map((d) => {
2416
+ if (d.id !== deckId) return d;
2417
+ const idx = d.slides.findIndex((s) => s.id === afterSlideId);
2418
+ const insertAt = idx === -1 ? d.slides.length : idx + 1;
2419
+ const slides = [...d.slides];
2420
+ slides.splice(insertAt, 0, newSlide);
2421
+ return { ...d, slides, updatedAt: new Date().toISOString() };
2422
+ }),
2423
+ );
2424
+ // Granular add-slide op, same as duplicateSlide — inserts after
2425
+ // afterSlideId regardless of whether that id is also the copy source.
2426
+ const op: PatchDeckOp = {
2427
+ op: "add-slide",
2428
+ slideId: newSlide.id,
2429
+ afterSlideId,
2430
+ fields: addSlideFields(newSlide),
2431
+ };
2432
+ enqueueDeckOp(deckId, op);
2433
+ recordUndo(before, op, { label: "Paste slide" });
2434
+ return newSlide.id;
2435
+ },
2436
+ [markDeckDirty, recordUndo, setDecksLocal],
2437
+ );
2438
+
2349
2439
  const reorderSlides = useCallback(
2350
2440
  (deckId: string, oldIndex: number, newIndex: number) => {
2351
2441
  markDeckDirty(deckId);
@@ -2432,6 +2522,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2432
2522
  updateSlide,
2433
2523
  deleteSlide,
2434
2524
  duplicateSlide,
2525
+ pasteSlide,
2435
2526
  reorderSlides,
2436
2527
  setDeckSlides,
2437
2528
  markDeckDirty,
@@ -524,6 +524,11 @@ const messages = {
524
524
  newSlide: "New slide",
525
525
  closeAddSlides: "Close",
526
526
  describeThisSlide: "Describe this slide",
527
+ cut: "Cut",
528
+ copy: "Copy",
529
+ paste: "Paste",
530
+ skipSlide: "Skip slide",
531
+ unskipSlide: "Unskip slide",
527
532
  },
528
533
  presentation: {
529
534
  loadFailed: "Could not load this presentation.",
@@ -60,6 +60,7 @@ import { Button } from "@/components/ui/button";
60
60
  import {
61
61
  deckIdFromPathname,
62
62
  hasUnsavedDeckChanges,
63
+ type Slide,
63
64
  useDecks,
64
65
  useSaveState,
65
66
  } from "@/context/DeckContext";
@@ -185,6 +186,7 @@ export default function DeckEditor() {
185
186
  updateSlide,
186
187
  deleteSlide,
187
188
  duplicateSlide,
189
+ pasteSlide,
188
190
  duplicateDeck,
189
191
  addSlide,
190
192
  flushDeckSave,
@@ -852,11 +854,107 @@ export default function DeckEditor() {
852
854
  return () => document.removeEventListener("keydown", handleKeyDown);
853
855
  }, [deck, id, activeSlideId, deleteSlideWithUndo, pinMode, drawMode]);
854
856
 
855
- // Command/Ctrl+C then Command/Ctrl+V on the slide rail duplicates the
857
+ // Slide-level clipboard backing both the Cmd+C/Cmd+V shortcut below and the
858
+ // rail's right-click Cut/Copy/Paste menu. Holds a full slide snapshot
859
+ // (rather than just an id) so paste still works after Cut has already
860
+ // removed the original slide from the deck.
861
+ const slideClipboardRef = useRef<Slide | null>(null);
862
+ const [hasSlideClipboard, setHasSlideClipboard] = useState(false);
863
+
864
+ const copySlide = useCallback(
865
+ (slideId: string) => {
866
+ const slide = deck?.slides.find((s) => s.id === slideId);
867
+ if (!slide) return;
868
+ slideClipboardRef.current = slide;
869
+ setHasSlideClipboard(true);
870
+ },
871
+ [deck],
872
+ );
873
+
874
+ const cutSlide = useCallback(
875
+ (slideId: string) => {
876
+ if (!deck || !id || deck.slides.length <= 1) return; // don't cut the last slide
877
+ const slide = deck.slides.find((s) => s.id === slideId);
878
+ if (!slide) return;
879
+ slideClipboardRef.current = slide;
880
+ setHasSlideClipboard(true);
881
+ const idx = deck.slides.findIndex((s) => s.id === slideId);
882
+ const nextSlide = deck.slides[idx + 1] || deck.slides[idx - 1];
883
+ deleteSlideWithUndo(id, slideId);
884
+ if (activeSlideId === slideId && nextSlide) {
885
+ setActiveSlideId(nextSlide.id);
886
+ }
887
+ },
888
+ [deck, id, activeSlideId, deleteSlideWithUndo],
889
+ );
890
+
891
+ const pasteSlideAfter = useCallback(
892
+ (targetSlideId: string) => {
893
+ const clipboard = slideClipboardRef.current;
894
+ if (!clipboard || !id) return;
895
+ const { id: _clipboardId, ...fields } = clipboard;
896
+ const newId = pasteSlide(id, targetSlideId, fields);
897
+ if (newId) setActiveSlideId(newId);
898
+ },
899
+ [id, pasteSlide],
900
+ );
901
+
902
+ // Handlers backing the slide rail's right-click menu.
903
+ const handleDeleteSlideFromRail = useCallback(
904
+ (slideId: string) => {
905
+ if (!deck || !id || deck.slides.length <= 1) return; // don't delete the last slide
906
+ const idx = deck.slides.findIndex((s) => s.id === slideId);
907
+ const nextSlide = deck.slides[idx + 1] || deck.slides[idx - 1];
908
+ deleteSlideWithUndo(id, slideId);
909
+ if (activeSlideId === slideId && nextSlide) {
910
+ setActiveSlideId(nextSlide.id);
911
+ }
912
+ },
913
+ [deck, id, activeSlideId, deleteSlideWithUndo],
914
+ );
915
+
916
+ const handleDuplicateSlideFromRail = useCallback(
917
+ (slideId: string) => {
918
+ if (!id) return;
919
+ const newId = duplicateSlide(id, slideId);
920
+ if (newId) setActiveSlideId(newId);
921
+ },
922
+ [id, duplicateSlide],
923
+ );
924
+
925
+ const handleNewSlideAfter = useCallback(
926
+ (afterSlideId: string) => {
927
+ if (!deck || !id) return;
928
+ const afterIdx = deck.slides.findIndex((s) => s.id === afterSlideId);
929
+ // Immediate persistence: mirrors handleAddEmptySlide, since this also
930
+ // opens the "describe this slide" popover right away.
931
+ const newId = addSlide(
932
+ id,
933
+ "blank",
934
+ afterIdx >= 0 ? afterIdx : undefined,
935
+ { persistence: "immediate" },
936
+ );
937
+ setActiveSlideId(newId);
938
+ setSidebarOpen(true);
939
+ setDescribeSlideId(newId);
940
+ },
941
+ [deck, id, addSlide],
942
+ );
943
+
944
+ const handleToggleSkipSlide = useCallback(
945
+ (slideId: string) => {
946
+ if (!deck || !id) return;
947
+ const slide = deck.slides.find((s) => s.id === slideId);
948
+ if (!slide) return;
949
+ updateSlide(id, slideId, { skipped: !slide.skipped });
950
+ },
951
+ [deck, id, updateSlide],
952
+ );
953
+
954
+ // Command/Ctrl+C then Command/Ctrl+V on the slide rail copies/pastes the
856
955
  // selected slide directly below itself. Only claims the shortcut when no
857
956
  // slide element is selected — SlideEditor owns Cmd+C/V for object copy/paste
858
957
  // in that case.
859
- const copiedSlideIdRef = useRef<string | null>(null);
860
958
  useEffect(() => {
861
959
  const handleKeyDown = (e: KeyboardEvent) => {
862
960
  if (!deck || !id || !canEdit) return;
@@ -925,19 +1023,27 @@ export default function DeckEditor() {
925
1023
 
926
1024
  if (key === "c") {
927
1025
  if (!activeSlideId) return;
928
- copiedSlideIdRef.current = activeSlideId;
1026
+ copySlide(activeSlideId);
929
1027
  return;
930
1028
  }
931
1029
 
932
- const copiedId = copiedSlideIdRef.current;
933
- if (!copiedId || !deck.slides.some((s) => s.id === copiedId)) return;
1030
+ if (!hasSlideClipboard || !activeSlideId) return;
934
1031
  e.preventDefault();
935
- const newId = duplicateSlide(id, copiedId);
936
- if (newId) setActiveSlideId(newId);
1032
+ pasteSlideAfter(activeSlideId);
937
1033
  };
938
1034
  document.addEventListener("keydown", handleKeyDown);
939
1035
  return () => document.removeEventListener("keydown", handleKeyDown);
940
- }, [deck, id, canEdit, activeSlideId, duplicateSlide, pinMode, drawMode]);
1036
+ }, [
1037
+ deck,
1038
+ id,
1039
+ canEdit,
1040
+ activeSlideId,
1041
+ copySlide,
1042
+ hasSlideClipboard,
1043
+ pasteSlideAfter,
1044
+ pinMode,
1045
+ drawMode,
1046
+ ]);
941
1047
 
942
1048
  // Resolve the active slide from URL/deck state. Imports replace slide IDs, so
943
1049
  // keep this valid after deck contents change instead of only on first load.
@@ -1371,6 +1477,14 @@ export default function DeckEditor() {
1371
1477
  setGeneratingSlideSelected(true);
1372
1478
  if (window.innerWidth < 768) setSidebarOpen(false);
1373
1479
  }}
1480
+ hasSlideClipboard={hasSlideClipboard}
1481
+ onCutSlide={cutSlide}
1482
+ onCopySlide={copySlide}
1483
+ onPasteSlide={pasteSlideAfter}
1484
+ onDeleteSlide={handleDeleteSlideFromRail}
1485
+ onNewSlideAfter={handleNewSlideAfter}
1486
+ onDuplicateSlide={handleDuplicateSlideFromRail}
1487
+ onToggleSkipSlide={handleToggleSkipSlide}
1374
1488
  />
1375
1489
  </DndContext>
1376
1490
  </div>
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "videos",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "node ../../scripts/build-retired-netlify-site.ts videos"
7
+ },
8
+ "agentNativeRetiredCompatibility": true
9
+ }
@@ -3,11 +3,11 @@ export declare const MAX_A2A_CORRELATION_VALUE_CHARS = 200;
3
3
  export declare const MAX_A2A_DELEGATION_HOPS = 3;
4
4
  export declare function sanitizeA2ACorrelationId(value: unknown): string | undefined;
5
5
  /**
6
- * Keep only bounded, opaque ASCII correlation identifiers. These values
7
- * remain telemetry hints; authentication continues to come exclusively from
8
- * the verified A2A token/request context. `callerModel` is the one value here
9
- * a receiver may act on, and only as a preference — it never reaches identity,
10
- * org, access, or approval resolution, and it can only name a model the
11
- * receiver's own engine already offers (see `resolveDelegatedRunModel`).
6
+ * Keep only bounded, opaque ASCII correlation and routing identifiers.
7
+ * Authentication continues to come exclusively from the verified A2A
8
+ * token/request context. A receiver may use `selectedReceiverApp` only to
9
+ * prioritize its matching local tool surface, and `callerModel` only to choose
10
+ * a model its own engine already offers. Neither reaches identity, data
11
+ * ownership, org, access, or approval resolution.
12
12
  */
13
13
  export declare function sanitizeA2ACorrelationMetadata(value: unknown): A2ACorrelationMetadata;
@@ -19,18 +19,19 @@ export function sanitizeA2ACorrelationId(value) {
19
19
  return boundedIdentifier(value, CORRELATION_ID_PATTERN);
20
20
  }
21
21
  /**
22
- * Keep only bounded, opaque ASCII correlation identifiers. These values
23
- * remain telemetry hints; authentication continues to come exclusively from
24
- * the verified A2A token/request context. `callerModel` is the one value here
25
- * a receiver may act on, and only as a preference — it never reaches identity,
26
- * org, access, or approval resolution, and it can only name a model the
27
- * receiver's own engine already offers (see `resolveDelegatedRunModel`).
22
+ * Keep only bounded, opaque ASCII correlation and routing identifiers.
23
+ * Authentication continues to come exclusively from the verified A2A
24
+ * token/request context. A receiver may use `selectedReceiverApp` only to
25
+ * prioritize its matching local tool surface, and `callerModel` only to choose
26
+ * a model its own engine already offers. Neither reaches identity, data
27
+ * ownership, org, access, or approval resolution.
28
28
  */
29
29
  export function sanitizeA2ACorrelationMetadata(value) {
30
30
  if (!value || typeof value !== "object" || Array.isArray(value))
31
31
  return {};
32
32
  const metadata = value;
33
33
  const callerApp = boundedIdentifier(metadata.callerApp, APP_ID_PATTERN);
34
+ const selectedReceiverApp = boundedIdentifier(metadata.selectedReceiverApp, APP_ID_PATTERN);
34
35
  const callerThreadId = sanitizeA2ACorrelationId(metadata.callerThreadId);
35
36
  const parentRunId = sanitizeA2ACorrelationId(metadata.parentRunId);
36
37
  const parentTurnId = sanitizeA2ACorrelationId(metadata.parentTurnId);
@@ -62,6 +63,7 @@ export function sanitizeA2ACorrelationMetadata(value) {
62
63
  : undefined;
63
64
  return {
64
65
  ...(callerApp ? { callerApp } : {}),
66
+ ...(selectedReceiverApp ? { selectedReceiverApp } : {}),
65
67
  ...(callerThreadId ? { callerThreadId } : {}),
66
68
  ...(parentRunId ? { parentRunId } : {}),
67
69
  ...(parentTurnId ? { parentTurnId } : {}),
@@ -117,14 +117,16 @@ export interface A2ASourceContextReference {
117
117
  integrationTaskId: string;
118
118
  }
119
119
  /**
120
- * Telemetry-only cross-app correlation, plus `callerModel` a preference
121
- * hint. Receivers must never use any caller-supplied value here for identity,
122
- * ownership, org scoping, access, or approval decisions. `callerModel` widens
123
- * this channel to a preference, never to an authorization: it may at most pick
124
- * a model the receiver's already-resolved engine advertises.
120
+ * Bounded cross-app correlation and routing preferences. Receivers must never
121
+ * use any caller-supplied value here for identity, data ownership, org scoping,
122
+ * access, or approval decisions. `selectedReceiverApp` may only prioritize the
123
+ * matching receiver's local tool surface, while `callerModel` may only pick a
124
+ * model the receiver's already-resolved engine advertises.
125
125
  */
126
126
  export interface A2ACorrelationMetadata {
127
127
  callerApp?: string;
128
+ /** App the caller deliberately selected for this delegated objective. */
129
+ selectedReceiverApp?: string;
128
130
  callerThreadId?: string;
129
131
  parentRunId?: string;
130
132
  parentTurnId?: string;
@@ -3,6 +3,7 @@ import { IconAlertCircle, IconAlertTriangle, IconCheck, IconChevronDown, IconChe
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { parseRuntimeConfigReport, } from "../shared/runtime-config.js";
5
5
  import { agentNativePath } from "./api-path.js";
6
+ import { agentNativeApiDisabledReason } from "./api-surface.js";
6
7
  import { writeClipboardText } from "./clipboard.js";
7
8
  import { useT } from "./i18n.js";
8
9
  function injectedAppConfig() {
@@ -31,6 +32,8 @@ export function RuntimeConfigNotice() {
31
32
  useEffect(() => {
32
33
  if (typeof window.fetch !== "function")
33
34
  return;
35
+ if (agentNativeApiDisabledReason())
36
+ return;
34
37
  const controller = new AbortController();
35
38
  let active = true;
36
39
  const timeout = window.setTimeout(() => controller.abort(), 5000);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Some surfaces are framed by a host and carry no agent-native session at all —
3
+ * Builder's Design tab frames a canvas that owns no design row and holds no
4
+ * credential. Every `/_agent-native/*` call from one is unauthenticated by
5
+ * construction, so the client must not make them: a 401 per poll buries real
6
+ * failures, and a write can never land.
7
+ */
8
+ /** Pass `null` to re-enable, so a surface can be entered and left. */
9
+ export declare function setAgentNativeApiDisabled(reason: string | null): void;
10
+ export declare function agentNativeApiDisabledReason(): string | null;
11
+ /**
12
+ * Thrown rather than resolved: a caller that cannot tell "no backend" from
13
+ * "empty result" reports success for work that never happened.
14
+ */
15
+ export declare class AgentNativeApiDisabledError extends Error {
16
+ readonly reason: string;
17
+ constructor(detail: string);
18
+ }
19
+ export declare function assertAgentNativeApiEnabled(detail: string): void;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Some surfaces are framed by a host and carry no agent-native session at all —
3
+ * Builder's Design tab frames a canvas that owns no design row and holds no
4
+ * credential. Every `/_agent-native/*` call from one is unauthenticated by
5
+ * construction, so the client must not make them: a 401 per poll buries real
6
+ * failures, and a write can never land.
7
+ */
8
+ let disabledReason = null;
9
+ /** Pass `null` to re-enable, so a surface can be entered and left. */
10
+ export function setAgentNativeApiDisabled(reason) {
11
+ disabledReason = reason?.trim() ? reason.trim() : null;
12
+ }
13
+ export function agentNativeApiDisabledReason() {
14
+ return disabledReason;
15
+ }
16
+ /**
17
+ * Thrown rather than resolved: a caller that cannot tell "no backend" from
18
+ * "empty result" reports success for work that never happened.
19
+ */
20
+ export class AgentNativeApiDisabledError extends Error {
21
+ reason;
22
+ constructor(detail) {
23
+ const reason = disabledReason ?? "unknown surface";
24
+ super(`agent-native API is disabled on this surface (${reason}): ${detail}`);
25
+ this.name = "AgentNativeApiDisabledError";
26
+ this.reason = reason;
27
+ }
28
+ }
29
+ export function assertAgentNativeApiEnabled(detail) {
30
+ if (disabledReason)
31
+ throw new AgentNativeApiDisabledError(detail);
32
+ }
@@ -1,4 +1,5 @@
1
1
  import { agentNativePath } from "./api-path.js";
2
+ import { assertAgentNativeApiEnabled } from "./api-surface.js";
2
3
  const APP_STATE_KEY_PATTERN = /^[a-zA-Z0-9_:-]+$/;
3
4
  function appStateUrl(key) {
4
5
  if (!APP_STATE_KEY_PATTERN.test(key)) {
@@ -63,6 +64,7 @@ function jsonBody(value) {
63
64
  /** Server caps a batch at 100 keys; stay under it when splitting. */
64
65
  const MAX_BATCH_KEYS = 100;
65
66
  export async function readClientAppStateMany(keys, options = {}) {
67
+ assertAgentNativeApiEnabled(`read application state [${keys.join(", ")}]`);
66
68
  const unique = [...new Set(keys)];
67
69
  for (const key of unique)
68
70
  appStateUrl(key); // validates the key shape
@@ -142,6 +144,7 @@ export async function readClientAppState(key, options = {}) {
142
144
  return (batch.values[key] ?? null);
143
145
  }
144
146
  export async function writeClientAppState(key, value, options = {}) {
147
+ assertAgentNativeApiEnabled(`write application state "${key}"`);
145
148
  const response = await fetch(appStateUrl(key), {
146
149
  method: "PUT",
147
150
  headers: buildHeaders(options.requestSource),
@@ -152,6 +155,7 @@ export async function writeClientAppState(key, value, options = {}) {
152
155
  return parseAppStateResponse(response, `Write application state "${key}"`);
153
156
  }
154
157
  export async function deleteClientAppState(key, options = {}) {
158
+ assertAgentNativeApiEnabled(`delete application state "${key}"`);
155
159
  const response = await fetch(appStateUrl(key), {
156
160
  method: "DELETE",
157
161
  // DELETE carries no JSON body, so this custom header is the only
@@ -19,6 +19,12 @@ export interface BuilderChatMessage {
19
19
  submit?: boolean;
20
20
  mode?: "act" | "plan";
21
21
  requestMode?: "act" | "plan";
22
+ /**
23
+ * Origin an embedder already verified for itself. `getBuilderParentOrigin()`
24
+ * needs `?builder.*` params to trust a loopback parent, which a handshake-based
25
+ * embed never carries — without this the message falls back to `"*"`.
26
+ */
27
+ targetOrigin?: string;
22
28
  }
23
29
  export declare function sendToBuilderChat(opts: BuilderChatMessage): boolean;
24
30
  /**
@@ -116,7 +116,7 @@ export function sendToBuilderChat(opts) {
116
116
  if (typeof window === "undefined" || !opts.message?.trim())
117
117
  return false;
118
118
  const hasParentFrame = window.parent !== window;
119
- const targetOrigin = getBuilderParentOrigin() ?? "*";
119
+ const targetOrigin = opts.targetOrigin ?? getBuilderParentOrigin() ?? "*";
120
120
  const payload = {
121
121
  type: "builder.submitChat",
122
122
  data: {
@@ -1,4 +1,5 @@
1
1
  import { agentNativePath } from "./api-path.js";
2
+ import { agentNativeApiDisabledReason } from "./api-surface.js";
2
3
  const RESULT_TTL_MS = 500;
3
4
  const REQUEST_TIMEOUT_MS = 15_000;
4
5
  const cache = new Map();
@@ -31,6 +32,10 @@ function installInvalidationListeners() {
31
32
  }
32
33
  }
33
34
  async function fetchClientStatus(path) {
35
+ // "unavailable" rather than a fabricated payload: callers already treat it as
36
+ // "could not read", and there is genuinely nothing to read here.
37
+ if (agentNativeApiDisabledReason())
38
+ return { state: "unavailable" };
34
39
  installInvalidationListeners();
35
40
  const url = agentNativePath(path);
36
41
  const cached = cache.get(url);
@@ -1,4 +1,5 @@
1
1
  export { initializeAgentNativeClient } from "../client-bootstrap.js";
2
+ export { agentNativeApiDisabledReason, AgentNativeApiDisabledError, setAgentNativeApiDisabled, } from "../api-surface.js";
2
3
  export { ensureEmbedAuthFetchInterceptor, getEmbedAuthToken, isEmbedAuthActive, isEmbedMcpChatBridgeActive, } from "../embed-auth.js";
3
4
  export { sendToFrame, onFrameMessage, requestUserInfo, getFrameOrigin, getFramePostMessageTargetOrigin, getCallbackOrigin, oauthRedirectUri, isInFrame, enterStyleEditing, enterTextEditing, exitSelectionMode, type UserInfo, } from "../frame.js";
4
5
  export { getBuilderParentOrigin, isInBuilderFrame, sendToBuilderChat, type BuilderChatMessage, } from "../builder-frame.js";
@@ -1,4 +1,5 @@
1
1
  export { initializeAgentNativeClient } from "../client-bootstrap.js";
2
+ export { agentNativeApiDisabledReason, AgentNativeApiDisabledError, setAgentNativeApiDisabled, } from "../api-surface.js";
2
3
  export { ensureEmbedAuthFetchInterceptor, getEmbedAuthToken, isEmbedAuthActive, isEmbedMcpChatBridgeActive, } from "../embed-auth.js";
3
4
  export { sendToFrame, onFrameMessage, requestUserInfo, getFrameOrigin, getFramePostMessageTargetOrigin, getCallbackOrigin, oauthRedirectUri, isInFrame, enterStyleEditing, enterTextEditing, exitSelectionMode, } from "../frame.js";
4
5
  export { getBuilderParentOrigin, isInBuilderFrame, sendToBuilderChat, } from "../builder-frame.js";
@@ -73,7 +73,7 @@ export declare const ACTION_KEEPALIVE_BODY_BUDGET_BYTES = 48000;
73
73
  * `/_agent-native/actions/*` in components.
74
74
  */
75
75
  export declare function callAction<TResult = undefined, TName extends ActionName = ActionName>(actionName: TName, params?: ActionParams<TName>, options?: ClientActionCallOptions): Promise<TResult extends undefined ? ActionResult<TName> : TResult>;
76
- export type KeepaliveActionCallRejectionReason = "body-too-large" | "budget-exhausted";
76
+ export type KeepaliveActionCallRejectionReason = "body-too-large" | "budget-exhausted" | "api-disabled";
77
77
  export type KeepaliveActionCallResult<TResult> = {
78
78
  accepted: true;
79
79
  bodyBytes: number;
@@ -27,6 +27,7 @@ import { getAnalyticsClientPlatform } from "./analytics-platform.js";
27
27
  import { getOrCreateAnalyticsSessionId } from "./analytics-session.js";
28
28
  import { trackEvent } from "./analytics.js";
29
29
  import { agentNativePath } from "./api-path.js";
30
+ import { agentNativeApiDisabledReason, assertAgentNativeApiEnabled, } from "./api-surface.js";
30
31
  import { getBrowserTabId } from "./browser-tab-id.js";
31
32
  import { clientBuildId, clientCompatibilityVersion, reloadForClientCompatibilityMismatch, } from "./build-compatibility.js";
32
33
  import { ensureEmbedAuthFetchInterceptor } from "./embed-auth.js";
@@ -398,6 +399,7 @@ function shouldTrackActionResponse(error, durationMs, response) {
398
399
  return Math.random() < rate;
399
400
  }
400
401
  async function actionFetch(name, method, params, options) {
402
+ assertAgentNativeApiEnabled(`${method} ${name}`);
401
403
  const startedAt = actionTelemetryNow();
402
404
  let response;
403
405
  let responseAt;
@@ -501,6 +503,16 @@ export function callAction(actionName, params, options = {}) {
501
503
  export function tryCallActionKeepalive(actionName, params, options = {}) {
502
504
  const serializedBody = JSON.stringify(params ?? {});
503
505
  const bodyBytes = utf8ByteLength(serializedBody);
506
+ // Reported as a refusal rather than thrown: callers keep the work queued on
507
+ // `accepted: false`, which is the honest outcome for a surface with no backend.
508
+ if (agentNativeApiDisabledReason()) {
509
+ return {
510
+ accepted: false,
511
+ bodyBytes,
512
+ reason: "api-disabled",
513
+ completion: null,
514
+ };
515
+ }
504
516
  if (bodyBytes > ACTION_KEEPALIVE_BODY_BUDGET_BYTES) {
505
517
  return {
506
518
  accepted: false,
@@ -547,6 +559,10 @@ export function tryCallActionKeepalive(actionName, params, options = {}) {
547
559
  * ```
548
560
  */
549
561
  export function useActionQuery(actionName, params, options) {
562
+ // Not `enabled: false` via options: a disabled surface must win over whatever
563
+ // the caller asked for, and an unfired query reads as "no data" rather than
564
+ // as an error the UI has to special-case.
565
+ const apiDisabled = Boolean(agentNativeApiDisabledReason());
550
566
  return useQuery({
551
567
  queryKey: ["action", actionName, params],
552
568
  // Thread React Query's per-fetch AbortSignal into the network request so
@@ -556,6 +572,7 @@ export function useActionQuery(actionName, params, options) {
556
572
  retry: defaultActionQueryRetry,
557
573
  retryDelay: defaultActionQueryRetryDelay,
558
574
  ...options,
575
+ ...(apiDisabled ? { enabled: false } : {}),
559
576
  });
560
577
  }
561
578
  // ---------------------------------------------------------------------------
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
  import { setSentryUser, trackSessionStatus } from "./analytics.js";
3
+ import { agentNativeApiDisabledReason } from "./api-surface.js";
3
4
  import { fetchAuthSessionStatus, invalidateClientStatusRequest, } from "./client-status-requests.js";
4
5
  import { getFrameOrigin, getFramePostMessageTargetOrigin } from "./frame.js";
5
6
  const SESSION_CACHE_TTL_MS = 30_000;
@@ -98,6 +99,10 @@ export function notifySessionInvalidated() {
98
99
  }
99
100
  }
100
101
  function fetchSharedSession() {
102
+ // A surface with no agent-native backend is genuinely signed out. `undefined`
103
+ // would read as "unavailable" and retry until it gave up with an error.
104
+ if (agentNativeApiDisabledReason())
105
+ return Promise.resolve(null);
101
106
  if (hasFreshSessionCache())
102
107
  return Promise.resolve(cachedSession ?? null);
103
108
  if (sessionRequest)