@agent-native/core 0.155.0 → 0.157.1

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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  5. package/corpus/templates/assets/app/global.css +4 -0
  6. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  7. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +18 -15
  8. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  9. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  10. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  11. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  12. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  13. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  14. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  15. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  17. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  18. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  19. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  20. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +177 -134
  22. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  23. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  24. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  25. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +24 -17
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +151 -20
  28. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  30. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  31. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  32. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  33. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  34. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  35. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  36. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  37. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  38. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  39. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  40. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  41. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  44. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  45. package/corpus/templates/clips/server/db/index.ts +1 -0
  46. package/corpus/templates/clips/server/db/schema.ts +1 -0
  47. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  48. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  49. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  50. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  51. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  52. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  53. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  54. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  55. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  56. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  59. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  61. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  62. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  63. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  64. package/corpus/templates/factory/AGENTS.md +5 -1
  65. package/corpus/templates/factory/README.md +6 -2
  66. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  67. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  68. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  69. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  70. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  71. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  72. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  73. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  74. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  75. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  76. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  77. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  78. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  79. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  80. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  81. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  82. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  83. package/corpus/templates/slides/app/context/DeckContext.tsx +108 -19
  84. package/corpus/templates/slides/app/global.css +7 -40
  85. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  86. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  87. package/corpus/templates/slides/app/pages/DeckEditor.tsx +143 -45
  88. package/dist/agent/index.d.ts +1 -1
  89. package/dist/agent/production-agent.d.ts +30 -0
  90. package/dist/agent/production-agent.js +100 -8
  91. package/dist/agent/types.d.ts +9 -0
  92. package/dist/client/AgentPanel.d.ts +13 -1
  93. package/dist/client/AgentPanel.js +22 -17
  94. package/dist/client/AssistantChat.d.ts +15 -1
  95. package/dist/client/AssistantChat.js +135 -48
  96. package/dist/client/HighlightedCodeBlock.js +12 -2
  97. package/dist/client/MultiTabAssistantChat.js +44 -27
  98. package/dist/client/agent-chat/index.d.ts +1 -0
  99. package/dist/client/agent-chat/index.js +1 -0
  100. package/dist/client/chat/markdown-renderer.js +20 -5
  101. package/dist/client/chat/message-components.d.ts +2 -2
  102. package/dist/client/chat/message-components.js +69 -35
  103. package/dist/client/chat/run-recovery.d.ts +4 -1
  104. package/dist/client/chat/run-recovery.js +62 -30
  105. package/dist/client/chat/tool-call-display.d.ts +12 -3
  106. package/dist/client/chat/tool-call-display.js +79 -40
  107. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  108. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  109. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  110. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  111. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  112. package/dist/client/chat-first/apps-rail.js +1 -1
  113. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  114. package/dist/client/components/ui/message-scroller.js +7 -3
  115. package/dist/client/composer/runtime-adapters.js +3 -2
  116. package/dist/client/context-xray/ContextMeter.js +5 -1
  117. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  118. package/dist/client/error-format.d.ts +4 -0
  119. package/dist/client/error-format.js +93 -5
  120. package/dist/client/i18n.js +83 -17
  121. package/dist/client/observability/ThumbsFeedback.js +10 -8
  122. package/dist/client/settings/AccountSettingsCard.js +48 -19
  123. package/dist/client/sharing/ShareButton.d.ts +1 -2
  124. package/dist/client/sharing/ShareButton.js +151 -81
  125. package/dist/client/sharing/ShareDialog.js +5 -4
  126. package/dist/client/use-chat-threads.js +10 -14
  127. package/dist/coding-tools/run-code.js +2 -1
  128. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  129. package/dist/coding-tools/sandbox/background.js +8 -1
  130. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  131. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  132. package/dist/collab/routes.d.ts +1 -1
  133. package/dist/collab/struct-routes.d.ts +1 -1
  134. package/dist/index.d.ts +1 -1
  135. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  136. package/dist/localization/core-messages/ar-SA.js +541 -0
  137. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  138. package/dist/localization/core-messages/de-DE.js +513 -0
  139. package/dist/localization/core-messages/en-US.d.ts +513 -0
  140. package/dist/localization/core-messages/en-US.js +513 -0
  141. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  142. package/dist/localization/core-messages/es-ES.js +520 -0
  143. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  144. package/dist/localization/core-messages/fr-FR.js +520 -0
  145. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  146. package/dist/localization/core-messages/hi-IN.js +513 -0
  147. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  148. package/dist/localization/core-messages/ja-JP.js +506 -0
  149. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  150. package/dist/localization/core-messages/ko-KR.js +506 -0
  151. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  152. package/dist/localization/core-messages/pt-BR.js +520 -0
  153. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  154. package/dist/localization/core-messages/zh-CN.js +506 -0
  155. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  156. package/dist/localization/core-messages/zh-TW.js +506 -0
  157. package/dist/localization/core-messages.d.ts +524 -0
  158. package/dist/localization/core-messages.js +124 -0
  159. package/dist/localization/default-messages.d.ts +3 -0
  160. package/dist/localization/default-messages.js +3 -0
  161. package/dist/notifications/routes.d.ts +1 -1
  162. package/dist/observability/routes.d.ts +5 -5
  163. package/dist/progress/routes.d.ts +1 -1
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  165. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/secrets/routes.d.ts +9 -9
  168. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  169. package/dist/server/agent-chat-plugin.d.ts +10 -0
  170. package/dist/server/agent-chat-plugin.js +144 -41
  171. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  172. package/dist/server/agent-run-context.d.ts +7 -1
  173. package/dist/server/agent-run-context.js +9 -2
  174. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  175. package/dist/server/agent-teams.js +38 -9
  176. package/dist/server/auth-marketing.d.ts +1 -0
  177. package/dist/server/auth-marketing.js +0 -2
  178. package/dist/server/auth.d.ts +1 -0
  179. package/dist/server/auth.js +18 -0
  180. package/dist/server/index.d.ts +1 -1
  181. package/dist/server/onboarding-html.d.ts +1 -0
  182. package/dist/server/onboarding-html.js +2 -88
  183. package/dist/server/realtime-token.d.ts +1 -1
  184. package/dist/server/request-context.d.ts +7 -0
  185. package/dist/server/request-context.js +15 -0
  186. package/dist/shared/markdown-block-split.d.ts +16 -0
  187. package/dist/shared/markdown-block-split.js +81 -5
  188. package/dist/styles/agent-native.css +13 -0
  189. package/dist/voice/voice-cleanup-prompt.js +1 -0
  190. package/docs/content/agent-surfaces.mdx +30 -0
  191. package/package.json +2 -2
  192. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -926,7 +926,7 @@ export function SlideContextToolbar({
926
926
  )}
