mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-12 02:59:00 +00:00
⏩ refactor: Speed Up Subagent Ticker Refresh (#13141)
This commit is contained in:
parent
27266bbcdc
commit
f3b165ea84
2 changed files with 80 additions and 23 deletions
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue