@agent-native/core 0.172.3 → 0.172.6

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 (55) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/get-settings.ts +2 -5
  3. package/corpus/templates/calendar/actions/list-events.ts +21 -80
  4. package/corpus/templates/calendar/actions/update-settings.ts +11 -16
  5. package/corpus/templates/calendar/actions/view-screen.ts +25 -10
  6. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
  7. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
  8. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
  9. package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
  10. package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
  11. package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
  12. package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
  13. package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
  14. package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
  15. package/corpus/templates/calendar/shared/settings.ts +15 -9
  16. package/corpus/templates/calendar/shared/timezone.ts +127 -0
  17. package/corpus/templates/factory/AGENTS.md +5 -3
  18. package/corpus/templates/factory/actions/babysit-pull-request.ts +6 -0
  19. package/corpus/templates/factory/actions/list-factory-automations.ts +2 -2
  20. package/corpus/templates/factory/actions/save-factory-automation.ts +0 -5
  21. package/corpus/templates/factory/changelog/2026-08-24-slack-checking-works-for-teammate-created-factories.md +6 -0
  22. package/corpus/templates/factory/changelog/2026-08-24-teammates-can-edit-and-run-factory-jobs.md +6 -0
  23. package/corpus/templates/factory/server/lib/factory-scope.ts +8 -0
  24. package/corpus/templates/factory/server/lib/require-factory-automation.ts +1 -9
  25. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -1
  26. package/corpus/templates/slides/actions/list-decks.ts +79 -5
  27. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
  28. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
  29. package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
  30. package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
  31. package/corpus/templates/slides/app/pages/Index.tsx +11 -13
  32. package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
  33. package/dist/agent/production-agent.js +5 -1
  34. package/dist/agent/types.d.ts +5 -0
  35. package/dist/automations/service.d.ts +6 -0
  36. package/dist/automations/service.js +20 -0
  37. package/dist/client/settings/SettingsPanel.js +80 -33
  38. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  39. package/dist/jobs/run-now.js +2 -2
  40. package/dist/observability/posthog-ai.d.ts +36 -11
  41. package/dist/observability/posthog-ai.js +99 -30
  42. package/dist/observability/routes.js +9 -2
  43. package/dist/observability/traces.js +330 -142
  44. package/dist/tracking/providers.js +9 -3
  45. package/dist/vite/client.js +5 -2
  46. package/docs/content/docs-components.mdx +20 -1
  47. package/docs/content/observability.mdx +27 -10
  48. package/docs/content/template-calendar-agent.mdx +63 -26
  49. package/docs/content/template-calendar-developers.mdx +165 -40
  50. package/docs/content/template-calendar-features.mdx +276 -0
  51. package/docs/content/template-calendar-integrations.mdx +78 -0
  52. package/docs/content/template-calendar.mdx +56 -123
  53. package/package.json +1 -1
  54. package/docs/content/template-calendar-booking-links.mdx +0 -73
  55. package/docs/content/template-calendar-scheduling.mdx +0 -128
