From 7df49a769aff9d0955818402ba6b6ed43495b0fd Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 21 Apr 2025 02:25:37 -0700 Subject: [PATCH 01/27] add @ to mention! --- .../contrib/void/browser/chatThreadService.ts | 37 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 27 +- .../void/browser/react/src/util/inputs.tsx | 538 +++++++++++++----- .../void/browser/react/src/util/services.tsx | 2 + .../src/void-onboarding/VoidOnboarding.tsx | 24 +- .../contrib/void/browser/sidebarActions.ts | 56 +- .../contrib/void/browser/toolsService.ts | 1 + 7 files changed, 478 insertions(+), 207 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7a09327d..baae23e4 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -41,7 +41,7 @@ const CHAT_RETRIES = 3 const RETRY_DELAY = 2500 -export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { +const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { if (!currentSelections) return null for (let i = 0; i < currentSelections.length; i += 1) { @@ -203,6 +203,8 @@ export interface IChatThreadService { isCurrentlyFocusingMessage(): boolean; setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void; + addNewStagingSelection(newSelection: StagingSelectionItem): void; + dangerousSetState: (newState: ThreadsState) => void; resetState: () => void; @@ -1517,6 +1519,39 @@ We only need to do it for files that were edited since `from`, ie files between // this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) } + + addNewStagingSelection(newSelection: StagingSelectionItem): void { + + const focusedMessageIdx = this.getCurrentFocusedMessageIdx() + + // set the selections to the proper value + let selections: StagingSelectionItem[] = [] + let setSelections = (s: StagingSelectionItem[]) => { } + + if (focusedMessageIdx === undefined) { + selections = this.getCurrentThreadState().stagingSelections + setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s }) + } else { + selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections + setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) + } + + // if matches with existing selection, overwrite (since text may change) + const idx = findStagingSelectionIndex(selections, newSelection) + if (idx !== null && idx !== -1) { + setSelections([ + ...selections!.slice(0, idx), + newSelection, + ...selections!.slice(idx + 1, Infinity) + ]) + } + // if no match, add it + else { + setSelections([...(selections ?? []), newSelection]) + } + } + + // set message.state private _setCurrentMessageState(state: Partial, messageIdx: number): void { 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 70e4000e..60441371 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,7 +20,7 @@ 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, Info, CirclePlus, Ellipsis, CircleEllipsis } from 'lucide-react'; +import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; @@ -492,12 +492,12 @@ export const getFolderName = (pathStr: string) => { return lastTwo.join('/') + '/' } -export const getBasename = (pathStr: string) => { +export const getBasename = (pathStr: string, parts: number = 1) => { // 'unixify' path pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / - const parts = pathStr.split('/') // split on / - if (parts.length === 0) return pathStr - return parts[parts.length - 1] + const allParts = pathStr.split('/') // split on / + if (allParts.length === 0) return pathStr + return allParts.slice(-parts).join('/') } export const SelectedFiles = ( @@ -576,6 +576,13 @@ export const SelectedFiles = ( : selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath : i + const SelectionIcon = ( + selection.type === 'File' ? File + : selection.type === 'Folder' ? Folder + : selection.type === 'CodeSelection' ? Text + : (undefined as never) + ) + return
+ {} + { // file name and range getBasename(selection.uri.fsPath) + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') } {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? - + {`(Current File)`} : null @@ -972,6 +981,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr setSelections={setStagingSelections} > { onClickAnywhere={() => { textAreaRef.current?.focus() }} > { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} 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 91281214..4b8375da 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 @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, ForwardRefExoticComponent, MutableRefObject, RefAttributes, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; @@ -16,6 +16,10 @@ import { ITextModel } from '../../../../../../../editor/common/model.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { getBasename } from '../sidebar-tsx/SidebarChat.js'; +import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react'; +import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js'; // type guard @@ -48,48 +52,233 @@ export const WidgetComponent = ({ ctor, prop return
{children}
} -type GenerateNextOptions = (newPathText: string) => Option[] +type GenerateNextOptions = (optionText: string) => Promise type Option = { - name: string, - displayName: string, + nameInMenu: string, + iconInMenu: ForwardRefExoticComponent & RefAttributes>, // type for lucide-react components } & ( - | { nextOptions: Option[], generateNextOptions?: undefined } - | { nextOptions?: undefined, generateNextOptions: GenerateNextOptions } - | { nextOptions?: undefined, generateNextOptions?: undefined } + | { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined } + | { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined } + | { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, } ) -const getOptionsAtPath = (accessor: ReturnType, path: string[], newPathText: string) => { +const isSubsequence = (text: string, pattern: string): boolean => { + + text = text.toLowerCase() + pattern = pattern.toLowerCase() + + if (pattern === '') return true; + if (text === '') return false; + if (pattern.length > text.length) return false; + + const seq: boolean[][] = Array(pattern.length + 1) + .fill(null) + .map(() => Array(text.length + 1).fill(false)); + + for (let j = 0; j <= text.length; j++) { + seq[0][j] = true; + } + + for (let i = 1; i <= pattern.length; i++) { + for (let j = 1; j <= text.length; j++) { + if (pattern[i - 1] === text[j - 1]) { + seq[i][j] = seq[i - 1][j - 1]; + } else { + seq[i][j] = seq[i][j - 1]; + } + } + } + return seq[pattern.length][text.length]; +}; + + +const scoreSubsequence = (text: string, pattern: string): number => { + if (pattern === '') return 0; + + text = text.toLowerCase(); + pattern = pattern.toLowerCase(); + + // We'll use dynamic programming to find the longest consecutive substring + const n = text.length; + const m = pattern.length; + + // This will track our maximum consecutive match length + let maxConsecutive = 0; + + // For each starting position in the text + for (let i = 0; i < n; i++) { + // Check for matches starting from this position + let consecutiveCount = 0; + + // For each character in the pattern + for (let j = 0; j < m; j++) { + // If we have a match and we're still within text bounds + if (i + j < n && text[i + j] === pattern[j]) { + consecutiveCount++; + } else { + // Break on first non-match + break; + } + } + + // Update our maximum + maxConsecutive = Math.max(maxConsecutive, consecutiveCount); + } + + return maxConsecutive; +} + + +export function getRelativeWorkspacePath(accessor: ReturnType, uri: URI): string { + const workspaceService = accessor.get('IWorkspaceContextService'); + const workspaceFolders = workspaceService.getWorkspace().folders; + + if (!workspaceFolders.length) { + return uri.fsPath; // No workspace folders, return original path + } + + // Sort workspace folders by path length (descending) to match the most specific folder first + const sortedFolders = [...workspaceFolders].sort((a, b) => + b.uri.fsPath.length - a.uri.fsPath.length + ); + + // Add trailing slash to paths for exact matching + const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/'; + + // Check if the URI is inside any workspace folder + for (const folder of sortedFolders) { + + + const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/'; + if (uriPath.startsWith(folderPath)) { + // Calculate the relative path by removing the workspace folder path + let relativePath = uri.fsPath.slice(folder.uri.fsPath.length); + // Remove leading slash if present + if (relativePath.startsWith('/')) { + relativePath = relativePath.slice(1); + } + console.log({ folderPath, relativePath, uriPath }); + + return relativePath; + } + } + + // URI is not in any workspace folder, return original path + return uri.fsPath; +} + + + +const numOptionsToShow = 100 + +const getOptionsAtPath = async (accessor: ReturnType, path: string[], optionText: string): Promise => { + + const toolsService = accessor.get('IToolsService') + + const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => { + try { + + const searchResults = (await (await toolsService.callTool.search_pathnames_only({ + query: t, + includePattern: null, + pageNumber: 1, + })).result).uris + + if (searchFor === 'files') { + const res: Option[] = searchResults.map(uri => { + const relativePath = getRelativeWorkspacePath(accessor, uri) + return { + leafNodeType: 'File', + uri: uri, + iconInMenu: File, + nameInMenu: relativePath, + nameToPaste: getBasename(relativePath, 2), + } + }) + return res + } + + else if (searchFor === 'folders') { + // Extract unique directory paths from the results + const directoryMap = new Map(); + + for (const uri of searchResults) { + if (!uri) continue; + + // Get the full path and extract directories + const relativePath = getRelativeWorkspacePath(accessor, uri) + const pathParts = relativePath.split('/'); + + // Get workspace info + const workspaceService = accessor.get('IWorkspaceContextService'); + const workspaceFolders = workspaceService.getWorkspace().folders; + + // Find the workspace folder containing this URI + let workspaceFolderUri: URI | undefined; + if (workspaceFolders.length) { + // Sort workspace folders by path length (descending) to match the most specific folder first + const sortedFolders = [...workspaceFolders].sort((a, b) => + b.uri.fsPath.length - a.uri.fsPath.length + ); + + // Find the containing workspace folder + for (const folder of sortedFolders) { + const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/'; + const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/'; + + if (uriPath.startsWith(folderPath)) { + workspaceFolderUri = folder.uri; + break; + } + } + } + + if (workspaceFolderUri) { + // Add each directory and its parents to the map + let currentPath = ''; + for (let i = 0; i < pathParts.length - 1; i++) { + currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`; + + console.log('filepath', currentPath); + + // Create a proper directory URI + const directoryUri = URI.joinPath( + workspaceFolderUri, + currentPath.startsWith('/') ? currentPath.substring(1) : currentPath + ); + + directoryMap.set(currentPath, directoryUri); + } + } + } + // Convert map to array + return Array.from(directoryMap.entries()).map(([relativePath, uri]) => ({ + leafNodeType: 'Folder', + uri: uri, + iconInMenu: Folder, // Folder + nameInMenu: relativePath, + nameToPaste: getBasename(relativePath, 2) + })) satisfies Option[]; + } + } catch (error) { + console.error('Error fetching directories:', error); + return []; + } + }; const allOptions: Option[] = [ { - name: 'files', - displayName: 'files', - generateNextOptions: () => [ - { name: 'a.txt', displayName: 'a.txt', }, - { name: 'b.txt', displayName: 'b.txt', }, - { name: 'c.txt', displayName: 'c.txt', }, - { name: 'd.txt', displayName: 'd.txt', }, - { name: 'e.txt', displayName: 'e.txt', }, - { name: 'f.txt', displayName: 'f.txt', }, - { name: 'g.txt', displayName: 'g.txt', }, - { name: '!a.txt', displayName: '!a.txt', }, - { name: '!b.txt', displayName: '!b.txt', }, - { name: '!c.txt', displayName: '!c.txt', }, - { name: '!d.txt', displayName: '!d.txt', }, - { name: '!e.txt', displayName: '!e.txt', }, - { name: '!f.txt', displayName: '!f.txt', }, - { name: '!g.txt', displayName: '!g.txt', }, - ] + nameInMenu: 'files', + iconInMenu: File, + generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [], }, { - name: 'folders', - displayName: 'folders', - nextOptions: [ - { name: 'FOLDER', displayName: 'FOLDER', }, - ] + nameInMenu: 'folders', + iconInMenu: FolderClosed, + generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [], }, ] @@ -100,9 +289,9 @@ const getOptionsAtPath = (accessor: ReturnType, path: string for (const pn of path) { - const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase()) + const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase()) - if (!selectedOption) return; + if (!selectedOption) return []; nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option) generateNextOptionsAtPath = selectedOption.generateNextOptions @@ -111,11 +300,17 @@ const getOptionsAtPath = (accessor: ReturnType, path: string if (generateNextOptionsAtPath) { - nextOptionsAtPath = generateNextOptionsAtPath(newPathText) + nextOptionsAtPath = await generateNextOptionsAtPath(optionText) } - const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText)) - + const optionsAtPath = nextOptionsAtPath + .filter(o => isSubsequence(o.nameInMenu, optionText)) + .sort((a, b) => { // this is a hack but good for now + const scoreA = scoreSubsequence(a.nameInMenu, optionText); + const scoreB = scoreSubsequence(b.nameInMenu, optionText); + return scoreB - scoreA; + }) + .slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints return optionsAtPath @@ -128,6 +323,7 @@ type InputBox2Props = { initValue?: string | null; placeholder: string; multiline: boolean; + enableAtToMention?: boolean; fnsRef?: { current: null | TextAreaFns }; className?: string; onChangeText?: (value: string) => void; @@ -136,34 +332,28 @@ type InputBox2Props = { onBlur?: (e: React.FocusEvent) => void; onChangeHeight?: (newHeight: number) => void; } -export const VoidInputBox2 = forwardRef(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) { +export const VoidInputBox2 = forwardRef(function X({ initValue, placeholder, multiline, enableAtToMention, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) { // mirrors whatever is in ref const accessor = useAccessor() - const toolsService = accessor.get('IToolsService') - - - - - - - - - - + const chatThreadService = accessor.get('IChatThreadService') + const languageService = accessor.get('ILanguageService') const textAreaRef = useRef(null) const selectedOptionRef = useRef(null); - const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isMenuOpen, _setIsMenuOpen] = useState(false); // the @ to mention menu + const setIsMenuOpen: typeof _setIsMenuOpen = (value) => { + if (!enableAtToMention) { return; } // never open menu if not enabled + _setIsMenuOpen(value); + } - const [path, setPath] = useState([]); + // logic for @ to mention vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + const [optionPath, setOptionPath] = useState([]); const [optionIdx, setOptionIdx] = useState(0); const [options, setOptions] = useState([]); - const [newPathText, setNewPathText] = useState(''); - - + const [optionText, setOptionText] = useState(''); const insertTextAtCursor = (text: string) => { const textarea = textAreaRef.current; if (!textarea) return; @@ -184,68 +374,142 @@ export const VoidInputBox2 = forwardRef(fun }; - - const onSelectOption = () => { + const onSelectOption = async () => { if (!options.length) { return; } const option = options[optionIdx]; - const newPath = [...path, option.name] + const newPath = [...optionPath, option.nameInMenu] const isLastOption = !option.generateNextOptions && !option.nextOptions - setPath(newPath) - setNewPathText('') + setOptionPath(newPath) + setOptionText('') setOptionIdx(0) if (isLastOption) { setIsMenuOpen(false) - insertTextAtCursor(`TODO-${option.displayName}`) + insertTextAtCursor(option.nameToPaste) + + const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? { + type: 'File', + uri: option.uri, + language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '', + state: { wasAddedAsCurrentFile: false } + } : option.leafNodeType === 'Folder' ? { + type: 'Folder', + uri: option.uri, + language: undefined, + state: undefined, + } : (undefined as never) + chatThreadService.addNewStagingSelection(newSelection) + console.log('selected', option.uri?.fsPath) } else { - setOptions(getOptionsAtPath(accessor, newPath, '') || []) + const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + setOptions(newOpts) } } - const onRemoveOption = () => { - const newPath = [...path.slice(0, path.length - 1)] - setPath(newPath) - setNewPathText('') + const onRemoveOption = async () => { + const newPath = [...optionPath.slice(0, optionPath.length - 1)] + setOptionPath(newPath) + setOptionText('') setOptionIdx(0) - setOptions(getOptionsAtPath(accessor, newPath, '') || []) + const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + setOptions(newOpts) } - const onOpenOptionMenu = () => { - setPath([]) - setNewPathText('') + const onOpenOptionMenu = async () => { + setOptionPath([]) + setOptionText('') setIsMenuOpen(true); setOptionIdx(0); - setOptions(getOptionsAtPath(accessor, [], '') || []); + const newOpts = await getOptionsAtPath(accessor, [], '') || [] + setOptions(newOpts); } const onCloseOptionMenu = () => { setIsMenuOpen(false); } - const onNavigateUp = () => { + const onNavigateUp = (step = 1, periodic = true) => { if (options.length === 0) return; - setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length); + setOptionIdx((prevIdx) => { + const newIdx = prevIdx - step; + return periodic ? (newIdx + options.length) % options.length : Math.max(0, newIdx); + }); } - const onNavigateDown = () => { + const onNavigateDown = (step = 1, periodic = true) => { if (options.length === 0) return; - setOptionIdx((prevIdx) => (prevIdx + 1) % options.length); + setOptionIdx((prevIdx) => { + const newIdx = prevIdx + step; + return periodic ? newIdx % options.length : Math.min(options.length - 1, newIdx); + }); } - const onPathTextChange = (newStr: string) => { - setNewPathText(newStr); - setOptions(getOptionsAtPath(accessor, path, newStr) || []); - + const onNavigateToTop = () => { + if (options.length === 0) return; + setOptionIdx(0); } + const onNavigateToBottom = () => { + if (options.length === 0) return; + setOptionIdx(options.length - 1); + } + + const debounceTimerRef = useRef(null); + + useEffect(() => { + // Cleanup function to cancel any pending timeouts when unmounting + return () => { + if (debounceTimerRef.current !== null) { + window.clearTimeout(debounceTimerRef.current); + debounceTimerRef.current = null; + } + }; + }, []); + + // debounced + const onPathTextChange = useCallback((newStr: string) => { + + setOptionText(newStr); + + if (debounceTimerRef.current !== null) { + window.clearTimeout(debounceTimerRef.current); + } + + // Set a new timeout to fetch options after a delay + debounceTimerRef.current = window.setTimeout(async () => { + const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || []; + setOptions(newOpts); + setOptionIdx(0); + debounceTimerRef.current = null; + }, 300); + }, [optionPath, accessor]); const onMenuKeyDown = (e: React.KeyboardEvent) => { + + const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey; + if (e.key === 'ArrowUp') { - onNavigateUp(); + if (isCommandKeyPressed) { + onNavigateToTop() + } else { + if (e.altKey) { + onNavigateUp(10, false); + } else { + onNavigateUp(); + } + } } else if (e.key === 'ArrowDown') { - onNavigateDown(); + if (isCommandKeyPressed) { + onNavigateToBottom() + } else { + if (e.altKey) { + onNavigateDown(10, false); + } else { + onNavigateDown(); + } + } } else if (e.key === 'ArrowLeft') { - onSelectOption(); + onRemoveOption(); } else if (e.key === 'ArrowRight') { onSelectOption(); } else if (e.key === 'Enter') { @@ -254,25 +518,26 @@ export const VoidInputBox2 = forwardRef(fun onCloseOptionMenu() } else if (e.key === 'Backspace') { - if (!newPathText) { // No text remaining - if (path.length === 0) { + if (!optionText) { // No text remaining + if (optionPath.length === 0) { onCloseOptionMenu() + return; // don't prevent defaults (backspaces the @ symbol) } else { onRemoveOption(); } } - else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace + else if (isCommandKeyPressed) { // Ctrl+Backspace onPathTextChange('') } else { // Backspace - onPathTextChange(newPathText.slice(0, -1)) + onPathTextChange(optionText.slice(0, -1)) } } else if (e.key.length === 1) { - if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter + if (isCommandKeyPressed) { // Ctrl+letter // do nothing } else { // letter - onPathTextChange(newPathText + e.key) + onPathTextChange(optionText + e.key) } } @@ -281,7 +546,7 @@ export const VoidInputBox2 = forwardRef(fun }; - // scroll the selected optionIdx into view on optionIdx and newPathText changes + // scroll the selected optionIdx into view on optionIdx and optionText changes useEffect(() => { if (isMenuOpen && selectedOptionRef.current) { selectedOptionRef.current.scrollIntoView({ @@ -290,9 +555,7 @@ export const VoidInputBox2 = forwardRef(fun inline: 'nearest', }); } - }, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]); - - + }, [optionIdx, isMenuOpen, optionText, selectedOptionRef]); const measureRef = useRef(null); const gapPx = 2 @@ -307,7 +570,7 @@ export const VoidInputBox2 = forwardRef(fun } = useFloating({ open: isMenuOpen, onOpenChange: setIsMenuOpen, - placement: 'top', + placement: 'bottom', middleware: [ offset({ mainAxis: gapPx, crossAxis: offsetPx }), @@ -320,13 +583,9 @@ export const VoidInputBox2 = forwardRef(fun padding: 8, }), size({ - apply({ availableHeight, elements, rects }) { - const maxHeight = Math.min(availableHeight) - + apply({ elements, rects }) { + // Just set width on the floating element and let content handle scrolling Object.assign(elements.floating.style, { - maxHeight: `${maxHeight}px`, - overflowY: 'auto', - // Ensure the width isn't constrained by the parent width: `${Math.max( rects.reference.width, measureRef.current?.offsetWidth ?? 0 @@ -364,7 +623,7 @@ export const VoidInputBox2 = forwardRef(fun document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [isMenuOpen, refs.floating, refs.reference]); - + // logic for @ to mention ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const [isEnabled, setEnabled] = useState(true) @@ -467,11 +726,10 @@ export const VoidInputBox2 = forwardRef(fun rows={1} placeholder={placeholder} /> -
{`idx ${optionIdx}`}
{isMenuOpen && (
(fun }} onWheel={(e) => e.stopPropagation()} > -
- {/* Path navigation breadcrumbs */} -
- {[...path, newPathText].join(' > ')} + {/* Breadcrumbs Header */} +
+ {optionPath.length || optionText ? +
+ {optionPath.map((path, index) => ( + + {path} + + + ))} + {optionText} +
+ :
Enter text to filter...
+ } +
+ + + {/* Options list */} +
+
+ {options.length === 0 ? +
No results found
+ : options.map((o, oIdx) => { + + return ( + // Option +
{ onSelectOption(); }} + onMouseOver={() => { setOptionIdx(oIdx) }} + > + {} + {o.nameInMenu} + {o.nextOptions || o.generateNextOptions ? ( + + ) : null} +
+ ) + }) + }
- - {/* Options list */} - {options.length === 0 ? ( -
No options available
- ) : ( - options.map((o, oIdx) => ( -
{ onSelectOption(); }} - > -
- {o.displayName} - {o.nextOptions || o.generateNextOptions ? ( - - - - ) : null} -
-
- )) - )}
)} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index a38f6401..6bd07485 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -49,6 +49,7 @@ import { INativeHostService } from '../../../../../../../platform/native/common/ import { IEditCodeService } from '../../../editCodeServiceInterface.js' import { IToolsService } from '../../../toolsService.js' import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js' +import { ISearchService } from '../../../../../../services/search/common/search.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -204,6 +205,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ILanguageDetectionService: accessor.get(ILanguageDetectionService), ILanguageFeaturesService: accessor.get(ILanguageFeaturesService), IKeybindingService: accessor.get(IKeybindingService), + ISearchService: accessor.get(ISearchService), IExplorerService: accessor.get(IExplorerService), IEnvironmentService: accessor.get(IEnvironmentService), diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 87eafad0..8281fc77 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -465,6 +465,7 @@ const VoidOnboardingContent = () => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') + const voidMetricsService = accessor.get('IMetricsService') const voidSettingsState = useSettingsState() @@ -480,6 +481,7 @@ const VoidOnboardingContent = () => { const [selectedPrivateProvider, setSelectedPrivateProvider] = useState('ollama'); const [selectedAffordableProvider, setSelectedAffordableProvider] = useState('gemini'); const [selectedAllProvider, setSelectedAllProvider] = useState('anthropic'); + const [didDoubleClickSkip, setDidDoubleClickSkip] = useState(false) // Helper function to get the current selected provider based on active tab const getSelectedProvider = (): ProviderName => { @@ -535,7 +537,10 @@ const VoidOnboardingContent = () => { onClick={() => { setPageIndex(pageIndex - 1) }} /> { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }} + onClick={() => { + voidSettingsService.setGlobalSetting('isOnboardingComplete', true); + voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption }) + }} ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined} >Enter the Void
@@ -618,15 +623,16 @@ const VoidOnboardingContent = () => {
+
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index d6c85982..095e0e07 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -30,31 +30,6 @@ import { getActiveWindow } from '../../../../base/browser/dom.js'; // ---------- Register commands and keybindings ---------- -const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { - if (!currentSelections) return null - - for (let i = 0; i < currentSelections.length; i += 1) { - const s = currentSelections[i] - - if (s.uri.fsPath !== newSelection.uri.fsPath) continue - - if (s.type === 'File' && newSelection.type === 'File') { - return i - } - if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') { - if (s.uri.fsPath !== newSelection.uri.fsPath) continue - // if there's any collision return true - const [oldStart, oldEnd] = s.range - const [newStart, newEnd] = newSelection.range - if (oldStart !== newStart || oldEnd !== newEnd) continue - return i - } - if (s.type === 'Folder' && newSelection.type === 'Folder') { - return i - } - } - return null -} export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => { if (!range) @@ -104,8 +79,6 @@ registerAction2(class extends Action2 { }) - - // Action: when press ctrl+L, show the sidebar chat and add to the selection const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select' registerAction2(class extends Action2 { @@ -147,36 +120,9 @@ registerAction2(class extends Action2 { state: { wasAddedAsCurrentFile: false } } - // update the staging selections const chatThreadService = accessor.get(IChatThreadService) - const focusedMessageIdx = chatThreadService.getCurrentFocusedMessageIdx() - - // set the selections to the proper value - let selections: StagingSelectionItem[] = [] - let setSelections = (s: StagingSelectionItem[]) => { } - - if (focusedMessageIdx === undefined) { - selections = chatThreadService.getCurrentThreadState().stagingSelections - setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s }) - } else { - selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections - setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) - } - - // if matches with existing selection, overwrite (since text may change) - const idx = findStagingSelectionIndex(selections, newSelection) - if (idx !== null && idx !== -1) { - setSelections([ - ...selections!.slice(0, idx), - newSelection, - ...selections!.slice(idx + 1, Infinity) - ]) - } - // if no match, add it - else { - setSelections([...(selections ?? []), newSelection]) - } + chatThreadService.addNewStagingSelection(newSelection) } }); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index bdacb18e..be394b7b 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -308,6 +308,7 @@ export class ToolsService implements IToolsService { const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, includePattern: includePattern ?? undefined, + sortByScore: true, // makes results 10x better }) const data = await searchService.fileSearch(query, CancellationToken.None) From d67cfb5b33f53529b2ea19a4f02cc0ec2d3db910 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 21 Apr 2025 02:27:21 -0700 Subject: [PATCH 02/27] small --- src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4b8375da..d9d1f5b4 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 @@ -363,7 +363,7 @@ export const VoidInputBox2 = forwardRef(fun // The most reliable way to simulate typing is to use execCommand // which will trigger all the appropriate native events - document.execCommand('insertText', false, text); + document.execCommand('insertText', false, text + ' '); // add space after too // React's onChange relies on a SyntheticEvent system // The best way to ensure it runs is to call callbacks directly From c52a754076eea8bdaa2d77c431bd7dd4f1b911a3 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 21 Apr 2025 05:18:24 -0700 Subject: [PATCH 03/27] lots of ux --- .../react/src/sidebar-tsx/SidebarChat.tsx | 35 ++++++++++++++++--- .../src/sidebar-tsx/SidebarThreadSelector.tsx | 8 ++--- .../src/void-onboarding/VoidOnboarding.tsx | 22 ++++++++---- 3 files changed, 49 insertions(+), 16 deletions(-) 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 60441371..d0f136f3 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 @@ -1422,7 +1422,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
: null - return
+ return
{approveButton} {cancelButton} {approvalToggle} @@ -2714,15 +2714,15 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - const onSubmit = useCallback(async () => { + const onSubmit = useCallback(async (_forceSubmit?: string) => { - if (isDisabled) return + if (isDisabled && !_forceSubmit) return if (isRunning) return const threadId = chatThreadsService.state.currentThreadId // send message to LLM - const userMessage = textAreaRef.current?.value ?? '' + const userMessage = _forceSubmit || textAreaRef.current?.value || '' try { await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, threadId }) @@ -2884,6 +2884,26 @@ export const SidebarChat = () => { const isLandingPage = previousMessages.length === 0 + const initiallySuggestedPromptsHTML =
+ {[ + 'Summarize my codebase', + 'How do types work in Rust?', + 'Create a .voidrules file for me' + ].map((text, index) => ( +
onSubmit(text)} + > + {text} +
+ ))} +
+ + + console.log('!!!', Object.keys(chatThreadsState.allThreads).length) + + const threadPageInput =
@@ -2907,11 +2927,16 @@ export const SidebarChat = () => { {landingPageInput} - {Object.values(chatThreadsState.allThreads).length > 0 && // show if there are threads + {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads
Previous Threads
+ : + +
Suggestions
+ {initiallySuggestedPromptsHTML} +
}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 6e96aa22..6276f36b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -176,8 +176,8 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { return (
- {displayThreads.length === 0 - ? <> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo + {displayThreads.length === 0 // this should never happen + ? <> : displayThreads.map((threadId, i) => { const pastThread = allThreads[threadId]; if (!pastThread) { @@ -199,7 +199,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { {hasMoreThreads && !showAll && (
setShowAll(true)} > Show {sortedThreadIds.length - numInitialThreads} more... @@ -207,7 +207,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { )} {hasMoreThreads && showAll && (
setShowAll(false)} > Show less diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 8281fc77..31031bc5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -131,16 +131,25 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch // prev/next const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes) => { + + // Create a new props object without the disabled attribute + const { disabled, ...buttonProps } = props; + return ( @@ -481,7 +490,6 @@ const VoidOnboardingContent = () => { const [selectedPrivateProvider, setSelectedPrivateProvider] = useState('ollama'); const [selectedAffordableProvider, setSelectedAffordableProvider] = useState('gemini'); const [selectedAllProvider, setSelectedAllProvider] = useState('anthropic'); - const [didDoubleClickSkip, setDidDoubleClickSkip] = useState(false) // Helper function to get the current selected provider based on active tab const getSelectedProvider = (): ProviderName => { From c689dcad124f2774af6435dd4df4af87dc70c416 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 21 Apr 2025 05:19:23 -0700 Subject: [PATCH 04/27] accurate space --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0f136f3..80b8d715 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 @@ -1422,7 +1422,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
: null - return
+ return
{approveButton} {cancelButton} {approvalToggle} From 19313c7e5d8ca63e5d20e548d5849ffbd042f67a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 21 Apr 2025 05:31:50 -0700 Subject: [PATCH 05/27] ux --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 80b8d715..609215b3 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 @@ -756,12 +756,11 @@ const ToolHeaderWrapper = ({
{info && } {isError && {
{
Date: Mon, 21 Apr 2025 14:45:20 -0700 Subject: [PATCH 06/27] ajv --- package-lock.json | 146 ++++++++++++++++++++++++++-------------------- package.json | 1 + 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc4edf47..330e6147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.98", "@xterm/headless": "^5.6.0-beta.98", "@xterm/xterm": "^5.6.0-beta.98", + "ajv": "^8.17.1", "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.4", @@ -1686,6 +1687,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.25.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", @@ -5477,16 +5502,15 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -5511,30 +5535,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -9272,6 +9272,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -9285,6 +9302,13 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -9743,7 +9767,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -9788,7 +9811,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, "funding": [ { "type": "github", @@ -9898,6 +9920,23 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/file-loader/node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -9908,6 +9947,13 @@ "ajv": "^6.9.1" } }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/file-loader/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -14925,10 +14971,9 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -20618,7 +20663,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21109,30 +21153,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/scope-tailwind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz", diff --git a/package.json b/package.json index de9b869e..23fd68a8 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@xterm/addon-webgl": "^0.19.0-beta.98", "@xterm/headless": "^5.6.0-beta.98", "@xterm/xterm": "^5.6.0-beta.98", + "ajv": "^8.17.1", "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.4", From 3de9faed5c21a240fb3d02f2dc3d014592c3970e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 21 Apr 2025 16:37:31 -0700 Subject: [PATCH 07/27] fix katex? --- .../react/src/markdown/ChatMarkdownRender.tsx | 95 +++++++++---------- .../void/browser/react/src/util/inputs.tsx | 2 +- 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 36ac3e53..e3fadf98 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -5,9 +5,6 @@ import React, { JSX, useMemo, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' -import katex from 'katex' -import 'katex/dist/katex.min.css' -import dompurify from '../../../../../../../base/browser/dompurify/dompurify.js' import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js' import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js' @@ -36,59 +33,59 @@ function isValidUri(s: string): boolean { // renders contiguous string of latex eg $e^{i\pi}$ const LatexRender = ({ latex }: { latex: string }) => { + return {latex} + // try { + // let formula = latex; + // let displayMode = false; - try { - let formula = latex; - let displayMode = false; + // // Extract the formula from delimiters + // if (latex.startsWith('$') && latex.endsWith('$')) { + // // Check if it's display math $$...$$ + // if (latex.startsWith('$$') && latex.endsWith('$$')) { + // formula = latex.slice(2, -2); + // displayMode = true; + // } else { + // formula = latex.slice(1, -1); + // } + // } else if (latex.startsWith('\\(') && latex.endsWith('\\)')) { + // formula = latex.slice(2, -2); + // } else if (latex.startsWith('\\[') && latex.endsWith('\\]')) { + // formula = latex.slice(2, -2); + // displayMode = true; + // } - // Extract the formula from delimiters - if (latex.startsWith('$') && latex.endsWith('$')) { - // Check if it's display math $$...$$ - if (latex.startsWith('$$') && latex.endsWith('$$')) { - formula = latex.slice(2, -2); - displayMode = true; - } else { - formula = latex.slice(1, -1); - } - } else if (latex.startsWith('\\(') && latex.endsWith('\\)')) { - formula = latex.slice(2, -2); - } else if (latex.startsWith('\\[') && latex.endsWith('\\]')) { - formula = latex.slice(2, -2); - displayMode = true; - } + // // Render LaTeX + // const html = katex.renderToString(formula, { + // displayMode: displayMode, + // throwOnError: false, + // output: 'html' + // }); - // Render LaTeX - const html = katex.renderToString(formula, { - displayMode: displayMode, - throwOnError: false, - output: 'html' - }); + // // Sanitize the HTML output with DOMPurify + // const sanitizedHtml = dompurify.sanitize(html, { + // RETURN_TRUSTED_TYPE: true, + // USE_PROFILES: { html: true, svg: true, mathMl: true } + // }); - // Sanitize the HTML output with DOMPurify - const sanitizedHtml = dompurify.sanitize(html, { - RETURN_TRUSTED_TYPE: true, - USE_PROFILES: { html: true, svg: true, mathMl: true } - }); + // // Add proper styling based on mode + // const className = displayMode + // ? 'katex-block my-2 text-center' + // : 'katex-inline'; - // Add proper styling based on mode - const className = displayMode - ? 'katex-block my-2 text-center' - : 'katex-inline'; + // // Use the ref approach to avoid dangerouslySetInnerHTML + // const mathRef = React.useRef(null); - // Use the ref approach to avoid dangerouslySetInnerHTML - const mathRef = React.useRef(null); + // React.useEffect(() => { + // if (mathRef.current) { + // mathRef.current.innerHTML = sanitizedHtml as unknown as string; + // } + // }, [sanitizedHtml]); - React.useEffect(() => { - if (mathRef.current) { - mathRef.current.innerHTML = sanitizedHtml as unknown as string; - } - }, [sanitizedHtml]); - - return ; - } catch (error) { - console.error('KaTeX rendering error:', error); - return {latex}; - } + // return ; + // } catch (error) { + // console.error('KaTeX rendering error:', error); + // return {latex}; + // } } const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { 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 91281214..69132cfd 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 @@ -467,7 +467,7 @@ export const VoidInputBox2 = forwardRef(fun rows={1} placeholder={placeholder} /> -
{`idx ${optionIdx}`}
+ {/*
{`idx ${optionIdx}`}
*/} {isMenuOpen && (
Date: Tue, 22 Apr 2025 12:30:02 +0200 Subject: [PATCH 08/27] Update VoidOnboarding.tsx --- .../void/browser/react/src/void-onboarding/VoidOnboarding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 87eafad0..14150f3e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -590,7 +590,7 @@ const VoidOnboardingContent = () => { {/* Slice of Void image */}
- + {/* */}
From 94c3d38dc90681149e496e3c7106f352b3bd2bcb Mon Sep 17 00:00:00 2001 From: Dylan-86 <58813956+Dylan-86@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:05:13 +0200 Subject: [PATCH 09/27] Update HOW_TO_CONTRIBUTE.md --- HOW_TO_CONTRIBUTE.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index 15323527..c010f385 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -42,7 +42,7 @@ Go to the "Individual Components" tab and select: Finally, click Install. -### c. Build Prerequisites - Linux +### c. Linux - Build Prerequisites First, run `npm install -g node-gyp`. Then: @@ -50,12 +50,12 @@ First, run `npm install -g node-gyp`. Then: - Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`. - Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). -### d. Building Void +### d. Building Void from Visual Studio Code To build Void, open `void/` inside VSCode. Then open your terminal and run: 1. `npm install` to install all dependencies. -2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`). +2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`). If you get an error, try running it with `NODE_OPTIONS="--max-old-space-size=8192`, for example `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. 3. Build Void. - Press Cmd+Shift+B (Mac). - Press Ctrl+Shift+B (Windows/Linux). @@ -70,7 +70,11 @@ To build Void, open `void/` inside VSCode. Then open your terminal and run: #### Building Void from Terminal -Alternatively, if you want to build Void from the terminal, instead of pressing Cmd+Shift+B you can run `npm run watch`. The build is done when you see something like this: +Alternatively, if you want to build Void from the terminal, you can follow these steps. +1. Clone this repo with `git clone https://github.com/voideditor/void/`. +2. Go inside the "void" folder with `cd void` and then run `npm install`. This will install all dependencies. It can take a few minutes. +3. Now run `npm run buildreact`). If you get an error, try running it with `NODE_OPTIONS="--max-old-space-size=8192`, for example `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. +The build is done when you see something like this: ``` [watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms @@ -79,6 +83,10 @@ Alternatively, if you want to build Void from the terminal, instead of pressing [watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms ``` +5. Now you can run void by simply typing the following commands (the first time you run, it can take several minutes to load): + - Mac/Linux: Run `./scripts/code.sh` . + - Windows: Run `./scripts/code.bat`. + #### Common Fixes From d1db3946931baa76cea20681f410083b2bc7a55c Mon Sep 17 00:00:00 2001 From: Dylan-86 <58813956+Dylan-86@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:24:25 +0200 Subject: [PATCH 10/27] Updated instructions based on my quite extensive experience with Void on Linux/Ubuntu --- HOW_TO_CONTRIBUTE.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index c010f385..21516eb0 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -63,6 +63,17 @@ To build Void, open `void/` inside VSCode. Then open your terminal and run: 4. Run Void. - Run `./scripts/code.sh` (Mac/Linux). - Run `./scripts/code.bat` (Windows). +5. On Linux, If you get this error with code.sh : + +``` +[366157:0422/132119.648030:FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/user/Applications/void/void/.build/electron/chrome-sandbox is owned by root and has mode 4755. +Trace/breakpoint trap (core dumped) +``` + +Simply run +`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` +and then run again `./scripts/code.sh` - it should fix the problem and launch Void in a few seconds. + 6. Nice-to-knows. - You can always press Ctrl+R (Cmd+R) inside the new window to reload and see your new changes. It's faster than Ctrl+Shift+P and `Reload Window`. - You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing. @@ -86,7 +97,14 @@ The build is done when you see something like this: 5. Now you can run void by simply typing the following commands (the first time you run, it can take several minutes to load): - Mac/Linux: Run `./scripts/code.sh` . - Windows: Run `./scripts/code.bat`. - +6. If you get this error when running code.sh on Linux: +``` +[366157:0422/132119.648030:FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/user/Applications/void/void/.build/electron/chrome-sandbox is owned by root and has mode 4755. +Trace/breakpoint trap (core dumped) +``` +Simply run +`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` +and then run again `./scripts/code.sh` - it should fix the problem and launch Void in a few seconds. #### Common Fixes From 1a45baa23e50c3b2283945524c07250c7c329b8b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 23 Apr 2025 19:32:37 -0700 Subject: [PATCH 11/27] fix persistent terminal id? --- .../contrib/void/browser/toolsService.ts | 41 +++++++++---------- .../contrib/void/common/prompt/prompts.ts | 6 +-- .../contrib/void/common/toolsServiceTypes.ts | 6 +-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index bdacb18e..dd7840a8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -92,7 +92,7 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { } const validateProposedTerminalId = (terminalIdUnknown: unknown) => { - if (!terminalIdUnknown) return '1' + if (!terminalIdUnknown) throw new Error(`A value for terminalID must be specified, but the value was "${terminalIdUnknown}"`) const terminalId = terminalIdUnknown + '' return terminalId } @@ -251,19 +251,19 @@ export class ToolsService implements IToolsService { // --- run_command: (params: RawToolParamsObj) => { - const { command: commandUnknown, terminal_id: terminalIdUnknown } = params; + const { command: commandUnknown, persistent_terminal_id: terminalIdUnknown } = params; const command = validateStr('command', commandUnknown); - const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null; - return { command, bgTerminalId: proposedTerminalId }; + const persistentTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null; + return { command, persistentTerminalId }; }, open_persistent_terminal: (_params: RawToolParamsObj) => { // No parameters needed; will open a new background terminal return {}; }, kill_persistent_terminal: (params: RawToolParamsObj) => { - const { terminal_id: terminalIdUnknown } = params; - const terminalId = validateProposedTerminalId(terminalIdUnknown); - return { terminalId }; + const { persistent_terminal_id: terminalIdUnknown } = params; + const persistentTerminalId = validateProposedTerminalId(terminalIdUnknown); + return { persistentTerminalId }; }, } @@ -414,21 +414,20 @@ export class ToolsService implements IToolsService { return { result: lintErrorsPromise, interruptTool } }, // --- - run_command: async ({ command, bgTerminalId }) => { - const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId) + run_command: async ({ command, persistentTerminalId }) => { + const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, persistentTerminalId) const interruptTool = () => { this.terminalToolService.killTerminal(terminalId) } return { result: resPromise, interruptTool } }, open_persistent_terminal: async () => { - // Open a new background terminal without waiting for completion - const terminalId = await this.terminalToolService.createTerminal() - return { result: { terminalId } } + const persistentTerminalId = await this.terminalToolService.createTerminal() + return { result: { persistentTerminalId } } }, - kill_persistent_terminal: async ({ terminalId }) => { + kill_persistent_terminal: async ({ persistentTerminalId }) => { // Close the background terminal by sending exit - await this.terminalToolService.killTerminal(terminalId) + await this.terminalToolService.killTerminal(persistentTerminalId) return { result: {} } }, @@ -497,18 +496,18 @@ export class ToolsService implements IToolsService { resolveReason, result: result_, } = result - const { bgTerminalId } = params + const { persistentTerminalId } = params // success if (resolveReason.type === 'done') { - const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : '' + const desc = persistentTerminalId ? ` in terminal ${persistentTerminalId}` : '' return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}` } // bg command - if (bgTerminalId !== null) { + if (persistentTerminalId !== 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_}` + return `Terminal command is running in the background in terminal ${persistentTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}` } } // normal command @@ -521,11 +520,11 @@ export class ToolsService implements IToolsService { throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) }, open_persistent_terminal: (_params, result) => { - const { terminalId } = result; - return `Successfully created background terminal with ID ${terminalId}`; + const { persistentTerminalId } = result; + return `Successfully created background terminal with ID ${persistentTerminalId}`; }, kill_persistent_terminal: (params, _result) => { - return `Successfully closed terminal ${params.terminalId}.`; + return `Successfully closed terminal ${params.persistentTerminalId}.`; }, } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index f1305896..e9e34117 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -221,7 +221,7 @@ Here's an example of a good output:\n${editToolDescriptionExample}` 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 (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, params: { command: { description: 'The terminal command to run.' }, - bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' }, + persistent_terminal_id: { description: 'Optional. Runs the command in the persistent terminal that you created with open_persistent_terminal.' }, }, }, @@ -232,8 +232,8 @@ Here's an example of a good output:\n${editToolDescriptionExample}` }, kill_persistent_terminal: { name: 'kill_persistent_terminal', - description: `Closes a BG terminal with the given ID.`, - params: { terminal_id: { description: `The terminal ID to interrupt and close.` } } + description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`, + params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } } } diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index b234689c..d09a7d87 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -46,9 +46,9 @@ export type ToolCallParams = { 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, // --- - 'run_command': { command: string; bgTerminalId: string | null }, + 'run_command': { command: string; persistentTerminalId: string | null }, 'open_persistent_terminal': {}, - 'kill_persistent_terminal': { terminalId: string }, + 'kill_persistent_terminal': { persistentTerminalId: string }, } // RESULT OF TOOL CALL @@ -66,7 +66,7 @@ export type ToolResultType = { 'delete_file_or_folder': {}, // --- 'run_command': { result: string; resolveReason: TerminalResolveReason; }, - 'open_persistent_terminal': { terminalId: string }, + 'open_persistent_terminal': { persistentTerminalId: string }, 'kill_persistent_terminal': {}, } From 71fc2895a8db40172ecc482de8bf7d7956380c9b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 23 Apr 2025 20:46:30 -0700 Subject: [PATCH 12/27] fix persistent terminal ID --- .../react/src/sidebar-tsx/SidebarChat.tsx | 53 +++++++++---------- .../void/browser/terminalToolService.ts | 52 ++++++++++-------- .../contrib/void/browser/toolsService.ts | 8 +-- .../contrib/void/common/prompt/prompts.ts | 1 + 4 files changed, 59 insertions(+), 55 deletions(-) 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 70e4000e..a35a1ca2 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 @@ -30,6 +30,7 @@ import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import ErrorBoundary from './ErrorBoundary.js'; import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; +import { terminalNameOfId } from '../../../terminalToolService.js'; @@ -683,6 +684,7 @@ type ToolHeaderParams = { children?: React.ReactNode; bottomChildren?: React.ReactNode; onClick?: () => void; + desc2OnClick?: () => void; isOpen?: boolean; className?: string; } @@ -700,6 +702,7 @@ const ToolHeaderWrapper = ({ bottomChildren, isError, onClick, + desc2OnClick, isOpen, isRejected, className, // applies to the main content @@ -769,7 +772,7 @@ const ToolHeaderWrapper = ({ data-tooltip-content={'Canceled'} data-tooltip-place='top' />} - {desc2 && + {desc2 && {desc2} } {numResults !== undefined && ( @@ -1317,7 +1320,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, - desc1Info: toolParams.bgTerminalId + desc1Info: toolParams.persistentTerminalId } }, 'open_persistent_terminal': () => { @@ -1326,7 +1329,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName }, 'kill_persistent_terminal': () => { const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal'] - return { desc1: toolParams.terminalId } + return { desc1: toolParams.persistentTerminalId } }, 'get_dir_tree': () => { const toolParams = _toolParams as ToolCallParams['get_dir_tree'] @@ -2053,6 +2056,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') + const toolsService = accessor.get('IToolsService') const isError = toolMessage.type === 'tool_error' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) @@ -2062,19 +2066,21 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const { command, persistentTerminalId } = params + if (persistentTerminalId) { + componentParams.desc2 = terminalNameOfId(persistentTerminalId) + componentParams.desc2OnClick = () => terminalToolsService.focusTerminal(persistentTerminalId) + } + + if (toolMessage.type === 'success') { const { result } = toolMessage - const { command } = params - const { resolveReason, result: terminalResult } = result // it's unclear that this is a button and not an icon. // componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} // /> - - const additionalDetailsStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) - : resolveReason.type === 'timeout' ? `\n(timed out)` - : null + const msg = toolsService.stringOfResult['run_command'](params, result) componentParams.children =
@@ -2082,25 +2088,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, {`Ran command: `} {command}
- {(terminalResult + additionalDetailsStr).length &&
+ {msg.length &&
{`Result: `} - {terminalResult} - {additionalDetailsStr} + {msg}
}
- - if (params.bgTerminalId) - componentParams.desc2 = `(terminal ${params.bgTerminalId})` - } else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error' || toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - 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 componentParams.children = {result} @@ -2130,12 +2125,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'success') { const { result } = toolMessage - const { terminalId } = result - if (terminalId) { - componentParams.desc2 = `(terminal ${terminalId})` - if (terminalToolsService.terminalExists(terminalId)) - componentParams.onClick = () => terminalToolsService.focusTerminal(terminalId) - } + const { persistentTerminalId } = result + componentParams.desc1 = terminalNameOfId(persistentTerminalId) + componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId) } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage @@ -2153,6 +2145,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const terminalToolsService = accessor.get('ITerminalToolService') const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const title = getTitle(toolMessage) @@ -2167,7 +2160,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } if (toolMessage.type === 'success') { - const { result } = toolMessage + const { persistentTerminalId } = params + componentParams.desc1 = terminalNameOfId(persistentTerminalId) + componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId) } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 7abc98d6..d8f0efb0 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.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 { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'; import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; @@ -39,11 +39,11 @@ function isCommandComplete(output: string) { } -const nameOfId = (id: string) => { +export const terminalNameOfId = (id: string) => { if (id === '1') return 'Void Agent' return `Void Agent (${id})` } -const idOfName = (name: string) => { +export const idOfTerminalName = (name: string) => { if (name === 'Void Agent') return '1' const match = name.match(/Void Agent \((\d+)\)/) @@ -66,7 +66,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ const initializeTerminal = (terminal: ITerminalInstance) => { // when exit, remove const d = terminal.onExit(() => { - const terminalId = idOfName(terminal.title) + const terminalId = idOfTerminalName(terminal.title) if (terminalId !== null && (terminalId in this.terminalInstanceOfId)) delete this.terminalInstanceOfId[terminalId] d.dispose() }) @@ -75,7 +75,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // initialize any terminals that are already open for (const terminal of terminalService.instances) { - const proposedTerminalId = idOfName(terminal.title) + const proposedTerminalId = idOfTerminalName(terminal.title) if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal initializeTerminal(terminal) @@ -111,7 +111,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ location: TerminalLocation.Panel, - config: { name: nameOfId(terminalId), title: nameOfId(terminalId) }, + config: { name: terminalNameOfId(terminalId), title: terminalNameOfId(terminalId) }, }) @@ -207,26 +207,34 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // send the command here await terminal.sendText(command, true) - // inactivity-based timeout - const waitUntilInactive = new Promise(res => { - let globalTimeoutId: ReturnType; - const resetTimer = () => { - clearTimeout(globalTimeoutId); - globalTimeoutId = setTimeout(() => { - if (resolveReason) return - + const waitUntilInterrupt = isBG ? + // timeout after X seconds + new Promise((res) => { + setTimeout(() => { resolveReason = { type: 'timeout' }; - res(); - }, MAX_TERMINAL_INACTIVE_TIME * 1000); - }; + res() + }, MAX_TERMINAL_BG_COMMAND_TIME * 1000) + }) + // inactivity-based timeout + : new Promise(res => { + let globalTimeoutId: ReturnType; + const resetTimer = () => { + clearTimeout(globalTimeoutId); + globalTimeoutId = setTimeout(() => { + if (resolveReason) return - const dTimeout = terminal.onData(() => { resetTimer(); }); - disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId))); - resetTimer(); - }); + 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,]) + await Promise.any([waitUntilDone, waitUntilInterrupt]) disposables.forEach(d => d.dispose()) if (!isBG) { diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index dd7840a8..8fefb605 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 { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' +import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' @@ -507,7 +507,7 @@ export class ToolsService implements IToolsService { // bg command if (persistentTerminalId !== null) { if (resolveReason.type === 'timeout') { - return `Terminal command is running in the background in terminal ${persistentTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}` + return `Terminal command is running in terminal ${persistentTerminalId}. Here are the current outputs (after ${MAX_TERMINAL_BG_COMMAND_TIME} seconds):\n${result_}` } } // normal command @@ -521,10 +521,10 @@ export class ToolsService implements IToolsService { }, open_persistent_terminal: (_params, result) => { const { persistentTerminalId } = result; - return `Successfully created background terminal with ID ${persistentTerminalId}`; + return `Successfully created persistent terminal. persistentTerminalId="${persistentTerminalId}"`; }, kill_persistent_terminal: (params, _result) => { - return `Successfully closed terminal ${params.persistentTerminalId}.`; + return `Successfully closed terminal "${params.persistentTerminalId}".`; }, } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index e9e34117..71795a26 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -27,6 +27,7 @@ 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 +export const MAX_TERMINAL_BG_COMMAND_TIME = 5 // Maximum character limits for prefix and suffix context From 735b4038209230f5fc767c589114889c6bcd54a4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 23 Apr 2025 21:25:10 -0700 Subject: [PATCH 13/27] improve prompting --- .../contrib/void/common/prompt/prompts.ts | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 71795a26..0e90fd92 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -44,12 +44,12 @@ const changesExampleContent = `\ // {{change 3}} // ... existing code ...` -const editToolDescriptionExample = `\ +const editToolDiffExample = `\ ${tripleTick[0]} ${changesExampleContent} ${tripleTick[1]}` -const fileNameEditExample = `${tripleTick[0]}typescript +const chatSuggestionDiffExample = `${tripleTick[0]}typescript /Users/username/Dekstop/my_project/app.ts ${changesExampleContent} ${tripleTick[1]}` @@ -91,6 +91,13 @@ const paginationParam = { // [K in keyof T as SnakeCase>]: T[K] // }; +const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\ +${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \ +Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \ +Your DIFF MUST be wrapped in triple backticks. \ +NEVER re-write the whole file. Always bias towards writing as little as possible. \ +Use comments like "// ... existing code ..." to condense your writing. \ +Here's an example of a good output:\n${type === 'edit tool' ? editToolDiffExample : chatSuggestionDiffExample}` export const voidTools = { @@ -206,13 +213,7 @@ export const voidTools = { params: { ...uriParam('file'), change_diff: { - description: `\ -A code diff describing the change to make to the file. \ -Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \ -Your DIFF MUST be wrapped in triple backticks. \ -NEVER re-write the whole file. Always bias towards writing as little as possible. \ -Use comments like "// ... existing code ..." to condense your writing. \ -Here's an example of a good output:\n${editToolDescriptionExample}` + description: applyToolDescription('edit tool') } }, }, @@ -330,16 +331,16 @@ Please assist the user with their query.`) - ${os} -- Open workspaces: -${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'} +- The user's workspace contains these folders: +${workspaceFolders.join('\n') || 'NO FOLDERS OPEN'} - Active file: ${activeURI} - Open files: -${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? ` +${openedURIs.join('\n') || 'NO OPENED FILES'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? ` -- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''} +- Existing persistent terminal IDs: ${runningTerminalIds.join(', ')}` : ''} `) @@ -369,7 +370,7 @@ ${directoryStr} details.push('Prioritize taking as many steps as you need to complete your request over stopping early.') details.push(`You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.`) details.push(`ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`) - details.push(`NEVER modify a file outside the user's workspace(s) without permission from the user.`) + details.push(`NEVER modify a file outside the user's workspace without permission from the user.`) } if (mode === 'gather') { @@ -384,12 +385,8 @@ ${directoryStr} - The remaining contents of the file should proceed as usual.`) details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S). -- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit). -- The remaining contents should be \ -a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \ -NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \ -Here's an example of a good edit suggestion: -${fileNameEditExample}.`) +- The first line of the code block must be the FULL PATH of the related file. +- The remaining contents should be ${applyToolDescription('chat suggestion')}`) } details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`) From 1e5f9808b4069676cdd88bd767449bbda3d31d0d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 19:57:21 -0700 Subject: [PATCH 14/27] edit_file -> replace_in_file (SEARCH/REPLACE blocks) --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/editCodeService.ts | 452 ++++++++++-------- .../void/browser/editCodeServiceInterface.ts | 1 + .../src/markdown/ApplyBlockHoverButtons.tsx | 18 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 29 +- .../contrib/void/browser/toolsService.ts | 37 +- .../contrib/void/common/prompt/prompts.ts | 168 ++++--- .../contrib/void/common/toolsServiceTypes.ts | 6 +- 8 files changed, 367 insertions(+), 346 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index baae23e4..a4444838 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -506,7 +506,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { return {} } // once validated, add checkpoint for edit - if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } + if (toolName === 'replace_in_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['replace_in_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b310c355..e21b1776 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1164,6 +1164,9 @@ class EditCodeService extends Disposable implements IEditCodeService { } + public instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }: { uri: URI, searchReplaceBlocks: string }) { + this._instantlyApplySRBlocks(uri, searchReplaceBlocks) + } private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { @@ -1509,6 +1512,77 @@ class EditCodeService extends Disposable implements IEditCodeService { } + private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { + + const descStr = str === `Not found` ? + `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : str === `Not unique` ? + `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : str === 'Has overlap' ? + `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : `` + + // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has + // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') + // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' + // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' + // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` + const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' + const errMsg = `${descStr}\n${soFarStr}` + return errMsg + + } + + + private _instantlyApplySRBlocks(uri: URI, blocksStr: string) { + const blocks = extractSearchReplaceBlocks(blocksStr) + if (blocks.length === 0) throw new Error(`No Search/Replace blocks were received!`) + + const { model } = this._voidModelService.getModel(uri) + if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`) + const modelStr = model.getValue(EndOfLinePreference.LF) + + const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = [] + for (const b of blocks) { + const i = modelStr.indexOf(b.orig) + if (i === -1) + throw new Error(this._errContentOfInvalidStr('Not found', replacements[i].block.orig)) + const j = modelStr.lastIndexOf(b.orig) + if (i !== j) + throw new Error(this._errContentOfInvalidStr('Not unique', replacements[i].block.orig)) + + replacements.push({ + origStart: i, + origEnd: i + b.orig.length - 1, // INCLUSIVE + block: b, + }) + } + + // sort in increasing order + replacements.sort((a, b) => a.origStart - b.origStart) + + // ensure no overlap + for (let i = 1; i < replacements.length; i++) { + if (replacements[i].origStart < replacements[i - 1].origEnd) { + // There's an overlap + throw new Error(this._errContentOfInvalidStr('Has overlap', replacements[i].block.orig)) + } + } + + // apply each replacement from right to left (so indexes don't shift) + let newCode: string = modelStr + for (let i = replacements.length - 1; i >= 0; i--) { + const { origStart, origEnd, block } = replacements[i] + newCode = newCode.slice(0, origStart) + block.final + newCode.slice(origEnd + 1, Infinity) + } + + this._writeURIText(uri, newCode, + 'wholeFileRange', + { shouldRealignDiffAreas: true } + ) + + } + private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { const { from, applyStr, } = opts const featureName: FeatureName = 'Apply' @@ -1526,10 +1600,10 @@ class EditCodeService extends Disposable implements IEditCodeService { // build messages - ask LLM to generate search/replace block text const originalFileCode = model.getValue(EndOfLinePreference.LF) - const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) + const userMessageContent = searchReplaceGivenDescription_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({ - systemMessage: searchReplace_systemMessage, + systemMessage: searchReplaceGivenDescription_systemMessage, simpleMessages: [{ role: 'user', content: userMessageContent, }], featureName, modelSelection, @@ -1577,27 +1651,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { - - const descStr = str === `Not found` ? - `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : str === `Not unique` ? - `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : str === 'Has overlap' ? - `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : `` - - // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has - // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') - // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' - // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' - // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` - const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' - const errMsg = `${descStr}\n${soFarStr}` - return errMsg - - } - const onDone = () => { diffZone._streamState = { isStreaming: false, } this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1652,6 +1705,159 @@ class EditCodeService extends Disposable implements IEditCodeService { let resMessageDonePromise: () => void = () => { } const messageDonePromise = new Promise((res, rej) => { resMessageDonePromise = res }) + + const onText = (params: { fullText: string; fullReasoning: string }) => { + const { fullText } = params + // blocks are [done done done ... {writingFinal|writingOriginal}] + // ^ + // currStreamingBlockNum + + const blocks = extractSearchReplaceBlocks(fullText) + + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + + if (block.state === 'writingOriginal') { + // update stream state to the first line of original if some portion of original has been written + if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { + const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line + const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) + if (typeof originalRange !== 'string') { + const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = startLine + shouldUpdateOrigStreamStyle = false + } + } + + // // starting line is at least the number of lines in the generated code minus 1 + // const numLinesInOrig = numLinesOfStr(block.orig) + // const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) + // if (newLine !== diffZone._streamState.line) { + // diffZone._streamState.line = newLine + // this._refreshStylesAndDiffsInURI(uri) + // } + + + // must be done writing original to move on to writing streamed content + continue + } + shouldUpdateOrigStreamStyle = true + + + // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it + if (!(blockNum in addedTrackingZoneOfBlockNum)) { + + const originalBounds = findTextInCode(block.orig, originalFileCode, true) + // if error + // Check for overlap with existing modified ranges + const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { + const [existingStart, existingEnd] = trackingZone.metadata.originalBounds; + const hasNoOverlap = endLine < existingStart || startLine > existingEnd + return !hasNoOverlap + }); + + if (typeof originalBounds === 'string' || hasOverlap) { + const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const + + console.log('--------------Error finding text in code:') + console.log('originalFileCode', { originalFileCode }) + console.log('fullText', { fullText }) + console.log('error:', errorMessage) + console.log('block.orig:', block.orig) + console.log('---------') + const content = this._errContentOfInvalidStr(errorMessage, block.orig) + messages.push( + { role: 'assistant', content: fullText }, // latest output + { role: 'user', content: content } // user explanation of what's wrong + ) + + // REVERT ALL BLOCKS + currStreamingBlockNum = 0 + latestStreamLocationMutable = null + shouldUpdateOrigStreamStyle = true + oldBlocks = [] + for (const trackingZone of addedTrackingZoneOfBlockNum) + this._deleteTrackingZone(trackingZone) + addedTrackingZoneOfBlockNum.splice(0, Infinity) + + this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) + + // abort and resolve + shouldSendAnotherMessage = true + if (streamRequestIdRef.current) { + weAreAborting = true + this._llmMessageService.abort(streamRequestIdRef.current) + weAreAborting = false + } + diffZone._streamState.line = 1 + resMessageDonePromise() + this._refreshStylesAndDiffsInURI(uri) + return + } + + + + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + + // console.log('---------adding-------') + // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + // console.log('block', deepClone(block)) + // console.log('origBounds', originalBounds) + // console.log('start end', startLine, endLine) + + // otherwise if no error, add the position as a diffarea + const adding: Omit, 'diffareaid'> = { + type: 'TrackingZone', + startLine: startLine, + endLine: endLine, + _URI: uri, + metadata: { + originalBounds: [...originalBounds], + originalCode: block.orig, + }, + } + const trackingZone = this._addDiffArea(adding) + addedTrackingZoneOfBlockNum.push(trackingZone) + latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } // end adding diffarea + + + // should always be in streaming state here + if (!diffZone._streamState.isStreaming) { + console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') + continue + } + + // if a block is done, finish it by writing all + if (block.state === 'done') { + const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] + this._writeURIText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + diffZone._streamState.line = finalEndLine + 1 + currStreamingBlockNum = blockNum + 1 + continue + } + + // write the added text to the file + if (!latestStreamLocationMutable) continue + const oldBlock = oldBlocks[blockNum] + const oldFinalLen = (oldBlock?.final ?? '').length + const deltaFinalText = block.final.substring(oldFinalLen, Infinity) + + this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + oldBlocks = blocks // oldblocks is only used if writingFinal + + // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable + // diffZone._streamState.line = currentEndLine + diffZone._streamState.line = latestStreamLocationMutable.line + + } // end for + + this._refreshStylesAndDiffsInURI(uri) + } + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', logging: { loggingName: `Edit (Search/Replace) - ${from}` }, @@ -1661,201 +1867,25 @@ class EditCodeService extends Disposable implements IEditCodeService { separateSystemMessage, chatMode: null, // not chat onText: (params) => { - const { fullText } = params - // blocks are [done done done ... {writingFinal|writingOriginal}] - // ^ - // currStreamingBlockNum - - const blocks = extractSearchReplaceBlocks(fullText) - - for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - - if (block.state === 'writingOriginal') { - // update stream state to the first line of original if some portion of original has been written - if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { - const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) - if (typeof originalRange !== 'string') { - const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) - diffZone._streamState.line = startLine - shouldUpdateOrigStreamStyle = false - } - } - - // // starting line is at least the number of lines in the generated code minus 1 - // const numLinesInOrig = numLinesOfStr(block.orig) - // const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) - // if (newLine !== diffZone._streamState.line) { - // diffZone._streamState.line = newLine - // this._refreshStylesAndDiffsInURI(uri) - // } - - - // must be done writing original to move on to writing streamed content - continue - } - shouldUpdateOrigStreamStyle = true - - - // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it - if (!(blockNum in addedTrackingZoneOfBlockNum)) { - - const originalBounds = findTextInCode(block.orig, originalFileCode, true) - // if error - // Check for overlap with existing modified ranges - const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { - const [existingStart, existingEnd] = trackingZone.metadata.originalBounds; - const hasNoOverlap = endLine < existingStart || startLine > existingEnd - return !hasNoOverlap - }); - - if (typeof originalBounds === 'string' || hasOverlap) { - const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const - - console.log('--------------Error finding text in code:') - console.log('originalFileCode', { originalFileCode }) - console.log('fullText', { fullText }) - console.log('error:', errorMessage) - console.log('block.orig:', block.orig) - console.log('---------') - const content = errContentOfInvalidStr(errorMessage, block.orig) - messages.push( - { role: 'assistant', content: fullText }, // latest output - { role: 'user', content: content } // user explanation of what's wrong - ) - - // REVERT ALL BLOCKS - currStreamingBlockNum = 0 - latestStreamLocationMutable = null - shouldUpdateOrigStreamStyle = true - oldBlocks = [] - for (const trackingZone of addedTrackingZoneOfBlockNum) - this._deleteTrackingZone(trackingZone) - addedTrackingZoneOfBlockNum.splice(0, Infinity) - - this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) - - // abort and resolve - shouldSendAnotherMessage = true - if (streamRequestIdRef.current) { - weAreAborting = true - this._llmMessageService.abort(streamRequestIdRef.current) - weAreAborting = false - } - diffZone._streamState.line = 1 - resMessageDonePromise() - this._refreshStylesAndDiffsInURI(uri) - return - } - - - - const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) - - // console.log('---------adding-------') - // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - // console.log('block', deepClone(block)) - // console.log('origBounds', originalBounds) - // console.log('start end', startLine, endLine) - - // otherwise if no error, add the position as a diffarea - const adding: Omit, 'diffareaid'> = { - type: 'TrackingZone', - startLine: startLine, - endLine: endLine, - _URI: uri, - metadata: { - originalBounds: [...originalBounds], - originalCode: block.orig, - }, - } - const trackingZone = this._addDiffArea(adding) - addedTrackingZoneOfBlockNum.push(trackingZone) - latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - } // end adding diffarea - - - // should always be in streaming state here - if (!diffZone._streamState.isStreaming) { - console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') - continue - } - - // if a block is done, finish it by writing all - if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] - this._writeURIText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - diffZone._streamState.line = finalEndLine + 1 - currStreamingBlockNum = blockNum + 1 - continue - } - - // write the added text to the file - if (!latestStreamLocationMutable) continue - const oldBlock = oldBlocks[blockNum] - const oldFinalLen = (oldBlock?.final ?? '').length - const deltaFinalText = block.final.substring(oldFinalLen, Infinity) - - this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - oldBlocks = blocks // oldblocks is only used if writingFinal - - // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable - // diffZone._streamState.line = currentEndLine - diffZone._streamState.line = latestStreamLocationMutable.line - - } // end for - - this._refreshStylesAndDiffsInURI(uri) + onText(params) }, onFinalMessage: async (params) => { const { fullText } = params + onText(params) - - // 1. wait 500ms and fix lint errors - call lint error workflow - // (update react state to say "Fixing errors") const blocks = extractSearchReplaceBlocks(fullText) - if (blocks.length === 0) { - this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`) - } - // writeover the whole file - let newCode = originalFileCode - - // IMPORTANT - sort by lineNum - addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) - - // const { model } = this._voidModelService.getModel(uri) - // console.log('DONE - editCode!', { fullText }) - // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - // console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum) - // console.log('blocks', deepClone(blocks)) - - for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata - const finalCode = blocks[blockNum].final - - if (finalCode === null) continue - - const [originalStart, originalEnd] = originalBounds - const lines = newCode.split('\n') - newCode = [ - ...lines.slice(0, (originalStart - 1)), - ...finalCode.split('\n'), - ...lines.slice((originalEnd - 1) + 1, Infinity) - ].join('\n') + this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`) } - this._writeURIText(uri, newCode, - 'wholeFileRange', - { shouldRealignDiffAreas: true } - ) - - onDone() - resMessageDonePromise() + try { + this._instantlyApplySRBlocks(uri, fullText) + onDone() + resMessageDonePromise() + } + catch (e) { + onError(e) + } }, onError: (e) => { onError(e) diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index a63fde7e..5ad58a33 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -44,6 +44,7 @@ export interface IEditCodeService { callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | null; + instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index a6858032..ab664332 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -231,7 +231,7 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: } -export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => { +export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') @@ -287,12 +287,6 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }, [applyBoxId, editCodeService]) - // const onReapply = useCallback(() => { - // onReject() - // onClickSubmit() - // }, [onReject, onClickSubmit]) - - if (currStreamState === 'streaming') { return } if (currStreamState === 'idle-has-changes') { return <> - {/* */} {currStreamState === 'idle-no-changes' && } - +
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 609215b3..78d4dcdf 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 @@ -1239,7 +1239,7 @@ const titleOfToolName = { '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') }, + 'replace_in_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, @@ -1315,8 +1315,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1Info: getRelative(toolParams.uri, accessor), } }, - 'edit_file': () => { - const toolParams = _toolParams as ToolCallParams['edit_file'] + 'replace_in_file': () => { + const toolParams = _toolParams as ToolCallParams['replace_in_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), @@ -1459,10 +1459,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: -const EditToolChildren = ({ uri, changeDiff }: { uri: URI | undefined, changeDiff: string }) => { +const EditToolChildren = ({ uri, searchReplaceBlocks }: { uri: URI | undefined, searchReplaceBlocks: string }) => { return
- +
} @@ -1513,7 +1513,6 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin {currStreamState === 'idle-no-changes' && } -
} @@ -1974,7 +1973,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'edit_file': { + 'replace_in_file': { resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() const isError = toolMessage.type === 'tool_error' @@ -1992,7 +1991,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.children = componentParams.desc2 = @@ -2009,7 +2008,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.desc2 = } @@ -2022,7 +2021,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.children = } @@ -2039,7 +2038,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, {/* content */} } @@ -2632,7 +2631,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown') - const title = titleOfToolName['edit_file'].proposed + const title = titleOfToolName['replace_in_file'].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = @@ -2650,7 +2649,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => > @@ -2700,7 +2699,7 @@ export const SidebarChat = () => { const reasoningSoFar = currThreadStreamState?.reasoningSoFar // this is just if it's currently being generated, NOT if it's currently running - const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'replace_in_file' // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2791,7 +2790,7 @@ export const SidebarChat = () => { // the tool currently being generated const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'edit_file' ? diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index be394b7b..e3773f1e 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -11,7 +11,6 @@ import { ITerminalToolService } from './terminalToolService.js' import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js' import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' -import { basename } from '../../../../base/common/path.js' import { IVoidCommandBarService } from './voidCommandBarService.js' import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' @@ -37,6 +36,7 @@ const isFalsy = (u: unknown) => { } const validateStr = (argName: string, value: unknown) => { + if (value === null) return `Invalid LLM output: ${argName} was null.` if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`) return value } @@ -241,11 +241,12 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, - edit_file: (params: RawToolParamsObj) => { - const { uri: uriStr, change_diff: changeDiffUnknown } = params + replace_in_file: (params: RawToolParamsObj) => { + const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params const uri = validateURI(uriStr) - const changeDiff = validateStr('changeDiff', changeDiffUnknown) - return { uri, changeDiff } + const searchReplaceBlocks = validateStr('searchReplaceBlocks', searchReplaceBlocksUnknown) + console.log('params!!!', uri, searchReplaceBlocks, 'nnnnn', searchReplaceBlocksUnknown) + return { uri, searchReplaceBlocks } }, // --- @@ -383,36 +384,22 @@ export class ToolsService implements IToolsService { await fileService.del(uri, { recursive: isRecursive }) return { result: {} } }, - - edit_file: async ({ uri, changeDiff }) => { + replace_in_file: async ({ uri, searchReplaceBlocks }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } - const opts = { - uri, - applyStr: changeDiff, - from: 'ClickApply', - startBehavior: 'keep-conflicts', - } as const - - await editCodeService.callBeforeStartApplying(opts) - const res = editCodeService.startApplying(opts) - if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) - const [diffZoneURI, applyDonePromise] = res - - const interruptTool = () => { // must reject the applyPromiseDone promise - editCodeService.interruptURIStreaming({ uri: diffZoneURI }) - } + console.log('aaaa', searchReplaceBlocks) + editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }) // at end, get lint errors - const lintErrorsPromise = applyDonePromise.then(async () => { + const lintErrorsPromise = Promise.resolve().then(async () => { await timeout(2000) const { lintErrors } = this._getLintErrors(uri) return { lintErrors } }) - return { result: lintErrorsPromise, interruptTool } + return { result: lintErrorsPromise } }, // --- run_command: async ({ command, bgTerminalId }) => { @@ -484,7 +471,7 @@ export class ToolsService implements IToolsService { delete_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully deleted.` }, - edit_file: (params, result) => { + replace_in_file: (params, result) => { const lintErrsString = ( this.voidSettingsService.state.globalSettings.includeToolLintErrors ? (result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 7051ac76..3a4b8ea0 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -33,6 +33,85 @@ export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds export const MAX_PREFIX_SUFFIX_CHARS = 20_000 + +export const ORIGINAL = `<<<<<<< ORIGINAL` +export const DIVIDER = `=======` +export const FINAL = `>>>>>>> UPDATED` + + + +const searchReplaceBlockTemplate = `\ +${tripleTick[0]} +${ORIGINAL} +// ... original code goes here +${DIVIDER} +// ... final code goes here +${FINAL} +${tripleTick[1]}` + + + + +const createSearchReplaceBlocks_systemMessage = `\ +You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff. +The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`. + +Format your SEARCH/REPLACE blocks as follows: +${searchReplaceBlockTemplate} + +1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out. + +2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change. + +3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output. + +4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. + +5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. + +6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. + +7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. + +## EXAMPLE 1 +DIFF +${tripleTick[0]} +// ... existing code +let x = 6.5 +// ... existing code +${tripleTick[1]} + +ORIGINAL_FILE +${tripleTick[0]} +let w = 5 +let x = 6 +let y = 7 +let z = 8 +${tripleTick[1]} + +ACCEPTED OUTPUT +${tripleTick[0]} +${ORIGINAL} +let x = 6 +${DIVIDER} +let x = 6.5 +${FINAL} +${tripleTick[1]}` + + +const replaceTool_description = `\ +Output a single string of SEARCH/REPLACE block(s) here. Your string should be wrapped in triple backticks. Here's how to format your SEARCH/REPLACE blocks: +${searchReplaceBlockTemplate} + +1. You are allowed to output multiple SEARCH/REPLACE blocks to implement your desired change. Just write them sequentially. + +2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. + +3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. + +4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.` + + // ======================================================== tools ======================================================== const changesExampleContent = `\ // ... existing code ... @@ -43,10 +122,10 @@ const changesExampleContent = `\ // {{change 3}} // ... existing code ...` -const editToolDescriptionExample = `\ -${tripleTick[0]} -${changesExampleContent} -${tripleTick[1]}` +// const editToolDescriptionExample = `\ +// ${tripleTick[0]} +// ${changesExampleContent} +// ${tripleTick[1]}` const fileNameEditExample = `${tripleTick[0]}typescript /Users/username/Dekstop/my_project/app.ts @@ -199,26 +278,18 @@ export const voidTools = { }, }, - edit_file: { // APPLY TOOL - name: 'edit_file', - description: `Edits the contents of a file given the file's URI and a description.`, + replace_in_file: { // APPLY TOOL + name: 'replace_in_file', + description: `Edit the contents of a file. You must provide the file's URI as well as SEARCH/REPLACE block(s) that will be used to apply the edit.`, params: { ...uriParam('file'), - change_diff: { - description: `\ -A code diff describing the change to make to the file. \ -Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \ -Your DIFF MUST be wrapped in triple backticks. \ -NEVER re-write the whole file. Always bias towards writing as little as possible. \ -Use comments like "// ... existing code ..." to condense your writing. \ -Here's an example of a good output:\n${editToolDescriptionExample}` - } + search_replace_blocks: { description: replaceTool_description } }, }, run_command: { name: 'run_command', - 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 (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, + 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 replace_in_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, params: { command: { description: 'The terminal command to run.' }, bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' }, @@ -502,74 +573,17 @@ Please finish writing the new file by applying the change to the original file. // ======================================================== apply (fast apply - search/replace) ======================================================== +export const searchReplaceGivenDescription_systemMessage = createSearchReplaceBlocks_systemMessage -export const ORIGINAL = `<<<<<<< ORIGINAL` -export const DIVIDER = `=======` -export const FINAL = `>>>>>>> UPDATED` - -export const searchReplace_systemMessage = `\ -You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff. -The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`. - -Format your SEARCH/REPLACE blocks as follows: -${tripleTick[0]} -${ORIGINAL} -// ... original code goes here -${DIVIDER} -// ... final code goes here -${FINAL} -${tripleTick[1]} - -1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out. - -2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change. - -3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output. - -4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. - -5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. - -6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. - -7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. - -## EXAMPLE 1 -DIFF -${tripleTick[0]} -// ... existing code -let x = 6.5 -// ... existing code -${tripleTick[1]} - -ORIGINAL_FILE -${tripleTick[0]} -let w = 5 -let x = 6 -let y = 7 -let z = 8 -${tripleTick[1]} - -## ACCEPTED OUTPUT -${tripleTick[0]} -${ORIGINAL} -let x = 6 -${DIVIDER} -let x = 6.5 -${FINAL} -${tripleTick[1]} -` - -export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +export const searchReplaceGivenDescription_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ DIFF ${applyStr} ORIGINAL_FILE ${tripleTick[0]} ${originalCode} -${tripleTick[1]} -` +${tripleTick[1]}` diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index b234689c..8972c4ff 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -19,7 +19,7 @@ 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', + 'replace_in_file': 'edits', 'run_command': 'terminal', } @@ -42,7 +42,7 @@ export type ToolCallParams = { 'search_in_file': { uri: URI, query: string, isRegex: boolean }, 'read_lint_errors': { uri: URI }, // --- - 'edit_file': { uri: URI, changeDiff: string }, + 'replace_in_file': { uri: URI, searchReplaceBlocks: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, // --- @@ -61,7 +61,7 @@ export type ToolResultType = { 'search_in_file': { lines: number[]; }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- - 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, + 'replace_in_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, // --- From fec283a0f1fffbe44149ab3f9eb555d356c58623 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 27 Apr 2025 18:19:03 -0700 Subject: [PATCH 15/27] count --- .../browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 6276f36b..5e31c59b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -384,6 +384,8 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni null} {/* name */} {firstMsg} + + {`(${numMessages})`}
From 991e6a9d5f52dff369304316d2e5c1b475b0ba0b Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 27 Apr 2025 20:03:28 -0700 Subject: [PATCH 16/27] prepare large change --- .../src/markdown/ApplyBlockHoverButtons.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 18 +++++++++++------- .../react/src/void-settings-tsx/Settings.tsx | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index ab664332..a668dbe7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -183,7 +183,7 @@ export const StatusIndicator = ({ indicatorColor, title, className, ...props }: {title && {title}}
{ e.stopPropagation(); // don't open/close selection if (type !== 'staging') return; setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) }} - size={10} - /> + > + +
: <> }
@@ -2592,7 +2596,7 @@ const CommandBarInChat = () => {
{
{ { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} 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 4359afa5..7e07184f 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 @@ -8,7 +8,7 @@ import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, Voi import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' -import { X, RefreshCw, Loader2, Check, } from 'lucide-react' +import { X, RefreshCw, Loader2, Check, Asterisk } from 'lucide-react' import { URI } from '../../../../../../../base/common/uri.js' import { env } from '../../../../../../../base/common/process.js' import { ModelDropdown } from './ModelDropdown.js' @@ -147,7 +147,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t return @@ -206,7 +206,7 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length if (showCheckmark) { - return + return } if (!isOpen) { @@ -339,6 +339,13 @@ export const ModelDump = () => { : 'Disabled' ) + + const detailAboutModel = type === 'autodetected' ? + + : type === 'default' ? undefined + : + + return
{ {/* left part is width:full */}
{isNewProviderName ? providerTitle : ''} - {modelName} + {modelName}{detailAboutModel}
{/* right part is anything that fits */}
{ // : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``) // } > - {type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'} + + + {/* {type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'} */} Date: Sun, 27 Apr 2025 20:09:22 -0700 Subject: [PATCH 17/27] fix --- .../void/browser/react/src/void-onboarding/VoidOnboarding.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 14150f3e..936cf13a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -12,6 +12,7 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js'; import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'; import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'; +import { isLinux } from '../../../../../../../base/common/platform.js'; const OVERRIDE_VALUE = false @@ -590,7 +591,7 @@ const VoidOnboardingContent = () => { {/* Slice of Void image */}
- {/* */} + {!isLinux && }
From 5173ef70c870a64434a7480171427c9c957091e6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 20:20:59 -0700 Subject: [PATCH 18/27] update how to contribute --- HOW_TO_CONTRIBUTE.md | 53 +++++++++++++------------------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index 21516eb0..693ddd5c 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -23,11 +23,11 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`. ## Building Void -### a. Build Prerequisites - Mac +### a. Mac - Build Prerequisites If you're using a Mac, you need Python and XCode. You probably have these by default. -### b. Build Prerequisites - Windows +### b. Windows - Build Prerequisites If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them. @@ -50,42 +50,28 @@ First, run `npm install -g node-gyp`. Then: - Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`. - Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). -### d. Building Void from Visual Studio Code +### d. Building Void from inside VSCode To build Void, open `void/` inside VSCode. Then open your terminal and run: 1. `npm install` to install all dependencies. -2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`). If you get an error, try running it with `NODE_OPTIONS="--max-old-space-size=8192`, for example `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. -3. Build Void. +2. Build Void. - Press Cmd+Shift+B (Mac). - Press Ctrl+Shift+B (Windows/Linux). - This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code). -4. Run Void. +3. Run Void. - Run `./scripts/code.sh` (Mac/Linux). - Run `./scripts/code.bat` (Windows). -5. On Linux, If you get this error with code.sh : - -``` -[366157:0422/132119.648030:FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/user/Applications/void/void/.build/electron/chrome-sandbox is owned by root and has mode 4755. -Trace/breakpoint trap (core dumped) -``` - -Simply run -`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` -and then run again `./scripts/code.sh` - it should fix the problem and launch Void in a few seconds. - -6. Nice-to-knows. +4. Nice-to-knows. - You can always press Ctrl+R (Cmd+R) inside the new window to reload and see your new changes. It's faster than Ctrl+Shift+P and `Reload Window`. - You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing. - You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build). +If you get any errors, scroll down for common fixes. + #### Building Void from Terminal -Alternatively, if you want to build Void from the terminal, you can follow these steps. -1. Clone this repo with `git clone https://github.com/voideditor/void/`. -2. Go inside the "void" folder with `cd void` and then run `npm install`. This will install all dependencies. It can take a few minutes. -3. Now run `npm run buildreact`). If you get an error, try running it with `NODE_OPTIONS="--max-old-space-size=8192`, for example `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. -The build is done when you see something like this: +To build Void from the terminal instead of from inside VSCode, follow the steps above, but instead of pressing Cmd+Shift+B, run `npm run watch`. The build is done when you see something like this: ``` [watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms @@ -94,27 +80,18 @@ The build is done when you see something like this: [watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms ``` -5. Now you can run void by simply typing the following commands (the first time you run, it can take several minutes to load): - - Mac/Linux: Run `./scripts/code.sh` . - - Windows: Run `./scripts/code.bat`. -6. If you get this error when running code.sh on Linux: -``` -[366157:0422/132119.648030:FATAL:setuid_sandbox_host.cc(163)] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /home/user/Applications/void/void/.build/electron/chrome-sandbox is owned by root and has mode 4755. -Trace/breakpoint trap (core dumped) -``` -Simply run -`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` -and then run again `./scripts/code.sh` - it should fix the problem and launch Void in a few seconds. - #### Common Fixes -- Make sure you followed the prerequisite steps. +- Make sure you followed the prerequisite steps above. - Make sure you have Node version `20.18.2` (the version in `.nvmrc`)! - If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`. +- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. - If you see missing styles, wait a few seconds and then reload. -- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. -- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos) +- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos) +- If you get erorrs like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run +`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again. +- If you have any other questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. ## Packaging From 0bb948ee2d325a49c6d7677dace81fa663b76498 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 20:49:23 -0700 Subject: [PATCH 19/27] improve edit tool --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/editCodeService.ts | 23 ++++++++++++++++ .../react/src/sidebar-tsx/SidebarChat.tsx | 18 ++++++------- .../contrib/void/browser/toolsService.ts | 7 +++-- .../contrib/void/common/prompt/prompts.ts | 27 ++++++++++++------- .../contrib/void/common/toolsServiceTypes.ts | 6 ++--- 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a4444838..baae23e4 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -506,7 +506,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { return {} } // once validated, add checkpoint for edit - if (toolName === 'replace_in_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['replace_in_file']).uri }) } + 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 diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e21b1776..d9d0d9f3 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1165,7 +1165,30 @@ class EditCodeService extends Disposable implements IEditCodeService { public instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }: { uri: URI, searchReplaceBlocks: string }) { + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef: { current: null }, + startBehavior: 'keep-conflicts', + linkedCtrlKZone: null, + onWillUndo: () => { }, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + + + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + this._instantlyApplySRBlocks(uri, searchReplaceBlocks) + + + onDone() } 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 b44a46c4..7e34f9fd 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 @@ -1243,7 +1243,7 @@ const titleOfToolName = { '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`) }, - 'replace_in_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, + 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, @@ -1319,8 +1319,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1Info: getRelative(toolParams.uri, accessor), } }, - 'replace_in_file': () => { - const toolParams = _toolParams as ToolCallParams['replace_in_file'] + 'edit_file': () => { + const toolParams = _toolParams as ToolCallParams['edit_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), @@ -1977,7 +1977,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'replace_in_file': { + 'edit_file': { resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() const isError = toolMessage.type === 'tool_error' @@ -2633,9 +2633,9 @@ const CommandBarInChat = () => { const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { - const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown') + const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined - const title = titleOfToolName['replace_in_file'].proposed + const title = titleOfToolName['edit_file'].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = @@ -2649,7 +2649,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => return } + desc2={uri && } > { const reasoningSoFar = currThreadStreamState?.reasoningSoFar // this is just if it's currently being generated, NOT if it's currently running - const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'replace_in_file' // show loading for slow tools (right now just edit) + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2794,7 +2794,7 @@ export const SidebarChat = () => { // the tool currently being generated const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'replace_in_file' ? diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index e3773f1e..9a55d134 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -241,11 +241,10 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, - replace_in_file: (params: RawToolParamsObj) => { + edit_file: (params: RawToolParamsObj) => { const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params const uri = validateURI(uriStr) const searchReplaceBlocks = validateStr('searchReplaceBlocks', searchReplaceBlocksUnknown) - console.log('params!!!', uri, searchReplaceBlocks, 'nnnnn', searchReplaceBlocksUnknown) return { uri, searchReplaceBlocks } }, @@ -384,7 +383,7 @@ export class ToolsService implements IToolsService { await fileService.del(uri, { recursive: isRecursive }) return { result: {} } }, - replace_in_file: async ({ uri, searchReplaceBlocks }) => { + edit_file: async ({ uri, searchReplaceBlocks }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) @@ -471,7 +470,7 @@ export class ToolsService implements IToolsService { delete_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully deleted.` }, - replace_in_file: (params, result) => { + edit_file: (params, result) => { const lintErrsString = ( this.voidSettingsService.state.globalSettings.includeToolLintErrors ? (result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 3a4b8ea0..acc8ecb1 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -43,10 +43,16 @@ export const FINAL = `>>>>>>> UPDATED` const searchReplaceBlockTemplate = `\ ${tripleTick[0]} ${ORIGINAL} -// ... original code goes here +// ... original code 1 goes here ${DIVIDER} -// ... final code goes here +// ... final code 1 goes here ${FINAL} +${ORIGINAL} +// ... original code 2 goes here +${DIVIDER} +// ... final code 2 goes here +${FINAL} +... ${tripleTick[1]}` @@ -69,7 +75,7 @@ ${searchReplaceBlockTemplate} 5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. -6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. +6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible. 7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. @@ -100,14 +106,15 @@ ${tripleTick[1]}` const replaceTool_description = `\ -Output a single string of SEARCH/REPLACE block(s) here. Your string should be wrapped in triple backticks. Here's how to format your SEARCH/REPLACE blocks: +Output a string of SEARCH/REPLACE block(s) to implement your desired change. +You are encouraged to output multiple changes at once. Here's how to format your blocks: ${searchReplaceBlockTemplate} -1. You are allowed to output multiple SEARCH/REPLACE blocks to implement your desired change. Just write them sequentially. +1. Don't forget to wrap your output in triple backticks. -2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. +2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code. -3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. +3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible. 4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.` @@ -278,8 +285,8 @@ export const voidTools = { }, }, - replace_in_file: { // APPLY TOOL - name: 'replace_in_file', + edit_file: { // APPLY TOOL + name: 'edit_file', description: `Edit the contents of a file. You must provide the file's URI as well as SEARCH/REPLACE block(s) that will be used to apply the edit.`, params: { ...uriParam('file'), @@ -289,7 +296,7 @@ export const voidTools = { run_command: { name: 'run_command', - 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 replace_in_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, + 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 (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, params: { command: { description: 'The terminal command to run.' }, bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' }, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 8972c4ff..016344da 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -19,7 +19,7 @@ export type ShallowDirectoryItem = { export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = { 'create_file_or_folder': 'edits', 'delete_file_or_folder': 'edits', - 'replace_in_file': 'edits', + 'edit_file': 'edits', 'run_command': 'terminal', } @@ -42,7 +42,7 @@ export type ToolCallParams = { 'search_in_file': { uri: URI, query: string, isRegex: boolean }, 'read_lint_errors': { uri: URI }, // --- - 'replace_in_file': { uri: URI, searchReplaceBlocks: string }, + 'edit_file': { uri: URI, searchReplaceBlocks: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, // --- @@ -61,7 +61,7 @@ export type ToolResultType = { 'search_in_file': { lines: number[]; }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- - 'replace_in_file': Promise<{ lintErrors: LintErrorItem[] | null }>, + 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, // --- From 039d5cb812be325762adb195617042e0788dd37e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 21:36:47 -0700 Subject: [PATCH 20/27] rewrite file tool --- .../contrib/void/browser/chatThreadService.ts | 1 + .../contrib/void/browser/editCodeService.ts | 25 +++ .../void/browser/editCodeServiceInterface.ts | 1 + .../react/src/sidebar-tsx/SidebarChat.tsx | 180 ++++++++++-------- .../contrib/void/browser/toolsService.ts | 39 +++- .../contrib/void/common/prompt/prompts.ts | 27 ++- .../contrib/void/common/toolsServiceTypes.ts | 3 + 7 files changed, 183 insertions(+), 93 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index baae23e4..b3aa1651 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -507,6 +507,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // once validated, add checkpoint for edit if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } + if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index d9d0d9f3..c42d3139 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1192,6 +1192,31 @@ class EditCodeService extends Disposable implements IEditCodeService { } + public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) { + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef: { current: null }, + startBehavior: 'keep-conflicts', + linkedCtrlKZone: null, + onWillUndo: () => { }, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + + + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false }) + onDone() + } + + private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { // check if there's overlap with any other diffAreas and return early if there is for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 5ad58a33..26ff9d2b 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -45,6 +45,7 @@ export interface IEditCodeService { callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | null; instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void; + instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; 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 7e34f9fd..5bd3e2fc 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 @@ -808,6 +808,84 @@ const ToolHeaderWrapper = ({ +const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters>[0] & { content: string }) => { + const accessor = useAccessor() + const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' + + const title = getTitle(toolMessage) + + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const icon = null + + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { + componentParams.children = + + + componentParams.desc2 = + } + else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { + // add apply box + if (params) { + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + + componentParams.desc2 = + } + + // add children + if (toolMessage.type !== 'tool_error') { + const { result } = toolMessage + + componentParams.bottomChildren = + + componentParams.children = + + + } + else { + // error + const { result } = toolMessage + if (params) { + componentParams.children = + {/* error */} + + {result} + + + {/* content */} + + + } + else { + componentParams.children = + {result} + + } + } + } + + return +} const SimplifiedToolHeader = ({ title, @@ -1243,6 +1321,7 @@ const titleOfToolName = { '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`) }, + 'rewrite_file': { done: `Rewrote file`, proposed: 'Rewrite file', running: loadingTitleWrapper('Rewriting file') }, 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, @@ -1319,6 +1398,13 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1Info: getRelative(toolParams.uri, accessor), } }, + 'rewrite_file': () => { + const toolParams = _toolParams as ToolCallParams['rewrite_file'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, 'edit_file': () => { const toolParams = _toolParams as ToolCallParams['edit_file'] return { @@ -1463,10 +1549,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: -const EditToolChildren = ({ uri, searchReplaceBlocks }: { uri: URI | undefined, searchReplaceBlocks: string }) => { +const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => { return
- +
} @@ -1977,84 +2063,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, + 'rewrite_file': { + resultWrapper: (params) => { + return + } + }, 'edit_file': { - resultWrapper: ({ toolMessage, messageIdx, threadId }) => { - const accessor = useAccessor() - const isError = toolMessage.type === 'tool_error' - const isRejected = toolMessage.type === 'rejected' - - const title = getTitle(toolMessage) - - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } - - if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - componentParams.children = - - - componentParams.desc2 = - } - else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { - // add apply box - if (params) { - const applyBoxId = getApplyBoxId({ - threadId: threadId, - messageIdx: messageIdx, - tokenIdx: 'N/A', - }) - - componentParams.desc2 = - } - - // add children - if (toolMessage.type !== 'tool_error') { - const { result } = toolMessage - - componentParams.bottomChildren = - - componentParams.children = - - - } - else { - // error - const { result } = toolMessage - if (params) { - componentParams.children = - {/* error */} - - {result} - - - {/* content */} - - - } - else { - componentParams.children = - {result} - - } - } - } - - return + resultWrapper: (params) => { + return } }, @@ -2635,7 +2651,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined - const title = titleOfToolName['edit_file'].proposed + const title = titleOfToolName[toolCallSoFar.name].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = @@ -2653,7 +2669,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => > @@ -2794,7 +2810,7 @@ export const SidebarChat = () => { // the tool currently being generated const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'edit_file' ? diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 9a55d134..6987e873 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -36,8 +36,8 @@ const isFalsy = (u: unknown) => { } const validateStr = (argName: string, value: unknown) => { - if (value === null) return `Invalid LLM output: ${argName} was null.` - if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`) + if (value === null) throw new Error(`Invalid LLM output: ${argName} was null.`) + if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a(n) ${typeof value}. Value: ${JSON.stringify(value)}.`) return value } @@ -45,7 +45,8 @@ const validateStr = (argName: string, value: unknown) => { // We are NOT checking to make sure in workspace // TODO!!!! check to make sure folder/file exists const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`) + if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`) + if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Value: ${uriStr}.`) const uri = URI.file(uriStr) return uri } @@ -241,6 +242,13 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, + rewrite_file: (params: RawToolParamsObj) => { + const { uri: uriStr, new_content: newContentUnknown } = params + const uri = validateURI(uriStr) + const newContent = validateStr('newContent', newContentUnknown) + return { uri, newContent } + }, + edit_file: (params: RawToolParamsObj) => { const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params const uri = validateURI(uriStr) @@ -383,6 +391,22 @@ export class ToolsService implements IToolsService { await fileService.del(uri, { recursive: isRecursive }) return { result: {} } }, + + rewrite_file: async ({ uri, newContent }) => { + await voidModelService.initializeModel(uri) + if (this.commandBarService.getStreamState(uri) === 'streaming') { + throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) + } + editCodeService.instantlyApplyNewContent({ uri, newContent }) + // at end, get lint errors + const lintErrorsPromise = Promise.resolve().then(async () => { + await timeout(2000) + const { lintErrors } = this._getLintErrors(uri) + return { lintErrors } + }) + return { result: lintErrorsPromise } + }, + edit_file: async ({ uri, searchReplaceBlocks }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { @@ -479,6 +503,15 @@ export class ToolsService implements IToolsService { return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}` }, + rewrite_file: (params, result) => { + const lintErrsString = ( + this.voidSettingsService.state.globalSettings.includeToolLintErrors ? + (result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` + : ` No lint errors found.`) + : '') + + return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}` + }, run_command: (params, result) => { const { resolveReason, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index acc8ecb1..f8c7fc11 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -106,17 +106,18 @@ ${tripleTick[1]}` const replaceTool_description = `\ -Output a string of SEARCH/REPLACE block(s) to implement your desired change. -You are encouraged to output multiple changes at once. Here's how to format your blocks: +A string of SEARCH/REPLACE block(s) to apply to the given file. +You are encouraged to output multiple changes in this string when possible. For example: ${searchReplaceBlockTemplate} -1. Don't forget to wrap your output in triple backticks. +Guidelines: +1. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code. -2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code. +2. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible. -3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible. +3. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. -4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.` +4. This field is a STRING (not an array). You should wrap the string in triple backticks.` // ======================================================== tools ======================================================== @@ -285,15 +286,25 @@ export const voidTools = { }, }, - edit_file: { // APPLY TOOL + edit_file: { name: 'edit_file', - description: `Edit the contents of a file. You must provide the file's URI as well as SEARCH/REPLACE block(s) that will be used to apply the edit.`, + description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`, params: { ...uriParam('file'), search_replace_blocks: { description: replaceTool_description } }, }, + rewrite_file: { + name: 'rewrite_file', + description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, + params: { + ...uriParam('file'), + new_content: { description: `The new contents of the file.` } + }, + }, + + run_command: { name: 'run_command', 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 (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 016344da..0f48992f 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -19,6 +19,7 @@ export type ShallowDirectoryItem = { export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = { 'create_file_or_folder': 'edits', 'delete_file_or_folder': 'edits', + 'rewrite_file': 'edits', 'edit_file': 'edits', 'run_command': 'terminal', } @@ -42,6 +43,7 @@ export type ToolCallParams = { 'search_in_file': { uri: URI, query: string, isRegex: boolean }, 'read_lint_errors': { uri: URI }, // --- + 'rewrite_file': { uri: URI, newContent: string }, 'edit_file': { uri: URI, searchReplaceBlocks: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, @@ -61,6 +63,7 @@ export type ToolResultType = { 'search_in_file': { lines: number[]; }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- + 'rewrite_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, From 5134ebc761099c1afc0b93e96a684ff63642077f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 23:45:05 -0700 Subject: [PATCH 21/27] EOL pref --- src/vs/workbench/contrib/void/browser/autocompleteService.ts | 4 ++-- src/vs/workbench/contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/convertToLLMMessageService.ts | 3 ++- src/vs/workbench/contrib/void/browser/editCodeService.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 95ec65c2..bfd62321 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -6,7 +6,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITextModel } from '../../../../editor/common/model.js'; +import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; import { Position } from '../../../../editor/common/core/position.js'; import { InlineCompletion, } from '../../../../editor/common/languages.js'; import { Range } from '../../../../editor/common/core/range.js'; @@ -425,7 +425,7 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { - const fullText = model.getValue(); + const fullText = model.getValue(EndOfLinePreference.LF); const cursorOffset = model.getOffsetAt(position) const prefix = fullText.substring(0, cursorOffset) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index b3aa1651..12614595 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -799,7 +799,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if (fsPath in lastIdxOfURI) continue // if already visisted, don't visit again // const { model } = this._voidModelService.getModelFromFsPath(fsPath) // if (!model) continue - // currStrOfFsPath[fsPath] = model.getValue() + // currStrOfFsPath[fsPath] = model.getValue(EndOfLinePreference.LF) // } return { voidFileSnapshotOfURI } diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 7ed729ae..ea9bc60c 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -15,6 +15,7 @@ import { IDirectoryStrService } from './directoryStrService.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; +import { EndOfLinePreference } from '../../../../editor/common/model.js'; @@ -447,7 +448,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess const uri = URI.joinPath(folder.uri, '.voidrules') const { model } = this.voidModelService.getModel(uri) if (!model) continue - voidRules += model.getValue() + '\n\n'; + voidRules += model.getValue(EndOfLinePreference.LF) + '\n\n'; } return voidRules.trim(); } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c42d3139..d6c849df 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1848,7 +1848,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) // console.log('---------adding-------') - // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + // console.log('CURRENT TEXT!!!', { current: model?.getValue(EndOfLinePreference.LF) }) // console.log('block', deepClone(block)) // console.log('origBounds', originalBounds) // console.log('start end', startLine, endLine) From 6a7590ed03792573845147ffe88d5f2acb1767a6 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 28 Apr 2025 02:37:27 -0700 Subject: [PATCH 22/27] prepare merge --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5bd3e2fc..40ab9860 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 @@ -2657,7 +2657,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => const desc1 = {uriDone ? getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') - : `Generating`} + : `Running`} From e2464e16c68032a84d7031422fed0c3de18b4e63 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 28 Apr 2025 02:49:07 -0700 Subject: [PATCH 23/27] descs --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 40ab9860..d975034c 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 @@ -1702,7 +1702,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only search in ${rel}` + if (rel) componentParams.info = `Only searches in ${rel}` } if (toolMessage.type === 'success') { @@ -1750,7 +1750,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only search in ${rel}` + if (rel) componentParams.info = `Only searches in ${rel}` } if (toolMessage.type === 'success') { @@ -1802,7 +1802,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } if (params.includePattern) { - componentParams.info = `Only search in ${params.includePattern}` + componentParams.info = `Only searches in ${params.includePattern}` } if (toolMessage.type === 'success') { @@ -1854,10 +1854,10 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, let info: string[] = [] if (params.searchInFolder) { const rel = getRelative(params.searchInFolder, accessor) - if (rel) info.push(`Only search in ${rel}`) + if (rel) info.push(`Only searches in ${rel}`) } if (params.isRegex) { - info.push(`Treat as regex`) + info.push(`Uses regex search`) } componentParams.info = info.join('; ') } From ace109b0df6e201443b156083f4449e6d5a06c3a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 28 Apr 2025 02:49:48 -0700 Subject: [PATCH 24/27] grammar --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d975034c..59cbb4e7 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 @@ -1857,7 +1857,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (rel) info.push(`Only searches in ${rel}`) } if (params.isRegex) { - info.push(`Uses regex search`) + info.push(`Uses regex in search`) } componentParams.info = info.join('; ') } From a12d4fa9a75f4436987bfd3609ab63b0faffad09 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 28 Apr 2025 02:50:33 -0700 Subject: [PATCH 25/27] missed a spot --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 59cbb4e7..a44c9153 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 @@ -1857,7 +1857,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (rel) info.push(`Only searches in ${rel}`) } if (params.isRegex) { - info.push(`Uses regex in search`) + info.push(`Uses regex`) } componentParams.info = info.join('; ') } @@ -1904,7 +1904,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const isError = toolMessage.type === 'tool_error'; const { rawParams, params } = toolMessage; const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon }; - if (params.isRegex) componentParams.info = 'Treat as regex' + if (params.isRegex) componentParams.info = 'Uses regex' if (toolMessage.type === 'success') { const { result } = toolMessage; // result is array of snippets From 7605931abe398c18e699136f09783bf52cb5745f Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 28 Apr 2025 02:54:19 -0700 Subject: [PATCH 26/27] wording --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 a44c9153..3ebd78da 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 @@ -1702,7 +1702,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only searches in ${rel}` + if (rel) componentParams.info = `Searches inside ${rel}` } if (toolMessage.type === 'success') { @@ -1750,7 +1750,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only searches in ${rel}` + if (rel) componentParams.info = `Searches inside ${rel}` } if (toolMessage.type === 'success') { @@ -1802,7 +1802,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } if (params.includePattern) { - componentParams.info = `Only searches in ${params.includePattern}` + componentParams.info = `Searches inside ${params.includePattern}` } if (toolMessage.type === 'success') { @@ -1854,10 +1854,10 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, let info: string[] = [] if (params.searchInFolder) { const rel = getRelative(params.searchInFolder, accessor) - if (rel) info.push(`Only searches in ${rel}`) + if (rel) info.push(`Searches inside ${rel}`) } if (params.isRegex) { - info.push(`Uses regex`) + info.push(`Uses regex search`) } componentParams.info = info.join('; ') } @@ -1904,7 +1904,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const isError = toolMessage.type === 'tool_error'; const { rawParams, params } = toolMessage; const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon }; - if (params.isRegex) componentParams.info = 'Uses regex' + if (params.isRegex) componentParams.info = 'Uses regex search' if (toolMessage.type === 'success') { const { result } = toolMessage; // result is array of snippets From 6b4142e9b1d74ea2149d5352c3fc3cc73877a7d2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 28 Apr 2025 15:56:03 -0700 Subject: [PATCH 27/27] better terminal context and search/replace fix --- .../browser/convertToLLMMessageService.ts | 4 +- .../contrib/void/browser/editCodeService.ts | 9 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +-- .../void/browser/terminalToolService.ts | 4 +- .../contrib/void/browser/toolsService.ts | 4 +- .../contrib/void/common/prompt/prompts.ts | 43 ++++++++++--------- .../llmMessage/sendLLMMessage.impl.ts | 8 +++- 7 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index ea9bc60c..9810f4bf 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -483,8 +483,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess }) const includeXMLToolDefinitions = !specialToolFormat - const runningTerminalIds = this.terminalToolService.listTerminalIds() - const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions }) + const persistentTerminalIDs = this.terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions }) return systemMessage } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index d6c849df..240ce9a4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1594,10 +1594,10 @@ class EditCodeService extends Disposable implements IEditCodeService { for (const b of blocks) { const i = modelStr.indexOf(b.orig) if (i === -1) - throw new Error(this._errContentOfInvalidStr('Not found', replacements[i].block.orig)) + throw new Error(this._errContentOfInvalidStr('Not found', b.orig)) const j = modelStr.lastIndexOf(b.orig) if (i !== j) - throw new Error(this._errContentOfInvalidStr('Not unique', replacements[i].block.orig)) + throw new Error(this._errContentOfInvalidStr('Not unique', b.orig)) replacements.push({ origStart: i, @@ -1611,9 +1611,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // ensure no overlap for (let i = 1; i < replacements.length; i++) { - if (replacements[i].origStart < replacements[i - 1].origEnd) { - // There's an overlap - throw new Error(this._errContentOfInvalidStr('Has overlap', replacements[i].block.orig)) + if (replacements[i].origStart <= replacements[i - 1].origEnd) { + throw new Error(this._errContentOfInvalidStr('Has overlap', replacements[i]?.block?.orig)) } } 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 3ebd78da..b85f8d76 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 @@ -1321,8 +1321,8 @@ const titleOfToolName = { '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`) }, - 'rewrite_file': { done: `Rewrote file`, proposed: 'Rewrite file', running: loadingTitleWrapper('Rewriting file') }, 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, + 'rewrite_file': { done: `Wrote file`, proposed: 'Write file', running: loadingTitleWrapper('Writing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, @@ -2065,12 +2065,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, }, 'rewrite_file': { resultWrapper: (params) => { - return + return } }, 'edit_file': { resultWrapper: (params) => { - return + return } }, diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 7abc98d6..96e9ec76 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -136,7 +136,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ async killTerminal(terminalId: string) { const terminal = this.terminalInstanceOfId[terminalId] - if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`); + if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} does not exist.`); terminal.dispose(TerminalExitReason.Extension) delete this.terminalInstanceOfId[terminalId] return @@ -168,7 +168,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ 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.`); + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} does not exist.`); terminalId = bgTerminalId } else { diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 6987e873..8c77a001 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -37,7 +37,7 @@ const isFalsy = (u: unknown) => { const validateStr = (argName: string, value: unknown) => { if (value === null) throw new Error(`Invalid LLM output: ${argName} was null.`) - if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a(n) ${typeof value}. Value: ${JSON.stringify(value)}.`) + if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but its type is "${typeof value}". Full value: ${JSON.stringify(value)}.`) return value } @@ -46,7 +46,7 @@ const validateStr = (argName: string, value: unknown) => { // TODO!!!! check to make sure folder/file exists const validateURI = (uriStr: unknown) => { if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`) - if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Value: ${uriStr}.`) + if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`) const uri = URI.file(uriStr) return uri } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index f8c7fc11..3820c871 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -41,19 +41,17 @@ export const FINAL = `>>>>>>> UPDATED` const searchReplaceBlockTemplate = `\ -${tripleTick[0]} ${ORIGINAL} -// ... original code 1 goes here +// ... original code goes here ${DIVIDER} -// ... final code 1 goes here +// ... final code goes here ${FINAL} + ${ORIGINAL} -// ... original code 2 goes here +// ... original code goes here ${DIVIDER} -// ... final code 2 goes here -${FINAL} -... -${tripleTick[1]}` +// ... final code goes here +${FINAL}` @@ -63,7 +61,9 @@ You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`. Format your SEARCH/REPLACE blocks as follows: +${tripleTick[0]} ${searchReplaceBlockTemplate} +${tripleTick[1]} 1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out. @@ -106,18 +106,21 @@ ${tripleTick[1]}` const replaceTool_description = `\ -A string of SEARCH/REPLACE block(s) to apply to the given file. -You are encouraged to output multiple changes in this string when possible. For example: +A string of SEARCH/REPLACE block(s) which will be applied to the given file. +Your SEARCH/REPLACE blocks string must be formatted as follows: ${searchReplaceBlockTemplate} -Guidelines: -1. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code. +## Guidelines: -2. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible. +1. You are encouraged to output multiple changes whenever possible. -3. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. +2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code. -4. This field is a STRING (not an array). You should wrap the string in triple backticks.` +3. Each ORIGINAL text must be large enough to uniquely identify the change. However, bias towards writing as little as possible. + +4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. + +5. This field is a STRING (not an array).` // ======================================================== tools ======================================================== @@ -300,7 +303,7 @@ export const voidTools = { description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, params: { ...uriParam('file'), - new_content: { description: `The new contents of the file.` } + new_content: { description: `The new contents of the file. Must be a string.` } }, }, @@ -403,7 +406,7 @@ ${toolCallXMLGuidelines}` // ======================================================== chat (normal, gather, agent) ======================================================== -export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => { +export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => { const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \ ${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.` : mode === 'gather' ? `to search, understand, and reference files in the user's codebase.` @@ -425,9 +428,9 @@ ${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'} ${activeURI} - Open files: -${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? ` +${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && persistentTerminalIDs.length !== 0 ? ` -- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''} +- Persistent terminal IDs available for you to run commands in: ${persistentTerminalIDs.join(', ')}` : ''} `) @@ -510,7 +513,7 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`) // // log all prompts // for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) { // console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`, -// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', })) +// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', })) // } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 232bdfab..4a515e50 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -162,6 +162,10 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { const { name, description, params } = toolInfo + + const paramsWithType: { [s: string]: { description: string; type: 'string' } } = {} + for (const key in params) { paramsWithType[key] = { ...params[key], type: 'string' } } + return { type: 'function', function: { @@ -358,12 +362,14 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, // ------------ ANTHROPIC (HELPERS) ------------ const toAnthropicTool = (toolInfo: InternalToolInfo) => { const { name, description, params } = toolInfo + const paramsWithType: { [s: string]: { description: string; type: 'string' } } = {} + for (const key in params) { paramsWithType[key] = { ...params[key], type: 'string' } } return { name: name, description: description, input_schema: { type: 'object', - properties: params, + properties: paramsWithType, // required: Object.keys(params), }, } satisfies Anthropic.Messages.Tool