mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge remote-tracking branch 'origin/edit-chats' into model-selection
This commit is contained in:
commit
a98106072c
5 changed files with 413 additions and 73 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue