Ver Fonte

fix(app): reserve review pane minimum instead of capping chat width at 45% (#35078)

Luke Parker há 1 mês atrás
pai
commit
dd25d143c5

+ 41 - 4
packages/app/src/pages/session.tsx

@@ -11,6 +11,7 @@ import {
   createMemo,
   createEffect,
   createComputed,
+  createSignal,
   on,
   onMount,
   type ParentProps,
@@ -69,6 +70,11 @@ import { createTimelineModel } from "@/pages/session/timeline/model"
 import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { syncSessionModel } from "@/pages/session/session-model-helpers"
+import {
+  clampSessionPanelWidth,
+  SESSION_PANEL_WIDTH_MIN,
+  sessionPanelWidthMax,
+} from "@/pages/session/session-panel-width"
 import { SessionSidePanel } from "@/pages/session/session-side-panel"
 import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
 import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
@@ -442,9 +448,39 @@ export default function Page() {
     newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(),
   )
   const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
+  let panelRow: HTMLDivElement | undefined
+  const [panelRowWidth, setPanelRowWidth] = createSignal<number>()
+  createResizeObserver(
+    () => panelRow,
+    ({ width }) => setPanelRowWidth(width),
+  )
+  const splitReview = createMemo(
+    () => (newSessionDesign() ? desktopV2ReviewOpen() : desktopReviewOpen()) && layout.review.diffStyle() === "split",
+  )
+  // The observer reports the content-box width, which already excludes the row
+  // padding; only the flex gap between the panels remains to subtract.
+  const sessionPanelAvailable = createMemo(() => {
+    const width = panelRowWidth()
+    if (width === undefined) return undefined
+    return width - (settings.general.newLayoutDesigns() ? 8 : 0)
+  })
+  const sessionPanelMax = createMemo(() => {
+    const available = sessionPanelAvailable()
+    if (available === undefined) return 1000
+    return sessionPanelWidthMax({ available, split: splitReview() })
+  })
+  // Clamp at render time so window or sidebar resizes squeeze the chat panel
+  // instead of the review pane, without overwriting the persisted width.
+  const sessionPanelResizedWidth = createMemo(() =>
+    clampSessionPanelWidth({
+      width: layout.session.width(),
+      available: sessionPanelAvailable(),
+      split: splitReview(),
+    }),
+  )
   const sessionPanelWidth = createMemo(() => {
     if (!desktopSidePanelOpen()) return "100%"
-    if (desktopSessionResizeOpen()) return `${layout.session.width()}px`
+    if (desktopSessionResizeOpen()) return `${sessionPanelResizedWidth()}px`
     return `calc(100% - ${layout.fileTree.width()}px)`
   })
   const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
@@ -2148,6 +2184,7 @@ export default function Page() {
     <SessionRouteFrame>
       <SessionHeader />
       <div
+        ref={panelRow}
         class="flex-1 min-h-0 flex flex-col md:flex-row"
         classList={{
           "gap-2 p-2": settings.general.newLayoutDesigns(),
@@ -2186,9 +2223,9 @@ export default function Page() {
                   "-right-1": settings.general.newLayoutDesigns(),
                 }}
                 direction="horizontal"
-                size={layout.session.width()}
-                min={450}
-                max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
+                size={sessionPanelResizedWidth()}
+                min={SESSION_PANEL_WIDTH_MIN}
+                max={sessionPanelMax()}
                 onResize={(width) => {
                   size.touch()
                   layout.session.resize(width)

+ 52 - 0
packages/app/src/pages/session/session-panel-width.test.ts

@@ -0,0 +1,52 @@
+import { describe, expect, test } from "bun:test"
+import {
+  clampSessionPanelWidth,
+  REVIEW_PANE_WIDTH_MIN,
+  REVIEW_PANE_WIDTH_MIN_SPLIT,
+  SESSION_PANEL_WIDTH_MIN,
+  sessionPanelWidthMax,
+} from "./session-panel-width"
+
+describe("sessionPanelWidthMax", () => {
+  test("reserves the unified review pane minimum", () => {
+    expect(sessionPanelWidthMax({ available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN)
+  })
+
+  test("reserves a larger minimum for split diffs", () => {
+    expect(sessionPanelWidthMax({ available: 1700, split: true })).toBe(1700 - REVIEW_PANE_WIDTH_MIN_SPLIT)
+    expect(REVIEW_PANE_WIDTH_MIN_SPLIT).toBeGreaterThan(REVIEW_PANE_WIDTH_MIN)
+  })
+
+  test("lets the chat panel take everything beyond the review pane minimum", () => {
+    // Regression: the old cap was 45% of the window, forcing the review pane
+    // to at least 55% of the window regardless of content.
+    const available = 3440
+    expect(sessionPanelWidthMax({ available, split: false })).toBeGreaterThan(available * 0.45)
+  })
+
+  test("never drops below the chat panel minimum on small windows", () => {
+    expect(sessionPanelWidthMax({ available: 600, split: true })).toBe(SESSION_PANEL_WIDTH_MIN)
+    expect(sessionPanelWidthMax({ available: 0, split: false })).toBe(SESSION_PANEL_WIDTH_MIN)
+  })
+})
+
+describe("clampSessionPanelWidth", () => {
+  test("keeps widths already within the limit", () => {
+    expect(clampSessionPanelWidth({ width: 800, available: 1700, split: false })).toBe(800)
+  })
+
+  test("forces the width down when the window shrinks", () => {
+    expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN)
+    expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: true })).toBe(
+      1700 - REVIEW_PANE_WIDTH_MIN_SPLIT,
+    )
+  })
+
+  test("holds the chat panel minimum when there is no room for both", () => {
+    expect(clampSessionPanelWidth({ width: 1600, available: 700, split: true })).toBe(SESSION_PANEL_WIDTH_MIN)
+  })
+
+  test("skips clamping before the layout is measured", () => {
+    expect(clampSessionPanelWidth({ width: 1600, available: undefined, split: false })).toBe(1600)
+  })
+})

+ 19 - 0
packages/app/src/pages/session/session-panel-width.ts

@@ -0,0 +1,19 @@
+// The review pane has no width of its own: it takes whatever the chat panel
+// leaves behind. Instead of capping the chat panel at a fraction of the window
+// (which forces the review pane to grow with the monitor), reserve a fixed
+// minimum for the review pane and let the chat panel take everything else.
+export const SESSION_PANEL_WIDTH_MIN = 450
+export const REVIEW_PANE_WIDTH_MIN = 480
+export const REVIEW_PANE_WIDTH_MIN_SPLIT = 800
+
+export function sessionPanelWidthMax(input: { available: number; split: boolean }) {
+  const pane = input.split ? REVIEW_PANE_WIDTH_MIN_SPLIT : REVIEW_PANE_WIDTH_MIN
+  return Math.max(SESSION_PANEL_WIDTH_MIN, input.available - pane)
+}
+
+// `available` is undefined until the layout row is first measured; render the
+// stored width untouched until then to avoid a first-frame snap.
+export function clampSessionPanelWidth(input: { width: number; available: number | undefined; split: boolean }) {
+  if (input.available === undefined) return input.width
+  return Math.min(input.width, sessionPanelWidthMax({ available: input.available, split: input.split }))
+}