@agent-native/core 0.146.2 → 0.146.4

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 (213) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +13 -10
  3. package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +7 -3
  4. package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  5. package/corpus/templates/analytics/actions/compose-dashboard.ts +53 -6
  6. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +8 -2
  7. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +3 -1
  8. package/corpus/templates/analytics/actions/mutate-dashboard.ts +48 -9
  9. package/corpus/templates/analytics/actions/update-dashboard.ts +80 -6
  10. package/corpus/templates/analytics/changelog/2026-08-07-dashboard-creation-and-edits-now-save-faster-with-reliable-s.md +6 -0
  11. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +19 -19
  12. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +21 -2
  13. package/corpus/templates/analytics/server/lib/bigquery.ts +59 -24
  14. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +44 -8
  15. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
  16. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +33 -69
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +90 -24
  18. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -1
  19. package/corpus/templates/analytics/server/plugins/agent-chat.ts +17 -2
  20. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +2 -0
  21. package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +7 -3
  22. package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  23. package/corpus/templates/assets/netlify.toml +1 -1
  24. package/corpus/templates/assets/package.json +1 -0
  25. package/corpus/templates/assets/scripts/migrate-production.ts +24 -0
  26. package/corpus/templates/assets/server/plugins/db.ts +1 -1
  27. package/corpus/templates/assets/tsconfig.json +1 -0
  28. package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +7 -3
  29. package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  30. package/corpus/templates/brain/netlify.toml +1 -1
  31. package/corpus/templates/brain/package.json +2 -0
  32. package/corpus/templates/brain/scripts/migrate-production.ts +24 -0
  33. package/corpus/templates/brain/server/plugins/db.ts +1 -1
  34. package/corpus/templates/brain/tsconfig.json +1 -0
  35. package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +7 -3
  36. package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  37. package/corpus/templates/calendar/netlify.toml +1 -1
  38. package/corpus/templates/calendar/package.json +1 -0
  39. package/corpus/templates/calendar/scripts/migrate-production.ts +24 -0
  40. package/corpus/templates/calendar/server/plugins/db.ts +1 -1
  41. package/corpus/templates/calendar/tsconfig.json +1 -0
  42. package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  43. package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  44. package/corpus/templates/chat/netlify.toml +1 -1
  45. package/corpus/templates/chat/package.json +1 -0
  46. package/corpus/templates/chat/scripts/migrate-production.ts +31 -0
  47. package/corpus/templates/chat/tsconfig.json +1 -0
  48. package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +7 -3
  49. package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  50. package/corpus/templates/clips/netlify.toml +1 -1
  51. package/corpus/templates/clips/package.json +1 -0
  52. package/corpus/templates/clips/scripts/migrate-production.ts +24 -0
  53. package/corpus/templates/clips/server/plugins/db.ts +1 -1
  54. package/corpus/templates/clips/tsconfig.json +1 -0
  55. package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +7 -3
  56. package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  57. package/corpus/templates/content/netlify.toml +1 -1
  58. package/corpus/templates/content/package.json +2 -0
  59. package/corpus/templates/content/scripts/migrate-production.ts +28 -0
  60. package/corpus/templates/content/server/plugins/db.ts +2 -2
  61. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  62. package/corpus/templates/crm/netlify.toml +1 -1
  63. package/corpus/templates/crm/package.json +2 -0
  64. package/corpus/templates/crm/scripts/migrate-production.ts +24 -0
  65. package/corpus/templates/crm/server/plugins/db.ts +1 -1
  66. package/corpus/templates/crm/tsconfig.json +1 -0
  67. package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +7 -3
  68. package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  69. package/corpus/templates/design/netlify.toml +1 -1
  70. package/corpus/templates/design/package.json +1 -0
  71. package/corpus/templates/design/scripts/migrate-production.ts +24 -0
  72. package/corpus/templates/design/server/plugins/db.ts +1 -1
  73. package/corpus/templates/design/tsconfig.json +1 -0
  74. package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +7 -3
  75. package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  76. package/corpus/templates/dispatch/netlify.toml +1 -1
  77. package/corpus/templates/dispatch/package.json +1 -0
  78. package/corpus/templates/dispatch/scripts/migrate-production.ts +23 -0
  79. package/corpus/templates/dispatch/tsconfig.json +1 -0
  80. package/corpus/templates/factory/.agents/skills/self-modifying-code/SKILL.md +7 -3
  81. package/corpus/templates/factory/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  82. package/corpus/templates/factory/netlify.toml +1 -1
  83. package/corpus/templates/factory/package.json +1 -0
  84. package/corpus/templates/factory/scripts/migrate-production.ts +31 -0
  85. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -0
  86. package/corpus/templates/factory/tsconfig.json +1 -0
  87. package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +7 -3
  88. package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  89. package/corpus/templates/forms/netlify.toml +1 -1
  90. package/corpus/templates/forms/package.json +2 -0
  91. package/corpus/templates/forms/scripts/migrate-production.ts +24 -0
  92. package/corpus/templates/forms/server/plugins/db.ts +1 -1
  93. package/corpus/templates/forms/tsconfig.json +1 -0
  94. package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  95. package/corpus/templates/macros/netlify.toml +1 -1
  96. package/corpus/templates/macros/package.json +1 -0
  97. package/corpus/templates/macros/scripts/migrate-production.ts +24 -0
  98. package/corpus/templates/macros/server/plugins/db.ts +1 -1
  99. package/corpus/templates/macros/tsconfig.json +1 -0
  100. package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +7 -3
  101. package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  102. package/corpus/templates/mail/netlify.toml +1 -1
  103. package/corpus/templates/mail/package.json +1 -0
  104. package/corpus/templates/mail/scripts/migrate-production.ts +24 -0
  105. package/corpus/templates/mail/server/plugins/db.ts +1 -1
  106. package/corpus/templates/mail/tsconfig.json +1 -0
  107. package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +7 -3
  108. package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  109. package/corpus/templates/plan/netlify.toml +1 -1
  110. package/corpus/templates/plan/package.json +1 -0
  111. package/corpus/templates/plan/scripts/migrate-production.ts +24 -0
  112. package/corpus/templates/plan/server/plugins/00-mcp.ts +3 -2
  113. package/corpus/templates/plan/server/plugins/db.ts +1 -1
  114. package/corpus/templates/plan/tsconfig.json +1 -0
  115. package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +7 -3
  116. package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  117. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +56 -23
  118. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
  119. package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +4 -1
  120. package/corpus/templates/slides/app/context/DeckContext.tsx +91 -25
  121. package/corpus/templates/slides/app/hooks/use-deck-role.ts +14 -4
  122. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  123. package/corpus/templates/slides/app/pages/DeckEditor.tsx +19 -7
  124. package/corpus/templates/slides/app/pages/Index.tsx +11 -2
  125. package/corpus/templates/slides/app/root.tsx +5 -1
  126. package/corpus/templates/slides/netlify.toml +1 -1
  127. package/corpus/templates/slides/package.json +1 -0
  128. package/corpus/templates/slides/scripts/migrate-production.ts +24 -0
  129. package/corpus/templates/slides/server/plugins/db.ts +1 -1
  130. package/corpus/templates/slides/tsconfig.json +1 -0
  131. package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +7 -3
  132. package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  133. package/corpus/templates/tasks/netlify.toml +1 -1
  134. package/corpus/templates/tasks/package.json +1 -0
  135. package/corpus/templates/tasks/scripts/migrate-production.ts +24 -0
  136. package/corpus/templates/tasks/server/plugins/db.ts +1 -1
  137. package/corpus/templates/tasks/tsconfig.json +1 -0
  138. package/dist/action.d.ts +2 -0
  139. package/dist/agent/production-agent.d.ts +1 -0
  140. package/dist/agent/production-agent.js +2 -0
  141. package/dist/agent/thread-data-builder.d.ts +4 -0
  142. package/dist/agent/thread-data-builder.js +44 -0
  143. package/dist/automations/service.d.ts +1 -0
  144. package/dist/automations/service.js +21 -2
  145. package/dist/cli/create.js +20 -2
  146. package/dist/client/agent-page/AgentJobsTab.js +3 -1
  147. package/dist/client/agent-page/AutomationDetailsDialog.js +9 -2
  148. package/dist/collab/awareness.d.ts +2 -2
  149. package/dist/collab/struct-routes.d.ts +1 -1
  150. package/dist/db/migrations.js +7 -13
  151. package/dist/jobs/actions/list-automation-runs.d.ts +1 -0
  152. package/dist/jobs/actions/list-automation-runs.js +9 -2
  153. package/dist/jobs/actions/list-recurring-jobs.js +3 -1
  154. package/dist/jobs/actions/manage-recurring-job.js +1 -1
  155. package/dist/jobs/actions/run-automation-now.js +1 -0
  156. package/dist/jobs/background-automation-runner.js +2 -0
  157. package/dist/jobs/frontmatter.d.ts +2 -0
  158. package/dist/jobs/frontmatter.js +4 -0
  159. package/dist/jobs/run-history.d.ts +4 -0
  160. package/dist/jobs/run-history.js +28 -8
  161. package/dist/jobs/run-now.d.ts +4 -1
  162. package/dist/jobs/run-now.js +4 -3
  163. package/dist/jobs/scheduler.js +18 -0
  164. package/dist/jobs/tools.d.ts +3 -2
  165. package/dist/jobs/tools.js +26 -12
  166. package/dist/localization/default-messages.d.ts +1 -0
  167. package/dist/localization/default-messages.js +1 -0
  168. package/dist/observability/routes.d.ts +3 -3
  169. package/dist/secrets/routes.d.ts +6 -6
  170. package/dist/server/action-routes.d.ts +2 -0
  171. package/dist/server/action-routes.js +1 -0
  172. package/dist/server/agent-chat-plugin.js +3 -2
  173. package/dist/server/builder-browser.js +24 -8
  174. package/dist/server/transcribe-voice.d.ts +1 -1
  175. package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  176. package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  177. package/dist/templates/chat/netlify.toml +1 -1
  178. package/dist/templates/chat/package.json +1 -0
  179. package/dist/templates/chat/scripts/migrate-production.ts +31 -0
  180. package/dist/templates/chat/tsconfig.json +1 -0
  181. package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  182. package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  183. package/dist/templates/default/package.json +1 -0
  184. package/dist/templates/default/scripts/migrate-production.ts +31 -0
  185. package/dist/templates/default/tsconfig.json +1 -0
  186. package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  187. package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  188. package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  189. package/dist/templates/workspace-root/AGENTS.md +2 -1
  190. package/dist/triggers/actions/list-automations.js +1 -1
  191. package/dist/triggers/actions/manage-automation.js +5 -1
  192. package/dist/triggers/actions.d.ts +1 -1
  193. package/dist/triggers/actions.js +21 -15
  194. package/dist/triggers/dispatcher.js +17 -2
  195. package/dist/triggers/routes.d.ts +1 -0
  196. package/dist/triggers/routes.js +1 -0
  197. package/docs/content/docs-components.mdx +122 -11
  198. package/package.json +1 -1
  199. package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
  200. package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  201. package/src/templates/chat/netlify.toml +1 -1
  202. package/src/templates/chat/package.json +1 -0
  203. package/src/templates/chat/scripts/migrate-production.ts +31 -0
  204. package/src/templates/chat/tsconfig.json +1 -0
  205. package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
  206. package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  207. package/src/templates/default/package.json +1 -0
  208. package/src/templates/default/scripts/migrate-production.ts +31 -0
  209. package/src/templates/default/tsconfig.json +1 -0
  210. package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  211. package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
  212. package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
  213. package/src/templates/workspace-root/AGENTS.md +2 -1
