From 429521b66945f4eac7bde830db0a70da04188520 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 17 Jul 2026 18:02:50 +0800 Subject: [PATCH] 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 --- apps/vscode/src/bridge-handler.ts | 19 ++++--- apps/vscode/test/workspace-paths.test.ts | 69 ++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/apps/vscode/src/bridge-handler.ts b/apps/vscode/src/bridge-handler.ts index d1a5f5a7f..04ef41a5e 100644 --- a/apps/vscode/src/bridge-handler.ts +++ b/apps/vscode/src/bridge-handler.ts @@ -210,14 +210,21 @@ export class BridgeHandler { selection: vscode.Selection, ): Promise { 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( diff --git a/apps/vscode/test/workspace-paths.test.ts b/apps/vscode/test/workspace-paths.test.ts index 008d06621..1e64f4338 100644 --- a/apps/vscode/test/workspace-paths.test.ts +++ b/apps/vscode/test/workspace-paths.test.ts @@ -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(); });