authorgravatar for 134572227+pc-style@users.noreply.github.compcstyle <134572227+pc-style@users.noreply.github.com> 2026-09-17 06:48:22+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 23:48:22-05:00
log886c83450797c46ee84ae28fc16c2208cafa965e
tree212155b11fa4787be58965afdd4691420788cee9
parent6d1d549441be84f19696ab59ed7e2fbf305280d4
signature Signed by PGP key B5690EEEBB952194

fix(web): folder links from chat open the file tree instead of a broken preview (#10909)

Co-authored-by: exe.dev user <exedev@ropeway-swimming.exe.xyz> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Yash Singh <saiansh2525@gmail.com>

6 files changed, 167 insertions(+), 50 deletions(-)

apps/web/src/components/files/FileBrowserPanel.tsx+13-4
...@@ -31,7 +31,7 @@ interface FileBrowserPanelProps {...@@ -31,7 +31,7 @@ interface FileBrowserPanelProps {
31 environmentId: EnvironmentId;31 environmentId: EnvironmentId;
32 cwd: string;32 cwd: string;
33 projectName: string;33 projectName: string;
34 /** File currently open in the preview pane; revealed and selected in the tree. */34 /** Entry currently open in the surface; revealed and selected in the tree. A directory is expanded. */
35 selectedPath: string | null;35 selectedPath: string | null;
36 /** Bumped when the same path should be revealed again (e.g. re-opened from search). */36 /** Bumped when the same path should be revealed again (e.g. re-opened from search). */
37 selectedPathRevealId: number;37 selectedPathRevealId: number;
...@@ -376,7 +376,10 @@ export default function FileBrowserPanel({...@@ -376,7 +376,10 @@ export default function FileBrowserPanel({
376 handledRevealRef.current = null;376 handledRevealRef.current = null;
377 return;377 return;
378 }378 }
379 if (entryKinds.get(selectedPath) !== "file") {379 const selectedKind = entryKinds.get(selectedPath);
380 // An unloaded entry has no row to reveal yet; folders do, and chat links can
381 // point at them.
382 if (selectedKind === undefined) {
380 handledRevealRef.current = null;383 handledRevealRef.current = null;
381 return;384 return;
382 }385 }
...@@ -390,7 +393,9 @@ export default function FileBrowserPanel({...@@ -390,7 +393,9 @@ export default function FileBrowserPanel({
390 ) {393 ) {
391 return;394 return;
392 }395 }
393 const selectedItem = model.getItem(selectedPath);396 // Directory rows are registered with a trailing slash (see treePath).
397 const selectedTreePath = selectedKind === "directory" ? `${selectedPath}/` : selectedPath;
398 const selectedItem = model.getItem(selectedTreePath);
394 if (!selectedItem) return;399 if (!selectedItem) return;
395400
396 // A selection that originated inside the tree (clicking a row, possibly401 // A selection that originated inside the tree (clicking a row, possibly
...@@ -425,8 +430,12 @@ export default function FileBrowserPanel({...@@ -425,8 +430,12 @@ export default function FileBrowserPanel({
425 if (item && "expand" in item) item.expand();430 if (item && "expand" in item) item.expand();
426 }431 }
427432
433 if ("expand" in selectedItem) selectedItem.expand();
428 selectedItem.select();434 selectedItem.select();
429 model.scrollToPath(selectedPath, { focus: true, offset: "center" });435 model.scrollToPath(selectedTreePath, {
436 focus: true,
437 offset: "center",
438 });
430 queueMicrotask(() => {439 queueMicrotask(() => {
431 syncingSelectionRef.current = false;440 syncingSelectionRef.current = false;
432 });441 });
apps/web/src/components/files/FilePreviewPanel.tsx+31-24
...@@ -941,15 +941,22 @@ export default function FilePreviewPanel({...@@ -941,15 +941,22 @@ export default function FilePreviewPanel({
941 // A file outside the workspace (an absolute path) is shown, never edited.941 // A file outside the workspace (an absolute path) is shown, never edited.
942 const isHostFile =942 const isHostFile =
943 attachment !== undefined || (relativePath !== null && isAbsolutePath(relativePath));943 attachment !== undefined || (relativePath !== null && isAbsolutePath(relativePath));
944 const file = useProjectFileQuery(944 // Media and PDFs render from their absolute path, so their contents are never
945 environmentId,945 // shown. The read still runs: a folder named `assets.png` is only knowable as a
946 cwd,946 // folder from the read failure, and the server stats before reading, so a folder
947 relativePath,947 // costs an open and a stat and returns no body.
948 attachment === undefined && !isMedia && !isPdf,948 const file = useProjectFileQuery(environmentId, cwd, relativePath, attachment === undefined);
949 );949 // A chat link cannot tell a folder from a file, so a folder arrives here as
950 // a file surface and the read fails. Keep the breadcrumbs, drop the preview
951 // pane, and let the tree fill the surface with the folder revealed. Mutation
952 // refresh stays on so the surface notices if the path becomes a file. A host
953 // path cannot be revealed in the workspace tree, so it keeps the read error.
954 const isDirectory = file.isNotFile && !isHostFile;
955 // Everything preview-related keys off previewPath; a folder has no preview.
956 const previewPath = isDirectory ? null : relativePath;
950 const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen);957 const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen);
951 const showExplorer = shouldShowFileExplorer({958 const showExplorer = shouldShowFileExplorer({
952 relativePath,959 relativePath: previewPath,
953 explorerOpen,960 explorerOpen,
954 attachmentOpen: attachment !== undefined,961 attachmentOpen: attachment !== undefined,
955 });962 });
...@@ -977,9 +984,9 @@ export default function FilePreviewPanel({...@@ -977,9 +984,9 @@ export default function FilePreviewPanel({
977 null,984 null,
978 );985 );
979 const breadcrumbRef = useRef<HTMLDivElement>(null);986 const breadcrumbRef = useRef<HTMLDivElement>(null);
980 const isMarkdown = relativePath ? isMarkdownPreviewFile(relativePath) : false;987 const isMarkdown = previewPath ? isMarkdownPreviewFile(previewPath) : false;
981 const tableDelimiter =988 const tableDelimiter =
982 relativePath && attachment === undefined ? filePreviewDelimiter({ name: relativePath }) : null;989 previewPath && attachment === undefined ? filePreviewDelimiter({ name: previewPath }) : null;
983 // A reveal still wins over the preference: the line only exists in the source.990 // A reveal still wins over the preference: the line only exists in the source.
984 const revealHandled =991 const revealHandled =
985 revealLine === null ||992 revealLine === null ||
...@@ -994,12 +1001,13 @@ export default function FilePreviewPanel({...@@ -994,12 +1001,13 @@ export default function FilePreviewPanel({
994 : isHtml1001 : isHtml
995 ? ("html" as const)1002 ? ("html" as const)
996 : null;1003 : null;
997 const canToggleRendered = attachment === undefined && renderedMode !== null;1004 const canToggleRendered =
1005 previewPath !== null && attachment === undefined && renderedMode !== null;
998 const updateClientSettings = useUpdateClientSettings();1006 const updateClientSettings = useUpdateClientSettings();
999 // Word wrap only reaches the text bodies. A rendered Markdown document, a table and the1007 // Word wrap only reaches the text bodies. A rendered Markdown document, a table and the
1000 // browser frame all lay themselves out, so the toggle stays hidden rather than inert.1008 // browser frame all lay themselves out, so the toggle stays hidden rather than inert.
1001 const showsRawText =1009 const showsRawText =
1002 relativePath !== null &&1010 previewPath !== null &&
1003 file.data !== null &&1011 file.data !== null &&
1004 !(isMarkdown && renderMarkdown) &&1012 !(isMarkdown && renderMarkdown) &&
1005 !(tableDelimiter && renderTable) &&1013 !(tableDelimiter && renderTable) &&
...@@ -1011,11 +1019,11 @@ export default function FilePreviewPanel({...@@ -1011,11 +1019,11 @@ export default function FilePreviewPanel({
1011 ? setRenderTablePreferred1019 ? setRenderTablePreferred
1012 : setRenderBrowserFilePreferred;1020 : setRenderBrowserFilePreferred;
1013 const canOpenInBrowser =1021 const canOpenInBrowser =
1014 relativePath !== null &&1022 previewPath !== null &&
1015 attachment === undefined &&1023 attachment === undefined &&
1016 !isVideo &&1024 !isVideo &&
1017 isPreviewSupportedInRuntime() &&1025 isPreviewSupportedInRuntime() &&
1018 isBrowserPreviewFile(relativePath);1026 isBrowserPreviewFile(previewPath);
1019 const absolutePath =1027 const absolutePath =
1020 relativePath && attachment === undefined ? resolvePathLinkTarget(relativePath, cwd) : null;1028 relativePath && attachment === undefined ? resolvePathLinkTarget(relativePath, cwd) : null;
1021 const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId);1029 const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId);
...@@ -1023,8 +1031,10 @@ export default function FilePreviewPanel({...@@ -1023,8 +1031,10 @@ export default function FilePreviewPanel({
1023 enabled:1031 enabled:
1024 attachment === undefined &&1032 attachment === undefined &&
1025 relativePath !== null &&1033 relativePath !== null &&
1026 !isMedia &&1034 // Media and PDFs never show their contents, so re-reading them on every
1027 !isPdf &&1035 // workspace mutation is waste. A folder named like one still re-reads, so
1036 // it notices when the path becomes a file.
1037 (isDirectory || (!isMedia && !isPdf)) &&
1028 !selectedFilePending,1038 !selectedFilePending,
1029 mutationId: workspaceMutationId,1039 mutationId: workspaceMutationId,
1030 refresh: file.refresh,1040 refresh: file.refresh,
...@@ -1145,7 +1155,7 @@ export default function FilePreviewPanel({...@@ -1145,7 +1155,7 @@ export default function FilePreviewPanel({
1145 <Globe2 className="size-3.5" />1155 <Globe2 className="size-3.5" />
1146 </FileSurfaceAction>1156 </FileSurfaceAction>
1147 ) : null}1157 ) : null}
1148 {!isHostFile ? (1158 {!isHostFile && previewPath !== null ? (
1149 <FileSurfaceAction1159 <FileSurfaceAction
1150 label={explorerOpen ? "Hide file explorer" : "Show file explorer"}1160 label={explorerOpen ? "Hide file explorer" : "Show file explorer"}
1151 pressed={explorerOpen}1161 pressed={explorerOpen}
...@@ -1156,7 +1166,7 @@ export default function FilePreviewPanel({...@@ -1156,7 +1166,7 @@ export default function FilePreviewPanel({
1156 ) : null}1166 ) : null}
1157 </div>1167 </div>
1158 ) : null}1168 ) : null}
1159 {relativePath &&1169 {previewPath &&
1160 attachment === undefined &&1170 attachment === undefined &&
1161 !isMedia &&1171 !isMedia &&
1162 !renderBrowserFile &&1172 !renderBrowserFile &&
...@@ -1167,12 +1177,9 @@ export default function FilePreviewPanel({...@@ -1167,12 +1177,9 @@ export default function FilePreviewPanel({
1167 ) : null}1177 ) : null}
1168 <div className="flex min-h-0 flex-1 overflow-hidden">1178 <div className="flex min-h-0 flex-1 overflow-hidden">
1169 <div1179 <div
1170 className={cn(1180 className={cn("min-w-0 flex-1 flex-col overflow-hidden", previewPath ? "flex" : "hidden")}
1171 "min-w-0 flex-1 flex-col overflow-hidden",
1172 relativePath ? "flex" : "hidden",
1173 )}
1174 >1181 >
1175 {relativePath && attachment ? (1182 {isDirectory ? null : relativePath && attachment ? (
1176 <AttachmentFilePreview1183 <AttachmentFilePreview
1177 key={`${environmentId}:${attachment.id}`}1184 key={`${environmentId}:${attachment.id}`}
1178 name={attachment.name}1185 name={attachment.name}
...@@ -1279,7 +1286,7 @@ export default function FilePreviewPanel({...@@ -1279,7 +1286,7 @@ export default function FilePreviewPanel({
1279 <aside1286 <aside
1280 className={cn(1287 className={cn(
1281 "flex min-h-0 shrink-0 bg-background",1288 "flex min-h-0 shrink-0 bg-background",
1282 relativePath1289 previewPath
1283 ? "w-[min(22rem,46%)] min-w-64 border-l border-border/60"1290 ? "w-[min(22rem,46%)] min-w-64 border-l border-border/60"
1284 : "min-w-0 flex-1",1291 : "min-w-0 flex-1",
1285 )}1292 )}
...@@ -1293,7 +1300,7 @@ export default function FilePreviewPanel({...@@ -1293,7 +1300,7 @@ export default function FilePreviewPanel({
1293 selectedPathRevealId={revealRequestId}1300 selectedPathRevealId={revealRequestId}
1294 onOpenFile={onOpenFile}1301 onOpenFile={onOpenFile}
1295 workspaceMutationId={workspaceMutationId}1302 workspaceMutationId={workspaceMutationId}
1296 {...(relativePath && !isMedia && !isPdf1303 {...(previewPath && !isMedia && !isPdf
1297 ? { onRefreshSelectedFile: file.refresh }1304 ? { onRefreshSelectedFile: file.refresh }
1298 : {})}1305 : {})}
1299 />1306 />
apps/web/src/components/files/projectFilesQueryState.test.tsx+59
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import {1import {
2 EnvironmentId,2 EnvironmentId,
3 type ProjectListEntriesResult,3 type ProjectListEntriesResult,
4 ProjectReadFileError,
4 type ProjectReadFileResult,5 type ProjectReadFileResult,
5} from "@t3tools/contracts";6} from "@t3tools/contracts";
6import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
...@@ -250,4 +251,62 @@ describe("project query refresh", () => {...@@ -250,4 +251,62 @@ describe("project query refresh", () => {
250 atomHooks.registry = null;251 atomHooks.registry = null;
251 }252 }
252 });253 });
254
255 it("reports a directory named like an image as not a file", async () => {
256 const readAtom = Atom.make(
257 Effect.fail(
258 new ProjectReadFileError({
259 cwd: "/repo",
260 relativePath: "assets.png",
261 failure: "path_not_file",
262 }),
263 ),
264 );
265 const registry = AtomRegistry.make();
266 const unmount = registry.mount(readAtom);
267 projectMocks.readFile.mockReturnValue(readAtom);
268 projectMocks.optimisticFile.mockReturnValue(Atom.make(null));
269 atomHooks.registry = registry;
270
271 try {
272 await flushEffects();
273 reactHooks.beginRender();
274 const query = useProjectFileQuery(environmentId, "/repo", "assets.png");
275 expect(query.isNotFile).toBe(true);
276 expect(query.data).toBeNull();
277 } finally {
278 unmount();
279 registry.dispose();
280 atomHooks.registry = null;
281 }
282 });
283
284 it("reports a directory read as not a file", async () => {
285 const readAtom = Atom.make(
286 Effect.fail(
287 new ProjectReadFileError({
288 cwd: "/repo",
289 relativePath: ".agents/skills",
290 failure: "path_not_file",
291 }),
292 ),
293 );
294 const registry = AtomRegistry.make();
295 const unmount = registry.mount(readAtom);
296 projectMocks.readFile.mockReturnValue(readAtom);
297 projectMocks.optimisticFile.mockReturnValue(Atom.make(null));
298 atomHooks.registry = registry;
299
300 try {
301 await flushEffects();
302 reactHooks.beginRender();
303 const query = useProjectFileQuery(environmentId, "/repo", ".agents/skills");
304 expect(query.isNotFile).toBe(true);
305 expect(query.data).toBeNull();
306 } finally {
307 unmount();
308 registry.dispose();
309 atomHooks.registry = null;
310 }
311 });
253});312});
apps/web/src/components/files/projectFilesQueryState.ts+29-21
...@@ -1,15 +1,13 @@...@@ -1,15 +1,13 @@
1import { useAtomRefresh, useAtomValue } from "@effect/atom-react";1import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
2import type {
3 EnvironmentId,
4 ProjectListEntriesResult,
5 ProjectReadFileResult,
6} from "@t3tools/contracts";
7import {2import {
8 isWorkspaceImagePreviewPath,3 type EnvironmentId,
9 isWorkspaceVideoPreviewPath,4 type ProjectListEntriesResult,
10} from "@t3tools/shared/filePreview";5 ProjectReadFileError,
6 type ProjectReadFileResult,
7} from "@t3tools/contracts";
11import * as Cause from "effect/Cause";8import * as Cause from "effect/Cause";
12import * as Option from "effect/Option";9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
13import { AsyncResult, Atom } from "effect/unstable/reactivity";11import { AsyncResult, Atom } from "effect/unstable/reactivity";
14import { useCallback } from "react";12import { useCallback } from "react";
1513
...@@ -33,6 +31,11 @@ interface ProjectQueryState<A> {...@@ -33,6 +31,11 @@ interface ProjectQueryState<A> {
33 readonly refresh: () => void;31 readonly refresh: () => void;
34}32}
3533
34interface ProjectFileQueryState extends ProjectQueryState<ProjectReadFileResult> {
35 /** The path exists but is not a regular file, typically a directory. */
36 readonly isNotFile: boolean;
37}
38
36function getProjectEntriesQueryAtom(39function getProjectEntriesQueryAtom(
37 environmentId: EnvironmentId,40 environmentId: EnvironmentId,
38 cwd: string,41 cwd: string,
...@@ -126,12 +129,17 @@ export function clearProjectFileQueryData(...@@ -126,12 +129,17 @@ export function clearProjectFileQueryData(
126 appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), null);129 appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), null);
127}130}
128131
129function errorMessage<A>(result: AsyncResult.AsyncResult<A, unknown>): string | null {132function failureCause<A>(result: AsyncResult.AsyncResult<A, unknown>): unknown {
130 if (result._tag !== "Failure") return null;133 return result._tag === "Failure" ? Cause.squash(result.cause) : null;
131 const cause = Cause.squash(result.cause);134}
135
136function errorMessage(cause: unknown): string | null {
137 if (cause === null) return null;
132 return cause instanceof Error ? cause.message : "Workspace query failed.";138 return cause instanceof Error ? cause.message : "Workspace query failed.";
133}139}
134140
141const isProjectReadFileError = Schema.is(ProjectReadFileError);
142
135export function useProjectEntriesQuery(143export function useProjectEntriesQuery(
136 environmentId: EnvironmentId,144 environmentId: EnvironmentId,
137 cwd: string,145 cwd: string,
...@@ -143,7 +151,7 @@ export function useProjectEntriesQuery(...@@ -143,7 +151,7 @@ export function useProjectEntriesQuery(
143 const refresh = useCallback(() => refreshAtom(), [refreshAtom]);151 const refresh = useCallback(() => refreshAtom(), [refreshAtom]);
144 return {152 return {
145 data: Option.getOrNull(AsyncResult.value(result)),153 data: Option.getOrNull(AsyncResult.value(result)),
146 error: errorMessage(result),154 error: errorMessage(failureCause(result)),
147 isPending: result.waiting,155 isPending: result.waiting,
148 refresh,156 refresh,
149 };157 };
...@@ -189,14 +197,12 @@ export function useProjectFileQuery(...@@ -189,14 +197,12 @@ export function useProjectFileQuery(
189 cwd: string,197 cwd: string,
190 relativePath: string | null,198 relativePath: string | null,
191 enabled = true,199 enabled = true,
192): ProjectQueryState<ProjectReadFileResult> {200): ProjectFileQueryState {
193 const isMedia =201 // The caller decides what to read. A media path is not skipped here: a folder
194 relativePath !== null &&202 // named `assets.png` is only knowable as a folder from the read failure.
195 (isWorkspaceImagePreviewPath(relativePath) || isWorkspaceVideoPreviewPath(relativePath));203 const atom = enabled
196 const atom =204 ? getProjectFileQueryAtom(environmentId, cwd, relativePath)
197 enabled && !isMedia205 : EMPTY_PROJECT_FILE_QUERY_ATOM;
198 ? getProjectFileQueryAtom(environmentId, cwd, relativePath)
199 : EMPTY_PROJECT_FILE_QUERY_ATOM;
200 const result = useAtomValue(atom);206 const result = useAtomValue(atom);
201 const refreshAtom = useAtomRefresh(atom);207 const refreshAtom = useAtomRefresh(atom);
202 const refresh = useCallback(() => refreshAtom(), [refreshAtom]);208 const refresh = useCallback(() => refreshAtom(), [refreshAtom]);
...@@ -205,10 +211,12 @@ export function useProjectFileQuery(...@@ -205,10 +211,12 @@ export function useProjectFileQuery(
205 optimisticFileAtom(environmentId, cwd, relativePath ?? EMPTY_PROJECT_FILE_PATH),211 optimisticFileAtom(environmentId, cwd, relativePath ?? EMPTY_PROJECT_FILE_PATH),
206 );212 );
207 const optimisticFile = relativePath === null ? null : optimisticResult;213 const optimisticFile = relativePath === null ? null : optimisticResult;
214 const cause = failureCause(result);
208215
209 return {216 return {
210 data: optimisticFile?.data ?? data,217 data: optimisticFile?.data ?? data,
211 error: errorMessage(result),218 error: errorMessage(cause),
219 isNotFile: isProjectReadFileError(cause) && cause.failure === "path_not_file",
212 isPending: result.waiting,220 isPending: result.waiting,
213 refresh,221 refresh,
214 };222 };
apps/web/src/rightPanelStore.test.ts+30
...@@ -475,6 +475,36 @@ describe("rightPanelStore", () => {...@@ -475,6 +475,36 @@ describe("rightPanelStore", () => {
475 });475 });
476 });476 });
477477
478 it.each([
479 ["generated\\", "generated"],
480 ["notes/meeting ", "notes/meeting"],
481 [" notes/meeting", "notes/meeting"],
482 ])("keeps %j and %j in separate file tabs", (firstPath, secondPath) => {
483 useRightPanelStore.getState().openFile(refA, firstPath);
484 useRightPanelStore.getState().openFile(refA, secondPath);
485
486 expect(
487 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces,
488 ).toMatchObject([
489 { id: `file:${firstPath}`, relativePath: firstPath },
490 { id: `file:${secondPath}`, relativePath: secondPath },
491 ]);
492 });
493
494 it.each([
495 ["docs/", "docs"],
496 ["docs///", "docs"],
497 ["/", "/"],
498 ["C:/", "C:/"],
499 ])("reuses the folder tab for %j and %j", (linkPath, treePath) => {
500 useRightPanelStore.getState().openFile(refA, linkPath);
501 useRightPanelStore.getState().openFile(refA, treePath);
502
503 expect(
504 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces,
505 ).toMatchObject([{ id: `file:${treePath}`, relativePath: treePath, revealRequestId: 2 }]);
506 });
507
478 it("opens an attachment as a file surface without the standalone explorer", () => {508 it("opens an attachment as a file surface without the standalone explorer", () => {
479 const attachment = {509 const attachment = {
480 type: "file" as const,510 type: "file" as const,
apps/web/src/rightPanelStore.ts+5-1
...@@ -575,9 +575,13 @@ export const useRightPanelStore = create<RightPanelStoreState>()(...@@ -575,9 +575,13 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
575 : next;575 : next;
576 }),576 }),
577 ),577 ),
578 openFile: (ref, relativePath, line) =>578 openFile: (ref, requestedPath, line) =>
579 set((state) =>579 set((state) =>
580 userAction(state, scopedThreadKey(ref), (current) => {580 userAction(state, scopedThreadKey(ref), (current) => {
581 // Workspace entry paths use '/', including on Windows.
582 const relativePath = /^[A-Za-z]:\/+$/.test(requestedPath)
583 ? requestedPath
584 : requestedPath.replace(/\/+$/, "") || requestedPath;
581 const withoutStandaloneExplorer = current.surfaces.filter(585 const withoutStandaloneExplorer = current.surfaces.filter(
582 (surface) => surface.kind !== "files",586 (surface) => surface.kind !== "files",
583 );587 );