Files
paseo/packages/app/src/assistant-file-links/path-open.test.ts
T
paseo-bot[bot]andMoy fa70b215a8 Open chat folder links in Files with their contents visible (#7205)
* fix(app): open folders from inline path links in the file explorer

Clicking a folder path in an assistant message opened a file tab that
failed with "Requested path is not a file". Extension-less
workspace-relative paths without a line number are now probed with a
directory listing before opening: directories open the Files explorer
navigated to that folder, everything else opens as a file like before.
Lexical directories (trailing slash, workspace root) now navigate the
explorer to the folder instead of only preloading its listing.

* fix(app): reveal linked folders and their contents

Resolve workspace folder links from directory metadata and expand their ancestor paths in Files. Preserve file and line-reference opening, including extensionless files.

Co-authored-by: Moy <moyfoxther@gmail.com>

* fix(app): cancel stale path opens and reveal hidden folders

Keep pending directory checks inside the path-opening model so a later click or navigation supersedes them. Explicitly revealing a hidden directory also enables the existing hidden-file visibility preference.

---------

Co-authored-by: Moy <moyfoxther@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-17 09:53:28 +02:00

105 lines
3.8 KiB
TypeScript

import { mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, expect, it } from "vitest";
import { resolveInlinePathOpen } from "./path-open";
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
it("distinguishes dotted and extensionless folders from files using the filesystem", async () => {
const workspaceRoot = await mkdtemp(path.join(tmpdir(), "folder-link-"));
roots.push(workspaceRoot);
await mkdir(path.join(workspaceRoot, "docs/guides"), { recursive: true });
await mkdir(path.join(workspaceRoot, "docs/reports.v1"));
await writeFile(path.join(workspaceRoot, "Makefile"), "all:\n");
await writeFile(path.join(workspaceRoot, "README.md"), "# Readme\n");
const client = {
listDirectory: (root: string, file: string) => readdir(path.resolve(root, file)),
};
for (const file of ["docs/guides", "docs/reports.v1"]) {
await expect(
resolveInlinePathOpen({ file, directory: "docs", workspaceRoot, client }),
).resolves.toEqual({ kind: "directory", directoryPath: file });
}
for (const file of ["Makefile", "README.md", "missing.md"]) {
await expect(
resolveInlinePathOpen({ file, directory: ".", workspaceRoot, client }),
).resolves.toEqual({ kind: "file" });
}
});
it("preserves explicit directory destinations and file line references", async () => {
const workspaceRoot = "/repo";
await expect(
resolveInlinePathOpen({ directory: "docs/guides", workspaceRoot, client: null }),
).resolves.toEqual({ kind: "directory", directoryPath: "docs/guides" });
await expect(
resolveInlinePathOpen({
file: "docs/guides",
directory: "docs",
lineStart: 12,
workspaceRoot,
client: null,
}),
).resolves.toEqual({ kind: "file" });
});
it("keeps unavailable probes and external paths on the existing file-open path", async () => {
for (const file of ["Makefile", "/external/folder", "C:/external/folder"]) {
await expect(
resolveInlinePathOpen({ file, directory: ".", workspaceRoot: "/repo", client: null }),
).resolves.toEqual({ kind: "file" });
}
});
it("ignores an older probe after a newer file link opens", async () => {
const { createInlinePathOpener } = await import("./path-open");
let rejectProbe!: (error: Error) => void;
const pendingProbe = new Promise<never>((_, reject) => {
rejectProbe = reject;
});
const opened: string[] = [];
const opener = createInlinePathOpener({
workspaceRoot: "/repo",
client: { listDirectory: () => pendingProbe },
openFile: (location) => {
opened.push(location.path);
},
openDirectory: (directory) => {
opened.push(directory);
},
});
const first = opener.open({ raw: "./first.md", path: "/repo/first.md" }, "preferred");
await opener.open({ raw: "./second.md:2", path: "/repo/second.md", lineStart: 2 }, "preferred");
rejectProbe(new Error("Not a directory"));
await first;
expect(opened).toEqual(["second.md"]);
});
it("ignores a pending probe when its view is left", async () => {
const { createInlinePathOpener } = await import("./path-open");
let resolveProbe!: (value: unknown) => void;
const pendingProbe = new Promise((resolve) => {
resolveProbe = resolve;
});
const opened: string[] = [];
const opener = createInlinePathOpener({
workspaceRoot: "/repo",
client: { listDirectory: () => pendingProbe },
openFile: (location) => {
opened.push(location.path);
},
openDirectory: (directory) => {
opened.push(directory);
},
});
const pending = opener.open({ raw: "./docs", path: "/repo/docs" }, "preferred");
opener.cancel();
resolveProbe({});
await pending;
expect(opened).toEqual([]);
});