From e1693d11ad5b135dbbdb9b6117653b3cea9abdda Mon Sep 17 00:00:00 2001 From: kirillbaldin Date: Mon, 2 Nov 2026 17:17:52 +0400 Subject: [PATCH 1/3] feat(web): use spare space for the initial right panel width --- apps/web/src/components/ChatView.tsx | 1 + .../src/components/chat/ChatRightPanel.tsx | 22 +++++++++++++++++++ .../chat/threadDetailsCardLayout.ts | 2 +- .../src/hooks/usePreviewPanelInlineSize.ts | 14 +++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 6fd54db6bd52..f45fca718b27 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3941,6 +3941,7 @@ export default function ChatView(props: ChatViewProps) { & { readonly sheet: boolean; readonly maximized: boolean; readonly workspaceLayoutElement: HTMLElement | null; + readonly canvasStore: ChatCanvasStore; readonly widthStorageKey: string; readonly sheetAnimationDurationMs: number; readonly onCloseSheet: () => void; @@ -20,6 +26,7 @@ export function ChatRightPanel(props: ChatRightPanelProps) { const { sheet, workspaceLayoutElement, + canvasStore, widthStorageKey, sheetAnimationDurationMs, onCloseSheet, @@ -27,9 +34,24 @@ export function ChatRightPanel(props: ChatRightPanelProps) { layoutControls, ...tabs } = props; + const chatWidth = useClientSettings((settings) => settings.chatWidth); + const autoSize = !sheet && !maximized && chatWidth !== "full"; + // Keep the card's slot when closed so reopening it does not switch to a popover. + const preferredSiblingWidth = useStore(canvasStore, ({ measurements }) => { + if (!autoSize) return undefined; + const { padding, maxChatWidth } = measurements; + return ( + maxChatWidth + + padding + + DETAILS_CARD_CLEARANCE + + THREAD_DETAILS_CARD_WIDTH + + THREAD_DETAILS_CARD_GAP + ); + }); 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; From ebf10c775ecd3ebd1cbe2c54e6af3c460e9838cc Mon Sep 17 00:00:00 2001 From: kirillbaldin Date: Mon, 2 Nov 2026 17:39:52 +0400 Subject: [PATCH 2/3] fix(web): preserve room for floating previews when opening panels --- apps/web/src/components/ChatView.tsx | 1 + apps/web/src/components/chat/ChatRightPanel.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f45fca718b27..d5aeecb4e862 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3942,6 +3942,7 @@ export default function ChatView(props: ChatViewProps) { sheet={shouldUsePlanSidebarSheet} workspaceLayoutElement={workspaceLayoutElement} canvasStore={chatCanvasStore} + hasFloatingPreview={canvasPreview !== null} widthStorageKey={`t3code:preview-panel-width:${activeThreadKey}`} sheetAnimationDurationMs={panelAnimationsActive ? panelAnimationDurationMs : 0} onCloseSheet={closePreviewPanelStable} diff --git a/apps/web/src/components/chat/ChatRightPanel.tsx b/apps/web/src/components/chat/ChatRightPanel.tsx index d92c2dbf7468..273fda5fe6b8 100644 --- a/apps/web/src/components/chat/ChatRightPanel.tsx +++ b/apps/web/src/components/chat/ChatRightPanel.tsx @@ -12,6 +12,7 @@ type ChatRightPanelProps = Omit void; @@ -27,6 +28,7 @@ export function ChatRightPanel(props: ChatRightPanelProps) { sheet, workspaceLayoutElement, canvasStore, + hasFloatingPreview, widthStorageKey, sheetAnimationDurationMs, onCloseSheet, @@ -35,7 +37,7 @@ export function ChatRightPanel(props: ChatRightPanelProps) { ...tabs } = props; const chatWidth = useClientSettings((settings) => settings.chatWidth); - const autoSize = !sheet && !maximized && chatWidth !== "full"; + const autoSize = !sheet && !maximized && !hasFloatingPreview && chatWidth !== "full"; // Keep the card's slot when closed so reopening it does not switch to a popover. const preferredSiblingWidth = useStore(canvasStore, ({ measurements }) => { if (!autoSize) return undefined; From 4ebd777dbefbf6f88cdc59fc851dcee6a235f5e6 Mon Sep 17 00:00:00 2001 From: kirillbaldin Date: Mon, 2 Nov 2026 18:00:34 +0400 Subject: [PATCH 3/3] fix(web): release details card space on short canvases --- .../src/components/chat/ChatRightPanel.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/chat/ChatRightPanel.tsx b/apps/web/src/components/chat/ChatRightPanel.tsx index 273fda5fe6b8..d3b7d473f52f 100644 --- a/apps/web/src/components/chat/ChatRightPanel.tsx +++ b/apps/web/src/components/chat/ChatRightPanel.tsx @@ -5,7 +5,11 @@ import { RightPanelSheet } from "../RightPanelSheet"; import { RightPanelTabs, type RightPanelTabsProps } from "../RightPanelTabs"; import type { ChatCanvasStore } from "./canvasStore"; import { DETAILS_CARD_CLEARANCE } from "./chatCanvasLayout"; -import { THREAD_DETAILS_CARD_GAP, THREAD_DETAILS_CARD_WIDTH } from "./threadDetailsCardLayout"; +import { + resolveThreadDetailsCardLayout, + THREAD_DETAILS_CARD_GAP, + THREAD_DETAILS_CARD_WIDTH, +} from "./threadDetailsCardLayout"; type ChatRightPanelProps = Omit & { readonly sheet: boolean; @@ -38,17 +42,23 @@ export function ChatRightPanel(props: ChatRightPanelProps) { } = props; const chatWidth = useClientSettings((settings) => settings.chatWidth); const autoSize = !sheet && !maximized && !hasFloatingPreview && chatWidth !== "full"; - // Keep the card's slot when closed so reopening it does not switch to a popover. + // 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 } = measurements; - return ( + const { padding, maxChatWidth, height, findBar } = measurements; + const widthWithCard = maxChatWidth + padding + DETAILS_CARD_CLEARANCE + THREAD_DETAILS_CARD_WIDTH + - THREAD_DETAILS_CARD_GAP - ); + 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,