@agent-native/core-corpus 0.216.0 → 0.217.0

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 (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
  3. package/corpus/templates/analytics/AGENTS.md +1 -1
  4. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
  6. package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
  7. package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
  8. package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
  9. package/corpus/templates/analytics/actions/view-screen.ts +29 -3
  10. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  11. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  12. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  14. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  15. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  17. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  18. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  20. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  21. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  22. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  23. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  24. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  25. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  26. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  27. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  28. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  29. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  30. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  31. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  32. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  33. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  34. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  35. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  36. package/corpus/templates/community/qr/package.json +1 -1
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  38. package/corpus/templates/content/CHANGELOG.md +8 -0
  39. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  40. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  41. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  42. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  43. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  44. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  45. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  46. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  47. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  48. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  49. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  50. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  51. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  52. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  53. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  54. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  55. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  56. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  57. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  58. package/corpus/templates/content/actions/delete-document.ts +41 -3
  59. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  60. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  61. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  62. package/corpus/templates/content/actions/get-document.ts +2 -0
  63. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  64. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  65. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  66. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  67. package/corpus/templates/content/actions/update-document.ts +139 -20
  68. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  70. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  71. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  72. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  73. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  74. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  75. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  76. package/corpus/templates/content/app/i18n-data.ts +1 -0
  77. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  78. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  79. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  80. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  81. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  82. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  83. package/corpus/templates/content/parity/matrix.md +42 -42
  84. package/corpus/templates/content/parity/matrix.ts +3 -1
  85. package/corpus/templates/content/server/db/schema.ts +20 -0
  86. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  87. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  88. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  89. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  90. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  91. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  92. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  93. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  94. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  95. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  96. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  97. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  98. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  99. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  100. package/corpus/templates/content/server/plugins/db.ts +28 -0
  101. package/corpus/templates/content/shared/api.ts +30 -1
  102. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  103. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  104. package/corpus/templates/content/shared/og-description.ts +258 -36
  105. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  106. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  107. package/corpus/templates/design/CHANGELOG.md +2 -0
  108. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  109. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  110. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  111. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  112. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  113. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  114. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  115. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  116. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  117. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  118. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  119. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  120. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  121. package/corpus/templates/design/app/global.css +6 -0
  122. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  123. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  124. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  125. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  126. package/corpus/templates/design/app/pages/Index.tsx +10 -3
  127. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  128. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  129. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  130. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  131. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  132. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  133. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  134. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  135. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  136. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  137. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  138. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  139. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  140. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  141. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  142. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  143. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  144. package/corpus/templates/slides/CHANGELOG.md +6 -0
  145. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  146. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  147. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  148. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  149. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ContentDatabaseItemLayout,
3
3
  ContentDatabaseItemLeadingKind,
4
+ ContentDatabaseItemLine2Source,
4
5
  ContentDatabaseItemTitleWeight,
5
6
  } from "./api";
6
7
  import {
@@ -25,6 +26,11 @@ export const CONTENT_DATABASE_ITEM_TITLE_WEIGHTS = [
25
26
 
26
27
  export const CONTENT_DATABASE_ITEM_LINE2_LINES = [1, 2, 3] as const;
27
28
 
29
+ export const CONTENT_DATABASE_ITEM_LINE2_SOURCES = [
30
+ "property",
31
+ "page",
32
+ ] as const satisfies readonly ContentDatabaseItemLine2Source[];
33
+
28
34
  /** Most properties line 3 or the trailing slot may show. */
29
35
  export const MAX_ITEM_LAYOUT_META_PROPERTIES = 6;
30
36
 
@@ -92,7 +98,7 @@ export function itemLayoutLeadingCandidates<
92
98
  );
93
99
  }
94
100
 
95
- /** Text-like properties line 2 can show. The page body arrives separately. */
101
+ /** Text-like properties line 2 can show. Page text is its own choice. */
96
102
  export function itemLayoutLine2Candidates<
97
103
  P extends ContentDatabaseItemLayoutPropertyLike,
