@agent-native/core 0.180.0-nightly-20260915184036 → 0.180.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 +10 -0
- package/corpus/templates/calendar/CHANGELOG.md +39 -0
- package/corpus/templates/calendar/INTERACTION-MATRIX.md +19 -17
- package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +21 -0
- package/corpus/templates/calendar/actions/view-screen.ts +10 -2
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +11 -0
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +174 -145
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +20 -16
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -0
- package/corpus/templates/calendar/app/hooks/use-events.ts +16 -0
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +126 -14
- package/corpus/templates/calendar/app/i18n-data.ts +90 -0
- package/corpus/templates/calendar/app/lib/calendar-navigation.ts +29 -9
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +2 -0
- package/corpus/templates/calendar/app/lib/event-time-range.ts +34 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +245 -35
- package/corpus/templates/calendar/changelog/2026-09-13-calendar-sidebar-overflow-is-easier-to-discover.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-15-calendar-mirrors-notion-calendar-s-customizable-day-ranges-a.md +6 -0
- package/corpus/templates/calendar/shared/calendar-view-preferences.ts +36 -0
- package/corpus/templates/calendar/shared/timezone.ts +9 -2
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +2 -0
- package/corpus/templates/clips/CHANGELOG.md +19 -0
- package/corpus/templates/clips/actions/save-browser-diagnostics.ts +43 -4
- package/corpus/templates/clips/app/components/player/browser-diagnostics-panel.tsx +114 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
- package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +108 -0
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-09-15-recordings-include-a-structured-timeline-of-browser-interact.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +7 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +235 -9
- package/corpus/templates/clips/chrome-extension/src/content-history-bridge.ts +80 -0
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +166 -5
- package/corpus/templates/clips/chrome-extension/vite.config.ts +4 -0
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +40 -2
- package/corpus/templates/clips/server/plugins/db.ts +5 -0
- package/corpus/templates/clips/shared/browser-diagnostics.ts +171 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +4 -2
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -1
- package/corpus/templates/content/AGENTS.md +5 -6
- package/corpus/templates/content/CHANGELOG.md +24 -0
- package/corpus/templates/content/actions/_content-database-row-migration.ts +67 -36
- package/corpus/templates/content/actions/_document-edit-mutation.ts +74 -23
- package/corpus/templates/content/actions/edit-document.ts +48 -8
- package/corpus/templates/content/actions/migrate-content-database-rows.ts +69 -28
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/design/CHANGELOG.md +22 -0
- package/corpus/templates/factory/CHANGELOG.md +11 -0
- package/corpus/templates/forms/CHANGELOG.md +6 -0
- package/corpus/templates/mail/.agents/skills/mail-interaction-parity/references/interaction-matrix.md +24 -4
- package/corpus/templates/mail/CHANGELOG.md +68 -0
- package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +8 -0
- package/corpus/templates/mail/app/components/email/ComposeModal.tsx +33 -13
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +23 -4
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +56 -13
- package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +23 -4
- package/corpus/templates/mail/app/components/email/RecipientInput.tsx +9 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +8 -1
- package/corpus/templates/mail/app/i18n/en-US.ts +31 -0
- package/corpus/templates/mail/changelog/2026-09-15-mail-labels-recipient-and-account-controls-for-keyboard-and-.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +32 -0
- package/dist/client/CommandMenu.js +14 -2
- package/dist/client/settings/AccountSettingsCard.js +3 -2
- package/dist/client/settings/SettingsTabsPage.js +9 -1
- package/dist/client/settings/agent-settings-search.js +5 -1
- package/dist/client/sign-out.d.ts +1 -0
- package/dist/client/sign-out.js +8 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +4 -4
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import { chunks } from "./_batch-utils.js";
|
|
14
14
|
|
|
15
15
|
const propertyType = z.enum(["text", "url", "date", "multi_select"]);
|
|
16
|
+
export const MAX_MIGRATION_ROWS = 250;
|
|
17
|
+
export const MAX_MIGRATION_PLAN_BYTES = 5 * 1024 * 1024;
|
|
16
18
|
const option = z.object({
|
|
17
19
|
id: z.string().min(1),
|
|
18
20
|
name: z.string().min(1),
|
|
@@ -28,42 +30,56 @@ const option = z.object({
|
|
|
28
30
|
"red",
|
|
29
31
|
]),
|
|
30
32
|
});
|
|
31
|
-
export const migrationPlanSchema = z
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
33
|
+
export const migrationPlanSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
databaseId: z.string().min(1),
|
|
36
|
+
databaseDocumentId: z.string().min(1),
|
|
37
|
+
idempotencyKey: z.string().min(1).max(200),
|
|
38
|
+
expectedRowCount: z.number().int().min(1).max(MAX_MIGRATION_ROWS),
|
|
39
|
+
propertyDefinitions: z
|
|
40
|
+
.array(
|
|
41
|
+
z.object({
|
|
42
|
+
id: z.string().min(1),
|
|
43
|
+
name: z.string().min(1),
|
|
44
|
+
type: propertyType,
|
|
45
|
+
visibility: z.enum(DOCUMENT_PROPERTY_VISIBILITIES),
|
|
46
|
+
options: z.array(option).max(100).optional(),
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
.max(100),
|
|
50
|
+
rows: z
|
|
51
|
+
.array(
|
|
52
|
+
z.object({
|
|
53
|
+
itemId: z.string().min(1),
|
|
54
|
+
documentId: z.string().min(1),
|
|
55
|
+
expectedUpdatedAt: z.string().min(1),
|
|
56
|
+
content: z.string(),
|
|
57
|
+
propertyValues: z.array(
|
|
58
|
+
z.object({ propertyId: z.string().min(1), value: z.unknown() }),
|
|
59
|
+
),
|
|
60
|
+
protectedPropertyValues: z
|
|
61
|
+
.array(
|
|
62
|
+
z.object({
|
|
63
|
+
propertyId: z.string().min(1),
|
|
64
|
+
valueJson: z.string(),
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
.max(100),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
.max(MAX_MIGRATION_ROWS),
|
|
71
|
+
legacyPropertyIds: z.array(z.string().min(1)).max(100).default([]),
|
|
72
|
+
})
|
|
73
|
+
.superRefine((plan, context) => {
|
|
74
|
+
if (
|
|
75
|
+
Buffer.byteLength(JSON.stringify(plan), "utf8") > MAX_MIGRATION_PLAN_BYTES
|
|
76
|
+
) {
|
|
77
|
+
context.addIssue({
|
|
78
|
+
code: "custom",
|
|
79
|
+
message: `Migration plan is limited to ${MAX_MIGRATION_PLAN_BYTES} bytes.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
67
83
|
export type MigrationPlan = z.infer<typeof migrationPlanSchema>;
|
|
68
84
|
|
|
69
85
|
export function canonical(value: unknown): string {
|
|
@@ -462,6 +478,21 @@ export function snapshotDigest(
|
|
|
462
478
|
});
|
|
463
479
|
}
|
|
464
480
|
|
|
481
|
+
export function snapshotBodyRevisionDigest(
|
|
482
|
+
snapshot: Awaited<ReturnType<typeof snapshotMigration>>,
|
|
483
|
+
) {
|
|
484
|
+
return digest([
|
|
485
|
+
{
|
|
486
|
+
documentId: snapshot.databaseDocument.id,
|
|
487
|
+
bodyRevision: snapshot.databaseDocument.bodyRevision,
|
|
488
|
+
},
|
|
489
|
+
...snapshot.rows.map((row: any) => ({
|
|
490
|
+
documentId: row.document.id,
|
|
491
|
+
bodyRevision: row.document.bodyRevision,
|
|
492
|
+
})),
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
|
|
465
496
|
export async function applyMigration(
|
|
466
497
|
tx: any,
|
|
467
498
|
plan: MigrationPlan,
|
|
@@ -64,7 +64,8 @@ function validateDocumentEditSnapshot(
|
|
|
64
64
|
document: typeof schema.documents.$inferSelect | undefined,
|
|
65
65
|
base: { revision: number; contentHash: string },
|
|
66
66
|
baseRevision: string,
|
|
67
|
-
edits: DocumentTextEdit[],
|
|
67
|
+
edits: DocumentTextEdit[] | undefined,
|
|
68
|
+
initializeContent?: string,
|
|
68
69
|
) {
|
|
69
70
|
if (!document) {
|
|
70
71
|
throw new ActionContractError("Document not found.", {
|
|
@@ -88,7 +89,19 @@ function validateDocumentEditSnapshot(
|
|
|
88
89
|
currentContentHash: beforeHash,
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
|
-
const resolved =
|
|
92
|
+
const resolved = edits
|
|
93
|
+
? resolveDocumentTextEdits(beforeContent, edits)
|
|
94
|
+
: beforeContent === ""
|
|
95
|
+
? {
|
|
96
|
+
ok: true as const,
|
|
97
|
+
content: initializeContent as string,
|
|
98
|
+
ranges: [{ start: 0, end: 0 }],
|
|
99
|
+
}
|
|
100
|
+
: conflict(
|
|
101
|
+
"DOCUMENT_BODY_NOT_EMPTY",
|
|
102
|
+
"Document initialization requires a literally empty body.",
|
|
103
|
+
{ currentRevision: baseRevision },
|
|
104
|
+
);
|
|
92
105
|
if (!resolved.ok) {
|
|
93
106
|
conflict(
|
|
94
107
|
resolved.error.kind === "missing"
|
|
@@ -178,19 +191,24 @@ export interface DocumentEditCreativeContext {
|
|
|
178
191
|
}>;
|
|
179
192
|
}
|
|
180
193
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
type DocumentBodyMutation =
|
|
195
|
+
| { edits: DocumentTextEdit[]; initializeContent?: never }
|
|
196
|
+
| { edits?: never; initializeContent: string };
|
|
197
|
+
|
|
198
|
+
export async function mutateDocumentBody(
|
|
199
|
+
args: {
|
|
200
|
+
documentId: string;
|
|
201
|
+
baseRevision: string;
|
|
202
|
+
idempotencyKey: string;
|
|
203
|
+
creativeContext?: DocumentEditCreativeContext;
|
|
204
|
+
creativeContextDigest?: unknown;
|
|
205
|
+
resolveCreativeContext?: () => Promise<
|
|
206
|
+
DocumentEditCreativeContext | undefined
|
|
207
|
+
>;
|
|
208
|
+
ctx: ActionRunContext;
|
|
209
|
+
db?: Db;
|
|
210
|
+
} & DocumentBodyMutation,
|
|
211
|
+
): Promise<DocumentEditMutationResult> {
|
|
194
212
|
const db = args.db ?? getDb();
|
|
195
213
|
const scope = callerScope(args.ctx);
|
|
196
214
|
const base = parseDocumentRevisionToken(args.baseRevision);
|
|
@@ -203,16 +221,47 @@ export async function mutateDocumentBody(args: {
|
|
|
203
221
|
},
|
|
204
222
|
);
|
|
205
223
|
}
|
|
206
|
-
const normalizedEdits = args.edits
|
|
224
|
+
const normalizedEdits = args.edits?.map(({ find, replace }) => ({
|
|
207
225
|
find,
|
|
208
226
|
replace,
|
|
209
227
|
}));
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
const initializationContent = args.initializeContent;
|
|
229
|
+
if (!normalizedEdits && initializationContent === undefined) {
|
|
230
|
+
throw new ActionContractError("A document body mutation is required.", {
|
|
231
|
+
errorCode: "DOCUMENT_EDIT_MODE_REQUIRED",
|
|
232
|
+
statusCode: 400,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
initializationContent !== undefined &&
|
|
237
|
+
initializationContent.trim().length === 0
|
|
238
|
+
) {
|
|
239
|
+
throw new ActionContractError(
|
|
240
|
+
"initializeContent must contain non-whitespace content.",
|
|
241
|
+
{
|
|
242
|
+
errorCode: "DOCUMENT_INITIALIZATION_CONTENT_REQUIRED",
|
|
243
|
+
statusCode: 400,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const payloadDigest = digest(
|
|
248
|
+
normalizedEdits
|
|
249
|
+
? {
|
|
250
|
+
documentId: args.documentId,
|
|
251
|
+
baseRevision: args.baseRevision,
|
|
252
|
+
edits: normalizedEdits,
|
|
253
|
+
creativeContext:
|
|
254
|
+
args.creativeContextDigest ?? args.creativeContext ?? null,
|
|
255
|
+
}
|
|
256
|
+
: {
|
|
257
|
+
documentId: args.documentId,
|
|
258
|
+
baseRevision: args.baseRevision,
|
|
259
|
+
operation: "initialize",
|
|
260
|
+
initializeContent: initializationContent,
|
|
261
|
+
creativeContext:
|
|
262
|
+
args.creativeContextDigest ?? args.creativeContext ?? null,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
216
265
|
|
|
217
266
|
const readReplay = async (client: Db) => {
|
|
218
267
|
const [stored] = await client
|
|
@@ -250,6 +299,7 @@ export async function mutateDocumentBody(args: {
|
|
|
250
299
|
base,
|
|
251
300
|
args.baseRevision,
|
|
252
301
|
normalizedEdits,
|
|
302
|
+
initializationContent,
|
|
253
303
|
);
|
|
254
304
|
const creativeContext = args.resolveCreativeContext
|
|
255
305
|
? await args.resolveCreativeContext()
|
|
@@ -274,6 +324,7 @@ export async function mutateDocumentBody(args: {
|
|
|
274
324
|
base,
|
|
275
325
|
args.baseRevision,
|
|
276
326
|
normalizedEdits,
|
|
327
|
+
initializationContent,
|
|
277
328
|
);
|
|
278
329
|
const { beforeContent, beforeHash, resolved } = validated;
|
|
279
330
|
|
|
@@ -352,7 +403,7 @@ export async function mutateDocumentBody(args: {
|
|
|
352
403
|
}));
|
|
353
404
|
const result: DocumentEditMutationResult = {
|
|
354
405
|
applied: changed ? resolved.ranges.length : 0,
|
|
355
|
-
total: normalizedEdits
|
|
406
|
+
total: normalizedEdits?.length ?? 1,
|
|
356
407
|
receipt: {
|
|
357
408
|
receiptId,
|
|
358
409
|
outcome: changed ? "applied" : "unchanged",
|
|
@@ -96,6 +96,16 @@ const editDocumentSchema = z.object({
|
|
|
96
96
|
.describe(
|
|
97
97
|
"JSON array of {find, replace} objects for a snapshot-stable batch; use instead of find/replace.",
|
|
98
98
|
),
|
|
99
|
+
initializeContent: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.refine((value) => value.trim().length > 0, {
|
|
103
|
+
message: "initializeContent must contain non-whitespace content.",
|
|
104
|
+
})
|
|
105
|
+
.optional()
|
|
106
|
+
.describe(
|
|
107
|
+
"Exact Markdown containing non-whitespace content, used only to initialize a literally empty document body; mutually exclusive with find, replace, and edits.",
|
|
108
|
+
),
|
|
99
109
|
contextPackId: z
|
|
100
110
|
.string()
|
|
101
111
|
.optional()
|
|
@@ -207,7 +217,7 @@ async function resolveEditCreativeContext(args: {
|
|
|
207
217
|
|
|
208
218
|
export default defineAction({
|
|
209
219
|
description:
|
|
210
|
-
"
|
|
220
|
+
"Edit an existing document's Markdown with exact search-and-replace operations, or initialize a literally empty body with initializeContent. Every find string must match exactly once in the immutable base. First call get-document, then pass its baseRevision and a caller-generated idempotencyKey.",
|
|
211
221
|
deferLoading: false,
|
|
212
222
|
mcpTool: true,
|
|
213
223
|
agentInputSchema: externalEditDocumentSchema,
|
|
@@ -223,9 +233,24 @@ export default defineAction({
|
|
|
223
233
|
const isAgentCaller =
|
|
224
234
|
ctx?.caller === "tool" || ctx?.caller === "mcp" || ctx?.caller === "a2a";
|
|
225
235
|
|
|
226
|
-
let edits: TextEdit[];
|
|
236
|
+
let edits: TextEdit[] = [];
|
|
237
|
+
const initializesBody = args.initializeContent !== undefined;
|
|
227
238
|
|
|
228
|
-
if (
|
|
239
|
+
if (
|
|
240
|
+
initializesBody &&
|
|
241
|
+
(args.find !== undefined ||
|
|
242
|
+
args.replace !== undefined ||
|
|
243
|
+
args.edits !== undefined)
|
|
244
|
+
) {
|
|
245
|
+
throw new ActionContractError(
|
|
246
|
+
"initializeContent is mutually exclusive with find, replace, and edits.",
|
|
247
|
+
{ errorCode: "DOCUMENT_EDIT_MODE_CONFLICT", statusCode: 400 },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (initializesBody) {
|
|
252
|
+
edits = [];
|
|
253
|
+
} else if (Array.isArray(args.edits)) {
|
|
229
254
|
edits = args.edits;
|
|
230
255
|
} else if (args.edits !== undefined) {
|
|
231
256
|
throw new Error("--edits must be a JSON array");
|
|
@@ -233,7 +258,10 @@ export default defineAction({
|
|
|
233
258
|
if (!args.find) throw new Error("--find cannot be empty");
|
|
234
259
|
edits = [{ find: args.find, replace: args.replace ?? "" }];
|
|
235
260
|
} else {
|
|
236
|
-
throw new
|
|
261
|
+
throw new ActionContractError(
|
|
262
|
+
"One of initializeContent, find, or edits is required.",
|
|
263
|
+
{ errorCode: "DOCUMENT_EDIT_MODE_REQUIRED", statusCode: 400 },
|
|
264
|
+
);
|
|
237
265
|
}
|
|
238
266
|
|
|
239
267
|
const access = await assertAccess("document", id, "editor");
|
|
@@ -243,13 +271,19 @@ export default defineAction({
|
|
|
243
271
|
ctx?.caller === "mcp" ||
|
|
244
272
|
ctx?.caller === "webmcp" ||
|
|
245
273
|
ctx?.caller === "a2a";
|
|
246
|
-
if (isExternalCaller) {
|
|
274
|
+
if (isExternalCaller || initializesBody) {
|
|
247
275
|
if (!args.baseRevision || !args.idempotencyKey) {
|
|
248
276
|
throw new ActionContractError(
|
|
249
277
|
"External document edits require baseRevision and idempotencyKey from get-document.",
|
|
250
278
|
{ errorCode: "DOCUMENT_EDIT_PROTOCOL_REQUIRED", statusCode: 400 },
|
|
251
279
|
);
|
|
252
280
|
}
|
|
281
|
+
if (!ctx) {
|
|
282
|
+
throw new ActionContractError(
|
|
283
|
+
"Revisioned document edits require an authenticated caller context.",
|
|
284
|
+
{ errorCode: "CALLER_SCOPE_REQUIRED", statusCode: 401 },
|
|
285
|
+
);
|
|
286
|
+
}
|
|
253
287
|
const isLinkedLocalSource =
|
|
254
288
|
existing.sourceMode === "local-files" &&
|
|
255
289
|
existing.sourceKind !== "folder" &&
|
|
@@ -265,11 +299,14 @@ export default defineAction({
|
|
|
265
299
|
},
|
|
266
300
|
);
|
|
267
301
|
}
|
|
302
|
+
const mutation = initializesBody
|
|
303
|
+
? { initializeContent: args.initializeContent as string }
|
|
304
|
+
: { edits };
|
|
268
305
|
const result = await mutateDocumentBody({
|
|
269
306
|
documentId: id,
|
|
270
307
|
baseRevision: args.baseRevision,
|
|
271
308
|
idempotencyKey: args.idempotencyKey,
|
|
272
|
-
|
|
309
|
+
...mutation,
|
|
273
310
|
creativeContextDigest: {
|
|
274
311
|
contextPackId: args.contextPackId ?? null,
|
|
275
312
|
contextModeOverride: args.contextModeOverride ?? null,
|
|
@@ -290,7 +327,10 @@ export default defineAction({
|
|
|
290
327
|
edit: {
|
|
291
328
|
descriptor: {
|
|
292
329
|
kind: "text",
|
|
293
|
-
quote:
|
|
330
|
+
quote:
|
|
331
|
+
args.initializeContent?.slice(0, 80) ??
|
|
332
|
+
edits?.[0]?.replace.slice(0, 80) ??
|
|
333
|
+
"",
|
|
294
334
|
},
|
|
295
335
|
label: existing.title || undefined,
|
|
296
336
|
},
|
|
@@ -307,7 +347,7 @@ export default defineAction({
|
|
|
307
347
|
output_id: id,
|
|
308
348
|
output_type: "document",
|
|
309
349
|
edit_count: result.applied,
|
|
310
|
-
refine_type: "exact_replace",
|
|
350
|
+
refine_type: initializesBody ? "full_update" : "exact_replace",
|
|
311
351
|
},
|
|
312
352
|
ctx,
|
|
313
353
|
);
|
|
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
import { getDb, schema } from "../server/db/index.js";
|
|
9
9
|
import { bodyRevisionForContent } from "../server/lib/document-body-revision.js";
|
|
10
|
+
import { chunks } from "./_batch-utils.js";
|
|
10
11
|
import {
|
|
11
12
|
lockContentDatabaseMutation,
|
|
12
13
|
touchContentDatabase,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
deterministicId,
|
|
18
19
|
digest,
|
|
19
20
|
migrationPlanSchema,
|
|
21
|
+
snapshotBodyRevisionDigest,
|
|
20
22
|
snapshotDigest,
|
|
21
23
|
snapshotMigration,
|
|
22
24
|
serializeMigrationValue,
|
|
@@ -77,6 +79,21 @@ function parseJson(text: string) {
|
|
|
77
79
|
throw new Error("Migration receipt is corrupt.");
|
|
78
80
|
}
|
|
79
81
|
}
|
|
82
|
+
|
|
83
|
+
function migrationStateMatches(
|
|
84
|
+
snapshot: Awaited<ReturnType<typeof snapshotMigration>>,
|
|
85
|
+
receipt: { postDigest: string; resultJson: string },
|
|
86
|
+
) {
|
|
87
|
+
const result = parseJson(receipt.resultJson);
|
|
88
|
+
if (!("bodyRevisionDigest" in result))
|
|
89
|
+
return snapshotDigest(snapshot) === receipt.postDigest;
|
|
90
|
+
if (typeof result.bodyRevisionDigest !== "string")
|
|
91
|
+
throw new Error("Migration receipt body revision guard is corrupt.");
|
|
92
|
+
return (
|
|
93
|
+
snapshotBodyRevisionDigest(snapshot) === result.bodyRevisionDigest &&
|
|
94
|
+
snapshotDigest(snapshot) === receipt.postDigest
|
|
95
|
+
);
|
|
96
|
+
}
|
|
80
97
|
function receiptResult(receipt: any, replayed: boolean) {
|
|
81
98
|
const {
|
|
82
99
|
plan: _plan,
|
|
@@ -105,22 +122,36 @@ async function lockCurrentDatabaseMemberships(tx: any, databaseId: string) {
|
|
|
105
122
|
);
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
export const MAX_MIGRATION_FLUSH_CONCURRENCY = 20;
|
|
126
|
+
|
|
108
127
|
async function flushMigrationDocuments(rows: Array<{ documentId: string }>) {
|
|
109
|
-
const accesses =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
const accesses: Awaited<ReturnType<typeof assertAccess>>[] = [];
|
|
129
|
+
for (const batch of chunks(rows, MAX_MIGRATION_FLUSH_CONCURRENCY)) {
|
|
130
|
+
accesses.push(
|
|
131
|
+
...(await Promise.all(
|
|
132
|
+
batch.map((row) => assertAccess("document", row.documentId, "editor")),
|
|
133
|
+
)),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
for (
|
|
137
|
+
let offset = 0;
|
|
138
|
+
offset < rows.length;
|
|
139
|
+
offset += MAX_MIGRATION_FLUSH_CONCURRENCY
|
|
140
|
+
) {
|
|
141
|
+
const batch = rows.slice(offset, offset + MAX_MIGRATION_FLUSH_CONCURRENCY);
|
|
142
|
+
const flushes = await Promise.allSettled(
|
|
143
|
+
batch.map((row, index) =>
|
|
144
|
+
flushOpenDocumentEditorToSql({
|
|
145
|
+
documentId: row.documentId,
|
|
146
|
+
ownerEmail: accesses[offset + index]?.resource.ownerEmail,
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
const failed = flushes.find(
|
|
151
|
+
(flush): flush is PromiseRejectedResult => flush.status === "rejected",
|
|
152
|
+
);
|
|
153
|
+
if (failed) throw failed.reason;
|
|
154
|
+
}
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
export async function runMigration(args: MigrationInput) {
|
|
@@ -164,9 +195,10 @@ export async function runMigration(args: MigrationInput) {
|
|
|
164
195
|
if (existing.state !== "applied" && existing.state !== "verified")
|
|
165
196
|
throw new Error(`Migration receipt is already ${existing.state}.`);
|
|
166
197
|
if (
|
|
167
|
-
|
|
198
|
+
!migrationStateMatches(
|
|
168
199
|
await snapshotMigration(tx, args.plan.databaseId),
|
|
169
|
-
|
|
200
|
+
existing,
|
|
201
|
+
)
|
|
170
202
|
)
|
|
171
203
|
throw new Error(
|
|
172
204
|
"Applied migration has drifted; replay is refused.",
|
|
@@ -212,7 +244,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
212
244
|
if (receipt.state === "rolled_back") {
|
|
213
245
|
if (
|
|
214
246
|
result.transitionExpectedPostDigest !== args.expectedPostDigest ||
|
|
215
|
-
|
|
247
|
+
!migrationStateMatches(current, receipt)
|
|
216
248
|
)
|
|
217
249
|
throw new Error(
|
|
218
250
|
"Terminal migration result has drifted; replay is refused.",
|
|
@@ -223,7 +255,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
223
255
|
throw new Error(`Migration receipt is already ${receipt.state}.`);
|
|
224
256
|
if (
|
|
225
257
|
receipt.postDigest !== args.expectedPostDigest ||
|
|
226
|
-
|
|
258
|
+
!migrationStateMatches(current, receipt)
|
|
227
259
|
)
|
|
228
260
|
throw new Error(
|
|
229
261
|
"Migration has drifted; guarded operation is refused.",
|
|
@@ -304,8 +336,10 @@ export async function runMigration(args: MigrationInput) {
|
|
|
304
336
|
"Expected post-migration digest does not match receipt.",
|
|
305
337
|
);
|
|
306
338
|
if (
|
|
307
|
-
|
|
308
|
-
|
|
339
|
+
!migrationStateMatches(
|
|
340
|
+
await snapshotMigration(tx, args.databaseId),
|
|
341
|
+
receipt,
|
|
342
|
+
)
|
|
309
343
|
)
|
|
310
344
|
throw new Error(
|
|
311
345
|
args.phase === "verify"
|
|
@@ -353,7 +387,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
353
387
|
`Migration receipt is already ${existing.state}.`,
|
|
354
388
|
);
|
|
355
389
|
const current = await snapshotMigration(tx, args.plan.databaseId);
|
|
356
|
-
if (
|
|
390
|
+
if (!migrationStateMatches(current, existing))
|
|
357
391
|
throw new Error(
|
|
358
392
|
"Applied migration has drifted; replay is refused.",
|
|
359
393
|
);
|
|
@@ -433,6 +467,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
433
467
|
orderedIds,
|
|
434
468
|
written: args.plan.rows.length,
|
|
435
469
|
verified: false,
|
|
470
|
+
bodyRevisionDigest: snapshotBodyRevisionDigest(current),
|
|
436
471
|
plan: args.plan,
|
|
437
472
|
};
|
|
438
473
|
const claimed = await tx
|
|
@@ -492,8 +527,10 @@ export async function runMigration(args: MigrationInput) {
|
|
|
492
527
|
"Expected post-migration digest does not match receipt.",
|
|
493
528
|
);
|
|
494
529
|
if (
|
|
495
|
-
|
|
496
|
-
|
|
530
|
+
!migrationStateMatches(
|
|
531
|
+
await snapshotMigration(tx, args.databaseId),
|
|
532
|
+
receipt,
|
|
533
|
+
)
|
|
497
534
|
)
|
|
498
535
|
throw new Error("Migration has drifted; verification is refused.");
|
|
499
536
|
return receiptResult(receipt, true);
|
|
@@ -509,8 +546,10 @@ export async function runMigration(args: MigrationInput) {
|
|
|
509
546
|
"Expected post-migration digest does not match receipt.",
|
|
510
547
|
);
|
|
511
548
|
if (
|
|
512
|
-
|
|
513
|
-
|
|
549
|
+
!migrationStateMatches(
|
|
550
|
+
await snapshotMigration(tx, args.databaseId),
|
|
551
|
+
receipt,
|
|
552
|
+
)
|
|
514
553
|
)
|
|
515
554
|
throw new Error(
|
|
516
555
|
"Terminal migration result has drifted; replay is refused.",
|
|
@@ -525,7 +564,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
525
564
|
"Expected post-migration digest does not match receipt.",
|
|
526
565
|
);
|
|
527
566
|
const current = await snapshotMigration(tx, args.databaseId);
|
|
528
|
-
if (
|
|
567
|
+
if (!migrationStateMatches(current, receipt))
|
|
529
568
|
throw new Error("Migration has drifted; verification is refused.");
|
|
530
569
|
const plan = parseJson(receipt.resultJson).plan;
|
|
531
570
|
if (!plan)
|
|
@@ -656,7 +695,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
656
695
|
);
|
|
657
696
|
}
|
|
658
697
|
const current = await snapshotMigration(tx, args.databaseId);
|
|
659
|
-
if (
|
|
698
|
+
if (!migrationStateMatches(current, receipt))
|
|
660
699
|
throw new Error("Migration has drifted; guarded operation is refused.");
|
|
661
700
|
const rollback = parseJson(receipt.rollbackJson);
|
|
662
701
|
const now = new Date().toISOString();
|
|
@@ -707,6 +746,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
707
746
|
const resultJson = {
|
|
708
747
|
phase: "rollback",
|
|
709
748
|
transitionExpectedPostDigest: receipt.postDigest,
|
|
749
|
+
bodyRevisionDigest: snapshotBodyRevisionDigest(restored),
|
|
710
750
|
counts: {
|
|
711
751
|
rows: (rollback.versions ?? []).length,
|
|
712
752
|
properties: ids.length,
|
|
@@ -788,6 +828,7 @@ export async function runMigration(args: MigrationInput) {
|
|
|
788
828
|
const resultJson = {
|
|
789
829
|
phase: "finalize",
|
|
790
830
|
transitionExpectedPostDigest: receipt.postDigest,
|
|
831
|
+
bodyRevisionDigest: snapshotBodyRevisionDigest(finalized),
|
|
791
832
|
counts: { rows: 0, properties: legacyIds.length },
|
|
792
833
|
verified: true,
|
|
793
834
|
};
|
|
@@ -32,7 +32,7 @@ export default createAgentChatPlugin({
|
|
|
32
32
|
mcp: {
|
|
33
33
|
externalAgents: { writes: "allowlisted" },
|
|
34
34
|
instructions:
|
|
35
|
-
"Find documents with list-documents or search-documents; read
|
|
35
|
+
"Find documents with list-documents or search-documents; read with get-document (pull-document for raw Markdown). Author and persist content with create-document. For body changes use revision-guarded edit-document; pass initializeContent only when get-document returns an empty body. Use update-document for metadata and browser rewrites. For provider data use provider-api-catalog → provider-api-docs → provider-api-request.",
|
|
36
36
|
},
|
|
37
37
|
anonymousOwner: resolvePublicViewerOwner,
|
|
38
38
|
extraContext: publicDocumentExtraContext,
|
|
@@ -9,8 +9,30 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
9
9
|
|
|
10
10
|
- A message that only mentions a design, or pastes Design's own guidance back into chat, no longer ends with a false "couldn't confirm that a Design artifact was saved" reply.
|
|
11
11
|
|
|
12
|
+
## 2026-09-14
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Component duplicates stay linked to their main across Screens, and detaching keeps their current appearance
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Rapid layout and padding edits preserve previously committed styles.
|
|
21
|
+
- Screen creation preserves drawn dimensions and layer identity for immediate visual edits.
|
|
22
|
+
- Selecting an element in a live React app now anchors to the authored component file and owner instead of the JSX runtime, so source edits and repeated-instance counts target the real code
|
|
23
|
+
- Undo and redo follow the order of edits across screens.
|
|
24
|
+
- Zoom to selection includes selected layers across Screens and the board, and multiline text stays a single Text layer.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Creative Context is now in Labs and off by default
|
|
29
|
+
|
|
12
30
|
## 2026-09-13
|
|
13
31
|
|
|
32
|
+
### Improved
|
|
33
|
+
|
|
34
|
+
- Screen layout, style edits, keyboard shortcuts, and edits to AI-generated designs work more reliably through undo and reload
|
|
35
|
+
|
|
14
36
|
### Fixed
|
|
15
37
|
|
|
16
38
|
- Connect Code imports explain when a design-system name is already in use
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable user-facing changes to Chat are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-09-14
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Factory PR babysit uses a fair queue so re-queued pull requests are not starved, re-prioritizes open PRs when new review work arrives without a commit, and posts a v2 ping comment with per-thread disposition guidance.
|
|
11
|
+
- PR babysit now terminalizes merged and closed pull requests instead of re-listing them with "Seen before".
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Factory automations keep display names, Slack channels, and custom prompts through metadata repair, tab switches, and accidental clears.
|
|
16
|
+
|
|
6
17
|
## 2026-09-11
|
|
7
18
|
|
|
8
19
|
### Improved
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Forms are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-09-15
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Asking the agent for a second, unrelated form now creates a separate form instead of overwriting the one already open in the chat.
|
|
11
|
+
|
|
6
12
|
## 2026-09-12
|
|
7
13
|
|
|
8
14
|
### Fixed
|