diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.test.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.test.tsx
index c95da914201..e4b3856a5d3 100644
--- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.test.tsx
+++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.test.tsx
@@ -2,6 +2,7 @@
* @vitest-environment jsdom
*/
import { act } from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { OrganizationChat } from '@/app/o/[organizationId]/components/organization-sidebar/hooks'
@@ -9,7 +10,16 @@ import type { OrganizationChat } from '@/app/o/[organizationId]/components/organ
const hoverState = vi.hoisted(() => ({ isOpen: false }))
vi.mock('next/link', () => ({
- default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => (
+ default: ({
+ href,
+ children,
+ prefetch: _prefetch,
+ ...props
+ }: {
+ href: string
+ children: React.ReactNode
+ prefetch?: boolean
+ }) => (
{children}
@@ -36,6 +46,8 @@ const CHATS: OrganizationChat[] = Array.from({ length: 8 }, (_, index) => ({
let container: HTMLDivElement
let root: Root
+let queryClient: QueryClient
+let prefetchQuery: ReturnType
beforeEach(() => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
@@ -48,6 +60,8 @@ beforeEach(() => {
}
)
hoverState.isOpen = false
+ queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
+ prefetchQuery = vi.spyOn(queryClient, 'prefetchQuery').mockResolvedValue()
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
@@ -56,22 +70,25 @@ beforeEach(() => {
afterEach(async () => {
await act(async () => root.unmount())
container.remove()
+ queryClient.clear()
vi.unstubAllGlobals()
})
async function render(props: Partial[0]> = {}) {
await act(async () => {
root.render(
- {}}
- onMoreClick={() => {}}
- {...props}
- />
+
+ {}}
+ onMoreClick={() => {}}
+ {...props}
+ />
+
)
})
}
@@ -103,6 +120,28 @@ describe('ChatsSection', () => {
await act(async () => button?.click())
expect(onMoreClick).toHaveBeenCalledWith(expect.anything(), '/o/org-1/chat/chat-2')
+ expect(prefetchQuery).not.toHaveBeenCalled()
+ })
+
+ it.each([false, true])(
+ 'prefetches focused destination history with collapsed=%s',
+ async (isCollapsed) => {
+ hoverState.isOpen = isCollapsed
+ await render({ isCollapsed })
+ prefetchQuery.mockClear()
+ const link = document.body.querySelector('a[href="/o/org-1/chat/chat-3"]')!
+ await act(async () => link.focus())
+ expect(prefetchQuery).toHaveBeenCalledWith(
+ expect.objectContaining({ queryKey: ['mothership-chats', 'detail', 'chat-3'] })
+ )
+ }
+ )
+
+ it('does not prefetch the active conversation', async () => {
+ await render({ pathname: '/o/org-1/chat/chat-3' })
+ const link = container.querySelector('a[href="/o/org-1/chat/chat-3"]')!
+ await act(async () => link.focus())
+ expect(prefetchQuery).not.toHaveBeenCalled()
})
it('shows the empty state when there are no chats', async () => {
diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
index 042bca94ad8..351afadfeae 100644
--- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
+++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
@@ -2,10 +2,10 @@
import { chipVariants, cn, DropdownMenuItem, Loader, OverflowText, Skeleton } from '@sim/emcn'
import { MoreHorizontal, Pin, Task } from '@sim/emcn/icons'
-import Link from 'next/link'
import type { OrganizationChat } from '@/app/o/[organizationId]/components/organization-sidebar/hooks'
import { ConversationListItem } from '@/app/workspace/[workspaceId]/components'
import {
+ ChatNavigationLink,
CollapsedSidebarMenu,
SidebarSection,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
@@ -41,8 +41,10 @@ function ChatRow({ chat, isCurrentRoute, isMenuOpen, onContextMenu, onMoreClick
const showStatusDot = Boolean(chat.isActive) || (!isCurrentRoute && Boolean(chat.isUnread))
return (
- onContextMenu(e, chat.href)}
>
@@ -83,7 +85,7 @@ function ChatRow({ chat, isCurrentRoute, isMenuOpen, onContextMenu, onMoreClick
-
+
)
}
@@ -140,13 +142,18 @@ export function ChatsSection({
const isCurrentRoute = pathname === chat.href
return (
- onContextMenu(e, chat.href)}>
+ onContextMenu(e, chat.href)}
+ >
-
+
)
})
diff --git a/apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx b/apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx
index e0992209015..9189db61670 100644
--- a/apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx
+++ b/apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx
@@ -1,8 +1,10 @@
'use client'
+import { useRef } from 'react'
import { Button, cn } from '@sim/emcn'
import { ArrowUp } from '@sim/emcn/icons'
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
+import { useChatInputFocus } from '@/hooks/use-chat-input-focus'
const SEND_BUTTON_BASE = 'size-[28px] rounded-full border-0 p-0 transition-colors'
const SEND_BUTTON_ACTIVE =
@@ -32,6 +34,8 @@ export function Composer({
onSubmit,
onStop,
}: ComposerProps) {
+ const textareaRef = useRef(null)
+ useChatInputFocus({ textareaRef })
const canSubmit = value.trim().length > 0
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'
@@ -50,6 +54,7 @@ export function Composer({
)}
>