From 429c625a8515f9c2e4af28c488fbf32ce7789645 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sat, 3 Oct 2026 13:31:44 -0700 Subject: [PATCH] fix(web): workspace card docks beside chat when the window is narrow (#14992) Chat stays centered while the workspace card fits beside it with 32px to spare. When it does not fit, chat moves left only as far as needed, narrows only after it reaches the left padding, and the card becomes a popover below a 640px chat. The card is lighter: 280px wide, 32px rows, no section labels, no "Project folder" hint. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../BranchToolbarEnvModeSelector.tsx | 14 ++++-- apps/web/src/components/GitActionsControl.tsx | 2 +- apps/web/src/components/chat/ChatCanvas.tsx | 6 ++- .../src/components/chat/ChatCanvasContext.ts | 1 + .../src/components/chat/ThreadDetailsCard.tsx | 9 +++- .../components/chat/ThreadDetailsControl.tsx | 2 +- .../components/chat/ThreadDetailsPanel.tsx | 4 +- .../chat/ThreadRelationshipsControl.tsx | 4 +- .../components/chat/chatCanvasLayout.test.ts | 46 ++++++++++++++++++- .../src/components/chat/chatCanvasLayout.ts | 26 +++++++++-- .../chat/threadDetailsCardLayout.test.ts | 28 +++++------ .../chat/threadDetailsCardLayout.ts | 15 ++++-- .../chat/threadDetailsPanelStyles.ts | 4 +- apps/web/src/index.css | 2 +- 14 files changed, 123 insertions(+), 40 deletions(-) diff --git a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx index 465be828364b5e6d9b19d8a0798e4f4b243e6b9d..3afa31e2acee15e16b32bf2f678afebcc72a9bdc 100644 --- a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx +++ b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx @@ -59,7 +59,11 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe }: BranchToolbarEnvModeSelectorProps) { const workspacePath = displayMode === "panel" ? (activeWorktreePath ?? workspaceRoot) : null; const workspaceDisplayName = resolveWorkspaceDisplayName(workspacePath); - const workspaceKind = activeWorktreePath ? "Worktree" : "Project folder"; + // The panel names the workspace kind only when it is not the project folder. + const workspaceKind = activeWorktreePath ? "Worktree" : null; + const lockedWorkspaceKind = forceNewWorktree ? "Worktree" : workspaceKind; + const selectWorkspaceKind = + effectiveEnvMode === "worktree" && !activeWorktreePath ? "Create" : workspaceKind; const composerFloatingLayerProps = useComposerMenuProps(); const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree); const envModeItems = useMemo( @@ -147,9 +151,9 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe : (workspaceDisplayName ?? resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} - {displayMode === "panel" ? ( + {displayMode === "panel" && lockedWorkspaceKind ? ( - {forceNewWorktree ? "Worktree" : workspaceKind} + {lockedWorkspaceKind} ) : null} @@ -210,9 +214,9 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe - {displayMode === "panel" ? ( + {displayMode === "panel" && selectWorkspaceKind ? ( - {effectiveEnvMode === "worktree" && !activeWorktreePath ? "Create" : workspaceKind} + {selectWorkspaceKind} ) : null} diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 9d9d82c6273f46f7da85efd8cd4b7d748c421adf..e6635d8921657ac86ede20021ea2710cd6c4e57b 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -1880,7 +1880,7 @@ export default function GitActionsControl({ // the output row expands below. ) : ( <> diff --git a/apps/web/src/components/chat/ChatCanvas.tsx b/apps/web/src/components/chat/ChatCanvas.tsx index bc222d07852e6f31b79f69fb415a81753020df0c..504db173048995d6e81ad0617b59ba24900cc1de 100644 --- a/apps/web/src/components/chat/ChatCanvas.tsx +++ b/apps/web/src/components/chat/ChatCanvas.tsx @@ -11,7 +11,10 @@ import { ChatCanvasContext } from "./ChatCanvasContext"; import { resolveChatCanvasLayout, type ChatCanvasPreview } from "./chatCanvasLayout"; import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLayout"; -/** Owns the available conversation space. Floating cards never reserve it themselves. */ +/** + * Owns the available conversation space. Cards only report where they sit; the + * canvas decides when chat moves over to make room for them. + */ export function ChatCanvas({ composerOverlayElement, children, @@ -96,6 +99,7 @@ export function ChatCanvas({ const container = { width: measurements.width, height: measurements.height }; return { container, + lane: { padding: measurements.padding, minChatWidth: measurements.minChatWidth }, layout: resolveChatCanvasLayout({ ...measurements, container, preview, detailsCard }), previewKey: preview?.key ?? null, reportPreview, diff --git a/apps/web/src/components/chat/ChatCanvasContext.ts b/apps/web/src/components/chat/ChatCanvasContext.ts index b810b02392405fa837f88430fd4d2da203824590..44b91f82632af9aaf931477aec94fcbb09f62465 100644 --- a/apps/web/src/components/chat/ChatCanvasContext.ts +++ b/apps/web/src/components/chat/ChatCanvasContext.ts @@ -4,6 +4,7 @@ import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLay export const ChatCanvasContext = createContext<{ container: { width: number; height: number }; + lane: { padding: number; minChatWidth: number }; layout: ReturnType; previewKey: string | null; reportPreview: (preview: ChatCanvasPreview) => void; diff --git a/apps/web/src/components/chat/ThreadDetailsCard.tsx b/apps/web/src/components/chat/ThreadDetailsCard.tsx index 1886aa1ec483c9c8ea2bb7757836497dd03f889e..dfee1453eda984e7e07865690e6827704576a250 100644 --- a/apps/web/src/components/chat/ThreadDetailsCard.tsx +++ b/apps/web/src/components/chat/ThreadDetailsCard.tsx @@ -29,12 +29,17 @@ export function ThreadDetailsCard({ const preferredPlacement = canvas ? resolveThreadDetailsCardLayout({ container: canvas.container, - chat: canvas.layout.chat, + lane: canvas.lane, frame: null, }) : null; const placement = canvas - ? resolveThreadDetailsCardLayout({ container: canvas.container, ...canvas.layout }) + ? resolveThreadDetailsCardLayout({ + container: canvas.container, + lane: canvas.lane, + frame: canvas.layout.frame, + overlapsDetailsCard: canvas.layout.overlapsDetailsCard, + }) : null; const mode = placement ? "inline" : "popover"; const inlineOpen = useRightPanelStore((state) => diff --git a/apps/web/src/components/chat/ThreadDetailsControl.tsx b/apps/web/src/components/chat/ThreadDetailsControl.tsx index 551cc54282aa0ca171b3aabf2f1c8096415993a5..3169f80184886aa99df6da21dca45b8c02f7c226 100644 --- a/apps/web/src/components/chat/ThreadDetailsControl.tsx +++ b/apps/web/src/components/chat/ThreadDetailsControl.tsx @@ -56,7 +56,7 @@ export function ThreadDetailsControl({ className: cn( "relative inline-flex shrink-0 cursor-pointer items-center whitespace-nowrap border outline-none transition-[box-shadow,scale] [&:active:not([aria-haspopup])]:scale-[0.97] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 disabled:pointer-events-none disabled:opacity-64 aria-disabled:cursor-not-allowed aria-disabled:opacity-64 [&_svg]:shrink-0", parts[part], - multiline && "h-auto min-h-9 py-0.75 disabled:opacity-100 sm:h-auto", + multiline && "h-auto min-h-8 py-0.75 disabled:opacity-100 sm:h-auto", tone === "muted" && "text-muted-foreground/70 hover:text-foreground/80", tone === "destructive" && "text-destructive hover:text-destructive data-pressed:text-destructive", diff --git a/apps/web/src/components/chat/ThreadDetailsPanel.tsx b/apps/web/src/components/chat/ThreadDetailsPanel.tsx index 1d02a0ada889031c99b07daeadb299dc18ea9c13..5b79f0c00f5724130d62887639a18ae9e78c1964 100644 --- a/apps/web/src/components/chat/ThreadDetailsPanel.tsx +++ b/apps/web/src/components/chat/ThreadDetailsPanel.tsx @@ -126,7 +126,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { headingId="thread-details-workspace-heading" title="Workspace" separated={false} - showHeading={density === "full"} + showHeading={false} > {props.versionMismatch ? (
@@ -198,7 +198,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index ac0235f85d1c05d046f98638b46936d90c13c6d1..418f917a3f617a6af13ff33738d7779867e12c0a 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -97,7 +97,7 @@ export function ThreadLineageRowList(props: {