927
927
  {zoomControls && (
928
928
  <>
929
- <div className={TOOLBAR_DIVIDER} />
929
+ <div className={cn(TOOLBAR_DIVIDER, "ml-auto")} />
930
930
  <Tooltip>
931
931
  <TooltipTrigger asChild>
932
932
  <Button
@@ -217,13 +217,18 @@ interface DeckContextType {
217
217
  ) => void;
218
218
  reloadDecks: () => Promise<void>;
219
219
  reloadDecksWithStatus: () => Promise<DeckReloadStatus>;
220
- refreshOpenDeck: (deckId: string) => Promise<Deck | null>;
220
+ refreshOpenDeck: (
221
+ deckId: string,
222
+ options?: { clearPendingWrites?: boolean },
223
+ ) => Promise<Deck | null>;
221
224
  getDeck: (id: string) => Deck | undefined;
222
225
  addSlide: (
223
226
  deckId: string,
224
227
  layout?: SlideLayout,
225
228
  afterIndex?: number,
229
+ options?: { persistence?: "debounced" | "immediate" },
226
230
  ) => string;
231
+ flushDeckSave: (deckId: string) => Promise<void>;
227
232
  updateSlide: (
228
233
  deckId: string,
229
234
  slideId: string,
@@ -310,6 +315,8 @@ function normalizeActionDeck(value: unknown): Deck | null {
310
315
  const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
311
316
  const inFlightSaves = new Set<string>();
312
317
  const inFlightSaveChains = new Map<string, Promise<void>>();
318
+ const inFlightSaveControllers = new Map<string, AbortController>();
319
+ const deckSaveGenerations = new Map<string, number>();
313
320
  const immediateFlushRequests = new Set<string>();
314
321
  const deckSaveRetryAttempts = new Map<string, number>();
315
322
  const failedSaveDecks = new Set<string>();
@@ -401,7 +408,11 @@ function deckPayload(deck: Deck): Record<string, unknown> {
401
408
  * the `save-deck` action is called first, then any trailing granular ops are
402
409
  * sent through `patch-deck`.
403
410
  */
404
- async function persistDeckOps(deckId: string, ops: GranularOp[]) {
411
+ async function persistDeckOps(
412
+ deckId: string,
413
+ ops: GranularOp[],
414
+ signal?: AbortSignal,
415
+ ) {
405
416
  if (ops[0].op === "full-replace") {
406
417
  // Legacy full-deck write — used by undo/redo and setDeckSlides.
407
418
  // `callAction` bounds it so a stalled save can't wedge `inFlightSaves`
@@ -411,20 +422,28 @@ async function persistDeckOps(deckId: string, ops: GranularOp[]) {
411
422
  await callAction(
412
423
  "save-deck",
413
424
  { deckId, deck: deckPayload(deck) },
414
- { method: "PUT" },
425
+ { method: "PUT", signal },
415
426
  );
416
427
  const trailingOps = ops.slice(1) as PatchDeckOp[];
417
428
  if (trailingOps.length > 0) {
418
- await callAction("patch-deck", {
419
- deckId,
420
- operations: trailingOps,
421
- });
429
+ await callAction(
430
+ "patch-deck",
431
+ {
432
+ deckId,
433
+ operations: trailingOps,
434
+ },
435
+ { signal },
436
+ );
422
437
  }
423
438
  } else {
424
- await callAction("patch-deck", {
425
- deckId,
426
- operations: ops as PatchDeckOp[],
427
- });
439
+ await callAction(
440
+ "patch-deck",
441
+ {
442
+ deckId,
443
+ operations: ops as PatchDeckOp[],
444
+ },
445
+ { signal },
446
+ );
428
447
  }
429
448
  }
430
449
 
@@ -453,15 +472,23 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
453
472
  return Promise.resolve();
454
473
  }
455
474
 
475
+ const generation = deckSaveGenerations.get(deckId) ?? 0;
476
+ const controller =
477
+ typeof AbortController === "undefined" ? null : new AbortController();
478
+ if (controller) inFlightSaveControllers.set(deckId, controller);
456
479
  inFlightSaves.add(deckId);
457
- let succeeded = false;
458
- const next = persistDeckOps(deckId, ops)
480
+ const isCurrentGeneration = () =>
481
+ (deckSaveGenerations.get(deckId) ?? 0) === generation;
482
+ const next = persistDeckOps(deckId, ops, controller?.signal)
459
483
  .then(() => {
460
- succeeded = true;
484
+ if (!isCurrentGeneration()) return;
461
485
  deckSaveRetryAttempts.delete(deckId);
462
486
  failedSaveDecks.delete(deckId);
463
487
  })
464
488
  .catch((err) => {
489
+ // A restore or delete invalidated this request. Its result must not
490
+ // resurrect the old queue or schedule a retry after the boundary.
491
+ if (!isCurrentGeneration()) return;
465
492
  console.error(`Failed to save deck ${deckId}:`, err);
466
493
  const pending = pendingOpsQueue.get(deckId) ?? [];
467
494
  pendingOpsQueue.set(deckId, [...ops, ...pending]);
@@ -486,10 +513,13 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
486
513
  .finally(() => {
487
514
  if (inFlightSaveChains.get(deckId) === next) {
488
515
  inFlightSaveChains.delete(deckId);
516
+ if (controller && inFlightSaveControllers.get(deckId) === controller) {
517
+ inFlightSaveControllers.delete(deckId);
518
+ }
489
519
  inFlightSaves.delete(deckId);
490
520
  const flushImmediately = immediateFlushRequests.delete(deckId);
491
521
  notifySaveListeners();
492
- if (succeeded && flushImmediately) {
522
+ if (flushImmediately) {
493
523
  void drainPendingDeckOps(deckId);
494
524
  }
495
525
  }
@@ -499,6 +529,39 @@ function drainPendingDeckOps(deckId: string): Promise<void> {
499
529
  return next;
500
530
  }
501
531
 
532
+ /**
533
+ * Wait for a deck's in-flight save(s) to fully settle, including any
534
+ * follow-up drain chained by immediateFlushRequests for ops queued while a
535
+ * save was already running, and any requeued retry after a failed attempt.
536
+ * Used when a caller must not proceed (e.g. firing an agent request against
537
+ * a slide or restoring a saved version) until every write issued before that
538
+ * boundary has reached the server. Throws if the save ultimately fails after
539
+ * retries exhaust, since `drainPendingDeckOps` swallows save errors internally
540
+ * to drive its own retry loop and its promise always resolves regardless of
541
+ * outcome.
542
+ */
543
+ async function flushDeckSave(deckId: string): Promise<void> {
544
+ while (true) {
545
+ const active = inFlightSaveChains.get(deckId);
546
+ if (active) {
547
+ await active;
548
+ continue;
549
+ }
550
+ if (failedSaveDecks.has(deckId)) {
551
+ throw new Error(
552
+ `Failed to save deck ${deckId} after ${MAX_DECK_SAVE_RETRIES} attempts`,
553
+ );
554
+ }
555
+ if (pendingOpsQueue.has(deckId) || pendingSaves.has(deckId)) {
556
+ // A failed op was requeued for retry, or a debounced save is armed;
557
+ // wait for it to actually run rather than declaring success early.
558
+ await new Promise((resolve) => setTimeout(resolve, 50));
559
+ continue;
560
+ }
561
+ return;
562
+ }
563
+ }
564
+
502
565
  function enqueueDeckOp(
503
566
  deckId: string,
504
567
  op: GranularOp,
@@ -613,6 +676,8 @@ function flushPendingSaves() {
613
676
  }
614
677
 
615
678
  function discardPendingDeckOps(deckId: string) {
679
+ deckSaveGenerations.set(deckId, (deckSaveGenerations.get(deckId) ?? 0) + 1);
680
+ inFlightSaveControllers.get(deckId)?.abort();
616
681
  const timer = pendingSaves.get(deckId);
617
682
  if (timer) clearTimeout(timer);
618
683
  pendingSaves.delete(deckId);
@@ -1320,7 +1385,15 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1320
1385
  * this, chat-driven edits land in the editor with Undo disabled.
1321
1386
  */
1322
1387
  const applyRemoteDeckUpdate = useCallback(
1323
- (updated: Deck, label = "Agent edit") => {
1388
+ (
1389
+ updated: Deck,
1390
+ label = "Agent edit",
1391
+ options?: { clearPendingWrites?: boolean },
1392
+ ) => {
1393
+ if (options?.clearPendingWrites) {
1394
+ discardPendingDeckOps(updated.id);
1395
+ dirtyDeckIdsRef.current.delete(updated.id);
1396
+ }
1324
1397
  const before = decksRef.current.find((d) => d.id === updated.id);
1325
1398
  if (
1326
1399
  before &&
@@ -1413,7 +1486,10 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1413
1486
  // - Dirty deck / unsaved local create → additive merge only: surface
1414
1487
  // agent-added slides without ever overwriting or dropping local slides.
1415
1488
  const refetchOpenDeckIfChanged = useCallback(
1416
- async (currentOpenId: string): Promise<Deck | null> => {
1489
+ async (
1490
+ currentOpenId: string,
1491
+ options?: { clearPendingWrites?: boolean },
1492
+ ): Promise<Deck | null> => {
1417
1493
  const requestId =
1418
1494
  (openDeckRequestIdByDeckRef.current.get(currentOpenId) ?? 0) + 1;
1419
1495
  openDeckRequestIdByDeckRef.current.set(currentOpenId, requestId);
@@ -1425,6 +1501,13 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1425
1501
  // still-pending create — nothing authoritative to reconcile.
1426
1502
  if (!serverDeck) return null;
1427
1503
  const clientDeck = decksRef.current.find((d) => d.id === currentOpenId);
1504
+ if (options?.clearPendingWrites) {
1505
+ lastExternalUpdateRef.current = Date.now();
1506
+ applyRemoteDeckUpdate(serverDeck, "Deck restored", {
1507
+ clearPendingWrites: true,
1508
+ });
1509
+ return serverDeck;
1510
+ }
1428
1511
 
1429
1512
  const hasLocalEdits =
1430
1513
  pendingCreateIdsRef.current.has(currentOpenId) ||
@@ -2095,7 +2178,12 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2095
2178
  );
2096
2179
 
2097
2180
  const addSlide = useCallback(
2098
- (deckId: string, layout: SlideLayout = "content", afterIndex?: number) => {
2181
+ (
2182
+ deckId: string,
2183
+ layout: SlideLayout = "content",
2184
+ afterIndex?: number,
2185
+ addOptions?: { persistence?: "debounced" | "immediate" },
2186
+ ) => {
2099
2187
  markDeckDirty(deckId);
2100
2188
  const newSlide: Slide = {
2101
2189
  id: nanoid(8),
@@ -2128,7 +2216,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2128
2216
  afterSlideId,
2129
2217
  fields: addSlideFields(newSlide),
2130
2218
  };
2131
- enqueueDeckOp(deckId, op);
2219
+ enqueueDeckOp(deckId, op, addOptions);
2132
2220
  if (before) recordUndo(before, op, { label: "Add slide" });
2133
2221
 
2134
2222
  return newSlide.id;
@@ -2329,6 +2417,7 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2329
2417
  refreshOpenDeck: refetchOpenDeckIfChanged,
2330
2418
  getDeck,
2331
2419
  addSlide,
2420
+ flushDeckSave,
2332
2421
  updateSlide,
2333
2422
  deleteSlide,
2334
2423
  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.",
@@ -0,0 +1,46 @@
1
+ interface EditorShortcutEvent {
2
+ key: string;
3
+ altKey: boolean;
4
+ ctrlKey: boolean;
5
+ metaKey: boolean;
6
+ shiftKey: boolean;
7
+ repeat: boolean;
8
+ isComposing: boolean;
9
+ target: EventTarget | null;
10
+ }
11
+
12
+ function isEditableSurface(target: EventTarget | null): boolean {
13
+ return (
14
+ target instanceof HTMLElement &&
15
+ (target.closest(
16
+ "input, textarea, select, [contenteditable='true'], [role='textbox']",
17
+ ) !== null ||
18
+ target.isContentEditable)
19
+ );
20
+ }
21
+
22
+ export function shouldStopSlidesItalicShortcut(event: EditorShortcutEvent) {
23
+ if (
24
+ event.repeat ||
25
+ event.isComposing ||
26
+ event.key.toLowerCase() !== "i" ||
27
+ event.altKey ||
28
+ event.shiftKey ||
29
+ !(event.ctrlKey || event.metaKey)
30
+ ) {
31
+ return false;
32
+ }
33
+
34
+ return true;
35
+ }
36
+
37
+ export function isSlidesItalicEditableTarget(event: EditorShortcutEvent) {
38
+ return isEditableSurface(event.target);
39
+ }
40
+
41
+ export function shouldSuppressSlidesItalicShortcut(event: EditorShortcutEvent) {
42
+ return (
43
+ shouldStopSlidesItalicShortcut(event) &&
44
+ !isSlidesItalicEditableTarget(event)
45
+ );
46
+ }
@@ -76,6 +76,7 @@ import {
76
76
  shouldShowDeckEditorSkeleton,
77
77
  } from "@/lib/deck-editor-loading";
78
78
  import { getPreset } from "@/lib/design-systems";
79
+ import { shouldSuppressSlidesItalicShortcut } from "@/lib/editor-shortcuts";
79
80
  import { exportDeckToGoogleSlides } from "@/lib/export-google-slides-client";
80
81
  import { exportDeckAsPdf } from "@/lib/export-pdf-client";
81
82
  import { exportDeckAsPptx } from "@/lib/export-pptx-client";
@@ -182,6 +183,7 @@ export default function DeckEditor() {
182
183
  duplicateSlide,
183
184
  duplicateDeck,
184
185
  addSlide,
186
+ flushDeckSave,
185
187
  reorderSlides,
186
188
  markDeckDirty,
187
189
  undo,
@@ -218,6 +220,25 @@ export default function DeckEditor() {
218
220
  pendingDeckNavigationBlocker.proceed();
219
221
  }, [pendingDeckNavigationBlocker]);
220
222
  const { generating } = useAgentGenerating();
223
+ // Dedicated instance (not the `generating` one above, which reflects ANY
224
+ // agent chat activity) so an unrelated concurrent run can't be mistaken
225
+ // for this one finishing and clear the flag early. Owning the submit call
226
+ // here — instead of in EditorSidebar, which unmounts when the rail closes
227
+ // on narrow viewports — keeps both the run-scoping and the completion
228
+ // tracking correct across a remount.
229
+ const { generating: addSlideAgentGenerating, submit: addSlideAgentSubmit } =
230
+ useAgentGenerating();
231
+ const sawAddSlideAgentGeneratingRef = useRef(false);
232
+ useEffect(() => {
233
+ if (addSlideAgentGenerating) {
234
+ sawAddSlideAgentGeneratingRef.current = true;
235
+ return;
236
+ }
237
+ if (addSlideGenerating && sawAddSlideAgentGeneratingRef.current) {
238
+ sawAddSlideAgentGeneratingRef.current = false;
239
+ setAddSlideGenerating(false);
240
+ }
241
+ }, [addSlideGenerating, addSlideAgentGenerating]);
221
242
  // Generation intent can arrive after this route mounts because the user
222
243
  // answers pre-generation questions from the empty editor.
223
244
  const wasNewDeckCreation = useRef(searchParams.get("generating") === "1");
@@ -231,6 +252,14 @@ export default function DeckEditor() {
231
252
  const [sidebarOpen, setSidebarOpen] = useState(
232
253
  () => typeof window !== "undefined" && window.innerWidth >= 768,
233
254
  );
255
+ // The slide just inserted via the toolbar's New Slide button, so the rail
256
+ // can anchor the "describe this slide" popover to its thumbnail once it
257
+ // mounts — even though the button that sets this now lives in the
258
+ // toolbar, outside the rail.
259
+ const [describeSlideId, setDescribeSlideId] = useState<string | null>(null);
260
+ useEffect(() => {
261
+ setDescribeSlideId(null);
262
+ }, [id]);
234
263
  const [contextToolbarSlot, setContextToolbarSlot] =
235
264
  useState<HTMLDivElement | null>(null);
236
265
  const [wideContextToolbarSlot, setWideContextToolbarSlot] =
@@ -756,6 +785,18 @@ export default function DeckEditor() {
756
785
  document.removeEventListener("keydown", handleTextToolShortcut);
757
786
  }, [canEdit]);
758
787
 
788
+ useEffect(() => {
789
+ const handleItalicShortcut = (event: KeyboardEvent) => {
790
+ if (!shouldSuppressSlidesItalicShortcut(event)) return;
791
+ event.preventDefault();
792
+ event.stopImmediatePropagation();
793
+ };
794
+
795
+ document.addEventListener("keydown", handleItalicShortcut, true);
796
+ return () =>
797
+ document.removeEventListener("keydown", handleItalicShortcut, true);
798
+ }, []);
799
+
759
800
  // Delete key deletes the current slide
760
801
  useEffect(() => {
761
802
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -807,49 +848,92 @@ export default function DeckEditor() {
807
848
  return () => document.removeEventListener("keydown", handleKeyDown);
808
849
  }, [deck, id, activeSlideId, deleteSlideWithUndo, pinMode, drawMode]);
809
850
 
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.
851
+ // Command/Ctrl+C then Command/Ctrl+V on the slide rail duplicates the
852
+ // selected slide directly below itself. Only claims the shortcut when no
853
+ // slide element is selected — SlideEditor owns Cmd+C/V for object copy/paste
854
+ // in that case.
814
855
  const copiedSlideIdRef = useRef<string | null>(null);
815
856
  useEffect(() => {
816
857
  const handleKeyDown = (e: KeyboardEvent) => {
817
- if (!deck || !id) return;
858
+ if (!deck || !id || !canEdit) return;
818
859
  if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
819
860
  const key = e.key.toLowerCase();
820
861
  if (key !== "c" && key !== "v") return;
821
862
  if (pinMode || drawMode) return;
863
+
864
+ // A live browser text selection (e.g. the user triple-clicked rendered,
865
+ // non-editable slide copy) means Cmd/Ctrl+C is a normal text copy —
866
+ // let it through instead of hijacking it into a slide duplicate.
867
+ if (key === "c" && (window.getSelection()?.toString().length ?? 0) > 0) {
868
+ return;
869
+ }
870
+
871
+ // Radix Popper positions Popover/DropdownMenu/Select/Tooltip content
872
+ // inside the same [data-radix-popper-content-wrapper]. A tooltip opens
873
+ // on plain hover, so treating every such wrapper as blocking would
874
+ // disable this shortcut just by mousing over a toolbar button; only
875
+ // wrappers that aren't tooltips (marked with data-agent-native-tooltip)
876
+ // should count as an open menu/popover/dialog owning the keystroke.
877
+ const isBlockingPopperWrapper = (el: Element) =>
878
+ el.matches("[data-radix-popper-content-wrapper]") &&
879
+ !el.querySelector("[data-agent-native-tooltip]");
880
+ const isInsideSafeZone = (el: Element | null) => {
881
+ if (!el) return false;
882
+ if (el instanceof HTMLInputElement) return true;
883
+ if (el instanceof HTMLTextAreaElement) return true;
884
+ if (el instanceof HTMLElement) {
885
+ if (el.isContentEditable) return true;
886
+ if (el.closest("[contenteditable='true']")) return true;
887
+ if (el.closest("input, textarea, [role='textbox']")) return true;
888
+ if (el.closest("[data-pin-popover]")) return true;
889
+ if (el.closest("[data-add-slide-popover]")) return true;
890
+ if (el.closest(".agent-panel-root")) return true;
891
+ if (el.closest("[role='dialog'], [role='alertdialog']")) return true;
892
+ const popperWrapper = el.closest(
893
+ "[data-radix-popper-content-wrapper]",
894
+ );
895
+ if (popperWrapper && isBlockingPopperWrapper(popperWrapper))
896
+ return true;
897
+ }
898
+ return false;
899
+ };
900
+ if (isInsideSafeZone(e.target as Element | null)) return;
901
+ if (isInsideSafeZone(document.activeElement)) return;
902
+ if (document.querySelector("[data-pin-popover]")) return;
903
+ if (document.querySelector("[data-add-slide-popover]")) return;
904
+ // A dialog/sheet/menu/popover owning focus elsewhere in the DOM (not
905
+ // just under the event target) still shouldn't let this document-level
906
+ // shortcut duplicate the slide underneath it.
822
907
  if (
823
908
  document.querySelector(
824
- "[role='dialog'], [role='menu'], [role='listbox']",
909
+ "[role='dialog'], [role='alertdialog'], [role='menu'], [role='listbox']",
825
910
  )
826
- ) {
911
+ )
912
+ return;
913
+ if (
914
+ Array.from(
915
+ document.querySelectorAll("[data-radix-popper-content-wrapper]"),
916
+ ).some(isBlockingPopperWrapper)
917
+ )
918
+ return;
919
+ if (document.querySelector("[data-slide-element-selected='true']"))
827
920
  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
921
 
839
922
  if (key === "c") {
840
- copiedSlideIdRef.current = focusedSlideId;
923
+ if (!activeSlideId) return;
924
+ copiedSlideIdRef.current = activeSlideId;
841
925
  return;
842
926
  }
843
- const copiedSlideId = copiedSlideIdRef.current;
844
- if (!copiedSlideId) return;
845
- if (!deck.slides.some((s) => s.id === copiedSlideId)) return;
927
+
928
+ const copiedId = copiedSlideIdRef.current;
929
+ if (!copiedId || !deck.slides.some((s) => s.id === copiedId)) return;
846
930
  e.preventDefault();
847
- const newSlideId = duplicateSlide(id, copiedSlideId);
848
- if (newSlideId) setActiveSlideId(newSlideId);
931
+ const newId = duplicateSlide(id, copiedId);
932
+ if (newId) setActiveSlideId(newId);
849
933
  };
850
934
  document.addEventListener("keydown", handleKeyDown);
851
935
  return () => document.removeEventListener("keydown", handleKeyDown);
852
- }, [deck, id, duplicateSlide, canEdit, pinMode, drawMode]);
936
+ }, [deck, id, canEdit, activeSlideId, duplicateSlide, pinMode, drawMode]);
853
937
 
854
938
  // Resolve the active slide from URL/deck state. Imports replace slide IDs, so
855
939
  // keep this valid after deck contents change instead of only on first load.
@@ -1083,9 +1167,30 @@ export default function DeckEditor() {
1083
1167
 
1084
1168
  const handleAddEmptySlide = () => {
1085
1169
  const activeIdx = deck.slides.findIndex((s) => s.id === activeSlideId);
1086
- setActiveSlideId(
1087
- addSlide(id, "blank", activeIdx >= 0 ? activeIdx : undefined),
1170
+ // Immediate persistence: this placeholder is immediately followed by an
1171
+ // agent request to `update-slide` it, which can reach the server before
1172
+ // the default 500ms debounce would have flushed the `add-slide` op.
1173
+ const newId = addSlide(
1174
+ id,
1175
+ "blank",
1176
+ activeIdx >= 0 ? activeIdx : undefined,
1177
+ {
1178
+ persistence: "immediate",
1179
+ },
1088
1180
  );
1181
+ setActiveSlideId(newId);
1182
+ return newId;
1183
+ };
1184
+
1185
+ const handleNewSlideClick = () => {
1186
+ const newId = handleAddEmptySlide();
1187
+ if (newId) {
1188
+ // The rail owns the anchor node the describe-slide popover attaches
1189
+ // to, so it must be mounted even if it started closed on a narrow
1190
+ // viewport where the toolbar button is still reachable.
1191
+ setSidebarOpen(true);
1192
+ setDescribeSlideId(newId);
1193
+ }
1089
1194
  };
1090
1195
 
1091
1196
  return (
@@ -1101,7 +1206,6 @@ export default function DeckEditor() {
1101
1206
  canEdit={canEdit}
1102
1207
  canComment={canComment}
1103
1208
  onTitleChange={(title) => updateDeck(id, { title })}
1104
- slideCount={deck.slides.length}
1105
1209
  currentSlideIndex={currentIndex >= 0 ? currentIndex : 0}
1106
1210
  sidebarOpen={sidebarOpen}
1107
1211
  onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
@@ -1113,6 +1217,8 @@ export default function DeckEditor() {
1113
1217
  onShowHistory={() => setHistoryOpen((open) => !open)}
1114
1218
  historyButtonRef={historyButtonRef}
1115
1219
  currentSlide={currentSlide}
1220
+ onAddEmptySlide={canEdit ? handleNewSlideClick : undefined}
1221
+ addSlideGenerating={addSlideGenerating}
1116
1222
  onWideContextToolbarSlotChange={setWideContextToolbarSlot}
1117
1223
  activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
1118
1224
  agentPresent={agentPresent}
@@ -1187,13 +1293,6 @@ export default function DeckEditor() {
1187
1293
  }
1188
1294
  return exportDeckToGoogleSlides(deck.title, slides, deck.aspectRatio);
1189
1295
  }}
1190
- currentSlideId={currentSlide?.id}
1191
- addSlideGenerating={addSlideGenerating}
1192
- onAddSlideGeneratingChange={setAddSlideGenerating}
1193
- onAddEmptySlide={handleAddEmptySlide}
1194
- onDuplicateCurrentSlide={
1195
- currentSlide ? () => duplicateSlide(id, currentSlide.id) : undefined
1196
- }
1197
1296
  onChangeSlideTransition={
1198
1297
  canEdit && currentSlide
1199
1298
  ? (transition) => updateSlide(id, currentSlide.id, { transition })
@@ -1226,6 +1325,13 @@ export default function DeckEditor() {
1226
1325
  slides={deck.slides}
1227
1326
  activeSlideId={currentSlide?.id || ""}
1228
1327
  deckId={id}
1328
+ deckTitle={deck.title}
1329
+ describeSlideId={describeSlideId}
1330
+ onCloseDescribe={() => setDescribeSlideId(null)}
1331
+ onAwaitAddSlidePersisted={() => flushDeckSave(id)}
1332
+ onRemoveFailedSlide={(slideId) => deleteSlide(id, slideId)}
1333
+ addSlideAgentSubmit={addSlideAgentSubmit}
1334
+ onAddSlideGeneratingChange={setAddSlideGenerating}
1229
1335
  onSelectSlide={(slideId) => {
1230
1336
  setGeneratingSlideSelected(false);
1231
1337
  setActiveSlideId(slideId);
@@ -1304,19 +1410,11 @@ export default function DeckEditor() {
1304
1410
  contextToolbarLeading={
1305
1411
  canEdit ? (
1306
1412
  <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
1413
  textBoxMode={textBoxMode}
1319
1414
  onToggleTextBoxMode={toggleTextBoxMode}
1415
+ onAddEmptySlide={handleNewSlideClick}
1416
+ addSlideGenerating={addSlideGenerating}
1417
+ currentSlideId={currentSlide.id}
1320
1418
  slideTransition={currentSlide.transition}
1321
1419
  onChangeSlideTransition={(transition) =>
1322
1420
  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";