diff options
| author | Adam <[email protected]> | 2026-02-04 13:50:56 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-04 19:50:56 +0000 |
| commit | 222bddc41a945aec8f18536bda09c0e596748bbd (patch) | |
| tree | d12a648ec5e5ec735acafe918c71cda4143960df /packages/ui/src/components/session-review.tsx | |
| parent | 9436cb575bf64ba5867511c998d1e1c51782173c (diff) | |
| download | opencode-222bddc41a945aec8f18536bda09c0e596748bbd.tar.gz opencode-222bddc41a945aec8f18536bda09c0e596748bbd.zip | |
fix(app): last turn changes rendered in review pane (#12182)
Diffstat (limited to 'packages/ui/src/components/session-review.tsx')
| -rw-r--r-- | packages/ui/src/components/session-review.tsx | 611 |
1 files changed, 309 insertions, 302 deletions
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 84ec934e2..70d9fe802 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -36,6 +36,8 @@ export type SessionReviewLineComment = { export type SessionReviewFocus = { file: string; id: string } export interface SessionReviewProps { + title?: JSX.Element + empty?: JSX.Element split?: boolean diffStyle?: SessionReviewDiffStyle onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void @@ -184,6 +186,7 @@ export const SessionReview = (props: SessionReviewProps) => { const open = () => props.open ?? store.open const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified") + const hasDiffs = () => props.diffs.length > 0 const handleChange = (open: string[]) => { props.onOpenChange?.(open) @@ -287,9 +290,9 @@ export const SessionReview = (props: SessionReviewProps) => { [props.classes?.header ?? ""]: !!props.classes?.header, }} > - <div data-slot="session-review-title">{i18n.t("ui.sessionReview.title")}</div> + <div data-slot="session-review-title">{props.title ?? i18n.t("ui.sessionReview.title")}</div> <div data-slot="session-review-actions"> - <Show when={props.onDiffStyleChange}> + <Show when={hasDiffs() && props.onDiffStyleChange}> <RadioGroup options={["unified", "split"] as const} current={diffStyle()} @@ -300,12 +303,14 @@ export const SessionReview = (props: SessionReviewProps) => { onSelect={(style) => style && props.onDiffStyleChange?.(style)} /> </Show> - <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}> - <Switch> - <Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match> - <Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match> - </Switch> - </Button> + <Show when={hasDiffs()}> + <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}> + <Switch> + <Match when={open().length > 0}>{i18n.t("ui.sessionReview.collapseAll")}</Match> + <Match when={true}>{i18n.t("ui.sessionReview.expandAll")}</Match> + </Switch> + </Button> + </Show> {props.actions} </div> </div> @@ -315,322 +320,324 @@ export const SessionReview = (props: SessionReviewProps) => { [props.classes?.container ?? ""]: !!props.classes?.container, }} > - <Accordion multiple value={open()} onChange={handleChange}> - <For each={props.diffs}> - {(diff) => { - let wrapper: HTMLDivElement | undefined - - const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) - const commentedLines = createMemo(() => comments().map((c) => c.selection)) - - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") - - const isAdded = () => beforeText().length === 0 && afterText().length > 0 - const isDeleted = () => afterText().length === 0 && beforeText().length > 0 - const isImage = () => isImageFile(diff.file) - const isAudio = () => isAudioFile(diff.file) - - const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc) - const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") - - const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc) - const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") - const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined) - - const selectedLines = createMemo(() => { - const current = selection() - if (!current || current.file !== diff.file) return null - return current.range - }) - - const draftRange = createMemo(() => { - const current = commenting() - if (!current || current.file !== diff.file) return null - return current.range - }) - - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal<Record<string, number>>({}) - const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) - - const getRoot = () => { - const el = wrapper - if (!el) return - - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - return host.shadowRoot ?? undefined - } - - const updateAnchors = () => { - const el = wrapper - if (!el) return - - const root = getRoot() - if (!root) return - - const next: Record<string, number> = {} - for (const item of comments()) { - const marker = findMarker(root, item.selection) - if (!marker) continue - next[item.id] = markerTop(el, marker) + <Show when={hasDiffs()} fallback={props.empty}> + <Accordion multiple value={open()} onChange={handleChange}> + <For each={props.diffs}> + {(diff) => { + let wrapper: HTMLDivElement | undefined + + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) + const commentedLines = createMemo(() => comments().map((c) => c.selection)) + + const beforeText = () => (typeof diff.before === "string" ? diff.before : "") + const afterText = () => (typeof diff.after === "string" ? diff.after : "") + + const isAdded = () => beforeText().length === 0 && afterText().length > 0 + const isDeleted = () => afterText().length === 0 && beforeText().length > 0 + const isImage = () => isImageFile(diff.file) + const isAudio = () => isAudioFile(diff.file) + + const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc) + const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") + + const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc) + const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") + const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined) + + const selectedLines = createMemo(() => { + const current = selection() + if (!current || current.file !== diff.file) return null + return current.range + }) + + const draftRange = createMemo(() => { + const current = commenting() + if (!current || current.file !== diff.file) return null + return current.range + }) + + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal<Record<string, number>>({}) + const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) + + const getRoot = () => { + const el = wrapper + if (!el) return + + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + return host.shadowRoot ?? undefined } - setPositions(next) - const range = draftRange() - if (!range) { - setDraftTop(undefined) - return + const updateAnchors = () => { + const el = wrapper + if (!el) return + + const root = getRoot() + if (!root) return + + const next: Record<string, number> = {} + for (const item of comments()) { + const marker = findMarker(root, item.selection) + if (!marker) continue + next[item.id] = markerTop(el, marker) + } + setPositions(next) + + const range = draftRange() + if (!range) { + setDraftTop(undefined) + return + } + + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) + return + } + + setDraftTop(markerTop(el, marker)) } - const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) - return + const scheduleAnchors = () => { + requestAnimationFrame(updateAnchors) } - setDraftTop(markerTop(el, marker)) - } - - const scheduleAnchors = () => { - requestAnimationFrame(updateAnchors) - } - - createEffect(() => { - comments() - scheduleAnchors() - }) - - createEffect(() => { - const range = draftRange() - if (!range) return - setDraft("") - scheduleAnchors() - }) - - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isImage()) return - if (imageSrc()) return - if (imageStatus() !== "idle") return - - const reader = props.readFile - if (!reader) return - - setImageStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { + createEffect(() => { + comments() + scheduleAnchors() + }) + + createEffect(() => { + const range = draftRange() + if (!range) return + setDraft("") + scheduleAnchors() + }) + + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isImage()) return + if (imageSrc()) return + if (imageStatus() !== "idle") return + + const reader = props.readFile + if (!reader) return + + setImageStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { + setImageStatus("error") + return + } + setImageSrc(src) + setImageStatus("idle") + }) + .catch(() => { setImageStatus("error") - return - } - setImageSrc(src) - setImageStatus("idle") - }) - .catch(() => { - setImageStatus("error") - }) - }) - - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isAudio()) return - if (audioSrc()) return - if (audioStatus() !== "idle") return - - const reader = props.readFile - if (!reader) return - - setAudioStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { + }) + }) + + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isAudio()) return + if (audioSrc()) return + if (audioStatus() !== "idle") return + + const reader = props.readFile + if (!reader) return + + setAudioStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { + setAudioStatus("error") + return + } + setAudioMime(normalizeMimeType(result?.mimeType)) + setAudioSrc(src) + setAudioStatus("idle") + }) + .catch(() => { setAudioStatus("error") - return - } - setAudioMime(normalizeMimeType(result?.mimeType)) - setAudioSrc(src) - setAudioStatus("idle") - }) - .catch(() => { - setAudioStatus("error") - }) - }) - - const handleLineSelected = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return - - if (!range) { - setSelection(null) - return - } + }) + }) - setSelection({ file: diff.file, range }) - } + const handleLineSelected = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - const handleLineSelectionEnd = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + if (!range) { + setSelection(null) + return + } - if (!range) { - setCommenting(null) - return + setSelection({ file: diff.file, range }) } - setSelection({ file: diff.file, range }) - setCommenting({ file: diff.file, range }) - } + const handleLineSelectionEnd = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - const openComment = (comment: SessionReviewComment) => { - setOpened({ file: comment.file, id: comment.id }) - setSelection({ file: comment.file, range: comment.selection }) - } + if (!range) { + setCommenting(null) + return + } - const isCommentOpen = (comment: SessionReviewComment) => { - const current = opened() - if (!current) return false - return current.file === comment.file && current.id === comment.id - } + setSelection({ file: diff.file, range }) + setCommenting({ file: diff.file, range }) + } - return ( - <Accordion.Item - value={diff.file} - id={diffId(diff.file)} - data-file={diff.file} - data-slot="session-review-accordion-item" - data-selected={props.focusedFile === diff.file ? "" : undefined} - > - <StickyAccordionHeader> - <Accordion.Trigger> - <div data-slot="session-review-trigger-content"> - <div data-slot="session-review-file-info"> - <FileIcon node={{ path: diff.file, type: "file" }} /> - <div data-slot="session-review-file-name-container"> - <Show when={diff.file.includes("/")}> - <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span> - </Show> - <span data-slot="session-review-filename">{getFilename(diff.file)}</span> - <Show when={props.onViewFile}> - <button - data-slot="session-review-view-button" - type="button" - onClick={(e) => { - e.stopPropagation() - props.onViewFile?.(diff.file) - }} - > - <Icon name="eye" size="small" /> - </button> - </Show> + const openComment = (comment: SessionReviewComment) => { + setOpened({ file: comment.file, id: comment.id }) + setSelection({ file: comment.file, range: comment.selection }) + } + + const isCommentOpen = (comment: SessionReviewComment) => { + const current = opened() + if (!current) return false + return current.file === comment.file && current.id === comment.id + } + + return ( + <Accordion.Item + value={diff.file} + id={diffId(diff.file)} + data-file={diff.file} + data-slot="session-review-accordion-item" + data-selected={props.focusedFile === diff.file ? "" : undefined} + > + <StickyAccordionHeader> + <Accordion.Trigger> + <div data-slot="session-review-trigger-content"> + <div data-slot="session-review-file-info"> + <FileIcon node={{ path: diff.file, type: "file" }} /> + <div data-slot="session-review-file-name-container"> + <Show when={diff.file.includes("/")}> + <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span> + </Show> + <span data-slot="session-review-filename">{getFilename(diff.file)}</span> + <Show when={props.onViewFile}> + <button + data-slot="session-review-view-button" + type="button" + onClick={(e) => { + e.stopPropagation() + props.onViewFile?.(diff.file) + }} + > + <Icon name="eye" size="small" /> + </button> + </Show> + </div> + </div> + <div data-slot="session-review-trigger-actions"> + <Switch> + <Match when={isAdded()}> + <span data-slot="session-review-change" data-type="added"> + {i18n.t("ui.sessionReview.change.added")} + </span> + </Match> + <Match when={isDeleted()}> + <span data-slot="session-review-change" data-type="removed"> + {i18n.t("ui.sessionReview.change.removed")} + </span> + </Match> + <Match when={true}> + <DiffChanges changes={diff} /> + </Match> + </Switch> + <Icon name="chevron-grabber-vertical" size="small" /> </div> </div> - <div data-slot="session-review-trigger-actions"> - <Switch> - <Match when={isAdded()}> - <span data-slot="session-review-change" data-type="added"> - {i18n.t("ui.sessionReview.change.added")} - </span> - </Match> - <Match when={isDeleted()}> - <span data-slot="session-review-change" data-type="removed"> - {i18n.t("ui.sessionReview.change.removed")} - </span> - </Match> - <Match when={true}> - <DiffChanges changes={diff} /> - </Match> - </Switch> - <Icon name="chevron-grabber-vertical" size="small" /> - </div> - </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content data-slot="session-review-accordion-content"> - <div - data-slot="session-review-diff-wrapper" - ref={(el) => { - wrapper = el - anchors.set(diff.file, el) - scheduleAnchors() - }} - > - <Dynamic - component={diffComponent} - preloadedDiff={diff.preloaded} - diffStyle={diffStyle()} - onRendered={() => { - props.onDiffRendered?.() + </Accordion.Trigger> + </StickyAccordionHeader> + <Accordion.Content data-slot="session-review-accordion-content"> + <div + data-slot="session-review-diff-wrapper" + ref={(el) => { + wrapper = el + anchors.set(diff.file, el) scheduleAnchors() }} - enableLineSelection={props.onLineComment != null} - onLineSelected={handleLineSelected} - onLineSelectionEnd={handleLineSelectionEnd} - selectedLines={selectedLines()} - commentedLines={commentedLines()} - before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", - }} - /> - - <For each={comments()}> - {(comment) => ( - <LineComment - id={comment.id} - top={positions()[comment.id]} - onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })} - onClick={() => { - if (isCommentOpen(comment)) { - setOpened(null) - return - } - - openComment(comment) - }} - open={isCommentOpen(comment)} - comment={comment.comment} - selection={selectionLabel(comment.selection)} - /> - )} - </For> - - <Show when={draftRange()}> - {(range) => ( - <Show when={draftTop() !== undefined}> - <LineCommentEditor - top={draftTop()} - value={draft()} - selection={selectionLabel(range())} - onInput={setDraft} - onCancel={() => setCommenting(null)} - onSubmit={(comment) => { - props.onLineComment?.({ - file: diff.file, - selection: range(), - comment, - preview: selectionPreview(diff, range()), - }) - setCommenting(null) + > + <Dynamic + component={diffComponent} + preloadedDiff={diff.preloaded} + diffStyle={diffStyle()} + onRendered={() => { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", + }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", + }} + /> + + <For each={comments()}> + {(comment) => ( + <LineComment + id={comment.id} + top={positions()[comment.id]} + onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })} + onClick={() => { + if (isCommentOpen(comment)) { + setOpened(null) + return + } + + openComment(comment) }} + open={isCommentOpen(comment)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} /> - </Show> - )} - </Show> - </div> - </Accordion.Content> - </Accordion.Item> - ) - }} - </For> - </Accordion> + )} + </For> + + <Show when={draftRange()}> + {(range) => ( + <Show when={draftTop() !== undefined}> + <LineCommentEditor + top={draftTop()} + value={draft()} + selection={selectionLabel(range())} + onInput={setDraft} + onCancel={() => setCommenting(null)} + onSubmit={(comment) => { + props.onLineComment?.({ + file: diff.file, + selection: range(), + comment, + preview: selectionPreview(diff, range()), + }) + setCommenting(null) + }} + /> + </Show> + )} + </Show> + </div> + </Accordion.Content> + </Accordion.Item> + ) + }} + </For> + </Accordion> + </Show> </div> </div> ) |
