Skip to content

Commit 81c2f07

Browse files
authored
Merge branch 'Zoo-Code-Org:main' into fix/shell-defaultProfileName-type-guard
2 parents ab8f32b + 0084cc8 commit 81c2f07

69 files changed

Lines changed: 1939 additions & 56 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Enhance the `apply_diff` tool description and parameter instructions to recommend `:start_line:` with exact syntax and emphasize copy-paste exact matching requirements, improving success rates for Gemini Flash and other smaller/faster models.

.github/workflows/e2e.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ jobs:
3434
- name: Install xvfb
3535
if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true'
3636
run: sudo apt-get install -y xvfb
37+
38+
- name: Get VS Code version from package.json
39+
if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true'
40+
id: vscode-ver
41+
run: |
42+
VERSION=$(node -p 'require("./apps/vscode-e2e/package.json").devDependencies["@types/vscode"]')
43+
echo "version=$VERSION" >> $GITHUB_OUTPUT
44+
45+
- name: Cache VS Code test binary
46+
if: github.event_name != 'pull_request' || steps.e2e-marker.outputs.cache-hit != 'true'
47+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
48+
with:
49+
path: |
50+
apps/vscode-e2e/.vscode-test/
51+
key: vscode-test-${{ runner.os }}-${{ steps.vscode-ver.outputs.version }}-v1
52+
3753
- name: Run mocked E2E tests
3854
id: run-e2e
3955
# merge_group and workflow_dispatch always run; cache skip is pull_request only

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ for this exact support, so if you are having problems or if you have question, j
8787
- [简体中文](locales/zh-CN/README.md)
8888
- [繁體中文](locales/zh-TW/README.md)
8989
- ...
90-
</details>
90+
</details>
9191

9292
---
9393

apps/vscode-e2e/src/runTest.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from "path"
22
import * as os from "os"
33
import * as fs from "fs/promises"
4+
import { readFileSync } from "fs"
45

56
import { runTests } from "@vscode/test-electron"
67
import { LLMock } from "@copilotkit/aimock"
@@ -156,12 +157,16 @@ async function main() {
156157
}
157158

