@agent-native/core 0.149.4 → 0.149.5

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 (30) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +23 -4
  3. package/corpus/templates/slides/actions/get-deck.ts +25 -9
  4. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  5. package/corpus/templates/slides/actions/update-slide.ts +144 -14
  6. package/corpus/templates/slides/app/lib/create-deck-generation.ts +12 -4
  7. package/corpus/templates/slides/changelog/2026-08-11-pdf-requests-to-restyle-and-preserve-a-source-deck-now-impor.md +6 -0
  8. package/corpus/templates/slides/changelog/2026-08-11-slide-edits-preserve-their-layout-without-introducing-hidden.md +6 -0
  9. package/corpus/templates/slides/changelog/2026-08-11-slides-reliably-imports-uploaded-pdfs-and-presentations.md +6 -0
  10. package/corpus/templates/slides/changelog/2026-08-11-slides-support-atomic-code-style-html-patches-without-regene.md +6 -0
  11. package/corpus/templates/slides/package.json +1 -0
  12. package/corpus/templates/slides/server/lib/slide-content-patch.ts +355 -0
  13. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +28 -9
  14. package/corpus/templates/slides/server/plugins/agent-chat.ts +12 -3
  15. package/dist/cli/create.js +49 -0
  16. package/dist/client/chat-first/index.d.ts +1 -1
  17. package/dist/client/chat-first/index.js +1 -1
  18. package/dist/client/chat-first/primary-nav.d.ts +3 -1
  19. package/dist/client/chat-first/primary-nav.js +8 -2
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/notifications/routes.d.ts +3 -3
  23. package/dist/private-blob/registry.js +67 -4
  24. package/dist/secrets/routes.d.ts +9 -9
  25. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  26. package/dist/templates/default/pnpm-workspace.yaml +44 -0
  27. package/dist/templates/workspace-root/pnpm-workspace.yaml +44 -0
  28. package/package.json +1 -1
  29. package/src/templates/default/pnpm-workspace.yaml +44 -0
  30. package/src/templates/workspace-root/pnpm-workspace.yaml +44 -0
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8038
34
+ - template files: 8043
@@ -72,10 +72,17 @@ To edit a slide's content:
72
72
  3. **Modify the content** HTML string for the intended slide. Preserve an
73
73
  approved native template or component when it already fits; generate
74
74
  net-new structure only when the relevant corpus is empty.
75
- 4. **Update the slide** with the `update-slide` action using `deckId`,
76
- `slideId`, and `fullContent`. Do not write deck rows directly or add raw
77
- full-deck writes for normal slide edits; use `patch-deck` for browser/editor
78
- changes.
75
+ 4. **Update the slide** with `update-slide` using `deckId`, `slideId`, and
76
+ ordered `edits`. For code-style work, first read with `get-deck` using the
77
+ target `slideId`, `compact=false`, and `format=true`, then send the returned
78
+ `contentHash` as `baseContentHash`. Use exact replace, insert before/after,
79
+ replace between markers, or regex replace. Set `expectedMatches` whenever a
80
+ marker could be ambiguous. All edits are applied in memory under the deck
81
+ lock; if one required edit fails, nothing is written. Set `format=true` on
82
+ `update-slide` to persist readable Prettier line breaks. Use `fullContent`
83
+ only for an intentional full rewrite - do not regenerate a slide to make a
84
+ small change. Do not write deck rows directly or add raw full-deck writes;
85
+ use `patch-deck` for browser/editor changes.
79
86
  5. For browser/editor code, enqueue granular deck operations through
80
87
  `patch-deck` / `DeckContext.tsx` instead of replacing the whole deck JSON.
81
88
 
@@ -83,6 +90,18 @@ To edit a slide's content:
83
90
  preserve quote, speaker, date, metric, and uncertainty status. Existing HTML
84
91
  or visual similarity is not proof of source fidelity.
85
92
 