@@ -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
  }
@@ -2,7 +2,7 @@
2
2
  ignore = "node ./scripts/netlify-ignore-build.mjs slides"
3
3
  # Rebuild Core explicitly so a cached Netlify install cannot bundle an older
4
4
  # agent loop or A2A handler into Slides' server functions.
5
- command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build"
5
+ command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter slides migrate:production; fi"
6
6
  publish = "templates/slides/dist"
7
7
  functions = "templates/slides/.netlify/functions-internal"
8
8
 
@@ -10,6 +10,7 @@
10
10
  "test": "vitest --run",
11
11
  "format.fix": "oxfmt --write .",
12
12
  "typecheck": "agent-native typecheck",
13
+ "migrate:production": "tsx scripts/migrate-production.ts",
13
14
  "action": "agent-native action",
14
15
  "script": "agent-native script"
15
16
  },
@@ -0,0 +1,24 @@
1
+ import { closeDbExec, withMigrationRuntime } from "@agent-native/core/db";
2
+ import { runFrameworkReleaseMigrations } from "@agent-native/core/server";
3
+
4
+ import { runSlidesMigrations } from "../server/plugins/db.js";
5
+
6
+ /**
7
+ * Release-time schema entrypoint for Slides.
8
+ *
9
+ * This script is the production owner of schema changes. It runs against the
10
+ * direct migration endpoint selected by core, while request functions skip
11
+ * all migration and ensure-table work automatically.
12
+ */
13
+ async function main(): Promise<void> {
14
+ await withMigrationRuntime(async () => {
15
+ await runFrameworkReleaseMigrations(null);
16
+ await runSlidesMigrations(null);
17
+ });
18
+ }
19
+
20
+ try {
21
+ await main();
22
+ } finally {
23
+ await closeDbExec();
24
+ }
@@ -33,7 +33,7 @@ const schemaTables = Object.values(schema).filter(isDrizzleTable);
33
33
  // packages/core/src/db/migrations.ts for the full rationale). Version numbers
