Skip to content

Commit

Permalink
feat(chat-ui): support toggling active selection collection in chat s…
Browse files Browse the repository at this point in the history
…idebar (#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>
  • Loading branch information
liangfung and autofix-ci[bot] authored Nov 15, 2024
1 parent e9b2ded commit 41785e0
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 149 deletions.
13 changes: 12 additions & 1 deletion clients/vscode/src/chat/WebviewHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export class WebviewHelper {

public async syncActiveSelection(editor: TextEditor | undefined) {
if (!editor) {
this.syncActiveSelectionToChatPanel(null);
return;
}

Expand All @@ -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);
Expand Down
8 changes: 3 additions & 5 deletions ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -146,9 +146,7 @@ export default function ChatPage() {
document.documentElement.className =
themeClass + ` client client-${client}`
},
updateActiveSelection: context => {
return updateActiveSelection(context)
}
updateActiveSelection
})

useEffect(() => {
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions ee/tabby-ui/app/files/components/chat-side-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -26,7 +25,7 @@ export const ChatSideBar: React.FC<ChatSideBarProps> = ({
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<HTMLIFrameElement>(null)
const repoMapRef = useLatest(repoMap)
const onNavigate = async (context: Context) => {
Expand Down
107 changes: 78 additions & 29 deletions ee/tabby-ui/components/chat/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -54,6 +59,9 @@ function ChatPanelRenderer(
removeRelevantContext,
activeSelection
} = React.useContext(ChatContext)
const enableActiveSelection = useChatStore(
state => state.enableActiveSelection
)

React.useImperativeHandle(
ref,
Expand Down Expand Up @@ -105,40 +113,81 @@ function ChatPanelRenderer(
)}
</div>
<div className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4">
{(!!activeSelection || relevantContext.length > 0) && (
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2">
<AnimatePresence presenceAffectsLayout>
{activeSelection ? (
<Badge
variant="outline"
key={`${activeSelection.filepath}_active_selection`}
className="inline-flex flex-nowrap items-center gap-1.5 overflow-hidden rounded text-sm font-semibold"
<motion.div
initial={{ opacity: 0, scale: 0.9, y: -5 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
ease: 'easeInOut',
duration: 0.1
}}
exit={{ opacity: 0, scale: 0.9, y: 5 }}
>
<ContextLabel
context={activeSelection}
className="flex-1 truncate"
/>
<span className="shrink-0 text-muted-foreground">
Current file
</span>
</Badge>
) : null}
{relevantContext.map((item, idx) => {
return (
<Badge
variant="outline"
key={item.filepath + idx}
className="inline-flex flex-nowrap items-center gap-0.5 overflow-hidden rounded text-sm font-semibold"
className={cn(
'inline-flex h-7 flex-nowrap items-center gap-1.5 overflow-hidden rounded-md pr-0 text-sm font-semibold',
{
'border-dashed !text-muted-foreground italic line-through':
!enableActiveSelection
}
)}
>
<ContextLabel context={item} />
<IconRemove
className="shrink-0 cursor-pointer text-muted-foreground transition-all hover:text-red-300"
onClick={removeRelevantContext.bind(null, idx)}
<ContextLabel
context={activeSelection}
className="flex-1 truncate"
/>
<span className="shrink-0 text-muted-foreground">
Current file
</span>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 shrink-0 rounded-l-none"
onClick={e => {
updateEnableActiveSelection(!enableActiveSelection)
}}
>
{enableActiveSelection ? <IconEye /> : <IconEyeOff />}
</Button>
</Badge>
</motion.div>
) : null}
{relevantContext.map((item, idx) => {
return (
<motion.div
// `filepath + range` as unique key
key={item.filepath + item.range.start + item.range.end}
initial={{ opacity: 0, scale: 0.9, y: -5 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{
ease: 'easeInOut',
duration: 0.1
}}
exit={{ opacity: 0, scale: 0.9, y: 5 }}
layout
>
<Badge
variant="outline"
className="inline-flex h-7 flex-nowrap items-center gap-1 overflow-hidden rounded-md pr-0 text-sm font-semibold"
>
<ContextLabel context={item} />
<Button
size="icon"
variant="ghost"
className="h-7 w-7 shrink-0 rounded-l-none"
onClick={removeRelevantContext.bind(null, idx)}
>
<IconRemove />
</Button>
</Badge>
</motion.div>
)
})}
</div>
)}
</AnimatePresence>
</div>
<PromptForm
ref={promptFormRef}
onSubmit={onSubmit}
Expand Down Expand Up @@ -168,13 +217,13 @@ function ContextLabel({
const [fileName] = context.filepath.split('/').slice(-1)
const line =
context.range.start === context.range.end
? `${context.range.start}`
: `${context.range.start}-${context.range.end}`
? `:${context.range.start}`
: `:${context.range.start}-${context.range.end}`

return (
<span className={cn('truncate text-foreground', className)}>
<span className={cn('truncate', className)}>
{fileName}
<span className="text-muted-foreground">{`:${line}`}</span>
<span className="text-muted-foreground">{line}</span>
</span>
)
}
48 changes: 35 additions & 13 deletions ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -116,6 +117,9 @@ function ChatRenderer(
const [activeSelection, setActiveSelection] = React.useState<Context | null>(
null
)
const enableActiveSelection = useChatStore(
state => state.enableActiveSelection
)
const chatPanelRef = React.useRef<ChatPanelRef>(null)

const {
Expand Down Expand Up @@ -171,7 +175,8 @@ function ChatRenderer(
]
setQaPairs(nextQaPairs)
const [userMessage, threadRunOptions] = generateRequestPayload(
qaPair.user
qaPair.user,
enableActiveSelection
)

return regenerate({
Expand Down Expand Up @@ -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<CodeQueryInput> = contextForCodeQuery
? {
Expand All @@ -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
Expand Down Expand Up @@ -391,14 +403,18 @@ function ChatRenderer(
}\n${'```'}\n`
}

const finalActiveContext = activeSelection || userMessage.activeContext
const newUserMessage: UserMessage = {
...userMessage,
message: userMessage.message + selectCodeSnippet,
// If no id is provided, set a fallback id.
id: userMessage.id ?? nanoid(),
selectContext: userMessage.selectContext,
// For forward compatibility
activeContext: activeSelection || userMessage.activeContext
activeContext:
enableActiveSelection && finalActiveContext
? finalActiveContext
: undefined
}

const nextQaPairs = [
Expand All @@ -416,7 +432,9 @@ function ChatRenderer(

setQaPairs(nextQaPairs)

sendUserMessage(...generateRequestPayload(newUserMessage))
sendUserMessage(
...generateRequestPayload(newUserMessage, enableActiveSelection)
)
}
)

Expand Down Expand Up @@ -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(
Expand All @@ -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'
Expand Down
16 changes: 15 additions & 1 deletion ee/tabby-ui/components/ui/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
CircleAlert,
CircleHelp,
CirclePlay,
Eye,
EyeOff,
Files,
FileText,
Filter,
Expand Down Expand Up @@ -1692,6 +1694,16 @@ function IconPanelLeft({
return <PanelLeft className={cn('h-4 w-4', className)} {...props} />
}

function IconEye({ className, ...props }: React.ComponentProps<typeof Eye>) {
return <Eye className={cn('h-4 w-4', className)} {...props} />
}
function IconEyeOff({
className,
...props
}: React.ComponentProps<typeof EyeOff>) {
return <EyeOff className={cn('h-4 w-4', className)} {...props} />
}

export {
IconEdit,
IconNextChat,
Expand Down Expand Up @@ -1797,5 +1809,7 @@ export {
IconSquareActivity,
IconCircleAlert,
IconCircleHelp,
IconPanelLeft
IconPanelLeft,
IconEye,
IconEyeOff
}
2 changes: 2 additions & 0 deletions ee/tabby-ui/lib/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends noop> extends DebounceSettings {
Expand Down
Loading

0 comments on commit 41785e0

Please sign in to comment.