authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-29 00:58:38+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 00:58:38+02:00
log1ba3d01bc61d142eacda7c45aa3af748e4afa1bb
tree3e9188f25e8c5d83df25531aab8a5a769c360148
parent5fcdefd057751eb511de9e61142ef797f142059b
signature Signed by PGP key B5690EEEBB952194

fix(web): defer command palette filesystem navigation (#2109)

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

3 files changed, 276 insertions(+), 48 deletions(-)

apps/web/src/components/CommandPalette.logic.test.ts+103
...@@ -2,7 +2,10 @@ import { describe, expect, it, vi } from "vite-plus/test";...@@ -2,7 +2,10 @@ import { describe, expect, it, vi } from "vite-plus/test";
2import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";2import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
3import type { Thread } from "../types";3import type { Thread } from "../types";
4import {4import {
5 buildBrowseGroups,
5 buildThreadActionItems,6 buildThreadActionItems,
7 canPreloadBrowsePath,
8 createBrowseNavigationCoordinator,
6 enumerateCommandPaletteItems,9 enumerateCommandPaletteItems,
7 filterCommandPaletteGroups,10 filterCommandPaletteGroups,
8 type CommandPaletteGroup,11 type CommandPaletteGroup,
...@@ -193,3 +196,103 @@ describe("buildThreadActionItems", () => {...@@ -193,3 +196,103 @@ describe("buildThreadActionItems", () => {
193 expect(items.map((item) => item.value)).toEqual(["thread:thread-active"]);196 expect(items.map((item) => item.value)).toEqual(["thread:thread-active"]);
194 });197 });
195});198});
199
200describe("buildBrowseGroups", () => {
201 it("waits for asynchronous browse navigation actions", async () => {
202 let finishNavigation: (() => void) | undefined;
203 const browseTo = vi.fn(
204 () =>
205 new Promise<void>((resolve) => {
206 finishNavigation = resolve;
207 }),
208 );
209 const groups = buildBrowseGroups({
210 browseEntries: [{ name: "Downloads", fullPath: "/Users/test/Downloads" }],
211 browseQuery: "~/",
212 canBrowseUp: false,
213 upIcon: null,
214 directoryIcon: null,
215 browseUp: vi.fn(),
216 browseTo,
217 });
218 const item = groups[0]?.items[0];
219 if (!item || item.kind !== "action") {
220 throw new Error("Expected a browse action");
221 }
222
223 let actionSettled = false;
224 const action = item.run().then(() => {
225 actionSettled = true;
226 });
227 await Promise.resolve();
228
229 expect(browseTo).toHaveBeenCalledWith("Downloads");
230 expect(actionSettled).toBe(false);
231
232 finishNavigation?.();
233 await action;
234 expect(actionSettled).toBe(true);
235 });
236});
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+39-13
...@@ -3,6 +3,7 @@ import {...@@ -3,6 +3,7 @@ import {
3 type FilesystemBrowseEntry,3 type FilesystemBrowseEntry,
4 THREAD_JUMP_KEYBINDING_COMMANDS,4 THREAD_JUMP_KEYBINDING_COMMANDS,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
6import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";7import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
7import * as Arr from "effect/Array";8import * as Arr from "effect/Array";
8import * as Result from "effect/Result";9import * as Result from "effect/Result";
...@@ -15,6 +16,39 @@ export const RECENT_THREAD_LIMIT = 12;...@@ -15,6 +16,39 @@ export const RECENT_THREAD_LIMIT = 12;
15export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80";16export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80";
16export const ADDON_ICON_CLASS = "size-4";17export const ADDON_ICON_CLASS = "size-4";
1718
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
18export interface CommandPaletteItem {52export interface CommandPaletteItem {
19 readonly kind: "action" | "submenu";53 readonly kind: "action" | "submenu";
20 readonly value: string;54 readonly value: string;
...@@ -73,10 +107,8 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b...@@ -73,10 +107,8 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b
73export function filterBrowseEntries(input: {107export function filterBrowseEntries(input: {
74 browseEntries: ReadonlyArray<FilesystemBrowseEntry>;108 browseEntries: ReadonlyArray<FilesystemBrowseEntry>;
75 browseFilterQuery: string;109 browseFilterQuery: string;
76 highlightedItemValue: string | null;
77}): {110}): {
78 filteredEntries: FilesystemBrowseEntry[];111 filteredEntries: FilesystemBrowseEntry[];
79 highlightedEntry: FilesystemBrowseEntry | null;
80 exactEntry: FilesystemBrowseEntry | null;112 exactEntry: FilesystemBrowseEntry | null;
81} {113} {
82 const lowerFilter = input.browseFilterQuery.toLowerCase();114 const lowerFilter = input.browseFilterQuery.toLowerCase();
...@@ -88,18 +120,12 @@ export function filterBrowseEntries(input: {...@@ -88,18 +120,12 @@ export function filterBrowseEntries(input: {
88 (showHidden || !entry.name.startsWith(".")),120 (showHidden || !entry.name.startsWith(".")),
89 );121 );
90122
91 let highlightedEntry: FilesystemBrowseEntry | null = null;
92 if (input.highlightedItemValue?.startsWith("browse:")) {
93 const highlightedPath = input.highlightedItemValue.slice("browse:".length);
94 highlightedEntry = filteredEntries.find((entry) => entry.fullPath === highlightedPath) ?? null;
95 }
96
97 const exactEntry =123 const exactEntry =
98 input.browseFilterQuery.length > 0124 input.browseFilterQuery.length > 0
99 ? (filteredEntries.find((entry) => entry.name === input.browseFilterQuery) ?? null)125 ? (filteredEntries.find((entry) => entry.name === input.browseFilterQuery) ?? null)
100 : null;126 : null;
101127
102 return { filteredEntries, highlightedEntry, exactEntry };128 return { filteredEntries, exactEntry };
103}129}
104130
105export function normalizeSearchText(value: string): string {131export function normalizeSearchText(value: string): string {
...@@ -302,8 +328,8 @@ export function buildBrowseGroups(input: {...@@ -302,8 +328,8 @@ export function buildBrowseGroups(input: {
302 canBrowseUp: boolean;328 canBrowseUp: boolean;
303 upIcon: ReactNode;329 upIcon: ReactNode;
304 directoryIcon: ReactNode;330 directoryIcon: ReactNode;
305 browseUp: () => void;331 browseUp: () => void | Promise<void>;
306 browseTo: (name: string) => void;332 browseTo: (name: string) => void | Promise<void>;
307}): CommandPaletteGroup[] {333}): CommandPaletteGroup[] {
308 const items: CommandPaletteActionItem[] = [];334 const items: CommandPaletteActionItem[] = [];
309335
...@@ -316,7 +342,7 @@ export function buildBrowseGroups(input: {...@@ -316,7 +342,7 @@ export function buildBrowseGroups(input: {
316 icon: input.upIcon,342 icon: input.upIcon,
317 keepOpen: true,343 keepOpen: true,
318 run: async () => {344 run: async () => {
319 input.browseUp();345 await input.browseUp();
320 },346 },
321 });347 });
322 }348 }
...@@ -330,7 +356,7 @@ export function buildBrowseGroups(input: {...@@ -330,7 +356,7 @@ export function buildBrowseGroups(input: {
330 icon: input.directoryIcon,356 icon: input.directoryIcon,
331 keepOpen: true,357 keepOpen: true,
332 run: async () => {358 run: async () => {
333 input.browseTo(entry.name);359 await input.browseTo(entry.name);
334 },360 },
335 });361 });
336 }362 }
apps/web/src/components/CommandPalette.tsx+134-35
...@@ -92,6 +92,8 @@ import {...@@ -92,6 +92,8 @@ import {
92 buildProjectActionItems,92 buildProjectActionItems,
93 buildRootGroups,93 buildRootGroups,
94 buildThreadActionItems,94 buildThreadActionItems,
95 canPreloadBrowsePath,
96 createBrowseNavigationCoordinator,
95 enumerateCommandPaletteItems,97 enumerateCommandPaletteItems,
96 type CommandPaletteActionItem,98 type CommandPaletteActionItem,
97 type CommandPaletteSubmenuItem,99 type CommandPaletteSubmenuItem,
...@@ -486,6 +488,10 @@ function OpenCommandPaletteDialog(props: {...@@ -486,6 +488,10 @@ function OpenCommandPaletteDialog(props: {
486 const lookupRepository = useAtomQueryRunner(sourceControlEnvironment.repository, {488 const lookupRepository = useAtomQueryRunner(sourceControlEnvironment.repository, {
487 reportFailure: false,489 reportFailure: false,
488 });490 });
491 const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, {
492 reportFailure: false,
493 reportDefect: false,
494 });
489 const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, {495 const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, {
490 reportFailure: false,496 reportFailure: false,
491 });497 });
...@@ -502,6 +508,13 @@ function OpenCommandPaletteDialog(props: {...@@ -502,6 +508,13 @@ function OpenCommandPaletteDialog(props: {
502 const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);508 const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);
503 const currentView = viewStack.at(-1) ?? null;509 const currentView = viewStack.at(-1) ?? null;
504 const [browseGeneration, setBrowseGeneration] = useState(0);510 const [browseGeneration, setBrowseGeneration] = useState(0);
511 const browseNavigationRef = useRef<ReturnType<typeof createBrowseNavigationCoordinator> | null>(
512 null,
513 );
514 if (browseNavigationRef.current === null) {
515 browseNavigationRef.current = createBrowseNavigationCoordinator();
516 }
517 const browseNavigation = browseNavigationRef.current;
505 const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState<EnvironmentId | null>(518 const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState<EnvironmentId | null>(
506 null,519 null,
507 );520 );
...@@ -710,6 +723,11 @@ function OpenCommandPaletteDialog(props: {...@@ -710,6 +723,11 @@ function OpenCommandPaletteDialog(props: {
710 browseEnvironmentId && currentProjectEnvironmentId === browseEnvironmentId723 browseEnvironmentId && currentProjectEnvironmentId === browseEnvironmentId
711 ? currentProjectCwd724 ? currentProjectCwd
712 : null;725 : null;
726 const getBrowseCwdForEnvironment = useCallback(
727 (environmentId: EnvironmentId | null): string | null =>
728 environmentId && currentProjectEnvironmentId === environmentId ? currentProjectCwd : null,
729 [currentProjectCwd, currentProjectEnvironmentId],
730 );
713 const relativePathNeedsActiveProject =731 const relativePathNeedsActiveProject =
714 isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null;732 isExplicitRelativeProjectPath(query.trim()) && currentProjectCwdForBrowse === null;
715 const browseDirectoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";733 const browseDirectoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";
...@@ -733,8 +751,42 @@ function OpenCommandPaletteDialog(props: {...@@ -733,8 +751,42 @@ function OpenCommandPaletteDialog(props: {
733 const isBrowsePending = browseQuery.isPending;751 const isBrowsePending = browseQuery.isPending;
734 const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES;752 const browseEntries = browseResult?.entries ?? EMPTY_BROWSE_ENTRIES;
735 const { filteredEntries: filteredBrowseEntries, exactEntry: exactBrowseEntry } = useMemo(753 const { filteredEntries: filteredBrowseEntries, exactEntry: exactBrowseEntry } = useMemo(
736 () => filterBrowseEntries({ browseEntries, browseFilterQuery, highlightedItemValue }),754 () => filterBrowseEntries({ browseEntries, browseFilterQuery }),
737 [browseEntries, browseFilterQuery, highlightedItemValue],755 [browseEntries, browseFilterQuery],
756 );
757
758 const prefetchBrowsePath = useCallback(
759 async (
760 partialPath: string,
761 environmentId: EnvironmentId | null = browseEnvironmentId,
762 cwd: string | null = currentProjectCwdForBrowse,
763 ): Promise<void> => {
764 if (!environmentId) {
765 return;
766 }
767 const environment = environments.find(
768 (candidate) => candidate.environmentId === environmentId,
769 );
770 if (!canPreloadBrowsePath(environment?.connection.phase)) {
771 return;
772 }
773
774 await loadBrowsePath({
775 environmentId,
776 input: {
777 partialPath,
778 ...(cwd ? { cwd } : {}),
779 },
780 });
781 },
782 [browseEnvironmentId, currentProjectCwdForBrowse, environments, loadBrowsePath],
783 );
784
785 useEffect(
786 () => () => {
787 browseNavigation.invalidate();
788 },
789 [browseNavigation],
738 );790 );
739791
740 const openProjectFromSearch = useMemo(792 const openProjectFromSearch = useMemo(
...@@ -865,18 +917,22 @@ function OpenCommandPaletteDialog(props: {...@@ -865,18 +917,22 @@ function OpenCommandPaletteDialog(props: {
865 );917 );
866 const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT);918 const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT);
867919
868 function pushPaletteView(view: CommandPaletteView): void {920 const pushPaletteView = useCallback(
869 setViewStack((previousViews) => [921 (view: CommandPaletteView): void => {
870 ...previousViews,922 browseNavigation.invalidate();
871 {923 setViewStack((previousViews) => [
872 addonIcon: view.addonIcon,924 ...previousViews,
873 groups: view.groups,925 {
874 ...(view.initialQuery ? { initialQuery: view.initialQuery } : {}),926 addonIcon: view.addonIcon,
875 },927 groups: view.groups,
876 ]);928 ...(view.initialQuery ? { initialQuery: view.initialQuery } : {}),
877 setHighlightedItemValue(null);929 },
878 setQuery(view.initialQuery ?? "");930 ]);
879 }931 setHighlightedItemValue(null);
932 setQuery(view.initialQuery ?? "");
933 },
934 [browseNavigation],
935 );
880936
881 function pushView(item: CommandPaletteSubmenuItem): void {937 function pushView(item: CommandPaletteSubmenuItem): void {
882 pushPaletteView({938 pushPaletteView({
...@@ -887,6 +943,7 @@ function OpenCommandPaletteDialog(props: {...@@ -887,6 +943,7 @@ function OpenCommandPaletteDialog(props: {
887 }943 }
888944
889 function popView(): void {945 function popView(): void {
946 browseNavigation.invalidate();
890 setAddProjectCloneFlow(null);947 setAddProjectCloneFlow(null);
891 if (viewStack.length <= 1) {948 if (viewStack.length <= 1) {
892 setAddProjectEnvironmentId(null);949 setAddProjectEnvironmentId(null);
...@@ -897,6 +954,7 @@ function OpenCommandPaletteDialog(props: {...@@ -897,6 +954,7 @@ function OpenCommandPaletteDialog(props: {
897 }954 }
898955
899 function handleQueryChange(nextQuery: string): void {956 function handleQueryChange(nextQuery: string): void {
957 browseNavigation.invalidate();
900 setHighlightedItemValue(null);958 setHighlightedItemValue(null);
901 setQuery(nextQuery);959 setQuery(nextQuery);
902 if (nextQuery === "" && currentView?.initialQuery) {960 if (nextQuery === "" && currentView?.initialQuery) {
...@@ -905,16 +963,35 @@ function OpenCommandPaletteDialog(props: {...@@ -905,16 +963,35 @@ function OpenCommandPaletteDialog(props: {
905 }963 }
906964
907 const startAddProjectBrowse = useCallback(965 const startAddProjectBrowse = useCallback(
908 (environmentId: EnvironmentId): void => {966 async (environmentId: EnvironmentId): Promise<void> => {
909 setAddProjectEnvironmentId(environmentId);967 const initialQuery = getAddProjectInitialQueryForEnvironment(environmentId);
910 setAddProjectCloneFlow(null);968 const initialBrowsePath = getBrowseDirectoryPath(initialQuery);
911 pushPaletteView({969 const browseCwd = getBrowseCwdForEnvironment(environmentId);
970 const view: CommandPaletteView = {
912 addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />,971 addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />,
913 groups: [],972 groups: [],
914 initialQuery: getAddProjectInitialQueryForEnvironment(environmentId),973 initialQuery,
974 };
975
976 await browseNavigation.run({
977 load: () =>
978 initialBrowsePath.length > 0
979 ? prefetchBrowsePath(initialBrowsePath, environmentId, browseCwd)
980 : Promise.resolve(),
981 commit: () => {
982 setAddProjectEnvironmentId(environmentId);
983 setAddProjectCloneFlow(null);
984 pushPaletteView(view);
985 },
915 });986 });
916 },987 },
917 [getAddProjectInitialQueryForEnvironment],988 [
989 browseNavigation,
990 getAddProjectInitialQueryForEnvironment,
991 getBrowseCwdForEnvironment,
992 prefetchBrowsePath,
993 pushPaletteView,
994 ],
918 );995 );
919996
920 const startAddProjectClone = useCallback(997 const startAddProjectClone = useCallback(
...@@ -927,7 +1004,7 @@ function OpenCommandPaletteDialog(props: {...@@ -927,7 +1004,7 @@ function OpenCommandPaletteDialog(props: {
927 initialQuery: "",1004 initialQuery: "",
928 });1005 });
929 },1006 },
930 [],1007 [pushPaletteView],
931 );1008 );
9321009
933 const openSourceControlSettings = useCallback(() => {1010 const openSourceControlSettings = useCallback(() => {
...@@ -950,7 +1027,7 @@ function OpenCommandPaletteDialog(props: {...@@ -950,7 +1027,7 @@ function OpenCommandPaletteDialog(props: {
950 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,1027 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
951 keepOpen: true,1028 keepOpen: true,
952 run: async () => {1029 run: async () => {
953 startAddProjectBrowse(environmentId);1030 await startAddProjectBrowse(environmentId);
954 },1031 },
955 },1032 },
956 ];1033 ];
...@@ -1043,7 +1120,12 @@ function OpenCommandPaletteDialog(props: {...@@ -1043,7 +1120,12 @@ function OpenCommandPaletteDialog(props: {
1043 ),1120 ),
1044 });1121 });
1045 },1122 },
1046 [browseEnvironmentId, buildAddProjectSourceGroups, sourceControlDiscovery.data],1123 [
1124 browseEnvironmentId,
1125 buildAddProjectSourceGroups,
1126 pushPaletteView,
1127 sourceControlDiscovery.data,
1128 ],
1047 );1129 );
10481130
1049 const addProjectEnvironmentItems: CommandPaletteActionItem[] = addProjectEnvironmentOptions.map(1131 const addProjectEnvironmentItems: CommandPaletteActionItem[] = addProjectEnvironmentOptions.map(
...@@ -1098,6 +1180,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1098,6 +1180,7 @@ function OpenCommandPaletteDialog(props: {
1098 addProjectEnvironmentGroups,1180 addProjectEnvironmentGroups,
1099 addProjectEnvironmentOptions.length,1181 addProjectEnvironmentOptions.length,
1100 defaultAddProjectEnvironmentId,1182 defaultAddProjectEnvironmentId,
1183 pushPaletteView,
1101 startAddProjectSourceSelection,1184 startAddProjectSourceSelection,
1102 ]);1185 ]);
11031186
...@@ -1114,6 +1197,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1114,6 +1197,7 @@ function OpenCommandPaletteDialog(props: {
1114 return;1197 return;
1115 }1198 }
1116 clearOpenIntent();1199 clearOpenIntent();
1200 browseNavigation.invalidate();
1117 setAddProjectCloneFlow(null);1201 setAddProjectCloneFlow(null);
1118 setViewStack([]);1202 setViewStack([]);
1119 setQuery("");1203 setQuery("");
...@@ -1139,10 +1223,12 @@ function OpenCommandPaletteDialog(props: {...@@ -1139,10 +1223,12 @@ function OpenCommandPaletteDialog(props: {
1139 });1223 });
1140 }, [1224 }, [
1141 clearOpenIntent,1225 clearOpenIntent,
1226 browseNavigation,
1142 currentProjectEnvironmentId,1227 currentProjectEnvironmentId,
1143 currentProjectId,1228 currentProjectId,
1144 openIntent,1229 openIntent,
1145 projectThreadItems,1230 projectThreadItems,
1231 pushPaletteView,
1146 ]);1232 ]);
11471233
1148 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];1234 const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];
...@@ -1225,7 +1311,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1225,7 +1311,7 @@ function OpenCommandPaletteDialog(props: {
1225 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,1311 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
1226 keepOpen: true,1312 keepOpen: true,
1227 run: async () => {1313 run: async () => {
1228 startAddProjectBrowse(wslAddProjectEnvironmentOption.environmentId);1314 await startAddProjectBrowse(wslAddProjectEnvironmentOption.environmentId);
1229 },1315 },
1230 });1316 });
1231 }1317 }
...@@ -1541,23 +1627,36 @@ function OpenCommandPaletteDialog(props: {...@@ -1541,23 +1627,36 @@ function OpenCommandPaletteDialog(props: {
1541 await handleAddProject(cloneResult.value.cwd);1627 await handleAddProject(cloneResult.value.cwd);
1542 }1628 }
15431629
1544 function browseTo(name: string): void {1630 const browseTo = useCallback(
1545 const nextQuery = appendBrowsePathSegment(query, name);1631 async (name: string): Promise<void> => {
1546 setHighlightedItemValue(null);1632 const nextQuery = appendBrowsePathSegment(query, name);
1547 setQuery(nextQuery);1633 await browseNavigation.run({
1548 setBrowseGeneration((generation) => generation + 1);1634 load: () => prefetchBrowsePath(getBrowseDirectoryPath(nextQuery)),
1549 }1635 commit: () => {
1636 setHighlightedItemValue(null);
1637 setQuery(nextQuery);
1638 setBrowseGeneration((generation) => generation + 1);
1639 },
1640 });
1641 },
1642 [browseNavigation, prefetchBrowsePath, query],
1643 );
15501644
1551 function browseUp(): void {1645 const browseUp = useCallback(async (): Promise<void> => {
1552 const parentPath = getBrowseParentPath(query);1646 const parentPath = getBrowseParentPath(query);
1553 if (parentPath === null) {1647 if (parentPath === null) {
1554 return;1648 return;
1555 }1649 }
15561650
1557 setHighlightedItemValue(null);1651 await browseNavigation.run({
1558 setQuery(parentPath);1652 load: () => prefetchBrowsePath(parentPath),
1559 setBrowseGeneration((generation) => generation + 1);1653 commit: () => {
1560 }1654 setHighlightedItemValue(null);
1655 setQuery(parentPath);
1656 setBrowseGeneration((generation) => generation + 1);
1657 },
1658 });
1659 }, [browseNavigation, prefetchBrowsePath, query]);
15611660
1562 // Resolve the add-project path from browse data when available. When the1661 // Resolve the add-project path from browse data when available. When the
1563 // query has a trailing separator (e.g. "~/projects/foo/"), parentPath is the1662 // query has a trailing separator (e.g. "~/projects/foo/"), parentPath is the