diff --git a/apps/docs/content/docs/search/confluence.mdx b/apps/docs/content/docs/search/confluence.mdx index 92ab84db517..79ea9ede7ba 100644 --- a/apps/docs/content/docs/search/confluence.mdx +++ b/apps/docs/content/docs/search/confluence.mdx @@ -58,7 +58,7 @@ Enter **Confluence Domain**, then choose one or more **Spaces**. The picker show Open **More options** to change **Content Type**, **Filter by Label**, or **Metadata tags**. The default includes pages; choose **All content** for pages and blog posts. Leave the label filter empty unless you want a smaller scope. -Confluence central source setup with an indexing account, domain, and spaces +Organization Integrations showing approved providers and Connect account actions @@ -37,7 +37,7 @@ In the new tab, select **Connect** and complete the provider's authorization. Ch Return to Integrations when the connection completes, or select **Return to Search** to open your organization’s Search page. Your account is saved when authorization completes; there is no separate submit step. If the popup was blocked or closed, allow popups and select **Connect account** again. While authorization is pending, use **Open again**. -Gmail account connection with Connect and Return to Search actions +Gmail account connection with Connect and Return to Search actions diff --git a/apps/docs/content/docs/search/github.mdx b/apps/docs/content/docs/search/github.mdx index a1ab75a3178..91ad9551e78 100644 --- a/apps/docs/content/docs/search/github.mdx +++ b/apps/docs/content/docs/search/github.mdx @@ -45,7 +45,7 @@ https:///api/auth/oauth2/callback/github-repositories Authorization starts from Sim so the callback can finish the pending connection. The connector polls GitHub's API and does not need a webhook. -GitHub App registration with the Redirect URI, expiring tokens enabled, and installation authorization, Device Flow, and webhooks disabled +GitHub App registration with the Redirect URI, expiring tokens enabled, and installation authorization, Device Flow, and webhooks disabled *Example registration. Replace `sim.example.com` with your Sim domain.* @@ -56,11 +56,11 @@ Authorization starts from Sim so the callback can finish the pending connection. Expand **Permissions → Repository permissions**. Set **Contents → Access: Read-only**; leave the mandatory **Metadata** permission at **Read-only**. -GitHub repository permissions with Contents set to Read-only and Metadata shown as mandatory Read-only +GitHub repository permissions with Contents set to Read-only and Metadata shown as mandatory Read-only Expand **Account permissions** and set **Email addresses → Access: Read-only**. -GitHub account permissions with only Email addresses selected for Read-only access +GitHub account permissions with only Email addresses selected for Read-only access | Permission area | Permission | Access | |---|---|---| @@ -111,7 +111,7 @@ As a Sim organization admin, open **Settings → Sources → Add integration** a Enter **Repository** as `owner/repo`. Open **More options** only if you need a different branch, path or extension filters, metadata tags, or a dedicated indexing account. **Sync documents with** defaults to **Connected members**. -GitHub source setup with a required Repository field and More options +GitHub source setup with a required Repository field and More options | Field | What to enter | |---|---| diff --git a/apps/docs/content/docs/search/gitlab.mdx b/apps/docs/content/docs/search/gitlab.mdx index 36915257d21..c60636bfc24 100644 --- a/apps/docs/content/docs/search/gitlab.mdx +++ b/apps/docs/content/docs/search/gitlab.mdx @@ -9,7 +9,7 @@ import { Image } from '@/components/ui/image' GitLab Search uses an administrator connection to sync a project's content and permissions. Teammates do not connect individual GitLab accounts. They sign in to the Sim organization with a verified email matching their confirmed GitLab email. -GitLab source setup in Sim Search +GitLab source setup in Sim Search Admin setup uses your organization's **Settings → Sources** page. Teammates do not need a personal GitLab connection. For workspace Search, use **Search → Add source** instead. @@ -30,7 +30,7 @@ Use a self-managed GitLab instance running version **17.4 or later**. Setup need Sign in to your self-managed GitLab instance as an instance administrator, then open **Avatar → Edit profile → Access → Personal access tokens**. On current releases choose **Generate token → Legacy token**; older versions show **Add new token** or the token form directly. -Official GitLab example of the Access Tokens page with the Add new token button +Official GitLab example of the Access Tokens page with the Add new token button *Official [GitLab Handbook example](https://handbook.gitlab.com/handbook/security/product-security/security-platforms-architecture/product-security-engineering/runbooks/rotate-service-account-personal-access-tokens/). Navigation and button labels vary by version. The example's existing `api` tokens are unrelated to Sim; use the scopes below.* @@ -63,7 +63,7 @@ Open **Settings → Sources → Add integration** and choose **GitLab**. On its **More options** contains the repository and issue filters, **Max Items**, and **Metadata tags**. Unlike member-account sources, GitLab retains this optional item limit. -GitLab source setup with More options expanded for repository and issue filters, item limit, and metadata tags +GitLab source setup with More options expanded for repository and issue filters, item limit, and metadata tags Select **Connect & Sync**. Sim validates the token and source policy, then starts indexing. diff --git a/apps/docs/content/docs/search/gmail.mdx b/apps/docs/content/docs/search/gmail.mdx index 40c9b5955b0..e207ab493df 100644 --- a/apps/docs/content/docs/search/gmail.mdx +++ b/apps/docs/content/docs/search/gmail.mdx @@ -39,7 +39,7 @@ Click **Add source**. Gmail appears in the provider's **Sources** list. Each per -Gmail Search source configuration +Gmail Search source configuration ## Connect your account @@ -93,7 +93,7 @@ Users do not need to create Google Cloud credentials. The deployment operator co https:///api/auth/oauth2/callback/google-email ``` -Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs +Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs This Google Cloud example uses one client for all three services. Replace `https://sim.example.com` with your Sim origin and add only the callbacks for services you enable. diff --git a/apps/docs/content/docs/search/google-calendar.mdx b/apps/docs/content/docs/search/google-calendar.mdx index 1ac04bc23b1..661e972445e 100644 --- a/apps/docs/content/docs/search/google-calendar.mdx +++ b/apps/docs/content/docs/search/google-calendar.mdx @@ -41,7 +41,7 @@ Keep the default date range for the previous and next 30 days. **More options** -Google Calendar Search source configuration +Google Calendar Search source configuration `primary` means the connected person's main calendar. A calendar selected from the list is a specific calendar ID, even when it is your main calendar. That same ID applies to every member, and only members with access to it can search its events. @@ -100,7 +100,7 @@ The deployment operator configures Google OAuth once; teammates then use the nor https:///api/auth/oauth2/callback/google-calendar ``` -Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs +Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs This Google Cloud example uses one client for all three services. Replace `https://sim.example.com` with your Sim origin and add only the callbacks for services you enable. diff --git a/apps/docs/content/docs/search/google-drive.mdx b/apps/docs/content/docs/search/google-drive.mdx index 2e267fbc7f9..f7317aec464 100644 --- a/apps/docs/content/docs/search/google-drive.mdx +++ b/apps/docs/content/docs/search/google-drive.mdx @@ -56,7 +56,7 @@ Open **Settings → Sources → Google Drive → Sources → Add source**. This This requires a Google Workspace domain and a Workspace super administrator to authorize domain-wide delegation. Consumer Gmail accounts cannot use this path. -Google Drive central source setup with an indexing account, sharing policy, and folder scope +Google Drive central source setup with an indexing account, sharing policy, and folder scope @@ -65,11 +65,11 @@ This requires a Google Workspace domain and a Workspace super administrator to a In [Google Cloud Console](https://console.cloud.google.com/), select your project and enable **Google Drive API** and **Admin SDK API** under **APIs & Services → Library**. Then open **IAM & Admin → Service Accounts → Create service account**, enter a name, and finish creation. Google Cloud project roles do not grant access to Workspace files; they are not required for this crawl. -Google Cloud Console form for creating a service account +Google Cloud Console form for creating a service account Open the service account's **Keys** tab and choose **Add key → Create new key → JSON**, then select **Create** to download the key. Store it securely; you will add it to Sim next. See [Google's key creation guide](https://docs.cloud.google.com/iam/docs/keys-create-delete#creating). -Google Cloud Create private key dialog with JSON selected +Google Cloud Create private key dialog with JSON selected @@ -78,7 +78,7 @@ Open the service account's **Keys** tab and choose **Add key → Create new key In the service account's **Details**, expand **Advanced settings** and copy its numeric **Client ID**. Sign in to the [Workspace Admin Console](https://admin.google.com/ac/owl/domainwidedelegation) as a super administrator. Open **Security → Access and data control → API controls → Manage Domain Wide Delegation → Add new**. -Google Workspace Admin Console Add a new client ID dialog with Client ID and OAuth scopes fields +Google Workspace Admin Console Add a new client ID dialog with Client ID and OAuth scopes fields Paste that Client ID into **Client ID**, then enter these exact scopes as a comma-separated list under **OAuth scopes**: @@ -97,7 +97,7 @@ These are Search's central crawl scopes. The general [Google service account gui Under **Indexing account**, choose the service-account connection action, or select an existing service account. Paste the JSON key into **Add Google Service Account**, give it a name, and add it. Sim returns you to the source form with that credential selected. -Add Google Service Account credential modal in Sim +Add Google Service Account credential modal in Sim @@ -146,7 +146,7 @@ The deployment operator configures Google OAuth for member accounts and **Accoun https:///api/auth/oauth2/callback/google-drive ``` -Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs +Google Cloud Web application client form with Sim's Gmail, Calendar, and Drive callback URLs This Google Cloud example uses one client for all three services. Replace `https://sim.example.com` with your Sim origin and add only the callbacks for services you enable. diff --git a/apps/docs/content/docs/search/index.mdx b/apps/docs/content/docs/search/index.mdx index 1b45c074528..907ea2876d3 100644 --- a/apps/docs/content/docs/search/index.mdx +++ b/apps/docs/content/docs/search/index.mdx @@ -39,7 +39,7 @@ Open **Integrations** in the main sidebar and select **Connect account** if prom -Organization Sources settings with the Add integration action +Organization Sources settings with the Add integration action Source availability depends on the deployment and organization policy. An unavailable source needs operator configuration before setup can continue. @@ -84,13 +84,13 @@ These requests are separate from organization invitations. They let recipients c **Revoke all account access** withdraws every account that person contributed to the organization, including other providers. It does not remove their organization membership. -Request Gmail connections with an Emails field and Send requests action +Request Gmail connections with an Emails field and Send requests action ## Manage sources and documents Open **Settings → Sources → [integration]**, then select a source. Providers with personal connections have **Sources** and **Accounts** tabs; providers such as GitLab open directly to the source list. Multiple sources can have different folder, repository, space, or project scopes. -Integration detail with Sources and Accounts tabs and a nested source list +Integration detail with Sources and Accounts tabs and a nested source list | Source tab | What you can do | | --- | --- | @@ -100,9 +100,9 @@ Open **Settings → Sources → [integration]**, then select a source. Providers Use the source header to sync, pause, resume, or remove that source. The back link returns to its integration. **Deactivate** on the integration page makes its content unavailable in Search, Assistant, and MCP while preserving its sources and connected accounts; **Activate** enables it again. -Gmail source Settings tab with label, date range, and search filters +Gmail source Settings tab with label, date range, and search filters -Source Sync history with no connected accounts or synced members +Source Sync history with no connected accounts or synced members ## Search, Assistant, and MCP @@ -141,7 +141,7 @@ A completed sync means the source was checked; some documents may still be index As an admin, open **Settings → Sources → [integration] → Sources → [source]**. In **Documents**, select **Failed** from the status dropdown to inspect those files. Use the search field to find a document by name. Select **Retry indexing** beside a file to try again. **Exclude** removes a file from search; select **Excluded** and then **Restore** to include it again. Fix a disconnected account or source configuration before retrying a sync that needs attention. -Empty source Documents tab with search and an Included status filter +Empty source Documents tab with search and an Included status filter Temporary indexing-capacity waits retry automatically in the background. If automatic retries are exhausted, the document appears as failed so an admin can retry it. Other searchable documents remain available while this work finishes. diff --git a/apps/docs/content/docs/search/jira.mdx b/apps/docs/content/docs/search/jira.mdx index 21c421fe5c2..c15fa80f975 100644 --- a/apps/docs/content/docs/search/jira.mdx +++ b/apps/docs/content/docs/search/jira.mdx @@ -45,7 +45,7 @@ If you already know the project keys, use the switch beside **Projects** to sele **Account for browsing** only populates the project picker. It does not enroll you or share that account's issue access with teammates. -Jira source setup with an account for browsing and required site and project fields/api/auth/oauth2/callback/jira` to **Callback URLs**, keeping any callbacks already used by your deployment, then save. - Atlassian OAuth Authorization form with an example Sim Jira callback URL +Slack app setup for Sim Search with application and client credential fields ## Before you start @@ -34,7 +34,7 @@ Open **Settings → Sources → Add integration** and choose **Slack**. Select * On the [Slack Apps page](https://api.slack.com/apps), create an app for the target workspace, or use an app dedicated to your Sim organization. Under **OAuth & Permissions**, add the user scopes and both redirect URLs listed below. Keep **Token Rotation** disabled. -Slack app settings showing Basic Information and App Credentials +Slack app settings showing Basic Information and App Credentials *Official Slack example: [Basic Information](https://docs.slack.dev/tools/bolt-python/creating-an-app/#create-a-new-app). Use your own app's credentials.* diff --git a/apps/sim/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage.test.ts b/apps/sim/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage.test.ts index a17aaf1e831..2b749f2db55 100644 --- a/apps/sim/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage.test.ts +++ b/apps/sim/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage.test.ts @@ -114,6 +114,43 @@ describe('calculateFitScale', () => { }) describe('ResponsiveDesignStage', () => { + it.each([ + { width: 280, height: 400, supportsZoom: true }, + { width: 280, height: 400, supportsZoom: false }, + { width: 350, height: 340, supportsZoom: true }, + { width: 350, height: 340, supportsZoom: false }, + ])( + 'fits an uncapped $width × $height stage with zoom support: $supportsZoom', + ({ width, height, supportsZoom }) => { + vi.stubGlobal('CSS', { supports: vi.fn(() => supportsZoom) }) + act(() => { + root.render( + createElement( + ResponsiveDesignStage, + { width, height, maxScale: Number.POSITIVE_INFINITY }, + createElement('span', null, 'Preview') + ) + ) + }) + + const surface = container.firstElementChild?.firstElementChild + if (!(surface instanceof HTMLElement) || !resizeObserver) { + throw new Error('responsive stage did not mount') + } + const observer = resizeObserver + + act(() => observer.deliver(width * 2, height * 1.5)) + expect(surface.style.zoom).toBe(supportsZoom ? '1.5' : '1') + expect(surface.style.transform).toBe(supportsZoom ? '' : 'scale(1.5)') + expect(surface.style.opacity).toBe('1') + + act(() => observer.deliver(width / 2, height * 2)) + expect(surface.style.zoom).toBe(supportsZoom ? '0.5' : '1') + expect(surface.style.transform).toBe(supportsZoom ? '' : 'scale(0.5)') + expect(surface.style.opacity).toBe('1') + } + ) + it('hides an already visible surface until a measurable size returns', () => { act(() => { root.render( diff --git a/apps/sim/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css b/apps/sim/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css index 1fe3f88e310..eb11c05760a 100644 --- a/apps/sim/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css +++ b/apps/sim/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css @@ -44,9 +44,9 @@ .folderButton { position: relative; - min-width: 0; - min-height: 0; - container-type: size; + display: block; + width: 100%; + height: 100%; cursor: pointer; touch-action: manipulation; border-radius: 12px; @@ -60,11 +60,10 @@ width: 321px; height: 270px; left: 50%; - bottom: 12px; + bottom: 0; margin-left: -160.5px; perspective: 800px; transform-origin: bottom center; - scale: min(tan(atan2(100cqw, 350px)), tan(atan2(100cqh, 340px))); } .folderBack { position: absolute; diff --git a/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx b/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx index 0b423b59801..ad1d78fd5b5 100644 --- a/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx +++ b/apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { cn } from '@sim/emcn' import { motion, useReducedMotion } from 'framer-motion' +import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage' import styles from '@/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css' /** @@ -72,53 +73,68 @@ export function InteractiveLibraryFolder({ : { type: 'spring' as const, stiffness: 120, damping: 14 } return ( - + + + ) } diff --git a/apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx b/apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx index 8cb36e7b7b2..dcda47e3221 100644 --- a/apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx +++ b/apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx @@ -1,5 +1,6 @@ import { ChipTag, cn } from '@sim/emcn' import colorMixFallbacks from '@/app/(landing)/components/shared/color-mix-fallbacks/color-mix-fallbacks.module.css' +import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage' import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics' import styles from '@/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.module.css' @@ -65,11 +66,14 @@ const EDGE_DRAW_CLASSES = [styles.edgeDraw0, styles.edgeDraw1, styles.edgeDraw2] export function WorkflowCanvasGraphic() { return ( - ) 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({ )} >