Merge remote-tracking branch 'origin/edit-chats' into model-selection

This commit is contained in:
Andrew Pareles 2025-02-10 15:35:37 -08:00
commit a98106072c
5 changed files with 413 additions and 73 deletions

View file

@ -33,6 +33,14 @@ export type FileSelection = {
export type StagingSelectionItem = CodeSelection | FileSelection
export type StagingInfo = {
isBeingEdited: boolean;
selections: StagingSelectionItem[] | null; // staging selections in edit mode
}
const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] }
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
@ -40,6 +48,7 @@ export type ChatMessage =
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
staging: StagingInfo | null
}
| {
role: 'assistant';
@ -59,13 +68,14 @@ export type ChatThreads = {
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
staging: StagingInfo | null;
focusedMessageIdx?: number | undefined; // index of the message that is being edited (undefined if none)
};
}
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
currentStagingSelections: StagingSelectionItem[] | null;
}
export type ThreadStreamState = {
@ -84,11 +94,16 @@ const newThreadObject = () => {
createdAt: now,
lastModified: now,
messages: [],
focusedMessageIdx: undefined,
staging: {
isBeingEdited: true,
selections: [],
}
} satisfies ChatThreads[string]
}
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
const THREAD_VERSION = 'v1'
const THREAD_VERSION = 'v2'
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
@ -105,8 +120,13 @@ export interface IChatThreadService {
openNewThread(): void;
switchToThread(threadId: string): void;
setStaging(stagingSelection: StagingSelectionItem[] | null): void;
getFocusedMessageIdx(): number | undefined;
isFocusingMessage(): boolean;
setFocusedMessageIdx(messageIdx: number | undefined): void;
_useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
@ -137,7 +157,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this.state = {
allThreads: this._readAllThreads(),
currentThreadId: null as unknown as string, // gets set in startNewThread()
currentStagingSelections: null,
}
// always be in a thread
@ -145,14 +164,71 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// for now just write the version, anticipating bigger changes in the future where we'll want to access this
this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
}
private _readAllThreads(): ChatThreads {
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
// CAN ADD "v0" TAG IN STORAGE AND CONVERT
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
return threads ? JSON.parse(threads) : {}
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
this._updateThreadsToVersion(threads, THREAD_VERSION)
return threads
}
private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) {
if (toVersion === 'v2') {
const threads: ChatThreads = oldThreadsObject
/** v1 -> v2
- threadsState.currentStagingSelections: CodeStagingSelection[] | null;
+ thread.staging: StagingInfo
+ thread.focusedMessageIdx?: number | undefined;
+ chatMessage.staging: StagingInfo | null
*/
// check if we need to update
let shouldUpdate = false
for (const thread of Object.values(threads)) {
if (!thread.staging) {
shouldUpdate = true
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.staging) {
shouldUpdate = true
}
}
}
if (!shouldUpdate) return;
// update the threads
for (const thread of Object.values(threads)) {
if (!thread.staging) {
thread.staging = defaultStaging
thread.focusedMessageIdx = undefined
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.staging) {
chatMessage.staging = defaultStaging
}
}
}
// push the update
this._storeAllThreads(threads)
}
}
private _storeAllThreads(threads: ChatThreads) {
@ -187,18 +263,51 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
}
async addUserMessageAndStreamResponse(userMessage: string) {
const threadId = this.getCurrentThread().id
const currSelns = this.state.currentStagingSelections ?? []
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
const thread = this.getCurrentThread()
const messageToReplace = thread.messages[messageIdx]
if (messageToReplace?.role !== 'user') {
console.log(`Error: tried to edit non-user message. messageIdx=${messageIdx}, numMessages=${thread.messages.length}`)
return
}
// 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)
// stream the edit
this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging)
}
async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) {
const thread = this.getCurrentThread()
const threadId = thread.id
let threadStaging = thread.staging
const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing
const { selections: currSelns, } = currStaging
// add user's message to chat history
const instructions = userMessage
const content = await chat_userMessage(instructions, currSelns, this._modelService)
const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns }
const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, }
this._addMessageToThread(threadId, userHistoryElt)
this._setStreamState(threadId, { error: undefined })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
@ -241,12 +350,29 @@ class ChatThreadService extends Disposable implements IChatThreadService {
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId];
return state.allThreads[state.currentThreadId]
}
getFocusedMessageIdx() {
const thread = this.getCurrentThread()
// get the focusedMessageIdx
const focusedMessageIdx = thread.focusedMessageIdx
if (focusedMessageIdx === undefined) return;
// check that the message is actually being edited
const focusedMessage = thread.messages[focusedMessageIdx]
if (focusedMessage.role !== 'user') return;
if (!focusedMessage.staging?.isBeingEdited) return;
return focusedMessageIdx
}
isFocusingMessage() {
return this.getFocusedMessageIdx() !== undefined
}
switchToThread(threadId: string) {
// console.log('threadId', threadId)
// console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ currentThreadId: threadId }, true)
}
@ -291,11 +417,93 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
setFocusedMessageIdx(messageIdx: number | undefined) {
setStaging(stagingSelection: StagingSelectionItem[] | null): void {
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
focusedMessageIdx: messageIdx
}
}
}, true)
}
// set thread.messages[messageIdx].stagingSelections
private setEditMessageStaging(staging: StagingInfo, messageIdx: number): void {
const thread = this.getCurrentThread()
const message = thread.messages[messageIdx]
if (message.role !== 'user') return;
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
messages: thread.messages.map((m, i) =>
i === messageIdx ? {
...m,
staging,
} : m
)
}
}
}, true)
}
// set thread.stagingSelections
private setDefaultStaging(staging: StagingInfo): void {
const thread = this.getCurrentThread()
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
staging,
}
}
}, true)
}
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
_useFocusedStagingState(messageIdx?: number | undefined) {
const defaultStaging = { isBeingEdited: false, selections: [], text: '' }
let staging: StagingInfo = defaultStaging
let setStaging: (selections: StagingInfo) => void = () => { }
const thread = this.getCurrentThread()
const isFocusingMessage = messageIdx !== undefined
if (isFocusingMessage) { // is editing message
const message = thread.messages[messageIdx!]
if (message.role === 'user') {
staging = message.staging || defaultStaging
setStaging = (s) => this.setEditMessageStaging(s, messageIdx)
}
}
else { // is editing the default input box
staging = thread.staging || defaultStaging
setStaging = this.setDefaultStaging.bind(this)
}
return [staging, setStaging] as const
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);

