diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 96b47e50..b2616ff2 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -16,7 +16,7 @@ import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sen import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -486,9 +486,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval - const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName) - if (toolRequiresApproval) { - const autoApprove = this._settingsService.state.globalSettings.autoApprove + + + const approvalType = approvalTypeOfToolName[toolName] + if (approvalType) { + const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType] // 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: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams }) if (!autoApprove) { @@ -1199,7 +1201,7 @@ We only need to do it for files that were edited since `from`, ie files between // else search codebase for `target` let uris: URI[] = [] try { - const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 }) + const { result } = await this._toolsService.callTool['search_pathnames_only']({ query: target, includePattern: null, pageNumber: 0 }) uris = result.uris } catch (e) { return null diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 44cfbd75..7ed729ae 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -452,7 +452,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess return voidRules.trim(); } catch (e) { - console.log('Could not read .voidrules, continuing...') return '' } } diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index 1d62638c..dcdbaa42 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -10,11 +10,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IFileService } from '../../../../platform/files/common/files.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'; -import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js'; import { IExplorerService } from '../../files/browser/files.js'; import { SortOrder } from '../../files/common/files.js'; import { ExplorerItem } from '../../files/common/explorerModel.js'; -import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js'; +import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js'; const MAX_FILES_TOTAL = 300; @@ -111,14 +110,14 @@ export const computeDirectoryTree1Deep = async ( export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => { if (!result.children) { - return `Error: ${params.rootURI} is not a directory`; + return `Error: ${params.uri} is not a directory`; } let output = ''; const entries = result.children; if (!result.hasPrevPage) { // is first page - output += `${params.rootURI.fsPath}\n`; + output += `${params.uri.fsPath}\n`; } for (let i = 0; i < entries.length; i++) { @@ -419,7 +418,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { } if (cutOff) { - return `${str}\n${cutOffMessage}` + return `${str.trimEnd()}\n${cutOffMessage}` } return str diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 72d90ce7..b310c355 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -31,14 +31,11 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { isMacintosh } from '../../../../base/common/platform.js'; +import { INotificationService, } from '../../../../platform/notification/common/notification.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { Emitter } from '../../../../base/common/event.js'; -import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; +import { LLMChatMessage } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; @@ -48,6 +45,8 @@ import { deepClone } from '../../../../base/common/objects.js'; import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js'; import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js'; import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; +// import { isMacintosh } from '../../../../base/common/platform.js'; +// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -199,7 +198,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, - @ICommandService private readonly _commandService: ICommandService, + // @ICommandService private readonly _commandService: ICommandService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IFileService private readonly _fileService: IFileService, @IVoidModelService private readonly _voidModelService: IVoidModelService, @@ -279,24 +278,24 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _notifyError = (e: Parameters[0]) => { - const details = errorDetails(e.fullError) - this._notificationService.notify({ - severity: Severity.Warning, - message: `Void Error: ${e.message}`, - actions: { - secondary: [{ - id: 'void.onerror.opensettings', - enabled: true, - label: `Open Void's settings`, - tooltip: '', - class: undefined, - run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } - }] - }, - source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined - }) - } + // private _notifyError = (e: Parameters[0]) => { + // const details = errorDetails(e.fullError) + // this._notificationService.notify({ + // severity: Severity.Warning, + // message: `Void Error: ${e.message}`, + // actions: { + // secondary: [{ + // id: 'void.onerror.opensettings', + // enabled: true, + // label: `Open Void's settings`, + // tooltip: '', + // class: undefined, + // run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + // }] + // }, + // source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined + // }) + // } @@ -1393,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // throws const onError = (e: { message: string; fullError: Error | null; }) => { - this._notifyError(e) + // this._notifyError(e) onDone() this._undoHistory(uri) throw e.fullError @@ -1612,7 +1611,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } const onError = (e: { message: string; fullError: Error | null; }) => { - this._notifyError(e) + // this._notifyError(e) onDone() this._undoHistory(uri) throw e.fullError || new Error(e.message) // throw error h diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 9464bf91..4f5d4529 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -20,17 +20,17 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon } from 'lucide-react'; +import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; -import { LintErrorItem, ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; -import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; +import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; -import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js'; import jsonStringify from 'fast-json-stable-stringify' import ErrorBoundary from './ErrorBoundary.js'; +import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; @@ -464,6 +464,22 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe ); }; + +const getRelative = (uri: URI, accessor: ReturnType) => { + const workspaceContextService = accessor.get('IWorkspaceContextService') + let path: string + const isInside = workspaceContextService.isInsideWorkspace(uri) + if (isInside) { + const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath)) + if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') } + else { path = uri.fsPath } + } + else { + path = uri.fsPath + } + return path || undefined +} + export const getFolderName = (pathStr: string) => { // 'unixify' path pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / @@ -495,6 +511,9 @@ export const SelectedFiles = ( const commandService = accessor.get('ICommandService') const modelReferenceService = accessor.get('IVoidModelService') + + + // state for tracking prospective files const { uri: currentURI } = useActiveURI() const [recentUris, setRecentUris] = useState([]) @@ -653,6 +672,8 @@ type ToolHeaderParams = { desc1: React.ReactNode; desc2?: React.ReactNode; isError?: boolean; + info?: string; + desc1Info?: string; isRejected?: boolean; numResults?: number; hasNextPage?: boolean; @@ -667,10 +688,12 @@ const ToolHeaderWrapper = ({ icon, title, desc1, + desc1Info, desc2, numResults, hasNextPage, children, + info, bottomChildren, isError, onClick, @@ -707,13 +730,33 @@ const ToolHeaderWrapper = ({ `} />)} {title} - {desc1} + {desc1} {/* right */}
- {isError && } - {isRejected && } + + + {isError && } + {isRejected && } {desc2 && {desc2} } @@ -722,6 +765,13 @@ const ToolHeaderWrapper = ({ {`${numResults}${hasNextPage ? '+' : ''} result${numResults !== 1 ? 's' : ''}`} )} + {info && }
@@ -1170,13 +1220,15 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, - 'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, + 'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') }, 'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, 'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, 'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) }, 'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) }, 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, - 'command_tool': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, + 'run_terminal': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, + 'open_bg_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, + 'kill_bg_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, 'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') }, } as const satisfies Record @@ -1191,43 +1243,103 @@ const getTitle = (toolMessage: Pick { +const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType): { + desc1: string, + desc1Info?: string, +} => { if (!_toolParams) { - return ''; + return { desc1: '' }; } - if (toolName === 'read_file') { - const toolParams = _toolParams as ToolCallParams['read_file'] - return getBasename(toolParams.uri.fsPath); - } else if (toolName === 'ls_dir') { - const toolParams = _toolParams as ToolCallParams['ls_dir'] - return `${getFolderName(toolParams.rootURI.fsPath)}`; - } else if (toolName === 'search_pathnames_only') { - const toolParams = _toolParams as ToolCallParams['search_pathnames_only'] - return `"${toolParams.queryStr}"`; - } else if (toolName === 'search_for_files') { - const toolParams = _toolParams as ToolCallParams['search_for_files'] - return `"${toolParams.queryStr}"`; - } else if (toolName === 'create_file_or_folder') { - const toolParams = _toolParams as ToolCallParams['create_file_or_folder'] - return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); - } else if (toolName === 'delete_file_or_folder') { - const toolParams = _toolParams as ToolCallParams['delete_file_or_folder'] - return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); - } else if (toolName === 'edit_file') { - const toolParams = _toolParams as ToolCallParams['edit_file'] - return getBasename(toolParams.uri.fsPath); - } else if (toolName === 'command_tool') { - const toolParams = _toolParams as ToolCallParams['command_tool'] - return `"${toolParams.command}"`; - } else { - return '' + const x = { + 'read_file': () => { + const toolParams = _toolParams as ToolCallParams['read_file'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + }; + }, + 'ls_dir': () => { + const toolParams = _toolParams as ToolCallParams['ls_dir'] + return { + desc1: getFolderName(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + }; + }, + 'search_pathnames_only': () => { + const toolParams = _toolParams as ToolCallParams['search_pathnames_only'] + return { + desc1: `"${toolParams.query}"`, + } + }, + 'search_for_files': () => { + const toolParams = _toolParams as ToolCallParams['search_for_files'] + return { + desc1: `"${toolParams.query}"`, + } + }, + 'create_file_or_folder': () => { + const toolParams = _toolParams as ToolCallParams['create_file_or_folder'] + return { + desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + 'delete_file_or_folder': () => { + const toolParams = _toolParams as ToolCallParams['delete_file_or_folder'] + return { + desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + 'edit_file': () => { + const toolParams = _toolParams as ToolCallParams['edit_file'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + 'run_terminal': () => { + const toolParams = _toolParams as ToolCallParams['run_terminal'] + return { + desc1: `"${toolParams.command}"`, + desc1Info: toolParams.bgTerminalId + } + }, + 'open_bg_terminal': () => { + const toolParams = _toolParams as ToolCallParams['open_bg_terminal'] + return { desc1: '' } + }, + 'kill_bg_terminal': () => { + const toolParams = _toolParams as ToolCallParams['kill_bg_terminal'] + return { desc1: toolParams.terminalId } + }, + 'get_dir_tree': () => { + const toolParams = _toolParams as ToolCallParams['get_dir_tree'] + return { + desc1: getFolderName(toolParams.uri.fsPath) ?? '/', + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + 'read_lint_errors': () => { + const toolParams = _toolParams as ToolCallParams['read_lint_errors'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + } + } + + try { + return x[toolName]?.() || { desc1: '' } + } + catch { + return { desc1: '' } } } - -const ToolRequestAcceptRejectButtons = () => { +const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const metricsService = accessor.get('IMetricsService') @@ -1250,11 +1362,6 @@ const ToolRequestAcceptRejectButtons = () => { metricsService.capture('Tool Request Rejected', {}) }, [chatThreadsService, metricsService]) - const onToggleAutoApprove = useCallback((newValue: boolean) => { - voidSettingsService.setGlobalSetting('autoApprove', newValue) - metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue }) - }, [voidSettingsService, metricsService]) - const approveButton = ( ) - const autoApproveToggle = ( -
- - Auto-approve -
- ) + const approvalType = approvalTypeOfToolName[toolName] + const approvalToggle = approvalType ?
+ +
: null return
{approveButton} {cancelButton} - {autoApproveToggle} + {approvalToggle}
} @@ -1432,8 +1533,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const title = getTitle(toolMessage) - const { uri } = toolMessage.params ?? {} - const desc1 = uri ? getBasename(uri.fsPath) : ''; + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); const icon = null if (toolMessage.type === 'tool_request') return null @@ -1441,7 +1541,8 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) { const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}` @@ -1451,7 +1552,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (result.hasNextPage && params.pageNumber === 1) // first page componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)` @@ -1459,8 +1560,8 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.desc2 = `(part ${params.pageNumber})` } else if (toolMessage.type === 'tool_error') { - const { params, result } = toolMessage - if (params) componentParams.desc2 = + const { result } = toolMessage + componentParams.desc2 = componentParams.children = {result} @@ -1471,13 +1572,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return }, }, - 'get_dir_structure': { + 'get_dir_tree': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null if (toolMessage.type === 'tool_request') return null @@ -1485,10 +1586,16 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (params.uri) { + const rel = getRelative(params.uri, accessor) + if (rel) componentParams.info = `Only in ${rel}` + } if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.children = , } else { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.children = {result} @@ -1519,7 +1626,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null if (toolMessage.type === 'tool_request') return null @@ -1527,10 +1634,16 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (params.uri) { + const rel = getRelative(params.uri, accessor) + if (rel) componentParams.info = `Only in ${rel}` + } if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.numResults = result.children?.length componentParams.hasNextPage = result.hasNextPage componentParams.children = !result.children || (result.children.length ?? 0) === 0 ? undefined @@ -1550,7 +1663,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.children = {result} @@ -1567,22 +1680,25 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const commandService = accessor.get('ICommandService') const isError = toolMessage.type === 'tool_error' const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null if (toolMessage.type === 'tool_request') return null if (toolMessage.type === 'rejected') return null // will never happen, not rejectable if (toolMessage.type === 'running_now') return null // do not show running - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (params.includePattern) + componentParams.info = `Only in ${params.includePattern}` if (toolMessage.type === 'success') { - const { params, result, rawParams } = toolMessage + const { result, rawParams } = toolMessage componentParams.numResults = result.uris.length componentParams.hasNextPage = result.hasNextPage componentParams.children = result.uris.length === 0 ? undefined : - {rawParams.search_in_folder ? `Search in ${rawParams.search_in_folder}` : null} {result.uris.map((uri, i) => (, } else { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.children = {result} @@ -1612,22 +1728,27 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const commandService = accessor.get('ICommandService') const isError = toolMessage.type === 'tool_error' const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null if (toolMessage.type === 'tool_request') return null if (toolMessage.type === 'rejected') return null // will never happen, not rejectable if (toolMessage.type === 'running_now') return null // do not show running - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (params.searchInFolder) { + const rel = getRelative(params.searchInFolder, accessor) + if (rel) componentParams.desc1Info = `Only in ${rel}` + } if (toolMessage.type === 'success') { - const { params, result, rawParams } = toolMessage + const { result, rawParams } = toolMessage componentParams.numResults = result.uris.length componentParams.hasNextPage = result.hasNextPage componentParams.children = result.uris.length === 0 ? undefined : - {rawParams.search_in_folder ? `Search in ${rawParams.search_in_folder}` : null} {result.uris.map((uri, i) => (, } else { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.children = {result} @@ -1659,7 +1780,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const title = getTitle(toolMessage) const { uri } = toolMessage.params ?? {} - const desc1 = uri ? getBasename(uri.fsPath) : ''; + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null if (toolMessage.type === 'tool_request') return null @@ -1667,10 +1788,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + componentParams.info = getRelative(uri, accessor) // full path if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (result.lintErrors) componentParams.children = @@ -1679,7 +1803,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else if (toolMessage.type === 'tool_error') { - const { params, result } = toolMessage + const { result } = toolMessage if (params) componentParams.desc2 = componentParams.children = @@ -1701,21 +1825,24 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const isError = toolMessage.type === 'tool_error' const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } + + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + componentParams.info = getRelative(params.uri, accessor) // full path if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.type === 'rejected') { - const { params } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.type === 'tool_error') { - const { params, result } = toolMessage + const { result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = @@ -1741,21 +1868,23 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const isError = toolMessage.type === 'tool_error' const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + componentParams.info = getRelative(params.uri, accessor) // full path if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.type === 'rejected') { - const { params } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.type === 'tool_error') { - const { params, result } = toolMessage + const { result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = @@ -1764,11 +1893,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else if (toolMessage.type === 'running_now') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.type === 'tool_request') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } @@ -1783,13 +1912,15 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + componentParams.info = getRelative(params.uri, accessor) // full path if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - const { params } = toolMessage componentParams.children = , componentParams.desc2 = } else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { - const { params } = toolMessage - // add apply box if (params) { const applyBoxId = getApplyBoxId({ @@ -1818,7 +1947,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, // add children if (toolMessage.type !== 'tool_error') { - const { params, result } = toolMessage + const { result } = toolMessage componentParams.bottomChildren = @@ -1831,7 +1960,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { // error - const { params, result } = toolMessage + const { result } = toolMessage if (params) { componentParams.children = {/* error */} @@ -1857,23 +1986,27 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'command_tool': { + + // --- + + 'run_terminal': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = toolMessage.type === 'tool_error' const title = getTitle(toolMessage) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null const isRejected = toolMessage.type === 'rejected' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (toolMessage.type === 'success') { - const { params, result } = toolMessage + const { result } = toolMessage const { command } = params - const { terminalId, resolveReason, result: terminalResult } = result + const { resolveReason, result: terminalResult } = result // it's unclear that this is a button and not an icon. // componentParams.desc2 = , // /> const additionalDetailsStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) - : resolveReason.type === 'bgtask' ? null : - resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : - resolveReason.type === 'toofull' ? `\n(truncated)` - : null + : resolveReason.type === 'timeout' ? `\n(timed out)` + : null componentParams.children = -
{`Ran command: `} {command}
{(terminalResult + additionalDetailsStr).length &&
- {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} {`Result: `} {terminalResult} {additionalDetailsStr} @@ -1902,18 +2031,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper,
+ if (params.bgTerminalId) + componentParams.desc2 = `(terminal ${params.bgTerminalId})` - if (resolveReason.type === 'bgtask') - componentParams.desc2 = '(background task)' } else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { - const { params } = toolMessage if (params) { - const { proposedTerminalId, waitForCompletion } = params - if (terminalToolsService.terminalExists(proposedTerminalId)) - componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - if (!waitForCompletion) - componentParams.desc2 = '(background task)' + const { bgTerminalId, command } = params + if (bgTerminalId) { + componentParams.desc2 = '(persistent terminal)' + if (terminalToolsService.terminalExists(bgTerminalId)) + componentParams.onClick = () => terminalToolsService.focusTerminal(bgTerminalId) + } } if (toolMessage.type === 'tool_error') { const { result } = toolMessage @@ -1921,16 +2050,81 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } else if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - const { proposedTerminalId, waitForCompletion } = toolMessage.params - if (terminalToolsService.terminalExists(proposedTerminalId)) - componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - if (!waitForCompletion) - componentParams.desc2 = '(background task)' + const { bgTerminalId } = toolMessage.params + if (bgTerminalId) { + componentParams.desc2 = '(persistent terminal)' + if (terminalToolsService.terminalExists(bgTerminalId)) + componentParams.onClick = () => terminalToolsService.focusTerminal(bgTerminalId) + } } return } - } + }, + + 'open_bg_terminal': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const title = getTitle(toolMessage) + const icon = null + + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running + + const isError = toolMessage.type === 'tool_error' + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (toolMessage.type === 'success') { + const { result } = toolMessage + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.children = + + {result} + + + } + + return + }, + }, + 'kill_bg_terminal': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const title = getTitle(toolMessage) + const icon = null + + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running + + const isError = toolMessage.type === 'tool_error' + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + + if (toolMessage.type === 'success') { + const { result } = toolMessage + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.children = + + {result} + + + } + + return + }, + }, }; @@ -2024,7 +2218,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
{chatMessage.type === 'tool_request' ?
- +
: null} return 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 8966ccf7..606ee858 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 @@ -16,6 +16,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' const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -711,6 +712,34 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit } + + +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) => { + voidSettingsService.setGlobalSetting('autoApprove', { + ...voidSettingsService.state.globalSettings.autoApprove, + [approvalType]: newValue + }) + metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue }) + }, [voidSettingsService, metricsService]) + + return <> + onToggleAutoApprove(approvalType, newVal)} + /> + {desc} + +} + + + export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => { const accessor = useAccessor() const fileService = accessor.get('IFileService') @@ -933,15 +962,12 @@ export const Settings = () => {
{/* Auto Accept Switch */} + {[...toolApprovalTypes].map((approvalType) => { + return
+ +
+ })} -
- voidSettingsService.setGlobalSetting('autoApprove', newVal)} - /> - {settingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'} -
{/* Tool Lint Errors Switch */} diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 5876e54b..dff20924 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -3,14 +3,14 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; +import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'; import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; -import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js'; @@ -18,9 +18,12 @@ export interface ITerminalToolService { readonly _serviceBrand: undefined; listTerminalIds(): string[]; - runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>; - openTerminal(terminalId: string): Promise + runCommand(command: string, bgTerminalId: string | null): Promise<{ result: string, resolveReason: TerminalResolveReason }>; + focusTerminal(terminalId: string): Promise terminalExists(terminalId: string): boolean + + createTerminal(): Promise + killTerminal(terminalId: string): Promise } export const ITerminalToolService = createDecorator('TerminalToolService'); @@ -103,12 +106,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ throw new Error('This should never be reached by pigeonhole principle'); } - - - private async _getOrCreateTerminal(proposedTerminalId: string) { - // if terminal ID exists, return it - if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false } - + async createTerminal() { // create new terminal and return its ID const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ @@ -127,13 +125,21 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ }) disposables.push(d) }) - const waitForTimeout = new Promise(res => { setTimeout(() => { res() }, 1000) }) + const waitForTimeout = new Promise(res => { setTimeout(() => { res() }, 5000) }) await Promise.any([waitForMount, waitForTimeout,]) disposables.forEach(d => d.dispose()) this.terminalInstanceOfId[terminalId] = terminal - return { terminalId, didCreateTerminal: true } + return terminalId + } + + async killTerminal(terminalId: string) { + const terminal = this.terminalInstanceOfId[terminalId] + if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`); + terminal.dispose(TerminalExitReason.Extension) + delete this.terminalInstanceOfId[terminalId] + return } terminalExists(terminalId: string): boolean { @@ -141,7 +147,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } - openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => { + focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => { if (!terminalId) return const terminal = this.terminalInstanceOfId[terminalId] if (!terminal) return // should never happen @@ -151,11 +157,26 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ - runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => { + + runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => { await this.terminalService.whenConnected; - const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId) - const terminal = this.terminalInstanceOfId[terminalId]; - if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`); + + let terminal: ITerminalInstance + const disposables: IDisposable[] = [] + + const isBG = bgTerminalId !== null + let terminalId: string + if (isBG) { // BG process + terminal = this.terminalInstanceOfId[bgTerminalId]; + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} did not exist.`); + terminalId = bgTerminalId + } + else { + terminalId = await this.createTerminal() + terminal = this.terminalInstanceOfId[terminalId] + if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`) + } + // focus the terminal about to run this.terminalService.setActiveInstance(terminal) @@ -164,23 +185,12 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ let result: string = '' let resolveReason: TerminalResolveReason | undefined = undefined - const disposables: IDisposable[] = [] + // create this before we send so that we don't miss events on terminal const waitUntilDone = new Promise((res, rej) => { const d2 = terminal.onData(async newData => { if (resolveReason) return - result += newData - - // onPageFull - if (result.length > MAX_TERMINAL_CHARS_PAGE) { - result = result.substring(0, MAX_TERMINAL_CHARS_PAGE) - await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C - resolveReason = { type: 'toofull' } - res() - return - } - // onDone const isDone = isCommandComplete(result) if (isDone) { @@ -196,36 +206,49 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // send the command here await terminal.sendText(command, true) - // timeout promise - const waitUntilTimeout = new Promise((res, rej) => { - setTimeout(async () => { - if (resolveReason) return - await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C - resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' } - res() - return - }, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000) - }) - await Promise.any([ - waitUntilDone, - waitUntilTimeout, - ]) + // inactivity-based timeout + const waitUntilInactive = new Promise(res => { + let globalTimeoutId: ReturnType; + const resetTimer = () => { + clearTimeout(globalTimeoutId); + globalTimeoutId = setTimeout(() => { + if (resolveReason) return + + resolveReason = { type: 'timeout' }; + res(); + }, MAX_TERMINAL_INACTIVE_TIME * 1000); + }; + + const dTimeout = terminal.onData(() => { resetTimer(); }); + disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId))); + resetTimer(); + }); + + // wait for result + await Promise.any([waitUntilDone, waitUntilInactive,]) disposables.forEach(d => d.dispose()) + if (!isBG) { + await this.killTerminal(terminalId) + } if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') - result = removeAnsiEscapeCodes(result) .split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %) .join('\n') - return { terminalId, didCreateTerminal, result, resolveReason } + if (result.length > MAX_TERMINAL_CHARS) { + const half = MAX_TERMINAL_CHARS / 2 + result = result.slice(0, half) + + '\n...\n' + + result.slice(result.length - half, Infinity) + } + + return { result, resolveReason } } - - } registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 82f1819c..75429f0f 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -17,7 +17,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' import { timeout } from '../../../../base/common/async.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' -import { ToolName } from '../common/prompt/prompts.js' +import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' @@ -32,15 +32,6 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awai - -// pagination info -export const MAX_FILE_CHARS_PAGE = 500_000 -export const MAX_CHILDREN_URIs_PAGE = 500 -export const MAX_TERMINAL_CHARS_PAGE = 20_000 -export const TERMINAL_TIMEOUT_TIME = 5 // seconds -export const TERMINAL_BG_WAIT_TIME = 1 - - const isFalsy = (u: unknown) => { return !u || u === 'null' || u === 'undefined' } @@ -176,12 +167,12 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - return { rootURI: uri, pageNumber } + return { uri, pageNumber } }, - get_dir_structure: (params: RawToolParamsObj) => { + get_dir_tree: (params: RawToolParamsObj) => { const { uri: uriStr, } = params const uri = validateURI(uriStr) - return { rootURI: uri } + return { uri } }, search_pathnames_only: (params: RawToolParamsObj) => { const { @@ -192,9 +183,9 @@ export class ToolsService implements IToolsService { const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) - const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown) + const includePattern = validateOptionalStr('include_pattern', includeUnknown) - return { queryStr, searchInFolder, pageNumber } + return { query: queryStr, includePattern, pageNumber } }, search_for_files: (params: RawToolParamsObj) => { @@ -211,7 +202,7 @@ export class ToolsService implements IToolsService { const searchInFolder = validateOptionalURI(searchInFolderUnknown) const isRegex = validateBoolean(isRegexUnknown, { default: false }) - return { queryStr, searchInFolder, isRegex, pageNumber } + return { query: queryStr, searchInFolder, isRegex, pageNumber } }, read_lint_errors: (params: RawToolParamsObj) => { @@ -248,12 +239,22 @@ export class ToolsService implements IToolsService { return { uri, changeDescription } }, - command_tool: (params: RawToolParamsObj) => { - const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params - const command = validateStr('command', commandUnknown) - const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) - const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true }) - return { command, proposedTerminalId, waitForCompletion } + // --- + + run_terminal: (params: RawToolParamsObj) => { + const { command: commandUnknown, terminal_id: terminalIdUnknown } = params; + const command = validateStr('command', commandUnknown); + const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null; + return { command, bgTerminalId: proposedTerminalId }; + }, + open_bg_terminal: (_params: RawToolParamsObj) => { + // No parameters needed; will open a new background terminal + return {}; + }, + kill_bg_terminal: (params: RawToolParamsObj) => { + const { terminal_id: terminalIdUnknown } = params; + const terminalId = validateProposedTerminalId(terminalIdUnknown); + return { terminalId }; }, } @@ -283,21 +284,21 @@ export class ToolsService implements IToolsService { return { result: { fileContents, totalFileLen, hasNextPage } } }, - ls_dir: async ({ rootURI, pageNumber }) => { - const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber) + ls_dir: async ({ uri, pageNumber }) => { + const dirResult = await computeDirectoryTree1Deep(fileService, uri, pageNumber) return { result: dirResult } }, - get_dir_structure: async ({ rootURI }) => { - const str = await this.directoryStrService.getDirectoryStrTool(rootURI) + get_dir_tree: async ({ uri }) => { + const str = await this.directoryStrService.getDirectoryStrTool(uri) return { result: { str } } }, - search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => { + search_pathnames_only: async ({ query: queryStr, includePattern, pageNumber }) => { const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, - includePattern: searchInFolder ?? undefined, + includePattern: includePattern ?? undefined, }) const data = await searchService.fileSearch(query, CancellationToken.None) @@ -311,7 +312,7 @@ export class ToolsService implements IToolsService { return { result: { uris, hasNextPage } } }, - search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => { + search_for_files: async ({ query: queryStr, isRegex, searchInFolder, pageNumber }) => { const searchFolders = searchInFolder === null ? workspaceContextService.getWorkspace().folders.map(f => f.uri) : [searchInFolder] @@ -385,10 +386,22 @@ export class ToolsService implements IToolsService { return { result: lintErrorsPromise, interruptTool } }, - command_tool: async ({ command, proposedTerminalId, waitForCompletion }) => { - const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) - return { result: { terminalId, didCreateTerminal, result, resolveReason } } + // --- + run_terminal: async ({ command, bgTerminalId }) => { + const { result, resolveReason } = await this.terminalToolService.runCommand(command, bgTerminalId) + return { result: { result, resolveReason } } }, + open_bg_terminal: async () => { + // Open a new background terminal without waiting for completion + const terminalId = await this.terminalToolService.createTerminal() + return { result: { terminalId } } + }, + kill_bg_terminal: async ({ terminalId }) => { + // Close the background terminal by sending exit + await this.terminalToolService.killTerminal(terminalId) + return { result: {} } + }, + } @@ -401,7 +414,7 @@ export class ToolsService implements IToolsService { .substring(0, MAX_FILE_CHARS_PAGE) } - // given to the LLM after the call + // given to the LLM after the call for successful tool calls this.stringOfResult = { read_file: (params, result) => { return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}` @@ -410,7 +423,7 @@ export class ToolsService implements IToolsService { const dirTreeStr = stringifyDirectoryTree1Deep(params, result) return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining }, - get_dir_structure: (params, result) => { + get_dir_tree: (params, result) => { return result.str }, search_pathnames_only: (params, result) => { @@ -440,30 +453,41 @@ export class ToolsService implements IToolsService { return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}` }, - command_tool: (params, result) => { + run_terminal: (params, result) => { const { - terminalId, - didCreateTerminal, resolveReason, result: result_, } = result + const { bgTerminalId } = params - const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}` + // success + if (resolveReason.type === 'done') { + const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : '' + return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}` + } - if (resolveReason.type === 'timeout') { - return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}` + // bg command + if (bgTerminalId !== null) { + if (resolveReason.type === 'timeout') { + return `Terminal command is running in the background in terminal ${bgTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}` + } } - else if (resolveReason.type === 'bgtask') { - return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}` - } - else if (resolveReason.type === 'toofull') { - return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}` - } - else if (resolveReason.type === 'done') { - return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}` + // normal command + else { + if (resolveReason.type === 'timeout') { + return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}` + } } + throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) }, + open_bg_terminal: (_params, result) => { + const { terminalId } = result; + return `Successfully created background terminal with ID ${terminalId}`; + }, + kill_bg_terminal: (params, _result) => { + return `Successfully closed terminal ${params.terminalId}.`; + }, } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 2a0ddbeb..8815a2e9 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -7,7 +7,7 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; -import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js'; +import { approvalTypeOfToolName, ToolResultType } from '../toolsServiceTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { ChatMode } from '../voidSettingsTypes.js'; @@ -20,6 +20,14 @@ export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000 export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100 export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100 +// tool info +export const MAX_FILE_CHARS_PAGE = 500_000 +export const MAX_CHILDREN_URIs_PAGE = 500 + +// terminal tool info +export const MAX_TERMINAL_CHARS = 100_000 +export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds + // Maximum character limits for prefix and suffix context export const MAX_PREFIX_SUFFIX_CHARS = 20_000 @@ -66,7 +74,36 @@ const paginationParam = { } as const + + +// export type SnakeCase = +// // exact acronym URI +// S extends 'URI' ? 'uri' +// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri' +// : S extends `${infer Prefix}URI` ? `${SnakeCase}_uri` +// // default: for each char, prefix '_' on uppercase letters +// : S extends `${infer C}${infer Rest}` +// ? `${C extends Lowercase ? C : `_${Lowercase}`}${SnakeCase}` +// : S; + +// export type SnakeCaseKeys> = { +// [K in keyof T as SnakeCase>]: T[K] +// }; + + + export const voidTools = { + // export const voidTools + // : { + // [T in keyof ToolCallParams]: { + // name: string; + // description: string; + // params: { + // [paramName in keyof SnakeCaseKeys]: { description: string } + // } + // } + // } + // = { // --- context-gathering (read/search/list) --- read_file: { @@ -89,8 +126,8 @@ export const voidTools = { }, }, - get_dir_structure: { - name: 'get_dir_structure', + get_dir_tree: { + name: 'get_dir_tree', description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `, params: { ...uriParam('folder') @@ -106,7 +143,7 @@ export const voidTools = { description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, params: { query: { description: `Your query for the search.` }, - search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, + include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, ...paginationParam, }, }, @@ -118,7 +155,7 @@ export const voidTools = { description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`, params: { query: { description: `Your query for the search.` }, - search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, + search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, is_regex: { description: 'Optional. Default is false. Whether query is a regex.' }, ...paginationParam, }, @@ -166,23 +203,34 @@ Here's an example of a good description:\n${editToolDescriptionExample}` }, }, - command_tool: { - name: 'command_tool', - description: `Runs a terminal command. You can use this tool to run any command: sed, grep, etc. We just prefer you edit with the edit tool, not this tool if possible.`, + run_terminal: { + name: 'run_terminal', + description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor like vim, you might need to pipe to cat to get all results.`, params: { command: { description: 'The terminal command to run.' }, - wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` }, - terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' }, + bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_bg_terminal. Runs the command in the terminal with the specified ID.' }, }, }, + open_bg_terminal: { + name: 'open_bg_terminal', + description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`, + params: {} + }, + kill_bg_terminal: { + name: 'kill_bg_terminal', + description: `Closes a BG terminal with the given ID.`, + params: { terminal_id: { description: `The terminal ID to interrupt and close.` } } + } + + // go_to_definition // go_to_usages -} satisfies { [name: string]: InternalToolInfo } +} satisfies { [T in keyof ToolResultType]: InternalToolInfo } -export type ToolName = keyof typeof voidTools +export type ToolName = keyof ToolResultType export const toolNames = Object.keys(voidTools) as ToolName[] type ToolParamNameOfTool = keyof (typeof voidTools)[T]['params'] @@ -197,7 +245,7 @@ export const isAToolName = (toolName: string): toolName is ToolName => { export const availableTools = (chatMode: ChatMode) => { const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined - : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) + : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName)) : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] : undefined diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 2fd919fa..3305af77 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -3,7 +3,7 @@ import { ToolName } from './prompt/prompts.js'; -export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } +export type TerminalResolveReason = { type: 'timeout' } | { type: 'done', exitCode: number } export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number } @@ -16,32 +16,45 @@ export type ShallowDirectoryItem = { } +export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = { + 'create_file_or_folder': 'edits', + 'delete_file_or_folder': 'edits', + 'edit_file': 'edits', + 'run_terminal': 'terminal', +} -const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'command_tool'] as const satisfies readonly ToolName[] -export type ToolNameWithApproval = typeof toolNamesWithApproval[number] -export const toolNamesThatRequireApproval = new Set(toolNamesWithApproval) + + +// {{add: define new type for approval types}} +export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>; + +export const toolApprovalTypes = new Set( + Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined) +) // PARAMS OF TOOL CALL export type ToolCallParams = { 'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number }, - 'ls_dir': { rootURI: URI, pageNumber: number }, - 'get_dir_structure': { rootURI: URI }, - 'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number }, - 'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number }, + 'ls_dir': { uri: URI, pageNumber: number }, + 'get_dir_tree': { uri: URI }, + 'search_pathnames_only': { query: string, includePattern: string | null, pageNumber: number }, + 'search_for_files': { query: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number }, 'read_lint_errors': { uri: URI }, // --- 'edit_file': { uri: URI, changeDescription: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, - 'command_tool': { command: string, proposedTerminalId: string, waitForCompletion: boolean }, + // --- + 'run_terminal': { command: string; bgTerminalId: string | null }, + 'open_bg_terminal': {}, + 'kill_bg_terminal': { terminalId: string }, } - // RESULT OF TOOL CALL export type ToolResultType = { 'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'get_dir_structure': { str: string, }, + 'get_dir_tree': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 'search_for_files': { uris: URI[], hasNextPage: boolean }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, @@ -49,6 +62,9 @@ export type ToolResultType = { 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, - 'command_tool': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, + // --- + 'run_terminal': { result: string; resolveReason: TerminalResolveReason; }, + 'open_bg_terminal': { terminalId: string }, + 'kill_bg_terminal': {}, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index b8b3fd10..2cb033ca 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -237,6 +237,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { readS = await this._readState(); // 1.0.3 addition, remove when enough users have had this code run if (readS.globalSettings.includeToolLintErrors === undefined) readS.globalSettings.includeToolLintErrors = true + + // autoapprove is now an obj not a boolean (1.2.5) + if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {} } catch (e) { readS = defaultState() diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index eea91932..5ef8059e 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -5,6 +5,7 @@ *--------------------------------------------------------------------------------------*/ import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js'; +import { ToolApprovalType } from './toolsServiceTypes.js'; import { VoidSettingsState } from './voidSettingsService.js' @@ -425,7 +426,7 @@ export type GlobalSettings = { syncApplyToChat: boolean; enableFastApply: boolean; chatMode: ChatMode; - autoApprove: boolean; + autoApprove: { [approvalType in ToolApprovalType]?: boolean }; showInlineSuggestions: boolean; includeToolLintErrors: boolean; isOnboardingComplete: boolean; @@ -438,7 +439,7 @@ export const defaultGlobalSettings: GlobalSettings = { syncApplyToChat: true, enableFastApply: true, chatMode: 'agent', - autoApprove: false, + autoApprove: {}, showInlineSuggestions: true, includeToolLintErrors: true, isOnboardingComplete: false,