Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions apps/web/src/components/files/FileBrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface FileBrowserPanelProps {
environmentId: EnvironmentId;
cwd: string;
projectName: string;
/** File currently open in the preview pane; revealed and selected in the tree. */
/** Entry currently open in the surface; revealed and selected in the tree. A directory is expanded. */
selectedPath: string | null;
/** Bumped when the same path should be revealed again (e.g. re-opened from search). */
selectedPathRevealId: number;
Expand Down Expand Up @@ -299,8 +299,13 @@ export default function FileBrowserPanel({
) {
return;
}
if (entryKinds.get(selectedPath) !== "file") return;
const selectedItem = model.getItem(selectedPath);
const selectedKind = entryKinds.get(selectedPath);
if (selectedKind === undefined) return;
// Directory rows are registered with a trailing slash (see treePath).
const selectedItem =
selectedKind === "directory"
? model.getItem(`${selectedPath}/`)
: model.getItem(selectedPath);
if (!selectedItem) return;

// A selection that originated inside the tree (clicking a row, possibly
Expand Down Expand Up @@ -334,8 +339,12 @@ export default function FileBrowserPanel({
if (item && "expand" in item) item.expand();
}

if (selectedKind === "directory" && "expand" in selectedItem) selectedItem.expand();
selectedItem.select();
model.scrollToPath(selectedPath, { focus: true, offset: "center" });
model.scrollToPath(selectedKind === "directory" ? `${selectedPath}/` : selectedPath, {
focus: true,
offset: "center",
});
queueMicrotask(() => {
syncingSelectionRef.current = false;
});
Expand Down
29 changes: 19 additions & 10 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -995,12 +995,20 @@ export default function FilePreviewPanel({
relativePath,
attachment === undefined && !isMedia && !isPdf,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium files/FilePreviewPanel.tsx:996

Directory links whose names end in .pdf or a media extension are still rendered as file previews instead of showing the folder tree. The isPdf/isMedia guards prevent useProjectFileQuery from producing the path_not_file result that sets file.isNotFile; allow a path-type check for these extensions (or use a separate directory query) so isDirectory is detected before selecting WorkspaceBrowserPreview or a media preview.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 996:

Directory links whose names end in `.pdf` or a media extension are still rendered as file previews instead of showing the folder tree. The `isPdf`/`isMedia` guards prevent `useProjectFileQuery` from producing the `path_not_file` result that sets `file.isNotFile`; allow a path-type check for these extensions (or use a separate directory query) so `isDirectory` is detected before selecting `WorkspaceBrowserPreview` or a media preview.

@pc-style pc-style Sep 9, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 agent reply

posted by an ai agent in adam's name. model: Claude Fable 5.1. harness: Claude Code. adam hasn't read this before it went out.

FABLE WROTE THAT NO IDEA WHY IT SKIPPED THE "IM AN AGENT" part of my pr skill.
Leaving this one. A folder named like report.pdf or images.png skipped the read before this PR as well, so the broken preview there is pre-existing and unchanged here. Handling it means a stat round trip for every media path just to catch a folder with a file-like name, which is not worth it for this fix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
// A chat link cannot tell a folder from a file, so a folder arrives here as
// a file surface and the read fails. Keep the breadcrumbs, drop the preview
// pane, and let the tree fill the surface with the folder revealed. Mutation
// refresh stays on so the surface notices if the path becomes a file. A host
// path cannot be revealed in the workspace tree, so it keeps the read error.
const isDirectory = file.isNotFile && !isHostFile;
const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen);
const showExplorer = shouldShowFileExplorer({
relativePath,
explorerOpen,
attachmentOpen: attachment !== undefined,
});
const showExplorer =
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
isDirectory ||
shouldShowFileExplorer({
relativePath,
explorerOpen,
attachmentOpen: attachment !== undefined,
});
// Reading markdown rendered is a preference, not a property of one file. Keeping
// it on the panel meant a thread switch dropped it and forced source back.
const [renderMarkdownPreferred, setRenderMarkdownPreferred] = useLocalStorage(
Expand All @@ -1027,12 +1035,13 @@ export default function FilePreviewPanel({
(handledReveal?.path === relativePath && handledReveal.requestId === revealRequestId);
const renderMarkdown = isMarkdown && renderMarkdownPreferred && revealHandled;
const renderBrowserFile = isPdf || (isHtml && renderBrowserFilePreferred && revealHandled);
const canToggleRendered = attachment === undefined && (isMarkdown || isHtml);
const canToggleRendered = !isDirectory && attachment === undefined && (isMarkdown || isHtml);
const rendered = isMarkdown ? renderMarkdown : renderBrowserFile;
const setRenderedPreferred = isMarkdown
? setRenderMarkdownPreferred
: setRenderBrowserFilePreferred;
const canOpenInBrowser =
!isDirectory &&
relativePath !== null &&
attachment === undefined &&
!isVideo &&
Expand Down Expand Up @@ -1190,7 +1199,7 @@ export default function FilePreviewPanel({
<TooltipPopup>Open file in preview browser</TooltipPopup>
</Tooltip>
) : null}
{!isHostFile ? (
{!isHostFile && !isDirectory ? (
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<Tooltip>
<TooltipTrigger
render={
Expand Down Expand Up @@ -1222,7 +1231,7 @@ export default function FilePreviewPanel({
<div
className={cn(
"min-w-0 flex-1 flex-col overflow-hidden",
relativePath ? "flex" : "hidden",
relativePath && !isDirectory ? "flex" : "hidden",
)}
>
{relativePath && attachment ? (
Expand Down Expand Up @@ -1332,7 +1341,7 @@ export default function FilePreviewPanel({
<aside
className={cn(
"flex min-h-0 shrink-0 bg-background",
relativePath
relativePath && !isDirectory
? "w-[min(22rem,46%)] min-w-64 border-l border-border/60"
: "min-w-0 flex-1",
)}
Expand All @@ -1346,7 +1355,7 @@ export default function FilePreviewPanel({
selectedPathRevealId={revealRequestId}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium files/FilePreviewPanel.tsx:1353

A directory link ending in / is not revealed or selected in FileBrowserPanel: selectedPath retains the trailing slash, so its entryKinds.get(selectedPath) lookup misses the directory key without the slash. Strip trailing slashes before passing the selected path.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 1353:

A directory link ending in `/` is not revealed or selected in `FileBrowserPanel`: `selectedPath` retains the trailing slash, so its `entryKinds.get(selectedPath)` lookup misses the directory key without the slash. Strip trailing slashes before passing the selected path.

@pc-style pc-style Sep 9, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 agent reply

posted by an ai agent in adam's name. model: Claude Fable 5.1. harness: Claude Code. adam hasn't read this before it went out.

Done in 419f555. The store strips a trailing slash when opening a file surface, so docs/ and docs reach the tree the same way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

onOpenFile={onOpenFile}
workspaceMutationId={workspaceMutationId}
{...(relativePath && !isMedia && !isPdf
{...(relativePath && !isMedia && !isPdf && !isDirectory
? { onRefreshSelectedFile: file.refresh }
: {})}
/>
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/components/files/projectFilesQueryState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
EnvironmentId,
type ProjectListEntriesResult,
ProjectReadFileError,
type ProjectReadFileResult,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -250,4 +251,33 @@ describe("project query refresh", () => {
atomHooks.registry = null;
}
});

it("reports a directory read as not a file", async () => {
const readAtom = Atom.make(
Effect.fail(
new ProjectReadFileError({
cwd: "/repo",
relativePath: ".agents/skills",
failure: "path_not_file",
}),
),
);
const registry = AtomRegistry.make();
const unmount = registry.mount(readAtom);
projectMocks.readFile.mockReturnValue(readAtom);
projectMocks.optimisticFile.mockReturnValue(Atom.make(null));
atomHooks.registry = registry;

try {
await flushEffects();
reactHooks.beginRender();
const query = useProjectFileQuery(environmentId, "/repo", ".agents/skills");
expect(query.isNotFile).toBe(true);
expect(query.data).toBeNull();
} finally {
unmount();
registry.dispose();
atomHooks.registry = null;
}
});
});
26 changes: 21 additions & 5 deletions apps/web/src/components/files/projectFilesQueryState.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import type {
EnvironmentId,
ProjectListEntriesResult,
ProjectReadFileResult,
import {
type EnvironmentId,
type ProjectListEntriesResult,
ProjectReadFileError,
type ProjectReadFileResult,
} from "@t3tools/contracts";
import {
isWorkspaceImagePreviewPath,
isWorkspaceVideoPreviewPath,
} from "@t3tools/shared/filePreview";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import { AsyncResult, Atom } from "effect/unstable/reactivity";
import { useCallback } from "react";

Expand All @@ -33,6 +35,11 @@ interface ProjectQueryState<A> {
readonly refresh: () => void;
}

interface ProjectFileQueryState extends ProjectQueryState<ProjectReadFileResult> {
/** The path exists but is not a regular file, typically a directory. */
readonly isNotFile: boolean;
}

function getProjectEntriesQueryAtom(environmentId: EnvironmentId, cwd: string) {
return projectEnvironment.listEntries({ environmentId, input: { cwd } });
}
Expand Down Expand Up @@ -125,6 +132,14 @@ function errorMessage<A>(result: AsyncResult.AsyncResult<A, unknown>): string |
return cause instanceof Error ? cause.message : "Workspace query failed.";
}

const isProjectReadFileError = Schema.is(ProjectReadFileError);

function isNotFileFailure<A>(result: AsyncResult.AsyncResult<A, unknown>): boolean {
if (result._tag !== "Failure") return false;
const cause = Cause.squash(result.cause);
return isProjectReadFileError(cause) && cause.failure === "path_not_file";
}

export function useProjectEntriesQuery(
environmentId: EnvironmentId,
cwd: string,
Expand Down Expand Up @@ -181,7 +196,7 @@ export function useProjectFileQuery(
cwd: string,
relativePath: string | null,
enabled = true,
): ProjectQueryState<ProjectReadFileResult> {
): ProjectFileQueryState {
const isMedia =
relativePath !== null &&
(isWorkspaceImagePreviewPath(relativePath) || isWorkspaceVideoPreviewPath(relativePath));
Expand All @@ -201,6 +216,7 @@ export function useProjectFileQuery(
return {
data: optimisticFile?.data ?? data,
error: errorMessage(result),
isNotFile: isNotFileFailure(result),
isPending: result.waiting,
refresh,
};
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/rightPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,11 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
: next;
}),
),
openFile: (ref, relativePath, line) =>
openFile: (ref, requestedPath, line) =>
set((state) =>
userAction(state, scopedThreadKey(ref), (current) => {
// A folder link may end in a slash; the tree keys folders without it.
const relativePath = requestedPath.replace(/\/+$/, "") || requestedPath;
const withoutStandaloneExplorer = current.surfaces.filter(
(surface) => surface.kind !== "files",
);
Expand Down
Loading