@agent-native/core-corpus 0.216.0 → 0.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +8 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/Index.tsx +10 -3
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/CHANGELOG.md +6 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
|
5
5
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
6
6
|
import { withCommittedActionAudit } from "@agent-native/core/audit";
|
|
7
7
|
import { agentTouchDocument } from "@agent-native/core/collab";
|
|
8
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
8
9
|
import {
|
|
9
10
|
iconValueSchema,
|
|
10
11
|
parseIconValue,
|
|
@@ -49,12 +50,20 @@ import {
|
|
|
49
50
|
parseDocumentFavorite,
|
|
50
51
|
parseDocumentHideFromSearch,
|
|
51
52
|
} from "../server/lib/documents.js";
|
|
53
|
+
import {
|
|
54
|
+
liveBodyWriterGateForRow,
|
|
55
|
+
liveBodyWriterPageColumns,
|
|
56
|
+
} from "../server/lib/live-body-writer.js";
|
|
52
57
|
import {
|
|
53
58
|
syncPrivateCalloutReferences,
|
|
54
59
|
syncPrivateIconReference,
|
|
55
60
|
verifyPrivateIconAssignment,
|
|
56
61
|
} from "../server/lib/private-icon-references.js";
|
|
57
|
-
import
|
|
62
|
+
import { liveCopyHoldsSave } from "../server/live-copy/holds.js";
|
|
63
|
+
import type {
|
|
64
|
+
DocumentUpdateResponse,
|
|
65
|
+
LiveCopyBodySave,
|
|
66
|
+
} from "../shared/api.js";
|
|
58
67
|
import { applyContentPersonalNavigationPatch } from "../shared/content-personal-navigation-patch.js";
|
|
59
68
|
import { mergeDocumentBodyIntents } from "../shared/document-intent-merge.js";
|
|
60
69
|
import { inspectNfmFidelity } from "../shared/nfm.js";
|
|
@@ -206,6 +215,13 @@ function documentUpdateResponse(
|
|
|
206
215
|
source: serializeDocumentSource(doc),
|
|
207
216
|
softDeletedDatabaseIds,
|
|
208
217
|
...(browserSaveAttempt ? { browserSaveAttempt } : {}),
|
|
218
|
+
// Only a save the live copy holds keeps a receipt.
|
|
219
|
+
...(browserSaveAttempt?.bodySave
|
|
220
|
+
? {
|
|
221
|
+
bodySave: browserSaveAttempt.bodySave,
|
|
222
|
+
bodyIntentOutcome: { status: "applied" as const },
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
209
225
|
};
|
|
210
226
|
}
|
|
211
227
|
|
|
@@ -974,7 +990,7 @@ const updateDocumentAction = defineAction({
|
|
|
974
990
|
args.authoredCandidateContent === undefined
|
|
975
991
|
? undefined
|
|
976
992
|
: normalizeAuthoredContent(args.authoredCandidateContent);
|
|
977
|
-
let
|
|
993
|
+
let staleBodySave: "stale_builder_image" | "stale_empty_body" | undefined;
|
|
978
994
|
if (content !== undefined && !args.preserveLeadingTitleHeading) {
|
|
979
995
|
if (
|
|
980
996
|
content.includes('componentName="Image"') &&
|
|
@@ -1012,12 +1028,11 @@ const updateDocumentAction = defineAction({
|
|
|
1012
1028
|
sourceContent,
|
|
1013
1029
|
})
|
|
1014
1030
|
) {
|
|
1015
|
-
|
|
1016
|
-
measurement.reason_code = "stale_builder_image";
|
|
1017
|
-
content = existing.content;
|
|
1031
|
+
staleBodySave = "stale_builder_image";
|
|
1018
1032
|
}
|
|
1019
1033
|
}
|
|
1020
1034
|
if (
|
|
1035
|
+
!staleBodySave &&
|
|
1021
1036
|
shouldRejectStaleEmptyBodySave({
|
|
1022
1037
|
incomingContent: content,
|
|
1023
1038
|
currentContent: existing.content,
|
|
@@ -1026,13 +1041,26 @@ const updateDocumentAction = defineAction({
|
|
|
1026
1041
|
loadedContentWasEmpty: args.loadedContentWasEmpty,
|
|
1027
1042
|
})
|
|
1028
1043
|
) {
|
|
1029
|
-
|
|
1030
|
-
measurement.stale_base = "true";
|
|
1031
|
-
measurement.reason_code = "stale_empty_body";
|
|
1032
|
-
content = existing.content;
|
|
1044
|
+
staleBodySave = "stale_empty_body";
|
|
1033
1045
|
}
|
|
1034
1046
|
}
|
|
1035
|
-
|
|
1047
|
+
// These guard a body write, and a switched page's body may not be
|
|
1048
|
+
// written here at all: the gate, read under the page lock, decides.
|
|
1049
|
+
const staleBodySaveDeferred =
|
|
1050
|
+
ctx?.caller === "frontend" && !!existing.liveBodySwitchedAt;
|
|
1051
|
+
/** Keeps the stored body; true when the save must answer a conflict. */
|
|
1052
|
+
const refuseStaleBodySave = (storedContent: string) => {
|
|
1053
|
+
measurement.reason_code = staleBodySave;
|
|
1054
|
+
if (staleBodySave === "stale_empty_body")
|
|
1055
|
+
measurement.stale_base = "true";
|
|
1056
|
+
content = storedContent;
|
|
1057
|
+
return !!args.browserSaveAttemptId;
|
|
1058
|
+
};
|
|
1059
|
+
if (
|
|
1060
|
+
staleBodySave &&
|
|
1061
|
+
!staleBodySaveDeferred &&
|
|
1062
|
+
refuseStaleBodySave(existing.content)
|
|
1063
|
+
) {
|
|
1036
1064
|
measurement.outcome = "conflict";
|
|
1037
1065
|
return respond(
|
|
1038
1066
|
documentConflictResponse(existing, access.role, currentFavorite),
|
|
@@ -1072,6 +1100,11 @@ const updateDocumentAction = defineAction({
|
|
|
1072
1100
|
let browserSaveConfirmation: BrowserSaveAttemptConfirmation | undefined;
|
|
1073
1101
|
let bodyIntentOutcome: BrowserDocumentUpdateResponse["bodyIntentOutcome"];
|
|
1074
1102
|
let discardedEditorGeneration: number | undefined;
|
|
1103
|
+
let liveCopyBodySave: LiveCopyBodySave | undefined;
|
|
1104
|
+
// Only a save the live copy already holds settles its draft or keeps a
|
|
1105
|
+
// receipt. A tab whose updates never reached the live copy has nothing
|
|
1106
|
+
// else on the server holding its text.
|
|
1107
|
+
let settlesLiveCopyBody = false;
|
|
1075
1108
|
let preservationRequired:
|
|
1076
1109
|
| {
|
|
1077
1110
|
reason: "structure" | "provenance";
|
|
@@ -1226,10 +1259,76 @@ const updateDocumentAction = defineAction({
|
|
|
1226
1259
|
contentCasConflict = true;
|
|
1227
1260
|
return;
|
|
1228
1261
|
}
|
|
1262
|
+
if (ctx?.caller === "frontend" && content !== undefined) {
|
|
1263
|
+
const [page] = await tx
|
|
1264
|
+
.select(liveBodyWriterPageColumns)
|
|
1265
|
+
.from(schema.documents)
|
|
1266
|
+
.where(eq(schema.documents.id, id))
|
|
1267
|
+
.limit(1);
|
|
1268
|
+
const gate = await liveBodyWriterGateForRow(page, {
|
|
1269
|
+
transaction: getDbExec(),
|
|
1270
|
+
});
|
|
1271
|
+
if (gate.writePath === "live_copy") {
|
|
1272
|
+
// The editor's updates reach a live copy over collab, so the
|
|
1273
|
+
// body part of its save is not written here. Comparing it with
|
|
1274
|
+
// the stored body would read every peer's live-copy commit as
|
|
1275
|
+
// an unmerged edit and divert this save to History.
|
|
1276
|
+
const saveBodies = new Set(
|
|
1277
|
+
[
|
|
1278
|
+
args.content,
|
|
1279
|
+
args.authoredCandidateContent,
|
|
1280
|
+
args.editorSnapshotContent,
|
|
1281
|
+
].filter((body): body is string => body !== undefined),
|
|
1282
|
+
);
|
|
1283
|
+
const matchesLiveCopy = [...saveBodies].every(
|
|
1284
|
+
(body) => body === historyBefore.content,
|
|
1285
|
+
);
|
|
1286
|
+
liveCopyBodySave = {
|
|
1287
|
+
status: "live_copy",
|
|
1288
|
+
matchesLiveCopy,
|
|
1289
|
+
bodyRevision: historyBefore.bodyRevision,
|
|
1290
|
+
collabVersion: page.liveCopyVersion as number,
|
|
1291
|
+
};
|
|
1292
|
+
// A tab's body trails peers' commits it hasn't received, so
|
|
1293
|
+
// most saves from two open tabs match no stored body. Old
|
|
1294
|
+
// editors read an unsettled save as unsaved and send its text
|
|
1295
|
+
// to History when the tab reopens.
|
|
1296
|
+
// A receipt acknowledges the submitted body, authored candidate
|
|
1297
|
+
// and recovery snapshot. SQL strips title headings they retain.
|
|
1298
|
+
settlesLiveCopyBody = true;
|
|
1299
|
+
for (const body of saveBodies) {
|
|
1300
|
+
if (body === historyBefore.content) continue;
|
|
1301
|
+
const hold = authoredBase
|
|
1302
|
+
? await liveCopyHoldsSave({
|
|
1303
|
+
base: args.authoredBaseContent as string,
|
|
1304
|
+
stored: historyBefore.content,
|
|
1305
|
+
candidate: body,
|
|
1306
|
+
})
|
|
1307
|
+
: undefined;
|
|
1308
|
+
if (hold) measurement.reason_code = `live_copy_save_${hold}`;
|
|
1309
|
+
if (hold !== "held") {
|
|
1310
|
+
settlesLiveCopyBody = false;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (settlesLiveCopyBody) {
|
|
1315
|
+
bodyIntentOutcome = { status: "applied" };
|
|
1316
|
+
}
|
|
1317
|
+
content = undefined;
|
|
1318
|
+
} else if (
|
|
1319
|
+
staleBodySave &&
|
|
1320
|
+
staleBodySaveDeferred &&
|
|
1321
|
+
refuseStaleBodySave(historyBefore.content)
|
|
1322
|
+
) {
|
|
1323
|
+
contentCasConflict = true;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1229
1327
|
const confirmBrowserSave = async (
|
|
1230
1328
|
snapshot: { title: string; content: string },
|
|
1231
1329
|
now: string,
|
|
1232
1330
|
) => {
|
|
1331
|
+
if (liveCopyBodySave && !settlesLiveCopyBody) return;
|
|
1233
1332
|
if (
|
|
1234
1333
|
settlesPreviewDraft &&
|
|
1235
1334
|
args.editorSnapshotTitle !== undefined &&
|
|
@@ -1261,17 +1360,23 @@ const updateDocumentAction = defineAction({
|
|
|
1261
1360
|
updatedAt: schema.documents.updatedAt,
|
|
1262
1361
|
})
|
|
1263
1362
|
.from(schema.documents)
|
|
1264
|
-
.where(
|
|
1363
|
+
.where(
|
|
1364
|
+
and(
|
|
1365
|
+
eq(schema.documents.id, id),
|
|
1366
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
1367
|
+
),
|
|
1368
|
+
)
|
|
1265
1369
|
.limit(1);
|
|
1266
1370
|
const confirmation: BrowserSaveAttemptConfirmation = {
|
|
1267
1371
|
attemptId: args.browserSaveAttemptId,
|
|
1268
|
-
result: "applied",
|
|
1372
|
+
result: liveCopyBodySave ? "live_copy" : "applied",
|
|
1269
1373
|
revision: documentRevisionToken(
|
|
1270
1374
|
confirmed.bodyRevision,
|
|
1271
1375
|
confirmed.content,
|
|
1272
1376
|
),
|
|
1273
1377
|
updatedAt: confirmed.updatedAt,
|
|
1274
1378
|
softDeletedDatabaseIds,
|
|
1379
|
+
...(liveCopyBodySave ? { bodySave: liveCopyBodySave } : {}),
|
|
1275
1380
|
};
|
|
1276
1381
|
await tx.insert(schema.documentBrowserSaveAttempts).values({
|
|
1277
1382
|
id: randomUUID(),
|
|
@@ -1333,7 +1438,11 @@ const updateDocumentAction = defineAction({
|
|
|
1333
1438
|
});
|
|
1334
1439
|
}
|
|
1335
1440
|
};
|
|
1336
|
-
if (
|
|
1441
|
+
if (
|
|
1442
|
+
authoredBase &&
|
|
1443
|
+
authoredCandidateContent !== undefined &&
|
|
1444
|
+
!liveCopyBodySave
|
|
1445
|
+
) {
|
|
1337
1446
|
const prior = await findDocumentBodyIntent({
|
|
1338
1447
|
db: tx as ReturnType<typeof getDb>,
|
|
1339
1448
|
ownerEmail,
|
|
@@ -1382,7 +1491,11 @@ const updateDocumentAction = defineAction({
|
|
|
1382
1491
|
{ status: "resolved" }
|
|
1383
1492
|
>
|
|
1384
1493
|
| undefined;
|
|
1385
|
-
if (
|
|
1494
|
+
if (
|
|
1495
|
+
authoredBase &&
|
|
1496
|
+
authoredCandidateContent !== undefined &&
|
|
1497
|
+
!liveCopyBodySave
|
|
1498
|
+
) {
|
|
1386
1499
|
const priorIntents = await readDocumentBodyIntents({
|
|
1387
1500
|
db: tx as ReturnType<typeof getDb>,
|
|
1388
1501
|
ownerEmail,
|
|
@@ -1497,7 +1610,10 @@ const updateDocumentAction = defineAction({
|
|
|
1497
1610
|
if (
|
|
1498
1611
|
intentMerge ||
|
|
1499
1612
|
(useBodyRevisionCas && lockedContentChanged) ||
|
|
1500
|
-
|
|
1613
|
+
// Peers' live-copy commits move updatedAt, so on a live-copy
|
|
1614
|
+
// save the snapshot guards only the fields it still writes.
|
|
1615
|
+
(useDocumentCas &&
|
|
1616
|
+
(!liveCopyBodySave || lockedDocumentFieldsChanged))
|
|
1501
1617
|
) {
|
|
1502
1618
|
const rows = await tx
|
|
1503
1619
|
.update(schema.documents)
|
|
@@ -1596,11 +1712,13 @@ const updateDocumentAction = defineAction({
|
|
|
1596
1712
|
);
|
|
1597
1713
|
}
|
|
1598
1714
|
committedContentChanged = lockedContentChanged;
|
|
1599
|
-
measurement.outcome =
|
|
1600
|
-
?
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1715
|
+
measurement.outcome = liveCopyBodySave
|
|
1716
|
+
? "live_copy_body"
|
|
1717
|
+
: lockedDocumentFieldsChanged
|
|
1718
|
+
? intentMerge && bodyBaseStale && lockedContentChanged
|
|
1719
|
+
? "merged"
|
|
1720
|
+
: "written"
|
|
1721
|
+
: "unchanged";
|
|
1604
1722
|
committedContentBefore = historyBefore.content;
|
|
1605
1723
|
|
|
1606
1724
|
if (lockedTitleChanged && args.title !== undefined) {
|
|
@@ -1924,6 +2042,7 @@ const updateDocumentAction = defineAction({
|
|
|
1924
2042
|
browserSaveConfirmation,
|
|
1925
2043
|
),
|
|
1926
2044
|
...(bodyIntentOutcome ? { bodyIntentOutcome } : {}),
|
|
2045
|
+
...(liveCopyBodySave ? { bodySave: liveCopyBodySave } : {}),
|
|
1927
2046
|
...(creativeContext
|
|
1928
2047
|
? {
|
|
1929
2048
|
contextMode: creativeContext.contextMode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineAction } from "@agent-native/core/action";
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -8,6 +8,14 @@ import type {
|
|
|
8
8
|
ContentDatabaseResponse,
|
|
9
9
|
ValidateBuilderSourceExecutionRequest,
|
|
10
10
|
} from "../shared/api.js";
|
|
11
|
+
import {
|
|
12
|
+
builderExecutionPayloadReference,
|
|
13
|
+
cleanupBuilderPrivatePayload,
|
|
14
|
+
enqueueBuilderPrivatePayloadCleanup,
|
|
15
|
+
readBuilderExecutionPayload,
|
|
16
|
+
storeBuilderExecutionPayload,
|
|
17
|
+
} from "./_builder-cms-blob-custody.js";
|
|
18
|
+
import { isBuilderExecutionClaimStale } from "./_builder-cms-execution-lease.js";
|
|
11
19
|
import {
|
|
12
20
|
buildBuilderCmsExecutionPlan,
|
|
13
21
|
builderCmsExecutionIdempotencyKey,
|
|
@@ -19,17 +27,6 @@ import {
|
|
|
19
27
|
} from "./_database-source-utils.js";
|
|
20
28
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
21
29
|
|
|
22
|
-
function parsePayload(value: string) {
|
|
23
|
-
try {
|
|
24
|
-
const parsed = JSON.parse(value) as unknown;
|
|
25
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
26
|
-
? (parsed as Record<string, unknown>)
|
|
27
|
-
: {};
|
|
28
|
-
} catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
30
|
export default defineAction({
|
|
34
31
|
description:
|
|
35
32
|
"Validate a prepared Builder CMS execution gate as a local dry run. This rebuilds and compares the write plan, but never calls Builder APIs.",
|
|
@@ -117,10 +114,45 @@ export default defineAction({
|
|
|
117
114
|
"Prepare the Builder execution gate before validating it.",
|
|
118
115
|
);
|
|
119
116
|
}
|
|
120
|
-
|
|
121
117
|
const now = new Date().toISOString();
|
|
118
|
+
if (execution.state === "succeeded") {
|
|
119
|
+
fail("Builder execution already succeeded and cannot be revalidated.", {
|
|
120
|
+
errorCode: "builder_execution_already_succeeded",
|
|
121
|
+
statusCode: 409,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
execution.state === "response_received" ||
|
|
126
|
+
execution.state === "reconciliation_required"
|
|
127
|
+
) {
|
|
128
|
+
fail("Builder execution requires reconciliation before validation.", {
|
|
129
|
+
errorCode: "builder_execution_reconciliation_required",
|
|
130
|
+
statusCode: 409,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
execution.state === "running" &&
|
|
135
|
+
!isBuilderExecutionClaimStale(execution.updatedAt, now)
|
|
136
|
+
) {
|
|
137
|
+
fail("Builder execution is already running.", {
|
|
138
|
+
errorCode: "builder_execution_running",
|
|
139
|
+
statusCode: 409,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const binding = {
|
|
144
|
+
ownerEmail: execution.ownerEmail,
|
|
145
|
+
sourceId: execution.sourceId,
|
|
146
|
+
changeSetId: execution.changeSetId,
|
|
147
|
+
executionId: execution.id,
|
|
148
|
+
idempotencyKey: execution.idempotencyKey,
|
|
149
|
+
};
|
|
150
|
+
const storedPayload = await readBuilderExecutionPayload({
|
|
151
|
+
payloadJson: execution.payloadJson,
|
|
152
|
+
binding,
|
|
153
|
+
});
|
|
122
154
|
const payload = validateBuilderCmsExecutionDryRun({
|
|
123
|
-
storedPayload
|
|
155
|
+
storedPayload,
|
|
124
156
|
plan,
|
|
125
157
|
now,
|
|
126
158
|
});
|
|
@@ -136,17 +168,94 @@ export default defineAction({
|
|
|
136
168
|
? `${plan.summary} Dry run validated blockers locally.`
|
|
137
169
|
: `${plan.summary} Dry run found a stale execution gate.${stale}`;
|
|
138
170
|
|
|
139
|
-
await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
const payloadJson = await storeBuilderExecutionPayload({
|
|
172
|
+
payload,
|
|
173
|
+
binding,
|
|
174
|
+
});
|
|
175
|
+
const nextReference = builderExecutionPayloadReference(payloadJson);
|
|
176
|
+
const previousReference = builderExecutionPayloadReference(
|
|
177
|
+
execution.payloadJson,
|
|
178
|
+
);
|
|
179
|
+
try {
|
|
180
|
+
const updatedRows = await db.transaction(async (tx) => {
|
|
181
|
+
const updated = await tx
|
|
182
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
183
|
+
.set({
|
|
184
|
+
state: dryRun?.status === "stale" ? "blocked" : plan.state,
|
|
185
|
+
summary,
|
|
186
|
+
payloadJson,
|
|
187
|
+
attemptToken: null,
|
|
188
|
+
lastError:
|
|
189
|
+
dryRun?.status === "stale"
|
|
190
|
+
? dryRun.mismatches[0]
|
|
191
|
+
: plan.lastError,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
})
|
|
194
|
+
.where(
|
|
195
|
+
and(
|
|
196
|
+
eq(schema.contentDatabaseSourceExecutions.id, execution.id),
|
|
197
|
+
eq(
|
|
198
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
199
|
+
execution.payloadJson,
|
|
200
|
+
),
|
|
201
|
+
eq(schema.contentDatabaseSourceExecutions.state, execution.state),
|
|
202
|
+
execution.attemptToken
|
|
203
|
+
? eq(
|
|
204
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
205
|
+
execution.attemptToken,
|
|
206
|
+
)
|
|
207
|
+
: isNull(schema.contentDatabaseSourceExecutions.attemptToken),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
.returning({ id: schema.contentDatabaseSourceExecutions.id });
|
|
211
|
+
if (
|
|
212
|
+
updated[0] &&
|
|
213
|
+
previousReference &&
|
|
214
|
+
previousReference !== nextReference
|
|
215
|
+
) {
|
|
216
|
+
await enqueueBuilderPrivatePayloadCleanup(
|
|
217
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
218
|
+
previousReference,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return updated;
|
|
222
|
+
});
|
|
223
|
+
const [updated] = updatedRows;
|
|
224
|
+
if (!updated)
|
|
225
|
+
fail("Builder execution changed during validation.", {
|
|
226
|
+
errorCode: "builder_execution_changed",
|
|
227
|
+
statusCode: 409,
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
let currentPayloadJson: string | null | undefined;
|
|
231
|
+
try {
|
|
232
|
+
const [current] = await db
|
|
233
|
+
.select({
|
|
234
|
+
payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
|
|
235
|
+
})
|
|
236
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
237
|
+
.where(eq(schema.contentDatabaseSourceExecutions.id, execution.id))
|
|
238
|
+
.limit(1);
|
|
239
|
+
currentPayloadJson = current?.payloadJson;
|
|
240
|
+
} catch {
|
|
241
|
+
// Inconclusive readback retains both refs.
|
|
242
|
+
}
|
|
243
|
+
if (currentPayloadJson !== payloadJson) {
|
|
244
|
+
if (currentPayloadJson !== undefined && nextReference) {
|
|
245
|
+
await cleanupBuilderPrivatePayload(
|
|
246
|
+
nextReference,
|
|
247
|
+
"uncommitted validated execution",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (previousReference && previousReference !== nextReference) {
|
|
254
|
+
await cleanupBuilderPrivatePayload(
|
|
255
|
+
previousReference,
|
|
256
|
+
"superseded validated execution",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
150
259
|
|
|
151
260
|
await db
|
|
152
261
|
.update(schema.contentDatabaseSources)
|
|
@@ -1232,6 +1232,18 @@ function DatabaseTable({
|
|
|
1232
1232
|
if (panel !== "source") sourceHandoffActiveRef.current = false;
|
|
1233
1233
|
setSettingsPanel(panel);
|
|
1234
1234
|
};
|
|
1235
|
+
// Opens Sources at one source's own page, as the GitHub status line's
|
|
1236
|
+
// "From the … folder" does; each request has a new id so a repeat opens it.
|
|
1237
|
+
const [sourceNavRequest, setSourceNavRequest] =
|
|
1238
|
+
useState<SourceNavRequest | null>(null);
|
|
1239
|
+
const openGitHubSource = () => {
|
|
1240
|
+
changeDatabaseSettingsPanel("source");
|
|
1241
|
+
setSourceNavRequest((request) => ({
|
|
1242
|
+
id: (request?.id ?? 0) + 1,
|
|
1243
|
+
stack: [{ kind: "github" }],
|
|
1244
|
+
}));
|
|
1245
|
+
setSettingsOpen(true);
|
|
1246
|
+
};
|
|
1235
1247
|
const [savedViewConfig, setSavedViewConfig] =
|
|
1236
1248
|
useState<ContentDatabaseViewConfig>(defaultDatabaseViewConfig());
|
|
1237
1249
|
const [personalQueryDirty, setPersonalQueryDirty] = useState(false);
|
|
@@ -3532,6 +3544,7 @@ function DatabaseTable({
|
|
|
3532
3544
|
documentId={document.id}
|
|
3533
3545
|
canEdit={canEdit}
|
|
3534
3546
|
pageCount={totalItemCount}
|
|
3547
|
+
onOpenSource={canEditWidgetRows ? undefined : openGitHubSource}
|
|
3535
3548
|
/>
|
|
3536
3549
|
{canEdit &&
|
|
3537
3550
|
databaseId &&
|
|
@@ -4082,6 +4095,7 @@ function DatabaseTable({
|
|
|
4082
4095
|
}
|
|
4083
4096
|
sourcePendingOperations={sourcePendingOperations}
|
|
4084
4097
|
preserveSourceNavigationOnClose={preserveSourceNavigationOnClose}
|
|
4098
|
+
sourceNavRequest={sourceNavRequest}
|
|
4085
4099
|
onViewTypeChange={(type) =>
|
|
4086
4100
|
setViewConfig(updateDatabaseViewType(viewConfig, activeView.id, type))
|
|
4087
4101
|
}
|
|
@@ -7927,6 +7941,9 @@ type SourceNavStep =
|
|
|
7927
7941
|
| { kind: "fieldPicker"; sourceId: string; sourceName: string }
|
|
7928
7942
|
| { kind: "github" };
|
|
7929
7943
|
|
|
7944
|
+
/** A request to open Sources at a given page; a new id repeats it. */
|
|
7945
|
+
type SourceNavRequest = { id: number; stack: SourceNavStep[] };
|
|
7946
|
+
|
|
7930
7947
|
function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
7931
7948
|
const top = stack[stack.length - 1];
|
|
7932
7949
|
if (!top) return dbText("sources");
|
|
@@ -8007,6 +8024,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
8007
8024
|
sourceActionPending,
|
|
8008
8025
|
sourcePendingOperations,
|
|
8009
8026
|
preserveSourceNavigationOnClose,
|
|
8027
|
+
sourceNavRequest,
|
|
8010
8028
|
onViewTypeChange,
|
|
8011
8029
|
onWrapCellsChange,
|
|
8012
8030
|
onOpenPagesInChange,
|
|
@@ -8060,6 +8078,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
8060
8078
|
sourceActionPending: boolean;
|
|
8061
8079
|
sourcePendingOperations: DatabaseSourcePendingOperations;
|
|
8062
8080
|
preserveSourceNavigationOnClose: boolean;
|
|
8081
|
+
sourceNavRequest: SourceNavRequest | null;
|
|
8063
8082
|
onViewTypeChange: (type: ContentDatabaseViewType) => void;
|
|
8064
8083
|
onWrapCellsChange: (wrapCells: boolean) => void;
|
|
8065
8084
|
onOpenPagesInChange: (openPagesIn: ContentDatabaseOpenPagesIn) => void;
|
|
@@ -8113,6 +8132,10 @@ function DatabaseSettingsPanelSheet({
|
|
|
8113
8132
|
setSourceNavStack([]);
|
|
8114
8133
|
}
|
|
8115
8134
|
}, [open, panel, preserveSourceNavigationOnClose]);
|
|
8135
|
+
// Runs after the reset above, so a request that opens the sheet keeps its page.
|
|
8136
|
+
useEffect(() => {
|
|
8137
|
+
if (sourceNavRequest) setSourceNavStack(sourceNavRequest.stack);
|
|
8138
|
+
}, [sourceNavRequest]);
|
|
8116
8139
|
|
|
8117
8140
|
const title =
|
|
8118
8141
|
panel === "main"
|
|
@@ -312,11 +312,14 @@ export function GitHubSourceStatusLine({
|
|
|
312
312
|
documentId,
|
|
313
313
|
canEdit,
|
|
314
314
|
pageCount,
|
|
315
|
+
onOpenSource,
|
|
315
316
|
}: {
|
|
316
317
|
source: ContentDatabaseSource | null;
|
|
317
318
|
documentId: string;
|
|
318
319
|
canEdit: boolean;
|
|
319
320
|
pageCount: number;
|
|
321
|
+
/** Opens the folder's page in Sources; without it, the folder is plain text. */
|
|
322
|
+
onOpenSource?: () => void;
|
|
320
323
|
}) {
|
|
321
324
|
const now = useNow();
|
|
322
325
|
const [unlinkOpen, setUnlinkOpen] = useState(false);
|
|
@@ -334,6 +337,7 @@ export function GitHubSourceStatusLine({
|
|
|
334
337
|
documentId={documentId}
|
|
335
338
|
canEdit={canEdit}
|
|
336
339
|
pageCount={pageCount}
|
|
340
|
+
onOpenSource={onOpenSource}
|
|
337
341
|
detailsOpen={detailsOpen}
|
|
338
342
|
onDetailsOpenChange={setDetailsOpen}
|
|
339
343
|
unlinkOpen={unlinkOpen}
|
|
@@ -349,6 +353,7 @@ function GitHubStatusLineContent({
|
|
|
349
353
|
documentId,
|
|
350
354
|
canEdit,
|
|
351
355
|
pageCount,
|
|
356
|
+
onOpenSource,
|
|
352
357
|
detailsOpen,
|
|
353
358
|
onDetailsOpenChange,
|
|
354
359
|
unlinkOpen,
|
|
@@ -360,6 +365,7 @@ function GitHubStatusLineContent({
|
|
|
360
365
|
documentId: string;
|
|
361
366
|
canEdit: boolean;
|
|
362
367
|
pageCount: number;
|
|
368
|
+
onOpenSource?: () => void;
|
|
363
369
|
detailsOpen: boolean;
|
|
364
370
|
onDetailsOpenChange: (open: boolean) => void;
|
|
365
371
|
unlinkOpen: boolean;
|
|
@@ -387,8 +393,27 @@ function GitHubStatusLineContent({
|
|
|
387
393
|
<div className="-m-px overflow-hidden p-px">
|
|
388
394
|
<div className="-ms-3 flex min-w-0 flex-wrap items-center gap-y-1 text-muted-foreground">
|
|
389
395
|
<Dotted>
|
|
390
|
-
|
|
391
|
-
|
|
396
|
+
{onOpenSource ? (
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
onClick={onOpenSource}
|
|
400
|
+
className="inline-flex min-w-0 items-center rounded-sm text-start underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
401
|
+
>
|
|
402
|
+
<IconBrandGithub
|
|
403
|
+
className="me-1.5 size-3.5 shrink-0"
|
|
404
|
+
aria-hidden
|
|
405
|
+
/>
|
|
406
|
+
<FromFolder status={status} />
|
|
407
|
+
</button>
|
|
408
|
+
) : (
|
|
409
|
+
<>
|
|
410
|
+
<IconBrandGithub
|
|
411
|
+
className="me-1.5 size-3.5 shrink-0"
|
|
412
|
+
aria-hidden
|
|
413
|
+
/>
|
|
414
|
+
<FromFolder status={status} />
|
|
415
|
+
</>
|
|
416
|
+
)}
|
|
392
417
|
</Dotted>
|
|
393
418
|
{stops ? null : (
|
|
394
419
|
<Dotted>
|