From 323d80ef55d3487356d87f7ed23ee7123d4d25d9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 6 Feb 2025 03:02:46 -0800 Subject: [PATCH 01/34] add divs for html --- .../browser/react/src/markdown/ChatMarkdownRender.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 28cd2539..9c214055 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 @@ -193,11 +193,11 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token if (t.type === "html") { return ( -
-				{``}
+			
+
{``}
{t.raw} - {``} -
+
{``}
+ ) } From 826d148305ab15b52fd8de5717b35f1785344b54 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 6 Feb 2025 03:03:14 -0800 Subject: [PATCH 02/34] html --- .../void/browser/react/src/markdown/ChatMarkdownRender.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9c214055..6d25cb24 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 @@ -193,11 +193,11 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token if (t.type === "html") { return ( -
+
 				
{``}
{t.raw}
{``}
-
+ ) } From 63efa219f5014caafecac1de4c3a7a68cfca39fe Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 6 Feb 2025 03:04:48 -0800 Subject: [PATCH 03/34] div --- .../void/browser/react/src/markdown/ChatMarkdownRender.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6d25cb24..0bbb573b 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 @@ -193,11 +193,11 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token if (t.type === "html") { return ( -
+			
{``}
{t.raw}
{``}
-
+ ) } From 5ce25e20c333284a655203bf28c6f3984495cddd Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 6 Feb 2025 03:10:07 -0800 Subject: [PATCH 04/34] css for md tags --- .../browser/react/src/markdown/ChatMarkdownRender.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 0bbb573b..ec0d2241 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 @@ -188,16 +188,14 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token if (nested) return contents - return

{contents}

+ return

{contents}

} if (t.type === "html") { return ( -
-
{``}
+

{t.raw} -

{``}
-
+

) } From 4ffd269db5f923c2b30b23f7ab2b537118a3e8e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 6 Feb 2025 03:11:26 -0800 Subject: [PATCH 05/34] + --- .../browser/react/src/markdown/ChatMarkdownRender.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 ec0d2241..15f67d9a 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 @@ -186,9 +186,11 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token ))} - if (nested) - return contents - return

{contents}

+ if (nested) return contents + + return

