open-swe/ui/src/components/agents/ReviewSidebar.tsx

132 lines
3.2 KiB
TypeScript
Raw Normal View History

import { createContext, useContext, useEffect, useMemo, useState } from "react"
import {
FileTree,
useFileTree,
useFileTreeSelection,
} from "@pierre/trees/react"
import type { GitStatusEntry } from "@pierre/trees"
import type { ReviewDiffFile } from "@/lib/api"
import { Skeleton } from "@/components/ui/skeleton"
import { treeThemeStyle } from "@/components/agents/AgentGitPanel"
export interface ReviewSidebarData {
title: string
files: Array<ReviewDiffFile> | null
selected: string | null
viewed: Set<string>
onSelect: (path: string) => void
}
const ReviewSidebarContext = createContext<{
data: ReviewSidebarData | null
setData: (data: ReviewSidebarData | null) => void
} | null>(null)
export function ReviewSidebarProvider({
children,
}: {
children: React.ReactNode
}) {
const [data, setData] = useState<ReviewSidebarData | null>(null)
const value = useMemo(() => ({ data, setData }), [data])
return (
<ReviewSidebarContext.Provider value={value}>
{children}
</ReviewSidebarContext.Provider>
)
}
export function useReviewSidebarData(): ReviewSidebarData | null {
return useContext(ReviewSidebarContext)?.data ?? null
}
export function useRegisterReviewSidebar(data: ReviewSidebarData) {
const setData = useContext(ReviewSidebarContext)?.setData
useEffect(() => {
if (!setData) return
setData(data)
return () => setData(null)
}, [setData, data])
}
export function ReviewFileTree({ data }: { data: ReviewSidebarData }) {
return (
<div className="flex min-h-0 flex-1 flex-col pb-2">
<div className="px-4 py-1 text-[10px] font-semibold tracking-wide text-[var(--ui-text-dim)] uppercase">
{data.title}
</div>
{!data.files ? (
<div className="px-4 pt-1">
<Skeleton className="h-40 w-full" />
</div>
) : (
<ReviewFileTreeExplorer
files={data.files}
selected={data.selected}
onSelect={data.onSelect}
/>
)}
</div>
)
}
function ReviewFileTreeExplorer({
files,
selected,
onSelect,
}: {
files: Array<ReviewDiffFile>
selected: string | null
onSelect: (path: string) => void
}) {
const paths = useMemo(() => files.map((file) => file.path), [files])
const gitStatus = useMemo<Array<GitStatusEntry>>(
() => files.map((file) => ({ path: file.path, status: file.status })),
[files]
)
const { model } = useFileTree({
paths,
gitStatus,
initialExpansion: "open",
flattenEmptyDirectories: true,
icons: "standard",
})
useEffect(() => {
model.resetPaths(paths)
}, [model, paths])
useEffect(() => {
model.setGitStatus(gitStatus)
}, [model, gitStatus])
const selection = useFileTreeSelection(model)
useEffect(() => {
const path = selection[0]
if (path) onSelect(path)
}, [selection, onSelect])
useEffect(() => {
if (selected) {
model.scrollToPath(selected, { focus: false })
}
}, [model, selected])
return (
<div className="min-h-0 flex-1">
<FileTree
model={model}
style={
{
height: "100%",
...treeThemeStyle(),
"--trees-theme-sidebar-bg": "transparent",
} as React.CSSProperties
}
/>
</div>
)
}