mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
checkpoint UX and dropdown UI
This commit is contained in:
parent
b62943fffd
commit
884548615b
7 changed files with 157 additions and 136 deletions
|
|
@ -349,36 +349,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
|
||||
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error(`Error: editing a message with role !=='user'`)
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _currentModelSelectionProps = () => {
|
||||
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
|
||||
const featureName: FeatureName = 'Chat'
|
||||
|
|
@ -883,7 +853,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const [_, toIdx] = c
|
||||
if (toIdx === fromIdx) return
|
||||
|
||||
// console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
console.log(`going from ${fromIdx} to ${toIdx}`)
|
||||
|
||||
// update the user's checkpoint
|
||||
this._addUserModificationsToCurrCheckpoint({ threadId })
|
||||
|
|
@ -895,15 +865,14 @@ A,B,C are all files.
|
|||
x means a checkpoint where the file changed.
|
||||
|
||||
A B C D E F G H I
|
||||
x x x x x x x x x
|
||||
| | | | | | | | |
|
||||
x | | | | | | | x
|
||||
---x-|-|-|-x-|-x-|----- <-- to
|
||||
x | | | | | x
|
||||
| | x x |
|
||||
| | | |
|
||||
-------x-|---x-x------- <-- from
|
||||
x
|
||||
x x x x x x <-- you can't always go up to find the "before" version; sometimes you need to go down
|
||||
| | | | | | x
|
||||
--x-|-|-|-x---x-|----- <-- to
|
||||
| | | | x x
|
||||
| | x x |
|
||||
| | | |
|
||||
----x-|---x-x------- <-- from
|
||||
x
|
||||
|
||||
We need to revert anything that happened between to+1 and from.
|
||||
**We do this by finding the last x from 0...`to` for each file and applying those contents.**
|
||||
|
|
@ -911,9 +880,19 @@ We only need to do it for files that were edited since `to`, ie files between to
|
|||
*/
|
||||
if (toIdx < fromIdx) {
|
||||
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx })
|
||||
|
||||
const idxes = function* () {
|
||||
for (let k = toIdx; k >= 0; k -= 1) { // first go up
|
||||
yield k
|
||||
}
|
||||
for (let k = toIdx + 1; k < thread.messages.length; k += 1) { // then go down
|
||||
yield k
|
||||
}
|
||||
}
|
||||
|
||||
for (const fsPath in lastIdxOfURI) {
|
||||
// apply lowest down content for each uri (or original if not found)
|
||||
for (let k = toIdx; k >= 0; k -= 1) {
|
||||
// find the first instance of this file starting at toIdx (go up to latest file; if there is none, go down)
|
||||
for (const k of idxes()) {
|
||||
const message = thread.messages[k]
|
||||
if (message.role !== 'checkpoint') continue
|
||||
const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
|
||||
|
|
@ -929,16 +908,16 @@ We only need to do it for files that were edited since `to`, ie files between to
|
|||
/*
|
||||
if redoing
|
||||
|
||||
A B C D E F G H I
|
||||
x x x x x x x x x
|
||||
| | | | | | | | |
|
||||
x | | | | | | | x
|
||||
---x-|-|-|-x-|-x-|----- <-- from
|
||||
x | | | | | x
|
||||
| | x x |
|
||||
| | | |
|
||||
-------x-|---x-x------- <-- to
|
||||
x
|
||||
A B C D E F G H I J
|
||||
x x x x x x x
|
||||
| | | | | | x x x
|
||||
--x-|-|-|-x---x-|-|--- <-- from
|
||||
| | | | x x
|
||||
| | x x |
|
||||
| | | |
|
||||
----x-|---x-x-----|--- <-- to
|
||||
x x
|
||||
|
||||
|
||||
We need to apply latest change for anything that happened between from+1 and to.
|
||||
We only need to do it for files that were edited since `from`, ie files between from+1...to.
|
||||
|
|
@ -954,7 +933,6 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
if (!res) continue
|
||||
const { voidFileSnapshot } = res
|
||||
if (!voidFileSnapshot) continue
|
||||
|
||||
this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot)
|
||||
break
|
||||
}
|
||||
|
|
@ -1003,11 +981,15 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
})
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined }, 'merge')
|
||||
}
|
||||
|
||||
|
||||
private async _addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread
|
||||
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') {
|
||||
// if about to call the other LLM, just wait for it by stopping right now
|
||||
|
|
@ -1016,14 +998,11 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
// stop it (this simply resolves the promise to free up space)
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
||||
|
||||
|
||||
// add dummy before this message to keep checkpoint before user message idea consistent
|
||||
if (thread.messages.length === 0) {
|
||||
this._addUserCheckpoint({ threadId })
|
||||
}
|
||||
|
||||
|
||||
const { chatMode } = this._settingsService.state.globalSettings
|
||||
|
||||
// add user's message to chat history
|
||||
|
|
@ -1043,11 +1022,61 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
)
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined }, 'merge')
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId];
|
||||
if (!thread) return
|
||||
|
||||
// if there's a current checkpoint, delete all messages after it
|
||||
if (thread.state.currCheckpointIdx !== null) {
|
||||
const checkpointIdx = thread.state.currCheckpointIdx;
|
||||
const newMessages = thread.messages.slice(0, checkpointIdx + 1);
|
||||
|
||||
// Update the thread with truncated messages
|
||||
const newThreads = {
|
||||
...this.state.allThreads,
|
||||
[threadId]: {
|
||||
...thread,
|
||||
lastModified: new Date().toISOString(),
|
||||
messages: newMessages,
|
||||
}
|
||||
};
|
||||
this._storeAllThreads(newThreads);
|
||||
this._setState({ allThreads: newThreads }, true);
|
||||
}
|
||||
|
||||
// Now call the original method to add the user message and stream the response
|
||||
await this._addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId });
|
||||
|
||||
}
|
||||
|
||||
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
|
||||
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
if (thread.messages?.[messageIdx]?.role !== 'user') {
|
||||
throw new Error(`Error: editing a message with role !=='user'`)
|
||||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
this._setState({
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[thread.id]: {
|
||||
...thread,
|
||||
messages: slicedMessages
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
|
||||
}
|
||||
|
||||
// ---------- the rest ----------
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
this._realignAllDiffAreasLines(uri, change.text, change.range)
|
||||
}
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
// if diffarea has no diffs after a user edit, delete it
|
||||
const diffAreasToDelete: DiffZone[] = []
|
||||
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid] ?? null
|
||||
const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0
|
||||
if (shouldDelete) {
|
||||
diffAreasToDelete.push(diffArea)
|
||||
}
|
||||
}
|
||||
if (diffAreasToDelete.length !== 0) {
|
||||
const { onFinishEdit } = this._addToHistory(uri)
|
||||
diffAreasToDelete.forEach(da => this._deleteDiffZone(da))
|
||||
onFinishEdit()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1949,10 +1949,14 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs
|
|||
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
|
||||
onClick={() => {
|
||||
if (threadIsRunning) return
|
||||
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
chatThreadService.jumpToCheckpointBeforeMessageIdx({
|
||||
threadId,
|
||||
messageIdx,
|
||||
jumpToUserModified: messageIdx === (chatThreadService.state.allThreads[threadId]?.messages.length ?? 0) - 1
|
||||
})
|
||||
}}
|
||||
>
|
||||
Checkpoint
|
||||
Checkpoint
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
--void-bg-1: var(--vscode-input-background);
|
||||
--void-bg-1-alt: var(--vscode-badge-background);
|
||||
--void-bg-2: var(--vscode-sideBar-background);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
|
||||
--void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
|
|
|
|||
|
|
@ -664,57 +664,60 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
{isOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
|
||||
className="z-10 bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: matchInputWidth
|
||||
width: (matchInputWidth
|
||||
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
|
||||
: Math.max(
|
||||
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
|
||||
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
|
||||
),
|
||||
))
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
><div className='overflow-auto max-h-80'>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'bg-void-bg-2-alt hover:bg-void-bg-2'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ const VoidOnboardingContent = () => {
|
|||
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
|
||||
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
|
||||
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
|
||||
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
|
|
@ -520,7 +520,7 @@ const VoidOnboardingContent = () => {
|
|||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligence</div>
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
|
|
@ -531,7 +531,7 @@ const VoidOnboardingContent = () => {
|
|||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Lock size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Privacy</div>
|
||||
<div className="text-lg font-medium text-void-fg-1">Private</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
|
||||
</button>
|
||||
|
|
@ -542,7 +542,7 @@ const VoidOnboardingContent = () => {
|
|||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordability</div>
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -44,37 +44,6 @@ const ModelSelectBox = ({ options, featureName, className }: { options: ModelOpt
|
|||
matchInputWidth={false}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
// const accessor = useAccessor()
|
||||
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// let weChangedText = false
|
||||
|
||||
// return <VoidSelectBox
|
||||
// className='@@[&_select]:!void-text-xs text-void-fg-3'
|
||||
// options={options}
|
||||
// onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
// if (weChangedText) return
|
||||
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// // we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
// onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
// const syncInstance = () => {
|
||||
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
// weChangedText = true
|
||||
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
// weChangedText = false
|
||||
// }
|
||||
// syncInstance()
|
||||
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
// return [disposable]
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// />
|
||||
// }
|
||||
|
||||
|
||||
|
||||
const MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue