@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
@@ -80,7 +80,11 @@ function appendPresentationNode(
80
80
  node: SuggestionPresentationNode,
81
81
  showLinkDestination: boolean,
82
82
  ): void {
83
- if (node.type === "text" || node.type === "indent") {
83
+ if (node.type === "lineBreak") {
84
+ parent.append(document.createElement("br"));
85
+ return;
86
+ }
87
+ if (node.type === "text" || node.type === "indent" || node.type === "block") {
84
88
  parent.append(document.createTextNode(node.value));
85
89
  return;
86
90
  }
@@ -127,7 +131,40 @@ function appendSuggestionText(
127
131
  }
128
132
  }
129
133
 
130
- function insertionWidget(spec: SuggestionHighlightSpec, active: boolean) {
134
+ // While the reader can type, a click on suggested text that isn't in the
135
+ // document places the caret where the widget sits instead of focusing it.
136
+ // `control` takes the button role when the widget itself has a role to keep.
137
+ function identifyWidget(
138
+ widget: HTMLElement,
139
+ spec: SuggestionHighlightSpec,
140
+ label: string,
141
+ position = spec.from,
142
+ control = widget,
143
+ ) {
144
+ widget.setAttribute("data-suggestion-id", spec.suggestionId);
145
+ widget.setAttribute("data-suggestion-widget", "true");
146
+ if (spec.editableBoundary) {
147
+ widget.setAttribute("data-suggestion-edit-boundary", "true");
148
+ widget.setAttribute("data-suggestion-position", String(position));
149
+ } else {
150
+ control.setAttribute("role", "button");
151
+ control.setAttribute("tabindex", "0");
152
+ control.setAttribute("aria-label", label);
153
+ }
154
+ }
155
+
156
+ function insertionClass(spec: SuggestionHighlightSpec) {
157
+ return `${
158
+ spec.kind === "add_block" ? "suggestion-add-block" : "suggestion-insert"
159
+ } suggestion-proposed-text`;
160
+ }
161
+
162
+ function insertionWidget(
163
+ spec: SuggestionHighlightSpec,
164
+ active: boolean,
165
+ part?: { text: string; context?: SuggestionPresentationContext },
166
+ position?: number,
167
+ ) {
131
168
  return () => {
132
169
  const widget = document.createElement("span");
133
170
  if (spec.settling) {
@@ -142,46 +179,35 @@ function insertionWidget(spec: SuggestionHighlightSpec, active: boolean) {
142
179
  return widget;
143
180
  }
144
181
  widget.className = classes(
145
- `${
146
- spec.kind === "add_block" ? "suggestion-add-block" : "suggestion-insert"
147
- } suggestion-proposed-text suggestion-inline-widget`,
182
+ `${insertionClass(spec)} suggestion-inline-widget`,
148
183
  active,
149
184
  );
150
- widget.setAttribute("data-suggestion-id", spec.suggestionId);
151
- widget.setAttribute("data-suggestion-widget", "true");
152
- widget.setAttribute("role", "button");
153
- widget.setAttribute("tabindex", "0");
154
- widget.setAttribute("aria-label", "Inspect suggested insertion");
185
+ identifyWidget(widget, spec, "Inspect suggested insertion", position);
155
186
  appendSuggestionText(
156
187
  widget,
157
- spec.insertedText ?? "",
158
- spec.insertedPresentation,
188
+ part ? part.text : (spec.insertedText ?? ""),
189
+ part ? part.context : spec.insertedPresentation,
159
190
  );
160
191
  return widget;
161
192
  };
162
193
  }
163
194
 
164
- function deletionWidget(spec: SuggestionHighlightSpec, active: boolean) {
195
+ function deletionWidget(
196
+ spec: SuggestionHighlightSpec,
197
+ active: boolean,
198
+ part?: { text: string; context?: SuggestionPresentationContext },
199
+ ) {
165
200
  return () => {
166
201
  const widget = document.createElement("span");
167
202
  widget.className = classes(
168
203
  "suggestion-delete-widget suggestion-deleted-text suggestion-inline-widget",
169
204
  active,
170
205
  );
171
- widget.setAttribute("data-suggestion-id", spec.suggestionId);
172
- widget.setAttribute("data-suggestion-widget", "true");
173
- if (spec.editableBoundary) {
174
- widget.setAttribute("data-suggestion-edit-boundary", "true");
175
- widget.setAttribute("data-suggestion-position", String(spec.from));
176
- } else {
177
- widget.setAttribute("role", "button");
178
- widget.setAttribute("tabindex", "0");
179
- widget.setAttribute("aria-label", "Inspect suggested deletion");
180
- }
206
+ identifyWidget(widget, spec, "Inspect suggested deletion");
181
207
  appendSuggestionText(
182
208
  widget,
183
- spec.deletedText ?? "",
184
- spec.deletedPresentation,
209
+ part ? part.text : (spec.deletedText ?? ""),
210
+ part ? part.context : spec.deletedPresentation,
185
211
  );
186
212
  return widget;
187
213
  };
@@ -199,6 +225,300 @@ export function editorDocumentSource(doc: ProseMirrorNode): string {
199
225
  return source;
200
226
  }
201
227
 
228
+ type SourceSpan = { from: number; to: number };
229
+
230
+ // How suggested text with line breaks meets the lines around it: the part
231
+ // that joins the line before the point, the whole lines it adds or removes,
232
+ // and the part that joins the line after.
233
+ interface SuggestedLines {
234
+ source: string;
235
+ head: SourceSpan | null;
236
+ lines: SourceSpan[];
237
+ tail: SourceSpan | null;
238
+ side: "before" | "after";
239
+ }
240
+
241
+ function suggestedLines(
242
+ text: string | undefined,
243
+ context: SuggestionPresentationContext | undefined,
244
+ ): SuggestedLines | null {
245
+ if (!text || !context || !text.includes("\n")) return null;
246
+ const { source, from, to } = context;
247
+ if (source.slice(from, to) !== text) return null;
248
+ const startsLine = from === 0 || source[from - 1] === "\n";
249
+ const endsLine = to === source.length || source[to] === "\n";
250
+ // Text that splits a line in two has no whole line to show on its own.
251
+ if (!startsLine && !endsLine) return null;
252
+ const spans: SourceSpan[] = [];
253
+ let start = from;
254
+ for (let index = from; index < to; index += 1) {
255
+ if (source[index] !== "\n") continue;
256
+ spans.push({ from: start, to: index });
257
+ start = index + 1;
258
+ }
259
+ spans.push({ from: start, to });
260
+ const head = startsLine ? null : spans.shift()!;
261
+ const tail = endsLine ? null : spans.pop()!;
262
+ const lines = spans.filter((span) => span.to > span.from);
263
+ if (!lines.length) return null;
264
+ return {
265
+ source,
266
+ head: head && head.to > head.from ? head : null,
267
+ lines,
268
+ tail: tail && tail.to > tail.from ? tail : null,
269
+ side: startsLine ? "before" : "after",
270
+ };
271
+ }
272
+
273
+ const LIST_LINE = /^\t*(?:[-*+] (\[[ xX]\] )?|(\d+)[.)] )/;
274
+ const LIST = /^(?:bulletList|orderedList|taskList)$/;
275
+
276
+ // Where a whole line belongs: how far it is indented, and the kind of list
277
+ // it adds an item to, if it is a list item.
278
+ interface LineShape {
279
+ indent: number;
280
+ list: string | null;
281
+ }
282
+
283
+ function lineShape(text: string): LineShape {
284
+ const list = LIST_LINE.exec(text);
285
+ return {
286
+ indent: /^\t*/.exec(text)![0].length,
287
+ list: !list
288
+ ? null
289
+ : list[1]
290
+ ? "taskList"
291
+ : list[2]
292
+ ? "orderedList"
293
+ : "bulletList",
294
+ };
295
+ }
296
+
297
+ // The document position beside the block at `point` where a whole line
298
+ // goes: out of the containers the point sits at the edge of, but no further
299
+ // than the line's indentation allows, and into a list only of its own kind.
300
+ // A line that can't be placed as written has no position.
301
+ function lineBoundary(
302
+ doc: ProseMirrorNode,
303
+ point: number,
304
+ side: "before" | "after",
305
+ shape: LineShape,
306
+ ): { position: number; inList: boolean } | null {
307
+ if (point < 0 || point > doc.content.size) return null;
308
+ const $point = doc.resolve(point);
309
+ // Lines added to code are code text, not blocks of their own.
310
+ if (
311
+ !$point.parent.isTextblock ||
312
+ $point.parent.type.spec.code ||
313
+ $point.parentOffset !== (side === "before" ? 0 : $point.parent.content.size)
314
+ )
315
+ return null;
316
+ // A Page indents a block one tab per block it sits in; a list's items
317
+ // indent their children, so the list itself adds none.
318
+ const indentInside = (depth: number) => {
319
+ let indent = 0;
320
+ for (let level = 1; level <= depth; level += 1) {
321
+ const node = $point.node(level);
322
+ indent += LIST.test(node.type.name) ? 0 : 1;
323
+ indent += Number(node.attrs.indent) || 0;
324
+ }
325
+ return indent;
326
+ };
327
+ let depth = $point.depth;
328
+ while (depth > 1) {
329
+ const container = $point.node(depth - 1);
330
+ if (
331
+ container.type.name === shape.list &&
332
+ indentInside(depth - 1) === shape.indent
333
+ )
334
+ break;
335
+ const index = $point.index(depth - 1);
336
+ if (side === "before" ? index !== 0 : index !== container.childCount - 1)
337
+ break;
338
+ if (indentInside(depth - 2) < shape.indent) break;
339
+ depth -= 1;
340
+ }
341
+ const container = $point.node(depth - 1);
342
+ const inList = LIST.test(container.type.name);
343
+ if (
344
+ indentInside(depth - 1) !== shape.indent ||
345
+ (inList && container.type.name !== shape.list)
346
+ )
347
+ return null;
348
+ return {
349
+ position: side === "before" ? $point.before(depth) : $point.after(depth),
350
+ inList,
351
+ };
352
+ }
353
+
354
+ // A whole suggested line drawn as the block it would become.
355
+ function lineWidget(
356
+ spec: SuggestionHighlightSpec,
357
+ active: boolean,
358
+ className: string,
359
+ label: string,
360
+ text: string,
361
+ nodes: SuggestionPresentationNode[],
362
+ inList: boolean,
363
+ ) {
364
+ return () => {
365
+ const list = LIST_LINE.exec(text);
366
+ const heading = /^\t*(#{1,6}) /.exec(text);
367
+ const quote = /^\t*> /.test(text);
368
+ const widget = document.createElement(inList ? "li" : "div");
369
+ widget.className = classes(`${className} suggestion-block-widget`, active);
370
+ const block = document.createElement(
371
+ heading ? `h${heading[1]!.length}` : "p",
372
+ );
373
+ if (list?.[1])
374
+ block.append(document.createTextNode(/x/i.test(list[1]) ? "☑ " : "☐ "));
375
+ // The element and where it sits show the line's structure, so its
376
+ // indentation and marker aren't repeated; an empty paragraph keeps its
377
+ // pilcrow so the line can be seen.
378
+ for (const node of nodes)
379
+ if (
380
+ node.type !== "indent" &&
381
+ (node.type !== "block" || node.value === "¶")
382
+ )
383
+ appendPresentationNode(block, node, true);
384
+ let content: HTMLElement = block;
385
+ if (quote) {
386
+ content = document.createElement("blockquote");
387
+ content.append(block);
388
+ }
389
+ if (list && !inList) {
390
+ const item = document.createElement("li");
391
+ item.append(content);
392
+ content = document.createElement(list[2] ? "ol" : "ul");
393
+ // Each line is its own list, so a numbered line keeps its number.
394
+ if (list[2]) content.setAttribute("start", list[2]);
395
+ content.append(item);
396
+ }
397
+ widget.append(content);
398
+ // An item in a list stays a list item to assistive tech; its content is
399
+ // the button.
400
+ identifyWidget(widget, spec, label, undefined, inList ? content : widget);
401
+ return widget;
402
+ };
403
+ }
404
+
405
+ // Whole suggested lines go beside the block at the point, in order; the
406
+ // parts that join a line stay inline at the point.
407
+ function lineDecorations(
408
+ doc: ProseMirrorNode,
409
+ spec: SuggestionHighlightSpec,
410
+ active: boolean,
411
+ lines: SuggestedLines,
412
+ deleted: boolean,
413
+ attrs: Record<string, string>,
414
+ ): Decoration[] | null {
415
+ const className = deleted
416
+ ? "suggestion-delete-widget suggestion-deleted-text"
417
+ : insertionClass(spec);
418
+ const label = deleted
419
+ ? "Inspect suggested deletion"
420
+ : "Inspect suggested insertion";
421
+ const placed = lines.lines.map((line) => {
422
+ const text = lines.source.slice(line.from, line.to);
423
+ return {
424
+ line,
425
+ boundary: lineBoundary(doc, spec.from, lines.side, lineShape(text)),
426
+ nodes: suggestionTextPresentationForSource(text, {
427
+ source: lines.source,
428
+ ...line,
429
+ }),
430
+ };
431
+ });
432
+ if (placed.some(({ boundary, nodes }) => !boundary || !nodes)) return null;
433
+ const decorations: Decoration[] = [];
434
+ const inline = (span: SourceSpan | null, part: "head" | "tail") => {
435
+ if (!span) return;
436
+ const text = lines.source.slice(span.from, span.to);
437
+ const context = { source: lines.source, ...span };
438
+ decorations.push(
439
+ Decoration.widget(
440
+ spec.from,
441
+ deleted
442
+ ? deletionWidget(spec, active, { text, context })
443
+ : insertionWidget(spec, active, { text, context }),
444
+ {
445
+ key: JSON.stringify([
446
+ spec.suggestionId,
447
+ part,
448
+ spec.from,
449
+ text,
450
+ active,
451
+ !!spec.editableBoundary,
452
+ ]),
453
+ marks: [],
454
+ side: part === "head" ? 1 : 2,
455
+ ...attrs,
456
+ },
457
+ ),
458
+ );
459
+ };
460
+ inline(lines.head, "head");
461
+ placed.forEach(({ line, boundary, nodes }, index) => {
462
+ decorations.push(
463
+ Decoration.widget(
464
+ boundary!.position,
465
+ lineWidget(
466
+ spec,
467
+ active,
468
+ className,
469
+ label,
470
+ lines.source.slice(line.from, line.to),
471
+ nodes!,
472
+ boundary!.inList,
473
+ ),
474
+ {
475
+ key: JSON.stringify([
476
+ spec.suggestionId,
477
+ deleted ? "deleted-line" : "inserted-line",
478
+ boundary!.position,
479
+ lines.source.slice(line.from, line.to),
480
+ boundary!.inList,
481
+ active,
482
+ !!spec.editableBoundary,
483
+ ]),
484
+ marks: [],
485
+ side: lines.side === "before" ? index - placed.length : index + 1,
486
+ ...attrs,
487
+ },
488
+ ),
489
+ );
490
+ });
491
+ inline(lines.tail, "tail");
492
+ return decorations;
493
+ }
494
+
495
+ // Whole blocks a saved deletion removes are marked as blocks, so the
496
+ // paragraph itself reads as removed rather than only its text.
497
+ function deletedBlockDecorations(
498
+ doc: ProseMirrorNode,
499
+ range: Range,
500
+ active: boolean,
501
+ ): Decoration[] {
502
+ const decorations: Decoration[] = [];
503
+ doc.nodesBetween(range.from, range.to, (node, position) => {
504
+ if (!node.isTextblock) return true;
505
+ const contentFrom = position + 1;
506
+ const contentTo = position + 1 + node.content.size;
507
+ if (
508
+ node.content.size > 0 &&
509
+ range.from <= contentFrom &&
510
+ range.to >= contentTo
511
+ )
512
+ decorations.push(
513
+ Decoration.node(position, position + node.nodeSize, {
514
+ class: classes("suggestion-delete-block", active),
515
+ }),
516
+ );
517
+ return false;
518
+ });
519
+ return decorations;
520
+ }
521
+
202
522
  function insertionAtOperationAnchor(
203
523
  doc: ProseMirrorNode,
204
524
  from: number,
@@ -414,6 +734,8 @@ function buildDecorations(
414
734
  class: classes("suggestion-delete suggestion-deleted-text", active),
415
735
  }),
416
736
  );
737
+ if (spec.kind === "delete")
738
+ decorations.push(...deletedBlockDecorations(doc, range, active));
417
739
  }
418
740
  } else if (spec.kind === "mark" && range) {
419
741
  decorations.push(
@@ -429,20 +751,45 @@ function buildDecorations(
429
751
  spec.kind === "insert" ||
430
752
  spec.kind === "add_block"
431
753
  ) {
754
+ const lines =
755
+ spec.kind === "replace"
756
+ ? null
757
+ : suggestedLines(spec.insertedText, spec.insertedPresentation);
758
+ const lineParts =
759
+ lines && lineDecorations(doc, spec, active, lines, false, attrs);
432
760
  const anchor = clampPosition(
433
761
  spec.kind === "replace" && range ? range.to : spec.from,
434
762
  size,
435
763
  );
436
- decorations.push(
437
- Decoration.widget(anchor, insertionWidget(spec, active), {
438
- key: `${spec.suggestionId}:inserted:${anchor}`,
439
- marks: [],
440
- side: 1,
441
- ...attrs,
442
- }),
443
- );
764
+ if (lineParts) decorations.push(...lineParts);
765
+ else
766
+ decorations.push(
767
+ Decoration.widget(
768
+ anchor,
769
+ insertionWidget(spec, active, undefined, anchor),
770
+ {
771
+ // ProseMirror reuses a widget's element while its key holds, so
772
+ // the key covers everything the element shows.
773
+ key: `${spec.suggestionId}:inserted:${anchor}:${JSON.stringify([
774
+ spec.insertedText,
775
+ active,
776
+ !!spec.editableBoundary,
777
+ ])}`,
778
+ marks: [],
779
+ side: 1,
780
+ ...attrs,
781
+ },
782
+ ),
783
+ );
444
784
  }
445
- if (spec.deletedText) {
785
+ const deletedLines =
786
+ spec.deletedText &&
787
+ suggestedLines(spec.deletedText, spec.deletedPresentation);
788
+ const deletedLineParts =
789
+ deletedLines &&
790
+ lineDecorations(doc, spec, active, deletedLines, true, attrs);
791
+ if (deletedLineParts) decorations.push(...deletedLineParts);
792
+ else if (spec.deletedText) {
446
793
  decorations.push(
447
794
  Decoration.widget(
448
795
  clampPosition(spec.from, size),
@@ -451,8 +798,10 @@ function buildDecorations(
451
798
  key: JSON.stringify([
452
799
  spec.suggestionId,
453
800
  "deleted",
801
+ clampPosition(spec.from, size),
454
802
  spec.deletedText,
455
803
  active,
804
+ !!spec.editableBoundary,
456
805
  ]),
457
806
  marks: [],
458
807
  side: 1,
@@ -173,6 +173,37 @@ export function suggestionAmendmentIdempotencyKey(
173
173
  return key;
174
174
  }
175
175
 
176
+ type ProposalCreationRequest<Request extends object> = Request & {
177
+ idempotencyKey?: string;
178
+ };
179
+
180
+ function proposalCreationRequestKey<Request extends object>(
181
+ request: ProposalCreationRequest<Request>,
182
+ ) {
183
+ const { idempotencyKey: _idempotencyKey, ...fingerprint } = request;
184
+ return JSON.stringify(fingerprint);
185
+ }
186
+
187
+ // The server fingerprints a proposal creation with its proposal, summary, and
188
+ // operations, and replays the first result for a repeated key. A key repeats
189
+ // only for a retry of the same request: once a creation is recorded or
190
+ // refused, the same text drafted again, as after undo and redo, is a new
191
+ // creation and needs a new key.
192
+ export function suggestionProposalCreationIdempotencyKey<
193
+ Request extends object,
194
+ >(keys: Map<string, string>, request: ProposalCreationRequest<Request>) {
195
+ const requestKey = proposalCreationRequestKey(request);
196
+ const key = keys.get(requestKey) ?? globalThis.crypto.randomUUID();
197
+ keys.set(requestKey, key);
198
+ return key;
199
+ }
200
+
201
+ export function forgetSuggestionProposalCreationIdempotencyKey<
202
+ Request extends object,
203
+ >(keys: Map<string, string>, request: ProposalCreationRequest<Request>) {
204
+ keys.delete(proposalCreationRequestKey(request));
205
+ }
206
+
176
207
  export function createSuggestionDraftSession(input: {
177
208
  id: string;
178
209
  baseContent: string;
@@ -1,7 +1,10 @@
1
1
  import type { ResourceSuggestion } from "@agent-native/core/review";
2
2
  import { canonicalizeNfm } from "@shared/nfm";
3
3
  import { markdownSuggestionOperations } from "@shared/suggestion-diff";
4
- import { SuggestionFormattingMappingError } from "@shared/suggestion-formatting";
4
+ import {
5
+ SuggestionFormattingMappingError,
6
+ suggestionSourceAlignment,
7
+ } from "@shared/suggestion-formatting";
5
8
  import {
6
9
  resolveMarkdownSuggestionRange,
7
10
  resolveOutsideChange,
@@ -75,6 +78,18 @@ function intersects(left: Range, right: Range) {
75
78
  : left.from < right.to && left.to > right.from;
76
79
  }
77
80
 
81
+ // An edit that only touches another, even an insertion at another insertion's
82
+ // offset, still has one place beside it.
83
+ function overlaps(left: Range, right: Range) {
84
+ return left.from < right.to && left.to > right.from;
85
+ }
86
+
87
+ // Acceptance holds every accepted range fixed and places a pending edit in the
88
+ // first stretch of text between them that contains it.
89
+ function placedAfter(change: Range, target: Range) {
90
+ return change.to <= target.from && change.from < target.to;
91
+ }
92
+
78
93
  function reconstruct(before: string, changes: Change[]) {
79
94
  let cursor = 0;
80
95
  let result = "";
@@ -234,6 +249,40 @@ export function createObservedSuggestionPresentationTransition(
234
249
  return verifiedTransition(transition) ? transition : null;
235
250
  }
236
251
 
252
+ // A draft's operations address the stored Page, but the editor shows them on
253
+ // the Page's canonical form, which can differ, as a pipe table does.
254
+ export function createDraftSuggestionPresentationTransition(
255
+ drafts: Array<Pick<ResourceSuggestion, "operations">>,
256
+ draftMarkdown: string,
257
+ ): SuggestionPresentationTransition | null {
258
+ const operations = drafts.flatMap((draft) => draft.operations);
259
+ const base =
260
+ operations[0] && checkedOperation(operations[0])?.before.markdown;
261
+ const alignment = base === undefined ? null : suggestionSourceAlignment(base);
262
+ if (!alignment) return null;
263
+ const changes: Change[] = [];
264
+ for (const operation of operations) {
265
+ const checked = checkedOperation(operation);
266
+ if (!checked || checked.before.markdown !== base) return null;
267
+ const from = alignment.map(checked.range.from, "stored");
268
+ const to = alignment.map(checked.range.to, "stored");
269
+ if (from === null || to === null) return null;
270
+ changes.push({
271
+ from,
272
+ to,
273
+ beforeText: alignment.canonical.slice(from, to),
274
+ afterText: checked.after.changedText,
275
+ });
276
+ }
277
+ changes.sort((left, right) => left.from - right.from || left.to - right.to);
278
+ const transition = {
279
+ before: alignment.canonical,
280
+ after: draftMarkdown,
281
+ changes,
282
+ };
283
+ return verifiedTransition(transition) ? transition : null;
284
+ }
285
+
237
286
  function dependentOperation(
238
287
  operation: MarkdownOperation,
239
288
  transition: SuggestionPresentationTransition,
@@ -243,7 +292,7 @@ function dependentOperation(
243
292
  if (
244
293
  !checked ||
245
294
  checked.before.markdown !== transition.before ||
246
- transition.changes.some((change) => intersects(checked.range, change))
295
+ transition.changes.some((change) => overlaps(checked.range, change))
247
296
  )
248
297
  return null;
249
298
  return checked;
@@ -480,7 +529,7 @@ export function resolveSuggestionPresentationRange(
480
529
  .sort((left, right) => left.from - right.from || left.to - right.to);
481
530
  const union: Range[] = [];
482
531
  for (const sibling of siblings) {
483
- if (intersects(checked.range, sibling)) return null;
532
+ if (overlaps(checked.range, sibling)) return null;
484
533
  const previous = union[union.length - 1];
485
534
  if (previous && sibling.from <= previous.to)
486
535
  previous.to = Math.max(previous.to, sibling.to);
@@ -492,7 +541,7 @@ export function resolveSuggestionPresentationRange(
492
541
  });
493
542
  // Masked context must agree with the exact committed edits and unchanged target.
494
543
  const shift = transition.changes
495
- .filter((change) => change.to <= checked.range.from)
544
+ .filter((change) => placedAfter(change, checked.range))
496
545
  .reduce(
497
546
  (delta, change) =>
498
547
  delta + change.afterText.length - change.beforeText.length,
@@ -552,3 +601,32 @@ export function resolveSuggestionPresentationRange(
552
601
  },
553
602
  });
554
603
  }
604
+
605
+ // Text typed at a saved suggestion's spot splits the context that places it on
606
+ // the draft, so place it on the draft's base and carry it across the draft's
607
+ // edits. The editor maps a highlight past text typed at its position, so a
608
+ // saved edit stays after a draft insertion at the same offset.
609
+ export function resolveSuggestionPresentationRangeInDraft(
610
+ draft: SuggestionPresentationTransition,
611
+ operation: MarkdownOperation,
612
+ transition?: SuggestionPresentationTransition,
613
+ observedTransition?: SuggestionPresentationTransition,
614
+ ): Range | null {
615
+ if (!verifiedTransition(draft)) return null;
616
+ const range = resolveSuggestionPresentationRange(
617
+ draft.before,
618
+ operation,
619
+ transition,
620
+ observedTransition,
621
+ );
622
+ if (!range || draft.changes.some((change) => overlaps(range, change)))
623
+ return null;
624
+ const shift = draft.changes
625
+ .filter((change) => change.to <= range.from)
626
+ .reduce(
627
+ (delta, change) =>
628
+ delta + change.afterText.length - change.beforeText.length,
629
+ 0,
630
+ );
631
+ return { from: range.from + shift, to: range.to + shift };
632
+ }
@@ -2595,6 +2595,33 @@ audio.media-block__content {
2595
2595
  color: hsl(var(--suggestion));
2596
2596
  }
2597
2597
 
2598
+ /* A whole suggested line is drawn as the block it adds or removes, marked by
2599
+ a bar at its edge rather than an underline across it. */
2600
+ .content-editor .suggestion-block-widget[data-suggestion-widget] {
2601
+ border-radius: 0.2em;
2602
+ box-shadow: inset 2px 0 hsl(var(--suggestion) / 0.55);
2603
+ }
2604
+ /* A list item keeps its list's layout, which places the marker. */
2605
+ .content-editor .suggestion-block-widget[data-suggestion-widget]:not(li) {
2606
+ display: block;
2607
+ }
2608
+ .content-editor
2609
+ .suggestion-block-widget.suggestion-highlight--active[data-suggestion-widget] {
2610
+ box-shadow: inset 3px 0 hsl(var(--suggestion));
2611
+ }
2612
+ .content-editor .suggestion-block-widget[data-suggestion-widget] > * {
2613
+ color: inherit;
2614
+ }
2615
+ .content-editor
2616
+ .suggestion-delete-widget.suggestion-block-widget[data-suggestion-widget],
2617
+ .content-editor .suggestion-delete-block {
2618
+ box-shadow: inset 2px 0 hsl(var(--muted-foreground) / 0.5);
2619
+ }
2620
+ .content-editor .suggestion-delete-block {
2621
+ border-radius: 0.2em;
2622
+ background: hsl(var(--muted) / 0.7);
2623
+ }
2624
+
2598
2625
  .content-editor .suggestion-highlight--active {
2599
2626
  background: hsl(var(--suggestion) / 0.16);
2600
2627
  box-shadow: inset 0 -2px hsl(var(--suggestion));
@@ -2609,6 +2636,10 @@ audio.media-block__content {
2609
2636
  color: hsl(var(--foreground));
2610
2637
  text-decoration: none;
2611
2638
  }
2639
+ .content-editor .suggestion-delete-block.suggestion-highlight--active {
2640
+ background: hsl(var(--muted));
2641
+ text-decoration: none;
2642
+ }
2612
2643
 
2613
2644
  /* Comment composer. Rests as a filled single-line field; expands into a
2614
2645
  bordered box with a toolbar once focused or filled. */