Skip to content
Closed
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
178 changes: 110 additions & 68 deletions src/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { execFileSync, execSync } from "child_process";
import { exec, execFile } from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
import ejs from "ejs";
import type { SessionMessage } from "./session";
import { findGitBashPath, resolveShellPath } from "./common/shell-utils";
import { supportsMultimodal } from "./common/model-capabilities";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const execFileAsync = promisify(execFile) as (...args: any[]) => Promise<{ stdout: string; stderr: string }>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const execAsync = promisify(exec) as (...args: any[]) => Promise<{ stdout: string; stderr: string }>;

const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.

Expand Down Expand Up @@ -186,105 +192,141 @@ export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
return `${COMPACT_PROMPT_BASE}\n\nconversation below:\n\n\`\`\`jsonl\n${jsonl}\n\`\`\``;
}

export function getRuntimeContext(projectRoot: string, model?: string): string {
const uname = getUnameInfo();
const shellPath = getShellPathInfo();
const shellModeOpts = process.platform === "win32" ? { "shell mode": "git-bash" } : {};
const runtimeVersions = getRuntimeVersionInfo();
const env = {
"root path": projectRoot,
pwd: projectRoot,
homedir: os.homedir(),
"system info": uname,
"shell path": shellPath,
...shellModeOpts,
...runtimeVersions,
"command installed": {
ripgrep: checkToolInstalled("rg"),
jq: checkToolInstalled("jq"),
},
};
return `${getCurrentDateAndModelPrompt(model)}

# Local Workspace Environment
// ─── Async Runtime Context (cached, non-blocking) ─────────────────────────
// The synchronous version blocks the event loop ~12s on Windows due to 5 Git
// Bash spawns. This async+cached version runs probes in parallel and caches
// the result so createSession() never blocks.

\`\`\`json
${JSON.stringify(env, null, 2)}
\`\`\``;
}
let runtimeEnvJsonPromise: Promise<string> | null = null;
let runtimeEnvJsonCached: string | null = null;

