From bd31ecd129637442811d61d487010c1b0a3ce355 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:27:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20morphing=20state=20ic?= =?UTF-8?q?ons=20across=20the=20client=20(#14697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(client-ui): add morphing icon support * feat(client-ui): morph shared state icons * feat(chat-ui): morph chat action icons * feat(client-ui): morph remaining action icons * fix(client-ui): type morph icon test factory * test(client): type agent detail fixture * test(client-ui): guard morph icon accessibility defaults * test(chat-ui): assert mermaid view toggle via morph icon * style(chat-ui): sort mermaid header imports * test(api): wait for rebuilt MCP session to finish connecting The SSE conflict recovery test treated a new session id as a finished reconnect. initialize assigns that id before connectClient emits connected, so isConnected() can still be false. Wait for a live connection on the rebuilt session instead. * fix(client): clean up rebase fallout --- client/jest.config.cjs | 2 +- client/package.json | 4 +- client/src/components/Agents/AgentDetail.tsx | 6 +- .../components/Agents/AgentDetailContent.tsx | 13 ++- .../Agents/tests/AgentDetailContent.spec.tsx | 83 ++++++++++++------- .../components/Artifacts/DownloadArtifact.tsx | 10 +-- .../__tests__/DownloadArtifact.test.tsx | 20 ++++- .../components/Chat/Input/CollapseChat.tsx | 10 +-- .../components/Chat/Input/InFlightSteers.tsx | 20 ++--- .../src/components/Chat/Input/SteerMenu.tsx | 22 ++--- .../Input/__tests__/InFlightSteers.test.tsx | 36 +++++--- .../__tests__/PendingSteerChips.test.tsx | 12 ++- .../components/EndpointModelItem.tsx | 10 +-- .../Endpoints/components/ModelSpecItem.tsx | 10 +-- .../Chat/Messages/Content/DialogImage.tsx | 14 ++-- .../Chat/Messages/Content/Parts/Summary.tsx | 15 +--- .../Chat/Messages/Content/Parts/Thinking.tsx | 26 ++---- .../components/Chat/Messages/HoverButtons.tsx | 6 +- .../Chat/Messages/MinimalHoverButtons.tsx | 9 +- .../Messages/Content/ActionButton.tsx | 27 ++---- .../Messages/Content/CopyButton.tsx | 2 +- .../Messages/Content/DownloadButton.tsx | 2 +- .../Content/Mermaid/MermaidDialog.tsx | 18 ++-- .../Content/Mermaid/MermaidHeader.test.tsx | 14 +++- .../Content/Mermaid/MermaidHeader.tsx | 11 +-- .../Messages/Content/Mermaid/ZoomControls.tsx | 5 +- .../components/Messages/Content/RunCode.tsx | 29 ++++--- .../Content/__tests__/CopyButton.test.tsx | 38 +++++++++ .../Prompts/display/PromptTextCard.tsx | 10 +-- .../Sharing/GenericGrantAccessDialog.tsx | 10 +-- .../SidePanel/Agents/Search/Action.tsx | 14 ++-- .../MCPBuilder/MCPServerDialog/index.tsx | 5 +- .../MCPServerDialog/sections/AuthSection.tsx | 14 +++- .../Skills/display/SkillFileViewer.tsx | 7 +- .../Skills/forms/SkillContentEditor.tsx | 13 +-- client/test/mockMorphIcon.tsx | 82 ++++++++++++++++++ package-lock.json | 54 +++++++++--- .../MCPConnectionSseConflict.test.ts | 5 ++ packages/client/jest.config.js | 2 +- packages/client/package.json | 4 + packages/client/src/components/Badge.tsx | 5 +- .../components/DataTable/DataTable.spec.tsx | 30 +++---- .../src/components/DataTable/DataTable.tsx | 27 ++++-- .../client/src/components/MorphIcon.spec.tsx | 29 +++++++ packages/client/src/components/MorphIcon.ts | 2 + .../src/components/SecretInput.spec.tsx | 51 ++++++++++++ .../client/src/components/SecretInput.tsx | 7 +- .../src/components/ThemeSelector.spec.tsx | 64 ++++++++++++++ .../client/src/components/ThemeSelector.tsx | 18 ++-- packages/client/src/components/index.ts | 1 + packages/client/src/test/mockMorphIcon.tsx | 30 +++++++ 51 files changed, 667 insertions(+), 291 deletions(-) create mode 100644 client/src/components/Messages/Content/__tests__/CopyButton.test.tsx create mode 100644 client/test/mockMorphIcon.tsx create mode 100644 packages/client/src/components/MorphIcon.spec.tsx create mode 100644 packages/client/src/components/MorphIcon.ts create mode 100644 packages/client/src/components/SecretInput.spec.tsx create mode 100644 packages/client/src/components/ThemeSelector.spec.tsx create mode 100644 packages/client/src/test/mockMorphIcon.tsx diff --git a/client/jest.config.cjs b/client/jest.config.cjs index ed7ce852e8..7a32adf945 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -49,7 +49,7 @@ module.exports = { 'jest-file-loader', }, transformIgnorePatterns: [ - '/node_modules/(?!(@ariakit/react-components|@ariakit/react-utils|@ariakit/react-store|@ariakit/components|@ariakit/store|@ariakit/utils|@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/', + '/node_modules/(?!(@ariakit/react-components|@ariakit/react-utils|@ariakit/react-store|@ariakit/components|@ariakit/store|@ariakit/utils|@zattoo/use-double-click|@dicebear|@react-dnd|react-dnd.*|dnd-core|filenamify|filename-reserved-regex|heic-to|lowlight|highlight\\.js|fault|lucide-react|lucide|morphicons|react-markdown|unified|bail|trough|devlop|is-.*|parse-entities|stringify-entities|character-.*|trim-lines|style-to-object|inline-style-parser|html-url-attributes|escape-string-regexp|longest-streak|zwitch|ccount|markdown-table|comma-separated-tokens|space-separated-tokens|web-namespaces|property-information|remark-.*|rehype-.*|recma-.*|hast.*|mdast-.*|unist-.*|vfile.*|micromark.*|estree-util-.*|decode-named-character-reference)/)/', ], setupFiles: ['/test/polyfills.js'], setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '/test/setupTests.js'], diff --git a/client/package.json b/client/package.json index 3787321f3e..0e73dbb04c 100644 --- a/client/package.json +++ b/client/package.json @@ -80,7 +80,8 @@ "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.23", - "lucide-react": "^0.394.0", + "lucide": "^0.525.0", + "lucide-react": "^0.525.0", "match-sorter": "^8.1.0", "mdast-util-directive": "^3.0.0", "mdast-util-from-markdown": "^2.0.1", @@ -94,6 +95,7 @@ "micromark-util-character": "^2.1.0", "micromark-util-symbol": "^2.0.0", "monaco-editor": "^0.56.0", + "morphicons": "^1.4.1", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", "react": "^18.2.0", diff --git a/client/src/components/Agents/AgentDetail.tsx b/client/src/components/Agents/AgentDetail.tsx index 75dbf747ef..b32a0c879b 100644 --- a/client/src/components/Agents/AgentDetail.tsx +++ b/client/src/components/Agents/AgentDetail.tsx @@ -1,10 +1,12 @@ import React, { useRef } from 'react'; -import { Link, Pin, PinOff } from 'lucide-react'; +import { Link } from 'lucide-react'; +import { Pin, PinOff } from 'lucide'; import { useQueryClient } from '@tanstack/react-query'; import { OGDialog, OGDialogContent, Button, + MorphIcon, TooltipAnchor, useToastContext, } from '@librechat/client'; @@ -181,7 +183,7 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => onClick={handleFavoriteClick} aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')} > - {isFavorite ? : } + } /> diff --git a/client/src/components/Agents/AgentDetailContent.tsx b/client/src/components/Agents/AgentDetailContent.tsx index 4dde6f88d7..0ea51d4504 100644 --- a/client/src/components/Agents/AgentDetailContent.tsx +++ b/client/src/components/Agents/AgentDetailContent.tsx @@ -1,7 +1,14 @@ import React from 'react'; -import { Link, Pin, PinOff } from 'lucide-react'; +import { Link } from 'lucide-react'; +import { Pin, PinOff } from 'lucide'; import { useQueryClient } from '@tanstack/react-query'; -import { OGDialogContent, Button, TooltipAnchor, useToastContext } from '@librechat/client'; +import { + Button, + MorphIcon, + TooltipAnchor, + OGDialogContent, + useToastContext, +} from '@librechat/client'; import { QueryKeys, Constants, @@ -129,7 +136,7 @@ const AgentDetailContent: React.FC = ({ agent }) => { onClick={handleFavoriteClick} aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')} > - {isFavorite ? : } + } /> diff --git a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx index 4ad9a8789a..f6e638bec8 100644 --- a/client/src/components/Agents/tests/AgentDetailContent.spec.tsx +++ b/client/src/components/Agents/tests/AgentDetailContent.spec.tsx @@ -4,10 +4,12 @@ import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { TConversation } from 'librechat-data-provider'; import '@testing-library/jest-dom'; +import type t from 'librechat-data-provider'; import AgentDetailContent from '../AgentDetailContent'; const mockNewConversation = jest.fn(); let mockConversation: Partial | undefined; +const mockIsFavoriteAgent = jest.fn(() => false); jest.mock('librechat-data-provider', () => ({ QueryKeys: { @@ -28,23 +30,27 @@ jest.mock('librechat-data-provider', () => ({ }, })); -jest.mock('@librechat/client', () => ({ - OGDialogContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( - - ), - TooltipAnchor: ({ render }: { render: React.ReactNode }) => render, - useToastContext: () => ({ - showToast: jest.fn(), - }), -})); +jest.mock('@librechat/client', () => { + const { createPinMorphIconMock } = jest.requireActual('~/../test/mockMorphIcon'); + return { + OGDialogContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), + TooltipAnchor: ({ render }: { render: React.ReactNode }) => render, + MorphIcon: createPinMorphIconMock(), + useToastContext: () => ({ + showToast: jest.fn(), + }), + }; +}); jest.mock('~/hooks', () => ({ useDefaultConvo: () => jest.fn((value) => value.conversation), useFavorites: () => ({ - isFavoriteAgent: jest.fn(() => false), + isFavoriteAgent: mockIsFavoriteAgent, toggleFavoriteAgent: jest.fn(), }), useLocalize: () => (key: string, values?: Record) => { @@ -92,31 +98,40 @@ const renderWithClient = (children: React.ReactNode) => { return render({children}); }; -const baseAgent = { +const baseAgent: t.Agent = { id: 'agent-1', name: 'Agent One', description: 'Agent description', + created_at: 0, + avatar: null, provider: 'openai', model: 'gpt-4', - model_parameters: {}, -} as React.ComponentProps['agent']; + model_parameters: { + temperature: null, + maxContextTokens: null, + max_context_tokens: null, + max_output_tokens: null, + top_p: null, + frequency_penalty: null, + presence_penalty: null, + }, +}; describe('AgentDetailContent', () => { beforeEach(() => { jest.clearAllMocks(); mockConversation = undefined; + mockIsFavoriteAgent.mockReturnValue(false); }); it('renders support contact with mailto link', () => { renderWithClient( , ); @@ -131,12 +146,10 @@ describe('AgentDetailContent', () => { it('falls back to owner contact when support contact is missing', () => { renderWithClient( , ); @@ -171,4 +184,16 @@ describe('AgentDetailContent', () => { }), }); }); + + it('shows pin icon when agent is not a favorite and pin-off when it is', () => { + const { unmount } = renderWithClient(); + expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument(); + expect(screen.getByTestId('morph-icon')).toHaveAttribute('data-icon', 'pin'); + unmount(); + + mockIsFavoriteAgent.mockReturnValue(true); + renderWithClient(); + expect(screen.getByRole('button', { name: 'Unpin' })).toBeInTheDocument(); + expect(screen.getByTestId('morph-icon')).toHaveAttribute('data-icon', 'pin-off'); + }); }); diff --git a/client/src/components/Artifacts/DownloadArtifact.tsx b/client/src/components/Artifacts/DownloadArtifact.tsx index 3e2ff7b0e2..ca48eebe7d 100644 --- a/client/src/components/Artifacts/DownloadArtifact.tsx +++ b/client/src/components/Artifacts/DownloadArtifact.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Button } from '@librechat/client'; -import { Download, CircleCheckBig } from 'lucide-react'; +import { Download, CircleCheckBig } from 'lucide'; +import { Button, MorphIcon } from '@librechat/client'; import type { Artifact } from '~/common'; import { useAttachmentLink, @@ -94,11 +94,7 @@ const DownloadArtifact = ({ artifact }: { artifact: Artifact }) => { onClick={handleDownload} aria-label={localize('com_ui_download_artifact')} > - {isDownloaded ? ( -