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 { 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}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> */}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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() }}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue