@agent-native/core 0.114.0 → 0.114.1

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 (162) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/build-compatibility.ts +77 -0
  5. package/corpus/core/src/client/route-chunk-recovery/index.ts +1 -0
  6. package/corpus/core/src/client/use-action.ts +32 -1
  7. package/corpus/core/src/deploy/build.ts +4 -1
  8. package/corpus/core/src/org/context.ts +56 -30
  9. package/corpus/core/src/server/action-routes.ts +52 -2
  10. package/corpus/core/src/server/auth-plugin.ts +9 -4
  11. package/corpus/core/src/server/better-auth-instance.ts +5 -1
  12. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  13. package/corpus/core/src/vite/client.ts +17 -0
  14. package/corpus/templates/content/AGENTS.md +2 -0
  15. package/corpus/templates/content/actions/_content-database-personal-view.ts +49 -5
  16. package/corpus/templates/content/actions/_content-document-access.ts +21 -0
  17. package/corpus/templates/content/actions/_content-favorites.ts +91 -0
  18. package/corpus/templates/content/actions/_content-files.ts +79 -23
  19. package/corpus/templates/content/actions/_content-sidebar-state.ts +24 -0
  20. package/corpus/templates/content/actions/_content-space-access.ts +15 -17
  21. package/corpus/templates/content/actions/_content-space-catalog-guards.ts +20 -0
  22. package/corpus/templates/content/actions/_content-spaces.ts +364 -167
  23. package/corpus/templates/content/actions/_database-source-utils.ts +8 -0
  24. package/corpus/templates/content/actions/_database-utils.ts +249 -15
  25. package/corpus/templates/content/actions/_delete-content-space.ts +84 -0
  26. package/corpus/templates/content/actions/_files-system-properties.ts +499 -0
  27. package/corpus/templates/content/actions/_local-file-documents.ts +0 -1
  28. package/corpus/templates/content/actions/_property-utils.ts +18 -9
  29. package/corpus/templates/content/actions/bind-content-database-source-field.ts +3 -0
  30. package/corpus/templates/content/actions/configure-document-property.ts +3 -0
  31. package/corpus/templates/content/actions/create-content-database.ts +2 -2
  32. package/corpus/templates/content/actions/create-content-space.ts +34 -0
  33. package/corpus/templates/content/actions/create-document.ts +4 -1
  34. package/corpus/templates/content/actions/delete-content-space.ts +19 -0
  35. package/corpus/templates/content/actions/delete-database-items.ts +28 -1
  36. package/corpus/templates/content/actions/delete-document-property.ts +3 -0
  37. package/corpus/templates/content/actions/delete-document.ts +39 -2
  38. package/corpus/templates/content/actions/duplicate-database-item.ts +6 -0
  39. package/corpus/templates/content/actions/duplicate-database-items.ts +6 -0
  40. package/corpus/templates/content/actions/duplicate-document-property.ts +3 -0
  41. package/corpus/templates/content/actions/get-content-database.ts +11 -2
  42. package/corpus/templates/content/actions/get-content-sidebar-state.ts +23 -0
  43. package/corpus/templates/content/actions/get-document.ts +32 -7
  44. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  45. package/corpus/templates/content/actions/list-content-spaces.ts +34 -2
  46. package/corpus/templates/content/actions/list-documents.ts +39 -7
  47. package/corpus/templates/content/actions/reorder-document-property.ts +9 -0
  48. package/corpus/templates/content/actions/set-document-property.ts +41 -23
  49. package/corpus/templates/content/actions/share-local-file-document.ts +15 -0
  50. package/corpus/templates/content/actions/submit-content-database-form.ts +7 -0
  51. package/corpus/templates/content/actions/sync-local-folder-source.ts +14 -0
  52. package/corpus/templates/content/actions/update-content-database-view.ts +1 -0
  53. package/corpus/templates/content/actions/update-content-sidebar-state.ts +27 -0
  54. package/corpus/templates/content/actions/update-document.ts +158 -66
  55. package/corpus/templates/content/app/components/EmptyState.tsx +4 -57
  56. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +8 -1
  57. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +519 -178
  58. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +39 -0
  59. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  60. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +266 -7
  61. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +430 -152
  62. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +467 -66
  63. package/corpus/templates/content/app/components/editor/database/view-config.ts +9 -3
  64. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +720 -196
  65. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +173 -1
  66. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +1 -1
  67. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +91 -16
  68. package/corpus/templates/content/app/entry.client.tsx +5 -1
  69. package/corpus/templates/content/app/hooks/use-content-database.ts +84 -3
  70. package/corpus/templates/content/app/hooks/use-content-spaces.ts +60 -2
  71. package/corpus/templates/content/app/hooks/use-create-page.ts +30 -0
  72. package/corpus/templates/content/app/hooks/use-documents.ts +270 -19
  73. package/corpus/templates/content/app/i18n-data.ts +243 -20
  74. package/corpus/templates/content/app/lib/local-folder-picker-safety.ts +54 -0
  75. package/corpus/templates/content/app/lib/local-folder-picker-support.ts +18 -0
  76. package/corpus/templates/content/app/routes/_app.favorites.tsx +30 -0
  77. package/corpus/templates/content/app/routes/_app.local-files.tsx +18 -6
  78. package/corpus/templates/content/changelog/2026-07-17-workspace-files-now-stay-in-a-loading-state-until-automatic-.md +6 -0
  79. package/corpus/templates/content/changelog/2026-07-17-workspace-switching-now-opens-the-selected-files-database-pr.md +6 -0
  80. package/corpus/templates/content/changelog/2026-07-17-workspaces-now-expand-above-their-own-files-list-without-dup.md +6 -0
  81. package/corpus/templates/content/changelog/2026-07-18-create-named-workspaces-each-with-its-own-private-files-data.md +6 -0
  82. package/corpus/templates/content/changelog/2026-07-18-favorites-align-with-workspace-and-file-rows-in-the-sidebar.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-18-local-folder-selection-no-longer-invokes-the-unsafe-native-p.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-18-made-content-workspaces-independently-expandable-restored-or.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-18-page-details-now-live-in-a-shared-info-and-comments-rail-dat.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-18-workspace-database-views-now-control-sidebar-workspace-navig.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-18-workspace-navigation-now-aligns-favorites-with-workspace-pag.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-19-favorites-now-open-as-a-table-and-compact-breadcrumbs-make-w.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-19-files-databases-now-start-unfiltered-sidebar-rows-exactly-fo.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-19-sidebar-page-trees-now-open-only-when-expanded-remember-thei.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-19-workspace-file-databases-now-use-the-workspace-name-open-as-.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-19-workspace-navigation-now-stays-stable-on-hover-uses-folder-i.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-19-workspace-rows-now-use-folder-icons-and-deleting-a-user-crea.md +6 -0
  94. package/corpus/templates/content/changelog/2026-07-19-workspaces-can-be-created-without-leaving-the-workspaces-dat.md +6 -0
  95. package/corpus/templates/content/e2e/global-setup.ts +3 -2
  96. package/corpus/templates/content/parity/matrix.md +28 -28
  97. package/corpus/templates/content/parity/matrix.ts +8 -2
  98. package/corpus/templates/content/server/db/index.ts +1 -0
  99. package/corpus/templates/content/server/db/schema.ts +10 -0
  100. package/corpus/templates/content/server/lib/documents.ts +12 -7
  101. package/corpus/templates/content/server/plugins/db.ts +17 -0
  102. package/corpus/templates/content/shared/api.ts +9 -0
  103. package/corpus/templates/content/shared/database-form.ts +11 -2
  104. package/corpus/templates/content/vite.config.ts +1 -0
  105. package/dist/client/build-compatibility.d.ts +11 -0
  106. package/dist/client/build-compatibility.d.ts.map +1 -0
  107. package/dist/client/build-compatibility.js +56 -0
  108. package/dist/client/build-compatibility.js.map +1 -0
  109. package/dist/client/route-chunk-recovery/index.d.ts +1 -0
  110. package/dist/client/route-chunk-recovery/index.d.ts.map +1 -1
  111. package/dist/client/route-chunk-recovery/index.js +1 -0
  112. package/dist/client/route-chunk-recovery/index.js.map +1 -1
  113. package/dist/client/use-action.d.ts.map +1 -1
  114. package/dist/client/use-action.js +19 -1
  115. package/dist/client/use-action.js.map +1 -1
  116. package/dist/collab/routes.d.ts +1 -1
  117. package/dist/collab/struct-routes.d.ts +1 -1
  118. package/dist/deploy/build.js +2 -1
  119. package/dist/deploy/build.js.map +1 -1
  120. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  121. package/dist/notifications/routes.d.ts +1 -1
  122. package/dist/observability/routes.d.ts +5 -5
  123. package/dist/org/context.d.ts +4 -2
  124. package/dist/org/context.d.ts.map +1 -1
  125. package/dist/org/context.js +44 -22
  126. package/dist/org/context.js.map +1 -1
  127. package/dist/progress/routes.d.ts +1 -1
  128. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  129. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  130. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/dist/secrets/routes.d.ts +9 -9
  133. package/dist/server/action-routes.d.ts.map +1 -1
  134. package/dist/server/action-routes.js +32 -2
  135. package/dist/server/action-routes.js.map +1 -1
  136. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  137. package/dist/server/auth-plugin.d.ts.map +1 -1
  138. package/dist/server/auth-plugin.js +9 -5
  139. package/dist/server/auth-plugin.js.map +1 -1
  140. package/dist/server/better-auth-instance.d.ts.map +1 -1
  141. package/dist/server/better-auth-instance.js +4 -1
  142. package/dist/server/better-auth-instance.js.map +1 -1
  143. package/dist/server/transcribe-voice.d.ts +1 -1
  144. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  145. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  146. package/dist/shared/mcp-embed-headers.js +1 -1
  147. package/dist/shared/mcp-embed-headers.js.map +1 -1
  148. package/dist/vite/client.d.ts +6 -0
  149. package/dist/vite/client.d.ts.map +1 -1
  150. package/dist/vite/client.js +8 -0
  151. package/dist/vite/client.js.map +1 -1
  152. package/package.json +1 -1
  153. package/src/client/build-compatibility.ts +77 -0
  154. package/src/client/route-chunk-recovery/index.ts +1 -0
  155. package/src/client/use-action.ts +32 -1
  156. package/src/deploy/build.ts +4 -1
  157. package/src/org/context.ts +56 -30
  158. package/src/server/action-routes.ts +52 -2
  159. package/src/server/auth-plugin.ts +9 -4
  160. package/src/server/better-auth-instance.ts +5 -1
  161. package/src/shared/mcp-embed-headers.ts +1 -1
  162. package/src/vite/client.ts +17 -0
