fix(app): reserve review pane minimum instead of capping chat width at 45% (#35078)
This commit is contained in:
parent
d8a57ee15c
commit
dd25d143c5
3 changed files with 112 additions and 4 deletions
|
|
@ -11,6 +11,7 @@ import {
|
||||||
createMemo,
|
createMemo,
|
||||||
createEffect,
|
createEffect,
|
||||||
createComputed,
|
createComputed,
|
||||||
|
createSignal,
|
||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
type ParentProps,
|
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 { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { syncSessionModel } from "@/pages/session/session-model-helpers"
|
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 { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||||
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
|
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
|
||||||
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
|
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(),
|
newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(),
|
||||||
)
|
)
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
|
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(() => {
|
const sessionPanelWidth = createMemo(() => {
|
||||||
if (!desktopSidePanelOpen()) return "100%"
|
if (!desktopSidePanelOpen()) return "100%"
|
||||||
if (desktopSessionResizeOpen()) return `${layout.session.width()}px`
|
if (desktopSessionResizeOpen()) return `${sessionPanelResizedWidth()}px`
|
||||||
return `calc(100% - ${layout.fileTree.width()}px)`
|
return `calc(100% - ${layout.fileTree.width()}px)`
|
||||||
})
|
})
|
||||||
const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
|
const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
|
||||||
|
|
@ -2148,6 +2184,7 @@ export default function Page() {
|
||||||
<SessionRouteFrame>
|
<SessionRouteFrame>
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div
|
<div
|
||||||
|
ref={panelRow}
|
||||||
class="flex-1 min-h-0 flex flex-col md:flex-row"
|
class="flex-1 min-h-0 flex flex-col md:flex-row"
|
||||||
classList={{
|
classList={{
|
||||||
"gap-2 p-2": settings.general.newLayoutDesigns(),
|
"gap-2 p-2": settings.general.newLayoutDesigns(),
|
||||||
|
|
@ -2186,9 +2223,9 @@ export default function Page() {
|
||||||
"-right-1": settings.general.newLayoutDesigns(),
|
"-right-1": settings.general.newLayoutDesigns(),
|
||||||
}}
|
}}
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
size={layout.session.width()}
|
size={sessionPanelResizedWidth()}
|
||||||
min={450}
|
min={SESSION_PANEL_WIDTH_MIN}
|
||||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
|
max={sessionPanelMax()}
|
||||||
onResize={(width) => {
|
onResize={(width) => {
|
||||||
size.touch()
|
size.touch()
|
||||||
layout.session.resize(width)
|
layout.session.resize(width)
|
||||||
|
|
|
||||||
52
packages/app/src/pages/session/session-panel-width.test.ts
Normal file
52
packages/app/src/pages/session/session-panel-width.test.ts
Normal file
|
|
@ -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
packages/app/src/pages/session/session-panel-width.ts
Normal file
19
packages/app/src/pages/session/session-panel-width.ts
Normal file
|
|
@ -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 }))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue