@agent-native/core 0.149.1 → 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 (115) 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 +122 -9
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  37. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
  38. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  39. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  40. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  41. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  42. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
  43. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  44. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  45. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  46. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  48. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  49. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  50. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  51. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  52. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
  54. package/corpus/templates/slides/app/pages/Index.tsx +240 -89
  55. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  60. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  63. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  64. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  65. package/corpus/templates/slides/shared/api.ts +2 -1
  66. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  67. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  68. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  69. package/dist/agent/production-agent.d.ts +21 -1
  70. package/dist/agent/production-agent.js +129 -10
  71. package/dist/agent/tool-approval-store.d.ts +18 -0
  72. package/dist/agent/tool-approval-store.js +122 -0
  73. package/dist/agent/types.d.ts +1 -1
  74. package/dist/cli/code-agent-executor.js +11 -2
  75. package/dist/client/AgentPanel.js +31 -6
  76. package/dist/client/AssistantChat.d.ts +3 -2
  77. package/dist/client/AssistantChat.js +7 -20
  78. package/dist/client/agent-chat/index.d.ts +1 -1
  79. package/dist/client/agent-chat-adapter.js +11 -0
  80. package/dist/client/agent-chat.d.ts +1 -1
  81. package/dist/client/chat/markdown-renderer.js +14 -2
  82. package/dist/client/chat/message-components.d.ts +6 -2
  83. package/dist/client/chat/message-components.js +25 -47
  84. package/dist/client/chat/tool-call-display.d.ts +3 -5
  85. package/dist/client/chat/tool-call-display.js +7 -212
  86. package/dist/client/chat-first/apps-rail.js +3 -3
  87. package/dist/client/chat-first/browser-pane.js +4 -1
  88. package/dist/client/chat-first/chat-history.d.ts +3 -1
  89. package/dist/client/chat-first/chat-history.js +2 -2
  90. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  91. package/dist/client/chat-first/surface-tabs.js +52 -41
  92. package/dist/client/chat-first/types.d.ts +3 -0
  93. package/dist/client/chat-first.d.ts +10 -0
  94. package/dist/client/chat-first.js +37 -2
  95. package/dist/client/components/ui/message-scroller.js +4 -1
  96. package/dist/client/i18n.js +3 -1
  97. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  98. package/dist/client/sse-event-processor.js +10 -0
  99. package/dist/client/use-chat-models.js +156 -80
  100. package/dist/collab/struct-routes.d.ts +1 -1
  101. package/dist/integrations/webhook-handler.js +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  104. package/dist/resources/handlers.d.ts +1 -1
  105. package/dist/secrets/routes.d.ts +9 -9
  106. package/dist/server/auth.js +262 -31
  107. package/dist/server/onboarding-html.js +56 -9
  108. package/dist/shared/reasoning-effort.d.ts +8 -8
  109. package/dist/shared/reasoning-effort.js +10 -10
  110. package/dist/shared/streaming-text-smoothing.js +41 -11
  111. package/dist/styles/agent-native.css +19 -32
  112. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  113. package/docs/content/template-chat.mdx +1 -1
  114. package/package.json +2 -2
  115. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -12,11 +12,10 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
12
12
 
13
13
  ## Core Rules
14
14
 
15
- - Store large file/blob payloads in configured file/blob storage, not SQL: no
16
- base64, `data:` URLs, images, video/audio, PDFs, ZIPs, screenshots,
17
- thumbnails, or replay chunks in app tables, `application_state`, `settings`,
18
- or `resources`; persist URLs, ids, or handles instead.
19
- - Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
15
+ - Keep large files/blobs in configured file storage, not SQL, settings, or
16
+ resources; persist only URLs, ids, or handles.
17
+ - Never hardcode secrets or private/customer data; use vault/OAuth/runtime
18
+ configuration and fake placeholders in examples.
20
19
  - Use actions for deck lifecycle, slide edits, imports, exports, images, design
21
20
  systems, and sharing. Do not write deck/slide rows directly.
