From 41785e0c8592a6c72ac9209d658827dec79f3825 Mon Sep 17 00:00:00 2001 From: aliang Date: Fri, 15 Nov 2024 21:27:43 +0700 Subject: [PATCH] feat(chat-ui): support toggling active selection collection in chat sidebar (#3419) * feat(chat-ui): support toggling active selection collection in chat sidebar * [autofix.ci] apply automated fixes * update * update * update * [autofix.ci] apply automated fixes * update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- clients/vscode/src/chat/WebviewHelper.ts | 13 ++- ee/tabby-ui/app/chat/page.tsx | 8 +- .../app/files/components/chat-side-bar.tsx | 3 +- ee/tabby-ui/components/chat/chat-panel.tsx | 107 +++++++++++++----- ee/tabby-ui/components/chat/chat.tsx | 48 +++++--- ee/tabby-ui/components/ui/icons.tsx | 16 ++- ee/tabby-ui/lib/hooks/use-debounce.ts | 2 + ee/tabby-ui/lib/stores/chat-actions.ts | 76 +------------ ee/tabby-ui/lib/stores/chat-store.ts | 36 ++---- 9 files changed, 160 insertions(+), 149 deletions(-) diff --git a/clients/vscode/src/chat/WebviewHelper.ts b/clients/vscode/src/chat/WebviewHelper.ts index f87392a0dde6..3e12bc4488f6 100644 --- a/clients/vscode/src/chat/WebviewHelper.ts +++ b/clients/vscode/src/chat/WebviewHelper.ts @@ -336,6 +336,7 @@ export class WebviewHelper { public async syncActiveSelection(editor: TextEditor | undefined) { if (!editor) { + this.syncActiveSelectionToChatPanel(null); return; } @@ -362,8 +363,18 @@ export class WebviewHelper { } public addTextEditorEventListeners() { + window.onDidChangeActiveTextEditor((e) => { + if (e && e.document.uri.scheme !== "file") { + this.syncActiveSelection(undefined); + return; + } + + this.syncActiveSelection(e); + }); + window.onDidChangeTextEditorSelection((e) => { - if (e.textEditor !== window.activeTextEditor) { + // This listener only handles text files. + if (e.textEditor.document.uri.scheme !== "file") { return; } this.syncActiveSelection(e.textEditor); diff --git a/ee/tabby-ui/app/chat/page.tsx b/ee/tabby-ui/app/chat/page.tsx index 8fa3e6c5364a..a5e23dd6fff3 100644 --- a/ee/tabby-ui/app/chat/page.tsx +++ b/ee/tabby-ui/app/chat/page.tsx @@ -94,7 +94,7 @@ export default function ChatPage() { const updateActiveSelection = (ctx: Context | null) => { if (chatRef.current) { chatRef.current.updateActiveSelection(ctx) - } else { + } else if (ctx) { setPendingActiveSelection(ctx) } } @@ -146,9 +146,7 @@ export default function ChatPage() { document.documentElement.className = themeClass + ` client client-${client}` }, - updateActiveSelection: context => { - return updateActiveSelection(context) - } + updateActiveSelection }) useEffect(() => { @@ -255,7 +253,7 @@ export default function ChatPage() { const onChatLoaded = () => { pendingRelevantContexts.forEach(addRelevantContext) pendingMessages.forEach(sendMessage) - updateActiveSelection(pendingActiveSelection) + chatRef.current?.updateActiveSelection(pendingActiveSelection) clearPendingState() setChatLoaded(true) 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 a17d7c118e4a..5b2235141073 100644 --- a/ee/tabby-ui/app/files/components/chat-side-bar.tsx +++ b/ee/tabby-ui/app/files/components/chat-side-bar.tsx @@ -5,7 +5,6 @@ import { useClient } from 'tabby-chat-panel/react' import { useLatest } from '@/lib/hooks/use-latest' import { useMe } from '@/lib/hooks/use-me' -import { useStore } from '@/lib/hooks/use-store' import { filename2prism } from '@/lib/language-utils' import { useChatStore } from '@/lib/stores/chat-store' import { cn, formatLineHashForCodeBrowser } from '@/lib/utils' @@ -26,7 +25,7 @@ export const ChatSideBar: React.FC = ({ const [{ data }] = useMe() const { pendingEvent, setPendingEvent, repoMap, updateActivePath } = React.useContext(SourceCodeBrowserContext) - const activeChatId = useStore(useChatStore, state => state.activeChatId) + const activeChatId = useChatStore(state => state.activeChatId) const iframeRef = React.useRef(null) const repoMapRef = useLatest(repoMap) const onNavigate = async (context: Context) => { diff --git a/ee/tabby-ui/components/chat/chat-panel.tsx b/ee/tabby-ui/components/chat/chat-panel.tsx index 7b1c7e77ee7e..faf93ef75b17 100644 --- a/ee/tabby-ui/components/chat/chat-panel.tsx +++ b/ee/tabby-ui/components/chat/chat-panel.tsx @@ -1,11 +1,16 @@ import React, { RefObject } from 'react' import type { UseChatHelpers } from 'ai/react' +import { AnimatePresence, motion } from 'framer-motion' import type { Context } from 'tabby-chat-panel' +import { updateEnableActiveSelection } from '@/lib/stores/chat-actions' +import { useChatStore } from '@/lib/stores/chat-store' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { + IconEye, + IconEyeOff, IconRefresh, IconRemove, IconStop, @@ -54,6 +59,9 @@ function ChatPanelRenderer( removeRelevantContext, activeSelection } = React.useContext(ChatContext) + const enableActiveSelection = useChatStore( + state => state.enableActiveSelection + ) React.useImperativeHandle( ref, @@ -105,40 +113,81 @@ function ChatPanelRenderer( )}
- {(!!activeSelection || relevantContext.length > 0) && ( -
+
+ {activeSelection ? ( - - - - Current file - - - ) : null} - {relevantContext.map((item, idx) => { - return ( - - + + Current file + + + + ) : null} + {relevantContext.map((item, idx) => { + return ( + + + + + + ) })} -
- )} + +
+ {fileName} - {`:${line}`} + {line} ) } diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 6b52313e1a9e..de77667c018e 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -14,6 +14,7 @@ import { useDebounceCallback } from '@/lib/hooks/use-debounce' import { useLatest } from '@/lib/hooks/use-latest' import { useThreadRun } from '@/lib/hooks/use-thread-run' import { filename2prism } from '@/lib/language-utils' +import { useChatStore } from '@/lib/stores/chat-store' import { ExtendedCombinedError } from '@/lib/types' import { AssistantMessage, @@ -116,6 +117,9 @@ function ChatRenderer( const [activeSelection, setActiveSelection] = React.useState( null ) + const enableActiveSelection = useChatStore( + state => state.enableActiveSelection + ) const chatPanelRef = React.useRef(null) const { @@ -171,7 +175,8 @@ function ChatRenderer( ] setQaPairs(nextQaPairs) const [userMessage, threadRunOptions] = generateRequestPayload( - qaPair.user + qaPair.user, + enableActiveSelection ) return regenerate({ @@ -328,11 +333,16 @@ function ChatRenderer( }, [error]) const generateRequestPayload = ( - userMessage: UserMessage + userMessage: UserMessage, + enableActiveSelection?: boolean ): [CreateMessageInput, ThreadRunOptionsInput] => { - // use selectContext or activeContext for code query - const contextForCodeQuery: FileContext | undefined = - userMessage.selectContext || userMessage.activeContext + // use selectContext for code query by default + let contextForCodeQuery: FileContext | undefined = userMessage.selectContext + + // if enableActiveSelection, use selectContext or activeContext for code query + if (enableActiveSelection) { + contextForCodeQuery = contextForCodeQuery || userMessage.activeContext + } const codeQuery: InputMaybe = contextForCodeQuery ? { @@ -345,9 +355,11 @@ function ChatRenderer( } : null + const hasUsableActiveContext = + enableActiveSelection && !!userMessage.activeContext const fileContext: FileContext[] = uniqWith( compact([ - userMessage?.activeContext, + hasUsableActiveContext && userMessage.activeContext, ...(userMessage?.relevantContext || []) ]), isEqual @@ -391,6 +403,7 @@ function ChatRenderer( }\n${'```'}\n` } + const finalActiveContext = activeSelection || userMessage.activeContext const newUserMessage: UserMessage = { ...userMessage, message: userMessage.message + selectCodeSnippet, @@ -398,7 +411,10 @@ function ChatRenderer( id: userMessage.id ?? nanoid(), selectContext: userMessage.selectContext, // For forward compatibility - activeContext: activeSelection || userMessage.activeContext + activeContext: + enableActiveSelection && finalActiveContext + ? finalActiveContext + : undefined } const nextQaPairs = [ @@ -416,7 +432,9 @@ function ChatRenderer( setQaPairs(nextQaPairs) - sendUserMessage(...generateRequestPayload(newUserMessage)) + sendUserMessage( + ...generateRequestPayload(newUserMessage, enableActiveSelection) + ) } ) @@ -455,8 +473,15 @@ function ChatRenderer( onThreadUpdates?.(qaPairs) }, [qaPairs]) + const debouncedUpdateActiveSelection = useDebounceCallback( + (ctx: Context | null) => { + setActiveSelection(ctx) + }, + 300 + ) + const updateActiveSelection = (ctx: Context | null) => { - setActiveSelection(ctx) + debouncedUpdateActiveSelection.run(ctx) } React.useImperativeHandle( @@ -475,11 +500,8 @@ function ChatRenderer( ) React.useEffect(() => { - if (isOnLoadExecuted.current) return - - isOnLoadExecuted.current = true - onLoaded?.() setInitialzed(true) + onLoaded?.() }, []) const chatMaxWidthClass = maxWidth ? `max-w-${maxWidth}` : 'max-w-2xl' diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index 4c11d7d4b36b..719c53ea68c7 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -15,6 +15,8 @@ import { CircleAlert, CircleHelp, CirclePlay, + Eye, + EyeOff, Files, FileText, Filter, @@ -1692,6 +1694,16 @@ function IconPanelLeft({ return } +function IconEye({ className, ...props }: React.ComponentProps) { + return +} +function IconEyeOff({ + className, + ...props +}: React.ComponentProps) { + return +} + export { IconEdit, IconNextChat, @@ -1797,5 +1809,7 @@ export { IconSquareActivity, IconCircleAlert, IconCircleHelp, - IconPanelLeft + IconPanelLeft, + IconEye, + IconEyeOff } diff --git a/ee/tabby-ui/lib/hooks/use-debounce.ts b/ee/tabby-ui/lib/hooks/use-debounce.ts index 2b41222a6661..c5e6d95b6f6c 100644 --- a/ee/tabby-ui/lib/hooks/use-debounce.ts +++ b/ee/tabby-ui/lib/hooks/use-debounce.ts @@ -4,6 +4,8 @@ import { debounce, DebouncedFunc, type DebounceSettings } from 'lodash-es' import { useLatest } from './use-latest' import { useUnmount } from './use-unmount' +// import { useUnmount } from './use-unmount' + type noop = (...args: any[]) => any interface UseDebounceOptions extends DebounceSettings { diff --git a/ee/tabby-ui/lib/stores/chat-actions.ts b/ee/tabby-ui/lib/stores/chat-actions.ts index 17cd7b21b704..eb88174aad17 100644 --- a/ee/tabby-ui/lib/stores/chat-actions.ts +++ b/ee/tabby-ui/lib/stores/chat-actions.ts @@ -1,83 +1,15 @@ -import type { Chat, QuestionAnswerPair } from '@/lib/types' -import { nanoid } from '@/lib/utils' - import { useChatStore } from './chat-store' -const get = useChatStore.getState const set = useChatStore.setState -export const updateHybrated = (state: boolean) => { - set(() => ({ _hasHydrated: state })) -} export const setActiveChatId = (id: string) => { set(() => ({ activeChatId: id })) } -export const addChat = (_id?: string, title?: string) => { - const id = _id ?? nanoid() - set(state => ({ - activeChatId: id, - chats: [ - { - id, - title: title ?? '', - messages: [], - createdAt: new Date(), - userId: '', - path: '' - }, - ...(state.chats || []) - ] - })) -} - -export const deleteChat = (id: string) => { - set(state => { - return { - activeChatId: nanoid(), - chats: state.chats?.filter(chat => chat.id !== id) - } - }) -} - -export const clearChats = () => { - set(() => ({ - activeChatId: nanoid(), - chats: [] - })) -} - -export const updateMessages = (id: string, messages: QuestionAnswerPair[]) => { - set(state => ({ - chats: state.chats?.map(chat => { - if (chat.id === id) { - return { - ...chat, - messages - } - } - return chat - }) - })) -} - -export const updateChat = (id: string, chat: Partial) => { - set(state => ({ - chats: state.chats?.map(c => { - if (c.id === id) { - return { - ...c, - ...chat - } - } - return c - }) - })) +export const updateSelectedModel = (model: string | undefined) => { + set(() => ({ selectedModel: model })) } -export const updateSelectedModel = (model: string | undefined) => { - set(state => ({ - ...state, - selectedModel: model - })) +export const updateEnableActiveSelection = (enable: boolean) => { + set(() => ({ enableActiveSelection: enable })) } diff --git a/ee/tabby-ui/lib/stores/chat-store.ts b/ee/tabby-ui/lib/stores/chat-store.ts index cf3c959ae633..1c09b032c6b7 100644 --- a/ee/tabby-ui/lib/stores/chat-store.ts +++ b/ee/tabby-ui/lib/stores/chat-store.ts @@ -1,38 +1,27 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import { Chat } from '@/lib/types' import { nanoid } from '@/lib/utils' -const excludeFromState = ['_hasHydrated', 'setHasHydrated', 'activeChatId'] +const excludeFromState = ['activeChatId'] export interface ChatState { - chats: Chat[] | undefined activeChatId: string | undefined - _hasHydrated: boolean - setHasHydrated: (state: boolean) => void selectedModel: string | undefined + enableActiveSelection: boolean } -const initialState: Omit = { - _hasHydrated: false, - chats: undefined, +const initialState: ChatState = { activeChatId: nanoid(), - selectedModel: undefined + selectedModel: undefined, + enableActiveSelection: true } export const useChatStore = create()( persist( - set => { - return { - ...initialState, - setHasHydrated: (state: boolean) => { - set({ - _hasHydrated: state - }) - } - } - }, + () => ({ + ...initialState + }), { name: 'tabby-chat-storage', partialize: state => @@ -41,13 +30,8 @@ export const useChatStore = create()( ([key]) => !excludeFromState.includes(key) ) ), - onRehydrateStorage() { - return state => { - if (state) { - state.setHasHydrated(true) - } - } - } + // version for breaking change + version: 1 } ) )