@agent-native/core 0.146.2 → 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.
@@ -96,7 +96,10 @@ interface EditorToolbarProps {
96
96
  onShowHistory: () => void;
97
97
  historyButtonRef: React.RefObject<HTMLButtonElement | null>;
98
98
  currentSlide?: Slide;
99
- onUpdateSlide?: (updates: Partial<Omit<Slide, "id">>) => void;
99
+ onUpdateSlide?: (
100
+ updates: Partial<Omit<Slide, "id">>,
101
+ slideIdOverride?: string,
102
+ ) => void;
100
103
  /** Active users on the current slide (from collab awareness) */
101
104
  activeUsers?: CollabUser[];
102
105
  /** Whether the agent has a durable presence entry on this slide */
@@ -352,7 +355,7 @@ export default function EditorToolbar({
352
355
  // That row rides on SlideEditor, which only mounts for a real slide, so an
353
356
  // empty deck must keep this fallback or it has no way to add one.
354
357
  const contextToolbarVisible = canEdit && Boolean(currentSlide);
355
- const { undo, redo, canUndo, canRedo } = useDecks();
358
+ const { undo, redo, canUndo, canRedo, getDeck } = useDecks();
356
359
  const fileInputRef = useRef<HTMLInputElement>(null);
357
360
  const [importing, setImporting] = useState(false);
358
361
  const { setTheme, resolvedTheme } = useTheme();
@@ -553,7 +556,7 @@ export default function EditorToolbar({
553
556
  >
554
557
  <div className="py-1.5">
555
558
  {/* Layout section */}
556
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
559
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
557
560
  {t("editorToolbar.layout")}
558
561
  </div>
559
562
  {slideLayoutOptions.map((opt) => (
@@ -573,7 +576,7 @@ export default function EditorToolbar({
573
576
 
574
577
  {/* Background section */}
575
578
  <div className="mx-2 my-1.5 border-t border-border" />
576
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
579
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
577
580
  {t("editorToolbar.background")}
578
581
  </div>
579
582
  <div className="px-3 pb-2">
@@ -619,7 +622,7 @@ export default function EditorToolbar({
619
622
 
620
623
  {/* Image & Assets section */}
621
624
  <div className="mx-2 my-1.5 border-t border-border" />
622
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
625
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
623
626
  {t("editorToolbar.media")}
624
627
  </div>
625
628
  <button
@@ -647,27 +650,46 @@ export default function EditorToolbar({
647
650
 
648
651
  {/* Diagrams section */}
649
652
  <div className="mx-2 my-1.5 border-t border-border" />
650
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
653
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
651
654
  {t("editorToolbar.diagrams")}
652
655
  </div>
653
656
  <button
654
- onClick={() => {
657
+ onClick={async () => {
655
658
  if (!onUpdateSlide || !currentSlide) return;
656
- const mermaidTemplate = `<div class="fmd-slide" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:60px 80px;font-family:'Poppins',sans-serif;">
657
- <div class="mermaid">
658
- graph TD
659
+ // Pin the slide this insert targets before the async
660
+ // import/parse below, so navigating to another slide in the
661
+ // meantime can't redirect where the diagram lands.
662
+ const targetSlideId = currentSlide.id;
663
+ const defaultMermaidDefinition = `graph TD
659
664
  A[Start] --> B{Decision}
660
665
  B -->|Yes| C[Action A]
661
666
  B -->|No| D[Action B]
662
667
  C --> E[End]
663
- D --> E
664
- </div>
665
- </div>`;
666
- onUpdateSlide({
667
- content: mermaidTemplate,
668
- layout: "blank",
669
- });
670
- setLayoutOpen(false);
668
+ D --> E`;
669
+ try {
670
+ const { convertMermaidToExcalidraw } =
671
+ await import("./MermaidToExcalidrawPanel");
672
+ const data = await convertMermaidToExcalidraw(
673
+ defaultMermaidDefinition,
674
+ );
675
+ // The target slide may have been deleted while the import/
676
+ // parse above was in flight — patch-deck rejects the whole
677
+ // batch for a missing slide id, so re-check with the live
678
+ // deck (not the stale `currentSlide` this closure captured)
679
+ // and drop the result instead of enqueueing a dead op.
680
+ const stillExists = getDeck(deckId)?.slides.some(
681
+ (s) => s.id === targetSlideId,
682
+ );
683
+ if (!stillExists) return;
684
+ onUpdateSlide({ excalidrawData: data }, targetSlideId);
685
+ setLayoutOpen(false);
686
+ } catch (err) {
687
+ console.error("Insert Mermaid diagram failed:", err);
688
+ toast.error(t("editorToolbar.insertMermaidFailed"), {
689
+ description:
690
+ err instanceof Error ? err.message : undefined,
691
+ });
692
+ }
671
693
  }}
672
694
  className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
673
695
  >
@@ -696,6 +718,8 @@ graph TD
696
718
  <button
697
719
  onClick={async () => {
698
720
  if (!onUpdateSlide || !currentSlide) return;
721
+ // Same pinning as the insert handler above.
722
+ const targetSlideId = currentSlide.id;
699
723
  try {
700
724
  const match = currentSlide.content.match(
701
725
  /<div\s+class="mermaid"[^>]*>([\s\S]*?)<\/div>/i,
@@ -706,13 +730,22 @@ graph TD
706
730
  const data = await convertMermaidToExcalidraw(
707
731
  match[1].trim(),
708
732
  );
709
- onUpdateSlide({ excalidrawData: data });
733
+ // Same stale-target guard as the insert handler above.
734
+ const stillExists = getDeck(deckId)?.slides.some(
735
+ (s) => s.id === targetSlideId,
736
+ );
737
+ if (!stillExists) return;
738
+ onUpdateSlide({ excalidrawData: data }, targetSlideId);
710
739
  setLayoutOpen(false);
711
- } catch (err: any) {
740
+ } catch (err) {
712
741
  console.error("Mermaid to Excalidraw failed:", err);
742
+ toast.error(t("editorToolbar.convertMermaidFailed"), {
743
+ description:
744
+ err instanceof Error ? err.message : undefined,
745
+ });
713
746
  }
714
747
  }}
715
- className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-[hsl(var(--accent-cyan))]/80 hover:text-[hsl(var(--accent-cyan))] hover:bg-accent/50 transition-colors"
748
+ className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-[hsl(var(--accent-cyan))] hover:bg-accent/50 transition-colors"
716
749
  >
717
750
  <IconTransform className="w-3 h-3" />
718
751
  {t("editorToolbar.convertMermaidToExcalidraw")}
@@ -734,7 +767,7 @@ graph TD
734
767
 
735
768
  {/* Transitions section */}
736
769
  <div className="mx-2 my-1.5 border-t border-border" />
737
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
770
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
738
771
  {t("editorToolbar.transition")}
739
772
  </div>
740
773
  <div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
@@ -767,7 +800,7 @@ graph TD
767
800
  {onSetAspectRatio && (
768
801
  <>
769
802
  <div className="mx-2 my-1.5 border-t border-border" />
770
- <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
803
+ <div className="px-3 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
771
804
  {t("editorToolbar.aspectRatio")}
772
805
  </div>
773
806
  <div className="px-3 pb-2.5 grid grid-cols-4 gap-1">
@@ -4409,6 +4409,7 @@ export default function SlideEditor({
4409
4409
 
4410
4410
  <DrawOverlay
4411
4411
  visible={!!drawMode}
4412
+ scopeKey={slideId || slide.id}
4412
4413
  onClose={() => onExitDrawMode?.()}
4413
4414
  onSend={(annotations, instruction, canvasSize) => {
4414
4415
  const summary = annotations
@@ -31,7 +31,10 @@ export function SlideOverflowWarning({
31
31
  <div
32
32
  role="status"
33
33
  aria-live="polite"
34
- className="absolute -top-12 left-0 z-20 flex items-center gap-2 rounded-md border border-foreground/40 px-2 py-1 text-xs text-foreground"
34
+ // w-max/nowrap: the containing block is the zoomed slide canvas, so at
35
+ // low zoom the banner would otherwise wrap to a height taller than its
36
+ // own -top-12 offset and spill down over the slide.
37
+ className="absolute -top-12 left-0 z-20 flex w-max items-center gap-2 whitespace-nowrap rounded-md border border-foreground/40 px-2 py-1 text-xs text-foreground"
35
38
  onPointerDown={(event) => event.stopPropagation()}
36
39
  onClick={(event) => event.stopPropagation()}
37
40
  >
@@ -143,6 +143,27 @@ export type DeckPersistenceResult =
143
143
  | { persisted: false; reason: "request-failed"; error: unknown }
144
144
  | { persisted: false; reason: "not-found" };
145
145
 
146
+ /**
147
+ * Ask the server whether a deck row exists, keeping "absent" and "could not
148
+ * check" distinct — a rejected write is not by itself proof the row is missing.
149
+ */
150
+ async function probeDeckPersisted(id: string): Promise<DeckPersistenceResult> {
151
+ try {
152
+ const result = await callAction<unknown>(
153
+ "get-deck",
154
+ { id },
155
+ {
156
+ method: "GET",
157
+ },
158
+ );
159
+ return normalizeActionDeck(result)
160
+ ? { persisted: true }
161
+ : { persisted: false, reason: "not-found" };
162
+ } catch (error) {
163
+ return { persisted: false, reason: "request-failed", error };
164
+ }
165
+ }
166
+
146
167
  export function describeDeckPersistenceFailure(
147
168
  result: DeckPersistenceResult,
148
169
  fallback: string,
@@ -178,6 +199,7 @@ interface DeckContextType {
178
199
  sourceDeckId: string,
179
200
  newId: string,
180
201
  title?: string,
202
+ onFailure?: () => void,
181
203
  ) => Deck | null;
182
204
  deleteDeck: (id: string) => void;
183
205
  updateDeck: (
@@ -851,18 +873,52 @@ async function fetchDeckListLightFromAPI(): Promise<{ id: string }[] | null> {
851
873
  }
852
874
  }
853
875
 
876
+ const DECK_FETCH_RETRY_ATTEMPTS = 3;
877
+ const DECK_FETCH_RETRY_DELAY_MS = 400;
878
+
879
+ // `get-deck` returns a real 404/403 only when the deck is genuinely gone or
880
+ // the caller genuinely lacks access (see actions/get-deck.ts). A network blip
881
+ // or 5xx is transient and must not be coerced into the same "not found" null
882
+ // the caller uses to show the owner-facing "deck unavailable" pane — that
883
+ // flashed a wrong message on brief server hiccups even though the deck still
884
+ // existed.
885
+ function isConfirmedDeckAbsence(err: unknown): boolean {
886
+ const status = (err as { status?: unknown } | null)?.status;
887
+ return status === 404 || status === 403;
888
+ }
889
+
890
+ // A timeout already made the caller wait the full action timeout window once
891
+ // (see DEFAULT_ACTION_TIMEOUT_MS in use-action.ts); retrying would multiply
892
+ // that wait instead of surfacing the failure — same reasoning as
893
+ // `isActionTimeout` in the shared action-query retry policy.
894
+ function isDeckFetchTimeout(err: unknown): boolean {
895
+ return (err as { timedOut?: unknown } | null)?.timedOut === true;
896
+ }
897
+
854
898
  async function fetchDeckFromAPI(id: string): Promise<Deck | null> {
855
- try {
856
- const result = await callAction<unknown>(
857
- "get-deck",
858
- { id },
859
- { method: "GET" },
860
- );
861
- return normalizeActionDeck(result);
862
- } catch (err) {
863
- console.error(`Failed to fetch deck ${id}:`, err);
864
- return null;
899
+ for (let attempt = 1; attempt <= DECK_FETCH_RETRY_ATTEMPTS; attempt++) {
900
+ try {
901
+ const result = await callAction<unknown>(
902
+ "get-deck",
903
+ { id },
904
+ { method: "GET" },
905
+ );
906
+ return normalizeActionDeck(result);
907
+ } catch (err) {
908
+ if (
909
+ isConfirmedDeckAbsence(err) ||
910
+ isDeckFetchTimeout(err) ||
911
+ attempt === DECK_FETCH_RETRY_ATTEMPTS
912
+ ) {
913
+ console.error(`Failed to fetch deck ${id}:`, err);
914
+ return null;
915
+ }
916
+ await new Promise((resolve) =>
917
+ setTimeout(resolve, attempt * DECK_FETCH_RETRY_DELAY_MS),
918
+ );
919
+ }
865
920
  }
921
+ return null;
866
922
  }
867
923
 
868
924
  export function deckIdFromPathname(pathname: string): string | null {
@@ -1776,24 +1832,18 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1776
1832
  }
1777
1833
  }
1778
1834
 
1779
- try {
1780
- const result = await callAction<unknown>(
1781
- "get-deck",
1782
- { id },
1783
- { method: "GET" },
1784
- );
1785
- return normalizeActionDeck(result)
1786
- ? { persisted: true }
1787
- : { persisted: false, reason: "not-found" };
1788
- } catch (error) {
1789
- return { persisted: false, reason: "request-failed", error };
1790
- }
1835
+ return probeDeckPersisted(id);
1791
1836
  },
1792
1837
  [],
1793
1838
  );
1794
1839
 
1795
1840
  const duplicateDeck = useCallback(
1796
- (sourceDeckId: string, newId: string, title?: string): Deck | null => {
1841
+ (
1842
+ sourceDeckId: string,
1843
+ newId: string,
1844
+ title?: string,
1845
+ onFailure?: () => void,
1846
+ ): Deck | null => {
1797
1847
  if (pendingDuplicateSourceIdsRef.current.has(sourceDeckId)) return null;
1798
1848
  const source = decks.find((d) => d.id === sourceDeckId);
1799
1849
  if (!source) return null;
@@ -1843,10 +1893,26 @@ export function DeckProvider({ children }: { children: ReactNode }) {
1843
1893
  ).then(() => undefined);
1844
1894
  pendingCreatePromisesRef.current.set(newId, duplicatePromise);
1845
1895
  duplicatePromise
1846
- .catch((err) => {
1896
+ .catch(async (err) => {
1897
+ // A rejected request is not proof the row is missing: a timeout or
1898
+ // dropped response can land after the server committed the insert.
1899
+ // Discarding the copy then would delete work that actually exists
1900
+ // and tell the user it failed, so confirm against the server first.
1901
+ const probe = await probeDeckPersisted(newId);
1902
+ if (probe.persisted) {
1903
+ console.warn(
1904
+ `Duplicate request for ${newId} failed but the deck persisted:`,
1905
+ err,
1906
+ );
1907
+ return;
1908
+ }
1847
1909
  console.error("Duplicate failed:", err);
1848
- // Roll back: drop the optimistic deck from local state.
1910
+ // Roll back: drop the optimistic deck from local state. The caller
1911
+ // (via onFailure) is responsible for navigating away if the user is
1912
+ // still sitting on this now-gone deck's route — otherwise they're
1913
+ // stranded on a "Deck unavailable" screen with no way back.
1849
1914
  setDecks((prev) => prev.filter((d) => d.id !== newId));
1915
+ onFailure?.();
1850
1916
  })
1851
1917
  .finally(() => {
1852
1918
  pendingCreateIdsRef.current.delete(newId);
@@ -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
  }
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- ok?: undefined;
17
16
  error?: undefined;
17
+ ok?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- ok?: undefined;
22
21
  error?: undefined;
22
+ ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
37
37
  ok?: undefined;
38
38
  status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
44
  ok?: undefined;
45
45
  error: string;
46
46
  removed?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
58
58
  error: string;
59
59
  note?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -191,6 +191,113 @@ Each card is a `###` heading (optionally linked) followed by the description. Ca
191
191
 
192
192
  ---
193
193
 
194
+ ## Notice
195
+
196
+ <Notice tone="risk" title="Bold, not subtle">
197
+
198
+ Use `Notice` when a `Callout` would be too easy to skim past — something the reader must not miss, like a destructive action or a hard requirement. It shares the same five tones as `Callout` (`info`, `decision`, `risk`, `warning`, `success`) but renders as a filled card with an icon and optional title instead of a left-border note.
199
+
200
+ </Notice>
201
+
202
+ ```mdx
203
+ <Notice tone="risk" title="Bold, not subtle">
204
+
205
+ Use `Notice` when a `Callout` would be too easy to skim past — something the reader must not miss, like a destructive action or a hard requirement.
206
+
207
+ </Notice>
208
+ ```
209
+
210
+ `title` is optional — omit it for a shorter card with just the icon and body. Leave a blank line between the opening tag and the body, same as `Callout`. Reach for `Callout` first; use `Notice` only when a subtle left-border note isn't enough attention.
211
+
212
+ ---
213
+
214
+ ## Banner
215
+
216
+ <Banner
217
+ tone="warning"
218
+ body="This page describes the v9 API. See [Migrating to v10](/docs/migrating-v10) for what changed."
219
+ />
220
+
221
+ ```mdx
222
+ <Banner
223
+ tone="warning"
224
+ body="This page describes the v9 API. See [Migrating to v10](/docs/migrating-v10) for what changed."
225
+ />
226
+ ```
227
+
228
+ `Banner` is self-closing — `body` is a flat attribute (kept short and single-purpose on purpose), not markdown children. Place it right under a page's title or a section heading — deprecated, beta, moved, version-scoped. For content the reader needs to actually read (not just notice), use `Notice` or `Callout` instead.
229
+
230
+ ---
231
+
232
+ ## Accordion
233
+
234
+ <Accordion>
235
+
236
+ ### Do I need Toolkit to use Core?
237
+
238
+ No. Toolkit is optional shared UI — Core works standalone.
239
+
240
+ ### Can I mix Templates and Toolkit?
241
+
242
+ Yes, most first-party templates already do.
243
+
244
+ ### Where do I ask questions?
245
+
246
+ The `#agent-native` Discord channel, linked in the site header.
247
+
248
+ </Accordion>
249
+
250
+ ```mdx
251
+ <Accordion>
252
+
253
+ ### Do I need Toolkit to use Core?
254
+
255
+ No. Toolkit is optional shared UI — Core works standalone.
256
+
257
+ ### Can I mix Templates and Toolkit?
258
+
259
+ Yes, most first-party templates already do.
260
+
261
+ </Accordion>
262
+ ```
263
+
264
+ Same authoring shape as `Steps`/`Cards` — a `### Question` heading per item followed by its answer — but each item renders collapsed behind native `<details>`. Best for FAQ-style content where most readers only need one or two entries, not a full top-to-bottom read.
265
+
266
+ ---
267
+
268
+ ## Badge
269
+
270
+ <Badge label="Beta" color="orange" icon="rocket" />
271
+
272
+ <Badge label="Deprecated" color="red" />
273
+
274
+ <Badge label="v9.1+" color="blue" shape="pill" />
275
+
276
+ <Badge label="Server-only" color="gray" stroke />
277
+
278
+ ```mdx
279
+ <Badge label="Beta" color="orange" icon="rocket" />
280
+
281
+ <Badge label="Deprecated" color="red" />
282
+
283
+ <Badge label="v9.1+" color="blue" shape="pill" />
284
+
285
+ <Badge label="Server-only" color="gray" stroke />
286
+ ```
287
+
288
+ A small status chip for the top of a page or section. Self-closing, block-level only — **not** embeddable mid-sentence (the docs MDX pipeline only recognizes a custom component when its tag opens its own line, so `text <Badge label="x"/> more text` renders as literal text, not a chip). Place it on its own line, typically right after a heading. Leave a blank line between adjacent Badges (or any two block components) — without one, the scanner can't tell where one ends and the next begins and falls back to rendering literal text.
289
+
290
+ | Prop | Values |
291
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
292
+ | `color` | `gray` · `blue` · `green` · `yellow` · `orange` · `red` · `purple` · `white` · `surface` · `white-destructive` · `surface-destructive` |
293
+ | `size` | `xs` · `sm` · `md` · `lg` |
294
+ | `shape` | `rounded` (default) · `pill` |
295
+ | `icon` | one of a curated set — `circle-check`, `circle-info`, `alert-triangle`, `alert-octagon`, `ban`, `clock`, `star`, `flame`, `sparkles`, `lock`, `rocket` |
296
+ | `stroke` | `true` for an outlined, transparent-background variant |
297
+ | `disabled` | `true` to dim it for an inactive/unavailable state |
298
+
299
+ ---
300
+
194
301
  ## Comparison
195
302
 
196
303
  <Comparison>
@@ -478,14 +585,18 @@ Like `TabsBlock`, `Columns` is self-closing with a single JSON `columns` attribu
478
585
 
479
586
  ## When to use which component
480
587
 
481
- | Situation | Component |
482
- | ----------------------------------------- | ------------------------------------ |
483
- | Reader must not miss something | `Callout` (warning / info / success) |
484
- | Sequential procedure with titled steps | `Steps` |
485
- | Navigation or feature overview grid | `Cards` |
486
- | Before vs. after, old vs. new (prose) | `Comparison` |
487
- | Same task in multiple tools or frameworks | `Tabs` |
488
- | Side-by-side code (labeled columns) | `Columns` |
489
- | Project directory structure | `FileTree` |
490
- | Inline keyboard shortcut | `[[Key]]` |
491
- | Architecture or flow diagram | Mermaid fence |
588
+ | Situation | Component |
589
+ | ----------------------------------------------------- | ------------------------------------ |
590
+ | Reader must not miss something, inline in prose | `Callout` (warning / info / success) |
591
+ | Reader must not miss something, standalone | `Notice` |
592
+ | Deprecated/beta/version notice at page or section top | `Banner` |
593
+ | Status or version tag at page or section top | `Badge` |
594
+ | Sequential procedure with titled steps | `Steps` |
595
+ | FAQ-style content, one answer at a time | `Accordion` |
596
+ | Navigation or feature overview grid | `Cards` |
597
+ | Before vs. after, old vs. new (prose) | `Comparison` |
598
+ | Same task in multiple tools or frameworks | `Tabs` |
599
+ | Side-by-side code (labeled columns) | `Columns` |
600
+ | Project directory structure | `FileTree` |
601
+ | Inline keyboard shortcut | `[[Key]]` |
602
+ | Architecture or flow diagram | Mermaid fence |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.146.2",
3
+ "version": "0.146.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {