@agent-native/core 0.161.23 → 0.162.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
  4. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  5. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  6. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  7. package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
  8. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  9. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
  10. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
  11. package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
  12. package/corpus/templates/clips/desktop/src/app.tsx +30 -40
  13. package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
  14. package/corpus/templates/clips/desktop/src/styles.css +110 -0
  15. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
  16. package/corpus/templates/clips/shared/recording-capture.ts +18 -13
  17. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
  20. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
  21. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
  22. package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
  23. package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
  24. package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
  25. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  26. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  27. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  28. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  29. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  30. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  31. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  32. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  33. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  34. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  35. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  36. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  37. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  38. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
  40. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  41. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  42. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  43. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  44. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  45. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  46. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  47. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  48. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  49. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  50. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  51. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  52. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  53. package/corpus/templates/slides/shared/api.ts +7 -1
  54. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  55. package/dist/agent/engine/translate-anthropic.js +27 -2
  56. package/dist/agent/production-agent.d.ts +2 -2
  57. package/dist/agent/production-agent.js +16 -8
  58. package/dist/agent/thread-data-builder.d.ts +1 -0
  59. package/dist/agent/thread-data-builder.js +6 -4
  60. package/dist/agent/tool-approval-store.d.ts +10 -1
  61. package/dist/agent/tool-approval-store.js +19 -2
  62. package/dist/agent/types.d.ts +9 -0
  63. package/dist/client/AssistantChat.js +14 -6
  64. package/dist/client/EnvironmentBadge.d.ts +15 -0
  65. package/dist/client/EnvironmentBadge.js +79 -0
  66. package/dist/client/MultiTabAssistantChat.js +73 -17
  67. package/dist/client/agent-chat-adapter.js +11 -1
  68. package/dist/client/api-path.js +14 -1
  69. package/dist/client/app-providers.js +2 -1
  70. package/dist/client/chat/tool-call-display.d.ts +12 -2
  71. package/dist/client/chat/tool-call-display.js +17 -7
  72. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  73. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  74. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  75. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  76. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  77. package/dist/client/onboarding/use-onboarding.js +22 -7
  78. package/dist/client/session-replay.js +58 -15
  79. package/dist/client/settings/SettingsTabsPage.js +19 -6
  80. package/dist/client/sse-event-processor.d.ts +7 -0
  81. package/dist/client/sse-event-processor.js +4 -1
  82. package/dist/client/ui/index.d.ts +1 -0
  83. package/dist/client/ui/index.js +1 -0
  84. package/dist/client/use-action.js +26 -0
  85. package/dist/collab/awareness.d.ts +2 -2
  86. package/dist/collab/struct-routes.d.ts +1 -1
  87. package/dist/config.d.ts +15 -0
  88. package/dist/config.js +53 -0
  89. package/dist/extensions/fetch-tool.js +8 -2
  90. package/dist/mcp/embed-app.js +1 -1
  91. package/dist/notifications/routes.d.ts +1 -1
  92. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  93. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  94. package/dist/provider-api/index.d.ts +10 -0
  95. package/dist/provider-api/index.js +45 -9
  96. package/dist/server/auth.js +10 -3
  97. package/dist/server/poll.js +7 -3
  98. package/dist/server/sentry.d.ts +1 -1
  99. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  100. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  101. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  102. package/dist/vite/client.d.ts +5 -0
  103. package/dist/vite/client.js +64 -2
  104. package/dist/workspace-files/actions.d.ts +1 -8
  105. package/dist/workspace-files/actions.js +5 -21
  106. package/dist/workspace-files/index.d.ts +1 -1
  107. package/dist/workspace-files/index.js +1 -1
  108. package/dist/workspace-files/store.d.ts +18 -0
  109. package/dist/workspace-files/store.js +24 -3
  110. package/package.json +3 -3
  111. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  112. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  113. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
