improve Apply state and misc other improvements for Apply

This commit is contained in:
Andrew Pareles 2025-04-02 00:25:23 -07:00
parent a94ce5d474
commit 7e8af9c2ef
9 changed files with 231 additions and 199 deletions

View file

@ -40,7 +40,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
@ -252,9 +252,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>();
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event;
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
// ctrlKZone: [uri], isStreaming // listen on change streaming
@ -994,7 +994,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (diffArea?.type !== 'DiffZone') continue
// fire changed diffs (this is the only place Diffs are added)
if (!diffArea._streamState.isStreaming) {
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid })
}
}
}
@ -1160,29 +1160,50 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
// SR
if (opts.from === 'ClickApply') {
const uri = this._uriOfGivenURI(opts.uri)
if (!uri) return
return uri
}
else if (opts.from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone?.type !== 'CtrlKZone') return
const { _URI: uri } = ctrlKZone
return uri
}
return
}
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
}
// the applyDonePromise this returns can reject, and should be caught with .catch
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
let res: [DiffZone, Promise<void>] | undefined = undefined
if (opts.from === 'QuickEdit') {
res = await this._initializeWriteoverStream(opts) // rewrite
res = this._initializeWriteoverStream(opts) // rewrite
}
else if (opts.from === 'ClickApply') {
if (this._settingsService.state.globalSettings.enableFastApply) {
const numCharsInFile = this._fileLengthOfGivenURI(opts.uri)
if (numCharsInFile === null) return null
if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files)
res = await this._initializeWriteoverStream(opts)
res = this._initializeWriteoverStream(opts)
}
else {
res = await this._initializeSearchAndReplaceStream(opts) // fast apply
res = this._initializeSearchAndReplaceStream(opts) // fast apply
}
}
else {
res = await this._initializeWriteoverStream(opts) // rewrite
res = this._initializeWriteoverStream(opts) // rewrite
}
}
@ -1278,6 +1299,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
_removeStylesFns: new Set(),
}
console.log('FIRING START STREAMING IN DIFFZONE!!!')
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
@ -1308,19 +1330,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | undefined> {
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
const { from, } = opts
let uri: URI
let startRange: 'fullFile' | [number, number]
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
let startRange: 'fullFile' | [number, number]
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
if (from === 'ClickApply') {
const uri_ = this._uriOfGivenURI(opts.uri)
if (!uri_) return
uri = uri_
startRange = 'fullFile'
}
else if (from === 'QuickEdit') {
@ -1328,15 +1348,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone?.type !== 'CtrlKZone') return
ctrlKZoneIfQuickEdit = ctrlKZone
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
uri = _URI
const { startLine: startLine_, endLine: endLine_ } = ctrlKZone
startRange = [startLine_, endLine_]
}
else {
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
@ -1530,13 +1548,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | undefined> {
const { from, applyStr, uri: givenURI, } = opts
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const uri = this._uriOfGivenURI(givenURI)
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
const { model } = this._voidModelService.getModel(uri)
if (!model) return

View file

@ -13,7 +13,15 @@ import { Diff, DiffArea } from './editCodeService.js';
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
export type StartApplyingOpts = ({
export type CallBeforeStartApplyingOpts = {
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
} | {
from: 'ClickApply';
uri: 'current' | URI;
}
export type StartApplyingOpts = {
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
startBehavior: StartBehavior;
@ -22,7 +30,7 @@ export type StartApplyingOpts = ({
applyStr: string;
uri: 'current' | URI;
startBehavior: StartBehavior;
})
}
@ -37,7 +45,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
@ -49,7 +58,7 @@ export interface IEditCodeService {
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;

View file

@ -3,10 +3,9 @@ import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsS
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
enum CopyButtonText {
Idle = 'Copy',
@ -64,9 +63,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro
// </button>
// )
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
const CopyButton = ({ codeStr }: { codeStr: string }) => {
export const CopyButton = ({ codeStr }: { codeStr: string }) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
@ -94,11 +93,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
}
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
@ -113,164 +107,78 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
}}
/>
)
return jumpToFileButton
}
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
return (
<IconShell1
Icon={Terminal}
onClick={onClick}
className="text-void-fg-1"
/>
)
}
// 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 getUriBeingApplied = (applyBoxId: string) => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const metricsService = accessor.get('IMetricsService')
const [_, rerender] = useState(0)
const getUriBeingApplied = useCallback(() => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}, [applyBoxId])
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied()
const uri = getUriBeingApplied(applyBoxId)
console.log('uri',uri?.fsPath)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, getUriBeingApplied])
}, [voidCommandBarService, applyBoxId])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
)
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'keep-conflicts',
}) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
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])
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const uri = getUriBeingApplied()
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReapply = useCallback(() => {
onReject()
onClickSubmit()
}, [onReject, onClickSubmit])
if (shouldUpdate) {
rerender(c => c + 1)
console.log('rerendering....')
}
}, [applyBoxId, applyBoxId, uri]))
const currStreamState = getStreamState()
console.log('curr stream state', currStreamState)
const copyButton = (
<CopyButton codeStr={codeStr} />
)
const playButton = (
<IconShell1
Icon={Play}
onClick={onClickSubmit}
/>
)
const stopButton = (
<IconShell1
Icon={Square}
onClick={onInterrupt}
/>
)
const reapplyButton = (
<IconShell1
Icon={RotateCw}
onClick={onReapply}
/>
)
const acceptButton = (
<IconShell1
Icon={Check}
onClick={onAccept}
className="text-green-600"
/>
)
const rejectButton = (
<IconShell1
Icon={X}
onClick={onReject}
className="text-red-600"
/>
)
let buttonsHTML = <></>
if (currStreamState === 'streaming') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{stopButton}
</>
return {
getStreamState,
isDisabled,
currStreamState,
}
}
if (currStreamState === 'idle-no-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{playButton}
</>
}
if (currStreamState === 'idle-has-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{reapplyButton}
{rejectButton}
{acceptButton}
</>
}
export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const statusIndicatorHTML = <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
return <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
<div
className={` size-1.5 rounded-full border
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
@ -278,18 +186,97 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
}
/>
</div>
}
return {
statusIndicatorHTML,
buttonsHTML,
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 onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const opts = {
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'reject-conflicts',
} as const
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
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])
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [getStreamState, applyBoxId, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
// const onReapply = useCallback(() => {
// onReject()
// onClickSubmit()
// }, [onReject, onClickSubmit])
if (currStreamState === 'streaming') {
return <IconShell1 Icon={Square} onClick={onInterrupt} />
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1 Icon={Play} onClick={onClickSubmit} />
}
if (currStreamState === 'idle-has-changes') {
return <>
{/* <IconShell1
Icon={RotateCw}
onClick={onReapply}
/> */}
<IconShell1
Icon={X}
onClick={onReject}
className="text-red-600"
/>
<IconShell1
Icon={Check}
onClick={onAccept}
className="text-green-600"
/>
</>
}
}
export const BlockCodeApplyWrapper = ({
children,
initValue,
@ -305,10 +292,10 @@ export const BlockCodeApplyWrapper = ({
language: string;
uri: URI | 'current',
}) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const name = uri !== 'current' ?
<ListableToolItem
@ -324,13 +311,15 @@ export const BlockCodeApplyWrapper = ({
{/* header */}
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
<div className="flex items-center">
{statusIndicatorHTML}
<StatusIndicatorHTML uri={uri} applyBoxId={applyBoxId} />
<span className="text-[13px] font-light text-void-fg-3">
{name}
</span>
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
{buttonsHTML}
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
</div>
</div>

View file

@ -63,11 +63,14 @@ export const QuickEditChat = ({
if (isStreamingRef.current) return
textAreaFnsRef.current?.disable()
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
const opts = {
from: 'QuickEdit',
diffareaid,
startBehavior: 'keep-conflicts',
}) ?? []
} as const
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })

View file

@ -28,7 +28,7 @@ import { getModelCapabilities, getIsResoningEnabledState } from '../../../../com
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
@ -733,7 +733,7 @@ const ToolHeaderWrapper = ({
{/* left */}
<div className={`flex items-center gap-x-2 min-w-0 overflow-hidden ${isClickable ? 'hover:brightness-125 transition-all duration-150' : ''}`}>
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
<span className="text-void-fg-4 text-xs italic truncate leading-[1]">{desc1}</span>
<span className="text-void-fg-4 text-xs italic truncate">{desc1}</span>
</div>
{/* right */}
@ -1197,7 +1197,7 @@ const titleOfToolName = {
running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`)
},
'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') },
'terminal_command': { done: `Ran terminal command`, proposed: 'Run terminal command', running: loadingTitleWrapper('Running terminal command') }
'terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
@ -1345,13 +1345,6 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
</div>
}
const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDescription: string, applyBoxId: string, uri: URI }) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: changeDescription, applyBoxId, uri })
return <div className='flex items-center gap-1'>
{statusIndicatorHTML}
{buttonsHTML}
</div>
}
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
@ -1362,6 +1355,15 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
</div>
}
const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
return <div className='flex items-center gap-1'>
<StatusIndicatorHTML applyBoxId={applyBoxId} uri={uri} />
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} />}
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} />
</div>
}
type ToolRequestState = 'awaiting_user' | 'running'
@ -1682,10 +1684,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolApplyButton
changeDescription={params.changeDescription}
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={params.changeDescription}
/>
}
@ -1764,6 +1767,10 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const { command } = params
const { terminalId, resolveReason, result } = value
componentParams.desc2 = <JumpToTerminalButton
onClick={() => { terminalToolsService.openTerminal(terminalId) }}
/>
const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null)
: resolveReason.type === 'bgtask' ? null :
resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` :
@ -2052,7 +2059,7 @@ export const SidebarChat = () => {
const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed
const currStreamingToolHTML = toolIsLoading ?
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={toolTitle} desc1={<span className='flex items-center'>Getting parameters<IconLoading /></span>} />
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={toolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
: null
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML]

View file

@ -353,12 +353,15 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`)
}
const res = await editCodeService.startApplying({
const opts = {
uri,
applyStr: changeDescription,
from: 'ClickApply',
startBehavior: 'keep-conflicts',
})
} as const
await editCodeService.callBeforeStartApplying(opts)
const res = editCodeService.startApplying(opts)
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
const [diffZoneURI, applyDonePromise] = res

