mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
improve Apply state and misc other improvements for Apply
This commit is contained in:
parent
a94ce5d474
commit
7e8af9c2ef
9 changed files with 231 additions and 199 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) })
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue