@agent-native/core 0.182.1-nightly-20260917183518 → 0.182.1
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 +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +1 -0
- package/corpus/templates/clips/CHANGELOG.md +7 -0
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +2 -2
- package/corpus/templates/clips/changelog/2026-09-17-clips-waits-for-authentication-before-showing-the-recording-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-17-the-clips-sort-control-keeps-its-keyboard-focus-ring-visible.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +1 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +30 -21
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +181 -16
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +28 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -0
- package/corpus/templates/content/app/components/editor/document-save-rebase.ts +13 -10
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +80 -0
- package/corpus/templates/content/changelog/2026-09-16-recovery-choices-stay-attached-to-the-draft-you-reviewed.md +6 -0
- package/corpus/templates/design/CHANGELOG.md +29 -0
- package/corpus/templates/design/actions/import-design-source.ts +42 -2
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +26 -15
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +83 -1
- package/corpus/templates/design/app/i18n-data.ts +3 -1
- package/corpus/templates/design/app/lib/design-file-upload.ts +6 -4
- package/corpus/templates/design/app/lib/design-import.ts +2 -0
- package/corpus/templates/design/app/lib/fig-client-import.ts +207 -49
- package/corpus/templates/design/changelog/2026-09-17-large-fig-files-import-directly-in-the-browser-without-the-o.md +6 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +31 -8
- package/corpus/templates/design/server/lib/import-design-files.ts +188 -75
- package/corpus/templates/design/server/plugins/db.ts +16 -7
- package/corpus/templates/design/shared/fig-to-frames.ts +55 -25
- package/corpus/templates/factory/CHANGELOG.md +6 -0
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +12 -0
- package/corpus/templates/slides/app/components/editor/AssetLibraryPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +1 -2
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +82 -2
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +9 -2
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/server/handlers/assets.ts +134 -8
- package/corpus/templates/slides/server/handlers/uploads.ts +23 -10
- package/corpus/templates/slides/shared/upload-types.ts +1 -0
- package/dist/application-state/store.d.ts +9 -0
- package/dist/application-state/store.js +18 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +7 -0
- package/dist/file-upload/actions/upload-image.d.ts +41 -10
- package/dist/file-upload/actions/upload-image.js +399 -19
- package/dist/jobs/scheduler.js +11 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/package.json +4 -4
package/corpus/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Dashboards open without background synchronization scans during cold starts
|
|
11
|
+
|
|
6
12
|
## 2026-09-15
|
|
7
13
|
|
|
8
14
|
### Improved
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable user-facing changes to Clips are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-09-17
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Clips waits for authentication before showing the recording action
|
|
11
|
+
- The Clips sort control keeps its keyboard focus ring visible
|
|
12
|
+
|
|
6
13
|
## 2026-09-16
|
|
7
14
|
|
|
8
15
|
### Improved
|
|
@@ -33,9 +33,9 @@ export function SortMenu({ value, onChange }: SortMenuProps) {
|
|
|
33
33
|
<TooltipTrigger asChild>
|
|
34
34
|
<DropdownMenuTrigger asChild>
|
|
35
35
|
<Button
|
|
36
|
-
variant="ghost"
|
|
36
|
+
variant="ghost-inset"
|
|
37
37
|
size="icon"
|
|
38
|
-
className="h-8 w-8 shrink-0
|
|
38
|
+
className="h-8 w-8 shrink-0"
|
|
39
39
|
aria-label={LABELS[value]}
|
|
40
40
|
>
|
|
41
41
|
<IconArrowsSort className="h-3.5 w-3.5" />
|
|
@@ -388,7 +388,7 @@
|
|
|
388
388
|
</button>
|
|
389
389
|
</div>
|
|
390
390
|
|
|
391
|
-
<button id="start" class="primary start" type="button">
|
|
391
|
+
<button id="start" class="primary start" type="button" hidden>
|
|
392
392
|
Start recording
|
|
393
393
|
</button>
|
|
394
394
|
<button id="sign-in" class="primary start" type="button" hidden>
|
|
@@ -75,6 +75,17 @@ type PopupStatusResponse = {
|
|
|
75
75
|
|
|
76
76
|
type AuthStatus = "checking" | "signed-in" | "signed-out";
|
|
77
77
|
|
|
78
|
+
export function recordingControlVisibility(
|
|
79
|
+
recording: NativeRecording | null,
|
|
80
|
+
authStatus: AuthStatus,
|
|
81
|
+
): { startHidden: boolean; signInHidden: boolean } {
|
|
82
|
+
const active = Boolean(recording);
|
|
83
|
+
return {
|
|
84
|
+
startHidden: active || authStatus !== "signed-in",
|
|
85
|
+
signInHidden: active || authStatus !== "signed-out",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
type StoredAuth = {
|
|
79
90
|
token: string;
|
|
80
91
|
email?: string;
|
|
@@ -769,6 +780,7 @@ function formatDuration(startedAtMs: number): string {
|
|
|
769
780
|
function renderActiveRecording(
|
|
770
781
|
recording: NativeRecording | null,
|
|
771
782
|
arming = false,
|
|
783
|
+
authStatus: AuthStatus = "checking",
|
|
772
784
|
): void {
|
|
773
785
|
const idleContent = byId<HTMLDivElement>("idle-content");
|
|
774
786
|
const activeContent = byId<HTMLDivElement>("active-content");
|
|
@@ -783,11 +795,12 @@ function renderActiveRecording(
|
|
|
783
795
|
document.querySelector<HTMLDivElement>(".recording-actions");
|
|
784
796
|
|
|
785
797
|
const active = Boolean(recording);
|
|
798
|
+
const controlVisibility = recordingControlVisibility(recording, authStatus);
|
|
786
799
|
idleContent.hidden = active;
|
|
787
800
|
activeContent.hidden = !active;
|
|
788
|
-
start.hidden =
|
|
801
|
+
start.hidden = controlVisibility.startHidden;
|
|
789
802
|
start.disabled = arming;
|
|
790
|
-
signIn.hidden =
|
|
803
|
+
signIn.hidden = controlVisibility.signInHidden;
|
|
791
804
|
if (recordingActions) recordingActions.hidden = !active;
|
|
792
805
|
if (!recording) {
|
|
793
806
|
setStorageHelp(false);
|
|
@@ -991,7 +1004,7 @@ async function init(): Promise<void> {
|
|
|
991
1004
|
await sendSimpleMessage<PopupStatusResponse>("CLIPS_POPUP_STATUS");
|
|
992
1005
|
activeRecording = status.activeRecording ?? null;
|
|
993
1006
|
arming = Boolean(status.arming);
|
|
994
|
-
renderActiveRecording(activeRecording, arming);
|
|
1007
|
+
renderActiveRecording(activeRecording, arming, authStatus);
|
|
995
1008
|
};
|
|
996
1009
|
await refreshActiveRecording();
|
|
997
1010
|
if (activeRecording || arming) {
|
|
@@ -1222,11 +1235,8 @@ async function init(): Promise<void> {
|
|
|
1222
1235
|
});
|
|
1223
1236
|
|
|
1224
1237
|
authStatus = await readAuthStatus(settings);
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
signIn.hidden = false;
|
|
1228
|
-
setStatus("");
|
|
1229
|
-
}
|
|
1238
|
+
renderActiveRecording(activeRecording, arming, authStatus);
|
|
1239
|
+
if (authStatus === "signed-out") setStatus("");
|
|
1230
1240
|
|
|
1231
1241
|
start.addEventListener("click", async () => {
|
|
1232
1242
|
start.disabled = true;
|
|
@@ -1236,9 +1246,7 @@ async function init(): Promise<void> {
|
|
|
1236
1246
|
try {
|
|
1237
1247
|
authStatus = await readAuthStatus(settings);
|
|
1238
1248
|
if (authStatus === "signed-out") {
|
|
1239
|
-
|
|
1240
|
-
start.hidden = true;
|
|
1241
|
-
signIn.hidden = false;
|
|
1249
|
+
renderActiveRecording(activeRecording, arming, authStatus);
|
|
1242
1250
|
setStatus("");
|
|
1243
1251
|
return;
|
|
1244
1252
|
}
|
|
@@ -1375,8 +1383,7 @@ async function init(): Promise<void> {
|
|
|
1375
1383
|
await sendSimpleMessage<PopupStartResponse>("CLIPS_POPUP_CANCEL");
|
|
1376
1384
|
if (response.ok) {
|
|
1377
1385
|
activeRecording = null;
|
|
1378
|
-
renderActiveRecording(null);
|
|
1379
|
-
if (authStatus === "signed-in") start.hidden = false;
|
|
1386
|
+
renderActiveRecording(null, false, authStatus);
|
|
1380
1387
|
setStatus("");
|
|
1381
1388
|
stop.disabled = false;
|
|
1382
1389
|
discard.disabled = false;
|
|
@@ -1398,12 +1405,14 @@ async function init(): Promise<void> {
|
|
|
1398
1405
|
});
|
|
1399
1406
|
}
|
|
1400
1407
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1408
|
+
if (typeof document !== "undefined") {
|
|
1409
|
+
void init().catch((err) => {
|
|
1410
|
+
captureExtensionError(err, {
|
|
1411
|
+
tags: { surface: "popup", action: "init" },
|
|
1412
|
+
});
|
|
1413
|
+
setStatus(
|
|
1414
|
+
err instanceof Error ? err.message : "Could not load popup.",
|
|
1415
|
+
"error",
|
|
1416
|
+
);
|
|
1404
1417
|
});
|
|
1405
|
-
|
|
1406
|
-
err instanceof Error ? err.message : "Could not load popup.",
|
|
1407
|
-
"error",
|
|
1408
|
-
);
|
|
1409
|
-
});
|
|
1418
|
+
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Content are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Content now combines non-overlapping edits from another tab or agent automatically, while recovery choices stay attached to the exact draft and saved page version you reviewed.
|
|
11
|
+
|
|
6
12
|
## 2026-09-15
|
|
7
13
|
|
|
8
14
|
### Fixed
|
|
@@ -27,6 +33,7 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
27
33
|
|
|
28
34
|
### Fixed
|
|
29
35
|
|
|
36
|
+
- Tables now export as real tables. Downloading a page as PDF or HTML keeps rows, columns, header rows, header columns, and cell line breaks instead of flattening the table into plain text. Callouts, toggles, and column layouts survive the same export path, and printed tables repeat their header row across page breaks.
|
|
30
37
|
- Suggestion previews and highlights stay aligned when editing text near lists.
|
|
31
38
|
|
|
32
39
|
## 2026-09-12
|
|
@@ -35,6 +35,8 @@ const durableClaimPayload = z.object({
|
|
|
35
35
|
updatedAt: z.string().min(1),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
const PROCESSING_LEASE_MS = 5 * 60_000;
|
|
39
|
+
|
|
38
40
|
function conflict(message: string, details?: Record<string, unknown>): never {
|
|
39
41
|
throw new ActionContractError(message, {
|
|
40
42
|
errorCode: "PREVIEW_DRAFT_RECOVERY_CONFLICT",
|
|
@@ -48,8 +50,10 @@ async function recoveryDocumentId(args: {
|
|
|
48
50
|
expectedDraftVersion: number;
|
|
49
51
|
expectedDraftTitle: string;
|
|
50
52
|
expectedDraftContent: string;
|
|
53
|
+
expectedDocumentUpdatedAt: string;
|
|
51
54
|
ownerEmail: string;
|
|
52
55
|
orgId: string;
|
|
56
|
+
includeDocumentVersion: boolean;
|
|
53
57
|
}): Promise<string> {
|
|
54
58
|
const input = JSON.stringify([
|
|
55
59
|
args.ownerEmail,
|
|
@@ -58,6 +62,7 @@ async function recoveryDocumentId(args: {
|
|
|
58
62
|
args.expectedDraftVersion,
|
|
59
63
|
args.expectedDraftTitle,
|
|
60
64
|
args.expectedDraftContent,
|
|
65
|
+
...(args.includeDocumentVersion ? [args.expectedDocumentUpdatedAt] : []),
|
|
61
66
|
]);
|
|
62
67
|
const digest = await crypto.subtle.digest(
|
|
63
68
|
"SHA-256",
|
|
@@ -104,14 +109,88 @@ export default defineAction({
|
|
|
104
109
|
const access = await assertAccess("document", args.documentId, "editor");
|
|
105
110
|
const ownerEmail = access.resource.ownerEmail as string;
|
|
106
111
|
const db = getDb();
|
|
107
|
-
|
|
112
|
+
let recoveryId = await recoveryDocumentId({
|
|
113
|
+
...args,
|
|
114
|
+
ownerEmail: userEmail,
|
|
115
|
+
orgId,
|
|
116
|
+
includeDocumentVersion: true,
|
|
117
|
+
});
|
|
118
|
+
let claimId = `draft-claim-${recoveryId.slice("recovery-".length)}`;
|
|
119
|
+
let claimDocumentId = ownerEmail === userEmail ? args.documentId : claimId;
|
|
120
|
+
const legacyRecoveryId = await recoveryDocumentId({
|
|
108
121
|
...args,
|
|
109
122
|
ownerEmail: userEmail,
|
|
110
123
|
orgId,
|
|
124
|
+
includeDocumentVersion: false,
|
|
111
125
|
});
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
const legacyClaimId = `draft-claim-${legacyRecoveryId.slice(
|
|
127
|
+
"recovery-".length,
|
|
128
|
+
)}`;
|
|
129
|
+
const [currentClaim] = await db
|
|
130
|
+
.select({ id: schema.documentVersions.id })
|
|
131
|
+
.from(schema.documentVersions)
|
|
132
|
+
.where(
|
|
133
|
+
and(
|
|
134
|
+
eq(schema.documentVersions.id, claimId),
|
|
135
|
+
eq(schema.documentVersions.ownerEmail, userEmail),
|
|
136
|
+
eq(schema.documentVersions.documentId, claimDocumentId),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
.limit(1);
|
|
140
|
+
if (legacyClaimId !== claimId && !currentClaim) {
|
|
141
|
+
const [legacyClaim] = await db
|
|
142
|
+
.select({
|
|
143
|
+
id: schema.documentVersions.id,
|
|
144
|
+
chatContext: schema.documentVersions.chatContext,
|
|
145
|
+
})
|
|
146
|
+
.from(schema.documentVersions)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
eq(schema.documentVersions.id, legacyClaimId),
|
|
150
|
+
eq(schema.documentVersions.ownerEmail, userEmail),
|
|
151
|
+
eq(
|
|
152
|
+
schema.documentVersions.documentId,
|
|
153
|
+
ownerEmail === userEmail ? args.documentId : legacyClaimId,
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
.limit(1);
|
|
158
|
+
const [matchingDraft] = await db
|
|
159
|
+
.select({ id: schema.documentPreviewDrafts.id })
|
|
160
|
+
.from(schema.documentPreviewDrafts)
|
|
161
|
+
.where(
|
|
162
|
+
and(
|
|
163
|
+
eq(schema.documentPreviewDrafts.ownerEmail, userEmail),
|
|
164
|
+
eq(schema.documentPreviewDrafts.orgId, orgId),
|
|
165
|
+
eq(schema.documentPreviewDrafts.documentId, args.documentId),
|
|
166
|
+
eq(schema.documentPreviewDrafts.version, args.expectedDraftVersion),
|
|
167
|
+
eq(schema.documentPreviewDrafts.title, args.expectedDraftTitle),
|
|
168
|
+
eq(schema.documentPreviewDrafts.content, args.expectedDraftContent),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
.limit(1);
|
|
172
|
+
let legacyPayload: ReturnType<
|
|
173
|
+
typeof durableClaimPayload.safeParse
|
|
174
|
+
> | null = null;
|
|
175
|
+
if (legacyClaim?.chatContext) {
|
|
176
|
+
try {
|
|
177
|
+
legacyPayload = durableClaimPayload.safeParse(
|
|
178
|
+
JSON.parse(legacyClaim.chatContext),
|
|
179
|
+
);
|
|
180
|
+
} catch {
|
|
181
|
+
legacyPayload = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
legacyClaim &&
|
|
186
|
+
legacyPayload?.success &&
|
|
187
|
+
(!matchingDraft || legacyPayload.data.draftId === matchingDraft.id)
|
|
188
|
+
) {
|
|
189
|
+
recoveryId = legacyRecoveryId;
|
|
190
|
+
claimId = legacyClaimId;
|
|
191
|
+
claimDocumentId = ownerEmail === userEmail ? args.documentId : claimId;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
115
194
|
const processingToken = crypto.randomUUID();
|
|
116
195
|
const draftFilter = and(
|
|
117
196
|
eq(schema.documentPreviewDrafts.ownerEmail, userEmail),
|
|
@@ -272,6 +351,13 @@ export default defineAction({
|
|
|
272
351
|
if (payload.choice !== args.choice)
|
|
273
352
|
conflict("This draft was already resolved with another choice.");
|
|
274
353
|
if (payload.status === "resolved") {
|
|
354
|
+
if (
|
|
355
|
+
payload.draftId !== draft.id ||
|
|
356
|
+
payload.expectedDocumentUpdatedAt !==
|
|
357
|
+
args.expectedDocumentUpdatedAt
|
|
358
|
+
) {
|
|
359
|
+
conflict("The saved draft changed during recovery.");
|
|
360
|
+
}
|
|
275
361
|
const deleted = await tx
|
|
276
362
|
.delete(schema.documentPreviewDrafts)
|
|
277
363
|
.where(draftFilter)
|
|
@@ -280,10 +366,13 @@ export default defineAction({
|
|
|
280
366
|
conflict("The saved draft changed during recovery.");
|
|
281
367
|
return { status: "resolved" as const };
|
|
282
368
|
}
|
|
369
|
+
if (payload.draftId !== draft.id)
|
|
370
|
+
conflict("The saved draft changed during recovery.");
|
|
283
371
|
if (
|
|
284
372
|
payload.status === "processing" &&
|
|
285
|
-
payload.processingStartedAt
|
|
286
|
-
|
|
373
|
+
(!payload.processingStartedAt ||
|
|
374
|
+
Date.now() - Date.parse(payload.processingStartedAt) <
|
|
375
|
+
PROCESSING_LEASE_MS)
|
|
287
376
|
)
|
|
288
377
|
conflict("This recovery choice is already being applied.");
|
|
289
378
|
await tx
|
|
@@ -350,15 +439,64 @@ export default defineAction({
|
|
|
350
439
|
) {
|
|
351
440
|
conflict("The page changed after this recovery choice was reviewed.");
|
|
352
441
|
}
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
442
|
+
if (payload.status === "processing") {
|
|
443
|
+
const alreadyApplied =
|
|
444
|
+
args.choice === "use_saved" ||
|
|
445
|
+
(args.choice === "keep_mine" &&
|
|
446
|
+
currentDocument.title === claim.title &&
|
|
447
|
+
currentDocument.content === claim.content) ||
|
|
448
|
+
(args.choice === "save_separately" &&
|
|
449
|
+
(
|
|
450
|
+
await tx
|
|
451
|
+
.select({
|
|
452
|
+
ownerEmail: schema.documents.ownerEmail,
|
|
453
|
+
title: schema.documents.title,
|
|
454
|
+
content: schema.documents.content,
|
|
455
|
+
})
|
|
456
|
+
.from(schema.documents)
|
|
457
|
+
.where(
|
|
458
|
+
and(
|
|
459
|
+
eq(schema.documents.id, recoveryId),
|
|
460
|
+
eq(schema.documents.ownerEmail, userEmail),
|
|
461
|
+
eq(schema.documents.title, claim.title),
|
|
462
|
+
eq(schema.documents.content, claim.content),
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
.limit(1)
|
|
466
|
+
).length === 1);
|
|
467
|
+
if (!alreadyApplied) {
|
|
468
|
+
if (
|
|
469
|
+
!payload.processingStartedAt ||
|
|
470
|
+
Date.now() - Date.parse(payload.processingStartedAt) <
|
|
471
|
+
PROCESSING_LEASE_MS
|
|
472
|
+
) {
|
|
473
|
+
conflict("This recovery choice is already being applied.");
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const resolvedAt = new Date().toISOString();
|
|
477
|
+
const resolved = await tx
|
|
478
|
+
.update(schema.documentVersions)
|
|
479
|
+
.set({
|
|
480
|
+
chatContext: JSON.stringify({
|
|
481
|
+
...payload,
|
|
482
|
+
status: "resolved",
|
|
483
|
+
}),
|
|
484
|
+
updatedAt: resolvedAt,
|
|
485
|
+
})
|
|
486
|
+
.where(
|
|
487
|
+
and(
|
|
488
|
+
eq(schema.documentVersions.id, claimId),
|
|
489
|
+
eq(schema.documentVersions.chatContext, claim.chatContext),
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
.returning({ id: schema.documentVersions.id });
|
|
493
|
+
if (resolved.length !== 1)
|
|
494
|
+
conflict("This recovery choice is already being applied.");
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
359
497
|
}
|
|
360
498
|
const now = new Date().toISOString();
|
|
361
|
-
await tx
|
|
499
|
+
const acquired = await tx
|
|
362
500
|
.update(schema.documentVersions)
|
|
363
501
|
.set({
|
|
364
502
|
chatContext: JSON.stringify({
|
|
@@ -369,7 +507,15 @@ export default defineAction({
|
|
|
369
507
|
}),
|
|
370
508
|
updatedAt: now,
|
|
371
509
|
})
|
|
372
|
-
.where(
|
|
510
|
+
.where(
|
|
511
|
+
and(
|
|
512
|
+
eq(schema.documentVersions.id, claimId),
|
|
513
|
+
eq(schema.documentVersions.chatContext, claim.chatContext),
|
|
514
|
+
),
|
|
515
|
+
)
|
|
516
|
+
.returning({ id: schema.documentVersions.id });
|
|
517
|
+
if (acquired.length !== 1)
|
|
518
|
+
conflict("This recovery choice is already being applied.");
|
|
373
519
|
return true;
|
|
374
520
|
});
|
|
375
521
|
const markClaimResolved = async () => {
|
|
@@ -395,7 +541,7 @@ export default defineAction({
|
|
|
395
541
|
if (payload.status === "resolved") return;
|
|
396
542
|
if (payload.processingToken !== processingToken)
|
|
397
543
|
conflict("This recovery choice is already being applied.");
|
|
398
|
-
await tx
|
|
544
|
+
const resolved = await tx
|
|
399
545
|
.update(schema.documentVersions)
|
|
400
546
|
.set({
|
|
401
547
|
chatContext: JSON.stringify({ ...payload, status: "resolved" }),
|
|
@@ -406,8 +552,11 @@ export default defineAction({
|
|
|
406
552
|
eq(schema.documentVersions.id, claimId),
|
|
407
553
|
eq(schema.documentVersions.ownerEmail, userEmail),
|
|
408
554
|
eq(schema.documentVersions.documentId, claimDocumentId),
|
|
555
|
+
eq(schema.documentVersions.chatContext, claim.chatContext),
|
|
409
556
|
),
|
|
410
|
-
)
|
|
557
|
+
)
|
|
558
|
+
.returning({ id: schema.documentVersions.id });
|
|
559
|
+
if (resolved.length !== 1) conflict("The recovery claim was lost.");
|
|
411
560
|
});
|
|
412
561
|
};
|
|
413
562
|
const restoreClaimedDraft = async (
|
|
@@ -511,6 +660,22 @@ export default defineAction({
|
|
|
511
660
|
ctx,
|
|
512
661
|
);
|
|
513
662
|
if ((saved as DocumentUpdateConflictResponse).conflict === true) {
|
|
663
|
+
const [winner] = await db
|
|
664
|
+
.select()
|
|
665
|
+
.from(schema.documents)
|
|
666
|
+
.where(eq(schema.documents.id, args.documentId))
|
|
667
|
+
.limit(1);
|
|
668
|
+
if (
|
|
669
|
+
winner?.title === draft.title &&
|
|
670
|
+
winner.content === draft.content
|
|
671
|
+
) {
|
|
672
|
+
await markClaimResolved();
|
|
673
|
+
return {
|
|
674
|
+
status: "resolved" as const,
|
|
675
|
+
choice: args.choice,
|
|
676
|
+
document: winner,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
514
679
|
await restoreClaimedDraft(draft);
|
|
515
680
|
return {
|
|
516
681
|
status: "document_conflict" as const,
|
|
@@ -1855,6 +1855,7 @@ function PageEditorSessionBody({
|
|
|
1855
1855
|
const {
|
|
1856
1856
|
report: reportReconcile,
|
|
1857
1857
|
resolve: resolveReconcile,
|
|
1858
|
+
resolveAutomatically: resolveReconcileAutomatically,
|
|
1858
1859
|
resolveChoice: resolveReconcileChoice,
|
|
1859
1860
|
updateDraft: updateReconcileDraft,
|
|
1860
1861
|
reportRetentionFailure,
|
|
@@ -2419,7 +2420,9 @@ function PageEditorSessionBody({
|
|
|
2419
2420
|
...(baseUpdatedAt !== undefined ? { baseUpdatedAt } : {}),
|
|
2420
2421
|
...(baseRevision !== undefined ? { baseRevision } : {}),
|
|
2421
2422
|
...(updates.title !== undefined
|
|
2422
|
-
? {
|
|
2423
|
+
? {
|
|
2424
|
+
baseTitle: options.titleBase ?? lastSavedTitleRef.current.title,
|
|
2425
|
+
}
|
|
2423
2426
|
: {}),
|
|
2424
2427
|
});
|
|
2425
2428
|
} catch (error) {
|
|
@@ -3941,26 +3944,36 @@ function PageEditorSessionBody({
|
|
|
3941
3944
|
}
|
|
3942
3945
|
localContentRef.current = result.content;
|
|
3943
3946
|
setLocalContent(result.content);
|
|
3944
|
-
reportReconcile(
|
|
3945
|
-
result.status === "conflict" ? "conflict" : "failed",
|
|
3946
|
-
result.content,
|
|
3947
|
-
);
|
|
3948
|
-
retainActiveRecoveryDraft({
|
|
3949
|
-
localTitle: localTitleRef.current,
|
|
3950
|
-
localDraft: result.content,
|
|
3951
|
-
});
|
|
3952
3947
|
if (result.status === "merged") {
|
|
3953
3948
|
if (documentContentRef.current === result.serverContent) {
|
|
3954
|
-
void
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3949
|
+
void resolveReconcileAutomatically(
|
|
3950
|
+
{
|
|
3951
|
+
localTitle: localTitleRef.current,
|
|
3952
|
+
localDraft: result.content,
|
|
3953
|
+
},
|
|
3954
|
+
{
|
|
3955
|
+
title: documentTitleRef.current,
|
|
3956
|
+
content: result.serverContent,
|
|
3957
|
+
updatedAt: documentUpdatedAtRef.current,
|
|
3958
|
+
revision: result.serverRevision,
|
|
3959
|
+
},
|
|
3960
|
+
);
|
|
3961
|
+
} else {
|
|
3962
|
+
reportReconcile("failed", result.content);
|
|
3963
|
+
retainActiveRecoveryDraft({
|
|
3964
|
+
localTitle: localTitleRef.current,
|
|
3965
|
+
localDraft: result.content,
|
|
3959
3966
|
});
|
|
3960
3967
|
}
|
|
3968
|
+
return;
|
|
3961
3969
|
}
|
|
3970
|
+
reportReconcile(result.status, result.content);
|
|
3971
|
+
retainActiveRecoveryDraft({
|
|
3972
|
+
localTitle: localTitleRef.current,
|
|
3973
|
+
localDraft: result.content,
|
|
3974
|
+
});
|
|
3962
3975
|
},
|
|
3963
|
-
[reportReconcile,
|
|
3976
|
+
[reportReconcile, resolveReconcileAutomatically, retainActiveRecoveryDraft],
|
|
3964
3977
|
);
|
|
3965
3978
|
|
|
3966
3979
|
const handleResolveReconcile = useCallback(
|
|
@@ -147,6 +147,16 @@ import { LinkHoverPreview } from "./LinkHoverPreview";
|
|
|
147
147
|
import { SlashCommandMenu } from "./SlashCommandMenu";
|
|
148
148
|
import { TableHoverControls } from "./TableHoverControls";
|
|
149
149
|
|
|
150
|
+
function compareDocumentBodyRevisions(
|
|
151
|
+
first: string,
|
|
152
|
+
second: string,
|
|
153
|
+
): number | null {
|
|
154
|
+
const firstMatch = /^body:(0|[1-9]\d*):/.exec(first);
|
|
155
|
+
const secondMatch = /^body:(0|[1-9]\d*):/.exec(second);
|
|
156
|
+
if (!firstMatch || !secondMatch) return null;
|
|
157
|
+
return Number(firstMatch[1]) - Number(secondMatch[1]);
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
/**
|
|
151
161
|
* Override the paragraph node's markdown serialization so that empty
|
|
152
162
|
* paragraphs survive round-trips. Without this, prosemirror-markdown
|
|
@@ -3648,6 +3658,7 @@ export function VisualEditor({
|
|
|
3648
3658
|
contentRevision: propsPredateAcknowledgedRestore
|
|
3649
3659
|
? acknowledgedRestore!.contentRevision
|
|
3650
3660
|
: contentRevision,
|
|
3661
|
+
compareContentRevisions: compareDocumentBodyRevisions,
|
|
3651
3662
|
acknowledgedLocalSnapshot,
|
|
3652
3663
|
collabContentRevision: propsPredateAcknowledgedRestore
|
|
3653
3664
|
? null
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { planDocReconcile } from "@agent-native/toolkit/editor";
|
|
2
2
|
import type { Document } from "@shared/api";
|
|
3
|
-
import { nfmToDoc } from "@shared/nfm";
|
|
3
|
+
import { docToNfm, nfmToDoc } from "@shared/nfm";
|
|
4
4
|
import { getSchema } from "@tiptap/core";
|
|
5
5
|
|
|
6
6
|
import { isDocumentUpdateConflict } from "@/hooks/use-documents";
|
|
@@ -42,13 +42,15 @@ export async function saveDocumentWithRebase({
|
|
|
42
42
|
};
|
|
43
43
|
}): Promise<RebasedDocumentSaveResult> {
|
|
44
44
|
let attemptedBase = base;
|
|
45
|
-
|
|
45
|
+
let candidate = content;
|
|
46
46
|
const conflict = (): RebasedDocumentSaveResult => {
|
|
47
47
|
const current = owner?.current();
|
|
48
48
|
return {
|
|
49
49
|
status: "conflict",
|
|
50
50
|
localDraft:
|
|
51
|
-
current && current.version !== owner!.version
|
|
51
|
+
current && current.version !== owner!.version
|
|
52
|
+
? current.content
|
|
53
|
+
: candidate,
|
|
52
54
|
};
|
|
53
55
|
};
|
|
54
56
|
const confirmed = (document: Document): RebasedDocumentSaveResult => {
|
|
@@ -57,12 +59,12 @@ export async function saveDocumentWithRebase({
|
|
|
57
59
|
return { status: "saved", document, content: document.content };
|
|
58
60
|
};
|
|
59
61
|
for (let attempt = 0; attempt <= 2; attempt++) {
|
|
60
|
-
const saved = await persist(
|
|
62
|
+
const saved = await persist(candidate, attemptedBase);
|
|
61
63
|
if (!isDocumentUpdateConflict(saved)) {
|
|
62
64
|
return confirmed(saved);
|
|
63
65
|
}
|
|
64
66
|
const winner = saved.document;
|
|
65
|
-
if (winner.content ===
|
|
67
|
+
if (winner.content === candidate && confirmsWrite(winner)) {
|
|
66
68
|
return confirmed(winner);
|
|
67
69
|
}
|
|
68
70
|
if (
|
|
@@ -75,16 +77,17 @@ export async function saveDocumentWithRebase({
|
|
|
75
77
|
}
|
|
76
78
|
try {
|
|
77
79
|
contentSchema ??= getSchema(createVisualEditorExtensions());
|
|
78
|
-
const localDoc = contentSchema.nodeFromJSON(nfmToDoc(
|
|
80
|
+
const localDoc = contentSchema.nodeFromJSON(nfmToDoc(candidate));
|
|
79
81
|
const plan = planDocReconcile(
|
|
80
82
|
localDoc,
|
|
81
83
|
contentSchema.nodeFromJSON(nfmToDoc(attemptedBase.content)),
|
|
82
84
|
contentSchema.nodeFromJSON(nfmToDoc(winner.content)),
|
|
83
85
|
);
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Carry an unambiguous three-way merge into the retry. Ambiguous edits
|
|
87
|
+
// still need recovery because choosing either side would discard work.
|
|
88
|
+
if (plan.status === "applied") {
|
|
89
|
+
candidate = docToNfm(plan.mergedDoc.toJSON());
|
|
90
|
+
} else if (plan.status !== "noop") {
|
|
88
91
|
return conflict();
|
|
89
92
|
}
|
|
90
93
|
attemptedBase = {
|