34
34
  // alone are not a safe identity across parallel branches that each extend
35
35
  // this list independently.
36
- const runSlidesMigrations = runMigrations(
36
+ export const runSlidesMigrations = runMigrations(
37
37
  [
38
38
  {
39
39
  version: 1,
@@ -13,6 +13,7 @@
13
13
  "app/**/*",
14
14
  "server/**/*",
15
15
  "shared/**/*",
16
+ "scripts/**/*",
16
17
  "vite.config.ts",
17
18
  "react-router.config.ts",
18
19
  ".react-router/types/**/*",
@@ -36,8 +36,10 @@ Tier 4 includes **all** of the following — not only editing package source:
36
36
  - Files under `node_modules/@agent-native/*` (core, dispatch, scheduling, …)
37
37
  - `pnpm.overrides`, `overrides`, `resolutions`, or `patchedDependencies` that
38
38
  target any `@agent-native/*` package
39
- - Local package patches or invented "dispatch/core behavior" shims meant to
40
- paper over a version skew or failed upgrade
39
+ - `pnpm patch`, `pnpm patch-commit`, or local package patch artifacts for any
40
+ dependency
41
+ - Invented "dispatch/core behavior" shims meant to paper over a version skew
42
+ or failed upgrade
41
43
 
42
44
  This does not prohibit intentional app-owned UI customization. When public
43
45
  props and composition are insufficient, the `customizing-agent-native` skill
@@ -100,8 +102,10 @@ inline strings in components.
100
102
  - Don't confuse readable package source with app-owned code: use
101
103
  `customizing-agent-native` and `agent-native eject` for supported ownership
102
104
  transfer
105
+ - Don't use `pnpm patch` / `pnpm patch-commit`, commit `patches/` artifacts, or
106
+ add `pnpm.patchedDependencies` to "make the app run" after a version bump
103
107
  - Don't add `pnpm.overrides` / `patchedDependencies` / `resolutions` for
104
- `@agent-native/*` to "make the app run" after a version bump
108
+ `@agent-native/*` to paper over a broken upgrade
105
109
  - Don't invent local dispatch/core behavior overrides when upgrade fails —
106
110
  run `npx @agent-native/core@latest upgrade`, then fix app-level errors only
107
111
  - Don't modify `.agents/skills/` or `AGENTS.md` unless explicitly requested
@@ -15,8 +15,9 @@ metadata:
15
15
 
16
16
  When an older Agent Native app/branch needs to run on current packages, use
17
17
  `agent-native upgrade`. Never "fix" upgrade breakage with
18
- `pnpm.overrides`, `patchedDependencies`, `resolutions`, local patches, or
19
- edits under `node_modules/@agent-native/*` — especially not against
18
+ `pnpm patch`, `pnpm patch-commit`, `pnpm.patchedDependencies`,
19
+ `pnpm.overrides`, `patchedDependencies`, `resolutions`, local patch artifacts,
20
+ or edits under `node_modules/@agent-native/*` — especially not against
20
21
  `@agent-native/core` or `@agent-native/dispatch`.
21
22
 
22
23
  ## Why
@@ -109,6 +110,8 @@ install → refresh scaffold skills → verify, then fix **app** code only.
109
110
 
110
111
  - Don't add `pnpm.overrides`, `overrides`, `resolutions`, or
111
112
  `patchedDependencies` for any `@agent-native/*` package
113
+ - Don't run `pnpm patch` or `pnpm patch-commit`, or commit files under
114
+ `patches/`, for any dependency
112
115
  - Don't edit `node_modules/@agent-native/core` or
113
116
  `node_modules/@agent-native/dispatch`
114
117
  - Don't invent local "dispatch behavior" shims to paper over version skew
@@ -1,6 +1,6 @@
1
1
  [build]
2
2
  ignore = "node ./scripts/netlify-ignore-build.mjs tasks"
3
- command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build"
3
+ command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter tasks migrate:production; fi"
4
4
  publish = "templates/tasks/dist"
5
5
  functions = "templates/tasks/.netlify/functions-internal"
6
6
 
@@ -10,6 +10,7 @@
10
10
  "start": "agent-native start",
11
11
  "format.fix": "oxfmt --write .",
12
12
  "typecheck": "agent-native typecheck",
13
+ "migrate:production": "tsx scripts/migrate-production.ts",
13
14
  "action": "agent-native action",
14
15
  "script": "agent-native script",
15
16
  "db:push": "drizzle-kit push",
@@ -0,0 +1,24 @@
1
+ import { closeDbExec, withMigrationRuntime } from "@agent-native/core/db";
2
+ import { runFrameworkReleaseMigrations } from "@agent-native/core/server";
3
+
4
+ import { runTasksMigrations } from "../server/plugins/db.js";
5
+
6
+ /**
7
+ * Release-time schema entrypoint for Tasks.
8
+ *
9
+ * This script is the production owner of schema changes. It runs against the
10
+ * direct migration endpoint selected by core, while request functions skip
11
+ * all migration and ensure-table work automatically.
12
+ */
13
+ async function main(): Promise<void> {
14
+ await withMigrationRuntime(async () => {
15
+ await runFrameworkReleaseMigrations(null);
16
+ await runTasksMigrations(null);
17
+ });
18
+ }
19
+
20
+ try {
21
+ await main();
22
+ } finally {
23
+ await closeDbExec();
24
+ }
@@ -8,7 +8,7 @@ import * as schema from "../db/schema.js";
8
8
 
9
9
  // Keep historical entries unnamed for legacy-ledger compatibility; every new
10
10
  // migration must add a stable, unique `name` slug.
11
- const runTasksMigrations = runMigrations(
11
+ export const runTasksMigrations = runMigrations(
12
12
  [
13
13
  {
14
14
  version: 1,
@@ -14,6 +14,7 @@
14
14
  "server/**/*",
15
15
  "shared/**/*",
16
16
  "actions/**/*",
17
+ "scripts/**/*",
17
18
  "vite.config.ts",
18
19
  "vitest.config.ts",
19
20
  "react-router.config.ts",
package/dist/action.d.ts CHANGED
@@ -60,6 +60,8 @@ export interface ActionRunContext {
60
60
  requestHeaders?: Headers;
61
61
  /** Resolved org id, or `null` when the request has no org. */
62
62
  orgId?: string | null;
63
+ /** Hosting app/template id used for app-owned resource boundaries. */
64
+ appId?: string;
63
65
  /** How this action was invoked. */
64
66
  caller: ActionCaller;
65
67
  /** Present only for trigger-dispatched automation calls. */
@@ -697,6 +697,7 @@ export declare function runAgentLoop(opts: {
697
697
  onOutcome?: (outcome: AgentLoopOutcome) => void;
698
698
  ownerEmail?: string | null;
699
699
  orgId?: string | null;
700
+ appId?: string;
700
701
  /** Action invocation attribution. Defaults to the normal agent tool loop. */
701
702
  actionCaller?: ActionCaller;
702
703
  /** Trusted trigger lineage for automation-dispatched action calls. */
@@ -3974,6 +3974,7 @@ export async function runAgentLoop(opts) {
3974
3974
  ? Boolean(await actionEntry.needsApproval(toolCall.input, {
3975
3975
  userEmail: getRequestUserEmail(),
3976
3976
  orgId: getRequestOrgId() ?? null,
3977
+ appId: opts.appId,
3977
3978
  caller: opts.actionCaller ?? "tool",
3978
3979
  automation: opts.automation,
3979
3980
  networkProtocol: opts.networkProtocol,
@@ -4370,6 +4371,7 @@ export async function runAgentLoop(opts) {
4370
4371
  send,
4371
4372
  userEmail: actionUserEmail ?? undefined,
4372
4373
  orgId: actionOrgId,
4374
+ appId: opts.appId,
4373
4375
  caller: opts.actionCaller ?? "tool",
4374
4376
  automation: opts.automation,
4375
4377
  networkProtocol: opts.networkProtocol,
@@ -17,6 +17,10 @@ interface ContentPart {
17
17
  completedSideEffect?: boolean;
18
18
  mcpApp?: AgentMcpAppPayload;
19
19
  chatUI?: ActionChatUIConfig;
20
+ approval?: {
21
+ approvalKey: string;
22
+ dismissed?: boolean;
23
+ };
20
24
  }
21
25
  interface BuildAssistantMessageOptions {
22
26
  suppressInternalContinuation?: boolean;
@@ -114,6 +114,14 @@ export function buildAssistantMessage(events, runId, options = {}) {
114
114
  });
115
115
  continue;
116
116
  }
117
+ if (event.type === "approval_required") {
118
+ const matchingIndex = findApprovalToolCallIndex(content, event.tool ?? "unknown", event.toolCallId);
119
+ const part = content[matchingIndex];
120
+ if (part?.type === "tool-call") {
121
+ part.approval = { approvalKey: event.approvalKey };
122
+ }
123
+ continue;
124
+ }
117
125
  if (event.type === "tool_done") {
118
126
  const eventToolCallId = event.id?.trim();
119
127
  let matchingIndex = -1;
@@ -339,6 +347,42 @@ function normalizeAttachmentIdentity(attachments) {
339
347
  contentType: att?.contentType,
340
348
  }));
341
349
  }
350
+ function findApprovalToolCallIndex(content, toolName, toolCallId) {
351
+ if (toolCallId) {
352
+ for (let i = content.length - 1; i >= 0; i--) {
353
+ const part = content[i];
354
+ if (part.type === "tool-call" &&
355
+ part.toolCallId === toolCallId &&
356
+ part.result === undefined) {
357
+ return i;
358
+ }
359
+ }
360
+ // Older tool_start events without an id use one reader-local tc_N id.
361
+ // Only accept that fallback when it is unambiguous, so a replayed approval
362
+ // cannot attach its key to another same-name call.
363
+ const readerLocalCandidates = [];
364
+ for (let i = 0; i < content.length; i += 1) {
365
+ const part = content[i];
366
+ if (part.type === "tool-call" &&
367
+ part.toolName === toolName &&
368
+ part.result === undefined &&
369
+ typeof part.toolCallId === "string" &&
370
+ /^tc_\d+$/.test(part.toolCallId)) {
371
+ readerLocalCandidates.push(i);
372
+ }
373
+ }
374
+ return readerLocalCandidates.length === 1 ? readerLocalCandidates[0] : -1;
375
+ }
376
+ for (let i = content.length - 1; i >= 0; i--) {
377
+ const part = content[i];
378
+ if (part.type === "tool-call" &&
379
+ part.toolName === toolName &&
380
+ part.result === undefined) {
381
+ return i;
382
+ }
383
+ }
384
+ return -1;
385
+ }
342
386
  // Strip the render-only `toolCallId` before fingerprinting. The id is generated
343
387
  // differently depending on who built the message — the server now scopes it by
344
388
  // run (`${runId}:tc_1`) while the client's live stream uses a bare counter
@@ -4,6 +4,7 @@ export type AutomationScope = "personal" | "organization";
4
4
  export interface AutomationActor {
5
5
  userEmail: string;
6
6
  orgId?: string | null;
7
+ appId?: string | null;
7
8
  }
8
9
  export interface AutomationDefinition {
9
10
  resource: Resource;
@@ -11,7 +11,11 @@ function normalizeActor(actor) {
11
11
  const userEmail = actor.userEmail.trim().toLowerCase();
12
12
  if (!userEmail)
13
13
  throw httpError("Not authenticated.", 401);
14
- return { userEmail, orgId: actor.orgId?.trim() || null };
14
+ return {
15
+ userEmail,
16
+ orgId: actor.orgId?.trim() || null,
17
+ appId: actor.appId?.trim() || null,
18
+ };
15
19
  }
16
20
  function automationName(path) {
17
21
  return path.replace(/^jobs\//, "").replace(/\.md$/, "");
@@ -56,6 +60,12 @@ async function requireOrganizationMembership(actor) {
56
60
  function isOrganizationAdmin(membership) {
57
61
  return membership.role === "owner" || membership.role === "admin";
58
62
  }
63
+ function automationBelongsToApp(meta, appId) {
64
+ const ownerAppId = meta.appId?.trim();
65
+ if (!ownerAppId || !appId?.trim())
66
+ return true;
67
+ return ownerAppId === appId.trim();
68
+ }
59
69
  async function mutationAccess(actorInput, resource, meta) {
60
70
  const actor = normalizeActor(actorInput);
61
71
  const resourceOrgId = organizationIdFromResourceOwner(resource.owner);
@@ -83,7 +93,10 @@ async function mutationAccess(actorInput, resource, meta) {
83
93
  */
84
94
  export async function canUpdateAutomationResource(actorInput, resource) {
85
95
  const { meta } = parseJobResource(resource.content);
86
- return (await mutationAccess(actorInput, resource, meta)).canUpdate;
96
+ const actor = normalizeActor(actorInput);
97
+ if (!automationBelongsToApp(meta, actor.appId))
98
+ return false;
99
+ return (await mutationAccess(actor, resource, meta)).canUpdate;
87
100
  }
88
101
  function assertExplicitAutomation(resource) {
89
102
  const parsed = parseJobResource(resource.content);
@@ -111,6 +124,9 @@ async function readDefinition(actorInput, scope, name) {
111
124
  throw httpError(`Automation "${automationName(path)}" not found.`, 404);
112
125
  }
113
126
  const definition = assertExplicitAutomation(resource);
127
+ if (!automationBelongsToApp(definition.meta, actor.appId)) {
128
+ throw httpError(`Automation "${automationName(path)}" not found.`, 404);
129
+ }
114
130
  const access = await mutationAccess(actor, resource, definition.meta);
115
131
  return {
116
132
  ...definition,
@@ -138,6 +154,8 @@ export async function listAutomationDefinitions(actorInput, scope) {
138
154
  const parsed = parseJobResource(resource.content);
139
155
  if (parsed.classification.kind !== "automation")
140
156
  continue;
157
+ if (!automationBelongsToApp(parsed.meta, actor.appId))
158
+ continue;
141
159
  const isCreator = parsed.meta.createdBy?.trim().toLowerCase() === actor.userEmail;
142
160
  automations.push({
143
161
  resource,
@@ -197,6 +215,7 @@ export async function defineAutomation(actorInput, input) {
197
215
  condition: input.condition?.trim() || undefined,
198
216
  mode: "agentic",
199
217
  domain: input.domain?.trim() || undefined,
218
+ appId: actor.appId || undefined,
200
219
  delegatedPolicyId: input.delegatedPolicyId?.trim() || undefined,
201
220
  createdBy: actor.userEmail,
202
221
  orgId: input.scope === "organization" ? actor.orgId : undefined,