📐 fix: Keep the Live Slot Under a Phase Card One Row Tall (#15639)
Some checks are pending
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* ♻️ refactor: Share the Tool Row Box Across Call Cards

Eight call cards carried the same literal for their header row's box —
`relative my-1.5 flex h-5 shrink-0 items-center gap-2.5` — one per card kind.
It is one design decision (a 20px line with 6px above and below), and the
next commit needs a ninth element to match it exactly, so it becomes
`TOOL_ROW_CLASSES` in `rows.ts`. No rendered output changes.

* 📐 fix: Keep the Live Slot Under a Phase Card One Row Tall

While a run streams, the slot beneath a collapsed synthesized phase card
alternates between two things: the streaming cursor, rendered the moment a
label fills and its row folds into the card, and the next call's row, rendered
the moment that call starts. Replaying a real export through `ContentParts`
step by step showed those two boxes differ: the cursor sat in a bare
`Container` (20px, no margins) while every call row is `my-1.5 h-5` (32px).
Everything beneath the card therefore stepped up on each absorb and back down
on each call — measured at 6px in Chromium with the app's generated CSS, and
larger in the live column where the row's bottom margin cannot collapse.

The cursor now takes `TOOL_ROW_CLASSES`, the exact box of the row it stands
in for, so the swap is height-neutral; a 2px inset centers the 12px dot on
the 16px glyph rail. The test asserts the cursor carries every token of the
row box, and fails against the previous container.
This commit is contained in:
Danny Avila 2026-09-05 08:47:19 -04:00 • committed by GitHub
parent f4e967bf22
commit a9da01fbe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 53 additions and 18 deletions

View file

@ -13,7 +13,7 @@ import {
import useSmoothStreaming from '~/hooks/Messages/useSmoothStreaming';
import { getActivityLabelText } from '~/utils/activityLabels';
import { AttachmentGroup, EmptyText } from './Parts';
import Container from './Container';
import { TOOL_ROW_CLASSES } from './rows';
import { cn } from '~/utils';
/** Matches `EXPAND_TRANSITION` so the panel and the label ticker resolve on
@ -269,10 +269,18 @@ export default function ActivityPhaseGroup({
};
}, [foldsIn, isSettled]);
/** The live slot under a collapsed card alternates between this cursor and
* the next call's row for the rest of the run: a label fills, the row it
* headed folds into the card and the cursor takes its place; the next call
* starts and the cursor gives way to a row again. A cursor in a bare
* `Container` (20px, no margins) was 12px shorter than the tool row
* (`my-1.5 h-5`), so everything beneath the card stepped up on every
* absorb and back down on every call. The cursor takes the row's exact box
* and a 2px inset centers the 12px dot on the 16px glyph rail. */
const cursor = showCursor ? (
<Container>
<div className={cn(TOOL_ROW_CLASSES, 'ps-0.5')} data-testid="activity-phase-cursor">
<EmptyText />
</Container>
</div>
) : null;
const media =
attachments != null && attachments.length > 0 ? (

View file

@ -11,6 +11,7 @@ import AskUserQuestionProgress from './AskUserQuestionProgress';
import { useLocalize, useExpandCollapse } from '~/hooks';
import ProgressText from './ProgressText';
import EmptyText from './Parts/EmptyText';
import { TOOL_ROW_CLASSES } from './rows';
import Container from './Container';
import store from '~/store';
@ -147,10 +148,7 @@ export default function AskUserQuestionCall({
return (
<>
<div
className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5"
data-testid="ask-user-question-call"
>
<div className={TOOL_ROW_CLASSES} data-testid="ask-user-question-call">
<ProgressText
phase="completed"
onClick={toggleExpanded}

View file

@ -14,6 +14,7 @@ import useFollowScroll from './useFollowScroll';
import { ERROR_PATTERNS } from './ExecuteCode';
import { AttachmentGroup } from './Attachment';
import { useToolCallIntent } from './intent';
import { TOOL_ROW_CLASSES } from '../rows';
import PtcToolTrace from './PtcToolTrace';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -121,7 +122,7 @@ export default function BashCall({
return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<ProgressText
phase={phase}
onClick={toggleCode}

View file

@ -11,6 +11,7 @@ import CodeWindowHeader from './CodeWindowHeader';
import useFollowScroll from './useFollowScroll';
import { AttachmentGroup } from './Attachment';
import { useToolCallIntent } from './intent';
import { TOOL_ROW_CLASSES } from '../rows';
import PtcToolTrace from './PtcToolTrace';
import { useLocalize } from '~/hooks';
import Stdout from './Stdout';
@ -130,7 +131,7 @@ export default function ExecuteCode({
return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<ProgressText
phase={phase}
onClick={toggleCode}

View file

@ -10,6 +10,7 @@ import useFollowScroll from './useFollowScroll';
import { AttachmentGroup } from './Attachment';
import { langFromPath } from './ReadFileCall';
import { useToolCallIntent } from './intent';
import { TOOL_ROW_CLASSES } from '../rows';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -176,7 +177,7 @@ export default function FileAuthoringCall({
return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<ProgressText
phase={phase}
onClick={toggleCode}

View file

@ -1,4 +1,5 @@
import { ScrollText } from 'lucide-react';
import { TOOL_ROW_CLASSES } from '../rows';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -33,7 +34,7 @@ export default function PendingSkillCall({
: localize('com_ui_skill_running', { 0: skillName });
return (
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<div className="progress-text-wrapper text-token-text-secondary relative -mt-[0.75px] h-5 w-full leading-5">
<div
className="progress-text-content absolute left-0 top-0 max-w-full overflow-visible whitespace-nowrap"

View file

@ -8,6 +8,7 @@ import CodeWindowHeader from './CodeWindowHeader';
import { AttachmentGroup } from './Attachment';
import parseJsonField from './parseJsonField';
import { useToolCallIntent } from './intent';
import { TOOL_ROW_CLASSES } from '../rows';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -102,7 +103,7 @@ export default function ReadFileCall({
return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<ProgressText
phase={phase}
onClick={toggleCode}

View file

@ -6,6 +6,7 @@ import useToolCallState from './useToolCallState';
import { AttachmentGroup } from './Attachment';
import parseJsonField from './parseJsonField';
import { useToolCallIntent } from './intent';
import { TOOL_ROW_CLASSES } from '../rows';
import { useLocalize } from '~/hooks';
import Stdout from './Stdout';
import { cn } from '~/utils';
@ -46,7 +47,7 @@ export default function SkillCall({
return (
<>
<div className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5">
<div className={TOOL_ROW_CLASSES}>
<ProgressText
phase={phase}
onClick={toggleCode}

View file

@ -18,6 +18,7 @@ import { useToolCallIntent } from './Parts/intent';
import { AttachmentGroup } from './Parts';
import ToolCallInfo from './ToolCallInfo';
import ProgressText from './ProgressText';
import { TOOL_ROW_CLASSES } from './rows';
import { logger } from '~/utils';
import store from '~/store';
@ -274,11 +275,7 @@ export default function ToolCall({
return getFinishedText();
})()}
</span>
<div
className="relative my-1.5 flex h-5 shrink-0 items-center gap-2.5"
data-testid="tool-call"
data-tool-call-id={toolCallId}
>
<div className={TOOL_ROW_CLASSES} data-testid="tool-call" data-tool-call-id={toolCallId}>
<ProgressText
phase={phase}
onClick={handleToggleInfo}

View file

@ -2,6 +2,7 @@ import { ContentTypes } from 'librechat-data-provider';
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { TMessageContentParts } from 'librechat-data-provider';
import ActivityPhaseGroup from '../ActivityPhaseGroup';
import { TOOL_ROW_CLASSES } from '../rows';
const mockUseSmoothStreaming = jest.fn(() => true);
const mockScheduleLayoutReconcile = jest.fn((_target: HTMLElement | null) => jest.fn());
@ -99,6 +100,23 @@ describe('ActivityPhaseGroup', () => {
expect(container.querySelector('.result-thinking')).toBeInTheDocument();
});
test('the cursor occupies exactly a tool row box', () => {
render(
<ActivityPhaseGroup labelPart={labelPart} hasContent showCursor>
<div data-testid="phase-content" />
</ActivityPhaseGroup>,
);
/** The cursor and the next call's row trade places under the card for the
* rest of the run. Any difference between their boxes moves everything
* beneath the card on every swap. */
const cursor = screen.getByTestId('activity-phase-cursor');
for (const token of TOOL_ROW_CLASSES.split(' ')) {
expect(cursor).toHaveClass(token);
}
expect(cursor.querySelector('.result-thinking')).toBeInTheDocument();
});
test('opens the summary on the same glyph rail as the rows it replaces', () => {
render(
<ActivityPhaseGroup labelPart={labelPart} hasContent>

View file

@ -0,0 +1,8 @@
/**
* The vertical box of a live tool row: a 20px line with 6px margins above and
* below, shared by every call card header and by the streaming cursor a
* collapsed phase card renders beneath itself. The two trade places on every
* absorb → next-call cycle of a run, so they must occupy the same height or
* everything under the card moves on each swap.
*/
export const TOOL_ROW_CLASSES = 'relative my-1.5 flex h-5 shrink-0 items-center gap-2.5';