@agent-native/core 0.161.22 → 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 (125) 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/design/app/components/design/keyboard-shortcuts.ts +13 -7
  26. package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
  27. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
  28. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
  29. package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
  30. package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
  31. package/corpus/templates/design/playwright.config.ts +19 -2
  32. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  33. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  34. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  35. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  36. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  37. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  38. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  39. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  40. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  41. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  42. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  43. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  44. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  45. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  46. package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
  47. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  48. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  49. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  50. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  51. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  52. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  53. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  54. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  55. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  56. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  57. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  58. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  59. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  60. package/corpus/templates/slides/shared/api.ts +7 -1
  61. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  62. package/dist/agent/engine/builder-engine.js +15 -8
  63. package/dist/agent/engine/translate-anthropic.js +27 -2
  64. package/dist/agent/production-agent.d.ts +17 -2
  65. package/dist/agent/production-agent.js +48 -17
  66. package/dist/agent/thread-data-builder.d.ts +1 -0
  67. package/dist/agent/thread-data-builder.js +6 -4
  68. package/dist/agent/tool-approval-store.d.ts +10 -1
  69. package/dist/agent/tool-approval-store.js +19 -2
  70. package/dist/agent/types.d.ts +9 -0
  71. package/dist/client/AssistantChat.js +14 -6
  72. package/dist/client/EnvironmentBadge.d.ts +15 -0
  73. package/dist/client/EnvironmentBadge.js +79 -0
  74. package/dist/client/MultiTabAssistantChat.js +73 -17
  75. package/dist/client/agent-chat-adapter.js +11 -1
  76. package/dist/client/api-path.js +14 -1
  77. package/dist/client/app-providers.js +2 -1
  78. package/dist/client/chat/tool-call-display.d.ts +12 -2
  79. package/dist/client/chat/tool-call-display.js +17 -7
  80. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  81. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  82. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  83. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  84. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  85. package/dist/client/onboarding/use-onboarding.js +22 -7
  86. package/dist/client/session-replay.js +58 -15
  87. package/dist/client/settings/SettingsTabsPage.js +19 -6
  88. package/dist/client/sse-event-processor.d.ts +7 -0
  89. package/dist/client/sse-event-processor.js +4 -1
  90. package/dist/client/ui/index.d.ts +1 -0
  91. package/dist/client/ui/index.js +1 -0
  92. package/dist/client/use-action.js +26 -0
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/config.d.ts +15 -0
  96. package/dist/config.js +53 -0
  97. package/dist/extensions/fetch-tool.js +8 -2
  98. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  99. package/dist/mcp/embed-app.js +1 -1
  100. package/dist/observability/routes.d.ts +5 -5
  101. package/dist/progress/routes.d.ts +1 -1
  102. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  103. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  104. package/dist/provider-api/index.d.ts +10 -0
  105. package/dist/provider-api/index.js +45 -9
  106. package/dist/secrets/routes.d.ts +9 -9
  107. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  108. package/dist/server/auth.js +10 -3
  109. package/dist/server/poll.js +7 -3
  110. package/dist/server/sentry.d.ts +1 -1
  111. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  112. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  113. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  114. package/dist/vite/client.d.ts +5 -0
  115. package/dist/vite/client.js +64 -2
  116. package/dist/workspace-files/actions.d.ts +1 -8
  117. package/dist/workspace-files/actions.js +5 -21
  118. package/dist/workspace-files/index.d.ts +1 -1
  119. package/dist/workspace-files/index.js +1 -1
  120. package/dist/workspace-files/store.d.ts +18 -0
  121. package/dist/workspace-files/store.js +24 -3
  122. package/package.json +3 -3
  123. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  124. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  125. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
@@ -22,9 +22,24 @@ const E2E_DATABASE_URL =
22
22
  process.env.E2E_DATABASE_URL ??
23
23
  `file:${path.join(import.meta.dirname, "data", "e2e.db")}`;
24
24
  const BROWSER_CHANNEL = process.env.E2E_BROWSER_CHANNEL;
25
+ const SHOW_SECONDARY_PANELS_IN_E2E =
26
+ process.env.E2E_SHOW_DESIGN_SECONDARY_LEFT_PANELS !== "0";
27
+ const SECONDARY_PANELS_ENV = SHOW_SECONDARY_PANELS_IN_E2E
28
+ ? "VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS=1 "
29
+ : "VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS=0 ";
30
+ const ADVANCED_PANEL_SPEC_FILES = [
31
+ /canvas-tools\.spec\.ts$/,
32
+ /code-native-deep-surfaces\.spec\.ts$/,
33
+ /code-native-pr-surfaces\.spec\.ts$/,
34
+ /code-workbench-local-files\.spec\.ts$/,
35
+ ];
25
36
 
