diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f5b2f29990c86..7b5366915d387 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -4668,11 +4668,22 @@ export default function ChatView(props: ChatViewProps) { modelStrip.offsetHeight + Number.parseFloat(getComputedStyle(modelStrip).marginTop), ) : 0; + const noticeStack = composerOverlayElement?.querySelector( + '[data-composer-banner-stack="true"]', + ); + const expandedNoticesHeight = noticeStack + ? Math.max( + 0, + noticeStack.getBoundingClientRect().height - + Number.parseFloat(getComputedStyle(noticeStack).minHeight), + ) + : 0; const nextInset = resolveComposerTimelineInset({ currentInset: composerTimelineInsetRef.current, - overlayHeight: nextHeight, + overlayHeight: height, isResting, restingOnlyHeight, + expandedNoticesHeight, }); if (composerTimelineInsetRef.current !== nextInset) { composerTimelineInsetRef.current = nextInset; diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index dd9d7c570db37..8e410d90e648d 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -121,6 +121,7 @@ export function ComposerBannerStack({ className, items, attachedAbove }: Compose {hasStack ? (
{ diff --git a/apps/web/src/components/composerFooterLayout.test.ts b/apps/web/src/components/composerFooterLayout.test.ts index f913c04391819..ef021a7dc013f 100644 --- a/apps/web/src/components/composerFooterLayout.test.ts +++ b/apps/web/src/components/composerFooterLayout.test.ts @@ -140,6 +140,35 @@ describe("resolveComposerTimelineInset", () => { resolveComposerTimelineInset({ currentInset: 0, overlayHeight: 60, isResting: true }), ).toBe(60 + COMPOSER_RESTING_EXPANSION_MIN_PX); }); + + it("keeps the timeline reservation stable through notice expansion and collapse", () => { + const collapsed = resolveComposerTimelineInset({ + currentInset: 0, + overlayHeight: 172, + isResting: false, + }); + const expanded = resolveComposerTimelineInset({ + currentInset: collapsed, + overlayHeight: 532, + isResting: false, + expandedNoticesHeight: 360, + }); + expect(expanded).toBe(collapsed); + const resting = resolveComposerTimelineInset({ + currentInset: expanded, + overlayHeight: 470, + isResting: true, + expandedNoticesHeight: 360, + }); + expect(resting).toBe(collapsed); + expect( + resolveComposerTimelineInset({ + currentInset: resting, + overlayHeight: 110, + isResting: true, + }), + ).toBe(collapsed); + }); }); describe("shouldUseRestingComposerLayout", () => { diff --git a/apps/web/src/components/composerFooterLayout.ts b/apps/web/src/components/composerFooterLayout.ts index e0dce39603e01..5ae971f19a344 100644 --- a/apps/web/src/components/composerFooterLayout.ts +++ b/apps/web/src/components/composerFooterLayout.ts @@ -59,16 +59,20 @@ export function resolveComposerTimelineInset(input: { overlayHeight: number; isResting: boolean; restingOnlyHeight?: number; + expandedNoticesHeight?: number; }): number { + // Expanded notices temporarily cover the timeline. Remove their growth before + // rounding so fractional measurements reserve exactly the collapsed height. + const overlayHeight = Math.ceil(input.overlayHeight - (input.expandedNoticesHeight ?? 0)); return input.isResting ? Math.max( input.currentInset, - input.overlayHeight + + overlayHeight + (input.currentInset === 0 ? COMPOSER_RESTING_EXPANSION_MIN_PX - (input.restingOnlyHeight ?? 0) : 0), ) - : input.overlayHeight; + : overlayHeight; } export function overlayComposerIsResting(input: {