@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
@@ -8,13 +8,9 @@ import {
8
8
  import {
9
9
  setClientAppState,
10
10
  useActionMutation,
11
+ useActionQuery,
11
12
  } from "@agent-native/core/client/hooks";
12
13
  import { useT } from "@agent-native/core/client/i18n";
13
- import {
14
- OrgSwitcher,
15
- useOrg,
16
- useSwitchOrg,
17
- } from "@agent-native/core/client/org";
18
14
  import { FeedbackButton } from "@agent-native/core/client/ui";
19
15
  import {
20
16
  closestCenter,
@@ -31,20 +27,25 @@ import {
31
27
  sortableKeyboardCoordinates,
32
28
  verticalListSortingStrategy,
33
29
  } from "@dnd-kit/sortable";
34
- import type { Document, DocumentTreeNode } from "@shared/api";
30
+ import type {
31
+ ContentDatabaseItem,
32
+ ContentDatabaseResponse,
33
+ Document,
34
+ DocumentTreeNode,
35
+ } from "@shared/api";
36
+ import { CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION } from "@shared/api";
35
37
  import {
36
- IconDatabase,
37
38
  IconBrain,
38
- IconFileText,
39
+ IconFolder,
40
+ IconFolderOpen,
39
41
  IconPlus,
40
42
  IconRestore,
41
43
  IconSearch,
42
- IconStar,
43
44
  IconSettings,
44
45
  IconTrashX,
45
46
  IconLayoutSidebarLeftCollapse,
46
47
  IconLayoutSidebarLeftExpand,
47
- IconFolderOpen,
48
+ IconChevronDown,
48
49
  IconChevronRight,
49
50
  IconDots,
50
51
  IconTrash,
@@ -75,6 +76,14 @@ import {
75
76
  AlertDialogTitle,
76
77
  AlertDialogTrigger,
77
78
  } from "@/components/ui/alert-dialog";
79
+ import {
80
+ Dialog,
81
+ DialogContent,
82
+ DialogDescription,
83
+ DialogFooter,
84
+ DialogHeader,
85
+ DialogTitle,
86
+ } from "@/components/ui/dialog";
78
87
  import {
79
88
  DropdownMenu,
80
89
  DropdownMenuContent,
@@ -82,6 +91,7 @@ import {
82
91
  DropdownMenuSeparator,
83
92
  DropdownMenuTrigger,
84
93
  } from "@/components/ui/dropdown-menu";
94
+ import { Input } from "@/components/ui/input";
85
95
  import { ScrollArea } from "@/components/ui/scroll-area";
86
96
  import {
87
97
  Tooltip,
@@ -89,6 +99,9 @@ import {
89
99
  TooltipTrigger,
90
100
  } from "@/components/ui/tooltip";
91
101
  import {
102
+ applyOptimisticItemToContentDatabase,
103
+ contentDatabaseByIdQueryKey,
104
+ removeOptimisticItemFromContentDatabase,
92
105
  useContentDatabaseById,
93
106
  useContentDatabasePersonalView,
94
107
  useUpdateContentDatabasePersonalView,
@@ -98,8 +111,10 @@ import {
98
111
  useTrashedContentDatabases,
99
112
  } from "@/hooks/use-content-database";
100
113
  import {
114
+ useCreateContentSpace,
101
115
  useContentSpaces,
102
116
  useEnsureContentSpaces,
117
+ type ContentSpaceSummary,
103
118
  } from "@/hooks/use-content-spaces";
104
119
  import {
105
120
  useDocuments,
@@ -117,11 +132,20 @@ import {
117
132
  getDocumentSidebarSections,
118
133
  isDirectLocalDocument,
119
134
  } from "./document-sidebar-sections";
120
- import { DocumentSidebarIcon, DocumentTreeItem } from "./DocumentTreeItem";
135
+ import {
136
+ DocumentSidebarIcon,
137
+ DocumentTreeItem,
138
+ FavoriteDocumentItem,
139
+ } from "./DocumentTreeItem";
121
140
  import { NotionButton } from "./NotionButton";
122
141
  import {
123
- contentSpaceForActiveOrg,
142
+ contentSpaceAvailability,
143
+ contentSpaceForStoredSelection,
144
+ createContentSpaceSelectionQueue,
145
+ ensureWorkspaceExpanded,
146
+ SELECTED_CONTENT_SPACE_STORAGE_KEY,
124
147
  selectContentSpace,
148
+ toggleExpandedWorkspaceIds,
125
149
  } from "./select-content-space";
126
150
 
127
151
  function nanoid(size = 12): string {
@@ -177,6 +201,7 @@ function collectDocumentSubtreeIds(documents: Document[], rootId: string) {
177
201
  }
178
202
 
179
203
  type SidebarSectionId =
204
+ | "favorites"
180
205
  | "local-files"
181
206
  | "shared-copies"
182
207
  | "private"
@@ -187,9 +212,9 @@ type CollapsedSectionsState = Record<SidebarSectionId, boolean>;
187
212
 
188
213
  const SIDEBAR_SECTION_COLLAPSE_STORAGE_KEY =
189
214
  "content-sidebar-collapsed-sections";
190
- const SELECTED_CONTENT_SPACE_STORAGE_KEY = "content-selected-space";
191
-
215
+ const CONTENT_SIDEBAR_STATE_VERSION = 1 as const;
192
216
  const DEFAULT_COLLAPSED_SECTIONS: CollapsedSectionsState = {
217
+ favorites: false,
193
218
  "local-files": false,
194
219
  "shared-copies": false,
195
220
  private: false,
@@ -201,6 +226,7 @@ function normalizeCollapsedSections(
201
226
  value: Partial<Record<SidebarSectionId, boolean>> | null | undefined,
202
227
  ): CollapsedSectionsState {
203
228
  return {
229
+ favorites: value?.favorites ?? false,
204
230
  "local-files": value?.["local-files"] ?? false,
205
231
  "shared-copies": value?.["shared-copies"] ?? false,
206
232
  private: value?.private ?? false,
@@ -214,6 +240,99 @@ interface RemoveLocalFileSourceResult {
214
240
  deleted: number;
215
241
  }
216
242
 
243
+ function WorkspaceFilesSection({
244
+ space,
245
+ selected,
246
+ activeDocumentId,
247
+ expandedDocumentIds,
248
+ onDocumentExpandedChange,
249
+ onActivate,
250
+ onCreateChildPage,
251
+ onCreateChildDatabase,
252
+ onDeleteItem,
253
+ onToggleFavorite,
254
+ }: {
255
+ space: ContentSpaceSummary;
256
+ selected: boolean;
257
+ activeDocumentId: string | null;
258
+ expandedDocumentIds: ReadonlySet<string>;
259
+ onDocumentExpandedChange: (documentId: string, expanded: boolean) => void;
260
+ onActivate: (space: ContentSpaceSummary, documentId?: string) => void;
261
+ onCreateChildPage: (
262
+ space: ContentSpaceSummary,
263
+ item: ContentDatabaseItem,
264
+ ) => void;
265
+ onCreateChildDatabase: (
266
+ space: ContentSpaceSummary,
267
+ item: ContentDatabaseItem,
268
+ ) => void;
269
+ onDeleteItem: (item: ContentDatabaseItem) => void;
270
+ onToggleFavorite: (item: ContentDatabaseItem) => void;
271
+ }) {
272
+ const t = useT();
273
+ const filesDatabase = useContentDatabaseById(space.filesDatabaseId);
274
+ const filesPersonalView = useContentDatabasePersonalView(
275
+ space.filesDatabaseId,
276
+ );
277
+ const updateFilesPersonalView = useUpdateContentDatabasePersonalView(
278
+ space.filesDatabaseId,
279
+ );
280
+ const failed = filesDatabase.isError || filesPersonalView.isError;
281
+
282
+ return (
283
+ <div className="ms-3 border-s border-border/70 pb-1 ps-1">
284
+ {failed ? (
285
+ <QueryErrorState
286
+ compact
287
+ onRetry={() => {
288
+ void filesDatabase.refetch();
289
+ void filesPersonalView.refetch();
290
+ }}
291
+ retrying={filesDatabase.isFetching || filesPersonalView.isFetching}
292
+ />
293
+ ) : (
294
+ <ContentFilesSidebarView
295
+ data={filesDatabase.data}
296
+ overrides={filesPersonalView.data?.overrides}
297
+ isLoading={filesDatabase.isLoading || filesPersonalView.isLoading}
298
+ activeDocumentId={activeDocumentId}
299
+ expandedDocumentIds={expandedDocumentIds}
300
+ onDocumentExpandedChange={onDocumentExpandedChange}
301
+ onSelectView={(viewId) => {
302
+ const current = filesPersonalView.data?.overrides;
303
+ updateFilesPersonalView.mutate({
304
+ databaseId: space.filesDatabaseId,
305
+ overrides: {
306
+ version:
307
+ current?.version ??
308
+ CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION,
309
+ activeViewId: viewId,
310
+ views: current?.views ?? [],
311
+ },
312
+ });
313
+ }}
314
+ onOpenItem={(item: ContentDatabaseItem) => {
315
+ if (selected) return false;
316
+ onActivate(space, item.document.id);
317
+ return true;
318
+ }}
319
+ onCreateChildPage={(item) => onCreateChildPage(space, item)}
320
+ onCreateChildDatabase={(item) => onCreateChildDatabase(space, item)}
321
+ onDeleteItem={onDeleteItem}
322
+ onToggleFavorite={onToggleFavorite}
323
+ labels={{
324
+ loadingLabel: t("sidebar.loadingFiles"),
325
+ noMatchesLabel: t("database.noRowsMatchThisView"),
326
+ clearLabel: t("database.clearSearchAndFilters"),
327
+ navigationLabel: `${space.name} ${t("sidebar.files")}`,
328
+ untitledLabel: t("sidebar.untitled"),
329
+ }}
330
+ />
331
+ )}
332
+ </div>
333
+ );
334
+ }
335
+
217
336
  export function DocumentSidebar({
218
337
  activeDocumentId,
219
338
  collapsed,
@@ -238,10 +357,22 @@ export function DocumentSidebar({
238
357
  const { isCodeMode } = useCodeMode();
239
358
  const updateDocument = useUpdateDocument();
240
359
  const contentSpacesQuery = useContentSpaces();
360
+ const createContentSpace = useCreateContentSpace();
241
361
  const ensureContentSpaces = useEnsureContentSpaces();
242
- const { data: activeOrg } = useOrg();
243
- const switchOrg = useSwitchOrg();
362
+ const workspaceSelectionQueueRef = useRef(createContentSpaceSelectionQueue());
244
363
  const contentSpaces = contentSpacesQuery.data?.spaces ?? [];
364
+ const workspaceCatalogDatabaseId =
365
+ contentSpacesQuery.data?.catalogDatabaseId ?? null;
366
+ const favoritesDocumentId =
367
+ contentSpacesQuery.data?.favoritesDocumentId ?? null;
368
+ const workspaceCatalogDatabase = useContentDatabaseById(
369
+ workspaceCatalogDatabaseId,
370
+ );
371
+ const workspaceCatalogPersonalView = useContentDatabasePersonalView(
372
+ workspaceCatalogDatabaseId,
373
+ );
374
+ const updateWorkspaceCatalogPersonalView =
375
+ useUpdateContentDatabasePersonalView(workspaceCatalogDatabaseId);
245
376
  const spaceProvisionAttemptedRef = useRef(false);
246
377
  useEffect(() => {
247
378
  if (
@@ -261,31 +392,215 @@ export function DocumentSidebar({
261
392
  SELECTED_CONTENT_SPACE_STORAGE_KEY,
262
393
  null,
263
394
  );
264
- const selectedSpace = contentSpaceForActiveOrg({
395
+ const selectedSpace = contentSpaceForStoredSelection({
265
396
  spaces: contentSpaces,
266
397
  storedSpaceId,
267
- activeOrgId: activeOrg?.orgId,
268
398
  });
399
+ const sidebarStateQuery = useActionQuery("get-content-sidebar-state", {});
400
+ const updateSidebarState = useActionMutation("update-content-sidebar-state", {
401
+ skipActionQueryInvalidation: true,
402
+ onSuccess: (data) => {
403
+ queryClient.setQueryData(
404
+ ["action", "get-content-sidebar-state", {}],
405
+ data,
406
+ );
407
+ },
408
+ });
409
+ const [expandedWorkspaceIds, setExpandedWorkspaceIds] = useState<string[]>(
410
+ [],
411
+ );
412
+ const [expandedDocumentIds, setExpandedDocumentIds] = useState<string[]>([]);
413
+ const expandedDocumentIdSet = useMemo(
414
+ () => new Set(expandedDocumentIds),
415
+ [expandedDocumentIds],
416
+ );
417
+ const sidebarStateHydratedRef = useRef(false);
418
+ const expandedWorkspaceIdsRef = useRef<string[]>([]);
419
+ const expandedDocumentIdsRef = useRef<string[]>([]);
420
+ const sidebarStateWriteTimerRef = useRef<ReturnType<
421
+ typeof setTimeout
422
+ > | null>(null);
423
+ const [createWorkspaceDialogOpen, setCreateWorkspaceDialogOpen] =
424
+ useState(false);
425
+ const [newWorkspaceName, setNewWorkspaceName] = useState("");
426
+ const createWorkspaceRequestIdRef = useRef<string | null>(null);
427
+ const contentSpaceState = contentSpaceAvailability({
428
+ hasSelectedSpace: Boolean(selectedSpace),
429
+ contentSpacesLoading: contentSpacesQuery.isLoading,
430
+ contentSpacesFetching: contentSpacesQuery.isFetching,
431
+ contentSpacesError: contentSpacesQuery.isError,
432
+ provisioningAttempted: spaceProvisionAttemptedRef.current,
433
+ provisioningPending: ensureContentSpaces.isPending,
434
+ provisioningError: ensureContentSpaces.isError,
435
+ });
436
+ const handleRetryContentSpaces = useCallback(() => {
437
+ if (contentSpacesQuery.isError) {
438
+ spaceProvisionAttemptedRef.current = false;
439
+ void contentSpacesQuery.refetch();
440
+ return;
441
+ }
442
+ spaceProvisionAttemptedRef.current = true;
443
+ ensureContentSpaces.mutate({});
444
+ }, [contentSpacesQuery, ensureContentSpaces]);
269
445
  useEffect(() => {
270
446
  if (selectedSpace && selectedSpace.id !== storedSpaceId) {
271
447
  setStoredSpaceId(selectedSpace.id);
272
448
  }
273
449
  }, [selectedSpace, setStoredSpaceId, storedSpaceId]);
450
+ useEffect(() => {
451
+ if (
452
+ sidebarStateHydratedRef.current ||
453
+ !contentSpacesQuery.isSuccess ||
454
+ sidebarStateQuery.isLoading
455
+ ) {
456
+ return;
457
+ }
458
+ const stored = sidebarStateQuery.data?.state;
459
+ const workspaceIds =
460
+ stored?.expandedWorkspaceIds ?? contentSpaces.map((space) => space.id);
461
+ const documentIds = stored?.expandedDocumentIds ?? [];
462
+ expandedWorkspaceIdsRef.current = workspaceIds;
463
+ expandedDocumentIdsRef.current = documentIds;
464
+ setExpandedWorkspaceIds(workspaceIds);
465
+ setExpandedDocumentIds(documentIds);
466
+ sidebarStateHydratedRef.current = true;
467
+ }, [
468
+ contentSpaces,
469
+ contentSpacesQuery.isSuccess,
470
+ sidebarStateQuery.data?.state,
471
+ sidebarStateQuery.isLoading,
472
+ ]);
473
+
474
+ const queueSidebarStateWrite = useCallback(
475
+ (workspaceIds: string[], documentIds: string[]) => {
476
+ if (!sidebarStateHydratedRef.current) return;
477
+ if (sidebarStateWriteTimerRef.current) {
478
+ clearTimeout(sidebarStateWriteTimerRef.current);
479
+ }
480
+ sidebarStateWriteTimerRef.current = setTimeout(() => {
481
+ sidebarStateWriteTimerRef.current = null;
482
+ updateSidebarState.mutate({
483
+ version: CONTENT_SIDEBAR_STATE_VERSION,
484
+ expandedWorkspaceIds: workspaceIds,
485
+ expandedDocumentIds: documentIds,
486
+ });
487
+ }, 150);
488
+ },
489
+ [updateSidebarState],
490
+ );
491
+
492
+ const updateExpandedWorkspaceIds = useCallback(
493
+ (update: (current: string[]) => string[]) => {
494
+ setExpandedWorkspaceIds((current) => {
495
+ const next = update(current);
496
+ if (next === current) return current;
497
+ expandedWorkspaceIdsRef.current = next;
498
+ queueSidebarStateWrite(next, expandedDocumentIdsRef.current);
499
+ return next;
500
+ });
501
+ },
502
+ [queueSidebarStateWrite],
503
+ );
504
+
505
+ const handleDocumentExpandedChange = useCallback(
506
+ (documentId: string, expanded: boolean) => {
507
+ setExpandedDocumentIds((current) => {
508
+ const nextSet = new Set(current);
509
+ if (expanded) nextSet.add(documentId);
510
+ else nextSet.delete(documentId);
511
+ const next = [...nextSet];
512
+ expandedDocumentIdsRef.current = next;
513
+ queueSidebarStateWrite(expandedWorkspaceIdsRef.current, next);
514
+ return next;
515
+ });
516
+ },
517
+ [queueSidebarStateWrite],
518
+ );
519
+
520
+ useEffect(
521
+ () => () => {
522
+ if (sidebarStateWriteTimerRef.current) {
523
+ clearTimeout(sidebarStateWriteTimerRef.current);
524
+ }
525
+ },
526
+ [],
527
+ );
528
+
529
+ useEffect(() => {
530
+ if (!selectedSpace || !sidebarStateHydratedRef.current) return;
531
+ updateExpandedWorkspaceIds((current) =>
532
+ ensureWorkspaceExpanded(current, selectedSpace.id),
533
+ );
534
+ }, [selectedSpace, updateExpandedWorkspaceIds]);
274
535
  const handleSelectContentSpace = useCallback(
275
- async (space: (typeof contentSpaces)[number]) => {
536
+ async (
537
+ space: (typeof contentSpaces)[number],
538
+ targetDocumentId?: string | null,
539
+ ) => {
540
+ updateExpandedWorkspaceIds((current) =>
541
+ ensureWorkspaceExpanded(current, space.id),
542
+ );
276
543
  try {
277
- await selectContentSpace({
278
- space,
279
- activeOrgId: activeOrg?.orgId,
280
- switchOrg: (orgId) => switchOrg.mutateAsync(orgId),
281
- persistSelection: setStoredSpaceId,
282
- });
544
+ await workspaceSelectionQueueRef.current(() =>
545
+ selectContentSpace({
546
+ space,
547
+ syncApplicationState: (selected) =>
548
+ setClientAppState(
549
+ "content-space",
550
+ {
551
+ spaceId: selected.id,
552
+ name: selected.name,
553
+ kind: selected.kind,
554
+ filesDatabaseId: selected.filesDatabaseId,
555
+ },
556
+ { requestSource: "content-sidebar" },
557
+ ),
558
+ persistSelection: setStoredSpaceId,
559
+ openFiles: (documentId) => {
560
+ if (targetDocumentId === null) return;
561
+ navigate(`/page/${targetDocumentId ?? documentId}`, {
562
+ flushSync: true,
563
+ });
564
+ },
565
+ }),
566
+ );
567
+ return true;
283
568
  } catch (error) {
284
569
  toast.error(error instanceof Error ? error.message : String(error));
570
+ return false;
285
571
  }
286
572
  },
287
- [activeOrg?.orgId, setStoredSpaceId, switchOrg, t],
573
+ [navigate, setStoredSpaceId, updateExpandedWorkspaceIds],
288
574
  );
575
+ const handleCreateWorkspace = useCallback(async () => {
576
+ const name = newWorkspaceName.trim();
577
+ if (!name) return;
578
+ const requestId = createWorkspaceRequestIdRef.current ?? nanoid();
579
+ createWorkspaceRequestIdRef.current = requestId;
580
+ try {
581
+ const created = await createContentSpace.mutateAsync({ name, requestId });
582
+ const space: ContentSpaceSummary = {
583
+ id: created.spaceId,
584
+ name: created.name,
585
+ kind: created.kind,
586
+ filesDatabaseId: created.filesDatabaseId,
587
+ filesDocumentId: created.filesDocumentId,
588
+ orgId: null,
589
+ role: "owner",
590
+ catalogItemId: created.catalogItemId,
591
+ catalogDocumentId: created.catalogDocumentId,
592
+ };
593
+ const selected = await handleSelectContentSpace(space);
594
+ if (!selected) return;
595
+ setCreateWorkspaceDialogOpen(false);
596
+ setNewWorkspaceName("");
597
+ createWorkspaceRequestIdRef.current = null;
598
+ } catch (error) {
599
+ toast.error(t("sidebar.failedCreateWorkspace"), {
600
+ description: error instanceof Error ? error.message : String(error),
601
+ });
602
+ }
603
+ }, [createContentSpace, handleSelectContentSpace, newWorkspaceName, t]);
289
604
  useEffect(() => {
290
605
  if (!selectedSpace) return;
291
606
  void setClientAppState(
@@ -301,15 +616,6 @@ export function DocumentSidebar({
301
616
  // Space selection remains usable when best-effort agent context sync fails.
302
617
  });
303
618
  }, [selectedSpace]);
304
- const filesDatabase = useContentDatabaseById(
305
- selectedSpace?.filesDatabaseId ?? null,
306
- );
307
- const filesPersonalView = useContentDatabasePersonalView(
308
- selectedSpace?.filesDatabaseId ?? null,
309
- );
310
- const updateFilesPersonalView = useUpdateContentDatabasePersonalView(
311
- selectedSpace?.filesDatabaseId ?? null,
312
- );
313
619
  const removeLocalFileSource = useActionMutation<
314
620
  RemoveLocalFileSourceResult,
315
621
  { sourceRootPath?: string | null }
@@ -330,7 +636,6 @@ export function DocumentSidebar({
330
636
  );
331
637
  const [removeLocalFilesDialogOpen, setRemoveLocalFilesDialogOpen] =
332
638
  useState(false);
333
- const localFilesActive = location.pathname.startsWith("/local-files");
334
639
  const agentActive = location.pathname.startsWith("/agent");
335
640
  const settingsActive = location.pathname.startsWith("/settings");
336
641
  const sensors = useSensors(
@@ -390,7 +695,7 @@ export function DocumentSidebar({
390
695
  // Match the tree rows' right-side inset so favorite titles clip inside the
391
696
  // visible sidebar instead of widening the scroll surface.
392
697
  const favoriteRowWidth =
393
- width === undefined ? undefined : Math.max(224, width - 8);
698
+ width === undefined ? undefined : Math.max(208, width - 24);
394
699
  const activeDocument = activeDocumentId
395
700
  ? documents.find((doc) => doc.id === activeDocumentId)
396
701
  : null;
@@ -436,13 +741,18 @@ export function DocumentSidebar({
436
741
  );
437
742
 
438
743
  const handleCreatePage = useCallback(
439
- async (parentId?: string) => {
744
+ async (
745
+ parentId?: string,
746
+ rootSpaceId = selectedSpace?.id,
747
+ optimisticId?: string,
748
+ rootFilesDatabaseId?: string,
749
+ ) => {
440
750
  if (localFileMode) {
441
751
  try {
442
752
  const created = await createDocument.mutateAsync({
443
753
  title: "",
444
754
  parentId: parentId ?? undefined,
445
- spaceId: selectedSpace?.id,
755
+ spaceId: parentId ? undefined : rootSpaceId,
446
756
  });
447
757
  queryClient.setQueryData(
448
758
  ["action", "get-document", { id: created.id }],
@@ -462,7 +772,7 @@ export function DocumentSidebar({
462
772
  return;
463
773
  }
464
774
 
465
- const id = nanoid();
775
+ const id = optimisticId ?? nanoid();
466
776
  const now = new Date().toISOString();
467
777
  const tempDoc: Document = {
468
778
  id,
@@ -488,6 +798,20 @@ export function DocumentSidebar({
488
798
  return { documents: [...docs, tempDoc] };
489
799
  });
490
800
  queryClient.setQueryData(["action", "get-document", { id }], tempDoc);
801
+ if (rootFilesDatabaseId) {
802
+ const optimisticItem: ContentDatabaseItem = {
803
+ id: `optimistic-${id}`,
804
+ databaseId: rootFilesDatabaseId,
805
+ document: tempDoc,
806
+ position: tempDoc.position,
807
+ properties: [],
808
+ };
809
+ queryClient.setQueryData<ContentDatabaseResponse>(
810
+ contentDatabaseByIdQueryKey(rootFilesDatabaseId),
811
+ (current) =>
812
+ applyOptimisticItemToContentDatabase(current, optimisticItem),
813
+ );
814
+ }
491
815
 
492
816
  navigateToDocument(id);
493
817
  onNavigate?.();
@@ -497,7 +821,7 @@ export function DocumentSidebar({
497
821
  id,
498
822
  title: "",
499
823
  parentId: parentId ?? undefined,
500
- spaceId: selectedSpace?.id,
824
+ spaceId: parentId ? undefined : rootSpaceId,
501
825
  });
502
826
  const nextId = created?.id || id;
503
827
  if (nextId !== id) {
@@ -518,6 +842,11 @@ export function DocumentSidebar({
518
842
  queryClient.invalidateQueries({
519
843
  queryKey: ["action", "list-documents"],
520
844
  });
845
+ if (rootFilesDatabaseId) {
846
+ queryClient.invalidateQueries({
847
+ queryKey: contentDatabaseByIdQueryKey(rootFilesDatabaseId),
848
+ });
849
+ }
521
850
  } catch (err) {
522
851
  // Revert optimistic updates
523
852
  queryClient.invalidateQueries({
@@ -526,6 +855,12 @@ export function DocumentSidebar({
526
855
  queryClient.removeQueries({
527
856
  queryKey: ["action", "get-document", { id }],
528
857
  });
858
+ if (rootFilesDatabaseId) {
859
+ queryClient.setQueryData<ContentDatabaseResponse>(
860
+ contentDatabaseByIdQueryKey(rootFilesDatabaseId),
861
+ (current) => removeOptimisticItemFromContentDatabase(current, id),
862
+ );
863
+ }
529
864
  navigate("/");
530
865
  toast.error(t("sidebar.failedCreatePage"), {
531
866
  description:
@@ -545,11 +880,11 @@ export function DocumentSidebar({
545
880
  );
546
881
 
547
882
  const handleCreateDatabase = useCallback(
548
- async (parentId?: string | null) => {
883
+ async (parentId?: string | null, rootSpaceId = selectedSpace?.id) => {
549
884
  try {
550
885
  const result = await createDatabase.mutateAsync({
551
886
  parentId: parentId ?? null,
552
- spaceId: parentId ? undefined : selectedSpace?.id,
887
+ spaceId: parentId ? undefined : rootSpaceId,
553
888
  title: t("editor.untitledDatabase"),
554
889
  });
555
890
  navigateToDocument(result.database.documentId);
@@ -564,6 +899,33 @@ export function DocumentSidebar({
564
899
  [createDatabase, navigateToDocument, onNavigate, selectedSpace?.id, t],
565
900
  );
566
901
 
902
+ const handleCreatePageInSpace = useCallback(
903
+ async (space: ContentSpaceSummary) => {
904
+ const id = nanoid();
905
+ if (selectedSpace?.id !== space.id) {
906
+ void handleSelectContentSpace(space, null);
907
+ }
908
+ await handleCreatePage(undefined, space.id, id, space.filesDatabaseId);
909
+ },
910
+ [handleCreatePage, handleSelectContentSpace, selectedSpace?.id],
911
+ );
912
+
913
+ const handleOpenFavorite = useCallback(
914
+ (document: Document) => {
915
+ const space = contentSpaces.find(
916
+ (candidate) =>
917
+ candidate.filesDocumentId ===
918
+ document.databaseMembership?.databaseDocumentId,
919
+ );
920
+ if (space) {
921
+ void handleSelectContentSpace(space, document.id);
922
+ return;
923
+ }
924
+ navigateToDocument(document.id);
925
+ },
926
+ [contentSpaces, handleSelectContentSpace, navigateToDocument],
927
+ );
928
+
567
929
  const handleDelete = useCallback(
568
930
  async (id: string) => {
569
931
  const deletedDocument = documents.find((doc) => doc.id === id) ?? null;
@@ -727,9 +1089,21 @@ export function DocumentSidebar({
727
1089
 
728
1090
  const handleToggleFavorite = useCallback(
729
1091
  (id: string, isFavorite: boolean) => {
730
- updateDocument.mutate({ id, isFavorite });
1092
+ updateDocument.mutate(
1093
+ { id, isFavorite },
1094
+ {
1095
+ onError: (error) => {
1096
+ toast.error(t("sidebar.failedUpdateFavorite"), {
1097
+ description:
1098
+ error instanceof Error
1099
+ ? error.message
1100
+ : t("empty.genericError"),
1101
+ });
1102
+ },
1103
+ },
1104
+ );
731
1105
  },
732
- [updateDocument],
1106
+ [t, updateDocument],
733
1107
  );
734
1108
 
735
1109
  const handleRestoreDatabase = useCallback(
@@ -821,33 +1195,17 @@ export function DocumentSidebar({
821
1195
  </SortableContext>
822
1196
  );
823
1197
 
824
- const renderNewButton = () =>
825
- selectedSpace ? (
826
- <div className="flex items-center gap-1">
827
- <button
828
- type="button"
829
- className="flex min-w-0 flex-1 items-center gap-2 rounded-md px-3 py-[5px] text-sm text-muted-foreground hover:bg-accent/50 hover:text-foreground"
830
- disabled={createDocument.isPending}
831
- onClick={() => void handleCreatePage()}
832
- >
833
- <IconPlus size={14} className="shrink-0" />
834
- <span>{t("sidebar.newPage")}</span>
835
- </button>
836
- <Tooltip>
837
- <TooltipTrigger asChild>
838
- <button
839
- type="button"
840
- className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/50 hover:text-foreground"
841
- disabled={createDatabase.isPending}
842
- aria-label={t("sidebar.newDatabase")}
843
- onClick={() => void handleCreateDatabase()}
844
- >
845
- <IconDatabase size={14} />
846
- </button>
847
- </TooltipTrigger>
848
- <TooltipContent>{t("sidebar.newDatabase")}</TooltipContent>
849
- </Tooltip>
850
- </div>
1198
+ const renderNewButton = (space = selectedSpace) =>
1199
+ space ? (
1200
+ <button
1201
+ type="button"
1202
+ className="flex w-full min-w-0 items-center gap-2 rounded-md px-3 py-[5px] text-sm text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1203
+ disabled={createDocument.isPending}
1204
+ onClick={() => void handleCreatePageInSpace(space)}
1205
+ >
1206
+ <IconPlus size={14} className="shrink-0" />
1207
+ <span>{t("sidebar.newPage")}</span>
1208
+ </button>
851
1209
  ) : null;
852
1210
 
853
1211
  const renderCollapsedNewButton = () =>
@@ -858,7 +1216,7 @@ export function DocumentSidebar({
858
1216
  type="button"
859
1217
  className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-accent text-muted-foreground hover:text-foreground"
860
1218
  disabled={createDocument.isPending}
861
- onClick={() => void handleCreatePage()}
1219
+ onClick={() => void handleCreatePageInSpace(selectedSpace)}
862
1220
  >
863
1221
  <IconPlus size={16} />
864
1222
  </button>
@@ -867,23 +1225,6 @@ export function DocumentSidebar({
867
1225
  </Tooltip>
868
1226
  ) : null;
869
1227
 
870
- const renderLocalFilesNavButton = () => (
871
- <Link
872
- to="/local-files"
873
- className={cn(
874
- "flex h-8 w-full items-center gap-2 rounded-md px-2 text-sm",
875
- localFilesActive
876
- ? "bg-accent text-accent-foreground"
877
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
878
- )}
879
- >
880
- <IconFolderOpen size={15} className="shrink-0" />
881
- <span className="min-w-0 flex-1 truncate text-start">
882
- {t("sidebar.localFiles")}
883
- </span>
884
- </Link>
885
- );
886
-
887
1228
  const renderSettingsNavButton = () => (
888
1229
  <Link
889
1230
  to="/settings"
@@ -1065,69 +1406,200 @@ export function DocumentSidebar({
1065
1406
  );
1066
1407
  };
1067
1408
 
1068
- const renderWorkspaceNavigation = () => (
1069
- <div className="mb-2 space-y-1 px-2">
1070
- <div className="px-1 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
1071
- {t("sidebar.workspaces")}
1072
- </div>
1073
- {contentSpaces.map((space) => (
1074
- <button
1075
- key={space.id}
1076
- type="button"
1409
+ const renderWorkspaceRoot = (space: ContentSpaceSummary) => {
1410
+ const selected = selectedSpace?.id === space.id;
1411
+ const expanded = expandedWorkspaceIds.includes(space.id);
1412
+ return (
1413
+ <div className="min-w-0">
1414
+ <div
1077
1415
  className={cn(
1078
- "flex h-8 w-full min-w-0 items-center rounded-md px-2 text-sm",
1079
- selectedSpace?.id === space.id
1080
- ? "bg-accent text-accent-foreground"
1081
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
1416
+ "group/workspace-header flex h-7 w-full min-w-0 items-center rounded-md",
1417
+ selected
1418
+ ? "text-foreground"
1419
+ : "text-muted-foreground hover:bg-accent/40 hover:text-foreground",
1082
1420
  )}
1083
- onClick={() => void handleSelectContentSpace(space)}
1084
- disabled={switchOrg.isPending}
1085
1421
  >
1086
- <span className="min-w-0 flex-1 truncate text-start">
1422
+ <button
1423
+ type="button"
1424
+ aria-expanded={expanded}
1425
+ aria-label={`${expanded ? t("sidebar.collapse") : t("sidebar.expand")} ${space.name}`}
1426
+ className="relative flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-background/60"
1427
+ onClick={() =>
1428
+ updateExpandedWorkspaceIds((current) =>
1429
+ toggleExpandedWorkspaceIds(current, space.id),
1430
+ )
1431
+ }
1432
+ >
1433
+ <span className="group-hover/workspace-header:opacity-0 group-focus-within/workspace-header:opacity-0">
1434
+ {expanded ? (
1435
+ <IconFolderOpen size={14} />
1436
+ ) : (
1437
+ <IconFolder size={14} />
1438
+ )}
1439
+ </span>
1440
+ <span className="absolute inset-0 flex items-center justify-center opacity-0 group-hover/workspace-header:opacity-100 group-focus-within/workspace-header:opacity-100">
1441
+ {expanded ? (
1442
+ <IconChevronDown size={14} />
1443
+ ) : (
1444
+ <IconChevronRight size={14} />
1445
+ )}
1446
+ </span>
1447
+ </button>
1448
+ <button
1449
+ type="button"
1450
+ className="h-7 min-w-0 flex-1 truncate pe-2 text-start text-[10px] font-semibold uppercase tracking-wider"
1451
+ onClick={() => void handleSelectContentSpace(space)}
1452
+ >
1087
1453
  {space.name}
1088
- </span>
1089
- </button>
1090
- ))}
1091
- </div>
1092
- );
1093
-
1094
- const renderFilesDatabase = () => (
1095
- <div className="min-w-0">
1096
- <div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
1097
- {t("sidebar.files")}
1098
- </div>
1099
- {selectedSpace ? (
1100
- <>
1101
- <ContentFilesSidebarView
1102
- data={filesDatabase.data}
1103
- overrides={filesPersonalView.data?.overrides}
1104
- isLoading={filesDatabase.isLoading || filesPersonalView.isLoading}
1105
- onSelectView={(viewId) => {
1106
- if (!selectedSpace) return;
1107
- const current = filesPersonalView.data?.overrides;
1108
- updateFilesPersonalView.mutate({
1109
- databaseId: selectedSpace.filesDatabaseId,
1110
- overrides: {
1111
- version: current?.version ?? 1,
1112
- activeViewId: viewId,
1113
- views: current?.views ?? [],
1114
- },
1115
- });
1116
- }}
1117
- labels={{
1118
- loadingLabel: t("sidebar.loadingFiles"),
1119
- noMatchesLabel: t("database.noRowsMatchThisView"),
1120
- clearLabel: t("database.clearSearchAndFilters"),
1121
- navigationLabel: t("sidebar.files"),
1122
- untitledLabel: t("sidebar.untitled"),
1123
- }}
1454
+ </button>
1455
+ <button
1456
+ type="button"
1457
+ className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background/60 hover:text-foreground disabled:opacity-50"
1458
+ disabled={createDocument.isPending}
1459
+ aria-label={`${t("sidebar.newPage")} — ${space.name}`}
1460
+ onClick={() => void handleCreatePageInSpace(space)}
1461
+ >
1462
+ <IconPlus size={14} />
1463
+ </button>
1464
+ </div>
1465
+ {expanded && (
1466
+ <WorkspaceFilesSection
1467
+ space={space}
1468
+ selected={selected}
1469
+ activeDocumentId={activeDocumentId}
1470
+ expandedDocumentIds={expandedDocumentIdSet}
1471
+ onDocumentExpandedChange={handleDocumentExpandedChange}
1472
+ onActivate={(nextSpace, documentId) =>
1473
+ void handleSelectContentSpace(nextSpace, documentId)
1474
+ }
1475
+ onCreateChildPage={(nextSpace, item) =>
1476
+ void handleCreatePage(
1477
+ item.document.id,
1478
+ nextSpace.id,
1479
+ undefined,
1480
+ nextSpace.filesDatabaseId,
1481
+ )
1482
+ }
1483
+ onCreateChildDatabase={(nextSpace, item) =>
1484
+ void handleCreateDatabase(item.document.id, nextSpace.id)
1485
+ }
1486
+ onDeleteItem={(item) => void handleDelete(item.document.id)}
1487
+ onToggleFavorite={(item) =>
1488
+ handleToggleFavorite(item.document.id, !item.document.isFavorite)
1489
+ }
1124
1490
  />
1125
- {renderNewButton()}
1126
- </>
1127
- ) : (
1491
+ )}
1492
+ </div>
1493
+ );
1494
+ };
1495
+
1496
+ const renderWorkspaceNavigation = () => (
1497
+ <div className="mb-2 min-w-0 overflow-x-hidden px-2">
1498
+ {contentSpaceState === "ready" && selectedSpace ? (
1499
+ <div className="grid gap-1">
1500
+ {workspaceCatalogDatabase.isError ||
1501
+ workspaceCatalogPersonalView.isError ? (
1502
+ <QueryErrorState
1503
+ compact
1504
+ onRetry={() => {
1505
+ void workspaceCatalogDatabase.refetch();
1506
+ void workspaceCatalogPersonalView.refetch();
1507
+ }}
1508
+ retrying={
1509
+ workspaceCatalogDatabase.isFetching ||
1510
+ workspaceCatalogPersonalView.isFetching
1511
+ }
1512
+ />
1513
+ ) : (
1514
+ <ContentFilesSidebarView
1515
+ data={workspaceCatalogDatabase.data}
1516
+ overrides={workspaceCatalogPersonalView.data?.overrides}
1517
+ isLoading={
1518
+ workspaceCatalogDatabase.isLoading ||
1519
+ workspaceCatalogPersonalView.isLoading
1520
+ }
1521
+ onSelectView={(viewId) => {
1522
+ if (!workspaceCatalogDatabaseId) return;
1523
+ const current = workspaceCatalogPersonalView.data?.overrides;
1524
+ updateWorkspaceCatalogPersonalView.mutate({
1525
+ databaseId: workspaceCatalogDatabaseId,
1526
+ overrides: {
1527
+ version:
1528
+ current?.version ??
1529
+ CONTENT_DATABASE_PERSONAL_VIEW_OVERRIDES_VERSION,
1530
+ activeViewId: viewId,
1531
+ views: current?.views ?? [],
1532
+ },
1533
+ });
1534
+ }}
1535
+ renderItem={(item) => {
1536
+ const space = contentSpaces.find(
1537
+ (candidate) =>
1538
+ candidate.catalogDocumentId === item.document.id,
1539
+ );
1540
+ return space
1541
+ ? renderWorkspaceRoot({
1542
+ ...space,
1543
+ name: item.document.title || space.name,
1544
+ })
1545
+ : null;
1546
+ }}
1547
+ scroll={false}
1548
+ labels={{
1549
+ loadingLabel: t("sidebar.loadingFiles"),
1550
+ noMatchesLabel: t("database.noRowsMatchThisView"),
1551
+ clearLabel: t("database.clearSearchAndFilters"),
1552
+ navigationLabel: "Content navigation",
1553
+ untitledLabel: t("sidebar.untitled"),
1554
+ }}
1555
+ />
1556
+ )}
1557
+ <div className="px-1">
1558
+ <DropdownMenu>
1559
+ <DropdownMenuTrigger asChild>
1560
+ <button
1561
+ type="button"
1562
+ className="flex h-7 w-full min-w-0 items-center rounded-md text-muted-foreground hover:bg-accent/40 hover:text-foreground"
1563
+ aria-label={t("sidebar.addWorkspace")}
1564
+ >
1565
+ <span className="flex size-7 shrink-0 items-center justify-center">
1566
+ <IconPlus size={14} />
1567
+ </span>
1568
+ <span className="truncate text-start text-[10px] font-semibold uppercase tracking-wider">
1569
+ {t("sidebar.addWorkspace")}
1570
+ </span>
1571
+ </button>
1572
+ </DropdownMenuTrigger>
1573
+ <DropdownMenuContent align="start" className="w-52">
1574
+ <DropdownMenuItem
1575
+ onSelect={() => setCreateWorkspaceDialogOpen(true)}
1576
+ >
1577
+ <IconPlus className="me-2 size-4" />
1578
+ {t("sidebar.newWorkspace")}
1579
+ </DropdownMenuItem>
1580
+ <DropdownMenuSeparator />
1581
+ <DropdownMenuItem asChild>
1582
+ <Link to="/local-files">
1583
+ <IconFolder className="me-2 size-4" />
1584
+ {t("sidebar.localFolder")}
1585
+ </Link>
1586
+ </DropdownMenuItem>
1587
+ </DropdownMenuContent>
1588
+ </DropdownMenu>
1589
+ </div>
1590
+ </div>
1591
+ ) : contentSpaceState === "loading" ? (
1128
1592
  <div className="px-3 py-4 text-center text-sm text-muted-foreground">
1129
- {t("sidebar.noWorkspaces")}
1593
+ {t("sidebar.loadingFiles")}
1130
1594
  </div>
1595
+ ) : (
1596
+ <QueryErrorState
1597
+ compact
1598
+ onRetry={handleRetryContentSpaces}
1599
+ retrying={
1600
+ contentSpacesQuery.isFetching || ensureContentSpaces.isPending
1601
+ }
1602
+ />
1131
1603
  )}
1132
1604
  </div>
1133
1605
  );
@@ -1244,22 +1716,6 @@ export function DocumentSidebar({
1244
1716
  <TooltipContent>{t("sidebar.expand")}</TooltipContent>
1245
1717
  </Tooltip>
1246
1718
  {renderCollapsedNewButton()}
1247
- <Tooltip>
1248
- <TooltipTrigger asChild>
1249
- <Link
1250
- to="/local-files"
1251
- className={cn(
1252
- "w-10 h-10 flex items-center justify-center rounded-lg hover:bg-accent",
1253
- localFilesActive
1254
- ? "bg-accent text-accent-foreground"
1255
- : "text-muted-foreground hover:text-foreground",
1256
- )}
1257
- >
1258
- <IconFolderOpen size={16} />
1259
- </Link>
1260
- </TooltipTrigger>
1261
- <TooltipContent>{t("sidebar.localFiles")}</TooltipContent>
1262
- </Tooltip>
1263
1719
  <Tooltip>
1264
1720
  <TooltipTrigger asChild>
1265
1721
  <Link
@@ -1416,46 +1872,62 @@ export function DocumentSidebar({
1416
1872
  <>
1417
1873
  {/* Favorites */}
1418
1874
  {showFavorites && (
1419
- <div className="mb-2 min-w-0">
1420
- <div className="flex min-w-0 items-center gap-1 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
1421
- <IconStar size={10} />
1422
- <span className="min-w-0 flex-1 truncate">
1423
- {t("sidebar.favorites")}
1424
- </span>
1425
- </div>
1426
- {favorites.map((doc) => (
1875
+ <div className="mb-2 min-w-0 px-2">
1876
+ <div className="flex h-7 w-full min-w-0 items-center rounded-md px-1 text-muted-foreground hover:bg-accent/40 hover:text-foreground">
1427
1877
  <button
1428
- key={doc.id}
1878
+ type="button"
1879
+ aria-expanded={!collapsedSections.favorites}
1880
+ aria-label={`${collapsedSections.favorites ? t("sidebar.expand") : t("sidebar.collapse")} ${t("sidebar.favorites")}`}
1881
+ className="flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-background/60"
1882
+ onClick={() => toggleSection("favorites")}
1883
+ >
1884
+ {collapsedSections.favorites ? (
1885
+ <IconChevronRight size={14} />
1886
+ ) : (
1887
+ <IconChevronDown size={14} />
1888
+ )}
1889
+ </button>
1890
+ <Link
1891
+ to={
1892
+ favoritesDocumentId
1893
+ ? `/page/${favoritesDocumentId}`
1894
+ : "/favorites"
1895
+ }
1429
1896
  className={cn(
1430
- "flex w-full min-w-0 items-center gap-2 rounded-md px-4 py-[5px] text-start text-sm",
1431
- doc.id === activeDocumentId
1432
- ? "bg-accent text-accent-foreground"
1433
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
1897
+ "h-7 min-w-0 flex-1 truncate pe-2 text-start text-[10px] font-semibold uppercase tracking-wider leading-7",
1898
+ (location.pathname === "/favorites" ||
1899
+ activeDocumentId === favoritesDocumentId) &&
1900
+ "text-foreground",
1434
1901
  )}
1435
- style={{
1436
- width:
1437
- favoriteRowWidth === undefined
1438
- ? undefined
1439
- : `${favoriteRowWidth}px`,
1440
- }}
1441
- onClick={() => {
1442
- navigateToDocument(doc.id);
1443
- onNavigate?.();
1444
- }}
1445
1902
  >
1446
- <span className="flex-shrink-0 w-5 text-center">
1447
- <DocumentSidebarIcon document={doc} />
1448
- </span>
1449
- <span className="min-w-0 flex-1 truncate">
1450
- {doc.title || t("sidebar.untitled")}
1451
- </span>
1452
- </button>
1453
- ))}
1903
+ {t("sidebar.favorites")}
1904
+ </Link>
1905
+ </div>
1906
+ {!collapsedSections.favorites &&
1907
+ favorites.map((doc) => (
1908
+ <FavoriteDocumentItem
1909
+ key={doc.id}
1910
+ document={doc}
1911
+ active={doc.id === activeDocumentId}
1912
+ sidebarWidth={favoriteRowWidth}
1913
+ onSelect={() => {
1914
+ handleOpenFavorite(doc);
1915
+ onNavigate?.();
1916
+ }}
1917
+ onCreateChildPage={() => void handleCreatePage(doc.id)}
1918
+ onCreateChildDatabase={() =>
1919
+ void handleCreateDatabase(doc.id)
1920
+ }
1921
+ onRemoveFavorite={() =>
1922
+ handleToggleFavorite(doc.id, false)
1923
+ }
1924
+ onDelete={() => void handleDelete(doc.id)}
1925
+ />
1926
+ ))}
1454
1927
  </div>
1455
1928
  )}
1456
1929
 
1457
1930
  {renderWorkspaceNavigation()}
1458
- {renderFilesDatabase()}
1459
1931
  {renderTrashSection()}
1460
1932
  </>
1461
1933
  )}
@@ -1464,7 +1936,6 @@ export function DocumentSidebar({
1464
1936
 
1465
1937
  <div className="shrink-0 px-3 py-2">
1466
1938
  <div className="space-y-1">
1467
- {renderLocalFilesNavButton()}
1468
1939
  {renderAgentNavButton()}
1469
1940
  {renderSettingsNavButton()}
1470
1941
  </div>
@@ -1487,7 +1958,6 @@ export function DocumentSidebar({
1487
1958
 
1488
1959
  {/* Footer */}
1489
1960
  <div className="shrink-0 space-y-2 px-3 py-2">
1490
- <OrgSwitcher />
1491
1961
  {isCodeMode ? <DevDatabaseLink /> : null}
1492
1962
  <div className="flex items-center gap-1">
1493
1963
  <FeedbackButton className="h-8 min-w-0 flex-1 gap-2 rounded-md px-2 py-0" />
@@ -1508,6 +1978,60 @@ export function DocumentSidebar({
1508
1978
  onMouseDown={handleMouseDown}
1509
1979
  />
1510
1980
  )}
1981
+ <Dialog
1982
+ open={createWorkspaceDialogOpen}
1983
+ onOpenChange={(open) => {
1984
+ setCreateWorkspaceDialogOpen(open);
1985
+ if (!open && !createContentSpace.isPending) {
1986
+ setNewWorkspaceName("");
1987
+ createWorkspaceRequestIdRef.current = null;
1988
+ }
1989
+ }}
1990
+ >
1991
+ <DialogContent className="sm:max-w-sm">
1992
+ <form
1993
+ className="grid gap-4"
1994
+ onSubmit={(event) => {
1995
+ event.preventDefault();
1996
+ void handleCreateWorkspace();
1997
+ }}
1998
+ >
1999
+ <DialogHeader>
2000
+ <DialogTitle>{t("sidebar.newWorkspace")}</DialogTitle>
2001
+ <DialogDescription>
2002
+ {t("sidebar.newWorkspaceDescription")}
2003
+ </DialogDescription>
2004
+ </DialogHeader>
2005
+ <Input
2006
+ autoFocus
2007
+ aria-label={t("sidebar.workspaceName")}
2008
+ placeholder={t("sidebar.workspaceName")}
2009
+ value={newWorkspaceName}
2010
+ maxLength={200}
2011
+ onChange={(event) => setNewWorkspaceName(event.target.value)}
2012
+ />
2013
+ <DialogFooter>
2014
+ <button
2015
+ type="button"
2016
+ className="inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium hover:bg-accent"
2017
+ disabled={createContentSpace.isPending}
2018
+ onClick={() => setCreateWorkspaceDialogOpen(false)}
2019
+ >
2020
+ {t("comments.cancel")}
2021
+ </button>
2022
+ <button
2023
+ type="submit"
2024
+ className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
2025
+ disabled={
2026
+ createContentSpace.isPending || !newWorkspaceName.trim()
2027
+ }
2028
+ >
2029
+ {t("sidebar.createWorkspace")}
2030
+ </button>
2031
+ </DialogFooter>
2032
+ </form>
2033
+ </DialogContent>
2034
+ </Dialog>
1511
2035
  <AlertDialog
1512
2036
  open={removeLocalFilesDialogOpen}
1513
2037
  onOpenChange={setRemoveLocalFilesDialogOpen}