@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +12 -0
- package/corpus/templates/analytics/app/global.css +5 -1
- package/corpus/templates/analytics/changelog/2026-09-25-chat-composer-background.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/calendar/app/root.tsx +7 -7
- package/corpus/templates/calendar/app/routes/meet.$username.tsx +8 -0
- package/corpus/templates/calendar/changelog/2026-09-25-booking-links-without-a-meeting-type-now-show-a-not-found-pa.md +6 -0
- package/corpus/templates/clips/CHANGELOG.md +4 -0
- package/corpus/templates/content/app/root.tsx +0 -7
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-design.ts +9 -9
- package/corpus/templates/mail/CHANGELOG.md +13 -0
- package/corpus/templates/mail/app/root.tsx +1 -16
- package/corpus/templates/slides/CHANGELOG.md +8 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +76 -0
- package/corpus/templates/slides/app/hooks/use-new-deck-generation.ts +16 -4
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +591 -30
- package/corpus/templates/slides/changelog/2026-09-25-empty-decks-keep-their-retry-option-when-a-retry-cannot-be-s.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-prevent-setup-generation-while-figma-indexing-is-in-progress.md +6 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/ErrorBoundary.js +2 -14
- package/dist/client/ErrorReportActions.js +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/org/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- 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 [
|
|
848
|
-
string
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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 =
|
|
884
|
-
generationAttemptId,
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
:
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
? "
|
|
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
|