make apply actually work

This commit is contained in:
Mathew Pareles 2025-03-12 00:01:52 -07:00
parent d6d5f77183
commit 967f7dc85e
5 changed files with 280 additions and 122 deletions

View file

@ -3,6 +3,8 @@ import { useAccessor, useURIStreamState, useSettingsState } from '../util/servic
import { useRefState } from '../util/helpers.js' import { useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js' import { URI } from '../../../../../../../base/common/uri.js'
import { LucideIcon, RotateCw } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
enum CopyButtonText { enum CopyButtonText {
Idle = 'Copy', Idle = 'Copy',
@ -10,6 +12,53 @@ enum CopyButtonText {
Error = 'Could not copy', Error = 'Could not copy',
} }
type IconButtonProps = {
onClick: () => void
title: string
Icon: LucideIcon
disabled?: boolean
className?: string
}
export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
<button
title={title}
disabled={disabled}
onClick={onClick}
className={`
size-6
flex items-center justify-center
text-sm bg-void-bg-3 text-void-fg-1
hover:brightness-110
border border-void-border-1 rounded
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
>
<Icon size={14} />
</button>
)
export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
<button
title={title}
disabled={disabled}
onClick={onClick}
className={`
size-6
flex items-center justify-center
text-sm
hover:opacity-80
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
>
<Icon size={14} />
</button>
)
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CopyButton = ({ codeStr }: { codeStr: string }) => { const CopyButton = ({ codeStr }: { codeStr: string }) => {
@ -26,7 +75,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
}, COPY_FEEDBACK_TIMEOUT) }, COPY_FEEDBACK_TIMEOUT)
}, [copyButtonText]) }, [copyButtonText])
const onCopy = useCallback(() => { const onCopy = useCallback(() => {
clipboardService.writeText(codeStr) clipboardService.writeText(codeStr)
.then(() => { setCopyButtonText(CopyButtonText.Copied) }) .then(() => { setCopyButtonText(CopyButtonText.Copied) })
@ -34,26 +82,20 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
}, [metricsService, clipboardService, codeStr, setCopyButtonText]) }, [metricsService, clipboardService, codeStr, setCopyButtonText])
const isSingleLine = false //!codeStr.includes('\n') return <IconShell1
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
return <button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
onClick={onCopy} onClick={onCopy}
> title={copyButtonText}
{copyButtonText} />
</button>
} }
// state persisted for duration of react only // 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: {} }
export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
const settingsState = useSettingsState() const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
@ -64,21 +106,21 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
const [_, rerender] = useState(0) const [_, rerender] = useState(0)
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri]) const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied])
// listen for stream updates // listen for stream updates
useURIStreamState( useURIStreamState(
useCallback((uri, newStreamState) => { useCallback((uri, newStreamState) => {
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath const shouldUpdate = getUriBeingApplied()?.fsPath === uri.fsPath
if (shouldUpdate) return if (!shouldUpdate) return
rerender(c => c + 1) rerender(c => c + 1)
}, [applyBoxId, editCodeService, applyingUri]) }, [applyBoxId, editCodeService, getUriBeingApplied])
) )
const onSubmit = useCallback(() => { const onSubmit = useCallback(() => {
if (isDisabled) return if (isDisabled) return
if (streamState() === 'streaming') return if (getStreamState() === 'streaming') return
const [newApplyingUri, _] = editCodeService.startApplying({ const [newApplyingUri, _] = editCodeService.startApplying({
from: 'ClickApply', from: 'ClickApply',
type: 'searchReplace', type: 'searchReplace',
@ -88,61 +130,122 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1) rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService]) }, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService])
const onInterrupt = useCallback(() => { const onInterrupt = useCallback(() => {
if (streamState() !== 'streaming') return if (getStreamState() !== 'streaming') return
const uri = applyingUri() const uri = getUriBeingApplied()
if (!uri) return if (!uri) return
editCodeService.interruptURIStreaming({ uri }) editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {}) metricsService.capture('Stop Apply', {})
}, [streamState, applyingUri, editCodeService, metricsService]) }, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReapply = useCallback(() => {
onReject()
onSubmit()
}, [onReject, onSubmit])
const currStreamState = getStreamState()
const copyButton = (
<CopyButton codeStr={codeStr} />
)
const playButton = (
<IconShell1
Icon={Play}
onClick={onSubmit}
title="Apply changes"
/>
)
const stopButton = (
<IconShell1
Icon={Square}
onClick={onInterrupt}
title="Stop applying"
/>
)
const reapplyButton = (
<IconShell1
Icon={RotateCw}
onClick={onReapply}
title="Reapply changes"
/>
)
const acceptButton = (
<IconShell1
Icon={Check}
onClick={onAccept}
title="Accept changes"
className="text-green-600"
/>
)
const rejectButton = (
<IconShell1
Icon={X}
onClick={onReject}
title="Reject changes"
className="text-red-600"
/>
)
const isSingleLine = false //!codeStr.includes('\n') let buttonsHTML = <></>
const applyButton = <button if (currStreamState === 'streaming') {
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`} buttonsHTML = <>
onClick={onSubmit} {stopButton}
> </>
Apply }
</button>
const stopButton = <button if (currStreamState === 'idle') {
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`} buttonsHTML = <>
onClick={onInterrupt} {copyButton}
> {playButton}
Stop </>
</button> }
const acceptRejectButtons = <> if (currStreamState === 'acceptRejectAll') {
<button buttonsHTML = <>
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`} {reapplyButton}
onClick={() => { {rejectButton}
const uri = applyingUri() {acceptButton}
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) </>
}} }
const statusIndicatorHTML = <div className='flex flex-row gap-2 items-center'>
<div
className={`size-1.5 rounded-full border
${currStreamState === 'idle' ? '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 === 'acceptRejectAll' ? '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'
}`
}
> >
Accept </div>
</button> </div>
<button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`} return {
onClick={() => { statusIndicatorHTML,
const uri = applyingUri() buttonsHTML
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }
}}
>
Reject
</button>
</>
const currStreamState = streamState()
return <>
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
{currStreamState === 'idle' && !isDisabled && applyButton}
{currStreamState === 'streaming' && stopButton}
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
</>
} }

