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';