@agent-native/core 0.101.7 → 0.101.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +22 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  16. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  17. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  18. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  19. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  20. package/corpus/core/src/server/auth.ts +25 -1
  21. package/corpus/core/src/server/builder-browser.ts +353 -1
  22. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  23. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  24. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  27. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  28. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  29. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  30. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  31. package/corpus/templates/content/AGENTS.md +3 -2
  32. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  33. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  34. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  35. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  36. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  37. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  38. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  39. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  40. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  41. package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
  42. package/corpus/templates/content/actions/_database-utils.ts +89 -12
  43. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  44. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  45. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  46. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  47. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  48. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  49. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  50. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  51. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  52. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  53. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  54. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
  55. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  56. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  57. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
  58. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  59. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  60. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  61. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  62. package/corpus/templates/content/app/global.css +18 -1
  63. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  64. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  65. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
  66. package/corpus/templates/content/app/i18n-data.ts +287 -0
  67. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  68. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  69. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  70. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  71. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  72. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  73. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  74. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  75. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  76. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  77. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  78. package/corpus/templates/content/parity/matrix.md +27 -25
  79. package/corpus/templates/content/parity/matrix.ts +50 -2
  80. package/corpus/templates/content/server/db/schema.ts +47 -0
  81. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  82. package/corpus/templates/content/server/plugins/db.ts +53 -0
  83. package/corpus/templates/content/shared/api.ts +70 -1
  84. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  85. package/corpus/templates/content/shared/nfm.ts +3 -3
  86. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  87. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  88. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  89. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  90. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  91. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  92. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  93. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  94. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  95. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  96. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  97. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  98. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  99. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  100. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  101. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  102. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  103. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  104. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  105. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  106. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  107. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  108. package/dist/client/review/ReviewCommentComposer.js +3 -3
  109. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  110. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  111. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  112. package/dist/client/review/ReviewThreadPanel.js +6 -4
  113. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  115. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  116. package/dist/client/settings/SettingsPanel.js +11 -4
  117. package/dist/client/settings/SettingsPanel.js.map +1 -1
  118. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  119. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  120. package/dist/client/settings/useBuilderStatus.js +19 -6
  121. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  122. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  123. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  124. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  125. package/dist/collab/awareness.d.ts +2 -2
  126. package/dist/collab/awareness.d.ts.map +1 -1
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/resources/handlers.d.ts +1 -1
  129. package/dist/secrets/routes.d.ts +9 -9
  130. package/dist/server/auth.d.ts +1 -0
  131. package/dist/server/auth.d.ts.map +1 -1
  132. package/dist/server/auth.js +19 -2
  133. package/dist/server/auth.js.map +1 -1
  134. package/dist/server/builder-browser.d.ts +78 -0
  135. package/dist/server/builder-browser.d.ts.map +1 -1
  136. package/dist/server/builder-browser.js +246 -1
  137. package/dist/server/builder-browser.js.map +1 -1
  138. package/dist/server/core-routes-plugin.d.ts +41 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +260 -14
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/docs/content/deployment.mdx +9 -7
  143. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  144. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  145. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  146. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  147. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  148. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  149. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  150. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  151. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  152. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  153. package/package.json +2 -2
@@ -1,5 +1,15 @@
1
1
  import { useT } from "@agent-native/core/client";