function checkToolInstalled(tool: string): boolean {
async function getUnameInfoAsync(): Promise<string> {
try {
if (process.platform === "win32") {
const bashPath = findGitBashPath();
execFileSync(bashPath, ["-lc", `command -v ${shellSingleQuote(tool)}`], {
const { stdout } = await execFileAsync(bashPath, ["-lc", "uname -a"], {
encoding: "utf8",
stdio: "ignore",
windowsHide: true,
});
return true;
return stdout.trim();
}
execSync(`command -v ${tool}`, { encoding: "utf8", stdio: "ignore" });
return true;
const { stdout } = await execAsync("uname -a", { encoding: "utf8" });
return stdout.trim();
} catch {
return false;
return `${os.type()} ${os.release()} ${os.arch()}`;
}
}

function getShellPathInfo(): string {
async function getCommandVersionAsync(command: string, args: string[]): Promise<string | null> {
try {
return resolveShellPath();
} catch (error) {
return error instanceof Error ? error.message : String(error);
if (process.platform === "win32") {
const bashPath = findGitBashPath();
const commandText = [command, ...args].map(shellSingleQuote).join(" ");
const { stdout } = await execFileAsync(bashPath, ["-lc", `${commandText} 2>&1`], {
encoding: "utf8",
windowsHide: true,
});
return stdout.trim();
}
const commandText = [command, ...args].map(shellSingleQuote).join(" ");
const { stdout } = await execAsync(`${commandText} 2>&1`, { encoding: "utf8" });
return stdout.trim();
} catch {
return null;
}
}

function shellSingleQuote(value: string): string {
return `'${value.replace(/'/g, "'\"'\"'")}'`;
async function checkToolInstalledAsync(tool: string): Promise<boolean> {
try {
if (process.platform === "win32") {
const bashPath = findGitBashPath();
await execFileAsync(bashPath, ["-lc", `command -v ${shellSingleQuote(tool)}`], {
encoding: "utf8",
stdio: "ignore",
windowsHide: true,
});
return true;
}
await execAsync(`command -v ${tool}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {
return false;
}
}

function getRuntimeVersionInfo(): Record<string, string> {
async function getRuntimeVersionInfoAsync(): Promise<Record<string, string>> {
const versions: Record<string, string> = {};
const pythonVersion = getCommandVersion("python3", ["--version"]);
const nodeVersion = getCommandVersion("node", ["--version"]);

const [pythonVersion, nodeVersion] = await Promise.all([
getCommandVersionAsync("python3", ["--version"]),
getCommandVersionAsync("node", ["--version"]),
]);
if (pythonVersion) {
versions["python3 version"] = pythonVersion.replace(/^Python\s+/i, "");
}
if (nodeVersion) {
versions["node version"] = nodeVersion;
}

return versions;
}

function getCommandVersion(command: string, args: string[]): string | null {
try {
const commandText = [command, ...args].map(shellSingleQuote).join(" ");
if (process.platform === "win32") {
return execFileSync(findGitBashPath(), ["-lc", `${commandText} 2>&1`], {
encoding: "utf8",
windowsHide: true,
}).trim();
}
return execSync(`${commandText} 2>&1`, { encoding: "utf8" }).trim();
} catch {
return null;
}
async function computeRuntimeEnvJson(projectRoot: string): Promise<string> {
const [uname, shellPath, runtimeVersions, hasRg, hasJq] = await Promise.all([
getUnameInfoAsync(),
(async () => {
try {
return resolveShellPath();
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
})(),
getRuntimeVersionInfoAsync(),
checkToolInstalledAsync("rg"),
checkToolInstalledAsync("jq"),
]);

const shellModeOpts = process.platform === "win32" ? { "shell mode": "git-bash" } : {};
const env = {
"root path": projectRoot,
pwd: projectRoot,
homedir: os.homedir(),
"system info": uname,
"shell path": shellPath,
...shellModeOpts,
...runtimeVersions,
"command installed": {
ripgrep: hasRg,
jq: hasJq,
},
};
const json = JSON.stringify(env, null, 2);
runtimeEnvJsonCached = json;
return json;
}

function getUnameInfo(): string {
try {
if (process.platform === "win32") {
return execFileSync(findGitBashPath(), ["-lc", "uname -a"], {
encoding: "utf8",
windowsHide: true,
}).trim();
}
return execSync("uname -a", { encoding: "utf8" }).trim();
} catch {
return `${os.type()} ${os.release()} ${os.arch()}`;
async function getRuntimeEnvJson(projectRoot: string): Promise<string> {
if (runtimeEnvJsonCached) return runtimeEnvJsonCached;
if (!runtimeEnvJsonPromise) {
runtimeEnvJsonPromise = computeRuntimeEnvJson(projectRoot);
}
return runtimeEnvJsonPromise;
}

/** Start pre-computing the runtime env in the background (called at startup). */
export function prewarmRuntimeContext(projectRoot: string): void {
void getRuntimeEnvJson(projectRoot).catch((err) => {
console.error("[prewarmRuntimeContext] Failed to pre-compute runtime environment:", err);
});
}

export async function getRuntimeContext(projectRoot: string, model?: string): Promise<string> {
const envJson = await getRuntimeEnvJson(projectRoot);
return `${getCurrentDateAndModelPrompt(model)}\n\n# Local Workspace Environment\n\n\`\`\`json\n${envJson}\n\`\`\``;
}

// ─── Sync helpers (kept for backward compat) ──────────────────────────────

function shellSingleQuote(value: string): string {
return `'${value.replace(/'/g, "'\"'\"'")}'`;
}

function getExtensionRoot(): string {
Expand Down
5 changes: 4 additions & 1 deletion src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getRuntimeContext,
getSystemPrompt,
getTools,
prewarmRuntimeContext,
type ToolDefinition,
} from "./prompt";
import {
Expand Down Expand Up @@ -278,6 +279,8 @@ export class SessionManager {
this.onProcessStdout = options.onProcessStdout;
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager);
this.mcpManager.prepare(this.getResolvedSettings().mcpServers);
// Kick off async runtime context pre-computation while user is typing.
prewarmRuntimeContext(this.projectRoot);
}

async initMcpServers(servers?: Record<string, McpServerConfig>): Promise<void> {
Expand Down Expand Up @@ -964,7 +967,7 @@ The candidate skills are as follows:\n\n`;

const runtimeContextMessage = this.buildSystemMessage(
sessionId,
getRuntimeContext(this.projectRoot, promptToolOptions.model)
await getRuntimeContext(this.projectRoot, promptToolOptions.model)
);
this.appendSessionMessage(sessionId, runtimeContextMessage);

Expand Down
4 changes: 2 additions & 2 deletions src/tests/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ test("getSystemPrompt does not include current date guidance", () => {
assert.equal(prompt.includes(expected), false);
});

test("getRuntimeContext includes current date and model guidance", () => {
test("getRuntimeContext includes current date and model guidance", async () => {
const now = new Date();
const expectedDate = `今天是${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日。随着对话的进行,时间在流逝。`;
const prompt = getRuntimeContext("/tmp/project", "deepseek-v4-pro");
const prompt = await getRuntimeContext("/tmp/project", "deepseek-v4-pro");
assert.equal(prompt.includes(expectedDate), true);
assert.equal(prompt.includes("当前LLM模型为deepseek-v4-pro,对话中可通过/model命令切换模型。"), true);
assert.equal(prompt.includes("# Local Workspace Environment"), true);
Expand Down
Loading