@agent-native/core 0.119.6 → 0.120.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.
Files changed (44) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/index.ts +64 -3
  5. package/corpus/core/src/templates/default/package.json +1 -1
  6. package/corpus/templates/assets/package.json +1 -1
  7. package/corpus/templates/brain/package.json +1 -1
  8. package/corpus/templates/calendar/package.json +1 -1
  9. package/corpus/templates/clips/package.json +1 -1
  10. package/corpus/templates/content/actions/add-database-item.ts +1 -0
  11. package/corpus/templates/content/actions/set-document-property.ts +0 -4
  12. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +10 -1
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +10 -4
  14. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +5 -1
  15. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -0
  16. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +52 -33
  17. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +16 -4
  18. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +16 -7
  19. package/corpus/templates/content/app/hooks/use-create-page.ts +8 -3
  20. package/corpus/templates/content/app/hooks/use-document-properties.ts +1 -0
  21. package/corpus/templates/content/app/hooks/use-documents.ts +3 -1
  22. package/corpus/templates/content/app/lib/optimistic-document.ts +29 -0
  23. package/corpus/templates/content/changelog/2026-07-21-editing-a-database-property-no-longer-refreshes-unrelated-co.md +6 -0
  24. package/corpus/templates/content/changelog/2026-07-23-new-database-ready.md +6 -0
  25. package/corpus/templates/content/package.json +1 -1
  26. package/corpus/templates/content/shared/api.ts +1 -0
  27. package/corpus/templates/design/package.json +1 -1
  28. package/corpus/templates/dispatch/package.json +1 -1
  29. package/corpus/templates/forms/package.json +1 -1
  30. package/corpus/templates/macros/package.json +1 -1
  31. package/corpus/templates/mail/package.json +1 -1
  32. package/corpus/templates/slides/package.json +1 -1
  33. package/dist/cli/index.js +53 -2
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/collab/struct-routes.d.ts +1 -1
  36. package/dist/notifications/routes.d.ts +3 -3
  37. package/dist/observability/routes.d.ts +2 -2
  38. package/dist/progress/routes.d.ts +1 -1
  39. package/dist/secrets/routes.d.ts +3 -3
  40. package/dist/server/realtime-token.d.ts +1 -1
  41. package/dist/templates/default/package.json +1 -1
  42. package/package.json +1 -1
  43. package/src/cli/index.ts +64 -3
  44. package/src/templates/default/package.json +1 -1
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1662
32
32
  - toolkit files: 159
