import { For, Show, type Accessor, type Component, type JSX } from "solid-js" import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer" import DiffToolbar from "../components/DiffToolbar" import SplitFilePanel from "../components/SplitFilePanel" import type { DiffContextMode, DiffViewMode } from "../types" interface ChangesTabProps { t: (key: string, vars?: Record) => string instanceId: string activeSessionId: Accessor activeSessionDiffs: Accessor selectedFile: Accessor onSelectFile: (file: string, closeList: boolean) => void diffViewMode: Accessor diffContextMode: Accessor onViewModeChange: (mode: DiffViewMode) => void onContextModeChange: (mode: DiffContextMode) => void listOpen: Accessor onToggleList: () => void splitWidth: Accessor onResizeMouseDown: (event: MouseEvent) => void onResizeTouchStart: (event: TouchEvent) => void isPhoneLayout: Accessor } const ChangesTab: Component = (props) => { const renderContent = (): JSX.Element => { const sessionId = props.activeSessionId() const hasSession = Boolean(sessionId && sessionId !== "info") const diffs = hasSession ? props.activeSessionDiffs() : null const sorted = Array.isArray(diffs) ? [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || ""))) : [] const totals = sorted.reduce( (acc, item) => { acc.additions += typeof item.additions === "number" ? item.additions : 0 acc.deletions += typeof item.deletions === "number" ? item.deletions : 0 return acc }, { additions: 0, deletions: 0 }, ) const mostChanged = sorted.length ? sorted.reduce((best, item) => { const bestAdd = typeof (best as any)?.additions === "number" ? (best as any).additions : 0 const bestDel = typeof (best as any)?.deletions === "number" ? (best as any).deletions : 0 const bestScore = bestAdd + bestDel const add = typeof (item as any)?.additions === "number" ? (item as any).additions : 0 const del = typeof (item as any)?.deletions === "number" ? (item as any).deletions : 0 const score = add + del if (score > bestScore) return item if (score < bestScore) return best return String(item.file || "").localeCompare(String((best as any)?.file || "")) < 0 ? item : best }, sorted[0]) : null // Auto-select the most-changed file if none selected. const currentSelected = props.selectedFile() const selectedFileData = sorted.find((f) => f.file === currentSelected) || mostChanged const scopeKey = `${props.instanceId}:${hasSession ? sessionId : "no-session"}` const isBinaryDiff = (item: any) => { const before = typeof item?.before === "string" ? item.before : "" const after = typeof item?.after === "string" ? item.after : "" if (before.length === 0 && after.length === 0) { // OpenCode stores empty before/after for binaries. return true } return false } const emptyViewerMessage = () => { if (!hasSession) return props.t("instanceShell.sessionChanges.noSessionSelected") if (diffs === undefined) return props.t("instanceShell.sessionChanges.loading") if (!Array.isArray(diffs) || diffs.length === 0) return props.t("instanceShell.sessionChanges.empty") return props.t("instanceShell.filesShell.viewerEmpty") } const renderViewer = () => (
0} fallback={
{emptyViewerMessage()}
} > {(file) => ( Binary file cannot be displayed
} > )}
) const renderEmptyList = () => (
{emptyViewerMessage()}
) const renderListPanel = () => ( 0} fallback={renderEmptyList()}> {(item) => (
{ props.onSelectFile(item.file, props.isPhoneLayout()) }} >
{item.file}
+{item.additions} -{item.deletions}
)}
) const renderListOverlay = () => ( 0} fallback={renderEmptyList()}> {(item) => (
{ props.onSelectFile(item.file, true) }} title={item.file} >
{item.file}
+{item.additions} -{item.deletions}
)}
) const headerPath = () => (selectedFileData?.file ? selectedFileData.file : props.t("instanceShell.rightPanel.tabs.changes")) return ( {headerPath()}
+{totals.additions} -{totals.deletions}
} list={{ panel: renderListPanel, overlay: renderListOverlay }} viewer={renderViewer()} listOpen={props.listOpen()} onToggleList={props.onToggleList} splitWidth={props.splitWidth()} onResizeMouseDown={props.onResizeMouseDown} onResizeTouchStart={props.onResizeTouchStart} isPhoneLayout={props.isPhoneLayout()} overlayAriaLabel="Changes" /> ) } return <>{renderContent()} } export default ChangesTab