From 38fe7815a3001308a0f24aad125da50161c2ceb0 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Thu, 6 Aug 2026 12:19:20 +0530 Subject: [PATCH] fix: tell models the workspace root so filesystem tools work on remote setups Models were told to use filesystem tools relative to the workspace root but never given the root itself, so they guessed paths like C:\workspace. On Remote-SSH / Dev Containers the workspace is a vscode-remote:// URI and no file context reveals the real path, so every filesystem tool call failed. Inject the resolved workspace root into the agent/plan system message so models can point tools at the real path. Fixes #13082 --- gui/src/redux/thunks/streamNormalInput.ts | 1 + .../redux/util/getBaseSystemMessage.test.ts | 98 ++++++++++++++++++- gui/src/redux/util/getBaseSystemMessage.ts | 56 +++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts index 23429852d7a..043fefb9af4 100644 --- a/gui/src/redux/thunks/streamNormalInput.ts +++ b/gui/src/redux/thunks/streamNormalInput.ts @@ -137,6 +137,7 @@ export const streamNormalInput = createAsyncThunk< state.session.mode, selectedChatModel, activeTools, + window.workspacePaths?.[0], ); const systemMessage = systemToolsFramework diff --git a/gui/src/redux/util/getBaseSystemMessage.test.ts b/gui/src/redux/util/getBaseSystemMessage.test.ts index ed069693af2..d989847219a 100644 --- a/gui/src/redux/util/getBaseSystemMessage.test.ts +++ b/gui/src/redux/util/getBaseSystemMessage.test.ts @@ -4,7 +4,11 @@ import { DEFAULT_CHAT_SYSTEM_MESSAGE, DEFAULT_PLAN_SYSTEM_MESSAGE, } from "core/llm/defaultSystemMessages"; -import { getBaseSystemMessage, NO_TOOL_WARNING } from "./getBaseSystemMessage"; +import { + getBaseSystemMessage, + getWorkspaceDisplayPath, + NO_TOOL_WARNING, +} from "./getBaseSystemMessage"; test("getBaseSystemMessage should return the correct system message based on mode", () => { const mockModel = { @@ -84,3 +88,95 @@ test("getBaseSystemMessage should append no-tools warning for agent/plan modes w "Custom Plan System Message" + NO_TOOL_WARNING, ); }); + +test("getBaseSystemMessage should inject the workspace root for agent and plan modes", () => { + const mockModel = { + baseChatSystemMessage: "Custom Chat System Message", + basePlanSystemMessage: "Custom Plan System Message", + baseAgentSystemMessage: "Custom Agent System Message", + } as ModelDescription; + + const mockTool = { + function: { + name: "testTool", + description: "Test tool", + parameters: {}, + }, + } as Tool; + + const workspaceDirectory = "vscode-remote://ssh-remote+debian/opt/billing"; + + // Agent mode: workspace root appended after the base message + const agentMessage = getBaseSystemMessage( + "agent", + mockModel, + [mockTool], + workspaceDirectory, + ); + expect(agentMessage).toContain("Custom Agent System Message"); + expect(agentMessage).toContain("Your workspace root is: /opt/billing"); + + // Plan mode: workspace root appended after the base message + const planMessage = getBaseSystemMessage( + "plan", + mockModel, + [mockTool], + workspaceDirectory, + ); + expect(planMessage).toContain("Custom Plan System Message"); + expect(planMessage).toContain("Your workspace root is: /opt/billing"); + + // Chat mode: workspace root is not injected + const chatMessage = getBaseSystemMessage( + "chat", + mockModel, + [mockTool], + workspaceDirectory, + ); + expect(chatMessage).toBe("Custom Chat System Message"); + + // No workspace: no injection + expect(getBaseSystemMessage("agent", mockModel, [mockTool], "")).toBe( + "Custom Agent System Message", + ); + expect(getBaseSystemMessage("agent", mockModel, [mockTool], undefined)).toBe( + "Custom Agent System Message", + ); + + // No tools: no injection (avoids contradicting NO_TOOL_WARNING) + expect(getBaseSystemMessage("agent", mockModel, [], workspaceDirectory)).toBe( + "Custom Agent System Message" + NO_TOOL_WARNING, + ); +}); + +test("getWorkspaceDisplayPath should convert workspace URIs to display paths", () => { + // Remote-SSH (the reported bug: model invented C:\workspace) + expect( + getWorkspaceDisplayPath("vscode-remote://ssh-remote+debian/opt/billing"), + ).toBe("/opt/billing"); + + // Dev Containers + expect( + getWorkspaceDisplayPath("vscode-remote://dev-container+abc123/workspace"), + ).toBe("/workspace"); + + // vscode-vfs + expect(getWorkspaceDisplayPath("vscode-vfs://github/continue/continue")).toBe( + "/continue/continue", + ); + + // Local POSIX + expect(getWorkspaceDisplayPath("file:///home/user/proj")).toBe( + "/home/user/proj", + ); + + // Local Windows + expect(getWorkspaceDisplayPath("file:///C:/Users/bob/my%20project")).toBe( + "C:/Users/bob/my project", + ); + + // Unsupported / missing + expect(getWorkspaceDisplayPath("untitled:Untitled-1")).toBeNull(); + expect(getWorkspaceDisplayPath("")).toBeNull(); + expect(getWorkspaceDisplayPath("not a uri")).toBeNull(); +}); diff --git a/gui/src/redux/util/getBaseSystemMessage.ts b/gui/src/redux/util/getBaseSystemMessage.ts index c03493b158a..f5b56d928cf 100644 --- a/gui/src/redux/util/getBaseSystemMessage.ts +++ b/gui/src/redux/util/getBaseSystemMessage.ts @@ -8,10 +8,55 @@ import { export const NO_TOOL_WARNING = "\n\nTHE USER HAS NOT PROVIDED ANY TOOLS, DO NOT ATTEMPT TO USE ANY TOOLS. STOP AND LET THE USER KNOW THAT THERE ARE NO TOOLS AVAILABLE. The user can provide tools by enabling them in the Tool Policies section of the notch (wrench icon)"; +/** + * Converts a workspace directory (URI or raw path) into an LLM-friendly + * absolute path that the filesystem tools can be pointed at. + * + * - file:///C:/Users/me/proj -> C:/Users/me/proj (Windows local) + * - file:///home/me/proj -> /home/me/proj (POSIX local) + * - vscode-remote://ssh-remote+host/opt -> /opt (Remote-SSH / Dev Containers) + * - vscode-vfs://github/repo -> /repo + * - anything else (untitled:, empty) -> null + */ +export function getWorkspaceDisplayPath( + workspaceDirectory: string, +): string | null { + if (!workspaceDirectory) { + return null; + } + + try { + if (workspaceDirectory.startsWith("file://")) { + const { pathname } = new URL(workspaceDirectory); + const decoded = decodeURIComponent(pathname); + // On Windows, file:///C:/... has a leading slash before the drive letter + return /^\/[a-zA-Z]:/.test(decoded) ? decoded.slice(1) : decoded; + } + + // Remote IDE schemes embed the remote path after the authority: + // vscode-remote://ssh-remote+host/opt/billing -> /opt/billing + const remotePath = /^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)$/.exec( + workspaceDirectory, + ); + if (remotePath) { + return decodeURIComponent(remotePath[1]); + } + } catch { + // Ignore malformed URIs and fall through to null + } + + return null; +} + +function workspaceInfoBlock(displayPath: string): string { + return `\n\n\nYour workspace root is: ${displayPath}\nWhen using filesystem tools, pass paths relative to this root (e.g. "src/main.ts" for ${displayPath}/src/main.ts) or absolute paths. Explore the workspace with the viewSubdirectory or ls tools instead of guessing paths.\n`; +} + export function getBaseSystemMessage( messageMode: string, model: ModelDescription, activeTools?: Tool[], + workspaceDirectory?: string, ): string { let baseMessage: string; @@ -28,5 +73,16 @@ export function getBaseSystemMessage( baseMessage += NO_TOOL_WARNING; } + // Tell the model where the workspace root actually is. Without this, models + // guess a workspace path (e.g. `C:\workspace`), which breaks filesystem tools + // on remote setups like Remote-SSH or Dev Containers where the workspace is a + // non-file:// URI. Only relevant when tools are available to use. + if (messageMode !== "chat" && activeTools && activeTools.length > 0) { + const displayPath = getWorkspaceDisplayPath(workspaceDirectory ?? ""); + if (displayPath) { + baseMessage += workspaceInfoBlock(displayPath); + } + } + return baseMessage; }