diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9c37821f..bdecb940 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -121,19 +121,19 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num } -type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming' +export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming' export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } @@ -177,6 +177,7 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; + chatApplyBoxId?: undefined; editorId: string; // the editor the input lives on @@ -196,11 +197,11 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea + chatApplyBoxId: string | null; _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; - codeBoxId: string | null; } | { isStreaming: false; streamRequestIdRef?: undefined; @@ -219,6 +220,7 @@ type TrackingZone = { originalCode?: undefined; editorId?: undefined; _removeStylesFns?: undefined; + chatApplyBoxId?: undefined; } & CommonZoneProps @@ -232,6 +234,7 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', + 'chatApplyBoxId', ] as const satisfies (keyof DiffArea)[] @@ -256,6 +259,7 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void; // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; @@ -263,9 +267,9 @@ export interface IEditCodeService { onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; // // DiffZone codeBoxId streaming state - isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean; - interruptCodeBoxId(opts: { codeBoxId: string }): void; - onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>; + getURIStreamState(opts: { uri: URI | null }): URIStreamState; + interruptURIStreaming(opts: { uri: URI }): void; + onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>; // testDiffs(): void; } @@ -291,8 +295,11 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event - private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>(); - onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event + private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>(); + onDidChangeURIStreamState = this._onDidChangeURIStreamState.event + + + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @@ -327,36 +334,30 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends, add/remove the accept|reject UI - let _removeAcceptRejectAllUI: (() => void) | null = null + // when a stream starts or ends, fire the event for onDidChangeURIStreamState + let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { - const uri = model.uri - const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] - .map(diffareaid => this.diffAreaOfId[diffareaid]) - .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') - const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + const state = this.getURIStreamState({ uri: model.uri }) + if (prevStreamState === state) return + this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) + } - const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + // 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(uri) ?? null + _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() })) - // codeBoxId - this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - const { codeBoxId } = diffZone._streamState - if (codeBoxId === null) return - this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid }) - })) } // initialize all existing models + initialize when a new model mounts @@ -505,7 +506,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false }) + this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { @@ -1266,7 +1267,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from, chatCodeBoxId } = opts + const { from, chatApplyBoxId } = opts let startLine: number let endLine: number @@ -1318,6 +1319,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode, startLine, endLine, @@ -1326,7 +1328,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1452,7 +1453,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, chatCodeBoxId } = opts + const { applyStr, chatApplyBoxId } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1493,6 +1494,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode: originalFileCode, startLine, endLine, @@ -1501,7 +1503,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1555,7 +1556,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._deleteTrackingZone(trackingZone) onFinishEdit() - shouldSendAnotherMessage = false } // refresh now in case onText takes a while to get 1st message @@ -1746,7 +1746,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - _interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + _interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') return if (!diffZone._streamState.isStreaming) return @@ -1774,35 +1774,33 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) } - isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) { - // brute force is OK for now - for (const diffareaid in this.diffAreaOfId) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue - if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) return true - } - return false + + getURIStreamState = ({ uri }: { uri: URI | null }) => { + if (uri === null) return 'idle' + + const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + .map(diffareaid => this.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + + const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + return state } - interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) { + interruptURIStreaming({ uri }: { uri: URI }) { // brute force for now is OK - for (const diffareaid in this.diffAreaOfId) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue + if (diffArea?.type !== 'DiffZone') continue if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) { - this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid }) - return - } + this._stopIfStreaming(diffArea) } + this._undoHistory(uri) } @@ -1829,7 +1827,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) { + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] if (diffareaids.size === 0) return // do nothing @@ -1842,7 +1840,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type == 'DiffZone') { if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) - else if (behavior === 'keep') this._deleteDiffZone(diffArea) + else if (behavior === 'accept') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { this._deleteCtrlKZone(diffArea) 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 ecd5d7d9..50f14775 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 @@ -1,7 +1,8 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js' +import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' +import { URI } from '../../../../../../../base/common/uri.js' enum CopyButtonText { Idle = 'Copy', @@ -44,80 +45,71 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } -const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => { - const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') - const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId })) - useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => { - if (codeBoxId !== codeBoxId2) return - setIsStreamingRef(isStreaming) - }, [codeBoxId, setIsStreamingRef])) - return [isStreamingRef, setIsStreamingRef] as const + + + +// 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 } +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { -const StopButton = ({ codeBoxId }: { codeBoxId: string }) => { - const accessor = useAccessor() - - const editCodeService = accessor.get('IEditCodeService') - const metricsService = accessor.get('IMetricsService') const settingsState = useSettingsState() - const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) - - - - return - -} - - - - - -export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { - - + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const settingsState = useSettingsState() + // get streaming URI of this applyBlockId (cached in react) + const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) + // 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 [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) const onSubmit = useCallback(() => { if (isDisabled) return - if (isStreamingRef.current) return - editCodeService.startApplying({ + const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, - chatCodeBoxId: codeBoxId, + chatApplyBoxId: applyBoxId, }) + setAppliedURI(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService]) + }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) const onInterrupt = useCallback(() => { - if (isStreamingRef.current) return - if (codeBoxId === null) return - editCodeService.interruptCodeBoxId({ codeBoxId, }) + if (!appliedURI) return + editCodeService.interruptURIStreaming({ uri: appliedURI, }) metricsService.capture('Stop Apply', {}) - }, [isStreamingRef, editCodeService, codeBoxId, metricsService]) - + }, [streamStateRef, editCodeService, appliedURI, metricsService]) const isSingleLine = !codeStr.includes('\n') @@ -130,11 +122,42 @@ export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string Apply + const stopButton = + + const acceptRejectButtons = <> + + + return <> - {!isStreamingRef.current && } - {!isStreamingRef.current && codeBoxId !== null && } - {!isStreamingRef.current && } + {streamStateRef.current !== 'streaming' && } + {streamStateRef.current === 'idle' && !isDisabled && applyButton} + {streamStateRef.current === 'streaming' && stopButton} + {streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons} } 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 d6af4843..f6d08287 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 @@ -14,7 +14,7 @@ import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -46,7 +46,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: if (t.type === "code") { - const codeBoxId = chatMessageLocation ? getCodeBoxId({ + const applyBoxId = chatMessageLocation ? getApplyBoxId({ threadId: chatMessageLocation.threadId, messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={} /> } 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 9e8f674f..1fbcc303 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 @@ -65,7 +65,7 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, - chatCodeBoxId: null, + chatApplyBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) 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 6e1f5102..5e164428 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 @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js' import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' @@ -24,7 +24,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js'; import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { IEditCodeService } from '../../../editCodeService.js'; +import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'; import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; @@ -43,6 +43,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js' import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js' +import { URI } from '../../../../../../../base/common/uri.js' @@ -76,7 +77,7 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() -const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set() +const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set() @@ -183,9 +184,9 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) disposables.push( - editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => { - const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId }) - codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming)) + editCodeService.onDidChangeURIStreamState(({ uri }) => { + const isStreaming = editCodeService.getURIStreamState({ uri }) + uriStreamingStateListeners.forEach(l => l(uri, isStreaming)) }) ) @@ -362,18 +363,14 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } -export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => { +export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => { useEffect(() => { - codeBoxIdStreamingStateListeners.add(listener) - return () => { codeBoxIdStreamingStateListeners.delete(listener) } - }, [listener, codeBoxIdStreamingStateListeners]) + uriStreamingStateListeners.add(listener) + return () => { uriStreamingStateListeners.delete(listener) } + }, [listener, uriStreamingStateListeners]) } - - - - export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => {