@agent-native/core 0.190.0-nightly-20260926012834 → 0.190.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 (33) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +12 -0
  3. package/corpus/templates/analytics/app/global.css +5 -1
  4. package/corpus/templates/analytics/changelog/2026-09-25-chat-composer-background.md +6 -0
  5. package/corpus/templates/calendar/CHANGELOG.md +7 -0
  6. package/corpus/templates/calendar/app/root.tsx +7 -7
  7. package/corpus/templates/calendar/app/routes/meet.$username.tsx +8 -0
  8. package/corpus/templates/calendar/changelog/2026-09-25-booking-links-without-a-meeting-type-now-show-a-not-found-pa.md +6 -0
  9. package/corpus/templates/clips/CHANGELOG.md +4 -0
  10. package/corpus/templates/content/app/root.tsx +0 -7
  11. package/corpus/templates/design/CHANGELOG.md +5 -0
  12. package/corpus/templates/design/actions/get-design.ts +9 -9
  13. package/corpus/templates/mail/CHANGELOG.md +13 -0
  14. package/corpus/templates/mail/app/root.tsx +1 -16
  15. package/corpus/templates/slides/CHANGELOG.md +8 -0
  16. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
  17. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +76 -0
  18. package/corpus/templates/slides/app/hooks/use-new-deck-generation.ts +16 -4
  19. package/corpus/templates/slides/app/pages/DeckEditor.tsx +591 -30
  20. package/corpus/templates/slides/changelog/2026-09-25-empty-decks-keep-their-retry-option-when-a-retry-cannot-be-s.md +6 -0
  21. package/corpus/templates/slides/changelog/2026-09-25-prevent-setup-generation-while-figma-indexing-is-in-progress.md +6 -0
  22. package/dist/automation/index.d.ts +1 -1
  23. package/dist/client/ErrorBoundary.js +2 -14
  24. package/dist/client/ErrorReportActions.js +1 -1
  25. package/dist/collab/struct-routes.d.ts +1 -1
  26. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  27. package/dist/notifications/routes.d.ts +1 -1
  28. package/dist/org/handlers.d.ts +1 -1
  29. package/dist/secrets/routes.d.ts +9 -9
  30. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  31. package/dist/server/transcribe-voice.d.ts +1 -1
  32. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  33. package/package.json +4 -4
@@ -1,5 +1,7 @@
1
1
  import {
2
+ AGENT_CHAT_SUBMIT_RESULT_EVENT,
2
3
  sendToAgentChat,
4
+ type AgentChatSubmitResult,
3
5
  useGuidedQuestionFlow,
4
6
  } from "@agent-native/core/client/agent-chat";
