@rightkit/document-workspace 0.0.0-stage → 0.1.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 (262) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +74 -2
  5. package/dist/app/App.d.ts +1 -0
  6. package/dist/app/App.js +6 -0
  7. package/dist/app/AppDialogs.d.ts +52 -0
  8. package/dist/app/AppDialogs.js +23 -0
  9. package/dist/app/AppDialogsHost.d.ts +31 -0
  10. package/dist/app/AppDialogsHost.js +30 -0
  11. package/dist/app/AppLifecycle.d.ts +19 -0
  12. package/dist/app/AppLifecycle.js +87 -0
  13. package/dist/app/AppProvider.d.ts +317 -0
  14. package/dist/app/AppProvider.js +217 -0
  15. package/dist/app/AppRoot.d.ts +8 -0
  16. package/dist/app/AppRoot.js +159 -0
  17. package/dist/app/AppShell.d.ts +2 -0
  18. package/dist/app/AppShell.js +81 -0
  19. package/dist/app/CommandPalette.d.ts +13 -0
  20. package/dist/app/CommandPalette.js +47 -0
  21. package/dist/app/DocActionBar.d.ts +36 -0
  22. package/dist/app/DocActionBar.js +300 -0
  23. package/dist/app/DocumentContextMenu.d.ts +29 -0
  24. package/dist/app/DocumentContextMenu.js +122 -0
  25. package/dist/app/DocumentPane.d.ts +52 -0
  26. package/dist/app/DocumentPane.js +30 -0
  27. package/dist/app/DocumentRouterTable.d.ts +58 -0
  28. package/dist/app/DocumentRouterTable.js +143 -0
  29. package/dist/app/ErrorBoundary.d.ts +12 -0
  30. package/dist/app/ErrorBoundary.js +16 -0
  31. package/dist/app/FindBar.d.ts +12 -0
  32. package/dist/app/FindBar.js +199 -0
  33. package/dist/app/LiveTitle.d.ts +14 -0
  34. package/dist/app/LiveTitle.js +14 -0
  35. package/dist/app/OpeningProgress.d.ts +13 -0
  36. package/dist/app/OpeningProgress.js +21 -0
  37. package/dist/app/PrintPreviewWindow.d.ts +2 -0
  38. package/dist/app/PrintPreviewWindow.js +57 -0
  39. package/dist/app/ShortcutHelp.d.ts +3 -0
  40. package/dist/app/ShortcutHelp.js +99 -0
  41. package/dist/app/Sidebar.d.ts +36 -0
  42. package/dist/app/Sidebar.js +121 -0
  43. package/dist/app/ToastViewport.d.ts +1 -0
  44. package/dist/app/ToastViewport.js +31 -0
  45. package/dist/app/Toolbar.d.ts +26 -0
  46. package/dist/app/Toolbar.js +63 -0
  47. package/dist/app/activeFormatSavers.d.ts +7 -0
  48. package/dist/app/activeFormatSavers.js +4 -0
  49. package/dist/app/clipboardPaths.d.ts +1 -0
  50. package/dist/app/clipboardPaths.js +31 -0
  51. package/dist/app/documentDrafts.d.ts +17 -0
  52. package/dist/app/documentDrafts.js +104 -0
  53. package/dist/app/draftResolution.d.ts +5 -0
  54. package/dist/app/draftResolution.js +25 -0
  55. package/dist/app/markdownPaletteActions.d.ts +8 -0
  56. package/dist/app/markdownPaletteActions.js +107 -0
  57. package/dist/app/resourceCensusDev.d.ts +41 -0
  58. package/dist/app/resourceCensusDev.js +518 -0
  59. package/dist/app/setupRecovery.d.ts +34 -0
  60. package/dist/app/setupRecovery.js +70 -0
  61. package/dist/app/shell/DocStatsPill.d.ts +18 -0
  62. package/dist/app/shell/DocStatsPill.js +32 -0
  63. package/dist/app/shell/DocumentTools.d.ts +9 -0
  64. package/dist/app/shell/DocumentTools.js +73 -0
  65. package/dist/app/shell/InfoInspector.d.ts +9 -0
  66. package/dist/app/shell/InfoInspector.js +67 -0
  67. package/dist/app/shell/StartPage.d.ts +9 -0
  68. package/dist/app/shell/StartPage.js +49 -0
  69. package/dist/app/shell/readEdit.d.ts +6 -0
  70. package/dist/app/shell/readEdit.js +6 -0
  71. package/dist/app/sidebar/SidebarDocumentMenu.d.ts +20 -0
  72. package/dist/app/sidebar/SidebarDocumentMenu.js +56 -0
  73. package/dist/app/sidebar/SidebarExpandedPanel.d.ts +9 -0
  74. package/dist/app/sidebar/SidebarExpandedPanel.js +78 -0
  75. package/dist/app/sidebar/SidebarIcons.d.ts +3 -0
  76. package/dist/app/sidebar/SidebarIcons.js +18 -0
  77. package/dist/app/sidebar/SidebarMenus.d.ts +19 -0
  78. package/dist/app/sidebar/SidebarMenus.js +64 -0
  79. package/dist/app/sidebar/SidebarPathUtils.d.ts +10 -0
  80. package/dist/app/sidebar/SidebarPathUtils.js +55 -0
  81. package/dist/app/sidebar/SidebarPrimitives.d.ts +12 -0
  82. package/dist/app/sidebar/SidebarPrimitives.js +45 -0
  83. package/dist/app/sidebar/SidebarRail.d.ts +5 -0
  84. package/dist/app/sidebar/SidebarRail.js +37 -0
  85. package/dist/app/sidebar/explorer/ExplorerRowMenu.d.ts +21 -0
  86. package/dist/app/sidebar/explorer/ExplorerRowMenu.js +37 -0
  87. package/dist/app/sidebar/explorer/ExplorerSections.d.ts +16 -0
  88. package/dist/app/sidebar/explorer/ExplorerSections.js +259 -0
  89. package/dist/app/sidebar/explorer/ExplorerTree.d.ts +46 -0
  90. package/dist/app/sidebar/explorer/ExplorerTree.js +387 -0
  91. package/dist/app/sidebar/explorer/explorerState.d.ts +37 -0
  92. package/dist/app/sidebar/explorer/explorerState.js +135 -0
  93. package/dist/app/tabMountPolicy.d.ts +45 -0
  94. package/dist/app/tabMountPolicy.js +104 -0
  95. package/dist/app/tabWarmOrder.d.ts +7 -0
  96. package/dist/app/tabWarmOrder.js +48 -0
  97. package/dist/app/tabs/TabFuseCurves.d.ts +3 -0
  98. package/dist/app/tabs/TabFuseCurves.js +15 -0
  99. package/dist/app/tabs/TabOverview.d.ts +24 -0
  100. package/dist/app/tabs/TabOverview.js +121 -0
  101. package/dist/app/tabs/TabStrip.d.ts +42 -0
  102. package/dist/app/tabs/TabStrip.js +380 -0
  103. package/dist/app/tabs/tabIds.d.ts +5 -0
  104. package/dist/app/tabs/tabIds.js +7 -0
  105. package/dist/app/tabs/useTabDirty.d.ts +3 -0
  106. package/dist/app/tabs/useTabDirty.js +11 -0
  107. package/dist/app/useAppChromeState.d.ts +21 -0
  108. package/dist/app/useAppChromeState.js +48 -0
  109. package/dist/app/useAppCommands.d.ts +66 -0
  110. package/dist/app/useAppCommands.js +71 -0
  111. package/dist/app/useAppDocumentCore.d.ts +210 -0
  112. package/dist/app/useAppDocumentCore.js +40 -0
  113. package/dist/app/useAppEffects.d.ts +18 -0
  114. package/dist/app/useAppEffects.js +52 -0
  115. package/dist/app/useAppPaneSearch.d.ts +37 -0
  116. package/dist/app/useAppPaneSearch.js +74 -0
  117. package/dist/app/useAppShellSetup.d.ts +40 -0
  118. package/dist/app/useAppShellSetup.js +52 -0
  119. package/dist/app/useAppSlots.d.ts +28 -0
  120. package/dist/app/useAppSlots.js +40 -0
  121. package/dist/app/useAppTabChrome.d.ts +51 -0
  122. package/dist/app/useAppTabChrome.js +66 -0
  123. package/dist/app/useAutoSave.d.ts +34 -0
  124. package/dist/app/useAutoSave.js +116 -0
  125. package/dist/app/useCloseAll.d.ts +52 -0
  126. package/dist/app/useCloseAll.js +295 -0
  127. package/dist/app/useClosedTabEviction.d.ts +10 -0
  128. package/dist/app/useClosedTabEviction.js +38 -0
  129. package/dist/app/useCommandPalette.d.ts +44 -0
  130. package/dist/app/useCommandPalette.js +254 -0
  131. package/dist/app/useCrossDocSearch.d.ts +7 -0
  132. package/dist/app/useCrossDocSearch.js +94 -0
  133. package/dist/app/useDistractionFree.d.ts +21 -0
  134. package/dist/app/useDistractionFree.js +73 -0
  135. package/dist/app/useDocumentCreation.d.ts +18 -0
  136. package/dist/app/useDocumentCreation.js +63 -0
  137. package/dist/app/useDocumentDirty.d.ts +31 -0
  138. package/dist/app/useDocumentDirty.js +46 -0
  139. package/dist/app/useDocumentLoader.d.ts +55 -0
  140. package/dist/app/useDocumentLoader.js +267 -0
  141. package/dist/app/useDocumentOutputActions.d.ts +6 -0
  142. package/dist/app/useDocumentOutputActions.js +45 -0
  143. package/dist/app/useDocumentRuntimeEffects.d.ts +36 -0
  144. package/dist/app/useDocumentRuntimeEffects.js +458 -0
  145. package/dist/app/useDocumentSaver.d.ts +58 -0
  146. package/dist/app/useDocumentSaver.js +511 -0
  147. package/dist/app/useDocumentTabs.d.ts +41 -0
  148. package/dist/app/useDocumentTabs.js +207 -0
  149. package/dist/app/useDocumentWatcher.d.ts +83 -0
  150. package/dist/app/useDocumentWatcher.js +393 -0
  151. package/dist/app/useDocumentZoom.d.ts +24 -0
  152. package/dist/app/useDocumentZoom.js +104 -0
  153. package/dist/app/useDocuments.d.ts +169 -0
  154. package/dist/app/useDocuments.js +455 -0
  155. package/dist/app/useDraftConfirmGate.d.ts +27 -0
  156. package/dist/app/useDraftConfirmGate.js +47 -0
  157. package/dist/app/useFileDragOverlay.d.ts +12 -0
  158. package/dist/app/useFileDragOverlay.js +49 -0
  159. package/dist/app/useFormatApis.d.ts +13 -0
  160. package/dist/app/useFormatApis.js +62 -0
  161. package/dist/app/useFormatDirtyState.d.ts +29 -0
  162. package/dist/app/useFormatDirtyState.js +81 -0
  163. package/dist/app/useGlobalShortcuts.d.ts +47 -0
  164. package/dist/app/useGlobalShortcuts.js +359 -0
  165. package/dist/app/useHtmlSession.d.ts +68 -0
  166. package/dist/app/useHtmlSession.js +463 -0
  167. package/dist/app/useHtmlSessionProps.d.ts +33 -0
  168. package/dist/app/useHtmlSessionProps.js +57 -0
  169. package/dist/app/useLibrary.d.ts +21 -0
  170. package/dist/app/useLibrary.js +81 -0
  171. package/dist/app/useOpenFromClipboard.d.ts +7 -0
  172. package/dist/app/useOpenFromClipboard.js +44 -0
  173. package/dist/app/usePaneLiveness.d.ts +29 -0
  174. package/dist/app/usePaneLiveness.js +199 -0
  175. package/dist/app/usePendingSavePaths.d.ts +19 -0
  176. package/dist/app/usePendingSavePaths.js +67 -0
  177. package/dist/app/useReadyShow.d.ts +12 -0
  178. package/dist/app/useReadyShow.js +142 -0
  179. package/dist/app/useSearchController.d.ts +22 -0
  180. package/dist/app/useSearchController.js +114 -0
  181. package/dist/app/useSetupRecovery.d.ts +41 -0
  182. package/dist/app/useSetupRecovery.js +153 -0
  183. package/dist/app/useTabChrome.d.ts +40 -0
  184. package/dist/app/useTabChrome.js +115 -0
  185. package/dist/app/useViewState.d.ts +27 -0
  186. package/dist/app/useViewState.js +160 -0
  187. package/dist/appName.d.ts +2 -0
  188. package/dist/appName.js +8 -0
  189. package/dist/documents/DocumentRouter.d.ts +102 -0
  190. package/dist/documents/DocumentRouter.js +153 -0
  191. package/dist/documents/FileTypeIcon.d.ts +6 -0
  192. package/dist/documents/FileTypeIcon.js +93 -0
  193. package/dist/documents/ImageDocument.d.ts +14 -0
  194. package/dist/documents/ImageDocument.js +61 -0
  195. package/dist/documents/capabilities.d.ts +17 -0
  196. package/dist/documents/capabilities.js +18 -0
  197. package/dist/documents/html/HtmlDocument.d.ts +15 -0
  198. package/dist/documents/html/HtmlDocument.js +584 -0
  199. package/dist/documents/html/HtmlDocumentLayout.d.ts +87 -0
  200. package/dist/documents/html/HtmlDocumentLayout.js +25 -0
  201. package/dist/documents/html/HtmlDocumentProps.d.ts +60 -0
  202. package/dist/documents/html/HtmlDocumentProps.js +1 -0
  203. package/dist/documents/html/HtmlDocumentSource.d.ts +34 -0
  204. package/dist/documents/html/HtmlDocumentSource.js +28 -0
  205. package/dist/documents/markdown/MarkdownSplitPane.d.ts +115 -0
  206. package/dist/documents/markdown/MarkdownSplitPane.js +352 -0
  207. package/dist/documents/mdx/MdxSplitPane.d.ts +32 -0
  208. package/dist/documents/mdx/MdxSplitPane.js +60 -0
  209. package/dist/documents/txt/LinkDialog.d.ts +8 -0
  210. package/dist/documents/txt/LinkDialog.js +34 -0
  211. package/dist/documents/txt/MarkdownSourceToolbar.d.ts +8 -0
  212. package/dist/documents/txt/MarkdownSourceToolbar.js +25 -0
  213. package/dist/documents/txt/SourceEditorToolbar.d.ts +12 -0
  214. package/dist/documents/txt/SourceEditorToolbar.js +4 -0
  215. package/dist/documents/txt/TxtEditor.d.ts +39 -0
  216. package/dist/documents/txt/TxtEditor.js +186 -0
  217. package/dist/documents/txt/sourceEditorTypes.d.ts +49 -0
  218. package/dist/documents/txt/sourceEditorTypes.js +1 -0
  219. package/dist/documents/txt/sourceExtensions.d.ts +38 -0
  220. package/dist/documents/txt/sourceExtensions.js +444 -0
  221. package/dist/documents/txt/sourceFormatting.d.ts +18 -0
  222. package/dist/documents/txt/sourceFormatting.js +183 -0
  223. package/dist/documents/txt/sourceTheme.d.ts +5 -0
  224. package/dist/documents/txt/sourceTheme.js +220 -0
  225. package/dist/documents/txt/useCodeMirrorSourceEditor.d.ts +69 -0
  226. package/dist/documents/txt/useCodeMirrorSourceEditor.js +546 -0
  227. package/dist/documents/txt/useSourceSearch.d.ts +39 -0
  228. package/dist/documents/txt/useSourceSearch.js +122 -0
  229. package/dist/documents/txt/useTextEditorCommands.d.ts +26 -0
  230. package/dist/documents/txt/useTextEditorCommands.js +151 -0
  231. package/dist/documents/web/LivePreviewPane.d.ts +11 -0
  232. package/dist/documents/web/LivePreviewPane.js +58 -0
  233. package/dist/env.d.ts +66 -0
  234. package/dist/index.d.ts +121 -0
  235. package/dist/index.js +123 -0
  236. package/dist/settings/AppearanceMenu.d.ts +9 -0
  237. package/dist/settings/AppearanceMenu.js +54 -0
  238. package/dist/settings/SettingsPanel.d.ts +7 -0
  239. package/dist/settings/SettingsPanel.js +104 -0
  240. package/dist/settings/publishedSections.d.ts +22 -0
  241. package/dist/settings/publishedSections.js +79 -0
  242. package/dist/settings/sections/AboutSection.d.ts +1 -0
  243. package/dist/settings/sections/AboutSection.js +13 -0
  244. package/dist/settings/sections/DiagnosticsSection.d.ts +1 -0
  245. package/dist/settings/sections/DiagnosticsSection.js +42 -0
  246. package/dist/settings/sections/PrivacySection.d.ts +3 -0
  247. package/dist/settings/sections/PrivacySection.js +51 -0
  248. package/dist/settings/sections/UpdateLicenseBlock.d.ts +10 -0
  249. package/dist/settings/sections/UpdateLicenseBlock.js +158 -0
  250. package/dist/state/explorer.d.ts +32 -0
  251. package/dist/state/explorer.js +111 -0
  252. package/dist/state/legal.d.ts +51 -0
  253. package/dist/state/legal.js +130 -0
  254. package/dist/state/library.d.ts +33 -0
  255. package/dist/state/library.js +110 -0
  256. package/dist/state/updateLicense.d.ts +31 -0
  257. package/dist/state/updateLicense.js +340 -0
  258. package/dist/styles/explorer.css +219 -0
  259. package/dist/styles/index.css +1477 -0
  260. package/dist/styles/tabs.css +467 -0
  261. package/dist/styles.css +6 -0
  262. package/package.json +88 -3