93
+ ## Click-to-reveal animations
94
+
95
+ Animations are metadata over the final slide HTML, not alternate slide markup.
96
+ Read the full target slide, keep its existing visual structure, and patch the
97
+ complete ordered `animations` list with `elementPath` values from that exact
98
+ HTML. Elements omitted from the list remain visible immediately, so labels and
99
+ headings need no duplicate markup. Do not add hidden duplicates, layout
100
+ spacers, absolute-positioned copies, transforms, or placeholder content to
101
+ simulate reveals. When content and reveals change together, send both fields in
102
+ one `patch-deck` operation. To remove reveals, send `animations: []` with the
103
+ existing content and verify the persisted slide afterward.
104
+
86
105
  If retrieval produces a new immutable context pack, keep its `contextPackId`
87
106
  and reuse labels with the deck provenance. Existing slide HTML is not proof of
88
107
  which source version influenced it.
@@ -4,7 +4,9 @@ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
4
  import { resolveAccess } from "@agent-native/core/sharing";
5
5
  import { z } from "zod";
6
6
 
7
+ import { formatSlideHtml } from "../server/lib/slide-content-patch.js";
7
8
  import { summarizeSlideAnimationTargets } from "../server/lib/validate-slide-animations.js";
9
+ import { hashSlideContent } from "../shared/slide-fit.js";
8
10
  import "../server/db/index.js"; // ensure registerShareableResource runs
9
11
 
