mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
changeDiff prompt, chat thread state, improve import/export
This commit is contained in:
parent
39ea63b207
commit
58b4d90b49
7 changed files with 121 additions and 85 deletions
|
|
@ -641,15 +641,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: async (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
nAttempts += 1
|
||||
shouldRetry = true
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
timeout(RETRY_DELAY).then(() => { resMessageIsDonePromise() })
|
||||
await timeout(RETRY_DELAY)
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
else {
|
||||
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||
|
|
@ -661,9 +662,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
aborted = true
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
aborted = true
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -678,12 +679,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
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 is a complete hack to make it so if an error loop was aborted, we stop (because onAbort does not get called if error happens instantly)
|
||||
// maybe we should remove all the abort stuff and just make it so that we only go by state?
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
|
||||
if (aborted) { return }
|
||||
if (shouldRetry) { continue }
|
||||
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
|
|
@ -692,8 +693,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
|
||||
// just detect tool interruption which is the same as chat interruption right now
|
||||
if (interrupted) { return }
|
||||
if (!this.streamState[threadId]?.isRunning) { return }
|
||||
if (aborted) { return }
|
||||
if (interrupted) { return }
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
console.log('awaiting...')
|
||||
|
|
@ -1419,14 +1421,13 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
openNewThread() {
|
||||
// if a thread with 0 messages already exists, switch to it
|
||||
const { allThreads: currentThreads } = this.state
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId]!.messages.length === 0) {
|
||||
|
||||
// switch to the thread
|
||||
this.switchToThread(threadId)
|
||||
|
||||
}
|
||||
}
|
||||
for (const threadId in currentThreads) {
|
||||
if (currentThreads[threadId]!.messages.length === 0) {
|
||||
// switch to the existing empty thread and exit
|
||||
this.switchToThread(threadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
// otherwise, start a new thread
|
||||
const newThread = newThreadObject()
|
||||
|
||||
|
|
|
|||
|
|
@ -1447,10 +1447,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
|
|||
|
||||
|
||||
|
||||
const EditToolChildren = ({ uri, changeDescription }: { uri: URI | undefined, changeDescription: string }) => {
|
||||
const EditToolChildren = ({ uri, changeDiff }: { uri: URI | undefined, changeDiff: string }) => {
|
||||
return <div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
<ChatMarkdownRender string={changeDiff} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</SmallProseWrapper>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -1980,7 +1980,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
changeDiff={params.changeDiff}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
|
||||
|
|
@ -1997,7 +1997,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
componentParams.desc2 = <EditToolHeaderButtons
|
||||
applyBoxId={applyBoxId}
|
||||
uri={params.uri}
|
||||
codeStr={params.changeDescription}
|
||||
codeStr={params.changeDiff}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
@ -2010,7 +2010,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
changeDiff={params.changeDiff}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
|
|
@ -2027,7 +2027,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
|
|||
{/* content */}
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
changeDiff={params.changeDiff}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
|
|
@ -2638,7 +2638,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
|
|||
>
|
||||
<EditToolChildren
|
||||
uri={uri}
|
||||
changeDescription={toolCallSoFar.rawParams.change_description ?? ''}
|
||||
changeDiff={toolCallSoFar.rawParams.change_diff ?? ''}
|
||||
/>
|
||||
<IconLoading />
|
||||
</ToolHeaderWrapper>
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ const TrashButton = ({ threadId }: { threadId: string }) => {
|
|||
onClick={() => { setIsTrashPressed(true); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Delete thread?'
|
||||
data-tooltip-content='Delete thread'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -386,7 +386,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
|||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2 opacity-60">
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
{idx === hoveredIdx ?
|
||||
<>
|
||||
{/* trash icon */}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,36 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
|
|||
|
||||
}
|
||||
|
||||
// ConfirmButton prompts for a second click to confirm an action, cancels if clicking outside
|
||||
const ConfirmButton = ({ children, onConfirm, className }: { children: React.ReactNode, onConfirm: () => void, className?: string }) => {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!confirm) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setConfirm(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, [confirm]);
|
||||
return (
|
||||
<div ref={ref} className={`inline-block`}>
|
||||
<VoidButtonBgDarken className={className} onClick={() => {
|
||||
if (!confirm) {
|
||||
setConfirm(true);
|
||||
} else {
|
||||
onConfirm();
|
||||
setConfirm(false);
|
||||
}
|
||||
}}>
|
||||
{confirm ? `Confirm Reset` : children}
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// shows a providerName dropdown if no `providerName` is given
|
||||
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
|
||||
|
|
@ -907,33 +937,7 @@ export const Settings = () => {
|
|||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
{/* Download & Upload Settings and Chats */}
|
||||
<div className='flex gap-2 mb-6'>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Upload Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => onDownload('Settings')}>
|
||||
Download Settings
|
||||
</VoidButtonBgDarken>
|
||||
|
||||
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Upload Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => onDownload('Chats')}>
|
||||
Download Chats
|
||||
</VoidButtonBgDarken>
|
||||
|
||||
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { voidSettingsService.resetState() }}>
|
||||
Reset Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { chatThreadsService.resetState() }}>
|
||||
Reset Chats
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
|
||||
{/* Models section (formerly FeaturesTab) */}
|
||||
<ErrorBoundary>
|
||||
|
|
@ -1112,10 +1116,10 @@ export const Settings = () => {
|
|||
{/* General section (formerly GeneralTab) */}
|
||||
<div className='mt-12'>
|
||||
<ErrorBoundary>
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
|
||||
<h4 className='text-void-fg-3 mb-4'>{`Transfer your settings from another editor to Void in one click.`}</h4>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
|
||||
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
|
||||
|
|
@ -1123,6 +1127,41 @@ export const Settings = () => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Import/Export section, as its own block right after One-Click Switch */}
|
||||
<div className='mt-12'>
|
||||
<h2 className='text-3xl mb-2'>Import/Export</h2>
|
||||
<div className='flex gap-8'>
|
||||
{/* Settings Subcategory */}
|
||||
<div className='flex flex-col gap-2 max-w-48 w-full'>
|
||||
<h3 className='text-xl mb-2'>Settings</h3>
|
||||
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
|
||||
Import Settings
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
|
||||
Export Settings
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
|
||||
Reset Settings
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
{/* Chats Subcategory */}
|
||||
<div className='flex flex-col gap-2 w-full max-w-48'>
|
||||
<h3 className='text-xl mb-2'>Chat</h3>
|
||||
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
|
||||
Import Chats
|
||||
</VoidButtonBgDarken>
|
||||
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
|
||||
Export Chats
|
||||
</VoidButtonBgDarken>
|
||||
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
|
||||
Reset Chats
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='mt-12'>
|
||||
|
|
@ -1131,23 +1170,17 @@ export const Settings = () => {
|
|||
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<ErrorBoundary>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
|
||||
Open Logs
|
||||
</VoidButtonBgDarken>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -242,10 +242,10 @@ export class ToolsService implements IToolsService {
|
|||
},
|
||||
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
|
||||
const { uri: uriStr, change_diff: changeDiffUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
return { uri, changeDescription }
|
||||
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
|
||||
return { uri, changeDiff }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
|
@ -383,14 +383,14 @@ export class ToolsService implements IToolsService {
|
|||
return { result: {} }
|
||||
},
|
||||
|
||||
edit_file: async ({ uri, changeDescription }) => {
|
||||
edit_file: async ({ uri, changeDiff }) => {
|
||||
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 resume later.`)
|
||||
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: changeDescription,
|
||||
applyStr: changeDiff,
|
||||
from: 'ClickApply',
|
||||
startBehavior: 'keep-conflicts',
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -204,12 +204,14 @@ export const voidTools = {
|
|||
description: `Edits the contents of a file given the file's URI and a description.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
change_description: {
|
||||
change_diff: {
|
||||
description: `\
|
||||
Your description MUST be wrapped in triple backticks. \
|
||||
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
|
||||
NEVER re-write the whole file. Bias towards writing as little as possible. \
|
||||
Here's an example of a good description:\n${editToolDescriptionExample}`
|
||||
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}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -506,7 +508,7 @@ export const DIVIDER = `=======`
|
|||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
export const searchReplace_systemMessage = `\
|
||||
You are a coding assistant that takes in a diff describing of a change to make, and outputs SEARCH/REPLACE code blocks which implement the change.
|
||||
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:
|
||||
|
|
@ -518,11 +520,11 @@ ${DIVIDER}
|
|||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
1. Your SEARCH/REPLACE block(s) must implement the change EXACTLY.
|
||||
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
|
||||
|
||||
2. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
|
||||
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
|
||||
|
||||
3. You are allowed to output multiple SEARCH/REPLACE blocks.
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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, changeDescription: string },
|
||||
'edit_file': { uri: URI, changeDiff: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
// ---
|
||||
|
|
|
|||
Loading…
Reference in a new issue