159 lines
4.4 KiB
TypeScript
159 lines
4.4 KiB
TypeScript
import { createEffect, on, onCleanup, createSignal, type JSX } from "solid-js"
|
|
import type { FileDiff } from "@opencode-ai/sdk/v2"
|
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
|
import type { SelectedLineRange } from "@/context/file"
|
|
import { useSDK } from "@/context/sdk"
|
|
import { useLayout } from "@/context/layout"
|
|
import type { LineComment } from "@/context/comments"
|
|
|
|
export type DiffStyle = "unified" | "split"
|
|
|
|
export interface SessionReviewTabProps {
|
|
title?: JSX.Element
|
|
empty?: JSX.Element
|
|
diffs: () => FileDiff[]
|
|
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
|
diffStyle: DiffStyle
|
|
onDiffStyleChange?: (style: DiffStyle) => void
|
|
onViewFile?: (file: string) => void
|
|
onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
|
|
comments?: LineComment[]
|
|
focusedComment?: { file: string; id: string } | null
|
|
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
|
|
focusedFile?: string
|
|
onScrollRef?: (el: HTMLDivElement) => void
|
|
classes?: {
|
|
root?: string
|
|
header?: string
|
|
container?: string
|
|
}
|
|
}
|
|
|
|
export function StickyAddButton(props: { children: JSX.Element }) {
|
|
const [stuck, setStuck] = createSignal(false)
|
|
let button: HTMLDivElement | undefined
|
|
|
|
createEffect(() => {
|
|
const node = button
|
|
if (!node) return
|
|
|
|
const scroll = node.parentElement
|
|
if (!scroll) return
|
|
|
|
const handler = () => {
|
|
const rect = node.getBoundingClientRect()
|
|
const scrollRect = scroll.getBoundingClientRect()
|
|
setStuck(rect.right >= scrollRect.right && scroll.scrollWidth > scroll.clientWidth)
|
|
}
|
|
|
|
scroll.addEventListener("scroll", handler, { passive: true })
|
|
const observer = new ResizeObserver(handler)
|
|
observer.observe(scroll)
|
|
handler()
|
|
onCleanup(() => {
|
|
scroll.removeEventListener("scroll", handler)
|
|
observer.disconnect()
|
|
})
|
|
})
|
|
|
|
return (
|
|
<div
|
|
ref={button}
|
|
class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-border-weak-base px-3"
|
|
classList={{ "border-l": stuck() }}
|
|
>
|
|
{props.children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function SessionReviewTab(props: SessionReviewTabProps) {
|
|
let scroll: HTMLDivElement | undefined
|
|
let frame: number | undefined
|
|
let pending: { x: number; y: number } | undefined
|
|
|
|
const sdk = useSDK()
|
|
|
|
const readFile = async (path: string) => {
|
|
return sdk.client.file
|
|
.read({ path })
|
|
.then((x) => x.data)
|
|
.catch(() => undefined)
|
|
}
|
|
|
|
const restoreScroll = () => {
|
|
const el = scroll
|
|
if (!el) return
|
|
|
|
const s = props.view().scroll("review")
|
|
if (!s) return
|
|
|
|
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
|
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
|
}
|
|
|
|
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
|
pending = {
|
|
x: event.currentTarget.scrollLeft,
|
|
y: event.currentTarget.scrollTop,
|
|
}
|
|
if (frame !== undefined) return
|
|
|
|
frame = requestAnimationFrame(() => {
|
|
frame = undefined
|
|
|
|
const next = pending
|
|
pending = undefined
|
|
if (!next) return
|
|
|
|
props.view().setScroll("review", next)
|
|
})
|
|
}
|
|
|
|
createEffect(
|
|
on(
|
|
() => props.diffs().length,
|
|
() => {
|
|
requestAnimationFrame(restoreScroll)
|
|
},
|
|
{ defer: true },
|
|
),
|
|
)
|
|
|
|
onCleanup(() => {
|
|
if (frame === undefined) return
|
|
cancelAnimationFrame(frame)
|
|
})
|
|
|
|
return (
|
|
<SessionReview
|
|
title={props.title}
|
|
empty={props.empty}
|
|
scrollRef={(el) => {
|
|
scroll = el
|
|
props.onScrollRef?.(el)
|
|
restoreScroll()
|
|
}}
|
|
onScroll={handleScroll}
|
|
onDiffRendered={() => requestAnimationFrame(restoreScroll)}
|
|
open={props.view().review.open()}
|
|
onOpenChange={props.view().review.setOpen}
|
|
classes={{
|
|
root: props.classes?.root ?? "pb-6",
|
|
header: props.classes?.header ?? "px-6",
|
|
container: props.classes?.container ?? "px-6",
|
|
}}
|
|
diffs={props.diffs()}
|
|
diffStyle={props.diffStyle}
|
|
onDiffStyleChange={props.onDiffStyleChange}
|
|
onViewFile={props.onViewFile}
|
|
focusedFile={props.focusedFile}
|
|
readFile={readFile}
|
|
onLineComment={props.onLineComment}
|
|
comments={props.comments}
|
|
focusedComment={props.focusedComment}
|
|
onFocusedCommentChange={props.onFocusedCommentChange}
|
|
/>
|
|
)
|
|
}
|