From 265d6600763b57a7c9510c4db031eb4297bbb9f7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 6 Jun 2026 18:40:31 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20ci:=20Type-check=20the=20Client?= =?UTF-8?q?=20Workspace=20(#13560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `client/` workspace was never type-checked: the existing typecheck job only covered `packages/` and `api/`, and Vite/esbuild transpiles without type-checking, so type errors shipped through every CI gate. - Add a `typecheck` job to frontend-review.yml running `tsc --noEmit` over `client/` (zero tolerance), reusing the data-provider + client-package build artifacts. Triggers on `client/**`, `packages/client/**`, `packages/data-provider/**`. - Fix all 168 pre-existing client type errors this surfaced (source + tests), including genuine latent bugs: - `getFileConfig()` was typed as merged `FileConfig`, but the server returns the raw config that `mergeFileConfig()` consumes (`TFileConfig`). - SidePanel/Agents `Retrieval`/`ImageVision` were bound to `AgentForm` but use the assistants `Capabilities` enum → `AssistantForm`. - `useSearchResultsByTurn` read a `sources` field its type lacked. - Removed orphaned dead code: `Artifacts/Mermaid.tsx` (imported a never-installed dep) and dead barrel re-exports (`./Plugins`, `./MCPAuth`). - Narrow `client/tsconfig.json` to the client app (drop `../e2e` and `../config/translations`, which reference backend/tooling modules) so the gate's scope matches its trigger. No `any`/`@ts-ignore`/`as unknown as`. Localized newly-surfaced strings. --- .github/workflows/frontend-review.yml | 45 +++++ client/src/Providers/BadgeRowContext.tsx | 4 +- client/src/a11y/LiveMessage.tsx | 8 +- .../Agents/VirtualizedAgentGrid.tsx | 6 +- .../Agents/tests/ErrorDisplay.spec.tsx | 2 +- .../VirtualScrollingPerformance.test.tsx | 5 +- .../tests/VirtualizedAgentGrid.test.tsx | 11 +- client/src/components/Artifacts/Mermaid.tsx | 176 ------------------ .../components/Auth/__tests__/Login.spec.tsx | 6 +- .../Auth/__tests__/LoginForm.spec.tsx | 33 +++- .../components/Chat/Menus/Models/fakeData.ts | 2 +- .../Parts/__tests__/ArtifactRouting.test.tsx | 39 ++-- .../Parts/__tests__/LogContent.test.tsx | 22 +-- .../Parts/__tests__/SubagentCall.test.tsx | 5 +- .../Parts/__tests__/TextAttachment.test.tsx | 10 +- .../Parts/__tests__/attachmentTypes.test.ts | 5 +- .../__tests__/UIResourceCarousel.test.tsx | 10 +- .../__tests__/MessageIcon.render.test.tsx | 2 +- .../Messages/__tests__/MessageIcon.test.ts | 10 +- .../ConvoOptions/SharedLinkButton.tsx | 16 +- .../Files/FileList/FileTableColumns.tsx | 20 +- .../Files/VectorStore/VectorStoreListItem.tsx | 7 +- client/src/components/Nav/AccountSettings.tsx | 3 +- .../Favorites/tests/FavoritesList.spec.tsx | 6 +- .../Account/TwoFactorAuthentication.tsx | 3 +- .../SettingsTabs/General/ArchivedChats.tsx | 2 +- .../Speech/STT/AutoSendTextSelector.tsx | 5 +- .../Prompts/buttons/AutoSendPrompt.tsx | 1 + .../SidePanel/Agents/AgentPanel.test.tsx | 1 - .../SidePanel/Agents/ImageVision.tsx | 6 +- .../components/SidePanel/Agents/Images.tsx | 1 - .../components/SidePanel/Agents/Retrieval.tsx | 6 +- .../Version/__tests__/VersionContent.spec.tsx | 2 +- .../Version/__tests__/VersionItem.spec.tsx | 2 +- .../Version/__tests__/isActiveVersion.spec.ts | 2 +- .../Agents/__tests__/AgentAvatar.spec.tsx | 17 +- .../__tests__/AgentPanel.helpers.spec.ts | 13 +- .../components/SidePanel/MCPBuilder/index.ts | 1 - .../SidePanel/Memories/MemoryPanel.tsx | 1 + .../Skills/dialogs/UploadSkillDialog.tsx | 4 +- .../layouts/__tests__/SkillsView.spec.tsx | 2 +- client/src/components/Web/Citation.tsx | 14 +- client/src/components/Web/Sources.tsx | 4 +- client/src/components/index.ts | 1 - client/src/components/ui/index.ts | 1 + client/src/data-provider/Agents/queries.ts | 6 +- client/src/data-provider/Auth/queries.ts | 4 +- client/src/data-provider/Files/queries.ts | 6 +- client/src/data-provider/MCP/mutations.ts | 11 +- client/src/hooks/Audio/MediaSourceAppender.ts | 4 +- client/src/hooks/Chat/__tests__/cache.spec.ts | 8 +- client/src/hooks/Chat/cache.ts | 2 +- .../Config/__tests__/useAppStartup.spec.tsx | 10 +- .../useAttachmentPreviewSync.spec.tsx | 35 ++-- client/src/hooks/Files/useDragHelpers.ts | 2 +- .../hooks/MCP/__tests__/useMCPSelect.test.tsx | 5 +- .../__tests__/useAttachments.spec.tsx | 31 +-- .../src/hooks/Messages/useCopyToClipboard.ts | 2 +- .../hooks/Messages/useSearchResultsByTurn.ts | 13 +- .../__tests__/useAttachmentHandler.spec.tsx | 32 +++- .../SSE/__tests__/useStepHandler.spec.ts | 30 ++- client/src/hooks/SSE/useStepHandler.ts | 16 +- client/src/lib/rum/useRum.spec.tsx | 7 +- client/src/lib/rum/useRum.ts | 3 +- client/src/locales/en/translation.json | 2 + client/src/utils/__tests__/collection.test.ts | 4 +- client/src/utils/conversationTags.spec.ts | 8 + client/src/utils/imageResize.ts | 4 +- client/tsconfig.json | 2 - packages/data-provider/src/data-service.ts | 17 +- 70 files changed, 407 insertions(+), 399 deletions(-) delete mode 100644 client/src/components/Artifacts/Mermaid.tsx diff --git a/.github/workflows/frontend-review.yml b/.github/workflows/frontend-review.yml index 62eef692c8..136a664c16 100644 --- a/.github/workflows/frontend-review.yml +++ b/.github/workflows/frontend-review.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'client/**' + - 'packages/client/**' - 'packages/data-provider/**' permissions: @@ -76,6 +77,50 @@ jobs: path: packages/client/dist retention-days: 2 + typecheck: + name: TypeScript type checks (client) + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 24.16.0 + uses: actions/setup-node@v4 + with: + node-version: '24.16.0' + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache@v4 + with: + path: | + node_modules + client/node_modules + packages/client/node_modules + packages/data-provider/node_modules + key: node-modules-frontend-${{ runner.os }}-24.16.0-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: npm ci + + - name: Download data-provider build + uses: actions/download-artifact@v4 + with: + name: build-data-provider + path: packages/data-provider/dist + + - name: Download client-package build + uses: actions/download-artifact@v4 + with: + name: build-client-package + path: packages/client/dist + + - name: Type check client + run: npm run typecheck + working-directory: client + test-ubuntu: name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/4)' needs: build diff --git a/client/src/Providers/BadgeRowContext.tsx b/client/src/Providers/BadgeRowContext.tsx index 025532f0c6..448af4339f 100644 --- a/client/src/Providers/BadgeRowContext.tsx +++ b/client/src/Providers/BadgeRowContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useSetRecoilState } from 'recoil'; import { Tools, Constants, LocalStorageKeys, AgentCapabilities } from 'librechat-data-provider'; -import type { TAgentsEndpoint } from 'librechat-data-provider'; +import type { TAgentsEndpoint, TEphemeralAgent } from 'librechat-data-provider'; import { useMCPServerManager, useSearchApiKeyForm, @@ -170,7 +170,7 @@ export default function BadgeRowProvider({ if (prev == null) { /** ephemeralAgent is null — use localStorage defaults */ if (hasOverrides || mcpOverrides) { - const result = { ...initialValues }; + const result: TEphemeralAgent = { ...initialValues }; if (mcpOverrides) { result.mcp = mcpOverrides; } diff --git a/client/src/a11y/LiveMessage.tsx b/client/src/a11y/LiveMessage.tsx index b773deae53..b25b48f108 100644 --- a/client/src/a11y/LiveMessage.tsx +++ b/client/src/a11y/LiveMessage.tsx @@ -16,17 +16,17 @@ const LiveMessage: React.FC = ({ useEffect(() => { if (ariaLive === 'assertive') { - announceAssertive(message); + announceAssertive({ message }); } else if (ariaLive === 'polite') { - announcePolite(message); + announcePolite({ message }); } }, [message, ariaLive, announceAssertive, announcePolite]); useEffect(() => { return () => { if (clearOnUnmount === true || clearOnUnmount === 'true') { - announceAssertive(''); - announcePolite(''); + announceAssertive({ message: '' }); + announcePolite({ message: '' }); } }; }, [clearOnUnmount, announceAssertive, announcePolite]); diff --git a/client/src/components/Agents/VirtualizedAgentGrid.tsx b/client/src/components/Agents/VirtualizedAgentGrid.tsx index 0fed2c1974..a2843c5b20 100644 --- a/client/src/components/Agents/VirtualizedAgentGrid.tsx +++ b/client/src/components/Agents/VirtualizedAgentGrid.tsx @@ -1,8 +1,8 @@ import React, { useMemo, useEffect, useCallback, useRef } from 'react'; -import { AutoSizer, List as VirtualList, WindowScroller } from 'react-virtualized'; import { throttle } from 'lodash'; import { Spinner } from '@librechat/client'; import { PermissionBits } from 'librechat-data-provider'; +import { AutoSizer, List as VirtualList, WindowScroller } from 'react-virtualized'; import type t from 'librechat-data-provider'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; import { useAgentCategories, useLocalize } from '~/hooks'; @@ -175,7 +175,7 @@ const VirtualizedAgentGrid: React.FC = ({ const globalIndex = index * cardsPerRow + cardIndex; return (
- onSelectAgent(agent)} /> +
); })} @@ -282,7 +282,7 @@ const VirtualizedAgentGrid: React.FC = ({ const rowCount = getRowCount(currentAgents.length, cardsPerRow); return ( -
+
}> { }); it('handles null/undefined errors', () => { - render(); + render(); expect(screen.getByText('Something went wrong')).toBeInTheDocument(); expect( diff --git a/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx b/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx index 43a5be20d5..198ba4271f 100644 --- a/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx +++ b/client/src/components/Agents/tests/VirtualScrollingPerformance.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { jest } from '@jest/globals'; import type * as t from 'librechat-data-provider'; import VirtualizedAgentGrid from '../VirtualizedAgentGrid'; @@ -48,7 +47,9 @@ const mockRowRenderer = jest.fn(); jest.mock('react-virtualized', () => { const ReactActual = jest.requireActual('react'); - const mockRowRendererRef = { current: jest.fn() }; + const mockRowRendererRef: { current: VirtualListMockProps['rowRenderer'] | null } = { + current: null, + }; return { AutoSizer: ({ diff --git a/client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx b/client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx index 9fb6c40209..2aed774e81 100644 --- a/client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx +++ b/client/src/components/Agents/tests/VirtualizedAgentGrid.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { jest } from '@jest/globals'; import type t from 'librechat-data-provider'; import VirtualizedAgentGrid from '../VirtualizedAgentGrid'; @@ -166,9 +165,15 @@ jest.mock('../SmartLoader', () => ({ })); jest.mock('../AgentCard', () => { - return function MockAgentCard({ agent, onClick }: { agent: t.Agent; onClick: () => void }) { + return function MockAgentCard({ + agent, + onSelect, + }: { + agent: t.Agent; + onSelect: (agent: t.Agent) => void; + }) { return ( -
+
onSelect(agent)}>

{agent.name}

{agent.description}

diff --git a/client/src/components/Artifacts/Mermaid.tsx b/client/src/components/Artifacts/Mermaid.tsx deleted file mode 100644 index 9d54285cb6..0000000000 --- a/client/src/components/Artifacts/Mermaid.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { Button } from '@librechat/client'; -import { ZoomIn, ZoomOut, RotateCcw } from 'lucide-react'; -import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; -import type { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; -import { artifactFlowchartConfig } from '~/utils/mermaid'; - -interface MermaidDiagramProps { - content: string; - isDarkMode?: boolean; -} - -let mermaidPromise: Promise | null = null; - -const loadMermaid = () => { - if (!mermaidPromise) { - mermaidPromise = import('mermaid').then((mod) => mod.default); - } - - return mermaidPromise; -}; - -const MermaidDiagram: React.FC = ({ content, isDarkMode = true }) => { - const mermaidRef = useRef(null); - const transformRef = useRef(null); - const [isRendered, setIsRendered] = useState(false); - const theme = isDarkMode ? 'dark' : 'neutral'; - const bgColor = isDarkMode ? '#212121' : '#FFFFFF'; - - useEffect(() => { - let isMounted = true; - - const renderDiagram = async () => { - try { - const mermaid = await loadMermaid(); - - mermaid.initialize({ - startOnLoad: false, - theme, - securityLevel: 'sandbox', - flowchart: artifactFlowchartConfig, - }); - - if (!mermaidRef.current) { - return; - } - - const { svg } = await mermaid.render('mermaid-diagram', content); - mermaidRef.current.innerHTML = svg; - - const svgElement = mermaidRef.current.querySelector('svg'); - if (svgElement) { - svgElement.style.width = '100%'; - svgElement.style.height = '100%'; - } - if (isMounted) { - setIsRendered(true); - } - } catch (error) { - console.error('Mermaid rendering error:', error); - if (mermaidRef.current) { - mermaidRef.current.innerHTML = 'Error rendering diagram'; - } - } - }; - - renderDiagram(); - - return () => { - isMounted = false; - }; - }, [content, theme]); - - const centerAndFitDiagram = useCallback(() => { - if (transformRef.current && mermaidRef.current) { - const { centerView, zoomToElement } = transformRef.current; - zoomToElement(mermaidRef.current as HTMLElement); - centerView(1, 0); - } - }, []); - - useEffect(() => { - if (isRendered) { - centerAndFitDiagram(); - } - }, [isRendered, centerAndFitDiagram]); - - const handlePanning = useCallback(() => { - if (!transformRef.current) { - return; - } - - const { state, instance } = transformRef.current; - if (!state || !instance) { - return; - } - const { scale, positionX, positionY } = state; - const { wrapperComponent, contentComponent } = instance; - - if (!wrapperComponent || !contentComponent) { - return; - } - - const wrapperRect = wrapperComponent.getBoundingClientRect(); - const contentRect = contentComponent.getBoundingClientRect(); - const maxX = wrapperRect.width - contentRect.width * scale; - const maxY = wrapperRect.height - contentRect.height * scale; - - let newX = positionX; - let newY = positionY; - - if (newX > 0) { - newX = 0; - } - if (newY > 0) { - newY = 0; - } - if (newX < maxX) { - newX = maxX; - } - if (newY < maxY) { - newY = maxY; - } - - if (newX !== positionX || newY !== positionY) { - instance.setTransformState(scale, newX, newY); - } - }, []); - - return ( -
- - {({ zoomIn, zoomOut }) => ( - <> - -
- -
- - - -
- - )} - -
- ); -}; - -export default MermaidDiagram; diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 3937deb624..f01c12d2cf 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -1,7 +1,7 @@ import reactRouter from 'react-router-dom'; import userEvent from '@testing-library/user-event'; -import { getByTestId, render, waitFor } from 'test/layout-test-utils'; import type { TStartupConfig } from 'librechat-data-provider'; +import { getByTestId, render, waitFor } from 'test/layout-test-utils'; import * as endpointQueries from '~/data-provider/Endpoints/queries'; import * as miscDataProvider from '~/data-provider/Misc/queries'; import * as authMutations from '~/data-provider/Auth/mutations'; @@ -176,7 +176,7 @@ test('calls loginUser.mutate on login', async () => { }); test('Navigates to / on successful login', async () => { - const { getByLabelText, history } = setup({ + const { getByLabelText } = setup({ // @ts-ignore - we don't need all parameters of the QueryObserverResult useLoginUserReturnValue: { isLoading: false, @@ -202,5 +202,5 @@ test('Navigates to / on successful login', async () => { await userEvent.type(passwordInput, 'password'); await userEvent.click(submitButton); - waitFor(() => expect(history.location.pathname).toBe('/')); + waitFor(() => expect(window.location.pathname).toBe('/')); }); diff --git a/client/src/components/Auth/__tests__/LoginForm.spec.tsx b/client/src/components/Auth/__tests__/LoginForm.spec.tsx index f6376d166d..14692befaa 100644 --- a/client/src/components/Auth/__tests__/LoginForm.spec.tsx +++ b/client/src/components/Auth/__tests__/LoginForm.spec.tsx @@ -1,9 +1,9 @@ -import { render, getByTestId } from 'test/layout-test-utils'; import userEvent from '@testing-library/user-event'; import type { TStartupConfig } from 'librechat-data-provider'; import * as endpointQueries from '~/data-provider/Endpoints/queries'; import * as miscDataProvider from '~/data-provider/Misc/queries'; import * as authMutations from '~/data-provider/Auth/mutations'; +import { render, getByTestId } from 'test/layout-test-utils'; import * as authQueries from '~/data-provider/Auth/queries'; import Login from '../LoginForm'; @@ -18,8 +18,10 @@ const mockStartupConfig: TStartupConfig = { githubLoginEnabled: true, googleLoginEnabled: true, openidLoginEnabled: true, + appleLoginEnabled: false, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', + openidAutoRedirect: false, samlLoginEnabled: true, samlLabel: 'Test SAML', samlImageUrl: 'http://test-server.com', @@ -33,9 +35,11 @@ const mockStartupConfig: TStartupConfig = { enabled: false, }, emailEnabled: false, - checkBalance: false, showBirthdayIcon: false, helpAndFaqURL: '', + sharedLinksEnabled: true, + publicSharedLinksEnabled: true, + allowAccountDeletion: true, }; const setup = ({ @@ -106,15 +110,25 @@ beforeEach(() => { test('renders login form', () => { const { getByLabelText } = render( - , + , ); expect(getByLabelText(/email/i)).toBeInTheDocument(); expect(getByLabelText(/password/i)).toBeInTheDocument(); }); test('submits login form', async () => { - const { getByLabelText, getByRole } = render( - , + const { getByLabelText } = render( + , ); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); @@ -128,8 +142,13 @@ test('submits login form', async () => { }); test('displays validation error messages', async () => { - const { getByLabelText, getByRole, getByText } = render( - , + const { getByLabelText, getByText } = render( + , ); const emailInput = getByLabelText(/email/i); const passwordInput = getByLabelText(/password/i); diff --git a/client/src/components/Chat/Menus/Models/fakeData.ts b/client/src/components/Chat/Menus/Models/fakeData.ts index 43d4cf489a..6095dde6ef 100644 --- a/client/src/components/Chat/Menus/Models/fakeData.ts +++ b/client/src/components/Chat/Menus/Models/fakeData.ts @@ -32,7 +32,7 @@ export const data: TModelSpec[] = [ // iconURL: 'https://i.ytimg.com/vi/SaneSRqePVY/maxresdefault.jpg', iconURL: EModelEndpoint.openAI, // Allow using project-included icons preset: { - chatGptLabel: 'Vision Helper', + modelLabel: 'Vision Helper', greeting: "What's up!!", endpoint: EModelEndpoint.openAI, model: 'gpt-4-turbo', diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx index 70a1661dcb..555c8c6ffe 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/ArtifactRouting.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { render, screen, fireEvent, act } from '@testing-library/react'; import { RecoilRoot, useRecoilValue } from 'recoil'; -import type { MutableSnapshot } from 'recoil'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import type { TAttachment } from 'librechat-data-provider'; +import type { MutableSnapshot } from 'recoil'; import Attachment, { AttachmentGroup } from '../Attachment'; import store from '~/store'; @@ -198,9 +198,8 @@ describe('Attachment routing for tool artifacts', () => { * yet); use it as the canonical "unrouted text" example. */ const json = baseAttachment({ filename: 'data.json', - type: 'application/json', text: '{"a":1,"b":2}', - } as Partial); + }); const { container } = renderWith(); expect(container.querySelector('pre')).not.toBeNull(); expect(screen.queryByTestId('mermaid-render')).not.toBeInTheDocument(); @@ -544,10 +543,9 @@ describe('ToolArtifactCard click behaviour', () => { const xlsx = baseAttachment({ file_id: 'just-resolved-xlsx', filename: 'data.xlsx', - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', text: 'resolved
', textFormat: 'html', - } as Partial); + }); const initializeState = (snap: MutableSnapshot) => { snap.set(store.isSubmittingFamily(0), false); snap.set(store.artifactsVisibility, false); @@ -580,10 +578,9 @@ describe('ToolArtifactCard click behaviour', () => { const xlsx = baseAttachment({ file_id: 'one-shot-xlsx', filename: 'data.xlsx', - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', text: 'resolved
', textFormat: 'html', - } as Partial); + }); const initializeState = (snap: MutableSnapshot) => { snap.set(store.isSubmittingFamily(0), false); snap.set(store.artifactsVisibility, false); @@ -710,15 +707,13 @@ describe('AttachmentGroup routing', () => { const empty = baseAttachment({ file_id: 'empty-zip', filename: 'placeholder.zip', - type: 'application/zip', bytes: 0, - } as Partial); + }); const real = baseAttachment({ file_id: 'real-zip', filename: 'archive.zip', - type: 'application/zip', bytes: 1024, - } as Partial); + }); const { container } = renderWith(); fireEvent.click(screen.getByRole('button', { name: 'com_ui_show_n_files' })); const chips = Array.from(container.querySelectorAll('[data-testid="file-container"]')); @@ -733,26 +728,22 @@ describe('AttachmentGroup routing', () => { const first = baseAttachment({ file_id: 'file-a', filename: 'a.zip', - type: 'application/zip', - } as Partial); + }); const second = baseAttachment({ file_id: 'file-b', filename: 'b.zip', - type: 'application/zip', - } as Partial); + }); const json = baseAttachment({ file_id: 'file-c', filename: 'c.json', - type: 'application/json', text: '{"c":true}', - } as Partial); + }); const image = baseAttachment({ file_id: 'image-a', filename: 'preview.png', - type: 'image/png', width: 16, height: 16, - } as Partial); + }); const { container } = renderWith( , @@ -784,9 +775,8 @@ describe('AttachmentGroup routing', () => { const sandboxFile = baseAttachment({ file_id: 'sandbox-zip', filename: 'archive-deadbe.zip', - type: 'application/zip', bytes: 1024, - } as Partial); + }); const { container } = renderWith(); const chip = container.querySelector('[data-testid="file-container"]'); expect(chip?.textContent).toBe('archive-deadbe.zip'); @@ -802,9 +792,8 @@ describe('AttachmentGroup routing', () => { const sandboxDotfile = baseAttachment({ file_id: 'sandbox-config', filename: '_.config-abcdef.zip', - type: 'application/zip', bytes: 12, - } as Partial); + }); const { container } = renderWith(); const chip = container.querySelector('[data-testid="file-container"]'); expect(chip?.textContent).toBe('.config.zip'); @@ -823,7 +812,6 @@ describe('AttachmentGroup routing', () => { baseAttachment({ file_id: 'pending-1', filename: 'data.xlsx', - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', status: 'pending', } as Partial), baseAttachment({ @@ -859,7 +847,6 @@ describe('AttachmentGroup routing', () => { baseAttachment({ file_id: 'c', filename: 'data.json', - type: 'application/json', text: '{"a":1}', } as Partial), baseAttachment({ diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/LogContent.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/LogContent.test.tsx index bb34bb1dee..19feccef2c 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/LogContent.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/LogContent.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; -import type { MutableSnapshot } from 'recoil'; +import { render, screen } from '@testing-library/react'; import type { TAttachment } from 'librechat-data-provider'; +import type { MutableSnapshot } from 'recoil'; import LogContent from '../LogContent'; import store from '~/store'; @@ -110,9 +110,8 @@ describe('LogContent attachment routing', () => { const json = baseAttachment({ file_id: 'c', filename: 'data.json', - type: 'application/json', text: '{"a":1,"b":2}', - } as Partial); + }); const { container } = renderWith(); expect(container.querySelector('pre')).not.toBeNull(); expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument(); @@ -123,8 +122,7 @@ describe('LogContent attachment routing', () => { const zip = baseAttachment({ file_id: 'd', filename: 'archive.zip', - type: 'application/zip', - } as Partial); + }); renderWith(); expect(screen.getByTestId('log-link')).toHaveAttribute('data-filename', 'archive.zip'); }); @@ -137,9 +135,8 @@ describe('LogContent attachment routing', () => { const pptx = baseAttachment({ file_id: 'e', filename: 'slides.pptx', - type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', text: '
  1. Slide 1
', - } as Partial); + }); renderWith(); expect(screen.getByText('slides.pptx')).toBeInTheDocument(); }); @@ -151,9 +148,8 @@ describe('LogContent attachment routing', () => { const pptx = baseAttachment({ file_id: 'e2', filename: 'slides.pptx', - type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - text: undefined as unknown as string, - } as Partial); + text: undefined, + }); renderWith(); expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument(); expect(screen.getByTestId('log-link')).toHaveAttribute('data-filename', 'slides.pptx'); @@ -168,10 +164,9 @@ describe('LogContent attachment routing', () => { const expired = baseAttachment({ file_id: 'x-expired', filename: 'slides.pptx', - type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', text: '
  1. Slide 1
', expiresAt: Date.now() - 60_000, - } as Partial); + }); renderWith(); // No panel card and no log-link (the expired branch returns plain text). expect(screen.queryByRole('button', { pressed: true })).not.toBeInTheDocument(); @@ -211,7 +206,6 @@ describe('LogContent attachment routing', () => { * bucket, so it would no longer satisfy the "inline pre" check * below. */ filename: 'notes.json', - type: 'application/json', text: '{"a":1,"b":2}', } as Partial), ] as TAttachment[]; diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/SubagentCall.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/SubagentCall.test.tsx index f78f71d75b..2b968071ad 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/SubagentCall.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/SubagentCall.test.tsx @@ -8,15 +8,14 @@ import type { SubagentAggregatorState, } from '~/utils/subagentContent'; import type { SubagentProgress } from '~/store/subagents'; - import { foldSubagentEvent, foldSubagentEventIntoTicker, initSubagentAggregatorState, initSubagentTickerState, } from '~/utils/subagentContent'; -import { subagentProgressByToolCallId } from '~/store/subagents'; import SubagentCall, { SUBAGENT_TICKER_THROTTLE_MS } from '../SubagentCall'; +import { subagentProgressByToolCallId } from '~/store/subagents'; jest.mock('~/hooks', () => ({ useLocalize: @@ -622,7 +621,7 @@ describe('SubagentCall — dialog content', () => { ); openSubagentDialog(); expect(screen.getByText('raw final text')).toBeInTheDocument(); - rerender(); + rerender({null}); }); it('renders persistedContent parts when no live events are available (page-refresh flow)', () => { diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/TextAttachment.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/TextAttachment.test.tsx index 6996550c52..aaf388b080 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/TextAttachment.test.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/TextAttachment.test.tsx @@ -71,7 +71,6 @@ const textAttachment = (overrides: Partial = {}): TAttachment => * bearing, downloadable, expandable) without the panel coupling. */ filename: 'output.json', filepath: '/files/output.json', - type: 'application/json', text: '{"a":1,"b":2,"c":3}', ...overrides, }) as TAttachment; @@ -190,8 +189,7 @@ describe('AttachmentGroup', () => { textAttachment({ file_id: 'b', filename: 'archive.zip', - type: 'application/zip', - text: undefined as unknown as string, + text: undefined, }), ] as TAttachment[]; const { container } = render(); @@ -205,8 +203,7 @@ describe('AttachmentGroup', () => { file_id: 'placeholder', filename: 'placeholder.zip', filepath: '', - type: 'application/zip', - text: undefined as unknown as string, + text: undefined, }), textAttachment({ file_id: 'json', @@ -230,8 +227,7 @@ describe('AttachmentGroup', () => { textAttachment({ file_id: 'archive', filename: 'archive.zip', - type: 'application/zip', - text: undefined as unknown as string, + text: undefined, }), textAttachment({ file_id: 'json', diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/attachmentTypes.test.ts b/client/src/components/Chat/Messages/Content/Parts/__tests__/attachmentTypes.test.ts index 02113c8a25..f624097f59 100644 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/attachmentTypes.test.ts +++ b/client/src/components/Chat/Messages/Content/Parts/__tests__/attachmentTypes.test.ts @@ -1,5 +1,4 @@ import type { TAttachment } from 'librechat-data-provider'; -import { TOOL_ARTIFACT_TYPES } from '~/utils/artifacts'; import { artifactTypeForAttachment, attachmentSalience, @@ -8,6 +7,7 @@ import { isInternalSandboxArtifact, isTextAttachment, } from '../attachmentTypes'; +import { TOOL_ARTIFACT_TYPES } from '~/utils/artifacts'; const baseAttachment = (overrides: Partial = {}): TAttachment => ({ @@ -138,9 +138,8 @@ describe('artifactTypeForAttachment', () => { * pipeline instead. */ const attachment = baseAttachment({ filename: 'photo.jpg', - type: 'image/jpeg', text: undefined, - } as Partial); + }); expect(artifactTypeForAttachment(attachment)).toBeNull(); }); }); diff --git a/client/src/components/Chat/Messages/Content/__tests__/UIResourceCarousel.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/UIResourceCarousel.test.tsx index 6e472e3f49..91d9499898 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/UIResourceCarousel.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/UIResourceCarousel.test.tsx @@ -35,11 +35,11 @@ Object.defineProperty(HTMLElement.prototype, 'scrollTo', { describe('UIResourceCarousel', () => { const mockUIResources: UIResource[] = [ - { uri: 'resource1', mimeType: 'text/html', text: 'Resource 1' }, - { uri: 'resource2', mimeType: 'text/html', text: 'Resource 2' }, - { uri: 'resource3', mimeType: 'text/html', text: 'Resource 3' }, - { uri: 'resource4', mimeType: 'text/html', text: 'Resource 4' }, - { uri: 'resource5', mimeType: 'text/html', text: 'Resource 5' }, + { resourceId: 'resource1', uri: 'resource1', mimeType: 'text/html', text: 'Resource 1' }, + { resourceId: 'resource2', uri: 'resource2', mimeType: 'text/html', text: 'Resource 2' }, + { resourceId: 'resource3', uri: 'resource3', mimeType: 'text/html', text: 'Resource 3' }, + { resourceId: 'resource4', uri: 'resource4', mimeType: 'text/html', text: 'Resource 4' }, + { resourceId: 'resource5', uri: 'resource5', mimeType: 'text/html', text: 'Resource 5' }, ]; const mockHandleUIAction = handleUIAction as jest.MockedFunction; diff --git a/client/src/components/Chat/Messages/__tests__/MessageIcon.render.test.tsx b/client/src/components/Chat/Messages/__tests__/MessageIcon.render.test.tsx index d37ad9692a..f17767f680 100644 --- a/client/src/components/Chat/Messages/__tests__/MessageIcon.render.test.tsx +++ b/client/src/components/Chat/Messages/__tests__/MessageIcon.render.test.tsx @@ -104,7 +104,7 @@ describe('MessageIcon render cycles', () => { const { rerender } = render(); iconRenderCount.current = 0; - const agent2 = makeAgent({ avatar: { filepath: '/images/new-avatar.png' } }); + const agent2 = makeAgent({ avatar: { filepath: '/images/new-avatar.png', source: 'local' } }); rerender(); expect(iconRenderCount.current).toBe(1); diff --git a/client/src/components/Chat/Messages/__tests__/MessageIcon.test.ts b/client/src/components/Chat/Messages/__tests__/MessageIcon.test.ts index db4e9df316..f4f7813918 100644 --- a/client/src/components/Chat/Messages/__tests__/MessageIcon.test.ts +++ b/client/src/components/Chat/Messages/__tests__/MessageIcon.test.ts @@ -62,8 +62,14 @@ describe('MessageIcon arePropsEqual', () => { it('returns false when agent avatar filepath changes', () => { expect( arePropsEqual( - { iconData: baseIconData, agent: makeAgent({ avatar: { filepath: '/a.png' } }) }, - { iconData: baseIconData, agent: makeAgent({ avatar: { filepath: '/b.png' } }) }, + { + iconData: baseIconData, + agent: makeAgent({ avatar: { filepath: '/a.png', source: 'local' } }), + }, + { + iconData: baseIconData, + agent: makeAgent({ avatar: { filepath: '/b.png', source: 'local' } }), + }, ), ).toBe(false); }); diff --git a/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx b/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx index 4c85dc6cc9..4cc36033d0 100644 --- a/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx @@ -1,6 +1,12 @@ import { useState, useRef } from 'react'; import { Trans } from 'react-i18next'; import { QrCode, RotateCw, Trash2 } from 'lucide-react'; +import { + PermissionTypes, + Permissions, + PermissionBits, + ResourceType, +} from 'librechat-data-provider'; import { Label, Button, @@ -13,19 +19,13 @@ import { useToastContext, OGDialogContent, } from '@librechat/client'; -import { - PermissionTypes, - Permissions, - PermissionBits, - ResourceType, -} from 'librechat-data-provider'; import type { TSharedLinkGetResponse } from 'librechat-data-provider'; -import GenericGrantAccessDialog from '~/components/Sharing/GenericGrantAccessDialog'; import { useCreateSharedLinkMutation, useUpdateSharedLinkMutation, useDeleteSharedLinkMutation, } from '~/data-provider'; +import GenericGrantAccessDialog from '~/components/Sharing/GenericGrantAccessDialog'; import { useHasAccess, useResourcePermissions, useLocalize } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { buildShareLinkUrl } from '~/utils'; @@ -220,7 +220,7 @@ export default function SharedLinkButton({ [] = [ +type TFileWithVectors = TFile & { vectorsAttached?: TVectorStore[] }; + +export const fileTableColumns: ColumnDef[] = [ { id: 'select', header: ({ table }) => { @@ -53,7 +56,7 @@ export const fileTableColumns: ColumnDef[] = [ size: '150px', }, accessorKey: 'filename', - header: ({ column }) => { + header: () => { const localize = useLocalize(); return <>{localize('com_ui_name')}; }, @@ -68,7 +71,8 @@ export const fileTableColumns: ColumnDef[] = [ return 'Vector Stores'; }, cell: ({ row }) => { - const { vectorsAttached: attachedVectorStores } = row.original; + const localize = useLocalize(); + const { vectorsAttached: attachedVectorStores = [] } = row.original; return ( <> {attachedVectorStores.map((vectorStore, index) => { @@ -80,7 +84,7 @@ export const fileTableColumns: ColumnDef[] = [ >   - {attachedVectorStores.length - index} more + {localize('com_ui_more_count', { 0: attachedVectorStores.length - index })} ); } @@ -100,17 +104,19 @@ export const fileTableColumns: ColumnDef[] = [ { accessorKey: 'updatedAt', header: () => { - const localize = useLocalize(); return 'Modified'; }, - cell: ({ row }) => formatDate(row.original.updatedAt), + cell: ({ row }) => { + const { updatedAt } = row.original; + return formatDate(updatedAt instanceof Date ? updatedAt.toISOString() : (updatedAt ?? '')); + }, }, { accessorKey: 'actions', header: () => { return 'Actions'; }, - cell: ({ row }) => { + cell: () => { return ( <>
diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx index a033ed322c..77565acb7f 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/AutoSendTextSelector.tsx @@ -40,8 +40,9 @@ export default function AutoSendTextSelector() { } }; - const handleInputChange = (value: number[] | null) => { - const newValue = value ? value[0] : 3; + const handleInputChange = (value: string | number | null) => { + const parsed = typeof value === 'number' ? value : parseInt(String(value ?? ''), 10); + const newValue = Number.isNaN(parsed) ? 3 : parsed; setDelayValue(newValue); if (isEnabled) { setAutoSendText(newValue); diff --git a/client/src/components/Prompts/buttons/AutoSendPrompt.tsx b/client/src/components/Prompts/buttons/AutoSendPrompt.tsx index 759c33739e..21194e248e 100644 --- a/client/src/components/Prompts/buttons/AutoSendPrompt.tsx +++ b/client/src/components/Prompts/buttons/AutoSendPrompt.tsx @@ -31,6 +31,7 @@ export default function AutoSendPrompt({ checked={autoSendPrompts} tabIndex={-1} aria-hidden="true" + aria-label={localize('com_nav_auto_send_prompts')} className="pointer-events-none" /> {localize('com_nav_auto_send_prompts')} diff --git a/client/src/components/SidePanel/Agents/AgentPanel.test.tsx b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx index a3df6d52c4..e2899bf1e6 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.test.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx @@ -3,7 +3,6 @@ */ import * as React from 'react'; import { render, waitFor, fireEvent } from '@testing-library/react'; -import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { Agent } from 'librechat-data-provider'; diff --git a/client/src/components/SidePanel/Agents/ImageVision.tsx b/client/src/components/SidePanel/Agents/ImageVision.tsx index bc4e117896..7ac1be83f8 100644 --- a/client/src/components/SidePanel/Agents/ImageVision.tsx +++ b/client/src/components/SidePanel/Agents/ImageVision.tsx @@ -1,12 +1,12 @@ import { Checkbox } from '@librechat/client'; import { Capabilities } from 'librechat-data-provider'; import { useFormContext, Controller } from 'react-hook-form'; -import type { AgentForm } from '~/common'; +import type { AssistantForm } from '~/common'; import { useLocalize } from '~/hooks'; export default function ImageVision() { const localize = useLocalize(); - const methods = useFormContext(); + const methods = useFormContext(); const { control, setValue, getValues } = methods; return ( @@ -21,10 +21,12 @@ export default function ImageVision() { onCheckedChange={field.onChange} className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" value={field.value?.toString()} + aria-labelledby="image-vision-label" /> )} />