From f287a5388f62399132ee22a54cd554aff5ee2c68 Mon Sep 17 00:00:00 2001 From: Shi Bai Date: Fri, 28 Feb 2025 16:08:43 -0800 Subject: [PATCH 1/5] Add history chat thread remove button in the chat side nav - Added a trash icon to the right of the chat thread name to remove a thread history. - Added 2 icon size constants in SidebarThreadSelector.tsx to avoid the usage of magic numbers. --- .../contrib/void/browser/chatThreadService.ts | 35 +++++++++++++++++++ .../react/src/sidebar-tsx/SidebarChat.tsx | 30 ++++++++++++++++ .../src/sidebar-tsx/SidebarThreadSelector.tsx | 16 +++++++-- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index d92fb772..3c1f1a2f 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -156,6 +156,7 @@ export interface IChatThreadService { getCurrentThread(): ChatThreads[string]; openNewThread(): void; switchToThread(threadId: string): void; + deleteThreadById(threadId: string): void; // you can edit multiple messages // the one you're currently editing is "focused", and we add items to that one when you press cmd+L. @@ -281,6 +282,40 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER) } + public deleteThreadById(threadId: string): void { + const { allThreads, currentThreadId } = this.state + const newThreads = { ...allThreads } + if (threadId in newThreads) { + delete newThreads[threadId] + } + + // If we're deleting the current thread, switch to another thread first + const needsThreadSwitch = threadId === currentThreadId + let newCurrentThreadId = currentThreadId + + if (needsThreadSwitch) { + // Find another thread to switch to + const remainingThreadIds = Object.keys(newThreads) + if (remainingThreadIds.length > 0) { + // Switch to the most recently modified thread + newCurrentThreadId = remainingThreadIds.sort((threadId1, threadId2) => + newThreads[threadId2].lastModified > newThreads[threadId1].lastModified ? 1 : -1 + )[0] + } else { + // If no threads left, create a new one + const newThread = newThreadObject() + newThreads[newThread.id] = newThread + newCurrentThreadId = newThread.id + } + } + + this._storeAllThreads(newThreads) + this._setState({ + allThreads: newThreads, + currentThreadId: newCurrentThreadId + }, true) + } + // this should be the only place this.state = ... appears besides constructor private _setState(state: Partial, affectsCurrent: boolean) { this.state = { diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 979ae67b..9cf7cc1a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -138,6 +138,36 @@ export const IconLoading = ({ className = '' }: { className?: string }) => { } +export const IconTrash = ({ + size, + className = '', + onClick +}: { + size: number, + className?: string, + onClick?: (event: React.MouseEvent) => void +}) => { + return ( + + + + ); +}; interface VoidChatAreaProps { // Required diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 6da3d5b9..1fcb946a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -6,8 +6,10 @@ import React from "react"; import { useAccessor, useChatThreadsState } from '../util/services.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; -import { IconX } from './SidebarChat.js'; +import { IconX, IconTrash } from './SidebarChat.js'; +const X_ICON_SIZE = 16 +const TRASH_ICON_SIZE = 14 const truncate = (s: string) => { let len = s.length @@ -45,7 +47,7 @@ export const SidebarThreadSelector = () => { onClick={() => sidebarStateService.setState({ isHistoryOpen: false })} > @@ -110,6 +112,16 @@ export const SidebarThreadSelector = () => { >
{`${firstMsg}`}
{`\u00A0(${numMessages})`}
+ { + // Prevent the click event from bubbling up to the parent button + // to prevent the deleted thread from being switched to. + e.stopPropagation(); + chatThreadsService.deleteThreadById(pastThread.id); + }} + /> ); From 1ef22641b758b584b6f16ee65d9697183a85d862 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 1 Mar 2025 18:19:33 -0800 Subject: [PATCH 2/5] minor changes --- .../contrib/void/browser/chatThreadService.ts | 50 +++++++++---------- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../src/sidebar-tsx/SidebarThreadSelector.tsx | 22 ++++---- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3c1f1a2f..7699ca30 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -169,6 +169,7 @@ export interface IChatThreadService { setCurrentMessageState: (messageIdx: number, newState: Partial) => void getCurrentThreadStagingSelections: () => StagingSelectionItem[] setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void + getSortedThreadIdsByTime: () => string[] // call to edit a message @@ -284,36 +285,30 @@ class ChatThreadService extends Disposable implements IChatThreadService { public deleteThreadById(threadId: string): void { const { allThreads, currentThreadId } = this.state - const newThreads = { ...allThreads } - if (threadId in newThreads) { - delete newThreads[threadId] + + if (!(threadId in allThreads)) { + console.error('Void: Tried deleting thread with id that does not exist.') + return } - // If we're deleting the current thread, switch to another thread first - const needsThreadSwitch = threadId === currentThreadId - let newCurrentThreadId = currentThreadId - - if (needsThreadSwitch) { - // Find another thread to switch to - const remainingThreadIds = Object.keys(newThreads) - if (remainingThreadIds.length > 0) { - // Switch to the most recently modified thread - newCurrentThreadId = remainingThreadIds.sort((threadId1, threadId2) => - newThreads[threadId2].lastModified > newThreads[threadId1].lastModified ? 1 : -1 - )[0] - } else { - // If no threads left, create a new one - const newThread = newThreadObject() - newThreads[newThread.id] = newThread - newCurrentThreadId = newThread.id + // If we're on the thread we're about to delete, switch away from it + if (threadId === currentThreadId) { + const switchToThreadId = this.getSortedThreadIdsByTime().find(id => id !== threadId) + if (switchToThreadId !== undefined) { + this.switchToThread(switchToThreadId) + } + else { + this.openNewThread() } } - this._storeAllThreads(newThreads) - this._setState({ - allThreads: newThreads, - currentThreadId: newCurrentThreadId - }, true) + // Delete the thread ID + const newAllThreads = { ...allThreads } + delete newAllThreads[threadId] + + this._storeAllThreads(newAllThreads) + this._setState({ allThreads: newAllThreads, }, false) + } // this should be the only place this.state = ... appears besides constructor @@ -572,6 +567,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true) } + getSortedThreadIdsByTime() { + const allThreads = this.state.allThreads + return Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads[threadId1].lastModified > allThreads[threadId2].lastModified ? -1 : 1) + } + _addMessageToThread(threadId: string, message: ChatMessage) { const { allThreads } = this.state diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 9cf7cc1a..8e965caf 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -278,7 +278,7 @@ export const VoidChatArea: React.FC = ({ }; const useResizeObserver = () => { - const ref = useRef(null); + const ref = useRef(null); const [dimensions, setDimensions] = useState({ height: 0, width: 0 }); useEffect(() => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 1fcb946a..2593add7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -30,9 +30,7 @@ export const SidebarThreadSelector = () => { const { allThreads } = threadsState // sorted by most recent to least recent - const sortedThreadIds = Object.keys(allThreads ?? {}) - .sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1) - .filter(threadId => allThreads![threadId].messages.length !== 0) + const sortedThreadIds = chatThreadsService.getSortedThreadIdsByTime().filter(threadId => allThreads![threadId].messages.length !== 0) return (
@@ -113,15 +111,15 @@ export const SidebarThreadSelector = () => {
{`${firstMsg}`}
{`\u00A0(${numMessages})`}
{ - // Prevent the click event from bubbling up to the parent button - // to prevent the deleted thread from being switched to. - e.stopPropagation(); - chatThreadsService.deleteThreadById(pastThread.id); - }} - /> + size={TRASH_ICON_SIZE} + className='ml-auto' + onClick={(e) => { + // Prevent the click event from bubbling up to the parent button + // to prevent the deleted thread from being switched to. + e.stopPropagation(); + chatThreadsService.deleteThreadById(pastThread.id); + }} + /> ); From 9f25d652d51afb47666ccd85bf599405e9b87b1b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 1 Mar 2025 18:46:10 -0800 Subject: [PATCH 3/5] merge --- src/vs/workbench/contrib/void/common/chatThreadService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index 5190a449..e066b2ba 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from './llmMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from '../browser/prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js'; import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js'; import { toLLMChatMessage } from './llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -360,7 +360,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const instructions = userMessage const userMessageContent = await chat_userMessageContent(instructions, currSelns) const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) - const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) + const userMessageFullContent = chat_userMessageContentWithAllFilesToo(userMessageContent, selectionsStr) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) From cbae8d5ec24f3bb232076534a804f2c49594e9c5 Mon Sep 17 00:00:00 2001 From: Shi Bai Date: Tue, 4 Mar 2025 21:07:54 -0800 Subject: [PATCH 4/5] Remove one empty line between methods --- src/vs/workbench/contrib/void/common/chatThreadService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index e066b2ba..175cbd03 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -541,7 +541,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { return Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads[threadId1].lastModified > allThreads[threadId2].lastModified ? -1 : 1) } - _addMessageToThread(threadId: string, message: ChatMessage) { const { allThreads } = this.state From f13279e84ae8be58d13e815735eb3fdbbcdb136c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 30 Apr 2025 13:58:51 -0700 Subject: [PATCH 5/5] temp --- .../react/src/sidebar-tsx/SidebarChat.tsx | 30 --------------- .../src/sidebar-tsx/SidebarThreadSelector.tsx | 20 +++------- .../contrib/void/common/chatThreadService.ts | 37 +------------------ 3 files changed, 7 insertions(+), 80 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index ea1c2034..a77aba9a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -138,36 +138,6 @@ export const IconLoading = ({ className = '' }: { className?: string }) => { } -export const IconTrash = ({ - size, - className = '', - onClick -}: { - size: number, - className?: string, - onClick?: (event: React.MouseEvent) => void -}) => { - return ( - - - - ); -}; const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`; diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 2593add7..6da3d5b9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -6,10 +6,8 @@ import React from "react"; import { useAccessor, useChatThreadsState } from '../util/services.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; -import { IconX, IconTrash } from './SidebarChat.js'; +import { IconX } from './SidebarChat.js'; -const X_ICON_SIZE = 16 -const TRASH_ICON_SIZE = 14 const truncate = (s: string) => { let len = s.length @@ -30,7 +28,9 @@ export const SidebarThreadSelector = () => { const { allThreads } = threadsState // sorted by most recent to least recent - const sortedThreadIds = chatThreadsService.getSortedThreadIdsByTime().filter(threadId => allThreads![threadId].messages.length !== 0) + const sortedThreadIds = Object.keys(allThreads ?? {}) + .sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1) + .filter(threadId => allThreads![threadId].messages.length !== 0) return (
@@ -45,7 +45,7 @@ export const SidebarThreadSelector = () => { onClick={() => sidebarStateService.setState({ isHistoryOpen: false })} > @@ -110,16 +110,6 @@ export const SidebarThreadSelector = () => { >
{`${firstMsg}`}
{`\u00A0(${numMessages})`}
- { - // Prevent the click event from bubbling up to the parent button - // to prevent the deleted thread from being switched to. - e.stopPropagation(); - chatThreadsService.deleteThreadById(pastThread.id); - }} - /> ); diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index 175cbd03..adfd3f29 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from './llmMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from '../browser/prompt/prompts.js'; import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js'; import { toLLMChatMessage } from './llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -153,11 +153,9 @@ export interface IChatThreadService { onDidChangeCurrentThread: Event; onDidChangeStreamState: Event<{ threadId: string }> - getSortedThreadIdsByTime: () => string[] getCurrentThread(): ChatThreads[string]; openNewThread(): void; switchToThread(threadId: string): void; - deleteThreadById(threadId: string): void; // you can edit multiple messages // the one you're currently editing is "focused", and we add items to that one when you press cmd+L. @@ -247,33 +245,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { ); } - public deleteThreadById(threadId: string): void { - const { allThreads, currentThreadId } = this.state - - if (!(threadId in allThreads)) { - console.error('Void: Tried deleting thread with id that does not exist.') - return - } - - // If we're on the thread we're about to delete, switch away from it - if (threadId === currentThreadId) { - const switchToThreadId = this.getSortedThreadIdsByTime().find(id => id !== threadId) - if (switchToThreadId !== undefined) { - this.switchToThread(switchToThreadId) - } - else { - this.openNewThread() - } - } - - // Delete the thread ID - const newAllThreads = { ...allThreads } - delete newAllThreads[threadId] - - this._storeAllThreads(newAllThreads) - this._setState({ allThreads: newAllThreads, }, false) - - } // this should be the only place this.state = ... appears besides constructor private _setState(state: Partial, affectsCurrent: boolean) { @@ -360,7 +331,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const instructions = userMessage const userMessageContent = await chat_userMessageContent(instructions, currSelns) const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) - const userMessageFullContent = chat_userMessageContentWithAllFilesToo(userMessageContent, selectionsStr) + const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) @@ -536,10 +507,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true) } - getSortedThreadIdsByTime() { - const allThreads = this.state.allThreads - return Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads[threadId1].lastModified > allThreads[threadId2].lastModified ? -1 : 1) - } _addMessageToThread(threadId: string, message: ChatMessage) { const { allThreads } = this.state