5
7
  import {
@@ -84,6 +86,7 @@ import {
84
86
  AlertDialogHeader,
85
87
  AlertDialogTitle,
86
88
  } from "@/components/ui/alert-dialog";
89
+ import { Button } from "@/components/ui/button";
87
90
  import {
88
91
  clearSlideEditingActive,
89
92
  deckIdFromPathname,
@@ -112,6 +115,8 @@ import { useDeckPresence } from "@/hooks/use-deck-presence";
112
115
  import { useDeckRole } from "@/hooks/use-deck-role";
113
116
  import {
114
117
  clearNewDeckGenerationRun,
118
+ NEW_DECK_GENERATION_SUBMIT_TARGET_EVENT,
119
+ rememberNewDeckGenerationRunTab,
115
120
  useNewDeckGeneration,
116
121
  useNewDeckGenerationRun,
117
122
  } from "@/hooks/use-new-deck-generation";
@@ -389,6 +394,100 @@ export type GenerationDeckRefreshResult =
389
394
  | { status: "not_ready" }
390
395
  | { status: "failed" };
391
396
 
397
+ type EmptyGenerationRecovery =
398
+ | {
399
+ kind: "retry_rollback";
400
+ retryAttemptId: string;
401
+ restoreAttemptId: string | null;
402
+ ownerTabId?: string;
403
+ restoreSearchParams?: string;
404
+ }
405
+ | { kind: "retry_accepted"; retryAttemptId: string }
406
+ | { kind: "generation_failure"; attemptId: string; failureCode: string };
407
+
408
+ function parseEmptyGenerationRecovery(
409
+ serialized: string,
410
+ ): EmptyGenerationRecovery | null {
411
+ let recovery: unknown;
412
+ try {
413
+ recovery = JSON.parse(serialized);
414
+ } catch (error) {
415
+ console.warn("Ignoring invalid Slides generation recovery data.", error);
416
+ return null;
417
+ }
418
+ if (typeof recovery !== "object" || recovery === null) return null;
419
+ const record = recovery as Record<string, unknown>;
420
+ if (
421
+ (record.kind === "retry_rollback" || record.kind === undefined) &&
422
+ typeof record.retryAttemptId === "string" &&
423
+ (typeof record.restoreAttemptId === "string" ||
424
+ record.restoreAttemptId === null)
425
+ ) {
426
+ return {
427
+ kind: "retry_rollback",
428
+ retryAttemptId: record.retryAttemptId,
429
+ restoreAttemptId: record.restoreAttemptId,
430
+ ...(typeof record.ownerTabId === "string"
431
+ ? { ownerTabId: record.ownerTabId }
432
+ : {}),
433
+ ...(typeof record.restoreSearchParams === "string"
434
+ ? { restoreSearchParams: record.restoreSearchParams }
435
+ : {}),
436
+ };
437
+ }
438
+ if (
439
+ record.kind === "retry_accepted" &&
440
+ typeof record.retryAttemptId === "string"
441
+ ) {
442
+ return {
443
+ kind: "retry_accepted",
444
+ retryAttemptId: record.retryAttemptId,
445
+ };
446
+ }
447
+ if (
448
+ record.kind === "generation_failure" &&
449
+ typeof record.attemptId === "string" &&
450
+ typeof record.failureCode === "string"
451
+ ) {
452
+ return {
453
+ kind: "generation_failure",
454
+ attemptId: record.attemptId,
455
+ failureCode: record.failureCode,
456
+ };
457
+ }
458
+ return null;
459
+ }
460
+
461
+ function getEmptyGenerationRetryOwnerTabId(deckId: string): string | null {
462
+ if (typeof window === "undefined") return null;
463
+ try {
464
+ const key = `slides:empty-generation-retry-owner:${deckId}`;
465
+ const stored = window.sessionStorage.getItem(key);
466
+ if (stored) return stored;
467
+ const ownerTabId = nanoid();
468
+ window.sessionStorage.setItem(key, ownerTabId);
469
+ return ownerTabId;
470
+ } catch (error) {
471
+ console.error("Failed to store Slides retry tab identity.", error);
472
+ return null;
473
+ }
474
+ }
475
+
476
+ function clearEmptyGenerationRecovery(
477
+ key: string | null,
478
+ expected?: string,
479
+ ): boolean {
480
+ if (!key || typeof window === "undefined") return false;
481
+ try {
482
+ if (expected && window.localStorage.getItem(key) !== expected) return false;
483
+ window.localStorage.removeItem(key);
484
+ return window.localStorage.getItem(key) === null;
485
+ } catch (error) {
486
+ console.error("Failed to clear Slides generation recovery data.", error);
487
+ return false;
488
+ }
489
+ }
490
+
392
491
  export async function refreshDeckForGenerationOutcome(
393
492
  refreshOpenDeck: (deckId: string) => Promise<Deck | null>,
394
493
  deckId: string,
@@ -506,8 +605,13 @@ export default function DeckEditor() {
506
605
  useAgentGenerating();
507
606
  const isNewDeckGenerationRoute = searchParams.get("generating") === "1";
508
607
  const generationSubmitId = searchParams.get("generationSubmitId");
608
+ const retryEmptyGenerationInFlightRef = useRef(false);
609
+ const emptyGenerationRecoveryRef = useRef<string | null>(null);
610
+ const [retryEmptyGenerationPending, setRetryEmptyGenerationPending] =
611
+ useState(false);
509
612
  const {
510
613
  generating: newDeckGenerationGenerating,
614
+ tabId: newDeckGenerationTabId,
511
615
  questionContinuationPending,
512
616
  submitQuestionContinuation: submitTrackedQuestionContinuation,
513
617
  } = useNewDeckGenerationRun(
@@ -788,6 +892,9 @@ export default function DeckEditor() {
788
892
  const uploadInputRef = useRef<HTMLInputElement>(null);
789
893
 
790
894
  const deck = getDeck(id || "");
895
+ const retryRecoveryStorageKey = id
896
+ ? `slides:empty-generation-retry-recovery:${id}`
897
+ : null;
791
898
 
792
899
  useEffect(() => {
793
900
  setAnimationTarget(null);
@@ -811,6 +918,10 @@ export default function DeckEditor() {
811
918
  ((org?.pendingInvitations?.length ?? 0) > 0 ||
812
919
  (org?.domainMatches?.length ?? 0) > 0);
813
920
  const slideCount = deck?.slides.length ?? 0;
921
+ // Mirror Google Slides: viewers see the editor shell with edit affordances
922
+ // disabled. Only assume edit access while the role is still loading when
923
+ // `createdByMe` already confirms ownership.
924
+ const { canEdit, canComment } = useDeckRole(id, deck?.createdByMe === true);
814
925
  const generationContext =
815
926
  deck?.generationContext &&
816
927
  typeof deck.generationContext === "object" &&
@@ -821,6 +932,141 @@ export default function DeckEditor() {
821
932
  typeof generationContext?.generationAttemptId === "string"
822
933
  ? generationContext.generationAttemptId
823
934
  : searchParams.get("generation_attempt_id");
935
+ const generationFailed =
936
+ slideCount === 0 &&
937
+ typeof generationContext?.generationFailureCode === "string";
938
+ const generationRetryPending =
939
+ retryEmptyGenerationPending ||
940
+ (generationContext !== null &&
941
+ "generationFailureAttemptId" in generationContext &&
942
+ generationContext.generationFailureAttemptId !== generationAttemptId);
943
+ useEffect(() => {
944
+ if (!id || !retryRecoveryStorageKey || !generationContext) return;
945
+
946
+ let serializedRecovery: string | null;
947
+ try {
948
+ serializedRecovery = window.localStorage.getItem(retryRecoveryStorageKey);
949
+ } catch {
950
+ return;
951
+ }
952
+ if (!serializedRecovery) return;
953
+
954
+ const recovery = parseEmptyGenerationRecovery(serializedRecovery);
955
+ if (!recovery) {
956
+ clearEmptyGenerationRecovery(retryRecoveryStorageKey, serializedRecovery);
957
+ return;
958
+ }
959
+ if (recovery.kind === "retry_rollback") {
960
+ if (recovery.retryAttemptId !== generationAttemptId) return;
961
+ if (recovery.ownerTabId) {
962
+ try {
963
+ if (
964
+ window.sessionStorage.getItem(
965
+ `slides:empty-generation-retry-owner:${id}`,
966
+ ) !== recovery.ownerTabId
967
+ ) {
968
+ return;
969
+ }
970
+ } catch {
971
+ return;
972
+ }
973
+ } else if (
974
+ searchParams.get("generation_attempt_id") !== recovery.retryAttemptId
975
+ ) {
976
+ return;
977
+ }
978
+ if (emptyGenerationRecoveryRef.current === serializedRecovery) return;
979
+ emptyGenerationRecoveryRef.current = serializedRecovery;
980
+ if (
981
+ searchParams.get("generation_attempt_id") === recovery.retryAttemptId
982
+ ) {
983
+ const restoredSearchParams = new URLSearchParams(
984
+ recovery.restoreSearchParams ?? searchParams,
985
+ );
986
+ if (recovery.restoreSearchParams === undefined) {
987
+ restoredSearchParams.delete("generating");
988
+ restoredSearchParams.delete("generation_attempt_id");
989
+ restoredSearchParams.delete("generationSubmitId");
990
+ }
991
+ setSearchParams(restoredSearchParams, { replace: true });
992
+ }
993
+ updateDeck(id, {
994
+ generationContext: {
995
+ ...generationContext,
996
+ generationAttemptId: recovery.restoreAttemptId ?? undefined,
997
+ },
998
+ });
999
+ } else if (recovery.kind === "generation_failure") {
1000
+ if (recovery.attemptId !== generationAttemptId) return;
1001
+ if (emptyGenerationRecoveryRef.current === serializedRecovery) return;
1002
+ if (
1003
+ generationContext.generationFailureCode === recovery.failureCode &&
1004
+ generationContext.generationFailureAttemptId === recovery.attemptId
1005
+ ) {
1006
+ clearEmptyGenerationRecovery(
1007
+ retryRecoveryStorageKey,
1008
+ serializedRecovery,
1009
+ );
1010
+ return;
1011
+ }
1012
+ emptyGenerationRecoveryRef.current = serializedRecovery;
1013
+ updateDeck(id, {
1014
+ generationContext: {
1015
+ ...generationContext,
1016
+ generationFailureCode: recovery.failureCode,
1017
+ generationFailureAttemptId: recovery.attemptId,
1018
+ },
1019
+ });
1020
+ } else {
1021
+ if (recovery.retryAttemptId !== generationAttemptId) return;
1022
+ if (emptyGenerationRecoveryRef.current === serializedRecovery) return;
1023
+ if (
1024
+ generationContext.generationFailureCode == null &&
1025
+ generationContext.generationFailureAttemptId == null
1026
+ ) {
1027
+ clearEmptyGenerationRecovery(
1028
+ retryRecoveryStorageKey,
1029
+ serializedRecovery,
1030
+ );
1031
+ return;
1032
+ }
1033
+ emptyGenerationRecoveryRef.current = serializedRecovery;
1034
+ updateDeck(id, {
1035
+ generationContext: {
1036
+ ...generationContext,
1037
+ generationFailureCode: null,
1038
+ generationFailureAttemptId: null,
1039
+ },
1040
+ });
1041
+ }
1042
+
1043
+ void flushDeckSave(id)
1044
+ .then(() => {
1045
+ if (
1046
+ !clearEmptyGenerationRecovery(
1047
+ retryRecoveryStorageKey,
1048
+ serializedRecovery,
1049
+ )
1050
+ ) {
1051
+ toast.error(t("settings.saveFailed"));
1052
+ return;
1053
+ }
1054
+ if (emptyGenerationRecoveryRef.current === serializedRecovery) {
1055
+ emptyGenerationRecoveryRef.current = null;
1056
+ }
1057
+ })
1058
+ .catch(() => toast.error(t("settings.saveFailed")));
1059
+ }, [
1060
+ flushDeckSave,
1061
+ generationAttemptId,
1062
+ generationContext,
1063
+ id,
1064
+ retryRecoveryStorageKey,
1065
+ searchParams,
1066
+ setSearchParams,
1067
+ t,
1068
+ updateDeck,
1069
+ ]);
824
1070
  useEffect(() => {
825
1071
  if (!id || !deck || slideCount === 0) {
826
1072
  return;
@@ -844,19 +1090,27 @@ export default function DeckEditor() {
844
1090
  }, [deck, generationAttemptId, id, slideCount]);
845
1091
  const generationLifecycleOwnedByEditor =
846
1092
  generationContext?.generationMode !== "action";
847
- const [generationAttemptTabId, setGenerationAttemptTabId] = useState<
848
- string | null
849
- >(() =>
850
- generationAttemptId && id
851
- ? getStartedGenerationAttemptTabId(generationAttemptId, id)
852
- : null,
853
- );
1093
+ const [generationAttemptTab, setGenerationAttemptTab] = useState<{
1094
+ attemptId: string;
1095
+ tabId: string;
1096
+ } | null>(() => {
1097
+ if (!generationAttemptId || !id) return null;
1098
+ const tabId =
1099
+ getStartedGenerationAttemptTabId(generationAttemptId, id) ??
1100
+ newDeckGenerationTabId;
1101
+ return tabId ? { attemptId: generationAttemptId, tabId } : null;
1102
+ });
1103
+ const generationAttemptTabId =
1104
+ generationAttemptTab?.attemptId === generationAttemptId
1105
+ ? generationAttemptTab.tabId
1106
+ : newDeckGenerationTabId;
854
1107
  const {
855
1108
  attempt: {
856
1109
  observedRun: attemptObservedRun,
857
1110
  runError: attemptRunError,
858
1111
  stopReason: attemptStopReason,
859
1112
  timedOut: attemptTimedOut,
1113
+ submitAndConfirm: submitGenerationAttemptAndConfirm,
860
1114
  },
861
1115
  generating: newDeckGenerationSignal,
862
1116
  } = useNewDeckGenerationSignal({
@@ -880,12 +1134,16 @@ export default function DeckEditor() {
880
1134
  !wasNewDeckCreation.current
881
1135
  )
882
1136
  return;
883
- generationRunStartedRef.current = hasStartedGenerationAttempt(
884
- generationAttemptId,
885
- id,
886
- );
887
- setGenerationAttemptTabId(
888
- getStartedGenerationAttemptTabId(generationAttemptId, id),
1137
+ generationRunStartedRef.current =
1138
+ hasStartedGenerationAttempt(generationAttemptId, id) ||
1139
+ newDeckGenerationTabId !== null;
1140
+ const startedTabId =
1141
+ getStartedGenerationAttemptTabId(generationAttemptId, id) ??
1142
+ newDeckGenerationTabId;
1143
+ setGenerationAttemptTab(
1144
+ startedTabId
1145
+ ? { attemptId: generationAttemptId, tabId: startedTabId }
1146
+ : null,
889
1147
  );
890
1148
  const handleGenerationStarted = (event: Event) => {
891
1149
  const detail = (event as CustomEvent).detail;
@@ -896,7 +1154,10 @@ export default function DeckEditor() {
896
1154
  return;
897
1155
  }
898
1156
  if (typeof detail.tabId !== "string") return;
899
- setGenerationAttemptTabId(detail.tabId);
1157
+ setGenerationAttemptTab({
1158
+ attemptId: generationAttemptId,
1159
+ tabId: detail.tabId,
1160
+ });
900
1161
  generationRunStartedRef.current = true;
901
1162
  };
902
1163
  window.addEventListener(
@@ -908,7 +1169,12 @@ export default function DeckEditor() {
908
1169
  SLIDES_GENERATION_STARTED_EVENT,
909
1170
  handleGenerationStarted,
910
1171
  );
911
- }, [generationAttemptId, generationLifecycleOwnedByEditor, id]);
1172
+ }, [
1173
+ generationAttemptId,
1174
+ generationLifecycleOwnedByEditor,
1175
+ id,
1176
+ newDeckGenerationTabId,
1177
+ ]);
912
1178
 
913
1179
  useEffect(() => {
914
1180
  if (
@@ -988,12 +1254,54 @@ export default function DeckEditor() {
988
1254
  ? "timeout"
989
1255
  : attemptRunError
990
1256
  ? "agent_error"
991
- : targetSlideCount !== null &&
992
- settledSlideCount < targetSlideCount
993
- ? "incomplete_output"
994
- : settledSlideCount === 0
995
- ? "no_output"
1257
+ : settledSlideCount === 0
1258
+ ? "no_output"
1259
+ : targetSlideCount !== null &&
1260
+ settledSlideCount < targetSlideCount
1261
+ ? "incomplete_output"
996
1262
  : null;
1263
+ if (failureCode && settledSlideCount === 0 && generationContext) {
1264
+ updateDeck(id, {
1265
+ generationContext: {
1266
+ ...generationContext,
1267
+ generationFailureCode: failureCode,
1268
+ generationFailureAttemptId: generationAttemptId,
1269
+ },
1270
+ });
1271
+ const recovery: EmptyGenerationRecovery = {
1272
+ kind: "generation_failure",
1273
+ attemptId: generationAttemptId,
1274
+ failureCode,
1275
+ };
1276
+ const serializedRecovery = JSON.stringify(recovery);
1277
+ if (retryRecoveryStorageKey) {
1278
+ try {
1279
+ window.localStorage.setItem(
1280
+ retryRecoveryStorageKey,
1281
+ serializedRecovery,
1282
+ );
1283
+ emptyGenerationRecoveryRef.current = serializedRecovery;
1284
+ } catch (error) {
1285
+ console.error(
1286
+ "Failed to store Slides generation recovery data.",
1287
+ error,
1288
+ );
1289
+ }
1290
+ }
1291
+ try {
1292
+ await flushDeckSave(id);
1293
+ if (
1294
+ clearEmptyGenerationRecovery(
1295
+ retryRecoveryStorageKey,
1296
+ serializedRecovery,
1297
+ )
1298
+ ) {
1299
+ emptyGenerationRecoveryRef.current = null;
1300
+ }
1301
+ } catch {
1302
+ toast.error(t("editorSidebar.newSlideSaveFailed"));
1303
+ }
1304
+ }
997
1305
  if (failureCode === "cancelled") {
998
1306
  trackEvent("generation_cancelled", {
999
1307
  ...properties,
@@ -1029,16 +1337,259 @@ export default function DeckEditor() {
1029
1337
  attemptObservedRun,
1030
1338
  generationAttemptId,
1031
1339
  generationLifecycleOwnedByEditor,
1340
+ generationContext,
1032
1341
  attemptRunError,
1033
1342
  attemptStopReason,
1034
1343
  attemptTimedOut,
1035
1344
  id,
1036
1345
  newDeckGenerationSignal,
1037
1346
  refreshOpenDeck,
1347
+ retryRecoveryStorageKey,
1348
+ updateDeck,
1349
+ flushDeckSave,
1350
+ t,
1038
1351
  slideCount,
1039
1352
  targetSlideCount,
1040
1353
  ]);
1041
1354
 
1355
+ const retryEmptyGeneration = useCallback(async () => {
1356
+ if (
1357
+ !id ||
1358
+ !generationContext ||
1359
+ !generationLifecycleOwnedByEditor ||
1360
+ !canEdit ||
1361
+ retryEmptyGenerationInFlightRef.current
1362
+ ) {
1363
+ return;
1364
+ }
1365
+ retryEmptyGenerationInFlightRef.current = true;
1366
+ setRetryEmptyGenerationPending(true);
1367
+ const originalSearchParams = new URLSearchParams(searchParams);
1368
+ const retryAttemptId = nanoid();
1369
+ const submitMessageId = nanoid();
1370
+ const ownerTabId = getEmptyGenerationRetryOwnerTabId(id);
1371
+ if (!ownerTabId) {
1372
+ retryEmptyGenerationInFlightRef.current = false;
1373
+ setRetryEmptyGenerationPending(false);
1374
+ toast.error(t("settings.saveFailed"));
1375
+ return;
1376
+ }
1377
+ const retryContext = {
1378
+ ...generationContext,
1379
+ generationAttemptId: retryAttemptId,
1380
+ generationFailureAttemptId:
1381
+ generationContext.generationFailureAttemptId ?? generationAttemptId,
1382
+ };
1383
+ const rollbackRecovery: EmptyGenerationRecovery = {
1384
+ kind: "retry_rollback",
1385
+ retryAttemptId,
1386
+ ownerTabId,
1387
+ restoreAttemptId:
1388
+ typeof generationContext.generationFailureAttemptId === "string"
1389
+ ? generationContext.generationFailureAttemptId
1390
+ : generationAttemptId,
1391
+ restoreSearchParams: originalSearchParams.toString(),
1392
+ };
1393
+ const storeRecovery = (recovery: EmptyGenerationRecovery) => {
1394
+ if (!retryRecoveryStorageKey) return null;
1395
+ const serialized = JSON.stringify(recovery);
1396
+ try {
1397
+ window.localStorage.setItem(retryRecoveryStorageKey, serialized);
1398
+ emptyGenerationRecoveryRef.current = serialized;
1399
+ return serialized;
1400
+ } catch (error) {
1401
+ console.error(
1402
+ "Failed to store Slides generation recovery data.",
1403
+ error,
1404
+ );
1405
+ return null;
1406
+ }
1407
+ };
1408
+ const rollbackRecoverySerialized = storeRecovery(rollbackRecovery);
1409
+ if (!rollbackRecoverySerialized) {
1410
+ retryEmptyGenerationInFlightRef.current = false;
1411
+ setRetryEmptyGenerationPending(false);
1412
+ toast.error(t("settings.saveFailed"));
1413
+ return;
1414
+ }
1415
+ setGenerationAttemptTab(null);
1416
+ generationRunStartedRef.current = false;
1417
+ generationSawActiveRef.current = false;
1418
+ generationTerminalAttemptRef.current = null;
1419
+ generationSettlingAttemptRef.current = null;
1420
+ generationStartedAtRef.current = null;
1421
+ const restoreFailedRetry = async () => {
1422
+ updateDeck(id, { generationContext });
1423
+ setSearchParams(new URLSearchParams(originalSearchParams));
1424
+ generationRunStartedRef.current = false;
1425
+ generationSawActiveRef.current = false;
1426
+ generationTerminalAttemptRef.current = null;
1427
+ generationSettlingAttemptRef.current = null;
1428
+ generationStartedAtRef.current = null;
1429
+ try {
1430
+ await flushDeckSave(id);
1431
+ clearEmptyGenerationRecovery(
1432
+ retryRecoveryStorageKey,
1433
+ rollbackRecoverySerialized,
1434
+ );
1435
+ emptyGenerationRecoveryRef.current = null;
1436
+ return { persisted: true };
1437
+ } catch {
1438
+ return { persisted: false };
1439
+ }
1440
+ };
1441
+
1442
+ try {
1443
+ try {
1444
+ updateDeck(id, { generationContext: retryContext });
1445
+ await flushDeckSave(id);
1446
+ } catch {
1447
+ await restoreFailedRetry();
1448
+ toast.error(t("settings.saveFailed"));
1449
+ return;
1450
+ }
1451
+ setSearchParams((current) => {
1452
+ const next = new URLSearchParams(current);
1453
+ next.set("generating", "1");
1454
+ next.set("generation_attempt_id", retryAttemptId);
1455
+ next.set("generationSubmitId", submitMessageId);
1456
+ return next;
1457
+ });
1458
+ const prompt =
1459
+ typeof generationContext.originalPrompt === "string"
1460
+ ? generationContext.originalPrompt
1461
+ : "Continue generating this deck.";
1462
+ const acceptedRecovery: EmptyGenerationRecovery = {
1463
+ kind: "retry_accepted",
1464
+ retryAttemptId,
1465
+ };
1466
+ let acceptedRecoverySerialized: string | null = null;
1467
+ const rememberConfirmedDelivery = (event: Event) => {
1468
+ const { detail } = event as CustomEvent<AgentChatSubmitResult>;
1469
+ if (detail?.submitMessageId === submitMessageId && detail.delivered) {
1470
+ acceptedRecoverySerialized = storeRecovery(acceptedRecovery);
1471
+ }
1472
+ };
1473
+ const rememberRetrySubmitTarget = (event: Event) => {
1474
+ const { detail } = event as CustomEvent<{
1475
+ submitMessageId?: string;
1476
+ tabId?: string;
1477
+ }>;
1478
+ if (
1479
+ detail?.submitMessageId === submitMessageId &&
1480
+ typeof detail.tabId === "string"
1481
+ ) {
1482
+ rememberNewDeckGenerationRunTab(id, submitMessageId, detail.tabId);
1483
+ }
1484
+ };
1485
+ let submission: Awaited<
1486
+ ReturnType<typeof submitGenerationAttemptAndConfirm>
1487
+ >;
1488
+ window.addEventListener(
1489
+ NEW_DECK_GENERATION_SUBMIT_TARGET_EVENT,
1490
+ rememberRetrySubmitTarget,
1491
+ );
1492
+ window.addEventListener(
1493
+ AGENT_CHAT_SUBMIT_RESULT_EVENT,
1494
+ rememberConfirmedDelivery,
1495
+ );
1496
+ try {
1497
+ submission = await submitGenerationAttemptAndConfirm(
1498
+ prompt,
1499
+ `Continue the original deck generation for deck ${id}. Call get-deck first and recover the canonical generationContext, including its original brief, target slide count, and reference handles. Continue the original sequence; do not start a new topic. The browser owns this attempt; use generationAttemptId "${retryAttemptId}" for tool calls that accept it.`,
1500
+ {
1501
+ generationAttemptId: retryAttemptId,
1502
+ generationOutputId: id,
1503
+ submitMessageId,
1504
+ newTab: true,
1505
+ reuseEmptyTab: true,
1506
+ openSidebar: true,
1507
+ },
1508
+ );
1509
+ } catch {
1510
+ const rollback = await restoreFailedRetry();
1511
+ toast.error(
1512
+ rollback.persisted
1513
+ ? t("home.generationStartFailed")
1514
+ : t("settings.saveFailed"),
1515
+ );
1516
+ return;
1517
+ } finally {
1518
+ window.removeEventListener(
1519
+ NEW_DECK_GENERATION_SUBMIT_TARGET_EVENT,
1520
+ rememberRetrySubmitTarget,
1521
+ );
1522
+ window.removeEventListener(
1523
+ AGENT_CHAT_SUBMIT_RESULT_EVENT,
1524
+ rememberConfirmedDelivery,
1525
+ );
1526
+ }
1527
+ if (!submission.delivered) {
1528
+ const rollback = await restoreFailedRetry();
1529
+ toast.error(
1530
+ rollback.persisted
1531
+ ? t("home.generationStartFailed")
1532
+ : t("settings.saveFailed"),
1533
+ );
1534
+ return;
1535
+ }
1536
+ acceptedRecoverySerialized ??= storeRecovery(acceptedRecovery);
1537
+ if (!acceptedRecoverySerialized) {
1538
+ if (
1539
+ clearEmptyGenerationRecovery(
1540
+ retryRecoveryStorageKey,
1541
+ rollbackRecoverySerialized,
1542
+ )
1543
+ ) {
1544
+ emptyGenerationRecoveryRef.current = null;
1545
+ }
1546
+ toast.error(t("settings.saveFailed"));
1547
+ return;
1548
+ }
1549
+ trackEvent("generation_started", {
1550
+ app_name: "slides",
1551
+ template_name: "slides",
1552
+ generation_attempt_id: retryAttemptId,
1553
+ output_id: id,
1554
+ output_type: "deck",
1555
+ source: "empty_output_retry",
1556
+ });
1557
+ updateDeck(id, {
1558
+ generationContext: {
1559
+ ...retryContext,
1560
+ generationFailureCode: null,
1561
+ generationFailureAttemptId: null,
1562
+ },
1563
+ });
1564
+ try {
1565
+ await flushDeckSave(id);
1566
+ clearEmptyGenerationRecovery(
1567
+ retryRecoveryStorageKey,
1568
+ acceptedRecoverySerialized,
1569
+ );
1570
+ emptyGenerationRecoveryRef.current = null;
1571
+ } catch {
1572
+ toast.error(t("settings.saveFailed"));
1573
+ }
1574
+ } finally {
1575
+ retryEmptyGenerationInFlightRef.current = false;
1576
+ setRetryEmptyGenerationPending(false);
1577
+ }
1578
+ }, [
1579
+ canEdit,
1580
+ generationAttemptId,
1581
+ generationContext,
1582
+ generationLifecycleOwnedByEditor,
1583
+ id,
1584
+ retryRecoveryStorageKey,
1585
+ searchParams,
1586
+ setSearchParams,
1587
+ submitGenerationAttemptAndConfirm,
1588
+ updateDeck,
1589
+ flushDeckSave,
1590
+ t,
1591
+ ]);
1592
+
1042
1593
  useEffect(() => {
1043
1594
  if (
1044
1595
  !generationLifecycleOwnedByEditor ||
@@ -1118,12 +1669,6 @@ export default function DeckEditor() {
1118
1669
  });
1119
1670
  };
1120
1671
  }, [generationAttemptId, generationLifecycleOwnedByEditor, id, slideCount]);
1121
- // Mirror Google Slides: viewers see the editor shell with edit affordances
1122
- // disabled (rather than a separate "viewer" route). Owners/Editors/Admins
1123
- // get the full editor. Only assume edit access while the role is still
1124
- // loading when `createdByMe` already confirms ownership — otherwise a
1125
- // viewer would briefly see (and could click) edit affordances.
1126
- const { canEdit, canComment } = useDeckRole(id, deck?.createdByMe === true);
1127
1672
  const fallbackCommentSlideId = deck?.slides[0]?.id ?? null;
1128
1673
  const openCommentComposer = useCallback(
1129
1674
  (
@@ -1524,7 +2069,7 @@ export default function DeckEditor() {
1524
2069
  !id ||
1525
2070
  !submitMessageId ||
1526
2071
  !shouldClearNewDeckGenerationRun({
1527
- generating: newDeckGenerationGenerating,
2072
+ generating: newDeckGenerationGenerating || newDeckGenerationSignal,
1528
2073
  waitingOnQuestions: waitingOnNewDeckQuestions,
1529
2074
  phase: newDeckGenerationPhase,
1530
2075
  })
@@ -1556,6 +2101,7 @@ export default function DeckEditor() {
1556
2101
  }, [
1557
2102
  id,
1558
2103
  newDeckGenerationGenerating,
2104
+ newDeckGenerationSignal,
1559
2105
  newDeckGenerationPhase,
1560
2106
  refetchPendingQuestion,
1561
2107
  searchParams,
@@ -3436,9 +3982,24 @@ export default function DeckEditor() {
3436
3982
  )}
3437
3983
 
3438
3984
  {!generatingSlideSelected &&
3439
- generatingSlideVisible &&
3440
3985
  deck.slides.length === 0 &&
3441
- !showQuestionFlow && (
3986
+ !showQuestionFlow &&
3987
+ (generationFailed ? (
3988
+ <div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
3989
+ <div
3990
+ className="m-auto flex max-w-md flex-col items-center gap-4 text-center"
3991
+ role="alert"
3992
+ >
3993
+ <p>{t("deckEditor.deckHasNoSlides")}</p>
3994
+ <Button
3995
+ disabled={!canEdit || generationRetryPending}
3996
+ onClick={() => void retryEmptyGeneration()}
3997
+ >
3998
+ {t("deckEditor.tryAgain")}
3999
+ </Button>
4000
+ </div>
4001
+ </div>
4002
+ ) : generatingSlideVisible ? (
3442
4003
  <div className="flex min-h-0 flex-1 overflow-auto bg-[var(--slides-editor-surface)] p-4 md:p-8">
3443
4004
  <div className="m-auto w-full max-w-6xl">
3444
4005
  <GeneratingSlidePreview
@@ -3448,7 +4009,7 @@ export default function DeckEditor() {
3448
4009
  />
3449
4010
  </div>
3450
4011
  </div>
3451
- )}
4012
+ ) : null)}
3452
4013
 
3453
4014
  {showCurrentSlideEditor && currentSlide && (
3454
4015
  <SlideEditor