@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
@@ -45,7 +45,11 @@ import {
45
45
  findReplaceSuggestionSummary,
46
46
  suggestionDiffParts,
47
47
  } from "@shared/suggestion-diff";
48
- import type { SuggestionPresentationContext } from "@shared/suggestion-text";
48
+ import {
49
+ suggestionAnchorText,
50
+ suggestionTextForDisplay,
51
+ type SuggestionPresentationContext,
52
+ } from "@shared/suggestion-text";
49
53
 
50
54
  import {
51
55
  DropdownMenu,
@@ -197,6 +201,20 @@ function isAmbiguousCommentCreateError(error: unknown) {
197
201
  );
198
202
  }
199
203
 
204
+ /** The session's name for the signed-in person's own entries, if it has one. */
205
+ function ownAuthorName(
206
+ authorEmail: string | null | undefined,
207
+ currentUserEmail?: string,
208
+ currentUserName?: string,
209
+ ) {
210
+ const name = currentUserName?.trim();
211
+ return name &&
212
+ authorEmail &&
213
+ authorEmail.trim().toLowerCase() === currentUserEmail?.trim().toLowerCase()
214
+ ? name
215
+ : null;
216
+ }
217
+
200
218
  function cssEscape(value: string) {
201
219
  return globalThis.CSS?.escape
202
220
  ? globalThis.CSS.escape(value)
@@ -757,6 +775,8 @@ interface CommentsSidebarOptions {
757
775
  onSelectedThreadChange?: (id: string | null) => void;
758
776
  onHoveredThreadChange?: (id: string | null) => void;
759
777
  currentUserEmail?: string;
778
+ /** The signed-in person's display name, for their own suggestions. */
779
+ currentUserName?: string;
760
780
  currentUserOrgId?: string;
761
781
  canComment?: boolean;
762
782
  canResolve?: boolean;
@@ -841,6 +861,7 @@ export function CommentsSidebar({
841
861
  onSelectedThreadChange,
842
862
  onHoveredThreadChange,
843
863
  currentUserEmail,
864
+ currentUserName,
844
865
  currentUserOrgId,
845
866
  canComment = true,
846
867
  canResolve = false,
@@ -1114,34 +1135,32 @@ export function CommentsSidebar({
1114
1135
  }, [canComment, presentation, selectedThreadId, selectedThreadIsOpen]);
1115
1136
  const historyAuthors = useMemo(() => {
1116
1137
  const authors = new Map<string, string>();
1117
- for (const suggestion of suggestions) {
1118
- if (suggestion.authorEmail) {
1119
- authors.set(
1120
- suggestion.authorEmail,
1121
- suggestion.authorEmail.split("@")[0],
1122
- );
1123
- }
1124
- }
1125
- for (const suggestion of draftSuggestions) {
1126
- if (suggestion.authorEmail) {
1127
- authors.set(
1128
- suggestion.authorEmail,
1129
- suggestion.authorEmail.split("@")[0],
1130
- );
1131
- }
1132
- }
1138
+ // A name from any record outranks the email's local part from another.
1139
+ const addAuthor = (email: string | null, name?: string | null) => {
1140
+ if (!email) return;
1141
+ const known =
1142
+ name?.trim() || ownAuthorName(email, currentUserEmail, currentUserName);
1143
+ if (known) authors.set(email, known);
1144
+ else if (!authors.has(email)) authors.set(email, email.split("@")[0]);
1145
+ };
1146
+ for (const suggestion of suggestions) addAuthor(suggestion.authorEmail);
1147
+ for (const suggestion of draftSuggestions)
1148
+ addAuthor(suggestion.authorEmail);
1133
1149
  for (const thread of threads) {
1134
1150
  for (const comment of thread.comments) {
1135
- authors.set(
1136
- comment.author_email,
1137
- comment.author_name ?? comment.author_email.split("@")[0],
1138
- );
1151
+ addAuthor(comment.author_email, comment.author_name);
1139
1152
  }
1140
1153
  }
1141
1154
  return [...authors.entries()].sort((left, right) =>
1142
1155
  left[1].localeCompare(right[1]),
1143
1156
  );
1144
- }, [draftSuggestions, suggestions, threads]);
1157
+ }, [
1158
+ currentUserEmail,
1159
+ currentUserName,
1160
+ draftSuggestions,
1161
+ suggestions,
1162
+ threads,
1163
+ ]);
1145
1164
  // Suggestions edit the page body, so they sit in its tab.
1146
1165
  const historyBodyTabShown =
1147
1166
  activeHistoryTab === null || activeHistoryTab === commentTabKey(null);
@@ -1465,6 +1484,22 @@ export function CommentsSidebar({
1465
1484
  null,
1466
1485
  );
1467
1486
  const pendingCardRef = useRef<HTMLDivElement>(null);
1487
+ // An inline group remounts under its next card once its leading edit is
1488
+ // decided, so whether it is open has to outlive the group.
1489
+ const [expandedProposalIds, setExpandedProposalIds] = useState<
1490
+ ReadonlySet<string>
1491
+ >(() => new Set());
1492
+ const setProposalExpanded = useCallback(
1493
+ (proposalId: string, expanded: boolean) =>
1494
+ setExpandedProposalIds((current) => {
1495
+ if (current.has(proposalId) === expanded) return current;
1496
+ const next = new Set(current);
1497
+ if (expanded) next.add(proposalId);
1498
+ else next.delete(proposalId);
1499
+ return next;
1500
+ }),
1501
+ [],
1502
+ );
1468
1503
  const inlineProposalLeaders = useMemo(() => {
1469
1504
  const leaders = new Map<string, string>();
1470
1505
  for (const proposalId of inlineProposalMembers.keys()) {
@@ -2097,6 +2132,7 @@ export function CommentsSidebar({
2097
2132
  surface={surface}
2098
2133
  onClose={onClose}
2099
2134
  currentUserEmail={currentUserEmail}
2135
+ currentUserName={currentUserName}
2100
2136
  compact={compact}
2101
2137
  replyDrafts={replyDrafts}
2102
2138
  key={suggestion.id}
@@ -2144,6 +2180,8 @@ export function CommentsSidebar({
2144
2180
  (member) =>
2145
2181
  member.id === activeSuggestionId || member.id === focusSuggestionId,
2146
2182
  )}
2183
+ expanded={expandedProposalIds.has(proposalId)}
2184
+ onExpandedChange={(expanded) => setProposalExpanded(proposalId, expanded)}
2147
2185
  deciding={members.some(suggestionBusy)}
2148
2186
  canDecide={canDecideSuggestions && !!onDecideSuggestionProposal}
2149
2187
  onDecide={(decision) =>
@@ -2176,6 +2214,7 @@ export function CommentsSidebar({
2176
2214
  surface={surface}
2177
2215
  onClose={onClose}
2178
2216
  currentUserEmail={currentUserEmail}
2217
+ currentUserName={currentUserName}
2179
2218
  key={suggestion.id}
2180
2219
  marginTop={marginTop}
2181
2220
  onHeightChange={handleThreadCardHeightChange}
@@ -2667,6 +2706,8 @@ function ProposalGroup({
2667
2706
  totalCount,
2668
2707
  members,
2669
2708
  active,
2709
+ expanded,
2710
+ onExpandedChange,
2670
2711
  deciding,
2671
2712
  canDecide,
2672
2713
  onDecide,
@@ -2679,6 +2720,8 @@ function ProposalGroup({
2679
2720
  totalCount: number;
2680
2721
  members: ResourceSuggestion[];
2681
2722
  active: boolean;
2723
+ expanded: boolean;
2724
+ onExpandedChange: (expanded: boolean) => void;
2682
2725
  deciding: boolean;
2683
2726
  canDecide: boolean;
2684
2727
  onDecide: (decision: SuggestionDecision) => void;
@@ -2686,11 +2729,10 @@ function ProposalGroup({
2686
2729
  children: ReactNode;
2687
2730
  t: ReturnType<typeof useT>;
2688
2731
  }) {
2689
- const [expanded, setExpanded] = useState(active);
2690
2732
  const groupRef = useRef<HTMLDivElement>(null);
2691
2733
  const detailsId = useId();
2692
- useEffect(() => {
2693
- if (active) setExpanded(true);
2734
+ useLayoutEffect(() => {
2735
+ if (active) onExpandedChange(true);
2694
2736
  }, [active]);
2695
2737
  useLayoutEffect(() => {
2696
2738
  const element = groupRef.current;
@@ -2714,7 +2756,7 @@ function ProposalGroup({
2714
2756
  type="button"
2715
2757
  aria-expanded={expanded}
2716
2758
  aria-controls={detailsId}
2717
- onClick={() => setExpanded((current) => !current)}
2759
+ onClick={() => onExpandedChange(!expanded)}
2718
2760
  className="flex w-full min-w-0 items-start gap-2 px-3 py-2 text-start text-xs hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
2719
2761
  >
2720
2762
  <IconChevronDown
@@ -2733,7 +2775,9 @@ function ProposalGroup({
2733
2775
  {summary}
2734
2776
  </span>
2735
2777
  <span className="shrink-0 text-muted-foreground">
2736
- {t("comments.proposalEditCount", { count: totalCount })}
2778
+ {t("comments.proposalEditCount", {
2779
+ count: pending.length || totalCount,
2780
+ })}
2737
2781
  </span>
2738
2782
  </button>
2739
2783
  {expanded ? (
@@ -2796,6 +2840,19 @@ export function agentSuggestionReason(
2796
2840
  return reason;
2797
2841
  }
2798
2842
 
2843
+ /**
2844
+ * A change that adds or removes a line break, or swaps one run of whitespace
2845
+ * for another, doesn't say which lines it joins or splits, and a break alone
2846
+ * reads as a lone "↵", so what it replaces is shown too.
2847
+ */
2848
+ function changesLayout(removed: string, inserted: string) {
2849
+ const before = suggestionAnchorText(removed);
2850
+ const after = suggestionAnchorText(inserted);
2851
+ return (
2852
+ /\n/.test(before + after) || (/^\s+$/.test(before) && /^\s+$/.test(after))
2853
+ );
2854
+ }
2855
+
2799
2856
  function SuggestionOperationSummary({
2800
2857
  operations,
2801
2858
  expanded,
@@ -2851,6 +2908,18 @@ function SuggestionOperationSummary({
2851
2908
  : null;
2852
2909
  const hasSharedContext = diff?.some((part) => part.type === "equal");
2853
2910
  const richText = /[<>*_`\[\]]/.test(previousText + nextText);
2911
+ const changed = (type: "delete" | "insert", whole: string) =>
2912
+ diff && hasSharedContext
2913
+ ? diff
2914
+ .flatMap((part) => (part.type === type ? [part.text] : []))
2915
+ .join("")
2916
+ : whole;
2917
+ const showReplaced =
2918
+ expanded ||
2919
+ changesLayout(
2920
+ changed("delete", previousText),
2921
+ changed("insert", nextText),
2922
+ );
2854
2923
  const renderDiff = (side: "before" | "after") => {
2855
2924
  let offset = 0;
2856
2925
  const presentation =
@@ -2879,7 +2948,9 @@ function SuggestionOperationSummary({
2879
2948
  to: from + part.text.length,
2880
2949
  },
2881
2950
  )
2882
- : part.text}
2951
+ : part.type !== "equal" && /^\s+$/.test(part.text)
2952
+ ? suggestionTextForDisplay(part.text)
2953
+ : part.text}
2883
2954
  </span>
2884
2955
  );
2885
2956
  });
@@ -2897,7 +2968,7 @@ function SuggestionOperationSummary({
2897
2968
  : renderSuggestionText(nextText, nextPresentation)}
2898
2969
  {"”"}
2899
2970
  </div>
2900
- {expanded ? (
2971
+ {showReplaced ? (
2901
2972
  <div className="text-muted-foreground">
2902
2973
  {t("comments.suggestionReplace")}: {"“"}
2903
2974
  {diff && hasSharedContext
@@ -2947,11 +3018,13 @@ function DraftSuggestionThreadView({
2947
3018
  surface = "rail",
2948
3019
  onClose,
2949
3020
  currentUserEmail,
3021
+ currentUserName,
2950
3022
  t,
2951
3023
  }: {
2952
3024
  surface?: CommentSurface;
2953
3025
  onClose?: () => void;
2954
3026
  currentUserEmail?: string;
3027
+ currentUserName?: string;
2955
3028
  marginTop?: number;
2956
3029
  onHeightChange: (threadId: string, height: number) => void;
2957
3030
  suggestion: DraftSuggestion;
@@ -2984,7 +3057,11 @@ function DraftSuggestionThreadView({
2984
3057
  {
2985
3058
  id: suggestion.id,
2986
3059
  author_email: suggestion.authorEmail ?? "",
2987
- author_name: null,
3060
+ author_name: ownAuthorName(
3061
+ suggestion.authorEmail,
3062
+ currentUserEmail,
3063
+ currentUserName,
3064
+ ),
2988
3065
  created_at: suggestion.createdAt,
2989
3066
  content: "",
2990
3067
  mentions: [],
@@ -3102,11 +3179,13 @@ function SuggestionThreadView({
3102
3179
  surface = "rail",
3103
3180
  onClose,
3104
3181
  currentUserEmail,
3182
+ currentUserName,
3105
3183
  t,
3106
3184
  }: {
3107
3185
  surface?: CommentSurface;
3108
3186
  onClose?: () => void;
3109
3187
  currentUserEmail?: string;
3188
+ currentUserName?: string;
3110
3189
  compact: boolean;
3111
3190
  replyDrafts: ReturnType<typeof useCommentReplyDrafts>;
3112
3191
  marginTop?: number;
@@ -3182,6 +3261,11 @@ function SuggestionThreadView({
3182
3261
  author_email: suggestion.authorEmail ?? "",
3183
3262
  author_name:
3184
3263
  root?.authorName ??
3264
+ ownAuthorName(
3265
+ suggestion.authorEmail,
3266
+ currentUserEmail,
3267
+ currentUserName,
3268
+ ) ??
3185
3269
  (suggestion.authorEmail ? null : suggestion.actorKind),
3186
3270
  created_at: suggestion.createdAt,
3187
3271
  content: "",
@@ -3192,6 +3276,11 @@ function SuggestionThreadView({
3192
3276
  author_email: comment.authorEmail ?? "",
3193
3277
  author_name:
3194
3278
  comment.authorName ??
3279
+ ownAuthorName(
3280
+ comment.authorEmail,
3281
+ currentUserEmail,
3282
+ currentUserName,
3283
+ ) ??
3195
3284
  (comment.authorEmail ? null : comment.createdBy),
3196
3285
  created_at: comment.createdAt,
3197
3286
  content: comment.body,
@@ -327,6 +327,8 @@ import {
327
327
  planSuggestionDraftPersistence,
328
328
  saveUnlessSuggestionChanged,
329
329
  suggestionAmendmentIdempotencyKey,
330
+ suggestionProposalCreationIdempotencyKey,
331
+ forgetSuggestionProposalCreationIdempotencyKey,
330
332
  type DraftSuggestion,
331
333
  type SuggestionDraftSession,
332
334
  type SuggestionPersistenceEntry,
@@ -336,11 +338,13 @@ import {
336
338
  import { suggestedEditorIsolation } from "./suggestions/editor-isolation";
337
339
  import { useRememberedEditorMode } from "./suggestions/editor-mode-memory";
338
340
  import {
341
+ createDraftSuggestionPresentationTransition,
339
342
  createObservedSuggestionPresentationTransition,
340
343
  hydrateSuggestionPresentationTransitions,
341
344
  preciseSuggestionPresentationOperations,
342
345
  retainCommittedSuggestionPresentationTransitions,
343
346
  resolveSuggestionPresentationRange,
347
+ resolveSuggestionPresentationRangeInDraft,
344
348
  suggestionPresentationTransitionKey,
345
349
  type SuggestionPresentationTransition,
346
350
  type SuggestionPresentationTransitions,
@@ -635,6 +639,7 @@ export function suggestionPresentation(
635
639
  currentMarkdown: string,
636
640
  transition?: SuggestionPresentationTransition,
637
641
  observedTransition?: SuggestionPresentationTransition,
642
+ draftTransition?: SuggestionPresentationTransition,
638
643
  ): VisualEditorSuggestion | null {
639
644
  if (suggestion.status !== "pending") return null;
640
645
  const operation = suggestion.operations[0];
@@ -671,12 +676,22 @@ export function suggestionPresentation(
671
676
  ) {
672
677
  return null;
673
678
  }
674
- const range = resolveSuggestionPresentationRange(
679
+ const canonicalRange = resolveSuggestionPresentationRange(
675
680
  currentMarkdown,
676
681
  operation,
677
682
  transition,
678
683
  observedTransition,
679
684
  );
685
+ const range =
686
+ canonicalRange ??
687
+ (draftTransition?.after === currentMarkdown
688
+ ? resolveSuggestionPresentationRangeInDraft(
689
+ draftTransition,
690
+ operation,
691
+ transition,
692
+ observedTransition,
693
+ )
694
+ : null);
680
695
  if (!range) return null;
681
696
  const editorMarkdown = canonicalizeNfm(currentMarkdown);
682
697
  const currentText = currentMarkdown.slice(range.from, range.to);
@@ -694,7 +709,9 @@ export function suggestionPresentation(
694
709
  return {
695
710
  id: suggestion.id,
696
711
  kind: operation.kind as VisualEditorSuggestion["kind"],
697
- canonicalOperation: operation,
712
+ // The editor re-resolves a canonical operation on its own text, where only
713
+ // the draft carries this one, so the anchor places it instead.
714
+ canonicalOperation: canonicalRange ? operation : undefined,
698
715
  canonicalTransition: transition,
699
716
  observedTransition,
700
717
  beforeText: before.changedText,
@@ -726,6 +743,7 @@ export function suggestionPresentations(
726
743
  currentMarkdown: string,
727
744
  transition?: SuggestionPresentationTransition,
728
745
  observedTransition?: SuggestionPresentationTransition,
746
+ draftTransition?: SuggestionPresentationTransition,
729
747
  ): VisualEditorSuggestion[] {
730
748
  // Decided edits have no presentation; skip their full-Page diff.
731
749
  if (suggestion.status !== "pending") return [];
@@ -734,6 +752,7 @@ export function suggestionPresentations(
734
752
  currentMarkdown,
735
753
  transition,
736
754
  observedTransition,
755
+ draftTransition,
737
756
  );
738
757
  if (suggestion.operations.length !== 1) return original ? [original] : [];
739
758
  const saved = suggestion.operations[0]!;
@@ -746,6 +765,7 @@ export function suggestionPresentations(
746
765
  currentMarkdown,
747
766
  transition,
748
767
  observedTransition,
768
+ draftTransition,
749
769
  ),
750
770
  );
751
771
  return precise.every((presentation) => presentation !== null)
@@ -764,6 +784,13 @@ export function documentEditorSuggestionPresentations(args: {
764
784
  transitions: SuggestionPresentationTransitions;
765
785
  observedTransition?: SuggestionPresentationTransition;
766
786
  }): VisualEditorSuggestion[] {
787
+ const draftTransition =
788
+ args.savedSuggestions.length > 0
789
+ ? (createDraftSuggestionPresentationTransition(
790
+ args.drafts,
791
+ args.currentMarkdown,
792
+ ) ?? undefined)
793
+ : undefined;
767
794
  const savedPresentations: VisualEditorSuggestion[] = [];
768
795
  for (const suggestion of args.savedSuggestions) {
769
796
  if (suggestion.id === args.editingSuggestionId) continue;
@@ -773,6 +800,7 @@ export function documentEditorSuggestionPresentations(args: {
773
800
  args.currentMarkdown,
774
801
  args.transitions.get(suggestionPresentationTransitionKey(suggestion)),
775
802
  args.observedTransition,
803
+ draftTransition,
776
804
  ),
777
805
  );
778
806
  }
@@ -5906,6 +5934,10 @@ function PageEditorSessionBody({
5906
5934
  unresolved.request.idempotencyKey,
5907
5935
  unresolved.baseId,
5908
5936
  );
5937
+ forgetSuggestionProposalCreationIdempotencyKey(
5938
+ suggestionProposalCreationKeysRef.current,
5939
+ unresolved.request,
5940
+ );
5909
5941
  unresolvedProposalCreationRef.current = null;
5910
5942
  }
5911
5943
  const operations = suggestionDraftOperations(base, draft);
@@ -6086,29 +6118,28 @@ function PageEditorSessionBody({
6086
6118
  if (create.length > 0) {
6087
6119
  const pending = create.map(({ operation }) => operation);
6088
6120
  const pendingKeys = create.map(({ key }) => key);
6089
- const requestKey = JSON.stringify([base.id, pendingKeys]);
6090
- const idempotencyKey =
6091
- suggestionProposalCreationKeysRef.current.get(requestKey) ??
6092
- globalThis.crypto.randomUUID();
6093
- suggestionProposalCreationKeysRef.current.set(
6094
- requestKey,
6095
- idempotencyKey,
6096
- );
6097
6121
  const existingProposal = suggestionProposalsRef.current.get(base.id);
6098
6122
  const proposalSummary =
6099
6123
  existingProposal?.summary ?? t("editor.toolbar.suggestEdits");
6100
- const request = {
6124
+ const fields = {
6101
6125
  resourceType: "document",
6102
6126
  resourceId: documentId,
6103
6127
  adapterKind: "content.document-markdown",
6104
6128
  baseRevision: base.baseRevision,
6105
6129
  summary: proposalSummary,
6106
6130
  proposalId: existingProposal?.id,
6107
- idempotencyKey,
6108
6131
  suggestions: pending.map((operation) => ({
6109
6132
  summary: proposalSummary,
6110
6133
  operations: [operation],
6111
6134
  })),
6135
+ };
6136
+ const idempotencyKey = suggestionProposalCreationIdempotencyKey(
6137
+ suggestionProposalCreationKeysRef.current,
6138
+ fields,
6139
+ );
6140
+ const request = {
6141
+ ...fields,
6142
+ idempotencyKey,
6112
6143
  } satisfies Parameters<
6113
6144
  typeof createSuggestionProposal.mutateAsync
6114
6145
  >[0];
@@ -6120,6 +6151,10 @@ function PageEditorSessionBody({
6120
6151
  };
6121
6152
  const created = await createSuggestionProposal.mutateAsync(request);
6122
6153
  recordCreated(created, pendingKeys, pending, idempotencyKey, base.id);
6154
+ forgetSuggestionProposalCreationIdempotencyKey(
6155
+ suggestionProposalCreationKeysRef.current,
6156
+ request,
6157
+ );
6123
6158
  unresolvedProposalCreationRef.current = null;
6124
6159
  pendingKeys.forEach((operationKey, index) => {
6125
6160
  persisted.set(operationKey, created.suggestions[index]!);
@@ -6135,7 +6170,14 @@ function PageEditorSessionBody({
6135
6170
  status < 500 &&
6136
6171
  status !== 408 &&
6137
6172
  status !== 429;
6138
- if (clientError) unresolvedProposalCreationRef.current = null;
6173
+ const refused = clientError && unresolvedProposalCreationRef.current;
6174
+ if (refused) {
6175
+ forgetSuggestionProposalCreationIdempotencyKey(
6176
+ suggestionProposalCreationKeysRef.current,
6177
+ refused.request,
6178
+ );
6179
+ unresolvedProposalCreationRef.current = null;
6180
+ }
6139
6181
  adoptConfirmedSuggestions();
6140
6182
  if (error instanceof SuggestionFormattingMappingError) {
6141
6183
  // The draft keeps its persistent unsupported-formatting alert.
@@ -8651,6 +8693,7 @@ function PageEditorSessionBody({
8651
8693
  onSelectedThreadChange={setSelectedThreadId}
8652
8694
  onHoveredThreadChange={setHoveredThreadId}
8653
8695
  currentUserEmail={session?.email}
8696
+ currentUserName={session?.name}
8654
8697
  currentUserOrgId={session?.orgId}
8655
8698
  canComment={canComment}
8656
8699
  canResolve={canEdit}
@@ -9,8 +9,9 @@ import { Fragment, type ReactNode } from "react";
9
9
 
10
10
  function renderNode(node: SuggestionPresentationNode, key: string): ReactNode {
11
11
  if (node.type === "text") return <Fragment key={key}>{node.value}</Fragment>;
12
- if (node.type === "indent")
12
+ if (node.type === "indent" || node.type === "block")
13
13
  return <Fragment key={key}>{node.value}</Fragment>;
14
+ if (node.type === "lineBreak") return <br key={key} />;
14
15
  const children = node.children.map((child, index) =>
15
16
  renderNode(child, `${key}-${index}`),
16
17
  );
@@ -42,16 +43,30 @@ export function SuggestionText({
42
43
  context?: SuggestionPresentationContext;
43
44
  }) {
44
45
  const t = useT();
45
- const nodes = context
46
+ const presented = context
46
47
  ? suggestionTextPresentationForSource(content, context)
47
48
  : suggestionTextPresentation(content);
48
- if (!nodes) {
49
+ if (!presented) {
49
50
  return (
50
51
  <span data-suggestion-text-unavailable="true">
51
52
  {t("editor.sourceComponent.previewUnavailable")}
52
53
  </span>
53
54
  );
54
55
  }
56
+ // A new paragraph's own line break would open or close the card with an
57
+ // empty line, so a break at either edge shows as its marker instead; the
58
+ // breaks between lines stay.
59
+ let start = 0;
60
+ let end = presented.length;
61
+ while (start < end && presented[start]!.type === "lineBreak") start += 1;
62
+ while (end > start && presented[end - 1]!.type === "lineBreak") end -= 1;
63
+ const marker = (count: number): SuggestionPresentationNode[] =>
64
+ count ? [{ type: "text", value: "↵".repeat(count) }] : [];
65
+ const nodes = [
66
+ ...marker(start),
67
+ ...presented.slice(start, end),
68
+ ...marker(presented.length - end),
69
+ ];
55
70
  return (
56
71
  <span className="whitespace-pre-wrap break-words">
57
72
  {nodes.map((node, index) => renderNode(node, String(index)))}