158159
// Download VS Code, unzip it and run the integration test
160+
// Read VS Code version from package.json to keep in sync with @types/vscode
161+
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"))
162+
const vscodeVersion = process.env.VSCODE_VERSION || pkg.devDependencies["@types/vscode"]
163+
159164
await runTests({
160165
extensionDevelopmentPath,
161166
extensionTestsPath,
162167
launchArgs: [testWorkspace],
163168
extensionTestsEnv,
164-
version: process.env.VSCODE_VERSION || "1.100.0",
169+
version: vscodeVersion,
165170
})
166171
} catch (error) {
167172
console.error("Failed to run tests", error)

apps/vscode-e2e/src/suite/providers/zai.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ suite("Z.ai GLM provider", function () {
220220

221221
await waitUntilCompleted({ api, taskId })
222222
const capturedMaxTokens = requestCapture.maxTokens
223+
assert.ok(
224+
capturedMaxTokens !== undefined,
225+
"max_tokens should have been captured by the fetch interceptor before task completion",
226+
)
223227

224228
const completionMessage = messages.find(
225229
({ say, text }) => (say === "completion_result" || say === "text") && text?.trim() === "4",
@@ -229,8 +233,6 @@ suite("Z.ai GLM provider", function () {
229233

230234
// Verify max_tokens uses the restored default clamp (20% of context window)
231235
// unless the user explicitly overrides it via modelMaxTokens.
232-
// Snapshot immediately after waitUntilCompleted to avoid straggling async calls
233-
// from this task overwriting requestCapture before the assertion runs.
234236
assert.strictEqual(
235237
capturedMaxTokens,
236238
40_000,
@@ -264,6 +266,10 @@ suite("Z.ai GLM provider", function () {
264266

265267
await waitUntilCompleted({ api, taskId })
266268
const capturedMaxTokens = requestCapture.maxTokens
269+
assert.ok(
270+
capturedMaxTokens !== undefined,
271+
"max_tokens should have been captured by the fetch interceptor before task completion",
272+
)
267273

268274
const completionMessage = messages.find(
269275
({ say, text }) => (say === "completion_result" || say === "text") && text?.trim() === "4",
@@ -273,11 +279,10 @@ suite("Z.ai GLM provider", function () {
273279

274280
// Verify max_tokens uses the restored default clamp (20% of context window)
275281
// unless the user explicitly overrides it via modelMaxTokens.
276-
// Snapshot immediately after waitUntilCompleted to avoid straggling async calls
277-
// from the prior test overwriting requestCapture before this assertion runs.
282+
const expectedMaxTokens = 40_551 // Math.ceil(202_752 * 0.2) for glm-5-turbo
278283
assert.strictEqual(
279284
capturedMaxTokens,
280-
40_551,
285+
expectedMaxTokens,
281286
`max_tokens should default to the glm-5-turbo clamp (40_551) but was ${capturedMaxTokens}`,
282287
)
283288
})

packages/types/src/__tests__/message.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
// pnpm --filter @roo-code/types test src/__tests__/message.test.ts
22

3-
import { clineAsks, isIdleAsk, isInteractiveAsk, isResumableAsk, isNonBlockingAsk } from "../message.js"
3+
import {
4+
clineAsks,
5+
getCompletionCheckpoint,
6+
isIdleAsk,
7+
isInteractiveAsk,
8+
isResumableAsk,
9+
isNonBlockingAsk,
10+
type ClineMessage,
11+
} from "../message.js"
412

513
describe("ask messages", () => {
614
test("all ask messages are classified", () => {
@@ -12,3 +20,46 @@ describe("ask messages", () => {
1220
}
1321
})
1422
})
23+
24+
describe("getCompletionCheckpoint", () => {
25+
it("returns the first checkpoint after the latest user prompt before completion", () => {
26+
const messages: ClineMessage[] = [
27+
{ type: "say", say: "text", ts: 1, text: "Initial task" },
28+
{ type: "say", say: "checkpoint_saved", ts: 2, text: "initial-checkpoint" },
29+
{ type: "say", say: "completion_result", ts: 3, text: "First completion" },
30+
{ type: "say", say: "user_feedback", ts: 4, text: "Change it" },
31+
{ type: "say", say: "checkpoint_saved", ts: 5, text: "latest-prompt-checkpoint" },
32+
{ type: "say", say: "checkpoint_saved", ts: 6, text: "later-edit-checkpoint" },
33+
{ type: "ask", ask: "completion_result", ts: 7, text: "", partial: false },
34+
]
35+
36+
expect(getCompletionCheckpoint(messages)).toEqual({
37+
ts: 5,
38+
commitHash: "latest-prompt-checkpoint",
39+
})
40+
})
41+
42+
it("returns the first checkpoint after an initial task row before completion", () => {
43+
const messages: ClineMessage[] = [
44+
{ type: "say", say: "task", ts: 1, text: "Initial task" },
45+
{ type: "say", say: "checkpoint_saved", ts: 2, text: "checkpoint-after-initial-task" },
46+
{ type: "ask", ask: "completion_result", ts: 3, text: "Task complete", partial: false },
47+
]
48+
49+
expect(getCompletionCheckpoint(messages)).toEqual({
50+
ts: 2,
51+
commitHash: "checkpoint-after-initial-task",
52+
})
53+
})
54+
55+
it("returns undefined when completion has no checkpoint after the latest user prompt", () => {
56+
const messages: ClineMessage[] = [
57+
{ type: "say", say: "text", ts: 1, text: "Initial task" },
58+
{ type: "say", say: "checkpoint_saved", ts: 2, text: "initial-checkpoint" },
59+
{ type: "say", say: "user_feedback", ts: 3, text: "Change it" },
60+
{ type: "ask", ask: "completion_result", ts: 4, text: "", partial: false },
61+
]
62+
63+
expect(getCompletionCheckpoint(messages)).toBeUndefined()
64+
})
65+
})

packages/types/src/global-settings.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { z } from "zod"
22

3-
import { type Keys } from "./type-fu.js"
3+
import { codebaseIndexConfigSchema, codebaseIndexModelsSchema } from "./codebase-index.js"
4+
import { experimentsSchema } from "./experiment.js"
5+
import { historyItemSchema } from "./history.js"
6+
import { customModePromptsSchema, customSupportPromptsSchema, modeConfigSchema } from "./mode.js"
47
import {
58
type ProviderSettings,
69
PROVIDER_SETTINGS_KEYS,
710
providerSettingsEntrySchema,
811
providerSettingsSchema,
912
} from "./provider-settings.js"
10-
import { historyItemSchema } from "./history.js"
11-
import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js"
12-
import { experimentsSchema } from "./experiment.js"
1313
import { telemetrySettingsSchema } from "./telemetry.js"
14-
import { modeConfigSchema } from "./mode.js"
15-
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
1614
import { toolNamesSchema } from "./tool.js"
15+
import { type Keys } from "./type-fu.js"
1716
import { languagesSchema } from "./vscode.js"
1817

1918
/**
@@ -23,6 +22,16 @@ import { languagesSchema } from "./vscode.js"
2322
*/
2423
export const DEFAULT_WRITE_DELAY_MS = 1000
2524

25+
/**
26+
* Default fuzzy matching threshold for the multi-search-replace diff strategy.
27+
* A value of 1.0 (exact match) is used by default for safety, especially when
28+
* auto-approval for writes is enabled. This prevents unintended changes from
29+
* being applied due to minor mismatches. Users can lower this threshold manually
30+
* in settings to reduce "Edit Unsuccessful" errors caused by minor whitespace
31+
* or formatting differences, accepting a higher risk of unintended edits.
32+
*/
33+
export const DEFAULT_DIFF_FUZZY_THRESHOLD = 1.0
34+
2635
/**
2736
* Terminal output preview size options for persisted command output.
2837
*
@@ -102,6 +111,12 @@ export const globalSettingsSchema = z.object({
102111
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
103112
alwaysAllowWriteProtected: z.boolean().optional(),
104113
writeDelayMs: z.number().min(0).optional(),
114+
/**
115+
* Fuzzy matching threshold for the multi-search-replace diff strategy.
116+
* Range: 0.5 (50% minimum similarity) to 1.0 (exact match only).
117+
* `@default` 1.0
118+
*/
119+
diffFuzzyThreshold: z.number().min(0.5).max(1).optional(),
105120
requestDelaySeconds: z.number().optional(),
106121
alwaysAllowMcp: z.boolean().optional(),
107122
alwaysAllowModeSwitch: z.boolean().optional(),

packages/types/src/message.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export const clineSays = [
150150
"api_req_rate_limit_wait",
151151
"api_req_deleted",
152152
"text",
153+
"task",
153154
"image",
154155
"reasoning",
155156
"completion_result",
@@ -275,6 +276,76 @@ export const clineMessageSchema = z.object({
275276

276277
export type ClineMessage = z.infer<typeof clineMessageSchema>
277278

279+
export interface CompletionCheckpoint {
280+
ts: number
281+
commitHash: string
282+
}
283+
284+
const isInitialTaskMessage = (message: ClineMessage | undefined): boolean => {
285+
return message?.type === "say" && (message.say === "text" || message.say === "task")
286+
}
287+
288+
const isUserFeedbackMessage = (message: ClineMessage): boolean => {
289+
return message.type === "say" && message.say === "user_feedback"
290+
}
291+
292+
const isCompletionMessage = (message: ClineMessage): boolean => {
293+
return (
294+
(message.type === "ask" && message.ask === "completion_result") ||
295+
(message.type === "say" && message.say === "completion_result")
296+
)
297+
}
298+
299+
const isCheckpointMessage = (message: ClineMessage): boolean => {
300+
return message.type === "say" && message.say === "checkpoint_saved" && typeof message.text === "string"
301+
}
302+
303+
function findLastIndexBefore(
304+
messages: ClineMessage[],
305+
beforeIndex: number,
306+
predicate: (message: ClineMessage) => boolean,
307+
): number {
308+
for (let i = beforeIndex - 1; i >= 0; i--) {
309+
const message = messages[i]
310+
311+
if (message && predicate(message)) {
312+
return i
313+
}
314+
}
315+
316+
return -1
317+
}
318+
319+
/**
320+
* Finds the checkpoint that should anchor completion-result actions.
321+
*
322+
* The baseline is the first checkpoint created after the latest user prompt in
323+
* the turn that produced the completion. Restoring to that checkpoint reverts
324+
* changes made for the latest prompt, and diffing from it shows the same scoped
325+
* changes.
326+
*/
327+
export function getCompletionCheckpoint(messages: ClineMessage[]): CompletionCheckpoint | undefined {
328+
const completionIndex = findLastIndexBefore(messages, messages.length, isCompletionMessage)
329+
const searchEnd = completionIndex === -1 ? messages.length : completionIndex
330+
const latestUserFeedbackIndex = findLastIndexBefore(messages, searchEnd, isUserFeedbackMessage)
331+
const latestUserPromptIndex =
332+
latestUserFeedbackIndex !== -1 ? latestUserFeedbackIndex : isInitialTaskMessage(messages[0]) ? 0 : -1
333+
334+
if (latestUserPromptIndex === -1) {
335+
return undefined
336+
}
337+
338+
for (let i = latestUserPromptIndex + 1; i < searchEnd; i++) {
339+
const message = messages[i]
340+
341+
if (message && isCheckpointMessage(message)) {
342+
return { ts: message.ts, commitHash: message.text! }
343+
}
344+
}
345+
346+
return undefined
347+
}
348+
278349
/**
279350
* TokenUsage
280351
*/

packages/types/src/vscode-extension-host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export type ExtensionState = Pick<
332332
taskHistory: HistoryItem[]
333333

334334
writeDelayMs: number
335+
diffFuzzyThreshold: number
335336

336337
enableCheckpoints: boolean
337338
checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15)
@@ -517,6 +518,8 @@ export interface WebviewMessage {
517518
| "openCustomModesSettings"
518519
| "checkpointDiff"
519520
| "checkpointRestore"
521+
| "completionCheckpointDiff"
522+
| "completionCheckpointRestore"
520523
| "deleteMcpServer"
521524
| "codebaseIndexEnabled"
522525
| "telemetrySetting"

src/__tests__/single-open-invariant.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,46 @@ describe("Single-open-task invariant", () => {
7373
expect(addClineToStack).toHaveBeenCalledTimes(1)
7474
})
7575

76+
it("Subtask create: keeps existing task open when parentTask is provided", async () => {
77+
vi.spyOn(ProfileValidatorMod.ProfileValidator, "isProfileAllowed").mockReturnValue(true)
78+
79+
const removeClineFromStack = vi.fn().mockResolvedValue(undefined)
80+
const addClineToStack = vi.fn().mockResolvedValue(undefined)
81+
const parentTask = { taskId: "parent-1" }
82+
83+
const provider = {
84+
clineStack: [parentTask],
85+
setValues: vi.fn(),
86+
getState: vi.fn().mockResolvedValue({
87+
apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 0 },
88+
organizationAllowList: "*",
89+
enableCheckpoints: true,
90+
checkpointTimeout: 60,
91+
cloudUserInfo: null,
92+
}),
93+
removeClineFromStack,
94+
addClineToStack,
95+
setProviderProfile: vi.fn(),
96+
log: vi.fn(),
97+
getStateToPostToWebview: vi.fn(),
98+
providerSettingsManager: { getModeConfigId: vi.fn(), listConfig: vi.fn() },
99+
customModesManager: { getCustomModes: vi.fn().mockResolvedValue([]) },
100+
taskCreationCallback: vi.fn(),
101+
contextProxy: {
102+
extensionUri: {},
103+
setValue: vi.fn(),
104+
getValue: vi.fn(),
105+
setProviderSettings: vi.fn(),
106+
getProviderSettings: vi.fn(() => ({})),
107+
},
108+
} as unknown as ClineProvider
109+
110+
await (ClineProvider.prototype as any).createTask.call(provider, "Subtask", undefined, parentTask as any)
111+
112+
expect(removeClineFromStack).not.toHaveBeenCalled()
113+
expect(addClineToStack).toHaveBeenCalledTimes(1)
114+
})
115+
76116
it("History resume path always closes current before rehydration (non-rehydrating case)", async () => {
77117
const removeClineFromStack = vi.fn().mockResolvedValue(undefined)
78118
const addClineToStack = vi.fn().mockResolvedValue(undefined)

0 commit comments

Comments
 (0)