fix(vscode): allow editor mentions for files outside the working directory (#1836)

* fix(vscode): allow editor mentions for files outside the working directory

* fix(vscode): quote editor mentions whose paths contain spaces
This commit is contained in:
qer 2026-07-17 18:02:50 +08:00 committed by GitHub
parent 1b907b07cd
commit 429521b669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 9 deletions

View file

@ -210,14 +210,21 @@ export class BridgeHandler {
selection: vscode.Selection,
): Promise<string | null> {
const workDirUri = this.getWorkDirUri(webviewId);
if (workDirUri === null || !(await isWorkspacePathContained(workDirUri, documentUri))) return null;
const relativePath = relativeWorkspacePath(workDirUri, documentUri);
if (relativePath === undefined) return null;
// Mirror the CLI/TUI: no UI-level directory gate on mentions. Inside the
// working directory the mention is relative; outside it (for example a
// file under the session's additionalDirs) it falls back to the absolute
// path, and the session's tool layer decides readability. Virtual
// documents (untitled:, git:, ...) have no meaningful path to mention.
if (workDirUri === null || documentUri.scheme !== workDirUri.scheme) return null;
const filePath = relativeWorkspacePath(workDirUri, documentUri) ?? documentUri.fsPath;
// Quote paths containing spaces, as the CLI/TUI mention completers do, so
// whitespace cannot split the path; any line range goes after the quote.
const mentionTarget = filePath.includes(" ") ? `"${filePath}"` : filePath;
if (selection.isEmpty) return `@${relativePath}`;
if (selection.isEmpty) return `@${mentionTarget}`;
return selection.start.line === selection.end.line
? `@${relativePath}:${selection.start.line + 1}`
: `@${relativePath}:${selection.start.line + 1}-${selection.end.line + 1}`;
? `@${mentionTarget}:${selection.start.line + 1}`
: `@${mentionTarget}:${selection.start.line + 1}-${selection.end.line + 1}`;
}
captureFileBaseline(

View file

@ -1,6 +1,7 @@
/**
* Scenario: Webview file paths stay inside the selected working directory.
* Responsibilities: directory/search/open/mention paths are scoped, normalized, and symlink-safe.
* Scenario: Webview file paths and the selected working directory.
* Responsibilities: directory/search/open paths are scoped, normalized, and symlink-safe;
* editor mentions use relative paths inside the working directory and absolute paths outside.
* Wiring: real temporary local files plus the public handler/bridge surfaces;
* VS Code host APIs are the only stubbed boundary.
* Run: pnpm --filter kimi-code exec vitest run --config vitest.config.ts test/workspace-paths.test.ts
@ -343,7 +344,7 @@ describe("Webview workspace paths (selected-directory containment)", () => {
expect(mention).toBe("@src/inside.ts");
});
it("omits an editor mention when the file is outside the selected working directory", async () => {
it("builds an absolute editor mention when the file is outside the selected working directory", async () => {
const workDir = join(root, "project", "subproject");
const sibling = join(root, "project", "sibling.ts");
await mkdir(workDir, { recursive: true });
@ -357,6 +358,68 @@ describe("Webview workspace paths (selected-directory containment)", () => {
emptySelection(),
);
expect(mention).toBe(`@${sibling}`);
});
it("builds an absolute editor mention when the file is outside the workspace root", async () => {
const otherRoot = await mkdtemp(join(tmpdir(), "kimi-vscode-mention-outside-"));
extraRoots.push(otherRoot);
const outside = join(otherRoot, "App.java");
await writeFile(outside, "class App {}");
const bridge = createBridge();
const mention = await bridge.getEditorMention(
"view-1",
vscodeHost.Uri.file(outside) as vscode.Uri,
emptySelection(),
);
expect(mention).toBe(`@${outside}`);
});
it("quotes an absolute editor mention whose path contains spaces", async () => {
const otherRoot = await mkdtemp(join(tmpdir(), "kimi vscode mention space-"));
extraRoots.push(otherRoot);
const outside = join(otherRoot, "App.java");
await writeFile(outside, "class App {}");
const bridge = createBridge();
const mention = await bridge.getEditorMention(
"view-1",
vscodeHost.Uri.file(outside) as vscode.Uri,
emptySelection(),
);
expect(mention).toBe(`@"${outside}"`);
});
it("places the line range after the closing quote of a mention with spaces", async () => {
const workDir = join(root, "project");
const inside = join(workDir, "some dir", "inside.ts");
await mkdir(join(workDir, "some dir"), { recursive: true });
await writeFile(inside, "inside");
const bridge = createBridge();
await bridge.handle({ id: "set", method: Methods.SetWorkDir, params: { workDir } }, "view-1");
const mention = await bridge.getEditorMention(
"view-1",
vscodeHost.Uri.file(inside) as vscode.Uri,
{ isEmpty: false, start: { line: 2 }, end: { line: 4 } } as vscode.Selection,
);
expect(mention).toBe('@"some dir/inside.ts":3-5');
});
it("omits an editor mention for a document that is not on the workspace file system", async () => {
const bridge = createBridge();
const untitled = vscodeHost.Uri.from({ scheme: "untitled", path: "Untitled-1" });
const mention = await bridge.getEditorMention(
"view-1",
untitled as vscode.Uri,
emptySelection(),
);
expect(mention).toBeNull();
});