From d00c478c5bd254970fd62c83a8bd882da25a5a1e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 8 May 2025 02:21:55 -0700 Subject: [PATCH] add accept|reject on edit tool! --- .../contrib/void/browser/editCodeService.ts | 6 +- .../void/browser/editCodeServiceInterface.ts | 4 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 223 +++++++++++++----- .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 14 +- .../void/browser/react/src/util/helpers.tsx | 7 +- .../contrib/void/browser/toolsService.ts | 4 +- 7 files changed, 179 insertions(+), 81 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 6bb52d7e..96e3bf6d 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1124,8 +1124,8 @@ class EditCodeService extends Disposable implements IEditCodeService { return } - public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) { - const uri = this._getURIBeforeStartApplying(opts) + public async callBeforeApplyOrEdit(givenURI: URI | 'current') { + const uri = this._uriOfGivenURI(givenURI) if (!uri) return await this._voidModelService.initializeModel(uri) await this._voidModelService.saveModel(uri) // save the URI @@ -1200,7 +1200,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } - public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) { + public instantlyRewriteFile({ uri, newContent }: { uri: URI, newContent: string }) { // start diffzone const res = this._startStreamingDiffZone({ uri, diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index f0712349..9e33fbd2 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -44,10 +44,10 @@ export interface IEditCodeService { processRawKeybindingText(keybindingStr: string): string; - callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; + callBeforeApplyOrEdit(uri: URI | 'current'): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | null; instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void; - instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void; + instantlyRewriteFile(opts: { uri: URI; newContent: string }): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; 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 2d397ecf..f412c1af 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 @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef, Fragment } from 'react' import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js' import { usePromise, useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' @@ -24,8 +24,9 @@ type IconButtonProps = { Icon: LucideIcon } -export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes) => ( - -) +} // export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( @@ -131,48 +132,41 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => { // state persisted for duration of react only // TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]` -const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } +const _applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } const getUriBeingApplied = (applyBoxId: string) => { - return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null + return _applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null } -export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => { - - const settingsState = useSettingsState() - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId - +export const useApplyStreamState = ({ applyBoxId }: { applyBoxId: string }) => { const accessor = useAccessor() const voidCommandBarService = accessor.get('IVoidCommandBarService') - const [_, rerender] = useState(0) - const getStreamState = useCallback(() => { const uri = getUriBeingApplied(applyBoxId) if (!uri) return 'idle-no-changes' return voidCommandBarService.getStreamState(uri) }, [voidCommandBarService, applyBoxId]) + + const [currStreamStateRef, setStreamState] = useRefState(getStreamState()) + + const setApplying = useCallback((uri: URI | undefined) => { + _applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri ?? undefined + setStreamState(getStreamState()) + }, [setStreamState, getStreamState, applyBoxId]) + // listen for stream updates on this box useCommandBarURIListener(useCallback((uri_) => { - const shouldUpdate = ( - getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath - || (uri !== 'current' && uri.fsPath === uri_.fsPath) - ) - if (shouldUpdate) { - rerender(c => c + 1) + const uri = getUriBeingApplied(applyBoxId) + if (uri?.fsPath === uri_.fsPath) { + setStreamState(getStreamState()) } - }, [applyBoxId, uri])) - - const currStreamState = getStreamState() + }, [setStreamState, applyBoxId, getStreamState])) - return { - getStreamState, - isDisabled, - currStreamState, - } + return { currStreamStateRef, setApplying } } @@ -202,10 +196,24 @@ const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'data-tooltip-offset': offset, }) +export const useEditToolStreamState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI }) => { + const accessor = useAccessor() + const voidCommandBarService = accessor.get('IVoidCommandBarService') + const [streamState, setStreamState] = useState(voidCommandBarService.getStreamState(uri)) + // listen for stream updates on this box + useCommandBarURIListener(useCallback((uri_) => { + const shouldUpdate = uri.fsPath === uri_.fsPath + if (shouldUpdate) { setStreamState(voidCommandBarService.getStreamState(uri)) } + }, [voidCommandBarService, applyBoxId, uri])) + + return { streamState, } +} export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes) => { - const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) + const { currStreamStateRef } = useApplyStreamState({ applyBoxId }) + const currStreamState = currStreamStateRef.current + const color = ( currStreamState === 'idle-no-changes' ? 'dark' : @@ -231,74 +239,88 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: } -export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { +export const ApplyButtonsHTML = ({ + codeStr, + applyBoxId, + uri, +}: { + codeStr: string, + applyBoxId: string, +} & ({ + uri: URI | 'current'; +}) +) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const { - currStreamState, - isDisabled, - getStreamState, - } = useApplyButtonState({ applyBoxId, uri }) + const settingsState = useSettingsState() + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId + + const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId }) + const onClickSubmit = useCallback(async () => { - if (isDisabled) return - if (getStreamState() === 'streaming') return - const opts = { + if (currStreamStateRef.current === 'streaming') return + + await editCodeService.callBeforeApplyOrEdit(uri) + + const [newApplyingUri, applyDonePromise] = editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, uri: uri, startBehavior: 'reject-conflicts', - } as const - - await editCodeService.callBeforeStartApplying(opts) - const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? [] + }) ?? [] + console.log('setting!!!', newApplyingUri) + setApplying(newApplyingUri) // catch any errors by interrupting the stream applyDonePromise?.catch(e => { const uri = getUriBeingApplied(applyBoxId) if (uri) editCodeService.interruptURIStreaming({ uri: uri }) }) - - applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined - - // rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService]) + + }, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService]) - const onInterrupt = useCallback(() => { - if (getStreamState() !== 'streaming') return + const onClickStop = useCallback(() => { + if (currStreamStateRef.current !== 'streaming') return const uri = getUriBeingApplied(applyBoxId) if (!uri) return editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [getStreamState, applyBoxId, editCodeService, metricsService]) + }, [currStreamStateRef, applyBoxId, editCodeService, metricsService]) const onAccept = useCallback(() => { const uri = getUriBeingApplied(applyBoxId) - if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) - }, [applyBoxId, editCodeService]) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'accept', removeCtrlKs: false }) + }, [uri, applyBoxId, editCodeService]) const onReject = useCallback(() => { const uri = getUriBeingApplied(applyBoxId) - if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) - }, [applyBoxId, editCodeService]) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false }) + }, [uri, applyBoxId, editCodeService]) + + + const currStreamState = currStreamStateRef.current + console.log('currStreamState...', currStreamState) if (currStreamState === 'streaming') { return } - if (currStreamState === 'idle-no-changes') { + if (isDisabled) { + return null + } + + if (currStreamState === 'idle-no-changes') { return + + + + } +} + + + + + +export const EditToolButtonsHTML = ({ + codeStr, + applyBoxId, + uri, + type, +}: { + codeStr: string, + applyBoxId: string, +} & ({ + uri: URI, + type: 'edit_file' | 'rewrite_file' +}) +) => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + const { streamState } = useEditToolStreamState({ applyBoxId, uri }) + const settingsState = useSettingsState() + + const isDisabled = !!isFeatureNameDisabled('Chat', settingsState) || !applyBoxId + + const onClickSubmit = useCallback(async () => { + await editCodeService.callBeforeApplyOrEdit(uri) + if (type === 'edit_file') { + editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks: codeStr }) + } + else if (type === 'rewrite_file') { + editCodeService.instantlyRewriteFile({ uri, newContent: codeStr }) + } + }, [type, editCodeService, codeStr, uri, applyBoxId, metricsService]) + + const onAccept = useCallback(() => { + editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + }, [uri, applyBoxId, editCodeService]) + + const onReject = useCallback(() => { + editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + }, [uri, applyBoxId, editCodeService]) + + if (isDisabled) return null + + if (streamState === 'idle-no-changes') { + return + } + + if (streamState === 'idle-has-changes') { return <> { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) + const { currStreamStateRef } = useApplyStreamState({ applyBoxId }) + const currStreamState = currStreamStateRef.current const name = uri !== 'current' ? 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 191b7212..30762d89 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 @@ -71,7 +71,7 @@ export const QuickEditChat = ({ startBehavior: 'keep-conflicts', } as const - await editCodeService.callBeforeStartApplying(opts) + await editCodeService.callBeforeApplyOrEdit(opts) const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? [] // catch any errors by interrupting the stream applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) }) 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 47ea83c0..10c44be3 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 @@ -23,7 +23,7 @@ import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../co import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js'; -import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; +import { CopyButton, EditToolButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js'; @@ -849,7 +849,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters // add children @@ -1620,14 +1621,13 @@ const BottomChildren = ({ children, title }: { children: React.ReactNode, title: } -const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => { - const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) +const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName }: { applyBoxId: string, uri: URI, codeStr: string, toolName: 'edit_file' | 'rewrite_file' }) => { + const { streamState } = useEditToolStreamState({ applyBoxId, uri }) return
- - - {currStreamState === 'idle-no-changes' && } + {streamState === 'idle-no-changes' && } +
} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx index d76f131a..75dcea8d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/helpers.tsx @@ -9,10 +9,13 @@ type ReturnType = [ // use this if state might be too slow to catch export const useRefState = (initVal: T): ReturnType => { - const [_, _setState] = useState(false) + // this actually makes a difference being an int, not a boolean. + // if it's a boolean and changes happen to fast, it goes with old values and leads to *very* weird bugs (like returning JSX, but not actually rendering it) + const [_s, _setState] = useState(0) + const ref = useRef(initVal) const setState = useCallback((newVal: T) => { - _setState(n => !n) // call rerender + _setState(n => n + 1) // call rerender ref.current = newVal }, []) return [ref, setState] diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ad6944d9..bba085d8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -400,7 +400,8 @@ export class ToolsService implements IToolsService { if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } - editCodeService.instantlyApplyNewContent({ uri, newContent }) + await editCodeService.callBeforeApplyOrEdit(uri) + editCodeService.instantlyRewriteFile({ uri, newContent }) // at end, get lint errors const lintErrorsPromise = Promise.resolve().then(async () => { await timeout(2000) @@ -415,6 +416,7 @@ export class ToolsService implements IToolsService { if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } + await editCodeService.callBeforeApplyOrEdit(uri) editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }) // at end, get lint errors