diff --git a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts index be6b00f9..88dbc6c1 100644 --- a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts +++ b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts @@ -38,7 +38,6 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise { - console.log('hi') const n = accessor.get(IDummyService) console.log('Hi', n._serviceBrand) } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 48791b7a..3265e43b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -33,8 +33,13 @@ import { truncate } from '../../../../base/common/strings.js'; import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; import { timeout } from '../../../../base/common/async.js'; +import { deepClone } from '../../../../base/common/objects.js'; + +// related to retrying when LLM message has error const CHAT_RETRIES = 3 +const RETRY_DELAY = 2500 + export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { if (!currentSelections) return null @@ -180,9 +185,12 @@ export interface IChatThreadService { getCurrentThread(): ThreadType; openNewThread(): void; - deleteThread(threadId: string): void; switchToThread(threadId: string): void; + // thread selector + deleteThread(threadId: string): void; + duplicateThread(threadId: string): void; + // exposed getters/setters // these all apply to current thread getCurrentMessageState: (messageIdx: number) => UserMessageState @@ -345,7 +353,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { else throw new Error(`setStreamState`) } - console.log('changeStreamState', threadId, state) this._onDidChangeStreamState.fire({ threadId }) } @@ -369,7 +376,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (!messages) return false const lastMsg = messages[messages.length - 1] if (!lastMsg) return false - if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) { + + if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') { this._editMessageInThread(threadId, messages.length - 1, tool) return true } @@ -386,9 +394,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (!thread) return // should never happen const lastMsg = thread.messages[thread.messages.length - 1] - if (!( - lastMsg.role === 'tool' && (lastMsg.type === 'tool_request') - )) return // should never happen + if (!(lastMsg.role === 'tool' && lastMsg.type === 'tool_request')) return // should never happen const callThisToolFirst: ToolMessage = lastMsg @@ -404,7 +410,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const lastMsg = thread.messages[thread.messages.length - 1] let params: ToolCallParams[ToolName] - if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) { + if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') { params = lastMsg.params } else return @@ -585,12 +591,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { isRunningWhenEnd = undefined nMessagesSent += 1 - let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) - - // send llm message - this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') - const chatMessages = this.state.allThreads[threadId]?.messages ?? [] const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({ chatMessages, @@ -598,15 +598,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { chatMode }) - let aborted = false - let shouldRetry = true let nAttempts = 0 - while (shouldRetry) { shouldRetry = false + let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + + // send llm message + this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', chatMode, @@ -632,7 +635,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { nAttempts += 1 shouldRetry = true this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') - timeout(2500).then(() => { resMessageIsDonePromise() }) + timeout(RETRY_DELAY).then(() => { resMessageIsDonePromise() }) } else { // const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar @@ -659,13 +662,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message const toolCall = await messageIsDonePromise // wait for message to complete + this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done + if (shouldRetry) { continue } if (aborted) { return } - this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done // call tool if there is one const tool: RawToolCallObj | undefined = toolCall @@ -884,7 +888,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const [_, toIdx] = c if (toIdx === fromIdx) return - console.log(`going from ${fromIdx} to ${toIdx}`) + // console.log(`going from ${fromIdx} to ${toIdx}`) // update the user's checkpoint this._addUserModificationsToCurrCheckpoint({ threadId }) @@ -1433,6 +1437,22 @@ We only need to do it for files that were edited since `from`, ie files between this._setState({ ...this.state, allThreads: newThreads }, true) } + duplicateThread(threadId: string) { + const { allThreads: currentThreads } = this.state + const threadToDuplicate = currentThreads[threadId] + if (!threadToDuplicate) return + const newThread = { + ...deepClone(threadToDuplicate), + id: generateUuid(), + } + const newThreads = { + ...currentThreads, + [newThread.id]: newThread, + } + this._storeAllThreads(newThreads) + this._setState({ allThreads: newThreads }, true) + } + private _addMessageToThread(threadId: string, message: ChatMessage) { const { allThreads } = this.state 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 0c957ad7..0ad54462 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 @@ -1232,6 +1232,7 @@ const titleOfToolName = { 'open_bg_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_bg_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, 'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') }, + 'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') }, } as const satisfies Record const getTitle = (toolMessage: Pick): React.ReactNode => { @@ -1281,6 +1282,13 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1: `"${toolParams.query}"`, } }, + 'search_in_file': () => { + const toolParams = _toolParams as ToolCallParams['search_in_file']; + return { + desc1: `"${toolParams.query}"`, + desc1Info: getRelative(toolParams.uri, accessor), + }; + }, 'create_file_or_folder': () => { const toolParams = _toolParams as ToolCallParams['create_file_or_folder'] return { @@ -1692,8 +1700,9 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } - if (params.includePattern) + if (params.includePattern) { componentParams.info = `Only search in ${params.includePattern}` + } if (toolMessage.type === 'success') { const { result, rawParams } = toolMessage @@ -1740,9 +1749,16 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } - if (params.searchInFolder) { - const rel = getRelative(params.searchInFolder, accessor) - if (rel) componentParams.info = `Only search in ${rel}` + if (params.searchInFolder || params.isRegex) { + let info: string[] = [] + if (params.searchInFolder) { + const rel = getRelative(params.searchInFolder, accessor) + if (rel) info.push(`Only search in ${rel}`) + } + if (params.isRegex) { + info.push(`Treat as regex`) + } + componentParams.info = info.join('; ') } if (toolMessage.type === 'success') { @@ -1774,6 +1790,46 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } }, + 'search_in_file': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor(); + const toolsService = accessor.get('IToolsService'); + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); + const icon = null; + + if (toolMessage.type === 'tool_request' || toolMessage.type === 'rejected' || toolMessage.type === 'running_now') return null; + + 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 (toolMessage.type === 'success') { + const { result } = toolMessage; // result is array of snippets + componentParams.numResults = result.lines.length; + componentParams.children = result.lines.length === 0 ? undefined : + + +
+								{toolsService.stringOfResult['search_in_file'](params, result)}
+							
+
+
+ } + else { + const { result } = toolMessage; + componentParams.children = + + {result} + + ; + } + + return ; + } + }, + 'read_lint_errors': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -2163,7 +2219,7 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs {...isDisabled ? { 'data-tooltip-id': 'void-tooltip', 'data-tooltip-content': `Disabled ${isRunning ? 'when running' : 'because another thread is running'}`, - 'data-tooltip-place': 'left', + 'data-tooltip-place': 'top', } : {}} > Checkpoint 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 621e3d71..e11369e3 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 @@ -7,7 +7,7 @@ import { useMemo, useState } from 'react'; import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js'; import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js'; import { IconX } from './SidebarChat.js'; -import { Check, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react'; +import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react'; import { IsRunningType, ThreadType } from '../../../chatThreadService.js'; @@ -247,6 +247,21 @@ const formatTime = (date: Date) => { }; +const DuplicateButton = ({ threadId }: { threadId: string }) => { + const accessor = useAccessor() + const chatThreadsService = accessor.get('IChatThreadService') + return { chatThreadsService.duplicateThread(threadId); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Duplicate thread' + > + + +} + const TrashButton = ({ threadId }: { threadId: string }) => { const accessor = useAccessor() @@ -374,6 +389,9 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
{idx === hoveredIdx ? <> + {/* trash icon */} + + {/* trash icon */} diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 78383b85..fe88dbe6 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -195,14 +195,23 @@ export class ToolsService implements IToolsService { is_regex: isRegexUnknown, page_number: pageNumberUnknown } = params - const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) - const searchInFolder = validateOptionalURI(searchInFolderUnknown) const isRegex = validateBoolean(isRegexUnknown, { default: false }) - - return { query: queryStr, searchInFolder, isRegex, pageNumber } + return { + query: queryStr, + isRegex, + searchInFolder, + pageNumber + } + }, + search_in_file: (params: RawToolParamsObj) => { + const { uri: uriStr, query: queryUnknown, is_regex: isRegexUnknown } = params; + const uri = validateURI(uriStr); + const query = validateStr('query', queryUnknown); + const isRegex = validateBoolean(isRegexUnknown, { default: false }); + return { uri, query, isRegex }; }, read_lint_errors: (params: RawToolParamsObj) => { @@ -333,6 +342,24 @@ export class ToolsService implements IToolsService { const hasNextPage = (data.results.length - 1) - toIdx >= 1 return { result: { queryStr, uris, hasNextPage } } }, + search_in_file: async ({ uri, query, isRegex }) => { + await voidModelService.initializeModel(uri); + const { model } = await voidModelService.getModelSafe(uri); + if (model === null) { throw new Error(`No contents; File does not exist.`); } + const contents = model.getValue(EndOfLinePreference.LF); + const contentOfLine = contents.split('\n'); + const totalLines = contentOfLine.length; + const regex = isRegex ? new RegExp(query) : null; + const lines: number[] = [] + for (let i = 0; i < totalLines; i++) { + const line = contentOfLine[i]; + if ((isRegex && regex!.test(line)) || (!isRegex && line.includes(query))) { + const matchLine = i + 1; + lines.push(matchLine); + } + } + return { result: { lines } }; + }, read_lint_errors: async ({ uri }) => { await timeout(1000) @@ -435,6 +462,15 @@ export class ToolsService implements IToolsService { search_for_files: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, + search_in_file: (params, result) => { + const { model } = voidModelService.getModel(params.uri) + if (!model) return '' + const lines = result.lines.map(n => { + const lineContent = model.getValueInRange({ startLineNumber: n, startColumn: 1, endLineNumber: n, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF) + return `Line ${n}:\n\`\`\`\n${lineContent}\n\`\`\`` + }).join('\n\n'); + return lines; + }, read_lint_errors: (params, result) => { return result.lintErrors ? stringifyLintErrors(result.lintErrors) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 8815a2e9..ffe9a8d9 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -111,8 +111,8 @@ export const voidTools = { description: `Returns full contents of a given file.`, params: { ...uriParam('file'), - start_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' }, - end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' }, + start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' }, + end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' }, ...paginationParam, }, }, @@ -156,11 +156,22 @@ export const voidTools = { params: { query: { description: `Your query for the search.` }, search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, - is_regex: { description: 'Optional. Default is false. Whether query is a regex.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }, ...paginationParam, }, }, + // add new search_in_file tool + search_in_file: { + name: 'search_in_file', + description: `Returns an array of all the start line numbers where the content appears in the file.`, + params: { + ...uriParam('file'), + query: { description: 'The string or regex to search for in the file.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' } + } + }, + read_lint_errors: { name: 'read_lint_errors', description: `Returns all lint errors on a given file.`, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 3305af77..54151ff0 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -39,6 +39,7 @@ export type ToolCallParams = { 'get_dir_tree': { uri: URI }, 'search_pathnames_only': { query: string, includePattern: string | null, pageNumber: number }, 'search_for_files': { query: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number }, + 'search_in_file': { uri: URI, query: string, isRegex: boolean }, 'read_lint_errors': { uri: URI }, // --- 'edit_file': { uri: URI, changeDescription: string }, @@ -57,6 +58,7 @@ export type ToolResultType = { 'get_dir_tree': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 'search_for_files': { uris: URI[], hasNextPage: boolean }, + 'search_in_file': { lines: number[]; }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,