@@ -13,11 +13,12 @@ import {
13
13
  parsePropertyOptions,
14
14
  type DocumentPropertyType,
15
15
  } from "../shared/properties.js";
16
+ import { resolveContentDocumentAccess } from "./_content-document-access.js";
16
17
  import {
17
- listPropertiesForDocument,
18
+ getDatabaseById,
19
+ listPropertiesForDatabaseDocuments,
18
20
  nanoid,
19
21
  normalizedValueJson,
20
- resolvePropertyDatabaseForDocument,
21
22
  writeBlockFieldContent,
22
23
  writePrimaryBlocksContent,
23
24
  } from "./_property-utils.js";
@@ -30,27 +31,34 @@ export default defineAction({
30
31
  value: z.unknown().describe("Value for the property type"),
31
32
  }),
32
33
  run: async ({ documentId, propertyId, value }) => {
33
- const access = await assertAccess("document", documentId, "editor");
34
- const document = access.resource;
35
34
  const db = getDb();
36
- const database = await resolvePropertyDatabaseForDocument(document);
37
- if (!database) throw new Error("Document is not part of a database.");
38
-
39
35
  const [definition] = await db
40
36
  .select()
41
37
  .from(schema.documentPropertyDefinitions)
38
+ .where(eq(schema.documentPropertyDefinitions.id, propertyId));
39
+ if (!definition) throw new Error(`Property "${propertyId}" not found`);
40
+ if (!definition.databaseId) {
41
+ throw new Error(`Property "${propertyId}" is not attached to a database`);
42
+ }
43
+ const database = await getDatabaseById(definition.databaseId);
44
+ if (!database) throw new Error("Document database not found.");
45
+ await assertAccess("document", database.documentId, "editor");
46
+ if (definition.systemRole) {
47
+ throw new Error("System properties are derived and cannot be edited.");
48
+ }
49
+ const access = await resolveContentDocumentAccess(documentId);
50
+ if (!access) throw new Error(`Document "${documentId}" not found`);
51
+ const document = access.resource;
52
+ const [membership] = await db
53
+ .select({ id: schema.contentDatabaseItems.id })
54
+ .from(schema.contentDatabaseItems)
42
55
  .where(
43
56
  and(
44
- eq(schema.documentPropertyDefinitions.id, propertyId),
45
- eq(
46
- schema.documentPropertyDefinitions.ownerEmail,
47
- document.ownerEmail,
48
- ),
49
- eq(schema.documentPropertyDefinitions.databaseId, database.id),
57
+ eq(schema.contentDatabaseItems.databaseId, database.id),
58
+ eq(schema.contentDatabaseItems.documentId, documentId),
50
59
  ),
51
60
  );
52
- if (!definition) throw new Error(`Property "${propertyId}" not found`);
53
-
61
+ if (!membership) throw new Error("Document is not part of this database.");
54
62
  const type = definition.type as DocumentPropertyType;
55
63
  if (isComputedPropertyType(type)) {
56
64
  throw new Error("Computed properties cannot be edited.");
@@ -62,6 +70,7 @@ export default defineAction({
62
70
  // primary "Content" field writes to the document body; additional Blocks
63
71
  // fields write to their own independent store.
64
72
  if (isBlocksPropertyType(type)) {
73
+ await assertAccess("document", documentId, "editor");
65
74
  const normalized = normalizePropertyValue(type, value);
66
75
  const content = typeof normalized === "string" ? normalized : "";
67
76
  const target = blocksStorageTarget(
@@ -73,7 +82,7 @@ export default defineAction({
73
82
  await writeBlockFieldContent({
74
83
  documentId,
75
84
  propertyId,
76
- ownerEmail: document.ownerEmail,
85
+ ownerEmail: database.ownerEmail,
77
86
  content,
78
87
  now,
79
88
  });
@@ -82,11 +91,17 @@ export default defineAction({
82
91
  return {
83
92
  documentId,
84
93
  databaseId: database.id,
85
- properties: await listPropertiesForDocument({
86
- ...document,
87
- content: target === "document_body" ? content : document.content,
88
- updatedAt: now,
89
- }),
94
+ properties:
95
+ (
96
+ await listPropertiesForDatabaseDocuments(database.id, [
97
+ {
98
+ ...document,
99
+ content:
100
+ target === "document_body" ? content : document.content,
101
+ updatedAt: now,
102
+ },
103
+ ])
104
+ ).get(documentId) ?? [],
90
105
  };
91
106
  }
92
107
 
@@ -109,7 +124,7 @@ export default defineAction({
109
124
  } else {
110
125
  await db.insert(schema.documentPropertyValues).values({
111
126
  id: nanoid(),
112
- ownerEmail: document.ownerEmail,
127
+ ownerEmail: database.ownerEmail,
113
128
  documentId,
114
129
  propertyId,
115
130
  valueJson,
@@ -123,7 +138,10 @@ export default defineAction({
123
138
  return {
124
139
  documentId,
125
140
  databaseId: database.id,
126
- properties: await listPropertiesForDocument(document),
141
+ properties:
142
+ (await listPropertiesForDatabaseDocuments(database.id, [document])).get(
143
+ documentId,
144
+ ) ?? [],
127
145
  };
128
146
  },
129
147
  });
@@ -13,6 +13,7 @@ import {
13
13
  parseDocumentFavorite,
14
14
  parseDocumentHideFromSearch,
15
15
  } from "../server/lib/documents.js";
16
+ import { setFavoriteMembership } from "./_content-favorites.js";
16
17
  import { ensureDocumentFilesMembership } from "./_content-files.js";
17
18
  import {
18
19
  organizationContentSpaceId,
@@ -136,6 +137,13 @@ export default defineAction({
136
137
  .where(eq(schema.documents.id, existing.id));
137
138
 
138
139
  await ensureDocumentFilesMembership(db, existing.id, now);
140
+ await setFavoriteMembership({
141
+ db,
142
+ userEmail,
143
+ documentId: existing.id,
144
+ favorite: localDocument.isFavorite,
145
+ now,
146
+ });
139
147
 
140
148
  const [row] = await db
141
149
  .select()
@@ -182,6 +190,13 @@ export default defineAction({
182
190
  updatedAt: now,
183
191
  });
184
192
  await ensureDocumentFilesMembership(db, documentId, now);
193
+ await setFavoriteMembership({
194
+ db,
195
+ userEmail,
196
+ documentId,
197
+ favorite: localDocument.isFavorite,
198
+ now,
199
+ });
185
200
  },
186
201
  );
187
202
 
@@ -9,6 +9,7 @@ import { z } from "zod";
9
9
  import { getDb, schema } from "../server/db/index.js";
10
10
  import type {
11
11
  ContentDatabaseView,
12
+ DocumentPropertySystemRole,
12
13
  SubmitContentDatabaseFormResponse,
13
14
  } from "../shared/api.js";
14
15
  import { contentDatabaseFormQuestions } from "../shared/database-form.js";
@@ -143,6 +144,11 @@ function resolveSubmittedProperties(
143
144
  }
144
145
  const definition = exact ?? named[0];
145
146
  if (!definition) throw new Error(`Unknown form property "${inputKey}".`);
147
+ if (definition.systemRole) {
148
+ throw new Error(
149
+ `System property "${definition.name}" cannot be submitted.`,
150
+ );
151
+ }
146
152
  if (!enabledPropertyIds.has(definition.id)) {
147
153
  throw new Error(
148
154
  `Property "${definition.name}" is not enabled in this form.`,
@@ -235,6 +241,7 @@ export default defineAction({
235
241
  definition: {
236
242
  id: definition.id,
237
243
  type: definition.type as DocumentPropertyType,
244
+ systemRole: definition.systemRole as DocumentPropertySystemRole | null,
238
245
  },
239
246
  }));
240
247
  const questions = contentDatabaseFormQuestions(formView, properties);
@@ -14,8 +14,10 @@ import {
14
14
  isContentSourcePath,
15
15
  parseContentSourceFile,
16
16
  } from "../shared/content-source.js";
17
+ import { setFavoriteMembership } from "./_content-favorites.js";
17
18
  import { ensureDocumentsFilesMembership } from "./_content-files.js";
18
19
  import { resolveContentSpaceAccess } from "./_content-space-access.js";
20
+ import { provisionContentSpaces } from "./_content-spaces.js";
19
21
  import { LOCAL_FOLDER_SOURCE_TYPE } from "./_local-folder-source.js";
20
22
 
21
23
  const MAX_SOURCE_FILES = 500;
@@ -175,6 +177,7 @@ export default defineAction({
175
177
  throw new Error(`Local folder source "${sourceId}" not found`);
176
178
  }
177
179
  await resolveContentSpaceAccess(target.database.spaceId, "editor");
180
+ if (!dryRun) await provisionContentSpaces(db, userEmail);
178
181
  const targetSpaceId = target.database.spaceId;
179
182
 
180
183
  const parsed = entries.map(([path, value]) =>
@@ -572,6 +575,17 @@ export default defineAction({
572
575
  }
573
576
  }
574
577
 
578
+ for (const plan of plans) {
579
+ if (plan.conflict || plan.existing || !plan.file.isFavorite) continue;
580
+ await setFavoriteMembership({
581
+ db: tx,
582
+ userEmail,
583
+ documentId: plan.id,
584
+ favorite: true,
585
+ now,
586
+ });
587
+ }
588
+
575
589
  const materializedIds = plans
576
590
  .filter((plan) => !plan.conflict)
577
591
  .map((plan) => plan.id);
@@ -76,6 +76,7 @@ const viewSchema = z.object({
76
76
  "form",
77
77
  "sidebar",
78
78
  ])
79
+ .transform((type) => (type === "sidebar" ? "table" : type))
79
80
  .default("table"),
80
81
  sorts: z.array(sortSchema).default([]),
81
82
  filters: z.array(filterSchema).default([]),
@@ -0,0 +1,27 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { putUserSetting } from "@agent-native/core/settings";
3
+
4
+ import {
5
+ CONTENT_SIDEBAR_STATE_SETTING_KEY,
6
+ contentSidebarStateSchema,
7
+ } from "./_content-sidebar-state.js";
8
+
9
+ export default defineAction({
10
+ description: "Persist the current user's Content sidebar expansion state.",
11
+ schema: contentSidebarStateSchema,
12
+ agentTool: false,
13
+ run: async (state, ctx) => {
14
+ if (!ctx?.userEmail) throw new Error("Not authenticated.");
15
+ const normalized = {
16
+ ...state,
17
+ expandedWorkspaceIds: [...new Set(state.expandedWorkspaceIds)],
18
+ expandedDocumentIds: [...new Set(state.expandedDocumentIds)],
19
+ };
20
+ await putUserSetting(
21
+ ctx.userEmail,
22
+ CONTENT_SIDEBAR_STATE_SETTING_KEY,
23
+ normalized,
24
+ );
25
+ return { state: normalized };
26
+ },
27
+ });
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { agentTouchDocument } from "@agent-native/core/collab";
4
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
5
  import { assertAccess } from "@agent-native/core/sharing";
5
6
  import {
6
7
  getGenerationCreativeContext,
@@ -9,7 +10,7 @@ import {
9
10
  validateGenerationCreativeContext,
10
11
  } from "@agent-native/creative-context/server";
11
12
  import type { CreativeContextReuseLabel } from "@agent-native/creative-context/types";
12
- import { and, eq, desc } from "drizzle-orm";
13
+ import { and, eq, desc, inArray } from "drizzle-orm";
13
14
  import { z } from "zod";
14
15
 
15
16
  import { getDb, schema } from "../server/db/index.js";
@@ -20,6 +21,12 @@ import {
20
21
  import type { DocumentUpdateResponse } from "../shared/api.js";
21
22
  import { BUILDER_CMS_BODY_CONTENT_KEY } from "./_builder-cms-source-adapter.js";
22
23
  import { reconcileInlineDatabasesForDocument } from "./_content-database-lifecycle.js";
24
+ import { resolveContentDocumentAccess } from "./_content-document-access.js";
25
+ import {
26
+ favoriteDocumentIds,
27
+ setFavoriteMembership,
28
+ } from "./_content-favorites.js";
29
+ import { provisionContentSpaces } from "./_content-spaces.js";
23
30
  import { serializeDocumentSource } from "./_document-source.js";
24
31
 
25
32
  // Not (yet) part of the shared API surface — kept local to avoid touching
@@ -137,6 +144,10 @@ function canManageRole(role: string) {
137
144
  return role === "owner" || role === "admin";
138
145
  }
139
146
 
147
+ function canEditRole(role: string) {
148
+ return role === "owner" || role === "admin" || role === "editor";
149
+ }
150
+
140
151
  function builderBodyWithoutImageSourceComponentMarkers(
141
152
  content: string | null | undefined,
142
153
  ) {
@@ -304,11 +315,30 @@ export default defineAction({
304
315
  const isAgentCaller =
305
316
  ctx?.caller === "tool" || ctx?.caller === "mcp" || ctx?.caller === "a2a";
306
317
 
307
- const access = await assertAccess("document", id, "editor");
318
+ const favoriteOnly =
319
+ args.isFavorite !== undefined &&
320
+ args.title === undefined &&
321
+ args.content === undefined &&
322
+ args.description === undefined &&
323
+ args.icon === undefined;
324
+ const access = favoriteOnly
325
+ ? await resolveContentDocumentAccess(id)
326
+ : await assertAccess("document", id, "editor");
327
+ if (!access) throw new Error(`Document "${id}" not found`);
308
328
  const existing = access.resource;
309
329
  const ownerEmail = existing.ownerEmail as string;
310
330
 
311
331
  const db = getDb();
332
+ const requestUserEmail = getRequestUserEmail();
333
+ if (args.isFavorite !== undefined && !requestUserEmail) {
334
+ throw new Error("no authenticated user");
335
+ }
336
+ if (args.isFavorite !== undefined) {
337
+ await provisionContentSpaces(db, requestUserEmail as string);
338
+ }
339
+ const currentFavorite = requestUserEmail
340
+ ? (await favoriteDocumentIds(db, requestUserEmail, [id])).has(id)
341
+ : parseDocumentFavorite(existing.isFavorite);
312
342
 
313
343
  // Strip leading H1 that duplicates the title
314
344
  let content = args.content;
@@ -383,11 +413,12 @@ export default defineAction({
383
413
  content !== undefined && content !== existing.content;
384
414
  const iconChanged = args.icon !== undefined && args.icon !== existing.icon;
385
415
  const favoriteChanged =
386
- args.isFavorite !== undefined &&
387
- (args.isFavorite ? 1 : 0) !== (existing.isFavorite ?? 0);
416
+ args.isFavorite !== undefined && args.isFavorite !== currentFavorite;
388
417
  const descriptionChanged =
389
418
  args.description !== undefined &&
390
419
  args.description.trim() !== existing.description;
420
+ const documentFieldsChanged =
421
+ titleChanged || contentChanged || iconChanged || descriptionChanged;
391
422
  const anyChange =
392
423
  titleChanged ||
393
424
  contentChanged ||
@@ -452,69 +483,127 @@ export default defineAction({
452
483
  updates.description = args.description.trim();
453
484
  if (contentChanged) updates.content = content;
454
485
  if (iconChanged) updates.icon = args.icon;
455
- if (favoriteChanged) updates.isFavorite = args.isFavorite ? 1 : 0;
456
-
457
- if (useContentCas) {
458
- const applied = await db
459
- .update(schema.documents)
460
- .set(updates)
461
- .where(
462
- and(
463
- eq(schema.documents.id, id),
464
- eq(schema.documents.updatedAt, args.baseUpdatedAt as string),
465
- ),
466
- )
467
- .returning({ id: schema.documents.id });
468
-
469
- if (!applied || applied.length === 0) {
470
- // Someone else's write landed after the caller's snapshot. Don't
471
- // apply this save at all (title/icon/favorite included — a partial
472
- // apply would desync the fields from what the caller believes it
473
- // just sent) and hand back the current server row instead so the
474
- // caller can reconcile.
475
- const [current] = await db
476
- .select()
477
- .from(schema.documents)
478
- .where(eq(schema.documents.id, id));
479
- return {
480
- conflict: true,
481
- id,
482
- document: {
483
- id: current.id,
484
- urlPath: `/page/${current.id}`,
485
- parentId: current.parentId,
486
- title: current.title,
487
- content: current.content,
488
- description: current.description,
489
- icon: current.icon,
490
- position: current.position,
491
- isFavorite: parseDocumentFavorite(current.isFavorite),
492
- hideFromSearch: parseDocumentHideFromSearch(
493
- current.hideFromSearch,
486
+ let contentCasConflict = false;
487
+ await db.transaction(async (tx) => {
488
+ if (useContentCas) {
489
+ const applied = await tx
490
+ .update(schema.documents)
491
+ .set(updates)
492
+ .where(
493
+ and(
494
+ eq(schema.documents.id, id),
495
+ eq(schema.documents.updatedAt, args.baseUpdatedAt as string),
494
496
  ),
495
- visibility: current.visibility,
496
- accessRole: access.role,
497
- canEdit: true,
498
- canManage: canManageRole(access.role),
499
- createdAt: current.createdAt,
500
- updatedAt: current.updatedAt,
501
- source: serializeDocumentSource(current),
502
- softDeletedDatabaseIds: [],
503
- },
504
- };
497
+ )
498
+ .returning({ id: schema.documents.id });
499
+ if (!applied || applied.length === 0) {
500
+ contentCasConflict = true;
501
+ return;
502
+ }
503
+ } else if (documentFieldsChanged) {
504
+ await tx
505
+ .update(schema.documents)
506
+ .set(updates)
507
+ .where(eq(schema.documents.id, id));
508
+ }
509
+
510
+ if (favoriteChanged) {
511
+ await setFavoriteMembership({
512
+ db: tx,
513
+ userEmail: requestUserEmail as string,
514
+ documentId: id,
515
+ favorite: args.isFavorite as boolean,
516
+ now: updates.updatedAt as string,
517
+ });
505
518
  }
506
- } else {
507
- await db
508
- .update(schema.documents)
509
- .set(updates)
510
- .where(eq(schema.documents.id, id));
511
- }
512
519
 
513
- if (titleChanged && args.title !== undefined) {
514
- await db
515
- .update(schema.contentDatabases)
516
- .set({ title: args.title, updatedAt: updates.updatedAt as string })
517
- .where(eq(schema.contentDatabases.documentId, id));
520
+ if (titleChanged && args.title !== undefined) {
521
+ const [database] = await tx
522
+ .select({
523
+ id: schema.contentDatabases.id,
524
+ spaceId: schema.contentDatabases.spaceId,
525
+ systemRole: schema.contentDatabases.systemRole,
526
+ })
527
+ .from(schema.contentDatabases)
528
+ .where(eq(schema.contentDatabases.documentId, id));
529
+ if (database) {
530
+ const title =
531
+ database.systemRole === "files"
532
+ ? args.title.trim() || "Untitled"
533
+ : args.title;
534
+ await tx
535
+ .update(schema.contentDatabases)
536
+ .set({ title, updatedAt: updates.updatedAt as string })
537
+ .where(eq(schema.contentDatabases.id, database.id));
538
+ if (database.systemRole === "files" && database.spaceId) {
539
+ await tx
540
+ .update(schema.documents)
541
+ .set({ title, updatedAt: updates.updatedAt as string })
542
+ .where(eq(schema.documents.id, id));
543
+ await tx
544
+ .update(schema.contentSpaces)
545
+ .set({ name: title, updatedAt: updates.updatedAt as string })
546
+ .where(eq(schema.contentSpaces.id, database.spaceId));
547
+ const catalogReferences = await tx
548
+ .select({
549
+ documentId: schema.contentSpaceCatalogItems.documentId,
550
+ })
551
+ .from(schema.contentSpaceCatalogItems)
552
+ .where(
553
+ eq(schema.contentSpaceCatalogItems.spaceId, database.spaceId),
554
+ );
555
+ if (catalogReferences.length > 0) {
556
+ await tx
557
+ .update(schema.documents)
558
+ .set({ title, updatedAt: updates.updatedAt as string })
559
+ .where(
560
+ inArray(
561
+ schema.documents.id,
562
+ catalogReferences.map(
563
+ (reference) => reference.documentId,
564
+ ),
565
+ ),
566
+ );
567
+ }
568
+ }
569
+ }
570
+ }
571
+ });
572
+
573
+ if (contentCasConflict) {
574
+ // Someone else's write landed after the caller's snapshot. Don't
575
+ // apply this save at all (title/icon/favorite included — a partial
576
+ // apply would desync the fields from what the caller believes it
577
+ // just sent) and hand back the current server row instead so the
578
+ // caller can reconcile.
579
+ const [current] = await db
580
+ .select()
581
+ .from(schema.documents)
582
+ .where(eq(schema.documents.id, id));
583
+ return {
584
+ conflict: true,
585
+ id,
586
+ document: {
587
+ id: current.id,
588
+ urlPath: `/page/${current.id}`,
589
+ parentId: current.parentId,
590
+ title: current.title,
591
+ content: current.content,
592
+ description: current.description,
593
+ icon: current.icon,
594
+ position: current.position,
595
+ isFavorite: currentFavorite,
596
+ hideFromSearch: parseDocumentHideFromSearch(current.hideFromSearch),
597
+ visibility: current.visibility,
598
+ accessRole: access.role,
599
+ canEdit: canEditRole(access.role),
600
+ canManage: canManageRole(access.role),
601
+ createdAt: current.createdAt,
602
+ updatedAt: current.updatedAt,
603
+ source: serializeDocumentSource(current),
604
+ softDeletedDatabaseIds: [],
605
+ },
606
+ };
518
607
  }
519
608
 
520
609
  if (contentChanged) {
@@ -570,6 +659,9 @@ export default defineAction({
570
659
  .select()
571
660
  .from(schema.documents)
572
661
  .where(eq(schema.documents.id, id));
662
+ const finalFavorite = requestUserEmail
663
+ ? (await favoriteDocumentIds(db, requestUserEmail, [id])).has(id)
664
+ : parseDocumentFavorite(doc.isFavorite);
573
665
 
574
666
  await writeAppState("refresh-signal", { ts: Date.now() });
575
667
 
@@ -582,11 +674,11 @@ export default defineAction({
582
674
  description: doc.description,
583
675
  icon: doc.icon,
584
676
  position: doc.position,
585
- isFavorite: parseDocumentFavorite(doc.isFavorite),
677
+ isFavorite: finalFavorite,
586
678
  hideFromSearch: parseDocumentHideFromSearch(doc.hideFromSearch),
587
679
  visibility: doc.visibility,
588
680
  accessRole: access.role,
589
- canEdit: true,
681
+ canEdit: canEditRole(access.role),
590
682
  canManage: canManageRole(access.role),
591
683
  createdAt: doc.createdAt,
592
684
  updatedAt: doc.updatedAt,
@@ -1,68 +1,15 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
- import type { Document } from "@shared/api";
3
2
  import { IconFileText, IconPlus } from "@tabler/icons-react";
4
- import { useQueryClient } from "@tanstack/react-query";
5
- import { useNavigate } from "react-router";
6
- import { toast } from "sonner";
7
3
 
8
4
  import { Button } from "@/components/ui/button";
9
- import { useCreateDocument } from "@/hooks/use-documents";
10
-
11
- function nanoid(size = 12): string {
12
- const chars =
13
- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
14
- const bytes = crypto.getRandomValues(new Uint8Array(size));
15
- return Array.from(bytes, (b) => chars[b % chars.length]).join("");
16
- }
5
+ import { useCreatePage } from "@/hooks/use-create-page";
17
6
 
18
7
  export function EmptyState() {
19
- const navigate = useNavigate();
20
- const queryClient = useQueryClient();
21
- const createDocument = useCreateDocument();
8
+ const createPage = useCreatePage();
22
9
  const t = useT();
23
10
 
24
- const handleCreate = async () => {
25
- const id = nanoid();
26
- const now = new Date().toISOString();
27
- const tempDoc: Document = {
28
- id,
29
- parentId: null,
30
- title: "",
31
- content: "",
32
- icon: null,
33
- position: 9999,
34
- isFavorite: false,
35
- hideFromSearch: false,
36
- visibility: "private",
37
- createdAt: now,
38
- updatedAt: now,
39
- };
40
-
41
- // Optimistically inject into cache and navigate immediately
42
- queryClient.setQueryData(
43
- ["action", "list-documents", undefined],
44
- (old: any) => {
45
- const docs: Document[] =
46
- old?.documents ?? (Array.isArray(old) ? old : []);
47
- return { documents: [...docs, tempDoc] };
48
- },
49
- );
50
- queryClient.setQueryData(["action", "get-document", { id }], tempDoc);
51
- navigate(`/page/${id}`, { flushSync: true });
52
-
53
- try {
54
- await createDocument.mutateAsync({ id, title: "" });
55
- } catch (err) {
56
- queryClient.invalidateQueries({ queryKey: ["action", "list-documents"] });
57
- queryClient.removeQueries({
58
- queryKey: ["action", "get-document", { id }],
59
- });
60
- navigate("/");
61
- toast.error(t("empty.createFailed"), {
62
- description:
63
- err instanceof Error ? err.message : t("empty.genericError"),
64
- });
65
- }
11
+ const handleCreate = () => {
12
+ void createPage().catch(() => undefined);
66
13
  };
67
14
 
68
15
  return (
@@ -188,6 +188,7 @@ interface CommentsSidebarProps {
188
188
  onSelectedThreadChange?: (id: string | null) => void;
189
189
  onHoveredThreadChange?: (id: string | null) => void;
190
190
  currentUserEmail?: string;
191
+ forceVisible?: boolean;
191
192
  }
192
193
 
193
194
  export function CommentsSidebar({
@@ -202,6 +203,7 @@ export function CommentsSidebar({
202
203
  onSelectedThreadChange,
203
204
  onHoveredThreadChange,
204
205
  currentUserEmail,
206
+ forceVisible = false,
205
207
  }: CommentsSidebarProps) {
206
208
  const t = useT();
207
209
  const { data: members = [] } = useMentionMembers();
@@ -433,7 +435,7 @@ export function CommentsSidebar({
433
435
 
434
436
  const hasContent =
435
437
  openThreads.length > 0 || !!pendingComment || resolvedThreads.length > 0;
436
- if (!hasContent && !isLoading) return null;
438
+ if (!hasContent && !isLoading && !forceVisible) return null;
437
439
 
438
440
  // Sort open threads by their position in the document.
439
441
  const sortedThreads = [...openThreads].sort((a, b) => {
@@ -488,6 +490,11 @@ export function CommentsSidebar({
488
490
  className="relative w-80 shrink-0 pb-16"
489
491
  data-comments-sidebar
490
492
  >
493
+ {!hasContent && !isLoading ? (
494
+ <div className="px-4 py-8 text-sm text-muted-foreground">
495
+ {t("comments.empty")}
496
+ </div>
497
+ ) : null}
491
498
  {/* Pending new comment — positioned at the selection Y offset */}
492
499
  {pendingComment && (
493
500
  <div