@agent-native/core-corpus 0.216.0 → 0.218.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 (196) 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 +3 -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/components/layout/Sidebar.tsx +38 -167
  11. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  12. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  14. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  15. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  16. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  19. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  20. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  21. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  22. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  23. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  24. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  25. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  26. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  27. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  28. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  30. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  31. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  32. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  33. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  34. package/corpus/templates/assets/CHANGELOG.md +6 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  36. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  37. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  38. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  39. package/corpus/templates/brain/vitest.config.ts +7 -0
  40. package/corpus/templates/calendar/CHANGELOG.md +12 -0
  41. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  42. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  43. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  44. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  45. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  46. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  47. package/corpus/templates/clips/CHANGELOG.md +7 -20
  48. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  49. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  50. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  51. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  52. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  54. package/corpus/templates/community/qr/package.json +1 -1
  55. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  56. package/corpus/templates/content/CHANGELOG.md +10 -0
  57. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  58. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  59. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  60. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  61. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  62. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  63. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  64. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  65. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  66. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  67. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  68. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  69. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  70. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  71. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  72. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  73. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  74. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  75. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  76. package/corpus/templates/content/actions/delete-document.ts +41 -3
  77. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  78. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  79. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  80. package/corpus/templates/content/actions/get-document.ts +2 -0
  81. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  82. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  83. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  84. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  85. package/corpus/templates/content/actions/update-document.ts +139 -20
  86. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  87. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
  88. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
  89. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  90. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  92. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  93. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  94. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  95. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  96. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  97. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  98. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  99. package/corpus/templates/content/app/global.css +31 -0
  100. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  101. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  102. package/corpus/templates/content/app/i18n-data.ts +1 -0
  103. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  104. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  105. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  106. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  107. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  108. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  109. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  110. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  111. package/corpus/templates/content/parity/matrix.md +42 -42
  112. package/corpus/templates/content/parity/matrix.ts +3 -1
  113. package/corpus/templates/content/server/db/schema.ts +20 -0
  114. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  115. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  116. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  117. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  118. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  119. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  120. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  121. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  122. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  123. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  124. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  125. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  126. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  127. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  128. package/corpus/templates/content/server/plugins/db.ts +28 -0
  129. package/corpus/templates/content/shared/api.ts +30 -1
  130. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  131. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  132. package/corpus/templates/content/shared/og-description.ts +258 -36
  133. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  134. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  135. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  136. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  137. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  138. package/corpus/templates/design/CHANGELOG.md +5 -0
  139. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  140. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  141. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  142. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  143. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  144. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  145. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  146. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  147. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  148. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  149. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  150. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  151. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  152. package/corpus/templates/design/app/global.css +6 -0
  153. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  154. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  155. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  156. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  157. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  158. package/corpus/templates/design/app/pages/Index.tsx +28 -67
  159. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  160. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  161. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  162. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  163. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  164. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  165. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  166. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  167. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  168. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  169. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  170. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  171. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  172. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  173. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  174. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  175. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  176. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  177. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  178. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  179. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  180. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  181. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  182. package/corpus/templates/plan/CHANGELOG.md +6 -0
  183. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  184. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  185. package/corpus/templates/slides/CHANGELOG.md +7 -0
  186. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  187. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  188. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  189. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  190. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  191. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  192. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  193. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  194. package/package.json +1 -1
  195. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  196. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -1439,7 +1439,26 @@ function suggestionAnchorRange(
1439
1439
  const collapsedRange = collapsedDeletionTextblock();
1440
1440
  if (collapsedRange) return collapsedRange;
1441
1441
  if (sourceMatches) {
1442
- const exactRange = sourceRangeToPm(sourceFrom, sourceTo);
1442
+ // An empty paragraph has no text to map, so a span that opens or closes
1443
+ // with one maps the text beside it, or the place it starts when that's
1444
+ // all it holds.
1445
+ const emptyLine = "<empty-block/>";
1446
+ let mapFrom = sourceFrom;
1447
+ let mapTo = sourceTo;
1448
+ for (;;) {
1449
+ if (
1450
+ mapTo - mapFrom > emptyLine.length &&
1451
+ source.slice(mapTo - emptyLine.length - 1, mapTo) === `\n${emptyLine}`
1452
+ )
1453
+ mapTo -= emptyLine.length + 1;
1454
+ else if (
1455
+ mapTo - mapFrom > emptyLine.length &&
1456
+ source.startsWith(`${emptyLine}\n`, mapFrom)
1457
+ )
1458
+ mapFrom += emptyLine.length + 1;
1459
+ else break;
1460
+ }
1461
+ const exactRange = sourceRangeToPm(mapFrom, mapTo);
1443
1462
  if (exactRange) return exactRange;
1444
1463
  }
1445
1464
  const mappedSource = suggestionAnchorText(source);
@@ -1592,11 +1611,18 @@ export function suggestionHighlightSpec(
1592
1611
  editableText: true,
1593
1612
  };
1594
1613
  }
