fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
This commit is contained in:
Max
2026-08-25 09:02:49 -07:00
committed by GitHub
parent 095385b985
commit 432e00eaa6
5 changed files with 137 additions and 10 deletions
+49 -4
View File
@@ -1,8 +1,17 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { buildUserInputMessage } from "./prompt";
import { basename, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { buildUserInputMessage, resolveSystemPrompt } from "./prompt";
const workspaceDirectories: string[] = [];
afterEach(() => {
for (const directory of workspaceDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
describe("buildUserInputMessage", () => {
it("extracts image mentions into userImages", async () => {
@@ -43,3 +52,39 @@ describe("buildUserInputMessage", () => {
expect(result.userFiles).toEqual([filePath]);
});
});
describe("resolveSystemPrompt workspace metadata", () => {
it("includes git remotes and the latest commit for Cline requests", async () => {
const cwd = mkdtempSync(join(tmpdir(), "cline-prompt-"));
workspaceDirectories.push(cwd);
execFileSync("git", ["init"], { cwd });
execFileSync("git", ["config", "user.email", "test@cline.bot"], { cwd });
execFileSync("git", ["config", "user.name", "Cline Test"], { cwd });
writeFileSync(join(cwd, "README.md"), "test\n");
execFileSync("git", ["add", "README.md"], { cwd });
execFileSync("git", ["commit", "-m", "initial"], { cwd });
execFileSync("git", ["remote", "add", "origin", "https://example.com/cline/repo.git"], { cwd });
const commit = execFileSync("git", ["rev-parse", "HEAD"], {
cwd,
encoding: "utf8",
}).trim();
const prompt = await resolveSystemPrompt({ cwd, providerId: "cline" });
expect(prompt).toContain("origin: https://example.com/cline/repo.git");
expect(prompt).toContain(commit);
});
it("includes parseable metadata outside a project", async () => {
const cwd = mkdtempSync(join(tmpdir(), "cline-prompt-"));
workspaceDirectories.push(cwd);
const prompt = await resolveSystemPrompt({ cwd, providerId: "cline" });
expect(prompt).toContain("# Workspace Configuration");
expect(prompt).toContain(JSON.stringify(cwd));
expect(prompt).toContain(`"hint": "${basename(cwd)}"`);
expect(prompt).not.toContain("associatedRemoteUrls");
expect(prompt).not.toContain("latestGitCommitHash");
});
});
@@ -355,6 +355,8 @@ describe("buildSessionConfig", () => {
expect(config.providerId).toBe("cline")
expect(config.apiKey).toBe("workos:test-access-token")
expect(config.systemPrompt).toContain("# Workspace Configuration")
expect(config.systemPrompt).toContain(JSON.stringify("/tmp/workspace"))
})
it("resolves ClinePass from the shared Cline OAuth credentials", async () => {
+14 -5
View File
@@ -9,6 +9,7 @@
// The factory does NOT handle UI concerns — that's the SdkController's job.
import {
buildWorkspaceMetadata,
type ClineCoreStartInput,
type CoreSessionConfig,
getProviderAuthHandler,
@@ -25,7 +26,7 @@ import {
MODEL_COLLECTIONS_BY_PROVIDER_ID,
OLLAMA_DEFAULT_CONTEXT_WINDOW,
} from "@cline/llms"
import { buildClineSystemPrompt } from "@cline/shared"
import { buildClineSystemPrompt, isClineProvider } from "@cline/shared"
import type { ApiConfiguration } from "@shared/api"
import { ClineClient } from "@shared/cline"
import type { HistoryItem } from "@shared/HistoryItem"
@@ -901,10 +902,17 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
? (resolveOcaReasoningConfig(mode, apiConfig) ?? resolveProviderReasoningConfig(providerId))
: resolveProviderReasoningConfig(providerId)
// Build the system prompt using the shared prompt builder. Core still
// expects callers to provide a concrete systemPrompt, but the prompt builder
// can derive baseline workspace context from the root path and workspace
// name, so we avoid duplicating core's richer workspace metadata pass here.
// Include rich workspace metadata so Cline API observability can extract
// git remotes and the latest commit hash from the system message.
let workspaceMetadata: string | undefined
if (isClineProvider(providerId)) {
try {
workspaceMetadata = await buildWorkspaceMetadata(workspaceRoot)
} catch (error) {
Logger.warn("[SessionFactory] Failed to build workspace metadata:", error)
}
}
let systemPrompt = ""
try {
const workspaceName = resolveWorkspaceName(cwd)
@@ -912,6 +920,7 @@ export async function buildSessionConfig(input: SessionConfigInput): Promise<Cor
ide: "VS Code",
workspaceRoot,
workspaceName,
metadata: workspaceMetadata,
mode: mode === "plan" ? "plan" : "act",
providerId,
platform: process.platform,
@@ -4,6 +4,7 @@ import {
MODE_TAG_INSTRUCTIONS,
PLAN_MODE_INSTRUCTIONS,
PLAN_MODE_INSTRUCTIONS_MANUAL_SWITCH,
processWorkspaceInfo,
} from "./cline";
const BASE_OPTIONS = {
@@ -13,6 +14,29 @@ const BASE_OPTIONS = {
platform: "linux",
};
describe("processWorkspaceInfo", () => {
it("redacts URL credentials while preserving SCP-style SSH remotes", () => {
const metadata = JSON.parse(
processWorkspaceInfo({
rootPath: "/workspace/project",
associatedRemoteUrls: [
"origin: https://user:token@github.com/cline/cline.git",
"backup: ssh://git:secret@example.com/cline/cline.git",
"mirror: git@github.com:cline/cline.git",
],
}),
);
expect(
metadata.workspaces["/workspace/project"].associatedRemoteUrls,
).toEqual([
"origin: https://github.com/cline/cline.git",
"backup: ssh://example.com/cline/cline.git",
"mirror: git@github.com:cline/cline.git",
]);
});
});
describe("buildClineSystemPrompt mode instructions", () => {
it("explains the user_input mode attribute in act mode", () => {
const prompt = buildClineSystemPrompt({ ...BASE_OPTIONS, mode: "act" });
@@ -78,6 +102,27 @@ describe("buildClineSystemPrompt mode instructions", () => {
expect(rulesIndex).toBeLessThan(prompt.indexOf(MODE_TAG_INSTRUCTIONS));
});
it("includes rich workspace metadata for the Cline backend parser", () => {
const metadata = JSON.stringify({
workspaces: {
"/workspace/project": {
hint: "project",
associatedRemoteUrls: [
"origin: https://github.com/cline/cline.git",
],
latestGitCommitHash: "abc123",
},
},
});
const prompt = buildClineSystemPrompt({
...BASE_OPTIONS,
providerId: "cline",
metadata,
});
expect(prompt).toContain(`# Workspace Configuration\n${metadata}`);
});
it("respects an explicit override prompt without injecting mode sections", () => {
const prompt = buildClineSystemPrompt({
...BASE_OPTIONS,
+27 -1
View File
@@ -58,13 +58,39 @@ export const PLAN_MODE_INSTRUCTIONS_MANUAL_SWITCH = `${PLAN_MODE_INSTRUCTIONS_BA
Once you have presented your plan, end your turn and wait for the user's response. You do NOT have the ability to switch to act mode yourself -- the user must do it manually with the Plan/Act toggle once they are satisfied with the plan. If the task requires tools that are only available in act mode, ask the user to "toggle to Act mode" (use those words).`;
function redactRemoteUrlCredentials(remote: string): string {
const schemeEnd = remote.indexOf("://");
if (schemeEnd < 1) return remote;
const authorityStart = schemeEnd + 3;
let authorityEnd = authorityStart;
while (authorityEnd < remote.length) {
const char = remote[authorityEnd];
if (
char === "/" ||
char === "?" ||
char === "#" ||
char.charCodeAt(0) <= 32
) {
break;
}
authorityEnd++;
}
const userInfoEnd = remote.lastIndexOf("@", authorityEnd - 1);
if (userInfoEnd < authorityStart) return remote;
return remote.slice(0, authorityStart) + remote.slice(userInfoEnd + 1);
}
export function processWorkspaceInfo(info: WorkspaceInfo): string {
return JSON.stringify(
{
workspaces: {
[info.rootPath]: {
hint: info.hint,
associatedRemoteUrls: info.associatedRemoteUrls,
associatedRemoteUrls: info.associatedRemoteUrls?.map(
redactRemoteUrlCredentials,
),
latestGitCommitHash: info.latestGitCommitHash,
latestGitBranchName: info.latestGitBranchName,
},