Add resizable session drawers
This commit is contained in:
@@ -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)",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo, JSX } from "solid-js"
|
||||
import { Component, For, Show, createSignal, createMemo, JSX } from "solid-js"
|
||||
import type { Session, SessionStatus } from "../types/session"
|
||||
import { getSessionStatus } from "../stores/session-status"
|
||||
import { MessageSquare, Info, X, Copy, Trash2, Pencil } from "lucide-solid"
|
||||
@@ -25,14 +25,8 @@ interface SessionListProps {
|
||||
showFooter?: boolean
|
||||
headerContent?: JSX.Element
|
||||
footerContent?: JSX.Element
|
||||
onWidthChange?: (width: number) => void
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 200
|
||||
const MAX_WIDTH = 520
|
||||
const DEFAULT_WIDTH = 360
|
||||
const STORAGE_KEY = "opencode-session-sidebar-width-v7"
|
||||
|
||||
function formatSessionStatus(status: SessionStatus): string {
|
||||
switch (status) {
|
||||
case "working":
|
||||
@@ -63,10 +57,6 @@ function arraysEqual(prev: readonly string[] | undefined, next: readonly string[
|
||||
}
|
||||
|
||||
const SessionList: Component<SessionListProps> = (props) => {
|
||||
const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH)
|
||||
const [isResizing, setIsResizing] = createSignal(false)
|
||||
const [startX, setStartX] = createSignal(0)
|
||||
const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH)
|
||||
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
||||
const [isRenaming, setIsRenaming] = createSignal(false)
|
||||
const infoShortcut = keyboardRegistry.get("switch-to-info")
|
||||
@@ -79,34 +69,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const selectSession = (sessionId: string) => {
|
||||
props.onSelect(sessionId)
|
||||
}
|
||||
|
||||
|
||||
let mouseMoveHandler: ((event: MouseEvent) => void) | null = null
|
||||
let mouseUpHandler: (() => void) | null = null
|
||||
let touchMoveHandler: ((event: TouchEvent) => void) | null = null
|
||||
let touchEndHandler: (() => void) | null = null
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const saved = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (!saved) return
|
||||
|
||||
const width = Number.parseInt(saved, 10)
|
||||
if (Number.isFinite(width) && width >= MIN_WIDTH && width <= MAX_WIDTH) {
|
||||
setSidebarWidth(width)
|
||||
setStartWidth(width)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const width = sidebarWidth()
|
||||
window.localStorage.setItem(STORAGE_KEY, width.toString())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.onWidthChange?.(sidebarWidth())
|
||||
})
|
||||
|
||||
const copySessionId = async (event: MouseEvent, sessionId: string) => {
|
||||
event.stopPropagation()
|
||||
@@ -150,7 +112,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const handleRenameSubmit = async (nextTitle: string) => {
|
||||
const target = renameTarget()
|
||||
if (!target) return
|
||||
|
||||
|
||||
setIsRenaming(true)
|
||||
try {
|
||||
await renameSession(props.instanceId, target.id, nextTitle)
|
||||
@@ -163,96 +125,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const clampWidth = (width: number) => Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
|
||||
|
||||
|
||||
|
||||
|
||||
const removeMouseListeners = () => {
|
||||
if (mouseMoveHandler) {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler)
|
||||
mouseMoveHandler = null
|
||||
}
|
||||
if (mouseUpHandler) {
|
||||
document.removeEventListener("mouseup", mouseUpHandler)
|
||||
mouseUpHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
const removeTouchListeners = () => {
|
||||
if (touchMoveHandler) {
|
||||
document.removeEventListener("touchmove", touchMoveHandler)
|
||||
touchMoveHandler = null
|
||||
}
|
||||
if (touchEndHandler) {
|
||||
document.removeEventListener("touchend", touchEndHandler)
|
||||
touchEndHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
const stopResizing = () => {
|
||||
setIsResizing(false)
|
||||
removeMouseListeners()
|
||||
removeTouchListeners()
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isResizing()) return
|
||||
const diff = event.clientX - startX()
|
||||
const newWidth = clampWidth(startWidth() + diff)
|
||||
setSidebarWidth(newWidth)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
stopResizing()
|
||||
}
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (!isResizing()) return
|
||||
const touch = event.touches[0]
|
||||
if (!touch) return
|
||||
const diff = touch.clientX - startX()
|
||||
const newWidth = clampWidth(startWidth() + diff)
|
||||
setSidebarWidth(newWidth)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
stopResizing()
|
||||
}
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
setIsResizing(true)
|
||||
setStartX(event.clientX)
|
||||
setStartWidth(sidebarWidth())
|
||||
|
||||
mouseMoveHandler = handleMouseMove
|
||||
mouseUpHandler = handleMouseUp
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove)
|
||||
document.addEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
event.preventDefault()
|
||||
const touch = event.touches[0]
|
||||
if (!touch) return
|
||||
setIsResizing(true)
|
||||
setStartX(touch.clientX)
|
||||
setStartWidth(sidebarWidth())
|
||||
|
||||
touchMoveHandler = handleTouchMove
|
||||
touchEndHandler = handleTouchEnd
|
||||
|
||||
document.addEventListener("touchmove", handleTouchMove)
|
||||
document.addEventListener("touchend", handleTouchEnd)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
removeMouseListeners()
|
||||
removeTouchListeners()
|
||||
})
|
||||
|
||||
const SessionRow: Component<{ sessionId: string; canClose?: boolean }> = (rowProps) => {
|
||||
const session = () => props.sessions.get(rowProps.sessionId)
|
||||
if (!session()) {
|
||||
@@ -392,14 +265,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
<div
|
||||
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
||||
>
|
||||
<div
|
||||
class="session-resize-handle"
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<Show when={props.showHeader !== false}>
|
||||
<div class="session-list-header p-3 border-b border-base">
|
||||
{props.headerContent ?? (
|
||||
|
||||
Reference in New Issue
Block a user