@agent-native/core 0.149.0 → 0.149.2

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 (134) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +139 -10
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  37. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  38. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +10 -1
  39. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  40. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
  41. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  42. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  43. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  44. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  45. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
  46. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  47. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  48. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  49. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  50. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  51. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  52. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  53. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  54. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  57. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  58. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  59. package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
  60. package/corpus/templates/slides/app/pages/Index.tsx +256 -98
  61. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  72. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  73. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  74. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  75. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  76. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  79. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  80. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  81. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  82. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  83. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  84. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  85. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  86. package/corpus/templates/slides/shared/api.ts +2 -1
  87. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  88. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  89. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  90. package/dist/agent/production-agent.d.ts +21 -1
  91. package/dist/agent/production-agent.js +129 -10
  92. package/dist/agent/tool-approval-store.d.ts +18 -0
  93. package/dist/agent/tool-approval-store.js +122 -0
  94. package/dist/agent/types.d.ts +1 -1
  95. package/dist/cli/code-agent-executor.js +11 -2
  96. package/dist/client/AgentPanel.js +31 -6
  97. package/dist/client/AssistantChat.d.ts +3 -2
  98. package/dist/client/AssistantChat.js +7 -20
  99. package/dist/client/agent-chat/index.d.ts +1 -1
  100. package/dist/client/agent-chat-adapter.js +11 -0
  101. package/dist/client/agent-chat.d.ts +1 -1
  102. package/dist/client/chat/markdown-renderer.js +14 -2
  103. package/dist/client/chat/message-components.d.ts +6 -2
  104. package/dist/client/chat/message-components.js +25 -47
  105. package/dist/client/chat/tool-call-display.d.ts +3 -5
  106. package/dist/client/chat/tool-call-display.js +7 -212
  107. package/dist/client/chat-first/apps-rail.js +3 -3
  108. package/dist/client/chat-first/browser-pane.js +4 -1
  109. package/dist/client/chat-first/chat-history.d.ts +3 -1
  110. package/dist/client/chat-first/chat-history.js +2 -2
  111. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  112. package/dist/client/chat-first/surface-tabs.js +52 -41
  113. package/dist/client/chat-first/types.d.ts +3 -0
  114. package/dist/client/chat-first.d.ts +10 -0
  115. package/dist/client/chat-first.js +37 -2
  116. package/dist/client/components/ui/message-scroller.js +4 -1
  117. package/dist/client/i18n.js +3 -1
  118. package/dist/client/progress/RunsTray.js +14 -1
  119. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  120. package/dist/client/sse-event-processor.js +10 -0
  121. package/dist/client/use-chat-models.js +156 -80
  122. package/dist/integrations/webhook-handler.js +1 -1
  123. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  124. package/dist/secrets/routes.d.ts +6 -6
  125. package/dist/server/auth.js +262 -31
  126. package/dist/server/onboarding-html.js +56 -9
  127. package/dist/shared/reasoning-effort.d.ts +8 -8
  128. package/dist/shared/reasoning-effort.js +10 -10
  129. package/dist/shared/streaming-text-smoothing.js +41 -11
  130. package/dist/styles/agent-native.css +19 -32
  131. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  132. package/docs/content/template-chat.mdx +1 -1
  133. package/package.json +2 -2
  134. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -0,0 +1,192 @@