@@ -94,15 +94,40 @@ export function formatOverflowForTool(
94
94
  deckId: string,
95
95
  m: SlideFitMeasurement,
96
96
  ): string {
97
+ const hasHorizontal = (m.horizontalOverflow ?? 0) > 0;
98
+ const hasVertical = m.verticalOverflow > 0;
99
+
100
+ // A row of side-by-side cards/columns that's too wide is a *width formula*
101
+ // bug (fixed px flex-basis, a wrong container-width assumption, a missing
102
+ // `min-width: 0`), not a density bug. The old list was vertical-only —
103
+ // "tighten copy" / "fewer stacked cards" — which cannot shrink a fixed
104
+ // column width no matter how many times it's applied, so a horizontal
105
+ // overflow looped forever against fixes that could never touch the actual
106
+ // defect (see the Oliver Robertson Slack thread this regression test
107
+ // reproduces).
108
+ const fixes = [
109
+ ...(hasHorizontal
110
+ ? [
111
+ `Use percentage or flex-based column widths (e.g. \`flex: 1 1 0%\` with \`min-width: 0\`, or \`width: calc((100% - <gaps>) / <count>)\` measured against this slide's own content width) instead of a fixed pixel width per column.`,
112
+ `Reduce the number of side-by-side columns, or wrap to a second row, if the content cannot fit at the slide's actual content width.`,
113
+ ]
114
+ : []),
115
+ ...(hasVertical || !hasHorizontal
116
+ ? [
117
+ `Tighten copy — shorter headings/bullets, drop low-value lines.`,
118
+ `Reduce vertical density — fewer stacked cards, smaller gaps, body font no smaller than 16px.`,
119
+ `Reduce slide padding (e.g. 40px top/bottom instead of 60-80px).`,
120
+ `Split across two slides only if the content cannot be compressed.`,
121
+ ]
122
+ : []),
123
+ ];
124
+
97
125
  return [
98
126
  ``,
99
- `⚠ Layout overflows the canvas${m.verticalOverflow > 0 ? ` vertically by ${m.verticalOverflow}px` : ""}${(m.horizontalOverflow ?? 0) > 0 ? ` and horizontally by ${m.horizontalOverflow}px` : ""} — natural content is ${m.contentWidth ?? "unknown"}x${m.contentHeight}px inside a ${m.viewportWidth ?? "unknown"}x${m.viewportHeight}px content area.`,
127
+ `⚠ Layout overflows the canvas${hasVertical ? ` vertically by ${m.verticalOverflow}px` : ""}${hasHorizontal ? ` and horizontally by ${m.horizontalOverflow}px` : ""} — natural content is ${m.contentWidth ?? "unknown"}x${m.contentHeight}px inside a ${m.viewportWidth ?? "unknown"}x${m.viewportHeight}px content area.`,
100
128
  ``,
101
129
  `Make one structural repair now with \`update-slide --deckId ${deckId} --slideId ${m.slideId}\`. Prefer small surgical patches (--find / --replace) over a full rewrite:`,
102
- `1. Tighten copy — shorter headings/bullets, drop low-value lines.`,
103
- `2. Reduce vertical density — fewer stacked cards, smaller gaps, body font no smaller than 16px.`,
104
- `3. Reduce slide padding (e.g. 40px top/bottom instead of 60-80px).`,
105
- `4. Split across two slides only if the content cannot be compressed.`,
130
+ ...fixes.map((fix, index) => `${index + 1}. ${fix}`),
106
131
  ``,
107
132
  `Do **not** use zoom, \`transform: scale\`, clipping, \`overflow: scroll\`, or a smaller-than-16px body font. Preserve existing absolute text boxes and fix the normal-flow HTML. Verify the action result and then use \`view-screen\`; do not spin through another repair loop.`,
108
133
  ].join("\n");
@@ -97,6 +97,7 @@ export default defineAction({
97
97
  "call it once per slide in slide order and wait for each result before adding the next slide. " +
98
98
  "Avoid parallel add-slide calls for the same deck; sequential writes keep the editor and agent connection stable. " +
99
99
  "If the deck has a designSystemId, first use `get-design-system` and apply its `agentContext` tokens/docs; do not use generic slide styling from the id alone. " +
100
+ "Pass presenter-only speaker notes in `notes`; keep them out of the slide HTML. " +
100
101
  "Returns the new slide ID, 1-based slideNumber, and updated slide count.",
101
102
  schema: z.object({
102
103
  deckId: z.string().describe("Target deck ID"),
@@ -120,7 +121,10 @@ export default defineAction({
120
121
  ])
121
122
  .optional()
122
123
  .describe("Layout type hint"),
123
- notes: z.string().optional().describe("Speaker notes for this slide"),
124
+ notes: z
125
+ .string()
126
+ .optional()
127
+ .describe("Optional presenter-only speaker notes; keep them out of HTML"),
124
128
  position: z
125
129
  .preprocess((value) => {
126
130
  if (typeof value !== "string") return value;
@@ -299,7 +303,7 @@ export default defineAction({
299
303
  creativeContextReuseLabels: slideReuseLabels,
300
304
  };
301
305
  if (layout) newSlide.layout = layout;
302
- if (notes) newSlide.notes = notes;
306
+ if (notes !== undefined) newSlide.notes = notes;
303
307
 
304
308
  const insertIndex =
305
309
  typeof position === "number"
@@ -96,12 +96,13 @@ export default defineAction({
96
96
  "Create a new deck, optionally already populated with slides, or atomically replace all slides in an existing deck. " +
97
97
  "For short AI-generated decks in MCP app hosts, pass all generated slides in this call so the real deck editor opens inline already populated. " +
98
98
  "For longer decks or live in-app generation, create the deck with slides: [] and then use add-slide sequentially so progress appears live. " +
99
+ "Pass presenter-only speaker notes in each slide's `notes` field; keep them out of slide HTML. " +
99
100
  "Pass deckId to replace an existing deck. " +
100
101
  "Returns the deck id, title, and slide count.",
101
102
  schema: z.object({
102
103
  title: z.string().describe("Deck title"),
103
104
  slides: SlidesSchema.describe(
104
- "Array of slides with id, content (HTML), and optional layout",
105
+ "Array of slides with id, content (HTML), optional layout, and presenter-only notes",
105
106
  ),
106
107
  deckId: z
107
108
  .string()
@@ -376,6 +376,7 @@ export async function startDeckGeneration({
376
376
  `After reading any requested or attached reference material, but before adding the first slide, choose a concise, specific deck title from the user's request and source material. Never use the deck id, run id, file id, uploaded filename, or another opaque alphanumeric token as the title. Do not reuse a generic placeholder like "Untitled scene" when the content or reference context gives you a better title. Call \`patch-deck\` with \`deckId: \"${deckId}\"\` and \`operations: [{ \"op\": \"patch-deck-fields\", \"fields\": { \"title\": \"<generated title>\" } }]\`. Include only \`title\` in \`fields\`; omit all other optional fields. Never leave a generated deck named \"Untitled Deck\" or another placeholder.`,
377
377
  "If the user asks for a standalone visual, diagram, hero, one-pager, poster, or a couple of visuals, create only the requested one/few polished visual slides. Do not pad the result into a full presentation.",
378
378
  "If the request is for a presentation or deck and does not explicitly ask for one slide, infer a coherent multi-slide outline from the scope and keep adding slides until that outline is complete. Do not stop after the first slide just because the prompt has few explicit instructions.",
379
+ "When the user requests speaker notes, write presenter-only text into each slide's `notes` field and keep it out of the slide HTML.",
379
380
  `Add slides ONE AT A TIME using the \`add-slide\` action with --deckId=${deckId}. Wait for each \`add-slide\` result before calling it again; do not batch or parallelize slide writes.`,
380
381
  "Use create-deck and add-slide for this already-created deck. Do not call the legacy generate-slides-ai action: it returns Markdown drafts rather than persisted rendered slide HTML. Treat each successful add-slide result as confirmation to continue with the next planned slide.",
381
382
  ].join("\n");
@@ -174,6 +174,27 @@ function MissingDeckAccessPane({
174
174
  );
175
175
  }
176
176
 
177
+ // The Cmd/Ctrl+C-then-V slide-duplicate shortcut can only tell "this key
178
+ // event targets the slide rail/canvas" apart from "focus fell back to
179
+ // nothing because a panel/dialog elsewhere just closed" by checking a
180
+ // deny-list of known text surfaces — and that list can never be complete
181
+ // (see the Andrew Rohman Slack thread this guards against: a slide copied
182
+ // once early in a session kept silently re-duplicating on unrelated later
183
+ // pastes). Bounding how long a copy stays "armed" turns a missed deny-list
184
+ // entry from a silent, indefinite landmine into, at worst, a narrow window
185
+ // that still covers the real copy-then-paste gesture.
186
+ export const SLIDE_CLIPBOARD_ARM_WINDOW_MS = 30_000;
187
+
188
+ /** True when a Cmd/Ctrl+V should still be treated as "paste the slide that
189
+ * was just copied" rather than unrelated clipboard activity landing outside
190
+ * every recognized text field. */
191
+ export function isSlideClipboardStillArmed(
192
+ armedAt: number | null,
193
+ now: number = Date.now(),
194
+ ): boolean {
195
+ return armedAt !== null && now - armedAt <= SLIDE_CLIPBOARD_ARM_WINDOW_MS;
196
+ }
197
+
177
198
  export default function DeckEditor() {
178
199
  const t = useT();
179
200
  const { id } = useParams<{ id: string }>();
@@ -861,6 +882,11 @@ export default function DeckEditor() {
861
882
  // (rather than just an id) so paste still works after Cut has already
862
883
  // removed the original slide from the deck.
863
884
  const slideClipboardRef = useRef<Slide | null>(null);
885
+ // Only gates the ambient document-level Cmd/Ctrl+V shortcut below — the
886
+ // rail's right-click "Paste" menu item is an explicit click with no
887
+ // ambiguity risk, so it keeps working off `hasSlideClipboard` alone however
888
+ // long ago the copy happened.
889
+ const slideClipboardArmedAtRef = useRef<number | null>(null);
864
890
  const [hasSlideClipboard, setHasSlideClipboard] = useState(false);
865
891
 
866
892
  const copySlide = useCallback(
@@ -868,6 +894,7 @@ export default function DeckEditor() {
868
894
  const slide = deck?.slides.find((s) => s.id === slideId);
869
895
  if (!slide) return;
870
896
  slideClipboardRef.current = slide;
897
+ slideClipboardArmedAtRef.current = Date.now();
871
898
  setHasSlideClipboard(true);
872
899
  },
873
900
  [deck],
@@ -879,6 +906,7 @@ export default function DeckEditor() {
879
906
  const slide = deck.slides.find((s) => s.id === slideId);
880
907
  if (!slide) return;
881
908
  slideClipboardRef.current = slide;
909
+ slideClipboardArmedAtRef.current = Date.now();
882
910
  setHasSlideClipboard(true);
883
911
  const idx = deck.slides.findIndex((s) => s.id === slideId);
884
912
  const nextSlide = deck.slides[idx + 1] || deck.slides[idx - 1];
@@ -1029,7 +1057,12 @@ export default function DeckEditor() {
1029
1057
  return;
1030
1058
  }
1031
1059
 
1032
- if (!hasSlideClipboard || !activeSlideId) return;
1060
+ if (
1061
+ !hasSlideClipboard ||
1062
+ !activeSlideId ||
1063
+ !isSlideClipboardStillArmed(slideClipboardArmedAtRef.current)
1064
+ )
1065
+ return;
1033
1066
  e.preventDefault();
1034
1067
  pasteSlideAfter(activeSlideId);
1035
1068
  };
@@ -81,7 +81,9 @@ function toSharedDeck(row: {
81
81
  return {
82
82
  title: row.title || data.title || "Untitled",
83
83
  slides: Array.isArray(data.slides)
84
- ? data.slides.map((slide, index) => toSharedDeckSlide(slide, index))
84
+ ? data.slides.map((slide, index) =>
85
+ toSharedDeckSlide(slide, index, { includeNotes: false }),
86
+ )
85
87
  : [],
86
88
  aspectRatio: data.aspectRatio,
87
89
  };
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-18
4
+ ---
5
+
6
+ Generated slides now preserve requested speaker notes.
@@ -1,7 +1,6 @@
1
1
  import path from "path";
2
2
 
3
3
  import { uploadFile } from "@agent-native/core/file-upload";
4
- import { getSession } from "@agent-native/core/server";
5
4
  import { runWithRequestContext } from "@agent-native/core/server";
6
5
  import { and, desc, eq } from "drizzle-orm";
7
6
  import {
@@ -13,6 +12,12 @@ import {
13
12
  import { nanoid } from "nanoid";
14
13
 
15
14
  import { getDb, schema } from "../db/index.js";
15
+ import {
16
+ resolveSlidesRequestAuth,
17
+ type SlidesRequestAuthContext,
18
+ } from "./request-auth-context.js";
19
+
20
+ type AuthedSlidesSession = SlidesRequestAuthContext & { email: string };
16
21
 
17
22
  export const MAX_ASSET_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
18
23
 
@@ -32,13 +37,19 @@ export interface ListedUploadedAsset {
32
37
  createdAt: string;
33
38
  }
34
39
 
35
- async function requireSession(event: Parameters<typeof getSession>[0]) {
36
- const session = await getSession(event).catch(() => null);
37
- if (!session?.email) {
40
+ async function requireSession(
41
+ event: Parameters<typeof resolveSlidesRequestAuth>[0],
42
+ ): Promise<{ session: AuthedSlidesSession | null; error: string | null }> {
43
+ const auth = await resolveSlidesRequestAuth(event);
44
+ if (!auth.ok) {
45
+ setResponseStatus(event, auth.statusCode);
46
+ return { session: null, error: auth.error };
47
+ }
48
+ if (!auth.context.email) {
38
49
  setResponseStatus(event, 401);
39
- return null;
50
+ return { session: null, error: "Unauthorized" };
40
51
  }
41
- return session;
52
+ return { session: auth.context as AuthedSlidesSession, error: null };
42
53
  }
43
54
 
44
55
  function isRasterAssetExtension(ext: string): boolean {
@@ -180,9 +191,9 @@ export async function uploadImageAsset(args: {
180
191
  * framework provider chain, return its hosted URL.
181
192
  */
182
193
  export const uploadAsset = defineEventHandler(async (event) => {
183
- const session = await requireSession(event);
194
+ const { session, error: authError } = await requireSession(event);
184
195
  if (!session) {
185
- return { error: "Unauthorized" };
196
+ return { error: authError };
186
197
  }
187
198
 
188
199
  const parts = await readMultipartFormData(event);
@@ -217,9 +228,9 @@ export const uploadAsset = defineEventHandler(async (event) => {
217
228
  * GET /api/assets — list assets this user has uploaded, most recent first.
218
229
  */
219
230
  export const listAssets = defineEventHandler(async (event) => {
220
- const session = await requireSession(event);
231
+ const { session, error } = await requireSession(event);
221
232
  if (!session) {
222
- return { error: "Unauthorized" };
233
+ return { error };
223
234
  }
224
235
  const db = getDb();
225
236
  const rows: ListedUploadedAsset[] = await db
@@ -244,9 +255,9 @@ export const listAssets = defineEventHandler(async (event) => {
244
255
  * that provider's own API — but it no longer appears in this app's library.
245
256
  */
246
257
  export const deleteAsset = defineEventHandler(async (event) => {
247
- const session = await requireSession(event);
258
+ const { session, error } = await requireSession(event);
248
259
  if (!session) {
249
- return { error: "Unauthorized" };
260
+ return { error };
250
261
  }
251
262
  const id = getRouterParam(event, "id");
252
263
  if (!id) {
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  FeatureNotConfiguredError,
3
3
  fetchBuilderDesignSystemDecodeJobStatus,
4
- getSession,
5
4
  } from "@agent-native/core/server";
6
5
  import { defineEventHandler, getQuery, setResponseStatus } from "h3";
7
6
 
8
- import { withSlidesRequestContext } from "./request-auth-context.js";
7
+ import {
8
+ resolveSlidesRequestAuth,
9
+ withSlidesRequestContext,
10
+ } from "./request-auth-context.js";
9
11
 
10
12
  /**
11
13
  * Reads a Builder design-system decode job's status. The UI polls this after
@@ -13,8 +15,13 @@ import { withSlidesRequestContext } from "./request-auth-context.js";
13
15
  * job leaves `pending` and exposes a `branchUrl` (or reports an `error`).
14
16
  */
15
17
  export const designSystemDecodeJobStatus = defineEventHandler(async (event) => {
16
- const session = await getSession(event).catch(() => null);
17
- if (!session?.email) {
18
+ const auth = await resolveSlidesRequestAuth(event);
19
+ if (!auth.ok) {
20
+ setResponseStatus(event, auth.statusCode);
21
+ return { error: auth.error };
22
+ }
23
+ const session = auth.context;
24
+ if (!session.email) {
18
25
  setResponseStatus(event, 401);
19
26
  return { error: "Unauthorized" };
20
27
  }
@@ -26,8 +33,10 @@ export const designSystemDecodeJobStatus = defineEventHandler(async (event) => {
26
33
  }
27
34
 
28
35
  try {
29
- return await withSlidesRequestContext(event, () =>
30
- fetchBuilderDesignSystemDecodeJobStatus(jobId.trim()),
36
+ return await withSlidesRequestContext(
37
+ event,
38
+ () => fetchBuilderDesignSystemDecodeJobStatus(jobId.trim()),
39
+ session,
31
40
  );
32
41
  } catch (err) {
33
42
  if (err instanceof FeatureNotConfiguredError) {
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  FeatureNotConfiguredError,
3
- getSession,
4
3
  startBuilderDesignSystemUpload,
5
4
  } from "@agent-native/core/server";
6
5
  import { defineEventHandler, readBody, setResponseStatus } from "h3";
7
6
 
8
- import { withSlidesRequestContext } from "./request-auth-context.js";
7
+ import {
8
+ resolveSlidesRequestAuth,
9
+ withSlidesRequestContext,
10
+ } from "./request-auth-context.js";
9
11
 
10
12
  const MAX_FIG_BYTES = 512 * 1024 * 1024;
11
13
 
@@ -21,8 +23,13 @@ interface AttachmentInput {
21
23
  * server; the file bytes never do.
22
24
  */
23
25
  export const designSystemUploadStart = defineEventHandler(async (event) => {
24
- const session = await getSession(event).catch(() => null);
25
- if (!session?.email) {
26
+ const auth = await resolveSlidesRequestAuth(event);
27
+ if (!auth.ok) {
28
+ setResponseStatus(event, auth.statusCode);
29
+ return { error: auth.error };
30
+ }
31
+ const session = auth.context;
32
+ if (!session.email) {
26
33
  setResponseStatus(event, 401);
27
34
  return { error: "Unauthorized" };
28
35
  }
@@ -66,8 +73,10 @@ export const designSystemUploadStart = defineEventHandler(async (event) => {
66
73
  }
67
74
 
68
75
  try {
69
- const uploads = await withSlidesRequestContext(event, () =>
70
- startBuilderDesignSystemUpload(attachments),
76
+ const uploads = await withSlidesRequestContext(
77
+ event,
78
+ () => startBuilderDesignSystemUpload(attachments),
79
+ session,
71
80
  );
72
81
  return { uploads };
73
82
  } catch (err) {
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  FeatureNotConfiguredError,
3
- getSession,
4
3
  indexBuilderDesignSystem,
5
4
  } from "@agent-native/core/server";
6
5
  import { defineEventHandler, readBody, setResponseStatus } from "h3";
7
6
 
8
7
  import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
9
- import { withSlidesRequestContext } from "./request-auth-context.js";
8
+ import {
9
+ resolveSlidesRequestAuth,
10
+ withSlidesRequestContext,
11
+ } from "./request-auth-context.js";
10
12
 
11
13
  /**
12
14
  * Finalizes Builder DSI indexing from upload tokens produced by the
@@ -14,8 +16,14 @@ import { withSlidesRequestContext } from "./request-auth-context.js";
14
16
  * storage; this endpoint only forwards the opaque tokens.
15
17
  */
16
18
  export const indexDesignSystemSources = defineEventHandler(async (event) => {
17
- const session = await getSession(event).catch(() => null);
18
- if (!session?.email) {
19
+ const auth = await resolveSlidesRequestAuth(event);
20
+ if (!auth.ok) {
21
+ setResponseStatus(event, auth.statusCode);
22
+ return { error: auth.error };
23
+ }
24
+ const session = auth.context;
25
+ const sessionEmail = session.email;
26
+ if (!sessionEmail) {
19
27
  setResponseStatus(event, 401);
20
28
  return { error: "Unauthorized" };
21
29
  }
@@ -45,21 +53,28 @@ export const indexDesignSystemSources = defineEventHandler(async (event) => {
45
53
  }));
46
54
 
47
55
  try {
48
- return await withSlidesRequestContext(event, async ({ email, orgId }) => {
49
- const result = await indexBuilderDesignSystem({ sources, projectName });
50
- const proxy = await upsertBuilderProxyDesignSystem({
51
- result,
52
- ownerEmail: email ?? session.email,
53
- orgId: orgId ?? null,
54
- projectName,
55
- sourceKind: "figma",
56
- });
57
- return {
58
- ...result,
59
- ...proxy,
60
- uploadedFileCount: uploadTokens.length,
61
- };
62
- });
56
+ return await withSlidesRequestContext(
57
+ event,
58
+ async ({ email, orgId }) => {
59
+ const result = await indexBuilderDesignSystem({
60
+ sources,
61
+ projectName,
62
+ });
63
+ const proxy = await upsertBuilderProxyDesignSystem({
64
+ result,
65
+ ownerEmail: email ?? sessionEmail,
66
+ orgId: orgId ?? null,
67
+ projectName,
68
+ sourceKind: "figma",
69
+ });
70
+ return {
71
+ ...result,
72
+ ...proxy,
73
+ uploadedFileCount: uploadTokens.length,
74
+ };
75
+ },
76
+ session,
77
+ );
63
78
  } catch (err) {
64
79
  if (err instanceof FeatureNotConfiguredError) {
65
80
  setResponseStatus(event, 412);
@@ -116,7 +116,7 @@ async function createShareLink(event: any, deckId: string) {
116
116
  }
117
117
 
118
118
  const slides = storedDeck.slides.map((slide: unknown, index: number) =>
119
- toSharedDeckSlide(slide, index),
119
+ toSharedDeckSlide(slide, index, { includeNotes: false }),
120
120
  );
121
121
 
122
122
  await db.insert(schema.deckShareLinks).values({
@@ -57,7 +57,7 @@ export default createAgentChatPlugin({
57
57
  return authContext.orgId === undefined ? null : authContext.orgId;
58
58
  },
59
59
  prepareRequest: prepareSlidesChatAttachments,
60
- systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow.
60
+ systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow. When speaker notes are requested, keep presenter-only text in each slide's notes field rather than the slide HTML, and preserve notes during source-preserving edits.
61
61
 
62
62
  When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck with compact=true for deck orientation. If you need slide markup, call get-deck with compact=false explicitly and only when the edit requires the full HTML. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. For a deck-wide restyle, use one patch-deck call with requireAllSourceSlides=true and one patch-slide operation with fields.content for every source slide ID; the action rejects partial coverage. Do not split a full-deck restyle into arbitrary batches or use one-by-one update-slide calls - reserve update-slide for targeted one-slide edits. After the patch succeeds, verify with get-deck using compact=true so the verification does not retransmit every slide's HTML. Do not claim a partial or initial pass. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
63
63
  For a targeted one-slide edit, prefer get-deck with the stable slideId from view-screen or compact get-deck. It returns only that slide's full HTML by default, so do not load the full deck when one slide is enough. For code-style edits, request compact=false and format=true, then call update-slide with that slide's contentHash as baseContentHash and an ordered edits list. Use exact replace, insert-before/after, replace-between, or regex-replace edits; include expectedMatches for ambiguous markers. The action applies the whole list atomically under the deck lock, so a failed required match writes nothing. Set format=true on update-slide when readable line breaks should be persisted. Use fullContent only for an intentional full rewrite. Pass compact=true when you only need a lightweight check of that slide.
@@ -77,7 +77,7 @@ export default defineEventHandler(async (event) => {
77
77
  const data = parseDeckData(deck.data);
78
78
  const slides = Array.isArray(data.slides)
79
79
  ? data.slides.map((slide: unknown, index: number) =>
80
- toSharedDeckSlide(slide, index),
80
+ toSharedDeckSlide(slide, index, { includeNotes: false }),
81
81
  )
82
82
  : [];
83
83
 
@@ -2,9 +2,10 @@ import { createReadStream } from "fs";
2
2
  import { stat } from "fs/promises";
3
3
  import path from "path";
4
4
 
5
- import { getSession, streamFile } from "@agent-native/core/server";
5
+ import { streamFile } from "@agent-native/core/server";
6
6
  import { defineEventHandler, getRouterParam, setResponseStatus } from "h3";
7
7
 
8
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
8
9
  import { tenantExportDir } from "../../../lib/tenant-files.js";
9
10
 
10
11
  const CONTENT_TYPES: Record<string, string> = {
@@ -15,8 +16,13 @@ const CONTENT_TYPES: Record<string, string> = {
15
16
  };
16
17
 
17
18
  export default defineEventHandler(async (event) => {
18
- const session = await getSession(event).catch(() => null);
19
- if (!session?.email) {
19
+ const auth = await resolveSlidesRequestAuth(event);
20
+ if (!auth.ok) {
21
+ setResponseStatus(event, auth.statusCode);
22
+ return { error: auth.error };
23
+ }
24
+ const session = auth.context;
25
+ if (!session.email) {
20
26
  setResponseStatus(event, 401);
21
27
  return { error: "Unauthorized" };
22
28
  }
@@ -1,10 +1,11 @@
1
- import { getSession, runWithRequestContext } from "@agent-native/core/server";
1
+ import { runWithRequestContext } from "@agent-native/core/server";
2
2
  import {
3
3
  defineEventHandler,
4
4
  readMultipartFormData,
5
5
  setResponseStatus,
6
6
  } from "h3";
7
7
 
8
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
8
9
  import { getGoogleDocsAccessToken } from "../../../lib/google-docs-oauth.js";
9
10
 
10
11
  const PPTX_CONTENT_TYPE =
@@ -28,8 +29,14 @@ const UPLOAD_URL =
28
29
  * See "Export Behavior" in `templates/slides/AGENTS.md` for the routing rule.
29
30
  */
30
31
  export default defineEventHandler(async (event) => {
31
- const session = await getSession(event).catch(() => null);
32
- if (!session?.email) {
32
+ const auth = await resolveSlidesRequestAuth(event);
33
+ if (!auth.ok) {
34
+ setResponseStatus(event, auth.statusCode);
35
+ return { error: auth.error };
36
+ }
37
+ const session = auth.context;
38
+ const sessionEmail = session.email;
39
+ if (!sessionEmail) {
33
40
  setResponseStatus(event, 401);
34
41
  return { error: "Unauthorized" };
35
42
  }
@@ -50,8 +57,8 @@ export default defineEventHandler(async (event) => {
50
57
  // be org-scoped vault secrets, and resolving them without the org reports the
51
58
  // integration as unconfigured.
52
59
  const account = await runWithRequestContext(
53
- { userEmail: session.email, orgId: session.orgId },
54
- () => getGoogleDocsAccessToken(session.email),
60
+ { userEmail: sessionEmail, orgId: session.orgId },
61
+ () => getGoogleDocsAccessToken(sessionEmail),
55
62
  );
56
63
  if (!account) {
57
64
  setResponseStatus(event, 409);
@@ -1,17 +1,19 @@
1
1
  import path from "path";
2
2
 
3
- import {
4
- getSession,
5
- readBody,
6
- runWithRequestContext,
7
- } from "@agent-native/core/server";
3
+ import { readBody, runWithRequestContext } from "@agent-native/core/server";
8
4
  import { defineEventHandler, setResponseStatus } from "h3";
9
5
 
10
6
  import exportHtmlAction from "../../../../actions/export-html.js";
7
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
11
8
 
12
9
  export default defineEventHandler(async (event) => {
13
- const session = await getSession(event).catch(() => null);
14
- if (!session?.email) {
10
+ const auth = await resolveSlidesRequestAuth(event);
11
+ if (!auth.ok) {
12
+ setResponseStatus(event, auth.statusCode);
13
+ return { error: auth.error };
14
+ }
15
+ const session = auth.context;
16
+ if (!session.email) {
15
17
  setResponseStatus(event, 401);
16
18
  return { error: "Unauthorized" };
17
19
  }
@@ -1,20 +1,22 @@
1
1
  import path from "path";
2
2
 
3
- import {
4
- getSession,
5
- readBody,
6
- runWithRequestContext,
7
- } from "@agent-native/core/server";
3
+ import { readBody, runWithRequestContext } from "@agent-native/core/server";
8
4
  import { defineEventHandler, setResponseStatus } from "h3";
9
5
 
10
6
  import exportPptxAction from "../../../../actions/export-pptx.js";
7
+ import { resolveSlidesRequestAuth } from "../../../handlers/request-auth-context.js";
11
8
 
12
9
  const PPTX_CONTENT_TYPE =
13
10
  "application/vnd.openxmlformats-officedocument.presentationml.presentation";
14
11
 
15
12
  export default defineEventHandler(async (event) => {
16
- const session = await getSession(event).catch(() => null);
17
- if (!session?.email) {
13
+ const auth = await resolveSlidesRequestAuth(event);
14
+ if (!auth.ok) {
15
+ setResponseStatus(event, auth.statusCode);
16
+ return { error: auth.error };
17
+ }
18
+ const session = auth.context;
19
+ if (!session.email) {
18
20
  setResponseStatus(event, 401);
19
21
  return { error: "Unauthorized" };
20
22
  }
@@ -126,6 +126,11 @@ export interface SharedDeckSlide {
126
126
  splitByParagraph?: boolean;
127
127
  }
128
128
 
129
+ export interface SharedDeckSlideOptions {
130
+ /** Include presenter-only notes for an authenticated internal read. */
131
+ includeNotes?: boolean;
132
+ }
133
+
129
134
  const SHARED_SLIDE_TRANSITIONS = new Set<SharedSlideTransition>([
130
135
  "instant",
131
136
  "none",
@@ -216,12 +221,13 @@ function normalizeSlideAnimation(
216
221
  export function toSharedDeckSlide(
217
222
  value: unknown,
218
223
  index: number,
224
+ options: SharedDeckSlideOptions = {},
219
225
  ): SharedDeckSlide {
220
226
  const slide = isRecord(value) ? value : {};
221
227
  const shared: SharedDeckSlide = {
222
228
  id: normalizeString(slide.id, `slide-${index + 1}`),
223
229
  content: normalizeString(slide.content, ""),
224
- notes: "",
230
+ notes: options.includeNotes ? normalizeString(slide.notes, "") : "",
225
231
  layout: normalizeString(slide.layout, "content"),
226
232
  };
227
233
 
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
50
50
 
51
51
  For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
52
52
 
53
- 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
53
+ 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
54
54
 
55
55
  5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
56
56