-
+
{index < messages.length - 1 && (
)}
diff --git a/ee/tabby-ui/components/chat-message-actions.tsx b/ee/tabby-ui/components/chat-message-actions.tsx
index d4e4b40..8e345a8 100644
--- a/ee/tabby-ui/components/chat-message-actions.tsx
+++ b/ee/tabby-ui/components/chat-message-actions.tsx
@@ -3,17 +3,26 @@
import { type Message } from 'ai'
import { Button } from '@/components/ui/button'
-import { IconCheck, IconCopy } from '@/components/ui/icons'
+import {
+ IconCheck,
+ IconCopy,
+ IconEdit,
+ IconRefresh,
+ IconTrash
+} from '@/components/ui/icons'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { cn } from '@/lib/utils'
+import { MessageActionType } from '@/lib/types'
interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
message: Message
+ handleMessageAction: (messageId: string, action: MessageActionType) => void
}
export function ChatMessageActions({
message,
className,
+ handleMessageAction,
...props
}: ChatMessageActionsProps) {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
@@ -26,11 +35,38 @@ export function ChatMessageActions({
return (
+ {message.role === 'user' ? (
+
+ ) : (
+
+ )}
+
)
diff --git a/ee/tabby-ui/components/chat-panel.tsx b/ee/tabby-ui/components/chat-panel.tsx
index 13e3c2f..35975b2 100644
--- a/ee/tabby-ui/components/chat-panel.tsx
+++ b/ee/tabby-ui/components/chat-panel.tsx
@@ -1,10 +1,11 @@
-import { type UseChatHelpers } from 'ai/react'
-
+import React from 'react'
import { Button } from '@/components/ui/button'
-import { PromptForm } from '@/components/prompt-form'
+import { PromptForm, PromptFormRef } from '@/components/prompt-form'
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
import { IconRefresh, IconStop } from '@/components/ui/icons'
import { FooterText } from '@/components/footer'
+import { cn } from '@/lib/utils'
+import type { UseChatHelpers } from 'ai/react'
export interface ChatPanelProps
extends Pick<
@@ -18,6 +19,8 @@ export interface ChatPanelProps
| 'setInput'
> {
id?: string
+ className?: string
+ onSubmit: (content: string) => Promise
}
export function ChatPanel({
@@ -28,10 +31,22 @@ export function ChatPanel({
reload,
input,
setInput,
- messages
+ messages,
+ className,
+ onSubmit
}: ChatPanelProps) {
+ const promptFormRef = React.useRef(null)
+ React.useEffect(() => {
+ promptFormRef?.current?.focus()
+ }, [id])
+
return (
-
+
@@ -57,15 +72,10 @@ export function ChatPanel({
)
)}
-
+
{
- await append({
- id,
- content: value,
- role: 'user'
- })
- }}
+ ref={promptFormRef}
+ onSubmit={onSubmit}
input={input}
setInput={setInput}
isLoading={isLoading}
diff --git a/ee/tabby-ui/components/chat-sessions.tsx b/ee/tabby-ui/components/chat-sessions.tsx
new file mode 100644
index 0000000..28d2ec2
--- /dev/null
+++ b/ee/tabby-ui/components/chat-sessions.tsx
@@ -0,0 +1,125 @@
+'use client'
+
+import React from 'react'
+import { cn, nanoid } from '@/lib/utils'
+import { useChatStore } from '@/lib/stores/chat-store'
+import {
+ clearChats,
+ deleteChat,
+ setActiveChatId
+} from '@/lib/stores/chat-actions'
+import { IconPlus, IconTrash } from '@/components/ui/icons'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger
+} from '@/components/ui/tooltip'
+import { EditChatTitleDialog } from '@/components/edit-chat-title-dialog'
+import { useStore } from '@/lib/hooks/use-store'
+import { Button } from '@/components/ui/button'
+import { ListSkeleton } from '@/components/skeleton'
+import { Separator } from '@/components/ui/separator'
+import { ClearChatsButton } from './clear-chats-button'
+
+interface ChatSessionsProps {
+ className?: string
+}
+
+export const ChatSessions = ({ className }: ChatSessionsProps) => {
+ const _hasHydrated = useStore(useChatStore, state => state._hasHydrated)
+ const chats = useStore(useChatStore, state => state.chats)
+ const activeChatId = useStore(useChatStore, state => state.activeChatId)
+
+ const onDeleteClick = (
+ e: React.MouseEvent,
+ chatId: string
+ ) => {
+ deleteChat(chatId)
+ }
+
+ const onNewChatClick = (e: React.MouseEvent) => {
+ setActiveChatId(nanoid())
+ }
+
+ const handleClearChats = () => {
+ clearChats()
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {!_hasHydrated ? (
+
+ ) : (
+ <>
+ {chats?.map(chat => {
+ const isActive = activeChatId === chat.id
+ return (
+
setActiveChatId(chat.id)}
+ className={cn(
+ 'hover:bg-accent flex cursor-pointer items-center justify-between gap-3 rounded-lg px-3 py-2 text-zinc-900 transition-all hover:text-zinc-900 dark:text-zinc-50 hover:dark:bg-zinc-900 dark:hover:text-zinc-50',
+ isActive && '!bg-zinc-200 dark:!bg-zinc-800'
+ )}
+ >
+
+ {chat.title || '(Untitled)'}
+
+ {isActive && (
+
e.stopPropagation()}
+ >
+
+
+
+
+
+
+ Delete
+
+
+
+ )}
+
+ )
+ })}
+ >
+ )}
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/ee/tabby-ui/components/chat.tsx b/ee/tabby-ui/components/chat.tsx
index 2405191..ef81831 100644
--- a/ee/tabby-ui/components/chat.tsx
+++ b/ee/tabby-ui/components/chat.tsx
@@ -1,22 +1,31 @@
'use client'
-import { useChat, type Message } from 'ai/react'
-
-import { cn } from '@/lib/utils'
+import React from 'react'
+import { useChat } from 'ai/react'
+import { cn, nanoid, truncateText } from '@/lib/utils'
import { ChatList } from '@/components/chat-list'
import { ChatPanel } from '@/components/chat-panel'
import { EmptyScreen } from '@/components/empty-screen'
import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
import { toast } from 'react-hot-toast'
import { usePatchFetch } from '@/lib/hooks/use-patch-fetch'
+import { addChat, updateMessages } from '@/lib/stores/chat-actions'
+import { find, findIndex } from 'lodash-es'
+import { useStore } from '@/lib/hooks/use-store'
+import { useChatStore } from '@/lib/stores/chat-store'
+import { ListSkeleton } from '@/components/skeleton'
+import type { MessageActionType } from '@/lib/types'
+import type { Message } from 'ai/react'
export interface ChatProps extends React.ComponentProps<'div'> {
initialMessages?: Message[]
id?: string
+ loading?: boolean
}
-export function Chat({ id, initialMessages, className }: ChatProps) {
+export function Chat({ id, initialMessages, loading, className }: ChatProps) {
usePatchFetch()
+ const chats = useStore(useChatStore, state => state.chats)
const {
messages,
@@ -39,28 +48,118 @@ export function Chat({ id, initialMessages, className }: ChatProps) {
}
}
})
+
+ const [selectedMessageId, setSelectedMessageId] = React.useState()
+
+ const onRegenerateResponse = (messageId: string) => {
+ const messageIndex = findIndex(messages, { id: messageId })
+ const prevMessage = messages?.[messageIndex - 1]
+ if (prevMessage?.role === 'user') {
+ setMessages(messages.slice(0, messageIndex - 1))
+ append(prevMessage)
+ }
+ }
+
+ const onDeleteMessage = (messageId: string) => {
+ const message = find(messages, { id: messageId })
+ if (message) {
+ setMessages(messages.filter(m => m.id !== messageId))
+ }
+ }
+
+ const onEditMessage = (messageId: string) => {
+ const message = find(messages, { id: messageId })
+ if (message) {
+ setInput(message.content)
+ setSelectedMessageId(messageId)
+ }
+ }
+
+ const handleMessageAction = (
+ messageId: string,
+ actionType: MessageActionType
+ ) => {
+ switch (actionType) {
+ case 'edit':
+ onEditMessage(messageId)
+ break
+ case 'delete':
+ onDeleteMessage(messageId)
+ break
+ case 'regenerate':
+ onRegenerateResponse(messageId)
+ break
+ default:
+ break
+ }
+ }
+
+ const handleSubmit = async (value: string) => {
+ if (findIndex(chats, { id }) === -1) {
+ addChat(id, truncateText(value))
+ } else if (selectedMessageId) {
+ let messageIdx = findIndex(messages, { id: selectedMessageId })
+ setMessages(messages.slice(0, messageIdx))
+ setSelectedMessageId(undefined)
+ }
+ await append({
+ id: nanoid(),
+ content: value,
+ role: 'user'
+ })
+ }
+
+ React.useEffect(() => {
+ if (id) {
+ updateMessages(id, messages)
+ }
+ }, [messages])
+
+ React.useEffect(() => {
+ const scrollHeight = document.documentElement.scrollHeight
+ window.scrollTo(0, scrollHeight)
+
+ return () => stop()
+ }, [])
+
return (
- <>
-
- {messages.length ? (
- <>
-
-
- >
- ) : (
-
- )}
+
+
+
+ {loading ? (
+
+ ) : messages.length ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
-
- >
+
)
}
diff --git a/ee/tabby-ui/components/clear-chats-button.tsx b/ee/tabby-ui/components/clear-chats-button.tsx
new file mode 100644
index 0000000..606804a
--- /dev/null
+++ b/ee/tabby-ui/components/clear-chats-button.tsx
@@ -0,0 +1,59 @@
+import React from 'react'
+import { Button, ButtonProps } from '@/components/ui/button'
+import { IconCheck, IconTrash } from '@/components/ui/icons'
+
+interface ClearChatsButtonProps extends ButtonProps {
+ onClear: () => void
+}
+
+export const ClearChatsButton = ({
+ onClear,
+ onClick,
+ onBlur,
+ ...rest
+}: ClearChatsButtonProps) => {
+ const [waitingConfirmation, setWaitingConfirmation] = React.useState(false)
+
+ const cancelConfirmation = () => {
+ setWaitingConfirmation(false)
+ }
+
+ const handleBlur: React.FocusEventHandler
= e => {
+ if (waitingConfirmation) {
+ cancelConfirmation()
+ }
+ onBlur?.(e)
+ }
+
+ const handleClick: React.MouseEventHandler = e => {
+ if (!waitingConfirmation) {
+ setWaitingConfirmation(true)
+ } else {
+ onClear()
+ setWaitingConfirmation(false)
+ }
+ onClick?.(e)
+ }
+
+ return (
+
+ )
+}
diff --git a/ee/tabby-ui/components/edit-chat-title-dialog.tsx b/ee/tabby-ui/components/edit-chat-title-dialog.tsx
new file mode 100644
index 0000000..4f3aeae
--- /dev/null
+++ b/ee/tabby-ui/components/edit-chat-title-dialog.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import React from 'react'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription
+} from '@/components/ui/dialog'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger
+} from '@/components/ui/tooltip'
+import { IconArrowElbow, IconEdit, IconTrash } from '@/components/ui/icons'
+
+import { Input } from '@/components/ui/input'
+import { updateChat } from '@/lib/stores/chat-actions'
+import { Button } from './ui/button'
+
+interface EditChatTitleDialogProps {
+ initialValue: string | undefined
+ chatId: string
+ children?: React.ReactNode
+}
+
+export const EditChatTitleDialog = ({
+ children,
+ initialValue,
+ chatId
+}: EditChatTitleDialogProps) => {
+ const [open, setOpen] = React.useState(false)
+ const formRef = React.useRef(null)
+ const [input, setInput] = React.useState(initialValue)
+
+ const handleSubmit: React.FormEventHandler = async e => {
+ e.preventDefault()
+ if (!input?.trim()) {
+ return
+ }
+ updateChat(chatId, { title: input })
+ setOpen(false)
+ }
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ formRef.current?.requestSubmit()
+ e.preventDefault()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/ee/tabby-ui/components/header.tsx b/ee/tabby-ui/components/header.tsx
index d83584c..c46ffa9 100644
--- a/ee/tabby-ui/components/header.tsx
+++ b/ee/tabby-ui/components/header.tsx
@@ -24,7 +24,7 @@ export function Header() {
const newVersionAvailable = isNewVersionAvailable(version, latestRelease)
return (
-
+
diff --git a/ee/tabby-ui/components/prompt-form.tsx b/ee/tabby-ui/components/prompt-form.tsx
index 165c4c9..dcbe38a 100644
--- a/ee/tabby-ui/components/prompt-form.tsx
+++ b/ee/tabby-ui/components/prompt-form.tsx
@@ -32,12 +32,14 @@ export interface PromptProps
isLoading: boolean
}
-export function PromptForm({
- onSubmit,
- input,
- setInput,
- isLoading
-}: PromptProps) {
+export interface PromptFormRef {
+ focus: () => void
+}
+
+function PromptFormRenderer(
+ { onSubmit, input, setInput, isLoading }: PromptProps,
+ ref: React.ForwardedRef
+) {
const { formRef, onKeyDown } = useEnterSubmit()
const [queryCompletionUrl, setQueryCompletionUrl] = React.useState<
string | null
@@ -62,6 +64,24 @@ export function PromptForm({
}
})
+ React.useImperativeHandle(ref, () => {
+ return {
+ focus: () => {
+ inputRef.current?.focus()
+ }
+ }
+ })
+
+ React.useEffect(() => {
+ if (
+ input &&
+ inputRef.current &&
+ inputRef.current !== document.activeElement
+ ) {
+ inputRef.current.focus()
+ }
+ }, [input])
+
React.useLayoutEffect(() => {
if (nextInputSelectionRange.current?.length) {
inputRef.current?.setSelectionRange?.(
@@ -268,6 +288,10 @@ export function PromptForm({
)
}
+export const PromptForm = React.forwardRef(
+ PromptFormRenderer
+)
+
/**
* Retrieves the name of the completion query from a given string@.
* @param {string} val - The input string to search for the completion query name.
diff --git a/ee/tabby-ui/components/skeleton.tsx b/ee/tabby-ui/components/skeleton.tsx
new file mode 100644
index 0000000..60f0673
--- /dev/null
+++ b/ee/tabby-ui/components/skeleton.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+export const ListSkeleton = () => {
+ return (
+
+ )
+}
diff --git a/ee/tabby-ui/lib/hooks/use-hydration.ts b/ee/tabby-ui/lib/hooks/use-hydration.ts
new file mode 100644
index 0000000..3823487
--- /dev/null
+++ b/ee/tabby-ui/lib/hooks/use-hydration.ts
@@ -0,0 +1,14 @@
+import React from 'react'
+
+let hydrating = true
+
+export function useHydrated() {
+ let [hydrated, setHydrated] = React.useState(() => !hydrating)
+
+ React.useEffect(function hydrate() {
+ hydrating = false
+ setHydrated(true)
+ }, [])
+
+ return hydrated
+}
diff --git a/ee/tabby-ui/lib/hooks/use-store.ts b/ee/tabby-ui/lib/hooks/use-store.ts
new file mode 100644
index 0000000..06bceaf
--- /dev/null
+++ b/ee/tabby-ui/lib/hooks/use-store.ts
@@ -0,0 +1,15 @@
+import React from 'react'
+
+export const useStore = (
+ store: (callback: (state: T) => unknown) => unknown,
+ callback: (state: T) => F
+) => {
+ const result = store(callback) as F
+ const [data, setData] = React.useState()
+
+ React.useEffect(() => {
+ setData(result)
+ }, [result])
+
+ return data
+}
diff --git a/ee/tabby-ui/lib/stores/chat-actions.ts b/ee/tabby-ui/lib/stores/chat-actions.ts
new file mode 100644
index 0000000..b4cdb31
--- /dev/null
+++ b/ee/tabby-ui/lib/stores/chat-actions.ts
@@ -0,0 +1,76 @@
+import { Message } from 'ai'
+import { nanoid } from '@/lib/utils'
+import type { Chat } from '@/lib/types'
+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: Message[]) => {
+ 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
+ })
+ }))
+}
diff --git a/ee/tabby-ui/lib/stores/chat-store.ts b/ee/tabby-ui/lib/stores/chat-store.ts
new file mode 100644
index 0000000..052bc34
--- /dev/null
+++ b/ee/tabby-ui/lib/stores/chat-store.ts
@@ -0,0 +1,50 @@
+import { Chat } from '@/lib/types'
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+import { nanoid } from '@/lib/utils'
+
+const excludeFromState = ['_hasHydrated', 'setHasHydrated']
+
+export interface ChatState {
+ chats: Chat[] | undefined
+ activeChatId: string | undefined
+ _hasHydrated: boolean
+ setHasHydrated: (state: boolean) => void
+}
+
+const initialState: Omit = {
+ _hasHydrated: false,
+ chats: undefined,
+ activeChatId: nanoid()
+}
+
+export const useChatStore = create()(
+ persist(
+ set => {
+ return {
+ ...initialState,
+ setHasHydrated: (state: boolean) => {
+ set({
+ _hasHydrated: state
+ })
+ }
+ }
+ },
+ {
+ name: 'tabby-chat-storage',
+ partialize: state =>
+ Object.fromEntries(
+ Object.entries(state).filter(
+ ([key]) => !excludeFromState.includes(key)
+ )
+ ),
+ onRehydrateStorage() {
+ return state => {
+ if (state) {
+ state.setHasHydrated(true)
+ }
+ }
+ }
+ }
+ )
+)
diff --git a/ee/tabby-ui/lib/stores/utils.ts b/ee/tabby-ui/lib/stores/utils.ts
new file mode 100644
index 0000000..9f14100
--- /dev/null
+++ b/ee/tabby-ui/lib/stores/utils.ts
@@ -0,0 +1,9 @@
+import { Chat } from '@/lib/types'
+
+export const getChatById = (
+ chats: Chat[] | undefined,
+ chatId: string | undefined
+): Chat | undefined => {
+ if (!Array.isArray(chats) || !chatId) return undefined
+ return chats.find(c => c.id === chatId)
+}
diff --git a/ee/tabby-ui/lib/types/chat.ts b/ee/tabby-ui/lib/types/chat.ts
new file mode 100644
index 0000000..acb0972
--- /dev/null
+++ b/ee/tabby-ui/lib/types/chat.ts
@@ -0,0 +1,29 @@
+import { type Message } from 'ai'
+
+export interface Chat extends Record {
+ id: string
+ title: string
+ createdAt: Date
+ userId: string
+ path: string
+ messages: Message[]
+ sharePath?: string
+}
+
+export type ISearchHit = {
+ id: number
+ doc?: {
+ body?: string
+ name?: string
+ filepath?: string
+ git_url?: string
+ kind?: string
+ language?: string
+ }
+}
+export type SearchReponse = {
+ hits?: Array
+ num_hits?: number
+}
+
+export type MessageActionType = 'edit' | 'delete' | 'regenerate'
diff --git a/ee/tabby-ui/lib/types/common.ts b/ee/tabby-ui/lib/types/common.ts
index 6f86a9c..ca3e53a 100644
--- a/ee/tabby-ui/lib/types/common.ts
+++ b/ee/tabby-ui/lib/types/common.ts
@@ -1,15 +1,3 @@
-import { type Message } from 'ai'
-
-export interface Chat extends Record {
- id: string
- title: string
- createdAt: Date
- userId: string
- path: string
- messages: Message[]
- sharePath?: string
-}
-
export type ServerActionResult = Promise<
| Result
| {
diff --git a/ee/tabby-ui/lib/types/index.ts b/ee/tabby-ui/lib/types/index.ts
index a022027..fa2431a 100644
--- a/ee/tabby-ui/lib/types/index.ts
+++ b/ee/tabby-ui/lib/types/index.ts
@@ -1,2 +1,2 @@
export * from './common'
-export * from './search'
+export * from './chat'
diff --git a/ee/tabby-ui/lib/types/search.ts b/ee/tabby-ui/lib/types/search.ts
deleted file mode 100644
index b030134..0000000
--- a/ee/tabby-ui/lib/types/search.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export type ISearchHit = {
- id: number
- doc?: {
- body?: string
- name?: string
- filepath?: string
- git_url?: string
- kind?: string
- language?: string
- }
-}
-export type SearchReponse = {
- hits?: Array
- num_hits?: number
-}
diff --git a/ee/tabby-ui/lib/utils.ts b/ee/tabby-ui/lib/utils.ts
index cea3d17..bfe780c 100644
--- a/ee/tabby-ui/lib/utils.ts
+++ b/ee/tabby-ui/lib/utils.ts
@@ -41,3 +41,30 @@ export function formatDate(input: string | number | Date): string {
year: 'numeric'
})
}
+
+export function truncateText(
+ text: string,
+ maxLength = 50,
+ delimiters = /[ ,.:;\n,。:;]/
+) {
+ if (!text) return ''
+ if (text.length <= maxLength) {
+ return text
+ }
+
+ let truncatedText = text.slice(0, maxLength)
+
+ let lastDelimiterIndex = -1
+ for (let i = maxLength - 1; i >= 0; i--) {
+ if (delimiters.test(truncatedText[i])) {
+ lastDelimiterIndex = i
+ break
+ }
+ }
+
+ if (lastDelimiterIndex !== -1) {
+ truncatedText = truncatedText.slice(0, lastDelimiterIndex)
+ }
+
+ return truncatedText + '...'
+}
diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json
index 9410ddd..f8b81a2 100644
--- a/ee/tabby-ui/package.json
+++ b/ee/tabby-ui/package.json
@@ -48,7 +48,8 @@
"react-textarea-autosize": "^8.4.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
- "swr": "^2.2.4"
+ "swr": "^2.2.4",
+ "zustand": "^4.4.6"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock
index 940998d..a2ba9be 100644
--- a/ee/tabby-ui/yarn.lock
+++ b/ee/tabby-ui/yarn.lock
@@ -4430,7 +4430,7 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"
-use-sync-external-store@^1.2.0:
+use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
@@ -4591,6 +4591,13 @@ zod@3.21.4:
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
+zustand@^4.4.6:
+ version "4.4.6"
+ resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.6.tgz#03c78e3e2686c47095c93714c0c600b72a6512bd"
+ integrity sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==
+ dependencies:
+ use-sync-external-store "1.2.0"
+
zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"