Skip to content

Commit e3bf52f

Browse files
authored
fix(app): reserve review pane minimum instead of capping chat width at 45% (anomalyco#35078)
1 parent 030825d commit e3bf52f

3 files changed

Lines changed: 112 additions & 4 deletions

File tree

packages/app/src/pages/session.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createMemo,
1212
createEffect,
1313
createComputed,
14+
createSignal,
1415
on,
1516
onMount,
1617
type ParentProps,
@@ -69,6 +70,11 @@ import { createTimelineModel } from "@/pages/session/timeline/model"
6970
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
7071
import { useSessionLayout } from "@/pages/session/session-layout"
7172
import { syncSessionModel } from "@/pages/session/session-model-helpers"
73+
import {
74+
clampSessionPanelWidth,
75+
SESSION_PANEL_WIDTH_MIN,
76+
sessionPanelWidthMax,
77+
} from "@/pages/session/session-panel-width"
7278
import { SessionSidePanel } from "@/pages/session/session-side-panel"
7379
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
7480
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
@@ -442,9 +448,39 @@ export default function Page() {
442448
newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(),
443449
)
444450
const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
451+
let panelRow: HTMLDivElement | undefined
452+
const [panelRowWidth, setPanelRowWidth] = createSignal<number>()
453+
createResizeObserver(
454+
() => panelRow,
455+
({ width }) => setPanelRowWidth(width),
456+
)
457+
const splitReview = createMemo(
458+
() => (newSessionDesign() ? desktopV2ReviewOpen() : desktopReviewOpen()) && layout.review.diffStyle() === "split",
459+
)
460+
// The observer reports the content-box width, which already excludes the row
461+
// padding; only the flex gap between the panels remains to subtract.
462+
const sessionPanelAvailable = createMemo(() => {
463+
const width = panelRowWidth()
464+
if (width === undefined) return undefined
465+
return width - (settings.general.newLayoutDesigns() ? 8 : 0)
466+
})
467+
const sessionPanelMax = createMemo(() => {
468+
const available = sessionPanelAvailable()
469+
if (available === undefined) return 1000
470+
return sessionPanelWidthMax({ available, split: splitReview() })
471+
})
472+
// Clamp at render time so window or sidebar resizes squeeze the chat panel
473+
// instead of the review pane, without overwriting the persisted width.
474+
const sessionPanelResizedWidth = createMemo(() =>
475+
clampSessionPanelWidth({
476+
width: layout.session.width(),
477+
available: sessionPanelAvailable(),
478+
split: splitReview(),
479+
}),
480+
)
445481
const sessionPanelWidth = createMemo(() => {
446482
if (!desktopSidePanelOpen()) return "100%"
447-
if (desktopSessionResizeOpen()) return `${layout.session.width()}px`
483+
if (desktopSessionResizeOpen()) return `${sessionPanelResizedWidth()}px`
448484
return `calc(100% - ${layout.fileTree.width()}px)`
449485
})
450486
const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
@@ -2148,6 +2184,7 @@ export default function Page() {
21482184
<SessionRouteFrame>
21492185
<SessionHeader />
21502186
<div
2187+
ref={panelRow}
21512188
class="flex-1 min-h-0 flex flex-col md:flex-row"
21522189
classList={{
21532190
"gap-2 p-2": settings.general.newLayoutDesigns(),
@@ -2186,9 +2223,9 @@ export default function Page() {
21862223
"-right-1": settings.general.newLayoutDesigns(),
21872224
}}
21882225
direction="horizontal"
2189-
size={layout.session.width()}
2190-
min={450}
2191-
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
2226+
size={sessionPanelResizedWidth()}
2227+
min={SESSION_PANEL_WIDTH_MIN}
2228+
max={sessionPanelMax()}
21922229
onResize={(width) => {
21932230
size.touch()
21942231
layout.session.resize(width)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, test } from "bun:test"
2+
import {
3+
clampSessionPanelWidth,
4+
REVIEW_PANE_WIDTH_MIN,
5+
REVIEW_PANE_WIDTH_MIN_SPLIT,
6+
SESSION_PANEL_WIDTH_MIN,
7+
sessionPanelWidthMax,
8+
} from "./session-panel-width"
9+
10+
describe("sessionPanelWidthMax", () => {
11+
test("reserves the unified review pane minimum", () => {
12+
expect(sessionPanelWidthMax({ available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN)
13+
})
14+
15+
test("reserves a larger minimum for split diffs", () => {
16+
expect(sessionPanelWidthMax({ available: 1700, split: true })).toBe(1700 - REVIEW_PANE_WIDTH_MIN_SPLIT)
17+
expect(REVIEW_PANE_WIDTH_MIN_SPLIT).toBeGreaterThan(REVIEW_PANE_WIDTH_MIN)
18+
})
19+
20+
test("lets the chat panel take everything beyond the review pane minimum", () => {
21+
// Regression: the old cap was 45% of the window, forcing the review pane
22+
// to at least 55% of the window regardless of content.
23+
const available = 3440
24+
expect(sessionPanelWidthMax({ available, split: false })).toBeGreaterThan(available * 0.45)
25+
})
26+
27+
test("never drops below the chat panel minimum on small windows", () => {
28+
expect(sessionPanelWidthMax({ available: 600, split: true })).toBe(SESSION_PANEL_WIDTH_MIN)
29+
expect(sessionPanelWidthMax({ available: 0, split: false })).toBe(SESSION_PANEL_WIDTH_MIN)
30+
})
31+
})
32+
33+
describe("clampSessionPanelWidth", () => {
34+
test("keeps widths already within the limit", () => {
35+
expect(clampSessionPanelWidth({ width: 800, available: 1700, split: false })).toBe(800)
36+
})
37+
38+
test("forces the width down when the window shrinks", () => {
39+
expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN)
40+
expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: true })).toBe(
41+
1700 - REVIEW_PANE_WIDTH_MIN_SPLIT,
42+
)
43+
})
44+
45+
test("holds the chat panel minimum when there is no room for both", () => {
46+
expect(clampSessionPanelWidth({ width: 1600, available: 700, split: true })).toBe(SESSION_PANEL_WIDTH_MIN)
47+
})
48+
49+
test("skips clamping before the layout is measured", () => {
50+
expect(clampSessionPanelWidth({ width: 1600, available: undefined, split: false })).toBe(1600)
51+
})
52+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// The review pane has no width of its own: it takes whatever the chat panel
2+
// leaves behind. Instead of capping the chat panel at a fraction of the window
3+
// (which forces the review pane to grow with the monitor), reserve a fixed
4+
// minimum for the review pane and let the chat panel take everything else.
5+
export const SESSION_PANEL_WIDTH_MIN = 450
6+
export const REVIEW_PANE_WIDTH_MIN = 480
7+
export const REVIEW_PANE_WIDTH_MIN_SPLIT = 800
8+
9+
export function sessionPanelWidthMax(input: { available: number; split: boolean }) {
10+
const pane = input.split ? REVIEW_PANE_WIDTH_MIN_SPLIT : REVIEW_PANE_WIDTH_MIN
11+
return Math.max(SESSION_PANEL_WIDTH_MIN, input.available - pane)
12+
}
13+
14+
// `available` is undefined until the layout row is first measured; render the
15+
// stored width untouched until then to avoid a first-frame snap.
16+
export function clampSessionPanelWidth(input: { width: number; available: number | undefined; split: boolean }) {
17+
if (input.available === undefined) return input.width
18+
return Math.min(input.width, sessionPanelWidthMax({ available: input.available, split: input.split }))
19+
}

0 commit comments

Comments
 (0)