Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ tmp/
# code graph skills
.venv
.codegraph
.zvec-grep/
.qwen/computer-use/installed.json
# Auto-generated computer-use marker can also appear under nested packages.
**/.qwen/computer-use/
Expand Down
49 changes: 48 additions & 1 deletion packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
SessionIdConflictError,
type CliArgs,
} from './config.js';
import type { Settings } from './settings.js';
import { LoadedSettings, SettingScope, type Settings } from './settings.js';
import * as ServerConfig from '@qwen-code/qwen-code-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { resetMcpApprovalsForTesting } from './mcpApprovals.js';
Expand Down Expand Up @@ -2546,6 +2546,53 @@ describe('mergeExcludeTools', () => {
expect(config.getPermissionsDeny()).toContain('tool_search');
});

it('should leave zvec_grep disabled by default', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const settings: Settings = {};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.isZvecGrepEnabled()).toBe(false);
});

it('should enable zvec_grep when tools.zvecGrep.enabled is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const settings: Settings = {
tools: { zvecGrep: { enabled: true } },
};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.isZvecGrepEnabled()).toBe(true);
});

it('should persist the zvec_grep workspace opt-out', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
const setValue = vi
.spyOn(LoadedSettings.prototype, 'setValue')
.mockImplementation(() => {});
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const settings: Settings = {
tools: { zvecGrep: { enabled: true } },
};
try {
const config = await loadCliConfig(settings, argv, undefined, []);

expect(config.canDisableZvecGrepForWorkspace()).toBe(true);
await config.disableZvecGrepForWorkspace();

expect(setValue).toHaveBeenCalledWith(
SettingScope.Workspace,
'tools.zvecGrep.enabled',
false,
);
} finally {
setValue.mockRestore();
}
});

