@agent-native/core-corpus 0.201.0-nightly-20261005153421 → 0.201.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 (235) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
  3. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
  4. package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
  5. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  6. package/corpus/templates/analytics/README.md +2 -2
  7. package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
  8. package/corpus/templates/analytics/package.json +1 -0
  9. package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
  10. package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
  13. package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
  14. package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
  15. package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
  16. package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
  17. package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
  18. package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
  19. package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
  20. package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
  21. package/corpus/templates/assets/CHANGELOG.md +7 -0
  22. package/corpus/templates/assets/README.md +2 -2
  23. package/corpus/templates/assets/package.json +1 -0
  24. package/corpus/templates/assets/server/plugins/auth.ts +2 -1
  25. package/corpus/templates/assets/server/plugins/otel.ts +6 -0
  26. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
  27. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
  28. package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
  29. package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
  30. package/corpus/templates/brain/CHANGELOG.md +6 -0
  31. package/corpus/templates/brain/README.md +12 -8
  32. package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
  33. package/corpus/templates/brain/package.json +1 -0
  34. package/corpus/templates/brain/server/lib/audiences.ts +1 -98
  35. package/corpus/templates/brain/server/lib/connectors.ts +2 -166
  36. package/corpus/templates/brain/server/plugins/otel.ts +6 -0
  37. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
  38. package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
  39. package/corpus/templates/calendar/CHANGELOG.md +7 -0
  40. package/corpus/templates/calendar/README.md +2 -2
  41. package/corpus/templates/calendar/package.json +1 -0
  42. package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
  43. package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
  44. package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
  45. package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  46. package/corpus/templates/chat/README.md +2 -2
  47. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
  48. package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
  49. package/corpus/templates/clips/CHANGELOG.md +24 -0
  50. package/corpus/templates/clips/README.md +2 -2
  51. package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
  52. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
  53. package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
  54. package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
  55. package/corpus/templates/clips/actions/request-transcript.ts +81 -135
  56. package/corpus/templates/clips/actions/search-recordings.ts +19 -5
  57. package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
  58. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
  59. package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
  60. package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
  61. package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
  62. package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
  63. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
  64. package/corpus/templates/clips/app/routes/record.tsx +16 -6
  65. package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
  66. package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
  67. package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
  68. package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
  69. package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
  70. package/corpus/templates/clips/package.json +1 -0
  71. package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
  72. package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
  73. package/corpus/templates/clips/server/plugins/auth.ts +2 -1
  74. package/corpus/templates/clips/server/plugins/otel.ts +6 -0
  75. package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
  76. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
  77. package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
  78. package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
  79. package/corpus/templates/content/CHANGELOG.md +41 -0
  80. package/corpus/templates/content/README.md +2 -2
  81. package/corpus/templates/content/actions/_database-navigation.ts +523 -344
  82. package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
  83. package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
  84. package/corpus/templates/content/actions/_database-utils.ts +299 -271
  85. package/corpus/templates/content/actions/_property-utils.ts +293 -190
  86. package/corpus/templates/content/actions/_relation-values.ts +3 -1
  87. package/corpus/templates/content/actions/get-content-database.ts +1 -0
  88. package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
  89. package/corpus/templates/content/actions/set-document-property.ts +17 -11
  90. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
  91. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
  92. package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
  93. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
  94. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
  95. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
  96. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
  97. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
  98. package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
  99. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
  100. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
  101. package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
  102. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
  103. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
  104. package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
  105. package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
  106. package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
  107. package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
  108. package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
  109. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
  110. package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
  111. package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
  112. package/corpus/templates/content/app/global.css +16 -8
  113. package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
  114. package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
  115. package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
  116. package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
  117. package/corpus/templates/content/app/i18n-data.ts +3 -2
  118. package/corpus/templates/content/app/lib/content-landing.ts +3 -31
  119. package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
  120. package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
  121. package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
  122. package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
  123. package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
  124. package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
  125. package/corpus/templates/content/app/root.tsx +3 -1
  126. package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
  127. package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
  128. package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
  132. package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
  133. package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
  134. package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
  135. package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
  136. package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
  137. package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
  138. package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
  139. package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
  140. package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
  141. package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
  142. package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
  143. package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
  144. package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
  145. package/corpus/templates/content/e2e/playwright.config.ts +1 -1
  146. package/corpus/templates/content/package.json +2 -1
  147. package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
  148. package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
  149. package/corpus/templates/content/server/db/index.ts +1 -1
  150. package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
  151. package/corpus/templates/content/server/plugins/auth.ts +2 -1
  152. package/corpus/templates/content/server/plugins/otel.ts +6 -0
  153. package/corpus/templates/content/shared/api.ts +17 -0
  154. package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
  155. package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
  156. package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
  157. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
  158. package/corpus/templates/crm/package.json +1 -0
  159. package/corpus/templates/crm/server/plugins/otel.ts +6 -0
  160. package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
  161. package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
  162. package/corpus/templates/design/CHANGELOG.md +41 -0
  163. package/corpus/templates/design/README.md +2 -2
  164. package/corpus/templates/design/actions/list-designs.ts +11 -4
  165. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
  166. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
  167. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
  168. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
  169. package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
  170. package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
  171. package/corpus/templates/design/app/global.css +1 -34
  172. package/corpus/templates/design/app/i18n/en-US.ts +4 -0
  173. package/corpus/templates/design/app/lib/design-filter.ts +4 -2
  174. package/corpus/templates/design/app/pages/Index.tsx +91 -59
  175. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
  176. package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
  177. package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
  178. package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
  179. package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
  180. package/corpus/templates/design/package.json +1 -0
  181. package/corpus/templates/design/server/plugins/auth.ts +2 -1
  182. package/corpus/templates/design/server/plugins/otel.ts +6 -0
  183. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
  184. package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
  185. package/corpus/templates/dispatch/README.md +2 -2
  186. package/corpus/templates/dispatch/package.json +1 -0
  187. package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
  188. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
  189. package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
  190. package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
  191. package/corpus/templates/factory/package.json +1 -0
  192. package/corpus/templates/factory/server/plugins/otel.ts +6 -0
  193. package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
  194. package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
  195. package/corpus/templates/forms/README.md +2 -2
  196. package/corpus/templates/forms/package.json +1 -0
  197. package/corpus/templates/forms/server/plugins/auth.ts +2 -1
  198. package/corpus/templates/forms/server/plugins/otel.ts +6 -0
  199. package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
  200. package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
  201. package/corpus/templates/mail/CHANGELOG.md +6 -0
  202. package/corpus/templates/mail/README.md +2 -2
  203. package/corpus/templates/mail/package.json +1 -0
  204. package/corpus/templates/mail/server/plugins/auth.ts +2 -1
  205. package/corpus/templates/mail/server/plugins/otel.ts +6 -0
  206. package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
  207. package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
  208. package/corpus/templates/plan/CHANGELOG.md +1 -0
  209. package/corpus/templates/plan/README.md +2 -2
  210. package/corpus/templates/plan/package.json +1 -0
  211. package/corpus/templates/plan/server/plugins/auth.ts +2 -1
  212. package/corpus/templates/plan/server/plugins/otel.ts +6 -0
  213. package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
  214. package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
  215. package/corpus/templates/slides/CHANGELOG.md +23 -0
  216. package/corpus/templates/slides/README.md +2 -2
  217. package/corpus/templates/slides/actions/list-decks.ts +11 -4
  218. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
  219. package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
  220. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
  221. package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
  222. package/corpus/templates/slides/app/global.css +3 -54
  223. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  224. package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
  225. package/corpus/templates/slides/app/pages/Index.tsx +102 -88
  226. package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
  227. package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
  228. package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
  229. package/corpus/templates/slides/package.json +1 -0
  230. package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
  231. package/corpus/templates/slides/server/plugins/auth.ts +2 -1
  232. package/corpus/templates/slides/server/plugins/otel.ts +6 -0
  233. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
  234. package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
  235. package/package.json +1 -1
