Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Sebastian <hasta84@gmail.com> Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com> Co-authored-by: Test User <test@test.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Qiping Li <liqiping1991@gmail.com> Co-authored-by: liqiping <liqiping@msh.team> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Matthias Reso <13337103+mreso@users.noreply.github.com> Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com> Co-authored-by: Daniel Polito <danielbpolito@gmail.com> Co-authored-by: opencode <noreply@opencode.ai> Co-authored-by: Devin R Leopold <devin.leopold@gmail.com> Co-authored-by: Zach Bruggeman <mail@bruggie.com> Co-authored-by: Zach Bruggeman <zbruggeman@ramp.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: David Siewert <david1gruppenplan@gmail.com> Co-authored-by: Andrei Dziahel <develop7@develop7.info> Co-authored-by: adityachaudhary99 <adityaachaudhary2003@gmail.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Matt Carey <mcarey@cloudflare.com>
172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
import { createEffect, onCleanup, type JSX } from "solid-js"
|
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
|
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
|
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
|
import { SessionReview } from "@opencode-ai/session-ui/session-review"
|
|
import type {
|
|
SessionReviewCommentActions,
|
|
SessionReviewCommentDelete,
|
|
SessionReviewCommentUpdate,
|
|
} from "@opencode-ai/session-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"
|
|
|
|
type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
|
|
|
|
export interface SessionReviewTabProps {
|
|
title?: JSX.Element
|
|
empty?: JSX.Element
|
|
diffs: () => ReviewDiff[]
|
|
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
|
|
onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void
|
|
onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void
|
|
lineCommentActions?: SessionReviewCommentActions
|
|
comments?: LineComment[]
|
|
focusedComment?: { file: string; id: string } | null
|
|
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
|
|
focusedFile?: string
|
|
onScrollRef?: (el: HTMLDivElement | undefined) => void
|
|
commentMentions?: {
|
|
items: (query: string) => string[] | Promise<string[]>
|
|
}
|
|
classes?: {
|
|
root?: string
|
|
header?: string
|
|
container?: string
|
|
}
|
|
}
|
|
|
|
export function SessionReviewTab(props: SessionReviewTabProps) {
|
|
let scroll: HTMLDivElement | undefined
|
|
let restoreFrame: number | undefined
|
|
let userInteracted = false
|
|
let restored: { x: number; y: number } | undefined
|
|
|
|
const sdk = useSDK()
|
|
const layout = useLayout()
|
|
|
|
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 handleInteraction = () => {
|
|
userInteracted = true
|
|
|
|
if (restoreFrame !== undefined) {
|
|
cancelAnimationFrame(restoreFrame)
|
|
restoreFrame = undefined
|
|
}
|
|
}
|
|
|
|
const doRestore = () => {
|
|
restoreFrame = undefined
|
|
const el = scroll
|
|
if (!el || !layout.ready() || userInteracted) return
|
|
if (el.clientHeight === 0 || el.clientWidth === 0) return
|
|
|
|
const s = props.view().scroll("review")
|
|
if (!s || (s.x === 0 && s.y === 0)) return
|
|
|
|
const maxY = Math.max(0, el.scrollHeight - el.clientHeight)
|
|
const maxX = Math.max(0, el.scrollWidth - el.clientWidth)
|
|
|
|
const targetY = Math.min(s.y, maxY)
|
|
const targetX = Math.min(s.x, maxX)
|
|
|
|
if (el.scrollTop === targetY && el.scrollLeft === targetX) return
|
|
|
|
if (el.scrollTop !== targetY) el.scrollTop = targetY
|
|
if (el.scrollLeft !== targetX) el.scrollLeft = targetX
|
|
restored = { x: el.scrollLeft, y: el.scrollTop }
|
|
}
|
|
|
|
const queueRestore = () => {
|
|
if (userInteracted || restoreFrame !== undefined) return
|
|
restoreFrame = requestAnimationFrame(doRestore)
|
|
}
|
|
|
|
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
|
|
const el = event.currentTarget
|
|
const prev = restored
|
|
if (prev && el.scrollTop === prev.y && el.scrollLeft === prev.x) {
|
|
restored = undefined
|
|
return
|
|
}
|
|
|
|
restored = undefined
|
|
handleInteraction()
|
|
if (!layout.ready()) return
|
|
if (el.clientHeight === 0 || el.clientWidth === 0) return
|
|
|
|
props.view().setScroll("review", {
|
|
x: el.scrollLeft,
|
|
y: el.scrollTop,
|
|
})
|
|
}
|
|
|
|
createEffect(() => {
|
|
props.diffs().length
|
|
props.diffStyle
|
|
if (!layout.ready()) return
|
|
queueRestore()
|
|
})
|
|
|
|
onCleanup(() => {
|
|
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
|
props.onScrollRef?.(undefined)
|
|
})
|
|
|
|
return (
|
|
<SessionReview
|
|
title={props.title}
|
|
empty={props.empty}
|
|
scrollRef={(el) => {
|
|
scroll = el
|
|
makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true })
|
|
makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true })
|
|
makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true })
|
|
makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true })
|
|
makeEventListener(el, "keydown", handleInteraction, { capture: true })
|
|
props.onScrollRef?.(el)
|
|
queueRestore()
|
|
}}
|
|
onScroll={handleScroll}
|
|
onDiffRendered={queueRestore}
|
|
open={props.view().review.open()}
|
|
onOpenChange={props.view().review.setOpen}
|
|
classes={{
|
|
root: props.classes?.root ?? "pr-3",
|
|
header: props.classes?.header ?? "px-3",
|
|
container: props.classes?.container ?? "pl-3",
|
|
}}
|
|
diffs={props.diffs()}
|
|
diffStyle={props.diffStyle}
|
|
onDiffStyleChange={props.onDiffStyleChange}
|
|
onViewFile={props.onViewFile}
|
|
focusedFile={props.focusedFile}
|
|
readFile={readFile}
|
|
onLineComment={props.onLineComment}
|
|
onLineCommentUpdate={props.onLineCommentUpdate}
|
|
onLineCommentDelete={props.onLineCommentDelete}
|
|
lineCommentActions={props.lineCommentActions}
|
|
lineCommentMention={props.commentMentions}
|
|
comments={props.comments}
|
|
focusedComment={props.focusedComment}
|
|
onFocusedCommentChange={props.onFocusedCommentChange}
|
|
/>
|
|
)
|
|
}
|