@@ -0,0 +1,458 @@
1
+ import { useEffect, useRef, useState, } from "react";
2
+ import { getCurrentWindow } from "@tauri-apps/api/window";
3
+ import { docTitle, isUntitled } from "@rightkit/document-core/documents/types";
4
+ import { getInitialOpenPaths, inTauri, onAppEvent, grantKnownFileToken, } from "@rightkit/document-core/state/tauriBridge";
5
+ import { clearSession, loadSession, saveSession } from "@rightkit/document-core/state/tauriBridge";
6
+ import { toast } from "@rightkit/platform-ui/headless";
7
+ import { readDraft } from "./documentDrafts.js";
8
+ import { scheduleAfterPaintOrDelay } from "@rightkit/document-core/app/deferredMount";
9
+ import { listenForNativeDrops } from "@rightkit/document-core/app/dropCoordination";
10
+ /** Route native open-file events after the Markdown canvas has had a chance to consume image drops. */
11
+ export function handleOpenFileEvent(path, open) {
12
+ // A multi-file Open With / rapid second-instance handoff emits one event per path back to back.
13
+ // Run them one at a time so overlapping opens cannot interleave their tab, recents and session
14
+ // writes; a failed open must not stall the files queued behind it.
15
+ openFileChain = openFileChain.then(() => open(path)).then(() => undefined, () => undefined);
16
+ }
17
+ let openFileChain = Promise.resolve();
18
+ let sessionSaveRunning = false;
19
+ let sessionSaveNext = null;
20
+ /** Persist the session tabs with at most one native write in flight, newest snapshot last, so
21
+ * out-of-order IPC completion can never leave an older, shorter tab list as the saved session. */
22
+ export function saveSessionOrdered(tabs) {
23
+ sessionSaveNext = tabs;
24
+ if (sessionSaveRunning)
25
+ return;
26
+ sessionSaveRunning = true;
27
+ void (async () => {
28
+ try {
29
+ while (sessionSaveNext) {
30
+ const snapshot = sessionSaveNext;
31
+ sessionSaveNext = null;
32
+ try {
33
+ await saveSession(snapshot);
34
+ }
35
+ catch {
36
+ /* best effort; the next snapshot retries */
37
+ }
38
+ }
39
+ }
40
+ finally {
41
+ sessionSaveRunning = false;
42
+ }
43
+ })();
44
+ }
45
+ /** Drop any queued session snapshot and wait for the in-flight native write to finish, so a purge
46
+ * cannot be followed by a stale write that resurrects deleted rows. The next normal save writes the
47
+ * current tabs only. */
48
+ export async function drainSessionSaves() {
49
+ sessionSaveNext = null;
50
+ while (sessionSaveRunning)
51
+ await new Promise((r) => setTimeout(r, 10));
52
+ sessionSaveNext = null;
53
+ }
54
+ function safelyUnlisten(listener) {
55
+ try {
56
+ // Tauri's dev bridge can return a rejecting thenable despite listener typing as void.
57
+ // Consume both synchronous throws & asynchronous bridge teardown races.
58
+ void Promise.resolve(listener()).catch(() => { });
59
+ }
60
+ catch {
61
+ /* Tauri dev bridge may already have removed this listener. */
62
+ }
63
+ }
64
+ // The benchmark autopilots are dev or browser-QA only (same build-time guard as
65
+ // `installResourceCensusDevHook`): in a production build these fold to constants and the
66
+ // autopilot branches are dropped, whatever the build environment carries.
67
+ const AUTOMATION_BUILD = import.meta.env?.DEV || import.meta.env?.VITE_RIGHTKIT_BROWSER_QA === "1";
68
+ export function nativeAutomationOwnsSession() {
69
+ return (AUTOMATION_BUILD &&
70
+ (import.meta.env?.VITE_PDF_BENCH_AUTOPILOT === "1" ||
71
+ import.meta.env?.VITE_RESOURCE_MATRIX_AUTOPILOT === "1"));
72
+ }
73
+ function benchmarkInitialOpenPaths(initialOpenPaths) {
74
+ if (!AUTOMATION_BUILD || import.meta.env?.VITE_PDF_BENCH_AUTOPILOT !== "1")
75
+ return initialOpenPaths;
76
+ const fixturePath = String(import.meta.env?.VITE_PDF_BENCH_FIXTURE_PATH ?? "").trim();
77
+ return fixturePath ? [...new Set([...initialOpenPaths, fixturePath])] : initialOpenPaths;
78
+ }
79
+ export function useDocumentRuntimeEffects({ openDocs, activePath, hydrated, untitledSeq, refreshLibrary, setOpenDocs, writeEdits, setViews, activate, open, reload, isDirty, setConfirmQuit, prepareSetupRecovery, restoreSetupRecovery, restoreSession, blockDrafts, setRestoringSession, }) {
80
+ // Latest committed active tab, for the async launch flow: a launched file (or the user) that has
81
+ // already taken activation must not be displaced by the background session restore.
82
+ const activePathRef = useRef(activePath);
83
+ activePathRef.current = activePath;
84
+ // Bumped once when a file-launch finishes hydrating. The launched tabs open BEFORE hydration (so
85
+ // they show first), which means their openDocs/activePath changes were skipped by the guard
86
+ // below; this re-runs the persist effect once so the launched file is saved like it used to be.
87
+ const [launchPersistTick, setLaunchPersistTick] = useState(0);
88
+ // Persist the open-tab session (all tabs incl. untitled, + active) so it restores on next launch.
89
+ // Guarded until after restore — otherwise the empty initial state would wipe the saved session.
90
+ useEffect(() => {
91
+ void launchPersistTick;
92
+ if (!hydrated.current)
93
+ return;
94
+ // Native benchmark launchers own their exact fixture set. Restoring or persisting normal user
95
+ // tabs would contaminate transport/resource measurements & overwrite the user's saved session.
96
+ if (nativeAutomationOwnsSession())
97
+ return;
98
+ const untitledPaths = openDocs.map((d) => d.path).filter(isUntitled);
99
+ localStorage.setItem("vr.session.open", JSON.stringify(untitledPaths));
100
+ localStorage.setItem("vr.session.formats", JSON.stringify(Object.fromEntries(openDocs
101
+ .filter((doc) => isUntitled(doc.path) && doc.format === "md")
102
+ .map((doc) => [doc.path, doc.format]))));
103
+ if (activePath && isUntitled(activePath)) {
104
+ localStorage.setItem("vr.session.active", activePath);
105
+ }
106
+ else {
107
+ localStorage.removeItem("vr.session.active");
108
+ }
109
+ if (!restoreSession) {
110
+ void clearSession();
111
+ return;
112
+ }
113
+ // Also persist file-backed tabs to SQLite for cross-launch restore.
114
+ const fileTabs = openDocs
115
+ .filter((d) => !isUntitled(d.path))
116
+ .map((d) => [d.path, d.path === activePath]);
117
+ saveSessionOrdered(fileTabs);
118
+ }, [hydrated, openDocs, activePath, restoreSession, launchPersistTick]);
119
+ // Launch: restore only never-saved buffers. Real files must re-enter through an OS-level
120
+ // grant (dialog, drag/drop, file association, or confirmed recent).
121
+ useEffect(() => {
122
+ let disposed = false;
123
+ let unOpenFile;
124
+ let unNativeDrop;
125
+ setRestoringSession(true);
126
+ (async () => {
127
+ try {
128
+ const restoreFiles = restoreSession && !nativeAutomationOwnsSession();
129
+ // The SQLite session read is independent of the listener / initial-path handshake below;
130
+ // start it now so it overlaps those round-trips instead of queueing behind them. The no-op
131
+ // catch only marks the rejection handled until it is awaited in the restore try/catch.
132
+ const sessionLoad = restoreFiles ? loadSession() : null;
133
+ sessionLoad?.catch(() => { });
134
+ // Register BEFORE `rk_shell_open_paths_ready`: that shell command atomically marks the renderer
135
+ // ready and drains paths queued by argv/macOS RunEvent::Opened. Once it returns, Rust may
136
+ // emit future paths immediately, so installing this listener afterward would leave a small
137
+ // but real startup window where Finder/second-instance opens disappear.
138
+ const [un, unNative] = await Promise.all([
139
+ onAppEvent("open-file", (path) => handleOpenFileEvent(path, open)),
140
+ listenForNativeDrops(open),
141
+ ]);
142
+ if (disposed) {
143
+ un();
144
+ unNative();
145
+ return;
146
+ }
147
+ unOpenFile = un;
148
+ unNativeDrop = unNative;
149
+ // Independent IPC round-trips — fan them out instead of paying two sequential awaits. The
150
+ // recents refresh only feeds the sidebar, so a launched file does not wait for it.
151
+ const libraryRefresh = refreshLibrary();
152
+ libraryRefresh.catch(() => { });
153
+ const initialOpenPaths = benchmarkInitialOpenPaths(await getInitialOpenPaths());
154
+ for (let i = localStorage.length - 1; i >= 0; i--) {
155
+ const key = localStorage.key(i);
156
+ if (key?.startsWith("vr.draft.") && !key.startsWith("vr.draft.untitled:")) {
157
+ localStorage.removeItem(key);
158
+ }
159
+ }
160
+ // File launch (double-click, "Open with", argv): the launched file is what the user asked
161
+ // to see, so it opens before the saved file-backed session and owns activation.
162
+ const launchedPaths = new Set(initialOpenPaths);
163
+ const launchOwnsActive = launchedPaths.size > 0;
164
+ let sessionPaths = [];
165
+ try {
166
+ const parsed = JSON.parse(localStorage.getItem("vr.session.open") ?? "[]");
167
+ sessionPaths = Array.isArray(parsed) ? parsed.filter(isUntitled) : [];
168
+ }
169
+ catch {
170
+ /* ignore */
171
+ }
172
+ let markdownDraftPaths = new Set();
173
+ try {
174
+ const formats = JSON.parse(localStorage.getItem("vr.session.formats") ?? "{}");
175
+ if (formats && typeof formats === "object" && !Array.isArray(formats)) {
176
+ markdownDraftPaths = new Set(Object.entries(formats)
177
+ .filter(([path, format]) => isUntitled(path) && format === "md")
178
+ .map(([path]) => path));
179
+ }
180
+ }
181
+ catch {
182
+ /* Older or malformed session metadata restores as plain text. */
183
+ }
184
+ const savedActive = localStorage.getItem("vr.session.active");
185
+ const paths = [...new Set(sessionPaths)];
186
+ for (const path of paths) {
187
+ const draft = readDraft(path) ?? "";
188
+ setOpenDocs((prev) => prev.some((x) => x.path === path)
189
+ ? prev
190
+ : [
191
+ ...prev,
192
+ {
193
+ path,
194
+ displayName: docTitle(path),
195
+ format: markdownDraftPaths.has(path) ? "md" : "txt",
196
+ content: "",
197
+ },
198
+ ]);
199
+ writeEdits((e) => ({ ...e, [path]: draft }));
200
+ setViews((v) => ({ ...v, [path]: "preview" }));
201
+ const n = Number.parseInt(path.split(":")[1] ?? "0", 10);
202
+ if (Number.isFinite(n) && n >= untitledSeq.current)
203
+ untitledSeq.current = n + 1;
204
+ }
205
+ // Activate the saved-active untitled buffer up front (vs a random fallback untitled)
206
+ // so the user always sees content — never the empty state — while the file-backed
207
+ // restore loop runs. Activating savedUntitledActive is safe: localStorage's
208
+ // `vr.session.active` is set ONLY for untitled paths (the persist effect removes it
209
+ // when the active becomes a file), so when savedUntitledActive is truthy, the file-
210
+ // backed loop's `sessionActivePath` is guaranteed null and no swap can happen.
211
+ //
212
+ // A `fallbackUntitled` (paths[0] when the previous active was a file — so savedActive
213
+ // is empty) is NOT activated up front: doing so would briefly show a wrong tab before
214
+ // the file-backed loop swapped to the real saved-active file (~300ms flash). Instead,
215
+ // the launch effect defers it; the "Restoring session…" indicator covers the wait
216
+ // because `openDocs.length > 0` (the untitled loop above populated at least one row).
217
+ const savedUntitledActive = savedActive && isUntitled(savedActive) && paths.includes(savedActive)
218
+ ? savedActive
219
+ : null;
220
+ const fallbackUntitled = savedUntitledActive ? null : paths[0];
221
+ if (savedUntitledActive && !launchOwnsActive)
222
+ await activate(savedUntitledActive);
223
+ // Session file path → its saved session position.
224
+ const restoredOrder = new Map();
225
+ // Untitled buffers are already visible. Launch files stay immediately after them;
226
+ // restored files follow in saved order, so background restore cannot move a launch row.
227
+ const tabGroup = (path) => launchedPaths.has(path) ? 1 : restoredOrder.has(path) ? 2 : 0;
228
+ const arrange = (docs) => {
229
+ const next = docs
230
+ .map((doc, index) => ({ doc, index, group: tabGroup(doc.path) }))
231
+ .sort((a, b) => a.group - b.group ||
232
+ (a.group === 2
233
+ ? (restoredOrder.get(a.doc.path) ?? 0) - (restoredOrder.get(b.doc.path) ?? 0)
234
+ : 0) ||
235
+ a.index - b.index)
236
+ .map(({ doc }) => doc);
237
+ return next.every((doc, i) => doc === docs[i]) ? docs : next;
238
+ };
239
+ if (launchOwnsActive) {
240
+ // Never await session metadata or grants before showing the requested file. The first
241
+ // successful foreground open owns activation; a failed earlier path must not leave
242
+ // later successes hidden behind the empty state.
243
+ let launchActivated = false;
244
+ for (const [index, path] of initialOpenPaths.entries()) {
245
+ await open(path, {
246
+ background: launchActivated,
247
+ deferLibraryRefresh: true,
248
+ keepError: index > 0,
249
+ arrange,
250
+ });
251
+ if (!launchActivated) {
252
+ // open() reports failures through the error banner, not its return value. Wait for
253
+ // its state to commit before deciding whether the next file should be background.
254
+ await new Promise((resolve) => scheduleAfterPaintOrDelay(resolve));
255
+ launchActivated = activePathRef.current !== null;
256
+ }
257
+ }
258
+ }
259
+ await libraryRefresh;
260
+ let fileMissing = 0;
261
+ let sessionActivePath = null;
262
+ let firstRestored = null;
263
+ let libraryRefreshed = false;
264
+ if (sessionLoad) {
265
+ // Restore file-backed tabs from the SQLite session. Open them in the BACKGROUND (no activation)
266
+ // so each tab is a lazy placeholder, then activate exactly ONE at the end — otherwise every tab
267
+ // would flash active as it opens and render up front, defeating the lazy first-render.
268
+ try {
269
+ const session = await sessionLoad;
270
+ // Restore saved-active first so the user sees their document before background work starts.
271
+ // Yield one committed frame before the remaining opens compete for startup resources.
272
+ const sessionOrder = new Map(session.map(([token], index) => [token, index]));
273
+ const restoreEntry = async ([token, active]) => {
274
+ const restored = await grantKnownFileToken(token);
275
+ if (!restored)
276
+ return { active, restored: null };
277
+ restoredOrder.set(restored, sessionOrder.get(token) ?? session.length);
278
+ // A launched file that was also in the session is already open and fresh.
279
+ if (!launchedPaths.has(restored)) {
280
+ // keepError: a restore open must not clear an error it didn't set (e.g. the
281
+ // launched file failing to open); its own failure still reports.
282
+ await open(restored, {
283
+ background: true,
284
+ deferLibraryRefresh: true,
285
+ keepError: true,
286
+ arrange,
287
+ });
288
+ }
289
+ return { active, restored };
290
+ };
291
+ const activeEntry = session.find(([, active]) => active);
292
+ const backgroundEntries = session.filter((entry) => entry !== activeEntry);
293
+ if (activeEntry) {
294
+ const activeResult = await restoreEntry(activeEntry);
295
+ if (activeResult.restored) {
296
+ sessionActivePath = activeResult.restored;
297
+ firstRestored = activeResult.restored;
298
+ if (!launchOwnsActive) {
299
+ await activate(activeResult.restored);
300
+ await new Promise((resolve) => scheduleAfterPaintOrDelay(resolve));
301
+ }
302
+ }
303
+ else {
304
+ fileMissing++;
305
+ }
306
+ }
307
+ // Fan out the remaining per-tab grant + background open — each is an independent IPC round-trip
308
+ // (token grant, then read_document), so serializing them was the dominant cost of a
309
+ // multi-tab restore. Promise.allSettled keeps one bad token/file from aborting the rest
310
+ // (matches the original per-token try/catch), and it preserves the INPUT order in its
311
+ // results array regardless of completion order, so which tab ends up "first restored" /
312
+ // "active" below is still decided by session order, not by which IPC call wins the race.
313
+ // Each open() defers its own refreshLibrary — it runs ONCE after the loop instead of once
314
+ // per tab.
315
+ // Bounded fan-out: each open() also prefetches its format island (CodeMirror, PDF.js,
316
+ // remark…), so restoring a big mixed session fully concurrently turns cold start into an
317
+ // I/O + chunk-download burst. Four at a time keeps the pipeline busy without the stampede.
318
+ const RESTORE_CONCURRENCY = 4;
319
+ const results = [];
320
+ for (let i = 0; i < backgroundEntries.length; i += RESTORE_CONCURRENCY) {
321
+ results.push(...(await Promise.allSettled(backgroundEntries.slice(i, i + RESTORE_CONCURRENCY).map(restoreEntry))));
322
+ }
323
+ for (const result of results) {
324
+ if (result.status !== "fulfilled" || !result.value.restored) {
325
+ fileMissing++;
326
+ continue;
327
+ }
328
+ const { active, restored } = result.value;
329
+ firstRestored ??= restored;
330
+ if (active)
331
+ sessionActivePath = restored;
332
+ }
333
+ if (firstRestored || launchedPaths.size > 0) {
334
+ await refreshLibrary();
335
+ libraryRefreshed = true;
336
+ }
337
+ // Each restored tab was inserted in place already, so this is normally a no-op (same
338
+ // array back); it still settles anything added outside this flow meanwhile, as before.
339
+ setOpenDocs(arrange);
340
+ }
341
+ catch {
342
+ /* session load failed — non-fatal */
343
+ }
344
+ }
345
+ if (launchedPaths.size > 0 && !libraryRefreshed)
346
+ await refreshLibrary();
347
+ // A file launch keeps activation unless its open failed and nothing else became active
348
+ // (then fall through to the normal restore choice below). Wait one committed frame first
349
+ // so the ref reflects the launched open's activation rather than a pending render.
350
+ let keepLaunchActive = false;
351
+ if (launchOwnsActive) {
352
+ await new Promise((resolve) => scheduleAfterPaintOrDelay(resolve));
353
+ keepLaunchActive = activePathRef.current !== null;
354
+ }
355
+ if (!keepLaunchActive) {
356
+ // Activate one tab so only it renders now. Prefer the saved-active file (covers the
357
+ // case where savedUntitledActive was null and we deferred), then the deferred
358
+ // fallbackUntitled (covers the case where the file-backed loop found no active file
359
+ // either, and restoreSession off, where no file-backed tabs load at all), then the
360
+ // first restored file (never land on the empty state while tabs are open). A
361
+ // saved-active untitled was already activated up front — except on a failed file
362
+ // launch, which skipped that — and the chain never overrides it with `firstRestored`,
363
+ // which is just a "first file we happened to open in the loop", not the user's actual
364
+ // previous active. activate() calls are idempotent (React skips the re-render if the
365
+ // path didn't change).
366
+ if (sessionActivePath)
367
+ await activate(sessionActivePath);
368
+ else if (savedUntitledActive) {
369
+ if (launchOwnsActive)
370
+ await activate(savedUntitledActive);
371
+ }
372
+ else if (fallbackUntitled)
373
+ await activate(fallbackUntitled);
374
+ else if (firstRestored)
375
+ await activate(firstRestored);
376
+ }
377
+ if (fileMissing > 0)
378
+ toast(`${fileMissing} session file(s) not found`, "info");
379
+ const recoveredActive = await restoreSetupRecovery();
380
+ if (recoveredActive && !keepLaunchActive)
381
+ await activate(recoveredActive);
382
+ hydrated.current = true;
383
+ // The launched tabs opened before hydration; persist the session they now belong to.
384
+ if (launchOwnsActive && !disposed)
385
+ setLaunchPersistTick((tick) => tick + 1);
386
+ }
387
+ finally {
388
+ // Always clear the restore indicator — even if restore throws, so the user never sees
389
+ // a stuck "Restoring session…" pane. The state setter is safe to call repeatedly.
390
+ if (!disposed)
391
+ setRestoringSession(false);
392
+ }
393
+ })();
394
+ return () => {
395
+ disposed = true;
396
+ unOpenFile?.();
397
+ unNativeDrop?.();
398
+ };
399
+ // eslint-disable-next-line react-hooks/exhaustive-deps
400
+ }, [
401
+ activate,
402
+ open,
403
+ restoreSession,
404
+ restoreSetupRecovery,
405
+ writeEdits,
406
+ untitledSeq,
407
+ setViews,
408
+ setOpenDocs,
409
+ refreshLibrary,
410
+ hydrated,
411
+ setRestoringSession,
412
+ ]);
413
+ // OS / drag-drop / watcher events.
414
+ useEffect(() => {
415
+ let disposed = false;
416
+ const uns = [];
417
+ const track = (p) => void p.then((u) => (disposed ? safelyUnlisten(u) : uns.push(u))).catch(() => { });
418
+ track(onAppEvent("file-changed", (path) => reload(path)));
419
+ track(onAppEvent("prepare-setup", () => void prepareSetupRecovery()));
420
+ // Diagnostic: surface the Rust drag-drop event stream in the devtools console so we can see
421
+ // whether drops over the sidebar reach the webview at all.
422
+ return () => {
423
+ disposed = true;
424
+ uns.forEach(safelyUnlisten);
425
+ };
426
+ }, [prepareSetupRecovery, reload]);
427
+ // Guard the window close: if any doc has unsaved changes, prompt instead of quitting — EXCEPT
428
+ // untitled txt scratch buffers, which are auto-persisted to localStorage and restored on next
429
+ // launch (Notepad-style), so quitting never loses them. Only file-backed / Word edits can be lost.
430
+ useEffect(() => {
431
+ if (!inTauri())
432
+ return;
433
+ let unlisten;
434
+ let disposed = false;
435
+ getCurrentWindow()
436
+ .onCloseRequested((event) => {
437
+ // txt (both untitled and file-backed) persists edits to localStorage and restores them on
438
+ // next launch (Notepad-style) — no data loss, so no prompt needed for any txt format.
439
+ const dirtyDocs = openDocs.some((d) => isDirty(d) && d.format !== "txt");
440
+ const openDrafts = blockDrafts ? blockDrafts.snapshotModified().length > 0 : false;
441
+ if (dirtyDocs || openDrafts) {
442
+ event.preventDefault();
443
+ setConfirmQuit(true);
444
+ }
445
+ })
446
+ .then((u) => {
447
+ if (disposed)
448
+ safelyUnlisten(u);
449
+ else
450
+ unlisten = u;
451
+ });
452
+ return () => {
453
+ disposed = true;
454
+ if (unlisten)
455
+ safelyUnlisten(unlisten);
456
+ };
457
+ }, [openDocs, isDirty, setConfirmQuit, blockDrafts]);
458
+ }
@@ -0,0 +1,58 @@
1
+ import { type Dispatch, type MutableRefObject, type SetStateAction } from "react";
2
+ import { type LoadedDocument, type ViewMode } from "@rightkit/document-core/documents/types";
3
+ import type { DocumentTabs } from "./useDocumentTabs.js";
4
+ import type { ImageSaveFunction, ImageSavePayload } from "@rightkit/media-ui";
5
+ import type { BlockDraftRegistry } from "@rightkit/document-core/app/blockDrafts";
6
+ import type { PdfSaveFunction } from "@rightkit/document-core/documents/pdf/pdfEditor";
7
+ import type { PdfSavePlan } from "@rightkit/document-core/documents/pdf/pdfEditModel";
8
+ interface DocumentSaverDeps {
9
+ editsRef: DocumentTabs["editsRef"];
10
+ writeEdits: DocumentTabs["writeEdits"];
11
+ setDocxDirty: DocumentTabs["setDocxDirty"];
12
+ setPdfDirty: DocumentTabs["setPdfDirty"];
13
+ pdfDirtyRef: DocumentTabs["pdfDirtyRef"];
14
+ setOpenDocs: DocumentTabs["setOpenDocs"];
15
+ setActivePath: DocumentTabs["setActivePath"];
16
+ setViews: Dispatch<SetStateAction<Record<string, ViewMode>>>;
17
+ setError: Dispatch<SetStateAction<string | null>>;
18
+ setConflict?: Dispatch<SetStateAction<string | null>>;
19
+ fingerprints: MutableRefObject<Map<string, {
20
+ mtimeMs: number;
21
+ size: number;
22
+ }>>;
23
+ refreshLibrary: () => Promise<void>;
24
+ imageSaver: MutableRefObject<ImageSaveFunction | null>;
25
+ imageSaversRef: MutableRefObject<Map<string, ImageSaveFunction>>;
26
+ docxSaver: MutableRefObject<(() => Promise<ArrayBuffer | null>) | null>;
27
+ docxSaversRef: MutableRefObject<Map<string, () => Promise<ArrayBuffer | null>>>;
28
+ pdfSaversRef: MutableRefObject<Map<string, PdfSaveFunction>>;
29
+ pdfSavePlansRef: MutableRefObject<Map<string, () => Promise<PdfSavePlan>>>;
30
+ clearImageDirty: (path: string) => void;
31
+ writeImagePayload: (path: string, payload: ImageSavePayload) => Promise<void>;
32
+ releaseReplacedBackingPath: (oldPath: string, newPath: string) => void;
33
+ withTabKey: <T extends LoadedDocument>(doc: T, existingKey?: string) => T;
34
+ blockDrafts?: BlockDraftRegistry;
35
+ }
36
+ /**
37
+ * Save / Save-As across every document format: text/markdown/mdx (writeDocument), DOCX (bytes +
38
+ * Save-As tab replacement), and images (payload write + Save-As tab replacement). Extracted verbatim
39
+ * from useDocuments — bodies, error handling, toasts, and fingerprint/dirty bookkeeping are unchanged.
40
+ * `markSelfSaved` + `refreshFingerprint` are owned here (this hook is the only writer of self-saved
41
+ * echo suppression and post-write fingerprints for save operations); `refreshFingerprint` is also
42
+ * exposed so the orchestrator's reload/reload-adjacent code can reuse the exact same fingerprint map.
43
+ */
44
+ export declare function useDocumentSaver({ editsRef, writeEdits, setDocxDirty, setPdfDirty, pdfDirtyRef, setOpenDocs, setActivePath, setViews, setError, setConflict, fingerprints, refreshLibrary, imageSaver, imageSaversRef, docxSaver, docxSaversRef, pdfSaversRef, pdfSavePlansRef, clearImageDirty, writeImagePayload, releaseReplacedBackingPath, withTabKey, blockDrafts, }: DocumentSaverDeps): {
45
+ onSave: (doc: LoadedDocument, options?: {
46
+ silent?: boolean;
47
+ resolveDrafts?: boolean;
48
+ }) => Promise<string | null>;
49
+ onSaveAs: (doc: LoadedDocument) => Promise<string | null>;
50
+ onDocxSaveAs: (doc: LoadedDocument, buffer?: ArrayBuffer) => Promise<string | null>;
51
+ onImageSaveAs: (doc: LoadedDocument) => Promise<string | null>;
52
+ markSelfSaved: (path: string) => void;
53
+ refreshFingerprint: (path: string) => void;
54
+ selfSaved: import("react").RefObject<Set<string>>;
55
+ pendingSavePaths: Set<string>;
56
+ pendingSavePathsRef: import("react").RefObject<ReadonlySet<string>>;
57
+ };
58
+ export {};