@agent-native/core 0.155.0 → 0.157.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 (95) hide show
  1. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  2. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  3. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +17 -13
  4. package/corpus/templates/clips/app/components/player/share-dialog.tsx +102 -59
  5. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +15 -1
  6. package/corpus/templates/clips/app/routes/share.$shareId.tsx +16 -2
  7. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  8. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  9. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  10. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  11. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  12. package/corpus/templates/slides/app/context/DeckContext.tsx +42 -2
  13. package/corpus/templates/slides/app/global.css +7 -40
  14. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  15. package/corpus/templates/slides/app/pages/DeckEditor.tsx +130 -45
  16. package/dist/agent/index.d.ts +1 -1
  17. package/dist/agent/production-agent.d.ts +30 -0
  18. package/dist/agent/production-agent.js +100 -8
  19. package/dist/agent/types.d.ts +9 -0
  20. package/dist/client/AgentPanel.js +12 -9
  21. package/dist/client/AssistantChat.d.ts +15 -1
  22. package/dist/client/AssistantChat.js +110 -43
  23. package/dist/client/MultiTabAssistantChat.js +44 -27
  24. package/dist/client/chat/markdown-renderer.js +4 -2
  25. package/dist/client/chat/message-components.d.ts +2 -2
  26. package/dist/client/chat/message-components.js +69 -35
  27. package/dist/client/chat/run-recovery.d.ts +4 -1
  28. package/dist/client/chat/run-recovery.js +62 -30
  29. package/dist/client/chat/tool-call-display.d.ts +12 -3
  30. package/dist/client/chat/tool-call-display.js +79 -40
  31. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  32. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  33. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  34. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  35. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  36. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  37. package/dist/client/components/ui/message-scroller.js +7 -3
  38. package/dist/client/composer/runtime-adapters.js +3 -2
  39. package/dist/client/context-xray/ContextMeter.js +5 -1
  40. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  41. package/dist/client/error-format.d.ts +4 -0
  42. package/dist/client/error-format.js +93 -5
  43. package/dist/client/i18n.js +83 -17
  44. package/dist/client/observability/ThumbsFeedback.js +10 -8
  45. package/dist/client/sharing/ShareButton.js +160 -54
  46. package/dist/coding-tools/run-code.js +2 -1
  47. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  48. package/dist/coding-tools/sandbox/background.js +8 -1
  49. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  50. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  51. package/dist/collab/routes.d.ts +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  54. package/dist/localization/core-messages/ar-SA.js +541 -0
  55. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  56. package/dist/localization/core-messages/de-DE.js +513 -0
  57. package/dist/localization/core-messages/en-US.d.ts +513 -0
  58. package/dist/localization/core-messages/en-US.js +513 -0
  59. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  60. package/dist/localization/core-messages/es-ES.js +520 -0
  61. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  62. package/dist/localization/core-messages/fr-FR.js +520 -0
  63. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  64. package/dist/localization/core-messages/hi-IN.js +513 -0
  65. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  66. package/dist/localization/core-messages/ja-JP.js +506 -0
  67. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  68. package/dist/localization/core-messages/ko-KR.js +506 -0
  69. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  70. package/dist/localization/core-messages/pt-BR.js +520 -0
  71. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  72. package/dist/localization/core-messages/zh-CN.js +506 -0
  73. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  74. package/dist/localization/core-messages/zh-TW.js +506 -0
  75. package/dist/localization/core-messages.d.ts +524 -0
  76. package/dist/localization/core-messages.js +124 -0
  77. package/dist/localization/default-messages.d.ts +2 -0
  78. package/dist/localization/default-messages.js +2 -0
  79. package/dist/notifications/routes.d.ts +1 -1
  80. package/dist/observability/routes.d.ts +5 -5
  81. package/dist/resources/handlers.d.ts +1 -1
  82. package/dist/secrets/routes.d.ts +9 -9
  83. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  84. package/dist/server/agent-chat-plugin.d.ts +10 -0
  85. package/dist/server/agent-chat-plugin.js +144 -41
  86. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  87. package/dist/server/agent-run-context.d.ts +7 -1
  88. package/dist/server/agent-run-context.js +9 -2
  89. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  90. package/dist/server/agent-teams.js +38 -9
  91. package/dist/server/index.d.ts +1 -1
  92. package/dist/server/request-context.d.ts +7 -0
  93. package/dist/server/request-context.js +15 -0
  94. package/docs/content/agent-surfaces.mdx +30 -0
  95. package/package.json +2 -2
@@ -223,7 +223,9 @@ interface DeckContextType {
223
223
  deckId: string,
224
224
  layout?: SlideLayout,
225
225
  afterIndex?: number,
226
+ options?: { persistence?: "debounced" | "immediate" },
226
227
  ) => string;
228
+ flushDeckSave: (deckId: string) => Promise<void>;
227
229
  updateSlide: (
228
230
  deckId: string,
229
231
  slideId: string,
@@ -499,6 +501,38 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
499
501
  return next;
500
502
  }
501
503
 
