diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index c325fc8e..e6a52c54 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -17,6 +17,7 @@ import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -123,7 +124,7 @@ export type ThreadStreamState = { const newThreadObject = () => { const now = new Date().toISOString() return { - id: new Date().getTime().toString(), + id: generateUuid(), createdAt: now, lastModified: now, messages: [], diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index bdecb940..a76ada5e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -338,23 +338,24 @@ class EditCodeService extends Disposable implements IEditCodeService { let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { const state = this.getURIStreamState({ uri: model.uri }) - if (prevStreamState === state) return + let prevStateActual = prevStreamState + prevStreamState = state + if (state === prevStateActual) return this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) } - // add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => { - if (uri_.fsPath !== model.uri.fsPath) return - const state = this.getURIStreamState({ uri: model.uri }) - if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null + this._register(this._onDidChangeURIStreamState.event(({ uri, state }) => { + if (uri.fsPath !== model.uri.fsPath) return + if (state === 'acceptRejectAll') { + if (!_removeAcceptRejectAllUI) + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null } else { _removeAcceptRejectAllUI?.() _removeAcceptRejectAllUI = null } })) - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) @@ -1666,8 +1667,10 @@ class EditCodeService extends Disposable implements IEditCodeService { this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal - const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] - diffZone._streamState.line = currentEndLine + // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable + // diffZone._streamState.line = currentEndLine + diffZone._streamState.line = latestStreamLocationMutable.line + } // end for @@ -1682,7 +1685,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const blocks = extractSearchReplaceBlocks(fullText) if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`) } // writeover the whole file diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index b7665eca..cd3276ff 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -201,7 +201,7 @@ export const extractSearchReplaceBlocks = (str: string) => { const ORIGINAL_ = ORIGINAL + `\n` const DIVIDER_ = '\n' + DIVIDER + `\n` - const FINAL_ = '\n' + FINAL + // logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty const blocks: ExtractedSearchReplaceBlock[] = [] @@ -229,7 +229,13 @@ export const extractSearchReplaceBlocks = (str: string) => { i = dividerStart // wrote ===== - let finalStart = str.indexOf(FINAL_, i) + + + const finalStartA = str.indexOf(FINAL, i) + const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive + const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL + let finalStart = finalStartB !== -1 ? finalStartB : finalStartA + if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) blocks.push({ diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 50f14775..94c11af0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -32,7 +32,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { .then(() => { setCopyButtonText(CopyButtonText.Copied) }) .catch(() => { setCopyButtonText(CopyButtonText.Error) }) metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only - }, [metricsService, clipboardService, codeStr]) + }, [metricsService, clipboardService, codeStr, setCopyButtonText]) const isSingleLine = !codeStr.includes('\n') @@ -49,67 +49,60 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { // state persisted for duration of react only -const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => { - const [_, ss] = useState(0) - const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId] - const setUri = useCallback((uri: URI | null) => { - if (applyBoxId === null) return - ss(c => c + 1) - if (uri === null) { - delete streamingURIOfApplyBoxIdRef.current[applyBoxId] - } - else { - streamingURIOfApplyBoxIdRef.current = { - ...streamingURIOfApplyBoxIdRef.current, - [applyBoxId]: uri, - } - } - }, [applyBoxId]) - return [uri, setUri] as const -} +const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { + console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef) const settingsState = useSettingsState() - - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - // get streaming URI of this applyBlockId (cached in react) - const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) + const [applyingUriRef, setApplyingUri_] = useRefState(applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null) + const [streamStateRef, setStreamState_] = useRefState(editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null })) - // get stream state of this URI - const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null })) - useURIStreamState(useCallback((uri, streamState) => { - if (appliedURI?.fsPath !== uri.fsPath) return - setStreamState(streamState) - }, [appliedURI, setStreamState])) + const setApplyingUri = useCallback((uri: URI | null) => { // switched the box's URI to whatever they clicked on most recently + setApplyingUri_(uri) + const newStreamState = editCodeService.getURIStreamState({ uri }) + if (uri) applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri + setStreamState_(newStreamState) + }, [applyBoxId, setApplyingUri_, editCodeService, setStreamState_]) + // listen for stream updates + useURIStreamState( + useCallback((uri, streamState) => { + const shouldUpdate = applyingUriRef.current?.fsPath === uri.fsPath + if (!shouldUpdate) return + setStreamState_(streamState) // editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null }) + }, [applyingUriRef, setStreamState_]) + ) const onSubmit = useCallback(() => { if (isDisabled) return + if (streamStateRef.current === 'streaming') return const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, chatApplyBoxId: applyBoxId, }) - setAppliedURI(uri) + setApplyingUri(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) + }, [editCodeService, applyBoxId, codeStr, metricsService, isDisabled, streamStateRef, setApplyingUri]) const onInterrupt = useCallback(() => { - if (!appliedURI) return - editCodeService.interruptURIStreaming({ uri: appliedURI, }) + if (streamStateRef.current !== 'streaming') return + if (!applyingUriRef.current) return + + editCodeService.interruptURIStreaming({ uri: applyingUriRef.current, }) metricsService.capture('Stop Apply', {}) - }, [streamStateRef, editCodeService, appliedURI, metricsService]) + }, [editCodeService, metricsService, streamStateRef, applyingUriRef]) const isSingleLine = !codeStr.includes('\n') @@ -135,8 +128,8 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'accept', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'accept', removeCtrlKs: false }) }} > Accept @@ -145,14 +138,15 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'reject', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'reject', removeCtrlKs: false }) }} > Reject + console.log('streamStateRef.current', streamStateRef.current) return <> {streamStateRef.current !== 'streaming' && } 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 f6d08287..320dccbb 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 @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={applyBoxId && } /> }