View file

@ -173,7 +173,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
}
}))
this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => {
this._register(this._editCodeService.onDidChangeDiffsInDiffZoneNotStreaming(e => {
for (const uri of this._listenToTheseURIs) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change

View file

@ -14,7 +14,7 @@ export type ToolMessage<T extends ToolName> = {
result:
| { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], }
| { type: 'error'; params: ToolCallParams[T] | undefined; value: string }
| { type: 'rejected'; params: ToolCallParams[T] }
| { type: 'rejected'; params: ToolCallParams[T] } // user rejected
}
export type ToolRequestApproval<T extends ToolName> = {
role: 'tool_request';

View file

@ -42,10 +42,14 @@ ${tripleTick[1]}`
// ======================================================== tools ========================================================
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
const uriParam = (object: string) => ({
uri: { type: 'string', description: `The FULL path to the ${object}.` }
})
export const voidTools = {
// --- context-gathering (read/search/list) ---
@ -53,16 +57,16 @@ export const voidTools = {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('file'),
...paginationHelper.param,
},
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('folder'),
...paginationHelper.param,
},
},
@ -91,7 +95,7 @@ export const voidTools = {
name: 'create_uri',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('file or folder'),
},
},
@ -99,7 +103,7 @@ export const voidTools = {
name: 'delete_uri',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('file or folder'),
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
},
@ -108,7 +112,7 @@ export const voidTools = {
name: 'edit',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
...uriParam('file'),
changeDescription: {
type: 'string', description: `\
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.