Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gui/src/redux/thunks/streamNormalInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const streamNormalInput = createAsyncThunk<
state.session.mode,
selectedChatModel,
activeTools,
window.workspacePaths?.[0],
);

const systemMessage = systemToolsFramework
Expand Down
98 changes: 97 additions & 1 deletion gui/src/redux/util/getBaseSystemMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
});
56 changes: 56 additions & 0 deletions gui/src/redux/util/getBaseSystemMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<workspace_info>\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</workspace_info>`;
}

export function getBaseSystemMessage(
messageMode: string,
model: ModelDescription,
activeTools?: Tool[],
workspaceDirectory?: string,
): string {
let baseMessage: string;

Expand All @@ -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;
}
Loading