authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-29 01:24:29+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 01:24:29+02:00
log936593c2583c6640a07fc310f97bd5d0c27a7c31
tree74432dc149083415e362965b655d7f413ebab396
parent2919147c80e4daae326a25f870a1c7b19cd16f42
signature Signed by PGP key B5690EEEBB952194

refactor(client): share filesystem browse navigation (#4797)

Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 196 insertions(+), 204 deletions(-)

apps/mobile/src/features/projects/AddProjectScreen.tsx+23-45
......@@ -10,16 +10,14 @@ import {
1010 sortAddProjectProviderSources,
1111 type AddProjectRemoteSource,
1212} from "@t3tools/client-runtime/operations/projects";
13import {
14 filterFilesystemBrowseEntries,
15 getFilesystemBrowsePath,
16} from "@t3tools/client-runtime/state/filesystem";
1317import {
1418 appendBrowsePathSegment,
15 canNavigateUp,
1619 ensureBrowseDirectoryPath,
17 getBrowseDirectoryPath,
18 getBrowseLeafPathSegment,
19 getBrowseParentPath,
20 hasTrailingPathSeparator,
2120 inferProjectTitleFromPath,
22 isFilesystemBrowseQuery,
2321} from "@t3tools/client-runtime/state/projects";
2422import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
2523import { StackActions, useNavigation } from "@react-navigation/native";
......@@ -61,11 +59,6 @@ const environmentOptionOrder = Order.mapInput(
6159 (environment: EnvironmentOption) => ({ label: environment.label }),
6260);
6361
64const browseEntryOrder = Order.mapInput(
65 Order.String,
66 (entry: { readonly name: string }) => entry.name,
67);
68
6962function platformFromOs(os: string | null | undefined): string {
7063 if (os === "windows") return "Win32";
7164 if (os === "darwin") return "MacIntel";
......@@ -594,20 +587,13 @@ function FolderBrowser(props: {
594587 readonly setPathInput: (path: string) => void;
595588}) {
596589 const accentColor = useThemeColor("--color-icon-muted");
597 const browseDirectoryPath = useMemo(
598 () =>
599 isFilesystemBrowseQuery(props.pathInput, props.environment.platform)
600 ? getBrowseDirectoryPath(props.pathInput)
601 : "",
590 const browsePath = useMemo(
591 () => getFilesystemBrowsePath(props.pathInput, props.environment.platform),
602592 [props.environment.platform, props.pathInput],
603593 );
604 const browseFilterQuery =
605 browseDirectoryPath.length > 0 && !hasTrailingPathSeparator(props.pathInput)
606 ? getBrowseLeafPathSegment(props.pathInput).toLowerCase()
607 : "";
608594 const browseInput = useMemo(
609 () => (browseDirectoryPath.length > 0 ? { partialPath: browseDirectoryPath } : null),
610 [browseDirectoryPath],
595 () => (browsePath.directoryPath.length > 0 ? { partialPath: browsePath.directoryPath } : null),
596 [browsePath.directoryPath],
611597 );
612598 const browseState = useEnvironmentQuery(
613599 browseInput === null
......@@ -617,20 +603,10 @@ function FolderBrowser(props: {
617603 input: browseInput,
618604 }),
619605 );
620 const visibleBrowseEntries = useMemo(
621 () =>
622 Arr.sort(
623 Arr.filter(
624 browseState.data?.entries ?? [],
625 (entry) =>
626 !entry.name.startsWith(".") && entry.name.toLowerCase().startsWith(browseFilterQuery),
627 ),
628 browseEntryOrder,
629 ),
630 [browseFilterQuery, browseState.data?.entries],
606 const { visibleEntries: visibleBrowseEntries } = useMemo(
607 () => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browsePath.filterQuery),
608 [browsePath.filterQuery, browseState.data?.entries],
631609 );
632 const parentBrowsePath = getBrowseParentPath(browseDirectoryPath);
633 const canBrowseUpPath = canNavigateUp(browseDirectoryPath);
634610
635611 return (
636612 <>
......@@ -642,7 +618,7 @@ function FolderBrowser(props: {
642618 <ActivityIndicator color={accentColor} />
643619 </View>
644620 ) : null}
645 {canBrowseUpPath ? (
621 {browsePath.canBrowseUp ? (
646622 <ListRow
647623 title=".."
648624 icon={
......@@ -656,7 +632,9 @@ function FolderBrowser(props: {
656632 isFirst
657633 right={null}
658634 onPress={() => {
659 if (parentBrowsePath) props.setPathInput(parentBrowsePath);
635 if (browsePath.parentPath) {
636 props.setPathInput(browsePath.parentPath);
637 }
660638 }}
661639 />
662640 ) : null}
......@@ -665,15 +643,15 @@ function FolderBrowser(props: {
665643 key={entry.fullPath}
666644 title={entry.name}
667645 icon={<SymbolView name="folder" size={17} tintColor={accentColor} type="monochrome" />}
668 isFirst={index === 0 && !canBrowseUpPath}
646 isFirst={index === 0 && !browsePath.canBrowseUp}
669647 right={null}
670 onPress={() =>
671 props.setPathInput(
672 browseDirectoryPath.length > 0
673 ? appendBrowsePathSegment(browseDirectoryPath, entry.name)
674 : ensureBrowseDirectoryPath(entry.fullPath),
675 )
676 }
648 onPress={() => {
649 const nextPath =
650 browsePath.directoryPath.length > 0
651 ? appendBrowsePathSegment(browsePath.directoryPath, entry.name)
652 : ensureBrowseDirectoryPath(entry.fullPath);
653 props.setPathInput(nextPath);
654 }}
677655 />
678656 ))}
679657 </ListSection>
apps/web/src/components/CommandPalette.logic.test.ts-64
......@@ -4,8 +4,6 @@ import type { Thread } from "../types";
44import {
55 buildBrowseGroups,
66 buildThreadActionItems,
7 canPreloadBrowsePath,
8 createBrowseNavigationCoordinator,
97 enumerateCommandPaletteItems,
108 filterCommandPaletteGroups,
119 type CommandPaletteGroup,
......@@ -234,65 +232,3 @@ describe("buildBrowseGroups", () => {
234232 expect(actionSettled).toBe(true);
235233 });
236234});
237
238describe("createBrowseNavigationCoordinator", () => {
239 it("only commits the latest overlapping navigation", async () => {
240 const coordinator = createBrowseNavigationCoordinator();
241 let finishFirst: (() => void) | undefined;
242 let finishSecond: (() => void) | undefined;
243 const commits: string[] = [];
244
245 const first = coordinator.run({
246 load: () =>
247 new Promise<void>((resolve) => {
248 finishFirst = resolve;
249 }),
250 commit: () => {
251 commits.push("first");
252 },
253 });
254 const second = coordinator.run({
255 load: () =>
256 new Promise<void>((resolve) => {
257 finishSecond = resolve;
258 }),
259 commit: () => {
260 commits.push("second");
261 },
262 });
263
264 finishSecond?.();
265 await expect(second).resolves.toBe(true);
266 finishFirst?.();
267 await expect(first).resolves.toBe(false);
268 expect(commits).toEqual(["second"]);
269 });
270
271 it("does not commit after newer user input invalidates the navigation", async () => {
272 const coordinator = createBrowseNavigationCoordinator();
273 let finishNavigation: (() => void) | undefined;
274 const commit = vi.fn();
275 const navigation = coordinator.run({
276 load: () =>
277 new Promise<void>((resolve) => {
278 finishNavigation = resolve;
279 }),
280 commit,
281 });
282
283 coordinator.invalidate();
284 finishNavigation?.();
285
286 await expect(navigation).resolves.toBe(false);
287 expect(commit).not.toHaveBeenCalled();
288 });
289});
290
291describe("canPreloadBrowsePath", () => {
292 it("only preloads paths for connected environments", () => {
293 expect(canPreloadBrowsePath("connected")).toBe(true);
294 expect(canPreloadBrowsePath("offline")).toBe(false);
295 expect(canPreloadBrowsePath("reconnecting")).toBe(false);
296 expect(canPreloadBrowsePath(null)).toBe(false);
297 });
298});
apps/web/src/components/CommandPalette.logic.ts+1-59
......@@ -1,9 +1,8 @@
11import {
2 type KeybindingCommand,
32 type FilesystemBrowseEntry,
3 type KeybindingCommand,
44 THREAD_JUMP_KEYBINDING_COMMANDS,
55} from "@t3tools/contracts";
6import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
76import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
87import * as Arr from "effect/Array";
98import * as Result from "effect/Result";
......@@ -16,39 +15,6 @@ export const RECENT_THREAD_LIMIT = 12;
1615export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80";
1716export const ADDON_ICON_CLASS = "size-4";
1817
19export interface BrowseNavigationCoordinator {
20 readonly invalidate: () => void;
21 readonly run: (input: {
22 readonly load: () => Promise<void>;
23 readonly commit: () => void;
24 }) => Promise<boolean>;
25}
26
27export function createBrowseNavigationCoordinator(): BrowseNavigationCoordinator {
28 let generation = 0;
29
30 return {
31 invalidate: () => {
32 generation += 1;
33 },
34 run: async (input) => {
35 const navigationGeneration = ++generation;
36 await input.load();
37 if (navigationGeneration !== generation) {
38 return false;
39 }
40 input.commit();
41 return true;
42 },
43 };
44}
45
46export function canPreloadBrowsePath(
47 connectionPhase: EnvironmentConnectionPhase | null | undefined,
48): boolean {
49 return connectionPhase === "connected";
50}
51
5218export interface CommandPaletteItem {
5319 readonly kind: "action" | "submenu";
5420 readonly value: string;
......@@ -104,30 +70,6 @@ export function enumerateCommandPaletteItems(
10470
10571export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-browse";
10672
107export function filterBrowseEntries(input: {
108 browseEntries: ReadonlyArray<FilesystemBrowseEntry>;
109 browseFilterQuery: string;
110}): {
111 filteredEntries: FilesystemBrowseEntry[];
112 exactEntry: FilesystemBrowseEntry | null;
113} {
114 const lowerFilter = input.browseFilterQuery.toLowerCase();
115 const showHidden = input.browseFilterQuery.startsWith(".");
116
117 const filteredEntries = input.browseEntries.filter(
118 (entry) =>
119 entry.name.toLowerCase().startsWith(lowerFilter) &&
120 (showHidden || !entry.name.startsWith(".")),
121 );
122
123 const exactEntry =
124 input.browseFilterQuery.length > 0
125 ? (filteredEntries.find((entry) => entry.name === input.browseFilterQuery) ?? null)
126 : null;
127
128 return { filteredEntries, exactEntry };
129}
130
13173export function normalizeSearchText(value: string): string {
13274 return value.trim().toLowerCase().replace(/\s+/g, " ");
13375}
apps/web/src/components/CommandPalette.tsx+33-34
......@@ -1,6 +1,12 @@
11"use client";
22
33import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
4import {
5 canPreloadBrowsePath,
6 createBrowseNavigationCoordinator,
7 filterFilesystemBrowseEntries,
8 getFilesystemBrowsePath,
9} from "@t3tools/client-runtime/state/filesystem";
410import {
511 isAtomCommandInterrupted,
612 settlePromise,
......@@ -61,16 +67,12 @@ import { useProjects, useThreadShells } from "../state/entities";
6167import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions";
6268import {
6369 appendBrowsePathSegment,
64 canNavigateUp,
6570 ensureBrowseDirectoryPath,
6671 findProjectByPath,
6772 getBrowseDirectoryPath,
68 getBrowseLeafPathSegment,
69 getBrowseParentPath,
7073 hasTrailingPathSeparator,
7174 inferProjectTitleFromPath,
7275 isExplicitRelativeProjectPath,
73 isFilesystemBrowseQuery,
7476 isUnsupportedWindowsProjectPath,
7577 resolveProjectPathForDispatch,
7678} from "../lib/projectPaths";
......@@ -92,13 +94,10 @@ import {
9294 buildProjectActionItems,
9395 buildRootGroups,
9496 buildThreadActionItems,
95 canPreloadBrowsePath,
96 createBrowseNavigationCoordinator,
9797 enumerateCommandPaletteItems,
9898 type CommandPaletteActionItem,
9999 type CommandPaletteSubmenuItem,
100100 type CommandPaletteView,
101 filterBrowseEntries,
102101 filterCommandPaletteGroups,
103102 getCommandPaletteInputPlaceholder,
104103 getCommandPaletteMode,
......@@ -684,8 +683,12 @@ function OpenCommandPaletteDialog(props: {
684683 );
685684 const isRemoteProjectCloneFlow = addProjectCloneFlow !== null;
686685 const isRemoteProjectRepositoryStep = addProjectCloneFlow?.step === "repository";
687 const isBrowsing =
688 !isRemoteProjectRepositoryStep && isFilesystemBrowseQuery(query, browseEnvironmentPlatform);
686 const browsePath = useMemo(
687 () => getFilesystemBrowsePath(query, browseEnvironmentPlatform, !isRemoteProjectRepositoryStep),
688 [browseEnvironmentPlatform, isRemoteProjectRepositoryStep, query],
689 );
690 const isBrowsing = browsePath.isBrowsing;
691 const browseDirectoryPath = browsePath.directoryPath;
689692 const paletteMode = getCommandPaletteMode({ currentView, isBrowsing });
690693 const getAddProjectInitialQueryForEnvironment = useCallback(
691694 (environmentId: EnvironmentId | null): string => {
......@@ -730,18 +733,15 @@ function OpenCommandPaletteDialog(props: {
730733 );
731734 const relativePathNeedsActiveProject =
732735 isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null;
733 const browseDirectoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";
734 const browseFilterQuery =
735 isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : "";
736736 const browseQuery = useEnvironmentQuery(
737737 isBrowsing &&
738 browseDirectoryPath.length > 0 &&
738 browsePath.directoryPath.length > 0 &&
739739 browseEnvironmentId !== null &&
740740 !relativePathNeedsActiveProject
741741 ? filesystemEnvironment.browse({
742742 environmentId: browseEnvironmentId,
743743 input: {
744 partialPath: browseDirectoryPath,
744 partialPath: browsePath.directoryPath,
745745 ...(currentProjectCwdForBrowse ? { cwd: currentProjectCwdForBrowse } : {}),
746746 },
747747 })
......@@ -750,9 +750,9 @@ function OpenCommandPaletteDialog(props: {
750750 const browseResult = browseQuery.data;
751751 const isBrowsePending = browseQuery.isPending;
752752 const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES;
753 const { filteredEntries: filteredBrowseEntries, exactEntry: exactBrowseEntry } = useMemo(
754 () => filterBrowseEntries({ browseEntries, browseFilterQuery }),
755 [browseEntries, browseFilterQuery],
753 const { visibleEntries: visibleBrowseEntries, exactEntry: exactBrowseEntry } = useMemo(
754 () => filterFilesystemBrowseEntries(browseEntries, browsePath.filterQuery),
755 [browseEntries, browsePath.filterQuery],
756756 );
757757
758758 const prefetchBrowsePath = useCallback(
......@@ -973,17 +973,17 @@ function OpenCommandPaletteDialog(props: {
973973 initialQuery,
974974 };
975975
976 await browseNavigation.run({
977 load: () =>
976 await browseNavigation.run(
977 () =>
978978 initialBrowsePath.length > 0
979979 ? prefetchBrowsePath(initialBrowsePath, environmentId, browseCwd)
980980 : Promise.resolve(),
981 commit: () => {
981 () => {
982982 setAddProjectEnvironmentId(environmentId);
983983 setAddProjectCloneFlow(null);
984984 pushPaletteView(view);
985985 },
986 });
986 );
987987 },
988988 [
989989 browseNavigation,
......@@ -1630,33 +1630,33 @@ function OpenCommandPaletteDialog(props: {
16301630 const browseTo = useCallback(
16311631 async (name: string): Promise<void> => {
16321632 const nextQuery = appendBrowsePathSegment(query, name);
1633 await browseNavigation.run({
1634 load: () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)),
1635 commit: () => {
1633 await browseNavigation.run(
1634 () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)),
1635 () => {
16361636 setHighlightedItemValue(null);
16371637 setQuery(nextQuery);
16381638 setBrowseGeneration((generation) => generation + 1);
16391639 },
1640 });
1640 );
16411641 },
16421642 [browseNavigation, prefetchBrowsePath, query],
16431643 );
16441644
16451645 const browseUp = useCallback(async (): Promise<void> => {
1646 const parentPath = getBrowseParentPath(query);
1646 const parentPath = browsePath.parentPath;
16471647 if (parentPath === null) {
16481648 return;
16491649 }
16501650
1651 await browseNavigation.run({
1652 load: () => prefetchBrowsePath(parentPath),
1653 commit: () => {
1651 await browseNavigation.run(
1652 () => prefetchBrowsePath(parentPath),
1653 () => {
16541654 setHighlightedItemValue(null);
16551655 setQuery(parentPath);
16561656 setBrowseGeneration((generation) => generation + 1);
16571657 },
1658 });
1659 }, [browseNavigation, prefetchBrowsePath, query]);
1658 );
1659 }, [browseNavigation, browsePath.parentPath, prefetchBrowsePath]);
16601660
16611661 // Resolve the add-project path from browse data when available. When the
16621662 // query has a trailing separator (e.g. "~/projects/foo/"), parentPath is the
......@@ -1666,11 +1666,10 @@ function OpenCommandPaletteDialog(props: {
16661666 ? (browseResult?.parentPath ?? query.trim())
16671667 : (exactBrowseEntry?.fullPath ?? query.trim());
16681668
1669 const canBrowseUp =
1670 isBrowsing && !relativePathNeedsActiveProject && canNavigateUp(browseDirectoryPath);
1669 const canBrowseUp = !relativePathNeedsActiveProject && browsePath.canBrowseUp;
16711670
16721671 const browseGroups = buildBrowseGroups({
1673 browseEntries: filteredBrowseEntries,
1672 browseEntries: visibleBrowseEntries,
16741673 browseQuery: query,
16751674 canBrowseUp,
16761675 upIcon: <CornerLeftUpIcon className={ITEM_ICON_CLASS} />,
packages/client-runtime/src/state/filesystem.test.ts created+70
......@@ -0,0 +1,70 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 canPreloadBrowsePath,
5 createBrowseNavigationCoordinator,
6 filterFilesystemBrowseEntries,
7 getFilesystemBrowsePath,
8} from "./filesystem.ts";
9
10describe("filesystem browse model", () => {
11 it("derives the browse target and navigation state", () => {
12 expect(getFilesystemBrowsePath("~/projects/t3")).toEqual({
13 isBrowsing: true,
14 directoryPath: "~/projects/",
15 filterQuery: "t3",
16 parentPath: "~/",
17 canBrowseUp: true,
18 });
19 expect(getFilesystemBrowsePath("C:\\Users\\test", "MacIntel").isBrowsing).toBe(false);
20 expect(getFilesystemBrowsePath("~/projects/", "", false).isBrowsing).toBe(false);
21 });
22
23 it("filters names, hidden directories, and exact matches consistently", () => {
24 const entries = [
25 { name: ".config", fullPath: "/Users/test/.config" },
26 { name: "Code", fullPath: "/Users/test/Code" },
27 { name: "codething", fullPath: "/Users/test/codething" },
28 ];
29
30 expect(filterFilesystemBrowseEntries(entries, "co")).toEqual({
31 visibleEntries: entries.slice(1, 3),
32 exactEntry: null,
33 });
34 expect(filterFilesystemBrowseEntries(entries, "").visibleEntries).toEqual(entries.slice(1));
35 expect(filterFilesystemBrowseEntries(entries, ".").visibleEntries).toEqual(entries.slice(0, 1));
36 expect(filterFilesystemBrowseEntries(entries, "Code").exactEntry).toEqual(entries[1]);
37 });
38});
39
40describe("browse navigation", () => {
41 it("only commits the latest valid navigation", async () => {
42 const navigation = createBrowseNavigationCoordinator();
43 const first = Promise.withResolvers<void>();
44 const second = Promise.withResolvers<void>();
45 const commits: string[] = [];
46 const commit = (name: string) => () => commits.push(name);
47 const firstRun = navigation.run(() => first.promise, commit("first"));
48 const secondRun = navigation.run(() => second.promise, commit("second"));
49
50 second.resolve();
51 await expect(secondRun).resolves.toBe(true);
52 first.resolve();
53 await expect(firstRun).resolves.toBe(false);
54
55 const invalidated = Promise.withResolvers<void>();
56 const invalidatedRun = navigation.run(() => invalidated.promise, commit("stale"));
57 navigation.invalidate();
58 invalidated.resolve();
59
60 await expect(invalidatedRun).resolves.toBe(false);
61 expect(commits).toEqual(["second"]);
62 });
63
64 it("only preloads connected environments", () => {
65 expect(canPreloadBrowsePath("connected")).toBe(true);
66 expect(canPreloadBrowsePath("offline")).toBe(false);
67 expect(canPreloadBrowsePath("reconnecting")).toBe(false);
68 expect(canPreloadBrowsePath(null)).toBe(false);
69 });
70});
packages/client-runtime/src/state/filesystem.ts+69-2
......@@ -1,8 +1,75 @@
1import { WS_METHODS } from "@t3tools/contracts";
1import { type FilesystemBrowseEntry, WS_METHODS } from "@t3tools/contracts";
22import { Atom } from "effect/unstable/reactivity";
33
4import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts";
4import type { EnvironmentConnectionPhase } from "../connection/presentation.ts";
55import type { EnvironmentRegistry } from "../connection/registry.ts";
6import {
7 canNavigateUp,
8 getBrowseDirectoryPath,
9 getBrowseLeafPathSegment,
10 getBrowseParentPath,
11 hasTrailingPathSeparator,
12 isFilesystemBrowseQuery,
13} from "./projects.ts";
14import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts";
15
16export function getFilesystemBrowsePath(query: string, platform = "", enabled = true) {
17 const isBrowsing = enabled && isFilesystemBrowseQuery(query, platform);
18 const directoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";
19 const filterQuery =
20 isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : "";
21 const parentPath = isBrowsing ? getBrowseParentPath(directoryPath) : null;
22
23 return {
24 isBrowsing,
25 directoryPath,
26 filterQuery,
27 parentPath,
28 canBrowseUp: isBrowsing && canNavigateUp(directoryPath),
29 };
30}
31
32export function filterFilesystemBrowseEntries(
33 entries: ReadonlyArray<FilesystemBrowseEntry>,
34 query: string,
35) {
36 const lowerQuery = query.toLowerCase();
37 const showHidden = query.startsWith(".");
38 const visibleEntries = entries.filter(
39 (entry) =>
40 entry.name.toLowerCase().startsWith(lowerQuery) &&
41 (showHidden || !entry.name.startsWith(".")),
42 );
43 const exactEntry =
44 query.length > 0 ? (visibleEntries.find((entry) => entry.name === query) ?? null) : null;
45
46 return { visibleEntries, exactEntry };
47}
48
49export function createBrowseNavigationCoordinator() {
50 let generation = 0;
51
52 return {
53 invalidate: () => {
54 generation += 1;
55 },
56 run: async (load: () => Promise<void>, commit: () => void) => {
57 const navigationGeneration = ++generation;
58 await load();
59 if (navigationGeneration !== generation) {
60 return false;
61 }
62 commit();
63 return true;
64 },
65 };
66}
67
68export function canPreloadBrowsePath(
69 connectionPhase: EnvironmentConnectionPhase | null | undefined,
70): boolean {
71 return connectionPhase === "connected";
72}
673
774export function createFilesystemEnvironmentAtoms<R, E>(
875 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,