View file

@ -3,17 +3,44 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------*/
import React from 'react';
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js';
export const BlockCodeWithApply = ({ initValue, language, applyBoxId }: { initValue: string, language?: string, applyBoxId: string }) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId })
return (
<div className="border border-void-border-3 rounded-sm overflow-hidden bg-void-bg-2">
<div className="flex justify-between items-center px-2 py-1 border-b border-void-border-3">
<div className="flex items-center gap-2">
<div className="text-sm opacity-50">{language || 'text'}</div>
{statusIndicatorHTML}
</div>
<div className="flex gap-1">
{buttonsHTML}
</div>
</div>
<BlockCode
initValue={initValue}
language={language}
/>
</div>
)
}
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => { export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => {
const isSingleLine = !codeEditorProps.initValue.includes('\n') const isSingleLine = !codeEditorProps.initValue.includes('\n')
return ( return (
<> <>
<div className="relative group w-full overflow-hidden"> <VoidCodeEditor {...codeEditorProps} />
{/* <div className="relative group w-full overflow-hidden">
{buttonsOnHover === null ? null : ( {buttonsOnHover === null ? null : (
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}> <div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}> <div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
@ -23,7 +50,8 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
)} )}
<VoidCodeEditor {...codeEditorProps} /> <VoidCodeEditor {...codeEditorProps} />
</div> </div> */}
</> </>
) )
} }

View file

