refactor: Speed Up Subagent Ticker Refresh (#13141)

This commit is contained in:
Danny Avila 2026-05-15 14:50:50 -04:00
parent 27266bbcdc
commit f3b165ea84
2 changed files with 80 additions and 23 deletions

View file

@ -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,
);

View file

@ -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 }) => (
<div data-testid="dialog-content">{children}</div>
),
OGDialogTitle: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-title">{children}</div>
),
OGDialogDescription: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-description">{children}</div>
),
}));
jest.mock(
'@librechat/client',
() => ({
OGDialog: ({ children }: { children: React.ReactNode }) => <>{children}</>,
OGDialogContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-content">{children}</div>
),
OGDialogTitle: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-title">{children}</div>
),
OGDialogDescription: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-description">{children}</div>
),
}),
{ 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: {
/>
</RecoilRoot>,
);
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', () => {