Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 44 additions & 23 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
import { MessagesTimeline } from "./chat/MessagesTimeline";
import type { AssistantCitationRequest } from "./chat/AssistantCitationSource";
import { resolveTimelineIsAtEnd } from "./chat/MessagesTimeline.logic";
import { resolveComposerTimelineInset } from "./composerFooterLayout";
import { resolveComposerTimelineInset, resolveScrollToEndClearance } from "./composerFooterLayout";
import { ChatHeader } from "./chat/ChatHeader";
import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";
import { expandedImageKey, type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
Expand Down Expand Up @@ -5121,27 +5121,48 @@ export default function ChatView(props: ChatViewProps) {
? activePlan.steps
: null;

const publishComposerOverlayHeight = useCallback((height: number) => {
const nextHeight = Math.ceil(height);
if (nextHeight <= 0) return;
const previousHeight = composerOverlayHeightRef.current;
if (previousHeight !== nextHeight) {
composerOverlayHeightRef.current = nextHeight;
setComposerOverlayHeight(nextHeight);
}
const nextInset = resolveComposerTimelineInset({
currentInset: composerTimelineInsetRef.current,
overlayHeight: nextHeight,
isResting: composerRestingRef.current,
});
if (composerTimelineInsetRef.current !== nextInset) {
composerTimelineInsetRef.current = nextInset;
setComposerTimelineInset(nextInset);
}
setScrollToEndClearance((currentClearance) =>
currentClearance === nextHeight ? currentClearance : nextHeight,
);
}, []);
const publishComposerOverlayHeight = useCallback(
(height: number) => {
const nextHeight = Math.ceil(height);
if (nextHeight <= 0) return;
const previousHeight = composerOverlayHeightRef.current;
if (previousHeight !== nextHeight) {
composerOverlayHeightRef.current = nextHeight;
setComposerOverlayHeight(nextHeight);
}
const nextInset = resolveComposerTimelineInset({
currentInset: composerTimelineInsetRef.current,
overlayHeight: nextHeight,
isResting: composerRestingRef.current,
});
if (composerTimelineInsetRef.current !== nextInset) {
composerTimelineInsetRef.current = nextInset;
setComposerTimelineInset(nextInset);
}
const mainSurface = composerOverlayElement?.querySelector<HTMLElement>(
'[data-chat-composer-main-surface="true"]',
);
const button = composerOverlayElement?.parentElement?.querySelector<HTMLElement>(
'button[aria-label="Scroll to end"]',
);
const clearance =
composerOverlayElement && mainSurface && button
? resolveScrollToEndClearance({
overlayHeight: nextHeight,
mainSurfaceTop: mainSurface.getBoundingClientRect().top,
button: button.getBoundingClientRect(),
attachments: Array.from(
composerOverlayElement.querySelectorAll<HTMLElement>(
'[data-composer-banner-surface="attached"]',
),
(element) => element.getBoundingClientRect(),
),
})
: nextHeight;
setScrollToEndClearance(clearance);
},
[composerOverlayElement],
);
// The composer reports its resting flag from a layout effect, which runs
// before this component's own layout effects and before any resize
// observation, so every measurement below sees the flag for its layout.
Expand Down Expand Up @@ -5174,7 +5195,7 @@ export default function ChatView(props: ChatViewProps) {
return () => {
resizeObserver.disconnect();
};
}, [composerOverlayElement, publishComposerOverlayHeight]);
}, [composerOverlayElement, publishComposerOverlayHeight, showScrollToBottom]);
const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef);
const activeThreadReferenceCopyTarget = useMemo(
() =>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/chat/ComposerBannerStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
>
<div className={cn("relative flex flex-col-reverse", hasStack && stackExpanded && "z-50")}>
<div
key={frontItem.id}
className={cn(
"relative z-10 transition-[translate,opacity] duration-220 ease-in",
exitingItemId === frontItem.id
Expand Down Expand Up @@ -147,7 +148,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
{hasStack ? (
<div
ref={noticesRef}
className={cn("relative z-20", stackExpanded && "min-h-3")}
className="relative z-20 min-h-3"
onPointerEnter={(event) => {
if (event.pointerType === "touch") return;
if (document.activeElement === peekRef.current) {
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/components/composerFooterLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
COMPOSER_RESTING_EXPANSION_MIN_PX,
getRestingComposerImagePreviewCounts,
resolveComposerTimelineInset,
resolveScrollToEndClearance,
resolveRestingComposerControlsLayout,
resolveRestingComposerControlsNaturalWidth,
shouldAnimateComposerRestingTransition,
Expand Down Expand Up @@ -424,3 +425,27 @@ describe("resolveRestingComposerControlsLayout hysteresis", () => {
).toEqual({ hiddenCount: 2, visible: true });
});
});

describe("resolveScrollToEndClearance", () => {
it("removes the side tab gap in both composer states while clearing overlapping attachments", () => {
for (const overlayHeight of [120, 214]) {
const layout = {
overlayHeight,
mainSurfaceTop: 534,
button: { left: 340, right: 460 },
attachments: [{ top: 500, left: 600, right: 700 }],
};
expect(resolveScrollToEndClearance(layout)).toBe(overlayHeight - 34);
expect(resolveScrollToEndClearance({ ...layout, attachments: [] })).toBe(overlayHeight);
expect(
resolveScrollToEndClearance({
...layout,
attachments: [...layout.attachments, { top: 500, left: 100, right: 700 }],
}),
).toBe(overlayHeight);
expect(resolveScrollToEndClearance({ ...layout, button: { left: 590, right: 710 } })).toBe(
overlayHeight,
);
}
});
});
17 changes: 17 additions & 0 deletions apps/web/src/components/composerFooterLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,20 @@ export function resolveRestingComposerControlsLayout(
: minimumWidth <= hostWidth;
return { hiddenCount, visible };
}

export function resolveScrollToEndClearance(input: {
overlayHeight: number;
mainSurfaceTop: number;
button: { left: number; right: number };
attachments: ReadonlyArray<{ top: number; left: number; right: number }>;
}): number {
let contentTop = input.mainSurfaceTop;
let top = contentTop;
for (const attachment of input.attachments) {
contentTop = Math.min(contentTop, attachment.top);
if (attachment.left < input.button.right && attachment.right > input.button.left) {
top = Math.min(top, attachment.top);
}
}
return Math.ceil(input.overlayHeight - (top - contentTop));
}
Loading