Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tabby-ui): add mention functionality in tabby chat ui #3607

Merged
merged 24 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d0c81a
feat(chat): add mention functionality with category selection and sty…
Sma1lboy Dec 21, 2024
3eea537
feat(chat): implement mention functionality with category support and…
Sma1lboy Dec 24, 2024
d491b37
feat(chat): add input management methods to PromptFormRef interface
Sma1lboy Dec 24, 2024
a5f01c6
refactor(chat): remove unused FileList, CategoryMenu, mention compone…
Sma1lboy Dec 24, 2024
f9b5bc4
refactor(chat): Revert Webviewhelper to a previous version
Sma1lboy Dec 24, 2024
0468316
Merge branch 'main' into feat-at-function-in-tabby-ui
Sma1lboy Jan 9, 2025
96e6ecb
refactor(chat): remove unused mention components and styles
Sma1lboy Jan 10, 2025
7d10eed
refactor(chat): remove PopoverMentionList component and its associate…
Sma1lboy Jan 10, 2025
c6b2413
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 10, 2025
b0eac5f
refactor(chat): enhance at-mention handling and improve file item pro…
Sma1lboy Jan 13, 2025
5f0a8d2
refactor(chat): implement file mention functionality and enhance ment…
Sma1lboy Jan 13, 2025
088a9aa
chore: fix some potential normalize issue
Sma1lboy Jan 13, 2025
52de94d
refactor(chat): update chatInputRef type to use PromptFormRef for imp…
Sma1lboy Jan 13, 2025
08f3175
Merge branch 'main' into feat-at-function-in-tabby-ui
Sma1lboy Jan 14, 2025
f2aaba0
update
liangfung Jan 15, 2025
39daf96
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 15, 2025
6bedc1e
update
liangfung Jan 15, 2025
41685d8
fix(chat): update fileItemToSourceItem to handle filepath extraction …
Sma1lboy Jan 16, 2025
e7a0c11
update
liangfung Jan 17, 2025
db1fab5
Merge branch 'main' into feat-at-function-in-tabby-ui
liangfung Jan 17, 2025
2a00236
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2025
6187bff
update
liangfung Jan 17, 2025
2dd4430
updae
liangfung Jan 17, 2025
5087067
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ee/tabby-ui/app/(home)/components/thread-feeds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '@/components/ui/pagination'
import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton'
import { replaceAtMentionPlaceHolderWithAt } from '@/components/chat/form-editor/utils'
import LoadingWrapper from '@/components/loading-wrapper'
import { Mention } from '@/components/mention-tag'
import { UserAvatar } from '@/components/user-avatar'
Expand Down Expand Up @@ -329,7 +330,9 @@ function ThreadItem({ data }: ThreadItemProps) {
<ThreadTitleWithMentions
className="break-anywhere truncate text-lg font-medium"
sources={sources}
message={threadMessages?.[0]['node']['content']}
message={replaceAtMentionPlaceHolderWithAt(
threadMessages?.[0]?.['node']['content'] ?? ''
)}
/>
</LoadingWrapper>
</div>
Expand Down
23 changes: 22 additions & 1 deletion ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { MemoizedReactMarkdown } from '@/components/markdown'
import './page.css'

import { saveFetcherOptions } from '@/lib/tabby/token-management'
import { PromptFormRef } from '@/components/chat/form-editor/types'

const convertToHSLColor = (style: string) => {
return Color(style)
Expand Down Expand Up @@ -64,7 +65,7 @@ export default function ChatPage() {
const chatRef = useRef<ChatRef>(null)
const { width } = useWindowSize()
const prevWidthRef = useRef(width)
const chatInputRef = useRef<HTMLTextAreaElement>(null)
const chatInputRef = useRef<PromptFormRef>(null)

const searchParams = useSearchParams()
const client = searchParams.get('client') as ClientType
Expand All @@ -83,6 +84,9 @@ export default function ChatPage() {
supportsStoreAndFetchSessionState,
setSupportsStoreAndFetchSessionState
] = useState(false)
const [supportsListFileInWorkspace, setSupportProvideFileAtInfo] =
useState(false)
const [supportsReadFileContent, setSupportsReadFileContent] = useState(false)

const executeCommand = (command: ChatCommand) => {
if (chatRef.current) {
Expand Down Expand Up @@ -248,6 +252,13 @@ export default function ChatPage() {
server
?.hasCapability('readWorkspaceGitRepositories')
.then(setSupportsReadWorkspaceGitRepoInfo)
server
?.hasCapability('listFileInWorkspace')
.then(setSupportProvideFileAtInfo)
server
?.hasCapability('readFileContent')
.then(setSupportsReadFileContent)

Promise.all([
server?.hasCapability('fetchSessionState'),
server?.hasCapability('storeSessionState')
Expand Down Expand Up @@ -453,6 +464,16 @@ export default function ChatPage() {
storeSessionState={
supportsStoreAndFetchSessionState ? storeSessionState : undefined
}
listFileInWorkspace={
isInEditor && supportsListFileInWorkspace
? server?.listFileInWorkspace
: undefined
}
readFileContent={
isInEditor && supportsReadFileContent
? server?.readFileContent
: undefined
}
/>
</ErrorBoundary>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export function AssistantMessageSection({
setDevPanelOpen(true)
}}
highlightIndex={relevantCodeHighlightIndex}
supportsOpenInEditor={false}
/>
)}

Expand Down
94 changes: 73 additions & 21 deletions ee/tabby-ui/components/chat/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { RefObject, useMemo, useState } from 'react'
import slugify from '@sindresorhus/slugify'
import { Content } from '@tiptap/core'
import { useWindowSize } from '@uidotdev/usehooks'
import type { UseChatHelpers } from 'ai/react'
import { AnimatePresence, motion } from 'framer-motion'
Expand All @@ -8,13 +9,16 @@ import { toast } from 'sonner'

import { SLUG_TITLE_MAX_LENGTH } from '@/lib/constants'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { useLatest } from '@/lib/hooks/use-latest'
import { updateEnableActiveSelection } from '@/lib/stores/chat-actions'
import { useChatStore } from '@/lib/stores/chat-store'
import { useMutation } from '@/lib/tabby/gql'
import { setThreadPersistedMutation } from '@/lib/tabby/query'
import type { Context } from '@/lib/types'
import type { Context, FileContext } from '@/lib/types'
import {
cn,
convertEditorContext,
getFileLocationFromContext,
getTitleFromMessages,
resolveFileNameForDisplay
} from '@/lib/utils'
Expand All @@ -24,6 +28,7 @@ import {
IconCheck,
IconEye,
IconEyeOff,
IconFile,
IconFileText,
IconRefresh,
IconRemove,
Expand All @@ -32,55 +37,57 @@ import {
IconTrash
} from '@/components/ui/icons'
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
import { PromptForm, PromptFormRef } from '@/components/chat/prompt-form'
import { PromptForm } from '@/components/chat/prompt-form'
import { FooterText } from '@/components/footer'

import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
import { ChatContext } from './chat'
import { PromptFormRef } from './form-editor/types'
import { isSameEntireFileContextFromMention } from './form-editor/utils'
import { RepoSelect } from './repo-select'

export interface ChatPanelProps
extends Pick<UseChatHelpers, 'stop' | 'input' | 'setInput'> {
export interface ChatPanelProps extends Pick<UseChatHelpers, 'stop' | 'input'> {
setInput: (v: string) => void
id?: string
className?: string
onSubmit: (content: string) => Promise<any>
reload: () => void
chatMaxWidthClass: string
chatInputRef: RefObject<HTMLTextAreaElement>
chatInputRef: RefObject<PromptFormRef>
}

export interface ChatPanelRef {
focus: () => void
setInput: (input: Content) => void
input: string
}

function ChatPanelRenderer(
{
stop,
reload,
input,
setInput,
className,
onSubmit,
chatMaxWidthClass,
chatInputRef
}: ChatPanelProps,
ref: React.Ref<ChatPanelRef>
) {
const promptFormRef = React.useRef<PromptFormRef>(null)
const {
threadId,
container,
onClearMessages,
qaPairs,
isLoading,
relevantContext,
removeRelevantContext,
activeSelection,
onCopyContent,
selectedRepoId,
setSelectedRepoId,
repos,
initialized
initialized,
setRelevantContext,
openInEditor
} = React.useContext(ChatContext)
const enableActiveSelection = useChatStore(
state => state.enableActiveSelection
Expand Down Expand Up @@ -135,6 +142,39 @@ function ChatPanelRenderer(
}
}

const removeRelevantContext = useLatest((idx: number) => {
const editor = chatInputRef.current?.editor
if (!editor) {
return
}

const { state, view } = editor
const { tr } = state
const positionsToDelete: any[] = []

const currentContext: FileContext = relevantContext[idx]
state.doc.descendants((node, pos) => {
if (node.type.name === 'mention' && node.attrs.category === 'file') {
const fileContext = convertEditorContext({
filepath: node.attrs.fileItem.filepath,
content: '',
kind: 'file'
})
if (isSameEntireFileContextFromMention(fileContext, currentContext)) {
positionsToDelete.push({ from: pos, to: pos + node.nodeSize })
}
}
})

setRelevantContext(prev => prev.filter((item, index) => index !== idx))
positionsToDelete.reverse().forEach(({ from, to }) => {
tr.delete(from, to)
})

view.dispatch(tr)
editor.commands.focus()
})

const onSelectRepo = (sourceId: string | undefined) => {
setSelectedRepoId(sourceId)

Expand All @@ -148,11 +188,15 @@ function ChatPanelRenderer(
() => {
return {
focus: () => {
promptFormRef.current?.focus()
}
chatInputRef.current?.focus()
},
setInput: str => {
chatInputRef.current?.setInput(str)
},
input: chatInputRef.current?.input ?? ''
}
},
[]
[chatInputRef]
)

return (
Expand Down Expand Up @@ -236,7 +280,10 @@ function ChatPanelRenderer(
</Tooltip>
)}
</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">
<div
id="chat-panel-container"
className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4"
>
<div className="flex flex-wrap gap-2">
<AnimatePresence presenceAffectsLayout>
<RepoSelect
Expand Down Expand Up @@ -304,14 +351,23 @@ function ChatPanelRenderer(
>
<Badge
variant="outline"
className="inline-flex h-7 flex-nowrap items-center gap-1 overflow-hidden rounded-md pr-0 text-sm font-semibold"
className={cn(
'inline-flex h-7 cursor-pointer flex-nowrap items-center gap-1 overflow-hidden rounded-md pr-0 text-sm font-semibold'
)}
onClick={() => {
openInEditor(getFileLocationFromContext(item))
}}
>
<IconFile className="shrink-0" />
<ContextLabel context={item} />
<Button
size="icon"
variant="ghost"
className="h-7 w-7 shrink-0 rounded-l-none hover:bg-muted/50"
onClick={removeRelevantContext.bind(null, idx)}
onClick={e => {
e.stopPropagation()
removeRelevantContext.current(idx)
}}
>
<IconRemove />
</Button>
Expand All @@ -322,13 +378,9 @@ function ChatPanelRenderer(
</AnimatePresence>
</div>
<PromptForm
ref={promptFormRef}
ref={chatInputRef}
onSubmit={onSubmit}
input={input}
setInput={setInput}
isLoading={isLoading}
chatInputRef={chatInputRef}
isInitializing={!initialized}
/>
<FooterText className="hidden sm:block" />
</div>
Expand Down
Loading
Loading