Skip to content

Commit e55203e

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr-697
# Conflicts: # README.md # webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts
2 parents becbd04 + 8d4ed32 commit e55203e

231 files changed

Lines changed: 13223 additions & 3829 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Fix LiteLLM provider cache key collision, credential priority, and model-selection fallback to non-existent default.
6+
7+
Two bugs are addressed:
8+
9+
1. **Cache key collision**: All URL-scoped providers (LiteLLM, Ollama, LM Studio, Poe, DeepSeek,
10+
Requesty) previously shared one cache entry keyed only on the provider name. Switching between
11+
profiles backed by different servers silently served the wrong model list and the stale list
12+
persisted across VS Code restarts via the disk cache. Fixed with a compound cache key:
13+
URL-scoped providers use `provider:baseUrl`; key-scoped providers (LiteLLM, Poe, Requesty)
14+
additionally include a short, irreversible discriminator derived from the API key
15+
(`provider:baseUrl:<discriminator>`) so that two different API keys on the same server never share
16+
a cache entry (relevant when the server enforces per-key model allowlists). Both the discriminator
17+
and the on-disk filename digest are derived via truncated PBKDF2 so neither can be reversed to
18+
identify the API key written to the cache filename. The `RouterProvider.getModel()` cold-start
19+
fallback is also corrected to pass the full options so it resolves the same compound key.
20+
21+
2. **Silent fallback to hardcoded default**: When the LiteLLM model list was empty (due to the
22+
collision above, a failed sync, or a transient error), `useSelectedModel` reset the configured
23+
model ID to `claude-3-7-sonnet-20250219` -- a model that typically does not exist on user
24+
LiteLLM servers. Four sub-fixes: preserve the configured model ID when the list is empty;
25+
invalidate the React Query router-models cache after a successful "Sync Models" click; pass the
26+
current LiteLLM credentials in the debounced `requestRouterModels` message; and correct the
27+
credential priority in `webviewMessageHandler.ts` so that message values (current unsaved field
28+
state) take precedence over stale saved config, matching the pattern already used for DeepSeek.
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/label-pr-review-state.yml

Lines changed: 169 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ name: Label PR review state
22

33
on:
44
schedule:
5-
- cron: '0 * * * *' # hourly
5+
- cron: '0 * * * *' # hourly fallback
66
workflow_dispatch:
7+
pull_request:
8+
types: [opened, reopened, ready_for_review, synchronize, review_requested]
9+
pull_request_review:
10+
types: [submitted, dismissed]
711

812
permissions:
913
pull-requests: write
14+
checks: read
1015

