@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
@@ -1,3 +1,4 @@
1
+ import { sendToAgentChatAndConfirm } from "@agent-native/core/client/agent-chat";
1
2
  import { useT } from "@agent-native/core/client/i18n";
2
3
  import {
3
4
  IconBold,
@@ -6,11 +7,15 @@ import {
6
7
  IconStrikethrough,
7
8
  IconLink,
8
9
  IconPalette,
10
+ IconPencilStar,
9
11
  IconCheck,
10
12
  IconX,
13
+ IconArrowUp,
14
+ IconLoader2,
11
15
  } from "@tabler/icons-react";
12
16
  import { useEffect, useState, useRef } from "react";
13
17
  import { createPortal } from "react-dom";
18
+ import { toast } from "sonner";
14
19
 
15
20
  import {
16
21
  Tooltip,
@@ -22,6 +27,17 @@ import { shortcutLabel } from "@/lib/utils";
22
27
  interface BlockBubbleMenuProps {
23
28
  /** The element currently in contentEditable mode. Menu only shows while selection is inside it. */
24
29
  editingEl: HTMLElement | null;
30
+ /** Slide the edited block belongs to, so an AI revision can target it. */
31
+ slideId?: string;
32
+ /** Deck that owns the slide — pins the revision to the right deck. */
33
+ deckId?: string;
34
+ /**
35
+ * Ends the inline edit session and persists whatever is in the DOM now.
36
+ * Required before handing work to the agent: otherwise the still-open
37
+ * contentEditable serializes its stale text on the user's next click and
38
+ * overwrites the revision the agent just wrote.
39
+ */
40
+ onCommitInlineEdit?: () => void;
25
41
  }
26
42
 
27
43
  interface Position {
@@ -44,32 +60,89 @@ const COLORS = [
44
60
  "#EF4444",
45
61
  ];
46
62
 
63
+ /** Shown above the input so the user can confirm what the agent will rewrite. */
64
+ const AI_TARGET_PREVIEW_LIMIT = 160;
65
+
66
+ const AI_SEND_BUTTON_CLASS =
67
+ // guard:allow-raw-color — same accent as the link Apply button below.
68
+ "rounded p-1.5 text-[#609FF8] hover:bg-accent disabled:pointer-events-none disabled:opacity-40";
69
+
70
+ export function buildReviseSelectionPrompt({
71
+ selectedText,
72
+ instruction,
73
+ slideId,
74
+ deckId,
75
+ }: {
76
+ selectedText: string;
77
+ instruction: string;
78
+ slideId?: string;
79
+ deckId?: string;
80
+ }): string {
81
+ // The deck is named explicitly rather than left to "the current slide": the
82
+ // request is queued, and if the user opens another deck before the agent
83
+ // runs, an implicit target would pair this slide id with the wrong deck.
84
+ const target = [
85
+ deckId ? `Deck id: \`${deckId}\`` : null,
86
+ slideId ? `Slide id: \`${slideId}\`` : null,
87
+ ].filter((line) => line !== null);
88
+
89
+ return [
90
+ `Revise this exact text:`,
91
+ ``,
92
+ `"""`,
93
+ selectedText,
94
+ `"""`,
95
+ ``,
96
+ `How to revise it: ${instruction}`,
97
+ ...(target.length > 0 ? [``, ...target] : []),
98
+ ``,
99
+ `Read that slide first with \`view-screen\`, then make one bounded \`update-slide --fullContent\` edit that replaces only the quoted text. Leave the surrounding HTML, inline styles, and layout untouched, and keep the replacement close to the original length so the slide still fits its canvas.`,
100
+ ]
101
+ .filter((line) => line !== null)
102
+ .join("\n");
103
+ }
104
+
47
105
  /**
48
106
  * Floating formatting toolbar for contentEditable text blocks. Shows on
49
107
  * non-empty selection inside the editing element and applies inline
50
108
  * formatting (bold, italic, underline, strike, link, color) directly to
51
109
  * the DOM. Designed to work with the in-place per-block editing in
52
110
  * SlideEditor — it never mutates anything outside the editing element.
111
+ *
112
+ * The "Revise with AI" action is the exception: it does not touch the DOM.
113
+ * It hands the selected text plus the user's instruction to the agent, which
114
+ * rewrites the slide through `update-slide`.
53
115
  */
54
- export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
116
+ export function BlockBubbleMenu({
117
+ editingEl,
118
+ slideId,
119
+ deckId,
120
+ onCommitInlineEdit,
121
+ }: BlockBubbleMenuProps) {
55
122
  const t = useT();
56
123
  const [pos, setPos] = useState<Position | null>(null);
57
124
  const [showColors, setShowColors] = useState(false);
58
125
  const [showLinkInput, setShowLinkInput] = useState(false);
59
126
  const [linkValue, setLinkValue] = useState("");
127
+ const [showAiInput, setShowAiInput] = useState(false);
128
+ const [aiInstruction, setAiInstruction] = useState("");
129
+ const [aiTargetText, setAiTargetText] = useState("");
130
+ const [aiSending, setAiSending] = useState(false);
60
131
  const savedRangeRef = useRef<Range | null>(null);
61
132
  // True while a popup/input has the user's focus — keeps the menu pinned
62
133
  // even when the contentEditable selection collapses behind the scenes.
63
134
  const interactingRef = useRef(false);
64
135
  useEffect(() => {
65
- interactingRef.current = showColors || showLinkInput;
66
- }, [showColors, showLinkInput]);
136
+ interactingRef.current = showColors || showLinkInput || showAiInput;
137
+ }, [showColors, showLinkInput, showAiInput]);
67
138
 
68
139
  // Hide menu when the editing element changes
69
140
  useEffect(() => {
70
141
  setPos(null);
71
142
  setShowColors(false);
72
143
  setShowLinkInput(false);
144
+ setShowAiInput(false);
145
+ setAiInstruction("");
73
146
  }, [editingEl]);
74
147
 
75
148
  // Track selection and position the menu
@@ -158,6 +231,61 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
158
231
  setLinkValue("");
159
232
  };
160
233
 
234
+ const openAiInput = () => {
235
+ if (showAiInput) {
236
+ setShowAiInput(false);
237
+ return;
238
+ }
239
+ // Snapshot the text now: opening the input moves focus out of the
240
+ // contentEditable and the live selection collapses.
241
+ const selected = savedRangeRef.current?.toString().trim() ?? "";
242
+ if (!selected) return;
243
+ setAiTargetText(selected);
244
+ setAiInstruction("");
245
+ interactingRef.current = true;
246
+ setShowAiInput(true);
247
+ setShowColors(false);
248
+ setShowLinkInput(false);
249
+ };
250
+
251
+ const submitAiRevision = async () => {
252
+ const instruction = aiInstruction.trim();
253
+ if (!instruction || !aiTargetText || aiSending) return;
254
+
255
+ // Close the inline edit first. The block is still a live contentEditable
256
+ // session; leaving it open means the next click away serializes the old
257
+ // text over whatever the agent writes.
258
+ onCommitInlineEdit?.();
259
+
260
+ setAiSending(true);
261
+ try {
262
+ const delivery = await sendToAgentChatAndConfirm({
263
+ message: buildReviseSelectionPrompt({
264
+ selectedText: aiTargetText,
265
+ instruction,
266
+ slideId,
267
+ deckId,
268
+ }),
269
+ submit: true,
270
+ chatTarget: "local",
271
+ });
272
+
273
+ if (!delivery.delivered) {
274
+ // Keep the typed instruction so the user can retry without retyping.
275
+ toast.error(t("raw.sendToAgent"), {
276
+ description: delivery.reason ?? "The agent did not receive this.",
277
+ });
278
+ return;
279
+ }
280
+
281
+ toast.success(t("raw.sentToAgent"), { description: instruction });
282
+ setShowAiInput(false);
283
+ setAiInstruction("");
284
+ } finally {
285
+ setAiSending(false);
286
+ }
287
+ };
288
+
161
289
  return createPortal(
162
290
  <div
163
291
  data-block-bubble-menu="true"
@@ -168,6 +296,13 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
168
296
  e.preventDefault();
169
297
  }}
