@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,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Generated decks keep a clear, human-readable title
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ New deck creation now shows progress, opens the editor directly, and remembers your reference choices
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Presentation reveals now follow validated ordered targets, queue rapid navigation, and settle cleanly after transitions.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Slides editing controls are consolidated into one focused menu
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Slides now verify and restyle every imported slide in one pass
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Slides only suggests a Google connection for relevant requests, and the suggestion can be dismissed.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: changed
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ The Google Slides import option is now labeled Slides.
@@ -1,3 +1,5 @@
1
+ import { ASPECT_RATIOS } from "@shared/aspect-ratios";
2
+
1
3
  import type {
2
4
  ParsedElement,
3
5
  ParsedParagraph,
@@ -165,10 +167,37 @@ export function convertToSlideHtml(
165
167
 
166
168
  const DEFAULT_SLIDE_WIDTH_EMU = 9144000;
167
169
  const DEFAULT_SLIDE_HEIGHT_EMU = 5143500;
168
- const CSS_PX_PER_POINT = 96 / 72;
169
170
  const DEFAULT_PPTX_BACKGROUND = "#000000"; // guard:allow-raw-color - preserve PPTX black when no background is declared
170
171
  const DEFAULT_PPTX_FOREGROUND = "#ffffff"; // guard:allow-raw-color - preserve PPTX white when no run color is declared
171
172
 
173
+ /**
174
+ * The absolute px box `toSlidePxX`/`toSlidePxY` scale positions and sizes
175
+ * against. It must match the aspect-ratio preset the deck actually renders
176
+ * into (`ASPECT_RATIOS`, chosen by the import actions' own
177
+ * `nearestAspectRatio`) rather than a fixed 16:9 box: a PDF page or a custom
178
+ * PPTX slide size is routinely portrait or square, and scaling its elements
179
+ * against a 960x540 reference while the deck itself renders in an 864x1080
180
+ * (or other) box stretches every element by the ratio between the two
181
+ * boxes, most visibly squashing everything into the top fraction of a
182
+ * taller-than-540 canvas.
183
+ */
184
+ function referenceBoxForSlide(
185
+ widthEmu: number,
186
+ heightEmu: number,
187
+ ): { width: number; height: number } {
188
+ const target = widthEmu / heightEmu;
189
+ let best: { width: number; height: number } = ASPECT_RATIOS["16:9"];
190
+ let bestDiff = Infinity;
191
+ for (const preset of Object.values(ASPECT_RATIOS)) {
192
+ const diff = Math.abs(preset.width / preset.height - target);
193
+ if (diff < bestDiff) {
194
+ bestDiff = diff;
195
+ best = preset;
196
+ }
197
+ }
198
+ return { width: best.width, height: best.height };
199
+ }
200
+
172
201
  function buildFidelitySlide(
173
202
  slide: ParsedSlide,
174
203
  imageUrls: string | Record<string, string> | undefined,
@@ -176,9 +205,10 @@ function buildFidelitySlide(
176
205
  ): string {
177
206
  const widthEmu = slide.widthEmu || DEFAULT_SLIDE_WIDTH_EMU;
178
207
  const heightEmu = slide.heightEmu || DEFAULT_SLIDE_HEIGHT_EMU;
208
+ const refBox = referenceBoxForSlide(widthEmu, heightEmu);
179
209
  const background = slide.backgroundColor ?? DEFAULT_PPTX_BACKGROUND;
180
210
  const gridStyle = slide.backgroundGrid
181
- ? `background-image:linear-gradient(to right, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu))}px, transparent ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu))}px),linear-gradient(to bottom, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu))}px, transparent ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu))}px);background-size:${toSlidePxX(slide.backgroundGrid.stepXEmu, widthEmu)}px ${toSlidePxY(slide.backgroundGrid.stepYEmu, heightEmu)}px;background-position:${toSlidePxX(slide.backgroundGrid.offsetXEmu, widthEmu)}px ${toSlidePxY(slide.backgroundGrid.offsetYEmu, heightEmu)}px;background-repeat:repeat;`
211
+ ? `background-image:linear-gradient(to right, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu, refBox.width))}px, transparent ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu, refBox.width))}px),linear-gradient(to bottom, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu, refBox.height))}px, transparent ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu, refBox.height))}px);background-size:${toSlidePxX(slide.backgroundGrid.stepXEmu, widthEmu, refBox.width)}px ${toSlidePxY(slide.backgroundGrid.stepYEmu, heightEmu, refBox.height)}px;background-position:${toSlidePxX(slide.backgroundGrid.offsetXEmu, widthEmu, refBox.width)}px ${toSlidePxY(slide.backgroundGrid.offsetYEmu, heightEmu, refBox.height)}px;background-repeat:repeat;`
182
212
  : "";
183
213
  const elements = slide.elements ?? [];
184
214
  const html = elements
@@ -188,6 +218,7 @@ function buildFidelitySlide(
188
218
  index,
189
219
  widthEmu,
190
220
  heightEmu,
221
+ refBox,
191
222
  imageUrls,
192
223
  themeFont,
193
224
  ),
@@ -203,10 +234,11 @@ function buildFidelityElement(
203
234
  index: number,
204
235
  widthEmu: number,
205
236
  heightEmu: number,
237
+ refBox: { width: number; height: number },
206
238
  imageUrls: string | Record<string, string> | undefined,
207
239
  themeFont: string | undefined,
208
240
  ): string {
209
- const position = `position: absolute; left: ${toSlidePxX(element.x, widthEmu)}px; top: ${toSlidePxY(element.y, heightEmu)}px; width: ${toSlidePxX(element.width, widthEmu)}px; height: ${toSlidePxY(element.height, heightEmu)}px; z-index: ${index}; box-sizing: border-box;`;
241
+ const position = `position: absolute; left: ${toSlidePxX(element.x, widthEmu, refBox.width)}px; top: ${toSlidePxY(element.y, heightEmu, refBox.height)}px; width: ${toSlidePxX(element.width, widthEmu, refBox.width)}px; height: ${toSlidePxY(element.height, heightEmu, refBox.height)}px; z-index: ${index}; box-sizing: border-box;`;
210
242
  const rotation = element.rotation
211
243
  ? ` transform: rotate(${element.rotation}deg); transform-origin: center center;`
212
244
  : "";
@@ -218,12 +250,18 @@ function buildFidelityElement(
218
250
  return `<div class="fmd-pptx-image" data-pptx-element-kind="image" data-pptx-image-name="${esc(element.image?.name ?? "image")}"${objectId} style="${position}${rotation} overflow: hidden;">${url ? `<img src="${esc(url)}" alt="" style="${imageStyle}" />` : `<div class="fmd-img-placeholder" style="width:100%;height:100%;">Imported image: ${esc(element.image?.name ?? "image")}</div>`}</div>`;
219
251
  }
220
252
 
221
- const decoration = shapeDecoration(element, widthEmu);
253
+ const decoration = shapeDecoration(element, widthEmu, refBox.width);
222
254
  if (element.kind === "shape") {
223
255
  return `<div class="fmd-pptx-shape" data-pptx-element-kind="shape"${objectId} style="${position}${rotation}${decoration}"></div>`;
224
256
  }
225
257
 
226
- const textStyle = textBoxStyle(element, widthEmu, heightEmu, themeFont);
258
+ const textStyle = textBoxStyle(
259
+ element,
260
+ widthEmu,
261
+ heightEmu,
262
+ refBox,
263
+ themeFont,
264
+ );
227
265
  const defaultFontWeight = element.placeholderType === "title" ? 700 : 400;
228
266
  const paragraphs = (element.paragraphs ?? [])
229
267
  .map((paragraph, paragraphIndex) =>
@@ -231,6 +269,7 @@ function buildFidelityElement(
231
269
  paragraph,
232
270
  paragraphIndex,
233
271
  widthEmu,
272
+ refBox.width,
234
273
  themeFont,
235
274
  defaultFontWeight,
236
275
  ),
@@ -239,12 +278,41 @@ function buildFidelityElement(
239
278
  return `<div class="fmd-pptx-text" data-pptx-element-kind="text"${objectId} style="${position}${rotation}${decoration}${textStyle}">${paragraphs}</div>`;
240
279
  }
241
280
 
242
- function toSlidePxX(valueEmu: number, slideWidthEmu: number): number {
243
- return Math.round((valueEmu / slideWidthEmu) * 960 * 1000) / 1000;
281
+ function toSlidePxX(
282
+ valueEmu: number,
283
+ slideWidthEmu: number,
284
+ refWidthPx: number,
285
+ ): number {
286
+ return Math.round((valueEmu / slideWidthEmu) * refWidthPx * 1000) / 1000;
244
287
  }
245
288
 
246
- function toSlidePxY(valueEmu: number, slideHeightEmu: number): number {
247
- return Math.round((valueEmu / slideHeightEmu) * 540 * 1000) / 1000;
289
+ function toSlidePxY(
290
+ valueEmu: number,
291
+ slideHeightEmu: number,
292
+ refHeightPx: number,
293
+ ): number {
294
+ return Math.round((valueEmu / slideHeightEmu) * refHeightPx * 1000) / 1000;
295
+ }
296
+
297
+ const EMU_PER_POINT = 12700;
298
+
299
+ /**
300
+ * A run's font size (and paragraph spacing) is stored in points, a physical
301
+ * unit independent of the source slide's own canvas size — unlike
302
+ * position/size EMUs, a fixed `pt * 96/72` conversion doesn't know how far
303
+ * `toSlidePxX`/`toSlidePxY` scaled that canvas down (or up) to fit the
304
+ * deck's aspect-ratio box. Converting the point value to EMU first and
305
+ * running it through the same `toSlidePxX` scale keeps text sized
306
+ * proportionally to its box on every source slide size, not just the one
307
+ * physical size (10in wide) that happens to make the fixed conversion agree
308
+ * with the 16:9 preset's box.
309
+ */
310
+ function ptToSlidePx(
311
+ valuePt: number,
312
+ widthEmu: number,
313
+ refWidthPx: number,
314
+ ): number {
315
+ return toSlidePxX(valuePt * EMU_PER_POINT, widthEmu, refWidthPx);
248
316
  }
249
317
 
250
318
  function imageUrlForElement(
@@ -263,10 +331,14 @@ function imageRenderStyle(element: ParsedElement): string {
263
331
  return `display:block;position:absolute;left:${(-crop.left / visibleWidth) * 100}%;top:${(-crop.top / visibleHeight) * 100}%;width:${(1 / visibleWidth) * 100}%;height:${(1 / visibleHeight) * 100}%;object-fit:fill;`;
264
332
  }
265
333
 
266
- function shapeDecoration(element: ParsedElement, widthEmu: number): string {
334
+ function shapeDecoration(
335
+ element: ParsedElement,
336
+ widthEmu: number,
337
+ refWidthPx: number,
338
+ ): string {
267
339
  const fill = element.fill ? `background: ${esc(element.fill)};` : "";
268
340
  const line = element.lineColor
269
- ? `border: ${Math.max(1, toSlidePxX(element.lineWidth ?? 12700, widthEmu))}px solid ${esc(element.lineColor)};`
341
+ ? `border: ${Math.max(1, toSlidePxX(element.lineWidth ?? 12700, widthEmu, refWidthPx))}px solid ${esc(element.lineColor)};`
270
342
  : "";
271
343
  const radius = element.shapeType === "roundRect" ? "border-radius: 6px;" : "";
272
344
  return `${fill}${line}${radius}`;
@@ -276,13 +348,16 @@ function textBoxStyle(
276
348
  element: ParsedElement,
277
349
  widthEmu: number,
278
350
  heightEmu: number,
351
+ refBox: { width: number; height: number },
279
352
  themeFont: string | undefined,
280
353
  ): string {
281
354
  const padding = element.padding;
282
- const left = padding ? toSlidePxX(padding.left, widthEmu) : 0;
283
- const right = padding ? toSlidePxX(padding.right, widthEmu) : 0;
284
- const top = padding ? toSlidePxY(padding.top, heightEmu) : 0;
285
- const bottom = padding ? toSlidePxY(padding.bottom, heightEmu) : 0;
355
+ const left = padding ? toSlidePxX(padding.left, widthEmu, refBox.width) : 0;
356
+ const right = padding ? toSlidePxX(padding.right, widthEmu, refBox.width) : 0;
357
+ const top = padding ? toSlidePxY(padding.top, heightEmu, refBox.height) : 0;
358
+ const bottom = padding
359
+ ? toSlidePxY(padding.bottom, heightEmu, refBox.height)
360
+ : 0;
286
361
  const align = element.paragraphs?.[0]?.alignment ?? "left";
287
362
  const vertical =
288
363
  element.verticalAlign === "middle"
@@ -297,37 +372,55 @@ function buildFidelityParagraph(
297
372
  paragraph: ParsedParagraph,
298
373
  paragraphIndex: number,
299
374
  widthEmu: number,
375
+ refWidthPx: number,
300
376
  themeFont: string | undefined,
301
377
  defaultFontWeight: number,
302
378
  ): string {
303
379
  const firstRun = paragraph.runs[0];
304
- const fontSize = (firstRun?.fontSize ?? 18) * CSS_PX_PER_POINT;
380
+ const fontSize = ptToSlidePx(firstRun?.fontSize ?? 18, widthEmu, refWidthPx);
305
381
  const lineHeight = paragraph.lineSpacing ?? 1.2;
382
+ const bulletFontSize = ptToSlidePx(
383
+ paragraph.bulletSize ?? firstRun?.fontSize ?? 18,
384
+ widthEmu,
385
+ refWidthPx,
386
+ );
306
387
  const bullet = paragraph.bulletChar
307
- ? `<span aria-hidden="true" style="display:inline-block;width:${fontSize * 0.75}px;min-width:${fontSize * 0.75}px;margin-right:${fontSize * 0.65}px;color:${esc(paragraph.bulletColor ?? firstRun?.color ?? DEFAULT_PPTX_FOREGROUND)};font-family:${cssFontFamily(paragraph.bulletFontFamily ?? themeFont)};font-size:${(paragraph.bulletSize ?? firstRun?.fontSize ?? 18) * CSS_PX_PER_POINT}px;">${esc(paragraph.bulletChar)}</span>`
388
+ ? `<span aria-hidden="true" style="display:inline-block;width:${fontSize * 0.75}px;min-width:${fontSize * 0.75}px;margin-right:${fontSize * 0.65}px;color:${esc(paragraph.bulletColor ?? firstRun?.color ?? DEFAULT_PPTX_FOREGROUND)};font-family:${cssFontFamily(paragraph.bulletFontFamily ?? themeFont)};font-size:${bulletFontSize}px;">${esc(paragraph.bulletChar)}</span>`
308
389
  : "";
309
390
  const marginLeft = paragraph.marginLeftEmu
310
- ? toSlidePxX(paragraph.marginLeftEmu, widthEmu)
391
+ ? toSlidePxX(paragraph.marginLeftEmu, widthEmu, refWidthPx)
311
392
  : 0;
312
393
  const indent = paragraph.indentEmu
313
- ? toSlidePxX(paragraph.indentEmu, widthEmu)
394
+ ? toSlidePxX(paragraph.indentEmu, widthEmu, refWidthPx)
314
395
  : 0;
315
396
  const spacingBefore = paragraph.spaceBeforePt ?? 0;
316
397
  const spacingAfter = paragraph.spaceAfterPt ?? 0;
317
398
  const bulletMargin = paragraph.bulletChar ? `margin-left:${indent}px;` : "";
399
+ const marginBefore = ptToSlidePx(spacingBefore, widthEmu, refWidthPx);
400
+ const marginAfter = ptToSlidePx(spacingAfter, widthEmu, refWidthPx);
318
401
  const text = paragraph.runs
319
- .map((run) => formatFidelityRun(run, themeFont, defaultFontWeight))
402
+ .map((run) =>
403
+ formatFidelityRun(
404
+ run,
405
+ widthEmu,
406
+ refWidthPx,
407
+ themeFont,
408
+ defaultFontWeight,
409
+ ),
410
+ )
320
411
  .join("");
321
- return `<p data-pptx-paragraph="${paragraphIndex}" style="display:block;flex:0 0 auto;text-align:${paragraph.alignment ?? "left"};white-space:pre-wrap;margin:${spacingBefore * CSS_PX_PER_POINT}px 0 ${spacingAfter * CSS_PX_PER_POINT}px;line-height:${lineHeight};font-size:${fontSize}px;min-height:${fontSize * lineHeight}px;padding-left:${marginLeft}px;text-indent:${paragraph.bulletChar ? 0 : indent}px;">${bullet.replace("display:inline-block;", `display:inline-block;${bulletMargin}`)}${text}</p>`;
412
+ return `<p data-pptx-paragraph="${paragraphIndex}" style="display:block;flex:0 0 auto;text-align:${paragraph.alignment ?? "left"};white-space:pre-wrap;margin:${marginBefore}px 0 ${marginAfter}px;line-height:${lineHeight};font-size:${fontSize}px;min-height:${fontSize * lineHeight}px;padding-left:${marginLeft}px;text-indent:${paragraph.bulletChar ? 0 : indent}px;">${bullet.replace("display:inline-block;", `display:inline-block;${bulletMargin}`)}${text}</p>`;
322
413
  }
323
414
 
324
415
  function formatFidelityRun(
325
416
  run: ParsedTextRun,
417
+ widthEmu: number,
418
+ refWidthPx: number,
326
419
  themeFont: string | undefined,
327
420
  defaultFontWeight = 400,
328
421
  ): string {
329
422
  const styles = [
330
- `font-size:${(run.fontSize ?? 18) * CSS_PX_PER_POINT}px`,
423
+ `font-size:${ptToSlidePx(run.fontSize ?? 18, widthEmu, refWidthPx)}px`,
331
424
  `font-family:${cssFontFamily(run.fontFamily ?? themeFont)}`,
332
425
  `color:${esc(run.color ?? DEFAULT_PPTX_FOREGROUND)}`,
333
426
  `font-weight:${run.bold ? 700 : fontWeightForFamily(run.fontFamily, defaultFontWeight)}`,
@@ -380,14 +380,23 @@ export function mergeLineRuns(items: TextRunBox[]): TextRunBox[] {
380
380
  prev.italic === item.italic &&
381
381
  prev.underline === item.underline &&
382
382
  prev.href === item.href;
383
+ const needsSpace =
384
+ prev !== undefined && item.left - prev.right > item.fontSize * 0.25;
383
385
  if (prev && sameStyle) {
384
- const needsSpace = item.left - prev.right > item.fontSize * 0.25;
385
386
  prev.text += (needsSpace ? " " : "") + item.text;
386
387
  prev.right = Math.max(prev.right, item.right);
387
388
  prev.top = Math.min(prev.top, item.top);
388
389
  prev.bottom = Math.max(prev.bottom, item.bottom);
389
390
  } else {
390
- runs.push({ ...item });
391
+ // A word-sized gap has to survive a style change too. Only the
392
+ // same-style branch used to re-add it, so a heading whose colour
393
+ // changed mid-line ("7 Air " + "purifying") lost the space at the
394
+ // boundary and rendered as one jammed-together word.
395
+ const separator =
396
+ needsSpace && !/\s$/.test(prev?.text ?? "") && !/^\s/.test(item.text)
397
+ ? " "
398
+ : "";
399
+ runs.push({ ...item, text: separator + item.text });
391
400
  }
392
401
  }
393
402
  return runs;
@@ -119,7 +119,7 @@ export async function prepareSlidesChatAttachments(args: {
119
119
  "",
120
120
  "File handling rules:",
121
121
  "- If the request refers to the current or visible deck, call `view-screen` first to confirm the active deckId, then pass that deckId to import or slide-edit actions.",
122
- '- PPTX files: when the user wants the visible deck improved, call `import-pptx --filePath "<path>" --deckId <deckId>` first, then edit those imported slide IDs in place with update-slide. Do not rebuild the source deck with add-slide.',
122
+ '- PPTX files: when the user wants the visible deck improved, call `import-pptx --filePath "<path>" --deckId <deckId>` first, then use one patch-deck call with requireAllSourceSlides=true and one content patch per imported slide for a deck-wide restyle. Use update-slide only for a targeted one-slide edit. Do not rebuild the source deck with add-slide.',
123
123
  '- PDF and DOCX files: call `import-file --filePath "<path>" --format auto --deckId <deckId>` and use the returned extracted text as source material before creating editable slides. For a visual PDF that the user wants preserved, beautified, or restyled from its original layout, pass `--importIntoDeck true` so each page is imported source-faithfully first; keep the full-page image and style around it, with source text persisted in slide notes for inspection.',
124
124
  '- Figma `.fig` files: call `import-file --filePath "<path>" --format fig` to start Builder design-system indexing. Do not create a local design system directly from the upload.',
125
125
  "- For deck-generation requests, start mutating promptly: create or update the first slide as soon as source material is extracted, then continue slide-by-slide with add-slide/update-slide.",
@@ -0,0 +1,192 @@
1
+ import { lookup as dnsLookup, type LookupAddress } from "node:dns";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import type { LookupFunction } from "node:net";
5
+
6
+ import {
7
+ isPrivateAddress,
8
+ MAX_PROXIED_IMAGE_BYTES,
9
+ MAX_PROXY_REDIRECTS,
10
+ parseProxyableImageUrl,
11
+ } from "./image-proxy-url.js";
12
+
13
+ export type RemoteImageFailure =
14
+ | "unsupported-url"
15
+ | "blocked-address"
16
+ | "fetch-failed"
17
+ | "too-many-redirects"
18
+ | "not-an-image"
19
+ | "too-large";
20
+
21
+ export type RemoteImageResult =
22
+ | { ok: true; contentType: string; body: Buffer }
23
+ | { ok: false; reason: RemoteImageFailure };
24
+
25
+ const REQUEST_TIMEOUT_MS = 15_000;
26
+
27
+ /**
28
+ * A `lookup` implementation that only ever hands the socket an address we have
29
+ * classified as public.
30
+ *
31
+ * Validating the hostname separately and then calling `fetch` leaves a gap: the
32
+ * two resolutions are independent, so a DNS-rebinding host can answer with a
33
+ * public address for the check and a loopback or metadata address for the
34
+ * actual connection. Because Node passes this straight to `net.connect`, the
35
+ * address that is checked here is the address that gets dialled.
36
+ */
37
+ export const publicOnlyLookup: LookupFunction = (
38
+ hostname,
39
+ options,
40
+ callback,
41
+ ) => {
42
+ const wantsAll =
43
+ typeof options === "object" && options !== null && options.all === true;
44
+ const hints = typeof options === "object" && options !== null ? options : {};
45
+
46
+ dnsLookup(hostname, { ...hints, all: true }, (err, addresses) => {
47
+ if (err) {
48
+ (callback as (e: NodeJS.ErrnoException) => void)(err);
49
+ return;
50
+ }
51
+ const safe = (addresses as LookupAddress[]).filter(
52
+ (entry) => !isPrivateAddress(entry.address),
53
+ );
54
+ if (safe.length === 0) {
55
+ const blocked: NodeJS.ErrnoException = new Error(
56
+ `Refusing to connect to a non-public address for ${hostname}`,
57
+ );
58
+ blocked.code = "EBLOCKED";
59
+ (callback as (e: NodeJS.ErrnoException) => void)(blocked);
60
+ return;
61
+ }
62
+ if (wantsAll) {
63
+ (callback as unknown as (e: null, a: LookupAddress[]) => void)(
64
+ null,
65
+ safe,
66
+ );
67
+ return;
68
+ }
69
+ callback(null, safe[0].address, safe[0].family);
70
+ });
71
+ };
72
+
73
+ interface HopResult {
74
+ status: number;
75
+ headers: http.IncomingHttpHeaders;
76
+ read: () => Promise<Buffer | "too-large">;
77
+ discard: () => void;
78
+ }
79
+
80
+ function requestHop(target: URL): Promise<HopResult> {
81
+ return new Promise<HopResult>((resolve, reject) => {
82
+ const transport = target.protocol === "https:" ? https : http;
83
+ const request = transport.request(
84
+ target,
85
+ {
86
+ method: "GET",
87
+ headers: { Accept: "image/*" },
88
+ lookup: publicOnlyLookup,
89
+ },
90
+ (response) => {
91
+ resolve({
92
+ status: response.statusCode ?? 0,
93
+ headers: response.headers,
94
+ discard: () => response.destroy(),
95
+ read: () =>
96
+ new Promise<Buffer | "too-large">((resolveBody, rejectBody) => {
97
+ const chunks: Buffer[] = [];
98
+ let total = 0;
99
+ response.on("data", (chunk: Buffer) => {
100
+ total += chunk.length;
101
+ // Stop at the cap instead of buffering first and measuring
102
+ // after: a chunked response, or one that lies about
103
+ // Content-Length, would otherwise pull the whole body into
104
+ // memory before anyone checks it.
105
+ if (total > MAX_PROXIED_IMAGE_BYTES) {
106
+ response.destroy();
107
+ resolveBody("too-large");
108
+ return;
109
+ }
110
+ chunks.push(chunk);
111
+ });
112
+ response.on("end", () => resolveBody(Buffer.concat(chunks)));
113
+ response.on("error", rejectBody);
114
+ }),
115
+ });
116
+ },
117
+ );
118
+
119
+ request.setTimeout(REQUEST_TIMEOUT_MS, () => {
120
+ request.destroy(new Error("Timed out fetching image"));
121
+ });
122
+ request.on("error", reject);
123
+ request.end();
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Fetch a remote image for the proxy route. Every hop is re-parsed against the
129
+ * URL policy and every connection is pinned to a validated public address.
130
+ */
131
+ export async function fetchRemoteImage(
132
+ raw: string,
133
+ ): Promise<RemoteImageResult> {
134
+ const firstTarget = parseProxyableImageUrl(raw);
135
+ if (!firstTarget) return { ok: false, reason: "unsupported-url" };
136
+ let target: URL = firstTarget;
137
+
138
+ for (let hop = 0; hop <= MAX_PROXY_REDIRECTS; hop++) {
139
+ let response: HopResult;
140
+ try {
141
+ response = await requestHop(target);
142
+ } catch (err) {
143
+ const code = (err as NodeJS.ErrnoException)?.code;
144
+ return {
145
+ ok: false,
146
+ reason: code === "EBLOCKED" ? "blocked-address" : "fetch-failed",
147
+ };
148
+ }
149
+
150
+ if (response.status >= 300 && response.status < 400) {
151
+ response.discard();
152
+ const location = response.headers.location;
153
+ const next: URL | null = location
154
+ ? parseProxyableImageUrl(new URL(location, target).href)
155
+ : null;
156
+ if (!next) return { ok: false, reason: "fetch-failed" };
157
+ target = next;
158
+ continue;
159
+ }
160
+
161
+ if (response.status < 200 || response.status >= 300) {
162
+ response.discard();
163
+ return { ok: false, reason: "fetch-failed" };
164
+ }
165
+
166
+ const contentType = String(response.headers["content-type"] ?? "");
167
+ if (!contentType.startsWith("image/")) {
168
+ response.discard();
169
+ return { ok: false, reason: "not-an-image" };
170
+ }
171
+
172
+ const declared = Number(response.headers["content-length"]);
173
+ if (Number.isFinite(declared) && declared > MAX_PROXIED_IMAGE_BYTES) {
174
+ response.discard();
175
+ return { ok: false, reason: "too-large" };
176
+ }
177
+
178
+ let body: Buffer | "too-large";
179
+ try {
180
+ body = await response.read();
181
+ } catch {
182
+ // coercion-ok: a truncated transfer has no partial-success form here;
183
+ // the caller answers 502 either way.
184
+ return { ok: false, reason: "fetch-failed" };
185
+ }
186
+ if (body === "too-large") return { ok: false, reason: "too-large" };
187
+
188
+ return { ok: true, contentType, body };
189
+ }
190
+
191
+ return { ok: false, reason: "too-many-redirects" };
192
+ }