1
+ import net from "node:net";
2
+
3
+ /**
4
+ * Hostnames that never belong to a legitimate slide image but are classic
5
+ * SSRF targets. Cloud metadata endpoints resolve to a link-local address
6
+ * that `isPrivateAddress` already rejects; they are listed here so the
7
+ * request is refused before a DNS query is issued.
8
+ */
9
+ const BLOCKED_HOSTNAMES = new Set([
10
+ "localhost",
11
+ "metadata.google.internal",
12
+ "metadata",
13
+ ]);
14
+
15
+ const BLOCKED_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];
16
+
17
+ /** Largest image we are willing to buffer and hand back to the browser. */
18
+ export const MAX_PROXIED_IMAGE_BYTES = 15 * 1024 * 1024;
19
+
20
+ /** Redirect hops to follow. Each hop is re-validated before it is fetched. */
21
+ export const MAX_PROXY_REDIRECTS = 3;
22
+
23
+ /**
24
+ * Whether an IP literal points somewhere inside our own infrastructure.
25
+ * Anything unroutable, loopback, link-local, or RFC1918 is refused, as is
26
+ * any address we cannot parse — unknown means unsafe here.
27
+ */
28
+ export function isPrivateAddress(address: string): boolean {
29
+ const version = net.isIP(address);
30
+
31
+ if (version === 4) {
32
+ const parts = address.split(".").map((part) => Number(part));
33
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
34
+ return true;
35
+ }
36
+ const [a, b] = parts;
37
+ if (a === 0 || a === 10 || a === 127) return true;
38
+ if (a === 169 && b === 254) return true;
39
+ if (a === 172 && b >= 16 && b <= 31) return true;
40
+ if (a === 192 && b === 168) return true;
41
+ if (a === 100 && b >= 64 && b <= 127) return true;
42
+ if (a >= 224) return true;
43
+ return false;
44
+ }
45
+
46
+ if (version === 6) {
47
+ // Classify on the expanded hextets, never on the text. `::ffff:127.0.0.1`
48
+ // and `::ffff:7f00:1` are the same address, and a prefix-matching check
49
+ // sees only the first.
50
+ const hextets = expandIpv6(address);
51
+ if (!hextets) return true;
52
+
53
+ const embedsIpv4 =
54
+ hextets.slice(0, 5).every((part) => part === 0) &&
55
+ (hextets[5] === 0xffff || hextets[5] === 0);
56
+ if (embedsIpv4 && (hextets[6] !== 0 || hextets[7] !== 0)) {
57
+ const ipv4 = [
58
+ hextets[6] >> 8,
59
+ hextets[6] & 0xff,
60
+ hextets[7] >> 8,
61
+ hextets[7] & 0xff,
62
+ ].join(".");
63
+ return isPrivateAddress(ipv4);
64
+ }
65
+
66
+ // Unspecified (::) and loopback (::1).
67
+ if (hextets.every((part) => part === 0)) return true;
68
+ if (hextets.slice(0, 7).every((part) => part === 0) && hextets[7] === 1) {
69
+ return true;
70
+ }
71
+ // Unique-local fc00::/7, link-local fe80::/10, multicast ff00::/8.
72
+ if ((hextets[0] & 0xfe00) === 0xfc00) return true;
73
+ if ((hextets[0] & 0xffc0) === 0xfe80) return true;
74
+ if ((hextets[0] & 0xff00) === 0xff00) return true;
75
+ // NAT64 well-known prefix 64:ff9b::/96 tunnels an IPv4 destination.
76
+ if (hextets[0] === 0x0064 && hextets[1] === 0xff9b) {
77
+ const ipv4 = [
78
+ hextets[6] >> 8,
79
+ hextets[6] & 0xff,
80
+ hextets[7] >> 8,
81
+ hextets[7] & 0xff,
82
+ ].join(".");
83
+ return isPrivateAddress(ipv4);
84
+ }
85
+ return false;
86
+ }
87
+
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Expand any textual IPv6 form into its 8 hextets, including `::` elision and
93
+ * a dotted IPv4 tail. Returns null when the input is not parseable.
94
+ */
95
+ export function expandIpv6(address: string): number[] | null {
96
+ // Zone indices ("fe80::1%eth0") are routing hints, not part of the address.
97
+ const bare = address.toLowerCase().split("%")[0];
98
+ if (!bare) return null;
99
+
100
+ let head = bare;
101
+ let tail = "";
102
+ const elision = bare.indexOf("::");
103
+ if (elision !== -1) {
104
+ head = bare.slice(0, elision);
105
+ tail = bare.slice(elision + 2);
106
+ }
107
+
108
+ const parseGroups = (segment: string): string[] =>
109
+ segment.length === 0 ? [] : segment.split(":");
110
+
111
+ const headGroups = parseGroups(head);
112
+ const tailGroups = parseGroups(tail);
113
+ const all = [...headGroups, ...tailGroups];
114
+
115
+ // A dotted IPv4 tail occupies the final two hextets.
116
+ const last = all[all.length - 1];
117
+ let ipv4Tail: number[] | null = null;
118
+ if (last?.includes(".")) {
119
+ if (net.isIP(last) !== 4) return null;
120
+ const octets = last.split(".").map((part) => Number(part));
121
+ if (
122
+ octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
123
+ )
124
+ return null;
125
+ ipv4Tail = [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
126
+ if (tailGroups.length > 0) tailGroups.pop();
127
+ else headGroups.pop();
128
+ }
129
+
130
+ const toHextets = (groups: string[]): number[] | null => {
131
+ const out: number[] = [];
132
+ for (const group of groups) {
133
+ if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
134
+ out.push(parseInt(group, 16));
135
+ }
136
+ return out;
137
+ };
138
+
139
+ const headParts = toHextets(headGroups);
140
+ const tailParts = toHextets(tailGroups);
141
+ if (!headParts || !tailParts) return null;
142
+
143
+ const explicit = [...headParts, ...tailParts, ...(ipv4Tail ? ipv4Tail : [])]
144
+ .length;
145
+ if (explicit > 8) return null;
146
+
147
+ if (elision === -1) {
148
+ if (explicit !== 8) return null;
149
+ return [...headParts, ...(ipv4Tail ?? [])];
150
+ }
151
+
152
+ const fill = new Array(8 - explicit).fill(0) as number[];
153
+ return [...headParts, ...fill, ...tailParts, ...(ipv4Tail ?? [])];
154
+ }
155
+
156
+ /**
157
+ * Parse a caller-supplied image URL, refusing anything that is not a plain
158
+ * public http(s) resource. Returns null rather than throwing so callers can
159
+ * answer with a single 400.
160
+ */
161
+ export function parseProxyableImageUrl(raw: string): URL | null {
162
+ if (!raw) return null;
163
+
164
+ let url: URL;
165
+ try {
166
+ url = new URL(raw);
167
+ } catch {
168
+ // coercion-ok: null is this function's documented "refused" result and
169
+ // the caller answers 400; an unparseable URL carries no other detail.
170
+ return null;
171
+ }
172
+
173
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
174
+ // Credentials in the URL would be replayed by the server on the user's
175
+ // behalf against a host they may not control.
176
+ if (url.username || url.password) return null;
177
+
178
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
179
+ if (!host) return null;
180
+ if (BLOCKED_HOSTNAMES.has(host)) return null;
181
+ if (BLOCKED_SUFFIXES.some((suffix) => host.endsWith(suffix))) return null;
182
+
183
+ const literal = host.startsWith("[") ? host.slice(1, -1) : host;
184
+ if (net.isIP(literal) && isPrivateAddress(literal)) return null;
185
+
186
+ return url;
187
+ }
188
+
189
+ // A hostname is no longer validated here before fetching. Resolving once for
190
+ // the check and again for the connection let a DNS-rebinding host answer
191
+ // differently each time, so the address check now lives in the socket's
192
+ // `lookup` — see `publicOnlyLookup` in fetch-remote-image.ts.
@@ -52,9 +52,11 @@ export default createAgentChatPlugin({
52
52
  prepareRequest: prepareSlidesChatAttachments,
53
53
  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.
54
54
 
55
- 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. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. Use update-slide on existing slide IDs. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. Verify the same slide IDs and count with get-deck after editing. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
55
+ 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. 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.
56
56
 
57
- For source-faithful PDF slides, keep the original full-page image and style around it with restrained design-system chrome such as a frame, edge treatment, caption, or safe overlay that does not obscure source content; never OCR-reconstruct the page from extracted text. For PPTX slides, preserve the imported positioned HTML and every uploaded source image. The update-slide action enforces these preservation rules by default; pass preserveSource=false only when the user explicitly requests a rewrite of that slide.
57
+ When the active Slides editor is already showing the deck you just changed, do not include an "Open the updated deck" or similar link in the final response. The deck is already open and the action updated it in place; say that plainly instead. Only provide an open-deck link when the user is elsewhere, the active deck is different, or the user explicitly asks for a link.
58
+
59
+ For source-faithful PDF slides, keep the original full-page image and style around it with restrained design-system chrome such as a frame, edge treatment, caption, or safe overlay that does not obscure source content; never OCR-reconstruct the page from extracted text. For PPTX slides, preserve the imported positioned HTML and every uploaded source image. The patch-deck and update-slide actions enforce these preservation rules by default; pass preserveSource=false only when the user explicitly requests a rewrite of that slide.
58
60
 
59
61
  If the deck has designSystemId, call get-design-system before writing and follow its exact agentContext tokens, assets, and custom instructions. If the user asks for on-brand styling and no design system is linked, call get-workspace-defaults, link its usable design system with patch-deck, then call get-design-system. Do not improvise a generic Builder-like palette when configured Builder.io design-system context is available.
60
62
 
@@ -68,7 +70,7 @@ Provider-specific Slides actions are shortcuts, not limits. If a first-class act
68
70
 
69
71
  Slides' Google Drive provider API uses the user's connected Google Docs OAuth account. Picker imports keep the per-file drive.file path, while pasted Google Slides links use Drive export access and may ask the user to reconnect Google. For large Drive file lists or metadata sweeps, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.
70
72
 
71
- When a Google Drive or Google Slides request needs authentication, tell the user to use the Connect Google button shown in Slides. Do not expose internal routes, API endpoints, OAuth setup instructions, client IDs, or keys in the response, and do not ask the user to configure an API manually.`,
73
+ When a Google Drive or Google Slides request needs authentication, tell the user to use the Connect Google button in the Google Slides import step. Do not expose internal routes, API endpoints, OAuth setup instructions, client IDs, or keys in the response, and do not ask the user to configure an API manually.`,
72
74
  mentionProviders: async () => {
73
75
  const { getDb } = await import("../db/index.js");
74
76
  const { decks, deckShares } = await import("../db/schema.js");
@@ -0,0 +1,65 @@
1
+ import { getSession } from "@agent-native/core/server";
2
+ import { defineEventHandler, getQuery, setResponseStatus } from "h3";
3
+
4
+ import {
5
+ fetchRemoteImage,
6
+ type RemoteImageFailure,
7
+ } from "../../lib/fetch-remote-image.js";
8
+
9
+ /**
10
+ * Re-serve a remote image from our own origin.
11
+ *
12
+ * PDF/PPTX export rasterizes the slide DOM through a canvas, and the browser
13
+ * blanks out any image whose host does not send `Access-Control-Allow-Origin`.
14
+ * No client-side flag can override that, so images on hosts without CORS have
15
+ * to come back through us to be same-origin.
16
+ *
17
+ * This is an authenticated, image-only, size-capped fetcher — not a general
18
+ * proxy. See `fetch-remote-image.ts` for the address pinning that keeps it
19
+ * from being turned into an SSRF primitive.
20
+ */
21
+ const FAILURE_STATUS: Record<RemoteImageFailure, number> = {
22
+ "unsupported-url": 400,
23
+ "blocked-address": 400,
24
+ "fetch-failed": 502,
25
+ "too-many-redirects": 502,
26
+ "not-an-image": 415,
27
+ "too-large": 413,
28
+ };
29
+
30
+ const FAILURE_MESSAGE: Record<RemoteImageFailure, string> = {
31
+ "unsupported-url": "Unsupported image URL",
32
+ "blocked-address": "Unsupported image URL",
33
+ "fetch-failed": "Could not fetch image",
34
+ "too-many-redirects": "Too many redirects",
35
+ "not-an-image": "Not an image",
36
+ "too-large": "Image too large",
37
+ };
38
+
39
+ export default defineEventHandler(async (event) => {
40
+ const session = await getSession(event);
41
+ if (!session?.email) {
42
+ setResponseStatus(event, 401);
43
+ return { error: "Unauthorized" };
44
+ }
45
+
46
+ const raw = getQuery(event).url;
47
+ if (typeof raw !== "string") {
48
+ setResponseStatus(event, 400);
49
+ return { error: "Missing url" };
50
+ }
51
+
52
+ const result = await fetchRemoteImage(raw);
53
+ if (!result.ok) {
54
+ setResponseStatus(event, FAILURE_STATUS[result.reason]);
55
+ return { error: FAILURE_MESSAGE[result.reason] };
56
+ }
57
+
58
+ event.node?.res?.setHeader("Content-Type", result.contentType);
59
+ event.node?.res?.setHeader("Content-Length", String(result.body.byteLength));
60
+ event.node?.res?.setHeader("Cache-Control", "private, max-age=3600");
61
+ // The canvas reads these pixels back, so the response must be explicitly
62
+ // usable cross-origin even though it is served from our own host.
63
+ event.node?.res?.setHeader("Access-Control-Allow-Origin", "*");
64
+ return result.body;
65
+ });
@@ -137,7 +137,7 @@ function normalizeString(value: unknown, fallback: string): string {
137
137
  }
138
138
 
139
139
  function normalizeElementPath(value: unknown): number[] | undefined {
140
- if (!Array.isArray(value)) return undefined;
140
+ if (!Array.isArray(value) || value.length === 0) return undefined;
141
141
  const path = value.filter(
142
142
  (part): part is number =>
143
143
  typeof part === "number" &&
@@ -155,6 +155,7 @@ function normalizeSlideAnimation(
155
155
  if (!isRecord(value)) return null;
156
156
 
157
157
  const elementPath = normalizeElementPath(value.elementPath);
158
+ if (value.elementPath !== undefined && !elementPath) return null;
158
159
  const rawElementIndex = value.elementIndex;
159
160
  const hasElementIndex =
160
161
  typeof rawElementIndex === "number" &&
@@ -0,0 +1,135 @@
1
+ const GENERATED_TITLE_PLACEHOLDERS = new Set([
2
+ "deck",
3
+ "date",
4
+ "image slide title",
5
+ "presentation title",
6
+ "section",
7
+ "section title",
8
+ "slide title",
9
+ "untitled",
10
+ "untitled deck",
11
+ "your name",
12
+ ]);
13
+
14
+ /**
15
+ * Generated deck ids should never become user-facing titles. Keep this
16
+ * deliberately narrow so normal titles with spaces and punctuation remain
17
+ * valid, while catching opaque mixed-case tokens such as H3sVsnns-TEVUOpz9w.
18
+ */
19
+ const OPAQUE_DECK_TITLE_PATTERN =
20
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z0-9_-]{12,64}$/;
21
+
22
+ export const DEFAULT_DECK_TITLE = "Untitled Deck";
23
+
24
+ export function isOpaqueDeckTitle(value: unknown): value is string {
25
+ return (
26
+ typeof value === "string" && OPAQUE_DECK_TITLE_PATTERN.test(value.trim())
27
+ );
28
+ }
29
+
30
+ export function isGeneratedDeckTitle(value: unknown): value is string {
31
+ if (typeof value !== "string") return false;
32
+ const title = value.trim();
33
+ return (
34
+ GENERATED_TITLE_PLACEHOLDERS.has(title.toLowerCase()) ||
35
+ isOpaqueDeckTitle(title)
36
+ );
37
+ }
38
+
39
+ function decodeHtmlEntities(value: string): string {
40
+ const decodeCodePoint = (raw: string, radix: number): string => {
41
+ const codePoint = Number.parseInt(raw, radix);
42
+ return Number.isNaN(codePoint) || codePoint > 0x10ffff
43
+ ? ""
44
+ : String.fromCodePoint(codePoint);
45
+ };
46
+
47
+ return value
48
+ .replace(/&nbsp;/gi, " ")
49
+ .replace(/&amp;/gi, "&")
50
+ .replace(/&lt;/gi, "<")
51
+ .replace(/&gt;/gi, ">")
52
+ .replace(/&quot;/gi, '"')
53
+ .replace(/(?:&#39;|&apos;)/gi, "'")
54
+ .replace(/&#x([0-9a-f]+);?/gi, (_, hex: string) => decodeCodePoint(hex, 16))
55
+ .replace(/&#(\d+);?/g, (_, digits: string) => decodeCodePoint(digits, 10));
56
+ }
57
+
58
+ function plainText(value: string): string {
59
+ return decodeHtmlEntities(
60
+ value.replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, " "),
61
+ )
62
+ .replace(/\s+/g, " ")
63
+ .trim();
64
+ }
65
+
66
+ function usableCandidate(value: string): string | null {
67
+ const candidate = plainText(value).replace(/^[•●▪‣\-\s]+/, "");
68
+ if (!candidate || candidate.length > 140) return null;
69
+ if (GENERATED_TITLE_PLACEHOLDERS.has(candidate.toLowerCase())) return null;
70
+ return candidate;
71
+ }
72
+
73
+ /**
74
+ * Recover a deck title from the largest title-like text in its first slide.
75
+ * This is intentionally HTML-string based because the action runs on the
76
+ * server without a browser DOM.
77
+ */
78
+ export function deriveDeckTitleFromSlideContent(
79
+ content: unknown,
80
+ ): string | null {
81
+ if (typeof content !== "string" || !content.trim()) return null;
82
+
83
+ const candidates: Array<{ score: number; text: string }> = [];
84
+ const headingPattern = /<(h[1-6])\b[^>]*>([\s\S]*?)<\/\1>/gi;
85
+ for (const match of content.matchAll(headingPattern)) {
86
+ const text = usableCandidate(match[2] ?? "");
87
+ if (text) {
88
+ const level = Number.parseInt(match[1].slice(1), 10);
89
+ candidates.push({ score: 1000 - level * 10, text });
90
+ }
91
+ }
92
+
93
+ const styledTextPattern =
94
+ /<([a-z][\w:-]*)\b[^>]*\bstyle\s*=\s*(["'])([\s\S]*?)\2[^>]*>([\s\S]*?)<\/\1>/gi;
95
+ for (const match of content.matchAll(styledTextPattern)) {
96
+ const fontSize = Number.parseFloat(
97
+ match[3]?.match(/(?:^|;)\s*font-size\s*:\s*([\d.]+)px/i)?.[1] ?? "0",
98
+ );
99
+ if (fontSize < 28) continue;
100
+ const text = usableCandidate(match[4] ?? "");
101
+ if (text) candidates.push({ score: fontSize, text });
102
+ }
103
+
104
+ return candidates.sort((a, b) => b.score - a.score)[0]?.text ?? null;
105
+ }
106
+
107
+ /**
108
+ * Return a human-readable replacement only when the requested title is a
109
+ * generated placeholder or opaque id. A meaningful existing title wins when
110
+ * a stale full-payload save tries to replace it with a generated value.
111
+ */
112
+ export function repairGeneratedDeckTitle(
113
+ requestedTitle: unknown,
114
+ firstSlideContent: unknown,
115
+ existingTitle?: unknown,
116
+ ): string | null {
117
+ if (!isGeneratedDeckTitle(requestedTitle)) return null;
118
+
119
+ return (
120
+ deriveDeckTitleFromSlideContent(firstSlideContent) ??
121
+ (typeof existingTitle === "string" &&
122
+ existingTitle.trim() &&
123
+ !isGeneratedDeckTitle(existingTitle)
124
+ ? existingTitle
125
+ : null)
126
+ );
127
+ }
128
+
129
+ export function assertHumanReadableDeckTitle(title: string): void {
130
+ if (isOpaqueDeckTitle(title)) {
131
+ throw new Error(
132
+ "Deck title must be a concise, human-readable title; generated ids are not valid titles.",
133
+ );
134
+ }
135
+ }
@@ -89,7 +89,10 @@ export function TaskListRow({
89
89
  onAnimationEnd={completion.handleRowAnimationEnd}
90
90
  dataAttributes={{ "data-task-id": item.id }}
91
91
  className={cn(
92
- "will-change-[opacity,transform]",
92
+ // Scoped to the phases that actually animate. Applied unconditionally
93
+ // it promoted a layer for every row in the list and never retired any
94
+ // of them, for an animation that only runs while a row completes.
95
+ completionPhase !== "idle" && "will-change-[opacity,transform]",
93
96
  displayDone && completionPhase === "idle" && !selected && "opacity-60",
94
97
  completionPhase === "completing" &&
95
98
  hideAfterComplete &&
@@ -138,7 +138,7 @@ function googleThinkingBudget(effort) {
138
138
  return -1;
139
139
  }
140
140
  /**
141
- * Map a reasoning effort level to Gemini 3.x thinkingLevel string.
141
+ * Map an effort level to Gemini 3.x thinkingLevel string.
142
142
  * Gemini 3 models (gemini-3.*) reject thinkingBudget and require thinkingLevel
143
143
  * with values 'low' | 'medium' | 'high'. Gemini 3.0 only supports 'low'/'high';
144
144
  * Gemini 3.1+ adds 'medium'. We always emit 'medium' for medium effort since it
@@ -266,7 +266,7 @@ export interface ProductionAgentOptions {
266
266
  model?: string;
267
267
  /** App/template id used for org-scoped per-app model defaults. */
268
268
  appId?: string;
269
- /** Default reasoning effort for requests that do not supply an override. */
269
+ /** Default effort for requests that do not supply an override. */
270
270
  reasoningEffort?: ReasoningEffort;
271
271
  /** Provider-specific options passed through to the engine */
272
272
  providerOptions?: EngineMessage extends never ? never : any;
@@ -632,8 +632,16 @@ export interface ExecuteAgentToolCallOptions {
632
632
  threadId?: string;
633
633
  turnId?: string;
634
634
  approvedToolCalls?: string[];
635
+ onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
636
+ consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
635
637
  send?: (event: AgentChatEvent) => void;
636
638
  }
639
+ export interface AgentApprovalBinding {
640
+ toolName: string;
641
+ input: unknown;
642
+ callId: string;
643
+ approvalKey: string;
644
+ }
637
645
  /**
638
646
  * Execute one externally-selected tool through the exact same guarded runtime
639
647
  * as a normal model-selected tool call. Realtime voice and other duplex
@@ -658,6 +666,11 @@ export declare function buildFirstRequestPayloadDetail(input: {
658
666
  availableToolCount: number;
659
667
  }): string;
660
668
  export declare function toolCallCacheKey(toolName: string, input: unknown): string;
669
+ export declare function findApprovedStructuredToolCall(history: AgentChatStructuredMessage[] | undefined, approvedToolCalls: readonly string[] | undefined): {
670
+ name: string;
671
+ input: unknown;
672
+ callId: string;
673
+ } | undefined;
661
674
  export declare function repeatedSourceSweepGuardMessage(opts: {
662
675
  toolName: string;
663
676
  priorCalls: number;
@@ -769,6 +782,13 @@ export declare function runAgentLoop(opts: {
769
782
  * `approval_required`. See `AgentChatRequest.approvedToolCalls`.
770
783
  */
771
784
  approvedToolCalls?: string[];
785
+ /**
786
+ * Server-side approval persistence hooks. The HTTP transport supplies these
787
+ * so client-supplied history is never the authorization source; direct loop
788
+ * callers may omit them when they own the surrounding approval boundary.
789
+ */
790
+ onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<void>;
791
+ consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
772
792
  /**
773
793
  * In-loop processor seam (see `processors.ts`). Each processor can observe
774
794
  * streamed chunks, observe model responses around tool execution, and