import { createEffect, on, onCleanup, type JSX } from "solid-js" import { createStore } from "solid-js/store" 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["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 [state, setState] = createStore({ stuck: 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() setState("stuck", 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 (
{props.children}
) } 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((error) => { console.debug("[session-review] failed to read file", { path, error }) return 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 ( { 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-3", container: props.classes?.container ?? "px-3", }} 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} /> ) }