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 { useParams } from "@solidjs/router" import { Tabs } from "@opencode-ai/ui/tabs" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip, 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 { 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 { DialogSelectFile } from "@/components/dialog-select-file" 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 { useSync } from "@/context/sync" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers" import { StickyAddButton } from "@/pages/session/review-tab" import { setSessionHandoff } from "@/pages/session/handoff" export function SessionSidePanel(props: { reviewPanel: () => JSX.Element activeDiff?: string focusReviewDiff: (path: string) => void }) { const params = useParams() const layout = useLayout() const sync = useSync() const file = useFile() const language = useLanguage() const command = useCommand() const dialog = useDialog() const isDesktop = createMediaQuery("(min-width: 768px)") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened())) const reviewTab = createMemo(() => isDesktop()) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) const hasReview = createMemo(() => reviewCount() > 0) const diffsReady = createMemo(() => { const id = params.id if (!id) return true if (!hasReview()) return true return sync.data.session_diff[id] !== undefined }) const diffFiles = createMemo(() => 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 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 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 contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) const openedTabs = createMemo(() => tabs() .all() .filter((tab) => tab !== "context" && tab !== "review"), ) const activeTab = createMemo(() => { const active = tabs().active() if (active === "context") return "context" if (active === "review" && reviewTab()) return "review" if (active && file.pathFromTab(active)) return normalizeTab(active) const first = openedTabs()[0] if (first) return first if (contextOpen()) return "context" if (reviewTab() && hasReview()) return "review" return "empty" }) const activeFileTab = createMemo(() => { const active = activeTab() if (!openedTabs().includes(active)) return return active }) 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 ( ) }