@agent-native/core-corpus 0.217.0 → 0.219.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
- package/corpus/templates/analytics/CHANGELOG.md +2 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
- package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/i18n-data.ts +8 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
- package/corpus/templates/design/CHANGELOG.md +8 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
- package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +18 -64
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
- package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
- package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
- package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
- package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
- package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
- package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +1 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- 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 === "
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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 {
|
|
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) =>
|
|
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 (
|
|
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
|
|
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. */
|