Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/app/src/context/layout-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { Accessor } from "solid-js"

// The review pane is `flex-1`, so it fills whatever the chat panel leaves — a wider chat
// means a narrower review pane. When the width was never set (`undefined`), fall back to the
// widest the chat panel is allowed to be, which is the review pane's smallest state, so the
// review pane starts minimized instead of grabbing the whole remainder of a wide monitor.
export function resolveSessionWidth(stored: number | undefined, maxWidth: number) {
return stored ?? maxWidth
}

export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
touch(key)
seed(key)
Expand Down
16 changes: 15 additions & 1 deletion packages/app/src/context/layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { describe, expect, test } from "bun:test"
import { createRoot, createSignal } from "solid-js"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys, resolveSessionWidth } from "./layout-helpers"

describe("resolveSessionWidth", () => {
test("defaults to the max chat width (smallest review pane) when never set", () => {
expect(resolveSessionWidth(undefined, 900)).toBe(900)
})

test("uses the remembered width once the user has resized", () => {
expect(resolveSessionWidth(720, 900)).toBe(720)
})

test("keeps a remembered width of zero rather than falling back", () => {
expect(resolveSessionWidth(0, 900)).toBe(0)
})
})

describe("layout session-key helpers", () => {
test("couples touch and scroll seed in order", () => {
Expand Down
27 changes: 17 additions & 10 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
import { createPathHelpers } from "./file/path"
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
import { migrateLegacySessionStateKeys, ServerScope, SessionStateKey } from "@/utils/server-scope"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys, resolveSessionWidth } from "./layout-helpers"
import { requireServerKey } from "@/utils/session-route"
import { type DraftTab, useTabs } from "./tabs"

Expand All @@ -28,6 +28,11 @@ const DEFAULT_SIDEBAR_WIDTH = 344
const DEFAULT_FILE_TREE_WIDTH = 200
const DEFAULT_SESSION_WIDTH = 600
const DEFAULT_TERMINAL_HEIGHT = 280

// Widest the chat panel is allowed to be (mirrors the `max` clamp on the resize handle),
// which is the review pane's smallest state. `resolveSessionWidth` uses it as the first-open
// default. SSR has no window, so fall back to the static default there.
const maxSessionWidth = () => (typeof window === "undefined" ? DEFAULT_SESSION_WIDTH : window.innerWidth * 0.45)
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]

export function getAvatarColors(key?: string) {
Expand Down Expand Up @@ -286,9 +291,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
width: DEFAULT_FILE_TREE_WIDTH,
tab: "changes" as "changes" | "all",
},
session: {
width: DEFAULT_SESSION_WIDTH,
},
mobileSidebar: {
opened: false,
},
Expand All @@ -303,6 +305,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}),
)

// Chat/review split width lives in the shared global scope, not the per-server layout
// store, so a width the user sets in one session is remembered across every session and
// server (each Forge task connects to its own server). `undefined` marks "never resized",
// which the getter resolves to the smallest review pane at read time.
const [sessionWidthStore, setSessionWidthStore] = persisted(
Persist.global("layout-session-width"),
createStore({ width: undefined as number | undefined }),
)

const MAX_SESSION_KEYS = 50
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
const usage = {
Expand Down Expand Up @@ -709,13 +720,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
session: {
width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH),
width: createMemo(() => resolveSessionWidth(sessionWidthStore.width, maxSessionWidth())),
resize(width: number) {
if (!store.session) {
setStore("session", { width })
return
}
setStore("session", "width", width)
setSessionWidthStore("width", width)
},
},
mobileSidebar: {
Expand Down
Loading