@@ -1,11 +1,35 @@
1
+ import { callActionWithRetry } from "@agent-native/core/client/hooks";
1
2
  import type {
2
- ContentDatabaseNavigationSort,
3
+ ContentDatabaseNavigationPageResponse,
3
4
  ContentDatabasePersonalViewOverrides,
5
+ ContentDatabaseUnavailableResponse,
4
6
  ContentSidebarViewOrder,
5
7
  } from "@shared/api";
8
+ import {
9
+ useQuery,
10
+ useQueryClient,
11
+ type QueryClient,
12
+ } from "@tanstack/react-query";
13
+ import { useEffect, useRef } from "react";
6
14
 
7
15
  export const FILES_NAVIGATION_PAGE_SIZE = 20;
8
16
 
17
+ // Most expanded folders one read asks for; the server caps what it returns.
18
+ const MAX_EXPANDED_IDS_PER_READ = 100;
19
+ const pendingExpansionReads = new WeakMap<QueryClient, Set<string>>();
20
+
21
+ /**
22
+ * The folders the Files tree draws open, most important first: both caps on
23
+ * the folders one read opens keep the first ones, so the open page's
24
+ * ancestors come before the folders a person expanded.
25
+ */
26
+ export function openFilesFolderIds(
27
+ activeAncestorIds: Iterable<string>,
28
+ expandedDocumentIds: Iterable<string>,
29
+ ): ReadonlySet<string> {
30
+ return new Set([...activeAncestorIds, ...expandedDocumentIds]);
31
+ }
32
+
9
33
  /** The Files tree order a person chose, as the paged navigation query reads it. */
