Skip to content

Commit b892be8

Browse files
authored
Merge branch 'lessweb:main' into main
2 parents 64edb90 + 3fa93e2 commit b892be8

11 files changed

Lines changed: 601 additions & 85 deletions

File tree

src/common/state.ts

Lines changed: 343 additions & 4 deletions
Large diffs are not rendered by default.

src/prompt.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,18 +493,17 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe
493493
parameters: {
494494
type: "object",
495495
properties: {
496-
file_path: {
496+
snippet_id: {
497497
type: "string",
498-
description: "Absolute path to file. Optional when snippet_id is provided.",
498+
description: "Required Read/Edit snippet_id.",
499499
},
500-
snippet_id: {
500+
file_path: {
501501
type: "string",
502-
description:
503-
"Snippet id returned by the Read or Edit tool to scope the search range after a partial read.",
502+
description: "Optional absolute path guard; must match snippet_id's file.",
504503
},
505504
old_string: {
506505
type: "string",
507-
description: "Exact text to replace inside the file or snippet scope",
506+
description: "Exact text to replace inside snippet_id's scope",
508507
},
509508
new_string: {
510509
type: "string",
@@ -520,7 +519,7 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe
520519
description: "Expected number of matches, especially useful as a safety check with replace_all",
521520
},
522521
},
523-
required: ["old_string", "new_string"],
522+
required: ["snippet_id", "old_string", "new_string"],
524523
additionalProperties: false,
525524
},
526525
},

src/session.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { logApiError } from "./common/error-logger";
3131
import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger";
3232
import { killProcessTree } from "./common/process-tree";
3333
import { GitFileHistory } from "./common/file-history";
34-
import { getSnippet } from "./common/state";
34+
import { clearSessionState, getSnippet, rebuildSessionStateFromHistory } from "./common/state";
3535
import {
3636
appendProjectPermissionAllows,
3737
buildPermissionToolExecution,
@@ -45,7 +45,6 @@ import {
4545
type UserToolPermission,
4646
} from "./common/permissions";
4747
import { clearSessionWorkingDir } from "./tools/bash-handler";
48-
import { clearSessionState } from "./common/state";
4948

5049
export type { PermissionScope } from "./settings";
5150
export type {
@@ -1143,6 +1142,7 @@ ${skillMd}
11431142
const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } =
11441143
this.createOpenAIClient();
11451144
const now = new Date().toISOString();
1145+
rebuildSessionStateFromHistory(sessionId, this.listSessionMessages(sessionId));
11461146

11471147
if (!client) {
11481148
this.updateSessionEntry(sessionId, (entry) => ({
@@ -2547,6 +2547,12 @@ ${skillMd}
25472547
return typeof args.explanation === "string" ? args.explanation.trim() : "";
25482548
} else if (toolName === "write") {
25492549
return typeof args.file_path === "string" ? args.file_path.trim() : "";
2550+
} else if (toolName === "edit") {
2551+
const filePath = typeof args.file_path === "string" ? args.file_path.trim() : "";
2552+
if (filePath) {
2553+
return filePath;
2554+
}
2555+
return typeof args.snippet_id === "string" ? args.snippet_id.trim() : "";
25502556
}
25512557

25522558
const firstKey = Object.keys(args)[0];

src/tests/file-mentions.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,62 @@ test("scanFileMentionItems returns relative slash-separated files and directorie
8686
try {
8787
fs.mkdirSync(path.join(root, "src"));
8888
fs.writeFileSync(path.join(root, "src", "index.ts"), "");
89-
fs.mkdirSync(path.join(root, "node_modules"));
90-
fs.writeFileSync(path.join(root, "node_modules", "ignored.js"), "");
89+
fs.mkdirSync(path.join(root, "vendor"));
90+
fs.writeFileSync(path.join(root, "vendor", "dep.js"), "");
9191

9292
assert.deepEqual(
9393
scanFileMentionItems(root).map((item) => item.path),
94-
["node_modules/", "node_modules/ignored.js", "src/", "src/index.ts"]
94+
["src/", "src/index.ts", "vendor/", "vendor/dep.js"]
9595
);
9696
} finally {
9797
fs.rmSync(root, { recursive: true, force: true });
9898
}
9999
});
100100

101+
test("scanFileMentionItems applies default noisy-directory ignores when no gitignore is applicable", () => {
102+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
103+
try {
104+
for (const directory of [
105+
".next",
106+
".pytest_cache",
107+
".ruff_cache",
108+
"__pycache__",
109+
"build",
110+
"dist",
111+
"node_modules",
112+
"out",
113+
"target",
114+
]) {
115+
fs.mkdirSync(path.join(root, directory));
116+
fs.writeFileSync(path.join(root, directory, "ignored.txt"), "");
117+
}
118+
fs.mkdirSync(path.join(root, ".config"));
119+
fs.writeFileSync(path.join(root, ".config", "settings.json"), "");
120+
fs.mkdirSync(path.join(root, "src"));
121+
fs.writeFileSync(path.join(root, "src", "index.ts"), "");
122+
123+
assert.deepEqual(
124+
scanFileMentionItems(root).map((item) => item.path),
125+
[".config/", ".config/settings.json", "src/", "src/index.ts"]
126+
);
127+
} finally {
128+
fs.rmSync(root, { recursive: true, force: true });
129+
}
130+
});
131+
132+
test("scanFileMentionItems default max item cap is above 2000", () => {
133+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
134+
try {
135+
for (let index = 0; index < 2001; index++) {
136+
fs.writeFileSync(path.join(root, `file-${index.toString().padStart(4, "0")}.txt`), "");
137+
}
138+
139+
assert.equal(scanFileMentionItems(root).length, 2001);
140+
} finally {
141+
fs.rmSync(root, { recursive: true, force: true });
142+
}
143+
});
144+
101145
test("scanFileMentionItems respects project gitignore patterns inside git repositories", () => {
102146
const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-"));
103147
try {

src/tests/session.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from "fs";
55
import * as os from "os";
66
import * as path from "path";
77
import { GitFileHistory } from "../common/file-history";
8+
import { clearSessionState } from "../common/state";
89
import { type SessionMessage } from "../session";
910
import { SessionManager } from "../session";
1011

@@ -1257,6 +1258,75 @@ test("replySession /continue runs trailing pending tool calls before requesting
12571258
);
12581259
});
12591260

1261+
test("replySession rebuilds snippet state from persisted read history before editing", async () => {
1262+
const workspace = createTempDir("deepcode-rebuild-snippet-workspace-");
1263+
const home = createTempDir("deepcode-rebuild-snippet-home-");
1264+
setHomeDir(home);
1265+
1266+
const filePath = path.join(workspace, "note.txt");
1267+
fs.writeFileSync(filePath, "alpha\nbeta\n", "utf8");
1268+
1269+
const responses = [
1270+
createToolCallResponse(
1271+
[
1272+
{
1273+
id: "call-edit",
1274+
type: "function",
1275+
function: {
1276+
name: "edit",
1277+
arguments: JSON.stringify({
1278+
snippet_id: "full_file_5",
1279+
file_path: filePath,
1280+
old_string: "beta",
1281+
new_string: "gamma",
1282+
}),
1283+
},
1284+
},
1285+
],
1286+
{ prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
1287+
),
1288+
createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }),
1289+
];
1290+
const manager = createMockedClientSessionManager(workspace, responses);
1291+
const originalActivateSession = manager.activateSession.bind(manager);
1292+
(manager as any).activateSession = async () => {};
1293+
1294+
const sessionId = await manager.createSession({ text: "first prompt" });
1295+
const readToolMessage = (manager as any).buildToolMessage(
1296+
sessionId,
1297+
"call-read",
1298+
JSON.stringify({
1299+
ok: true,
1300+
name: "read",
1301+
output: " 1\talpha\n 2\tbeta\n",
1302+
metadata: {
1303+
snippet: {
1304+
id: "full_file_5",
1305+
filePath,
1306+
startLine: 1,
1307+
endLine: 3,
1308+
},
1309+
},
1310+
}),
1311+
{ name: "read", arguments: JSON.stringify({ file_path: filePath }) }
1312+
) as SessionMessage;
1313+
(manager as any).appendSessionMessage(sessionId, readToolMessage);
1314+
1315+
clearSessionState(sessionId);
1316+
(manager as any).activateSession = originalActivateSession;
1317+
1318+
await manager.replySession(sessionId, { text: "change beta" });
1319+
1320+
assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\ngamma\n");
1321+
const editToolMessage = manager.listSessionMessages(sessionId).find((message) => {
1322+
const params = message.messageParams as { tool_call_id?: string } | null;
1323+
return message.role === "tool" && params?.tool_call_id === "call-edit";
1324+
});
1325+
assert.ok(editToolMessage);
1326+
assert.match(editToolMessage.content ?? "", /"ok":true|"ok": true/);
1327+
assert.doesNotMatch(editToolMessage.content ?? "", /Unknown snippet_id/);
1328+
});
1329+
12601330
test("activateSession pauses for permission when a tool call requires ask", async () => {
12611331
const workspace = createTempDir("deepcode-permission-ask-workspace-");
12621332
const home = createTempDir("deepcode-permission-ask-home-");
@@ -2712,6 +2782,13 @@ function createChatResponse(content: string, usage: Record<string, unknown>): un
27122782
};
27132783
}
27142784

2785+
function createToolCallResponse(toolCalls: unknown[], usage: Record<string, unknown>): unknown {
2786+
return {
2787+
choices: [{ message: { content: "", tool_calls: toolCalls } }],
2788+
usage,
2789+
};
2790+
}
2791+
27152792
function buildTestMessage(
27162793
id: string,
27172794
sessionId: string,

0 commit comments

Comments
 (0)