From e7faad34a7e8efaeb2b02942538c3f45388e46b1 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 8 Sep 2026 22:41:05 -0700 Subject: [PATCH 1/3] fix(chat): align organization focus and loading with workspace (#7647) --- .../chats-section/chats-section.test.tsx | 61 ++++++++-- .../chats-section/chats-section.tsx | 17 ++- .../home/components/composer/composer.tsx | 5 + .../home/organization-home.test.tsx | 48 +++++++- .../home/organization-home.tsx | 6 +- .../home/components/user-input/user-input.tsx | 22 +--- apps/sim/hooks/use-chat-input-focus.test.tsx | 108 ++++++++++++++++++ apps/sim/hooks/use-chat-input-focus.ts | 23 ++++ 8 files changed, 250 insertions(+), 40 deletions(-) create mode 100644 apps/sim/hooks/use-chat-input-focus.test.tsx create mode 100644 apps/sim/hooks/use-chat-input-focus.ts 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({ )} >