From a9da01fbe714f78b8ba381a578d3a3da2aed23d0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 5 Sep 2026 08:47:19 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=90=20fix:=20Keep=20the=20Live=20Slot?= =?UTF-8?q?=20Under=20a=20Phase=20Card=20One=20Row=20Tall=20(#15639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ 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. --- .../Messages/Content/ActivityPhaseGroup.tsx | 14 +++++++++++--- .../Messages/Content/AskUserQuestionCall.tsx | 6 ++---- .../Chat/Messages/Content/Parts/BashCall.tsx | 3 ++- .../Messages/Content/Parts/ExecuteCode.tsx | 3 ++- .../Content/Parts/FileAuthoringCall.tsx | 3 ++- .../Content/Parts/PendingSkillCall.tsx | 3 ++- .../Messages/Content/Parts/ReadFileCall.tsx | 3 ++- .../Chat/Messages/Content/Parts/SkillCall.tsx | 3 ++- .../Chat/Messages/Content/ToolCall.tsx | 7 ++----- .../__tests__/ActivityPhaseGroup.test.tsx | 18 ++++++++++++++++++ .../components/Chat/Messages/Content/rows.ts | 8 ++++++++ 11 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 client/src/components/Chat/Messages/Content/rows.ts diff --git a/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx b/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx index aa115260ec..fcbac246c8 100644 --- a/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx +++ b/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx @@ -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 ? ( - +
- +
) : null; const media = attachments != null && attachments.length > 0 ? ( diff --git a/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx b/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx index 8ff7d55629..e008f7130a 100644 --- a/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx +++ b/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx @@ -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 ( <> -
+
-
+
-
+
-
+
+
-
+
-
+
-
+
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( + +
+ , + ); + + /** 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( diff --git a/client/src/components/Chat/Messages/Content/rows.ts b/client/src/components/Chat/Messages/Content/rows.ts new file mode 100644 index 0000000000..c3b7bfbac0 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/rows.ts @@ -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';