@agent-native/core 0.146.1 → 0.146.3

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.
@@ -14,10 +14,18 @@ interface SharesResponse {
14
14
  * `Viewer` = no edit affordances, but the editor shell is still navigable;
15
15
  * any other role (Owner / Editor / Admin) gets full editing.
16
16
  *
17
- * Returns `canEdit = true` while the role is still loading so that owners
18
- * never see a flash of view-only chrome on first paint.
17
+ * `assumeEditorWhileLoading` should only be true when a faster, already-known
18
+ * signal (e.g. `deck.createdByMe`) confirms the caller is the owner — that
19
+ * avoids a flash of view-only chrome for owners without ever defaulting a
20
+ * viewer into edit affordances while the real role is still loading. Without
21
+ * that signal, `canEdit` stays false until the role query resolves, so a
22
+ * viewer can never trigger an edit that the server will reject and silently
23
+ * roll back.
19
24
  */
20
- export function useDeckRole(deckId: string | undefined): {
25
+ export function useDeckRole(
26
+ deckId: string | undefined,
27
+ assumeEditorWhileLoading = false,
28
+ ): {
21
29
  role: SharesResponse["role"] | undefined;
22
30
  canEdit: boolean;
23
31
  isLoading: boolean;
@@ -29,6 +37,8 @@ export function useDeckRole(deckId: string | undefined): {
29
37
  );
30
38
  const role = query.data?.role;
31
39
  const canEdit =
32
- role === undefined ? true : role === "owner" || role !== "viewer";
40
+ role === undefined
41
+ ? assumeEditorWhileLoading
42
+ : role === "owner" || role !== "viewer";
33
43
  return { role, canEdit, isLoading: query.isLoading };
34
44
  }
@@ -212,8 +212,10 @@ const messages = {
212
212
  assetLibrary: "Asset Library",
213
213
  diagrams: "Diagrams",
214
214
  insertMermaidDiagram: "Insert Mermaid Diagram",
215
+ insertMermaidFailed: "Failed to insert diagram",
215
216
  excalidrawCanvas: "Excalidraw Canvas",
216
217
  convertMermaidToExcalidraw: "Convert Mermaid to Excalidraw",
218
+ convertMermaidFailed: "Failed to convert diagram",
217
219
  removeExcalidrawCanvas: "Remove Excalidraw Canvas",
218
220
  transition: "Transition",
219
221
  transition_instant: "Instant",
@@ -44,7 +44,7 @@ import { QuestionFlow } from "@/components/editor/QuestionFlow";
44
44
  import SlideEditor from "@/components/editor/SlideEditor";
45
45
  import { TweaksPanel } from "@/components/editor/TweaksPanel";
46
46
  import { Button } from "@/components/ui/button";
47
- import { useDecks } from "@/context/DeckContext";
47
+ import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
48
48
  import { useAgentGenerating } from "@/hooks/use-agent-generating";
49
49
  import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
50
50
  import { useDeckPresence } from "@/hooks/use-deck-presence";
@@ -275,8 +275,10 @@ export default function DeckEditor() {
275
275
  });
276
276
  // Mirror Google Slides: viewers see the editor shell with edit affordances
277
277
  // disabled (rather than a separate "viewer" route). Owners/Editors/Admins
278
- // get the full editor.
279
- const { canEdit } = useDeckRole(id);
278
+ // get the full editor. Only assume edit access while the role is still
279
+ // loading when `createdByMe` already confirms ownership — otherwise a
280
+ // viewer would briefly see (and could click) edit affordances.
281
+ const { canEdit } = useDeckRole(id, deck?.createdByMe === true);
280
282
  const isNewDeckGenerating = shouldShowNewDeckGeneratingProgress({
281
283
  generating,
282
284
  isNewDeckCreation: wasNewDeckCreation.current,
@@ -1001,9 +1003,10 @@ export default function DeckEditor() {
1001
1003
  onShowHistory={() => setHistoryOpen(!historyOpen)}
1002
1004
  historyButtonRef={historyButtonRef}
1003
1005
  currentSlide={currentSlide}
1004
- onUpdateSlide={(updates) =>
1005
- currentSlide && updateSlide(id, currentSlide.id, updates)
1006
- }
1006
+ onUpdateSlide={(updates, slideIdOverride) => {
1007
+ const targetId = slideIdOverride ?? currentSlide?.id;
1008
+ if (targetId) updateSlide(id, targetId, updates);
1009
+ }}
1007
1010
  activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
1008
1011
  agentPresent={agentPresent}
1009
1012
  agentActive={agentActive}
@@ -1025,7 +1028,16 @@ export default function DeckEditor() {
1025
1028
  onToggleTextBoxMode={toggleTextBoxMode}
1026
1029
  onDuplicateDeck={() => {
1027
1030
  const newId = `deck-${nanoid()}`;
1028
- const optimistic = duplicateDeck(id, newId);
1031
+ const optimistic = duplicateDeck(id, newId, undefined, () => {
1032
+ // The background duplicate-deck action failed after we already
1033
+ // navigated to the optimistic copy. If the user is still there,
1034
+ // send them back instead of stranding them on a "Deck
1035
+ // unavailable" screen for a deck that no longer exists.
1036
+ if (deckIdFromPathname(window.location.pathname) === newId) {
1037
+ navigate("/");
1038
+ }
1039
+ toast.error(t("home.duplicateFailed"));
1040
+ });
1029
1041
  if (optimistic) navigate(`/deck/${optimistic.id}`);
1030
1042
  }}
1031
1043
  onExportPdf={async () => {
@@ -45,7 +45,7 @@ import {
45
45
  describeDeckPersistenceFailure,
46
46
  type Deck,
47
47
  } from "@/context/DeckContext";
48
- import { useDecks } from "@/context/DeckContext";
48
+ import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
49
49
  import { useAgentGenerating } from "@/hooks/use-agent-generating";
50
50
  import { useDesignSystems } from "@/hooks/use-design-systems";
51
51
  import { useWorkspaceDefaults } from "@/hooks/use-workspace-defaults";
@@ -1088,7 +1088,16 @@ export default function Index() {
1088
1088
  (id: string) => {
1089
1089
  let copy: ReturnType<typeof duplicateDeck> | undefined;
1090
1090
  flushSync(() => {
1091
- copy = duplicateDeck(id, `deck-${nanoid()}`);
1091
+ copy = duplicateDeck(id, `deck-${nanoid()}`, undefined, () => {
1092
+ // The background duplicate-deck action failed after we already
1093
+ // navigated to the optimistic copy's route. If the user is still
1094
+ // there, send them back to the deck list instead of stranding them
1095
+ // on a "Deck unavailable" screen for a deck that no longer exists.
1096
+ if (deckIdFromPathname(window.location.pathname) === copy?.id) {
1097
+ navigate("/");
1098
+ }
1099
+ toast.error(t("home.duplicateFailed"));
1100
+ });
1092
1101
  });
1093
1102
  // The context refuses a second copy of the same deck while the first
1094
1103
  // one's action is still in flight.
@@ -97,7 +97,11 @@ function useExitSelectionOnOutsideClick() {
97
97
  const target = e.target as HTMLElement;
98
98
  if (
99
99
  target.closest(".slide-content") ||
100
- target.closest(".slide-image-clickable")
100
+ target.closest(".slide-image-clickable") ||
101
+ target.closest("[data-slide-context-toolbar]") ||
102
+ target.closest(
103
+ '[role="dialog"], [role="menu"], [role="listbox"], [data-radix-popper-content-wrapper], [data-radix-menu-content]',
104
+ )
101
105
  ) {
102
106
  return;
103
107
  }
@@ -53,6 +53,7 @@
53
53
  "nanoid": "^5.1.9",
54
54
  "p-limit": "^7.3.0",
55
55
  "pdf-parse": "^2.4.5",
56
+ "pdfjs-dist": "5.4.296",
56
57
  "pptxgenjs": "4.0.1",
57
58
  "react-markdown": "^10.1.0",
58
59
  "rehype-raw": "^7.0.0",
@@ -334,9 +334,18 @@ function formatFidelityRun(
334
334
  `font-style:${run.italic ? "italic" : "normal"}`,
335
335
  `text-decoration:${run.underline ? "underline" : "none"}`,
336
336
  ].join(";");
337
+ const href = run.href && isSafeLinkHref(run.href) ? run.href : undefined;
338
+ if (href) {
339
+ return `<a href="${esc(href)}" target="_blank" rel="noopener noreferrer" style="${styles};">${esc(run.content)}</a>`;
340
+ }
337
341
  return `<span style="${styles};">${esc(run.content)}</span>`;
338
342
  }
339
343
 
344
+ /** A source PDF/PPTX link annotation is untrusted input — only render schemes a browser treats as navigation, never `javascript:`/`data:`/etc. */
345
+ function isSafeLinkHref(href: string): boolean {
346
+ return /^(https?:|mailto:)/i.test(href);
347
+ }
348
+
340
349
  function fontWeightForFamily(
341
350
  fontFamily: string | undefined,
342
351
  fallback: number,