From 967f7dc85ec3f0149cd997f4ff9fb4bd5eaad0b3 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 00:01:52 -0700 Subject: [PATCH] make apply actually work --- .../src/markdown/ApplyBlockHoverButtons.tsx | 227 +++++++++++++----- .../browser/react/src/markdown/BlockCode.tsx | 38 ++- .../react/src/markdown/ChatMarkdownRender.tsx | 52 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 81 ++++--- .../themes/common/workbenchThemeService.ts | 4 +- 5 files changed, 280 insertions(+), 122 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 90ec360f..7cd3e405 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -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) => ( + +) + + +export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( + +) + 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 + 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 = ( + + ) + + const playButton = ( + + ) + + const stopButton = ( + + ) + + const reapplyButton = ( + + ) + + const acceptButton = ( + + ) + + const rejectButton = ( + + ) - const isSingleLine = false //!codeStr.includes('\n') + let buttonsHTML = <> - const applyButton = + if (currStreamState === 'streaming') { + buttonsHTML = <> + {stopButton} + + } - const stopButton = + if (currStreamState === 'idle') { + buttonsHTML = <> + {copyButton} + {playButton} + + } - const acceptRejectButtons = <> - - - + + + + return { + statusIndicatorHTML, + buttonsHTML + } + - const currStreamState = streamState() - return <> - {currStreamState !== 'streaming' && } - {currStreamState === 'idle' && !isDisabled && applyButton} - {currStreamState === 'streaming' && stopButton} - {currStreamState === 'acceptRejectAll' && acceptRejectButtons} - } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index f7954b82..c1cd1de2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -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 ( +
+
+
+
{language || 'text'}
+ {statusIndicatorHTML} +
+
+ {buttonsHTML} +
+
+ + + +
+ ) +} -export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => { +export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => { + const isSingleLine = !codeEditorProps.initValue.includes('\n') return ( <> -
+ + + {/*
{buttonsOnHover === null ? null : (
@@ -23,7 +50,8 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov )} -
+
*/} + ) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 1b46bef1..baa0620e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -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
- } + language={language} + applyBoxId={applyBoxId} /> -
+ } + + return } if (t.type === "heading") { @@ -213,7 +223,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: return
  • - +
  • } @@ -229,7 +239,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: )} - + ))} @@ -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 + } return @@ -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) => ( - + ))} ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 1ee518e1..b0bafb9e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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'}`} > -
    +
    {children}
    @@ -971,6 +971,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast } @@ -978,6 +980,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast {/* loading indicator */} @@ -1009,7 +1013,7 @@ const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: strin } > -
    {errorMessage}
    +
    {errorMessage}
    ) @@ -1028,34 +1032,34 @@ const toolNameToTitle: Record = { } 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 }) => { +const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast, }: { toolRequest: ToolRequestApproval } & Omit) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - return <> -
    { chatThreadsService.approveTool(toolRequest.voidToolId) }}>Accept
    -
    { chatThreadsService.rejectTool(toolRequest.voidToolId) }}>Reject
    - + + const initRequestState = isLast ? 'awaiting_response' : 'rejected' + + const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState) + + + if (requestState === 'awaiting_response') { + return <> +
    { chatThreadsService.approveTool(toolRequest.voidToolId); setRequestState('accepted') }}>Accept
    +
    { chatThreadsService.rejectTool(toolRequest.voidToolId); setRequestState('rejected') }}>Reject
    + + } + } const toolNameToComponent: { [T in ToolName]: { @@ -1306,7 +1319,10 @@ const toolNameToComponent: { [T in ToolName]: { return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} > - + }, 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 <> - - + + } 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 - const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... - return + const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... + return } @@ -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) => - + ) }, [previousMessages, currentThread]) - - - const streamingChatIdx = pastMessagesHTML.length + const streamingChatIdx = previousMessagesHTML.length const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? { isLast={true} /> : null - const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML] - + const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] const threadSelector =
    {
    - - const messagesHTML = { 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() }} diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 505dd21c..52ec138d 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -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',