@ -5,9 +5,9 @@
import React, { JSX, useState } from 'react' import React, { JSX, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked' import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js' import { BlockCode, BlockCodeWithApply } from './BlockCode.js'
import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js' import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js'
import { useAccessor, useChatThreadsState } from '../util/services.js' import { useAccessor, useChatThreadsState } from '../util/services.js'
import { Range } from '../../../../../../services/search/common/searchExtTypes.js' import { Range } from '../../../../../../services/search/common/searchExtTypes.js'
import { IRange } from '../../../../../../../base/common/range.js' import { IRange } from '../../../../../../../base/common/range.js'
@ -56,7 +56,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
if (link === undefined) { if (link === undefined) {
// generate link and add to cache // if no link, generate link and add to cache
(chatThreadService.generateCodespanLink(text) (chatThreadService.generateCodespanLink(text)
.then(link => { .then(link => {
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
@ -99,7 +99,9 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
/> />
} }
const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => {
// deal with built-in tokens first (assume marked token) // deal with built-in tokens first (assume marked token)
const t = token as MarkedToken const t = token as MarkedToken
@ -114,21 +116,29 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
if (t.type === "code") { if (t.type === "code") {
const applyBoxId = chatMessageLocation ? getApplyBoxId({ const language = t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
tokenIdx: tokenIdx,
}) : null
// TODO user should only be able to apply this when the code has been closed (t.raw ends with "```") // TODO user should only be able to apply this when the code has been closed (t.raw ends with "```")
return <div> if (options.isApplyEnabled && chatMessageLocation) {
<BlockCode
const applyBoxId = getApplyBoxId({
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
tokenIdx: tokenIdx,
})
return <BlockCodeWithApply
initValue={t.text} initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} language={language}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />} applyBoxId={applyBoxId}
/> />
</div> }
return <BlockCode
initValue={t.text}
language={language}
/>
} }
if (t.type === "heading") { if (t.type === "heading") {
@ -213,7 +223,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
return <li> return <li>
<input type="checkbox" checked={t.checked} readOnly /> <input type="checkbox" checked={t.checked} readOnly />
<span> <span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} nested={true} /> <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} nested={true} {...options} />
</span> </span>
</li> </li>
} }
@ -229,7 +239,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
<input type="checkbox" checked={item.checked} readOnly /> <input type="checkbox" checked={item.checked} readOnly />
)} )}
<span> <span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} /> <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} {...options} />
</span> </span>
</li> </li>
))} ))}
@ -244,6 +254,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
token={token} token={token}
tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components
chatMessageLocation={chatMessageLocation} chatMessageLocation={chatMessageLocation}
{...options}
/> />
))} ))}
</> </>
@ -304,12 +315,15 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
// inline code // inline code
if (t.type === "codespan") { if (t.type === "codespan") {
if (chatMessageLocation) { console.log('isLinkDetectionEnabled', options.isLinkDetectionEnabled)
if (options.isLinkDetectionEnabled && chatMessageLocation) {
return <CodespanWithLink return <CodespanWithLink
text={t.text} text={t.text}
rawText={t.raw} rawText={t.raw}
chatMessageLocation={chatMessageLocation} chatMessageLocation={chatMessageLocation}
/> />
} }
return <Codespan text={t.text} /> return <Codespan text={t.text} />
@ -331,12 +345,12 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
) )
} }
export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined }) => { export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation, ...options }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return ( return (
<> <>
{tokens.map((token, index) => ( {tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} /> <RenderToken key={index} token={token} nested={nested} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} {...options} />
))} ))}
</> </>
) )

View file

@ -714,7 +714,7 @@ const DropdownComponent = ({
// the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition. // the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition.
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`} className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`}
> >
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm"> <div className="text-xs text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{children} {children}
</div> </div>
</div> </div>
@ -971,6 +971,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
<ChatMarkdownRender <ChatMarkdownRender
string={reasoningStr} string={reasoningStr}
chatMessageLocation={chatMessageLocation} chatMessageLocation={chatMessageLocation}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/> />
</DropdownComponent>} </DropdownComponent>}
@ -978,6 +980,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
<ChatMarkdownRender <ChatMarkdownRender
string={chatMessage.content || ''} string={chatMessage.content || ''}
chatMessageLocation={chatMessageLocation} chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
/> />
{/* loading indicator */} {/* loading indicator */}
@ -1009,7 +1013,7 @@ const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: strin
</span> </span>
} }
> >
<div className='text-xs text-wrap whitespace-pre-wrap break-all break-words'>{errorMessage}</div> <div className='text-wrap whitespace-pre-wrap break-all break-words'>{errorMessage}</div>
</DropdownComponent> </DropdownComponent>
) )
@ -1028,34 +1032,34 @@ const toolNameToTitle: Record<ToolName, string> = {
} }
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
if (_toolParams === undefined) { if (!_toolParams) {
return ''; return '';
} }
if (toolName === 'read_file') { if (toolName === 'read_file') {
const toolParams = _toolParams as ToolCallParams['read_file'] const toolParams = _toolParams as ToolCallParams['read_file']
return toolParams ? getBasename(toolParams.uri.fsPath) : ''; return getBasename(toolParams.uri.fsPath);
} else if (toolName === 'list_dir') { } else if (toolName === 'list_dir') {
const toolParams = _toolParams as ToolCallParams['list_dir'] const toolParams = _toolParams as ToolCallParams['list_dir']
return toolParams ? `${getBasename(toolParams.rootURI.fsPath)}/` : ''; return `${getBasename(toolParams.rootURI.fsPath)}/`;
} else if (toolName === 'pathname_search') { } else if (toolName === 'pathname_search') {
const toolParams = _toolParams as ToolCallParams['pathname_search'] const toolParams = _toolParams as ToolCallParams['pathname_search']
return toolParams ? `"${toolParams.queryStr}"` : ''; return `"${toolParams.queryStr}"`;
} else if (toolName === 'search') { } else if (toolName === 'search') {
const toolParams = _toolParams as ToolCallParams['search'] const toolParams = _toolParams as ToolCallParams['search']
return toolParams ? `"${toolParams.queryStr}"` : ''; return `"${toolParams.queryStr}"`;
} else if (toolName === 'create_uri') { } else if (toolName === 'create_uri') {
const toolParams = _toolParams as ToolCallParams['create_uri'] const toolParams = _toolParams as ToolCallParams['create_uri']
return toolParams ? getBasename(toolParams.uri.fsPath) : ''; return getBasename(toolParams.uri.fsPath);
} else if (toolName === 'delete_uri') { } else if (toolName === 'delete_uri') {
const toolParams = _toolParams as ToolCallParams['delete_uri'] const toolParams = _toolParams as ToolCallParams['delete_uri']
return toolParams ? getBasename(toolParams.uri.fsPath) + ' (deleted)' : ''; return getBasename(toolParams.uri.fsPath) + ' (deleted)';
} else if (toolName === 'edit') { } else if (toolName === 'edit') {
const toolParams = _toolParams as ToolCallParams['edit'] const toolParams = _toolParams as ToolCallParams['edit']
return toolParams ? getBasename(toolParams.uri.fsPath) : ''; return getBasename(toolParams.uri.fsPath);
} else if (toolName === 'terminal_command') { } else if (toolName === 'terminal_command') {
const toolParams = _toolParams as ToolCallParams['terminal_command'] const toolParams = _toolParams as ToolCallParams['terminal_command']
return toolParams ? `"${toolParams.command}"` : ''; return `"${toolParams.command}"`;
} else { } else {
return '' return ''
} }
@ -1063,13 +1067,22 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
const ToolRequestAcceptRejectButtons = ({ toolRequest }: { toolRequest: ToolRequestApproval<ToolName> }) => { const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast, }: { toolRequest: ToolRequestApproval<ToolName> } & Omit<ChatBubbleProps, 'chatMessage'>) => {
const accessor = useAccessor() const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService') const chatThreadsService = accessor.get('IChatThreadService')
return <>
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.approveTool(toolRequest.voidToolId) }}>Accept</div> const initRequestState = isLast ? 'awaiting_response' : 'rejected'
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.rejectTool(toolRequest.voidToolId) }}>Reject</div>
</> const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState)
if (requestState === 'awaiting_response') {
return <>
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.approveTool(toolRequest.voidToolId); setRequestState('accepted') }}>Accept</div>
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.rejectTool(toolRequest.voidToolId); setRequestState('rejected') }}>Reject</div>
</>
}
} }
const toolNameToComponent: { [T in ToolName]: { const toolNameToComponent: { [T in ToolName]: {
@ -1306,7 +1319,10 @@ const toolNameToComponent: { [T in ToolName]: {
return <DropdownComponent title={title} desc1={desc1} return <DropdownComponent title={title} desc1={desc1}
onClick={() => { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} onClick={() => { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }}
> >
<ChatMarkdownRender string={toolRequest.params.changeDescription} chatMessageLocation={undefined} /> <ChatMarkdownRender
string={toolRequest.params.changeDescription}
chatMessageLocation={undefined}
/>
</DropdownComponent> </DropdownComponent>
}, },
resultWrapper: ({ toolMessage }) => { resultWrapper: ({ toolMessage }) => {
@ -1412,10 +1428,10 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
else if (role === 'tool_request') { else if (role === 'tool_request') {
const isLastMessage = true // TODO!!! fix this const isLastMessage = true // TODO!!! fix this
if (!isLastMessage) return null if (!isLastMessage) return null
const ToolRequestComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
return <> return <>
<ToolRequestComponent toolRequest={chatMessage} /> <ToolRequestWrapper toolRequest={chatMessage} />
<ToolRequestAcceptRejectButtons toolRequest={chatMessage} /> <ToolRequestAcceptRejectButtons toolRequest={chatMessage} messageIdx={messageIdx} isLast={isLast} />
</> </>
} }
else if (role === 'tool') { else if (role === 'tool') {
@ -1423,8 +1439,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
const title = toolNameToTitle[chatMessage.name] const title = toolNameToTitle[chatMessage.name]
// if (chatMessage.result.type === 'error') return <ToolError title={title} params={chatMessage.result.params} errorMessage={chatMessage.result.value} /> // if (chatMessage.result.type === 'error') return <ToolError title={title} params={chatMessage.result.params} errorMessage={chatMessage.result.value} />
const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
return <ToolResultComponent toolMessage={chatMessage} /> return <ToolResultWrapper toolMessage={chatMessage} />
} }
@ -1524,19 +1540,19 @@ export const SidebarChat = () => {
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 }) scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
}, [isHistoryOpen, currentThread.id]) }, [isHistoryOpen, currentThread.id])
const numMessages = previousMessages.length + (isStreaming ? 1 : 0)
const pastMessagesHTML = useMemo(() => { const previousMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) => return previousMessages.map((message, i) =>
<ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} isLast={!isStreaming} /> <ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} isLast={i === numMessages - 1} />
) )
}, [previousMessages, currentThread]) }, [previousMessages, currentThread])
const streamingChatIdx = previousMessagesHTML.length
const streamingChatIdx = pastMessagesHTML.length
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)} <ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
messageIdx={streamingChatIdx} chatMessage={{ messageIdx={streamingChatIdx}
chatMessage={{
role: 'assistant', role: 'assistant',
content: messageSoFar ?? '', content: messageSoFar ?? '',
reasoning: reasoningSoFar ?? '', reasoning: reasoningSoFar ?? '',
@ -1546,8 +1562,7 @@ export const SidebarChat = () => {
isLast={true} isLast={true}
/> : null /> : null
const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML] const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]
const threadSelector = <div ref={historyRef} const threadSelector = <div ref={historyRef}
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`} className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
@ -1555,8 +1570,6 @@ export const SidebarChat = () => {
<SidebarThreadSelector /> <SidebarThreadSelector />
</div> </div>
const messagesHTML = <ScrollToBottomContainer const messagesHTML = <ScrollToBottomContainer
key={currentThread.id} // force rerender on all children if id changes key={currentThread.id} // force rerender on all children if id changes
scrollContainerRef={scrollContainerRef} scrollContainerRef={scrollContainerRef}
@ -1566,7 +1579,7 @@ export const SidebarChat = () => {
w-full h-auto w-full h-auto
overflow-x-hidden overflow-x-hidden
overflow-y-auto overflow-y-auto
${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} ${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
`} `}
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - (25) }} // the height of the previousMessages is determined by all other heights style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - (25) }} // the height of the previousMessages is determined by all other heights
> >
@ -1608,7 +1621,7 @@ export const SidebarChat = () => {
isStreaming={isStreaming} isStreaming={isStreaming}
isDisabled={isDisabled} isDisabled={isDisabled}
showSelections={true} showSelections={true}
showProspectiveSelections={pastMessagesHTML.length === 0} showProspectiveSelections={previousMessagesHTML.length === 0}
selections={selections} selections={selections}
setSelections={setSelections} setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }} onClickAnywhere={() => { textAreaRef.current?.focus() }}

View file

@ -39,12 +39,12 @@ export enum ThemeSettings {
} }
export enum ThemeSettingDefaults { export enum ThemeSettingDefaults {
COLOR_THEME_DARK = 'Default Dark+', COLOR_THEME_DARK = 'Default Dark+', // Void changed this from 'Default Dark Modern'
COLOR_THEME_LIGHT = 'Default Light Modern', COLOR_THEME_LIGHT = 'Default Light Modern',
COLOR_THEME_HC_DARK = 'Default High Contrast', COLOR_THEME_HC_DARK = 'Default High Contrast',
COLOR_THEME_HC_LIGHT = 'Default High Contrast Light', COLOR_THEME_HC_LIGHT = 'Default High Contrast Light',
COLOR_THEME_DARK_OLD = 'Default Dark Modern', COLOR_THEME_DARK_OLD = 'Default Dark Modern', // Void changed this from 'Default Dark+'
COLOR_THEME_LIGHT_OLD = 'Default Light+', COLOR_THEME_LIGHT_OLD = 'Default Light+',
FILE_ICON_THEME = 'vs-seti', FILE_ICON_THEME = 'vs-seti',