View file

@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
import { ChatMessage, StagingInfo, StagingSelectionItem } from '../../../chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
@ -21,11 +21,12 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { Pencil } from 'lucide-react';
import { Pencil, X } from 'lucide-react';
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
return (
<svg
@ -145,15 +146,19 @@ interface VoidChatAreaProps {
onAbort: () => void;
isStreaming: boolean;
isDisabled?: boolean;
divRef: React.RefObject<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement>;
// UI customization
featureName: FeatureName;
className?: string;
showModelDropdown?: boolean;
showSelections?: boolean;
selections?: any[];
onSelectionsChange?: (selections: any[]) => void;
showProspectiveSelections?: boolean;
staging?: StagingInfo
setStaging?: (s: StagingInfo) => void
// selections?: any[];
// onSelectionsChange?: (selections: any[]) => void;
onClickAnywhere?: () => void;
// Optional close button
@ -173,8 +178,9 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
showModelDropdown = true,
featureName,
showSelections = false,
selections = [],
onSelectionsChange,
showProspectiveSelections = true,
staging,
setStaging,
}) => {
return (
<div
@ -192,11 +198,12 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
}}
>
{/* Selections section */}
{showSelections && onSelectionsChange && (
{showSelections && staging && setStaging && (
<SelectedFiles
type='staging'
selections={selections}
setSelections={onSelectionsChange}
selections={staging.selections || []}
setSelections={(selections) => setStaging({ ...staging, selections })}
showProspectiveSelections={showProspectiveSelections}
/>
)}
@ -535,19 +542,57 @@ export const SelectedFiles = (
type ChatBubbleMode = 'display' | 'edit'
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
const role = chatMessage.role
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
// edit mode state
const [mode, setMode] = useState<ChatBubbleMode>('display')
const [editText, setEditText] = useState(chatMessage.displayContent ?? '')
const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx)
const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display'
const [isFocused, setIsFocused] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const [isDisabled, setIsDisabled] = useState(false)
const [textAreaRefState, setTextAreaRef] = useState<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
// initialize on first render, and when edit was just enabled
const _mustInitialize = useRef(true)
const _justEnabledEdit = useRef(false)
useEffect(() => {
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
if (canInitialize && shouldInitialize) {
setStaging({
...staging,
selections: chatMessage.selections || [],
})
if (textAreaFnsRef.current)
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
return null
textAreaRefState.focus();
_justEnabledEdit.current = false
_mustInitialize.current = false
}
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
const EditSymbol = mode === 'display' ? Pencil : X
const onOpenEdit = () => {
setStaging({ ...staging, isBeingEdited: true })
chatThreadsService.setFocusedMessageIdx(messageIdx)
_justEnabledEdit.current = true
}
const onCloseEdit = () => {
setIsFocused(false)
setIsHovered(false)
setStaging({ ...staging, isBeingEdited: false })
chatThreadsService.setFocusedMessageIdx(undefined)
}
// set chat bubble contents
let chatbubbleContents: React.ReactNode
if (role === 'user') {
@ -558,18 +603,73 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
</>
}
else if (mode === 'edit') {
const onSubmit = async () => {
if (isDisabled) return;
if (!textAreaRefState) return;
if (messageIdx === undefined) return;
// cancel any streams on this thread
const thread = chatThreadsService.getCurrentThread()
chatThreadsService.cancelStreaming(thread.id)
// reset state
setStaging({ ...staging, isBeingEdited: false })
chatThreadsService.setFocusedMessageIdx(undefined)
// stream the edit
const userMessage = textAreaRefState.value;
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
}
const onAbort = () => {
const threadId = chatThreadsService.state.currentThreadId
chatThreadsService.cancelStreaming(threadId)
}
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') {
onCloseEdit()
}
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
return null
}
chatbubbleContents = <>
<SelectedFiles type='past' selections={chatMessage.selections || []} />
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
className={`
w-full max-w-full
h-auto min-h-[81px] max-h-[500px]
bg-void-bg-1 resize-none
`}
style={{ marginTop: 0 }}
/>
<VoidChatArea
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={false}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={false}
featureName="Ctrl+L"
staging={staging}
setStaging={setStaging}
>
<VoidInputBox2
ref={setTextAreaRef}
className='min-h-[81px] max-h-[500px] p-1'
placeholder="Edit your message..."
onChangeText={(text) => setIsDisabled(!text)}
onFocus={() => {
setIsFocused(true)
chatThreadsService.setFocusedMessageIdx(messageIdx);
}}
onBlur={() => {
setIsFocused(false)
}}
onKeyDown={onKeyDown}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
</>
}
}
@ -594,8 +694,11 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
// style chatbubble according to role
className={`
text-left rounded-lg
overflow-x-auto max-w-full
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
max-w-full
${mode === 'edit' ? ''
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
}
`}
>
{chatbubbleContents}
@ -603,7 +706,7 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
</div>
{/* edit button */}
{role === 'user' && <Pencil
{role === 'user' && <EditSymbol
size={18}
className={`
absolute -top-1 right-1
@ -612,9 +715,15 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered ? 'opacity-100' : 'opacity-0'}
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => setMode(m => m === 'display' ? 'edit' : 'display')}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>}
</div>
}
@ -628,6 +737,7 @@ export const SidebarChat = () => {
const accessor = useAccessor()
// const modelService = accessor.get('IModelService')
const commandService = accessor.get('ICommandService')
const chatThreadsService = accessor.get('IChatThreadService')
const settingsState = useSettingsState()
// ----- HIGHER STATE -----
@ -636,8 +746,8 @@ export const SidebarChat = () => {
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.blur() })
)
return () => disposables.forEach(d => d.dispose())
}, [sidebarStateService, textAreaRef])
@ -646,11 +756,10 @@ export const SidebarChat = () => {
// threads state
const chatThreadsState = useChatThreadsState()
const chatThreadsService = accessor.get('IChatThreadService')
const currentThread = chatThreadsService.getCurrentThread()
const previousMessages = currentThread?.messages ?? []
const selections = chatThreadsState.currentStagingSelections
const [staging, setStaging] = chatThreadsService._useFocusedStagingState()
// stream state
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
@ -675,8 +784,6 @@ export const SidebarChat = () => {
const onSubmit = useCallback(async () => {
console.log('onSubmit')
if (isDisabled) return
if (isStreaming) return
@ -684,11 +791,11 @@ export const SidebarChat = () => {
const userMessage = textAreaRef.current?.value ?? ''
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
chatThreadsService.setStaging([]) // clear staging
setStaging({ ...staging, selections: [], }) // clear staging
textAreaFnsRef.current?.setValue('')
textAreaRef.current?.focus() // focus input after submit
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef])
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, staging, setStaging])
const onAbort = () => {
const threadId = currentThread.id
@ -709,7 +816,7 @@ export const SidebarChat = () => {
const prevMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
)
}, [previousMessages])
@ -773,8 +880,9 @@ export const SidebarChat = () => {
isStreaming={isStreaming}
isDisabled={isDisabled}
showSelections={true}
selections={selections || []}
onSelectionsChange={chatThreadsService.setStaging.bind(chatThreadsService)}
showProspectiveSelections={prevMessagesHTML.length === 0}
staging={staging}
setStaging={setStaging}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
featureName="Ctrl+L"
>
@ -783,6 +891,7 @@ export const SidebarChat = () => {
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}

View file

@ -58,9 +58,11 @@ type InputBox2Props = {
className?: string;
onChangeText?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onChangeHeight?: (newHeight: number) => void;
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
@ -114,6 +116,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
onFocus={onFocus}
onBlur={onBlur}
disabled={!isEnabled}
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}

View file

@ -288,6 +288,17 @@ export const useChatThreadsState = () => {
return () => { chatThreadsStateListeners.delete(ss) }
}, [ss])
return s
// allow user to set state natively in react
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
// _ss(action)
// if (typeof action === 'function') {
// const newState = action(chatThreadsState)
// chatThreadsState = newState
// } else {
// chatThreadsState = action
// }
// }
// return [s, ss] as const
}

View file

@ -67,6 +67,13 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
}
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
return currentSelections?.findIndex(s =>
s.fileURI.fsPath === newSelection.fileURI.fsPath
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
)
}
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
@ -124,26 +131,26 @@ registerAction2(class extends Action2 {
range: selectionRange,
}
// add selection to staging
// update the staging selections
const chatThreadService = accessor.get(IChatThreadService)
const currentStaging = chatThreadService.state.currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
chatThreadService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
const [staging, setStaging] = chatThreadService._useFocusedStagingState(focusedMessageIdx)
const selections = staging.selections || []
const setSelections = (s: StagingSelectionItem[]) => setStaging({ ...staging, selections: s })
// if matches with existing selection, overwrite (since text may change)
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
setSelections([
...selections!.slice(0, matchingStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
])
}
// if no match, add
// if no match, add it
else {
chatThreadService.setStaging([...(currentStaging ?? []), selection])
setSelections([...(selections ?? []), selection])
}
}