Add resizable session drawers

This commit is contained in:
Shantur Rathore
2025-12-14 13:01:29 +00:00
parent 8d5c6b37e9
commit 542b59940a
4 changed files with 178 additions and 147 deletions

View File

@@ -70,16 +70,22 @@ const DEFAULT_SESSION_SIDEBAR_WIDTH = 280
const MIN_SESSION_SIDEBAR_WIDTH = 220
const MAX_SESSION_SIDEBAR_WIDTH = 360
const RIGHT_DRAWER_WIDTH = 260
const MIN_RIGHT_DRAWER_WIDTH = 200
const MAX_RIGHT_DRAWER_WIDTH = 380
const SESSION_CACHE_LIMIT = 2
const APP_BAR_HEIGHT = 56
const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8"
const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1"
type LayoutMode = "desktop" | "tablet" | "phone"
const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
const clampRightWidth = (value: number) => Math.min(MAX_RIGHT_DRAWER_WIDTH, Math.max(MIN_RIGHT_DRAWER_WIDTH, value))
const InstanceShell2: Component<InstanceShellProps> = (props) => {
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH)
const [leftPinned, setLeftPinned] = createSignal(true)
const [leftOpen, setLeftOpen] = createSignal(true)
const [rightPinned, setRightPinned] = createSignal(true)
@@ -93,6 +99,9 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal<HTMLElement | null>(null)
const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal<HTMLElement | null>(null)
const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal<HTMLElement | null>(null)
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
const [resizeStartX, setResizeStartX] = createSignal(0)
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id))
@@ -145,6 +154,23 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
onMount(() => {
if (typeof window === "undefined") return
const savedLeft = window.localStorage.getItem(LEFT_DRAWER_STORAGE_KEY)
if (savedLeft) {
const parsed = Number.parseInt(savedLeft, 10)
if (Number.isFinite(parsed)) {
setSessionSidebarWidth(clampWidth(parsed))
}
}
const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY)
if (savedRight) {
const parsed = Number.parseInt(savedRight, 10)
if (Number.isFinite(parsed)) {
setRightDrawerWidth(clampRightWidth(parsed))
}
}
const handleResize = () => {
const width = clampWidth(window.innerWidth * 0.3)
setSessionSidebarWidth((current) => clampWidth(current || width))
@@ -156,6 +182,16 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
onCleanup(() => window.removeEventListener("resize", handleResize))
})
createEffect(() => {
if (typeof window === "undefined") return
window.localStorage.setItem(LEFT_DRAWER_STORAGE_KEY, sessionSidebarWidth().toString())
})
createEffect(() => {
if (typeof window === "undefined") return
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
})
createEffect(() => {
props.tabBarOffset
requestAnimationFrame(() => measureDrawerHost())
@@ -231,9 +267,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
setActiveSession(props.instance.id, sessionId)
}
const handleSidebarWidthChange = (nextWidth: number) => {
setSessionSidebarWidth(clampWidth(nextWidth))
}
const evictSession = (sessionId: string) => {
if (!sessionId) return
@@ -327,7 +360,89 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
return `calc(100% - ${floatingTop()}px)`
}
const scheduleDrawerMeasure = () => {
if (typeof window === "undefined") {
measureDrawerHost()
return
}
requestAnimationFrame(() => measureDrawerHost())
}
const applyDrawerWidth = (side: "left" | "right", width: number) => {
if (side === "left") {
setSessionSidebarWidth(width)
} else {
setRightDrawerWidth(width)
}
scheduleDrawerMeasure()
}
const handleDrawerPointerMove = (clientX: number) => {
const side = activeResizeSide()
if (!side) return
const startWidth = resizeStartWidth()
const clamp = side === "left" ? clampWidth : clampRightWidth
const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX
const nextWidth = clamp(startWidth + delta)
applyDrawerWidth(side, nextWidth)
}
function stopDrawerResize() {
setActiveResizeSide(null)
document.removeEventListener("mousemove", drawerMouseMove)
document.removeEventListener("mouseup", drawerMouseUp)
document.removeEventListener("touchmove", drawerTouchMove)
document.removeEventListener("touchend", drawerTouchEnd)
}
function drawerMouseMove(event: MouseEvent) {
event.preventDefault()
handleDrawerPointerMove(event.clientX)
}
function drawerMouseUp() {
stopDrawerResize()
}
function drawerTouchMove(event: TouchEvent) {
const touch = event.touches[0]
if (!touch) return
event.preventDefault()
handleDrawerPointerMove(touch.clientX)
}
function drawerTouchEnd() {
stopDrawerResize()
}
const startDrawerResize = (side: "left" | "right", clientX: number) => {
setActiveResizeSide(side)
setResizeStartX(clientX)
setResizeStartWidth(side === "left" ? sessionSidebarWidth() : rightDrawerWidth())
document.addEventListener("mousemove", drawerMouseMove)
document.addEventListener("mouseup", drawerMouseUp)
document.addEventListener("touchmove", drawerTouchMove, { passive: false })
document.addEventListener("touchend", drawerTouchEnd)
}
const handleDrawerResizeMouseDown = (side: "left" | "right") => (event: MouseEvent) => {
event.preventDefault()
startDrawerResize(side, event.clientX)
}
const handleDrawerResizeTouchStart = (side: "left" | "right") => (event: TouchEvent) => {
const touch = event.touches[0]
if (!touch) return
event.preventDefault()
startDrawerResize(side, touch.clientX)
}
onCleanup(() => {
stopDrawerResize()
})
type DrawerViewState = "pinned" | "floating-open" | "floating-closed"
const leftDrawerState = createMemo<DrawerViewState>(() => {
if (leftPinned()) return "pinned"
@@ -519,7 +634,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
}}
showHeader={false}
showFooter={false}
onWidthChange={handleSidebarWidthChange}
/>
<Divider />
@@ -593,8 +707,16 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
backgroundColor: "var(--surface-secondary)",
height: "100%",
minHeight: 0,
position: "relative",
}}
>
<div
class="session-resize-handle session-resize-handle--left"
onMouseDown={handleDrawerResizeMouseDown("left")}
onTouchStart={handleDrawerResizeTouchStart("left")}
role="presentation"
aria-hidden="true"
/>
<LeftDrawerContent />
</Box>
)
@@ -639,14 +761,22 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<Box
class="session-right-panel"
sx={{
width: RIGHT_DRAWER_WIDTH,
width: `${rightDrawerWidth()}px`,
flexShrink: 0,
borderLeft: "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",
height: "100%",
minHeight: 0,
position: "relative",
}}
>
<div
class="session-resize-handle session-resize-handle--right"
onMouseDown={handleDrawerResizeMouseDown("right")}
onTouchStart={handleDrawerResizeTouchStart("right")}
role="presentation"
aria-hidden="true"
/>
<RightDrawerContent />
</Box>
)
@@ -662,7 +792,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
ModalProps={modalProps}
sx={{
"& .MuiDrawer-paper": {
width: isPhoneLayout() ? "100vw" : `${RIGHT_DRAWER_WIDTH}px`,
width: isPhoneLayout() ? "100vw" : `${rightDrawerWidth()}px`,
boxSizing: "border-box",
borderLeft: isPhoneLayout() ? "none" : "1px solid var(--border-base)",
backgroundColor: "var(--surface-secondary)",