Merge pull request #474 from voideditor/model-selection

Accept|Reject on Edit tool fix
This commit is contained in:
Andrew Pareles 2025-05-08 12:07:01 -07:00 committed by GitHub
commit 301418bd12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 310 additions and 158 deletions

View file

@ -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,

View file

@ -44,10 +44,10 @@ export interface IEditCodeService {
processRawKeybindingText(keybindingStr: string): string;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
callBeforeApplyOrEdit(uri: URI | 'current'): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | 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;

View file

@ -35,6 +35,7 @@ const extensionBlacklist = [
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
'supermaven.supermaven' // supermaven
// 'github.copilot',
];

View file

@ -3,14 +3,14 @@
* 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'
import { URI } from '../../../../../../../base/common/uri.js'
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 { getBasename, ListableToolItem, voidOpenFileFn, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
@ -24,8 +24,9 @@ type IconButtonProps = {
Icon: LucideIcon
}
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
@ -34,19 +35,19 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
}}
// border border-void-border-1 rounded
className={`
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
)
}
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
@ -108,7 +109,7 @@ export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & R
<IconShell1
Icon={FileSymlink}
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
voidOpenFileFn(uri, accessor)
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
@ -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<HTMLDivElement>) => {
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 <IconShell1
Icon={Square}
onClick={onInterrupt}
onClick={onClickStop}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (currStreamState === 'idle-no-changes') {
if (isDisabled) {
return null
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
@ -307,6 +329,76 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string
}
if (currStreamState === 'idle-has-changes') {
return <Fragment>
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</Fragment>
}
}
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 <IconShell1
Icon={RotateCw}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Reapply' })}
/>
}
if (streamState === 'idle-has-changes') {
return <>
<IconShell1
Icon={X}
@ -340,7 +432,8 @@ export const BlockCodeApplyWrapper = ({
}) => {
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' ?
@ -348,7 +441,7 @@ export const BlockCodeApplyWrapper = ({
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
onClick={() => { voidOpenFileFn(uri, accessor) }}
/>
: <span>{language}</span>

View file

@ -9,12 +9,12 @@ import { marked, MarkedToken, Token } from 'marked'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
import { voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
export type ChatMessageLocation = {
@ -134,27 +134,10 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const onClick = () => {
if (!link || !link.selection) return;
if (!link) return;
const selection = link.selection
// open the file
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {
if (!selection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(selection)
editor.revealRange(selection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
// Use the updated voidOpenFileFn to open the file and handle selection
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
}
return <Codespan

View file

@ -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 }) })

View file

@ -7,6 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
@ -18,12 +19,13 @@ import { PastThreadsList } from './SidebarThreadSelector.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
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';
@ -530,6 +532,49 @@ export const getBasename = (pathStr: string, parts: number = 1) => {
}
// Open file utility function
export const voidOpenFileFn = (
uri: URI,
accessor: ReturnType<typeof useAccessor>,
range?: [number, number]
) => {
const commandService = accessor.get('ICommandService')
const editorService = accessor.get('ICodeEditorService')
// Get editor selection from CodeSelection range
let editorSelection = undefined;
// If we have a selection, create an editor selection from the range
if (range) {
editorSelection = {
startLineNumber: range[0],
startColumn: 1,
endLineNumber: range[1],
endColumn: Number.MAX_SAFE_INTEGER,
};
}
// open the file
commandService.executeCommand('vscode.open', uri).then(() => {
// select the text
setTimeout(() => {
if (!editorSelection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(editorSelection)
editor.revealRange(editorSelection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
};
export const SelectedFiles = (
{ type, selections, setSelections, showProspectiveSelections, messageIdx, }:
| { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, }
@ -640,11 +685,7 @@ export const SelectedFiles = (
setSelections([...selections, selection])
}
else if (selection.type === 'File') { // open files
commandService.executeCommand('vscode.open', selection.uri, {
preview: true,
// preserveFocus: false,
});
voidOpenFileFn(selection.uri, accessor);
const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile
if (wasAddedAsCurrentFile) {
@ -658,10 +699,7 @@ export const SelectedFiles = (
}
}
else if (selection.type === 'CodeSelection') {
commandService.executeCommand('vscode.open', selection.uri, {
preview: true,
// TODO!!! open in range
});
voidOpenFileFn(selection.uri, accessor, selection.range);
}
else if (selection.type === 'Folder') {
// TODO!!! reveal in tree
@ -714,6 +752,7 @@ type ToolHeaderParams = {
icon?: React.ReactNode;
title: React.ReactNode;
desc1: React.ReactNode;
desc1OnClick?: () => void;
desc2?: React.ReactNode;
isError?: boolean;
info?: string;
@ -733,6 +772,7 @@ const ToolHeaderWrapper = ({
icon,
title,
desc1,
desc1OnClick,
desc1Info,
desc2,
numResults,
@ -754,37 +794,51 @@ const ToolHeaderWrapper = ({
const isDropdown = children !== undefined // null ALLOWS dropdown
const isClickable = !!(isDropdown || onClick)
const isDesc1Clickable = !!desc1OnClick
const desc1HTML = <span
className={`text-void-fg-4 text-xs italic truncate ml-2
${isDesc1Clickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''}
`}
onClick={desc1OnClick}
{...desc1Info ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': desc1Info,
'data-tooltip-place': 'top',
'data-tooltip-delay-show': 1000,
} : {}}
>{desc1}</span>
return (<div className=''>
<div className={`w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ${className}`}>
{/* header */}
<div className={`select-none flex items-center min-h-[24px]`}>
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
{/* left */}
<div className={`
ml-1
<div // container for if desc1 is clickable
className='ml-1 flex items-center overflow-hidden'
>
{/* title eg "> Edited File" */}
<div className={`
flex items-center min-w-0 overflow-hidden grow
${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''}
`}
onClick={() => {
if (isDropdown) { setIsOpen(v => !v); }
if (onClick) { onClick(); }
}}
>
{isDropdown && (<ChevronRight
className={`
onClick={() => {
if (isDropdown) { setIsOpen(v => !v); }
if (onClick) { onClick(); }
}}
>
{isDropdown && (<ChevronRight
className={`
text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)]
${isExpanded ? 'rotate-90' : ''}
`}
/>)}
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
<span className="text-void-fg-4 text-xs italic truncate ml-2"
{...desc1Info ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': desc1Info,
'data-tooltip-place': 'top',
'data-tooltip-delay-show': 1000,
} : {}}
>{desc1}</span>
/>)}
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
{!isDesc1Clickable && desc1HTML}
</div>
{isDesc1Clickable && desc1HTML}
</div>
{/* right */}
@ -849,8 +903,9 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
const icon = null
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
const { rawParams, params, name } = toolMessage
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
@ -859,7 +914,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
code={content}
/>
</ToolChildrenWrapper>
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
// JumpToFileButton removed in favor of FileLinkText
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
// add apply box
@ -872,6 +927,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
toolName={name}
/>
// add children
@ -1364,12 +1420,12 @@ const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'ty
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
desc1: string,
desc1: React.ReactNode,
desc1Info?: string,
} => {
if (!_toolParams) {
return { desc1: '' };
return { desc1: '', };
}
const x = {
@ -1620,14 +1676,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 <div className='flex items-center gap-1'>
<StatusIndicatorForApplyButton applyBoxId={applyBoxId} uri={uri} />
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
{streamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<EditToolButtonsHTML type={toolName} codeStr={codeStr} applyBoxId={applyBoxId} uri={uri} />
</div>
}
@ -1781,16 +1836,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
let range: [number, number] | undefined = undefined
if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) {
const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}`
const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}`
const addStr = `(${start}-${end})`
componentParams.desc1 += ` ${addStr}`
range = [params.startLine || 1, params.endLine || 1]
}
if (toolMessage.type === 'success') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor, range) }
if (result.hasNextPage && params.pageNumber === 1) // first page
componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`
else if (params.pageNumber > 1) // subsequent pages
@ -1798,7 +1855,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
// JumpToFileButton removed in favor of FileLinkText
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
@ -1889,7 +1946,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
name={`${child.name}${child.isDirectory ? '/' : ''}`}
className='w-full overflow-auto'
onClick={() => {
commandService.executeCommand('vscode.open', child.uri, { preview: true })
voidOpenFileFn(child.uri, accessor)
// commandService.executeCommand('workbench.view.explorer'); // open in explorer folders view instead
// explorerService.select(child.uri, true);
}}
@ -1940,7 +1997,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
{result.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
className='w-full overflow-auto'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
onClick={() => { voidOpenFileFn(uri, accessor) }}
/>))}
{result.hasNextPage &&
<ListableToolItem name={'Results truncated.'} isSmall={true} className='w-full overflow-auto' />
@ -1995,7 +2052,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
{result.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
className='w-full overflow-auto'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
onClick={() => { voidOpenFileFn(uri, accessor) }}
/>))}
{result.hasNextPage &&
<ListableToolItem name={`Results truncated.`} isSmall={true} className='w-full overflow-auto' />
@ -2085,7 +2142,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type === 'success') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
if (result.lintErrors)
componentParams.children = <LintErrorChildren lintErrors={result.lintErrors} />
else
@ -2094,7 +2151,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
// JumpToFileButton removed in favor of FileLinkText
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
@ -2126,14 +2183,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type === 'success') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
else if (toolMessage.type === 'rejected') {
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
if (params) { componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } }
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
@ -2168,14 +2225,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
if (toolMessage.type === 'success') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
else if (toolMessage.type === 'rejected') {
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
if (params) { componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } }
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
@ -2184,11 +2241,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
else if (toolMessage.type === 'running_now') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
else if (toolMessage.type === 'tool_request') {
const { result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) }
}
return <ToolHeaderWrapper {...componentParams} />
@ -2558,7 +2615,7 @@ const CommandBarInChat = () => {
const fileNameHTML = <div
className="flex items-center gap-1.5 text-void-fg-3 hover:brightness-125 transition-all duration-200 cursor-pointer"
onClick={() => commandService.executeCommand('vscode.open', uri, { preview: true })}
onClick={() => voidOpenFileFn(uri, accessor)}
>
{/* <FileIcon size={14} className="text-void-fg-3" /> */}
<span className="text-void-fg-3">{basename}</span>
@ -2683,6 +2740,9 @@ const CommandBarInChat = () => {
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
const accessor = useAccessor()
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
const title = titleOfToolName[toolCallSoFar.name].proposed
@ -2695,11 +2755,13 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
<IconLoading />
</span>
const desc1OnClick = () => { uri && voidOpenFileFn(uri, accessor) }
// If URI has not been specified
return <ToolHeaderWrapper
title={title}
desc1={desc1}
desc2={uri && <JumpToFileButton uri={uri} />}
desc1OnClick={desc1OnClick}
>
<EditToolChildren
uri={uri}

View file

@ -220,13 +220,14 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
<span>{`(${numMessages})`}</span>
<span className='opacity-60'>{numMessages}</span>
{` `}
{formatDate(new Date(pastThread.lastModified))}
{/* {` messages `} */}
</span>
return <div
@ -249,8 +250,13 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
<span className="truncate overflow-hidden text-ellipsis"
data-tooltip-id='void-tooltip'
data-tooltip-content={numMessages + ' messages'}
data-tooltip-place='top'
>{firstMsg}</span>
{/* <span className='opacity-60'>{`(${numMessages})`}</span> */}
</span>
<div className="flex items-center gap-x-1 opacity-60">

View file

@ -9,10 +9,13 @@ type ReturnType<T> = [
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
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<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => !n) // call rerender
_setState(n => n + 1) // call rerender
ref.current = newVal
}, [])
return [ref, setState]

View file

@ -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

View file

@ -463,7 +463,7 @@ ${directoryStr}
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
details.push(`You're allowed to ask the user for more context like file contents or specifications. If this comes up, tell them to reference files and folders by typing @.`)
}
if (mode === 'agent') {

View file

@ -23,6 +23,8 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
'edit_file': 'edits',
'run_command': 'terminal',
'run_persistent_command': 'terminal',
'open_persistent_terminal': 'terminal',
'kill_persistent_terminal': 'terminal',
}