@agent-native/core 0.82.0 → 0.83.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/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -261,6 +261,8 @@ export interface CommandMenuProps {
|
|
|
261
261
|
open: boolean;
|
|
262
262
|
onOpenChange: (open: boolean) => void;
|
|
263
263
|
children: ReactNode;
|
|
264
|
+
/** Render app-specific dynamic results from the current search value. */
|
|
265
|
+
renderResults?: (search: string) => ReactNode;
|
|
264
266
|
/** Placeholder text for the search input */
|
|
265
267
|
placeholder?: string;
|
|
266
268
|
/** Text shown when no results match (before showing agent fallback) */
|
|
@@ -291,6 +293,7 @@ export function CommandMenu({
|
|
|
291
293
|
open,
|
|
292
294
|
onOpenChange,
|
|
293
295
|
children,
|
|
296
|
+
renderResults,
|
|
294
297
|
placeholder = "Type a command or ask AI...",
|
|
295
298
|
emptyText: _emptyText = "No commands found.",
|
|
296
299
|
showAgentFallback = true,
|
|
@@ -325,8 +328,15 @@ export function CommandMenu({
|
|
|
325
328
|
setTimeout(() => setChangelogOpen(true), 50);
|
|
326
329
|
}, [onOpenChange, markChangelogSeen]);
|
|
327
330
|
|
|
328
|
-
// Focus input when opening
|
|
331
|
+
// Focus input when opening; clear search while closed so reopen never renders
|
|
332
|
+
// dynamic results for the previous query.
|
|
329
333
|
useEffect(() => {
|
|
334
|
+
if (!open) {
|
|
335
|
+
setSearch("");
|
|
336
|
+
setSelectedIndex(0);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
330
340
|
if (open) {
|
|
331
341
|
setSearch("");
|
|
332
342
|
setSelectedIndex(0);
|
|
@@ -497,6 +507,8 @@ export function CommandMenu({
|
|
|
497
507
|
React.isValidElement(child) &&
|
|
498
508
|
(child.type === CommandGroup || child.type === CommandDocsGroup),
|
|
499
509
|
);
|
|
510
|
+
const dynamicResults = open ? renderResults?.(search) : null;
|
|
511
|
+
const hasDynamicResults = Boolean(dynamicResults);
|
|
500
512
|
|
|
501
513
|
return (
|
|
502
514
|
<>
|
|
@@ -528,6 +540,7 @@ export function CommandMenu({
|
|
|
528
540
|
|
|
529
541
|
{/* Command list */}
|
|
530
542
|
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden">
|
|
543
|
+
{dynamicResults}
|
|
531
544
|
{hasResults && filteredChildren}
|
|
532
545
|
|
|
533
546
|
{/* What's new — built-in changelog entry */}
|
|
@@ -567,7 +580,9 @@ export function CommandMenu({
|
|
|
567
580
|
{/* Ask AI — always visible at the bottom */}
|
|
568
581
|
{showAgentFallback && (
|
|
569
582
|
<>
|
|
570
|
-
{(hasResults || showChangelogRow) &&
|
|
583
|
+
{(hasResults || showChangelogRow || hasDynamicResults) && (
|
|
584
|
+
<CommandSeparator />
|
|
585
|
+
)}
|
|
571
586
|
<div className="p-1">
|
|
572
587
|
<div
|
|
573
588
|
className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-2 text-sm outline-none"
|
|
@@ -658,16 +673,21 @@ CommandMenu.Separator = CommandSeparator;
|
|
|
658
673
|
/**
|
|
659
674
|
* Hook to handle Cmd+K (or Ctrl+K) to open the command menu
|
|
660
675
|
*/
|
|
661
|
-
export function useCommandMenuShortcut(
|
|
676
|
+
export function useCommandMenuShortcut(
|
|
677
|
+
onOpen: () => void,
|
|
678
|
+
options: { allowContentEditable?: boolean } = {},
|
|
679
|
+
) {
|
|
662
680
|
useEffect(() => {
|
|
663
681
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
664
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
665
|
-
// Don't trigger if user is typing in
|
|
666
|
-
const target = e.target
|
|
682
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
683
|
+
// Don't trigger if user is typing in a native form control.
|
|
684
|
+
const target = e.target instanceof HTMLElement ? e.target : null;
|
|
685
|
+
const isContentEditable = target?.isContentEditable;
|
|
667
686
|
if (
|
|
668
|
-
target
|
|
669
|
-
target
|
|
670
|
-
target
|
|
687
|
+
target?.tagName === "INPUT" ||
|
|
688
|
+
target?.tagName === "TEXTAREA" ||
|
|
689
|
+
target?.tagName === "SELECT" ||
|
|
690
|
+
(!options.allowContentEditable && isContentEditable)
|
|
671
691
|
) {
|
|
672
692
|
return;
|
|
673
693
|
}
|
|
@@ -675,9 +695,11 @@ export function useCommandMenuShortcut(onOpen: () => void) {
|
|
|
675
695
|
onOpen();
|
|
676
696
|
}
|
|
677
697
|
};
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
698
|
+
const useCapture = Boolean(options.allowContentEditable);
|
|
699
|
+
document.addEventListener("keydown", handleKeyDown, useCapture);
|
|
700
|
+
return () =>
|
|
701
|
+
document.removeEventListener("keydown", handleKeyDown, useCapture);
|
|
702
|
+
}, [onOpen, options.allowContentEditable]);
|
|
681
703
|
}
|
|
682
704
|
|
|
683
705
|
export type {
|
|
@@ -264,6 +264,12 @@ function hasStructuredFilters(filters: ReturnType<typeof buildFilterSummary>) {
|
|
|
264
264
|
function buildGuidance(options: {
|
|
265
265
|
query: string | undefined;
|
|
266
266
|
structuredFilters: boolean;
|
|
267
|
+
truncated: boolean;
|
|
268
|
+
hasMore: boolean;
|
|
269
|
+
total: number;
|
|
270
|
+
returned: number;
|
|
271
|
+
offset: number;
|
|
272
|
+
limit: number;
|
|
267
273
|
}) {
|
|
268
274
|
const guidance: string[] = [];
|
|
269
275
|
|
|
@@ -285,6 +291,15 @@ function buildGuidance(options: {
|
|
|
285
291
|
);
|
|
286
292
|
}
|
|
287
293
|
|
|
294
|
+
if (options.truncated) {
|
|
295
|
+
const more = options.hasMore
|
|
296
|
+
? `Fetch the next page with offset ${options.offset + options.returned}.`
|
|
297
|
+
: "This is the last page of the cohort.";
|
|
298
|
+
guidance.push(
|
|
299
|
+
`Returned ${options.returned} of ${options.total} matching deals (limit ${options.limit}, offset ${options.offset}). This is a partial slice — do NOT treat it as the full cohort. Use total for counts/aggregates. ${more} Narrow the filters or, for exhaustive cohort analysis, use provider-api-request with provider = hubspot and stageAs.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
288
303
|
return guidance.join(" ");
|
|
289
304
|
}
|
|
290
305
|
|
|
@@ -293,7 +308,7 @@ export default defineAction({
|
|
|
293
308
|
// reusable across continuation retries (no re-fetch on resume).
|
|
294
309
|
readOnly: true,
|
|
295
310
|
description:
|
|
296
|
-
"Get HubSpot deals with normalized stage, pipeline, owner, forecast, and NBM fields. This is a bounded deal analytics shortcut, not the full HubSpot capability surface. Use query for a specific customer/deal/account deep dive. For cohorts like products field = Publish, closed-won, pipeline = New Business, or close date in a range, use the structured product, pipeline, closedStatus, closedDateFrom, and closedDateTo filters instead of query when the answer is the deal list itself. If the cohort feeds a cross-source join, transcript/message/ticket search, exhaustive absence check, or downstream code/corpus workflow, prefer provider-api-catalog/provider-api-request with provider = hubspot and stageAs so the cohort is available as a staged dataset. For non-deal CRM records use hubspot-records; for arbitrary HubSpot endpoints, filters, associations, batch APIs, or payloads use provider-api-catalog/provider-api-docs/provider-api-request with provider = hubspot.",
|
|
311
|
+
"Get HubSpot deals with normalized stage, pipeline, owner, forecast, and NBM fields. This is a bounded deal analytics shortcut, not the full HubSpot capability surface. Use query for a specific customer/deal/account deep dive. For cohorts like products field = Publish, closed-won, pipeline = New Business, or close date in a range, use the structured product, pipeline, closedStatus, closedDateFrom, and closedDateTo filters instead of query when the answer is the deal list itself. If the cohort feeds a cross-source join, transcript/message/ticket search, exhaustive absence check, or downstream code/corpus workflow, prefer provider-api-catalog/provider-api-request with provider = hubspot and stageAs so the cohort is available as a staged dataset. Both paths are bounded: at most limit deals are returned (default 25, max 100). The structured-filter path returns total as the true matched count and a truncated flag; page with offset (or narrow filters) instead of expecting the whole cohort in one call, since a full enriched cohort can be several MB and overruns extension and context budgets. For non-deal CRM records use hubspot-records; for arbitrary HubSpot endpoints, filters, associations, batch APIs, or payloads use provider-api-catalog/provider-api-docs/provider-api-request with provider = hubspot.",
|
|
297
312
|
schema: z.object({
|
|
298
313
|
properties: StringListSchema.describe(
|
|
299
314
|
"Optional comma-separated extra HubSpot deal property names to include.",
|
|
@@ -344,7 +359,17 @@ export default defineAction({
|
|
|
344
359
|
.min(1)
|
|
345
360
|
.max(100)
|
|
346
361
|
.default(25)
|
|
347
|
-
.describe(
|
|
362
|
+
.describe(
|
|
363
|
+
"Maximum deals to return. Applies to BOTH full-text query results and structured-filter cohorts. The structured-filter path returns at most this many enriched deals (use total for the true matched count and offset to page).",
|
|
364
|
+
),
|
|
365
|
+
offset: z.coerce
|
|
366
|
+
.number()
|
|
367
|
+
.int()
|
|
368
|
+
.min(0)
|
|
369
|
+
.default(0)
|
|
370
|
+
.describe(
|
|
371
|
+
"Number of structured-filter results to skip before returning limit deals. Use for paging through a large cohort; ignored when query is provided.",
|
|
372
|
+
),
|
|
348
373
|
after: z
|
|
349
374
|
.string()
|
|
350
375
|
.optional()
|
|
@@ -362,6 +387,7 @@ export default defineAction({
|
|
|
362
387
|
closedDateTo,
|
|
363
388
|
query,
|
|
364
389
|
limit = 25,
|
|
390
|
+
offset = 0,
|
|
365
391
|
after,
|
|
366
392
|
}) => {
|
|
367
393
|
const trimmedQuery = query?.trim();
|
|
@@ -414,7 +440,7 @@ export default defineAction({
|
|
|
414
440
|
closedDateTo: closedDateTo?.trim(),
|
|
415
441
|
});
|
|
416
442
|
const structuredFilters = hasStructuredFilters(filters);
|
|
417
|
-
const
|
|
443
|
+
const matchedDeals = rawDeals
|
|
418
444
|
.filter((d) => visibleIds.has(String(d.properties.pipeline)))
|
|
419
445
|
.map((deal) => enrichDeal(deal, lookups, owners))
|
|
420
446
|
.filter((deal) => {
|
|
@@ -435,18 +461,52 @@ export default defineAction({
|
|
|
435
461
|
return matchesDateRange(deal, fromMs, toMs);
|
|
436
462
|
});
|
|
437
463
|
|
|
464
|
+
// The full-text query path is already bounded by HubSpot's own `limit`.
|
|
465
|
+
// EVERY non-query call (with or without structured filters) scans the whole
|
|
466
|
+
// visible deal set, so bound the returned cohort here. This is deliberately
|
|
467
|
+
// NOT gated on `structuredFilters`: the unfiltered `hubspot-deals({})` call
|
|
468
|
+
// is the LARGEST payload of all (every visible deal), so it must be bounded
|
|
469
|
+
// too — a full enriched cohort can be multiple MB, which overruns the
|
|
470
|
+
// extension iframe bridge and the agent context. We
|
|
471
|
+
// keep `total` as the true matched count and signal partial slices via
|
|
472
|
+
// `truncated` so callers never mistake a page for the full cohort.
|
|
473
|
+
// `truncated` means "this response is only part of the cohort" (returned <
|
|
474
|
+
// total) and stays true on the LAST page of a paginated read too; pagination
|
|
475
|
+
// (is there a next page) is reported separately via `hasMore` / `nextOffset`.
|
|
476
|
+
const matchedTotal = matchedDeals.length;
|
|
477
|
+
const deals = trimmedQuery
|
|
478
|
+
? matchedDeals
|
|
479
|
+
: matchedDeals.slice(offset, offset + limit);
|
|
480
|
+
const truncated = !trimmedQuery && deals.length < matchedTotal;
|
|
481
|
+
const hasMore = !trimmedQuery && offset + deals.length < matchedTotal;
|
|
482
|
+
|
|
438
483
|
return {
|
|
439
484
|
deals,
|
|
440
485
|
stageLabels: lookups.stageLabels,
|
|
441
486
|
pipelineLabels: lookups.pipelineLabels,
|
|
442
|
-
total:
|
|
487
|
+
total: matchedTotal,
|
|
443
488
|
count: deals.length,
|
|
444
489
|
query: trimmedQuery || null,
|
|
445
490
|
filters,
|
|
446
491
|
nextAfter: Array.isArray(dealResult) ? null : dealResult.nextAfter,
|
|
492
|
+
...(trimmedQuery
|
|
493
|
+
? {}
|
|
494
|
+
: {
|
|
495
|
+
limit,
|
|
496
|
+
offset,
|
|
497
|
+
truncated,
|
|
498
|
+
hasMore,
|
|
499
|
+
nextOffset: hasMore ? offset + deals.length : null,
|
|
500
|
+
}),
|
|
447
501
|
guidance: buildGuidance({
|
|
448
502
|
query: trimmedQuery,
|
|
449
503
|
structuredFilters,
|
|
504
|
+
truncated,
|
|
505
|
+
hasMore,
|
|
506
|
+
total: matchedTotal,
|
|
507
|
+
returned: deals.length,
|
|
508
|
+
offset,
|
|
509
|
+
limit,
|
|
450
510
|
}),
|
|
451
511
|
...(Array.isArray(dealResult)
|
|
452
512
|
? {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Local database-mode development for Builder/source testing.
|
|
2
|
+
# Copy to .env.local or run `pnpm dev:database` to generate one.
|
|
3
|
+
# These values are local-only. Do not use them for production deploys.
|
|
4
|
+
|
|
5
|
+
AGENT_NATIVE_MODE=database
|
|
6
|
+
BETTER_AUTH_SECRET=<generated-local-development-secret>
|
|
@@ -113,10 +113,11 @@ cd templates/content && pnpm action <name> [args]
|
|
|
113
113
|
| `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock\|builder-cms] [--sourceName] [--sourceTable] [--relationshipMode items\|details] [--join <json>]` | Attach a source binding; use `items` to add more rows and `details` to match a source onto existing rows |
|
|
114
114
|
| `change-content-database-source-role` | `--databaseId <id>` or `--documentId <id> --sourceId <id> --relationshipMode items\|details [--join <json>]` | Change an attached source between adding rows and adding matched detail columns without removing the source |
|
|
115
115
|
| `refresh-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Refresh the read-only source status envelope; Builder CMS reads live entries only when configured |
|
|
116
|
+
| `process-builder-body-hydration` | `--sourceId <id> [--documentId <id>] [--limit <n>]` | Hydrate queued Builder article bodies into readable Content markdown with preserved source-component markers |
|
|
116
117
|
| `set-content-database-source-write-mode` | `--databaseId <id>` or `--documentId <id> --liveWritesEnabled true\|false [--allowedWriteModes <json>]` | Enable/disable per-source Builder live writes; enabling is allowed only for `agent-native-blog-article-test` with explicit modes |
|
|
117
118
|
| `stage-builder-revision` | `--databaseId <id>` or `--documentId <id>` | Stage pending local Builder CMS changes as a local-only save-revision record; never calls Builder |
|
|
118
119
|
| `review-content-database-source-change-set` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> --decision approve\|reject [--note]` | Approve or reject a local source change-set review record without provider writes |
|
|
119
|
-
| `prepare-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--pushModeConfirmation autosave\|draft\|publish]` | Prepare a dry-run Builder execution gate with request semantics/idempotency key; never calls Builder
|
|
120
|
+
| `prepare-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--pushModeConfirmation autosave\|draft\|publish]` | Prepare a dry-run Builder execution gate for approved field/body changes with request semantics/idempotency key; never calls Builder |
|
|
120
121
|
| `validate-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>]` | Validate/replay a prepared Builder execution gate locally as a dry run; never calls Builder |
|
|
121
122
|
| `execute-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>] [--pushModeConfirmation autosave\|draft\|publish]` | Execute a guarded live Builder write only when approved, validated, enabled, idempotent, and targeting `agent-native-blog-article-test` |
|
|
122
123
|
| `add-database-item` | `--databaseId <id> [--title <text>] [--propertyValues <json>]` | Add a page row to a database, optionally seeding property values |
|
|
@@ -208,6 +209,14 @@ Content has two file workflows:
|
|
|
208
209
|
Edit reusable symbol content in that emitted source file; do not retarget
|
|
209
210
|
`entry`, `model`, or `source` in the parent MDX unless a dedicated Builder
|
|
210
211
|
retargeting workflow is added.
|
|
212
|
+
- **Builder source components:** Builder CMS database body hydration renders
|
|
213
|
+
unsupported provider-native body blocks as `<SourceComponent ... />` markers.
|
|
214
|
+
Treat these as read-only preservation anchors, not editable local blocks.
|
|
215
|
+
Agents may edit surrounding prose, but must not delete, duplicate, move, or
|
|
216
|
+
rewrite `rawRef`, `rawHash`, or marker ids unless a dedicated provider
|
|
217
|
+
conversion workflow exists. Guarded Builder write-back refuses missing,
|
|
218
|
+
tampered, or structurally moved markers so source-native components are not
|
|
219
|
+
lost.
|
|
211
220
|
- **Picked folders and components:** browser-picked folders can be the
|
|
212
221
|
source of truth for `.md`/`.mdx` files, but the browser does not expose an
|
|
213
222
|
absolute path that Vite can compile. Component previews from a picked
|
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
BuilderCmsModelSummary,
|
|
6
6
|
BuilderCmsModelsResponse,
|
|
7
7
|
} from "../shared/api.js";
|
|
8
|
+
import {
|
|
9
|
+
builderBlocksHash,
|
|
10
|
+
builderEntryBlocks,
|
|
11
|
+
type BuilderContentEntry,
|
|
12
|
+
} from "../shared/builder-mdx.js";
|
|
8
13
|
import {
|
|
9
14
|
normalizeBuilderCmsApiEntry,
|
|
10
15
|
type BuilderCmsSourceEntry,
|
|
@@ -23,6 +28,7 @@ export interface BuilderCmsEntryLiveState {
|
|
|
23
28
|
exists: boolean;
|
|
24
29
|
published: "published" | "draft" | string | null;
|
|
25
30
|
lastUpdated: number | string | null;
|
|
31
|
+
blocksHash: string | null;
|
|
26
32
|
id: string | null;
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -42,7 +48,7 @@ const BUILDER_CMS_MAX_READ_LIMIT = 1000;
|
|
|
42
48
|
const BUILDER_CMS_PAGE_SIZE = 100;
|
|
43
49
|
const BUILDER_CMS_READ_RETRIES = 2;
|
|
44
50
|
const BUILDER_CMS_ENTRY_FIELDS =
|
|
45
|
-
"id,name,published,lastUpdated,createdDate,data.title,data.handle,data.url,data.slug,data.date,data.description,data.status,data.author,data.image";
|
|
51
|
+
"id,name,published,lastUpdated,createdDate,data.title,data.handle,data.url,data.slug,data.date,data.description,data.status,data.author,data.image,data.blocks,data.blocksString";
|
|
46
52
|
|
|
47
53
|
function builderContentApiHost() {
|
|
48
54
|
return (
|
|
@@ -76,6 +82,7 @@ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
|
|
|
76
82
|
exists: false,
|
|
77
83
|
published: null,
|
|
78
84
|
lastUpdated: null,
|
|
85
|
+
blocksHash: null,
|
|
79
86
|
id: null,
|
|
80
87
|
};
|
|
81
88
|
}
|
|
@@ -84,6 +91,7 @@ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
|
|
|
84
91
|
exists: false,
|
|
85
92
|
published: null,
|
|
86
93
|
lastUpdated: null,
|
|
94
|
+
blocksHash: null,
|
|
87
95
|
id: null,
|
|
88
96
|
};
|
|
89
97
|
}
|
|
@@ -97,6 +105,7 @@ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
|
|
|
97
105
|
exists: false,
|
|
98
106
|
published: null,
|
|
99
107
|
lastUpdated: null,
|
|
108
|
+
blocksHash: null,
|
|
100
109
|
id: null,
|
|
101
110
|
};
|
|
102
111
|
}
|
|
@@ -116,10 +125,12 @@ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
|
|
|
116
125
|
exists: false,
|
|
117
126
|
published: null,
|
|
118
127
|
lastUpdated: null,
|
|
128
|
+
blocksHash: null,
|
|
119
129
|
id: null,
|
|
120
130
|
};
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
const blocks = builderEntryBlocks(record as BuilderContentEntry);
|
|
123
134
|
return {
|
|
124
135
|
exists: true,
|
|
125
136
|
published:
|
|
@@ -129,6 +140,7 @@ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
|
|
|
129
140
|
stringOrNumberFromUnknown(record.updatedDate) ??
|
|
130
141
|
stringOrNumberFromUnknown(record.updatedAt) ??
|
|
131
142
|
stringOrNumberFromUnknown(data.updatedAt),
|
|
143
|
+
blocksHash: blocks.length > 0 ? builderBlocksHash(blocks) : null,
|
|
132
144
|
id,
|
|
133
145
|
};
|
|
134
146
|
}
|
|
@@ -618,6 +630,7 @@ export async function readBuilderCmsEntryLiveState(args: {
|
|
|
618
630
|
exists: false,
|
|
619
631
|
published: null,
|
|
620
632
|
lastUpdated: null,
|
|
633
|
+
blocksHash: null,
|
|
621
634
|
id: null,
|
|
622
635
|
};
|
|
623
636
|
}
|
|
@@ -3,6 +3,21 @@ import type {
|
|
|
3
3
|
ContentDatabaseSourceFieldMapping,
|
|
4
4
|
DocumentPropertyValue,
|
|
5
5
|
} from "../shared/api.js";
|
|
6
|
+
import {
|
|
7
|
+
builderBlocksHash,
|
|
8
|
+
builderEntryBlocks,
|
|
9
|
+
normalizeRemoteUpdatedAt,
|
|
10
|
+
type BuilderContentEntry,
|
|
11
|
+
} from "../shared/builder-mdx.js";
|
|
12
|
+
|
|
13
|
+
export const BUILDER_CMS_BODY_CONTENT_KEY = "__builder.body.content";
|
|
14
|
+
export const BUILDER_CMS_BODY_BLOCKS_HASH_KEY = "__builder.body.blocksHash";
|
|
15
|
+
export const BUILDER_CMS_BODY_SIDECARS_KEY = "__builder.body.sidecarsJson";
|
|
16
|
+
export const BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY =
|
|
17
|
+
"__builder.body.losslessContent";
|
|
18
|
+
export const BUILDER_CMS_BODY_READABLE_MAP_KEY =
|
|
19
|
+
"__builder.body.readableMapJson";
|
|
20
|
+
export const BUILDER_CMS_BODY_BLOCK_COUNT_KEY = "__builder.body.blockCount";
|
|
6
21
|
|
|
7
22
|
export interface BuilderCmsSourceEntry {
|
|
8
23
|
id: string;
|
|
@@ -11,6 +26,7 @@ export interface BuilderCmsSourceEntry {
|
|
|
11
26
|
urlPath: string;
|
|
12
27
|
updatedAt: string;
|
|
13
28
|
sourceValues: Record<string, DocumentPropertyValue>;
|
|
29
|
+
rawEntry?: BuilderContentEntry;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
export interface ExistingBuilderSourceRowIdentity {
|
|
@@ -291,6 +307,7 @@ function builderSourceValuesFromRecord(args: {
|
|
|
291
307
|
title: string;
|
|
292
308
|
urlPath: string;
|
|
293
309
|
updatedAt: string;
|
|
310
|
+
rawEntry: BuilderContentEntry;
|
|
294
311
|
}) {
|
|
295
312
|
const values: Record<string, DocumentPropertyValue> = {
|
|
296
313
|
"data.title": args.title,
|
|
@@ -298,6 +315,7 @@ function builderSourceValuesFromRecord(args: {
|
|
|
298
315
|
lastUpdated: args.updatedAt,
|
|
299
316
|
};
|
|
300
317
|
for (const [key, value] of Object.entries(args.data)) {
|
|
318
|
+
if (key === "blocks" || key === "blocksString") continue;
|
|
301
319
|
values[`data.${key}`] = sourceValueFromUnknown(value);
|
|
302
320
|
}
|
|
303
321
|
for (const key of ["published", "createdDate", "updatedDate", "updatedAt"]) {
|
|
@@ -305,6 +323,11 @@ function builderSourceValuesFromRecord(args: {
|
|
|
305
323
|
values[key] = sourceValueFromUnknown(args.record[key]);
|
|
306
324
|
}
|
|
307
325
|
}
|
|
326
|
+
const blocks = builderEntryBlocks(args.rawEntry);
|
|
327
|
+
if (blocks.length > 0) {
|
|
328
|
+
values[BUILDER_CMS_BODY_BLOCKS_HASH_KEY] = builderBlocksHash(blocks);
|
|
329
|
+
values[BUILDER_CMS_BODY_BLOCK_COUNT_KEY] = blocks.length;
|
|
330
|
+
}
|
|
308
331
|
return values;
|
|
309
332
|
}
|
|
310
333
|
|
|
@@ -342,6 +365,24 @@ export function normalizeBuilderCmsApiEntry(
|
|
|
342
365
|
]) ??
|
|
343
366
|
timestampStringFromRecord(data, ["updatedAt"]) ??
|
|
344
367
|
new Date().toISOString();
|
|
368
|
+
const rawEntry: BuilderContentEntry = {
|
|
369
|
+
...record,
|
|
370
|
+
id,
|
|
371
|
+
model,
|
|
372
|
+
name:
|
|
373
|
+
typeof record.name === "string" && record.name.trim()
|
|
374
|
+
? record.name.trim()
|
|
375
|
+
: title,
|
|
376
|
+
published:
|
|
377
|
+
typeof record.published === "string" ? record.published : undefined,
|
|
378
|
+
lastUpdated: normalizeRemoteUpdatedAt({
|
|
379
|
+
...record,
|
|
380
|
+
id,
|
|
381
|
+
model,
|
|
382
|
+
data,
|
|
383
|
+
} as BuilderContentEntry),
|
|
384
|
+
data,
|
|
385
|
+
};
|
|
345
386
|
|
|
346
387
|
return {
|
|
347
388
|
id,
|
|
@@ -355,6 +396,8 @@ export function normalizeBuilderCmsApiEntry(
|
|
|
355
396
|
title,
|
|
356
397
|
urlPath,
|
|
357
398
|
updatedAt,
|
|
399
|
+
rawEntry,
|
|
358
400
|
}),
|
|
401
|
+
rawEntry,
|
|
359
402
|
};
|
|
360
403
|
}
|
|
@@ -194,6 +194,70 @@ function nestedBuilderPatch(
|
|
|
194
194
|
return body;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
function mergeBuilderPatch(
|
|
198
|
+
base: Record<string, unknown>,
|
|
199
|
+
patch: Record<string, unknown>,
|
|
200
|
+
) {
|
|
201
|
+
const merged = { ...base };
|
|
202
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
203
|
+
if (
|
|
204
|
+
key === "data" &&
|
|
205
|
+
value &&
|
|
206
|
+
typeof value === "object" &&
|
|
207
|
+
!Array.isArray(value)
|
|
208
|
+
) {
|
|
209
|
+
merged.data = {
|
|
210
|
+
...((merged.data && typeof merged.data === "object"
|
|
211
|
+
? merged.data
|
|
212
|
+
: {}) as Record<string, unknown>),
|
|
213
|
+
...(value as Record<string, unknown>),
|
|
214
|
+
};
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
merged[key] = value;
|
|
218
|
+
}
|
|
219
|
+
return merged;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function builderBodyPatch(changeSet: ContentDatabaseSourceChangeSet) {
|
|
223
|
+
const bodyChange = changeSet.bodyChange;
|
|
224
|
+
const checks: string[] = [];
|
|
225
|
+
const blockers: string[] = [];
|
|
226
|
+
if (!bodyChange) {
|
|
227
|
+
return { patch: {}, checks, blockers, hasBodyOperation: false };
|
|
228
|
+
}
|
|
229
|
+
if (!bodyChange.proposedBlocksJson) {
|
|
230
|
+
blockers.push(
|
|
231
|
+
"Builder body diff could not be converted to Builder blocks.",
|
|
232
|
+
);
|
|
233
|
+
return { patch: {}, checks, blockers, hasBodyOperation: false };
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const blocks = JSON.parse(bodyChange.proposedBlocksJson) as unknown;
|
|
237
|
+
if (!Array.isArray(blocks)) {
|
|
238
|
+
blockers.push("Builder body diff did not produce a blocks array.");
|
|
239
|
+
return { patch: {}, checks, blockers, hasBodyOperation: false };
|
|
240
|
+
}
|
|
241
|
+
checks.push("Includes converted Builder body blocks.");
|
|
242
|
+
for (const warning of bodyChange.warnings ?? []) {
|
|
243
|
+
checks.push(`Body conversion warning: ${warning}`);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
patch: {
|
|
247
|
+
data: {
|
|
248
|
+
blocks,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
checks,
|
|
252
|
+
blockers,
|
|
253
|
+
hasBodyOperation: true,
|
|
254
|
+
};
|
|
255
|
+
} catch {
|
|
256
|
+
blockers.push("Builder body diff contains invalid converted blocks JSON.");
|
|
257
|
+
return { patch: {}, checks, blockers, hasBodyOperation: false };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
197
261
|
function builderRequestForEffect(args: {
|
|
198
262
|
effect: BuilderCmsWriteEffect;
|
|
199
263
|
model: string;
|
|
@@ -272,18 +336,19 @@ function builderSafetyChecks(args: {
|
|
|
272
336
|
entryId: string | null;
|
|
273
337
|
syntheticFixtureTarget: boolean;
|
|
274
338
|
operations: BuilderCmsExecutionOperation[];
|
|
339
|
+
hasBodyOperation: boolean;
|
|
340
|
+
bodyChecks: string[];
|
|
341
|
+
bodyBlockers: string[];
|
|
275
342
|
}) {
|
|
276
343
|
const checks = [
|
|
277
344
|
"Requires explicit approval before execution.",
|
|
278
345
|
"Uses the stored execution idempotency key.",
|
|
346
|
+
...args.bodyChecks,
|
|
279
347
|
];
|
|
280
|
-
const blockers: string[] = [];
|
|
348
|
+
const blockers: string[] = [...args.bodyBlockers];
|
|
281
349
|
|
|
282
|
-
if (args.operations.length === 0) {
|
|
283
|
-
blockers.push("No
|
|
284
|
-
}
|
|
285
|
-
if (args.changeSet.bodyChange) {
|
|
286
|
-
blockers.push("Builder body diffs are not executable in this slice.");
|
|
350
|
+
if (args.operations.length === 0 && !args.hasBodyOperation) {
|
|
351
|
+
blockers.push("No Builder operations are available for this change.");
|
|
287
352
|
}
|
|
288
353
|
if (args.effect === "autosave" || args.effect === "update_in_place") {
|
|
289
354
|
const label = args.effect === "autosave" ? "Autosave" : "Update in place";
|
|
@@ -418,7 +483,11 @@ export function buildBuilderCmsExecutionPlan(args: {
|
|
|
418
483
|
localFieldKey: field.localFieldKey,
|
|
419
484
|
value: field.proposedValue,
|
|
420
485
|
}));
|
|
421
|
-
const
|
|
486
|
+
const bodyDiffPatch = builderBodyPatch(args.changeSet);
|
|
487
|
+
const bodyPatch = mergeBuilderPatch(
|
|
488
|
+
nestedBuilderPatch(operations),
|
|
489
|
+
bodyDiffPatch.patch,
|
|
490
|
+
);
|
|
422
491
|
// State-preserving effects must not include `published` in the body. Builder
|
|
423
492
|
// PATCH preserves omitted publication state, so only transition/create effects
|
|
424
493
|
// are allowed to set it.
|
|
@@ -441,6 +510,9 @@ export function buildBuilderCmsExecutionPlan(args: {
|
|
|
441
510
|
args.source.sourceTable === SAFE_WRITE_MODEL &&
|
|
442
511
|
target?.isSyntheticFixture === true,
|
|
443
512
|
operations,
|
|
513
|
+
hasBodyOperation: bodyDiffPatch.hasBodyOperation,
|
|
514
|
+
bodyChecks: bodyDiffPatch.checks,
|
|
515
|
+
bodyBlockers: bodyDiffPatch.blockers,
|
|
444
516
|
});
|
|
445
517
|
const state: ContentDatabaseSourceExecutionState =
|
|
446
518
|
safety.blockers.length > 0
|