26
37
  export default defineConfig({
27
38
  testDir: "./e2e",
39
+ // These suites intentionally exercise panels that are not mounted in the
40
+ // disabled profile. Ignoring them keeps that profile focused on verifying
41
+ // the hidden-panel contract without failing on missing advanced controls.
42
+ testIgnore: SHOW_SECONDARY_PANELS_IN_E2E ? [] : ADVANCED_PANEL_SPEC_FILES,
28
43
  // The editor is heavy (iframe bridge + polling); give generous budgets.
29
44
  timeout: 90_000,
30
45
  expect: { timeout: 15_000 },
@@ -56,9 +71,11 @@ export default defineConfig({
56
71
  // generic DATABASE_URL, but set both to an absolute SQLite URL so a
57
72
  // `.env` Postgres URL or a changed command cwd can never override this
58
73
  // throwaway local db.
59
- command: `APP_NAME=design DESIGN_DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} PORT=${PORT} corepack pnpm dev`,
74
+ command: `APP_NAME=design ${SECONDARY_PANELS_ENV}DESIGN_DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} PORT=${PORT} corepack pnpm dev`,
60
75
  url: BASE_URL,
61
- reuseExistingServer: !process.env.CI,
76
+ // The panel flag is compiled into the Vite bundle. Reusing a local
77
+ // server can therefore run this profile with the opposite setting.
78
+ reuseExistingServer: false,
62
79
  // Cold Vite dep-optimization on first boot can exceed two minutes.
63
80
  timeout: 300_000,
64
81
  stdout: "ignore",
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
 
@@ -11,7 +11,10 @@ import { openCommandMenu } from "@agent-native/core/client/navigation";
11
11
  import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
12
12
  import { FeedbackButton } from "@agent-native/core/client/ui";
13
13
  import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
14
- import { normalizeMailLabel } from "@shared/gmail-labels";
14
+ import {
15
+ isInboxScopedAppLabel,
16
+ normalizeMailLabel,
17
+ } from "@shared/gmail-labels";
15
18
  import type { Label } from "@shared/types";
16
19
  import {
17
20
  IconMenu2,
@@ -171,6 +174,18 @@ function labelDepth(name: string): number {
171
174
  return Math.max(0, name.split("/").length - 1);
172
175
  }
173
176
 
177
+ // Gmail's inbox-only categories (important, social, promotions, ...) only
178
+ // ever exist inside the inbox, so their tab stays scoped there. Regular user
179
+ // labels are filed/archived independently of the inbox — routing them
180
+ // through /inbox forces `in:inbox` server-side and hides every message the
181
+ // user has archived out of the inbox while keeping the label, which reads as
182
+ // "label is empty" even though it has mail. Route those through /all so the
183
+ // label search is unscoped.
184
+ export function labelTabHref(labelId: string): string {
185
+ const view = isInboxScopedAppLabel(labelId) ? "inbox" : "all";
186
+ return `/${view}?label=${encodeURIComponent(labelId)}`;
187
+ }
188
+
174
189
  interface AppLayoutProps {
175
190
  children: React.ReactNode;
176
191
  }
@@ -590,7 +605,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
590
605
  pinnedId: id,
591
606
  label: aliasedName,
592
607
  fullLabel: lbl.name,
593
- href: `/inbox?label=${encodeURIComponent(lbl.id)}`,
608
+ href: labelTabHref(lbl.id),
594
609
  isActive: activeLabel === lbl.id,
595
610
  color: lbl.color,
596
611
  type: "label",
@@ -630,7 +645,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
630
645
  id: active.id,
631
646
  label: aliasedName,
632
647
  fullLabel: active.name,
633
- href: `/inbox?label=${encodeURIComponent(active.id)}`,
648
+ href: labelTabHref(active.id),
634
649
  isActive: true,
635
650
  color: active.color,
636
651
  type: "label",
@@ -58,23 +58,16 @@ function googleOAuthErrorPayload(
58
58
  message: string;
59
59
  code?: string;
60
60
  accountId?: string;
61
- existingOwner?: string;
62
- attemptedOwner?: string;
63
61
  } {
64
62
  if (
65
63
  error instanceof OAuthAccountOwnedByOtherUserError ||
66
64
  error?.name === "OAuthAccountOwnedByOtherUserError"
67
65
  ) {
68
66
  const account = error.accountId || "This Google account";
69
- const existingOwner = error.existingOwner || undefined;
70
- const attemptedOwner = error.attemptedOwner || undefined;
71
- const message = `${account} is connected to another login. Sign out, then sign in with ${account}.`;
72
67
  return {
73
- message,
68
+ message: `${account} is already connected to another login. Sign out, then sign in with the login that originally connected it.`,
74
69
  code: "account_owner_mismatch",
75
70
  accountId: error.accountId,
76
- existingOwner,
77
- attemptedOwner,
78
71
  };
79
72
  }
80
73
 
@@ -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
 
@@ -25,6 +25,10 @@ Context, reference deck, or source material that the app already provides.
25
25
  5. Navigate to the new deck.
26
26
  6. Call `add-slide` once per slide in slide order, waiting for each result.
27
27
 
28
+ When speaker notes are requested, put presenter-only text in each slide's
29
+ `notes` field on `create-deck` or `add-slide`; keep it out of the slide HTML.
30
+ Preserve existing notes when editing or importing a source deck.
31
+
28
32
  When the UI has already created the empty deck, keep its id and rename it before
29
33
  adding the first slide. Call `patch-deck` with a `patch-deck-fields` operation
30
34
  whose `fields.title` is the generated title. Do not leave the pre-created deck's
@@ -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
 
@@ -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) {