From 4ebfa91a8312df3b4ec8ce8fffd5b6b6cc8db4ec Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 24 Feb 2025 00:46:03 -0800 Subject: [PATCH 01/19] fix URI staleness bug in local storage --- .../contrib/void/browser/chatThreadService.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index d92fb772..3a5bc403 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -227,12 +227,32 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + // !!! this is important for properly restoring URIs from storage + private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { + return JSON.parse(threadsStr, (key, value) => { + if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI + return URI.from(value); + } + return value; + }); + } private _readAllThreads(): ChatThreads { - const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) - const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {} + const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION); + if (!threadsStr) { + return {}; + } + return this._convertThreadDataFromStorage(threadsStr); + } - return threads + private _storeAllThreads(threads: ChatThreads) { + const serializedThreads = JSON.stringify(threads); + this._storageService.store( + THREAD_STORAGE_KEY, + serializedThreads, + StorageScope.APPLICATION, + StorageTarget.USER + ); } @@ -277,9 +297,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - private _storeAllThreads(threads: ChatThreads) { - this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER) - } // this should be the only place this.state = ... appears besides constructor private _setState(state: Partial, affectsCurrent: boolean) { @@ -652,4 +669,3 @@ class ChatThreadService extends Disposable implements IChatThreadService { } registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager); - From df6de2d7fc603622be6837eea071aead3e82388c Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 24 Feb 2025 03:51:59 -0800 Subject: [PATCH 02/19] styles for tool use --- .../react/src/sidebar-tsx/SidebarChat.tsx | 218 +++++++++++------- .../void/browser/react/src/util/inputs.tsx | 29 ++- .../void/browser/react/src/util/services.tsx | 2 + .../src/void-settings-tsx/ModelDropdown.tsx | 3 +- .../void/browser/react/tailwind.config.js | 2 +- .../contrib/void/common/toolsService.ts | 4 +- 6 files changed, 161 insertions(+), 97 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 96e22bd4..1fda7cfb 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 @@ -543,12 +543,13 @@ export const SelectedFiles = ( } -type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } +type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } interface ToolResultProps { actionTitle: string; actionParam: string; actionNumResults?: number; children?: React.ReactNode; + onClick?: () => void; } const ToolResult = ({ @@ -556,26 +557,31 @@ const ToolResult = ({ actionParam, actionNumResults, children, + onClick, }: ToolResultProps) => { const [isExpanded, setIsExpanded] = useState(false); const isDropdown = !!children + const isClickable = !!isDropdown || !!onClick return (
-
+
children && setIsExpanded(!isExpanded)} + className={`flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`} + onClick={() => { + if (children) { setIsExpanded(v => !v); } + if (onClick) { onClick(); } + }} > - {isDropdown && ( + {isDropdown && ( )} -
+
{actionTitle} - {`"`}{actionParam}{`"`} + {actionParam} {actionNumResults !== undefined && ( {`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`} @@ -584,7 +590,8 @@ const ToolResult = ({
{children}
@@ -595,90 +602,127 @@ const ToolResult = ({ -const toolResultToComponent: ToolReusltToComponent = { - 'read_file': ({ message }) => ( - - ), - 'list_dir': ({ message }) => ( - -
- {message.result.children?.map((item, i) => ( -
- {item.name} - {item.isDirectory && '/'} -
- ))} - {message.result.hasNextPage && ( -
- {message.result.itemsRemaining} more items... -
- )} -
-
- ), - 'pathname_search': ({ message }) => ( - -
- {Array.isArray(message.result.uris) ? - message.result.uris.map((uri, i) => ( -
- { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + { commandService.executeCommand('vscode.open', message.result.uri, { preview: true }) }} + /> + ) + }, + 'list_dir': ({ message }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const explorerService = accessor.get('IExplorerService') + // message.result.hasNextPage = true + // message.result.itemsRemaining = 400 + return ( + +
+ {message.result.children?.map((child, i) => ( +
{ + commandService.executeCommand('workbench.view.explorer'); + explorerService.select(child.uri, true); + }} + > + + {`${child.name}${child.isDirectory ? '/' : ''}`} +
+ ))} + {message.result.hasNextPage && ( +
+ {message.result.itemsRemaining} more items... +
+ )} +
+
+ ) + }, + 'pathname_search': ({ message }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + +
+ {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
{message.result.uris}
+ } + {message.result.hasNextPage && ( +
+ More results available...
- )) : -
{message.result.uris}
- } - {message.result.hasNextPage && ( -
- More results available... -
- )} -
- - ), - 'search': ({ message }) => ( - -
- {typeof message.result.uris === 'string' ? - message.result.uris : - message.result.uris.map((uri, i) => ( -
- + + ) + }, + 'search': ({ message }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + +
+ {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
{message.result.uris}
+ } + {message.result.hasNextPage && ( +
+ More results available...
- )) - } - {message.result.hasNextPage && ( -
- More results available... -
- )} -
- - ) + )} +
+ + ) + } }; diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index be327655..7f871160 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -310,6 +310,7 @@ export const VoidCustomDropdownBox = ({ selectedOption, onChangeOption, getOptionDropdownName, + getOptionDropdownDetail, getOptionDisplayName, getOptionsEqual, className, @@ -321,6 +322,7 @@ export const VoidCustomDropdownBox = ({ selectedOption: T | undefined; onChangeOption: (newValue: T) => void; getOptionDropdownName: (option: T) => string; + getOptionDropdownDetail?: (option: T) => string; getOptionDisplayName: (option: T) => string; getOptionsEqual: (a: T, b: T) => boolean; className?: string; @@ -420,12 +422,21 @@ export const VoidCustomDropdownBox = ({ className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col" aria-hidden="true" > - {options.map((option) => ( -
-
- {getOptionDropdownName(option)} -
- ))} + {options.map((option) => { + const optionName = getOptionDropdownName(option); + const optionDetail = getOptionDropdownDetail?.(option) || ''; + + return ( +
+
+ + {optionName} + {optionDetail} + ______ + +
+ ) + })}
{/* Select Button */} @@ -473,6 +484,7 @@ export const VoidCustomDropdownBox = ({ {options.map((option) => { const thisOptionIsSelected = getOptionsEqual(option, selectedOption); const optionName = getOptionDropdownName(option); + const optionDetail = getOptionDropdownDetail?.(option) || ''; return (
({ )}
- {optionName} + + {optionName} + {optionDetail} +
); })} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 5e164428..68de21ec 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -15,6 +15,7 @@ import { VoidQuickEditState } from '../../../quickEditStateService.js' import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js'; +import { IExplorerService } from '../../../../../../../workbench/contrib/files/browser/files.js' import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js'; import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js'; @@ -226,6 +227,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ILanguageFeaturesService: accessor.get(ILanguageFeaturesService), IKeybindingService: accessor.get(IKeybindingService), + IExplorerService: accessor.get(IExplorerService), IEnvironmentService: accessor.get(IEnvironmentService), IConfigurationService: accessor.get(IConfigurationService), IPathService: accessor.get(IPathService), diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 2931b671..7ff66b32 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat selectedOption={selectedOption} onChangeOption={onChangeOption} getOptionDisplayName={(option) => option.selection.modelName} - getOptionDropdownName={(option) => option.name} + getOptionDropdownName={(option) => option.selection.modelName} + getOptionDropdownDetail={(option) => option.selection.providerName } getOptionsEqual={(a, b) => optionsEqual([a], [b])} className='text-xs text-void-fg-3 px-1' matchInputWidth={false} diff --git a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js index bc57116b..d7c0f7e0 100644 --- a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js @@ -46,7 +46,7 @@ module.exports = { "void-border-1": "var(--vscode-commandCenter-activeBorder)", "void-border-2": "var(--vscode-commandCenter-border)", "void-border-3": "var(--vscode-commandCenter-inactiveBorder)", - "void-border-3": "var(--vscode-settings-sashBorder)", + "void-border-4": "var(--vscode-editorGroup-border)", vscode: { diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 09ce82a2..0b2b5a37 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -101,6 +101,7 @@ export type ToolCallReturnType = { } type DirectoryItem = { + uri: URI; name: string; isDirectory: boolean; isSymbolicLink: boolean; @@ -133,8 +134,9 @@ const computeDirectoryResult = async ( const children: DirectoryItem[] = listChildren.map(child => ({ name: child.name, + uri: child.resource, isDirectory: child.isDirectory, - isSymbolicLink: child.isSymbolicLink || false + isSymbolicLink: child.isSymbolicLink })); const hasNextPage = (originalChildrenLength - 1) > toChildIdx; From 1dffbfb06121a2b36251ed450ffeae47820ef814 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 24 Feb 2025 04:09:09 -0800 Subject: [PATCH 03/19] style --- src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 7f871160..27a0d596 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -514,7 +514,7 @@ export const VoidCustomDropdownBox = ({
{optionName} - {optionDetail} + {optionDetail}
); From 764b1a2ccfcf0ba3f7fdaac9bf70e34530206b53 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 24 Feb 2025 05:47:58 -0800 Subject: [PATCH 04/19] improve input box --- .../browser/react/src/markdown/BlockCode.tsx | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 154 +++++++----------- 3 files changed, 65 insertions(+), 95 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index 4c2cbea4..f7954b82 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -13,7 +13,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov return ( <> -
+
{buttonsOnHover === null ? null : (
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 8168bed3..d2f24569 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -51,11 +51,13 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: tokenIdx: tokenIdx, }) : null - return + } /> +
} if (t.type === "heading") { 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 1fda7cfb..3d6fbcb7 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 @@ -187,12 +187,14 @@ export const VoidChatArea: React.FC = ({ return (
{ @@ -414,7 +416,7 @@ export const SelectedFiles = ( } return ( -
+
{allSelections.map((selection, i) => { @@ -424,94 +426,65 @@ export const SelectedFiles = ( const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` - const selectionHTML = (
- {/* selection summary */} -
{ + if (isThisSelectionProspective) { // add prospective selection to selections + if (type !== 'staging') return; // (never) + setSelections([...selections, selection]) + } else if (isThisSelectionAFile) { // open files + commandService.executeCommand('vscode.open', selection.fileURI, { + preview: true, + // preserveFocus: false, + }); + } else { // show text + setSelectionIsOpened(s => { + const newS = [...s] + newS[i] = !newS[i] + return newS + }); + } + }} > -
{ - if (isThisSelectionProspective) { // add prospective selection to selections - if (type !== 'staging') return; // (never) - setSelections([...selections, selection]) + { // file name and range + getBasename(selection.fileURI.fsPath) + + (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`) + } - } else if (isThisSelectionAFile) { // open files - commandService.executeCommand('vscode.open', selection.fileURI, { - preview: true, - // preserveFocus: false, - }); - } else { // show text - setSelectionIsOpened(s => { - const newS = [...s] - newS[i] = !newS[i] - return newS - }); - } - }} - > - - {/* file name */} - {getBasename(selection.fileURI.fsPath)} - {/* selection range */} - {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} - - - {/* X button */} - {type === 'staging' && !isThisSelectionProspective && - { - e.stopPropagation(); // don't open/close selection - if (type !== 'staging') return; - setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) - setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) - }} - > - - } - - -
- - {/* clear all selections button */} - {/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 - ? null - :
-
setIsClearHovered(true)} - onMouseLeave={() => setIsClearHovered(false)} - > - { setSelections([]) }} - /> -
-
- } */} + {type === 'staging' && !isThisSelectionProspective ? // X button + { + e.stopPropagation(); // don't open/close selection + if (type !== 'staging') return; + setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) + setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) + }} + size={10} + /> + : <> + }
- {/* selection text */} - {isThisSelectionOpened && + + {/* code box */} + {isThisSelectionOpened ?
{ @@ -525,14 +498,9 @@ export const SelectedFiles = ( showScrollbars={true} />
+ : <> } -
) - - return - {/* divider between `selections` and `prospectiveSelections` */} - {/* {selections.length > 0 && i === selections.length &&
} */} - {selectionHTML} -
+
})} @@ -851,7 +819,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM > setIsDisabled(!text)} onFocus={() => { @@ -1101,7 +1069,7 @@ export const SidebarChat = () => { featureName="Ctrl+L" > Date: Mon, 24 Feb 2025 05:59:56 -0800 Subject: [PATCH 05/19] + --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 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 3d6fbcb7..8c6d54f8 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 @@ -763,7 +763,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM if (mode === 'display') { chatbubbleContents = <> - {chatMessage.displayContent} + {chatMessage.displayContent} } else if (mode === 'edit') { @@ -862,7 +862,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM className={` relative ${mode === 'edit' ? 'px-2 w-full max-w-full' - : role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre + : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } `} @@ -875,7 +875,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM text-left rounded-lg max-w-full ${mode === 'edit' ? '' - : role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto' + : role === 'user' ? 'p-2 flex flex-col gap-1 bg-void-bg-1 text-void-fg-1 overflow-x-auto' : role === 'assistant' ? 'px-2 overflow-x-auto' : '' } `} From 1b437993714720bd31b10f441d8848f278370a06 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 25 Feb 2025 18:48:17 -0800 Subject: [PATCH 06/19] prepare merg --- src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 27a0d596..4cd68e6d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -688,7 +688,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars verticalScrollbarSize: 0, horizontal: 'auto', horizontalScrollbarSize: 8, - ignoreHorizontalScrollbarInContentHeight: true, + // ignoreHorizontalScrollbarInContentHeight: true, }, }, From 0de1c2055103ceee188606bd44c86ef2854ba78f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 26 Feb 2025 01:59:52 -0800 Subject: [PATCH 07/19] progress --- .../contrib/void/browser/chatThreadService.ts | 34 +++++++++----- .../browser/helpers/extractCodeFromResult.ts | 32 ++++++++----- .../react/src/sidebar-tsx/SidebarChat.tsx | 47 +++++++++++++++++-- .../contrib/void/common/llmMessageTypes.ts | 2 +- .../void/electron-main/llmMessage/MODELS.ts | 37 ++++++++++----- .../llmMessage/sendLLMMessage.ts | 19 ++++---- 6 files changed, 120 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3a5bc403..cc64fc92 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -77,6 +77,7 @@ export type ChatMessage = role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored + reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking } | ToolMessage @@ -116,6 +117,7 @@ export type ThreadStreamState = { [threadId: string]: undefined | { error?: { message: string, fullError: Error | null, }; messageSoFar?: string; + reasoningSoFar?: string; streamingToken?: string; } } @@ -330,10 +332,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { + private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }, reasoning?: string) => { // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null }) - this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) + this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null, reasoning: reasoning || null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) } @@ -431,17 +433,17 @@ class ChatThreadService extends Disposable implements IChatThreadService { tools: tools, - onText: ({ fullText }) => { - this._setStreamState(threadId, { messageSoFar: fullText }) + onText: ({ fullText, fullReasoning }) => { + this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, - onFinalMessage: async ({ fullText, toolCalls }) => { + onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => { if ((toolCalls?.length ?? 0) === 0) { - this._finishStreamingTextMessage(threadId, fullText) + this._finishStreamingTextMessage(threadId, fullText, undefined, fullReasoning) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText }) - this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, reasoning: fullReasoning || null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message for (const tool of toolCalls ?? []) { const toolName = tool.name as ToolName @@ -475,7 +477,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { res_() }, onError: (error) => { - this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + this._finishStreamingTextMessage(threadId, messageSoFar, error, reasoningSoFar) res_() }, }) @@ -493,7 +497,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { cancelStreaming(threadId: string) { const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '') + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + this._finishStreamingTextMessage(threadId, messageSoFar, undefined, reasoningSoFar) } dismissStreamError(threadId: string): void { @@ -506,7 +512,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { getCurrentThread(): ChatThreads[string] { const state = this.state - return state.allThreads[state.currentThreadId] + const thread = state.allThreads[state.currentThreadId] + return thread } getFocusedMessageIdx() { @@ -644,7 +651,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } getCurrentThreadStagingSelections = () => { - return this.getCurrentThread().state.stagingSelections + const currentThread = this.getCurrentThread() + return currentThread.state.stagingSelections } setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => { diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 00eb2ef1..564a565d 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -251,10 +251,8 @@ export const extractSearchReplaceBlocks = (str: string) => { // could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true -export const extractReasoningFromText = ( - onText_: OnText, - thinkTags: [string, string], -): OnText => { +export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => { + let latestAddIdx = 0 // exclusive let foundTag1 = false @@ -263,7 +261,8 @@ export const extractReasoningFromText = ( let fullText = '' let fullReasoning = '' - const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => { + const newOnText: OnText = ({ newText: newText_, fullText: fullText_ }) => { + // abcdefghi // | // until found the first think tag, keep adding to fullText @@ -283,7 +282,7 @@ export const extractReasoningFromText = ( fullText += newText fullReasoning += newReasoning latestAddIdx += newText.length + newReasoning.length - onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + onText({ newText, fullText, newReasoning: newReasoning, fullReasoning }) return } @@ -291,7 +290,7 @@ export const extractReasoningFromText = ( const newText = fullText.substring(latestAddIdx, Infinity) fullText += newText latestAddIdx += newText.length - onText_({ newText, fullText, newReasoning: '', fullReasoning }) + onText({ newText, fullText, newReasoning: '', fullReasoning }) return } // at this point, we found @@ -313,7 +312,7 @@ export const extractReasoningFromText = ( fullText += newText fullReasoning += newReasoning latestAddIdx += newText.length + newReasoning.length - onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + onText({ newText, fullText, newReasoning: newReasoning, fullReasoning }) return } @@ -321,7 +320,7 @@ export const extractReasoningFromText = ( const newReasoning = fullText.substring(latestAddIdx, Infinity) fullReasoning += newReasoning latestAddIdx += newReasoning.length - onText_({ newText: '', fullText, newReasoning, fullReasoning }) + onText({ newText: '', fullText, newReasoning, fullReasoning }) return } // at this point, we found @@ -329,8 +328,19 @@ export const extractReasoningFromText = ( fullText += newText_ const newText = fullText.substring(latestAddIdx, Infinity) latestAddIdx += newText.length - onText_({ newText, fullText, newReasoning: '', fullReasoning }) + onText({ newText, fullText, newReasoning: '', fullReasoning }) } - return onText + + return newOnText +} + + +export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => { + const tag1Idx = fullText_.lastIndexOf(thinkTags[0]) + const tag2Idx = fullText_.lastIndexOf(thinkTags[1]) + if (tag1Idx === -1 || tag2Idx === -1) return { fullText: fullText_, fullReasoning: '' } + const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity) + const fullReasoning = fullText.substring(tag1Idx + thinkTags[0].length, tag2Idx) + return { fullText, fullReasoning } } 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 d97647b0..05ec8419 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 @@ -699,6 +699,7 @@ type ChatBubbleMode = 'display' | 'edit' const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => { const role = chatMessage.role + const [isReasoningOpen, setIsReasoningOpen] = useState(false) const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -733,7 +734,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { setStagingSelections(chatMessage.selections || []) - if (textAreaFnsRef.current) textAreaFnsRef.current.setValue(chatMessage.displayContent || '') @@ -839,13 +839,46 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM } else if (role === 'assistant') { const thread = chatThreadsService.getCurrentThread() + const hasReasoning = !!chatMessage.reasoning const chatMessageLocation: ChatMessageLocation = { threadId: thread.id, messageIdx: messageIdx!, } - chatbubbleContents = + chatbubbleContents = ( + <> + {/* Always show the content */} + + + {/* Show reasoning in a dropdown if it exists */} + {hasReasoning && ( +
+
+
setIsReasoningOpen(!isReasoningOpen)} + > + +
+ Reasoning + Model's step-by-step thinking +
+
+
+
+ +
+
+
+
+ )} + + ) } else if (role === 'tool') { @@ -939,7 +972,7 @@ export const SidebarChat = () => { const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const selections = chatThreadsService.getCurrentThread().state.stagingSelections + const selections = currentThread.state.stagingSelections const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) } // stream state @@ -947,6 +980,7 @@ export const SidebarChat = () => { const isStreaming = !!currThreadStreamState?.streamingToken const latestError = currThreadStreamState?.error const messageSoFar = currThreadStreamState?.messageSoFar + const reasoningSoFar = currThreadStreamState?.reasoningSoFar // ----- SIDEBAR CHAT state (local) ----- @@ -1027,7 +1061,12 @@ export const SidebarChat = () => { {prevMessagesHTML} {/* message stream */} - + {messageSoFar && } {/* error message */} diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index abe88970..93ef12b3 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -46,7 +46,7 @@ export type ToolCallType = { export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void -export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id +export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index a4ad5487..c19aca3d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -12,7 +12,7 @@ import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, L import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; -import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromResult.js'; +import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; @@ -32,7 +32,7 @@ type ModelOptions = { supportsReasoningOutput: false | { // you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing) // if it's open source, put the think tags here so we parse them out in e.g. ollama - openSourceThinkTags?: [string, string] + readonly openSourceThinkTags?: [string, string] }; } @@ -641,9 +641,9 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) - onFinalMessage({ fullText: `Model ${modelName} does not support FIM.` }) + onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) else - onFinalMessage({ fullText: `Model ${modelName_} (${modelName}) does not support FIM.` }) + onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) return } @@ -691,11 +691,15 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj } const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} - if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags) - onText = extractReasoningFromText(onText, supportsReasoningOutput.openSourceThinkTags) - let fullReasoning = '' - let fullText = '' + const manuallyParseReasoning = needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags + if (manuallyParseReasoning) { + onText = extractReasoningOnTextWrapper(onText, supportsReasoningOutput.openSourceThinkTags) + } + + + let fullReasoningSoFar = '' + let fullTextSoFar = '' const toolCallOfIndex: ToolCallOfIndex = {} openai.chat.completions .create(options) @@ -713,19 +717,26 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage } // message const newText = chunk.choices[0]?.delta?.content ?? '' - fullText += newText + fullTextSoFar += newText // reasoning let newReasoning = '' if (nameOfReasoningFieldInDelta) { // @ts-ignore newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + '' - fullReasoning += newReasoning + fullReasoningSoFar += newReasoning } - onText({ newText, fullText, newReasoning, fullReasoning }) + onText({ newText, fullText: fullTextSoFar, newReasoning, fullReasoning: fullReasoningSoFar }) + } + // on final + const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex) + if (manuallyParseReasoning) { + const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, supportsReasoningOutput.openSourceThinkTags) + onFinalMessage({ fullText, fullReasoning, toolCalls }); + } else { + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls }); } - onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) }); }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(error => { @@ -787,7 +798,7 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { // supportsReasoning: modelSupportsReasoning, modelName, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 90deffe2..11ae2b76 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -63,22 +63,23 @@ export const sendLLMMessage = ({ _fullTextSoFar = fullText } - const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => { + const onFinalMessage: OnFinalMessage = (params) => { + const { fullText, fullReasoning } = params if (_didAbort) return - captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_({ fullText, toolCalls }) + captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) + onFinalMessage_(params) } - const onError: OnError = ({ message: error, fullError }) => { + const onError: OnError = ({ message: errorMessage, fullError }) => { if (_didAbort) return - console.error('sendLLMMessage onError:', error) + console.error('sendLLMMessage onError:', errorMessage) // handle failed to fetch errors, which give 0 information by design - if (error === 'TypeError: fetch failed') - error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.` + if (errorMessage === 'TypeError: fetch failed') + errorMessage = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.` - captureLLMEvent(`${loggingName} - Error`, { error }) - onError_({ message: error, fullError }) + captureLLMEvent(`${loggingName} - Error`, { error: errorMessage }) + onError_({ message: errorMessage, fullError }) } const onAbort = () => { From 0de63a088d7922c82e5204d30c94b566309de1e5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 26 Feb 2025 01:59:59 -0800 Subject: [PATCH 08/19] reasoning UI! --- .../contrib/void/browser/chatThreadService.ts | 19 ++-- .../contrib/void/browser/editCodeService.ts | 9 +- .../browser/helpers/extractCodeFromResult.ts | 99 +++++++++-------- .../react/src/markdown/ChatMarkdownRender.tsx | 14 +-- .../react/src/sidebar-tsx/SidebarChat.tsx | 103 ++++++++++-------- .../void/browser/searchReplaceCacheService.ts | 26 ++--- .../contrib/void/common/llmMessageTypes.ts | 4 +- .../void/electron-main/llmMessage/MODELS.ts | 30 ++--- 8 files changed, 163 insertions(+), 141 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cc64fc92..98f98f8e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -60,11 +60,7 @@ export type ToolMessage = { // 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 = - | { - role: 'system'; - content: string; - displayContent?: undefined; - } | { + { role: 'user'; content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored @@ -76,7 +72,6 @@ export type ChatMessage = } | { role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) - displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking } | ToolMessage @@ -332,9 +327,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }, reasoning?: string) => { + private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => { // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null, reasoning: reasoning || null }) + this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) } @@ -439,10 +434,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => { if ((toolCalls?.length ?? 0) === 0) { - this._finishStreamingTextMessage(threadId, fullText, undefined, fullReasoning) + this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning }) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText, reasoning: fullReasoning || null }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message for (const tool of toolCalls ?? []) { const toolName = tool.name as ToolName @@ -479,7 +474,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, messageSoFar, error, reasoningSoFar) + this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error) res_() }, }) @@ -499,7 +494,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, messageSoFar, undefined, reasoningSoFar) + this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 0f92d4ef..5ef2e555 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1400,7 +1400,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } // state used in onText: - let fullText = '' + let fullTextSoFar = '' // so far (INCLUDING ignored suffix) let prevIgnoredSuffix = '' streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ @@ -1408,12 +1408,13 @@ class EditCodeService extends Disposable implements IEditCodeService { useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', logging: { loggingName: `startApplying - ${from}` }, messages, - onText: ({ newText: newText_ }) => { + onText: ({ fullText: fullText_ }) => { + const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText // full text, including ```, etc + fullTextSoFar += newText // full text, including ```, etc - const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) + const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullTextSoFar, newText.length) const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 564a565d..21f6e6f5 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -173,7 +173,7 @@ export type ExtractedSearchReplaceBlock = { const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { // for each prefix - for (let i = anyPrefix.length; i >= 0; i--) { + for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string const prefix = anyPrefix.slice(0, i) if (str.endsWith(prefix)) return prefix } @@ -252,93 +252,104 @@ export const extractSearchReplaceBlocks = (str: string) => { // could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => { - - - let latestAddIdx = 0 // exclusive + let latestAddIdx = 0 // exclusive index in fullText_ let foundTag1 = false let foundTag2 = false - let fullText = '' - let fullReasoning = '' + let fullTextSoFar = '' + let fullReasoningSoFar = '' - const newOnText: OnText = ({ newText: newText_, fullText: fullText_ }) => { + let onText_ = onText + onText = (params) => { + onText_(params) + } - // abcdefghi - // | + const newOnText: OnText = ({ fullText: fullText_ }) => { // until found the first think tag, keep adding to fullText if (!foundTag1) { const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) if (endsWithTag1) { + console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ }) // wait until we get the full tag or know more return } // if found the first tag - const tag1Index = fullText_.lastIndexOf(thinkTags[0]) + const tag1Index = fullText_.indexOf(thinkTags[0]) if (tag1Index !== -1) { + console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ }) foundTag1 = true - const newText = fullText.substring(latestAddIdx, tag1Index) - const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity) - - fullText += newText - fullReasoning += newReasoning - latestAddIdx += newText.length + newReasoning.length - onText({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + // Add text before the tag to fullTextSoFar + fullTextSoFar += fullText_.substring(0, tag1Index) + // Update latestAddIdx to after the first tag + latestAddIdx = tag1Index + thinkTags[0].length + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } + console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar }) // add the text to fullText - const newText = fullText.substring(latestAddIdx, Infinity) - fullText += newText - latestAddIdx += newText.length - onText({ newText, fullText, newReasoning: '', fullReasoning }) + fullTextSoFar = fullText_ + latestAddIdx = fullText_.length + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } + // at this point, we found // until found the second think tag, keep adding to fullReasoning if (!foundTag2) { const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1]) if (endsWithTag2) { + console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar }) // wait until we get the full tag or know more return } - // if found the second tag - const tag2Index = fullText_.lastIndexOf(thinkTags[1]) - if (tag2Index !== -1) { - foundTag2 = true - const newReasoning = fullText.substring(latestAddIdx, tag2Index) - const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity) - fullText += newText - fullReasoning += newReasoning - latestAddIdx += newText.length + newReasoning.length - onText({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + // if found the second tag + const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx) + if (tag2Index !== -1) { + console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar }) + foundTag2 = true + // Add everything between first and second tag to reasoning + fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index) + // Update latestAddIdx to after the second tag + latestAddIdx = tag2Index + thinkTags[1].length + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } - // add the text to fullReasoning - const newReasoning = fullText.substring(latestAddIdx, Infinity) - fullReasoning += newReasoning - latestAddIdx += newReasoning.length - onText({ newText: '', fullText, newReasoning, fullReasoning }) + // add the text to fullReasoning (content after first tag but before second tag) + console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar }) + + // If we have more text than we've processed, add it to reasoning + if (fullText_.length > latestAddIdx) { + fullReasoningSoFar += fullText_.substring(latestAddIdx) + latestAddIdx = fullText_.length + } + + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } - // at this point, we found - fullText += newText_ - const newText = fullText.substring(latestAddIdx, Infinity) - latestAddIdx += newText.length - onText({ newText, fullText, newReasoning: '', fullReasoning }) + // at this point, we found - content after the second tag is normal text + console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar }) + + // Add any new text after the closing tag to fullTextSoFar + if (fullText_.length > latestAddIdx) { + fullTextSoFar += fullText_.substring(latestAddIdx) + latestAddIdx = fullText_.length + } + + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } - return newOnText } export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => { - const tag1Idx = fullText_.lastIndexOf(thinkTags[0]) - const tag2Idx = fullText_.lastIndexOf(thinkTags[1]) + const tag1Idx = fullText_.indexOf(thinkTags[0]) + const tag2Idx = fullText_.indexOf(thinkTags[1]) if (tag1Idx === -1 || tag2Idx === -1) return { fullText: fullText_, fullReasoning: '' } const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity) const fullReasoning = fullText.substring(tag1Idx + thinkTags[0].length, tag2Idx) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index d2f24569..1aa55e61 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -33,7 +33,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { +const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) @@ -45,9 +45,9 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: if (t.type === "code") { - const applyBoxId = chatMessageLocation ? getApplyBoxId({ - threadId: chatMessageLocation.threadId, - messageIdx: chatMessageLocation.messageIdx, + const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({ + threadId: chatMessageLocationForApply.threadId, + messageIdx: chatMessageLocationForApply.messageIdx, tokenIdx: tokenIdx, }) : null @@ -131,7 +131,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: )} - + ))} @@ -243,12 +243,12 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: ) } -export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => { +export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> {tokens.map((token, index) => ( - + ))} ) 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 05ec8419..dd35e8e7 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 @@ -139,6 +139,9 @@ export const IconLoading = ({ className = '' }: { className?: string }) => { } +const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`; + + interface VoidChatAreaProps { // Required children: React.ReactNode; // This will be the input component @@ -696,9 +699,12 @@ const toolResultToComponent: ToolResultToComponent = { type ChatBubbleMode = 'display' | 'edit' -const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => { +const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, }) => { const role = chatMessage.role + // Only show reasoning dropdown when there's actual content + const hasReasoning = chatMessage.role === 'assistant' && chatMessage.reasoning + const [isReasoningOpen, setIsReasoningOpen] = useState(false) const accessor = useAccessor() @@ -839,46 +845,45 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM } else if (role === 'assistant') { const thread = chatThreadsService.getCurrentThread() - const hasReasoning = !!chatMessage.reasoning const chatMessageLocation: ChatMessageLocation = { threadId: thread.id, - messageIdx: messageIdx!, + messageIdx: messageIdx, } - chatbubbleContents = ( - <> - {/* Always show the content */} - - {/* Show reasoning in a dropdown if it exists */} - {hasReasoning && ( -
-
-
setIsReasoningOpen(!isReasoningOpen)} - > - -
- Reasoning - Model's step-by-step thinking -
-
-
-
- -
-
+ const reasoningDropdown = hasReasoning ? ( +
+
+
setIsReasoningOpen(!isReasoningOpen)} + > + +
+ Reasoning + Model's step-by-step thinking
- )} - - ) +
+
+ +
+
+
+
+ ) : null + + chatbubbleContents = (<> + {/* Reasoning dropdown (conditional) */} + {reasoningDropdown} + {/* Main content */} + + ) } else if (role === 'tool') { @@ -1029,13 +1034,27 @@ export const SidebarChat = () => { }, [isHistoryOpen, currentThread.id]) - const prevMessagesHTML = useMemo(() => { + const pastMessagesHTML = useMemo(() => { return previousMessages.map((message, i) => - + ) }, [previousMessages]) + const streamingChatIdx = pastMessagesHTML.length + const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar) ? + : null + + const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML] + + const threadSelector =
@@ -1053,20 +1072,12 @@ export const SidebarChat = () => { overflow-x-hidden overflow-y-auto py-4 - ${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} + ${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} `} style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights > {/* previous messages */} - {prevMessagesHTML} - - {/* message stream */} - {messageSoFar && } + {allMessagesHTML} {/* error message */} @@ -1101,7 +1112,7 @@ export const SidebarChat = () => { isStreaming={isStreaming} isDisabled={isDisabled} showSelections={true} - showProspectiveSelections={prevMessagesHTML.length === 0} + showProspectiveSelections={pastMessagesHTML.length === 0} selections={selections} setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts index e7a9448e..e50a6d12 100644 --- a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts +++ b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts @@ -7,8 +7,8 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILLMMessageService } from '../common/llmMessageService.js'; -import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js'; +// import { ILLMMessageService } from '../common/llmMessageService.js'; +// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js'; @@ -24,22 +24,22 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService { readonly onDidChangeState: Event = this._onDidChangeState.event; constructor( - @ILLMMessageService private readonly llmMessageService: ILLMMessageService, + // @ILLMMessageService private readonly llmMessageService: ILLMMessageService, ) { super() } - send(params: Omit & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) { - this.llmMessageService.sendLLMMessage({ - ...params as ServiceSendLLMMessageParams, - onText: (p) => { - const { retry } = params.onText(p) - if (retry) { + // send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) { + // this.llmMessageService.sendLLMMessage({ + // ...params as ServiceSendLLMMessageParams, + // onText: (p) => { + // const { retry } = params.onText(p) + // if (retry) { - } - } - }) - } + // } + // } + // }) + // } } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 93ef12b3..e8800562 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -45,14 +45,14 @@ export type ToolCallType = { } -export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void +export type OnText = (p: { fullText: string; fullReasoning: string }) => void export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { - if (c.role === 'system' || c.role === 'user') { + if (c.role === 'user') { return { role: c.role, content: c.content || '(empty message)' } } else if (c.role === 'assistant') diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index c19aca3d..f9fd0cf5 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -3,16 +3,16 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import OpenAI, { ClientOptions } from 'openai'; import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; +import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; +import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; +import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js'; import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; -import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; @@ -677,7 +677,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage supportsReasoningOutput, supportsSystemMessage, supportsTools, - maxOutputTokens, + // maxOutputTokens, right now we are ignoring this } = getModelCapabilities(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -686,9 +686,8 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {} const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} - const maxTokensObj = maxOutputTokens ? { max_tokens: maxOutputTokens } : {} const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj } + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, } const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} @@ -727,15 +726,20 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage fullReasoningSoFar += newReasoning } - onText({ newText, fullText: fullTextSoFar, newReasoning, fullReasoning: fullReasoningSoFar }) + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } // on final const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex) - if (manuallyParseReasoning) { - const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, supportsReasoningOutput.openSourceThinkTags) - onFinalMessage({ fullText, fullReasoning, toolCalls }); - } else { - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls }); + if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) { + onError({ message: 'Void: Response from model was empty.', fullError: null }) + } + else { + if (manuallyParseReasoning) { + const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, supportsReasoningOutput.openSourceThinkTags) + onFinalMessage({ fullText, fullReasoning, toolCalls }); + } else { + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls }); + } } }) // when error/fail - this catches errors of both .create() and .then(for await) @@ -823,7 +827,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM }) // when receive text stream.on('text', (newText, fullText) => { - onText({ newText, fullText, newReasoning: '', fullReasoning: '' }) + onText({ fullText, fullReasoning: '' }) }) // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (response) => { From c1f3b92c51158b0830aa68cb2573841785f8b117 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 26 Feb 2025 02:14:00 -0800 Subject: [PATCH 09/19] fix manual reasoning parsing --- .../browser/helpers/extractCodeFromResult.ts | 20 ++++++++++--------- .../react/src/sidebar-tsx/SidebarChat.tsx | 7 ++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 21f6e6f5..e24bc232 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -269,14 +269,14 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string if (!foundTag1) { const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) if (endsWithTag1) { - console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ }) + // console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ }) // wait until we get the full tag or know more return } // if found the first tag const tag1Index = fullText_.indexOf(thinkTags[0]) if (tag1Index !== -1) { - console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ }) + // console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ }) foundTag1 = true // Add text before the tag to fullTextSoFar fullTextSoFar += fullText_.substring(0, tag1Index) @@ -286,7 +286,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string return } - console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar }) + // console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar }) // add the text to fullText fullTextSoFar = fullText_ latestAddIdx = fullText_.length @@ -300,7 +300,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string if (!foundTag2) { const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1]) if (endsWithTag2) { - console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar }) + // console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar }) // wait until we get the full tag or know more return } @@ -308,7 +308,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string // if found the second tag const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx) if (tag2Index !== -1) { - console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar }) + // console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar }) foundTag2 = true // Add everything between first and second tag to reasoning fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index) @@ -319,7 +319,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string } // add the text to fullReasoning (content after first tag but before second tag) - console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar }) + // console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar }) // If we have more text than we've processed, add it to reasoning if (fullText_.length > latestAddIdx) { @@ -332,7 +332,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string } // at this point, we found - content after the second tag is normal text - console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar }) + // console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar }) // Add any new text after the closing tag to fullTextSoFar if (fullText_.length > latestAddIdx) { @@ -350,8 +350,10 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => { const tag1Idx = fullText_.indexOf(thinkTags[0]) const tag2Idx = fullText_.indexOf(thinkTags[1]) - if (tag1Idx === -1 || tag2Idx === -1) return { fullText: fullText_, fullReasoning: '' } + if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning + if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning + + const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx) const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity) - const fullReasoning = fullText.substring(tag1Idx + thinkTags[0].length, tag2Idx) return { fullText, fullReasoning } } 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 dd35e8e7..7a2cd0ba 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 @@ -703,7 +703,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const role = chatMessage.role // Only show reasoning dropdown when there's actual content - const hasReasoning = chatMessage.role === 'assistant' && chatMessage.reasoning + const reasoningStr = (chatMessage.role === 'assistant' && chatMessage.reasoning?.trim()) || null + const hasReasoning = !!reasoningStr const [isReasoningOpen, setIsReasoningOpen] = useState(false) @@ -871,7 +872,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isReasoningOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`} >
- +
@@ -1042,7 +1043,7 @@ export const SidebarChat = () => { const streamingChatIdx = pastMessagesHTML.length - const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar) ? + const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? Date: Wed, 26 Feb 2025 02:22:58 -0800 Subject: [PATCH 10/19] marketplace URL --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index a79966a3..45265c48 100644 --- a/product.json +++ b/product.json @@ -31,8 +31,8 @@ "nodejsRepository": "https://nodejs.org", "urlProtocol": "void-editor", "extensionsGallery": { - "serviceUrl": "https://open-vsx.org/vscode/gallery", - "itemUrl": "https://open-vsx.org/vscode/item" + "serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery", + "itemUrl": "https://marketplace.visualstudio.com/items" }, "builtInExtensions": [] } From 6fa3901ab080978ffeae6fe4337fee3febb4443e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 27 Feb 2025 16:22:11 -0800 Subject: [PATCH 11/19] small style --- src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 4cd68e6d..27a0d596 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -688,7 +688,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars verticalScrollbarSize: 0, horizontal: 'auto', horizontalScrollbarSize: 8, - // ignoreHorizontalScrollbarInContentHeight: true, + ignoreHorizontalScrollbarInContentHeight: true, }, }, From de17b3add19465ec4ad3e7d9e5843c1bcea9ab38 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 27 Feb 2025 16:27:34 -0800 Subject: [PATCH 12/19] styles --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 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 7a2cd0ba..0d5cee5b 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 @@ -419,7 +419,7 @@ export const SelectedFiles = ( } return ( -
+
{allSelections.map((selection, i) => { @@ -432,7 +432,7 @@ export const SelectedFiles = ( return
From 661eba3ae9e399845ca2e2f45d95eb5cdd2c5137 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 27 Feb 2025 17:08:49 -0800 Subject: [PATCH 13/19] fix selection state bug --- .../contrib/void/browser/chatThreadService.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 98f98f8e..3a9f4156 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -201,6 +201,23 @@ class ChatThreadService extends Disposable implements IChatThreadService { ) { super() + + setInterval(() => { + const thread = this.getCurrentThread() + if (!thread) return + + // print out all staging selections for all messages + for (const message of thread.messages) { + if (message.role === 'user' && message.state.stagingSelections.length > 0) { + console.log('Message staging selections:', message.state.stagingSelections) + } + } + // also print thread-level staging selections + if (thread.state.stagingSelections.length > 0) { + console.log('Thread staging selections:', thread.state.stagingSelections) + } + }, 1000) + const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION) @@ -345,8 +362,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // get prev and curr selections before clearing the message - const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) - const currSelns = thread.messages[messageIdx].selections || [] + const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) // selections for previous messages + 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) From aa835d468b0043f13b24f924893b3962ddea14c4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 27 Feb 2025 20:13:50 -0800 Subject: [PATCH 14/19] misc --- .../contrib/void/browser/aiRegexService.ts | 221 ++++++------------ .../contrib/void/browser/prompt/prompts.ts | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../void/browser/react/src/util/services.tsx | 3 +- .../contrib/void/browser/sidebarActions.ts | 2 +- .../contrib/void/browser/void.contribution.ts | 6 +- .../{browser => common}/chatThreadService.ts | 95 +------- .../contrib/void/common/llmMessageTypes.ts | 2 +- .../void/common/voidSettingsService.ts | 77 +++--- 10 files changed, 139 insertions(+), 281 deletions(-) rename src/vs/workbench/contrib/void/{browser => common}/chatThreadService.ts (88%) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index f38236a9..4b2a919a 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -3,185 +3,106 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -// import { URI } from '../../../../base/common/uri.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -// import { IToolService, ToolService } from '../common/toolsService.js'; +// 1. search(ai) +// - tool use to find all possible changes +// - if search only: is this file related to the search? +// - if search + replace: should I modify this file? +// 2. replace(ai) +// - what changes to make? +// 3. postprocess errors +// -fastapply changes simultaneously +// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error) +// private async _searchUsingAI({ searchClause }: { searchClause: string }) { -export type ChatMessageLocation = { - threadId: string; - messageIdx: number; -} +// // const relevantURIs: URI[] = [] +// // const gatherPrompt = `\ +// // asdasdas +// // ` +// // const filterPrompt = `\ +// // Is this file relevant? +// // ` -export type SearchAndReplaceBlock = { - search: string; - replace: string; -} +// // // optimizations (DO THESE LATER!!!!!!) +// // // if tool includes a uri in uriSet, skip it obviously +// // let uriSet = new Set() +// // // gather +// // let messages = [] +// // while (true) { +// // const result = await new Promise((res, rej) => { +// // sendLLMMessage({ +// // messages, +// // tools: ['search'], +// // onFinalMessage: ({ result: r, }) => { +// // res(r) +// // }, +// // onError: (error) => { +// // rej(error) +// // } +// // }) +// // }) -// service that manages state -export type ApplyState = { - [applyBoxId: string]: { - searchAndReplaceBlocks: SearchAndReplaceBlock; - } -} +// // messages.push({ role: 'tool', content: turnToString(result) }) -// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion` +// // sendLLMMessage({ +// // messages: { 'Output ': result }, +// // onFinalMessage: (r) => { +// // // output is file1\nfile2\nfile3\n... +// // } +// // }) -export interface IFastApplyService { - readonly _serviceBrand: undefined; +// // uriSet.add(...) +// // } - // readonly state: ApplyState; // readonly to the user - // setState(newState: Partial): void; - // onDidChangeState: Event; -} +// // // writes +// // if (!replaceClause) return -export const IVoidFastApplyService = createDecorator('voidFastApplyService'); -class VoidFastApplyService extends Disposable implements IFastApplyService { - _serviceBrand: undefined; - - // static readonly ID = 'voidFastApplyService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - - // state - // state: ApplyState - - constructor( - // @IToolService private readonly toolService: ToolService - ) { - super() - - // initial state - // this.state = { currentUri: undefined } - } - - setState(newState: Partial) { - - // this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - aiSearch(searchStr: string) { - - } - - aiReplace(searchStr: string, replaceStr: string) { - - } - - - // 1. search(ai) - // - tool use to find all possible changes - // - if search only: is this file related to the search? - // - if search + replace: should I modify this file? - // 2. replace(ai) - // - what changes to make? - // 3. postprocess errors - // -fastapply changes simultaneously - // -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error) - - - // private async _searchUsingAI({ searchClause }: { searchClause: string }) { - - // // const relevantURIs: URI[] = [] - // // const gatherPrompt = `\ - // // asdasdas - // // ` - // // const filterPrompt = `\ - // // Is this file relevant? - // // ` - - - // // // optimizations (DO THESE LATER!!!!!!) - // // // if tool includes a uri in uriSet, skip it obviously - // // let uriSet = new Set() - // // // gather - // // let messages = [] - // // while (true) { - // // const result = await new Promise((res, rej) => { - // // sendLLMMessage({ - // // messages, - // // tools: ['search'], - // // onFinalMessage: ({ result: r, }) => { - // // res(r) - // // }, - // // onError: (error) => { - // // rej(error) - // // } - // // }) - // // }) - - // // messages.push({ role: 'tool', content: turnToString(result) }) - - // // sendLLMMessage({ - // // messages: { 'Output ': result }, - // // onFinalMessage: (r) => { - // // // output is file1\nfile2\nfile3\n... - // // } - // // }) - - // // uriSet.add(...) - // // } - - // // // writes - // // if (!replaceClause) return - - // // for (const uri of uriSet) { - // // // in future, batch these - // // applyWorkflow({ uri, applyStr: replaceClause }) - // // } +// // for (const uri of uriSet) { +// // // in future, batch these +// // applyWorkflow({ uri, applyStr: replaceClause }) +// // } - // // while (true) { - // // const result = new Promise((res, rej) => { - // // sendLLMMessage({ - // // messages, - // // tools: ['search'], - // // onResult: (r) => { - // // res(r) - // // } - // // }) - // // }) +// // while (true) { +// // const result = new Promise((res, rej) => { +// // sendLLMMessage({ +// // messages, +// // tools: ['search'], +// // onResult: (r) => { +// // res(r) +// // } +// // }) +// // }) - // // messages.push(result) +// // messages.push(result) - // // } +// // } - // } +// } - // private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) { +// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) { - // for (const uri of relevantURIs) { +// for (const uri of relevantURIs) { - // uri +// uri - // } +// } - // // should I change this file? - // // if so what changes to make? +// // should I change this file? +// // if so what changes to make? - // // fast apply the changes - // } +// // fast apply the changes +// } - - -} - -registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 90e01d50..22d0ad52 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -6,7 +6,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; -import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; +import { CodeSelection, StagingSelectionItem, FileSelection } from '../../common/chatThreadService.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { os } from '../helpers/systemInfo.js'; import { IVoidFileService } from '../../common/voidFileService.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 1aa55e61..c8571403 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -6,10 +6,14 @@ import React, { JSX } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' +export type ChatMessageLocation = { + threadId: string; + messageIdx: number; +} + type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } 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 0d5cee5b..5c413b30 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 @@ -7,10 +7,10 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; -import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; +import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; @@ -24,7 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChevronRight, Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatMessageLocation } from '../../../aiRegexService.js'; + import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 68de21ec..9898ef52 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { useState, useEffect, useCallback } from 'react' -import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js' +import { ThreadStreamState,IChatThreadService, ThreadsState } from '../../../../common/chatThreadService.js' import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' import { VoidSidebarState } from '../../../sidebarStateService.js' @@ -29,7 +29,6 @@ import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'; import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; -import { IChatThreadService } from '../../../chatThreadService.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js' import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js' import { ICommandService } from '../../../../../../../platform/commands/common/commands.js' diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 722eaa57..499c9887 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js'; +import { StagingSelectionItem, IChatThreadService } from '../common/chatThreadService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index b179e603..f9f6a29b 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -15,8 +15,6 @@ import './sidebarStateService.js' // register quick edit (Ctrl+K) import './quickEditActions.js' -// register Thread History -import './chatThreadService.js' // register Autocomplete import './autocompleteService.js' @@ -56,3 +54,7 @@ import '../common/voidUpdateService.js' // tools import '../common/toolsService.js' + +// register Thread History +import '../common/chatThreadService.js' + diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts similarity index 88% rename from src/vs/workbench/contrib/void/browser/chatThreadService.ts rename to src/vs/workbench/contrib/void/common/chatThreadService.ts index 3a9f4156..57d9dfe1 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo 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 '../common/llmMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; -import { toLLMChatMessage } from '../common/llmMessageTypes.js'; +import { ILLMMessageService } from './llmMessageService.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'; -import { IVoidFileService } from '../common/voidFileService.js'; +import { IVoidFileService } from './voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -101,7 +101,6 @@ export type ChatThreads = { type ThreadType = ChatThreads[string] -const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} } export type ThreadsState = { allThreads: ChatThreads; @@ -134,10 +133,7 @@ const newThreadObject = () => { } satisfies ChatThreads[string] } -const THREAD_VERSION_KEY = 'void.chatThreadVersion' -const LATEST_THREAD_VERSION = 'v2' - -const THREAD_STORAGE_KEY = 'void.chatThreadStorage' +export const THREAD_STORAGE_KEY = 'void.chatThreadStorage' type ChatMode = 'agent' | 'chat' @@ -200,35 +196,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { super() + this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state + const readThreads = this._readAllThreads() || {} - setInterval(() => { - const thread = this.getCurrentThread() - if (!thread) return - - // print out all staging selections for all messages - for (const message of thread.messages) { - if (message.role === 'user' && message.state.stagingSelections.length > 0) { - console.log('Message staging selections:', message.state.stagingSelections) - } - } - // also print thread-level staging selections - if (thread.state.stagingSelections.length > 0) { - console.log('Thread staging selections:', thread.state.stagingSelections) - } - }, 1000) - - const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION) - - - const readThreads = this._readAllThreads() - const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum) - - if (updatedThreads !== null) { - this._storeAllThreads(updatedThreads) - } - - const allThreads = updatedThreads ?? readThreads + const allThreads = readThreads this.state = { allThreads: allThreads, currentThreadId: null as unknown as string, // gets set in startNewThread() @@ -236,9 +208,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() - - this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) - } // !!! this is important for properly restoring URIs from storage @@ -251,10 +220,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { }); } - private _readAllThreads(): ChatThreads { + private _readAllThreads(): ChatThreads | null { const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION); if (!threadsStr) { - return {}; + return null } return this._convertThreadDataFromStorage(threadsStr); } @@ -270,48 +239,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - // returns if should update - private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null { - - if (!oldVersion) { - - // unknown, just reset chat? - return null - } - - /** v1 -> v2 - - threads.state.currentStagingSelections: CodeStagingSelection[] | null; - + thread[threadIdx].state - + message.state - + chatMessage.staging: StagingInfo | null - */ - else if (oldVersion === 'v1') { - const threads = oldThreadsObject as Omit - // update the threads - for (const thread of Object.values(threads)) { - if (!thread.state) { - thread.state = defaultThreadState - } - for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.state) { - chatMessage.state = defaultMessageState - } - } - } - - // push the update - return threads - } - else if (oldVersion === 'v2') { - return null - } - - // up to date - return null - - } - - // 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/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index e8800562..4f767928 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { ChatMessage } from '../browser/chatThreadService.js' +import { ChatMessage } from './chatThreadService.js' import { InternalToolInfo, ToolName } from './toolsService.js' import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 72095c83..98ab45fc 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -28,7 +28,7 @@ type SetModelSelectionOfFeatureFn = ( options?: { doNotApplyEffects?: true } ) => Promise; -type SetGlobalSettingFn = (settingName: T, newVal: GlobalSettings[T]) => void; +type SetGlobalSettingFn = (settingName: T, newVal: GlobalSettings[T]) => void; export type ModelOption = { name: string, selection: ModelSelection } @@ -49,6 +49,8 @@ export interface IVoidSettingsService { readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state readonly waitForInitState: Promise; + readAndInitializeState: (providedState?: VoidSettingsState) => Promise; + onDidChangeState: Event; setSettingOfProvider: SetSettingOfProviderFn; @@ -168,6 +170,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes state: VoidSettingsState; + + private readonly _resolver: () => void waitForInitState: Promise // await this if you need a valid state initially constructor( @@ -181,56 +185,57 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // at the start, we haven't read the partial config yet, but we need to set state to something this.state = defaultState() - let resolver: () => void = () => { } this.waitForInitState = new Promise((res, rej) => resolver = res) + this._resolver = resolver - // read and update the actual state immediately - this._readState().then(readS => { + this.readAndInitializeState() + } - // the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to) - const newSettingsOfProvider = { - // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) - ...{ deepseek: defaultSettingsOfProvider.deepseek }, + async readAndInitializeState(providedState?: VoidSettingsState) { + // If providedState is given, use it instead of reading from storage + const readS = providedState || await this._readState(); - // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) - ...{ xAI: defaultSettingsOfProvider.xAI }, + // the stored data structure might be outdated, so we need to update it here + const newSettingsOfProvider = { + // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) + ...{ deepseek: defaultSettingsOfProvider.deepseek }, - // A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS) - ...{ vLLM: defaultSettingsOfProvider.vLLM }, + // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) + ...{ xAI: defaultSettingsOfProvider.xAI }, + // A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS) + ...{ vLLM: defaultSettingsOfProvider.vLLM }, - ...readS.settingsOfProvider, + ...readS.settingsOfProvider, - // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) - gemini: { - ...readS.settingsOfProvider.gemini, - models: [ - ...readS.settingsOfProvider.gemini.models, - ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) - ] - } + // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) + gemini: { + ...readS.settingsOfProvider.gemini, + models: [ + ...readS.settingsOfProvider.gemini.models, + ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) + ] } + }; - const newModelSelectionOfFeature = { - // A HACK BECAUSE WE ADDED FastApply - ...{ 'Apply': null }, - ...readS.modelSelectionOfFeature, - } + const newModelSelectionOfFeature = { + // A HACK BECAUSE WE ADDED FastApply + ...{ 'Apply': null }, + ...readS.modelSelectionOfFeature, + }; - readS = { - ...readS, - settingsOfProvider: newSettingsOfProvider, - modelSelectionOfFeature: newModelSelectionOfFeature, - } + const finalState = { + ...readS, + settingsOfProvider: newSettingsOfProvider, + modelSelectionOfFeature: newModelSelectionOfFeature, + }; - this.state = _validatedState(readS) - - resolver() - this._onDidChangeState.fire() - }) + this.state = _validatedState(finalState); + this._resolver(); + this._onDidChangeState.fire(); } From 971ce88df2e492c899b9b025dddc26a4cbdbf8e5 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 27 Feb 2025 17:21:39 -0800 Subject: [PATCH 15/19] style --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5c413b30..09d03a16 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 @@ -432,7 +432,7 @@ export const SelectedFiles = ( return
From 6d6e2fa974557fd24b2c40c850c37729c23f2df8 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 28 Feb 2025 00:04:42 -0800 Subject: [PATCH 16/19] fix annoying refactor --- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/browser/sidebarActions.ts | 4 +-- .../contrib/void/common/chatThreadService.ts | 25 ++++++++++--------- 3 files changed, 16 insertions(+), 15 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 09d03a16..d3c08eaf 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 @@ -979,7 +979,7 @@ export const SidebarChat = () => { const previousMessages = currentThread?.messages ?? [] const selections = currentThread.state.stagingSelections - const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) } + const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadState({ stagingSelections: s }) } // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 499c9887..551a8cc8 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -141,8 +141,8 @@ registerAction2(class extends Action2 { let setSelections = (s: StagingSelectionItem[]) => { } if (focusedMessageIdx === undefined) { - selections = chatThreadService.getCurrentThreadStagingSelections() - setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s) + selections = chatThreadService.getCurrentThreadState().stagingSelections + setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s }) } else { selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index 57d9dfe1..2704cd58 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -94,13 +94,18 @@ export type ChatThreads = { state: { stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none) - isCheckedOfSelectionId: { [selectionId: string]: boolean }; + isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO } }; } type ThreadType = ChatThreads[string] +const defaultThreadState: ThreadType['state'] = { + stagingSelections: [], + focusedMessageIdx: undefined, + isCheckedOfSelectionId: {} +} export type ThreadsState = { allThreads: ChatThreads; @@ -124,11 +129,7 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], - state: { - stagingSelections: [], - focusedMessageIdx: undefined, - isCheckedOfSelectionId: {} - }, + state: defaultThreadState, } satisfies ChatThreads[string] } @@ -159,8 +160,8 @@ export interface IChatThreadService { // exposed getters/setters getCurrentMessageState: (messageIdx: number) => UserMessageState setCurrentMessageState: (messageIdx: number, newState: Partial) => void - getCurrentThreadStagingSelections: () => StagingSelectionItem[] - setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void + getCurrentThreadState: () => ThreadType['state'] + setCurrentThreadState: (newState: Partial) => void // call to edit a message @@ -589,13 +590,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - getCurrentThreadStagingSelections = () => { + getCurrentThreadState = () => { const currentThread = this.getCurrentThread() - return currentThread.state.stagingSelections + return currentThread.state } - setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => { - this._setCurrentThreadState({ stagingSelections }) + setCurrentThreadState = (newState: Partial) => { + this._setCurrentThreadState(newState) } // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) From 4a00bd3e00de0e00d26d8b127427baf13888a958 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 28 Feb 2025 01:07:44 -0800 Subject: [PATCH 17/19] Cmd+L selections open better ux --- .../react/src/sidebar-tsx/SidebarChat.tsx | 30 ++++++++++++------- .../contrib/void/browser/sidebarActions.ts | 5 ++++ .../contrib/void/common/chatThreadService.ts | 6 ++++ 3 files changed, 30 insertions(+), 11 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 d3c08eaf..e7f2ff37 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 @@ -376,9 +376,6 @@ export const SelectedFiles = ( | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean } ) => { - // index -> isOpened - const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? []) - // state for tracking hover on clear all button const [isClearHovered, setIsClearHovered] = useState(false) @@ -409,6 +406,7 @@ export const SelectedFiles = ( fileURI: uri, selectionStr: null, range: null, + state: { isOpened: false }, })) } @@ -423,7 +421,7 @@ export const SelectedFiles = ( {allSelections.map((selection, i) => { - const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i]) + const isThisSelectionOpened = (!!selection.selectionStr && selection.state.isOpened) //!!(selection.selectionStr && selectionIsOpened[i]) const isThisSelectionAFile = selection.selectionStr === null const isThisSelectionProspective = i > selections.length - 1 @@ -449,8 +447,8 @@ export const SelectedFiles = ( transition-all duration-150 `} onClick={() => { + if (type !== 'staging') return; // (never) if (isThisSelectionProspective) { // add prospective selection to selections - if (type !== 'staging') return; // (never) setSelections([...selections, selection]) } else if (isThisSelectionAFile) { // open files commandService.executeCommand('vscode.open', selection.fileURI, { @@ -458,11 +456,22 @@ export const SelectedFiles = ( // preserveFocus: false, }); } else { // show text - setSelectionIsOpened(s => { - const newS = [...s] - newS[i] = !newS[i] - return newS - }); + + const selection = selections[i] + const newSelection = { ...selection, state: { isOpened: !selection.state.isOpened } } + const newSelections = [ + ...selections.slice(0, i), + newSelection, + ...selections.slice(i + 1) + ] + setSelections(newSelections) + + // setSelectionIsOpened(s => { + // const newS = [...s] + // newS[i] = !newS[i] + // return newS + // }); + } }} > @@ -478,7 +487,6 @@ export const SelectedFiles = ( e.stopPropagation(); // don't open/close selection if (type !== 'staging') return; setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) - setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) }} size={10} /> diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 551a8cc8..fff6dda4 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -124,11 +124,13 @@ registerAction2(class extends Action2 { fileURI: model.uri, selectionStr: null, range: null, + state: { isOpened: false, } } : { type: 'Selection', fileURI: model.uri, selectionStr: selectionStr, range: selectionRange, + state: { isOpened: true, } } // update the staging selections @@ -148,6 +150,9 @@ registerAction2(class extends Action2 { setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) } + // close all selections besides the new one + selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) + // if matches with existing selection, overwrite (since text may change) const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection) if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) { diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index 2704cd58..956c770c 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -36,6 +36,9 @@ export type CodeSelection = { fileURI: URI; selectionStr: string; range: IRange; + state: { + isOpened: boolean; + }; } export type FileSelection = { @@ -43,6 +46,9 @@ export type FileSelection = { fileURI: URI; selectionStr: null; range: null; + state: { + isOpened: boolean; + }; } export type StagingSelectionItem = CodeSelection | FileSelection From 9c76a35837d93cf71f47ec332b6bb6100ab5b82d Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 28 Feb 2025 01:37:53 -0800 Subject: [PATCH 18/19] outline --- .../contrib/void/browser/prompt/prompts.ts | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 22d0ad52..a7259020 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -299,7 +299,7 @@ For example, if the user is asking you to "make this variable a better name", ma export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => { // we may want to do this in batches - const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } + const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } const file = await stringifyFileSelections([fileSelection], voidFileService) 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 e7f2ff37..75faa520 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 @@ -376,9 +376,6 @@ export const SelectedFiles = ( | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean } ) => { - // state for tracking hover on clear all button - const [isClearHovered, setIsClearHovered] = useState(false) - const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -417,7 +414,7 @@ export const SelectedFiles = ( } return ( -
+
{allSelections.map((selection, i) => { @@ -430,7 +427,7 @@ export const SelectedFiles = ( return
@@ -443,7 +440,13 @@ export const SelectedFiles = ( select-none ${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'} text-xs text-nowrap - border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1 + border rounded-sm ${isThisSelectionProspective + ? 'border-void-border-2' + : isThisSelectionOpened + ? 'border-void-border-1 ring-1 ring-[#007FD4]' + : 'border-void-border-1' + } + hover:border-void-border-1 transition-all duration-150 `} onClick={() => { @@ -497,7 +500,10 @@ export const SelectedFiles = ( {/* code box */} {isThisSelectionOpened ?
{ e.stopPropagation(); // don't focus input box }} From cfc39b83bbe712bac4855d44f6a88120bdb1082a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 28 Feb 2025 02:11:56 -0800 Subject: [PATCH 19/19] styles --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 1 + 1 file changed, 1 insertion(+) 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 75faa520..41da6151 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 @@ -198,6 +198,7 @@ export const VoidChatArea: React.FC = ({ rounded-md bg-vscode-input-bg outline-1 outline-void-border-3 focus-within:outline-void-border-1 hover:outline-void-border-1 + max-h-[80vh] overflow-y-auto ${className} `} onClick={(e) => {