@agent-native/core-corpus 0.217.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 (56) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +2 -0
  3. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  4. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  5. package/corpus/templates/assets/CHANGELOG.md +6 -0
  6. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  7. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  8. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  9. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  10. package/corpus/templates/brain/vitest.config.ts +7 -0
  11. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  12. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  13. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  14. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  15. package/corpus/templates/clips/CHANGELOG.md +7 -20
  16. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  17. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  18. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  19. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  20. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  21. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  22. package/corpus/templates/community/qr/package.json +1 -1
  23. package/corpus/templates/content/CHANGELOG.md +2 -0
  24. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
  25. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
  26. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  27. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  28. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  29. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  30. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  31. package/corpus/templates/content/app/global.css +31 -0
  32. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  33. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  34. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  35. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  36. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  37. package/corpus/templates/design/CHANGELOG.md +3 -0
  38. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  39. package/corpus/templates/design/app/pages/Index.tsx +18 -64
  40. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  41. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  42. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  43. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  44. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  45. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  46. package/corpus/templates/plan/CHANGELOG.md +6 -0
  47. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  48. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  49. package/corpus/templates/slides/CHANGELOG.md +1 -0
  50. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  51. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  52. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  53. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  54. package/package.json +1 -1
  55. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  56. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -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. */
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Accepting a suggestion on a Page that changed since it was suggested no longer shows "Something went wrong"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Suggested edits that add, remove, split, or merge paragraphs, line breaks, and list items now show where the break goes and save as one clear suggestion
@@ -483,6 +483,9 @@ function diffTokens(text: string): string[] {
483
483
  // is no longer than either, as diff-match-patch's semantic cleanup does.
484
484
  // Equalities between replacements stay, so separate word swaps remain
485
485
  // separately reviewable, and a line never folds, so blocks stay separate.
486
+ // The exception is a line the merged run begins or ends with: that is one
487
+ // insertion (text plus a new paragraph) Myers aligned across the existing
488
+ // separator, and it would otherwise split whenever another hunk exists.
486
489
  type ChangeRun = {
487
490
  deleted: string;
488
491
  inserted: string;
@@ -518,12 +521,17 @@ function foldShortEqualities(parts: DiffPart[]): DiffPart[] {
518
521
  const right = segments[index + 1];
519
522
  if (typeof equality !== "string" || typeof left !== "object") continue;
520
523
  if (typeof right !== "object") continue;
524
+ const insertions = left.ownDeleted === 0 && right.ownDeleted === 0;
521
525
  const oneSided =
522
- (left.ownDeleted === 0 && right.ownDeleted === 0) ||
523
- (left.ownInserted === 0 && right.ownInserted === 0);
526
+ insertions || (left.ownInserted === 0 && right.ownInserted === 0);
527
+ const merged = insertions
528
+ ? left.inserted + equality + right.inserted
529
+ : left.deleted + equality + right.deleted;
524
530
  if (
525
531
  !oneSided ||
526
- equality.includes("\n") ||
532
+ (equality.includes("\n") &&
533
+ !merged.startsWith(equality) &&
534
+ !merged.endsWith(equality)) ||
527
535
  equality.length > Math.max(left.ownDeleted, left.ownInserted) ||
528
536
  equality.length > Math.max(right.ownDeleted, right.ownInserted)
529
537
  )
@@ -814,16 +822,24 @@ function wholeWordReplacements(
814
822
  const removed = operation.before.changedText;
815
823
  const inserted = operation.after.changedText;
816
824
  const lexicalChange = /[\p{L}\p{M}\p{N}]/u.test(removed + inserted);
825
+ // An edge that is a line break, as when a whole paragraph is deleted,
826
+ // sits beside the neighboring block's word without splitting it.
827
+ const lineEdge = (edge: (text: string) => boolean) =>
828
+ edge(removed) && (!inserted || edge(inserted));
829
+ const widensBefore =
830
+ isWordBefore(before, from) && !lineEdge((text) => text.startsWith("\n"));
831
+ const widensAfter =
832
+ isWordAt(before, to) && !lineEdge((text) => text.endsWith("\n"));
817
833
  let group = { from, to, members: [operation] };
818
834
  if (
819
835
  lexicalChange &&
820
836
  !/<br\/?\s*>/u.test(removed + inserted) &&
821
837
  (removed || !/\s/u.test(inserted)) &&
822
- (isWordBefore(before, from) || isWordAt(before, to))
838
+ (widensBefore || widensAfter)
823
839
  ) {
824
- while (isWordBefore(before, group.from))
840
+ while (widensBefore && isWordBefore(before, group.from))
825
841
  group.from -= characterBefore(before, group.from).length;
826
- while (isWordAt(before, group.to))
842
+ while (widensAfter && isWordAt(before, group.to))
827
843
  group.to += characterAt(before, group.to).length;
828
844
  }
829
845
  // Two edits inside one word are one review decision.
@@ -1022,6 +1038,7 @@ export function markdownSuggestionOperationsForReplacements(input: {
1022
1038
  const [{ from, to }] = replacements;
1023
1039
  if (
1024
1040
  operations.length > 1 &&
1041
+ !before.slice(from, to).includes("\n") &&
1025
1042
  operations.every(
1026
1043
  (operation) =>
1027
1044
  operation.anchor.from >= from && operation.anchor.to <= to,
@@ -650,12 +650,21 @@ function resolveVerbatimRun(
650
650
  export type SuggestionFormattingSlicePart =
651
651
  | { type: "text"; text: string; marks: NonNullable<PMNode["marks"]> }
652
652
  | { type: "break"; text: string }
653
- | { type: "indent"; text: string };
653
+ | { type: "indent"; text: string }
654
+ | { type: "block"; text: string };
654
655
 
656
+ const EMPTY_BLOCK = "<empty-block/>";
657
+ // The syntax that opens a line's block: a heading, list item, task, or quote.
658
+ const BLOCK_MARKER = /^(?:#{1,6} |(?:> )+|[-*+] (?:\[[ xX]\] )?|\d+[.)] )/;
659
+
660
+ // `blocks` admits block syntax and empty paragraphs. Without it a gap holds
661
+ // only line breaks and indentation, which is what places a zero-width
662
+ // boundary on one side of a gap or the other.
655
663
  function structuralGapParts(
656
664
  source: string,
657
665
  from: number,
658
666
  to: number,
667
+ blocks = true,
659
668
  ): SuggestionFormattingSlicePart[] | null {
660
669
  const parts: SuggestionFormattingSlicePart[] = [];
661
670
  let offset = from;
@@ -678,15 +687,36 @@ function structuralGapParts(
678
687
  source.lastIndexOf("\r", offset - 1),
679
688
  ) + 1;
680
689
  const atLineStart = /^\t*$/.test(source.slice(lineStart, offset));
681
- if (!atLineStart || source[offset] !== "\t") return null;
682
- const indentFrom = offset;
683
- while (offset < to && source[offset] === "\t") offset += 1;
684
- parts.push({
685
- type: "indent",
686
- text: "⇥".repeat(offset - indentFrom),
687
- });
688
- const heading = /^(?:#{1,6}) /.exec(source.slice(offset, to));
689
- if (heading) offset += heading[0].length;
690
+ if (!atLineStart) return null;
691
+ if (source[offset] === "\t") {
692
+ const indentFrom = offset;
693
+ while (offset < to && source[offset] === "\t") offset += 1;
694
+ parts.push({
695
+ type: "indent",
696
+ text: "⇥".repeat(offset - indentFrom),
697
+ });
698
+ const heading = /^(?:#{1,6}) /.exec(source.slice(offset, to));
699
+ if (heading) offset += heading[0].length;
700
+ continue;
701
+ }
702
+ if (!blocks) return null;
703
+ // An empty paragraph or a block's opening syntax has no text of its own,
704
+ // so it is shown as written: a pilcrow for the paragraph, the marker
705
+ // otherwise. A marker is only whole when the slice holds all of it.
706
+ const lineEnd = offset + EMPTY_BLOCK.length;
707
+ if (
708
+ source.startsWith(EMPTY_BLOCK, offset) &&
709
+ lineEnd <= to &&
710
+ (lineEnd === source.length || /[\r\n]/.test(source[lineEnd]!))
711
+ ) {
712
+ parts.push({ type: "block", text: "¶" });
713
+ offset = lineEnd;
714
+ continue;
715
+ }
716
+ const marker = BLOCK_MARKER.exec(source.slice(offset, to));
717
+ if (!marker) return null;
718
+ parts.push({ type: "block", text: marker[0] });
719
+ offset += marker[0].length;
690
720
  }
691
721
  return parts;
692
722
  }
@@ -894,7 +924,7 @@ export function suggestionFormattingSourceRange(
894
924
  sourceTo: run.sourceFrom,
895
925
  offset: run.from,
896
926
  structural: Boolean(
897
- structuralGapParts(source, previousSourceTo, run.sourceFrom),
927
+ structuralGapParts(source, previousSourceTo, run.sourceFrom, false),
898
928
  ),
899
929
  afterText: index > 0,
900
930
  beforeText: true,
@@ -910,7 +940,7 @@ export function suggestionFormattingSourceRange(
910
940
  sourceTo: source.length,
911
941
  offset: previousTextTo,
912
942
  structural: Boolean(
913
- structuralGapParts(source, previousSourceTo, source.length),
943
+ structuralGapParts(source, previousSourceTo, source.length, false),
914
944
  ),
915
945
  afterText: mapped.runs.length > 0,
916
946
  beforeText: false,
@@ -983,13 +1013,13 @@ export function suggestionFormattingSourceRange(
983
1013
  if (
984
1014
  gap.afterText &&
985
1015
  offset > gap.sourceFrom &&
986
- structuralGapParts(source, gap.sourceFrom, offset)
1016
+ structuralGapParts(source, gap.sourceFrom, offset, false)
987
1017
  )
988
1018
  candidates.push({ offset: gap.offset, affinity: "left" });
989
1019
  if (
990
1020
  gap.beforeText &&
991
1021
  offset < gap.sourceTo &&
992
- (structuralGapParts(source, offset, gap.sourceTo) ||
1022
+ (structuralGapParts(source, offset, gap.sourceTo, false) ||
993
1023
  !/[\r\n]/.test(opening) ||
994
1024
  (gap.beforeCode && /^[^\r\n]*\r?\n$/.test(opening)))
995
1025
  )
@@ -11,6 +11,8 @@ export type SuggestionPresentationContext = {
11
11
  export type SuggestionPresentationNode =
12
12
  | { type: "text"; value: string }
13
13
  | { type: "indent"; value: string }
14
+ | { type: "block"; value: string }
15
+ | { type: "lineBreak" }
14
16
  | {
15
17
  type: "strong" | "emphasis" | "strike" | "code" | "underline";
16
18
  children: SuggestionPresentationNode[];
@@ -120,7 +122,12 @@ function stripInlineContext(
120
122
  const value = node.value.slice(context.length);
121
123
  return value ? { type: "text", value } : null;
122
124
  }
123
- if (node.type === "indent") return node;
125
+ if (
126
+ node.type === "indent" ||
127
+ node.type === "block" ||
128
+ node.type === "lineBreak"
129
+ )
130
+ return node;
124
131
  const children = node.children.flatMap((child) => {
125
132
  const strippedChild = stripNode(child);
126
133
  return strippedChild ? [strippedChild] : [];
@@ -246,11 +253,19 @@ function computeSuggestionTextPresentationForSource(
246
253
  ? text.replace(/ /g, "·").replace(/\t/g, "⇥")
247
254
  : text
248
255
  ).replace(/\n/g, "↵");
249
- return parts.flatMap((part) =>
250
- part.type === "indent"
251
- ? [{ type: "indent" as const, value: part.text }]
256
+ // A line break beside text breaks the line; on its own it stays a visible ↵.
257
+ const breaksLines = parts.some(
258
+ (part) => part.type === "text" && /\S/.test(part.text),
259
+ );
260
+ return parts.flatMap((part): SuggestionPresentationNode[] =>
261
+ part.type === "indent" || part.type === "block"
262
+ ? [{ type: part.type, value: part.text }]
252
263
  : part.type === "break"
253
- ? [{ type: "text" as const, value: part.text }]
264
+ ? [
265
+ breaksLines
266
+ ? { type: "lineBreak" }
267
+ : { type: "text", value: part.text },
268
+ ]
254
269
  : presentationNode(
255
270
  { type: "text", text: visibleText(part.text), marks: part.marks },
256
271
  (text) => text,
@@ -15,10 +15,12 @@ from the command menu (Cmd+K → "What's new") or from Settings.
15
15
 
16
16
  ### Added
17
17
 
18
+ - A Getting started checklist at the bottom of the sidebar tracks your first design, brand, sharing, teammates, and templates, and checks steps off as you finish them
18
19
  - Pulsing Border adds moving color highlights along an element’s outline, with concentrated bright cores, colored falloff, and controls for motion and shape.
19
20
 
20
21
  ### Improved
21
22
 
23
+ - Templates now open a read-only preview from the card, and the home library shows more starters, including dashboard, messaging, task board and design studio screens.
22
24
  - Effect thumbnails now show how shaders treat the Agent-Native mark against a quiet tonal background, making their differences easier to judge.
23
25
  - Native shader previews use less GPU memory when many mesh effects are visible.
24
26
  - Pulsing Border uses the standard Linear and Angular gradient picker for its moving border colors, with up to eight editable stops and a separate Background color. Thickness sets the bright edge, Softness feathers it, Bloom adjusts glow brightness, and Glow spread sets the halo radius. Smoke, margins, offsets, and layout settings stay grouped in collapsible sections.
@@ -128,6 +130,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
128
130
 
129
131
  ### Improved
130
132
 
133
+ - Design pages use localized visibility badges and a shared ownership filter.
131
134
  - Animated shader fills render together more efficiently and pause work outside the visible canvas.
132
135
  - Shader property sliders preview changes while dragging
133
136
  - Canvas selection, layer controls, text insertion, grouping, and auto-layout behave more predictably.
@@ -14,7 +14,7 @@ import {
14
14
  useSetPageTitle,
15
15
  } from "@agent-native/toolkit/app-shell";
16
16
  import { ShareButton } from "@agent-native/toolkit/app/sharing";
17
- import { VisibilityBadge } from "@agent-native/toolkit/sharing";
17
+ import { VisibilityBadge } from "@agent-native/toolkit/app/sharing/VisibilityBadge";
18
18
  import {
19
19
  IconCheckbox,
20
20
  IconChecks,
@@ -36,13 +36,13 @@ import {
36
36
  type TiptapComposerHandle,
37
37
  } from "@agent-native/toolkit/app/chat/composer/index";
38
38
  import { ClientOnly } from "@agent-native/toolkit/app/shared";
39
+ import { OwnershipFilter } from "@agent-native/toolkit/app/sharing/OwnershipFilter";
39
40
  import { designTemplateRetryKey } from "@shared/design-template-retry";
40
41
  import { FULL_APP_BUILDING_LAB } from "@shared/labs";
41
42
  import { derivePromptTitle } from "@shared/prompt-title";
42
43
  import {
43
44
  IconArrowRight,
44
45
  IconChecks,
45
- IconChevronDown,
46
46
  IconChevronLeft,
47
47
  IconChevronRight,
48
48
  IconPlus,
@@ -91,8 +91,6 @@ import {
91
91
  DropdownMenu,
92
92
  DropdownMenuContent,
93
93
  DropdownMenuItem,
94
- DropdownMenuRadioGroup,
95
- DropdownMenuRadioItem,
96
94
  DropdownMenuTrigger,
97
95
  } from "@/components/ui/dropdown-menu";
98
96
  import { Input } from "@/components/ui/input";
@@ -616,11 +614,6 @@ export default function Index() {
616
614
  ? chosenDesignSystemId
617
615
  : templateDesignSystemId;
618
616
 
619
- const ownerFilterLabels: Record<DesignFilter, string> = {
620
- all: t("home.ownedByAnyone"),
621
- mine: t("home.ownedByMe"),
622
- "not-mine": t("home.sharedWithMe"),
623
- };
624
617
  const showAuthors = designFilter !== "mine";
625
618
  const selectedDesignCount = selectedDesignIds.size;
626
619
  const isSelectingDesigns = selectedDesignCount > 0;
@@ -743,17 +736,19 @@ export default function Index() {
743
736
  );
744
737
  }, []);
745
738
 
746
- const handleDesignFilterChange = useCallback((next: string) => {
747
- if (next !== "all" && next !== "mine" && next !== "not-mine") return;
748
- designFilterWasSelectedRef.current = true;
749
- const nextFilter: DesignFilter = next;
750
- setDesignFilter(nextFilter);
751
- writeStoredDesignFilter(nextFilter);
752
- setPage(1);
753
- setSelectedDesignIds((current) =>
754
- current.size === 0 ? current : new Set(),
755
- );
756
- }, []);
739
+ const handleDesignFilterChange = useCallback(
740
+ (next: "all" | "mine" | "shared") => {
741
+ designFilterWasSelectedRef.current = true;
742
+ const nextFilter: DesignFilter = next === "shared" ? "not-mine" : next;
743
+ setDesignFilter(nextFilter);
744
+ writeStoredDesignFilter(nextFilter);
745
+ setPage(1);
746
+ setSelectedDesignIds((current) =>
747
+ current.size === 0 ? current : new Set(),
748
+ );
749
+ },
750
+ [],
751
+ );
757
752
 
758
753
  useEffect(() => {
759
754
  if (
@@ -1572,51 +1567,10 @@ export default function Index() {
1572
1567
  }
1573
1568
  search={searchInput}
1574
1569
  recentActions={
1575
- <DropdownMenu>
1576
- <DropdownMenuTrigger asChild>
1577
- <Button
1578
- type="button"
1579
- variant="outline"
1580
- size="sm"
1581
- aria-label={ownerFilterLabels[designFilter]}
1582
- className="h-9 shrink-0 gap-2"
1583
- >
1584
- <span className="grid">
1585
- {Object.entries(ownerFilterLabels).map(
1586
- ([filter, label]) => (
1587
- <span
1588
- key={filter}
1589
- aria-hidden="true"
1590
- className="invisible col-start-1 row-start-1 whitespace-nowrap"
1591
- >
1592
- {label}
1593
- </span>
1594
- ),
1595
- )}
1596
- <span className="col-start-1 row-start-1 whitespace-nowrap">
1597
- {ownerFilterLabels[designFilter]}
1598
- </span>
1599
- </span>
1600
- <IconChevronDown className="size-4" aria-hidden="true" />
1601
- </Button>
1602
- </DropdownMenuTrigger>
1603
- <DropdownMenuContent align="start">
1604
- <DropdownMenuRadioGroup
1605
- value={designFilter}
1606
- onValueChange={handleDesignFilterChange}
1607
- >
1608
- <DropdownMenuRadioItem value="all" indicator="check">
1609
- {ownerFilterLabels.all}
1610
- </DropdownMenuRadioItem>
1611
- <DropdownMenuRadioItem value="mine" indicator="check">
1612
- {ownerFilterLabels.mine}
1613
- </DropdownMenuRadioItem>
1614
- <DropdownMenuRadioItem value="not-mine" indicator="check">
1615
- {ownerFilterLabels["not-mine"]}
1616
- </DropdownMenuRadioItem>
1617
- </DropdownMenuRadioGroup>
1618
- </DropdownMenuContent>
1619
- </DropdownMenu>
1570
+ <OwnershipFilter
1571
+ value={designFilter === "not-mine" ? "shared" : designFilter}
1572
+ onChange={handleDesignFilterChange}
1573
+ />
1620
1574
  }
1621
1575
  templates={
1622
1576
  templatesError ? (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Design pages use localized visibility badges and a shared ownership filter.
@@ -5,6 +5,10 @@ time from the command menu (Cmd+K → "What's new").
5
5
 
6
6
  ## 2026-10-21
7
7
 
8
+ ### Added
9
+
10
+ - Owned and shared chats appear in separate history sections, with each shared chat's owner shown as an avatar and a Copy link action on every chat.
11
+
8
12
  ### Improved
9
13
 
10
14
  - Customer AI credit questions route to account health instead of workspace spend metrics.