1614
+ // Removed syntax with no text, such as indentation, a list marker, or an
1615
+ // empty paragraph, is not in the document text, so it is drawn instead.
1595
1616
  const structuralDeletion =
1596
1617
  /^\n+$/.test(suggestionAnchorText(suggestion.beforeText)) ||
1597
1618
  (beforePresentation &&
1598
1619
  beforePresentation.length > 0 &&
1599
- beforePresentation.every((node) => node.type === "indent"));
1620
+ beforePresentation.every(
1621
+ (node) =>
1622
+ node.type === "indent" ||
1623
+ node.type === "block" ||
1624
+ (node.type === "text" && /^↵+$/.test(node.value)),
1625
+ ));
1600
1626
  return {
1601
1627
  suggestionId: suggestion.id,
1602
1628
  kind:
@@ -4743,9 +4769,17 @@ export function VisualEditor({
4743
4769
  const apply = (report = true) => {
4744
4770
  if (editor.isDestroyed) return;
4745
4771
  const specs = suggestions
4746
- .map((suggestion) =>
4747
- suggestionHighlightSpec(editor.state.doc, suggestion),
4748
- )
4772
+ .map((suggestion) => {
4773
+ const spec = suggestionHighlightSpec(editor.state.doc, suggestion);
4774
+ // While suggesting, a saved suggestion's text outside the document
4775
+ // is somewhere to type beside, not a control to focus.
4776
+ return spec &&
4777
+ suggesting &&
4778
+ editable &&
4779
+ suggestion.presentation === "canonical"
4780
+ ? { ...spec, editableBoundary: true }
4781
+ : spec;
4782
+ })
4749
4783
  .filter((spec): spec is SuggestionHighlightSpec => spec !== null);
4750
4784
  if (report)
4751
4785
  onSuggestionAnchorsChange?.(
@@ -4771,8 +4805,10 @@ export function VisualEditor({
4771
4805
  };
4772
4806
  }, [
4773
4807
  activeSuggestionId,
4808
+ editable,
4774
4809
  editor,
4775
4810
  onSuggestionAnchorsChange,
4811
+ suggesting,
4776
4812
  suggestions,
4777
4813
  suggestionsSignature,
4778
4814
  showCommentIndicators,
@@ -4796,34 +4832,46 @@ export function VisualEditor({
4796
4832
  };
4797
4833
  }, [editor]);
4798
4834
 
4835
+ // Callers build the readbacks inline and render again on every report, so
4836
+ // these checks key on the readbacks' fields and report each divergent body
4837
+ // once: keyed on the objects, each report re-runs its check until React
4838
+ // aborts the render.
4839
+ const acceptedReadbackId = acceptedDecisionReadback?.id ?? null;
4840
+ const acceptedReadbackContent = acceptedDecisionReadback?.content ?? null;
4799
4841
  useLayoutEffect(() => {
4800
- if (!editor || editor.isDestroyed || !acceptedDecisionReadback) return;
4842
+ if (
4843
+ !editor ||
4844
+ editor.isDestroyed ||
4845
+ acceptedReadbackId === null ||
4846
+ acceptedReadbackContent === null
4847
+ )
4848
+ return;
4801
4849
  const settlingSuggestion = suggestions.find(
4802
- (suggestion) => suggestion.id === acceptedDecisionReadback.id,
4850
+ (suggestion) => suggestion.id === acceptedReadbackId,
4803
4851
  );
4804
4852
  if (!settlingSuggestion) return;
4853
+ let reported: string | null = null;
4805
4854
  const check = () => {
4806
4855
  if (editor.isDestroyed) return;
4807
4856
  const actualContent = docToNfm(editor.getJSON() as any);
4808
4857
  if (
4809
4858
  acceptedSuggestionRendered(
4810
4859
  actualContent,
4811
- acceptedDecisionReadback.content,
4860
+ acceptedReadbackContent,
4812
4861
  settlingSuggestion,
4813
4862
  )
4814
4863
  ) {
4815
- onAcceptedDecisionRendered?.(acceptedDecisionReadback.id);
4864
+ onAcceptedDecisionRendered?.(acceptedReadbackId);
4816
4865
  } else if (
4866
+ actualContent !== reported &&
4817
4867
  acceptedSuggestionReadbackOutdated(
4818
4868
  actualContent,
4819
- acceptedDecisionReadback.content,
4869
+ acceptedReadbackContent,
4820
4870
  settlingSuggestion,
4821
4871
  )
4822
4872
  ) {
4823
- onAcceptedDecisionReadbackOutdated?.(
4824
- acceptedDecisionReadback.id,
4825
- actualContent,
4826
- );
4873
+ reported = actualContent;
4874
+ onAcceptedDecisionReadbackOutdated?.(acceptedReadbackId, actualContent);
4827
4875
  }
4828
4876
  };
4829
4877
  editor.on("transaction", check);
@@ -4832,27 +4880,44 @@ export function VisualEditor({
4832
4880
  editor.off("transaction", check);
4833
4881
  };
4834
4882
  }, [
4835
- acceptedDecisionReadback,
4883
+ acceptedReadbackContent,
4884
+ acceptedReadbackId,
4836
4885
  editor,
4837
4886
  onAcceptedDecisionRendered,
4838
4887
  onAcceptedDecisionReadbackOutdated,
4839
4888
  suggestions,
4840
4889
  ]);
4841
4890
 
4891
+ const proposalReadbackGeneration =
4892
+ proposalDecisionReadback?.generation ?? null;
4893
+ const proposalReadbackContent = proposalDecisionReadback?.content ?? null;
4894
+ const proposalReadbackBefore =
4895
+ proposalDecisionReadback?.beforeContent ?? null;
4842
4896
  useLayoutEffect(() => {
4843
- if (!editor || editor.isDestroyed || !proposalDecisionReadback) return;
4897
+ if (
4898
+ !editor ||
4899
+ editor.isDestroyed ||
4900
+ proposalReadbackGeneration === null ||
4901
+ proposalReadbackContent === null ||
4902
+ proposalReadbackBefore === null
4903
+ )
4904
+ return;
4905
+ let reported: string | null = null;
4844
4906
  const check = () => {
4845
4907
  if (editor.isDestroyed) return;
4846
4908
  const actual = canonicalizeNfm(docToNfm(editor.getJSON() as any));
4847
- if (actual === canonicalizeNfm(proposalDecisionReadback.content))
4848
- onProposalDecisionRendered?.(proposalDecisionReadback.generation);
4909
+ if (actual === canonicalizeNfm(proposalReadbackContent))
4910
+ onProposalDecisionRendered?.(proposalReadbackGeneration);
4849
4911
  else if (
4850
- actual !== canonicalizeNfm(proposalDecisionReadback.beforeContent)
4851
- )
4912
+ actual !== reported &&
4913
+ actual !== canonicalizeNfm(proposalReadbackBefore)
4914
+ ) {
4915
+ reported = actual;
4852
4916
  onProposalDecisionReadbackOutdated?.(
4853
- proposalDecisionReadback.generation,
4917
+ proposalReadbackGeneration,
4854
4918
  actual,
4855
4919
  );
4920
+ }
4856
4921
  };
4857
4922
  editor.on("transaction", check);
4858
4923
  check();
@@ -4863,7 +4928,9 @@ export function VisualEditor({
4863
4928
  editor,
4864
4929
  onProposalDecisionReadbackOutdated,
4865
4930
  onProposalDecisionRendered,
4866
- proposalDecisionReadback,
4931
+ proposalReadbackBefore,
4932
+ proposalReadbackContent,
4933
+ proposalReadbackGeneration,
4867
4934
  ]);
4868
4935
 
4869
4936
  useEffect(() => {
@@ -1232,6 +1232,18 @@ function DatabaseTable({
1232
1232
  if (panel !== "source") sourceHandoffActiveRef.current = false;
1233
1233
  setSettingsPanel(panel);
1234
1234
  };
1235
+ // Opens Sources at one source's own page, as the GitHub status line's
1236
+ // "From the … folder" does; each request has a new id so a repeat opens it.
1237
+ const [sourceNavRequest, setSourceNavRequest] =
1238
+ useState<SourceNavRequest | null>(null);
1239
+ const openGitHubSource = () => {
1240
+ changeDatabaseSettingsPanel("source");
1241
+ setSourceNavRequest((request) => ({
1242
+ id: (request?.id ?? 0) + 1,
1243
+ stack: [{ kind: "github" }],
1244
+ }));
1245
+ setSettingsOpen(true);
1246
+ };
1235
1247
  const [savedViewConfig, setSavedViewConfig] =
1236
1248
  useState<ContentDatabaseViewConfig>(defaultDatabaseViewConfig());
1237
1249
  const [personalQueryDirty, setPersonalQueryDirty] = useState(false);
@@ -3532,6 +3544,7 @@ function DatabaseTable({
3532
3544
  documentId={document.id}
3533
3545
  canEdit={canEdit}
3534
3546
  pageCount={totalItemCount}
3547
+ onOpenSource={canEditWidgetRows ? undefined : openGitHubSource}
3535
3548
  />
3536
3549
  {canEdit &&
3537
3550
  databaseId &&
@@ -4082,6 +4095,7 @@ function DatabaseTable({
4082
4095
  }
4083
4096
  sourcePendingOperations={sourcePendingOperations}
4084
4097
  preserveSourceNavigationOnClose={preserveSourceNavigationOnClose}
4098
+ sourceNavRequest={sourceNavRequest}
4085
4099
  onViewTypeChange={(type) =>
4086
4100
  setViewConfig(updateDatabaseViewType(viewConfig, activeView.id, type))
4087
4101
  }
@@ -7927,6 +7941,9 @@ type SourceNavStep =
7927
7941
  | { kind: "fieldPicker"; sourceId: string; sourceName: string }
7928
7942
  | { kind: "github" };
7929
7943
 
7944
+ /** A request to open Sources at a given page; a new id repeats it. */
7945
+ type SourceNavRequest = { id: number; stack: SourceNavStep[] };
7946
+
7930
7947
  function sourceNavTitle(stack: SourceNavStep[]): string {
7931
7948
  const top = stack[stack.length - 1];
7932
7949
  if (!top) return dbText("sources");
@@ -8007,6 +8024,7 @@ function DatabaseSettingsPanelSheet({
8007
8024
  sourceActionPending,
8008
8025
  sourcePendingOperations,
8009
8026
  preserveSourceNavigationOnClose,
8027
+ sourceNavRequest,
8010
8028
  onViewTypeChange,
8011
8029
  onWrapCellsChange,
8012
8030
  onOpenPagesInChange,
@@ -8060,6 +8078,7 @@ function DatabaseSettingsPanelSheet({
8060
8078
  sourceActionPending: boolean;
8061
8079
  sourcePendingOperations: DatabaseSourcePendingOperations;
8062
8080
  preserveSourceNavigationOnClose: boolean;
8081
+ sourceNavRequest: SourceNavRequest | null;
8063
8082
  onViewTypeChange: (type: ContentDatabaseViewType) => void;
8064
8083
  onWrapCellsChange: (wrapCells: boolean) => void;
8065
8084
  onOpenPagesInChange: (openPagesIn: ContentDatabaseOpenPagesIn) => void;
@@ -8113,6 +8132,10 @@ function DatabaseSettingsPanelSheet({
8113
8132
  setSourceNavStack([]);
8114
8133
  }
8115
8134
  }, [open, panel, preserveSourceNavigationOnClose]);
8135
+ // Runs after the reset above, so a request that opens the sheet keeps its page.
8136
+ useEffect(() => {
8137
+ if (sourceNavRequest) setSourceNavStack(sourceNavRequest.stack);
8138
+ }, [sourceNavRequest]);
8116
8139
 
8117
8140
  const title =
8118
8141
  panel === "main"
@@ -312,11 +312,14 @@ export function GitHubSourceStatusLine({
312
312
  documentId,
313
313
  canEdit,
314
314
  pageCount,
315
+ onOpenSource,
315
316
  }: {
316
317
  source: ContentDatabaseSource | null;
317
318
  documentId: string;
318
319
  canEdit: boolean;
319
320
  pageCount: number;
321
+ /** Opens the folder's page in Sources; without it, the folder is plain text. */
322
+ onOpenSource?: () => void;
320
323
  }) {
321
324
  const now = useNow();
322
325
  const [unlinkOpen, setUnlinkOpen] = useState(false);
@@ -334,6 +337,7 @@ export function GitHubSourceStatusLine({
334
337
  documentId={documentId}
335
338
  canEdit={canEdit}
336
339
  pageCount={pageCount}
340
+ onOpenSource={onOpenSource}
337
341
  detailsOpen={detailsOpen}
338
342
  onDetailsOpenChange={setDetailsOpen}
339
343
  unlinkOpen={unlinkOpen}
@@ -349,6 +353,7 @@ function GitHubStatusLineContent({
349
353
  documentId,
350
354
  canEdit,
351
355
  pageCount,
356
+ onOpenSource,
352
357
  detailsOpen,
353
358
  onDetailsOpenChange,
354
359
  unlinkOpen,
@@ -360,6 +365,7 @@ function GitHubStatusLineContent({
360
365
  documentId: string;
361
366
  canEdit: boolean;
362
367
  pageCount: number;
368
+ onOpenSource?: () => void;
363
369
  detailsOpen: boolean;
364
370
  onDetailsOpenChange: (open: boolean) => void;
365
371
  unlinkOpen: boolean;
@@ -387,8 +393,27 @@ function GitHubStatusLineContent({
387
393
  <div className="-m-px overflow-hidden p-px">
388
394
  <div className="-ms-3 flex min-w-0 flex-wrap items-center gap-y-1 text-muted-foreground">
389
395
  <Dotted>
390
- <IconBrandGithub className="me-1.5 size-3.5 shrink-0" aria-hidden />
391
- <FromFolder status={status} />
396
+ {onOpenSource ? (
397
+ <button
398
+ type="button"
399
+ onClick={onOpenSource}
400
+ className="inline-flex min-w-0 items-center rounded-sm text-start underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
401
+ >
402
+ <IconBrandGithub
403
+ className="me-1.5 size-3.5 shrink-0"
404
+ aria-hidden
405
+ />
406
+ <FromFolder status={status} />
407
+ </button>
408
+ ) : (
409
+ <>
410
+ <IconBrandGithub
411
+ className="me-1.5 size-3.5 shrink-0"
412
+ aria-hidden
413
+ />
414
+ <FromFolder status={status} />
415
+ </>
416
+ )}
392
417
  </Dotted>
393
418
  {stops ? null : (
394
419
  <Dotted>
@@ -93,6 +93,7 @@ function PropertyChoices({
93
93
  selectedIds: savedIds,
94
94
  multiple,
95
95
  noneLabel,
96
+ otherChoice,
96
97
  onChange,
97
98
  }: {
98
99
  label: string;
@@ -100,6 +101,8 @@ function PropertyChoices({
100
101
  selectedIds: string[];
101
102
  multiple: boolean;
102
103
  noneLabel?: string;
104
+ /** A choice that isn't a property, listed after Nothing (Line 2's page text). */
105
+ otherChoice?: { label: string; selected: boolean; onSelect: () => void };
103
106
  onChange: (ids: string[]) => void;
104
107
  }) {
105
108
  // A saved slot can name a property this menu doesn't offer: one hidden, or
@@ -107,8 +110,11 @@ function PropertyChoices({
107
110
  // In Line 3 and End it stays saved for when it's shown again.
108
111
  const offeredIds = properties.map((property) => property.definition.id);
109
112
  const selectedIds = savedIds.filter((id) => offeredIds.includes(id));
110
- const rows = [
113
+ const rows: Array<{ id: string; name: string; other?: true }> = [
111
114
  ...(noneLabel ? [{ id: "", name: noneLabel }] : []),
115
+ ...(otherChoice
116
+ ? [{ id: "", name: otherChoice.label, other: true as const }]
117
+ : []),
112
118
  ...properties.map((property) => ({
113
119
  id: property.definition.id,
114
120
  name: property.definition.name,
@@ -121,9 +127,11 @@ function PropertyChoices({
121
127
  className="grid max-h-56 gap-0.5 overflow-y-auto"
122
128
  >
123
129
  {rows.map((row) => {
124
- const selected = row.id
125
- ? selectedIds.includes(row.id)
126
- : selectedIds.length === 0;
130
+ const selected = row.other
131
+ ? otherChoice!.selected
132
+ : row.id
133
+ ? selectedIds.includes(row.id)
134
+ : selectedIds.length === 0 && !otherChoice?.selected;
127
135
  const position = row.id ? selectedIds.indexOf(row.id) : -1;
128
136
  const full =
129
137
  multiple &&
@@ -132,12 +140,13 @@ function PropertyChoices({
132
140
  selectedIds.length >= MAX_ITEM_LAYOUT_META_PROPERTIES;
133
141
  return (
134
142
  <button
135
- key={row.id || "none"}
143
+ key={row.id || (row.other ? "other" : "none")}
136
144
  type="button"
137
145
  aria-pressed={selected}
138
146
  disabled={full}
139
147
  className="flex h-8 items-center gap-2 rounded px-2 text-left text-sm enabled:hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
140
148
  onClick={() => {
149
+ if (row.other) return otherChoice!.onSelect();
141
150
  if (!multiple) return onChange(row.id ? [row.id] : []);
142
151
  onChange(
143
152
  toggleContentDatabaseItemLayoutMetaProperty(
@@ -253,22 +262,37 @@ export function ListItemLayoutSlotEditor({
253
262
  );
254
263
  }
255
264
  if (slot === "line2") {
265
+ const pageText = layout.line2.source === "page";
266
+ const { lines } = layout.line2;
256
267
  return (
257
268
  <div className="grid gap-2">
258
269
  <PropertyChoices
259
270
  label={label}
260
271
  properties={itemLayoutLine2Candidates(properties)}
261
- selectedIds={layout.line2.propertyId ? [layout.line2.propertyId] : []}
272
+ selectedIds={
273
+ !pageText && layout.line2.propertyId
274
+ ? [layout.line2.propertyId]
275
+ : []
276
+ }
262
277
  multiple={false}
263
278
  noneLabel={dbText("listRowsNothing")}
279
+ otherChoice={{
280
+ label: dbText("listRowsPageText"),
281
+ selected: pageText,
282
+ onSelect: () =>
283
+ onChange({
284
+ ...layout,
285
+ line2: { source: "page", propertyId: null, lines },
286
+ }),
287
+ }}
264
288
  onChange={([id]) =>
265
289
  onChange({
266
290
  ...layout,
267
- line2: { ...layout.line2, propertyId: id ?? null },
291
+ line2: { propertyId: id ?? null, lines },
268
292
  })
269
293
  }
270
294
  />
271
- {layout.line2.propertyId ? (
295
+ {pageText || layout.line2.propertyId ? (
272
296
  <Segmented<1 | 2 | 3>
273
297
  label={label}
274
298
  value={layout.line2.lines}
@@ -7,7 +7,10 @@ import type {
7
7
  ContentDatabaseItemLayoutSlot,
8
8
  ResolvedContentDatabaseItemLayout,
9
9
  } from "@shared/database-item-layout";
10
- import { stripMarkdownForPreview } from "@shared/og-description";
10
+ import {
11
+ buildListRowExcerpt,
12
+ stripMarkdownForPreview,
13
+ } from "@shared/og-description";
11
14
  import {
12
15
  isEmptyPropertyValue,
13
16
  type DocumentPropertyOptionColor,
@@ -93,11 +96,29 @@ function line2Text(property: DocumentProperty) {
93
96
  : value.trim();
94
97
  }
95
98
 
99
+ /**
100
+ * The first lines of a row's page. The list read sends an excerpt, and saving
101
+ * the page's body in this tab rebuilds it. Until a read brings one (right after
102
+ * Page text is picked), a body this tab already holds stands in.
103
+ */
104
+ function pageExcerpt(item: ContentDatabaseItem) {
105
+ if (item.excerpt !== undefined) return item.excerpt;
106
+ return item.document.content
107
+ ? buildListRowExcerpt({
108
+ content: item.document.content,
109
+ title: item.document.title,
110
+ })
111
+ : "";
112
+ }
113
+
96
114
  /** The fields a slot shows, which can be set but blank on a given row. */
97
115
  function slotFieldNames(
98
116
  resolved: ResolvedContentDatabaseItemLayout<DocumentProperty>,
99
117
  slot: ContentDatabaseItemLayoutSlot,
100
118
  ) {
119
+ if (slot === "line2" && resolved.line2.pageText) {
120
+ return [dbText("listRowsPageText")];
121
+ }
101
122
  const fields =
102
123
  slot === "line2"
103
124
  ? resolved.line2.property
@@ -305,7 +326,11 @@ export function DatabaseLayoutListRow({
305
326
  const line2Property = resolved.line2.property
306
327
  ? itemValue(item, resolved.line2.property)
307
328
  : null;
308
- const line2 = line2Property ? line2Text(line2Property) : "";
329
+ const line2 = resolved.line2.pageText
330
+ ? pageExcerpt(item)
331
+ : line2Property
332
+ ? line2Text(line2Property)
333
+ : "";
309
334
  const filled = (properties: DocumentProperty[]) =>
310
335
  properties
311
336
  .map((property) => itemValue(item, property))
@@ -238,6 +238,7 @@ export function builderReviewRowCanCancelPrepared(
238
238
  ) {
239
239
  const execution = row.execution;
240
240
  if (!execution) return false;
241
+ if (execution.payloadUnavailable) return false;
241
242
  if (
242
243
  execution.state !== "ready" &&
243
244
  execution.state !== "write_disabled" &&