import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createMediaQuery } from "@solid-primitives/media" import { Tabs } from "@opencode-ai/ui/tabs" import { IconButton } from "@opencode-ai/ui/icon-button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Mark } from "@opencode-ai/ui/logo" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" import FileTree from "@/components/file-tree" import { SessionContextUsage } from "@/components/session-context-usage" import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" import { useCommand } from "@/context/command" import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" import { setSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" export function SessionSidePanel(props: { canReview: () => boolean diffs: () => (SnapshotFileDiff | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean reviewCount: () => number reviewPanel: () => JSX.Element activeDiff?: string focusReviewDiff: (path: string) => void reviewSnap: boolean size: Sizing }) { const layout = useLayout() const platform = usePlatform() const settings = useSettings() const file = useFile() const language = useLanguage() const command = useCommand() const dialog = useDialog() const { sessionKey, tabs, view } = useSessionLayout() const isDesktop = createMediaQuery("(min-width: 768px)") const shown = createMemo(() => platform.platform !== "desktop" || settings.general.showFileTree()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) const open = createMemo(() => reviewOpen() || fileOpen()) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return `calc(100% - ${layout.session.width()}px)` return `${layout.fileTree.width()}px` }) const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) const diffFiles = createMemo(() => props.diffs().map((d) => d.file)) const kinds = createMemo(() => { const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { if (!a) return b if (a === b) return a return "mix" as const } const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") const out = new Map() for (const diff of props.diffs()) { const file = normalize(diff.file) const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" out.set(file, kind) const parts = file.split("/") for (const [idx] of parts.slice(0, -1).entries()) { const dir = parts.slice(0, idx + 1).join("/") if (!dir) continue out.set(dir, merge(out.get(dir), kind)) } } return out }) const empty = (msg: string) => (
{msg}
) const nofiles = createMemo(() => { const state = file.tree.state("") if (!state?.loaded) return false return file.tree.children("").length === 0 }) const normalizeTab = (tab: string) => { if (!tab.startsWith("file://")) return tab return file.tab(tab) } const openReviewPanel = () => { if (!view().reviewPanel.opened()) view().reviewPanel.open() } const openTab = createOpenSessionFileTab({ normalizeTab, openTab: tabs().open, pathFromTab: file.pathFromTab, loadFile: file.load, openReviewPanel, setActive: tabs().setActive, }) const tabState = createSessionTabs({ tabs, pathFromTab: file.pathFromTab, normalizeTab, review: reviewTab, hasReview: props.canReview, }) const contextOpen = tabState.contextOpen const openedTabs = tabState.openedTabs const activeTab = tabState.activeTab const activeFileTab = tabState.activeFileTab const fileTreeTab = () => layout.fileTree.tab() const setFileTreeTabValue = (value: string) => { if (value !== "changes" && value !== "all") return layout.fileTree.setTab(value) } const showAllFiles = () => { if (fileTreeTab() !== "changes") return layout.fileTree.setTab("all") } const [store, setStore] = createStore({ activeDraggable: undefined as string | undefined, }) const handleDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return setStore("activeDraggable", id) } const handleDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (!draggable || !droppable) return const currentTabs = tabs().all() const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) if (toIndex === undefined) return tabs().move(draggable.id.toString(), toIndex) } const handleDragEnd = () => { setStore("activeDraggable", undefined) } createEffect(() => { if (!file.ready()) return setSessionHandoff(sessionKey(), { files: tabs() .all() .reduce>((acc, tab) => { const path = file.pathFromTab(tab) if (!path) return acc const selected = file.selectedLines(path) acc[path] = selected && typeof selected === "object" && "start" in selected && "end" in selected ? (selected as SelectedLineRange) : null return acc }, {}), }) }) return (
) }