170
298
  >
299
+ <ToolbarButton
300
+ icon={IconPencilStar}
301
+ tooltip="Revise with AI"
302
+ onClick={openAiInput}
303
+ active={showAiInput}
304
+ />
305
+ <div className="w-px h-4 bg-border mx-0.5" />
171
306
  <ToolbarButton
172
307
  icon={IconBold}
173
308
  tooltip={`Bold (${shortcutLabel("cmd+b")})`}
@@ -199,6 +334,7 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
199
334
  if (!showColors) interactingRef.current = true;
200
335
  setShowColors((v) => !v);
201
336
  setShowLinkInput(false);
337
+ setShowAiInput(false);
202
338
  }}
203
339
  active={showColors}
204
340
  />
@@ -229,9 +365,64 @@ export function BlockBubbleMenu({ editingEl }: BlockBubbleMenuProps) {
229
365
  if (!showLinkInput) interactingRef.current = true;
230
366
  setShowLinkInput((v) => !v);
231
367
  setShowColors(false);
368
+ setShowAiInput(false);
232
369
  }}
233
370
  active={showLinkInput}
234
371
  />
372
+ {showAiInput && (
373
+ <div
374
+ data-ai-revise-input="true"
375
+ // Own mousedown handler: the toolbar above blocks the default to keep
376
+ // the contentEditable focused, which would also stop this input from
377
+ // ever receiving a caret.
378
+ onMouseDown={(e) => e.stopPropagation()}
379
+ className="absolute top-full left-1/2 -translate-x-1/2 mt-1 w-80 p-2 rounded-lg bg-popover border border-border shadow-2xl shadow-black/60"
380
+ >
381
+ <p className="mb-1.5 line-clamp-2 text-[11px] leading-snug text-muted-foreground">
382
+ {aiTargetText.length > AI_TARGET_PREVIEW_LIMIT
383
+ ? `${aiTargetText.slice(0, AI_TARGET_PREVIEW_LIMIT)}…`
384
+ : aiTargetText}
385
+ </p>
386
+ <div className="flex items-end gap-1">
387
+ <textarea
388
+ value={aiInstruction}
389
+ onChange={(e) => setAiInstruction(e.target.value)}
390
+ onKeyDown={(e) => {
391
+ if (e.key === "Enter" && !e.shiftKey) {
392
+ e.preventDefault();
393
+ void submitAiRevision();
394
+ } else if (e.key === "Escape") {
395
+ e.preventDefault();
396
+ setShowAiInput(false);
397
+ }
398
+ }}
399
+ placeholder={t("raw.tellAgentDo")}
400
+ rows={2}
401
+ disabled={aiSending}
402
+ className="flex-1 resize-none rounded border border-border bg-muted px-2 py-1.5 text-xs text-foreground outline-none focus:border-ring disabled:opacity-60"
403
+ autoFocus
404
+ />
405
+ <Tooltip>
406
+ <TooltipTrigger asChild>
407
+ <button
408
+ type="button"
409
+ onClick={() => void submitAiRevision()}
410
+ disabled={!aiInstruction.trim() || aiSending}
411
+ aria-label={t("raw.sendToAgent")}
412
+ className={AI_SEND_BUTTON_CLASS}
413
+ >
414
+ {aiSending ? (
415
+ <IconLoader2 className="w-3.5 h-3.5 animate-spin" />
416
+ ) : (
417
+ <IconArrowUp className="w-3.5 h-3.5" />
418
+ )}
419
+ </button>
420
+ </TooltipTrigger>
421
+ <TooltipContent>{t("raw.sendToAgent")}</TooltipContent>
422
+ </Tooltip>
423
+ </div>
424
+ </div>
425
+ )}
235
426
  {showLinkInput && (
236
427
  <div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 flex items-center gap-1 p-1 rounded-lg bg-popover border border-border shadow-2xl shadow-black/60">
237
428
  <input
@@ -29,7 +29,7 @@ import {
29
29
  TooltipTrigger,
30
30
  } from "@/components/ui/tooltip";
31
31
  import type { Slide } from "@/context/DeckContext";
32
- import type { AspectRatio } from "@/lib/aspect-ratios";
32
+ import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
33
33
  import { TAB_ID } from "@/lib/tab-id";
34
34
 
35
35
  import type { DesignSystemData } from "../../../shared/api";
@@ -174,6 +174,8 @@ function SortableSlideThumb({
174
174
  opacity: isDragging ? 0.5 : 1,
175
175
  };
176
176
 
177
+ const thumbDims = getAspectRatioDims(aspectRatio);
178
+
177
179
  return (
178
180
  <div ref={setNodeRef} style={style}>
179
181
  <button
@@ -213,6 +215,11 @@ function SortableSlideThumb({
213
215
 
214
216
  {/* Thumbnail */}
215
217
  <div className="flex-1 min-w-0">
218
+ {/* Each thumbnail paints a full-resolution slide canvas scaled down by
219
+ * transform, so a long rail keeps dozens of large layers live and the
220
+ * browser drops frames or paints stale tiles. `content-visibility`
221
+ * lets it skip everything below the fold; `aspect-ratio` keeps the row
222
+ * the right height while its contents are skipped. */}
216
223
  <div
217
224
  className="w-full overflow-hidden rounded border"
218
225
  style={{
@@ -220,6 +227,8 @@ function SortableSlideThumb({
220
227
  presenceUsers.length > 0
221
228
  ? presenceUsers[0].color + "66"
222
229
  : "rgba(255,255,255,0.06)",
230
+ aspectRatio: `${thumbDims.width} / ${thumbDims.height}`,
231
+ contentVisibility: "auto",
223
232
  }}
224
233
  >
225
234
  <SlideRenderer