From 660cddd3bc9801e089afcabba11c62f41aeac5c3 Mon Sep 17 00:00:00 2001 From: Bilal Bakr <62337003+Bil0000@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:25:20 +0300 Subject: [PATCH] fix(web): four composer spacing defects (#8090) --- .../web/src/components/ChatView.logic.test.ts | 19 +++++++ apps/web/src/components/ChatView.logic.ts | 11 ++++ apps/web/src/components/ChatView.tsx | 56 +++++++++++-------- .../components/chat/ComposerStashBadge.tsx | 2 +- .../chat/ComposerTasksBadge.test.tsx | 39 +++++++++++-- .../components/chat/ComposerTasksBadge.tsx | 8 ++- 6 files changed, 105 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 6586c62d2f130cf698356534a6d62bc20d3324d1..5ff86bb2381b3a1880973148ed73d28725cfa3f0 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -32,6 +32,7 @@ import { resolveSendEnvMode, resolveDraftHeroState, scheduleEnvironmentReconnectWarning, + shoulderTabReserve, startNewThreadForProject, shouldDockDraftHeroForSubmission, shouldReleaseTimelineAnchorForToolActivity, @@ -45,6 +46,24 @@ const projectId = ProjectId.make("project-1"); const threadId = ThreadId.make("thread-1"); const now = "2026-03-29T00:00:00.000Z"; +describe("shoulderTabReserve", () => { + it("ignores the top drawer when measuring the shoulder tab band", () => { + const elementAt = (top: number) => ({ getBoundingClientRect: () => ({ top }) }) as HTMLElement; + const elements = new Map([ + ['[data-chat-composer-form="true"]', elementAt(20)], + [".chat-composer-shoulder-tab", elementAt(100)], + ['[data-chat-composer-main-surface="true"]', elementAt(128)], + ]); + const overlay = { + querySelector: (selector: string) => elements.get(selector) ?? null, + } as HTMLElement; + + expect(shoulderTabReserve(overlay)).toBe(28); + elements.set(".chat-composer-tasks-tab", elementAt(100)); + expect(shoulderTabReserve(overlay)).toBe(0); + }); +}); + describe("draft hero submission transition", () => { it("does not dock the composer before a background submission", () => { expect( diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index b435e7021c9b37127acbbeb0725086f08470e9cc..25a49f3efa2cbf354a5a3ba28230d79d4fe5520b 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -39,6 +39,17 @@ export const ENVIRONMENT_RECONNECT_WARNING_GRACE_MS = 2_000; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); +export function shoulderTabReserve(overlay: HTMLElement): number { + if (overlay.querySelector(".chat-composer-tasks-tab")) return 0; + const tab = overlay.querySelector(".chat-composer-shoulder-tab"); + const surface = overlay.querySelector('[data-chat-composer-main-surface="true"]'); + if (!tab || !surface) return 0; + return Math.max( + 0, + Math.round(surface.getBoundingClientRect().top - tab.getBoundingClientRect().top), + ); +} + export function shouldDockDraftHeroForSubmission(input: { isDraftHeroState: boolean; activeThreadKey: string | null; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 530678ba85d98cf4e00e7d95cdf21af48cc9fecd..a027a1848b30f269be0b48a358595b38a606c8c2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -350,6 +350,7 @@ import { shouldDockDraftHeroForSubmission, shouldReleaseTimelineAnchorForToolActivity, shouldShowBranchMismatchBanner, + shoulderTabReserve, shouldShowPlanFollowUpPrompt, getStartedThreadModelChangeBlockReason, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, @@ -1507,6 +1508,7 @@ function ChatViewContent(props: ChatViewProps) { const legendListRef = useRef(null); const [composerOverlayElement, setComposerOverlayElement] = useState(null); const [composerOverlayHeight, setComposerOverlayHeight] = useState(0); + const [scrollToEndClearance, setScrollToEndClearance] = useState(0); const isAtEndRef = useRef(true); const attachmentPreviewHandoffByMessageIdRef = useRef>({}); const attachmentPreviewPromotionInFlightByMessageIdRef = useRef>({}); @@ -1514,25 +1516,6 @@ function ChatViewContent(props: ChatViewProps) { const feedbackUploadsInFlightRef = useRef(new Set()); const terminalUiOpenByThreadRef = useRef>({}); - useLayoutEffect(() => { - if (!composerOverlayElement) return; - - const updateHeight = () => { - const nextHeight = Math.ceil(composerOverlayElement.getBoundingClientRect().height); - if (nextHeight <= 0) return; - setComposerOverlayHeight((currentHeight) => - currentHeight === nextHeight ? currentHeight : nextHeight, - ); - }; - - updateHeight(); - if (typeof ResizeObserver === "undefined") return; - - const observer = new ResizeObserver(updateHeight); - observer.observe(composerOverlayElement); - return () => observer.disconnect(); - }, [composerOverlayElement]); - const terminalUiState = useTerminalUiStateStore((state) => selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef), ); @@ -4467,6 +4450,35 @@ function ChatViewContent(props: ChatViewProps) { activeComposerTasksProgress && activePlan && activePlan.turnId === activeLatestTurn?.turnId ? activePlan.steps : null; + + useLayoutEffect(() => { + if (!composerOverlayElement) return; + + const updateHeight = () => { + const nextHeight = Math.ceil(composerOverlayElement.getBoundingClientRect().height); + if (nextHeight <= 0) return; + setComposerOverlayHeight((currentHeight) => + currentHeight === nextHeight ? currentHeight : nextHeight, + ); + const nextClearance = Math.max(0, nextHeight - shoulderTabReserve(composerOverlayElement)); + setScrollToEndClearance((currentClearance) => + currentClearance === nextClearance ? currentClearance : nextClearance, + ); + }; + + updateHeight(); + if (typeof ResizeObserver === "undefined") return; + + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(composerOverlayElement); + const tabObserver = new MutationObserver(updateHeight); + tabObserver.observe(composerOverlayElement, { childList: true, subtree: true }); + return () => { + resizeObserver.disconnect(); + tabObserver.disconnect(); + }; + }, [composerOverlayElement]); + const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays); const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge); const linkedPullRequestStatus = useLinkedThreadPullRequest( @@ -7148,7 +7160,7 @@ function ChatViewContent(props: ChatViewProps) { {showScrollToBottom && (