10
34
  export function filesNavigationOrder(
11
35
  overrides: ContentDatabasePersonalViewOverrides | null | undefined,
@@ -19,22 +43,170 @@ export function filesNavigationOrder(
19
43
  }
20
44
 
21
45
  // Every reader of one Files branch must build the same params so the sidebar
22
- // tree and breadcrumb menus share one cached request per page.
46
+ // tree and breadcrumb menus share one cached request per page. The server
47
+ // orders a branch by the person's saved view, so the order is not part of
48
+ // the key: a branch can be read before that view arrives.
23
49
  export function filesNavigationPageParams(args: {
24
50
  databaseId: string;
25
51
  parentId: string | null;
26
- sort: ContentDatabaseNavigationSort;
27
- viewId?: string;
28
52
  cursor?: string;
29
53
  }) {
30
54
  return {
31
55
  databaseId: args.databaseId,
32
56
  limit: FILES_NAVIGATION_PAGE_SIZE,
33
- navigation: {
34
- parentId: args.parentId,
35
- sort: args.sort,
36
- viewId: args.viewId,
37
- cursor: args.cursor,
38
- },
57
+ navigation: { parentId: args.parentId, cursor: args.cursor },
39
58
  };
40
59
  }
60
+
61
+ export type FilesNavigationPageParams = ReturnType<
62
+ typeof filesNavigationPageParams
63
+ >;
64
+
65
+ export function filesNavigationQueryKey(params: FilesNavigationPageParams) {
66
+ return ["action", "query-content-database-items", params] as const;
67
+ }
68
+
69
+ type FilesNavigationRead =
70
+ | ContentDatabaseNavigationPageResponse
71
+ | ContentDatabaseUnavailableResponse;
72
+
73
+ /**
74
+ * Reads one Files page and asks for any expanded folders that do not have a
75
+ * cached page yet, so opening a cached root can still load its new branches.
76
+ */
77
+ export async function readFilesNavigationPage(
78
+ queryClient: QueryClient,
79
+ params: FilesNavigationPageParams,
80
+ expanded: Iterable<string>,
81
+ signal?: AbortSignal,
82
+ ): Promise<FilesNavigationRead> {
83
+ const expand = [...expanded]
84
+ .filter((parentId) => {
85
+ const key = filesNavigationQueryKey(
86
+ filesNavigationPageParams({
87
+ databaseId: params.databaseId,
88
+ parentId,
89
+ }),
90
+ );
91
+ return queryClient.getQueryData(key) === undefined;
92
+ })
93
+ .slice(0, MAX_EXPANDED_IDS_PER_READ);
94
+ const expansionKey =
95
+ expand.length > 0 ? JSON.stringify([params.databaseId, expand]) : null;
96
+ let pending: Set<string> | undefined;
97
+ if (expansionKey) {
98
+ pending = pendingExpansionReads.get(queryClient);
99
+ if (!pending) {
100
+ pending = new Set();
101
+ pendingExpansionReads.set(queryClient, pending);
102
+ }
103
+ pending.add(expansionKey);
104
+ }
105
+ try {
106
+ const response = await callActionWithRetry<FilesNavigationRead>(
107
+ "query-content-database-items",
108
+ expand.length
109
+ ? { ...params, navigation: { ...params.navigation, expand } }
110
+ : params,
111
+ { method: "GET", signal },
112
+ );
113
+ if ("available" in response) return response;
114
+ // A folder left out of `branches`, whether the cap dropped it
115
+ // (`branchesTruncated`) or it has no children to show, is not seeded as
116
+ // empty: it reads its own page when it draws.
117
+ const { branches, branchesTruncated, ...page } = response;
118
+ for (const [parentId, branch] of Object.entries(branches ?? {})) {
119
+ const key = filesNavigationQueryKey(
120
+ filesNavigationPageParams({
121
+ databaseId: params.databaseId,
122
+ parentId,
123
+ }),
124
+ );
125
+ // A folder that already has its own page keeps it; that read is the
126
+ // one its later changes refresh.
127
+ if (queryClient.getQueryData(key) === undefined) {
128
+ queryClient.setQueryData(key, branch);
129
+ }
130
+ }
131
+ return page;
132
+ } finally {
133
+ if (expansionKey && pending) pending.delete(expansionKey);
134
+ }
135
+ }
136
+
137
+ function isExpansionReadPending(
138
+ queryClient: QueryClient,
139
+ databaseId: string,
140
+ expanded: Iterable<string>,
141
+ ) {
142
+ const expand = [...expanded]
143
+ .filter((parentId) => {
144
+ const key = filesNavigationQueryKey(
145
+ filesNavigationPageParams({ databaseId, parentId }),
146
+ );
147
+ return queryClient.getQueryData(key) === undefined;
148
+ })
149
+ .slice(0, MAX_EXPANDED_IDS_PER_READ);
150
+ if (expand.length === 0) return false;
151
+ return (
152
+ pendingExpansionReads
153
+ .get(queryClient)
154
+ ?.has(JSON.stringify([databaseId, expand])) ?? false
155
+ );
156
+ }
157
+
158
+ export function useFilesNavigationPage(
159
+ params: FilesNavigationPageParams,
160
+ expanded: ReadonlySet<string>,
161
+ ) {
162
+ const queryClient = useQueryClient();
163
+ const expansionKey = JSON.stringify([
164
+ params.databaseId,
165
+ params.navigation.parentId,
166
+ params.navigation.cursor ?? null,
167
+ [...expanded],
168
+ ]);
169
+ const rootReadExpansion = useRef<string | null>(null);
170
+ const query = useQuery({
171
+ queryKey: filesNavigationQueryKey(params),
172
+ queryFn: ({ signal }) => {
173
+ rootReadExpansion.current = expansionKey;
174
+ return readFilesNavigationPage(queryClient, params, expanded, signal);
175
+ },
176
+ retry: false,
177
+ });
178
+ useEffect(() => {
179
+ if (
180
+ !query.data ||
181
+ "available" in query.data ||
182
+ query.isFetching ||
183
+ isExpansionReadPending(queryClient, params.databaseId, expanded) ||
184
+ rootReadExpansion.current === expansionKey
185
+ ) {
186
+ return;
187
+ }
188
+ const hasMissingBranch = [...expanded].some((parentId) => {
189
+ const key = filesNavigationQueryKey(
190
+ filesNavigationPageParams({
191
+ databaseId: params.databaseId,
192
+ parentId,
193
+ }),
194
+ );
195
+ return queryClient.getQueryData(key) === undefined;
196
+ });
197
+ if (!hasMissingBranch) {
198
+ rootReadExpansion.current = expansionKey;
199
+ return;
200
+ }
201
+ void query.refetch();
202
+ }, [
203
+ expanded,
204
+ expansionKey,
205
+ params,
206
+ query.data,
207
+ query.isFetching,
208
+ query.refetch,
209
+ queryClient,
210
+ ]);
211
+ return query;
212
+ }
@@ -1,14 +1,16 @@
1
- import { callAction } from "@agent-native/core/client/hooks";
2
- import type { ContentDatabaseNavigationSort } from "@shared/api";
3
1
  import type { QueryClient } from "@tanstack/react-query";
4
2
 
5
- import { filesNavigationPageParams } from "@/lib/files-navigation";
3
+ import {
4
+ filesNavigationPageParams,
5
+ filesNavigationQueryKey,
6
+ readFilesNavigationPage,
7
+ } from "@/lib/files-navigation";
6
8
 
7
- // The Files tree's root page is keyed by the space's Files database and the
8
- // personal view's order, which arrive from two other reads. The inputs this
9
- // browser last used for the same person in the same active organization let
10
- // the root page start alongside those reads; when they turn out different,
11
- // the tree reads again with the confirmed ones and the early read is unused.
9
+ // The Files tree's root page is keyed by the space's Files database, which
10
+ // arrives from another read. The database this browser last used for the same
11
+ // person in the same active organization lets the root page start alongside
12
+ // that read; when it turns out different, the tree reads again with the
13
+ // confirmed one and the early read is unused.
12
14
  const FILES_ROOT_HINT_STORAGE_KEY = "content-sidebar-files-root-v1";
13
15
 
14
16
  export function filesRootHintScope(
@@ -19,11 +21,7 @@ export function filesRootHintScope(
19
21
  return account ? JSON.stringify([account, orgId ?? null]) : null;
20
22
  }
21
23
 
22
- export type PagedFilesRoot = {
23
- databaseId: string;
24
- sort: ContentDatabaseNavigationSort;
25
- viewId?: string;
26
- };
24
+ export type PagedFilesRoot = { databaseId: string };
27
25
 
28
26
  export function readPagedFilesRootHint(scope: string): PagedFilesRoot | null {
29
27
  let raw: string | null;
@@ -35,21 +33,11 @@ export function readPagedFilesRootHint(scope: string): PagedFilesRoot | null {
35
33
  }
36
34
  if (!raw) return null;
37
35
  try {
38
- const hint = JSON.parse(raw) as Partial<PagedFilesRoot> & {
39
- scope?: unknown;
40
- };
41
- if (
42
- hint.scope !== scope ||
43
- typeof hint.databaseId !== "string" ||
44
- typeof hint.sort !== "string"
45
- ) {
36
+ const hint = JSON.parse(raw) as { scope?: unknown; databaseId?: unknown };
37
+ if (hint.scope !== scope || typeof hint.databaseId !== "string") {
46
38
  return null;
47
39
  }
48
- return {
49
- databaseId: hint.databaseId,
50
- sort: hint.sort,
51
- ...(typeof hint.viewId === "string" ? { viewId: hint.viewId } : {}),
52
- };
40
+ return { databaseId: hint.databaseId };
53
41
  } catch {
54
42
  // coercion-ok: a malformed hint is ignored and replaced by the next write.
55
43
  return null;
@@ -67,17 +55,17 @@ export function rememberPagedFilesRoot(scope: string, root: PagedFilesRoot) {
67
55
  }
68
56
  }
69
57
 
58
+ /** Starts the root page, with the first pages of `expanded` folders. */
70
59
  export function prefetchPagedFilesRoot(
71
60
  queryClient: QueryClient,
72
61
  root: PagedFilesRoot,
62
+ expanded: readonly string[] = [],
73
63
  ) {
74
- const args = filesNavigationPageParams({ ...root, parentId: null });
64
+ const params = filesNavigationPageParams({ ...root, parentId: null });
75
65
  void queryClient.prefetchQuery({
76
- queryKey: ["action", "query-content-database-items", args],
66
+ queryKey: filesNavigationQueryKey(params),
77
67
  queryFn: ({ signal }) =>
78
- callAction("query-content-database-items", args, {
79
- method: "GET",
80
- signal,
81
- }),
68
+ readFilesNavigationPage(queryClient, params, expanded, signal),
69
+ retry: false,
82
70
  });
83
71
  }
@@ -0,0 +1,108 @@
1
+ import type {
2
+ DocumentEditorIconRow,
3
+ DocumentEditorShape,
4
+ } from "@/components/editor/document-editor-layout";
5
+
6
+ import { LAST_LOCATION_HINT_STORAGE_KEY } from "./last-location-hint";
7
+
8
+ // Before a page loads, nothing says what it will draw: whether it has an icon,
9
+ // whether this person can edit it, whether it is a collection, or whether its
10
+ // open comments hold the review margin open. What this browser last drew for a
11
+ // page lets its placeholder hold the same boxes. Only page ids and these flags
12
+ // are kept, and only for pages that differ from the default.
13
+ const PAGE_ICON_ROWS_STORAGE_KEY = "content-page-icon-rows-v1";
14
+ const PAGE_SHAPES_STORAGE_KEY = "content-page-shapes-v1";
15
+ const MAX_REMEMBERED_PAGES = 200;
16
+
17
+ export const STARTUP_PAGE_ICON_ROW_ATTRIBUTE = "data-content-page-icon-row";
18
+ export const STARTUP_PAGE_SHAPE_ATTRIBUTE = "data-content-page-shape";
19
+
20
+ const REMEMBERED_SHAPES = [
21
+ "review",
22
+ "database",
23
+ "database-constrained",
24
+ ] as const satisfies readonly DocumentEditorShape[];
25
+
26
+ function readStoredHints(key: string): Record<string, unknown> {
27
+ try {
28
+ const raw = localStorage.getItem(key);
29
+ const parsed: unknown = raw ? JSON.parse(raw) : null;
30
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
31
+ ? (parsed as Record<string, unknown>)
32
+ : {};
33
+ } catch {
34
+ // coercion-ok: an unreadable hint only means the page may move once.
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function rememberHint(
40
+ key: string,
41
+ documentId: string,
42
+ value: string,
43
+ defaultValue: string,
44
+ ) {
45
+ const hints = readStoredHints(key);
46
+ if (value === defaultValue && hints[documentId] === undefined) return;
47
+ delete hints[documentId];
48
+ if (value !== defaultValue) hints[documentId] = value;
49
+ const ids = Object.keys(hints);
50
+ const overflow = Math.max(0, ids.length - MAX_REMEMBERED_PAGES);
51
+ for (const id of ids.slice(0, overflow)) delete hints[id];
52
+ try {
53
+ localStorage.setItem(key, JSON.stringify(hints));
54
+ } catch {
55
+ // coercion-ok: without storage the next load holds the default boxes.
56
+ }
57
+ }
58
+
59
+ export function readPageIconRowHint(documentId: string): DocumentEditorIconRow {
60
+ const row = readStoredHints(PAGE_ICON_ROWS_STORAGE_KEY)[documentId];
61
+ return row === "icon" || row === "none" ? row : "add";
62
+ }
63
+
64
+ export function rememberPageIconRow(
65
+ documentId: string,
66
+ row: DocumentEditorIconRow,
67
+ ) {
68
+ rememberHint(PAGE_ICON_ROWS_STORAGE_KEY, documentId, row, "add");
69
+ }
70
+
71
+ export function readPageShapeHint(documentId: string): DocumentEditorShape {
72
+ const shape = readStoredHints(PAGE_SHAPES_STORAGE_KEY)[documentId];
73
+ return REMEMBERED_SHAPES.find((remembered) => remembered === shape) ?? "page";
74
+ }
75
+
76
+ // A loaded page knows whether it is a collection; the hint still says which
77
+ // boxes it last drew below the title.
78
+ export function readDocumentShapeHint(document: {
79
+ id: string;
80
+ database?: unknown;
81
+ }): DocumentEditorShape {
82
+ const shape = readPageShapeHint(document.id);
83
+ if (document.database) {
84
+ return shape === "database-constrained" ? shape : "database";
85
+ }
86
+ return shape === "review" ? shape : "page";
87
+ }
88
+
89
+ export function rememberPageShape(
90
+ documentId: string,
91
+ shape: DocumentEditorShape,
92
+ ) {
93
+ rememberHint(PAGE_SHAPES_STORAGE_KEY, documentId, shape, "page");
94
+ }
95
+
96
+ // Runs in <head> before the first paint, so the server-rendered placeholder
97
+ // holds the boxes a page last drew. The app may sit under a base path; on
98
+ // `/home`, use the last-location id unless the URL selects a specific space.
99
+ // coercion-ok: an unreadable hint only means the page may move once.
100
+ export const CONTENT_STARTUP_PAGE_HINTS_SCRIPT = `(function(){try{function h(k){try{return JSON.parse(localStorage.getItem(k)||"{}")[id]}catch(e){}}var p=location.pathname.split("/").filter(Boolean),i=p.indexOf("page"),id=i>=0&&p[i+1]?decodeURIComponent(p[i+1]):null;if(!id&&p[p.length-1]==="home"&&!/[?&]spaceId=/.test(location.search))id=(JSON.parse(localStorage.getItem(${JSON.stringify(
101
+ LAST_LOCATION_HINT_STORAGE_KEY,
102
+ )})||"null")||{}).documentId;if(typeof id!=="string"||!id)return;var d=document.documentElement,r=h(${JSON.stringify(
103
+ PAGE_ICON_ROWS_STORAGE_KEY,
104
+ )}),s=h(${JSON.stringify(PAGE_SHAPES_STORAGE_KEY)});if(r==="icon"||r==="none")d.setAttribute(${JSON.stringify(
105
+ STARTUP_PAGE_ICON_ROW_ATTRIBUTE,
106
+ )},r);if(${JSON.stringify(REMEMBERED_SHAPES)}.indexOf(s)>=0)d.setAttribute(${JSON.stringify(
107
+ STARTUP_PAGE_SHAPE_ATTRIBUTE,
108
+ )},s)}catch(e){}})();`; // coercion-ok: without storage the shell holds the default boxes.
@@ -0,0 +1,25 @@
1
+ import { safeJsonForHtml } from "@agent-native/core/shared";
2
+
3
+ export const PRIVATE_LINK_REDIRECT_ATTRIBUTE =
4
+ "data-content-private-link-redirect";
5
+
6
+ // Tailwind only finds whole class strings, so the attribute is spelled out
7
+ // here; a test keeps it equal to PRIVATE_LINK_REDIRECT_ATTRIBUTE.
8
+ export const HIDDEN_WHILE_PRIVATE_LINK_REDIRECTS =
9
+ "[html[data-content-private-link-redirect]_&]:invisible";
10
+
11
+ // Every visitor gets the same cached share page, so it can't know whether this
12
+ // browser may open the document. Browsers leave for the app page before first
13
+ // paint, which opens the document or says why it can't; the private notice is
14
+ // for fetchers that don't run scripts.
15
+ export function privateDocumentRedirectScript(pageHref: string) {
16
+ return `document.documentElement.setAttribute(${JSON.stringify(
17
+ PRIVATE_LINK_REDIRECT_ATTRIBUTE,
18
+ )},"");location.replace(${safeJsonForHtml(pageHref)});`;
19
+ }
20
+
21
+ // The script has already started the move, and starting it again cancels the
22
+ // request in flight.
23
+ export function privateLinkRedirectStarted() {
24
+ return document.documentElement.hasAttribute(PRIVATE_LINK_REDIRECT_ATTRIBUTE);
25
+ }
@@ -7,12 +7,15 @@ import {
7
7
  // that land in any order. What this browser last drew for the same person,
8
8
  // organization, and space lets the first frame reserve each section at that
9
9
  // size, so later rows fill in place instead of pushing the sections below.
10
- // Only counts and section settings are kept, never titles.
10
+ // The Files folders that were open are kept by ID, so the next load reads
11
+ // them with the tree. Only counts, IDs, and section settings are kept, never
12
+ // titles.
11
13
  const SIDEBAR_LAYOUT_HINT_STORAGE_KEY = "content-sidebar-layout-v1";
12
14
 
13
15
  // Rows each section shows before "Show more", and the most a hint reserves.
14
16
  export const SIDEBAR_SECTION_ROW_LIMIT = 5;
15
17
  const MAX_HINTED_FILES_ROWS = 100;
18
+ const MAX_HINTED_FILES_BRANCHES = 32;
16
19
 
17
20
  export type SidebarRowsHint = { rows: number; more: boolean };
18
21
 
@@ -21,6 +24,8 @@ export type SidebarLayoutHint = {
21
24
  pinned?: SidebarRowsHint;
22
25
  recent?: SidebarRowsHint;
23
26
  files?: SidebarRowsHint;
27
+ /** Rows each open Files folder drew, by folder document ID. */
28
+ branches?: Record<string, SidebarRowsHint>;
24
29
  };
25
30
 
26
31
  function parseRows(value: unknown, max: number): SidebarRowsHint | undefined {
@@ -32,6 +37,33 @@ function parseRows(value: unknown, max: number): SidebarRowsHint | undefined {
32
37
  return { rows: Math.min(rows, max), more: more === true };
33
38
  }
34
39
 
40
+ // Folders are stored oldest first, so the cap keeps the most recently drawn.
41
+ function parseBranches(
42
+ value: unknown,
43
+ ): Record<string, SidebarRowsHint> | undefined {
44
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
45
+ return undefined;
46
+ }
47
+ return Object.fromEntries(
48
+ Object.entries(value)
49
+ .slice(-MAX_HINTED_FILES_BRANCHES)
50
+ .flatMap(([documentId, rows]) => {
51
+ const parsed = parseRows(rows, MAX_HINTED_FILES_ROWS);
52
+ return parsed ? [[documentId, parsed]] : [];
53
+ }),
54
+ );
55
+ }
56
+
57
+ /** Open folders with `documentId` moved last, as the most recently drawn. */
58
+ export function withShownFilesBranch(
59
+ branches: Readonly<Record<string, SidebarRowsHint>> | undefined,
60
+ documentId: string,
61
+ rows: SidebarRowsHint,
62
+ ): Record<string, SidebarRowsHint> {
63
+ const { [documentId]: _previous, ...others } = branches ?? {};
64
+ return { ...others, [documentId]: rows };
65
+ }
66
+
35
67
  function readStoredHint(): {
36
68
  scope?: unknown;
37
69
  spaceId?: unknown;
@@ -75,6 +107,7 @@ export function readSidebarLayoutHint(
75
107
  pinned: parseRows(stored.hint.pinned, SIDEBAR_SECTION_ROW_LIMIT),
76
108
  recent: parseRows(stored.hint.recent, SIDEBAR_SECTION_ROW_LIMIT),
77
109
  files: parseRows(stored.hint.files, MAX_HINTED_FILES_ROWS),
110
+ branches: parseBranches(stored.hint.branches),
78
111
  };
79
112
  }
80
113
 
@@ -14,6 +14,8 @@ export const PAINTED_MARK_SUFFIX = ":painted";
14
14
  export type StartupAnchor =
15
15
  | "title"
16
16
  | "body"
17
+ | "database-tabs"
18
+ | "database-table"
17
19
  | "sidebar-space"
18
20
  | "sidebar-search"
19
21
  | "sidebar-section-pinned"
@@ -66,6 +66,7 @@ import { CONTENT_COMMAND_MENU_OPEN_EVENT } from "./lib/content-command-menu";
66
66
  import { isPersonalLanding } from "./lib/content-landing";
67
67
  import { readLastLocationHintForAnyAccount } from "./lib/last-location-hint";
68
68
  import { CONTENT_STARTUP_PAGE_ICON_ROW_SCRIPT } from "./lib/page-icon-row-hint";
69
+ import { CONTENT_STARTUP_PAGE_HINTS_SCRIPT } from "./lib/page-startup-hints";
69
70
 
70
71
  import stylesheet from "./global.css?url";
71
72
  import katexStylesheet from "katex/dist/katex.min.css?url";
@@ -216,7 +217,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
216
217
  dangerouslySetInnerHTML={{
217
218
  __html:
218
219
  CONTENT_STARTUP_SIDEBAR_SCRIPT +
219
- CONTENT_STARTUP_PAGE_ICON_ROW_SCRIPT,
220
+ CONTENT_STARTUP_PAGE_ICON_ROW_SCRIPT +
221
+ CONTENT_STARTUP_PAGE_HINTS_SCRIPT,
220
222
  }}
221
223
  />
222
224
  <meta name="theme-color" content="#10B981" />
@@ -28,17 +28,17 @@ import {
28
28
  startPageOpenDocumentReads,
29
29
  } from "@/hooks/use-documents";
30
30
  import { useLastLocationTitleHint } from "@/hooks/use-optimistic-document-title";
31
- import {
32
- isPersonalLanding,
33
- readContentLandingRecovery,
34
- } from "@/lib/content-landing";
31
+ import { isPersonalLanding } from "@/lib/content-landing";
35
32
  import {
36
33
  landingOptimisticTitle,
37
34
  stashLandingTitleHint,
38
35
  } from "@/lib/document-title-hint";
39
36
  import { filesRootHintScope } from "@/lib/files-root-hint";
40
37
  import { readLastLocationHint } from "@/lib/last-location-hint";
41
- import { readPageIconRowHint } from "@/lib/page-icon-row-hint";
38
+ import {
39
+ readPageIconRowHint,
40
+ readPageShapeHint,
41
+ } from "@/lib/page-startup-hints";
42
42
 
43
43
  const SEO_TITLE = "Content - Open Source, agent-friendly Obsidian alternative";
44
44
  const SEO_DESCRIPTION =
@@ -119,8 +119,6 @@ export default function HomeRoute() {
119
119
  const lastLocationHint = useLastLocationTitleHint();
120
120
  const lastLocationHintRef = useRef(lastLocationHint);
121
121
  lastLocationHintRef.current = lastLocationHint;
122
- const recoveredDocumentId =
123
- readContentLandingRecovery(location.state)?.unavailableDocumentId ?? null;
124
122
  const queryClient = useQueryClient();
125
123
  const { session } = useSession();
126
124
  const scope = filesRootHintScope(session?.email, session?.orgId);
@@ -181,9 +179,7 @@ export default function HomeRoute() {
181
179
  void navigate(contentRecentHref(result.target), { replace: true });
182
180
  return;
183
181
  }
184
- if (recoveredDocumentId) {
185
- toast.info(t("landing.requestedPageUnavailable"));
186
- } else if (result.fallbackReason === "saved-document-unavailable") {
182
+ if (result.fallbackReason === "saved-document-unavailable") {
187
183
  toast.info(t("landing.previousPageUnavailable"));
188
184
  }
189
185
  const hint = lastLocationHintRef.current;
@@ -201,15 +197,7 @@ export default function HomeRoute() {
201
197
  } catch (error) {
202
198
  console.error("Failed to resolve the Content landing page", error);
203
199
  }
204
- }, [
205
- location.hash,
206
- location.search,
207
- navigate,
208
- recoveredDocumentId,
209
- resolveLanding,
210
- spaceId,
211
- t,
212
- ]);
200
+ }, [location.hash, location.search, navigate, resolveLanding, spaceId, t]);
213
201
 
214
202
  useEffect(() => {
215
203
  void openLanding();
@@ -249,6 +237,9 @@ export default function HomeRoute() {
249
237
  iconRow={
250
238
  likelyDocumentId ? readPageIconRowHint(likelyDocumentId) : undefined
251
239
  }
240
+ shape={
241
+ likelyDocumentId ? readPageShapeHint(likelyDocumentId) : undefined
242
+ }
252
243
  />
253
244
  </>
254
245
  );
@@ -28,6 +28,11 @@ import type {
28
28
  import { data, redirect, useLoaderData } from "react-router";
29
29
 
30
30
  import { VisualEditor } from "@/components/editor/VisualEditor";
31
+ import {
32
+ HIDDEN_WHILE_PRIVATE_LINK_REDIRECTS,
33
+ privateDocumentRedirectScript,
34
+ privateLinkRedirectStarted,
35
+ } from "@/lib/private-link-redirect";
31
36
 
32
37
  import { getDb, schema } from "../../server/db";
33
38
  import {
@@ -93,7 +98,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
93
98
  const userEmail = getRequestUserEmail();
94
99
  if (userEmail) {
95
100
  const access = await resolveAccess("document", id);
96
- if (access) throw redirect(withBase(`/page/${id}`));
101
+ if (access) throw redirect(withBase(`/page/${encodeURIComponent(id)}`));
97
102
  }
98
103
 
99
104
  const [doc] = await getDb()
@@ -330,13 +335,24 @@ function PrivateDocumentNotice({
330
335
  origin?: string;
331
336
  }) {
332
337
  const t = useT();
338
+ const pageHref = id
339
+ ? `${basePath ?? ""}/page/${encodeURIComponent(id)}`
340
+ : null;
333
341
  useEffect(() => {
334
- if (!id) return;
335
- window.location.replace(`${basePath ?? ""}/page/${id}`);
336
- }, [id, basePath]);
342
+ if (!pageHref || privateLinkRedirectStarted()) return;
343
+ window.location.replace(pageHref);
344
+ }, [pageHref]);
337
345
 
338
346
  return (
339
347
  <main className="min-h-screen bg-background text-foreground">
348
+ {pageHref ? (
349
+ <script
350
+ suppressHydrationWarning
351
+ dangerouslySetInnerHTML={{
352
+ __html: privateDocumentRedirectScript(pageHref),
353
+ }}
354
+ />
355
+ ) : null}
340
356
  {id ? (
341
357
  <AgentReadableDocumentDiscovery
342
358
  document={{ id }}
@@ -345,7 +361,9 @@ function PrivateDocumentNotice({
345
361
  accessState="authentication-required"
346
362
  />
347
363
  ) : null}
348
- <section className="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6 text-center">
364
+ <section
365
+ className={`mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6 text-center ${HIDDEN_WHILE_PRIVATE_LINK_REDIRECTS}`}
366
+ >
349
367
  <div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl border border-border bg-muted text-muted-foreground">
350
368
  <IconLock size={22} />
351
369
  </div>
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-02
4
+ ---
5
+
6
+ A page link you can't open now says so, with the account you're signed in as, instead of opening another page. People who can open a private share link no longer see "This document is private" first.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-02
4
+ ---
5
+
6
+ After a refresh, the Files sidebar shows the open page's folders in one load instead of one level at a time