@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.
- package/corpus/README.md +1 -1
- 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 +2 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- 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/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/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/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/CHANGELOG.md +3 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +18 -64
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -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
|
@@ -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. */
|
|
@@ -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.
|
|
523
|
-
|
|
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
|
-
(
|
|
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
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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 (
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
? [
|
|
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(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
<
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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 ? (
|
|
@@ -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.
|