From 639c8570a5f47c4e8af10a1c2685767299807973 Mon Sep 17 00:00:00 2001 From: aliang <1098486429@qq.com> Date: Mon, 21 Oct 2024 12:24:55 +0800 Subject: [PATCH] feat(vscode): dynamically display the active selection context in chat side panel (#3286) * feat(vscode): dynamically display the active selection context in chat side panel * update: format * [autofix.ci] apply automated fixes * update: error handling & update version * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- clients/tabby-chat-panel/package.json | 2 +- clients/tabby-chat-panel/src/index.ts | 2 ++ .../vscode/src/chat/ChatSideViewProvider.ts | 3 ++ clients/vscode/src/chat/WebviewHelper.ts | 28 +++++++++++++++ ee/tabby-ui/app/chat/page.tsx | 24 +++++++++++-- ee/tabby-ui/components/chat/chat-panel.tsx | 36 ++++++++++++++----- ee/tabby-ui/components/chat/chat.tsx | 17 +++++++-- 7 files changed, 99 insertions(+), 13 deletions(-) diff --git a/clients/tabby-chat-panel/package.json b/clients/tabby-chat-panel/package.json index 7e6a14d4b592..2a8e6b54b04c 100644 --- a/clients/tabby-chat-panel/package.json +++ b/clients/tabby-chat-panel/package.json @@ -1,7 +1,7 @@ { "name": "tabby-chat-panel", "type": "module", - "version": "0.2.0", + "version": "0.2.1", "keywords": [], "sideEffects": false, "exports": { diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index 5538d56bb794..98c48e601be8 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -51,6 +51,7 @@ export interface ServerApi { cleanError: () => void addRelevantContext: (context: Context) => void updateTheme: (style: string, themeClass: string) => void + updateActiveSelection: (context: Context | null) => void } export interface ClientApi { @@ -106,6 +107,7 @@ export function createServer(api: ServerApi): ClientApi { cleanError: api.cleanError, addRelevantContext: api.addRelevantContext, updateTheme: api.updateTheme, + updateActiveSelection: api.updateActiveSelection, }, }) } diff --git a/clients/vscode/src/chat/ChatSideViewProvider.ts b/clients/vscode/src/chat/ChatSideViewProvider.ts index 3e90d1affdb5..cad14bbf53b9 100644 --- a/clients/vscode/src/chat/ChatSideViewProvider.ts +++ b/clients/vscode/src/chat/ChatSideViewProvider.ts @@ -61,6 +61,9 @@ export class ChatSideViewProvider implements WebviewViewProvider { await this.webviewHelper.displayPageBasedOnServerStatus(); this.webviewHelper.addAgentEventListeners(); + this.webviewHelper.syncActiveSelection(window.activeTextEditor); + this.webviewHelper.addTextEditorEventListeners(); + // The event will not be triggered during the initial rendering. webviewView.onDidChangeVisibility(() => { if (webviewView.visible) { diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index d31e2404a5bb..15d58061e343 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -343,6 +343,14 @@ export class WebviewHelper { this.client?.sendMessage(message); } + public async syncActiveSelectionToChatPanel(context: Context | null) { + try { + await this.client?.updateActiveSelection(context); + } catch { + this.logger.warn("Active selection sync failed. Please update your Tabby server to the latest version."); + } + } + public addRelevantContext(context: Context) { if (!this.client) { this.pendingRelevantContexts.push(context); @@ -370,6 +378,7 @@ export class WebviewHelper { this.pendingRelevantContexts.forEach((ctx) => this.addRelevantContext(ctx)); this.pendingMessages.forEach((message) => this.sendMessageToChatPanel(message)); + this.syncActiveSelection(window.activeTextEditor); if (serverInfo.config.token) { this.client?.cleanError(); @@ -412,6 +421,15 @@ export class WebviewHelper { } } + public syncActiveSelection(editor: TextEditor | undefined) { + if (!editor) { + return; + } + + const fileContext = WebviewHelper.getFileContextFromSelection({ editor, gitProvider: this.gitProvider }); + this.syncActiveSelectionToChatPanel(fileContext); + } + public addAgentEventListeners() { this.agent.on("didChangeStatus", async (status) => { if (status !== "disconnected") { @@ -430,6 +448,15 @@ export class WebviewHelper { }); } + public addTextEditorEventListeners() { + window.onDidChangeTextEditorSelection((e) => { + if (e.textEditor !== window.activeTextEditor) { + return; + } + this.syncActiveSelection(e.textEditor); + }); + } + public async displayPageBasedOnServerStatus() { // At this point, if the server instance is not set up, agent.status is 'notInitialized'. // We check for the presence of the server instance by verifying serverInfo.health["webserver"]. @@ -499,6 +526,7 @@ export class WebviewHelper { message: msg, relevantContext: [], }; + // FIXME: after synchronizing the activeSelection, perhaps there's no need to include activeSelection in the message. if (editor) { const fileContext = WebviewHelper.getFileContextFromSelection({ editor, gitProvider: this.gitProvider }); if (fileContext) diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 66308d0ce22a..8fa3e6c5364a 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -55,6 +55,8 @@ export default function ChatPage() { const [pendingRelevantContexts, setPendingRelevantContexts] = useState< Context[] >([]) + const [pendingActiveSelection, setPendingActiveSelection] = + useState(null) const [errorMessage, setErrorMessage] = useState(null) const [isRefreshLoading, setIsRefreshLoading] = useState(false) @@ -89,6 +91,14 @@ export default function ChatPage() { } } + const updateActiveSelection = (ctx: Context | null) => { + if (chatRef.current) { + chatRef.current.updateActiveSelection(ctx) + } else { + setPendingActiveSelection(ctx) + } + } + const server = useServer({ init: (request: InitRequest) => { if (chatRef.current) return @@ -135,6 +145,9 @@ export default function ChatPage() { // Sync with edit theme document.documentElement.className = themeClass + ` client client-${client}` + }, + updateActiveSelection: context => { + return updateActiveSelection(context) } }) @@ -233,11 +246,18 @@ export default function ChatPage() { prevWidthRef.current = width }, [width, chatLoaded]) + const clearPendingState = () => { + setPendingRelevantContexts([]) + setPendingMessages([]) + setPendingActiveSelection(null) + } + const onChatLoaded = () => { pendingRelevantContexts.forEach(addRelevantContext) pendingMessages.forEach(sendMessage) - setPendingRelevantContexts([]) - setPendingMessages([]) + updateActiveSelection(pendingActiveSelection) + + clearPendingState() setChatLoaded(true) } diff --git a/ee/tabby-ui/components/chat/chat-panel.tsx b/ee/tabby-ui/components/chat/chat-panel.tsx index e20c3cc0d770..86cdbe59b39f 100644 --- a/ee/tabby-ui/components/chat/chat-panel.tsx +++ b/ee/tabby-ui/components/chat/chat-panel.tsx @@ -1,5 +1,6 @@ import React, { RefObject } from 'react' import type { UseChatHelpers } from 'ai/react' +import type { Context } from 'tabby-chat-panel' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -49,7 +50,8 @@ function ChatPanelRenderer( qaPairs, isLoading, relevantContext, - removeRelevantContext + removeRelevantContext, + activeSelection } = React.useContext(ChatContext) React.useImperativeHandle( @@ -102,21 +104,29 @@ function ChatPanelRenderer( )}
- {relevantContext.length > 0 && ( + {(!!activeSelection || relevantContext.length > 0) && (
+ {activeSelection ? ( + + + {getContextLabel(activeSelection)} + + + ) : null} {relevantContext.map((item, idx) => { - const [fileName] = item.filepath.split('/').slice(-1) - const line = - item.range.start === item.range.end - ? `${item.range.start}` - : `${item.range.start}-${item.range.end}` return ( - {`${fileName}: ${line}`} + + {getContextLabel(item)} + ( ChatPanelRenderer ) + +function getContextLabel(context: Context) { + const [fileName] = context.filepath.split('/').slice(-1) + const line = + context.range.start === context.range.end + ? `${context.range.start}` + : `${context.range.start}-${context.range.end}` + + return `${fileName}: ${line}` +} diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 7007205cd4be..8c5a140ae0a5 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -42,6 +42,7 @@ type ChatContextValue = { onCopyContent?: (value: string) => void onApplyInEditor?: (value: string) => void relevantContext: Context[] + activeSelection: Context | null removeRelevantContext: (index: number) => void chatInputRef: RefObject } @@ -56,6 +57,7 @@ export interface ChatRef { isLoading: boolean addRelevantContext: (context: Context) => void focus: () => void + updateActiveSelection: (context: Context | null) => void } interface ChatProps extends React.ComponentProps<'div'> { @@ -104,6 +106,9 @@ function ChatRenderer( const [qaPairs, setQaPairs] = React.useState(initialMessages ?? []) const [input, setInput] = React.useState('') const [relevantContext, setRelevantContext] = React.useState([]) + const [activeSelection, setActiveSelection] = React.useState( + null + ) const chatPanelRef = React.useRef(null) const { @@ -348,6 +353,8 @@ function ChatRenderer( const newUserMessage = { ...userMessage, message: userMessage.message + selectCodeSnippet, + // For forward compatibility + activeSelection: activeSelection || userMessage.activeContext, // If no id is provided, set a fallback id. id: userMessage.id ?? nanoid() } @@ -406,6 +413,10 @@ function ChatRenderer( onThreadUpdates?.(qaPairs) }, [qaPairs]) + const updateActiveSelection = (ctx: Context | null) => { + setActiveSelection(ctx) + } + React.useImperativeHandle( ref, () => { @@ -414,7 +425,8 @@ function ChatRenderer( stop, isLoading, addRelevantContext, - focus: () => chatPanelRef.current?.focus() + focus: () => chatPanelRef.current?.focus(), + updateActiveSelection } }, [] @@ -448,7 +460,8 @@ function ChatRenderer( onApplyInEditor, relevantContext, removeRelevantContext, - chatInputRef + chatInputRef, + activeSelection }} >