edit state and apply state should work, edit UI

This commit is contained in:
Andrew Pareles 2025-03-15 02:59:32 -07:00
parent 8a6b75b6bb
commit bf2843b1bd
6 changed files with 336 additions and 306 deletions

View file

@ -23,7 +23,7 @@ import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js';
import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
@ -239,224 +239,224 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
const threads = this._convertThreadDataFromStorage(threadsStr);
// threads['abc'] = {
// id: 'abc',
// createdAt: new Date().toISOString(),
// lastModified: new Date().toISOString(),
// messages: [
// {
// role: 'tool',
// name: 'pathname_search',
// id: 'tool-1',
// paramsStr: '{"query": "hello", "pageNumber": 0}',
// content: '/users/andrew/void/Desktop/etc/abc.txt',
// result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } },
// } satisfies ToolMessage<'pathname_search'>,
// {
// role: 'tool',
// name: 'pathname_search',
// id: 'tool-1',
// paramsStr: '{"query": "hello", "pageNumber": 0}',
// content: '/users/andrew/void/Desktop/etc/abc.txt',
// result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } },
// } satisfies ToolMessage<'pathname_search'>,
threads['abc'] = {
id: 'abc',
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
messages: [
{
role: 'tool',
name: 'pathname_search',
id: 'tool-1',
paramsStr: '{"query": "hello", "pageNumber": 0}',
content: '/users/andrew/void/Desktop/etc/abc.txt',
result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } },
} satisfies ToolMessage<'pathname_search'>,
{
role: 'tool',
name: 'pathname_search',
id: 'tool-1',
paramsStr: '{"query": "hello", "pageNumber": 0}',
content: '/users/andrew/void/Desktop/etc/abc.txt',
result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } },
} satisfies ToolMessage<'pathname_search'>,
// // {
// // role: 'tool_request',
// // name: 'pathname_search',
// // params: { queryStr: 'hello', pageNumber: 0 },
// // paramsStr: '{"query": "hello", "pageNumber": 0}',
// // voidToolId: 'request-1',
// // } satisfies ToolRequestApproval<'pathname_search'>,
// {
// role: 'tool_request',
// name: 'pathname_search',
// params: { queryStr: 'hello', pageNumber: 0 },
// paramsStr: '{"query": "hello", "pageNumber": 0}',
// voidToolId: 'request-1',
// } satisfies ToolRequestApproval<'pathname_search'>,
// {
// role: 'tool',
// name: 'list_dir',
// id: 'tool-2',
// paramsStr: '{"uri": "/Users/username/Documents"}',
// content: 'Directory listing of /Users/username/Documents',
// result: {
// type: 'success',
// params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, },
// value: {
// children: [
// { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false },
// { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false }
// ],
// hasNextPage: true,
// hasPrevPage: true,
// itemsRemaining: 5,
// }
// },
// } satisfies ToolMessage<'list_dir'>,
{
role: 'tool',
name: 'list_dir',
id: 'tool-2',
paramsStr: '{"uri": "/Users/username/Documents"}',
content: 'Directory listing of /Users/username/Documents',
result: {
type: 'success',
params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, },
value: {
children: [
{ uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false },
{ uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false }
],
hasNextPage: true,
hasPrevPage: true,
itemsRemaining: 5,
}
},
} satisfies ToolMessage<'list_dir'>,
// // {
// // role: 'tool_request',
// // name: 'list_dir',
// // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 },
// // paramsStr: '{"uri": "/Users/username/Documents"}',
// // voidToolId: 'request-2',
// // } satisfies ToolRequestApproval<'list_dir'>,
// {
// role: 'tool_request',
// name: 'list_dir',
// params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 },
// paramsStr: '{"uri": "/Users/username/Documents"}',
// voidToolId: 'request-2',
// } satisfies ToolRequestApproval<'list_dir'>,
// {
// role: 'tool',
// name: 'read_file',
// id: 'tool-3',
// paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
// content: 'Content of file1.txt\nThis is a sample file.\nHello world!',
// result: {
// type: 'success',
// params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 },
// value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false }
// },
// } satisfies ToolMessage<'read_file'>,
{
role: 'tool',
name: 'read_file',
id: 'tool-3',
paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
content: 'Content of file1.txt\nThis is a sample file.\nHello world!',
result: {
type: 'success',
params: { uri: URI.file('/src/vs/workbench/hi'), pageNumber: 0 },
value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false }
},
} satisfies ToolMessage<'read_file'>,
// // {
// // role: 'tool_request',
// // name: 'read_file',
// // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 },
// // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
// // voidToolId: 'request-3',
// // } satisfies ToolRequestApproval<'read_file'>,
// {
// role: 'tool_request',
// name: 'read_file',
// params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 },
// paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
// voidToolId: 'request-3',
// } satisfies ToolRequestApproval<'read_file'>,
// {
// role: 'tool',
// name: 'search',
// id: 'tool-4',
// paramsStr: '{"query": "function main"}',
// content: 'Found matches in 3 files',
// result: {
// type: 'success',
// params: { queryStr: 'function main', pageNumber: 0 },
// value: {
// uris: [
// URI.file('/Users/username/Project/main.js'),
// URI.file('/Users/username/Project/src/app.js'),
// URI.file('/Users/username/Project/test/test.js')
// ],
// hasNextPage: false
// }
// },
// } satisfies ToolMessage<'search'>,
{
role: 'tool',
name: 'search',
id: 'tool-4',
paramsStr: '{"query": "function main"}',
content: 'Found matches in 3 files',
result: {
type: 'success',
params: { queryStr: 'function main', pageNumber: 0 },
value: {
uris: [
URI.file('/Users/username/Project/main.js'),
URI.file('/Users/username/Project/src/app.js'),
URI.file('/Users/username/Project/test/test.js')
],
hasNextPage: false
}
},
} satisfies ToolMessage<'search'>,
// // {
// // role: 'tool_request',
// // name: 'search',
// // params: { queryStr: 'function main', pageNumber: 0 },
// // paramsStr: '{"query": "function main"}',
// // voidToolId: 'request-4',
// // } satisfies ToolRequestApproval<'search'>,
// {
// role: 'tool_request',
// name: 'search',
// params: { queryStr: 'function main', pageNumber: 0 },
// paramsStr: '{"query": "function main"}',
// voidToolId: 'request-4',
// } satisfies ToolRequestApproval<'search'>,
// // ---
// ---
// {
// role: 'tool',
// name: 'edit',
// id: 'tool-5',
// paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
// content: 'Successfully edited the file at /Users/username/Project/main.js',
// result: {
// type: 'success',
// params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
// value: {}
// },
// } satisfies ToolMessage<'edit'>,
// {
// role: 'tool_request',
// name: 'edit',
// params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
// paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
// voidToolId: 'request-5',
// } satisfies ToolRequestApproval<'edit'>,
{
role: 'tool',
name: 'edit',
id: 'tool-5',
paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
content: 'Successfully edited the file at /Users/username/Project/main.js',
result: {
type: 'success',
params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
value: {}
},
} satisfies ToolMessage<'edit'>,
{
role: 'tool_request',
name: 'edit',
params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
voidToolId: 'request-5',
} satisfies ToolRequestApproval<'edit'>,
// {
// role: 'tool',
// 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',
// result: {
// type: 'success',
// params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false },
// value: {}
// },
// } satisfies ToolMessage<'create_uri'>,
// {
// role: 'tool_request',
// name: 'create_uri',
// params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false },
// paramsStr: '{"uri": "/Users/username/Project/new-file.js"}',
// voidToolId: 'request-6',
// } satisfies ToolRequestApproval<'create_uri'>,
{
role: 'tool',
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',
result: {
type: 'success',
params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi'), isFolder: false },
value: {}
},
} satisfies ToolMessage<'create_uri'>,
{
role: 'tool_request',
name: 'create_uri',
params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false },
paramsStr: '{"uri": "/Users/username/Project/new-file.js"}',
voidToolId: 'request-6',
} satisfies ToolRequestApproval<'create_uri'>,
// {
// role: 'tool',
// name: 'delete_uri',
// id: 'tool-7',
// paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
// content: 'Successfully deleted file at /Users/username/Project/old-file.js',
// result: {
// type: 'success',
// params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
// value: {}
// },
// } satisfies ToolMessage<'delete_uri'>,
// {
// role: 'tool_request',
// name: 'delete_uri',
// params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
// paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
// voidToolId: 'request-7',
// } satisfies ToolRequestApproval<'delete_uri'>,
{
role: 'tool',
name: 'delete_uri',
id: 'tool-7',
paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
content: 'Successfully deleted file at /Users/username/Project/old-file.js',
result: {
type: 'success',
params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
value: {}
},
} satisfies ToolMessage<'delete_uri'>,
{
role: 'tool_request',
name: 'delete_uri',
params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
voidToolId: 'request-7',
} satisfies ToolRequestApproval<'delete_uri'>,
// {
// role: 'tool',
// name: 'terminal_command',
// id: 'tool-8',
// paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
// content: 'Command executed: npm install\nAdded 123 packages in 3.5s',
// result: {
// type: 'success',
// params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
// value: {
// terminalId: '1',
// didCreateTerminal: false,
// result: 'Added 123 packages in 3.5s',
// resolveReason: { type: 'done', exitCode: 0 }
// }
// },
// } satisfies ToolMessage<'terminal_command'>,
// {
// role: 'tool_request',
// name: 'terminal_command',
// params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
// paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
// voidToolId: 'request-8',
// } satisfies ToolRequestApproval<'terminal_command'>,
{
role: 'tool',
name: 'terminal_command',
id: 'tool-8',
paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
content: 'Command executed: npm install\nAdded 123 packages in 3.5s',
result: {
type: 'success',
params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
value: {
terminalId: '1',
didCreateTerminal: false,
result: 'Added 123 packages in 3.5s',
resolveReason: { type: 'done', exitCode: 0 }
}
},
} satisfies ToolMessage<'terminal_command'>,
{
role: 'tool_request',
name: 'terminal_command',
params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
voidToolId: 'request-8',
} satisfies ToolRequestApproval<'terminal_command'>,
// // Examples of error and rejected states
// {
// role: 'tool',
// name: 'pathname_search',
// id: 'tool-error',
// paramsStr: '{"query": "invalid**query"}',
// content: 'Error: Invalid search pattern',
// result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' },
// } satisfies ToolMessage<'pathname_search'>,
// Examples of error and rejected states
{
role: 'tool',
name: 'pathname_search',
id: 'tool-error',
paramsStr: '{"query": "invalid**query"}',
content: 'Error: Invalid search pattern',
result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' },
} satisfies ToolMessage<'pathname_search'>,
// {
// role: 'tool',
// name: 'pathname_search',
// id: 'tool-rejected',
// paramsStr: '{"query": "sensitive-data"}',
// content: 'Tool call was rejected by the user.',
// result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } },
// } satisfies ToolMessage<'pathname_search'>,
// ],
// state: defaultThreadState,
// }
{
role: 'tool',
name: 'pathname_search',
id: 'tool-rejected',
paramsStr: '{"query": "sensitive-data"}',
content: 'Tool call was rejected by the user.',
result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } },
} satisfies ToolMessage<'pathname_search'>,
],
state: defaultThreadState,
}
return threads
}