+ {contents} +

} if (t.type === "html") { From 387fd64db0e89b1ecb1490d8fca5870530132393 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 6 Feb 2025 03:18:48 -0800 Subject: [PATCH 06/34] edit ability --- .../contrib/void/browser/chatThreadService.ts | 151 ++++++++++++++++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 104 +++++++++--- .../void/browser/react/src/util/services.tsx | 11 ++ .../contrib/void/browser/sidebarActions.ts | 32 ++-- 4 files changed, 255 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 34b4dad4..0027c42f 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -40,6 +40,7 @@ export type ChatMessage = content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection + stagingSelections: StagingSelectionItem[] | null; // staging selections in edit mode } | { role: 'assistant'; @@ -59,13 +60,14 @@ export type ChatThreads = { createdAt: string; // ISO string lastModified: string; // ISO string messages: ChatMessage[]; + stagingSelections: StagingSelectionItem[] | null; + focusedMessageIdx?: number | undefined; // index of the message that is being edited (undefined if none) }; } export type ThreadsState = { allThreads: ChatThreads; currentThreadId: string; // intended for internal use only - currentStagingSelections: StagingSelectionItem[] | null; } export type ThreadStreamState = { @@ -84,6 +86,8 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], + focusedMessageIdx: undefined, + stagingSelections: null, } satisfies ChatThreads[string] } @@ -105,8 +109,12 @@ export interface IChatThreadService { openNewThread(): void; switchToThread(threadId: string): void; - setStaging(stagingSelection: StagingSelectionItem[] | null): void; + getFocusedMessageIdx(): number | undefined; + setFocusedMessageIdx(messageIdx: number | undefined): void; + _useStagingSelectionsState(messageIdx?: number | undefined): readonly [StagingSelectionItem[], (selections: StagingSelectionItem[]) => void]; + + editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; @@ -137,7 +145,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this.state = { allThreads: this._readAllThreads(), currentThreadId: null as unknown as string, // gets set in startNewThread() - currentStagingSelections: null, } // always be in a thread @@ -187,18 +194,50 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) } - async addUserMessageAndStreamResponse(userMessage: string) { - const threadId = this.getCurrentThread().id - const currSelns = this.state.currentStagingSelections ?? [] + async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) { + + const thread = this.getCurrentThread() + + const messageToReplace = thread.messages[messageIdx] + if (messageToReplace?.role !== 'user') { + console.log(`Error: tried to edit non-user message. messageIdx=${messageIdx}, numMessages=${thread.messages.length}`) + return + } + + // clear messages up to the index + const slicedMessages = thread.messages.slice(0, messageIdx) + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + messages: slicedMessages + } + } + }, true) + + // stream the edit + this.addUserMessageAndStreamResponse(userMessage, messageToReplace.stagingSelections) + + } + + async addUserMessageAndStreamResponse(userMessage: string, selectionsOverride?: StagingSelectionItem[] | null) { + + + const thread = this.getCurrentThread() + const threadId = thread.id + + let defaultThreadSelections = thread.stagingSelections + + const currSelns = selectionsOverride ?? defaultThreadSelections ?? [] // don't use _useFocusedStagingState to avoid race conditions with focusing // add user's message to chat history const instructions = userMessage const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns } + const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, stagingSelections: [], } this._addMessageToThread(threadId, userHistoryElt) - this._setStreamState(threadId, { error: undefined }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ @@ -210,12 +249,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })), ], onText: ({ newText, fullText }) => { + console.log('onText', fullText) this._setStreamState(threadId, { messageSoFar: fullText }) }, onFinalMessage: ({ fullText: content }) => { + console.log('finalMessage', JSON.stringify(content)) this.finishStreaming(threadId, content) }, onError: (error) => { + console.log('onError', content) this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) }, @@ -241,7 +283,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { getCurrentThread(): ChatThreads[string] { const state = this.state - return state.allThreads[state.currentThreadId]; + return state.allThreads[state.currentThreadId] + } + + getFocusedMessageIdx() { + const thread = this.getCurrentThread() + return thread.focusedMessageIdx } switchToThread(threadId: string) { @@ -291,11 +338,93 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) } + // sets the currently selected message (must be undefined if no message is selected) + setFocusedMessageIdx(messageIdx: number | undefined) { - setStaging(stagingSelection: StagingSelectionItem[] | null): void { - this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return + + this._setState({ + allThreads: { + ...this.state.allThreads, + [threadId]: { + ...thread, + focusedMessageIdx: messageIdx + } + } + }, true) } + // set thread.messages[messageIdx].stagingSelections + private setEditMessageStagingSelections(stagingSelections: StagingSelectionItem[], messageIdx: number): void { + + const thread = this.getCurrentThread() + const message = thread.messages[messageIdx] + if (message.role !== 'user') return; + + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + messages: thread.messages.map((m, i) => + i === messageIdx ? { ...m, stagingSelections } : m + ) + } + } + }, true) + + } + + // set thread.stagingSelections + private setDefaultStagingSelections(stagingSelections: StagingSelectionItem[]): void { + + + console.log('Default1') + const thread = this.getCurrentThread() + + console.log('Default2') + + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + stagingSelections + } + } + }, true) + + } + + // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) + _useStagingSelectionsState(messageIdx?: number | undefined) { + + let staging: StagingSelectionItem[] = [] + let setStaging: (selections: StagingSelectionItem[]) => void = () => { } + + const thread = this.getCurrentThread() + const isFocusingMessage = messageIdx !== undefined + if (isFocusingMessage) { // is editing message + + const message = thread.messages[messageIdx!] + if (message.role === 'user') { + staging = message.stagingSelections || [] + setStaging = (s) => this.setEditMessageStagingSelections(s, messageIdx) + } + + } + else { // is editing the default input box + staging = thread.stagingSelections || [] + setStaging = this.setDefaultStagingSelections.bind(this) + } + + return [staging, setStaging] as const + } + + + } registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager); 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 7b46422a..f175c300 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 @@ -25,6 +25,7 @@ import { Pencil } from 'lucide-react'; import { FeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'; + export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { return ( void; // Optional close button @@ -171,6 +173,7 @@ export const VoidInputForm: React.FC = ({ featureName, showSelections = false, selections = [], + showProspectiveSelections = true, onSelectionsChange, }) => { return ( @@ -191,6 +194,7 @@ export const VoidInputForm: React.FC = ({ type='staging' selections={selections} setSelections={onSelectionsChange} + showProspectiveSelections={showProspectiveSelections} /> )} @@ -528,14 +532,61 @@ export const SelectedFiles = ( type ChatBubbleMode = 'display' | 'edit' -const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => { +const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => { const role = chatMessage.role + + const accessor = useAccessor() + const chatThreadsService = accessor.get('IChatThreadService') + // edit mode state + const [staging, setStaging] = chatThreadsService._useStagingSelectionsState(messageIdx) const [mode, setMode] = useState('display') - const [editText, setEditText] = useState(chatMessage.displayContent ?? '') const [isHovered, setIsHovered] = useState(false) + const textAreaRef = useRef(null) + const textAreaFnsRef = useRef(null) + const [isDisabled, setIsDisabled] = useState(false) + useEffect(() => { + if (role === 'user') { + setStaging(chatMessage.selections || []) + if (textAreaFnsRef.current) + textAreaFnsRef.current.setValue(chatMessage.displayContent || '') + if (textAreaRef.current) + textAreaRef.current.value = chatMessage.displayContent || '' + } + }, [role]) + + + const onSubmit = async () => { + + if (isDisabled) return; + if (!textAreaRef.current) return; + if (messageIdx === undefined) return; + + // reset visual state + setMode('display'); + + // stream the edit + const userMessage = textAreaRef.current.value; + await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx) + + textAreaFnsRef.current?.setValue(''); + } + + const onAbort = () => { + const threadId = chatThreadsService.state.currentThreadId + chatThreadsService.cancelStreaming(threadId) + } + + const onKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') { + setMode('display') + } + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit() + } + }, [onSubmit, setMode]) if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show) return null @@ -552,17 +603,27 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo } else if (mode === 'edit') { chatbubbleContents = <> - - {t.raw} } if (t.type === "code") { + const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```'); + + const applyBoxId = getApplyBoxId({ + threadId: chatLocation!.threadId, + messageIdx: chatLocation!.messageIdx, + codeblockId: tokenId, + }) + return } + language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} + buttonsOnHover={} /> } @@ -183,7 +196,7 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token if (t.type === "paragraph") { const contents = <> {t.tokens.map((token, index) => ( - + // assign a unique tokenId to nested components ))} if (nested) return contents @@ -266,12 +279,12 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token ) } -export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => { +export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => { 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 b2aa35a9..791eb634 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 @@ -24,6 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; +import { ChatLocation } from '../../../searchAndReplaceService.js'; @@ -578,9 +579,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM } }, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) - const EditSymbol = mode === 'display' ? Pencil : X - const onOpenEdit = () => { setStaging({ ...staging, isBeingEdited: true }) chatThreadsService.setFocusedMessageIdx(messageIdx) @@ -674,7 +673,14 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM } } else if (role === 'assistant') { - chatbubbleContents = + const thread = chatThreadsService.getCurrentThread() + + const chatLocation: ChatLocation = { + threadId: thread.id, + messageIdx: messageIdx!, + } + + chatbubbleContents = } return
{ + return `${threadId}-${messageIdx}-${codeblockId}}` +} + +export type SearchAndReplaceBlock = { + search: string; + replace: string; +} + +// service that manages state +export type ApplyState = { + [applyBoxId: string]: { + searchAndReplaceBlocks: SearchAndReplaceBlock; + } +} + +// 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` + +export interface IFastApplyService { + readonly _serviceBrand: undefined; + + // readonly state: ApplyState; // readonly to the user + // setState(newState: Partial): void; + // onDidChangeState: Event; +} + +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( + ) { + super() + + // initial state + // this.state = { currentUri: undefined } + } + + setState(newState: Partial) { + + // this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + +} + +registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager); From 0346e90ca70696337a1fa8f978cc50414c7cdfc6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 11 Feb 2025 21:38:56 -0800 Subject: [PATCH 31/34] add parsing of <<< ORIGINAL, ===, FINAL >>> --- .../browser/helpers/extractCodeFromResult.ts | 2 +- .../void/browser/inlineDiffsService.ts | 310 ++++++++---------- .../contrib/void/browser/prompt/prompts.ts | 36 +- 3 files changed, 140 insertions(+), 208 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 768f4cfc..1cb53e5c 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -62,7 +62,7 @@ class SurroundingsRemover { if (index === -1) { this.i = this.j + 1 - return false + return null } // console.log('index', index, until.length) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 998a286f..b90998b1 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage, tripleTick } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -41,6 +41,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; +import { string } from 'zod'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -1207,11 +1208,130 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { return null } + private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { + const ORIGINAL = `<<<<<<< ORIGINAL` + const DIVIDER = `=======` + const FINAL = `>>>>>>> UPDATED` - // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ]) + const searchReplaceGenSysMessage = `\ +You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. + +A SEARCH/REPLACE block describes the code before and after a change. Here is the format: +${ORIGINAL} +// ... original code goes here +${DIVIDER} +// ... final code goes here +${FINAL} + +You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. +Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. + +Directions: +1. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +2. The original code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. +3. The original code cannot be empty. +4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY, with NO ERRORS. + - Make sure you add all necessary imports. +5. Follow coding convention (spaces, semilcolons, comments, etc). + +## EXAMPLE 1 +ORIGINAL_FILE +${tripleTick[0]} +let w = 5 +let x = 6 +let y = 7 +let z = 8 +${tripleTick[1]} + +CHANGE +Make x equal to 6.5, not 6. +${tripleTick[0]} +// ... existing code +let x = 6.5 +// ... existing code +${tripleTick[1]} + + +## ACCEPTED OUTPUT +${tripleTick[0]} +${ORIGINAL} +let x = 6 +${DIVIDER} +let x = 6.5 +${FINAL} +${tripleTick[1]} +` + + function endsWithAnyPrefixOf(str: string, anyPrefix: string) { + // for each prefix + for (let i = anyPrefix.length; i >= 0; i--) { + const prefix = anyPrefix.slice(0, i) + if (str.endsWith(prefix)) return prefix + } + return null + } + + const extractBlocks = (str: string) => { + + const ORIGINAL_ = ORIGINAL + `\n` + const DIVIDER_ = '\n' + DIVIDER + `\n` + const FINAL_ = '\n' + FINAL + + + const blocks: ({ + state: 'writingOriginal' | 'writingFinal' | 'done', + orig: string, + final: string, + })[] = [] + + let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) + while (true) { + let origStart = str.indexOf(ORIGINAL_, i) + if (origStart === -1) { return blocks } + origStart += ORIGINAL_.length + i = origStart + // wrote <<<< ORIGINAL + + let dividerStart = str.indexOf(DIVIDER_, i) + if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now + const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_) + blocks.push({ + orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)), + final: '', + state: 'writingOriginal' + }) + return blocks + } + const origStrDone = str.substring(origStart, dividerStart) + dividerStart += DIVIDER_.length + i = dividerStart + // wrote ===== + + let finalStart = str.indexOf(FINAL_, i) + if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now + const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) + blocks.push({ + orig: origStrDone, + final: str.substring(origStart, str.length - (isWritingFINAL?.length ?? 0)), + state: 'writingFinal' + }) + return blocks + } + const finalStrDone = str.substring(dividerStart, finalStart) + finalStart += FINAL_.length + i = finalStart + // wrote >>>>> FINAL + + blocks.push({ + orig: origStrDone, + final: finalStrDone, + state: 'done' + }) + } + + } - // 1a output search block let uri: URI @@ -1222,6 +1342,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + + + // generate search/replace block text + + + // parse output, make sure: 1. not redundant search and 2. valid output (retry if not) + + // apply change to string, check if it looks good (retry if not) + + // TODO check dirty + + // + // in ctrl+L the start and end lines are the full document const numLines = this._getNumLines(uri) @@ -1243,177 +1376,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - - - - - let streamRequestIdRef: { current: string | null } = { current: null } - - - // add to history - const { onFinishEdit } = this._addToHistory(uri) - - // __TODO__ let users customize modelFimTags - const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' - const modelFimTags = defaultFimTags - - const adding: Omit = { - type: 'DiffZone', - originalCode, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - } - - // now handle messages - let messages: LLMChatMessage[] - - if (from === 'Chat') { - const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri }) - messages = [ - { role: 'system', content: fastApply_rewritewholething_systemMessage, }, - { role: 'user', content: userContent, } - ] - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - const { _mountInfo } = ctrlKZone - const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - - // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: - const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // if (isOllamaFIM) { - // messages = { - // type: 'ollamaFIM', - // prefix, - // suffix, - // } - - // } - // else { - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) - // type: 'messages', - messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, - { role: 'user', content: userContent, } - ] - // } - } - else { throw new Error(`featureName ${from} is invalid`) } - - - const onDone = (hadError: boolean) => { - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - - if (from === 'QuickEdit') { - const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone - - ctrlKZone._linkedStreamingDiffZone = null - this._deleteCtrlKZone(ctrlKZone) - } - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() - - // if had error, revert! - if (hadError) { - this._undoHistory(diffZone._URI) - } - } - - // refresh now in case onText takes a while to get 1st message - this._refreshStylesAndDiffsInURI(uri) - - - const extractText = (fullText: string, recentlyAddedTextLen: number) => { - if (from === 'QuickEdit') { - if (isOllamaFIM) return fullText - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) - } - else if (from === 'Chat') { - return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) - } - throw 1 - } - - const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - - // state used in onText: - let fullText = '' - let prevIgnoredSuffix = '' - - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', - logging: { loggingName: `startApplying - ${from}` }, - messages, - onText: ({ newText: newText_ }) => { - - const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText - - const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) - this._refreshStylesAndDiffsInURI(uri) - - prevIgnoredSuffix = ignoredSuffix - }, - onFinalMessage: ({ fullText }) => { - // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) - // at the end, re-write whole thing to make sure no sync errors - const [text, _] = extractText(fullText, 0) - this._writeText(uri, text, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - onDone(false) - }, - onError: (e) => { - const details = errorDetails(e.fullError) - this._notificationService.notify({ - severity: Severity.Warning, - message: `Void Error: ${e.message}`, - actions: { - secondary: [{ - id: 'void.onerror.opensettings', - enabled: true, - label: 'Open Void settings', - tooltip: '', - class: undefined, - run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } - }] - }, - source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined - }) - onDone(true) - }, - - }) - - return diffZone - } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index b3fb4482..1925bccc 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -12,7 +12,7 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; // this is just for ease of readability -const tripleTick = ['```', '```'] +export const tripleTick = ['```', '```'] export const chat_systemMessage = `\ You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. @@ -74,7 +74,7 @@ ${tripleTick[1]} INSTRUCTIONS add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power -ACCEPTED OUTPUT +## ACCEPTED OUTPUT We can add the following code to the file: ${tripleTick[0]}typescript // existing code... @@ -117,7 +117,7 @@ ${tripleTick[1]} INSTRUCTIONS memoize results -ACCEPTED OUTPUT +## ACCEPTED OUTPUT To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function: ${tripleTick[0]}typescript // existing code... @@ -222,36 +222,6 @@ Please finish writing the new file by applying the change to the original file. -export const fastApply_searchreplace_systemMessage = `\ -You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. - -Directions: -1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file. -2. Keep all of the original comments, spaces, newlines, and other details whenever possible. -3. ONLY output the full new file. Do not add any other explanations or text. -` - - -export const fastApply_searchreplace_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { - - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - - return `\ -ORIGINAL_FILE -\`\`\`${language} -${originalCode} -\`\`\` - -CHANGE -\`\`\` -${applyStr} -\`\`\` - -INSTRUCTIONS -Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation. -` -} - From fc979498878ffa50d1d97f9ca9ebdd3612cdd69c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 11 Feb 2025 22:02:46 -0800 Subject: [PATCH 32/34] autocomplete --- .../electron-main/llmMessage/sendLLMMessage.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 1364e251..dc70c36c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -136,9 +136,6 @@ export const sendLLMMessage = ({ try { switch (providerName) { - case 'anthropic': - sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); - break; case 'openAI': case 'openRouter': case 'deepseek': @@ -150,14 +147,21 @@ export const sendLLMMessage = ({ if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) break; + case 'anthropic': + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) + else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + break; case 'gemini': - sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' }) + else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'groq': - sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM' }) + else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'mistral': - sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM' }) + else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) From d504daffa50668e8b088e127a6ceceb699151264 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 00:34:24 -0800 Subject: [PATCH 33/34] searchRepalce --- .../void/browser/inlineDiffsService.ts | 229 +++++++++++------- .../contrib/void/browser/prompt/prompts.ts | 8 +- 2 files changed, 147 insertions(+), 90 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index b90998b1..88dc49e6 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags, fastApply_searchreplace_systemMessage, fastApply_searchreplace_userMessage, tripleTick } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, tripleTick } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -39,9 +39,9 @@ import { Emitter } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; +import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { string } from 'zod'; +import { VSReadFile } from './helpers/readFile.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -101,20 +101,19 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number return paddingLeft; }; -// similar to ServiceLLM + + export type StartApplyingOpts = { from: 'QuickEdit'; + type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) } | { - from: 'Chat'; + from: 'ClickApply'; + type: 'searchReplace' | 'rewrite'; applyStr: string; - applyBoxId: string; -} | { - from: 'Autocomplete'; - range: IRange; - userMessage: string; } + export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -139,6 +138,11 @@ export type Diff = { +type ExtractedCodeBlock = { + state: 'writingOriginal' | 'writingFinal' | 'done', + orig: string, + final: string, +} // _ means anything we don't include if we clone it // DiffArea.originalStartLine is the line in originalCode (not the file) @@ -998,7 +1002,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out @@ -1037,39 +1041,39 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // at the start, add a newline between the stream and originalCode to make reasoning easier - if (!latest.addedSplitYet) { + if (!latestMutable.addedSplitYet) { this._writeText(uri, '\n', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col, }, { shouldRealignDiffAreas: true } ) - latest.addedSplitYet = true + latestMutable.addedSplitYet = true } // insert deltaText at latest line and col this._writeText(uri, deltaText, - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) - latest.line += deltaText.split('\n').length - 1 + latestMutable.line += deltaText.split('\n').length - 1 const lastNewlineIdx = deltaText.lastIndexOf('\n') - latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx + latestMutable.col = lastNewlineIdx === -1 ? latestMutable.col + deltaText.length : deltaText.length - lastNewlineIdx // delete or insert to get original up to speed - if (latest.originalCodeStartLine < originalCodeStartLine) { + if (latestMutable.originalCodeStartLine < originalCodeStartLine) { // moved up, delete - const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine + const numLinesDeleted = originalCodeStartLine - latestMutable.originalCodeStartLine this._writeText(uri, '', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) } - else if (latest.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'), - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { + this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) } - latest.originalCodeStartLine = originalCodeStartLine + latestMutable.originalCodeStartLine = originalCodeStartLine // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine @@ -1187,7 +1191,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { public startApplying(opts: StartApplyingOpts) { - const addedDiffZone = this._initializeStartApplying(opts) + const addedDiffZone = this._initializeRewriteStream(opts) return addedDiffZone?.diffareaid } @@ -1209,12 +1213,12 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined { + private async _generateSearchAndReplaceBlocks({ uri, applyStr }: { uri: URI, applyStr: string }): Promise { const ORIGINAL = `<<<<<<< ORIGINAL` const DIVIDER = `=======` const FINAL = `>>>>>>> UPDATED` - const searchReplaceGenSysMessage = `\ + const searchReplaceSysMessage = `\ You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. A SEARCH/REPLACE block describes the code before and after a change. Here is the format: @@ -1228,11 +1232,12 @@ You will be given the original file \`ORIGINAL_FILE\` and a description of a cha Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. Directions: -1. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -2. The original code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. -3. The original code cannot be empty. -4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY, with NO ERRORS. +1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. +2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. +4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. + - Make sure the "final" code is complete and will not result in syntax/lint errors. 5. Follow coding convention (spaces, semilcolons, comments, etc). ## EXAMPLE 1 @@ -1263,6 +1268,18 @@ ${FINAL} ${tripleTick[1]} ` + + const searchReplaceUserMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +ORIGINAL_FILE +${originalCode} + +CHANGE +${applyStr} + +INSTRUCTIONS +Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. +` + function endsWithAnyPrefixOf(str: string, anyPrefix: string) { // for each prefix for (let i = anyPrefix.length; i >= 0; i--) { @@ -1279,11 +1296,7 @@ ${tripleTick[1]} const FINAL_ = '\n' + FINAL - const blocks: ({ - state: 'writingOriginal' | 'writingFinal' | 'done', - orig: string, - final: string, - })[] = [] + const blocks: ExtractedCodeBlock[] = [] let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) while (true) { @@ -1329,58 +1342,115 @@ ${tripleTick[1]} state: 'done' }) } - } - - let uri: URI - - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ + // generate search/replace block text + const fileContents = await VSReadFile(this._modelService, uri) + if (fileContents === null) return // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + const userMessageContent = searchReplaceUserMessage({ originalCode: fileContents, applyStr: applyStr }) + const messages: LLMChatMessage[] = [ + { role: 'system', content: searchReplaceSysMessage }, + { role: 'user', content: userMessageContent } + ] + let streamRequestIdRef: { current: string | null } = { current: null } + + const diffareaidOfBlockNum: number[] = [] + + const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { + const blocks = extractBlocks(fullText) + + // find block.orig in fileContents and return its range in file + const findBlock = (block: { orig: string }, fileContents: string) => { + const origText = block.orig + const idx = fileContents.indexOf(origText) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(origText) + if (lastIdx !== idx) return 'Not unique' as const + + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = origText.split('\n').length + const endLine = startLine + numLines - 1 + + return [startLine, endLine] + } + + let latestStreamInfoMutable: any = {} - // generate search/replace block text + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + const foundInCode = findBlock(block, fileContents) + if (typeof foundInCode === 'string') { + console.log('ERROR!!!!', foundInCode) + continue + } + const [startLine, endLine] = foundInCode - // parse output, make sure: 1. not redundant search and 2. valid output (retry if not) + if (block.state === 'writingOriginal') continue - // apply change to string, check if it looks good (retry if not) + // if should add new diffarea + if (blockNum > diffareaidOfBlockNum.length) { + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) - // TODO check dirty + diffareaidOfBlockNum.push(diffZone.diffareaid) - // + latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } - // in ctrl+L the start and end lines are the full document + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone.type !== 'DiffZone') continue - const numLines = this._getNumLines(uri) - if (numLines === null) return + this._writeStreamedDiffZoneLLMText(diffZone, fullText, newText, latestStreamInfoMutable) + this._refreshStylesAndDiffsInURI(uri) + } - let startLine: number - let endLine: number - - startLine = 1 - endLine = numLines - - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + } - // 1b find the start and end line that the search block lives on (if can't find it, retry 1a) + + // TODO turn this into a service and provide it + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'FastApply', + logging: { loggingName: `generateSearchAndReplace` }, + messages, + onText: ({ newText, fullText }) => { onText({ newText, fullText }) }, + onFinalMessage: ({ fullText }) => { }, + onError: (e) => { console.log('ERROR', e) }, + + }) + + } - - private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { + private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts @@ -1388,7 +1458,7 @@ ${tripleTick[1]} let endLine: number let uri: URI - if (from === 'Chat') { + if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1430,8 +1500,7 @@ ${tripleTick[1]} const { onFinishEdit } = this._addToHistory(uri) // __TODO__ let users customize modelFimTags - const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' - const modelFimTags = defaultFimTags + const quickEditFIMTags = defaultQuickEditFimTags const adding: Omit = { type: 'DiffZone', @@ -1462,7 +1531,7 @@ ${tripleTick[1]} // now handle messages let messages: LLMChatMessage[] - if (from === 'Chat') { + if (from === 'ClickApply') { const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ { role: 'system', content: fastApply_rewritewholething_systemMessage, }, @@ -1476,25 +1545,14 @@ ${tripleTick[1]} const { _mountInfo } = ctrlKZone const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // if (isOllamaFIM) { - // messages = { - // type: 'ollamaFIM', - // prefix, - // suffix, - // } - - // } - // else { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) // type: 'messages', messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, + { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, { role: 'user', content: userContent, } ] - // } } else { throw new Error(`featureName ${from} is invalid`) } @@ -1524,16 +1582,15 @@ ${tripleTick[1]} const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { - if (isOllamaFIM) return fullText - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) } - else if (from === 'Chat') { + else if (from === 'ClickApply') { return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) } throw 1 } - const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + const latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } // state used in onText: let fullText = '' @@ -1541,7 +1598,7 @@ ${tripleTick[1]} streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + useProviderFor: opts.from === 'ClickApply' ? 'FastApply' : 'Ctrl+K', logging: { loggingName: `startApplying - ${from}` }, messages, onText: ({ newText: newText_ }) => { @@ -1550,7 +1607,7 @@ ${tripleTick[1]} fullText += prevIgnoredSuffix + newText const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfoMutable) this._refreshStylesAndDiffsInURI(uri) prevIgnoredSuffix = ignoredSuffix diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 1925bccc..428625fd 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -279,19 +279,19 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF } -export type FimTagsType = { +export type QuickEditFimTagsType = { preTag: string, sufTag: string, midTag: string } -export const defaultFimTags: FimTagsType = { +export const defaultQuickEditFimTags: QuickEditFimTagsType = { preTag: 'ABOVE', sufTag: 'BELOW', midTag: 'SELECTION', } // this should probably be longer -export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => { +export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => { return `\ You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags. @@ -307,7 +307,7 @@ Instructions: } export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: { - selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string, + selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string, isOllamaFIM: false, // we require this be false for clarity }) => { const { preTag, sufTag, midTag } = fimTags From 0dfa81f637f56beb9d4de8cf06f473892de36b3f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Feb 2025 01:43:50 -0800 Subject: [PATCH 34/34] Search and replace? --- .../void/browser/inlineDiffsService.ts | 104 ++++++------------ .../react/src/markdown/ChatMarkdownRender.tsx | 26 +++-- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 + .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../void/browser/searchAndReplaceService.ts | 7 +- 5 files changed, 55 insertions(+), 89 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 88dc49e6..ed435a26 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1084,56 +1084,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - // // if streaming, use diffs to figure out where to write new code - // // these are two different coordinate systems - new and old line number - // let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted - // let originalCodeStartLine: number // get original[oldStartingPoint...] - - // const lastDiff = computedDiffs.pop() - - // if (!lastDiff) { - // // if the writing is identical so far, display no changes - // newFileEndLine = diffZone.startLine - // originalCodeStartLine = 1 - // } - // else { - // if (lastDiff.type === 'insertion') { - // newFileEndLine = lastDiff.endLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else if (lastDiff.type === 'deletion') { - // newFileEndLine = lastDiff.startLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else if (lastDiff.type === 'edit') { - // newFileEndLine = lastDiff.endLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else { - // throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) - // } - // } - - // diffZone._streamState.line = newFileEndLine - - // // lines are 1-indexed - // const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n') - // const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n') - - // const newCode = `${newFileTop}\n${oldFileBottom}` - - // this._writeText(uri, newCode, - // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed - // { shouldRealignDiffAreas: true } - // ) - - - // return computedDiffs - - - - - // called first, then call startApplying public addCtrlKZone({ startLine, endLine, editor }: AddCtrlKOpts) { @@ -1191,8 +1141,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { public startApplying(opts: StartApplyingOpts) { - const addedDiffZone = this._initializeRewriteStream(opts) - return addedDiffZone?.diffareaid + + if (opts.type === 'rewrite') { + const addedDiffZone = this._initializeRewriteStream(opts) + return addedDiffZone?.diffareaid + } + + else if (opts.type === 'searchReplace') { + this._initializeSearchAndReplaceStream(opts) + return undefined + } + + else return undefined + } @@ -1213,7 +1174,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - private async _generateSearchAndReplaceBlocks({ uri, applyStr }: { uri: URI, applyStr: string }): Promise { + private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { const ORIGINAL = `<<<<<<< ORIGINAL` const DIVIDER = `=======` const FINAL = `>>>>>>> UPDATED` @@ -1268,6 +1229,14 @@ ${FINAL} ${tripleTick[1]} ` + const uri_ = this._getActiveEditorURI() + if (!uri_) return + const uri = uri_ + + // generate search/replace block text + const fileContents = await VSReadFile(this._modelService, uri) + if (fileContents === null) return + const searchReplaceUserMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ ORIGINAL_FILE @@ -1280,7 +1249,7 @@ INSTRUCTIONS Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. ` - function endsWithAnyPrefixOf(str: string, anyPrefix: string) { + const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { // for each prefix for (let i = anyPrefix.length; i >= 0; i--) { const prefix = anyPrefix.slice(0, i) @@ -1344,9 +1313,6 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest } } - // generate search/replace block text - const fileContents = await VSReadFile(this._modelService, uri) - if (fileContents === null) return // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) @@ -1364,26 +1330,24 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const blocks = extractBlocks(fullText) // find block.orig in fileContents and return its range in file - const findBlock = (block: { orig: string }, fileContents: string) => { - const origText = block.orig - const idx = fileContents.indexOf(origText) + const findTextInCode = (text: string, fileContents: string) => { + const idx = fileContents.indexOf(text) if (idx === -1) return 'Not found' as const - const lastIdx = fileContents.lastIndexOf(origText) + const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = origText.split('\n').length + const numLines = text.split('\n').length const endLine = startLine + numLines - 1 - return [startLine, endLine] } let latestStreamInfoMutable: any = {} - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - const foundInCode = findBlock(block, fileContents) + if (block.state === 'writingOriginal') continue + + const foundInCode = findTextInCode(block.orig, fileContents) if (typeof foundInCode === 'string') { console.log('ERROR!!!!', foundInCode) continue @@ -1391,8 +1355,6 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest const [startLine, endLine] = foundInCode - if (block.state === 'writingOriginal') continue - // if should add new diffarea if (blockNum > diffareaidOfBlockNum.length) { const adding: Omit = { @@ -1443,13 +1405,11 @@ Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggest }) - - - - } + + private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts 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 86afcc33..7b03f068 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 @@ -7,7 +7,7 @@ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' -import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js' +import { ChatMessageLocation, } from '../../../searchAndReplaceService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' @@ -19,6 +19,16 @@ enum CopyButtonState { const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' + + +type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: number } + +const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { + return `${threadId}-${messageIdx}-${tokenIdx}` +} + + + const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => { const accessor = useAccessor() @@ -47,9 +57,9 @@ const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, apply const onApply = useCallback(() => { inlineDiffService.startApplying({ - from: 'Chat', + from: 'ClickApply', + type: 'searchReplace', applyStr, - applyBoxId, }) metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only }, [metricsService, inlineDiffService, applyStr]) @@ -87,7 +97,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tokenId = '', }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation, tokenId?: string, }): JSX.Element => { +const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: number }): JSX.Element => { // deal with built-in tokens first (assume marked token) @@ -104,7 +114,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok const applyBoxId = getApplyBoxId({ threadId: chatLocation!.threadId, messageIdx: chatLocation!.messageIdx, - codeblockId: tokenId, + tokenIdx: tokenIdx, }) return {t.tokens.map((token, index) => ( - // assign a unique tokenId to nested components + // assign a unique tokenId to nested components ))} if (nested) return contents @@ -279,12 +289,12 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok ) } -export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => { +export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: 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/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 37dd91bf..57dbb472 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -59,6 +59,7 @@ export const QuickEditChat = ({ const id = inlineDiffsService.startApplying({ from: 'QuickEdit', + type:'rewrite', diffareaid: diffareaid, }) setCurrentlyStreamingDiffZone(id ?? null) 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 2aaf9dd2..d8b4ef93 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 @@ -24,7 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatLocation } from '../../../searchAndReplaceService.js'; +import { ChatMessageLocation } from '../../../searchAndReplaceService.js'; @@ -675,12 +675,12 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM else if (role === 'assistant') { const thread = chatThreadsService.getCurrentThread() - const chatLocation: ChatLocation = { + const chatMessageLocation: ChatMessageLocation = { threadId: thread.id, messageIdx: messageIdx!, } - chatbubbleContents = + chatbubbleContents = } return
{ - return `${threadId}-${messageIdx}-${codeblockId}}` -} export type SearchAndReplaceBlock = { search: string;