fix running_tool state and double undo on click stop in Apply

This commit is contained in:
Andrew Pareles 2025-04-09 03:25:09 -07:00
parent 78671db5b8
commit ea65903b3f
6 changed files with 41 additions and 52 deletions

View file

@ -420,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
return false
}
private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
private _updateLatestTool = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool)
if (swapped) return
this._addMessageToThread(threadId, tool)
@ -430,7 +430,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const lastMsg = thread.messages[thread.messages.length - 1]
if (!(
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
@ -438,15 +437,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
this._updateLatestToolTo(threadId, {
role: 'tool',
type: 'running_now',
name: lastMsg.name,
params: lastMsg.params,
content: '(value not received yet...)', // this typically shouldn't ever get read
result: null
})
this._wrapRunAgentToNotify(
this._runChatAgent({ callThisToolFirst, threadId, ...this._currentModelSelectionProps() })
, threadId
@ -467,7 +457,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { name } = lastMsg
const errorMessage = this.errMsgs.rejected
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null })
this._setStreamState(threadId, {}, 'set')
}
@ -588,8 +578,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (!opts.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
console.log('VALIDATING PARAMS!!!', opts.unvalidatedToolParams)
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
} catch (error) {
@ -601,8 +589,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
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
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
if (requiresApproval) {
const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName)
if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
@ -617,6 +605,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null })
let interrupted = false
try {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
@ -633,7 +623,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
@ -642,12 +632,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 5. add to history and keep going
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
return {}
};
@ -704,7 +694,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
...llmMessages
]
console.log('SENDING!!', messages)
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
@ -718,7 +707,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
console.log('tool call!!', JSON.stringify(toolCall))
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {

View file

@ -303,7 +303,6 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
// Use our new approach with direct explorer service
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
console.log('dirtree', dirTree)
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length);
str += content;
if (wasCutOff) {

View file

@ -44,7 +44,6 @@ import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApply
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
@ -190,7 +189,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ITextFileService private readonly _textFileService: ITextFileService,
) {
super();
@ -720,16 +718,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
resource: uri,
label: 'Void Agent',
code: 'undoredo.editCode',
undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); },
redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) }
undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) },
redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) }
}
this._undoRedoService.pushElement(elt)
const onFinishEdit = async () => {
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
})
await this._voidModelService.saveModel(uri)
}
return { onFinishEdit }
}
@ -1105,6 +1101,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
}
@ -1878,6 +1875,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
interruptURIStreaming({ uri }: { uri: URI }) {
if (!this._uriIsStreaming(uri)) return
this._undoHistory(uri)
// brute force for now is OK
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
const diffArea = this.diffAreaOfId[diffareaid]
@ -1885,7 +1884,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!diffArea._streamState.isStreaming) continue
this._stopIfStreaming(diffArea)
}
this._undoHistory(uri)
}

View file

@ -216,7 +216,10 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined

View file

@ -773,7 +773,7 @@ const SimplifiedToolHeader = ({
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, currCheckpointIdx, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, currCheckpointIdx: number | undefined, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -924,7 +924,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
</VoidChatArea>
}
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
return <div
// align chatbubble accoridng to role
@ -934,7 +934,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}
${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@ -1850,19 +1850,20 @@ type ChatBubbleProps = {
isCommitted: boolean,
chatIsRunning: IsRunningType,
threadId: string,
currCheckpointIdx: number,
currCheckpointIdx: number | undefined,
_scrollToBottom: (() => void) | null,
}
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
const role = chatMessage.role
const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
if (role === 'user') {
return <UserMessageComponent
chatMessage={chatMessage}
isCheckpointGhost={isCheckpointGhost}
currCheckpointIdx={currCheckpointIdx}
messageIdx={messageIdx}
_scrollToBottom={_scrollToBottom}
/>
@ -2015,7 +2016,8 @@ export const SidebarChat = () => {
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolIsGenerating = !!toolCallSoFar && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit)
// 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)
// ----- SIDEBAR CHAT state (local) -----
@ -2067,11 +2069,10 @@ export const SidebarChat = () => {
const threadId = currentThread.id
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity)
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity)
const previousMessagesHTML = useMemo(() => {
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
// tool request shows up as Editing... if in progress
return previousMessages.map((message, i) => {
return <ChatBubble
@ -2085,13 +2086,13 @@ export const SidebarChat = () => {
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
/>
})
}, [previousMessages, isRunning, threadId])
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
const streamingChatIdx = previousMessagesHTML.length
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
<ChatBubble
key={getChatBubbleId(threadId, streamingChatIdx)}
currCheckpointIdx={currCheckpointIdx} // if streaming, can't be the case
currCheckpointIdx={currCheckpointIdx}
chatMessage={{
role: 'assistant',
displayContent: displayContentSoFar ?? '',
@ -2108,8 +2109,6 @@ export const SidebarChat = () => {
/> : null
const generatingToolTitle = toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ? titleOfToolName[toolCallSoFar.name as ToolName]?.proposed : toolCallSoFar?.name
const messagesHTML = <ScrollToBottomContainer
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
scrollContainerRef={scrollContainerRef}
@ -2124,13 +2123,15 @@ export const SidebarChat = () => {
>
{/* previous messages */}
{previousMessagesHTML}
{currStreamingMessageHTML}
{toolIsGenerating ?
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={generatingToolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)}
title={toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ?
titleOfToolName[toolCallSoFar.name as ToolName]?.proposed
: toolCallSoFar?.name}
desc1={<span className='flex items-center'>Generating<IconLoading /></span>}
/>
: null}
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>

View file

@ -339,11 +339,11 @@ ${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', }))
}
// // 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', }))
// }
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,