diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ad6ad508..53bf61d8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -379,10 +379,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { name: 'create_uri', id: 'tool-6', paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - content: 'Successfully created file at /Users/username/Project/new-file.js', + content: 'Successfully created file at /Users/username/Project/new-file/', result: { type: 'success', - params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi'), isFolder: false }, + params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi/'), isFolder: true }, value: {} }, } satisfies ToolMessage<'create_uri'>, 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 fe2d48bb..30865d49 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 @@ -243,6 +243,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri if (currStreamState === 'streaming') { buttonsHTML = <> + {copyButton} {stopButton} } 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 c379098c..47196c51 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 @@ -11,7 +11,6 @@ import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js' import { useAccessor } from '../util/services.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' import { URI } from '../../../../../../../base/common/uri.js' -import { getBasename } from '../sidebar-tsx/SidebarChat.js' import { isAbsolute } from '../../../../../../../base/common/path.js' import { separateOutFirstLine } from '../../../../common/helpers/util.js' import { BlockCode } from '../util/inputs.js' @@ -110,7 +109,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean } -const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { +const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => { const accessor = useAccessor() const languageService = accessor.get('ILanguageService') @@ -118,7 +117,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. const t = token as MarkedToken if (t.raw.trim() === '') { - return <>; + return null; } if (t.type === "space") { @@ -127,9 +126,11 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. if (t.type === "code") { const [firstLine, remainingContents] = separateOutFirstLine(t.text) - const firstLineIsURI = isValidUri(firstLine) + const firstLineIsURI = isValidUri(firstLine) && !codeURI const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents + if (!contents) return null + // figure out langauge and URI let uri: URI | null let language: string @@ -171,6 +172,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. /> } + return { + console.log('STRING!!!', string) const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> 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 1a36033e..b67214c5 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 @@ -484,7 +484,18 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe ); }; - +export const getFolderName = (pathStr: string) => { + // 'unixify' path + pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / + const parts = pathStr.split('/') // split on / + // Filter out empty parts (the last element will be empty if path ends with /) + const nonEmptyParts = parts.filter(part => part.length > 0) + if (nonEmptyParts.length === 0) return '/' // Root directory + if (nonEmptyParts.length === 1) return nonEmptyParts[0] + '/' // Only one folder + // Get the last two parts + const lastTwo = nonEmptyParts.slice(-2) + return lastTwo.join('/') + '/' +} export const getBasename = (pathStr: string) => { // 'unixify' path @@ -711,7 +722,7 @@ const ToolHeaderWrapper = ({
{/* header */}
{ if (isDropdown) { setIsOpen(v => !v); } if (onClick) { onClick(); } @@ -724,7 +735,7 @@ const ToolHeaderWrapper = ({ )}
{/* left */} -
+
{title} {desc1}
@@ -1105,16 +1116,19 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". -const toolNameToTitle: Record = { + +const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' +const toolNameToTitle = { 'read_file': { past: 'Read file', proposed: 'Read file' }, 'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' }, 'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' }, 'search': { past: 'Searched', proposed: 'Search' }, - 'create_uri': { past: 'Created file', proposed: 'Create file' }, - 'delete_uri': { past: 'Deleted file', proposed: 'Delete file' }, + 'create_uri': { past: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, + 'delete_uri': { past: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, 'edit': { past: 'Edited file', proposed: 'Edit file' }, 'terminal_command': { past: 'Ran terminal command', proposed: 'Run terminal command' } -} +} as const satisfies Record + const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { if (!_toolParams) { @@ -1135,10 +1149,10 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName return `"${toolParams.queryStr}"`; } else if (toolName === 'create_uri') { const toolParams = _toolParams as ToolCallParams['create_uri'] - return getBasename(toolParams.uri.fsPath); + return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); } else if (toolName === 'delete_uri') { const toolParams = _toolParams as ToolCallParams['delete_uri'] - return getBasename(toolParams.uri.fsPath); + return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); } else if (toolName === 'edit') { const toolParams = _toolParams as ToolCallParams['edit'] return getBasename(toolParams.uri.fsPath); @@ -1242,31 +1256,6 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes } -const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { command: string, terminalId: string, result: string, resolveReason: ResolveReason }) => { - const accessor = useAccessor() - const terminalToolsService = accessor.get('ITerminalToolService') - - const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) - : resolveReason.type === 'bgtask' ? null : - resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : - resolveReason.type === 'toofull' ? `\n(truncated)` - : null - - return - terminalToolsService.openTerminal(terminalId)} - /> -
- {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} - {result} - {resultStr} -
-
-} - const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { return
@@ -1334,7 +1323,7 @@ const toolNameToComponent: { [T in ToolName]: { : {value.children.map((child, i) => ( { commandService.executeCommand('vscode.open', child.uri, { preview: true }) // commandService.executeCommand('workbench.view.explorer'); // open in explorer folders view instead @@ -1342,7 +1331,7 @@ const toolNameToComponent: { [T in ToolName]: { }} />))} {value.hasNextPage && - + } } @@ -1376,11 +1365,11 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && - + } @@ -1415,11 +1404,11 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && - + } @@ -1440,26 +1429,19 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - const { params } = toolRequest - - // TODO!!! would be cool to open up the lowest parent that exists - // componentParams.onClick = () => { - // // open the parent - // } - return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolNameToTitle[toolMessage.name].past(toolMessage.result.params?.isFolder ?? false) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1489,7 +1471,7 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1504,7 +1486,8 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed + const isFolder = toolMessage.result.params?.isFolder ?? false + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1626,12 +1609,26 @@ const toolNameToComponent: { [T in ToolName]: { const { command } = toolMessage.result.params const { terminalId, resolveReason, result } = toolMessage.result.value - componentParams.children = + const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) + : resolveReason.type === 'bgtask' ? null : + resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : + resolveReason.type === 'toofull' ? `\n(truncated)` + : null + + componentParams.children = + terminalToolsService.openTerminal(terminalId)} + /> +
+ {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} + {result} + {resultStr} +
+
+ if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' @@ -1683,7 +1680,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubble } else if (role === 'tool_request') { const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... - if (ToolRequestWrapper && isLast) { // if it's the last message + if (ToolRequestWrapper) { // && isLast // if it's the last message return <> diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index e7461254..6abb6627 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -318,8 +318,11 @@ export class ToolsService implements IToolsService { // --- - create_uri: async ({ uri }) => { - await fileService.createFile(uri) + create_uri: async ({ uri, isFolder }) => { + if (isFolder) + await fileService.createFolder(uri) + else + await fileService.createFile(uri) return {} }, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d734be2f..aec37012 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -19,7 +19,7 @@ export const editToolDesc_toolDescription = `\ A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change.\ The model does not have ANY context except the file content and this description, so make sure to include all necessary information to make the change here.\ Typically the best description you can give is a code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ -Wrap your output in triple ticks. Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` +Wrap all code in triple backticks. Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index e17f6cb5..d5eaca1e 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -83,7 +83,7 @@ export const voidTools = { create_uri: { name: 'create_uri', - description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, params: { uri: { type: 'string', description: undefined }, }, @@ -92,7 +92,7 @@ export const voidTools = { delete_uri: { name: 'delete_uri', - description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, params: { uri: { type: 'string', description: undefined }, params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }