diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx
index 7aa73a54e6..3d13fa6ae0 100644
--- a/client/src/components/Chat/Messages/MessageParts.tsx
+++ b/client/src/components/Chat/Messages/MessageParts.tsx
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps, TMessageIcon } from '~/common';
import { useMessageHelpers, useLocalize, useAttachments, useContentMetadata } from '~/hooks';
+import { cn, getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import ContentParts from './Content/ContentParts';
import { fontSizeAtom } from '~/store/fontSize';
@@ -11,7 +12,6 @@ import SiblingSwitch from './SiblingSwitch';
import MultiMessage from './MultiMessage';
import HoverButtons from './HoverButtons';
import SubRow from './SubRow';
-import { cn, getMessageAriaLabel } from '~/utils';
import store from '~/store';
export default function Message(props: TMessageProps) {
@@ -125,6 +125,9 @@ export default function Message(props: TMessageProps) {
>
{!hasParallelContent && (
diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx
index 4114baefe4..6b3f05ce5d 100644
--- a/client/src/components/Messages/ContentRender.tsx
+++ b/client/src/components/Messages/ContentRender.tsx
@@ -4,13 +4,13 @@ import { useRecoilValue } from 'recoil';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps, TMessageIcon } from '~/common';
import { useAttachments, useLocalize, useMessageActions, useContentMetadata } from '~/hooks';
+import { cn, getHeaderPrefixForScreenReader, getMessageAriaLabel } from '~/utils';
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import SubRow from '~/components/Chat/Messages/SubRow';
-import { cn, getMessageAriaLabel } from '~/utils';
import { fontSizeAtom } from '~/store/fontSize';
import store from '~/store';
@@ -140,7 +140,10 @@ const ContentRender = memo(function ContentRender({
)}
>
{!hasParallelContent && (
-
{messageLabel}
+
+ {getHeaderPrefixForScreenReader(msg, localize)}
+ {messageLabel}
+
)}
diff --git a/client/src/utils/__tests__/messages.test.ts b/client/src/utils/__tests__/messages.test.ts
new file mode 100644
index 0000000000..4af9f69439
--- /dev/null
+++ b/client/src/utils/__tests__/messages.test.ts
@@ -0,0 +1,82 @@
+import type { TMessage } from 'librechat-data-provider';
+import type { LocalizeFunction } from '~/common';
+import { getMessageAriaLabel, getHeaderPrefixForScreenReader } from '../messages';
+
+const translations: Record = {
+ com_endpoint_message: 'Message',
+ com_endpoint_message_new: 'Message {{0}}',
+ com_ui_prompt: 'Prompt',
+ com_ui_response: 'Response',
+};
+
+const localize: LocalizeFunction = ((key: string, args?: Record) => {
+ const template = translations[key] ?? key;
+ if (args) {
+ return Object.entries(args).reduce(
+ (result, [k, v]) => result.replace(`{{${k}}}`, String(v)),
+ template,
+ );
+ }
+ return template;
+}) as LocalizeFunction;
+
+const makeMessage = (overrides: Partial = {}): TMessage =>
+ ({
+ messageId: 'msg-1',
+ isCreatedByUser: false,
+ ...overrides,
+ }) as TMessage;
+
+describe('getMessageAriaLabel', () => {
+ it('returns "Message N" when depth is present and valid', () => {
+ const msg = makeMessage({ depth: 2 });
+ expect(getMessageAriaLabel(msg, localize)).toBe('Message 3');
+ });
+
+ it('returns "Message" when depth is undefined', () => {
+ const msg = makeMessage({ depth: undefined });
+ expect(getMessageAriaLabel(msg, localize)).toBe('Message');
+ });
+
+ it('returns "Message" when depth is negative', () => {
+ const msg = makeMessage({ depth: -1 });
+ expect(getMessageAriaLabel(msg, localize)).toBe('Message');
+ });
+
+ it('returns "Message 1" for depth 0 (root message)', () => {
+ const msg = makeMessage({ depth: 0 });
+ expect(getMessageAriaLabel(msg, localize)).toBe('Message 1');
+ });
+});
+
+describe('getHeaderPrefixForScreenReader', () => {
+ it('returns "Prompt N: " for user messages with valid depth', () => {
+ const msg = makeMessage({ isCreatedByUser: true, depth: 2 });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt 3: ');
+ });
+
+ it('returns "Response N: " for AI messages with valid depth', () => {
+ const msg = makeMessage({ isCreatedByUser: false, depth: 0 });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response 1: ');
+ });
+
+ it('returns "Prompt: " for user messages without depth', () => {
+ const msg = makeMessage({ isCreatedByUser: true, depth: undefined });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt: ');
+ });
+
+ it('returns "Response: " for AI messages without depth', () => {
+ const msg = makeMessage({ isCreatedByUser: false, depth: undefined });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response: ');
+ });
+
+ it('omits number when depth is -1 (no "Prompt 0:" regression)', () => {
+ const msg = makeMessage({ isCreatedByUser: true, depth: -1 });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Prompt: ');
+ });
+
+ it('omits number when depth is negative', () => {
+ const msg = makeMessage({ isCreatedByUser: false, depth: -5 });
+ expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response: ');
+ });
+});
diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts
index 7197b6c2db..27dc063481 100644
--- a/client/src/utils/messages.ts
+++ b/client/src/utils/messages.ts
@@ -14,7 +14,6 @@ import type {
} from 'librechat-data-provider';
import type { QueryClient } from '@tanstack/react-query';
import type { LocalizeFunction } from '~/common';
-import _ from 'lodash';
export const TEXT_KEY_DIVIDER = '|||';
@@ -185,12 +184,36 @@ export const clearMessagesCache = (
}
};
+/** Returns a 1-based message number, or null if depth is absent or invalid. */
+const getMessageNumber = (message: TMessage): number | null => {
+ if (message.depth == null || message.depth < 0) {
+ return null;
+ }
+ return message.depth + 1;
+};
+
export const getMessageAriaLabel = (message: TMessage, localize: LocalizeFunction): string => {
- return !_.isNil(message.depth)
- ? localize('com_endpoint_message_new', { 0: message.depth + 1 })
+ const number = getMessageNumber(message);
+ return number != null
+ ? localize('com_endpoint_message_new', { 0: number })
: localize('com_endpoint_message');
};
+/**
+ * Provides a screen-reader-only heading prefix distinguishing prompts from responses,
+ * with an optional 1-based turn number derived from message depth.
+ */
+export const getHeaderPrefixForScreenReader = (
+ message: TMessage,
+ localize: LocalizeFunction,
+): string => {
+ const number = getMessageNumber(message);
+ const suffix = number != null ? ` ${number}` : '';
+ return message.isCreatedByUser
+ ? `${localize('com_ui_prompt')}${suffix}: `
+ : `${localize('com_ui_response')}${suffix}: `;
+};
+
/**
* Creates initial content parts for dual message display with agent-based grouping.
* Sets up primary and added agent content parts with agentId for column rendering.