@@ -7,6 +7,7 @@ import {
7
7
  cloneElement,
8
8
  isValidElement,
9
9
  useCallback,
10
+ useEffect,
10
11
  useState,
11
12
  type MouseEvent,
12
13
  type ReactElement,
@@ -22,7 +23,10 @@ import { getDeckShareLinkOrder } from "@/lib/deck-share-links";
22
23
  interface ShareDialogProps {
23
24
  deck: Deck;
24
25
  /** Trigger element rendered as the dialog anchor (usually the Share button). */
25
- children: ReactNode;
26
+ children?: ReactNode;
27
+ /** Controlled opening for menu items that must wait for their parent to close. */
28
+ open?: boolean;
29
+ onOpenChange?: (open: boolean) => void;
26
30
  }
27
31
 
28
32
  function getShareUrls(deckId: string) {
@@ -40,7 +44,12 @@ function getShareUrls(deckId: string) {
40
44
  };
41
45
  }
42
46
 
43
- export default function ShareDialog({ deck, children }: ShareDialogProps) {
47
+ export default function ShareDialog({
48
+ deck,
49
+ children,
50
+ open: requestedOpen,
51
+ onOpenChange,
52
+ }: ShareDialogProps) {
44
53
  const t = useT();
45
54
  const { isLocal } = useDbStatus();
46
55
  const [open, setOpen] = useState(false);
@@ -50,6 +59,14 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
50
59
  } | null>(null);
51
60
  const [creatingLink, setCreatingLink] = useState(false);
52
61
 
62
+ const setDialogOpen = useCallback(
63
+ (nextOpen: boolean) => {
64
+ setOpen(nextOpen);
65
+ onOpenChange?.(nextOpen);
66
+ },
67
+ [onOpenChange],
68
+ );
69
+
53
70
  const shareUrls = getShareUrls(deck.id);
54
71
  const shareLinkOrder = getDeckShareLinkOrder(deck.visibility);
55
72
  const shareToken =
@@ -61,11 +78,11 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
61
78
 
62
79
  const openShareDialog = useCallback(async () => {
63
80
  if (isLocal) {
64
- setOpen(true);
81
+ setDialogOpen(true);
65
82
  return;
66
83
  }
67
84
  if (shareToken) {
68
- setOpen(true);
85
+ setDialogOpen(true);
69
86
  return;
70
87
  }
71
88
  if (creatingLink) return;
@@ -97,29 +114,40 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
97
114
  throw new Error(t("share.createFailed"));
98
115
  }
99
116
  setShareLink({ deckId: deck.id, token: payload.shareToken });
100
- setOpen(true);
117
+ setDialogOpen(true);
101
118
  } catch (error) {
102
119
  toast.error(
103
120
  error instanceof Error ? error.message : t("share.createFailed"),
104
121
  );
122
+ if (requestedOpen !== undefined) setDialogOpen(false);
105
123
  } finally {
106
124
  setCreatingLink(false);
107
125
  }
108
- }, [creatingLink, deck, isLocal, shareToken, t]);
126
+ }, [creatingLink, deck, isLocal, setDialogOpen, shareToken, t]);
127
+
128
+ useEffect(() => {
129
+ if (requestedOpen === undefined) return;
130
+ if (!requestedOpen) {
131
+ setOpen(false);
132
+ return;
133
+ }
134
+ if (!open) void openShareDialog();
135
+ }, [open, openShareDialog, requestedOpen]);
109
136
 
110
- const trigger = isValidElement(children)
111
- ? (() => {
112
- const triggerElement = children as ReactElement<{
113
- onClick?: (event: MouseEvent) => void;
114
- }>;
115
- return cloneElement(triggerElement, {
116
- onClick: (event) => {
117
- triggerElement.props.onClick?.(event);
118
- if (!event.defaultPrevented) void openShareDialog();
119
- },
120
- });
121
- })()
122
- : children;
137
+ const trigger =
138
+ children && isValidElement(children)
139
+ ? (() => {
140
+ const triggerElement = children as ReactElement<{
141
+ onClick?: (event: MouseEvent) => void;
142
+ }>;
143
+ return cloneElement(triggerElement, {
144
+ onClick: (event) => {
145
+ triggerElement.props.onClick?.(event);
146
+ if (!event.defaultPrevented) void openShareDialog();
147
+ },
148
+ });
149
+ })()
150
+ : children;
123
151
 
124
152
  return (
125
153
  <>
@@ -128,12 +156,12 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
128
156
  <CloudUpgrade
129
157
  title={t("share.title")}
130
158
  description={t("share.cloudUpgradeDescription")}
131
- onClose={() => setOpen(false)}
159
+ onClose={() => setDialogOpen(false)}
132
160
  />
133
161
  ) : null}
134
162
  <CoreShareDialog
135
163
  open={open && !isLocal}
136
- onClose={() => setOpen(false)}
164
+ onClose={() => setDialogOpen(false)}
137
165
  resourceType="deck"
138
166
  resourceId={deck.id}
139
167
  resourceTitle={deck.title}
@@ -147,6 +147,8 @@ export interface Deck {
147
147
  starred?: boolean;
148
148
  /** Slide aspect ratio (defaults to 16:9 when absent for backwards compat) */
149
149
  aspectRatio?: AspectRatio;
150
+ /** First slide returned by the light deck listing for home-page previews. */
151
+ previewSlide?: Slide;
150
152
  }
151
153
 
152
154
  export type DeckPersistenceResult =
@@ -199,19 +201,18 @@ interface DeckContextType {
199
201
  ) => Deck;
200
202
  ensureDeckPersisted: (id: string) => Promise<DeckPersistenceResult>;
201
203
  /**
202
- * Optimistically duplicate a deck. Inserts a copy into local state with the
203
- * supplied `newId` immediately so the UI can navigate without awaiting the
204
- * server, then fires the duplicate-deck action in the background. On error,
205
- * the optimistic deck is rolled back.
204
+ * Duplicate a deck, hydrating a preview-only source before creating the
205
+ * optimistic copy. On error, the optimistic deck is rolled back.
206
206
  *
207
- * Returns the optimistic deck (or `null` if the source deck isn't found).
207
+ * Returns the optimistic deck (or `null` if the source deck isn't found or
208
+ * could not be hydrated).
208
209
  */
