From 3ac9dcf0c0c827dc0cd456bc31957de577fa872c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 2 Apr 2025 19:13:51 -0700 Subject: [PATCH] checkpoint progress --- .../contrib/void/browser/chatThreadService.ts | 39 ++++++++------- .../react/src/sidebar-tsx/SidebarChat.tsx | 48 +++++++++++-------- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8df93ec6..9331f841 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -104,7 +104,7 @@ type ThreadType = { // this doesn't need to go in a state object, but feels right state: { - latestCheckpointIdx: number | null; // the latest checkpoint we're standing at or null + currCheckpointIdx: number | null; // the latest checkpoint we're standing at or null stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) @@ -122,7 +122,7 @@ type ChatThreads = { } export const defaultThreadState: ThreadType['state'] = { - latestCheckpointIdx: null, + currCheckpointIdx: null, stagingSelections: [], focusedMessageIdx: undefined, linksOfMessageIdx: {}, @@ -629,8 +629,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { delete this._currentlyRunningToolInterruptor[threadId]; } toolResult = await result // ts is bad... await is needed - - if (toolName === 'edit') { this._addOrUpdateToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } } catch (error) { if (interrupted) { @@ -654,6 +652,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + + // 6. add a checkpoint + if (toolName === 'edit') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } return {} }; @@ -778,35 +779,41 @@ class ChatThreadService extends Disposable implements IChatThreadService { const newThread = this.state.allThreads[threadId] if (!newThread) return // should never happen const latestCheckpointIdx = newThread.messages.length - 1 - this._setThreadState(threadId, { latestCheckpointIdx }) + this._setThreadState(threadId, { currCheckpointIdx: latestCheckpointIdx }) } // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint // call this right after LLM edits a file - private _addOrUpdateToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { + private _addToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { const thread = this.state.allThreads[threadId] if (!thread) return const { model } = this._voidModelService.getModel(uri) if (!model) return // should never happen - const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') - const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') const afterStr = model.getValue() // afterStr = the value of the file right after the edit - let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined - if (prevLLMCheckpointIdx !== -1) { - prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } - this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) - } const newLLMCheckpoint: LLMCheckpoint = { role: 'checkpoint', type: 'after_tool_edits', afterStrOfURI: { - ...prevLLMCheckpoint?.afterStrOfURI, [uri.fsPath]: afterStr, }, } + + // remove and merge + // const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') + // const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') + // let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined + // if (prevLLMCheckpointIdx !== -1) { + // prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } + // this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) + // newLLMCheckpoint.afterStrOfURI = { + // ...newLLMCheckpoint.afterStrOfURI, + // ...prevLLMCheckpoint?.afterStrOfURI, + // } + // } + this._addCheckpoint(threadId, newLLMCheckpoint) } @@ -894,7 +901,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const c = this._getCheckpointAfter({ threadId, messageIdx }) if (c === undefined) return // should never happen - const fromIdx = thread.state.latestCheckpointIdx + const fromIdx = thread.state.currCheckpointIdx if (fromIdx === null) return // should never happen // TODO!!! change toIdx if there's a checkpointModification on the To, and add a checkpoint modification on the from @@ -990,7 +997,7 @@ We only need to do it for files that were edited since `from`, ie files between } } - this._setThreadState(threadId, { latestCheckpointIdx: toIdx }) + this._setThreadState(threadId, { currCheckpointIdx: toIdx }) // TODO!!! add/merge a checkpoint modification if relevant } 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 7e948af7..beba18f1 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 @@ -25,7 +25,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; +import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; @@ -988,7 +988,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB - {} + /> @@ -1821,12 +1821,18 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => { const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - - return + const commandBarService = accessor.get('IVoidCommandBarService') + return
{ + // reject all current changes and then jump back + commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }) + chatThreadService.jumpToCheckpointAfterMessageIdx({ threadId, messageIdx }) + }}> +
+
Checkpoint
+
+
} @@ -2021,18 +2027,22 @@ export const SidebarChat = () => { const previousMessagesHTML = useMemo(() => { const threadId = currentThread.id + const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) + return previousMessages.map((message, i) => { const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user') - return scrollToBottom(scrollContainerRef)} - /> + return
+ scrollToBottom(scrollContainerRef)} + /> +
}) }, [previousMessages, isRunning, currentThread, numMessages])