504
+ /**
505
+ * Wait for a deck's in-flight save(s) to fully settle, including any
506
+ * follow-up drain chained by immediateFlushRequests for ops queued while a
507
+ * save was already running, and any requeued retry after a failed attempt.
508
+ * Used when a caller must not proceed (e.g. firing an agent request against
509
+ * a slide) until an "immediate" persistence op has actually reached the
510
+ * server. Throws if the save ultimately fails after retries exhaust, since
511
+ * `drainPendingDeckOps` swallows save errors internally to drive its own
512
+ * retry loop and its promise always resolves regardless of outcome.
513
+ */
514
+ async function flushDeckSave(deckId: string): Promise<void> {
515
+ for (let i = 0; i < 40; i++) {
516
+ const active = inFlightSaveChains.get(deckId);
517
+ if (active) {
518
+ await active;
519
+ continue;
520
+ }
521
+ if (failedSaveDecks.has(deckId)) {
522
+ throw new Error(
523
+ `Failed to save deck ${deckId} after ${MAX_DECK_SAVE_RETRIES} attempts`,
524
+ );
525
+ }
526
+ if (pendingOpsQueue.has(deckId) || pendingSaves.has(deckId)) {
527
+ // A failed op was requeued for retry, or a debounced save is armed;
528
+ // wait for it to actually run rather than declaring success early.
529
+ await new Promise((resolve) => setTimeout(resolve, 50));
530
+ continue;
531
+ }
532
+ return;
533
+ }
534
+ }
535
+
502
536
  function enqueueDeckOp(
503
537
  deckId: string,
504
538
  op: GranularOp,
@@ -2095,7 +2129,12 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2095
2129
  );
2096
2130
 
