diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d1225065aeec..3dcfce764e96 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3942,6 +3942,8 @@ export default function ChatView(props: ChatViewProps) { & { readonly sheet: boolean; readonly maximized: boolean; readonly workspaceLayoutElement: HTMLElement | null; + readonly canvasStore: ChatCanvasStore; + readonly hasFloatingPreview: boolean; readonly widthStorageKey: string; readonly sheetAnimationDurationMs: number; readonly onCloseSheet: () => void; @@ -20,6 +31,8 @@ export function ChatRightPanel(props: ChatRightPanelProps) { const { sheet, workspaceLayoutElement, + canvasStore, + hasFloatingPreview, widthStorageKey, sheetAnimationDurationMs, onCloseSheet, @@ -27,9 +40,30 @@ export function ChatRightPanel(props: ChatRightPanelProps) { layoutControls, ...tabs } = props; + const chatWidth = useClientSettings((settings) => settings.chatWidth); + const autoSize = !sheet && !maximized && !hasFloatingPreview && chatWidth !== "full"; + // Size for the card even when closed; current presentation depends on the panel's width. + const preferredSiblingWidth = useStore(canvasStore, ({ measurements }) => { + if (!autoSize) return undefined; + const { padding, maxChatWidth, height, findBar } = measurements; + const widthWithCard = + maxChatWidth + + padding + + DETAILS_CARD_CLEARANCE + + THREAD_DETAILS_CARD_WIDTH + + THREAD_DETAILS_CARD_GAP; + const card = resolveThreadDetailsCardLayout({ + container: { width: widthWithCard, height }, + lane: { padding, minChatWidth: maxChatWidth }, + frame: null, + topInset: findBar?.bottom ?? 0, + }); + return card ? widthWithCard : maxChatWidth + padding * 2; + }); const inlineSize = usePreviewPanelInlineSize({ container: workspaceLayoutElement, widthStorageKey, + preferredSiblingWidth, }); if (!sheet) { return ; diff --git a/apps/web/src/components/chat/threadDetailsCardLayout.ts b/apps/web/src/components/chat/threadDetailsCardLayout.ts index 7b5e1be8f29b..b231d5cb5ee6 100644 --- a/apps/web/src/components/chat/threadDetailsCardLayout.ts +++ b/apps/web/src/components/chat/threadDetailsCardLayout.ts @@ -3,7 +3,7 @@ import { DETAILS_CARD_CLEARANCE } from "./chatCanvasLayout"; export const THREAD_DETAILS_CARD_GAP = 12; // Keep in sync with --thread-details-panel-width. -const THREAD_DETAILS_CARD_WIDTH = 280; +export const THREAD_DETAILS_CARD_WIDTH = 280; export function resolveThreadDetailsCardDensity( height: number, diff --git a/apps/web/src/hooks/usePreviewPanelInlineSize.ts b/apps/web/src/hooks/usePreviewPanelInlineSize.ts index 8881f5314f3a..79f4ed156e32 100644 --- a/apps/web/src/hooks/usePreviewPanelInlineSize.ts +++ b/apps/web/src/hooks/usePreviewPanelInlineSize.ts @@ -22,17 +22,19 @@ export function usePreviewPanelInlineSize( readonly enabled?: boolean | undefined; readonly widthStorageKey?: string | undefined; readonly defaultWidth?: number | undefined; + readonly preferredSiblingWidth?: number | undefined; readonly container?: HTMLElement | null | undefined; } = {}, ): PreviewPanelInlineSize { const enabled = options.enabled ?? true; const container = options.container; + const [availableWidth, setAvailableWidth] = useState(PREVIEW_PANEL_DEFAULT_WIDTH); const [maxWidth, setMaxWidth] = useState(() => getPreviewPanelMaxWidth(typeof window === "undefined" ? 1280 : window.innerWidth), ); const { width, handlers } = useResizableWidth({ storageKey: options.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY, - defaultWidth: options.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH, + defaultWidth: options.defaultWidth ?? availableWidth, minWidth: PREVIEW_PANEL_MIN_WIDTH, maxWidth, edge: "left", @@ -40,10 +42,16 @@ export function usePreviewPanelInlineSize( const observeRow = (host: HTMLElement) => { const row = container ?? (enabled ? host.parentElement : null) ?? null; // Ref callbacks run before paint, so the width is clamped without an over-wide - // flash on mount. Only the derived cap is stored: the row resizes every frame - // of a sidebar drag, but an unchanged cap skips re-rendering the owner. + // flash on mount. Store only derived widths so unchanged bounds and defaults + // skip re-rendering the owner during a sidebar drag. const measure = () => { setMaxWidth(getPreviewPanelMaxWidth(window.innerWidth, row?.clientWidth)); + setAvailableWidth( + Math.max( + PREVIEW_PANEL_DEFAULT_WIDTH, + Math.floor(row?.clientWidth ?? 0) - (options.preferredSiblingWidth ?? Infinity), + ), + ); }; measure(); let frame = 0;