mirror of
https://github.com/voideditor/void
synced 2026-05-23 01:18:25 +00:00
add workspace scope tool usage for edit / delete file (#12)
This commit is contained in:
parent
85a539d49d
commit
f9cb764fbc
6 changed files with 238 additions and 16 deletions
69
.cursorignore
Normal file
69
.cursorignore
Normal file
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = <T extends string>({
|
||||
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 (
|
||||
<div
|
||||
role="radiogroup"
|
||||
className={`inline-flex items-center gap-1 ${disabled ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const active = opt.value === value
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
title={opt.title ?? opt.label}
|
||||
onClick={() => !disabled && !active && onChange(opt.value)}
|
||||
className={`
|
||||
${btnPad}
|
||||
rounded
|
||||
leading-none
|
||||
whitespace-nowrap
|
||||
transition-colors duration-75
|
||||
border
|
||||
${active
|
||||
? 'bg-zinc-900 text-white border-zinc-900 dark:bg-white dark:text-zinc-900 dark:border-white font-medium'
|
||||
: 'bg-transparent text-void-fg-3 border-void-border-2 hover:text-void-fg-1 hover:border-void-fg-3 hover:bg-void-bg-2-hover cursor-pointer'}
|
||||
`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => {
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
const instanceRef = useRef<Checkbox | null>(null)
|
||||
|
|
|
|||
|
|
@ -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 <>
|
||||
<VoidSwitch
|
||||
size={size}
|
||||
value={currentMode !== 'off'}
|
||||
onChange={(newVal) => writeMode(newVal ? 'all' : 'off')}
|
||||
/>
|
||||
<span className="text-void-fg-3 text-xs">{desc}</span>
|
||||
</>
|
||||
}
|
||||
|
||||
// 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 <>
|
||||
<VoidSwitch
|
||||
size={size}
|
||||
value={voidSettingsState.globalSettings.autoApprove[approvalType] ?? false}
|
||||
onChange={(newVal) => onToggleAutoApprove(approvalType, newVal)}
|
||||
<VoidSegmentedControl
|
||||
size={segSize}
|
||||
value={currentMode}
|
||||
onChange={writeMode}
|
||||
options={options}
|
||||
/>
|
||||
<span className="text-void-fg-3 text-xs">{desc}</span>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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<ToolApprovalType>([
|
|||
])
|
||||
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue