mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
make apply actually work
This commit is contained in:
parent
d6d5f77183
commit
967f7dc85e
5 changed files with 280 additions and 122 deletions
|
|
@ -3,6 +3,8 @@ import { useAccessor, useURIStreamState, useSettingsState } from '../util/servic
|
|||
import { useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.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 {
|
||||
Idle = 'Copy',
|
||||
|
|
@ -10,6 +12,53 @@ enum CopyButtonText {
|
|||
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 CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
|
|
@ -26,7 +75,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
|||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}, [copyButtonText])
|
||||
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(codeStr)
|
||||
.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, clipboardService, codeStr, setCopyButtonText])
|
||||
|
||||
const isSingleLine = false //!codeStr.includes('\n')
|
||||
|
||||
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`}
|
||||
return <IconShell1
|
||||
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
title={copyButtonText}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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 ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
|
||||
export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
|
@ -64,21 +106,21 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
|
||||
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri])
|
||||
const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
|
||||
const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied])
|
||||
|
||||
// listen for stream updates
|
||||
useURIStreamState(
|
||||
useCallback((uri, newStreamState) => {
|
||||
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath
|
||||
if (shouldUpdate) return
|
||||
const shouldUpdate = getUriBeingApplied()?.fsPath === uri.fsPath
|
||||
if (!shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, applyingUri])
|
||||
}, [applyBoxId, editCodeService, getUriBeingApplied])
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (streamState() === 'streaming') return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const [newApplyingUri, _] = editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
|
|
@ -88,61 +130,122 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
rerender(c => c + 1)
|
||||
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(() => {
|
||||
if (streamState() !== 'streaming') return
|
||||
const uri = applyingUri()
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied()
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
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
|
||||
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={onSubmit}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
if (currStreamState === 'streaming') {
|
||||
buttonsHTML = <>
|
||||
{stopButton}
|
||||
</>
|
||||
}
|
||||
|
||||
const stopButton = <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={onInterrupt}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
if (currStreamState === 'idle') {
|
||||
buttonsHTML = <>
|
||||
{copyButton}
|
||||
{playButton}
|
||||
</>
|
||||
}
|
||||
|
||||
const acceptRejectButtons = <>
|
||||
<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={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}}
|
||||
if (currStreamState === 'acceptRejectAll') {
|
||||
buttonsHTML = <>
|
||||
{reapplyButton}
|
||||
{rejectButton}
|
||||
{acceptButton}
|
||||
</>
|
||||
}
|
||||
|
||||
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
|
||||
</button>
|
||||
<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={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
buttonsHTML
|
||||
}
|
||||
|
||||
|
||||
const currStreamState = streamState()
|
||||
return <>
|
||||
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
|
||||
{currStreamState === 'idle' && !isDisabled && applyButton}
|
||||
{currStreamState === 'streaming' && stopButton}
|
||||
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,44 @@
|
|||
* 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 { 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')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
|
||||
{/* <div className="relative group w-full overflow-hidden">
|
||||
{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={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
|
||||
|
|
@ -23,7 +50,8 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
|
|||
)}
|
||||
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
import React, { JSX, useState } from 'react'
|
||||
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 { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js'
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js'
|
||||
import { Range } from '../../../../../../services/search/common/searchExtTypes.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 })
|
||||
|
||||
if (link === undefined) {
|
||||
// generate link and add to cache
|
||||
// if no link, generate link and add to cache
|
||||
(chatThreadService.generateCodespanLink(text)
|
||||
.then(link => {
|
||||
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)
|
||||
const t = token as MarkedToken
|
||||
|
|
@ -114,21 +116,29 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
|
|||
|
||||
if (t.type === "code") {
|
||||
|
||||
const applyBoxId = chatMessageLocation ? getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
}) : null
|
||||
const language = t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]
|
||||
|
||||
// TODO user should only be able to apply this when the code has been closed (t.raw ends with "```")
|
||||
|
||||
return <div>
|
||||
<BlockCode
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
})
|
||||
|
||||
return <BlockCodeWithApply
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
language={language}
|
||||
applyBoxId={applyBoxId}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <BlockCode
|
||||
initValue={t.text}
|
||||
language={language}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
|
|
@ -213,7 +223,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
|
|||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} nested={true} {...options} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
|
@ -229,7 +239,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
|
|||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} {...options} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -244,6 +254,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
|
|||
token={token}
|
||||
tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
{...options}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -304,12 +315,15 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token:
|
|||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
|
||||
if (chatMessageLocation) {
|
||||
console.log('isLinkDetectionEnabled', options.isLinkDetectionEnabled)
|
||||
if (options.isLinkDetectionEnabled && chatMessageLocation) {
|
||||
|
||||
return <CodespanWithLink
|
||||
text={t.text}
|
||||
rawText={t.raw}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
/>
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
return (
|
||||
<>
|
||||
{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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -971,6 +971,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={false}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
</DropdownComponent>}
|
||||
|
||||
|
|
@ -978,6 +980,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
<ChatMarkdownRender
|
||||
string={chatMessage.content || ''}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
isApplyEnabled={true}
|
||||
isLinkDetectionEnabled={true}
|
||||
/>
|
||||
|
||||
{/* loading indicator */}
|
||||
|
|
@ -1009,7 +1013,7 @@ const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: strin
|
|||
</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>
|
||||
|
||||
)
|
||||
|
|
@ -1028,34 +1032,34 @@ const toolNameToTitle: Record<ToolName, string> = {
|
|||
}
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
|
||||
|
||||
if (_toolParams === undefined) {
|
||||
if (!_toolParams) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (toolName === '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') {
|
||||
const toolParams = _toolParams as ToolCallParams['list_dir']
|
||||
return toolParams ? `${getBasename(toolParams.rootURI.fsPath)}/` : '';
|
||||
return `${getBasename(toolParams.rootURI.fsPath)}/`;
|
||||
} else if (toolName === 'pathname_search') {
|
||||
const toolParams = _toolParams as ToolCallParams['pathname_search']
|
||||
return toolParams ? `"${toolParams.queryStr}"` : '';
|
||||
return `"${toolParams.queryStr}"`;
|
||||
} else if (toolName === 'search') {
|
||||
const toolParams = _toolParams as ToolCallParams['search']
|
||||
return toolParams ? `"${toolParams.queryStr}"` : '';
|
||||
return `"${toolParams.queryStr}"`;
|
||||
} else if (toolName === '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') {
|
||||
const toolParams = _toolParams as ToolCallParams['delete_uri']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) + ' (deleted)' : '';
|
||||
return getBasename(toolParams.uri.fsPath) + ' (deleted)';
|
||||
} else if (toolName === 'edit') {
|
||||
const toolParams = _toolParams as ToolCallParams['edit']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) : '';
|
||||
return getBasename(toolParams.uri.fsPath);
|
||||
} else if (toolName === 'terminal_command') {
|
||||
const toolParams = _toolParams as ToolCallParams['terminal_command']
|
||||
return toolParams ? `"${toolParams.command}"` : '';
|
||||
return `"${toolParams.command}"`;
|
||||
} else {
|
||||
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 chatThreadsService = accessor.get('IChatThreadService')
|
||||
return <>
|
||||
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.approveTool(toolRequest.voidToolId) }}>Accept</div>
|
||||
<div className='text-void-fg-4 italic' onClick={() => { chatThreadsService.rejectTool(toolRequest.voidToolId) }}>Reject</div>
|
||||
</>
|
||||
|
||||
const initRequestState = isLast ? 'awaiting_response' : 'rejected'
|
||||
|
||||
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]: {
|
||||
|
|
@ -1306,7 +1319,10 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
return <DropdownComponent title={title} desc1={desc1}
|
||||
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>
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
|
|
@ -1412,10 +1428,10 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
|
|||
else if (role === 'tool_request') {
|
||||
const isLastMessage = true // TODO!!! fix this
|
||||
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 <>
|
||||
<ToolRequestComponent toolRequest={chatMessage} />
|
||||
<ToolRequestAcceptRejectButtons toolRequest={chatMessage} />
|
||||
<ToolRequestWrapper toolRequest={chatMessage} />
|
||||
<ToolRequestAcceptRejectButtons toolRequest={chatMessage} messageIdx={messageIdx} isLast={isLast} />
|
||||
</>
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
|
|
@ -1423,8 +1439,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
|
|||
const title = toolNameToTitle[chatMessage.name]
|
||||
// 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...
|
||||
return <ToolResultComponent toolMessage={chatMessage} />
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
|
||||
return <ToolResultWrapper toolMessage={chatMessage} />
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1524,19 +1540,19 @@ export const SidebarChat = () => {
|
|||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
const numMessages = previousMessages.length + (isStreaming ? 1 : 0)
|
||||
|
||||
const pastMessagesHTML = useMemo(() => {
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
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])
|
||||
|
||||
|
||||
|
||||
const streamingChatIdx = pastMessagesHTML.length
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
messageIdx={streamingChatIdx} chatMessage={{
|
||||
messageIdx={streamingChatIdx}
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
content: messageSoFar ?? '',
|
||||
reasoning: reasoningSoFar ?? '',
|
||||
|
|
@ -1546,8 +1562,7 @@ export const SidebarChat = () => {
|
|||
isLast={true}
|
||||
/> : null
|
||||
|
||||
const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML]
|
||||
|
||||
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]
|
||||
|
||||
const threadSelector = <div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
|
|
@ -1555,8 +1570,6 @@ export const SidebarChat = () => {
|
|||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
key={currentThread.id} // force rerender on all children if id changes
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
|
|
@ -1566,7 +1579,7 @@ export const SidebarChat = () => {
|
|||
w-full h-auto
|
||||
overflow-x-hidden
|
||||
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
|
||||
>
|
||||
|
|
@ -1608,7 +1621,7 @@ export const SidebarChat = () => {
|
|||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={pastMessagesHTML.length === 0}
|
||||
showProspectiveSelections={previousMessagesHTML.length === 0}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ export enum ThemeSettings {
|
|||
}
|
||||
|
||||
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_HC_DARK = 'Default High Contrast',
|
||||
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+',
|
||||
|
||||
FILE_ICON_THEME = 'vs-seti',
|
||||
|
|
|
|||
Loading…
Reference in a new issue