mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
improve checkpoint logic + UI
This commit is contained in:
parent
bd0db41f67
commit
033de587f2
5 changed files with 231 additions and 181 deletions
|
|
@ -55,6 +55,9 @@ LLM Edit
|
|||
x
|
||||
LLM Edit
|
||||
|
||||
|
||||
INVARIANT:
|
||||
A checkpoint appears before every LLM message, and before every user message (before user really means directly after LLM is done).
|
||||
*/
|
||||
|
||||
|
||||
|
|
@ -99,7 +102,7 @@ type ThreadType = {
|
|||
|
||||
// this doesn't need to go in a state object, but feels right
|
||||
state: {
|
||||
currCheckpointIdx: number | null; // the latest checkpoint we're at (always defined unless chat is empty so there are no checkpts)
|
||||
currCheckpointIdx: number | null; // the latest checkpoint we're at (null if not at a particular checkpoint, like if the chat is streaming, or chat just finished and we haven't clicked on a checkpt)
|
||||
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none)
|
||||
|
|
@ -775,8 +778,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')
|
||||
|
||||
// if successful, add checkpoint
|
||||
this._addUserCheckpoint({ threadId })
|
||||
// add checkpoint before the next user message
|
||||
if (!isRunningWhenEnd)
|
||||
this._addUserCheckpoint({ threadId })
|
||||
|
||||
// capture number of messages sent
|
||||
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
|
||||
|
|
@ -785,31 +789,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
private _addCheckpoint(threadId: string, checkpoint: CheckpointEntry) {
|
||||
this._addMessageToThread(threadId, checkpoint)
|
||||
// update latest checkpoint idx to the one we just added
|
||||
const newThread = this.state.allThreads[threadId]
|
||||
if (!newThread) return // should never happen
|
||||
const currCheckpointIdx = newThread.messages.length - 1
|
||||
this._setThreadState(threadId, { currCheckpointIdx })
|
||||
// // update latest checkpoint idx to the one we just added
|
||||
// const newThread = this.state.allThreads[threadId]
|
||||
// if (!newThread) return // should never happen
|
||||
// const currCheckpointIdx = newThread.messages.length - 1
|
||||
// this._setThreadState(threadId, { currCheckpointIdx: currCheckpointIdx })
|
||||
}
|
||||
|
||||
// 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 _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 diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri)
|
||||
|
||||
this._addCheckpoint(threadId, {
|
||||
role: 'checkpoint',
|
||||
type: 'tool_edit',
|
||||
voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot },
|
||||
userModifications: { voidFileSnapshotOfURI: {} },
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private _editMessageInThread(threadId: string, messageIdx: number, newMessage: ChatMessage,) {
|
||||
const { allThreads } = this.state
|
||||
|
|
@ -833,17 +820,25 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => {
|
||||
const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null
|
||||
if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } }
|
||||
|
||||
private _computeCheckpointInfo({ threadId }: { threadId: string }) {
|
||||
const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null
|
||||
return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, }
|
||||
}
|
||||
|
||||
private _computeNewCheckpointInfo({ threadId }: { threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
const { currCheckpointIdx } = thread.state
|
||||
if (currCheckpointIdx === null) return
|
||||
|
||||
const lastCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint') ?? -1
|
||||
if (lastCheckpointIdx === -1) return
|
||||
|
||||
const voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined } = {}
|
||||
|
||||
// add a change for all the URIs in the checkpoint history
|
||||
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: currCheckpointIdx, }) ?? {}
|
||||
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: lastCheckpointIdx, }) ?? {}
|
||||
for (const fsPath in lastIdxOfURI ?? {}) {
|
||||
const { model } = this._voidModelService.getModelFromFsPath(fsPath)
|
||||
if (!model) continue
|
||||
|
|
@ -871,44 +866,32 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
return { voidFileSnapshotOfURI }
|
||||
}
|
||||
|
||||
// call this right before user sends message or reverts
|
||||
|
||||
private _addUserCheckpoint({ threadId }: { threadId: string }) {
|
||||
const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {}
|
||||
const { voidFileSnapshotOfURI } = this._computeNewCheckpointInfo({ threadId }) ?? {}
|
||||
this._addCheckpoint(threadId, {
|
||||
role: 'checkpoint',
|
||||
type: 'user_edit',
|
||||
voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {},
|
||||
userModifications: {
|
||||
voidFileSnapshotOfURI: {},
|
||||
},
|
||||
userModifications: { voidFileSnapshotOfURI: {}, },
|
||||
})
|
||||
}
|
||||
private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) {
|
||||
const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {}
|
||||
|
||||
const res = this._getCurrentCheckpoint(threadId)
|
||||
if (!res) return
|
||||
const [checkpoint, checkpointIdx] = res
|
||||
this._editMessageInThread(threadId, checkpointIdx, {
|
||||
...checkpoint,
|
||||
userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, },
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private _getCurrentCheckpoint(threadId: string): [CheckpointEntry, number] | undefined {
|
||||
// call this right after LLM edits a file
|
||||
private _addToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
const { currCheckpointIdx } = thread.state
|
||||
if (currCheckpointIdx === null) return
|
||||
|
||||
const checkpoint = thread.messages[currCheckpointIdx]
|
||||
if (!checkpoint) return
|
||||
if (checkpoint.role !== 'checkpoint') return
|
||||
return [checkpoint, currCheckpointIdx]
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return // should never happen
|
||||
const diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri)
|
||||
this._addCheckpoint(threadId, {
|
||||
role: 'checkpoint',
|
||||
type: 'tool_edit',
|
||||
voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot },
|
||||
userModifications: { voidFileSnapshotOfURI: {} },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private _getCheckpointBeforeMessage = ({ threadId, messageIdx }: { threadId: string, messageIdx: number }): [CheckpointEntry, number] | undefined => {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return undefined
|
||||
|
|
@ -927,7 +910,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const lastIdxOfURI: { [fsPath: string]: number } = {}
|
||||
for (let i = loIdx; i <= hiIdx; i += 1) {
|
||||
const message = thread.messages[i]
|
||||
if (message.role !== 'checkpoint') continue
|
||||
if (message?.role !== 'checkpoint') continue
|
||||
for (const fsPath in message.voidFileSnapshotOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes
|
||||
lastIdxOfURI[fsPath] = i
|
||||
}
|
||||
|
|
@ -935,27 +918,49 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
return { lastIdxOfURI }
|
||||
}
|
||||
|
||||
private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => {
|
||||
const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null
|
||||
if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } }
|
||||
private _readCurrentCheckpoint(threadId: string): [CheckpointEntry, number] | undefined {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null
|
||||
return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, }
|
||||
const { currCheckpointIdx } = thread.state
|
||||
if (currCheckpointIdx === null) return
|
||||
|
||||
const checkpoint = thread.messages[currCheckpointIdx]
|
||||
if (!checkpoint) return
|
||||
if (checkpoint.role !== 'checkpoint') return
|
||||
return [checkpoint, currCheckpointIdx]
|
||||
}
|
||||
private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) {
|
||||
const { voidFileSnapshotOfURI } = this._computeNewCheckpointInfo({ threadId }) ?? {}
|
||||
const res = this._readCurrentCheckpoint(threadId)
|
||||
if (!res) return
|
||||
const [checkpoint, checkpointIdx] = res
|
||||
this._editMessageInThread(threadId, checkpointIdx, {
|
||||
...checkpoint,
|
||||
userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => {
|
||||
// const { model } = this._voidModelService.getModelFromFsPath(fsPath)
|
||||
// if (!model) return // should never happen
|
||||
// model.applyEdits([{
|
||||
// range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file
|
||||
// text
|
||||
// }])
|
||||
// }
|
||||
|
||||
jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) {
|
||||
private _makeUsStandOnCheckpoint({ threadId }: { threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
if (thread.state.currCheckpointIdx === null) {
|
||||
const lastMsg = thread.messages[thread.messages.length - 1]
|
||||
if (lastMsg?.role !== 'checkpoint')
|
||||
this._addUserCheckpoint({ threadId })
|
||||
this._setThreadState(threadId, { currCheckpointIdx: thread.messages.length - 1 })
|
||||
}
|
||||
}
|
||||
|
||||
jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) {
|
||||
|
||||
// if null, add a new temp checkpoint so user can jump forward again
|
||||
this._makeUsStandOnCheckpoint({ threadId })
|
||||
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
if (this.streamState[threadId]?.isRunning) return
|
||||
|
||||
const c = this._getCheckpointBeforeMessage({ threadId, messageIdx })
|
||||
if (c === undefined) return // should never happen
|
||||
|
|
@ -1045,7 +1050,6 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
}
|
||||
|
||||
this._setThreadState(threadId, { currCheckpointIdx: toIdx })
|
||||
// TODO!!! add/merge a checkpoint modification if relevant
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1090,6 +1094,12 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
// add dummy before this message to keep checkpoint before user message idea consistent
|
||||
if (thread.messages.length === 0) {
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
// if the current thread is already streaming, stop it (this simply resolves the promise to free up space)
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
|
@ -1109,6 +1119,8 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming
|
||||
|
||||
this._wrapRunAgentToNotify(
|
||||
this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }),
|
||||
threadId,
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, u
|
|||
</div>
|
||||
}
|
||||
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
|
@ -255,7 +255,7 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string
|
|||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
return <IconShell1 Icon={Play} onClick={onClickSubmit} />
|
||||
return <IconShell1 Icon={reapplyIcon ? RotateCw : Play} onClick={onClickSubmit} />
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
|
|
@ -322,7 +322,7 @@ export const BlockCodeApplyWrapper = ({
|
|||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../
|
|||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { ChatMessage, CheckpointEntry, 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';
|
||||
import { IsRunningType } from '../../../chatThreadService.js';
|
||||
|
|
@ -769,7 +769,7 @@ const SimplifiedToolHeader = ({
|
|||
|
||||
|
||||
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, _scrollToBottom: (() => void) | null }) => {
|
||||
const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
|
@ -886,7 +886,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB
|
|||
}
|
||||
}
|
||||
|
||||
if (!chatMessage.content && isCommitted) { // don't show if empty and not loading (if loading, want to show).
|
||||
if (!chatMessage.content) { // don't show if empty and not loading (if loading, want to show).
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -929,6 +929,8 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB
|
|||
${mode === 'edit' ? 'w-full max-w-full'
|
||||
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
|
||||
}
|
||||
|
||||
${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
|
|
@ -1062,12 +1064,11 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => {
|
|||
{children}
|
||||
</div>
|
||||
}
|
||||
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, isToolBeingWritten }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType, isToolBeingWritten: boolean }) => {
|
||||
const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
|
||||
const reasoningStr = chatMessage.reasoning?.trim() || null
|
||||
const hasReasoning = !!reasoningStr
|
||||
const isDoneReasoning = !!chatMessage.content
|
||||
|
|
@ -1080,34 +1081,36 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas
|
|||
}
|
||||
|
||||
const isEmpty = !chatMessage.content && !chatMessage.reasoning
|
||||
const isLoading = !isCommitted && !isToolBeingWritten && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user')
|
||||
const isLastAndLoading = isLast && isLoading
|
||||
if (isEmpty && !isLastAndLoading) return null
|
||||
if (isEmpty) return null
|
||||
|
||||
return <>
|
||||
{/* reasoning token */}
|
||||
{hasReasoning && <ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={false}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</SmallProseWrapper>
|
||||
</ReasoningWrapper>}
|
||||
{hasReasoning &&
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={false}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</SmallProseWrapper>
|
||||
</ReasoningWrapper>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* assistant message */}
|
||||
<ProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={chatMessage.content || ''}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={true}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
{/* loading indicator */}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</ProseWrapper>
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<ProseWrapper>
|
||||
<ChatMarkdownRender
|
||||
string={chatMessage.content || ''}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={true}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</ProseWrapper>
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
|
|
@ -1321,7 +1324,7 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
|
|||
<StatusIndicatorHTML applyBoxId={applyBoxId} uri={uri} />
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} />}
|
||||
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} />
|
||||
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} reapplyIcon={true} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -1822,20 +1825,26 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
|
|||
};
|
||||
|
||||
|
||||
const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => {
|
||||
const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadService = accessor.get('IChatThreadService')
|
||||
// const commandBarService = accessor.get('IVoidCommandBarService')
|
||||
|
||||
return <div
|
||||
className='pointer-events-auto cursor-pointer select-none hover:brightness-125 flex items-center justify-center'
|
||||
onClick={() => {
|
||||
// reject all current changes and then jump back
|
||||
// commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' })
|
||||
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}}>
|
||||
<div className='bg-void-border-1 h-[1px] flex-grow'></div>
|
||||
<div className='px-2'>Checkpoint</div>
|
||||
<div className='bg-void-border-1 h-[1px] flex-grow'></div>
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
px-2 text-xs text-void-fg-3
|
||||
${isCheckpointGhost ? 'opacity-50' : ''}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className='cursor-pointer select-none hover:brightness-125'
|
||||
onClick={() => {
|
||||
if (threadIsRunning) return
|
||||
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}}
|
||||
>
|
||||
Checkpoint
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
|
@ -1845,45 +1854,57 @@ type ChatBubbleProps = {
|
|||
chatMessage: ChatMessage,
|
||||
messageIdx: number,
|
||||
isCommitted: boolean,
|
||||
isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message)
|
||||
canAcceptReject: boolean,
|
||||
chatIsRunning: IsRunningType,
|
||||
threadId: string,
|
||||
isToolBeingWritten: boolean,
|
||||
currCheckpointIdx: number,
|
||||
_scrollToBottom: (() => void) | null,
|
||||
}
|
||||
|
||||
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId, isToolBeingWritten, _scrollToBottom }: ChatBubbleProps) => {
|
||||
const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, canAcceptReject, chatIsRunning, threadId, _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)
|
||||
|
||||
if (role === 'user') {
|
||||
return <UserMessageComponent
|
||||
chatMessage={chatMessage}
|
||||
isCheckpointGhost={isCheckpointGhost}
|
||||
messageIdx={messageIdx}
|
||||
isCommitted={isCommitted}
|
||||
_scrollToBottom={_scrollToBottom}
|
||||
/>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
return <AssistantMessageComponent
|
||||
chatMessage={chatMessage}
|
||||
isCheckpointGhost={isCheckpointGhost}
|
||||
messageIdx={messageIdx}
|
||||
isCommitted={isCommitted}
|
||||
chatIsRunning={chatIsRunning}
|
||||
isLast={isLast}
|
||||
isToolBeingWritten={isToolBeingWritten}
|
||||
/>
|
||||
}
|
||||
else if (role === 'tool_request') {
|
||||
const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
|
||||
const toolRequestType = (
|
||||
const toolRequestState = (
|
||||
chatIsRunning === 'awaiting_user' ? 'awaiting_user'
|
||||
: chatIsRunning === 'tool' ? 'running'
|
||||
: null
|
||||
: chatIsRunning === 'message' ? null
|
||||
: null
|
||||
)
|
||||
if (ToolRequestWrapper && isLast) { // if it's the last message
|
||||
if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
|
||||
return <>
|
||||
{toolRequestType !== null && <ToolRequestWrapper toolRequestState={toolRequestType} toolRequest={chatMessage} messageIdx={messageIdx} threadId={threadId} />}
|
||||
{chatIsRunning === 'awaiting_user' && <ToolRequestAcceptRejectButtons />}
|
||||
{toolRequestState !== null &&
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<ToolRequestWrapper
|
||||
toolRequestState={toolRequestState}
|
||||
toolRequest={chatMessage}
|
||||
messageIdx={messageIdx}
|
||||
threadId={threadId}
|
||||
/>
|
||||
</div>}
|
||||
{chatIsRunning === 'awaiting_user' &&
|
||||
<div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<ToolRequestAcceptRejectButtons />
|
||||
</div>}
|
||||
</>
|
||||
}
|
||||
return null
|
||||
|
|
@ -1891,17 +1912,26 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin
|
|||
else if (role === 'tool') {
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
|
||||
if (ToolResultWrapper)
|
||||
return <ToolResultWrapper toolMessage={chatMessage} messageIdx={messageIdx} threadId={threadId} />
|
||||
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
<ToolResultWrapper
|
||||
toolMessage={chatMessage}
|
||||
messageIdx={messageIdx}
|
||||
threadId={threadId}
|
||||
/>
|
||||
</div>
|
||||
return null
|
||||
}
|
||||
|
||||
else if (role === 'checkpoint') {
|
||||
return <Checkpoint threadId={threadId} messageIdx={messageIdx} />
|
||||
return <Checkpoint
|
||||
threadId={threadId}
|
||||
message={chatMessage}
|
||||
messageIdx={messageIdx}
|
||||
isCheckpointGhost={isCheckpointGhost}
|
||||
threadIsRunning={!!chatIsRunning}
|
||||
/>
|
||||
}
|
||||
|
||||
else if (role === 'checkpoint_modification') {
|
||||
return <Checkpoint threadId={threadId} messageIdx={messageIdx} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1974,7 +2004,7 @@ export const SidebarChat = () => {
|
|||
|
||||
const toolNameSoFar = currThreadStreamState?.toolNameSoFar
|
||||
const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar
|
||||
const toolIsLoading = !!toolNameSoFar && toolNameSoFar === 'edit' // show loading for slow tools (right now just edit)
|
||||
const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit)
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
|
|
@ -2024,36 +2054,37 @@ export const SidebarChat = () => {
|
|||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
const numMessages = previousMessages.length
|
||||
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
|
||||
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 === lastMessageIdx && (isRunning === 'tool' || isRunning === 'awaiting_user')
|
||||
return <div
|
||||
key={getChatBubbleId(currentThread.id, i)}
|
||||
className={`${currCheckpointIdx < i ? 'opacity-50 pointer-events-none select-none' : ''}`}>
|
||||
<ChatBubble
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
isCommitted={true}
|
||||
chatIsRunning={isRunning}
|
||||
isLast={isLast}
|
||||
threadId={threadId}
|
||||
isToolBeingWritten={toolIsLoading}
|
||||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||
/>
|
||||
</div>
|
||||
})
|
||||
}, [previousMessages, isRunning, currentThread, numMessages])
|
||||
|
||||
const threadId = currentThread.id
|
||||
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity)
|
||||
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
|
||||
console.log('PREVMSGS', previousMessages)
|
||||
|
||||
return previousMessages.map((message, i) => {
|
||||
const canAcceptReject = i === lastMessageIdx && message.role === 'tool_request'
|
||||
|
||||
return <ChatBubble
|
||||
key={getChatBubbleId(threadId, i)}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
isCommitted={true}
|
||||
chatIsRunning={isRunning}
|
||||
canAcceptReject={canAcceptReject}
|
||||
threadId={threadId}
|
||||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||
/>
|
||||
})
|
||||
}, [previousMessages, isRunning, threadId])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ?
|
||||
<ChatBubble
|
||||
key={getChatBubbleId(threadId, streamingChatIdx)}
|
||||
currCheckpointIdx={currCheckpointIdx} // if streaming, can't be the case
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
content: messageSoFar ?? '',
|
||||
|
|
@ -2061,28 +2092,17 @@ export const SidebarChat = () => {
|
|||
anthropicReasoning: null,
|
||||
}}
|
||||
messageIdx={streamingChatIdx}
|
||||
isCommitted={!isRunning}
|
||||
isCommitted={false}
|
||||
chatIsRunning={isRunning}
|
||||
isLast={true}
|
||||
canAcceptReject={false}
|
||||
|
||||
threadId={threadId}
|
||||
isToolBeingWritten={toolIsLoading}
|
||||
_scrollToBottom={null}
|
||||
/> : null
|
||||
|
||||
|
||||
const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
|
||||
const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed
|
||||
const currStreamingToolHTML = toolIsLoading ?
|
||||
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={toolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
|
||||
: null
|
||||
|
||||
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML]
|
||||
|
||||
const threadSelector = <div
|
||||
className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
const proposedToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
|
||||
const generatingToolTitle = typeof proposedToolTitle === 'function' ? proposedToolTitle(null) : proposedToolTitle
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
|
||||
|
|
@ -2097,7 +2117,20 @@ export const SidebarChat = () => {
|
|||
`}
|
||||
>
|
||||
{/* previous messages */}
|
||||
{allMessagesHTML}
|
||||
{previousMessagesHTML}
|
||||
|
||||
|
||||
{currStreamingMessageHTML}
|
||||
|
||||
|
||||
{toolIsGenerating ?
|
||||
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={generatingToolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
|
||||
: null}
|
||||
|
||||
{isRunning === 'message' && !toolIsGenerating ? <ProseWrapper>
|
||||
{/* loading indicator */}
|
||||
{<IconLoading className='opacity-50 text-sm' />}
|
||||
</ProseWrapper> : null}
|
||||
|
||||
|
||||
{/* error message */}
|
||||
|
|
@ -2158,7 +2191,11 @@ export const SidebarChat = () => {
|
|||
|
||||
return (
|
||||
<div ref={sidebarRef} className='w-full h-full flex flex-col overflow-hidden'>
|
||||
{threadSelector}
|
||||
{/* History selector */}
|
||||
<div className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
<div className='flex-1 flex flex-col overflow-hidden'>
|
||||
<div className={`flex-1 overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
|
||||
{messagesHTML}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const SidebarThreadSelector = () => {
|
|||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ Here's an example of a good description:\n${editToolDescription}.`
|
|||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
|
||||
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
|
||||
: mode === 'normal' ? `to assist the user with their coding tasks.`
|
||||
|
|
@ -224,6 +224,7 @@ Misc:
|
|||
- Do not be lazy.
|
||||
- NEVER re-write the entire file.
|
||||
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.
|
||||
- Today's date is ${new Date().toDateString()}
|
||||
The user's codebase is structured as follows:\n${directoryStr}
|
||||
\
|
||||
`
|
||||
|
|
|
|||
Loading…
Reference in a new issue