98
104
  >(properties: readonly P[]): P[] {
@@ -135,6 +141,14 @@ function propertyIdList(value: unknown) {
135
141
  return ids;
136
142
  }
137
143
 
144
+ /** Line 2 with a new property ID, unless it shows page text. */
145
+ function withLine2PropertyId(
146
+ line2: ContentDatabaseItemLayout["line2"],
147
+ propertyId: string | null,
148
+ ): ContentDatabaseItemLayout["line2"] {
149
+ return line2.source === "page" ? line2 : { propertyId, lines: line2.lines };
150
+ }
151
+
138
152
  function record(value: unknown): Record<string, unknown> {
139
153
  return value && typeof value === "object" && !Array.isArray(value)
140
154
  ? (value as Record<string, unknown>)
@@ -174,7 +188,10 @@ export function normalizeContentDatabaseItemLayout(
174
188
  )
175
189
  ? (raw.titleWeight as ContentDatabaseItemTitleWeight)
176
190
  : "medium",
177
- line2: { propertyId: propertyIdOrNull(line2.propertyId), lines },
191
+ line2:
192
+ line2.source === "page"
193
+ ? { source: "page", propertyId: null, lines }
194
+ : { propertyId: propertyIdOrNull(line2.propertyId), lines },
178
195
  line3: propertyIdList(raw.line3),
179
196
  trailing: propertyIdList(raw.trailing),
180
197
  };
@@ -214,7 +231,8 @@ export function withSavedContentDatabaseItemLayout<
214
231
  export interface ResolvedContentDatabaseItemLayout<P> {
215
232
  leading: { kind: ContentDatabaseItemLeadingKind; property: P | null };
216
233
  titleWeight: ContentDatabaseItemTitleWeight;
217
- line2: { property: P | null; lines: 1 | 2 | 3 };
234
+ /** `pageText` rows show the first lines of their page instead of a property. */
235
+ line2: { property: P | null; pageText: boolean; lines: 1 | 2 | 3 };
218
236
  line3: P[];
219
237
  trailing: P[];
220
238
  }
@@ -248,9 +266,11 @@ export function resolveContentDatabaseItemLayout<
248
266
  : (leadingCandidates[0] ?? null)
249
267
  : null;
250
268
  const line2Candidates = new Set(itemLayoutLine2Candidates(properties));
251
- const line2Property = layout.line2.propertyId
252
- ? (byId.get(layout.line2.propertyId) ?? null)
253
- : null;
269
+ const pageText = layout.line2.source === "page";
270
+ const line2Property =
271
+ !pageText && layout.line2.propertyId
272
+ ? (byId.get(layout.line2.propertyId) ?? null)
273
+ : null;
254
274
  const meta = (ids: readonly string[]) =>
255
275
  ids.flatMap((id) => {
256
276
  const property = byId.get(id);
@@ -272,6 +292,7 @@ export function resolveContentDatabaseItemLayout<
272
292
  line2Property && line2Candidates.has(line2Property)
273
293
  ? line2Property
274
294
  : null,
295
+ pageText,
275
296
  lines: layout.line2.lines,
276
297
  },
277
298
  line3: meta(layout.line3),
@@ -353,10 +374,10 @@ export function pruneContentDatabaseItemLayout(
353
374
  propertyId: resolved.leading.property ? layout.leading.propertyId : null,
354
375
  },
355
376
  titleWeight: layout.titleWeight,
356
- line2: {
357
- propertyId: resolved.line2.property?.definition.id ?? null,
358
- lines: layout.line2.lines,
359
- },
377
+ line2: withLine2PropertyId(
378
+ layout.line2,
379
+ resolved.line2.property?.definition.id ?? null,
380
+ ),
360
381
  line3: ids(resolved.line3),
361
382
  trailing: ids(resolved.trailing),
362
383
  };
@@ -379,10 +400,7 @@ export function dropDeletedContentDatabaseItemLayoutReferences(
379
400
  ? { kind: "page_icon", propertyId: null }
380
401
  : layout.leading,
381
402
  titleWeight: layout.titleWeight,
382
- line2: {
383
- propertyId: kept(layout.line2.propertyId),
384
- lines: layout.line2.lines,
385
- },
403
+ line2: withLine2PropertyId(layout.line2, kept(layout.line2.propertyId)),
386
404
  line3: layout.line3.filter((id) => exists.has(id)),
387
405
  trailing: layout.trailing.filter((id) => exists.has(id)),
388
406
  };
@@ -444,13 +462,12 @@ export function savableContentDatabaseItemLayout(
444
462
  {
445
463
  leading: savedLeading ? layout.leading : showable.leading,
446
464
  titleWeight: layout.titleWeight,
447
- line2: {
448
- propertyId:
449
- saved?.line2.propertyId === layout.line2.propertyId
450
- ? layout.line2.propertyId
451
- : showable.line2.propertyId,
452
- lines: layout.line2.lines,
453
- },
465
+ line2: withLine2PropertyId(
466
+ layout.line2,
467
+ saved?.line2.propertyId === layout.line2.propertyId
468
+ ? layout.line2.propertyId
469
+ : showable.line2.propertyId,
470
+ ),
454
471
  line3: meta("line3"),
455
472
  trailing: meta("trailing"),
456
473
  },
@@ -462,7 +479,7 @@ export function savableContentDatabaseItemLayout(
462
479
  * The layout someone starts editing from on a view with no saved layout. It
463
480
  * matches the default rows: page icon, title, and the first four properties.
464
481
  * Line 2 is the only slot that shows a Blocks field, so one of those goes
465
- * there. The page body itself has no slot yet.
482
+ * there.
466
483
  */
467
484
  export function startingContentDatabaseItemLayout(
468
485
  visibleProperties: readonly ContentDatabaseItemLayoutPropertyLike[],
@@ -482,6 +499,21 @@ export function startingContentDatabaseItemLayout(
482
499
  };
483
500
  }
484
501
 
502
+ /**
503
+ * Whether any List view shows page text in line 2, so list reads carry each
504
+ * row's excerpt. Other collections never read page bodies for their rows.
505
+ */
506
+ export function contentDatabaseViewsShowPageText(
507
+ views: readonly {
508
+ type: string;
509
+ itemLayout?: ContentDatabaseItemLayout | null;
510
+ }[],
511
+ ): boolean {
512
+ return views.some(
513
+ (view) => view.type === "list" && view.itemLayout?.line2.source === "page",
514
+ );
515
+ }
516
+
485
517
  export interface ContentDatabaseItemLayoutPreset {
486
518
  id: ContentDatabaseItemLayoutPresetId;
487
519
  /** Null for Default, which keeps the view's default rows. */
@@ -518,6 +518,18 @@ function mergePlainParagraph(
518
518
  };
519
519
  }
520
520
 
521
+ /** The blocks `after` changed from `before`; none when the structure changed. */
522
+ export function changedBlocksBetween(before: string, after: string): number[] {
523
+ if (before === after) return [];
524
+ const base = nfmToDoc(before).content;
525
+ const candidate = nfmToDoc(after).content;
526
+ return base.length === candidate.length
527
+ ? base.flatMap((block, index) =>
528
+ stableBlock(block) !== stableBlock(candidate[index]) ? [index] : [],
529
+ )
530
+ : [];
531
+ }
532
+
521
533
  export function mergeDocumentBodyIntents(args: {
522
534
  authoredBaseContent: string;
523
535
  authoredCandidateContent: string;
@@ -530,18 +542,13 @@ export function mergeDocumentBodyIntents(args: {
530
542
  args.currentRevision === args.incoming.authoredBaseRevision &&
531
543
  args.currentContent === args.authoredBaseContent
532
544
  ) {
533
- const base = nfmToDoc(args.authoredBaseContent).content;
534
- const candidate = nfmToDoc(args.authoredCandidateContent).content;
535
- const changedBlockIndexes =
536
- base.length === candidate.length
537
- ? base.flatMap((block, index) =>
538
- stableBlock(block) !== stableBlock(candidate[index]) ? [index] : [],
539
- )
540
- : [];
541
545
  return {
542
546
  status: "resolved",
543
547
  content: args.authoredCandidateContent,
544
- changedBlockIndexes,
548
+ changedBlockIndexes: changedBlocksBetween(
549
+ args.authoredBaseContent,
550
+ args.authoredCandidateContent,
551
+ ),
545
552
  displaced: false,
546
553
  };
547
554
  }
@@ -7,7 +7,12 @@ function stripFrontmatter(markdown: string): string {
7
7
  }
8
8
 
9
9
  function stripFencedCode(markdown: string): string {
10
- return markdown.replace(/(?:^|\n)(```|~~~)[\s\S]*?(?:\n\1[ \t]*|$)/g, "\n");
10
+ // A closing fence is at least as long as the opening one and has nothing
11
+ // after it, so a line like ```js inside the block doesn't close it.
12
+ return markdown.replace(
13
+ /(?:^|\n) {0,3}((`|~)\2{2,})[\s\S]*?(?:\n {0,3}\1\2*[ \t]*(?=\n|$)|$)/g,
14
+ "\n",
15
+ );
11
16
  }
12
17
 
13
18
  function decodeCommonEntities(text: string): string {
@@ -22,9 +27,10 @@ function decodeCommonEntities(text: string): string {
22
27
 
23
28
  function normalizeForComparison(text: string): string {
24
29
  return stripMarkdownForPreview(text)
30
+ .normalize("NFC")
25
31
  .toLowerCase()
26
32
  .replace(/&/g, "and")
27
- .replace(/[^a-z0-9]+/g, " ")
33
+ .replace(/[^\p{L}\p{M}\p{N}]+/gu, " ")
28
34
  .trim();
29
35
  }
30
36
 
@@ -39,11 +45,19 @@ function isHeadingBlock(block: string): boolean {
39
45
  if (/^#{1,6}\s+\S/.test(trimmed)) return true;
40
46
  if (/^<h[1-6]\b/i.test(trimmed)) return true;
41
47
 
48
+ // Lines of paragraph text underlined with `=` or `-`. Under a list, quote
49
+ // or table, a `---` is a divider instead.
42
50
  const lines = trimmed.split("\n");
43
51
  return (
44
- lines.length === 2 &&
45
- lines[0].trim().length > 0 &&
46
- /^\s*(=+|-+)\s*$/.test(lines[1])
52
+ lines.length >= 2 &&
53
+ /^\s*(=+|-+)\s*$/.test(lines[lines.length - 1]) &&
54
+ lines
55
+ .slice(0, -1)
56
+ .every(
57
+ (line) =>
58
+ line.trim().length > 0 &&
59
+ !/^\s{0,3}(?:(?:[-+*]|\d+[.)])(?:\s|$)|>|\|)/.test(line),
60
+ )
47
61
  );
48
62
  }
49
63
 
@@ -52,41 +66,51 @@ function isDividerBlock(block: string): boolean {
52
66
  }
53
67
 
54
68
  export function stripMarkdownForPreview(markdown: string): string {
55
- return decodeCommonEntities(markdown)
56
- .replace(/<[^>\n]+\/>/g, " ")
57
- .replace(/<\/?[^>\n]+>/g, " ")
58
- .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
59
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
60
- .replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1")
61
- .replace(/`([^`]+)`/g, "$1")
62
- .replace(/^#{1,6}\s+/gm, "")
63
- .replace(/\s+#+\s*$/gm, "")
64
- .replace(/^\s{0,3}>\s?/gm, "")
65
- .replace(/^\s*[-+*]\s+\[[ xX]\]\s+/gm, "")
66
- .replace(/^\s*[-+*]\s+/gm, "")
67
- .replace(/^\s*\d+[.)]\s+/gm, "")
68
- .replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*$/gm, " ")
69
- .replace(/\|/g, " ")
70
- .replace(/\*\*([^*\n]+)\*\*/g, "$1")
71
- .replace(/__([^_\n]+)__/g, "$1")
72
- .replace(/\*([^*\n]+)\*/g, "$1")
73
- .replace(/_([^_\n]+)_/g, "$1")
74
- .replace(/~~([^~\n]+)~~/g, "$1")
75
- .replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, "$1")
76
- .replace(/\s+/g, " ")
77
- .trim();
69
+ return (
70
+ decodeCommonEntities(markdown)
71
+ .replace(/<[^>\n]+\/>/g, " ")
72
+ .replace(/<\/?[^>\n]+>/g, " ")
73
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
74
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
75
+ .replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1")
76
+ .replace(/`([^`]+)`/g, "$1")
77
+ .replace(/^#{1,6}\s+/gm, "")
78
+ .replace(/\s+#+\s*$/gm, "")
79
+ .replace(/^\s{0,3}>\s?/gm, "")
80
+ .replace(/^\s*[-+*]\s+\[[ xX]\]\s+/gm, "")
81
+ .replace(/^\s*[-+*]\s+/gm, "")
82
+ .replace(/^\s*\d+[.)]\s+/gm, "")
83
+ .replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*$/gm, " ")
84
+ // Heading underlines and dividers that share a block with text.
85
+ .replace(/^[ \t]{0,3}(?:=+|-+)[ \t]*$/gm, " ")
86
+ .replace(/\|/g, " ")
87
+ .replace(/\*\*([^*\n]+)\*\*/g, "$1")
88
+ .replace(/__([^_\n]+)__/g, "$1")
89
+ .replace(/\*([^*\n]+)\*/g, "$1")
90
+ .replace(/_([^_\n]+)_/g, "$1")
91
+ .replace(/~~([^~\n]+)~~/g, "$1")
92
+ .replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, "$1")
93
+ .replace(/\s+/g, " ")
94
+ .trim()
95
+ );
78
96
  }
79
97
 
80
- function truncateDescription(text: string, maxLength: number): string {
81
- if (text.length <= maxLength) return text;
82
-
98
+ /** Ends text with "...", shortening it at a word so the whole fits. */
99
+ function withEllipsis(text: string, maxLength: number): string {
83
100
  const hardLimit = Math.max(20, maxLength - 3);
84
- const prefix = text.slice(0, hardLimit);
85
- const wordBoundary = prefix.lastIndexOf(" ");
86
- const truncated =
87
- wordBoundary > hardLimit * 0.6 ? prefix.slice(0, wordBoundary) : prefix;
101
+ let kept = text;
102
+ if (kept.length > hardLimit) {
103
+ const prefix = kept.slice(0, hardLimit);
104
+ const wordBoundary = prefix.lastIndexOf(" ");
105
+ kept =
106
+ wordBoundary > hardLimit * 0.6 ? prefix.slice(0, wordBoundary) : prefix;
107
+ }
108
+
109
+ return `${kept.replace(/[.,;:!?\s-]+$/g, "")}...`;
110
+ }
88
111
 
89
- return `${truncated.replace(/[.,;:!?\s-]+$/g, "")}...`;
112
+ function truncateDescription(text: string, maxLength: number): string {
113
+ return text.length <= maxLength ? text : withEllipsis(text, maxLength);
90
114
  }
91
115
 
92
116
  export function buildPublicDocumentDescription({
@@ -126,3 +150,201 @@ export function buildPublicDocumentDescription({
126
150
  ? truncateDescription(fallback, maxLength)
127
151
  : DEFAULT_DESCRIPTION;
128
152
  }
153
+
154
+ /** A line frontmatter holds: a key, list item, indented value, comment or blank. */
155
+ /** A frontmatter key and its colon. A quoted key can hold colons. */
156
+ const FRONTMATTER_KEY_SOURCE = String.raw`(?:"[^"\n]*"|'[^'\n]*'|[^\s#:-][^:\n]*):(?=\s|$)`;
157
+ const FRONTMATTER_KEY = new RegExp(`^${FRONTMATTER_KEY_SOURCE}`);
158
+ const FRONTMATTER_LINE = new RegExp(
159
+ String.raw`^(?:[ \t]*(?:#.*)?|[ \t]+\S.*|-(?:[ \t].*)?|${FRONTMATTER_KEY_SOURCE}.*)$`,
160
+ );
161
+ const FRONTMATTER_START = new RegExp(
162
+ String.raw`^---[ \t]*\n(?:#|${FRONTMATTER_KEY_SOURCE})`,
163
+ );
164
+ // A key cut before its colon: one word, or a quoted name.
165
+ const FRONTMATTER_KEY_START = /^(?:"[^"\n]*|'[^'\n]*|[^\s#:-][^\s:]*)$/;
166
+ // A key written the way frontmatter keys are: one word that doesn't start
167
+ // with a capital, or a quoted name. A label like `Note:` starts prose.
168
+ const CONVENTIONAL_FRONTMATTER_KEY =
169
+ /^(?:"[^"\n]*"|'[^'\n]*'|[^\s\p{Lu}#:"'-][^\s:]*):(?=\s|$)/u;
170
+
171
+ /**
172
+ * The markdown after its frontmatter, or null when a prefix ends inside it.
173
+ * Frontmatter opens with `---` and a key or comment, and holds only YAML lines,
174
+ * at least one a key, up to a closing `---`. A divider at the top of a page is
175
+ * followed by a blank line or by text that isn't YAML.
176
+ */
177
+ function stripExcerptFrontmatter(
178
+ markdown: string,
179
+ truncated: boolean,
180
+ ): string | null {
181
+ if (!FRONTMATTER_START.test(markdown)) return markdown;
182
+ const lines = markdown.split("\n");
183
+ let hasKey = false;
184
+ for (let index = 1; index < lines.length; index++) {
185
+ const line = lines[index];
186
+ if (/^---[ \t]*$/.test(line)) {
187
+ return hasKey ? lines.slice(index + 1).join("\n") : markdown;
188
+ }
189
+ if (FRONTMATTER_KEY.test(line)) hasKey = true;
190
+ // A prefix's last line may be cut short. After a whole line, it counts if
191
+ // it could still begin a YAML line. Cut as the first line, it counts only
192
+ // with a conventional key, so a long `Note: …` paragraph after a top
193
+ // divider stays page text while a long `description:` stays hidden.
194
+ if (truncated && index === lines.length - 1) {
195
+ const couldBeYaml =
196
+ index > 1
197
+ ? FRONTMATTER_LINE.test(line) || FRONTMATTER_KEY_START.test(line)
198
+ : CONVENTIONAL_FRONTMATTER_KEY.test(line);
199
+ if (!couldBeYaml) return markdown;
200
+ break;
201
+ }
202
+ if (!FRONTMATTER_LINE.test(line)) return markdown;
203
+ }
204
+ // A prefix that has shown only YAML lines may end inside the frontmatter,
205
+ // even before its first key.
206
+ return truncated ? null : markdown;
207
+ }
208
+
209
+ /**
210
+ * Drops opening markers with no partner after them, like the `**` of a bold
211
+ * run a cut split. Closed spans keep their text, markers and all.
212
+ */
213
+ function dropUnclosedMarkers(markdown: string): string {
214
+ return markdown.replace(
215
+ /(^|\s)(\*{1,3}|_{1,3}|~~|`+)(?=[\p{L}\p{M}\p{N}])/gu,
216
+ (match, space: string, marker: string, offset: number) =>
217
+ markdown.slice(offset + match.length).includes(marker) ? match : space,
218
+ );
219
+ }
220
+
221
+ const DEFAULT_EXCERPT_LENGTH = 300;
222
+ /** Scripts written without spaces between words. */
223
+ const SPACELESS_SCRIPT =
224
+ /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
225
+
226
+ /**
227
+ * Plain text from the start of a page body, for a list row. Images, code,
228
+ * dividers and a first paragraph that repeats the title drop out, and
229
+ * headings only count when the page has no other text. `truncated` means the
230
+ * markdown is only the body's first part, so it may end partway through;
231
+ * `nextCharacter`, when known, tells whether its last word goes on.
232
+ */
233
+ export function buildDocumentExcerpt({
234
+ content,
235
+ title,
236
+ truncated = false,
237
+ nextCharacter,
238
+ maxLength = DEFAULT_EXCERPT_LENGTH,
239
+ }: {
240
+ content?: string | null;
241
+ title?: string | null;
242
+ truncated?: boolean;
243
+ nextCharacter?: string;
244
+ maxLength?: number;
245
+ }): string {
246
+ const body = stripExcerptFrontmatter(
247
+ content?.replace(/\r\n?/g, "\n") ?? "",
248
+ truncated,
249
+ );
250
+ // A prefix that ends inside the frontmatter has no body text yet.
251
+ if (body === null) return "";
252
+ let markdown = body;
253
+ if (truncated) {
254
+ // Drop an image or tag the cut leaves open. A link it leaves open keeps
255
+ // its text, and when the cut falls in its address, that text is whole.
256
+ let linkTextEnded = false;
257
+ markdown = markdown
258
+ .replace(/!\[[^\]\n]*(?:\]\([^)\n]*)?$/, "")
259
+ .replace(/<[A-Za-z/!][^>\n]*$/, "")
260
+ .replace(
261
+ /\[([^\]\n]*)(\]\([^)\n]*)?$/,
262
+ (_match, text: string, address?: string) => {
263
+ linkTextEnded = address !== undefined;
264
+ return text;
265
+ },
266
+ );
267
+ // Drop a word the cut splits, with any markup or punctuation in front of
268
+ // it. A cut right after punctuation or closing markup ends a whole word,
269
+ // an apostrophe after the cut means the word goes on, and text in a
270
+ // script written without spaces has no word to finish.
271
+ const lastWord = /\s(\S*[\p{L}\p{M}\p{N}'’])$/u.exec(markdown)?.[1];
272
+ const wordGoesOn =
273
+ nextCharacter === undefined || /[\p{L}\p{M}\p{N}'’]/u.test(nextCharacter);
274
+ if (
275
+ lastWord &&
276
+ wordGoesOn &&
277
+ !linkTextEnded &&
278
+ !SPACELESS_SCRIPT.test(lastWord)
279
+ ) {
280
+ markdown = markdown.slice(0, -lastWord.length);
281
+ }
282
+ // Then markup or an opening bracket or quote left with nothing after it.
283
+ markdown = markdown.replace(/(^|\s)[*_~`(["'“‘]+$/u, "$1");
284
+ }
285
+ const blocks = stripFencedCode(markdown)
286
+ // Headings and dividers stand alone even without blank lines around them.
287
+ // A line of `=` or `-` right under a line of text underlines it as a
288
+ // heading, so it stays with that line.
289
+ .replace(/^ {0,3}#{1,6}[ \t].*$/gm, "\n$&\n")
290
+ .replace(/(?<=\S[^\n]*\n) {0,3}(?:=+|-+)[ \t]*$/gm, "$&\n")
291
+ .replace(/^ {0,3}(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/gm, "\n")
292
+ .replace(/(^|\n\n)[ \t]{0,3}(?:[-*_][ \t]*){3,}(?=\n|$)/g, "$1\n")
293
+ .split(/\n{2,}/);
294
+ const paragraphs: string[] = [];
295
+ const headings: string[] = [];
296
+ let length = 0;
297
+ for (const [index, block] of blocks.entries()) {
298
+ if (length > maxLength) break;
299
+ if (!block.trim() || isDividerBlock(block)) continue;
300
+ // The cut can only leave markup open in the last block.
301
+ const source =
302
+ truncated && index === blocks.length - 1
303
+ ? dropUnclosedMarkers(block)
304
+ : block;
305
+ const text = stripMarkdownForPreview(
306
+ source.replace(/!\[[^\]]*\]\([^)]*\)/g, " "),
307
+ );
308
+ if (!text) continue;
309
+ if (isHeadingBlock(block)) {
310
+ headings.push(text);
311
+ } else if (paragraphs.length > 0 || !isTitleRepeat(text, title ?? "")) {
312
+ paragraphs.push(text);
313
+ length += text.length + 1;
314
+ }
315
+ }
316
+ const text = (paragraphs.length > 0 ? paragraphs : headings).join(" ");
317
+ if (text.length > maxLength) return truncateDescription(text, maxLength);
318
+ return truncated && text ? withEllipsis(text, maxLength) : text;
319
+ }
320
+
321
+ /**
322
+ * How much of a page body a list row's excerpt reads: enough markdown for
323
+ * three lines of text once links and formatting are stripped.
324
+ */
325
+ export const EXCERPT_SOURCE_CHARS = 1000;
326
+
327
+ /**
328
+ * A list row's excerpt, from only the first `EXCERPT_SOURCE_CHARS` characters
329
+ * of the page body. A whole body and a list read's prefix, which takes one
330
+ * character more to tell a longer body from one that fits, give the same text.
331
+ */
332
+ export function buildListRowExcerpt({
333
+ content,
334
+ title,
335
+ }: {
336
+ content?: string | null;
337
+ title?: string | null;
338
+ }): string {
339
+ // Count characters the way the database does, not UTF-16 units. No
340
+ // character takes more than two units.
341
+ const characters = Array.from(
342
+ (content ?? "").slice(0, 2 * (EXCERPT_SOURCE_CHARS + 1)),
343
+ );
344
+ return buildDocumentExcerpt({
345
+ content: characters.slice(0, EXCERPT_SOURCE_CHARS).join(""),
346
+ title,
347
+ truncated: characters.length > EXCERPT_SOURCE_CHARS,
348
+ nextCharacter: characters[EXCERPT_SOURCE_CHARS],
349
+ });
350
+ }
@@ -20,7 +20,11 @@ description: >-
20
20
  1. **Resolve the starting point.** When the user mentions a template, past
21
21
  design, or prior work, call both `list-design-templates` and `list-designs`
22
22
  before generating. Match the requested name or id; ask only when multiple
23
- plausible results remain.
23
+ plausible results remain. To look through a template before using it, call
24
+ `get-design-template --templateId="<id>"` (read-only); in the UI a template
25
+ card opens the same read-only preview, and its "Use template" button is the
26
+ copy step. Built-in starters include app screens (analytics dashboard,
27
+ messaging app, project tasks, design studio) beside ads, posters and pages.
24
28
  2. **Save a reusable template.** Call `save-design-as-template` with the source
25
29
  `designId`. The action snapshots inline screens, canvas dimensions,
26
30
  defaults, linked design system, and locked layers.
@@ -26,8 +26,10 @@ export const shaderRuntimeBridgeScript: string = `"use strict";
26
26
  const registry = {};
27
27
  const mounts = [];
28
28
  let rafId = null;
29
+ let lastFrameTs = null;
29
30
  let needsRender = false;
30
31
  let scanTimer = null;
32
+ let viewScale = 1;
31
33
  function nowMs() {
32
34
  return W.performance && W.performance.now ? W.performance.now() : Date.now();
33
35
  }
@@ -285,9 +287,10 @@ export const shaderRuntimeBridgeScript: string = `"use strict";
285
287
  const rect = el.getBoundingClientRect();
286
288
  const wCss = Math.max(1, Math.round(rect.width));
287
289
  const hCss = Math.max(1, Math.round(rect.height));
288
- const dpr = Math.min(2, W.devicePixelRatio || 1);
289
- const wPx = Math.max(1, Math.round(wCss * dpr));
290
- const hPx = Math.max(1, Math.round(hCss * dpr));
290
+ const deviceRatio = Math.min(2, W.devicePixelRatio || 1);
291
+ const ratio = Math.max(Math.min(1, deviceRatio), deviceRatio * viewScale);
292
+ const wPx = Math.max(1, Math.round(wCss * ratio));
293
+ const hPx = Math.max(1, Math.round(hCss * ratio));
291
294
  if (canvas.width !== wPx || canvas.height !== hPx) {
292
295
  canvas.width = wPx;
293
296
  canvas.height = hPx;
@@ -319,8 +322,15 @@ export const shaderRuntimeBridgeScript: string = `"use strict";
319
322
  gl.clear(gl.COLOR_BUFFER_BIT);
320
323
  gl.drawArrays(gl.TRIANGLES, 0, 3);
321
324
  }
322
- function frame() {
325
+ function frame(ts) {
323
326
  rafId = null;
327
+ if (typeof ts === "number") {
328
+ if (ts === lastFrameTs) {
329
+ startLoop();
330
+ return;
331
+ }
332
+ lastFrameTs = ts;
333
+ }
324
334
  const t = (nowMs() - t0) / 1e3;
325
335
  const animate = !reducedMotion && !document.hidden;
326
336
  for (let i = mounts.length - 1; i >= 0; i--) {
@@ -414,6 +424,17 @@ export const shaderRuntimeBridgeScript: string = `"use strict";
414
424
  document.addEventListener("visibilitychange", function() {
415
425
  if (!document.hidden) requestRender();
416
426
  });
427
+ W.addEventListener("message", function(event) {
428
+ if (W.parent === W || event.source !== W.parent) return;
429
+ const data = event.data;
430
+ if (!data || data.type !== "set-editor-chrome-scale") return;
431
+ const scale = Math.max(Number(data.scaleX) || 0, Number(data.scaleY) || 0);
432
+ if (!(scale > 0)) return;
433
+ const next = Math.min(1, Math.ceil(scale * 8) / 8);
434
+ if (next === viewScale) return;
435
+ viewScale = next;
436
+ requestRender();
437
+ });
417
438
  try {
418
439
  new MutationObserver(function() {
419
440
  scheduleScan();
@@ -7,6 +7,8 @@ from the command menu (Cmd+K → "What's new") or from Settings.
7
7
 
8
8
  ### Fixed
9
9
 
10
+ - Collapsed shader layers no longer interrupt other shaders on the canvas
11
+ - Shader fills keep rendering when other screen content cannot be captured
10
12
  - Shader fills and effects render correctly on canvas elements created or updated in the editor.
11
13
 
12
14
  ## 2026-10-21
@@ -0,0 +1,53 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import {
3
+ getOnboardingStepStatuses,
4
+ readOnboardingDismissed,
5
+ } from "@agent-native/core/onboarding/status";
6
+ import {
7
+ getRequestOrgId,
8
+ getRequestUserEmail,
9
+ } from "@agent-native/core/server/request-context";
10
+ import { z } from "zod";
11
+
12
+ import {
13
+ GETTING_STARTED_CHECKLIST,
14
+ registerGettingStartedSteps,
15
+ } from "../server/lib/getting-started.js";
16
+
17
+ export default defineAction({
18
+ description:
19
+ "Read the user's Getting started checklist: each step with whether the user has completed it and the in-app path that completes it. Use it to tell the user what is left or what to do next.",
20
+ schema: z.object({}),
21
+ http: { method: "GET" },
22
+ readOnly: true,
23
+ run: async () => {
24
+ const userEmail = getRequestUserEmail();
25
+ if (!userEmail) {
26
+ throw Object.assign(new Error("Not authenticated"), { statusCode: 401 });
27
+ }
28
+ registerGettingStartedSteps();
29
+ const context = {
30
+ sessionId: userEmail,
31
+ userEmail,
32
+ orgId: getRequestOrgId() ?? null,
33
+ };
34
+ const [steps, dismissed] = await Promise.all([
35
+ getOnboardingStepStatuses(context, {
36
+ checklist: GETTING_STARTED_CHECKLIST,
37
+ }),
38
+ readOnboardingDismissed(userEmail, GETTING_STARTED_CHECKLIST),
39
+ ]);
40
+ return {
41
+ dismissed,
42
+ completeCount: steps.filter((step) => step.complete).length,
43
+ totalCount: steps.length,
44
+ steps: steps.map(({ id, title, description, complete, href }) => ({
45
+ id,
46
+ title,
47
+ description,
48
+ complete,
49
+ href,
50
+ })),
51
+ };
52
+ },
53
+ });