22
21
  - In dev, call actions with `pnpm action <name>`; in production, use native
@@ -25,11 +24,10 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
25
24
  current layout is unclear.
26
25
  - Preserve deck structure and visual consistency. Prefer focused slide edits over
27
26
  regenerating whole decks unless requested.
28
- - When an uploaded PPTX or PDF is being improved, import it into the target deck
29
- first with `import-pptx` or `import-file --importIntoDeck true`. Treat the
30
- resulting `sourceImport` metadata as the source-of-truth contract: preserve
31
- slide count, order, IDs, factual copy, notes, images, and positioned objects;
32
- use `update-slide` for bounded visual edits and verify with `get-deck`.
27
+ - When improving an uploaded PPTX or PDF, import it into the target deck first.
28
+ Treat `sourceImport` as the source-of-truth contract: preserve slide count,
29
+ order, IDs, copy, notes, images, and positioned objects; verify with
30
+ `get-deck`.
33
31
  - A source import with `fidelity: partial` or `imagesSkipped` is not safe to
34
32
  restyle automatically. Report the exact warning rather than silently
35
33
  replacing missing content.
@@ -46,6 +44,9 @@ Before building common workspace or agent UI, read `agent-native-toolkit` and
46
44
  pasted URLs may need a one-time Google reconnect for Drive export. Preserve
47
45
  imported PPTX timing metadata, including by-paragraph reveals, on slide
48
46
  records.
47
+ - For per-click reveals, use ordered 0-based animation targets and patch the
48
+ complete animation list with content; stale or missing targets disable
49
+ reveals.
49
50
  - For images, use `generate-image-api` with provenance; show results as
50
51
  `![alt](url)`.
51
52
  - Ask a sibling app's agent with a natural-language `call-agent` message by
@@ -16,6 +16,7 @@ import { z } from "zod";
16
16
 
17
17
  import { getDb, schema } from "../server/db/index.js";
18
18
  import { notifyClients } from "../server/handlers/decks.js";
