From 5c15a7c629dec0c9ea18ca90b7faaffdc50cf7dd Mon Sep 17 00:00:00 2001 From: Jackson Chen <90215880+Sma1lboy@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:30:25 -0600 Subject: [PATCH] feat(tabby-chat-panel): adding capability check in tabby-ui and adding applyInEditorV2 (#3432) * feat: add hasCapability method to ClientApi interface * feat: adding custom createThread implementation * feat: add createThreadInsideIframe module and update dependencies * [autofix.ci] apply automated fixes * feat: add CHECK_CAPABILITY method to createThread function * feat: update server capabilities in ChatPage component * [autofix.ci] apply automated fixes * feat: add ChatContext to AssistantMessageSection component * feat(tabby-thread): adding tabby threads package * feat(tabby-threads): add customized version of @quilted/threads for Tabby project, also added readme * feat: add CHECK_CAPABILITY method to target.ts * feat: update dependencies for tabby-chat-panel and vscode clients * chore: delete custom thread * chore: remove custom thread * feat: adding apply in editor v2 * feat: implement applyEditorV2 on vscode * feat(web-ui): adding capability check, also auto apply applyEditor v1 and v2 * chore: adding a new interface to encapsulate client apis * chore: Update createClient function parameter name in index.ts and react.ts - In index.ts, update the second parameter of the createClient function from `api: ClientApi` to `api: ClientApiMethods`. - In react.ts, update the second parameter of the useClient function from `api: ClientApi` to `api: ClientApiMethods`. * [autofix.ci] apply automated fixes * chore: update tabby-chat-panel to version 0.3.0 * feat: Add clientApiKeys array to export available client API methods The `clientApiKeys` array is added to export the available client API methods. This array includes keys for methods such as `navigate`, `refresh`, `onSubmitMessage`, `onApplyInEditor`, `onApplyInEditorV2`, `onLoaded`, `onCopy`, and `onKeyboardEvent`. This change enhances the functionality of the `ClientApi` interface. * chore: Update createClient and useClient function parameter names Update the second parameter of the createClient function in index.ts and the useClient function in react.ts from `api: ClientApi` to `api: ClientApiMethods`. This change improves the clarity and consistency of the function parameter names. * chore: Update createClient and useClient function parameter names * chore: Update createClient and useClient function parameter names Update the second parameter of the createClient function in index.ts and the useClient function in react.ts from `api: ClientApi` to `api: ClientApiMethods`. This change improves the clarity and consistency of the function parameter names. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- clients/tabby-chat-panel/src/index.ts | 20 ++++- clients/vscode/src/chat/WebviewHelper.ts | 73 +++++++++++-------- clients/vscode/src/chat/chatPanel.ts | 3 +- ee/tabby-ui/app/chat/page.tsx | 20 ++++- .../app/files/components/chat-side-bar.tsx | 2 +- .../components/assistant-message-section.tsx | 4 + .../components/user-message-section.tsx | 4 +- ee/tabby-ui/components/chat/chat.tsx | 22 +++--- .../components/chat/question-answer.tsx | 18 ++++- .../components/message-markdown/index.tsx | 19 ++++- ee/tabby-ui/components/ui/codeblock.tsx | 12 ++- 11 files changed, 141 insertions(+), 56 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index ceabd054dc12..e2cc1ec63c85 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -60,7 +60,14 @@ export interface ClientApiMethods { onSubmitMessage: (msg: string, relevantContext?: Context[]) => Promise - onApplyInEditor: (content: string, opts?: { languageId: string, smart: boolean }) => void + // apply content into active editor, version 1, not support smart apply + onApplyInEditor: (content: string) => void + + // version 2, support smart apply and normal apply + onApplyInEditorV2?: ( + content: string, + opts?: { languageId: string, smart: boolean } + ) => void // On current page is loaded. onLoaded: (params?: OnLoadedParams | undefined) => void @@ -78,6 +85,17 @@ export interface ClientApi extends ClientApiMethods { hasCapability: (method: keyof ClientApiMethods) => Promise } +export const clientApiKeys: (keyof ClientApiMethods)[] = [ + 'navigate', + 'refresh', + 'onSubmitMessage', + 'onApplyInEditor', + 'onApplyInEditorV2', + 'onLoaded', + 'onCopy', + 'onKeyboardEvent', +] + export interface ChatMessage { message: string diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index 794aaf19ed02..238b42dd74eb 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -394,6 +394,36 @@ export class WebviewHelper { } public createChatClient(webview: Webview) { + const getIndentInfo = (document: TextDocument, selection: Selection) => { + // Determine the indentation for the content + // The calculation is based solely on the indentation of the first line + const lineText = document.lineAt(selection.start.line).text; + const match = lineText.match(/^(\s*)/); + const indent = match ? match[0] : ""; + + // Determine the indentation for the content's first line + // Note: + // If using spaces, selection.start.character = 1 means 1 space + // If using tabs, selection.start.character = 1 means 1 tab + const indentUnit = indent[0]; + const indentAmountForTheFirstLine = Math.max(indent.length - selection.start.character, 0); + const indentForTheFirstLine = indentUnit?.repeat(indentAmountForTheFirstLine) || ""; + + return { indent, indentForTheFirstLine }; + }; + + const applyInEditor = (editor: TextEditor, content: string) => { + const document = editor.document; + const selection = editor.selection; + const { indent, indentForTheFirstLine } = getIndentInfo(document, selection); + // Indent the content + const indentedContent = indentForTheFirstLine + content.replaceAll("\n", "\n" + indent); + // Apply into the editor + editor.edit((editBuilder) => { + editBuilder.replace(selection, indentedContent); + }); + }; + return createClient(webview, { navigate: async (context: Context, opts?: NavigateOpts) => { if (opts?.openInEditor) { @@ -444,41 +474,20 @@ export class WebviewHelper { // FIXME: maybe deduplicate on chatMessage.relevantContext this.sendMessage(chatMessage); }, - onApplyInEditor: async (content: string, opts?: { languageId: string; smart: boolean }) => { - const getIndentInfo = (document: TextDocument, selection: Selection) => { - // Determine the indentation for the content - // The calculation is based solely on the indentation of the first line - const lineText = document.lineAt(selection.start.line).text; - const match = lineText.match(/^(\s*)/); - const indent = match ? match[0] : ""; - - // Determine the indentation for the content's first line - // Note: - // If using spaces, selection.start.character = 1 means 1 space - // If using tabs, selection.start.character = 1 means 1 tab - const indentUnit = indent[0]; - const indentAmountForTheFirstLine = Math.max(indent.length - selection.start.character, 0); - const indentForTheFirstLine = indentUnit?.repeat(indentAmountForTheFirstLine) || ""; - - return { indent, indentForTheFirstLine }; - }; - - const applyInEditor = (editor: TextEditor) => { - const document = editor.document; - const selection = editor.selection; - const { indent, indentForTheFirstLine } = getIndentInfo(document, selection); - // Indent the content - const indentedContent = indentForTheFirstLine + content.replaceAll("\n", "\n" + indent); - // Apply into the editor - editor.edit((editBuilder) => { - editBuilder.replace(selection, indentedContent); - }); - }; + onApplyInEditor: async (content: string) => { + const editor = window.activeTextEditor; + if (!editor) { + window.showErrorMessage("No active editor found."); + return; + } + applyInEditor(editor, content); + }, + onApplyInEditorV2: async (content: string, opts?: { languageId: string; smart: boolean }) => { const smartApplyInEditor = async (editor: TextEditor, opts: { languageId: string; smart: boolean }) => { if (editor.document.languageId !== opts.languageId) { this.logger.debug("Editor's languageId:", editor.document.languageId, "opts.languageId:", opts.languageId); window.showInformationMessage("The active editor is not in the correct language. Did normal apply."); - applyInEditor(editor); + applyInEditor(editor, content); return; } @@ -524,7 +533,7 @@ export class WebviewHelper { return; } if (!opts || !opts.smart) { - applyInEditor(editor); + applyInEditor(editor, content); } else { smartApplyInEditor(editor, opts); } diff --git a/clients/vscode/src/chat/chatPanel.ts b/clients/vscode/src/chat/chatPanel.ts index 0214f654e42b..553880f532c4 100644 --- a/clients/vscode/src/chat/chatPanel.ts +++ b/clients/vscode/src/chat/chatPanel.ts @@ -29,8 +29,9 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi refresh: api.refresh, onSubmitMessage: api.onSubmitMessage, onApplyInEditor: api.onApplyInEditor, - onCopy: api.onCopy, + onApplyInEditorV2: api.onApplyInEditorV2, onLoaded: api.onLoaded, + onCopy: api.onCopy, onKeyboardEvent: api.onKeyboardEvent, }, }); diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index a5e23dd6fff3..6f14266a0909 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -71,6 +71,10 @@ export default function ChatPage() { const isInEditor = !!client || undefined const useMacOSKeyboardEventHandler = useRef() + // server feature support check + const [supportsOnApplyInEditorV2, setSupportsOnApplyInEditorV2] = + useState(false) + const sendMessage = (message: ChatMessage) => { if (chatRef.current) { chatRef.current.sendUserChat(message) @@ -227,6 +231,14 @@ export default function ChatPage() { server?.onLoaded({ apiVersion: TABBY_CHAT_PANEL_API_VERSION }) + + const checkCapabilities = async () => { + server + ?.hasCapability('onApplyInEditorV2') + .then(setSupportsOnApplyInEditorV2) + } + + checkCapabilities() } }, [server]) @@ -369,7 +381,13 @@ export default function ChatPage() { maxWidth={client === 'vscode' ? '5xl' : undefined} onCopyContent={isInEditor && server?.onCopy} onSubmitMessage={isInEditor && server?.onSubmitMessage} - onApplyInEditor={isInEditor && server?.onApplyInEditor} + onApplyInEditor={ + isInEditor && + (supportsOnApplyInEditorV2 + ? server?.onApplyInEditorV2 + : server?.onApplyInEditor) + } + supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} /> ) diff --git a/ee/tabby-ui/app/files/components/chat-side-bar.tsx b/ee/tabby-ui/app/files/components/chat-side-bar.tsx index 5b2235141073..de87e95fc496 100644 --- a/ee/tabby-ui/app/files/components/chat-side-bar.tsx +++ b/ee/tabby-ui/app/files/components/chat-side-bar.tsx @@ -76,7 +76,7 @@ export const ChatSideBar: React.FC = ({ }) }, async onSubmitMessage(_msg, _relevantContext) {}, - onApplyInEditor(_content, _args) {}, + onApplyInEditor(_content) {}, onLoaded() {}, onCopy(_content) {}, onKeyboardEvent() {} diff --git a/ee/tabby-ui/app/search/components/assistant-message-section.tsx b/ee/tabby-ui/app/search/components/assistant-message-section.tsx index 099ed0213be6..2aa7f3cc175a 100644 --- a/ee/tabby-ui/app/search/components/assistant-message-section.tsx +++ b/ee/tabby-ui/app/search/components/assistant-message-section.tsx @@ -56,6 +56,7 @@ import { TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { ChatContext } from '@/components/chat/chat' import { CodeReferences } from '@/components/chat/code-references' import { CopyButton } from '@/components/copy-button' import { @@ -94,6 +95,8 @@ export function AssistantMessageSection({ onUpdateMessage } = useContext(SearchContext) + const { supportsOnApplyInEditorV2 } = useContext(ChatContext) + const [isEditing, setIsEditing] = useState(false) const [showMoreSource, setShowMoreSource] = useState(false) const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] = useState< @@ -328,6 +331,7 @@ export function AssistantMessageSection({ contextInfo={contextInfo} fetchingContextInfo={fetchingContextInfo} canWrapLongLines={!isLoading} + supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} /> {/* if isEditing, do not display error message block */} {message.error && } diff --git a/ee/tabby-ui/app/search/components/user-message-section.tsx b/ee/tabby-ui/app/search/components/user-message-section.tsx index a37fd0cc2d5e..cd2cd2bd4f70 100644 --- a/ee/tabby-ui/app/search/components/user-message-section.tsx +++ b/ee/tabby-ui/app/search/components/user-message-section.tsx @@ -1,6 +1,7 @@ import { HTMLAttributes, useContext } from 'react' import { cn } from '@/lib/utils' +import { ChatContext } from '@/components/chat/chat' import { MessageMarkdown } from '@/components/message-markdown' import { ConversationMessage, SearchContext } from './search' @@ -15,12 +16,13 @@ export function UserMessageSection({ ...props }: QuestionBlockProps) { const { contextInfo, fetchingContextInfo } = useContext(SearchContext) - + const { supportsOnApplyInEditorV2 } = useContext(ChatContext) return (
void container?: HTMLDivElement onCopyContent?: (value: string) => void - onApplyInEditor?: ( - content: string, - opts?: { languageId: string; smart: boolean } - ) => void + onApplyInEditor?: + | ((content: string) => void) + | ((content: string, opts?: { languageId: string; smart: boolean }) => void) relevantContext: Context[] activeSelection: Context | null removeRelevantContext: (index: number) => void chatInputRef: RefObject + supportsOnApplyInEditorV2: boolean } export const ChatContext = React.createContext( @@ -80,11 +80,11 @@ interface ChatProps extends React.ComponentProps<'div'> { promptFormClassname?: string onCopyContent?: (value: string) => void onSubmitMessage?: (msg: string, relevantContext?: Context[]) => Promise - onApplyInEditor?: ( - content: string, - opts?: { languageId: string; smart: boolean } - ) => void + onApplyInEditor?: + | ((content: string) => void) + | ((content: string, opts?: { languageId: string; smart: boolean }) => void) chatInputRef: RefObject + supportsOnApplyInEditorV2: boolean } function ChatRenderer( @@ -104,7 +104,8 @@ function ChatRenderer( onCopyContent, onSubmitMessage, onApplyInEditor, - chatInputRef + chatInputRef, + supportsOnApplyInEditorV2 }: ChatProps, ref: React.ForwardedRef ) { @@ -531,7 +532,8 @@ function ChatRenderer( relevantContext, removeRelevantContext, chatInputRef, - activeSelection + activeSelection, + supportsOnApplyInEditorV2 }} >
diff --git a/ee/tabby-ui/components/chat/question-answer.tsx b/ee/tabby-ui/components/chat/question-answer.tsx index cc1644e1fa54..c5612862adb7 100644 --- a/ee/tabby-ui/components/chat/question-answer.tsx +++ b/ee/tabby-ui/components/chat/question-answer.tsx @@ -108,7 +108,8 @@ function UserMessageCard(props: { message: UserMessage }) { const { message } = props const [{ data }] = useMe() const selectContext = message.selectContext - const { onNavigateToContext } = React.useContext(ChatContext) + const { onNavigateToContext, supportsOnApplyInEditorV2 } = + React.useContext(ChatContext) const selectCodeSnippet = React.useMemo(() => { if (!selectContext?.content) return '' const language = selectContext?.filepath @@ -162,7 +163,11 @@ function UserMessageCard(props: { message: UserMessage }) {
- +
@@ -252,8 +257,12 @@ function AssistantMessageCard(props: AssistantMessageCardProps) { enableRegenerating, ...rest } = props - const { onNavigateToContext, onApplyInEditor, onCopyContent } = - React.useContext(ChatContext) + const { + onNavigateToContext, + onApplyInEditor, + onCopyContent, + supportsOnApplyInEditorV2 + } = React.useContext(ChatContext) const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] = React.useState(undefined) const serverCode: Array = React.useMemo(() => { @@ -389,6 +398,7 @@ function AssistantMessageCard(props: AssistantMessageCardProps) { onCodeCitationMouseEnter={onCodeCitationMouseEnter} onCodeCitationMouseLeave={onCodeCitationMouseLeave} canWrapLongLines={!isLoading} + supportsOnApplyInEditorV2={supportsOnApplyInEditorV2} /> {!!message.error && } diff --git a/ee/tabby-ui/components/message-markdown/index.tsx b/ee/tabby-ui/components/message-markdown/index.tsx index 4a8594a5ad06..8cfa7fa4f970 100644 --- a/ee/tabby-ui/components/message-markdown/index.tsx +++ b/ee/tabby-ui/components/message-markdown/index.tsx @@ -78,6 +78,7 @@ export interface MessageMarkdownProps { className?: string // wrapLongLines for code block canWrapLongLines?: boolean + supportsOnApplyInEditorV2: boolean } type MessageMarkdownContextValue = { @@ -92,6 +93,7 @@ type MessageMarkdownContextValue = { contextInfo: ContextInfo | undefined fetchingContextInfo: boolean canWrapLongLines: boolean + supportsOnApplyInEditorV2: boolean } const MessageMarkdownContext = createContext( @@ -109,6 +111,7 @@ export function MessageMarkdown({ fetchingContextInfo, className, canWrapLongLines, + supportsOnApplyInEditorV2, ...rest }: MessageMarkdownProps) { const messageAttachments: MessageAttachments = useMemo(() => { @@ -183,7 +186,8 @@ export function MessageMarkdown({ onCodeCitationMouseLeave: rest.onCodeCitationMouseLeave, contextInfo, fetchingContextInfo: !!fetchingContextInfo, - canWrapLongLines: !!canWrapLongLines + canWrapLongLines: !!canWrapLongLines, + supportsOnApplyInEditorV2 }} > ) @@ -300,9 +305,17 @@ export function ErrorMessageBlock({ } function CodeBlockWrapper(props: CodeBlockProps) { - const { canWrapLongLines } = useContext(MessageMarkdownContext) + const { canWrapLongLines, supportsOnApplyInEditorV2 } = useContext( + MessageMarkdownContext + ) - return + return ( + + ) } function CitationTag({ diff --git a/ee/tabby-ui/components/ui/codeblock.tsx b/ee/tabby-ui/components/ui/codeblock.tsx index 2831fc6f146a..0d452a4a8cd8 100644 --- a/ee/tabby-ui/components/ui/codeblock.tsx +++ b/ee/tabby-ui/components/ui/codeblock.tsx @@ -35,6 +35,7 @@ export interface CodeBlockProps { opts?: { languageId: string; smart: boolean } ) => void canWrapLongLines: boolean | undefined + supportsOnApplyInEditorV2: boolean } interface languageMap { @@ -78,7 +79,14 @@ export const generateRandomString = (length: number, lowercase = false) => { } const CodeBlock: FC = memo( - ({ language, value, onCopyContent, onApplyInEditor, canWrapLongLines }) => { + ({ + language, + value, + onCopyContent, + onApplyInEditor, + canWrapLongLines, + supportsOnApplyInEditorV2 + }) => { const [wrapLongLines, setWrapLongLines] = useState(false) const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000, @@ -115,7 +123,7 @@ const CodeBlock: FC = memo( )} - {onApplyInEditor && ( + {supportsOnApplyInEditorV2 && onApplyInEditor && (