diff --git a/client/src/components/Chat/Messages/Content/Parts/SubagentCall.tsx b/client/src/components/Chat/Messages/Content/Parts/SubagentCall.tsx
index 6a94c079c8..d36bca7fc8 100644
--- a/client/src/components/Chat/Messages/Content/Parts/SubagentCall.tsx
+++ b/client/src/components/Chat/Messages/Content/Parts/SubagentCall.tsx
@@ -42,10 +42,10 @@ interface SubagentCallProps {
}
const TICKER_MAX_LINES = 3;
-/** Trailing-edge throttle window for the live preview. Tuned down from
- * the original 1.2s so the ticker feels snappy when the container is
- * already full and frames are scrolling. */
-const TICKER_THROTTLE_MS = 800;
+/** Trailing-edge refresh window for the live preview once the ticker has
+ * enough text to fill the row. Keeps long streaming lines from repainting
+ * every token while still letting the collapsed subagent UI feel responsive. */
+export const SUBAGENT_TICKER_THROTTLE_MS = 400;
/** Below this live-buffer length we skip throttling entirely. Without
* this the user would see "Reasoning: I" for ~1s while the model
* streams the rest of the sentence — the pass-through lets early
@@ -243,7 +243,7 @@ export default function SubagentCall({
const displayedTickerLines = useThrottledValue(
tickerLines,
- TICKER_THROTTLE_MS,
+ SUBAGENT_TICKER_THROTTLE_MS,
shouldThrottleTicker,
);
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 47a9aa12cb..38a82682a2 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
@@ -1,6 +1,6 @@
import React from 'react';
import { RecoilRoot, useRecoilCallback } from 'recoil';
-import { render, screen, act, fireEvent, waitFor } from '@testing-library/react';
+import { render, screen, act, fireEvent, waitFor, within } from '@testing-library/react';
import type { SubagentUpdateEvent } from 'librechat-data-provider';
import type {
SubagentContentPart,
@@ -16,7 +16,7 @@ import {
initSubagentTickerState,
} from '~/utils/subagentContent';
import { subagentProgressByToolCallId } from '~/store/subagents';
-import SubagentCall from '../SubagentCall';
+import SubagentCall, { SUBAGENT_TICKER_THROTTLE_MS } from '../SubagentCall';
jest.mock('~/hooks', () => ({
useLocalize:
@@ -83,18 +83,22 @@ jest.mock('../Attachment', () => ({
),
}));
-jest.mock('@librechat/client', () => ({
- OGDialog: ({ children }: { children: React.ReactNode }) => <>{children}>,
- OGDialogContent: ({ children }: { children: React.ReactNode }) => (
-
{children}
- ),
- OGDialogTitle: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- OGDialogDescription: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}));
+jest.mock(
+ '@librechat/client',
+ () => ({
+ OGDialog: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ OGDialogContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ OGDialogTitle: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ OGDialogDescription: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ }),
+ { virtual: true },
+);
jest.mock('lucide-react', () => ({
// eslint-disable-next-line i18next/no-literal-string
@@ -131,6 +135,10 @@ jest.mock('~/utils', () => ({
cn: (...classes: unknown[]) => classes.filter(Boolean).join(' '),
}));
+afterEach(() => {
+ jest.useRealTimers();
+});
+
/** The dialog wraps single parts in `Container` and grouped tool_calls in
* `ToolCallGroup`. Stub both as transparent wrappers so the tests still
* assert on the leaf renderers (Text/Reasoning/ToolCall) without pulling
@@ -229,10 +237,13 @@ function renderWithState(args: {
/>
,
);
- act(() => {
- setter.current?.(args.progress ?? null);
- });
- return rendered;
+ const setProgress = (next: SubagentProgress | null) => {
+ act(() => {
+ setter.current?.(next);
+ });
+ };
+ setProgress(args.progress ?? null);
+ return { ...rendered, setProgress };
}
describe('SubagentCall — status resolution', () => {
@@ -443,6 +454,52 @@ describe('SubagentCall — ticker', () => {
/** Only one "Writing:" label, not three — deltas collapse into one live line. */
expect(screen.getAllByText('Writing:')).toHaveLength(1);
});
+
+ it('refreshes long live previews after the subagent ticker throttle window', () => {
+ jest.useFakeTimers();
+ const firstPreview = 'First live preview '.repeat(8).trim();
+ const secondPreview = 'Second live preview '.repeat(8).trim();
+ const eventForText = (text: string): SubagentUpdateEvent => ({
+ runId: 'p',
+ subagentRunId: 'run_a',
+ subagentType: 'self',
+ subagentAgentId: 'child',
+ phase: 'message_delta',
+ data: { delta: { content: [{ type: 'text', text }] } },
+ timestamp: '',
+ });
+ const progressForText = (text: string): SubagentProgress =>
+ progressFromEvents({
+ subagentRunId: 'run_a',
+ subagentType: 'self',
+ status: 'message_delta',
+ events: [eventForText(text)],
+ });
+
+ const { setProgress } = renderWithState({
+ toolCallId: 'call_throttled_writing',
+ initialProgress: 0.3,
+ isSubmitting: true,
+ progress: progressForText(firstPreview),
+ });
+ const ticker = within(screen.getByRole('button', { name: 'Running agent' }));
+
+ expect(ticker.getByText(firstPreview)).toBeInTheDocument();
+ setProgress(progressForText(secondPreview));
+ expect(ticker.getByText(firstPreview)).toBeInTheDocument();
+ expect(ticker.queryByText(secondPreview)).not.toBeInTheDocument();
+
+ act(() => {
+ jest.advanceTimersByTime(SUBAGENT_TICKER_THROTTLE_MS - 1);
+ });
+ expect(ticker.getByText(firstPreview)).toBeInTheDocument();
+ expect(ticker.queryByText(secondPreview)).not.toBeInTheDocument();
+
+ act(() => {
+ jest.advanceTimersByTime(1);
+ });
+ expect(ticker.getByText(secondPreview)).toBeInTheDocument();
+ });
});
describe('SubagentCall — dialog content', () => {