19
+ import { assertHumanReadableDeckTitle } from "../shared/deck-title.js";
19
20
  import {
20
21
  assertDesignSystemReadable,
21
22
  assertValidAspectRatio,
@@ -50,6 +51,7 @@ export default defineAction({
50
51
  const now = new Date().toISOString();
51
52
  deck.createdAt = deck.createdAt || now;
52
53
  deck.updatedAt = now;
54
+ assertHumanReadableDeckTitle(deckTitle(deck));
53
55
 
54
56
  const designSystemId = deckDesignSystemId(deck);
55
57
  await assertDesignSystemReadable(designSystemId);
@@ -17,6 +17,7 @@ import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
17
17
  import { getDb, schema } from "../server/db/index.js";
18
18
  import { notifyClients } from "../server/handlers/decks.js";
19
19
  import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
20
+ import { repairGeneratedDeckTitle } from "../shared/deck-title.js";
20
21
  import { hashSlideContent } from "../shared/slide-fit.js";
21
22
  import { slideLabelFor, touchAgentSlidePresence } from "./_agent-presence.js";
22
23
  import {
@@ -308,11 +309,20 @@ export default defineAction({
308
309
  typeof position === "number"
309
310
  ? Math.max(0, Math.min(position, slides.length))
310
311
  : slides.length;
312
+ const shouldRepairTitle = slides.length === 0;
311
313
  slides.splice(insertIndex, 0, newSlide);
312
314
 
313
315
  const now = new Date().toISOString();
314
316
  deck.slides = slides;
315
317
  deck.updatedAt = now;
318
+ const currentTitle =
319
+ typeof row.title === "string" && row.title.trim()
320
+ ? row.title
321
+ : deck.title;
322
+ const repairedTitle = shouldRepairTitle
323
+ ? repairGeneratedDeckTitle(currentTitle, newSlide.content)
324
+ : null;
325
+ if (repairedTitle) deck.title = repairedTitle;
316
326
  deck.creativeContext =
317
327
  contextMode === "off" && existingContext
318
328
  ? existingContext
@@ -334,7 +344,11 @@ export default defineAction({
334
344
  await db.transaction(async (tx: any) => {
335
345
  await tx
336
346
  .update(schema.decks)
337
- .set({ data: JSON.stringify(deck), updatedAt: now })
347
+ .set({
348
+ ...(repairedTitle ? { title: repairedTitle } : {}),
349
+ data: JSON.stringify(deck),
350
+ updatedAt: now,
351
+ })
338
352
  .where(eq(schema.decks.id, deckId));
339
353
  await recordGenerationCreativeContext(
340
354
  {
@@ -20,6 +20,10 @@ import { notifyClients } from "../server/handlers/decks.js";
20
20
  import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
21
21
  import { resolveDefaultDesignSystemId } from "../server/workspace-defaults.js";
22
22
  import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
23
+ import {
24
+ assertHumanReadableDeckTitle,
25
+ repairGeneratedDeckTitle,
26
+ } from "../shared/deck-title.js";
23
27
  import { getDeckUrl } from "./_app-url.js";
24
28
 
25
29
  const ReuseLabelSchema = z
@@ -216,6 +220,9 @@ export default defineAction({
216
220
  ...s,
217
221
  content: normalizeSlidePadding(s.content),
218
222
  }));
223
+ const firstSlideContent = slides[0]?.content;
224
+ const resolvedTitle =
225
+ repairGeneratedDeckTitle(title, firstSlideContent) ?? title;
219
226
 
220
227
  if (deckId) {
221
228
  if (designSystemId) {
@@ -231,6 +238,10 @@ export default defineAction({
231
238
  if (!existing[0]) {
232
239
  throw new Error(`Deck not found: ${deckId}`);
233
240
  }
241
+ const existingDeckTitle =
242
+ repairGeneratedDeckTitle(title, firstSlideContent, existing[0].title) ??
243
+ resolvedTitle;
244
+ assertHumanReadableDeckTitle(existingDeckTitle);
234
245
  await createDeckVersionSnapshot(
235
246
  {
236
247
  id: existing[0].id,
@@ -242,7 +253,7 @@ export default defineAction({
242
253
  );
243
254
  const prevData = existing[0] ? JSON.parse(existing[0].data) : {};
244
255
  const data = {
245
- title,
256
+ title: existingDeckTitle,
246
257
  slides,
247
258
  updatedAt: now,
248
259
  aspectRatio: aspectRatio ?? prevData.aspectRatio,
@@ -252,7 +263,7 @@ export default defineAction({
252
263
  await db
253
264
  .update(schema.decks)
254
265
  .set({
255
- title,
266
+ title: existingDeckTitle,
256
267
  data: JSON.stringify(data),
257
268
  designSystemId: designSystemId ?? existing[0]?.designSystemId ?? null,
258
269
  updatedAt: now,
@@ -271,7 +282,7 @@ export default defineAction({
271
282
  });
272
283
  return {
273
284
  id: deckId,
274
- title,
285
+ title: existingDeckTitle,
275
286
  slideCount: slides.length,
276
287
  url: getDeckUrl(deckId),
277
288
  deepLink: deckDeepLink(deckId),
@@ -282,6 +293,7 @@ export default defineAction({
282
293
 
283
294
  const ownerEmail = getRequestUserEmail();
284
295
  if (!ownerEmail) throw new Error("no authenticated user");
296
+ assertHumanReadableDeckTitle(resolvedTitle);
285
297
 
286
298
  let resolvedDesignSystemId = designSystemId;
287
299
  if (resolvedDesignSystemId) {
@@ -293,7 +305,7 @@ export default defineAction({
293
305
 
294
306
  const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
295
307
  const data: Record<string, unknown> = {
296
- title,
308
+ title: resolvedTitle,
297
309
  slides,
298
310
  createdAt: now,
299
311
  updatedAt: now,
@@ -303,7 +315,7 @@ export default defineAction({
303
315
  data.creativeContext = creativeContextProvenance;
304
316
  await db.insert(schema.decks).values({
305
317
  id,
306
- title,
318
+ title: resolvedTitle,
307
319
  data: JSON.stringify(data),
308
320
  designSystemId: resolvedDesignSystemId ?? null,
309
321
  ownerEmail,
@@ -323,7 +335,7 @@ export default defineAction({
323
335
  });
324
336
  return {
325
337
  id,
326
- title,
338
+ title: resolvedTitle,
327
339
  slideCount: slides.length,
328
340
  url: getDeckUrl(id),
329
341
  deepLink: deckDeepLink(id),
@@ -16,6 +16,31 @@ function stripHtml(html: string): string {
16
16
  .trim();
17
17
  }
18
18
 
19
+ function compactAnimationSummary(value: unknown) {
20
+ if (!Array.isArray(value)) return null;
21
+ return {
22
+ count: value.length,
23
+ steps: value.map((entry, index) => {
24
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
25
+ return { order: index + 1, valid: false };
26
+ }
27
+ const animation = entry as Record<string, unknown>;
28
+ return {
29
+ order: index + 1,
30
+ id: typeof animation.id === "string" ? animation.id : null,
31
+ elementIndex:
32
+ typeof animation.elementIndex === "number"
33
+ ? animation.elementIndex
34
+ : null,
35
+ elementPath: Array.isArray(animation.elementPath)
36
+ ? animation.elementPath
37
+ : null,
38
+ type: typeof animation.type === "string" ? animation.type : null,
39
+ };
40
+ }),
41
+ };
42
+ }
43
+
19
44
  function deckDeepLink(deckId: string): string {
20
45
  return buildDeepLink({
21
46
  app: "slides",
@@ -26,7 +51,8 @@ function deckDeepLink(deckId: string): string {
26
51
 
27
52
  export default defineAction({
28
53
  description:
29
- "Get a specific deck with all slides. Returns full deck JSON including slide content. User-visible slide numbers are 1-based and match the UI: slide 1 is the first slide. Use slideId for edits.",
54
+ "Get a specific deck with all slides. Returns full deck JSON including slide content. User-visible slide numbers are 1-based and match the UI: slide 1 is the first slide. Use slideId for edits. After a mutation or for a large deck when only IDs, count, previews, and animation targets are needed, pass compact=true to avoid retransmitting all slide HTML.",
55
+ timeoutMs: 60_000,
30
56
  schema: z.object({
31
57
  id: z.string().optional().describe("Deck ID (required)"),
32
58
  compact: z
@@ -89,6 +115,8 @@ export default defineAction({
89
115
  zeroBasedIndex: i,
90
116
  id: s.id,
91
117
  layout: s.layout ?? null,
118
+ transition: s.transition ?? null,
119
+ animations: compactAnimationSummary(s.animations),
92
120
  textPreview: stripHtml(s.content || "").slice(0, 120),
93
121
  })),
94
122
  };
@@ -32,6 +32,10 @@ import {
32
32
  type SourceImportMetadata,
33
33
  } from "../server/lib/source-import.js";
34
34
  import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
35
+ import {
36
+ assertHumanReadableDeckTitle,
37
+ repairGeneratedDeckTitle,
38
+ } from "../shared/deck-title.js";
35
39
 
36
40
  // ---------------------------------------------------------------------------
37
41
  // Per-deck write lock — same pattern as add-slide.ts so all client and agent
@@ -66,6 +70,27 @@ export function withDeckLock<T>(
66
70
  // Operation schemas
67
71
  // ---------------------------------------------------------------------------
68
72
 
73
+ const SlideAnimationSchema = z.object({
74
+ id: z.string().min(1).describe("Stable ID for this ordered reveal step"),
75
+ elementIndex: z
76
+ .number()
77
+ .int()
78
+ .min(0)
79
+ .describe(
80
+ "0-based legacy child index. Keep it paired with elementPath for compatibility.",
81
+ ),
82
+ elementPath: z
83
+ .array(z.number().int().min(0))
84
+ .min(1)
85
+ .optional()
86
+ .describe(
87
+ "Preferred 0-based child-index path from the outer .fmd-slide wrapper. Re-read final HTML after content edits.",
88
+ ),
89
+ type: z
90
+ .enum(["appear", "fade", "slide-up", "zoom"])
91
+ .describe("Animation used when this step is revealed"),
92
+ });
93
+
69
94
  const SlideFieldsSchema = z.object({
70
95
  content: z.string().optional(),
71
96
  notes: z.string().optional(),
@@ -75,8 +100,16 @@ const SlideFieldsSchema = z.object({
75
100
  imageLoading: z.boolean().optional(),
76
101
  imagePrompt: z.string().optional(),
77
102
  excalidrawData: z.string().optional(),
78
- transition: z.string().optional(),
79
- animations: z.array(z.unknown()).optional(),
103
+ transition: z
104
+ .enum(["instant", "none", "fade", "slide", "zoom"])
105
+ .optional()
106
+ .describe("Transition used when entering this slide"),
107
+ animations: z
108
+ .array(SlideAnimationSchema)
109
+ .optional()
110
+ .describe(
111
+ "Complete ordered on-click reveal list. Include every intended target in order; unlisted elements remain visible. Use elementPath from the final HTML and 0-based indexes.",
112
+ ),
80
113
  });
81
114
 
82
115
  /** Update fields on a single existing slide */
@@ -171,11 +204,51 @@ export function assertSourceImportOperationsPreserved(
171
204
  );
172
205
  }
173
206
 
207
+ /**
208
+ * A deck-wide source restyle must be atomic from the agent's point of view:
209
+ * accepting a partial batch makes an apparently successful run indistinguishable
210
+ * from a run that quietly left the tail of the imported deck untouched.
211
+ */
212
+ export function assertSourceImportSlidesCovered(
213
+ metadata: SourceImportMetadata | null,
214
+ operations: Operation[],
215
+ requireAllSourceSlides: boolean,
216
+ ): void {
217
+ if (!metadata || !requireAllSourceSlides) return;
218
+
219
+ const sourceSlideIds =
220
+ Array.isArray(metadata.slideIds) && metadata.slideIds.length > 0
221
+ ? metadata.slideIds
222
+ : metadata.slides.map((slide) => slide.id);
223
+ const patchedContentSlideIds = new Set(
224
+ operations.flatMap((operation) =>
225
+ operation.op === "patch-slide" && operation.fields.content !== undefined
226
+ ? [operation.slideId]
227
+ : [],
228
+ ),
229
+ );
230
+ const missingSlideIds = sourceSlideIds.filter(
231
+ (slideId) => !patchedContentSlideIds.has(slideId),
232
+ );
233
+ if (missingSlideIds.length === 0) return;
234
+
235
+ throw new Error(
236
+ `Deck-wide source restyle requires one content patch per imported slide. Missing ${missingSlideIds.length} slide(s): ${missingSlideIds.slice(0, 12).join(", ")}${missingSlideIds.length > 12 ? ", …" : ""}. Continue with every source slide ID in one patch-deck call before verifying with get-deck compact=true.`,
237
+ );
238
+ }
239
+
174
240
  // The browser uses the full operation union above. Agents additionally use
175
241
  // this action for one bounded, deck-wide layout repair: one patch-slide per
176
- // slide in a single SQL transaction, followed by a fresh read for verification.
242
+ // source slide in a single SQL transaction, followed by compact verification.
177
243
  const AgentPatchDeckInputSchema = z.object({
178
244
  deckId: z.string().describe("Deck ID"),
245
+ requireAllSourceSlides: z
246
+ .boolean()
247
+ .optional()
248
+ .default(false)
249
+ .describe(
250
+ "For a deck-wide source-import restyle, require one content patch for every imported slide before any write is committed.",
251
+ ),
179
252
  operations: z
180
253
  .array(
181
254
  z.union([
@@ -192,7 +265,7 @@ const AgentPatchDeckInputSchema = z.object({
192
265
  )
193
266
  .min(1)
194
267
  .describe(
195
- "One patch-slide operation per slide that needs a structural HTML repair. Use patch-deck-fields only for a deck title change.",
268
+ "For a deck-wide source restyle, include one patch-slide operation with content for every existing source slide. Use patch-deck-fields only for a deck title change.",
196
269
  ),
197
270
  });
198
271
 
@@ -332,7 +405,19 @@ export function applyOperation(deck: any, op: Operation): void {
332
405
 
333
406
  case "patch-deck-fields": {
334
407
  const { fields } = op;
335
- if (fields.title !== undefined) deck.title = fields.title;
408
+ if (fields.title !== undefined) {
409
+ const repairedTitle = repairGeneratedDeckTitle(
410
+ fields.title,
411
+ slides[0]?.content,
412
+ deck.title,
413
+ );
414
+ if (repairedTitle) {
415
+ deck.title = repairedTitle;
416
+ } else {
417
+ assertHumanReadableDeckTitle(fields.title);
418
+ deck.title = fields.title;
419
+ }
420
+ }
336
421
  if ("designSystemId" in fields)
337
422
  deck.designSystemId = fields.designSystemId;
338
423
  if (fields.tweaks !== undefined) deck.tweaks = fields.tweaks;
@@ -353,6 +438,7 @@ export function applyOperation(deck: any, op: Operation): void {
353
438
  export function resolveDeckColumnUpdates(
354
439
  current: { title: string; designSystemId: string | null },
355
440
  operations: Operation[],
441
+ resolvedTitle?: string,
356
442
  ): { title: string; designSystemId: string | null } {
357
443
  const fieldOps = operations
358
444
  .filter(
@@ -363,7 +449,7 @@ export function resolveDeckColumnUpdates(
363
449
  const titleOp = fieldOps.find((op) => typeof op.fields.title === "string");
364
450
  const dsOp = fieldOps.find((op) => "designSystemId" in op.fields);
365
451
  return {
366
- title: titleOp?.fields.title ?? current.title,
452
+ title: resolvedTitle ?? titleOp?.fields.title ?? current.title,
367
453
  designSystemId: dsOp
368
454
  ? (dsOp.fields.designSystemId ?? null)
369
455
  : current.designSystemId,
@@ -391,10 +477,22 @@ export default defineAction({
391
477
  "Granular deck patch used by the browser editor for concurrent-safe writes. " +
392
478
  "Each operation touches only the target slide or field — concurrent writers " +
393
479
  "on different slides never overwrite each other's work. For a deck-wide " +
394
- "layout repair, send one patch-slide operation per affected slide in one " +
395
- "call, then call get-deck to verify the persisted HTML before reporting success.",
480
+ "source restyle, set requireAllSourceSlides=true and send one patch-slide " +
481
+ "operation with content for every imported slide in one call; the action " +
482
+ "rejects partial coverage. For animations, inspect the final slide HTML, " +
483
+ "then patch content and the complete ordered animations list together; " +
484
+ "validate every 0-based elementPath and do not invent one-based indexes. " +
485
+ "Then call get-deck with compact=true to verify the persisted slide IDs, " +
486
+ "count, and animation metadata before reporting success.",
396
487
  schema: z.object({
397
488
  deckId: z.string().describe("Deck ID"),
489
+ requireAllSourceSlides: z
490
+ .boolean()
491
+ .optional()
492
+ .default(false)
493
+ .describe(
494
+ "For a deck-wide source-import restyle, require one content patch for every imported slide before any write is committed.",
495
+ ),
398
496
  operations: z
399
497
  .array(OperationSchema)
400
498
  .min(1)
@@ -414,7 +512,10 @@ export default defineAction({
414
512
  ),
415
513
  }),
416
514
  agentInputSchema: AgentPatchDeckInputSchema,
417
- run: async ({ deckId, operations, creativeContext }, ctx) => {
515
+ run: async (
516
+ { deckId, operations, requireAllSourceSlides, creativeContext },
517
+ ctx,
518
+ ) => {
418
519
  await assertAccess("deck", deckId, "editor");
419
520
  const isAgentCaller = isAgentPatchCaller(ctx?.caller);
420
521
 
@@ -450,6 +551,11 @@ export default defineAction({
450
551
  const sourceImport = sourceImportForDeck(deck.sourceImport);
451
552
  if (isAgentCaller) {
452
553
  assertSourceImportOperationsPreserved(sourceImport, operations);
554
+ assertSourceImportSlidesCovered(
555
+ sourceImport,
556
+ operations,
557
+ requireAllSourceSlides,
558
+ );
453
559
  }
454
560
  for (const op of operations) {
455
561
  if (
@@ -482,6 +588,13 @@ export default defineAction({
482
588
  resolveDeckColumnUpdates(
483
589
  { title: row.title, designSystemId: row.designSystemId },
484
590
  operations,
591
+ operations.some(
592
+ (operation) =>
593
+ operation.op === "patch-deck-fields" &&
594
+ operation.fields.title !== undefined,
595
+ ) && typeof deck.title === "string"
596
+ ? deck.title
597
+ : undefined,
485
598
  );
486
599
 
487
600
  let generationRecord:
@@ -19,6 +19,10 @@ import { z } from "zod";
19
19
  import { getDb, schema } from "../server/db/index.js";
20
20
  import { notifyClients } from "../server/handlers/decks.js";
21
21
  import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
22
+ import {
23
+ assertHumanReadableDeckTitle,
24
+ repairGeneratedDeckTitle,
25
+ } from "../shared/deck-title.js";
22
26
  import {
23
27
  assertDesignSystemReadable,
24
28
  assertValidAspectRatio,
@@ -71,7 +75,7 @@ export default defineAction({
71
75
 
72
76
  deck.id = deckId;
73
77
  deck.updatedAt = now;
74
- const title = deckTitle(deck);
78
+ const requestedTitle = deckTitle(deck);
75
79
  const nextDesignSystemId = deckDesignSystemId(deck);
76
80
 
77
81
  // Resolve access first — this loads the row AND tells us the caller's
@@ -88,6 +92,11 @@ export default defineAction({
88
92
  if (!ownerEmail) {
89
93
  throw deckHttpError(403, "Sign in to create a deck");
90
94
  }
95
+ const title =
96
+ repairGeneratedDeckTitle(requestedTitle, firstSlideContent(deck)) ??
97
+ requestedTitle;
98
+ assertHumanReadableDeckTitle(title);
99
+ deck.title = title;
91
100
  await assertDesignSystemReadable(nextDesignSystemId);
92
101
  try {
93
102
  await db.insert(schema.decks).values({
@@ -110,6 +119,14 @@ export default defineAction({
110
119
  access.role === "admin" ||
111
120
  access.role === "editor"
112
121
  ) {
122
+ const title =
123
+ repairGeneratedDeckTitle(
124
+ requestedTitle,
125
+ firstSlideContent(deck),
126
+ access.resource.title,
127
+ ) ?? requestedTitle;
128
+ assertHumanReadableDeckTitle(title);
129
+ deck.title = title;
113
130
  await assertDesignSystemReadable(nextDesignSystemId);
114
131
  if (shouldSnapshotDeckWrite(access.resource, title, deck)) {
115
132
  await createDeckVersionSnapshot(
@@ -142,3 +159,9 @@ export default defineAction({
142
159
  return deck;
143
160
  }),
144
161
  });
162
+
163
+ function firstSlideContent(deck: DeckPayload): string | null {
164
+ const slides = Array.isArray(deck.slides) ? deck.slides : [];
165
+ const content = slides[0] && (slides[0] as Record<string, unknown>).content;
166
+ return typeof content === "string" ? content : null;
167
+ }