import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo, JSX } from "solid-js" import type { Session, SessionStatus } from "../types/session" import { getSessionStatus } from "../stores/session-status" import { MessageSquare, Info, X, Copy } from "lucide-solid" import KeyboardHint from "./keyboard-hint" import Kbd from "./kbd" import { keyboardRegistry } from "../lib/keyboard-registry" import { formatShortcut } from "../lib/keyboard-utils" import { showToastNotification } from "../lib/notifications" interface SessionListProps { instanceId: string sessions: Map activeSessionId: string | null onSelect: (sessionId: string) => void onClose: (sessionId: string) => void onNew: () => void showHeader?: boolean 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": return "Working" case "compacting": return "Compacting" default: return "Idle" } } function arraysEqual(prev: readonly string[] | undefined, next: readonly string[]): boolean { if (!prev) { return false } if (prev.length !== next.length) { return false } for (let i = 0; i < prev.length; i++) { if (prev[i] !== next[i]) { return false } } return true } const SessionList: Component = (props) => { const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH) const [isResizing, setIsResizing] = createSignal(false) const [startX, setStartX] = createSignal(0) const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH) const infoShortcut = keyboardRegistry.get("switch-to-info") 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() try { if (typeof navigator === "undefined" || !navigator.clipboard) { throw new Error("Clipboard API unavailable") } await navigator.clipboard.writeText(sessionId) showToastNotification({ message: "Session ID copied", variant: "success" }) } catch (error) { console.error(`Failed to copy session ID ${sessionId}:`, error) showToastNotification({ message: "Unable to copy session ID", variant: "error" }) } } 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()) { return <> } const isActive = () => props.activeSessionId === rowProps.sessionId const title = () => session()?.title || "Untitled" const status = () => getSessionStatus(props.instanceId, rowProps.sessionId) const statusLabel = () => formatSessionStatus(status()) const pendingPermission = () => Boolean(session()?.pendingPermission) const statusClassName = () => (pendingPermission() ? "session-permission" : `session-${status()}`) const statusText = () => (pendingPermission() ? "Needs Permission" : statusLabel()) return (
) } const userSessionIds = createMemo( () => { const ids: string[] = [] for (const session of props.sessions.values()) { if (session.parentId === null) { ids.push(session.id) } } return ids }, undefined, { equals: arraysEqual }, ) const childSessionIds = createMemo( () => { const children: { id: string; updated: number }[] = [] for (const session of props.sessions.values()) { if (session.parentId !== null) { children.push({ id: session.id, updated: session.time.updated ?? 0 }) } } if (children.length <= 1) { return children.map((entry) => entry.id) } children.sort((a, b) => b.updated - a.updated) return children.map((entry) => entry.id) }, undefined, { equals: arraysEqual }, ) return (
) } export default SessionList