209
210
  duplicateDeck: (
210
211
  sourceDeckId: string,
211
212
  newId: string,
212
213
  title?: string,
213
214
  onFailure?: () => void,
214
- ) => Deck | null;
215
+ ) => Promise<Deck | null>;
215
216
  deleteDeck: (id: string) => void;
216
217
  updateDeck: (
217
218
  id: string,
@@ -325,6 +326,8 @@ function normalizeActionDeck(value: unknown): Deck | null {
325
326
  const deckRecord = deck as unknown as Record<string, unknown>;
326
327
  const cleanedDeck = { ...deckRecord };
327
328
  for (const field of GET_DECK_ONLY_DECK_FIELDS) delete cleanedDeck[field];
329
+ const previewSlide = deckRecord.previewSlide;
330
+ delete cleanedDeck.previewSlide;
328
331
 
329
332
  // Strip the same decorative fields from every slide, so a deck fetched from
330
333
  // `get-deck` is structurally identical to one built by local mutations —
@@ -356,9 +359,20 @@ function normalizeActionDeck(value: unknown): Deck | null {
356
359
  ? deck.updatedAt
357
360
  : deck.createdAt || "",
358
361
  slides,
362
+ ...(previewSlide && typeof previewSlide === "object"
363
+ ? { previewSlide: previewSlide as Slide }
364
+ : {}),
359
365
  } as Deck;
360
366
  }
361
367
 
368
+ export function getDuplicateSourceSlides(deck: Deck): Slide[] {
369
+ return deck.slides.length > 0
370
+ ? deck.slides
371
+ : deck.previewSlide
372
+ ? [deck.previewSlide]
373
+ : [];
374
+ }
375
+
362
376
  // Debounced save to API + save-state listeners (so the toolbar indicator
363
377
  // can show "Saving…" / "Saved"). The map tracks pending debounce timers;
364
378
  // `inFlight` tracks active fetches. Combined, they answer "is anything
@@ -1040,7 +1054,7 @@ async function fetchDecksFromAPI(): Promise<Deck[] | null> {
1040
1054
  try {
1041
1055
  const result = await callAction<DeckListActionResult>(
1042
1056
  "list-decks",
1043
- { light: "true" },
1057
+ { light: "true", includePreview: "true" },
1044
1058
  { method: "GET" },
1045
1059
  );
1046
1060
  if (!Array.isArray(result?.decks)) {
@@ -1172,7 +1186,7 @@ async function fetchDecksForCurrentRoute(): Promise<Deck[] | null> {
1172
1186
  }
1173
1187
  if (!currentOpenDeckId) return loaded;
1174
1188
 
1175
- // The list is intentionally metadata-only. Hydrate just the deck the user
1189
+ // The list has only first-slide previews. Hydrate just the deck the user
1176
1190
  // opened so the editor gets full slide content without making the home page
1177
1191
  // download every deck body.
1178
1192
  const directDeck = await fetchDeckFromAPI(currentOpenDeckId);
@@ -2143,15 +2157,27 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2143
2157
  );
2144
2158
 
2145
2159
  const duplicateDeck = useCallback(
2146
- (
2160
+ async (
2147
2161
  sourceDeckId: string,
2148
2162
  newId: string,
2149
2163
  title?: string,
2150
2164
  onFailure?: () => void,
2151
- ): Deck | null => {
2165
+ ): Promise<Deck | null> => {
2152
2166
  if (pendingDuplicateSourceIdsRef.current.has(sourceDeckId)) return null;
2153
- const source = decks.find((d) => d.id === sourceDeckId);
2154
- if (!source) return null;
2167
+ pendingDuplicateSourceIdsRef.current.add(sourceDeckId);
2168
+ let source = decks.find((d) => d.id === sourceDeckId);
2169
+ if (!source) {
2170
+ pendingDuplicateSourceIdsRef.current.delete(sourceDeckId);
2171
+ return null;
2172
+ }
2173
+ if (source.slides.length === 0 && source.previewSlide) {
2174
+ const hydrated = await fetchDeckFromAPI(sourceDeckId);
2175
+ if (!hydrated) {
2176
+ pendingDuplicateSourceIdsRef.current.delete(sourceDeckId);
2177
+ return null;
2178
+ }
2179
+ source = hydrated;
2180
+ }
2155
2181
 
2156
2182
  const now = new Date().toISOString();
2157
2183
  const newTitle = title || `Copy of ${source.title}`;
@@ -2172,7 +2198,8 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2172
2198
  createdByMe: true,
2173
2199
  shareToken: undefined,
2174
2200
  };
2175
- optimistic.slides = optimistic.slides.map((s) => ({
2201
+ delete optimistic.previewSlide;
2202
+ optimistic.slides = getDuplicateSourceSlides(source).map((s) => ({
2176
2203
  ...s,
2177
2204
  id: `slide-${nanoid(8)}`,
2178
2205
  }));
@@ -2181,7 +2208,6 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2181
2208
  // the duplicate-deck action's INSERT lands.
2182
2209
  pendingCreateIdsRef.current.add(newId);
2183
2210
  noteLocalCreate(newId);
2184
- pendingDuplicateSourceIdsRef.current.add(sourceDeckId);
2185
2211
 
2186
2212
  // Fire the action in the background. On error, roll back.
2187
2213
  const duplicatePromise = callAction<DuplicateDeckActionResult>(
@@ -2190,10 +2216,12 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2190
2216
  deckId: sourceDeckId,
2191
2217
  newId,
2192
2218
  title,
2193
- // The user can start editing the copy before this resolves, so the
2194
- // persisted slides must carry the ids the optimistic ones already
2195
- // have — otherwise those edits address slides the server never had.
2196
- slideIds: optimistic.slides.map((s) => s.id),
2219
+ // Preview-only sources were hydrated above. An empty array is not
2220
+ // a valid optimistic slide-id projection, so omit it for empty
2221
+ // decks and let the server generate ids for any uncovered slides.
2222
+ ...(optimistic.slides.length > 0
2223
+ ? { slideIds: optimistic.slides.map((s) => s.id) }
2224
+ : {}),
2197
2225
  },
2198
2226
  ).then(() => undefined);
2199
2227
  pendingCreatePromisesRef.current.set(newId, duplicatePromise);
@@ -2213,9 +2241,9 @@ export function DeckProvider({ children }: { children: ReactNode }) {
2213
2241
  }
2214
2242
  console.error("Duplicate failed:", err);
2215
2243
  // Roll back: drop the optimistic deck from local state. The caller
2216
- // (via onFailure) is responsible for navigating away if the user is
2217
- // still sitting on this now-gone deck's route — otherwise they're
2218
- // stranded on a "Deck unavailable" screen with no way back.
2244
+ // (via onFailure) is responsible for navigating away if the user
2245
+ // is still sitting on this now-gone deck's route — otherwise
2246
+ // they're stranded on a "Deck unavailable" screen with no way back.
2219
2247
  setDecks((prev) => prev.filter((d) => d.id !== newId));
2220
2248
  onFailure?.();
2221
2249
  })
@@ -1545,9 +1545,9 @@ export default function DeckEditor() {
1545
1545
  onTogglePinMode={togglePinMode}
1546
1546
  textBoxMode={textBoxMode}
1547
1547
  onToggleTextBoxMode={toggleTextBoxMode}
1548
- onDuplicateDeck={() => {
1548
+ onDuplicateDeck={async () => {
1549
1549
  const newId = `deck-${nanoid()}`;
1550
- const optimistic = duplicateDeck(id, newId, undefined, () => {
1550
+ const optimistic = await duplicateDeck(id, newId, undefined, () => {
1551
1551
  // The background duplicate-deck action failed after we already
1552
1552
  // navigated to the optimistic copy. If the user is still there,
1553
1553
  // send them back instead of stranding them on a "Deck
@@ -1289,19 +1289,17 @@ export default function Index() {
1289
1289
  // first (the same path the editor's own Duplicate uses) and navigate to
1290
1290
  // that; the background action reconciles or rolls the copy back.
1291
1291
  const handleDuplicate = useCallback(
1292
- (id: string) => {
1293
- let copy: ReturnType<typeof duplicateDeck> | undefined;
1294
- flushSync(() => {
1295
- copy = duplicateDeck(id, `deck-${nanoid()}`, undefined, () => {
1296
- // The background duplicate-deck action failed after we already
1297
- // navigated to the optimistic copy's route. If the user is still
1298
- // there, send them back to the deck list instead of stranding them
1299
- // on a "Deck unavailable" screen for a deck that no longer exists.
1300
- if (deckIdFromPathname(window.location.pathname) === copy?.id) {
1301
- navigate("/");
1302
- }
1303
- toast.error(t("home.duplicateFailed"));
1304
- });
1292
+ async (id: string) => {
1293
+ const newId = `deck-${nanoid()}`;
1294
+ const copy = await duplicateDeck(id, newId, undefined, () => {
1295
+ // The background duplicate-deck action failed after we already
1296
+ // navigated to the optimistic copy's route. If the user is still
1297
+ // there, send them back to the deck list instead of stranding them
1298
+ // on a "Deck unavailable" screen for a deck that no longer exists.
1299
+ if (deckIdFromPathname(window.location.pathname) === newId) {
1300
+ navigate("/");
1301
+ }
1302
+ toast.error(t("home.duplicateFailed"));
1305
1303
  });
1306
1304
  // The context refuses a second copy of the same deck while the first
1307
1305
  // one's action is still in flight.
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-24
4
+ ---
5
+ Decks now show a first-slide preview and keep sharing controls in the overflow menu.
@@ -3528,7 +3528,11 @@ export async function runAgentLoop(opts) {
3528
3528
  if (!modelStreamBracketOpen)
3529
3529
  return;
3530
3530
  modelStreamBracketOpen = false;
3531
- send({ type: "model_stream", status: "end" });
3531
+ send({
3532
+ type: "model_stream",
3533
+ status: "end",
3534
+ ...(terminalStopReason ? { reason: terminalStopReason } : {}),
3535
+ });
3532
3536
  };
3533
3537
  let lastModelStreamProgressAt = Date.now();
3534
3538
  // FIX 2: true once a real (non-heartbeat) engine-stream event has been
@@ -305,6 +305,11 @@ export type AgentChatEvent = {
305
305
  */
306
306
  type: "model_stream";
307
307
  status: "start" | "end";
308
+ /** Why the model stopped, on the closing event: `end_turn`, `tool_use`,
309
+ * `max_tokens`, `stop_sequence`, `error`. Absent when the stream was cut
310
+ * before the engine reported one — a truncated call and a call that
311
+ * ended cleanly must stay distinguishable. */
312
+ reason?: "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "error";
308
313
  } | {
309
314
  type: "tool_start";
310
315
  tool: string;
@@ -67,6 +67,12 @@ export interface UpdateAutomationInput {
67
67
  * automation classifier.
68
68
  */
69
69
  export declare function canUpdateAutomationResource(actorInput: AutomationActor, resource: Resource): Promise<boolean>;
70
+ /**
71
+ * Factory is a shared team workspace: any current org member may queue Run now
72
+ * for that app's Factory-domain org jobs. Mail/CRM and other automations stay
73
+ * on creator-or-admin `canUpdate`.
74
+ */
75
+ export declare function canQueueAutomationRunNow(actorInput: AutomationActor, resource: Resource, scope: AutomationScope): Promise<boolean>;
70
76
  export declare function listAutomationDefinitions(actorInput: AutomationActor, scope: AutomationScope): Promise<AutomationDefinition[]>;
71
77
  export declare function defineAutomation(actorInput: AutomationActor, input: DefineAutomationInput): Promise<DefinedAutomation>;
72
78
  export declare function updateAutomation(actorInput: AutomationActor, input: UpdateAutomationInput): Promise<AutomationDefinition>;
@@ -108,6 +108,26 @@ export async function canUpdateAutomationResource(actorInput, resource) {
108
108
  return false;
109
109
  return (await mutationAccess(actor, resource, meta)).canUpdate;
110
110
  }
111
+ /**
112
+ * Factory is a shared team workspace: any current org member may queue Run now
113
+ * for that app's Factory-domain org jobs. Mail/CRM and other automations stay
114
+ * on creator-or-admin `canUpdate`.
115
+ */
116
+ export async function canQueueAutomationRunNow(actorInput, resource, scope) {
117
+ const { meta } = parseJobResource(resource.content);
118
+ const actor = normalizeActor(actorInput);
119
+ if (!jobBelongsToApp(meta, actor.appId))
120
+ return false;
121
+ const access = await mutationAccess(actor, resource, meta);
122
+ if (access.canUpdate)
123
+ return true;
124
+ const resourceOrgId = organizationIdFromResourceOwner(resource.owner);
125
+ return (scope === "organization" &&
126
+ Boolean(resourceOrgId) &&
127
+ actor.orgId === resourceOrgId &&
128
+ actor.appId === "factory" &&
129
+ meta.domain === "factory");
130
+ }
111
131
  function assertExplicitAutomation(resource) {
112
132
  const parsed = parseJobResource(resource.content);
113
133
  if (parsed.classification.kind !== "automation") {