@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,387 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The explorer directory tree: lazy one-level listing per expanded dir (explorer_list_dir),
3
+ // non-recursive watches while a dir is expanded (explorer_watch_dir / explorer_unwatch_dir +
4
+ // "explorer:changed" re-lists just that dir), ARIA tree semantics with roving tabindex and
5
+ // arrow-key navigation, and the shared row chrome (caret, folder/format icon, name, "…" button).
6
+ import { memo, useCallback, useEffect, useMemo, useRef, useState, } from "react";
7
+ import { CaretDown } from "@phosphor-icons/react/dist/icons/CaretDown";
8
+ import { CaretRight } from "@phosphor-icons/react/dist/icons/CaretRight";
9
+ import { Cloud } from "@phosphor-icons/react/dist/icons/Cloud";
10
+ import { Desktop } from "@phosphor-icons/react/dist/icons/Desktop";
11
+ import { DotsThree } from "@phosphor-icons/react/dist/icons/DotsThree";
12
+ import { DownloadSimple } from "@phosphor-icons/react/dist/icons/DownloadSimple";
13
+ import { File } from "@phosphor-icons/react/dist/icons/File";
14
+ import { FileText } from "@phosphor-icons/react/dist/icons/FileText";
15
+ import { Folder } from "@phosphor-icons/react/dist/icons/Folder";
16
+ import { HardDrives } from "@phosphor-icons/react/dist/icons/HardDrives";
17
+ import { House } from "@phosphor-icons/react/dist/icons/House";
18
+ import { FileTypeIcon } from "../../../documents/FileTypeIcon.js";
19
+ import { explorerListDir, explorerUnwatchDir, explorerWatchDir, onExplorerChanged, } from "../../../state/explorer.js";
20
+ import { parseHostError } from "@rightkit/document-core/state/tauriBridge";
21
+ import { childNameOnPath, pathInside, rowKindOf } from "./explorerState.js";
22
+ import { hostAppName } from "../../../appName.js";
23
+ function explorerErrorMessage(e) {
24
+ // Commands reject with the typed {code,message} envelope (e.g. "permission_denied") — show its
25
+ // message inline in the tree rather than toasting or crashing.
26
+ return parseHostError(e).message;
27
+ }
28
+ /** `wantedPath` is the active file; a folder on its way is listed with the next segment's name so
29
+ * the backend keeps that row even past its entry cap (otherwise the reveal stops at that folder). */
30
+ export function useExplorerTree(wantedPath = null) {
31
+ const [expanded, setExpanded] = useState(() => new Set());
32
+ const [dirs, setDirs] = useState(() => new Map());
33
+ const expandedRef = useRef(expanded);
34
+ expandedRef.current = expanded;
35
+ const dirsRef = useRef(dirs);
36
+ dirsRef.current = dirs;
37
+ const wantedRef = useRef(wantedPath);
38
+ wantedRef.current = wantedPath;
39
+ // Synchronous guards so a double-fired effect (StrictMode) or rapid toggle can't double-list or
40
+ // double-watch a dir before the state update lands.
41
+ const watchedRef = useRef(new Set());
42
+ const loadingRef = useRef(new Set());
43
+ const setDir = useCallback((path, next) => {
44
+ setDirs((prev) => {
45
+ const m = new Map(prev);
46
+ m.set(path, next);
47
+ return m;
48
+ });
49
+ }, []);
50
+ const fetchDir = useCallback((path) => {
51
+ if (loadingRef.current.has(path))
52
+ return;
53
+ loadingRef.current.add(path);
54
+ const cur = dirsRef.current.get(path);
55
+ setDir(path, {
56
+ status: "loading",
57
+ entries: cur?.entries ?? [],
58
+ truncated: cur?.truncated ?? false,
59
+ });
60
+ void explorerListDir(path, childNameOnPath(path, wantedRef.current)).then((listing) => {
61
+ loadingRef.current.delete(path);
62
+ setDir(path, {
63
+ status: "ready",
64
+ // A folder never lists itself: a child carrying its parent's path (a link that
65
+ // resolves back to the folder) would make an expanded folder render inside itself
66
+ // without end.
67
+ entries: keepUnchangedEntries(dirsRef.current.get(path)?.entries, listing.entries.filter((e) => e.path !== path)),
68
+ truncated: listing.truncated,
69
+ });
70
+ }, (e) => {
71
+ loadingRef.current.delete(path);
72
+ setDir(path, {
73
+ status: "error",
74
+ entries: cur?.entries ?? [],
75
+ truncated: false,
76
+ message: explorerErrorMessage(e),
77
+ });
78
+ });
79
+ }, [setDir]);
80
+ /** List a dir on first expand; a later manual relistDir forces a fetch. */
81
+ const loadDir = useCallback((path) => {
82
+ const cur = dirsRef.current.get(path);
83
+ if (cur && cur.status !== "error")
84
+ return;
85
+ fetchDir(path);
86
+ }, [fetchDir]);
87
+ const watch = useCallback((path) => {
88
+ if (watchedRef.current.has(path))
89
+ return;
90
+ watchedRef.current.add(path);
91
+ void explorerWatchDir(path);
92
+ }, []);
93
+ const unwatch = useCallback((path) => {
94
+ if (!watchedRef.current.delete(path))
95
+ return;
96
+ void explorerUnwatchDir(path);
97
+ }, []);
98
+ const expandDir = useCallback((path) => {
99
+ setExpanded((prev) => {
100
+ if (prev.has(path))
101
+ return prev;
102
+ const next = new Set(prev);
103
+ next.add(path);
104
+ return next;
105
+ });
106
+ watch(path);
107
+ loadDir(path);
108
+ }, [watch, loadDir]);
109
+ const collapseDir = useCallback((path) => {
110
+ setExpanded((prev) => {
111
+ if (!prev.has(path))
112
+ return prev;
113
+ const next = new Set(prev);
114
+ next.delete(path);
115
+ return next;
116
+ });
117
+ unwatch(path);
118
+ }, [unwatch]);
119
+ const toggleDir = useCallback((path) => {
120
+ if (expandedRef.current.has(path))
121
+ collapseDir(path);
122
+ else
123
+ expandDir(path);
124
+ }, [expandDir, collapseDir]);
125
+ const expandPaths = useCallback((paths) => {
126
+ for (const p of paths)
127
+ expandDir(p);
128
+ }, [expandDir]);
129
+ // One global "explorer:changed" subscription: re-list only the changed dir, and only while it's
130
+ // expanded (its listing is on screen). On unmount, drop the listener and every dir watch.
131
+ useEffect(() => {
132
+ let live = true;
133
+ let un;
134
+ void onExplorerChanged((dir) => {
135
+ if (!live || !expandedRef.current.has(dir))
136
+ return;
137
+ fetchDir(dir);
138
+ }).then((unsub) => {
139
+ if (live)
140
+ un = unsub;
141
+ else
142
+ unsub();
143
+ });
144
+ return () => {
145
+ live = false;
146
+ un?.();
147
+ for (const w of watchedRef.current)
148
+ void explorerUnwatchDir(w);
149
+ watchedRef.current.clear();
150
+ };
151
+ }, [fetchDir]);
152
+ return useMemo(() => ({
153
+ expanded,
154
+ dirs,
155
+ expandDir,
156
+ collapseDir,
157
+ toggleDir,
158
+ expandPaths,
159
+ relistDir: fetchDir,
160
+ }), [expanded, dirs, expandDir, collapseDir, toggleDir, expandPaths, fetchDir]);
161
+ }
162
+ /** A re-list returns fresh objects for every entry. Keep the previous object for each entry that
163
+ * did not change, so memoised rows skip it: a watched folder that changes constantly (a temp or
164
+ * build directory) otherwise re-rendered every one of its rows on every change. */
165
+ function keepUnchangedEntries(previous, next) {
166
+ if (!previous || previous.length === 0)
167
+ return next;
168
+ const byPath = new Map(previous.map((e) => [e.path, e]));
169
+ return next.map((e) => {
170
+ const old = byPath.get(e.path);
171
+ return old &&
172
+ old.name === e.name &&
173
+ old.kind === e.kind &&
174
+ old.modified === e.modified &&
175
+ old.size === e.size
176
+ ? old
177
+ : e;
178
+ });
179
+ }
180
+ /** Rows of a folder mount in batches of this size, one batch per frame, so expanding a folder
181
+ * with thousands of entries never blocks the page in a single render. */
182
+ const ROW_MOUNT_BATCH = 200;
183
+ function locationIcon(icon) {
184
+ const props = { size: 15, weight: "regular", "aria-hidden": true };
185
+ switch (icon) {
186
+ case "home":
187
+ return _jsx(House, { ...props });
188
+ case "desktop":
189
+ return _jsx(Desktop, { ...props });
190
+ case "documents":
191
+ return _jsx(FileText, { ...props });
192
+ case "downloads":
193
+ return _jsx(DownloadSimple, { ...props });
194
+ case "icloud":
195
+ case "onedrive":
196
+ return _jsx(Cloud, { ...props });
197
+ case "volume":
198
+ case "root":
199
+ return _jsx(HardDrives, { ...props });
200
+ default:
201
+ return _jsx(Folder, { ...props });
202
+ }
203
+ }
204
+ function nodeIcon(node) {
205
+ if (node.kind === "dir") {
206
+ if (node.icon !== undefined)
207
+ return locationIcon(node.icon);
208
+ return _jsx(Folder, { size: 15, weight: "fill", "aria-hidden": true });
209
+ }
210
+ if (node.kind === "file") {
211
+ return (_jsx(FileTypeIcon, { format: (node.format ?? "unknown"), path: node.path, size: 14 }));
212
+ }
213
+ return _jsx(File, { size: 15, weight: "regular", "aria-hidden": true });
214
+ }
215
+ const TreeRow = memo(function TreeRow({ node, depth, parentPath, expanded, selected, open, tabbed, actions, }) {
216
+ const expandable = node.kind === "dir";
217
+ const unsupported = node.kind === "other";
218
+ return (_jsxs("div", { role: "treeitem", "aria-label": node.name, "aria-description": open ? "Open in a tab" : undefined, "aria-expanded": expandable ? expanded : undefined, "aria-selected": selected, "aria-disabled": unsupported || undefined, "data-vr-tree-path": node.path, "data-vr-parent": parentPath, tabIndex: tabbed ? 0 : -1, title: unsupported ? `${node.path} — ${hostAppName()} can't open this file type` : node.path, className: `vr-x-row${unsupported ? " vr-x-dim" : ""}`, style: { paddingLeft: 8 + depth * 14 }, onClick: () => {
219
+ if (expandable)
220
+ actions.toggleDir(node.path);
221
+ else if (node.kind === "file")
222
+ actions.onActivateRow(node);
223
+ }, onFocus: () => actions.setFocusPath(node.path), onContextMenu: (e) => {
224
+ e.preventDefault();
225
+ actions.onOpenMenu(node, e.clientX, e.clientY);
226
+ }, children: [_jsx("span", { className: "vr-x-caret", "aria-hidden": true, children: expandable ? (expanded ? (_jsx(CaretDown, { size: 10, weight: "regular" })) : (_jsx(CaretRight, { size: 10, weight: "regular" }))) : null }), _jsx("span", { className: "vr-x-icn", "aria-hidden": true, children: nodeIcon(node) }), _jsx("span", { className: "vr-x-name", children: node.name }), open && _jsx("span", { className: "vr-x-open", title: "Open in a tab", "aria-hidden": true }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Actions for ${node.name}`, className: "vr-x-dots vr-press vr-focus", onClick: (e) => {
227
+ e.stopPropagation();
228
+ const r = e.currentTarget.getBoundingClientRect();
229
+ actions.onOpenMenu(node, r.right, r.bottom + 2);
230
+ }, onContextMenu: (e) => {
231
+ e.preventDefault();
232
+ e.stopPropagation();
233
+ const r = e.currentTarget.getBoundingClientRect();
234
+ actions.onOpenMenu(node, r.right, r.bottom + 2);
235
+ }, children: _jsx(DotsThree, { size: 14, weight: "bold", "aria-hidden": true }) })] }));
236
+ });
237
+ // One spec object per listing entry, so a memoised row sees the same `node` on every render.
238
+ const specByEntry = new WeakMap();
239
+ function entryToSpec(e) {
240
+ const cached = specByEntry.get(e);
241
+ if (cached)
242
+ return cached;
243
+ const kind = rowKindOf(e.kind);
244
+ const spec = {
245
+ path: e.path,
246
+ name: e.name,
247
+ kind,
248
+ format: kind === "file" ? e.kind : undefined,
249
+ };
250
+ specByEntry.set(e, spec);
251
+ return spec;
252
+ }
253
+ function ChildrenRows({ dir, depth, ctx }) {
254
+ const st = ctx.tree.dirs.get(dir);
255
+ const pad = { paddingLeft: 24 + depth * 14 };
256
+ const total = st?.entries.length ?? 0;
257
+ const [mounted, setMounted] = useState(ROW_MOUNT_BATCH);
258
+ useEffect(() => {
259
+ if (mounted >= total)
260
+ return;
261
+ const frame = requestAnimationFrame(() => setMounted((count) => Math.min(total, count + ROW_MOUNT_BATCH)));
262
+ return () => cancelAnimationFrame(frame);
263
+ }, [mounted, total]);
264
+ // The row on the way to the file being revealed (or the active file) must exist at once, so the
265
+ // reveal can scroll to it; rows mounted for it stay mounted once the reveal has finished.
266
+ const wanted = ctx.revealPath ?? ctx.activePath;
267
+ const wantedIndex = useMemo(() => {
268
+ if (!st || !wanted || total <= ROW_MOUNT_BATCH)
269
+ return -1;
270
+ return st.entries.findIndex((e) => e.path === wanted || pathInside(e.path, wanted));
271
+ }, [st, wanted, total]);
272
+ const limit = Math.min(total, Math.max(mounted, wantedIndex + 1));
273
+ useEffect(() => {
274
+ if (limit > mounted)
275
+ setMounted(limit);
276
+ }, [limit, mounted]);
277
+ if (!st || (st.status === "loading" && st.entries.length === 0)) {
278
+ return (_jsx("div", { className: "vr-x-empty", style: pad, children: "Loading\u2026" }));
279
+ }
280
+ if (st.status === "error") {
281
+ return (_jsx("div", { className: "vr-x-note", role: "note", style: pad, children: st.message ?? "Couldn't list this folder." }));
282
+ }
283
+ return (_jsxs(_Fragment, { children: [st.entries.length === 0 && (_jsx("div", { className: "vr-x-empty", style: pad, children: "Empty folder" })), (limit < total ? st.entries.slice(0, limit) : st.entries).map((e) => (_jsx(ExplorerNode, { node: entryToSpec(e), depth: depth, parentPath: dir, ctx: ctx }, e.path))), st.truncated && limit >= total && (_jsx("div", { className: "vr-x-empty", style: pad, children: "\u2026 (more)" }))] }));
284
+ }
285
+ function ExplorerNode({ node, depth, parentPath, ctx, }) {
286
+ const expanded = node.kind === "dir" && ctx.tree.expanded.has(node.path);
287
+ const selected = node.path === ctx.activePath;
288
+ const open = !selected && node.kind !== "dir" && ctx.openPaths.has(node.path);
289
+ const tabbed = ctx.focusPath === node.path || (ctx.focusPath == null && ctx.firstPath === node.path);
290
+ return (_jsxs(_Fragment, { children: [_jsx(TreeRow, { node: node, depth: depth, parentPath: parentPath, expanded: expanded, selected: selected, open: open, tabbed: tabbed, actions: ctx.actions }), expanded && (_jsx("div", { role: "group", children: _jsx(ChildrenRows, { dir: node.path, depth: depth + 1, ctx: ctx }) }))] }));
291
+ }
292
+ /** One ARIA tree (roving tabindex, arrow keys) over a set of root nodes. Used for the Locations
293
+ * section (roots = explorer_locations) and for the Pinned section (roots = pinned dirs+files). */
294
+ export function ExplorerTree({ ariaLabel, roots, tree, activePath, revealPath = null, openPaths, onActivateRow, onOpenMenu, }) {
295
+ const ref = useRef(null);
296
+ const [focusPath, setFocusPath] = useState(null);
297
+ // The latest callbacks behind one object whose identity never changes (see RowActions).
298
+ const latest = useRef({ toggleDir: tree.toggleDir, onActivateRow, onOpenMenu });
299
+ latest.current = { toggleDir: tree.toggleDir, onActivateRow, onOpenMenu };
300
+ const actions = useMemo(() => ({
301
+ toggleDir: (path) => latest.current.toggleDir(path),
302
+ setFocusPath,
303
+ onActivateRow: (node) => latest.current.onActivateRow(node),
304
+ onOpenMenu: (node, x, y) => latest.current.onOpenMenu(node, x, y),
305
+ }), []);
306
+ const ctx = useMemo(() => ({
307
+ tree,
308
+ focusPath,
309
+ firstPath: roots[0]?.path,
310
+ activePath,
311
+ revealPath,
312
+ openPaths,
313
+ actions,
314
+ }), [tree, focusPath, roots, activePath, revealPath, openPaths, actions]);
315
+ const onKeyDown = (e) => {
316
+ const container = ref.current;
317
+ if (!container)
318
+ return;
319
+ const items = Array.from(container.querySelectorAll('[role="treeitem"]'));
320
+ if (items.length === 0)
321
+ return;
322
+ const activeEl = document.activeElement;
323
+ let idx = items.indexOf(activeEl);
324
+ if (idx < 0)
325
+ idx = items.findIndex((el) => el.dataset.vrTreePath === focusPath);
326
+ if (idx < 0)
327
+ idx = 0;
328
+ const cur = items[idx];
329
+ const focusItem = (el) => {
330
+ if (!el)
331
+ return;
332
+ el.focus();
333
+ el.scrollIntoView?.({ block: "nearest" });
334
+ };
335
+ switch (e.key) {
336
+ case "ArrowDown":
337
+ e.preventDefault();
338
+ focusItem(items[Math.min(items.length - 1, idx + 1)]);
339
+ break;
340
+ case "ArrowUp":
341
+ e.preventDefault();
342
+ focusItem(items[Math.max(0, idx - 1)]);
343
+ break;
344
+ case "Home":
345
+ e.preventDefault();
346
+ focusItem(items[0]);
347
+ break;
348
+ case "End":
349
+ e.preventDefault();
350
+ focusItem(items[items.length - 1]);
351
+ break;
352
+ case "ArrowRight": {
353
+ e.preventDefault();
354
+ const state = cur.getAttribute("aria-expanded");
355
+ const path = cur.dataset.vrTreePath;
356
+ if (state === "false" && path)
357
+ tree.expandDir(path);
358
+ else if (state === "true")
359
+ focusItem(items[idx + 1]);
360
+ break;
361
+ }
362
+ case "ArrowLeft": {
363
+ e.preventDefault();
364
+ const state = cur.getAttribute("aria-expanded");
365
+ const path = cur.dataset.vrTreePath;
366
+ if (state === "true" && path) {
367
+ tree.collapseDir(path);
368
+ break;
369
+ }
370
+ const parent = cur.dataset.vrParent;
371
+ if (parent) {
372
+ const parentEl = items.find((el) => el.dataset.vrTreePath === parent);
373
+ if (parentEl)
374
+ focusItem(parentEl);
375
+ }
376
+ break;
377
+ }
378
+ case "Enter":
379
+ case " ": {
380
+ e.preventDefault();
381
+ cur.click();
382
+ break;
383
+ }
384
+ }
385
+ };
386
+ return (_jsx("div", { role: "tree", "aria-label": ariaLabel, ref: ref, onKeyDown: onKeyDown, className: "vr-x-tree", children: roots.map((r, i) => (_jsx(ExplorerNode, { node: r, depth: 0, parentPath: undefined, ctx: ctx }, `${i}:${r.path}`))) }));
387
+ }
@@ -0,0 +1,37 @@
1
+ export declare const EXPLORER_SETTINGS_KEY = "vr.explorer.v1";
2
+ export interface ExplorerSectionState {
3
+ pinned: boolean;
4
+ recent: boolean;
5
+ locations: boolean;
6
+ }
7
+ export interface ExplorerPersisted {
8
+ /** Dir paths currently expanded in the Locations tree / Pinned folders. */
9
+ expanded: string[];
10
+ sections: ExplorerSectionState;
11
+ /** Sidebar pixel width (also mirrored to localStorage "vr.sidebar.width"). */
12
+ width: number;
13
+ /** Explorer scroll offset (scrollTop of the nav container). */
14
+ scroll: number;
15
+ }
16
+ export declare function defaultExplorerPersisted(): ExplorerPersisted;
17
+ /** Load the persisted explorer record once (SQLite mirror via settings_get); merges over defaults
18
+ * and caches the result so the sidebar shell and the sections share one IPC read. */
19
+ export declare function loadExplorerPersisted(): Promise<ExplorerPersisted>;
20
+ /** Synchronous read of the last-loaded record (defaults before hydration resolves). */
21
+ export declare function explorerPersistedSync(): ExplorerPersisted;
22
+ /** Merge a patch into the record and schedule a debounced (~300ms) mirror write. */
23
+ export declare function saveExplorerPersisted(patch: Partial<ExplorerPersisted>): void;
24
+ /** Case-insensitive "is `child` strictly inside `dir`" on path segments. */
25
+ export declare function pathInside(dir: string, child: string): boolean;
26
+ /** The chain of ancestor directories of `filePath` from `rootDir` (inclusive) down to the file's
27
+ * parent dir — used by "reveal the active file" to expand each level. Returns [] when the file is
28
+ * not under rootDir. Ancestors are rebuilt from the file path's own segments so they keep the
29
+ * backend's canonical casing (they must string-match the `path` keys of tree dir entries). */
30
+ export declare function ancestorDirs(filePath: string, rootDir: string): string[];
31
+ /** The name of the child of `dir` on the way to `target` (the target itself when it is a direct
32
+ * child), or undefined when `dir` is not a strict ancestor of `target`. Separator-agnostic:
33
+ * `C:\Temp` with `C:\Temp\abc\x.md` gives "abc". */
34
+ export declare function childNameOnPath(dir: string, target: string | null): string | undefined;
35
+ /** ExplorerEntry.kind → row kind. "dir" stays "dir", a supported format id becomes "file"
36
+ * (openable, broker-granted path), anything else is "other" (real path, no grant — never open). */
37
+ export declare function rowKindOf(kind: string): "dir" | "file" | "other";
@@ -0,0 +1,135 @@
1
+ // Shared state for the explorer sidebar (Part C): the "vr.explorer.v1" settings-mirror record
2
+ // (expanded dirs, section open/closed, sidebar width, scroll position) with a module-level cache +
3
+ // debounced write, plus the small path helpers the lazy tree and the active-file reveal need.
4
+ import { getSettingsMirror, setSettingsMirror } from "@rightkit/document-core/state/tauriBridge";
5
+ import { splitPath } from "../SidebarPathUtils.js";
6
+ export const EXPLORER_SETTINGS_KEY = "vr.explorer.v1";
7
+ export function defaultExplorerPersisted() {
8
+ return {
9
+ expanded: [],
10
+ sections: { pinned: true, recent: true, locations: true },
11
+ width: 0,
12
+ scroll: 0,
13
+ };
14
+ }
15
+ let cache = null;
16
+ /** Set once any save has landed — a late-resolving load must not stomp a fresher write. */
17
+ let mutated = false;
18
+ let writeTimer = null;
19
+ function sanitize(raw) {
20
+ const fallback = defaultExplorerPersisted();
21
+ if (raw === null || typeof raw !== "object")
22
+ return fallback;
23
+ const o = raw;
24
+ const s = o.sections;
25
+ return {
26
+ expanded: Array.isArray(o.expanded)
27
+ ? o.expanded.filter((p) => typeof p === "string")
28
+ : [],
29
+ sections: {
30
+ pinned: typeof s?.pinned === "boolean" ? s.pinned : true,
31
+ recent: typeof s?.recent === "boolean" ? s.recent : true,
32
+ locations: typeof s?.locations === "boolean" ? s.locations : true,
33
+ },
34
+ width: typeof o.width === "number" && Number.isFinite(o.width) ? o.width : 0,
35
+ scroll: typeof o.scroll === "number" && Number.isFinite(o.scroll) ? o.scroll : 0,
36
+ };
37
+ }
38
+ /** Load the persisted explorer record once (SQLite mirror via settings_get); merges over defaults
39
+ * and caches the result so the sidebar shell and the sections share one IPC read. */
40
+ export async function loadExplorerPersisted() {
41
+ if (cache)
42
+ return cache;
43
+ const raw = await getSettingsMirror(EXPLORER_SETTINGS_KEY);
44
+ if (mutated && cache)
45
+ return cache;
46
+ let parsed = defaultExplorerPersisted();
47
+ if (raw != null) {
48
+ try {
49
+ parsed = sanitize(JSON.parse(raw));
50
+ }
51
+ catch {
52
+ /* corrupt mirror value — keep defaults */
53
+ }
54
+ }
55
+ if (!mutated)
56
+ cache = parsed;
57
+ return cache ?? parsed;
58
+ }
59
+ /** Synchronous read of the last-loaded record (defaults before hydration resolves). */
60
+ export function explorerPersistedSync() {
61
+ return cache ?? defaultExplorerPersisted();
62
+ }
63
+ /** Merge a patch into the record and schedule a debounced (~300ms) mirror write. */
64
+ export function saveExplorerPersisted(patch) {
65
+ mutated = true;
66
+ cache = { ...(cache ?? defaultExplorerPersisted()), ...patch };
67
+ if (writeTimer != null)
68
+ window.clearTimeout(writeTimer);
69
+ writeTimer = window.setTimeout(() => {
70
+ writeTimer = null;
71
+ if (cache)
72
+ void setSettingsMirror(EXPLORER_SETTINGS_KEY, JSON.stringify(cache));
73
+ }, 300);
74
+ }
75
+ // ── Path helpers (separator-agnostic; the tree sees both / and \ paths) ──────
76
+ /** Case-insensitive "is `child` strictly inside `dir`" on path segments. */
77
+ export function pathInside(dir, child) {
78
+ const d = splitPath(dir).join("/").toLowerCase();
79
+ const c = splitPath(child).join("/").toLowerCase();
80
+ return c.length > d.length && c.startsWith(d + "/");
81
+ }
82
+ /** The chain of ancestor directories of `filePath` from `rootDir` (inclusive) down to the file's
83
+ * parent dir — used by "reveal the active file" to expand each level. Returns [] when the file is
84
+ * not under rootDir. Ancestors are rebuilt from the file path's own segments so they keep the
85
+ * backend's canonical casing (they must string-match the `path` keys of tree dir entries). */
86
+ export function ancestorDirs(filePath, rootDir) {
87
+ if (!pathInside(rootDir, filePath))
88
+ return [];
89
+ const rootSegs = splitPath(rootDir);
90
+ const fileSegs = splitPath(filePath);
91
+ const sep = filePath.includes("\\") ? "\\" : "/";
92
+ const out = [];
93
+ for (let i = rootSegs.length; i < fileSegs.length; i++) {
94
+ if (i === rootSegs.length) {
95
+ out.push(rootDir);
96
+ continue;
97
+ }
98
+ const head = fileSegs[0] ?? "";
99
+ if (/^[A-Za-z]:$/.test(head)) {
100
+ out.push(head + sep + fileSegs.slice(1, i).join(sep));
101
+ }
102
+ else {
103
+ out.push((filePath.startsWith("/") ? "/" : "") + fileSegs.slice(0, i).join(sep));
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ /** The name of the child of `dir` on the way to `target` (the target itself when it is a direct
109
+ * child), or undefined when `dir` is not a strict ancestor of `target`. Separator-agnostic:
110
+ * `C:\Temp` with `C:\Temp\abc\x.md` gives "abc". */
111
+ export function childNameOnPath(dir, target) {
112
+ if (!target || !pathInside(dir, target))
113
+ return undefined;
114
+ return splitPath(target)[splitPath(dir).length];
115
+ }
116
+ const KNOWN_FORMATS = new Set([
117
+ "md",
118
+ "mdx",
119
+ "txt",
120
+ "pdf",
121
+ "html",
122
+ "docx",
123
+ "image",
124
+ "web",
125
+ "unknown",
126
+ ]);
127
+ /** ExplorerEntry.kind → row kind. "dir" stays "dir", a supported format id becomes "file"
128
+ * (openable, broker-granted path), anything else is "other" (real path, no grant — never open). */
129
+ export function rowKindOf(kind) {
130
+ if (kind === "dir")
131
+ return "dir";
132
+ if (kind === "other" || !KNOWN_FORMATS.has(kind))
133
+ return "other";
134
+ return "file";
135
+ }
@@ -0,0 +1,45 @@
1
+ import { type DocumentFormat } from "@rightkit/document-core/app/resourceCensus";
2
+ /** Maximum number of ordinary document routers kept paint-warm (DocumentRouterTable's own limit). */
3
+ export declare const ORDINARY_LIVE_PANE_LIMIT = 2;
4
+ export declare const LIVE_TAB_LIMIT = 8;
5
+ export type TabMountPolicyInput = Readonly<{
6
+ activePath: string | null;
7
+ previousActivePath: string | null;
8
+ protectedPaths: ReadonlySet<string>;
9
+ /** Recently-active paths, most-recent-first. Only the first LIVE_TAB_LIMIT entries stay live. */
10
+ recentPaths?: readonly string[];
11
+ /**
12
+ * Document format per path, for the intrinsic-cost floor in `candidateWeight`. Optional and
13
+ * missing entries fall back to the census-only signal (matches prior behavior) so existing
14
+ * callers/tests that don't care about the eviction-cost-decay case keep working unchanged.
15
+ */
16
+ formatByPath?: ReadonlyMap<string, DocumentFormat>;
17
+ }>;
18
+ export type TabMountCounters = Readonly<{
19
+ shellCount: number;
20
+ liveCount: number;
21
+ protectedLiveCount: number;
22
+ ordinaryLiveLimit: number;
23
+ }>;
24
+ /**
25
+ * Dirty text sessions may leave the live set only after their authoritative source (or a
26
+ * structured draft snapshot) is owned above the router. Binary editors deliberately never get
27
+ * this escape hatch: their mounted runtime remains their only authoritative dirty state.
28
+ */
29
+ export declare function dirtyPaneMayShell(format: string, hasExternalSourceOrDraftOwner: boolean): boolean;
30
+ /**
31
+ * Active + previous-active + a resource-weighted MRU window of recently-active panes stay live.
32
+ * `protectedPaths` protection is deliberately additive and unbounded: a draft or pending save
33
+ * cannot be evicted merely because another tab became active or aged out of the MRU window.
34
+ *
35
+ * Beyond that, every candidate is checked against the resource census before it can age out:
36
+ * a document reporting pinned state (dirty with no external owner — `documentHasPinnedState`)
37
+ * is kept live no matter its MRU rank or weight, full stop — there is no limit or heuristic that
38
+ * overrides this. A document whose state is dirty-but-externalized, or simply clean, competes for
39
+ * the MRU budget like any other, weighted by `candidateWeight` so heavyweight panes shell out
40
+ * before lightweight ones at the same recency — and stay counted as heavyweight even once shelled,
41
+ * via `formatByPath`'s intrinsic-cost floor (see `candidateWeight`'s own doc comment).
42
+ */
43
+ export declare function liveDocumentPaths({ activePath, previousActivePath, protectedPaths, recentPaths, formatByPath, }: TabMountPolicyInput): Set<string>;
44
+ /** Deterministic observability for stress tests & diagnostics; never reads wall-clock time. */
45
+ export declare function tabMountCounters(documentPaths: readonly string[], livePaths: ReadonlySet<string>, protectedPaths: ReadonlySet<string>): TabMountCounters;