33
- - template files: 6331
33
+ - template files: 6334
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.120.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 20ebb96: `agent-native dev --inspect` (and `--inspect-brk`, optionally `=<port>`) now
8
+ attaches the Node inspector to **only** the Nitro API-server process, on a
9
+ single known port (default 9229). It selects Nitro's `node-process` dev runner
10
+ so the server is a real, attachable process, and injects `NODE_OPTIONS` through
11
+ a Vite preload that runs before Vite's own startup — so Vite, pnpm, and the CLI
12
+ are never inspected and there is exactly one debugger target. Set
13
+ `NITRO_DEV_RUNNER` yourself to override the runner.
14
+
3
15
  ## 0.119.6
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.119.6",
3
+ "version": "0.120.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": {
@@ -276,6 +276,22 @@ function findViteBin(): string {
276
276
  return findBinUpwards("vite") ?? "vite";
277
277
  }
278
278
 
279
+ function findViteJsEntry(): string | null {
280
+ try {
281
+ const require = createRequire(path.join(process.cwd(), "package.json"));
282
+ const pkgJsonPath = require.resolve("vite/package.json");
283
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as {
284
+ bin?: string | Record<string, string>;
285
+ };
286
+ const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.vite;
287
+ if (!rel) return null;
288
+ const entry = path.join(path.dirname(pkgJsonPath), rel);
289
+ return fs.existsSync(entry) ? entry : null;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
279
295
  function findTsxBin(): string {
280
296
  const localTsx = path.resolve("node_modules/.bin/tsx");
281
297
  if (fs.existsSync(localTsx)) return localTsx;
@@ -386,15 +402,31 @@ function isWorkspaceRoot(): boolean {
386
402
  }
387
403
  }
388
404
 
405
+ function extractNodeInspectFlag(args: string[]): {
406
+ inspectFlag: string | null;
407
+ rest: string[];
408
+ } {
409
+ const rest: string[] = [];
410
+ let inspectFlag: string | null = null;
411
+ for (const arg of args) {
412
+ if (/^--inspect(-brk)?(=.+)?$/.test(arg)) {
413
+ inspectFlag = arg;
414
+ continue;
415
+ }
416
+ rest.push(arg);
417
+ }
418
+ return { inspectFlag, rest };
419
+ }
420
+
389
421
  function run(
390
422
  cmd: string,
391
423
  cmdArgs: string[],
392
- opts?: { stdio?: "inherit" | "pipe" },
424
+ opts?: { stdio?: "inherit" | "pipe"; env?: NodeJS.ProcessEnv },
393
425
  ) {
394
426
  const child = spawn(cmd, cmdArgs, {
395
427
  stdio: opts?.stdio ?? "inherit",
396
428
  shell: process.platform === "win32",
397
- env: process.env,
429
+ env: opts?.env ?? process.env,
398
430
  });
399
431
  child.on("exit", (code) => process.exit(code ?? 0));
400
432
  // Forward signals to child so Cmd+C doesn't leave zombie processes holding ports
@@ -545,7 +577,36 @@ switch (command) {
545
577
  break;
546
578
  }
547
579
  const vite = findViteBin();
548
- run(vite, args);
580
+ const { inspectFlag, rest } = extractNodeInspectFlag(args);
581
+ if (!inspectFlag) {
582
+ run(vite, rest);
583
+ break;
584
+ }
585
+ const viteJsEntry = findViteJsEntry();
586
+ if (!viteJsEntry) {
587
+ console.warn(
588
+ "[agent-native] Could not resolve Vite's JS entry; starting dev " +
589
+ "server without the debugger.",
590
+ );
591
+ run(vite, rest);
592
+ break;
593
+ }
594
+ // Attach inspect flag to server process (not Vite or Nitro process)
595
+ const parsed = inspectFlag.match(/^--(inspect(?:-brk)?)(?:=(.+))?$/);
596
+ const kind = parsed?.[1] ?? "inspect";
597
+ const target = parsed?.[2] ?? "9229";
598
+ const directive = `--${kind}=${target}`;
599
+ const preload =
600
+ "data:text/javascript," +
601
+ encodeURIComponent(
602
+ `process.env.NODE_OPTIONS=((process.env.NODE_OPTIONS??"")+" ${directive}").trim();`,
603
+ );
604
+ const env = {
605
+ ...process.env,
606
+ NITRO_DEV_RUNNER: process.env.NITRO_DEV_RUNNER ?? "node-process",
607
+ };
608
+ console.log(`[agent-native] API server debugger listening on ${target}`);
609
+ run(process.execPath, ["--import", preload, viteJsEntry, ...rest], { env });
549
610
  break;
550
611
  }
551
612
 
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "dev": "agent-native dev",
6
+ "dev": "agent-native dev --inspect",
7
7
  "build": "agent-native build",
8
8
  "start": "agent-native start",
9
9
  "typecheck": "agent-native typecheck",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "test": "vitest --run",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "test": "vitest --run --config vitest.config.ts",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "test": "vitest --run",
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
7
+ "dev": "agent-native dev --inspect",
8
8
  "build": "agent-native build",
9
9
  "start": "agent-native start",
10
10
  "typecheck": "agent-native typecheck",
@@ -217,6 +217,7 @@ export default defineAction({
217
217
  ...(await getContentDatabaseResponse(databaseId)),
218
218
  createdItemId: itemId,
219
219
  createdDocumentId: documentId,
220
+ createdDocumentUpdatedAt: now,
220
221
  };
221
222
  },
222
223
  });
@@ -1,5 +1,4 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { writeAppState } from "@agent-native/core/application-state";
3
2
  import { assertAccess } from "@agent-native/core/sharing";
4
3
  import { and, eq } from "drizzle-orm";
5
4
  import { z } from "zod";
@@ -87,7 +86,6 @@ export default defineAction({
87
86
  now,
88
87
  });
89
88
  }
