Skip to content
Merged
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
20 changes: 20 additions & 0 deletions apps/web/src/components/files/FilePreviewPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
remapFileCommentAnnotations,
} from "./fileCommentAnnotations";
import {
isDirectoryEntry,
isMarkdownPreviewFile,
setMarkdownTaskChecked,
shouldShowFileExplorer,
Expand Down Expand Up @@ -120,3 +121,22 @@ describe("setMarkdownTaskChecked", () => {
expect(setMarkdownTaskChecked(markdown, 200, true)).toBe(markdown);
});
});

describe("isDirectoryEntry", () => {
const entries = [
{ kind: "directory" as const, path: ".agents" },
{ kind: "directory" as const, path: ".agents/skills" },
{ kind: "file" as const, path: ".agents/skills/SKILL.md" },
];

it("recognizes a listed directory with or without a trailing slash", () => {
expect(isDirectoryEntry(entries, ".agents/skills")).toBe(true);
expect(isDirectoryEntry(entries, ".agents/skills/")).toBe(true);
});

it("does not treat files or unknown paths as directories", () => {
expect(isDirectoryEntry(entries, ".agents/skills/SKILL.md")).toBe(false);
expect(isDirectoryEntry(entries, "missing")).toBe(false);
expect(isDirectoryEntry(undefined, ".agents")).toBe(false);
});
});
15 changes: 13 additions & 2 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { resolveCenteredFileLineScrollTop } from "./fileLineReveal";
import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation";
import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision";
import {
isDirectoryEntry,
isMarkdownPreviewFile,
setMarkdownTaskChecked,
shouldShowFileExplorer,
Expand All @@ -76,6 +77,7 @@ import { useFileSaveCoordinator } from "./useFileSaveCoordinator";
import {
getOptimisticProjectFileQueryData,
setProjectFileQueryData,
useProjectEntriesQuery,
useProjectFileQuery,
} from "./projectFilesQueryState";

Expand Down Expand Up @@ -956,7 +958,7 @@ export default function FilePreviewPanel({
environmentId,
cwd,
projectName,
relativePath,
relativePath: selectedPath,
attachment,
threadRef,
composerDraftTarget,
Expand All @@ -980,6 +982,15 @@ export default function FilePreviewPanel({
const openPreview = useAtomCommand(previewEnvironment.open, {
reportFailure: false,
});
// A chat link cannot tell a folder from a file, so a folder arrives as a file
// surface. The tree already knows every entry; a folder is revealed there
// and gets no preview pane instead of a read error.
const entries = useProjectEntriesQuery(environmentId, cwd);
const isDirectory =
selectedPath !== null &&
attachment === undefined &&
isDirectoryEntry(entries.data?.entries, selectedPath);
const relativePath = isDirectory ? null : selectedPath;
const isVideo = relativePath !== null && isWorkspaceVideoPreviewPath(relativePath);
const isImage = relativePath !== null && !isVideo && isWorkspaceImagePreviewPath(relativePath);
const isMedia = isImage || isVideo;
Expand Down Expand Up @@ -1342,7 +1353,7 @@ export default function FilePreviewPanel({
environmentId={environmentId}
cwd={cwd}
projectName={projectName}
selectedPath={relativePath}
selectedPath={selectedPath}
selectedPathRevealId={revealRequestId}
onOpenFile={onOpenFile}
workspaceMutationId={workspaceMutationId}
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/files/filePreviewMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ export function shouldShowFileExplorer(input: {
return input.explorerOpen || input.relativePath === null;
}

/** Whether a workspace path names a directory in the listed entries, with or without a trailing slash. */
export function isDirectoryEntry(
entries:
| ReadonlyArray<{ readonly kind: "file" | "directory"; readonly path: string }>
| undefined,
relativePath: string,
): boolean {
const normalizedPath = relativePath.replace(/\/+$/, "");
return (
entries?.some((entry) => entry.kind === "directory" && entry.path === normalizedPath) ?? false
);
}

export function setMarkdownTaskChecked(
markdown: string,
markerOffset: number,
Expand Down