it('should auto-disable tool_search for deepseek-v4 models', async () => {
process.argv = ['node', 'script.js', '--model', 'deepseek-v4-flash'];
const argv = await parseArguments();
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,16 @@ export async function loadCliConfig(
disabledSlashCommands.length > 0 ? disabledSlashCommands : undefined,
disabledSkillNamesProvider:
bareMode || safeMode ? undefined : disabledSkillNamesProvider,
zvecGrepEnabled:
bareMode || safeMode ? false : settings.tools?.zvecGrep?.enabled === true,
Comment on lines +2113 to +2114

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test asserts that zvecGrepEnabled is forced to false when bareMode or safeMode is active. — Failure scenario: a future refactor could naively read settings.tools?.zvecGrep?.enabled without the guard, exposing the semantic search tool (which contacts an external embedding API) in restricted modes. The three existing CLI config tests all use normal mode, so the regression would go undetected. Consider adding a test with bareMode: true + tools.zvecGrep.enabled: true.

— qwen3.7-max via Qwen Code /review

onDisableZvecGrepForWorkspace: async () => {
const currentSettings = loadSettings(cwd);
Comment on lines +2115 to +2116

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Running from $HOME, 'Disable for this workspace' writes to the USER settings file: the workspace path <cwd>/.qwen/settings.json equals ~/.qwen/settings.json (getUserSettingsPath) when cwd is $HOME, setValue has no collision guard, and saveSettings deep-merges the key into that file. Probe with a real LoadedSettings under a fake $HOME confirmed: paths identical, workspaceSettingsActive false at $HOME (so the value can never be honored as workspace scope), and the merged value read back as disabled in another project. — Failure scenario: A user who enabled zvec in user settings, runs qwen from $HOME once, and declines it 'for this workspace' silently disables the tool in every project, every session — a workspace-scoped choice with global effect that can only misbehave.

Suggested fix: Offer/persist the workspace opt-out only when a distinct effective workspace scope exists (workspaceSettingsActive true and workspace path != user path); otherwise omit the 'Disable for this workspace' choice.

— qwen3.8-max via Qwen Code /review (v0.21.9)

currentSettings.setValue(
SettingScope.Workspace,
Comment on lines +2117 to +2118

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] In an UNTRUSTED folder the workspace opt-out write succeeds but is never read back: mergeSettings drops the entire workspace scope while untrusted (safeWorkspace = {}), while zvecGrepEnabled itself has no trust gate (user-scope enabled:true survives). Distinct from the refused-write finding — here the write succeeds and is silently ignored. — Failure scenario: With folder trust enabled and the folder untrusted, the user picks 'Do not install or index here. Always use regular search in this workspace' — nothing is persisted that takes effect, and the prompt returns every session: the choice's promise breaks exactly in the folder type where a user is most likely to pick it.

Suggested fix: Gate the DISABLE_WORKSPACE_CHOICE on the workspace scope actually being honored (trusted + active), or surface a notice that the opt-out cannot be persisted while the folder is untrusted.

— qwen3.8-max via Qwen Code /review (v0.21.9)

'tools.zvecGrep.enabled',
false,
);
},
terminalImageRenderSupportProvider: interactive
? async () => {
const { getTerminalImageRenderSupport } = await import(
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,27 @@ const SETTINGS_SCHEMA = {
},
},
},
zvecGrep: {
type: 'object',
label: 'Zvec Grep',
category: 'Tools',
requiresRestart: true,
default: {},
description: 'Settings for the zvec-grep built-in search tool.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Zvec Grep',
category: 'Tools',
requiresRestart: true,
default: false,
description:
'When enabled, registers the zvec_grep built-in tool. Disabled by default.',
Comment on lines +2539 to +2540

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The opt-in setting still does not disclose the material side effects the latest maintainer review required at the consent boundary; the identical minimal text ships in packages/vscode-ide-companion/schemas/settings.schema.json. The interactive setup prompt discloses them, but it fires only interactively, after the setting is enabled — and for headless users (see the headless finding) the setting is the ONLY consent point. — Failure scenario: A user enables tools.zvecGrep believing it only 'registers the tool'; first semantic use may then run a global npm install (@zvec/zvec-grep@0.1.5 — ~214 packages, ~172 MB in an isolated prefix) and start a detached indexer that sends workspace fragments to the Qwen/DashScope embedding service with possible API cost.

Suggested fix: Expand the description (both schemas) to name the global install, background full-workspace indexing, default remote embedding data flow and cost, the relevant credential env vars, and the ZVEC_GREP_EMBEDDING local-model alternative.

— qwen3.8-max via Qwen Code /review (v0.21.9)

showInDialog: false,
},
},
},
shell: {
type: 'object',
label: 'Shell',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export default {
'toolDisplayName.ReadFile': 'toolDisplayName.ReadFile',
'toolDisplayName.ZoomImage': 'toolDisplayName.ZoomImage',
'toolDisplayName.Grep': 'toolDisplayName.Grep',
'toolDisplayName.ZvecGrep': 'toolDisplayName.ZvecGrep',
'toolDisplayName.Glob': 'toolDisplayName.Glob',
'toolDisplayName.Shell': 'toolDisplayName.Shell',
'toolDisplayName.Shell Command': 'toolDisplayName.Shell Command',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export default {
'toolDisplayName.ReadFile': '讀取檔案',
'toolDisplayName.ZoomImage': '縮放圖像',
'toolDisplayName.Grep': 'Grep',
'toolDisplayName.ZvecGrep': '語義搜尋',
'toolDisplayName.Glob': 'Glob',
'toolDisplayName.Shell': '運行命令',
'toolDisplayName.Shell Command': 'Shell 命令',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export default {
'toolDisplayName.ReadFile': '读取文件',
'toolDisplayName.ZoomImage': '缩放图像',
'toolDisplayName.Grep': 'Grep',
'toolDisplayName.ZvecGrep': '语义搜索',
'toolDisplayName.Glob': 'Glob',
'toolDisplayName.Shell': '运行命令',
'toolDisplayName.Shell Command': 'Shell 命令',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,22 @@ describe('<AskUserQuestionDialog />', () => {
expect(lastFrame()).toContain('Type something...');
});

it('hides custom input when the question only supports fixed choices', () => {
const details = createConfirmationDetails({
questions: [createSingleQuestion({ allowCustomInput: false })],
});
const onConfirm = vi.fn();

const { lastFrame } = renderWithProviders(
<AskUserQuestionDialog
confirmationDetails={details}
onConfirm={onConfirm}
/>,
);
Comment on lines +182 to +187

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The new fixed-choice test renders AskUserQuestionDialog without the required availableWidth prop (required, no default, AskUserQuestionDialog.tsx:51; every sibling test passes availableWidth={80}). This is the second of the two build errors LaZzyMan named on 2026-08-05, still unfixed. — Failure scenario: CLI typecheck/build fails: tsc reproduced in this worktree reports AskUserQuestionDialog.test.tsx(183,10): error TS2741: Property 'availableWidth' is missing ... but required in type 'AskUserQuestionDialogProps'.

Suggested fix: Add availableWidth={80} to the rendered dialog in this test.

— qwen3.8-max via Qwen Code /review (v0.21.9)


expect(lastFrame()).not.toContain('Type something...');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The allowCustomInput:false behavior spans five component gates (totalOptions, isCustomInputSelected, isCustomInputAnswer, the multi-select Enter early-return, and the Box display), but the single new test asserts only the rendered frame; sibling tests in the same file already drive stdin keys. The only production consumer of false is the zvec-grep setup prompt, operated via keyboard. — Failure scenario: Reverting totalOptions to options.length + 1 changes no rendered pixels (the slot is display:none), so the test stays green while Down/number-key navigation can land on the hidden slot past the last fixed choice — and in multi-select, Enter there falls through to premature submission.

Suggested fix: Drive keys in the test: Down on the last option asserts no hidden entry is reached; Enter on a selected option asserts onConfirm fires with that option.

— qwen3.8-max via Qwen Code /review (v0.21.9)

});

it('renders help text for single select', () => {
const details = createConfirmationDetails();
const onConfirm = vi.fn();
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/ui/components/messages/AskUserQuestionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,17 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
? null
: confirmationDetails.questions[currentQuestionIndex];
const isMultiSelect = currentQuestion?.multiSelect ?? false;
const allowCustomInput = currentQuestion?.allowCustomInput !== false;
// Options + custom input ("Other")
const totalOptions = currentQuestion ? currentQuestion.options.length + 1 : 2;
const totalOptions = currentQuestion
Comment on lines +91 to +93

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new allowCustomInput:false contract is honored only by the CLI TUI dialog. Desktop (AskUserQuestionRequest.tsx renders an unconditional 'Other' textarea; answerForQuestion returns custom || selected[0]) and web-shell (AskUserQuestion.tsx: answers[i] || customInputs[i] || '') ignore the field — grep-verified zero references in either package. — Failure scenario: On desktop/web-shell the zvec setup question still shows the free-text box; a typed answer ('yes, install it') fails the fixed-label match in zvec's onConfirm → falls through to useNativeGrep = true — semantic search silently declined for the session despite affirmative consent.

Suggested fix: Propagate allowCustomInput through the desktop/web-shell permission-request types and hide the custom-input control when false; and/or treat an unrecognized answer as an explicit 'unknown choice' notice instead of the silent fall-through.

— qwen3.8-max via Qwen Code /review (v0.21.9)

? currentQuestion.options.length + (allowCustomInput ? 1 : 0)
: 2;

// Check if the custom input option is selected
const isCustomInputSelected =
!isSubmitTab &&
currentQuestion &&
allowCustomInput &&
selectedIndex === currentQuestion.options.length;

const getCustomInputValue = (idx: number) =>
Expand All @@ -103,6 +107,7 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
const isCustomInputAnswer =
!isSubmitTab &&
currentQuestion &&
allowCustomInput &&
!isMultiSelect &&
selectedOptions[currentQuestionIndex] !== undefined &&
!currentQuestion.options.some(
Expand Down Expand Up @@ -331,7 +336,10 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
// Handle multi-select: Enter advances to next question / submits
if (isMultiSelect && currentQuestion) {
// Custom input is handled by TextInput's onSubmit
if (selectedIndex === currentQuestion.options.length) {
if (
allowCustomInput &&
selectedIndex === currentQuestion.options.length
) {
return;
}
handleMultiSelectSubmit();
Expand Down Expand Up @@ -523,7 +531,10 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
})}

{/* Type something option/input */}
<Box flexDirection="column">
<Box
flexDirection="column"
display={allowCustomInput ? 'flex' : 'none'}
>
{isCustomInputSelected ? (
// Inline TextInput replaces the option text
<Box>
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8542,6 +8542,34 @@ describe('setApprovalMode with folder trust', () => {
vi.clearAllMocks();
});

it('should not register zvec-grep tool by default', async () => {
const config = new Config({ ...baseParams, useRipgrep: false });
await config.initialize();

const calls = (ToolRegistry.prototype.registerFactory as Mock).mock.calls;
const zvecGrepRegistrations = calls.filter(
(call) => call[0] === ToolNames.ZVEC_GREP,
);

expect(zvecGrepRegistrations.length).toBe(0);
});

it('should register zvec-grep tool when enabled', async () => {
const config = new Config({
...baseParams,
useRipgrep: false,
zvecGrepEnabled: true,
});
await config.initialize();

const calls = (ToolRegistry.prototype.registerFactory as Mock).mock.calls;
const zvecGrepRegistrations = calls.filter(
(call) => call[0] === ToolNames.ZVEC_GREP,
);

expect(zvecGrepRegistrations.length).toBe(1);
});

it('registers the background-agent roster tool', async () => {
const config = new Config(baseParams);
await config.initialize();
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,9 @@ export interface ConfigParameters {
* Names returned must be lower-cased; consumers compare case-insensitively.
*/
disabledSkillNamesProvider?: () => ReadonlySet<string>;
zvecGrepEnabled?: boolean;
/** Persists a workspace-scoped opt-out selected from zvec-grep setup. */
onDisableZvecGrepForWorkspace?: () => Promise<void>;
terminalImageRenderSupportProvider?: () => Promise<TerminalImageRenderSupport>;
/**
* Skill discovery levels that should not be loaded. Sourced from
Expand Down Expand Up @@ -1813,6 +1816,7 @@ export class Config {
private readonly disabledSkillNamesProvider:
| (() => ReadonlySet<string>)
| null;
private readonly zvecGrepEnabled: boolean;
private readonly terminalImageRenderSupportProvider:
| (() => Promise<TerminalImageRenderSupport>)
| null;
Expand Down Expand Up @@ -2022,6 +2026,7 @@ export class Config {
ruleType: 'allow' | 'ask' | 'deny',
rule: string,
) => Promise<void>;
private readonly onDisableZvecGrepForWorkspaceCallback?: () => Promise<void>;
private initialized: boolean = false;
private initializationPromise?: Promise<void>;
private initializationSucceeded = false;
Expand Down Expand Up @@ -2132,6 +2137,7 @@ export class Config {
...(params.disabledSlashCommands ?? []),
]);
this.disabledSkillNamesProvider = params.disabledSkillNamesProvider ?? null;
this.zvecGrepEnabled = params.zvecGrepEnabled ?? false;
this.terminalImageRenderSupportProvider =
params.terminalImageRenderSupportProvider ?? null;
this.disabledSkillLevels = new Set(params.disabledSkillLevels ?? []);
Expand Down Expand Up @@ -2303,6 +2309,8 @@ export class Config {
this.allowedHttpHookUrls = params.allowedHttpHookUrls ?? [];
this.allowPrivateNetworkHooks = params.allowPrivateNetworkHooks ?? false;
this.onPersistPermissionRuleCallback = params.onPersistPermissionRule;
this.onDisableZvecGrepForWorkspaceCallback =
params.onDisableZvecGrepForWorkspace;

// (web search removed)
this.useRipgrep = params.useRipgrep ?? true;
Expand Down Expand Up @@ -5143,6 +5151,18 @@ export class Config {
return this.disabledSkillNamesProvider?.() ?? EMPTY_DISABLED_SKILL_NAMES;
}

isZvecGrepEnabled(): boolean {
return this.zvecGrepEnabled;
}

canDisableZvecGrepForWorkspace(): boolean {
return this.onDisableZvecGrepForWorkspaceCallback !== undefined;
}

async disableZvecGrepForWorkspace(): Promise<void> {
await this.onDisableZvecGrepForWorkspaceCallback?.();
}

/**
* Returns skill discovery levels excluded through
* `settings.skills.disabledLevels`.
Expand Down Expand Up @@ -8009,6 +8029,13 @@ export class Config {
return new ZoomImageTool(this);
});

if (this.isZvecGrepEnabled()) {
await registerLazy(ToolNames.ZVEC_GREP, async () => {
const { ZvecGrepTool } = await import('../tools/zvec-grep.js');
return new ZvecGrepTool(this);
});
}

// --- Grep / RipGrep (conditional) ---
if (this.getUseRipgrep()) {
let useRipgrep = false;
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/core/coreToolScheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16806,6 +16806,19 @@ describe('extractToolFilePaths', () => {
).toEqual(['packages/core', 'packages/core/**/*.ts']);
});

it('extracts zvec_grep paths and globs as path-shaped file filters', () => {
expect(
extractToolFilePaths('zvec_grep', {
operation: 'rg',
query: 'validate',
path: 'packages/core',
paths: ['src', 'include'],
glob: '**/*.{h,cc}',
exclude: ['thirdparty/**'],
}),
).toEqual(['packages/core', 'src', 'include', 'packages/core/**/*.{h,cc}']);
});

it('decodes file:// URIs for lsp via fileURLToPath', () => {
// Regression: LSP `filePath` is allowed to be a `file://` URI.
// Forwarding the URI as-is to the activation registry would never
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/core/coreToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ const FS_PATH_TOOL_NAMES: ReadonlySet<string> = new Set<string>([
ToolNames.EDIT,
ToolNames.WRITE_FILE,
ToolNames.GREP,
ToolNames.ZVEC_GREP,
ToolNames.GLOB,
ToolNames.LS,
ToolNames.LSP,
Expand Down Expand Up @@ -714,6 +715,27 @@ export function extractToolFilePaths(
return out;
}

case ToolNames.ZVEC_GREP: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] PATTERN (3 occurrences): zvec_grep was wired into some tool-classification tables but misses three sibling registrations its same-shape siblings have — (1) PermissionManager.CORE_TOOLS, so registerLazy's isToolEnabled skips the allowlist branch and operators pinning the surface with --core-tools/tools.core still get zvec_grep registered (the opt-in precedent web_search IS in CORE_TOOLS; zvec fits neither documented exemption; image_gen is also absent, so deliberate omission is possible); (2) PLAN_REQUIRED_TEAMMATE_PRE_APPROVAL_TOOLS, so a plan-required teammate awaiting approval is blocked from zvec_grep while identical-shape grep_search is allowed (fails closed, but defeats the gate's read-only-investigation purpose); (3) microcompaction's COMPACTABLE_TOOLS, so zvec_grep match listings persist in context forever while equivalent grep_search/glob outputs are evicted in long sessions. — Failure scenario: An operator's explicit tool allowlist still registers a tool that spawns background indexers and makes network embedding calls; plan-mode teammates lose one read-only investigation tool; long semantic-heavy sessions compact earlier than grep-equivalent ones.

Suggested fix: Add zvec_grep to CORE_TOOLS (or document the exemption), to PLAN_REQUIRED_TEAMMATE_PRE_APPROVAL_TOOLS (read-only; install path has its own gate), and to COMPACTABLE_TOOLS (same output contract as GREP).

— qwen3.8-max via Qwen Code /review (v0.21.9)

const pathField = obj['path'];
const pathsField = obj['paths'];
const globField = obj['glob'];
push(pathField);
if (Array.isArray(pathsField)) {
for (const item of pathsField) {
push(item);
Comment on lines +723 to +725

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] extractToolFilePaths' ZVEC_GREP case joins glob only with the singular path field, never with paths entries — LaZzyMan's P2 from the latest review, only partially addressed; the added test entrenches the gap. The tool itself searches the intersection of paths and glob. — Failure scenario: For zvec_grep({paths:['src/components'], glob:'.tsx'}) the scheduler emits ['src/components', '.tsx'] instead of 'src/components/**/*.tsx' — path-gated skill/conditional-rule activation can be skipped for searches that literally touched those files (bare directory candidates still fire broad rules, so impact is bounded).

Suggested fix: Also push joinSearchRootAndGlob(item, globField) for each string in paths, and extend the focused test to the paths-only shape.

— qwen3.8-max via Qwen Code /review (v0.21.9)

}
}
if (typeof globField === 'string' && globField.length > 0) {
push(
joinSearchRootAndGlob(
typeof pathField === 'string' ? pathField : undefined,
globField,
),
);
}
Comment on lines +728 to +735

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] extractToolFilePaths joins glob with the singular path field but not with items from the paths array — Concrete cost: a call like zvec_grep({ paths: ['src/components'], glob: '*.tsx' }) searches src/components/**/*.tsx, but the path extraction only emits src/components and *.tsx as separate entries. Path-gated skills scoped to src/components/**/*.tsx will not activate.

Suggested change
if (typeof globField === 'string' && globField.length > 0) {
push(
joinSearchRootAndGlob(
typeof pathField === 'string' ? pathField : undefined,
globField,
),
);
}
if (typeof globField === 'string' && globField.length > 0) {
push(
joinSearchRootAndGlob(
typeof pathField === 'string' ? pathField : undefined,
globField,
),
);
if (Array.isArray(pathsField)) {
for (const item of pathsField) {
if (typeof item === 'string' && item.length > 0) {
push(joinSearchRootAndGlob(item, globField));
}
}
}
}

— qwen3.7-max via Qwen Code /review

return out;
}

case ToolNames.LS:
push(obj['path']);
return out;
Expand Down
Loading
Loading