2097
2131
  const addSlide = useCallback(
2098
- (deckId: string, layout: SlideLayout = "content", afterIndex?: number) => {
2132
+ (
2133
+ deckId: string,
2134
+ layout: SlideLayout = "content",
2135
+ afterIndex?: number,
2136
+ addOptions?: { persistence?: "debounced" | "immediate" },
2137
+ ) => {
2099
2138
  markDeckDirty(deckId);
2100
2139
  const newSlide: Slide = {
2101
2140
  id: nanoid(8),
@@ -2128,7 +2167,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2128
2167
  afterSlideId,
2129
2168
  fields: addSlideFields(newSlide),
2130
2169
  };
2131
- enqueueDeckOp(deckId, op);
2170
+ enqueueDeckOp(deckId, op, addOptions);
2132
2171
  if (before) recordUndo(before, op, { label: "Add slide" });
2133
2172
 
2134
2173
  return newSlide.id;
@@ -2329,6 +2368,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2329
2368
  refreshOpenDeck: refetchOpenDeckIfChanged,
2330
2369
  getDeck,
2331
2370
  addSlide,
2371
+ flushDeckSave,
2332
2372
  updateSlide,
2333
2373
  deleteSlide,
2334
2374
  duplicateSlide,
@@ -290,53 +290,20 @@ button[title="Open agent sidebar"] {
290
290
 
291
291
  .deck-editor-context-toolbar-host--narrow {
292
292
  display: flex;
293
- justify-content: flex-end;
293
+ justify-content: flex-start;
294
294
  }
295
295
 
296
296
  .deck-editor-context-toolbar-host--narrow
297
297
  > [data-slide-inline-edit-surface="true"] {
298
298
  min-width: 0;
299
- width: fit-content;
300
- max-width: 100%;
301
- flex: 0 1 auto;
302
- }
303
-
304
- @container deck-editor (max-width: 1149px) {
305
- .deck-editor-context-toolbar-host--wide {
306
- display: none;
307
- }
308
-
309
- .deck-editor-context-toolbar-host--narrow {
310
- display: flex;
311
- }
299
+ width: 100%;
300
+ flex: 1 1 auto;
312
301
  }
313
302
 
314
- @container deck-editor (min-width: 1150px) {
315
- .deck-editor-context-toolbar-host--narrow {
316
- display: none;
317
- }
318
-
319
- .deck-editor-context-toolbar-host--wide[data-context-toolbar-visible="true"] {
320
- display: flex;
321
- flex: 1 1 0%;
322
- align-items: center;
323
- justify-content: flex-end;
324
- }
325
-
326
- .deck-editor-context-toolbar-host--wide
327
- > [data-slide-inline-edit-surface="true"] {
328
- min-width: 0;
329
- max-width: 100%;
330
- }
331
-
332
- .deck-editor-context-toolbar-host--wide .slide-context-toolbar--top-row {
333
- height: 2.75rem;
334
- justify-content: flex-end;
335
- border: 0;
336
- background: transparent;
337
- padding-inline: 0;
338
- }
339
- }
303
+ /* The element-controls toolbar (New Slide, text/style controls, zoom)
304
+ * always renders as its own row below the title/Share/Present bar, at every
305
+ * viewport size — the "wide" host above stays permanently hidden by the
306
+ * base rule so the row never merges into the title bar on large screens. */
340
307
 
341
308
  @container (max-width: 36rem) {
342
309
  .deck-grid {
@@ -514,11 +514,15 @@ const messages = {
514
514
  generatingSlide: "Generating slide",
515
515
  uploadFailed: "Upload failed",
516
516
  uploadAttachedFileFailed: "Could not upload the attached file.",
517
+ newSlideSaveFailed: "Couldn't save the new slide. Please try again.",
517
518
  addSlides: "Add slides",
518
519
  addEmptySlide: "Add empty slide",
519
520
  noAi: "no AI",
520
521
  duplicateCurrentSlide: "Duplicate current slide",
521
522
  promptPlaceholder: "Describe the slides you want...",
523
+ newSlide: "New slide",
524
+ closeAddSlides: "Close",
525
+ describeThisSlide: "Describe this slide",
522
526
  },
523
527
  presentation: {
524
528
  loadFailed: "Could not load this presentation.",
@@ -182,6 +182,7 @@ export default function DeckEditor() {
182
182
  duplicateSlide,
183
183
  duplicateDeck,
184
184
  addSlide,
185
+ flushDeckSave,
185
186
  reorderSlides,
186
187
  markDeckDirty,
187
188
  undo,
@@ -218,6 +219,25 @@ export default function DeckEditor() {
218
219
  pendingDeckNavigationBlocker.proceed();
219
220
  }, [pendingDeckNavigationBlocker]);
220
221
  const { generating } = useAgentGenerating();
222
+ // Dedicated instance (not the `generating` one above, which reflects ANY
223
+ // agent chat activity) so an unrelated concurrent run can't be mistaken
224
+ // for this one finishing and clear the flag early. Owning the submit call
225
+ // here — instead of in EditorSidebar, which unmounts when the rail closes
226
+ // on narrow viewports — keeps both the run-scoping and the completion
227
+ // tracking correct across a remount.
228
+ const { generating: addSlideAgentGenerating, submit: addSlideAgentSubmit } =
229
+ useAgentGenerating();
230
+ const sawAddSlideAgentGeneratingRef = useRef(false);
231
+ useEffect(() => {
232
+ if (addSlideAgentGenerating) {
233
+ sawAddSlideAgentGeneratingRef.current = true;
234
+ return;
235
+ }
236
+ if (addSlideGenerating && sawAddSlideAgentGeneratingRef.current) {
237
+ sawAddSlideAgentGeneratingRef.current = false;
238
+ setAddSlideGenerating(false);
239
+ }
240
+ }, [addSlideGenerating, addSlideAgentGenerating]);
221
241
  // Generation intent can arrive after this route mounts because the user
222
242
  // answers pre-generation questions from the empty editor.
223
243
  const wasNewDeckCreation = useRef(searchParams.get("generating") === "1");
@@ -231,6 +251,14 @@ export default function DeckEditor() {
231
251
  const [sidebarOpen, setSidebarOpen] = useState(
232
252
  () => typeof window !== "undefined" && window.innerWidth >= 768,
233
253
  );
254
+ // The slide just inserted via the toolbar's New Slide button, so the rail
255
+ // can anchor the "describe this slide" popover to its thumbnail once it
256
+ // mounts — even though the button that sets this now lives in the
257
+ // toolbar, outside the rail.
258
+ const [describeSlideId, setDescribeSlideId] = useState<string | null>(null);
259
+ useEffect(() => {
260
+ setDescribeSlideId(null);
261
+ }, [id]);
234
262
  const [contextToolbarSlot, setContextToolbarSlot] =
235
263
  useState<HTMLDivElement | null>(null);
236
264
  const [wideContextToolbarSlot, setWideContextToolbarSlot] =
@@ -807,49 +835,92 @@ export default function DeckEditor() {
807
835
  return () => document.removeEventListener("keydown", handleKeyDown);
808
836
  }, [deck, id, activeSlideId, deleteSlideWithUndo, pinMode, drawMode]);
809
837
 
810
- // Cmd/Ctrl+C then Cmd/Ctrl+V duplicates a slide thumbnail in the film
811
- // strip. Scoped to an actually-focused thumbnail (rather than "no other
812
- // handler claimed it", like the Delete-key handler below) so the shortcut
813
- // can't fire from an unrelated toolbar button, dialog, or menu.
838
+ // Command/Ctrl+C then Command/Ctrl+V on the slide rail duplicates the
839
+ // selected slide directly below itself. Only claims the shortcut when no
840
+ // slide element is selected — SlideEditor owns Cmd+C/V for object copy/paste
841
+ // in that case.
814
842
  const copiedSlideIdRef = useRef<string | null>(null);
815
843
  useEffect(() => {
816
844
  const handleKeyDown = (e: KeyboardEvent) => {
817
- if (!deck || !id) return;
845
+ if (!deck || !id || !canEdit) return;
818
846
  if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
819
847
  const key = e.key.toLowerCase();
820
848
  if (key !== "c" && key !== "v") return;
821
849
  if (pinMode || drawMode) return;
850
+
851
+ // A live browser text selection (e.g. the user triple-clicked rendered,
852
+ // non-editable slide copy) means Cmd/Ctrl+C is a normal text copy —
853
+ // let it through instead of hijacking it into a slide duplicate.
854
+ if (key === "c" && (window.getSelection()?.toString().length ?? 0) > 0) {
855
+ return;
856
+ }
857
+
858
+ // Radix Popper positions Popover/DropdownMenu/Select/Tooltip content
859
+ // inside the same [data-radix-popper-content-wrapper]. A tooltip opens
860
+ // on plain hover, so treating every such wrapper as blocking would
861
+ // disable this shortcut just by mousing over a toolbar button; only
862
+ // wrappers that aren't tooltips (marked with data-agent-native-tooltip)
863
+ // should count as an open menu/popover/dialog owning the keystroke.
864
+ const isBlockingPopperWrapper = (el: Element) =>
865
+ el.matches("[data-radix-popper-content-wrapper]") &&
866
+ !el.querySelector("[data-agent-native-tooltip]");
867
+ const isInsideSafeZone = (el: Element | null) => {
868
+ if (!el) return false;
869
+ if (el instanceof HTMLInputElement) return true;
870
+ if (el instanceof HTMLTextAreaElement) return true;
871
+ if (el instanceof HTMLElement) {
872
+ if (el.isContentEditable) return true;
873
+ if (el.closest("[contenteditable='true']")) return true;
874
+ if (el.closest("input, textarea, [role='textbox']")) return true;
875
+ if (el.closest("[data-pin-popover]")) return true;
876
+ if (el.closest("[data-add-slide-popover]")) return true;
877
+ if (el.closest(".agent-panel-root")) return true;
878
+ if (el.closest("[role='dialog'], [role='alertdialog']")) return true;
879
+ const popperWrapper = el.closest(
880
+ "[data-radix-popper-content-wrapper]",
881
+ );
882
+ if (popperWrapper && isBlockingPopperWrapper(popperWrapper))
883
+ return true;
884
+ }
885
+ return false;
886
+ };
887
+ if (isInsideSafeZone(e.target as Element | null)) return;
888
+ if (isInsideSafeZone(document.activeElement)) return;
889
+ if (document.querySelector("[data-pin-popover]")) return;
890
+ if (document.querySelector("[data-add-slide-popover]")) return;
891
+ // A dialog/sheet/menu/popover owning focus elsewhere in the DOM (not
892
+ // just under the event target) still shouldn't let this document-level
893
+ // shortcut duplicate the slide underneath it.
822
894
  if (
823
895
  document.querySelector(
824
- "[role='dialog'], [role='menu'], [role='listbox']",
896
+ "[role='dialog'], [role='alertdialog'], [role='menu'], [role='listbox']",
825
897
  )
826
- ) {
898
+ )
899
+ return;
900
+ if (
901
+ Array.from(
902
+ document.querySelectorAll("[data-radix-popper-content-wrapper]"),
903
+ ).some(isBlockingPopperWrapper)
904
+ )
905
+ return;
906
+ if (document.querySelector("[data-slide-element-selected='true']"))
827
907
  return;
828
- }
829
- if (!canEdit) return;
830
- const focusedThumbnail = document.activeElement?.closest(
831
- "[data-slide-thumbnail-id]",
832
- );
833
- if (!focusedThumbnail) return;
834
- const focusedSlideId = focusedThumbnail.getAttribute(
835
- "data-slide-thumbnail-id",
836
- );
837
- if (!focusedSlideId) return;
838
908
 
839
909
  if (key === "c") {
840
- copiedSlideIdRef.current = focusedSlideId;
910
+ if (!activeSlideId) return;
911
+ copiedSlideIdRef.current = activeSlideId;
841
912
  return;
842
913
  }
843
- const copiedSlideId = copiedSlideIdRef.current;
844
- if (!copiedSlideId) return;
845
- if (!deck.slides.some((s) => s.id === copiedSlideId)) return;
914
+
915
+ const copiedId = copiedSlideIdRef.current;
916
+ if (!copiedId || !deck.slides.some((s) => s.id === copiedId)) return;
846
917
  e.preventDefault();
847
- const newSlideId = duplicateSlide(id, copiedSlideId);
848
- if (newSlideId) setActiveSlideId(newSlideId);
918
+ const newId = duplicateSlide(id, copiedId);
919
+ if (newId) setActiveSlideId(newId);
849
920
  };
850
921
  document.addEventListener("keydown", handleKeyDown);
851
922
  return () => document.removeEventListener("keydown", handleKeyDown);
852
- }, [deck, id, duplicateSlide, canEdit, pinMode, drawMode]);
923
+ }, [deck, id, canEdit, activeSlideId, duplicateSlide, pinMode, drawMode]);
853
924
 
854
925
  // Resolve the active slide from URL/deck state. Imports replace slide IDs, so
855
926
  // keep this valid after deck contents change instead of only on first load.
@@ -1083,9 +1154,30 @@ export default function DeckEditor() {
1083
1154
 
1084
1155
  const handleAddEmptySlide = () => {
1085
1156
  const activeIdx = deck.slides.findIndex((s) => s.id === activeSlideId);
1086
- setActiveSlideId(
1087
- addSlide(id, "blank", activeIdx >= 0 ? activeIdx : undefined),
1157
+ // Immediate persistence: this placeholder is immediately followed by an
1158
+ // agent request to `update-slide` it, which can reach the server before
1159
+ // the default 500ms debounce would have flushed the `add-slide` op.
1160
+ const newId = addSlide(
1161
+ id,
1162
+ "blank",
1163
+ activeIdx >= 0 ? activeIdx : undefined,
1164
+ {
1165
+ persistence: "immediate",
1166
+ },
1088
1167
  );
1168
+ setActiveSlideId(newId);
1169
+ return newId;
1170
+ };
1171
+
1172
+ const handleNewSlideClick = () => {
1173
+ const newId = handleAddEmptySlide();
1174
+ if (newId) {
1175
+ // The rail owns the anchor node the describe-slide popover attaches
1176
+ // to, so it must be mounted even if it started closed on a narrow
1177
+ // viewport where the toolbar button is still reachable.
1178
+ setSidebarOpen(true);
1179
+ setDescribeSlideId(newId);
1180
+ }
1089
1181
  };
1090
1182
 
1091
1183
  return (
@@ -1101,7 +1193,6 @@ export default function DeckEditor() {
1101
1193
  canEdit={canEdit}
1102
1194
  canComment={canComment}
1103
1195
  onTitleChange={(title) => updateDeck(id, { title })}
1104
- slideCount={deck.slides.length}
1105
1196
  currentSlideIndex={currentIndex >= 0 ? currentIndex : 0}
1106
1197
  sidebarOpen={sidebarOpen}
1107
1198
  onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
@@ -1113,6 +1204,8 @@ export default function DeckEditor() {
1113
1204
  onShowHistory={() => setHistoryOpen((open) => !open)}
1114
1205
  historyButtonRef={historyButtonRef}
1115
1206
  currentSlide={currentSlide}
1207
+ onAddEmptySlide={canEdit ? handleNewSlideClick : undefined}
1208
+ addSlideGenerating={addSlideGenerating}
1116
1209
  onWideContextToolbarSlotChange={setWideContextToolbarSlot}
1117
1210
  activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
1118
1211
  agentPresent={agentPresent}
@@ -1187,13 +1280,6 @@ export default function DeckEditor() {
1187
1280
  }
1188
1281
  return exportDeckToGoogleSlides(deck.title, slides, deck.aspectRatio);
1189
1282
  }}
1190
- currentSlideId={currentSlide?.id}
1191
- addSlideGenerating={addSlideGenerating}
1192
- onAddSlideGeneratingChange={setAddSlideGenerating}
1193
- onAddEmptySlide={handleAddEmptySlide}
1194
- onDuplicateCurrentSlide={
1195
- currentSlide ? () => duplicateSlide(id, currentSlide.id) : undefined
1196
- }
1197
1283
  onChangeSlideTransition={
1198
1284
  canEdit && currentSlide
1199
1285
  ? (transition) => updateSlide(id, currentSlide.id, { transition })
@@ -1226,6 +1312,13 @@ export default function DeckEditor() {
1226
1312
  slides={deck.slides}
1227
1313
  activeSlideId={currentSlide?.id || ""}
1228
1314
  deckId={id}
1315
+ deckTitle={deck.title}
1316
+ describeSlideId={describeSlideId}
1317
+ onCloseDescribe={() => setDescribeSlideId(null)}
1318
+ onAwaitAddSlidePersisted={() => flushDeckSave(id)}
1319
+ onRemoveFailedSlide={(slideId) => deleteSlide(id, slideId)}
1320
+ addSlideAgentSubmit={addSlideAgentSubmit}
1321
+ onAddSlideGeneratingChange={setAddSlideGenerating}
1229
1322
  onSelectSlide={(slideId) => {
1230
1323
  setGeneratingSlideSelected(false);
1231
1324
  setActiveSlideId(slideId);
@@ -1304,19 +1397,11 @@ export default function DeckEditor() {
1304
1397
  contextToolbarLeading={
1305
1398
  canEdit ? (
1306
1399
  <EditorActionCluster
1307
- deckId={id}
1308
- deckTitle={deck.title}
1309
- currentSlideId={currentSlide.id}
1310
- slideCount={deck.slides.length}
1311
- currentSlideIndex={currentIndex >= 0 ? currentIndex : 0}
1312
- addSlideGenerating={addSlideGenerating}
1313
- onAddSlideGeneratingChange={setAddSlideGenerating}
1314
- onAddEmptySlide={handleAddEmptySlide}
1315
- onDuplicateCurrentSlide={() =>
1316
- duplicateSlide(id, currentSlide.id)
1317
- }
1318
1400
  textBoxMode={textBoxMode}
1319
1401
  onToggleTextBoxMode={toggleTextBoxMode}
1402
+ onAddEmptySlide={handleNewSlideClick}
1403
+ addSlideGenerating={addSlideGenerating}
1404
+ currentSlideId={currentSlide.id}
1320
1405
  slideTransition={currentSlide.transition}
1321
1406
  onChangeSlideTransition={(transition) =>
1322
1407
  updateSlide(id, currentSlide.id, { transition })
@@ -1,4 +1,4 @@
1
- export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type AgentLoopFinalResponseGuard, type AgentLoopFinalResponseGuardContext, type AgentLoopFinalResponseGuardResult, type AgentLoopToolCallSummary, type AgentLoopToolResultSummary, } from "./production-agent.js";
1
+ export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type AgentActionSurface, type AgentActionSurfaceDetails, type AgentLoopFinalResponseGuard, type AgentLoopFinalResponseGuardContext, type AgentLoopFinalResponseGuardResult, type AgentLoopToolCallSummary, type AgentLoopToolResultSummary, } from "./production-agent.js";
2
2
  export { type ActionTool, type ScriptTool, type AgentMessage, type AgentChatRequest, type AgentChatEvent, type AgentChatAttachment, type AgentChatReference, type MentionProvider, type MentionProviderItem, } from "./types.js";
3
3
  export { TripWire, type Processor, type ProcessorState, type ProcessorAbort, type ProcessOutputStreamArgs, type ProcessOutputStepArgs, type ProcessOutputResultArgs, } from "./processors.js";
4
4
  export { DEFAULT_MODEL } from "./default-model.js";
@@ -255,6 +255,29 @@ export interface ActionEntry {
255
255
  /** @deprecated Use `ActionEntry` instead */
256
256
  export type ScriptEntry = ActionEntry;
257
257
  export type AgentExecutionMode = "act" | "plan";
258
+ export interface AgentActionSurface {
259
+ allowedActionNames: readonly string[];
260
+ }
261
+ export interface AgentActionSurfaceDetails {
262
+ event: any;
263
+ ownerEmail: string | null;
264
+ orgId: string | null;
265
+ threadId?: string;
266
+ mode: AgentExecutionMode;
267
+ internalContinuation: boolean;
268
+ availableActionNames: readonly string[];
269
+ }
270
+ /** Read a persisted allowlist while preserving legacy absence and failing
271
+ * closed when the field is present but malformed. */
272
+ export declare function readPersistedAllowedActionNames(value: unknown): string[] | undefined;
273
+ export interface PersistedActionSurface {
274
+ orgId: string | null;
275
+ allowedActionNames: string[];
276
+ }
277
+ /** Read a nested persisted action surface. An absent envelope is legacy;
278
+ * a present but invalid envelope is an explicit org-less, empty surface. */
279
+ export declare function readPersistedActionSurface(value: unknown, propertyName: string): PersistedActionSurface | undefined;
280
+ export declare function filterActionsByAllowedNames(actions: Record<string, ActionEntry>, allowedActionNames: readonly string[]): Record<string, ActionEntry>;
258
281
  export declare const PLAN_MODE_SYSTEM_PROMPT = "## Plan Mode Active\n\nYou are in Plan mode. This turn is for research, clarification, and a proposed approach only.\n\nHard rules:\n- Use only read-only tools. Do not edit files, write resources, run mutating bash commands, mutate SQL rows, navigate the UI, send notifications, create jobs, create tools, send messages or tasks to external agents, or change external systems.\n- If a needed detail is unclear, ask a concise clarifying question before proposing a plan.\n- When ready, present a concrete plan with the files/tools you expect to touch, the intended changes, validation steps, and notable risks.\n- Do not treat approval as implicit while Plan mode is still active. Tell the user to switch to Act mode with the mode selector or /act before implementation.";
259
282
  export declare function isPlanModeToolCallAllowed(name: string, input: unknown, entry: ActionEntry): boolean;
260
283
  export declare function createPlanModeActionRegistry(actions: Record<string, ActionEntry>): Record<string, ActionEntry>;
@@ -314,6 +337,13 @@ export interface ProductionAgentOptions {
314
337
  displayMessage?: string;
315
338
  attachments?: AgentChatAttachment[];
316
339
  }>;
340
+ /**
341
+ * Resolve the exact action registry exposed to one interactive agent-chat
342
+ * request. Returned names are a hard allowlist: omitted actions are absent
343
+ * from both the provider schemas and the searchable registry. When set, all
344
+ * allowed actions are loaded directly on the first model request.
345
+ */
346
+ resolveActionSurface?: (details: AgentActionSurfaceDetails) => AgentActionSurface | Promise<AgentActionSurface>;
317
347
  /** Optional per-app agent run chunk budget in milliseconds. Defaults to
318
348
  * AGENT_RUN_SOFT_TIMEOUT_MS when set, otherwise no framework-imposed
319
349
  * timeout. When reached, the client receives an internal auto-continuation
@@ -16,7 +16,7 @@ import { completeRun as completeProgressRun, startRun as startProgressRun, updat
16
16
  import { isRuntimeVisibleScope, parseSkillFrontmatter, } from "../server/agent-chat/skill-frontmatter.js";
17
17
  import { canUseDeployCredentialFallbackForRequest, getProviderCredentialAuthFailure, readDeployCredentialEnv, } from "../server/credential-provider.js";
18
18
  import { readBody } from "../server/h3-helpers.js";
19
- import { getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
19
+ import { assertRequestActionSurfaceIsolation, getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
20
20
  import { fireInternalDispatch } from "../server/self-dispatch.js";
21
21
  import { isReasoningEffort, normalizeReasoningEffortForRequest, stepDownReasoningEffort, } from "../shared/reasoning-effort.js";
22
22
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
@@ -391,6 +391,55 @@ export async function resolveOwnerEngineApiKey(input) {
391
391
  export async function getOwnerAnthropicApiKey(ownerEmail) {
392
392
  return getOwnerApiKey("anthropic", ownerEmail);
393
393
  }
394
+ function hasOwn(value, propertyName) {
395
+ return (typeof value === "object" &&
396
+ value !== null &&
397
+ Object.prototype.hasOwnProperty.call(value, propertyName));
398
+ }
399
+ /** Read a persisted allowlist while preserving legacy absence and failing
400
+ * closed when the field is present but malformed. */
401
+ export function readPersistedAllowedActionNames(value) {
402
+ if (!hasOwn(value, "allowedActionNames"))
403
+ return undefined;
404
+ const names = value.allowedActionNames;
405
+ if (!Array.isArray(names) ||
406
+ !names.every((name) => typeof name === "string")) {
407
+ return [];
408
+ }
409
+ return [...new Set(names)];
410
+ }
411
+ /** Read a nested persisted action surface. An absent envelope is legacy;
412
+ * a present but invalid envelope is an explicit org-less, empty surface. */
413
+ export function readPersistedActionSurface(value, propertyName) {
414
+ if (!hasOwn(value, propertyName))
415
+ return undefined;
416
+ const surface = value[propertyName];
417
+ const allowedActionNames = readPersistedAllowedActionNames(surface) ?? [];
418
+ if (!hasOwn(surface, "orgId")) {
419
+ return { orgId: null, allowedActionNames: [] };
420
+ }
421
+ const orgId = surface.orgId;
422
+ if (orgId !== null &&
423
+ (typeof orgId !== "string" || orgId.trim().length === 0)) {
424
+ return { orgId: null, allowedActionNames: [] };
425
+ }
426
+ return { orgId, allowedActionNames };
427
+ }
428
+ export function filterActionsByAllowedNames(actions, allowedActionNames) {
429
+ const allowedNames = [...new Set(allowedActionNames)];
430
+ const unknownNames = allowedNames.filter((name) => !Object.prototype.hasOwnProperty.call(actions, name));
431
+ if (unknownNames.length > 0) {
432
+ throw new Error(`resolveActionSurface returned unknown action name(s): ${unknownNames.join(", ")}`);
433
+ }
434
+ const filtered = Object.fromEntries(allowedNames.map((name) => [name, actions[name]]));
435
+ // `attachToolSearch` closes over the registry it was originally attached to.
436
+ // Rebind it to this request's filtered registry so explicitly allowing
437
+ // `tool-search` cannot disclose actions that the resolver omitted.
438
+ if (filtered[TOOL_SEARCH_ACTION_NAME]) {
439
+ filtered[TOOL_SEARCH_ACTION_NAME] = createToolSearchEntry(() => filtered);
440
+ }
441
+ return filtered;
442
+ }
394
443
  export const PLAN_MODE_SYSTEM_PROMPT = `## Plan Mode Active
395
444
 
396
445
  You are in Plan mode. This turn is for research, clarification, and a proposed approach only.
@@ -6031,6 +6080,9 @@ export function resolveAgentRequestReasoningEffort({ model, requestEffort, confi
6031
6080
  return normalizeReasoningEffortForRequest(model, isReasoningEffort(requestEffort) ? requestEffort : configuredEffort);
6032
6081
  }
6033
6082
  export function createProductionAgentHandler(options) {
6083
+ if (options.resolveActionSurface) {
6084
+ assertRequestActionSurfaceIsolation();
6085
+ }
6034
6086
  // Undefined = let each engine pick its own defaultModel at request time.
6035
6087
  const configuredModel = options.model;
6036
6088
  // Resolve actions — prefer `actions`, fall back to deprecated `scripts`
@@ -6040,7 +6092,7 @@ export function createProductionAgentHandler(options) {
6040
6092
  // the settings UI) show up to the LLM without a process restart. MCP tools
6041
6093
  // are also scope-filtered per request — a user-scope server added by Alice
6042
6094
  // must not appear in Bob's tool list in a shared-process deployment.
6043
- const getEngineTools = (actions = resolvedActions) => {
6095
+ const getRequestActions = (actions = resolvedActions) => {
6044
6096
  const filtered = {};
6045
6097
  for (const [name, entry] of Object.entries(actions)) {
6046
6098
  if (name.startsWith("mcp__") && !isMcpToolAllowedForRequest(name)) {
@@ -6048,8 +6100,9 @@ export function createProductionAgentHandler(options) {
6048
6100
  }
6049
6101
  filtered[name] = entry;
6050
6102
  }
6051
- return actionsToEngineTools(filtered);
6103
+ return filtered;
6052
6104
  };
6105
+ const getEngineTools = (actions) => actionsToEngineTools(getRequestActions(actions));
6053
6106
  return defineEventHandler(async (event) => {
6054
6107
  // Diagnostic-only setup-timing instrumentation. Captures wall-clock offsets
6055
6108
  // from handler entry through the work done BEFORE startRun so a slow pre-run
@@ -6089,12 +6142,20 @@ export function createProductionAgentHandler(options) {
6089
6142
  // (the route HMAC-verifies the dispatch before invoking us). When set, we
6090
6143
  // run the loop inline with the background soft-timeout, reusing the
6091
6144
  // pre-claimed runId/turnId — we must NOT re-claim the slot or re-dispatch.
6092
- const backgroundRunMarker = body[AGENT_CHAT_BACKGROUND_RUN_FIELD] &&
6145
+ const backgroundRunMarker = preInjectedBody &&
6146
+ body[AGENT_CHAT_BACKGROUND_RUN_FIELD] &&
6093
6147
  typeof body[AGENT_CHAT_BACKGROUND_RUN_FIELD] === "object" &&
6094
6148
  typeof body[AGENT_CHAT_BACKGROUND_RUN_FIELD].runId === "string"
6095
6149
  ? body[AGENT_CHAT_BACKGROUND_RUN_FIELD]
6096
6150
  : null;
6097
6151
  const isBackgroundWorker = backgroundRunMarker !== null;
6152
+ // Both fields are internal durable-dispatch artifacts, never client input.
6153
+ // A body marker alone is not proof of worker identity: only the verified
6154
+ // `_process-run` route can install `preInjectedBody` on the event context.
6155
+ if (!isBackgroundWorker) {
6156
+ delete body[AGENT_CHAT_BACKGROUND_RUN_FIELD];
6157
+ delete body.__resolvedActionSurface;
6158
+ }
6098
6159
  // DIAGNOSTIC-ONLY: progressive per-stage hang localizer for the bg worker.
6099
6160
  // The worker's runId is available EARLY on the marker (the general `runId`
6100
6161
  // var resolves much later), so capture it now and emit the LAST setup stage
@@ -6197,6 +6258,35 @@ export function createProductionAgentHandler(options) {
6197
6258
  requestAttachments = preparedRequest.attachments;
6198
6259
  }
6199
6260
  }
6261
+ const availableRequestActions = getRequestActions();
6262
+ let surfacedRequestActions = availableRequestActions;
6263
+ if (options.resolveActionSurface) {
6264
+ const persistedSurface = isBackgroundWorker
6265
+ ? readPersistedActionSurface(body, "__resolvedActionSurface")
6266
+ : undefined;
6267
+ const surface = persistedSurface !== undefined
6268
+ ? persistedSurface
6269
+ : await options.resolveActionSurface({
6270
+ event,
6271
+ ownerEmail,
6272
+ orgId: getRequestOrgId() ?? null,
6273
+ threadId,
6274
+ mode: requestMode,
6275
+ internalContinuation: Boolean(internalContinuation),
6276
+ availableActionNames: Object.keys(availableRequestActions),
6277
+ });
6278
+ surfacedRequestActions = filterActionsByAllowedNames(availableRequestActions, surface.allowedActionNames);
6279
+ const allowedNames = Object.keys(surfacedRequestActions);
6280
+ const runCtx = ensureRequestRunContext();
6281
+ if (runCtx)
6282
+ runCtx.allowedActionNames = allowedNames;
6283
+ if (!isBackgroundWorker) {
6284
+ body.__resolvedActionSurface = {
6285
+ orgId: getRequestOrgId() ?? null,
6286
+ allowedActionNames: allowedNames,
6287
+ };
6288
+ }
6289
+ }
6200
6290
  // DIAGNOSTIC-ONLY: owner/request context prep (resolveAgentOwnerEmail +
6201
6291
  // prepareRequest) finished. A worker stuck before this points at the
6202
6292
  // owner/request-context awaits.
@@ -6428,7 +6518,7 @@ export function createProductionAgentHandler(options) {
6428
6518
  const screenContextThunk = () => (async () => {
6429
6519
  const screenStart = Date.now();
6430
6520
  try {
6431
- const viewScreenAction = resolvedActions["view-screen"];
6521
+ const viewScreenAction = surfacedRequestActions["view-screen"];
6432
6522
  if (viewScreenAction) {
6433
6523
  const result = await viewScreenAction.run({}, {
6434
6524
  userEmail: getRequestUserEmail(),
@@ -6667,10 +6757,12 @@ export function createProductionAgentHandler(options) {
6667
6757
  }
6668
6758
  const screenContext = timeBlock + screenBlock + urlBlock + selectionBlock;
6669
6759
  const requestActions = requestMode === "plan"
6670
- ? createPlanModeActionRegistry(resolvedActions)
6671
- : resolvedActions;
6760
+ ? createPlanModeActionRegistry(surfacedRequestActions)
6761
+ : surfacedRequestActions;
6672
6762
  const availableRequestTools = getEngineTools(requestActions);
6673
- const initialRequestTools = filterInitialEngineTools(availableRequestTools, options.initialToolNames);
6763
+ const initialRequestTools = options.resolveActionSurface
6764
+ ? availableRequestTools
6765
+ : filterInitialEngineTools(availableRequestTools, options.initialToolNames);
6674
6766
  const requestTools = requestMode === "plan"
6675
6767
  ? preloadPlanModeEngineTools({
6676
6768
  request: requestMessage,
@@ -186,6 +186,15 @@ export interface AgentChatRequest {
186
186
  */
187
187
  payloadRef?: boolean;
188
188
  };
189
+ /**
190
+ * Server-resolved action authorization carried across authenticated durable
191
+ * background dispatches. Normal client requests must not trust this field;
192
+ * the foreground handler deletes and replaces it before persistence.
193
+ */
194
+ __resolvedActionSurface?: {
195
+ orgId: string | null;
196
+ allowedActionNames: string[];
197
+ };
189
198
  /**
190
199
  * Stable identity for the logical assistant turn this request belongs to.
191
200
  * The client sends the SAME turnId for the initial POST and every