From f9cb764fbc00d521e60427feb4c2858caf6840d3 Mon Sep 17 00:00:00 2001 From: davi0015 Date: Wed, 22 Apr 2026 21:59:56 +0800 Subject: [PATCH] add workspace scope tool usage for edit / delete file (#12) --- .cursorignore | 69 +++++++++++++++++++ .../contrib/void/browser/chatThreadService.ts | 24 ++++++- .../void/browser/react/src/util/inputs.tsx | 63 +++++++++++++++++ .../react/src/void-settings-tsx/Settings.tsx | 50 +++++++++++--- .../contrib/void/common/toolsServiceTypes.ts | 39 ++++++++++- .../contrib/void/common/voidSettingsTypes.ts | 9 ++- 6 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 .cursorignore diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..e6ded568 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,69 @@ +# .cursorignore — paths excluded from Cursor's indexing / @-mentions / semantic search. +# Keep this list tight: anything here won't be searchable or available for AI context. +# Rule of thumb: exclude large generated/vendored content, keep source + real tests. + +# -------- Dependencies (~1.4 GB) -------- +node_modules/ +build/node_modules/ +remote/node_modules/ + +# -------- Build outputs -------- +# Top-level VSCode build artifacts (/out*/ pattern from .gitignore) +out/ +out-build/ +out-vscode/ +out-vscode-min/ +out-vscode-web/ +out-vscode-web-min/ +.build/ + +# Void scratch / dev-mode user data +.tmp/ +.tmp2/ +.vscode-test/ + +# Coverage / test outputs (sources stay indexable) +coverage/ +test-results/ +test-results.xml +test/smoke/out/ +test/unit/out/ +test/integration/out/ +test_data/ +.profile-oss + +# -------- Void React bundle (generated) -------- +# `react/src2/` is scope-tailwind's generated copy of `react/src/`. +# `react/out/` is the esbuild bundle. Indexing src/ (the real source) is enough. +src/vs/workbench/contrib/void/browser/react/src2/ +src/vs/workbench/contrib/void/browser/react/out/ + +# -------- Extension build outputs -------- +.vscode/extensions/**/out/ +extensions/**/out/ +extensions/**/dist/ +dist/ + +# -------- CLI / native builds -------- +cli/target/ +cli/openssl/ + +# -------- Generated / minified (noise in search) -------- +*.min.js +*.map +*.tsbuildinfo +*.snap.actual +vscode.lsif +vscode.db +product.overrides.json + +# -------- OS / editor junk -------- +.DS_Store +Thumbs.db +npm-debug.log +*.log +.cache + +# -------- Local notes (optional: keep indexed if you want Cursor to see them) -------- +# Uncomment to hide from Cursor search: +# mynote.md diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index b2b8703d..099a276c 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -16,7 +16,7 @@ import { AnthropicReasoning, getErrorMessage, type LLMUsage, RawToolCallObj, Raw import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; +import { approvalIsWorkspaceScoped, approvalTypeOfBuiltinToolName, BuiltinToolCallParams, normalizeAutoApproveMode, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -862,7 +862,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools' if (approvalType) { - const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType] + const mode = normalizeAutoApproveMode(this._settingsService.state.globalSettings.autoApprove[approvalType]) + // Tri-state resolution: + // 'off' → always prompt + // 'all' → skip prompt + // 'workspace' → skip prompt iff target URI is inside an open workspace folder. + // For non-workspace-scoped tiers ('terminal', 'MCP tools'), + // 'workspace' is semantically equivalent to 'all' — commands/MCPs + // don't have a single target URI to scope against and can + // legitimately operate outside the workspace. + let autoApprove = false + if (mode === 'all') { + autoApprove = true + } else if (mode === 'workspace') { + if (approvalIsWorkspaceScoped(approvalType) && isBuiltInTool) { + const targetUri = (toolParams as { uri?: URI } | undefined)?.uri + autoApprove = !!targetUri && this._workspaceContextService.isInsideWorkspace(targetUri) + } else { + autoApprove = true + } + } + // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, rawParamsStr, mcpServerName }) if (!autoApprove) { diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 49746d46..6725cfa6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -1217,6 +1217,69 @@ export const VoidSwitch = ({ +// N-way segmented control used for tri-state (or bi-state) settings like the tool auto-approve +// tier mode (off / workspace / all). Renders as a row of individually-rounded pill buttons with +// visible gaps between them, so each option reads as a distinct clickable target. The active pill +// fills with a high-contrast color (matching VoidSwitch's active state) and the inactive pills +// have a subtle bordered look so they're visible against the panel background. +// +// Uses explicit zinc-* Tailwind colors rather than theme variables for the active fill, because +// `void-*` color variables can be subtle in some themes and we want consistent high contrast. +export const VoidSegmentedControl = ({ + value, + onChange, + options, + size = 'xs', + disabled = false, +}: { + value: T; + onChange: (value: T) => void; + options: { value: T; label: string; title?: string }[]; + size?: 'xxs' | 'xs' | 'sm'; + disabled?: boolean; +}) => { + // Bigger padding + slightly larger font than VoidSwitch because labels are text, not a dot. + const btnPad = + size === 'xxs' ? 'px-2 py-0.5 text-xs' : + size === 'xs' ? 'px-3 py-1 text-sm' : + 'px-3.5 py-1.5 text-sm' + + return ( +
+ {options.map((opt) => { + const active = opt.value === value + return ( + + ) + })} +
+ ) +} + + export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => { const divRef = useRef(null) const instanceRef = useRef(null) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 093d3552..7123d117 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState, useRef } from 'react'; import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' -import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' +import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSegmentedControl, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useIsOptedOut, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' import { X, RefreshCw, Loader2, Check, Asterisk, Plus, GripVertical } from 'lucide-react' import { URI } from '../../../../../../../base/common/uri.js' @@ -15,7 +15,7 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { WarningBox } from './WarningBox.js' import { os } from '../../../../common/helpers/systemInfo.js' import { IconLoading } from '../sidebar-tsx/SidebarChat.js' -import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js' +import { AutoApproveMode, approvalIsWorkspaceScoped, normalizeAutoApproveMode, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js' import Severity from '../../../../../../../base/common/severity.js' import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../../../common/modelCapabilities.js'; import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js'; @@ -1107,25 +1107,55 @@ const RedoOnboardingButton = ({ className }: { className?: string }) => { +// Renders the auto-approve control for a given tool tier. For workspace-scoped tiers +// ('edits', 'delete') this is a 3-way segmented control (Off / Workspace / Everywhere). For +// unscoped tiers ('terminal', 'MCP tools'), where workspace scoping has no meaning, we keep the +// original compact on/off `VoidSwitch` — the extra radio options would just be UI noise. Under +// the hood the tri-state storage is preserved so the check in `chatThreadService` can be uniform: +// the switch maps Off → 'off' and On → 'all'. export const ToolApprovalTypeSwitch = ({ approvalType, size, desc }: { approvalType: ToolApprovalType, size: "xxs" | "xs" | "sm" | "sm+" | "md", desc: string }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') const voidSettingsState = useSettingsState() const metricsService = accessor.get('IMetricsService') - const onToggleAutoApprove = useCallback((approvalType: ToolApprovalType, newValue: boolean) => { + const writeMode = useCallback((newMode: AutoApproveMode) => { voidSettingsService.setGlobalSetting('autoApprove', { ...voidSettingsService.state.globalSettings.autoApprove, - [approvalType]: newValue + [approvalType]: newMode, }) - metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue }) - }, [voidSettingsService, metricsService]) + metricsService.capture('Tool Auto-Accept Toggle', { enabled: newMode !== 'off', mode: newMode, tier: approvalType }) + }, [voidSettingsService, metricsService, approvalType]) + + const currentMode = normalizeAutoApproveMode(voidSettingsState.globalSettings.autoApprove[approvalType]) + const isScoped = approvalIsWorkspaceScoped(approvalType) + + if (!isScoped) { + // Unscoped tier (terminal, MCP tools): simple boolean switch. Store Off→'off', On→'all'. + return <> + writeMode(newVal ? 'all' : 'off')} + /> + {desc} + + } + + // Workspace-scoped tier (edits, delete): tri-state radio. + const segSize: 'xxs' | 'xs' | 'sm' = size === 'xxs' ? 'xxs' : size === 'sm' || size === 'sm+' || size === 'md' ? 'sm' : 'xs' + const options: { value: AutoApproveMode; label: string; title?: string }[] = [ + { value: 'off', label: 'Off', title: 'Always ask for permission' }, + { value: 'workspace', label: 'Workspace', title: 'Auto-approve only when the file is inside an open workspace folder' }, + { value: 'all', label: 'Everywhere', title: 'Auto-approve regardless of path (including outside workspace)' }, + ] return <> - onToggleAutoApprove(approvalType, newVal)} + {desc} diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index d5da3e17..86cd22ea 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -18,9 +18,19 @@ export type ShallowDirectoryItem = { } -export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = { +// Approval tiers for built-in tools. Kept separate so users can opt into auto-approve at different +// safety levels: +// - 'edits' = reversible changes (edit_file always revertable via checkpoint, create/rewrite +// likewise). Workspace-scoped when auto-approved (see chatThreadService). +// - 'delete' = irreversible destructive ops (delete_file_or_folder). Split out from 'edits' so +// auto-approving normal edits doesn't silently enable auto-delete. Also workspace- +// scoped when auto-approved. +// - 'terminal' = run_command and persistent terminal ops. NOT workspace-scoped (commands can +// legitimately operate outside the workspace, e.g. `brew install`). +// - 'MCP tools'= all MCP tools; unscoped. +export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'delete' | 'terminal' | 'MCP tools' }> = { 'create_file_or_folder': 'edits', - 'delete_file_or_folder': 'edits', + 'delete_file_or_folder': 'delete', 'rewrite_file': 'edits', 'edit_file': 'edits', 'run_command': 'terminal', @@ -39,6 +49,31 @@ export const toolApprovalTypes = new Set([ ]) +// Auto-approve mode per tier. Tri-state: +// 'off' — always prompt +// 'workspace' — auto-approve when the target is inside an open workspace folder; prompt otherwise +// 'all' — auto-approve regardless of path +// For tiers that don't have a meaningful workspace scope ('terminal', 'MCP tools'), 'workspace' +// is rendered in UI as a simple on/off and stored as 'all' when enabled. See +// `approvalIsWorkspaceScoped` below. +export type AutoApproveMode = 'off' | 'workspace' | 'all' + +// Returns true if the tier's behavior differs between a workspace-internal vs external path. +// Only file-modification tiers are workspace-scoped today: `edits` and `delete`. +export const approvalIsWorkspaceScoped = (t: ToolApprovalType): boolean => + t === 'edits' || t === 'delete' + +// Normalizes stored auto-approve values to the tri-state enum. Accepts the legacy boolean shape +// that was written before the tri-state was introduced: +// true → 'workspace' (safe default — opt into 'all' explicitly via the radio) +// false → 'off' +export const normalizeAutoApproveMode = (raw: AutoApproveMode | boolean | undefined): AutoApproveMode => { + if (raw === undefined) return 'off' + if (typeof raw === 'boolean') return raw ? 'workspace' : 'off' + return raw +} + + // PARAMS OF TOOL CALL diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 38497c60..a3c7a91f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------*/ import { defaultModelsOfProvider, defaultProviderSettings, ModelOverrides } from './modelCapabilities.js'; -import { ToolApprovalType } from './toolsServiceTypes.js'; +import { AutoApproveMode, ToolApprovalType } from './toolsServiceTypes.js'; import { VoidSettingsState } from './voidSettingsService.js' @@ -446,7 +446,12 @@ export type GlobalSettings = { syncSCMToChat: boolean; enableFastApply: boolean; chatMode: ChatMode; - autoApprove: { [approvalType in ToolApprovalType]?: boolean }; + // Per-tier auto-approve configuration. Stored as the tri-state `AutoApproveMode` + // ('off' | 'workspace' | 'all'). For backward compatibility with settings files written before + // the tri-state was introduced, a legacy `boolean` value is also accepted at read time and + // normalized via `normalizeAutoApproveMode` in `toolsServiceTypes` (true → 'workspace', + // false → 'off'). All code reading this field should go through `normalizeAutoApproveMode`. + autoApprove: { [approvalType in ToolApprovalType]?: AutoApproveMode | boolean }; showInlineSuggestions: boolean; includeToolLintErrors: boolean; isOnboardingComplete: boolean;