1116
concurrency:
1217
group: label-pr-review-state
@@ -22,70 +27,193 @@ jobs:
2227
script: |
2328
const { owner, repo } = context.repo;
2429
const stateLabels = ['awaiting-author', 'awaiting-review'];
25-
const failures = [];
2630
27-
const prs = await github.paginate(github.rest.pulls.list, {
28-
owner, repo, state: 'open', per_page: 100,
29-
});
31+
// When triggered by a single PR event, only reconcile that PR.
32+
// The hourly schedule and workflow_dispatch reconcile all open PRs.
33+
let prs;
34+
const prNumber = context.payload.pull_request?.number;
35+
if (prNumber) {
36+
const { data: pr } = await github.rest.pulls.get({
37+
owner, repo, pull_number: prNumber,
38+
});
39+
prs = [pr];
40+
} else {
41+
prs = await github.paginate(github.rest.pulls.list, {
42+
owner, repo, state: 'open', per_page: 100,
43+
});
44+
}
45+
46+
// Strips stateLabels from a PR, optionally keeping one.
47+
// Also removes stale-awaiting-author when not keeping awaiting-author.
48+
async function reconcileLabels(pr, desiredLabel) {
49+
const currentLabels = new Set(pr.labels.map(l => l.name));
50+
for (const label of stateLabels) {
51+
if (label !== desiredLabel && currentLabels.has(label)) {
52+
try {
53+
await github.rest.issues.removeLabel({
54+
owner, repo, issue_number: pr.number, name: label,
55+
});
56+
} catch (err) {
57+
if (err.status !== 404) throw err; // 404 = already gone, benign
58+
}
59+
}
60+
}
61+
if (desiredLabel && !currentLabels.has(desiredLabel)) {
62+
await github.rest.issues.addLabels({
63+
owner, repo, issue_number: pr.number, labels: [desiredLabel],
64+
});
65+
}
66+
if (desiredLabel !== 'awaiting-author' && currentLabels.has('stale-awaiting-author')) {
67+
try {
68+
await github.rest.issues.removeLabel({
69+
owner, repo, issue_number: pr.number, name: 'stale-awaiting-author',
70+
});
71+
} catch (err) {
72+
if (err.status !== 404) throw err;
73+
}
74+
}
75+
}
76+
77+
// Fetch required status check names from the branch ruleset.
78+
// Uses the public /rules/branches endpoint — no admin token needed.
79+
// Falls back to blocking on all checks if the endpoint is unavailable.
80+
let requiredCheckNames = null;
81+
try {
82+
const { data: rules } = await github.request(
83+
'GET /repos/{owner}/{repo}/rules/branches/{branch}',
84+
{ owner, repo, branch: 'main' },
85+
);
86+
const statusRule = rules.find(r => r.type === 'required_status_checks');
87+
if (statusRule) {
88+
requiredCheckNames = new Set(
89+
statusRule.parameters.required_status_checks.map(c => c.context),
90+
);
91+
core.info(`Required checks: ${[...requiredCheckNames].join(', ')}`);
92+
}
93+
} catch (err) {
94+
core.warning(`Could not fetch branch rules, falling back to all checks: ${err.message}`);
95+
}
96+
97+
const failures = [];
3098
3199
for (const pr of prs) {
32100
try {
101+
// Draft PRs never get a state label.
102+
if (pr.draft) {
103+
core.info(`PR #${pr.number}: draft — stripping state labels`);
104+
await reconcileLabels(pr, null);
105+
continue;
106+
}
107+
108+
// Check CI status for required checks on the PR's head commit only.
109+
// Scoping to required checks avoids advisory checks (e.g. codecov/patch)
110+
// incorrectly blocking label assignment on otherwise-ready PRs.
111+
const [checkRuns, commitStatusRes] = await Promise.all([
112+
github.paginate(github.rest.checks.listForRef, {
113+
owner, repo, ref: pr.head.sha, per_page: 100,
114+
}),
115+
github.rest.repos.getCombinedStatusForRef({
116+
owner, repo, ref: pr.head.sha,
117+
}),
118+
]);
119+
120+
// Filter to required checks only (or all checks if rules unavailable).
121+
// Always exclude this workflow's own run to avoid self-referential loops.
122+
const relevantRuns = checkRuns.filter(run => {
123+
if (run.name === 'Reconcile PR review state labels') return false;
124+
return requiredCheckNames ? requiredCheckNames.has(run.name) : true;
125+
});
126+
127+
// For commit statuses (external CIs), there's no per-status name filtering
128+
// available from getCombinedStatusForRef — it aggregates all statuses.
129+
// If required checks are known, we only use commitStatus as a signal when
130+
// no required check runs exist for this ref (i.e. pure status-based CI).
131+
const useCommitStatus = !requiredCheckNames || relevantRuns.length === 0;
132+
133+
core.debug(`PR #${pr.number}: ${relevantRuns.length} required check run(s), commit status=${commitStatusRes.data.state} (used=${useCommitStatus})`);
134+
for (const run of relevantRuns) {
135+
core.debug(` check: "${run.name}" status=${run.status} conclusion=${run.conclusion}`);
136+
}
137+
138+
const ciPending = relevantRuns.some(
139+
run => run.status === 'queued' || run.status === 'in_progress',
140+
) || (useCommitStatus && commitStatusRes.data.state === 'pending');
141+
142+
const ciFailed = !ciPending && (
143+
relevantRuns.some(
144+
run => run.status === 'completed' &&
145+
run.conclusion !== 'success' &&
146+
run.conclusion !== 'skipped' &&
147+
run.conclusion !== 'neutral',
148+
) || (useCommitStatus && (
149+
commitStatusRes.data.state === 'failure' ||
150+
commitStatusRes.data.state === 'error'
151+
))
152+
);
153+
154+
// While CI is running or has failed, remove state labels and move on.
155+
// CI failure is its own signal; the label would add noise, not clarity.
156+
if (ciPending || ciFailed) {
157+
core.info(`PR #${pr.number}: CI ${ciPending ? 'pending' : 'failed'} — stripping state labels`);
158+
await reconcileLabels(pr, null);
159+
continue;
160+
}
161+
162+
// CI is passing. Now determine review state.
33163
const reviews = await github.paginate(github.rest.pulls.listReviews, {
34164
owner, repo, pull_number: pr.number, per_page: 100,
35165
});
36166
37-
// Reviews are returned chronologically, so later entries replace
38-
// each reviewer's earlier decision.
167+
// Reduce to each reviewer's latest meaningful state.
168+
// Reviews are returned oldest-first, so last-write-wins yields the latest state.
169+
// COMMENTED and DISMISSED are treated as neutral — they do not
170+
// block the PR or indicate the author needs to act.
39171
const latest = new Map();
40172
for (const r of reviews) {
41-
if (r.state !== 'COMMENTED') {
173+
if (r.state !== 'COMMENTED' && r.state !== 'DISMISSED') {
42174
latest.set(r.user.login, r);
43175
}
44176
}
45177
46-
const changeRequestReviewers = [...latest.entries()]
47-
.filter(([, review]) => review.state === 'CHANGES_REQUESTED')
48-
.map(([login]) => login);
49178
const requestedReviewers = new Set(
50-
pr.requested_reviewers.map(reviewer => reviewer.login),
179+
pr.requested_reviewers.map(r => r.login),
51180
);
52181
53-
let desiredLabel = null;
54-
if (changeRequestReviewers.length > 0) {
55-
desiredLabel = changeRequestReviewers.every(
56-
reviewer => requestedReviewers.has(reviewer),
57-
)
182+
const changeRequesters = [...latest.entries()]
183+
.filter(([, r]) => r.state === 'CHANGES_REQUESTED')
184+
.map(([login]) => login);
185+
186+
let desiredLabel;
187+
if (changeRequesters.length > 0) {
188+
// If every change-requester has been re-requested for review,
189+
// the author has addressed feedback and re-opened it for review.
190+
desiredLabel = changeRequesters.every(login => requestedReviewers.has(login))
58191
? 'awaiting-review'
59192
: 'awaiting-author';
60-
}
193+
} else {
194+
// No outstanding change requests: awaiting first review, or all approved.
195+
// awaiting-review if: there are pending requested reviewers, or nobody
196+
// has given a meaningful review yet. null (approved) if everyone approved.
197+
const allApproved = latest.size > 0 &&
198+
[...latest.values()].every(r => r.state === 'APPROVED') &&
199+
requestedReviewers.size === 0;
61200
62-
const currentLabels = new Set(pr.labels.map(label => label.name));
63-
for (const label of stateLabels) {
64-
if (label !== desiredLabel && currentLabels.has(label)) {
65-
await github.rest.issues.removeLabel({
66-
owner, repo, issue_number: pr.number, name: label,
67-
});
68-
}
201+
desiredLabel = allApproved ? null : 'awaiting-review';
69202
}
70203
71-
if (desiredLabel && !currentLabels.has(desiredLabel)) {
72-
await github.rest.issues.addLabels({
73-
owner, repo, issue_number: pr.number, labels: [desiredLabel],
74-
});
75-
}
204+
core.info(
205+
`PR #${pr.number}: CI passing, reviews=${latest.size}, ` +
206+
`changeRequesters=[${changeRequesters.join(',')}], ` +
207+
`requestedReviewers=[${[...requestedReviewers].join(',')}] → ${desiredLabel ?? '(none)'}`
208+
);
76209
77-
if (
78-
desiredLabel !== 'awaiting-author' &&
79-
currentLabels.has('stale-awaiting-author')
80-
) {
81-
await github.rest.issues.removeLabel({
82-
owner, repo, issue_number: pr.number,
83-
name: 'stale-awaiting-author',
84-
});
85-
}
210+
await reconcileLabels(pr, desiredLabel);
86211
} catch (error) {
87-
failures.push(`#${pr.number}: ${error.message}`);
88-
core.error(`Failed to reconcile PR #${pr.number}: ${error.message}`);
212+
const detail = error.status
213+
? `${error.message} (HTTP ${error.status}${error.response?.data?.message ? `: ${error.response.data.message}` : ''})`
214+
: error.message;
215+
failures.push(`#${pr.number}: ${detail}`);
216+
core.error(`Failed to reconcile PR #${pr.number}: ${detail}`);
89217
}
90218
}
91219

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Zoo Code Changelog
22

3+
## [3.64.0]
4+
5+
### Minor Changes
6+
7+
- Add Rules Management UI — new Rules tab in Settings to create, delete, and open global and workspace Zoo rules (#660 by @ivanarifin, PR #657 by @ivanarifin)
8+
- Add completion change review actions — "See New Changes" and "Restore Changes" buttons after task completion let you inspect and undo changes from the latest prompt (#661 by @ivanarifin, PR #633 by @ivanarifin)
9+
- Add kimi-k2p7-code model on Fireworks provider (PR #599 by @p12tic)
10+
- feat: add abort signal core plumbing — threads AbortSignal through the API metadata layer for future provider-level cancellation (#434 by @easonLiangWorldedtech, PR #674 by @easonLiangWorldedtech)
11+
- feat: add TaskSemaphore utility for parallel task coordination (#362 by @edelauna, PR #675 by @edelauna)
12+
- feat(experiments): register PARALLEL_TOOL_EXECUTION feature flag (internal-only) (#363 by @edelauna, PR #678 by @edelauna)
13+
- Add Roo Code history import to the About page (PR #141 by @roomote)
14+
- Fix: configurable relaxed diff thresholds and diagnostics reduce "edit unsuccessful" errors (#452 by @DannyVarodBlueVine, PR #470 by @nigeldelviero)
15+
- Fix: auto-closing edited files is now opt-in — the setting defaults to off (#719 by @edelauna, PR #720 by @edelauna)
16+
- Fix(diff-view): make auto-closing edited files opt-in, fixing setting that could not be unchecked (#667 by @navedmerchant, PR #668 by @navedmerchant)
17+
- Fix(delegation): serialize delegateParentAndOpenChild with atomicReadAndUpdate to prevent race conditions (#364 by @edelauna, #365 by @edelauna, PR #691 by @edelauna)
18+
- Fix(ask_followup_question): report non-array follow_up suggestions as a type error (#511 by @nh2, PR #662 by @nh2)
19+
- Fix: parse Gemma 4 `<thought>` reasoning tags alongside `<think>` (#323 by @sagidM, PR #324 by @sagidM)
20+
- docs(prompt): enhance apply_diff tool instructions to improve Gemini model success rate (#611 by @awschmeder, PR #619 by @awschmeder)
21+
- chore(deps): update undici to v6.27.0 [security] (PR #659 by @renovate)
22+
- chore(deps): update @types/node, @vscode/test-cli, execa, axios (PR #669, #670, #671, #673 by @renovate)
23+
- test(mcp): fix McpHub Windows command wrapping test ordering (PR #632 by @HappyLiang12)
24+
- fix(McpHub): resolve flaky McpHub.spec.ts tests after Vitest 4 upgrade (PR #666 by @edelauna)
25+
326
## [3.62.0]
427

528
### Minor Changes

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,16 @@
5353
You can find a quick guide for migrating from Roo Code to Zoo Code in the [Roo→Zoo migration guide](https://docs.zoocode.dev/roo-to-zoo-migration). We plan to try and help users as they transition over, we have our [Reddit](https://www.reddit.com/r/ZooCode) and [Discord](https://discord.gg/VxfP4Vx3gX)
5454
for this exact support, so if you are having problems or if you have question, jump on and ask.
5555

56-
## What's New in v3.62.0
57-
58-
- **GLM-5.2 support** — the latest GLM model is now available in your provider settings
59-
- **OpenCode-Go improvements** — native model parameters, Anthropic-format routing, and a context-token fix for more reliable responses
60-
- **Tool-writer mode** — a new specialized mode for writing and maintaining tool definitions, now available in the Marketplace
61-
- **LiteLLM session header** — forward taskId as X-Zoo-Session-ID request header for better request tracing
62-
- Fix apiRequestTimeout applied consistently across all providers
63-
- Fix diff view scroll position and tab handling on save/deny
64-
- Fix terminal completion signal delivery when end event wins the race
65-
- Refactor RateLimitClock out of Task static state for cleaner rate-limit handling
66-
- Security updates: vitest v4, shell-quote v1.8.4, esbuild v0.28.1, vite v8.0.16
56+
## What's New in v3.64.0
57+
58+
- **Rules Management UI** — a new Rules tab in Settings lets you create, delete, and open global and workspace Zoo rules directly from the editor
59+
- **Completion Change Review** — after a task completes, new "See New Changes" and "Restore Changes" buttons let you inspect and undo the changes from the latest prompt
60+
- **Relaxed Diff Thresholds** — configurable similarity thresholds reduce "edit unsuccessful" errors when applying diffs, with new diagnostics to help tune the settings
61+
- Add kimi-k2p7-code model on Fireworks provider
62+
- Fix: auto-closing edited files is now opt-in and defaults to off
63+
- Fix: delegation race condition in delegateParentAndOpenChild
64+
- Fix: parse Gemma 4 `<thought>` reasoning tags alongside `<think>`
65+
- Security update: undici v6.27.0
6766

6867
<details>
6968
<summary>🌐 Available languages</summary>

apps/cli/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"@trpc/client": "^11.8.1",
3030
"@vscode/ripgrep": "^1.15.9",
3131
"commander": "^12.1.0",
32-
"cross-spawn": "^7.0.6",
3332
"execa": "^9.5.2",
3433
"fuzzysort": "^3.1.0",
3534
"ink": "^6.6.0",
@@ -43,11 +42,11 @@
4342
"@roo-code/config-typescript": "workspace:^",
4443
"@types/node": "20.19.43",
4544
"@types/react": "18.3.23",
46-
"@vitest/coverage-v8": "4.1.0",
45+
"@vitest/coverage-v8": "4.1.9",
4746
"ink-testing-library": "4.0.0",
4847
"rimraf": "6.0.1",
4948
"tsup": "8.5.0",
50-
"tsx": "4.19.4",
51-
"vitest": "4.1.0"
49+
"tsx": "4.22.4",
50+
"vitest": "4.1.9"
5251
}
5352
}

apps/vscode-e2e/fixtures/openrouter.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
{
22
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "openrouter-image-e2e"
6+
},
7+
"response": {
8+
"toolCalls": [
9+
{
10+
"name": "attempt_completion",
11+
"arguments": "{\"result\":\"Red\"}",
12+
"id": "call_openrouter_image_001"
13+
}
14+
]
15+
}
16+
},
317
{
418
"match": {
519
"userMessage": "openrouter-identity-smoke"

0 commit comments

Comments
 (0)