2
- import { BUILDER_CMS_SAFE_WRITE_MODEL } from "@shared/api";
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogAction,
5
+ AlertDialogCancel,
6
+ AlertDialogContent,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
11
+ } from "@agent-native/toolkit/ui/alert-dialog";
12
+ import { Checkbox } from "@agent-native/toolkit/ui/checkbox";
3
13
  import type {
4
14
  BuilderCmsPublicationTransitionIntent,
5
15
  BuilderCmsWriteEffect,
@@ -15,7 +25,7 @@ import {
15
25
  IconCloudUpload,
16
26
  IconX,
17
27
  } from "@tabler/icons-react";
18
- import { useEffect, useMemo, useState } from "react";
28
+ import { useEffect, useMemo, useRef, useState } from "react";
19
29
 
20
30
  import { Button } from "@/components/ui/button";
21
31
  import {
@@ -41,6 +51,11 @@ export type BuilderReviewPublicationTransitions = Record<
41
51
  ExecuteBuilderSourceBatchTransition
42
52
  >;
43
53
 
54
+ export type BuilderReviewSelection = {
55
+ changeSetIds: string[];
56
+ transitions: BuilderReviewPublicationTransitions;
57
+ };
58
+
44
59
  const EFFECT_LABELS: Record<
45
60
  BuilderCmsWriteEffect,
46
61
  { tag: string; sentence: string }
@@ -152,6 +167,9 @@ export function builderReviewResultStatus(status?: string): {
152
167
  return { labelKey: "needsAttention", tone: "warn" };
153
168
  case "failed":
154
169
  return { labelKey: "failedYouCanRetry", tone: "danger" };
170
+ case "response_received":
171
+ case "reconciliation_required":
172
+ return { labelKey: "reconciliationRequired", tone: "warn" };
155
173
  case "stale":
156
174
  return { labelKey: "needsAFreshReview", tone: "warn" };
157
175
  case "running":
@@ -195,6 +213,59 @@ export function builderReviewPublicationTransitionsMap(
195
213
  return transitions;
196
214
  }
197
215
 
216
+ export function builderReviewScopedTransitionSelections(
217
+ changeSetIds: string[],
218
+ selections: BuilderReviewPublicationTransitionSelections,
219
+ ) {
220
+ const selectedIds = new Set(changeSetIds);
221
+ return Object.fromEntries(
222
+ Object.entries(selections).filter(([changeSetId]) =>
223
+ selectedIds.has(changeSetId),
224
+ ),
225
+ );
226
+ }
227
+
228
+ export function builderReviewHasUnconfirmedUnpublish(
229
+ changeSetIds: string[],
230
+ selections: BuilderReviewPublicationTransitionSelections,
231
+ ) {
232
+ return changeSetIds.some((changeSetId) => {
233
+ const selection = selections[changeSetId];
234
+ return (
235
+ selection?.publicationTransition === "unpublish" &&
236
+ selection.confirmUnpublish !== true
237
+ );
238
+ });
239
+ }
240
+
241
+ export function builderReviewRowCanCancelPrepared(
242
+ row: ContentDatabaseSourceReviewPayload["rows"][number],
243
+ ) {
244
+ const execution = row.execution;
245
+ if (!execution) return false;
246
+ if (
247
+ execution.state !== "ready" &&
248
+ execution.state !== "write_disabled" &&
249
+ execution.state !== "blocked"
250
+ ) {
251
+ return false;
252
+ }
253
+ if (
254
+ Object.prototype.hasOwnProperty.call(execution.payload, "response") ||
255
+ Object.prototype.hasOwnProperty.call(execution.payload, "dispatch")
256
+ ) {
257
+ return false;
258
+ }
259
+ if (execution.state !== "blocked") return true;
260
+ const dryRun = execution.payload.dryRun;
261
+ return (
262
+ !!dryRun &&
263
+ typeof dryRun === "object" &&
264
+ !Array.isArray(dryRun) &&
265
+ (dryRun as Record<string, unknown>).status === "blocked"
266
+ );
267
+ }
268
+
198
269
  const ATTENTION_TAG_EFFECTS: ReadonlySet<BuilderCmsWriteEffect> = new Set([
199
270
  "unpublish",
200
271
  ]);
@@ -221,9 +292,15 @@ export function BuilderSourceReviewDialog({
221
292
  canEdit,
222
293
  pending,
223
294
  batchResult = null,
295
+ error = null,
224
296
  checkedAt,
297
+ preparedForExecution = false,
298
+ autoSelectReviewRows = false,
299
+ selectionChangeSetIdMap = null,
225
300
  onClose,
226
301
  onValidate,
302
+ onCancelPrepared,
303
+ onSelectionChange,
227
304
  }: {
228
305
  open: boolean;
229
306
  review: ContentDatabaseSourceReviewPayload | null;
@@ -233,18 +310,20 @@ export function BuilderSourceReviewDialog({
233
310
  // Optional so the inline-database caller (DatabaseView) that doesn't surface
234
311
  // batch results can still mount the dialog.
235
312
  batchResult?: ExecuteBuilderSourceBatchResponse | null;
313
+ error?: string | null;
236
314
  checkedAt: string | null;
315
+ preparedForExecution?: boolean;
316
+ autoSelectReviewRows?: boolean;
317
+ selectionChangeSetIdMap?: Record<string, string> | null;
237
318
  onClose: () => void;
238
- onValidate: (transitions: BuilderReviewPublicationTransitions) => void;
319
+ onValidate: (selection: BuilderReviewSelection) => void;
320
+ onCancelPrepared?: (changeSetId: string) => void;
321
+ onSelectionChange?: () => void;
239
322
  }) {
240
323
  const t = useT();
241
324
  const checked = !!checkedAt;
242
- const safeModel =
243
- source?.sourceType === "builder-cms" &&
244
- source.sourceTable === BUILDER_CMS_SAFE_WRITE_MODEL;
245
325
  const writeMode = source?.metadata.writeMode;
246
326
  const allowPublicationTransitionControls =
247
- safeModel &&
248
327
  writeMode === "publish_updates" &&
249
328
  source?.metadata.allowPublicationTransitions === true;
250
329
  const reviewRows = useMemo(() => review?.rows ?? [], [review]);
@@ -264,8 +343,43 @@ export function BuilderSourceReviewDialog({
264
343
  [reviewRows],
265
344
  );
266
345
  const reviewRowIdsKey = reviewRowIds.join("\u0000");
346
+ const [selectedChangeSetIds, setSelectedChangeSetIds] = useState<string[]>(
347
+ [],
348
+ );
267
349
  const [transitionSelections, setTransitionSelections] =
268
350
  useState<BuilderReviewPublicationTransitionSelections>({});
351
+ const [cancelChangeSetId, setCancelChangeSetId] = useState<string | null>(
352
+ null,
353
+ );
354
+ const wasOpenRef = useRef(false);
355
+ useEffect(() => {
356
+ const wasOpen = wasOpenRef.current;
357
+ wasOpenRef.current = open;
358
+
359
+ if (!open || (!wasOpen && !autoSelectReviewRows)) {
360
+ setSelectedChangeSetIds([]);
361
+ return;
362
+ }
363
+
364
+ const reviewRowIdSet = new Set(
365
+ reviewRowIdsKey ? reviewRowIdsKey.split("\u0000") : [],
366
+ );
367
+ setSelectedChangeSetIds((current) => {
368
+ const next = current
369
+ .map(
370
+ (changeSetId) =>
371
+ selectionChangeSetIdMap?.[changeSetId] ?? changeSetId,
372
+ )
373
+ .filter((changeSetId) => reviewRowIdSet.has(changeSetId));
374
+ if (autoSelectReviewRows && next.length === 0) {
375
+ return [...reviewRowIdSet];
376
+ }
377
+ return next.length === current.length &&
378
+ next.every((changeSetId, index) => changeSetId === current[index])
379
+ ? current
380
+ : next;
381
+ });
382
+ }, [autoSelectReviewRows, open, reviewRowIdsKey, selectionChangeSetIdMap]);
269
383
  useEffect(() => {
270
384
  if (!open || !allowPublicationTransitionControls) {
271
385
  setTransitionSelections({});
@@ -278,75 +392,114 @@ export function BuilderSourceReviewDialog({
278
392
  setTransitionSelections((current) => {
279
393
  const next: BuilderReviewPublicationTransitionSelections = {};
280
394
  for (const [changeSetId, selection] of Object.entries(current)) {
281
- if (reviewRowIdSet.has(changeSetId)) next[changeSetId] = selection;
395
+ const mappedChangeSetId =
396
+ selectionChangeSetIdMap?.[changeSetId] ?? changeSetId;
397
+ if (reviewRowIdSet.has(mappedChangeSetId)) {
398
+ next[mappedChangeSetId] = selection;
399
+ }
282
400
  }
283
- return Object.keys(next).length === Object.keys(current).length
401
+ return Object.keys(next).length === Object.keys(current).length &&
402
+ Object.entries(next).every(
403
+ ([changeSetId, selection]) => current[changeSetId] === selection,
404
+ )
284
405
  ? current
285
406
  : next;
286
407
  });
287
- }, [allowPublicationTransitionControls, open, reviewRowIdsKey]);
408
+ }, [
409
+ allowPublicationTransitionControls,
410
+ open,
411
+ reviewRowIdsKey,
412
+ selectionChangeSetIdMap,
413
+ ]);
414
+ const selectedChangeSetIdSet = useMemo(
415
+ () => new Set(selectedChangeSetIds),
416
+ [selectedChangeSetIds],
417
+ );
418
+ const selectedReviewRows = useMemo(
419
+ () =>
420
+ reviewRows.filter((row) => selectedChangeSetIdSet.has(row.changeSetId)),
421
+ [reviewRows, selectedChangeSetIdSet],
422
+ );
423
+ const scopedTransitionSelections = useMemo(
424
+ () =>
425
+ builderReviewScopedTransitionSelections(
426
+ selectedChangeSetIds,
427
+ transitionSelections,
428
+ ),
429
+ [selectedChangeSetIds, transitionSelections],
430
+ );
288
431
  const transitionMap = useMemo(
289
- () => builderReviewPublicationTransitionsMap(transitionSelections),
290
- [transitionSelections],
432
+ () => builderReviewPublicationTransitionsMap(scopedTransitionSelections),
433
+ [scopedTransitionSelections],
291
434
  );
292
435
  const intentSummary = builderReviewIntentSummary(
293
- reviewRows,
294
- transitionSelections,
436
+ selectedReviewRows,
437
+ scopedTransitionSelections,
295
438
  );
296
439
  const destinationLine = builderReviewDestinationLine({
297
- rows: reviewRows,
298
- selections: transitionSelections,
440
+ rows: selectedReviewRows,
441
+ selections: scopedTransitionSelections,
299
442
  liveWritesEnabled: review?.liveWritesEnabled === true,
300
443
  });
301
- const hasUnconfirmedUnpublish = Object.values(transitionSelections).some(
302
- (selection) =>
303
- selection.publicationTransition === "unpublish" &&
304
- selection.confirmUnpublish !== true,
444
+ const hasUnconfirmedUnpublish = builderReviewHasUnconfirmedUnpublish(
445
+ selectedChangeSetIds,
446
+ scopedTransitionSelections,
447
+ );
448
+ const selectedBatchResults =
449
+ batchResult?.results.filter((result) =>
450
+ selectedChangeSetIdSet.has(result.changeSetId),
451
+ ) ?? [];
452
+ const batchHasIssues = selectedBatchResults.some(
453
+ (result) => result.status === "blocked" || result.status === "failed",
454
+ );
455
+ const batchNeedsReconciliation = selectedBatchResults.some(
456
+ (result) => result.status === "reconciliation_required",
305
457
  );
306
- const batchHasIssues =
307
- !!batchResult &&
308
- (batchResult.summary.blocked > 0 || batchResult.summary.failed > 0);
309
458
  const retryable =
310
- review?.result.status === "failed" ||
311
- review?.result.status === "blocked" ||
312
- review?.result.status === "stale" ||
313
- batchHasIssues;
314
- const unsafeLiveTarget = review?.liveWritesEnabled === true && !safeModel;
459
+ !batchNeedsReconciliation &&
460
+ review?.result.status !== "reconciliation_required" &&
461
+ (review?.result.status === "failed" ||
462
+ review?.result.status === "blocked" ||
463
+ review?.result.status === "stale" ||
464
+ batchHasIssues);
315
465
  const disabled =
316
466
  !canEdit ||
317
467
  pending ||
318
468
  (!retryable && checked) ||
319
469
  !review ||
320
- review.rows.length === 0 ||
321
- unsafeLiveTarget ||
470
+ selectedReviewRows.length === 0 ||
322
471
  hasUnconfirmedUnpublish;
323
472
  const rowTitleById = new Map(
324
473
  reviewRows.map((row) => [row.changeSetId, row.title]),
325
474
  );
326
- const batchIssueResults =
327
- batchResult?.results.filter((result) => result.status !== "succeeded") ??
328
- [];
475
+ const batchIssueResults = selectedBatchResults.filter(
476
+ (result) => result.status !== "succeeded",
477
+ );
329
478
  const resultStatus = builderReviewResultStatus(
330
479
  batchResult
331
- ? batchHasIssues
332
- ? "partial"
333
- : "succeeded"
480
+ ? batchNeedsReconciliation
481
+ ? "reconciliation_required"
482
+ : batchHasIssues
483
+ ? "partial"
484
+ : "succeeded"
334
485
  : review?.result.status,
335
486
  );
336
487
  const footerHint = pending
337
- ? review?.liveWritesEnabled
488
+ ? preparedForExecution
338
489
  ? "Sending to Builder…"
339
- : "Checking…"
490
+ : "Preparing full review…"
340
491
  : hasUnconfirmedUnpublish
341
492
  ? "Confirm unpublish on the selected rows first."
342
- : unsafeLiveTarget
343
- ? `Live pushes are limited to the ${BUILDER_CMS_SAFE_WRITE_MODEL} test model.`
344
- : null;
493
+ : preparedForExecution
494
+ ? "Review the full payload above, then confirm this Builder write."
495
+ : review?.liveWritesEnabled
496
+ ? "Prepare the full Builder payload before anything is sent."
497
+ : null;
345
498
  const effectiveEffects = new Set(
346
- reviewRows.map((row) =>
499
+ selectedReviewRows.map((row) =>
347
500
  builderReviewEffectiveRowEffect(
348
501
  row.effect,
349
- transitionSelections[row.changeSetId],
502
+ scopedTransitionSelections[row.changeSetId],
350
503
  ),
351
504
  ),
352
505
  );
@@ -357,30 +510,41 @@ export function BuilderSourceReviewDialog({
357
510
  : effectiveEffects.has("publish")
358
511
  ? "Publish"
359
512
  : effectiveEffects.size === 1 && effectiveEffects.has("create_draft")
360
- ? reviewRows.length > 1
361
- ? `Create ${reviewRows.length} drafts`
513
+ ? selectedReviewRows.length > 1
514
+ ? `Create ${selectedReviewRows.length} drafts`
362
515
  : "Create draft"
363
- : reviewRows.length > 1
364
- ? `Push ${reviewRows.length} updates`
516
+ : selectedReviewRows.length > 1
517
+ ? `Push ${selectedReviewRows.length} updates`
365
518
  : "Push update";
366
519
  const buttonLabel = pending
367
- ? review?.liveWritesEnabled
368
- ? "Working…"
369
- : "Checking…"
370
- : checked && batchResult
371
- ? batchHasIssues
372
- ? "Retry"
373
- : "Pushed"
374
- : checked && review?.result.status === "succeeded"
375
- ? "Pushed"
376
- : checked && !retryable
377
- ? "Checked"
378
- : pushVerb;
520
+ ? preparedForExecution
521
+ ? "Sending…"
522
+ : "Preparing…"
523
+ : checked && review?.result.status === "running"
524
+ ? t("database.working")
525
+ : checked && review?.result.status === "reconciliation_required"
526
+ ? t("database.reconciliationRequired")
527
+ : checked && batchResult
528
+ ? batchNeedsReconciliation
529
+ ? t("database.reconciliationRequired")
530
+ : batchHasIssues
531
+ ? "Retry"
532
+ : "Pushed"
533
+ : checked && review?.result.status === "succeeded"
534
+ ? "Pushed"
535
+ : checked && !retryable
536
+ ? "Checked"
537
+ : preparedForExecution
538
+ ? pushVerb
539
+ : review?.liveWritesEnabled
540
+ ? "Review details"
541
+ : pushVerb;
379
542
 
380
543
  function setRowPublicationTransition(
381
544
  changeSetId: string,
382
545
  publicationTransition: BuilderCmsPublicationTransitionIntent,
383
546
  ) {
547
+ onSelectionChange?.();
384
548
  setTransitionSelections((current) => {
385
549
  const currentSelection = current[changeSetId];
386
550
  const next = { ...current };
@@ -399,7 +563,28 @@ export function BuilderSourceReviewDialog({
399
563
  });
400
564
  }
401
565
 
566
+ function setRowSelected(changeSetId: string, selected: boolean) {
567
+ onSelectionChange?.();
568
+ setSelectedChangeSetIds((current) => {
569
+ if (selected) {
570
+ return current.includes(changeSetId)
571
+ ? current
572
+ : [...current, changeSetId];
573
+ }
574
+ return current.filter((id) => id !== changeSetId);
575
+ });
576
+ if (!selected) {
577
+ setTransitionSelections((current) => {
578
+ if (!current[changeSetId]) return current;
579
+ const next = { ...current };
580
+ delete next[changeSetId];
581
+ return next;
582
+ });
583
+ }
584
+ }
585
+
402
586
  function setRowConfirmUnpublish(changeSetId: string, confirmed: boolean) {
587
+ onSelectionChange?.();
403
588
  setTransitionSelections((current) => {
404
589
  const currentSelection = current[changeSetId];
405
590
  if (currentSelection?.publicationTransition !== "unpublish") {
@@ -435,7 +620,7 @@ export function BuilderSourceReviewDialog({
435
620
  {t("database.reviewBuilderUpdate")}
436
621
  </DialogTitle>
437
622
  <DialogDescription className="truncate text-xs text-muted-foreground">
438
- {review?.summary ?? "No pending Builder changes."}
623
+ {pending && !review ? "Loading complete diff…" : intentSummary}
439
624
  </DialogDescription>
440
625
  </div>
441
626
  <button
@@ -465,7 +650,11 @@ export function BuilderSourceReviewDialog({
465
650
  </div>
466
651
  ) : null}
467
652
  {visibleReviewRows.map((row) => {
468
- const selection = transitionSelections[row.changeSetId];
653
+ const selected = selectedChangeSetIdSet.has(
654
+ row.changeSetId,
655
+ );
656
+ const selection =
657
+ scopedTransitionSelections[row.changeSetId];
469
658
  const effect = builderReviewEffectiveRowEffect(
470
659
  row.effect,
471
660
  selection,
@@ -480,18 +669,32 @@ export function BuilderSourceReviewDialog({
480
669
  key={row.changeSetId}
481
670
  className="rounded-md border border-border p-3"
482
671
  >
483
- <div className="flex min-w-0 items-start justify-between gap-3">
672
+ <div className="flex min-w-0 items-start gap-3">
673
+ <Checkbox
674
+ className="mt-0.5"
675
+ checked={selected}
676
+ disabled={pending}
677
+ aria-label={row.title}
678
+ onCheckedChange={(checked) =>
679
+ setRowSelected(row.changeSetId, checked === true)
680
+ }
681
+ />
484
682
  <div className="min-w-0">
485
683
  <div className="truncate text-sm font-medium">
486
684
  {row.title}
487
685
  </div>
686
+ {row.targetEntryId ? (
687
+ <div className="mt-0.5 break-all font-mono text-[11px] text-muted-foreground">
688
+ Builder entry {row.targetEntryId}
689
+ </div>
690
+ ) : null}
488
691
  <div className="mt-1 text-xs text-muted-foreground">
489
692
  {sentence}
490
693
  </div>
491
694
  </div>
492
695
  <span
493
696
  className={
494
- "shrink-0 text-[11px] " +
697
+ "ms-auto shrink-0 text-[11px] " +
495
698
  rowEffectTagClass(effect)
496
699
  }
497
700
  >
@@ -500,6 +703,7 @@ export function BuilderSourceReviewDialog({
500
703
  </div>
501
704
  <div className="mt-3 grid gap-2">
502
705
  {allowPublicationTransitionControls &&
706
+ selected &&
503
707
  row.effect !== "create_draft" ? (
504
708
  <div className="flex flex-wrap items-center gap-1.5 text-xs">
505
709
  <Button
@@ -553,18 +757,17 @@ export function BuilderSourceReviewDialog({
553
757
  {transitionSelections[row.changeSetId]
554
758
  ?.publicationTransition === "unpublish" ? (
555
759
  <label className="flex items-center gap-1 rounded border border-destructive/30 bg-destructive/10 px-1.5 py-1 text-[11px] text-destructive">
556
- <input
557
- type="checkbox"
558
- className="size-3 accent-destructive"
760
+ <Checkbox
761
+ aria-label={t("database.confirmUnpublish")}
559
762
  checked={
560
763
  transitionSelections[row.changeSetId]
561
764
  ?.confirmUnpublish === true
562
765
  }
563
766
  disabled={pending}
564
- onChange={(event) =>
767
+ onCheckedChange={(checked) =>
565
768
  setRowConfirmUnpublish(
566
769
  row.changeSetId,
567
- event.currentTarget.checked,
770
+ checked === true,
568
771
  )
569
772
  }
570
773
  />
@@ -624,6 +827,22 @@ export function BuilderSourceReviewDialog({
624
827
  {row.execution.lastError}
625
828
  </div>
626
829
  ) : null}
830
+ {canEdit &&
831
+ onCancelPrepared &&
832
+ builderReviewRowCanCancelPrepared(row) ? (
833
+ <Button
834
+ type="button"
835
+ size="sm"
836
+ variant="ghost"
837
+ className="h-7 justify-self-start px-2 text-xs text-destructive hover:text-destructive"
838
+ disabled={pending}
839
+ onClick={() =>
840
+ setCancelChangeSetId(row.changeSetId)
841
+ }
842
+ >
843
+ {t("database.cancelPreparedUpdate")}
844
+ </Button>
845
+ ) : null}
627
846
  </div>
628
847
  </div>
629
848
  );
@@ -650,10 +869,12 @@ export function BuilderSourceReviewDialog({
650
869
  </div>
651
870
  </section>
652
871
 
653
- <div className="flex items-start gap-1.5 text-xs text-muted-foreground">
654
- <IconCloudUpload className="mt-0.5 size-3.5 shrink-0" />
655
- <span>{destinationLine}</span>
656
- </div>
872
+ {selectedReviewRows.length > 0 ? (
873
+ <div className="flex items-start gap-1.5 text-xs text-muted-foreground">
874
+ <IconCloudUpload className="mt-0.5 size-3.5 shrink-0" />
875
+ <span>{destinationLine}</span>
876
+ </div>
877
+ ) : null}
657
878
 
658
879
  {batchResult && batchIssueResults.length > 0 ? (
659
880
  <section className="grid gap-1.5 rounded-md border border-amber-300 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
@@ -672,6 +893,28 @@ export function BuilderSourceReviewDialog({
672
893
  ))}
673
894
  </section>
674
895
  ) : null}
896
+ {error ? (
897
+ <div
898
+ role="alert"
899
+ aria-live="assertive"
900
+ className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive"
901
+ >
902
+ {error}
903
+ </div>
904
+ ) : null}
905
+ </div>
906
+ ) : pending ? (
907
+ <div className="flex items-center gap-2 rounded-md border border-border p-4 text-sm text-muted-foreground">
908
+ <Spinner className="size-4" />
909
+ {t("database.loadingCompleteBuilderDiff")}
910
+ </div>
911
+ ) : error ? (
912
+ <div
913
+ role="alert"
914
+ aria-live="assertive"
915
+ className="rounded-md border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive"
916
+ >
917
+ {error}
675
918
  </div>
676
919
  ) : (
677
920
  <div className="rounded-md border border-border p-4 text-sm text-muted-foreground">
@@ -689,7 +932,7 @@ export function BuilderSourceReviewDialog({
689
932
  resultToneClass(resultStatus.tone)
690
933
  }
691
934
  >
692
- {checked ? (
935
+ {checked || preparedForExecution ? (
693
936
  resultStatus.tone === "ok" ? (
694
937
  <IconCheck className="size-3.5 shrink-0" />
695
938
  ) : (
@@ -697,7 +940,7 @@ export function BuilderSourceReviewDialog({
697
940
  )
698
941
  ) : null}
699
942
  <span>
700
- {checked
943
+ {checked || preparedForExecution
701
944
  ? `${t(`database.${resultStatus.labelKey}`)} · ${intentSummary}`
702
945
  : intentSummary}
703
946
  </span>
@@ -713,7 +956,12 @@ export function BuilderSourceReviewDialog({
713
956
  type="button"
714
957
  size="sm"
715
958
  disabled={disabled}
716
- onClick={() => onValidate(transitionMap)}
959
+ onClick={() =>
960
+ onValidate({
961
+ changeSetIds: selectedChangeSetIds,
962
+ transitions: transitionMap,
963
+ })
964
+ }
717
965
  >
718
966
  {pending ? (
719
967
  <Spinner className="mr-1.5 size-3.5" />
@@ -725,6 +973,41 @@ export function BuilderSourceReviewDialog({
725
973
  </div>
726
974
  </div>
727
975
  </DialogContent>
976
+ <AlertDialog
977
+ open={cancelChangeSetId !== null}
978
+ onOpenChange={(nextOpen) => {
979
+ if (!nextOpen && !pending) setCancelChangeSetId(null);
980
+ }}
981
+ >
982
+ <AlertDialogContent>
983
+ <AlertDialogHeader>
984
+ <AlertDialogTitle>
985
+ {t("database.cancelPreparedUpdateQuestion")}
986
+ </AlertDialogTitle>
987
+ <AlertDialogDescription>
988
+ {t("database.cancelPreparedUpdateDescription")}
989
+ </AlertDialogDescription>
990
+ </AlertDialogHeader>
991
+ <AlertDialogFooter>
992
+ <AlertDialogCancel disabled={pending}>
993
+ {t("database.keepPreparedUpdate")}
994
+ </AlertDialogCancel>
995
+ <AlertDialogAction
996
+ disabled={pending}
997
+ onClick={(event) => {
998
+ event.preventDefault();
999
+ if (!cancelChangeSetId) return;
1000
+ onCancelPrepared?.(cancelChangeSetId);
1001
+ setCancelChangeSetId(null);
1002
+ }}
1003
+ >
1004
+ {pending
1005
+ ? t("database.cancellingPreparedUpdate")
1006
+ : t("database.cancelPreparedUpdate")}
1007
+ </AlertDialogAction>
1008
+ </AlertDialogFooter>
1009
+ </AlertDialogContent>
1010
+ </AlertDialog>
728
1011
  </Dialog>
729
1012
  );
730
1013
  }