90
- await writeAppState("refresh-signal", { ts: Date.now() });
91
89
  return {
92
90
  documentId,
93
91
  databaseId: database.id,
@@ -133,8 +131,6 @@ export default defineAction({
133
131
  });
134
132
  }
135
133
 
136
- await writeAppState("refresh-signal", { ts: Date.now() });
137
-
138
134
  return {
139
135
  documentId,
140
136
  databaseId: database.id,
@@ -38,6 +38,7 @@ const BLOCK_FIELD_DRAG_THRESHOLD = 6;
38
38
 
39
39
  interface DocumentBlockFieldsProps {
40
40
  documentId: string;
41
+ databaseDocumentId: string;
41
42
  canEdit: boolean;
42
43
  /**
43
44
  * The fully-wired collaborative body editor for the primary "Content" field.
@@ -238,6 +239,7 @@ export function blockFieldsRenderState(args: {
238
239
  */
239
240
  export function DocumentBlockFields({
240
241
  documentId,
242
+ databaseDocumentId,
241
243
  canEdit,
242
244
  primaryEditor,
243
245
  }: DocumentBlockFieldsProps) {
@@ -303,6 +305,7 @@ export function DocumentBlockFields({
303
305
  // field while SAVING to another across an identity change.
304
306
  key={`${documentId}:${state.field.definition.id}`}
305
307
  documentId={documentId}
308
+ databaseDocumentId={databaseDocumentId}
306
309
  property={state.field}
307
310
  canEdit={canEdit}
308
311
  />
@@ -318,6 +321,7 @@ export function DocumentBlockFields({
318
321
  return (
319
322
  <MultiBlockFields
320
323
  documentId={documentId}
324
+ databaseDocumentId={databaseDocumentId}
321
325
  canEdit={canEdit}
322
326
  blockFields={state.fields}
323
327
  primaryEditor={primaryEditor}
@@ -329,12 +333,14 @@ export function DocumentBlockFields({
329
333
 
330
334
  function MultiBlockFields({
331
335
  documentId,
336
+ databaseDocumentId,
332
337
  canEdit,
333
338
  blockFields,
334
339
  primaryEditor,
335
340
  t,
336
341
  }: {
337
342
  documentId: string;
343
+ databaseDocumentId: string;
338
344
  canEdit: boolean;
339
345
  blockFields: DocumentProperty[];
340
346
  primaryEditor: ReactNode;
@@ -504,6 +510,7 @@ function MultiBlockFields({
504
510
  // doc's edits to the old field's closure.
505
511
  key={`${documentId}:${property.definition.id}`}
506
512
  documentId={documentId}
513
+ databaseDocumentId={databaseDocumentId}
507
514
  property={property}
508
515
  canEdit={canEdit}
509
516
  />
@@ -789,14 +796,16 @@ export function useBlockFieldEditor({
789
796
  */
790
797
  function AdditionalBlockEditor({
791
798
  documentId,
799
+ databaseDocumentId,
792
800
  property,
793
801
  canEdit,
794
802
  }: {
795
803
  documentId: string;
804
+ databaseDocumentId: string;
796
805
  property: DocumentProperty;
797
806
  canEdit: boolean;
798
807
  }) {
799
- const setProperty = useSetDocumentProperty(documentId);
808
+ const setProperty = useSetDocumentProperty(documentId, databaseDocumentId);
800
809
  const propertyId = property.definition.id;
801
810
  const initialContent =
802
811
  typeof property.value === "string" ? property.value : "";
@@ -80,6 +80,7 @@ import {
80
80
  canWriteLinkedLocalSource,
81
81
  writeDocumentToLinkedLocalSource,
82
82
  } from "@/lib/local-content-source-files";
83
+ import { isDatabaseChoicePending } from "@/lib/optimistic-document";
83
84
  import { cn } from "@/lib/utils";
84
85
 
85
86
  import {
@@ -1519,6 +1520,10 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1519
1520
  );
1520
1521
  const defaultIconKind = documentEditorDefaultIconKind(document);
1521
1522
  const isDatabasePage = Boolean(document.database);
1523
+ const databaseChoicePending = isDatabaseChoicePending(
1524
+ document,
1525
+ createDatabase.isPending,
1526
+ );
1522
1527
  const showNewDocumentTypeChooser =
1523
1528
  canEdit &&
1524
1529
  !isLocalFileDocument &&
@@ -1791,12 +1796,10 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1791
1796
  type="button"
1792
1797
  variant="outline"
1793
1798
  className="justify-start gap-2"
1794
- disabled={
1795
- !editorCanEdit || createDatabase.isPending
1796
- }
1799
+ disabled={!editorCanEdit || databaseChoicePending}
1797
1800
  onClick={() => void handleChooseDatabase()}
1798
1801
  >
1799
- {createDatabase.isPending ? (
1802
+ {databaseChoicePending ? (
1800
1803
  <IconLoader2 className="animate-spin" />
1801
1804
  ) : (
1802
1805
  <IconDatabase />
@@ -1867,6 +1870,9 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
1867
1870
  return (
1868
1871
  <DocumentBlockFields
1869
1872
  documentId={documentId}
1873
+ databaseDocumentId={
1874
+ document.databaseMembership.databaseDocumentId
1875
+ }
1870
1876
  canEdit={editorCanEdit}
1871
1877
  primaryEditor={primaryEditor}
1872
1878
  />
@@ -32,7 +32,11 @@ export function DocumentInfoPanel({
32
32
  onSave={onSaveDescription}
33
33
  />
34
34
  {document.databaseMembership && !isLocalFileDocument ? (
35
- <DocumentProperties documentId={document.id} canEdit={canEdit} />
35
+ <DocumentProperties
36
+ documentId={document.id}
37
+ databaseDocumentId={document.databaseMembership.databaseDocumentId}
38
+ canEdit={canEdit}
39
+ />
36
40
  ) : null}
37
41
  </div>
38
42
  );
@@ -165,6 +165,7 @@ function tWithFallback(
165
165
 
166
166
  interface DocumentPropertiesProps {
167
167
  documentId: string;
168
+ databaseDocumentId: string;
168
169
  canEdit: boolean;
169
170
  popoversPortalled?: boolean;
170
171
  }
@@ -796,6 +797,7 @@ function scalarPlaceholder(type: DocumentPropertyType, t: TFunction) {
796
797
 
797
798
  export function DocumentProperties({
798
799
  documentId,
800
+ databaseDocumentId,
799
801
  canEdit,
800
802
  popoversPortalled = true,
801
803
  }: DocumentPropertiesProps) {
@@ -826,6 +828,7 @@ export function DocumentProperties({
826
828
  key={property.definition.id}
827
829
  property={property}
828
830
  documentId={documentId}
831
+ databaseDocumentId={databaseDocumentId}
829
832
  canEdit={canEdit}
830
833
  popoversPortalled={popoversPortalled}
831
834
  t={t}
@@ -927,12 +930,14 @@ function HiddenPropertiesMenu({
927
930
  function PropertyRow({
928
931
  property,
929
932
  documentId,
933
+ databaseDocumentId,
930
934
  canEdit,
931
935
  popoversPortalled,
932
936
  t,
933
937
  }: {
934
938
  property: DocumentProperty;
935
939
  documentId: string;
940
+ databaseDocumentId: string;
936
941
  canEdit: boolean;
937
942
  popoversPortalled: boolean;
938
943
  t: TFunction;
@@ -978,6 +983,7 @@ function PropertyRow({
978
983
  <PropertyValuePopover
979
984
  property={property}
980
985
  documentId={documentId}
986
+ databaseDocumentId={databaseDocumentId}
981
987
  portalled={popoversPortalled}
982
988
  >
983
989
  {value}
@@ -197,6 +197,7 @@ import {
197
197
  } from "@/hooks/use-document-properties";
198
198
  import {
199
199
  isDocumentUpdateConflict,
200
+ type DocumentUpdateResult,
200
201
  useDeleteDocument,
201
202
  useDocument,
202
203
  seedDatabaseItemDocumentCaches,
@@ -254,7 +255,10 @@ import { EmojiPicker } from "../EmojiPicker";
254
255
  import {
255
256
  createPreviewDocumentSaveController,
256
257
  deferredPreviewDocumentSave,
258
+ type PreviewDocumentPayload,
257
259
  type PreviewDocumentSaveAdapter,
260
+ type PreviewDocumentSaveDeferred,
261
+ type PreviewDocumentSaveSuccess,
258
262
  } from "../previewDocumentSaveController";
259
263
  import {
260
264
  acquirePreviewDocumentSaveController,
@@ -436,7 +440,8 @@ export function databaseCreatedItemForImmediatePreview(
436
440
  if (returnedItem) return returnedItem;
437
441
  if (!response.createdItemId || !response.createdDocumentId) return null;
438
442
 
439
- const now = args.now ?? new Date().toISOString();
443
+ const now =
444
+ response.createdDocumentUpdatedAt ?? args.now ?? new Date().toISOString();
440
445
  const position = Math.max(
441
446
  0,
442
447
  (response.pagination?.totalItems ?? response.items.length + 1) - 1,
@@ -473,6 +478,42 @@ export function databaseCreatedItemForImmediatePreview(
473
478
  };
474
479
  }
475
480
 
481
+ export function previewDocumentSaveResult(args: {
482
+ result: DocumentUpdateResult;
483
+ payload: PreviewDocumentPayload;
484
+ baseline?: PreviewDocumentPayload;
485
+ contentChanged: boolean;
486
+ }): PreviewDocumentSaveDeferred | PreviewDocumentSaveSuccess {
487
+ const serverDocument = isDocumentUpdateConflict(args.result)
488
+ ? args.result.document
489
+ : args.result;
490
+ const titleSaveObservedExternalBody =
491
+ !args.contentChanged && serverDocument.content !== args.payload.content;
492
+
493
+ if (isDocumentUpdateConflict(args.result) || titleSaveObservedExternalBody) {
494
+ return deferredPreviewDocumentSave("conflict", {
495
+ lastSaved: args.baseline ?? args.payload,
496
+ pending: {
497
+ title: serverDocument.title,
498
+ content: serverDocument.content,
499
+ loadedUpdatedAt: serverDocument.updatedAt,
500
+ loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
501
+ serverDocument.content,
502
+ ),
503
+ },
504
+ deferredReason: "conflict",
505
+ });
506
+ }
507
+
508
+ return {
509
+ outcome: "saved",
510
+ loadedUpdatedAt: args.result.updatedAt,
511
+ loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
512
+ args.result.content,
513
+ ),
514
+ };
515
+ }
516
+
476
517
  export function databaseBuilderHydrationSourceForItem(
477
518
  item: ContentDatabaseItem,
478
519
  sources: ContentDatabaseSource[],
@@ -4098,8 +4139,6 @@ function DatabaseItemPreview({
4098
4139
  // ever touch its own row's state. See previewDocumentSaveRegistry.
4099
4140
  const updateDocumentRef = useRef(updateDocument);
4100
4141
  updateDocumentRef.current = updateDocument;
4101
- const queryClientRef = useRef(queryClient);
4102
- queryClientRef.current = queryClient;
4103
4142
  const bodyHydrationPendingRef = useRef(bodyHydrationPending);
4104
4143
  bodyHydrationPendingRef.current = bodyHydrationPending;
4105
4144
  const draftVersionsRef = useRef<Map<string, number | null>>(new Map());
@@ -4244,30 +4283,14 @@ function DatabaseItemPreview({
4244
4283
  },
4245
4284
  {
4246
4285
  onSuccess: (result) => {
4247
- if (isDocumentUpdateConflict(result)) {
4248
- resolve(
4249
- deferredPreviewDocumentSave("conflict", {
4250
- lastSaved: baseline ?? payload,
4251
- pending: {
4252
- title: result.document.title,
4253
- content: result.document.content,
4254
- loadedUpdatedAt: result.document.updatedAt,
4255
- loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
4256
- result.document.content,
4257
- ),
4258
- },
4259
- deferredReason: "conflict",
4260
- }),
4261
- );
4262
- return;
4263
- }
4264
- resolve({
4265
- outcome: "saved" as const,
4266
- loadedUpdatedAt: result.updatedAt,
4267
- loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
4268
- result.content,
4269
- ),
4270
- });
4286
+ resolve(
4287
+ previewDocumentSaveResult({
4288
+ result,
4289
+ payload,
4290
+ baseline,
4291
+ contentChanged,
4292
+ }),
4293
+ );
4271
4294
  },
4272
4295
  onError: reject,
4273
4296
  },
@@ -4276,12 +4299,6 @@ function DatabaseItemPreview({
4276
4299
  onSaved: (persistedPayload) => {
4277
4300
  const controller = peekPreviewDocumentSaveController(documentId);
4278
4301
  if (controller) enqueueDraftWrite(controller, "delete", persistedPayload);
4279
- void queryClientRef.current.invalidateQueries({
4280
- queryKey: contentDatabaseQueryKey(databaseDocumentId),
4281
- });
4282
- void queryClientRef.current.invalidateQueries({
4283
- queryKey: ["action", "list-documents"],
4284
- });
4285
4302
  },
4286
4303
  onError: (err) => {
4287
4304
  toast.error(dbText("failedToSavePagePreview"), {
@@ -4876,6 +4893,7 @@ function DatabaseItemPreview({
4876
4893
  {previewDocument.databaseMembership ? (
4877
4894
  <DocumentProperties
4878
4895
  documentId={previewDocument.id}
4896
+ databaseDocumentId={databaseDocumentId}
4879
4897
  canEdit={previewCanEdit}
4880
4898
  popoversPortalled={false}
4881
4899
  />
@@ -4913,6 +4931,7 @@ function DatabaseItemPreview({
4913
4931
  const editor = previewDocument.databaseMembership ? (
4914
4932
  <DocumentBlockFields
4915
4933
  documentId={previewDocument.id}
4934
+ databaseDocumentId={databaseDocumentId}
4916
4935
  canEdit={previewCanEdit}
4917
4936
  primaryEditor={primaryEditor}
4918
4937
  />
@@ -130,8 +130,8 @@ export interface PreviewDocumentSaveController {
130
130
  /** Adopt `payload` as the confirmed-saved baseline (no save scheduled). */
131
131
  mark(payload: PreviewDocumentPayload): void;
132
132
  /**
133
- * Adopt a fresher server baseline while retaining the user's pending title
134
- * and content. Used only after an explicit "keep local draft" choice.
133
+ * Adopt a fresher server baseline while retaining only fields the user
134
+ * changed locally. Used only after an explicit "keep local draft" choice.
135
135
  */
136
136
  rebasePending(payload: PreviewDocumentPayload): void;
137
137
  /** Replace callbacks captured by an older preview mount. */
@@ -261,8 +261,7 @@ export function createPreviewDocumentSaveController(
261
261
  // the controller was created/last marked — e.g. "empty" for a
262
262
  // brand-new page — forever, even after real content has been saved.
263
263
  const success = asSaveSuccess(result);
264
- lastSaved = {
265
- ...attempted,
264
+ const savedMetadata = {
266
265
  ...(success?.loadedUpdatedAt !== undefined
267
266
  ? { loadedUpdatedAt: success.loadedUpdatedAt }
268
267
  : {}),
@@ -270,6 +269,15 @@ export function createPreviewDocumentSaveController(
270
269
  ? { loadedContentWasEmpty: success.loadedContentWasEmpty }
271
270
  : {}),
272
271
  };
272
+ lastSaved = {
273
+ ...attempted,
274
+ ...savedMetadata,
275
+ };
276
+ // A later keystroke starts from `pending`, including while this save is
277
+ // in flight. Rebase that trailing payload onto our own successful write
278
+ // so its next CAS does not mistake the preceding save for an external
279
+ // change.
280
+ pending = { ...pending, ...savedMetadata };
273
281
  hasSavedLocally = true;
274
282
  deferredReason = null;
275
283
  inFlight = null;
@@ -336,9 +344,13 @@ export function createPreviewDocumentSaveController(
336
344
  },
337
345
  rebasePending(payload: PreviewDocumentPayload) {
338
346
  clearTimer();
347
+ const titleChangedLocally = pending.title !== lastSaved.title;
348
+ const contentChangedLocally = pending.content !== lastSaved.content;
339
349
  lastSaved = { ...payload };
340
350
  pending = {
341
351
  ...pending,
352
+ title: titleChangedLocally ? pending.title : payload.title,
353
+ content: contentChangedLocally ? pending.content : payload.content,
342
354
  loadedUpdatedAt: payload.loadedUpdatedAt,
343
355
  loadedContentWasEmpty: payload.loadedContentWasEmpty,
344
356
  };
@@ -121,6 +121,10 @@ import {
121
121
  filterDocumentTreeDocuments,
122
122
  } from "@/hooks/use-documents";
123
123
  import { useLocalStorage } from "@/hooks/use-local-storage";
124
+ import {
125
+ markDocumentCreationPending,
126
+ shouldCreateDocumentOptimistically,
127
+ } from "@/lib/optimistic-document";
124
128
  import { cn } from "@/lib/utils";
125
129
 
126
130
  import {
@@ -747,7 +751,12 @@ export function DocumentSidebar({
747
751
  optimisticId?: string,
748
752
  rootFilesDatabaseId?: string,
749
753
  ) => {
750
- if (localFileMode) {
754
+ if (
755
+ !shouldCreateDocumentOptimistically({
756
+ localFileMode,
757
+ filesDatabaseId: rootFilesDatabaseId,
758
+ })
759
+ ) {
751
760
  try {
752
761
  const created = await createDocument.mutateAsync({
753
762
  title: "",
@@ -774,7 +783,7 @@ export function DocumentSidebar({
774
783
 
775
784
  const id = optimisticId ?? nanoid();
776
785
  const now = new Date().toISOString();
777
- const tempDoc: Document = {
786
+ const tempDoc = markDocumentCreationPending({
778
787
  id,
779
788
  parentId: parentId ?? null,
780
789
  title: "",
@@ -789,7 +798,7 @@ export function DocumentSidebar({
789
798
  canManage: true,
790
799
  createdAt: now,
791
800
  updatedAt: now,
792
- };
801
+ });
793
802
 
794
803
  // Optimistically inject into caches so UI updates immediately
795
804
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
@@ -824,14 +833,14 @@ export function DocumentSidebar({
824
833
  spaceId: parentId ? undefined : rootSpaceId,
825
834
  });
826
835
  const nextId = created?.id || id;
836
+ queryClient.setQueryData(
837
+ ["action", "get-document", { id: nextId }],
838
+ created,
839
+ );
827
840
  if (nextId !== id) {
828
841
  queryClient.removeQueries({
829
842
  queryKey: ["action", "get-document", { id }],
830
843
  });
831
- queryClient.setQueryData(
832
- ["action", "get-document", { id: nextId }],
833
- created,
834
- );
835
844
  navigateToDocument(nextId);
836
845
  }
837
846
  // Replace optimistic doc with real server doc + clear any 404 error