View file

@ -98,7 +98,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
@ -112,13 +112,16 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a
const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied])
// listen for stream updates
// listen for stream updates on this box
useURIStreamState(
useCallback((uri, newStreamState) => {
const shouldUpdate = getUriBeingApplied()?.fsPath === uri.fsPath
useCallback((uri_, newStreamState) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
|| (uri === 'current' ? false : uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied])
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
)
const onClickSubmit = useCallback(async () => {
@ -127,14 +130,14 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a
const [newApplyingUri, _] = await editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
uri: 'current',
uri: uri,
startBehavior: 'reject-conflicts',
}) ?? []
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService])
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
const onInterrupt = useCallback(() => {
@ -263,17 +266,18 @@ export const BlockCodeApplyWrapper = ({
applyBoxId,
language,
canApply,
uri,
}: {
initValue: string;
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
language: string;
uri: URI | 'current',
}) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId })
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
return <div
className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3'
@ -287,7 +291,7 @@ export const BlockCodeApplyWrapper = ({
{language || 'text'}
</span>
</div>
<div className={`${canApply ? '' : 'hidden'} flex gap-1`}>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
{buttonsHTML}
</div>
</div>

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------*/
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js';
export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => {

View file

@ -20,7 +20,7 @@ export type ChatMessageLocation = {
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
return `${threadId}-${messageIdx}-${tokenIdx}`
}
@ -120,7 +120,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
const firstLineIsURI = URI.isUri(firstLine)
const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents
// figure out langauge
// figure out langauge and URI
let language: string | undefined = undefined
let uri: URI | undefined = undefined
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
@ -151,6 +151,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
applyBoxId={applyBoxId}
initValue={contents}
language={language}
uri={uri || 'current'}
>
<BlockCode
initValue={contents}

View file

@ -13,7 +13,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdownRender.js';
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
@ -30,6 +30,8 @@ import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
import { dirname } from '../../../../../../../base/common/resources.js';
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
@ -489,11 +491,11 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe
};
const getBasename = (pathStr: string) => {
// '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]
}
@ -679,7 +681,7 @@ type ToolHeaderParams = {
onClick?: () => void;
}
const ToolHeaderComponent = ({
const ToolHeaderWrapper = ({
icon,
title,
desc1,
@ -696,7 +698,7 @@ const ToolHeaderComponent = ({
const isClickable = !!(isDropdown || onClick)
return (<div className=''>
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-2-alt overflow-hidden">
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden">
{/* header */}
<div
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
@ -710,13 +712,14 @@ const ToolHeaderComponent = ({
className={`text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
/>
)}
<div className="flex items-center w-full gap-x-2 overflow-hidden">
<div className="flex items-center w-full gap-x-2 overflow-hidden justify-between">
{/* left */}
<div className="flex items-center gap-x-2 min-w-0 overflow-hidden">
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
{/* Fixed description with proper ellipsis */}
<span className="text-void-fg-4 text-xs italic truncate">{desc1}</span>
</div>
{/* right */}
<div className="flex items-center gap-x-2 flex-shrink-0">
{desc2 && <span className="text-void-fg-4 text-xs">
{desc2}
@ -733,13 +736,13 @@ const ToolHeaderComponent = ({
{/* children */}
{<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100' : 'max-h-0 opacity-0'}
text-void-fg-4 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm`}
text-void-fg-4 rounded-sm`}
// bg-black bg-opacity-10 border border-void-border-4 border-opacity-50
>
{children}
</div>}
</div>
</div>
);
</div>);
};
@ -939,28 +942,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <div className={`${className ? className : ''} max-h-64 overflow-x-auto cursor-default select-none`}>
<div className='px-2 py-1 min-w-full'>
{children}
</div>
</div>
}
const ListableToolItem = ({ name, onClick, isSmall, className }: { name: string, onClick?: () => void, isSmall?: boolean, className?: string }) => {
return <div
className={`
${onClick ? 'hover:brightness-125 hover:cursor-pointer transition-all duration-200 ' : ''}
flex items-center flex-nowrap whitespace-nowrap
${className ? className : ''}
`}
onClick={onClick}
>
<div className="flex-shrink-0"><svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg></div>
<div className={`${isSmall ? 'italic text-sm leading-4 flex items-center' : ''}`}>{name}</div>
</div>
}
const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => {
const accessor = useAccessor()
@ -1141,9 +1122,54 @@ const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string })
</div>
}
export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <div className={`${className ? className : ''} overflow-x-auto cursor-default select-none`}>
<div className='px-2 py-1 min-w-full'>
{children}
</div>
</div>
}
const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => {
return <div
className={`
${onClick ? 'hover:brightness-125 hover:cursor-pointer transition-all duration-200 ' : ''}
flex items-center flex-nowrap whitespace-nowrap
${className ? className : ''}
`}
onClick={onClick}
>
{showDot === false ? null : <div className="flex-shrink-0"><svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg></div>}
<div className={`${isSmall ? 'italic text-void-fg-4 flex items-center' : ''}`}>{name}</div>
</div>
}
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
return <ToolContentsWrapper className='bg-void-bg-3'>
<ListableToolItem
showDot={false}
className='w-full overflow-auto mb-2'
name={uri.fsPath}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
<div className='select-auto cursor-auto'>
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
</div>
</ToolContentsWrapper>
}
const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDescription: string, applyBoxId: string, uri: URI }) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: changeDescription, applyBoxId, uri })
return <div className='flex items-center gap-1'>
{statusIndicatorHTML}
{buttonsHTML}
</div>
}
const toolNameToComponent: { [T in ToolName]: {
requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval<T> }) => React.ReactNode) : null,
resultWrapper: (props: { toolMessage: ToolMessage<T> }) => React.ReactNode,
resultWrapper: (props: { toolMessage: ToolMessage<T>, messageIdx: number }) => React.ReactNode,
} } = {
'read_file': {
requestWrapper: null,
@ -1171,7 +1197,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
},
},
'list_dir': {
@ -1192,10 +1218,8 @@ const toolNameToComponent: { [T in ToolName]: {
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.children?.length
componentParams.children = <ToolContentsWrapper>
{!value.children || (value.children.length ?? 0) === 0 ? <>
<ListableToolItem name={'No results found.'} isSmall={true} />
</> : <>
componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined
: <ToolContentsWrapper>
{value.children.map((child, i) => (<ListableToolItem key={i}
name={`${child.name}${child.isDirectory ? '/' : ''}`}
onClick={() => {
@ -1206,8 +1230,7 @@ const toolNameToComponent: { [T in ToolName]: {
{value.hasNextPage &&
<ListableToolItem name={`Results truncated (${value.itemsRemaining} remaining).`} isSmall={true} />
}
</>}
</ToolContentsWrapper>
</ToolContentsWrapper>
}
else {
componentParams.children = <>
@ -1215,7 +1238,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
'pathname_search': {
@ -1235,10 +1258,8 @@ const toolNameToComponent: { [T in ToolName]: {
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.uris.length
componentParams.children = <ToolContentsWrapper>
{value.uris.length === 0 ? <>
<ListableToolItem name={'No results found.'} isSmall={true} />
</> : <>
componentParams.children = value.uris.length === 0 ? undefined
: <ToolContentsWrapper>
{value.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
@ -1246,8 +1267,8 @@ const toolNameToComponent: { [T in ToolName]: {
{value.hasNextPage &&
<ListableToolItem name={'Results truncated.'} isSmall={true} />
}
</>}
</ToolContentsWrapper>
</ToolContentsWrapper>
}
else {
componentParams.children = <>
@ -1255,7 +1276,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
'search': {
@ -1275,10 +1296,8 @@ const toolNameToComponent: { [T in ToolName]: {
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.uris.length
componentParams.children = <ToolContentsWrapper>
{value.uris.length === 0 ? <>
<ListableToolItem name={'No results found.'} isSmall={true} />
</> : <>
componentParams.children = value.uris.length === 0 ? undefined
: <ToolContentsWrapper>
{value.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
@ -1286,15 +1305,15 @@ const toolNameToComponent: { [T in ToolName]: {
{value.hasNextPage &&
<ListableToolItem name={`Results truncated.`} isSmall={true} />
}
</>}
</ToolContentsWrapper>
</ToolContentsWrapper>
}
else {
componentParams.children = <>
{toolMessage.result.value}
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
@ -1304,6 +1323,7 @@ const toolNameToComponent: { [T in ToolName]: {
requestWrapper: ({ toolRequest }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const explorerService = accessor.get('IExplorerService')
const title = toolNameToTitle[toolRequest.name].proposed
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
@ -1312,9 +1332,13 @@ const toolNameToComponent: { [T in ToolName]: {
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
const { params } = toolRequest
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
return <ToolHeaderComponent title={title} desc1={desc1} />
// TODO!!! would be cool to open up the lowest parent that exists
// componentParams.onClick = () => {
// // open the parent
// }
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1341,7 +1365,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
'delete_uri': {
@ -1358,7 +1382,7 @@ const toolNameToComponent: { [T in ToolName]: {
const { params } = toolRequest
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1384,7 +1408,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
'edit': {
@ -1399,21 +1423,16 @@ const toolNameToComponent: { [T in ToolName]: {
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
const { params } = toolRequest
componentParams.children = <ToolContentsWrapper className='bg-void-bg-3'>
<ListableToolItem
name={getBasename(params.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
/>
<div className='select-auto cursor-auto'>
<ChatMarkdownRender string={params.changeDescription} codeURI={params.uri} chatMessageLocation={undefined} />
</div>
</ToolContentsWrapper>
componentParams.children = <EditToolChildren
uri={params.uri}
changeDescription={params.changeDescription}
/>
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
resultWrapper: ({ toolMessage, messageIdx }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const chatThreadsService = accessor.get('IChatThreadService')
const title = toolNameToTitle[toolMessage.name].past
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
@ -1421,15 +1440,25 @@ const toolNameToComponent: { [T in ToolName]: {
const isError = toolMessage.result.type === 'error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') {
const { params } = toolMessage.result
componentParams.children = <ChatMarkdownRender string={params.changeDescription} codeURI={params.uri} chatMessageLocation={undefined} />
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.result.type === 'rejected') {
const { params } = toolMessage.result
componentParams.children = <ChatMarkdownRender string={params.changeDescription} codeURI={params.uri} chatMessageLocation={undefined} />
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
const threadId = chatThreadsService.getCurrentThread().id
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.children = <EditToolChildren
uri={params.uri}
changeDescription={params.changeDescription}
/>
componentParams.desc2 = <EditToolApplyButton
changeDescription={params.changeDescription}
applyBoxId={applyBoxId}
uri={params.uri}
/>
}
else if (toolMessage.result.type === 'error') {
componentParams.children = <>
@ -1437,7 +1466,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
},
'terminal_command': {
@ -1458,8 +1487,7 @@ const toolNameToComponent: { [T in ToolName]: {
if (!waitForCompletion)
componentParams.desc2 = '(background task)'
// TODO!!! open terminal
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@ -1510,9 +1538,7 @@ const toolNameToComponent: { [T in ToolName]: {
</>
}
// TODO!!! open terminal
return <ToolHeaderComponent {...componentParams} />
return <ToolHeaderWrapper {...componentParams} />
}
}
};
@ -1551,8 +1577,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
</>
}
else if (role === 'tool') {
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
return <ToolResultWrapper toolMessage={chatMessage} />
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough...
return <ToolResultWrapper toolMessage={chatMessage} messageIdx={messageIdx} />
}
}

View file

@ -67,10 +67,10 @@ const markdownLangToVscodeLang: { [key: string]: string } = {
'less': 'less',
'javascript': 'typescript',
'js': 'typescript', // use more general renderer
'jsx': 'typescript',
'jsx': 'typescriptreact',
'typescript': 'typescript',
'ts': 'typescript',
'tsx': 'typescript',
'tsx': 'typescriptreact',
'json': 'json',
'jsonc': 'json',