mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 17:22:47 +00:00
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.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { planInlinePathOpen } from "./inline-path-open";
|
||||
|
||||
describe("planInlinePathOpen", () => {
|
||||
it("opens paths with a line number directly as files", () => {
|
||||
expect(
|
||||
planInlinePathOpen({
|
||||
file: "src/moys-asr-workflow",
|
||||
lineStart: 12,
|
||||
workspaceRoot: "/repo",
|
||||
}),
|
||||
).toEqual({ kind: "file" });
|
||||
});
|
||||
|
||||
it("opens paths with a file extension directly as files", () => {
|
||||
expect(
|
||||
planInlinePathOpen({ file: "src/components/message.tsx", workspaceRoot: "/repo" }),
|
||||
).toEqual({ kind: "file" });
|
||||
expect(planInlinePathOpen({ file: "README.md", workspaceRoot: "/repo" })).toEqual({
|
||||
kind: "file",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a missing file segment as a directory", () => {
|
||||
expect(planInlinePathOpen({ file: undefined, workspaceRoot: "/repo" })).toEqual({
|
||||
kind: "directory",
|
||||
directoryPath: ".",
|
||||
});
|
||||
});
|
||||
|
||||
it("probes extension-less workspace-relative paths", () => {
|
||||
expect(
|
||||
planInlinePathOpen({ file: "packages/app/moys-asr-workflow", workspaceRoot: "/repo" }),
|
||||
).toEqual({ kind: "probe", directoryPath: "packages/app/moys-asr-workflow" });
|
||||
expect(planInlinePathOpen({ file: "Makefile", workspaceRoot: "/repo" })).toEqual({
|
||||
kind: "probe",
|
||||
directoryPath: "Makefile",
|
||||
});
|
||||
});
|
||||
|
||||
it("opens extension-less absolute paths directly as files", () => {
|
||||
expect(planInlinePathOpen({ file: "C:/Tools/some-folder", workspaceRoot: "C:/repo" })).toEqual({
|
||||
kind: "file",
|
||||
});
|
||||
});
|
||||
|
||||
it("opens extension-less paths directly as files when there is no workspace root", () => {
|
||||
expect(planInlinePathOpen({ file: "some-folder", workspaceRoot: " " })).toEqual({
|
||||
kind: "file",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
|
||||
export type InlinePathOpenPlan =
|
||||
| { kind: "file" }
|
||||
| { kind: "directory"; directoryPath: string }
|
||||
| { kind: "probe"; directoryPath: string };
|
||||
|
||||
/**
|
||||
* Decides how an inline path press opens. Extension-less workspace-relative paths without a
|
||||
* line number cannot be told apart from folders lexically ("moys-asr-workflow" is a folder,
|
||||
* "Makefile" is a file), so they come back as "probe" and the caller asks the daemon which
|
||||
* one it is before opening a file tab that would fail with "Requested path is not a file".
|
||||
*/
|
||||
export function planInlinePathOpen(input: {
|
||||
file?: string;
|
||||
lineStart?: number;
|
||||
workspaceRoot: string;
|
||||
}): InlinePathOpenPlan {
|
||||
const { file, lineStart, workspaceRoot } = input;
|
||||
if (!file) {
|
||||
// Lexical directory: a trailing slash, ".", or the workspace root itself.
|
||||
return { kind: "directory", directoryPath: "." };
|
||||
}
|
||||
if (lineStart !== undefined || hasFileExtension(file)) {
|
||||
return { kind: "file" };
|
||||
}
|
||||
if (isAbsolutePath(file) || !workspaceRoot.trim()) {
|
||||
return { kind: "file" };
|
||||
}
|
||||
return { kind: "probe", directoryPath: file };
|
||||
}
|
||||
|
||||
function hasFileExtension(value: string): boolean {
|
||||
const lastSegment = value.split("/").pop() ?? "";
|
||||
return /\.[A-Za-z0-9]+$/.test(lastSegment);
|
||||
}
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
AssistantFileLinkResolverProvider,
|
||||
normalizeInlinePathTarget,
|
||||
} from "@/assistant-file-links";
|
||||
import { planInlinePathOpen } from "./inline-path-open";
|
||||
import {
|
||||
createWorkspaceFileTabTarget,
|
||||
normalizeWorkspaceFileLocation,
|
||||
@@ -437,7 +438,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized.file) {
|
||||
const openFileLocation = () => {
|
||||
if (!normalized.file) {
|
||||
return;
|
||||
}
|
||||
const location = normalizeWorkspaceFileLocation({
|
||||
path: normalized.file,
|
||||
lineStart: target.lineStart,
|
||||
@@ -462,27 +466,51 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
target: createWorkspaceFileTabTarget(location),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openFilesExplorerAt = (directoryPath: string) => {
|
||||
void requestDirectoryListing(directoryPath);
|
||||
|
||||
openExplorerSidebarView({
|
||||
isCompact: isMobile,
|
||||
workspaceKey: buildWorkspaceTabPersistenceKey({
|
||||
serverId: resolvedServerId,
|
||||
workspaceId: context.workspaceId ?? "",
|
||||
}),
|
||||
checkout: {
|
||||
serverId: resolvedServerId,
|
||||
cwd: context.cwd,
|
||||
isGit: context.projectPlacement?.checkout?.isGit ?? true,
|
||||
},
|
||||
view: "files",
|
||||
});
|
||||
};
|
||||
|
||||
const plan = planInlinePathOpen({
|
||||
file: normalized.file,
|
||||
lineStart: target.lineStart,
|
||||
workspaceRoot,
|
||||
});
|
||||
|
||||
if (plan.kind === "file") {
|
||||
openFileLocation();
|
||||
return;
|
||||
}
|
||||
|
||||
void requestDirectoryListing(normalized.directory, {
|
||||
recordHistory: false,
|
||||
setCurrentPath: false,
|
||||
});
|
||||
if (plan.kind === "directory") {
|
||||
openFilesExplorerAt(normalized.directory || ".");
|
||||
return;
|
||||
}
|
||||
|
||||
openExplorerSidebarView({
|
||||
isCompact: isMobile,
|
||||
workspaceKey: buildWorkspaceTabPersistenceKey({
|
||||
serverId: resolvedServerId,
|
||||
workspaceId: context.workspaceId ?? "",
|
||||
}),
|
||||
checkout: {
|
||||
serverId: resolvedServerId,
|
||||
cwd: context.cwd,
|
||||
isGit: context.projectPlacement?.checkout?.isGit ?? true,
|
||||
},
|
||||
view: "files",
|
||||
});
|
||||
if (!client) {
|
||||
openFileLocation();
|
||||
return;
|
||||
}
|
||||
|
||||
void client.listDirectory(workspaceRoot, plan.directoryPath).then(
|
||||
() => openFilesExplorerAt(plan.directoryPath),
|
||||
() => openFileLocation(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user