10
12
  function stripHtml(html: string): string {
@@ -76,6 +78,12 @@ export default defineAction({
76
78
  .describe(
77
79
  "Set to 'true' for compact slide summaries, or 'false' for full slide HTML. In-app agent calls without slideId default to compact output.",
78
80
  ),
81
+ format: z
82
+ .enum(["true", "false"])
83
+ .optional()
84
+ .describe(
85
+ "Set to 'true' to return full slide HTML formatted with Prettier for code-style patches. The contentHash still identifies the persisted source.",
86
+ ),
79
87
  }),
80
88
  http: { method: "GET" },
81
89
  mcpApp: {
@@ -169,6 +177,22 @@ export default defineAction({
169
177
  const deckMetadata = { ...data };
170
178
  delete deckMetadata.slides;
171
179
 
180
+ const formatHtml = args.format === "true";
181
+ const fullSlides = await Promise.all(
182
+ slideEntries.map(async ({ slide: s, index: i }) => ({
183
+ ...s,
184
+ slideNumber: i + 1,
185
+ zeroBasedIndex: i,
186
+ id: s.id,
187
+ layout: s.layout ?? null,
188
+ content: formatHtml
189
+ ? await formatSlideHtml(String(s.content ?? ""))
190
+ : s.content,
191
+ contentHash: hashSlideContent(String(s.content ?? "")),
192
+ notes: s.notes ?? null,
193
+ })),
194
+ );
195
+
172
196
  return {
173
197
  ...deckMetadata,
174
198
  id: row.id,
@@ -184,15 +208,7 @@ export default defineAction({
184
208
  updatedAt: row.updatedAt,
185
209
  deepLink: deckDeepLink(row.id),
186
210
  ...(selectedSlide ? { selectedSlideId: selectedSlide.id } : {}),
187
- slides: slideEntries.map(({ slide: s, index: i }) => ({
188
- ...s,
189
- slideNumber: i + 1,
190
- zeroBasedIndex: i,
191
- id: s.id,
192
- layout: s.layout ?? null,
193
- content: s.content,
194
- notes: s.notes ?? null,
195
- })),
211
+ slides: fullSlides,
196
212
  };
197
213
  },
198
214
  link: ({ result, args }) => {
@@ -434,6 +434,52 @@ export function applyOperation(deck: any, op: Operation): void {
434
434
  }
435
435
  }
436
436
 
437
+ /**
438
+ * Agent content rewrites must not inherit click-reveal paths implicitly. A
439
+ * caller that wants to revise both HTML and reveals sends `animations` in the
440
+ * same patch, including the complete ordered list. Source-preserving edits are
441
+ * the exception: they retain imported reveal metadata unless the caller opts
442
+ * into an explicit source rewrite.
443
+ */
444
+ export function clearOmittedAnimationsForAgentContentPatches(
445
+ deck: any,
446
+ operations: readonly Operation[],
447
+ options?: { sourceImport?: SourceImportMetadata | null },
448
+ ): void {
449
+ const explicitAnimationSlideIds = new Set(
450
+ operations.flatMap((operation) =>
451
+ operation.op === "patch-slide" &&
452
+ operation.fields.animations !== undefined
453
+ ? [operation.slideId]
454
+ : [],
455
+ ),
456
+ );
457
+ const sourceSlideIds = new Set(
458
+ options?.sourceImport?.slideIds ??
459
+ options?.sourceImport?.slides.map((slide) => slide.id) ??
460
+ [],
461
+ );
462
+ const slideIds = new Set(
463
+ operations.flatMap((operation) =>
464
+ operation.op === "patch-slide" &&
465
+ operation.fields.content !== undefined &&
466
+ operation.fields.animations === undefined &&
467
+ !explicitAnimationSlideIds.has(operation.slideId) &&
468
+ (operation.preserveSource === false ||
469
+ !sourceSlideIds.has(operation.slideId))
470
+ ? [operation.slideId]
471
+ : [],
472
+ ),
473
+ );
474
+ if (!slideIds.size) return;
475
+
476
+ for (const slide of Array.isArray(deck.slides) ? deck.slides : []) {
477
+ if (slideIds.has(slide?.id) && Array.isArray(slide.animations)) {
478
+ delete slide.animations;
479
+ }
480
+ }
481
+ }
482
+
437
483
  /**
438
484
  * Content and animation metadata are one contract. Validate only slides whose
439
485
  * content or animation list changed so unrelated note/title writes do not
@@ -620,6 +666,11 @@ export default defineAction({
620
666
  for (const op of operations) {
621
667
  applyOperation(deck, op);
622
668
  }
669
+ if (isAgentCaller) {
670
+ clearOmittedAnimationsForAgentContentPatches(deck, operations, {
671
+ sourceImport,
672
+ });
673
+ }
623
674
  assertPatchedSlideAnimationsResolve(deck, operations, {
624
675
  requireElementPaths: isAgentCaller,
625
676
  });
@@ -16,6 +16,11 @@ import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
16
16
  import { getDb, schema } from "../server/db/index.js"; // ensure registerShareableResource runs
17
17
  import { notifyClients } from "../server/handlers/decks.js";
18
18
  import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
19
+ import {
20
+ applySlideContentEdits,
21
+ formatSlideHtml,
22
+ type SlideContentEdit,
23
+ } from "../server/lib/slide-content-patch.js";
19
24
  import {
20
25
  assertSourceSlidePreserved,
21
26
  sourceImportForDeck,
@@ -92,8 +97,7 @@ function storedCreativeContext(value: unknown): {
92
97
 
93
98
  export default defineAction({
94
99
  description:
95
- "Surgically edit a slide's content using search-replace or full replacement. " +
96
- "Syncs live to open editors. Prefer this over full deck rewrites. Source-imported slides preserve their original images and factual copy by default.",
100
+ "Atomically patch a slide's HTML like a code editor: send several exact edits against the current source, optionally format it with Prettier, and sync the result live to open editors. Prefer edits over fullContent so unrelated markup is not regenerated. Use baseContentHash from get-deck to reject stale patches. Content edits clear existing click-reveal metadata; use patch-deck with the complete animations list when the edit intentionally changes both content and reveals. Source-imported slides preserve their original images and factual copy by default.",
97
101
  schema: z.object({
98
102
  deckId: z.string().describe("Deck ID"),
99
103
  slideId: z.string().describe("Slide ID"),
@@ -109,6 +113,64 @@ export default defineAction({
109
113
  .string()
110
114
  .optional()
111
115
  .describe("Full HTML to replace entire slide content"),
116
+ edits: z
117
+ .array(
118
+ z.union([
119
+ z.object({
120
+ op: z.literal("replace").optional(),
121
+ find: z.string(),
122
+ replace: z.string(),
123
+ all: z.boolean().optional(),
124
+ occurrence: z.number().int().positive().optional(),
125
+ expectedMatches: z.number().int().nonnegative().optional(),
126
+ required: z.boolean().optional(),
127
+ }),
128
+ z.object({
129
+ op: z.enum(["insert-before", "insert-after"]),
130
+ marker: z.string(),
131
+ content: z.string(),
132
+ occurrence: z.number().int().positive().optional(),
133
+ expectedMatches: z.number().int().nonnegative().optional(),
134
+ required: z.boolean().optional(),
135
+ }),
136
+ z.object({
137
+ op: z.literal("replace-between"),
138
+ start: z.string(),
139
+ end: z.string(),
140
+ content: z.string(),
141
+ includeDelimiters: z.boolean().optional(),
142
+ expectedMatches: z.number().int().nonnegative().optional(),
143
+ required: z.boolean().optional(),
144
+ }),
145
+ z.object({
146
+ op: z.literal("regex-replace"),
147
+ pattern: z.string(),
148
+ replace: z.string(),
149
+ flags: z.string().optional(),
150
+ all: z.boolean().optional(),
151
+ expectedMatches: z.number().int().nonnegative().optional(),
152
+ required: z.boolean().optional(),
153
+ }),
154
+ ]),
155
+ )
156
+ .min(1)
157
+ .optional()
158
+ .describe(
159
+ "Ordered atomic edits against the current HTML. Each edit must match unless required=false. Use expectedMatches to make ambiguity explicit.",
160
+ ),
161
+ baseContentHash: z
162
+ .string()
163
+ .optional()
164
+ .describe(
165
+ "Hash returned by get-deck for the exact slide source being patched. The edit is rejected if the source changed since it was read.",
166
+ ),
167
+ format: z
168
+ .boolean()
169
+ .optional()
170
+ .default(false)
171
+ .describe(
172
+ "Format the resulting HTML with Prettier before persisting it.",
173
+ ),
112
174
  preserveSource: z
113
175
  .boolean()
114
176
  .optional()
@@ -142,13 +204,24 @@ export default defineAction({
142
204
  find,
143
205
  replace,
144
206
  fullContent,
207
+ edits,
208
+ baseContentHash,
209
+ format,
145
210
  preserveSource,
146
211
  contextPackId,
147
212
  contextModeOverride,
148
213
  reuseLabels,
149
214
  } = args;
150
- if (!find && !fullContent) {
151
- throw new Error("Either --find or --fullContent is required");
215
+ if (!edits && find === undefined && fullContent === undefined) {
216
+ throw new Error("One of --edits, --find, or --fullContent is required");
217
+ }
218
+ if (find !== undefined && find.length === 0) {
219
+ throw new Error("find must not be empty for legacy search/replace");
220
+ }
221
+ if (edits && (find !== undefined || fullContent !== undefined)) {
222
+ throw new Error(
223
+ "Use --edits instead of combining it with --find or --fullContent",
224
+ );
152
225
  }
153
226
  await assertAccess("deck", deckId, "editor");
154
227
 
@@ -203,6 +276,15 @@ export default defineAction({
203
276
  throw new Error(`Slide ${slideId} not found in deck ${deckId}`);
204
277
  }
205
278
 
279
+ if (
280
+ baseContentHash !== undefined &&
281
+ hashSlideContent(String(slide.content ?? "")) !== baseContentHash
282
+ ) {
283
+ throw new Error(
284
+ "Slide content changed since it was read. Call get-deck with this slideId again and rebase the patch.",
285
+ );
286
+ }
287
+
206
288
  // ─── Apply the edit to the slide content in decks.data ────────────────
207
289
  //
208
290
  // The agent edits the canonical slide HTML stored in `decks.data` (SQL is
@@ -215,8 +297,9 @@ export default defineAction({
215
297
  // structure renders and merges with concurrent typing via the Yjs CRDT.
216
298
  let applied = false;
217
299
  let notFound = false;
300
+ const previousContent = String(slide.content ?? "");
218
301
 
219
- if (fullContent) {
302
+ if (fullContent !== undefined) {
220
303
  const nextContent = normalizeSlidePadding(fullContent);
221
304
  assertSourceSlidePreserved({
222
305
  metadata: sourceImportForDeck(deck.sourceImport),
@@ -225,16 +308,35 @@ export default defineAction({
225
308
  preserveSource,
226
309
  });
227
310
  slide.content = nextContent;
228
- applied = true;
229
- } else if (find) {
230
- const idx = (slide.content as string).indexOf(find);
311
+ applied = nextContent !== previousContent;
312
+ } else if (edits) {
313
+ const sourceContent = format
314
+ ? await formatSlideHtml(previousContent)
315
+ : previousContent;
316
+ const patched = await applySlideContentEdits(
317
+ sourceContent,
318
+ edits as SlideContentEdit[],
319
+ format,
320
+ );
321
+ const nextContent = normalizeSlidePadding(patched.content);
322
+ assertSourceSlidePreserved({
323
+ metadata: sourceImportForDeck(deck.sourceImport),
324
+ slideId,
325
+ nextContent,
326
+ preserveSource,
327
+ });
328
+ slide.content = nextContent;
329
+ applied = patched.changed;
330
+ if (!applied) slide.content = previousContent;
331
+ } else if (find !== undefined) {
332
+ const idx = previousContent.indexOf(find);
231
333
  if (idx === -1) {
232
334
  notFound = true;
233
335
  } else {
234
336
  const nextContent =
235
- slide.content.slice(0, idx) +
337
+ previousContent.slice(0, idx) +
236
338
  (replace ?? "") +
237
- slide.content.slice(idx + find.length);
339
+ previousContent.slice(idx + find.length);
238
340
  assertSourceSlidePreserved({
239
341
  metadata: sourceImportForDeck(deck.sourceImport),
240
342
  slideId,
@@ -242,10 +344,19 @@ export default defineAction({
242
344
  preserveSource,
243
345
  });
244
346
  slide.content = nextContent;
245
- applied = true;
347
+ applied = nextContent !== previousContent;
246
348
  }
247
349
  }
248
350
 
351
+ // Animation targets are paths into the persisted HTML. A content edit
352
+ // can keep every path valid while changing which visual element lives at
353
+ // that path, so preserving the old list would reveal the wrong content.
354
+ // patch-deck is the explicit escape hatch when content and animations
355
+ // are intentionally revised together.
356
+ if (applied && Array.isArray(slide.animations)) {
357
+ delete slide.animations;
358
+ }
359
+
249
360
  // ─── Persist to SQL ───────────────────────────────────────────────────
250
361
  //
251
362
  // The fresh `updatedAt` (on both the deck JSON and the row) is the signal
@@ -352,23 +463,41 @@ export default defineAction({
352
463
  notFound,
353
464
  slide,
354
465
  slideIndex,
466
+ contentHash: hashSlideContent(String(slide.content ?? "")),
355
467
  contextMode,
356
468
  contextPackId: validated.contextPackId,
357
469
  reuseLabels: slideReuseLabels,
358
470
  };
359
471
  }
360
472
 
361
- return { applied, notFound, slide, slideIndex };
473
+ return {
474
+ applied,
475
+ notFound,
476
+ slide,
477
+ slideIndex,
478
+ contentHash: hashSlideContent(String(slide.content ?? "")),
479
+ };
362
480
  });
363
481
 
364
482
  if (rmw.notFound) {
365
483
  return {
366
484
  ok: false,
367
- message: `Text not found in slide: "${find!.slice(0, 60)}". Use get-deck with compact=false to see the current slide HTML.`,
485
+ message: `Text not found in slide: "${find!.slice(0, 60)}". Use get-deck with this slideId to see the current slide HTML.`,
368
486
  };
369
487
  }
370
488
 
371
489
  const { applied } = rmw;
490
+ if (!applied) {
491
+ return {
492
+ ok: true,
493
+ deckId,
494
+ slideId,
495
+ applied: false,
496
+ contentHash: rmw.contentHash,
497
+ deepLink: deckDeepLink(deckId),
498
+ };
499
+ }
500
+
372
501
  // Start the freshness window after the SQL write and before notifying the
373
502
  // editor. This keeps a fast render from being rejected as stale.
374
503
  const fitSince = Date.now();
@@ -390,7 +519,7 @@ export default defineAction({
390
519
  notifyClients(deckId, { slideId, actor: "agent" });
391
520
 
392
521
  console.log(
393
- `update-slide: deck=${deckId} slide=${slideId} ${find ? `find="${find.slice(0, 40)}"` : "fullContent"} applied=${applied}`,
522
+ `update-slide: deck=${deckId} slide=${slideId} ${edits ? `edits=${edits.length}` : find !== undefined ? `find="${find.slice(0, 40)}"` : "fullContent"} applied=${applied}`,
394
523
  );
395
524
 
396
525
  // Wait briefly for the editor to re-render and measure. If the patched
@@ -408,6 +537,7 @@ export default defineAction({
408
537
  deckId,
409
538
  slideId,
410
539
  applied,
540
+ contentHash: rmw.contentHash,
411
541
  deepLink: deckDeepLink(deckId),
412
542
  ...(rmw.contextMode
413
543
  ? {
@@ -108,11 +108,19 @@ export function isSourceImprovementRequest(
108
108
  /\bmake\b[\s\S]{0,40}\b(better|modern|professional|polished|prett\w*)\b/.test(
109
109
  normalized,
110
110
  );
111
+ const asksToPreserveSource =
112
+ /(?:\b(turn|convert|transform)\b[\s\S]{0,50}\b(into|to)\b[\s\S]{0,30}\b(deck|presentation|slides?)\b|\b(create|build|make|generate)\b[\s\S]{0,80}\b(deck|presentation|slides?)\b[\s\S]{0,80}\bfrom\b)/.test(
113
+ normalized,
114
+ ) &&
115
+ /\b(copy|slide[- ]for[- ]slide|preserv\w*|same order|before\s*\/?\s*after|placeholder\w*|out of order)\b/.test(
116
+ normalized,
117
+ );
111
118
  // A source deck attachment is the object being improved even when the
112
- // prompt uses an implicit phrase such as "make this prettier". Requiring a
113
- // source noun here silently falls back to reference-only generation and can
114
- // discard the uploaded deck's slide IDs and content.
115
- return asksToImprove;
119
+ // prompt uses an implicit phrase such as "make this prettier" or asks to
120
+ // copy and restyle it. Requiring a source noun here silently falls back to
121
+ // reference-only generation and can discard the uploaded deck's slide IDs
122
+ // and content.
123
+ return asksToImprove || asksToPreserveSource;
116
124
  }
117
125
 
118
126
  function describeUploadedFilesForAgent(
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ PDF requests to restyle and preserve a source deck now import the full deck before editing
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Slide edits preserve their layout without introducing hidden duplicate elements or stale click reveals
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Slides reliably imports uploaded PDFs and presentations
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-11
4
+ ---
5
+
6
+ Slides support atomic code-style HTML patches without regenerating whole slides
@@ -57,6 +57,7 @@
57
57
  "pdf-parse": "^2.4.5",
58
58
  "pdfjs-dist": "5.4.296",
59
59
  "pptxgenjs": "4.0.1",
60
+ "prettier": "^3.9.6",
60
61
  "react-markdown": "^10.1.0",
61
62
  "rehype-raw": "^7.0.0",
62
63
  "y-protocols": "catalog:",