2026-06-11 16:11:10 -07:00
|
|
|
import { createContext, useContext, useEffect, useMemo, useState } from "react"
|
|
|
|
|
import {
|
|
|
|
|
FileTree,
|
|
|
|
|
useFileTree,
|
|
|
|
|
useFileTreeSelection,
|
|
|
|
|
} from "@pierre/trees/react"
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
import {
|
|
|
|
|
CaretRightIcon,
|
|
|
|
|
ListBulletsIcon,
|
|
|
|
|
TreeViewIcon,
|
|
|
|
|
} from "@phosphor-icons/react"
|
|
|
|
|
import type { ReactNode } from "react"
|
2026-06-11 16:11:10 -07:00
|
|
|
|
2026-06-16 10:54:30 -07:00
|
|
|
import type {
|
|
|
|
|
FileTreeDirectoryHandle,
|
|
|
|
|
GitStatus,
|
|
|
|
|
GitStatusEntry,
|
|
|
|
|
} from "@pierre/trees"
|
2026-06-11 16:11:10 -07:00
|
|
|
import type { ReviewDiffFile } from "@/lib/api"
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
import { Markdown } from "@/components/agents/ported"
|
2026-06-11 16:11:10 -07:00
|
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
2026-06-16 10:54:30 -07:00
|
|
|
import { TREE_UNSAFE_CSS, treeThemeStyle } from "@/components/agents/AgentGitPanel"
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
import { cn } from "@/lib/utils"
|
2026-06-11 16:11:10 -07:00
|
|
|
|
2026-06-12 11:03:51 -07:00
|
|
|
function reviewFileGitStatus(status: ReviewDiffFile["status"]): GitStatus {
|
|
|
|
|
if (status === "removed") return "deleted"
|
|
|
|
|
if (status === "added") return "added"
|
|
|
|
|
if (status === "renamed") return "renamed"
|
|
|
|
|
return "modified"
|
|
|
|
|
}
|
|
|
|
|
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
export type ReviewSidebarView = "ai" | "files"
|
|
|
|
|
|
|
|
|
|
export interface ReviewSidebarGroup {
|
|
|
|
|
index: number
|
|
|
|
|
title: string
|
|
|
|
|
summary: string
|
|
|
|
|
additions: number
|
|
|
|
|
deletions: number
|
|
|
|
|
fileCount: number
|
|
|
|
|
files: Array<string>
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:11:10 -07:00
|
|
|
export interface ReviewSidebarData {
|
|
|
|
|
title: string
|
|
|
|
|
files: Array<ReviewDiffFile> | null
|
|
|
|
|
selected: string | null
|
|
|
|
|
viewed: Set<string>
|
|
|
|
|
onSelect: (path: string) => void
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
groups: Array<ReviewSidebarGroup> | null
|
|
|
|
|
view: ReviewSidebarView
|
|
|
|
|
onViewChange: (view: ReviewSidebarView) => void
|
|
|
|
|
onSelectGroup: (index: number) => void
|
|
|
|
|
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
2026-06-11 16:11:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
}
|
|
|
|
|
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
export function ReviewSidebarPanel({ data }: { data: ReviewSidebarData }) {
|
|
|
|
|
const hasGroups = data.groups !== null && data.groups.length > 0
|
|
|
|
|
const showAi = data.view === "ai" && hasGroups
|
|
|
|
|
|
2026-06-11 16:11:10 -07:00
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col pb-2">
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
<div className="flex items-center justify-between gap-2 px-4 py-1">
|
2026-06-16 14:56:11 -07:00
|
|
|
<span className="text-[10px] font-medium tracking-wide text-[var(--ui-text-dim)] uppercase">
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
{data.title}
|
|
|
|
|
</span>
|
|
|
|
|
{hasGroups && (
|
|
|
|
|
<ReviewViewToggle view={data.view} onChange={data.onViewChange} />
|
|
|
|
|
)}
|
2026-06-11 16:11:10 -07:00
|
|
|
</div>
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
{showAi ? (
|
|
|
|
|
<ReviewGroupList
|
|
|
|
|
groups={data.groups ?? []}
|
|
|
|
|
onSelectGroup={data.onSelectGroup}
|
|
|
|
|
onSelectFile={data.onSelect}
|
|
|
|
|
onLocationClick={data.onLocationClick}
|
|
|
|
|
/>
|
|
|
|
|
) : !data.files ? (
|
2026-06-11 16:11:10 -07:00
|
|
|
<div className="px-4 pt-1">
|
|
|
|
|
<Skeleton className="h-40 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<ReviewFileTreeExplorer
|
|
|
|
|
files={data.files}
|
|
|
|
|
selected={data.selected}
|
|
|
|
|
onSelect={data.onSelect}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
feat: AI-sorted PR review view with diff grouping (#1544)
* feat: AI-sorted PR review view with diff grouping
Group a PR's changed files into a logical top-to-bottom walkthrough via a best-effort structured-output LLM pass, kicked off concurrently with the reviewer run (~0 added latency) and persisted on reviewer thread metadata. Results render in the review UI behind an AI sorted / file tree toggle that persists across PRs; the view falls back to the file tree when groups are absent or stale.
Adds a grouping-model team default (inherits the Reviewer subagent model when unset) and the admin RolePicker for it.
* feat(reviews): richer AI-sorted explanations + sidebar polish
Sidebar group rows get Devin-style spacing (dividers, padding), a per-group file list (click a file to jump to its diff), inline-code chips in titles, and an accent Read explanation link.
Group explanations are now rich markdown: the grouping prompt feeds per-hunk line ranges and asks for inline code, a short code block, and [path:line](#loc=...) references. The Markdown renderer turns those #loc= links into in-page buttons that scroll the diff to the hunk and highlight the range, reusing the existing selectedLines path.
* fix(reviews): drop stale diff groups from the AI-sorted view
When groups were generated for a previous head, a persisted "ai" view in localStorage still rendered the outdated walkthrough. groupedView now returns null on diff_groups_stale, so the file-tree fallback is used and the view toggle hides until fresh groups arrive.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 13:59:37 -07:00
|
|
|
function ReviewViewToggle({
|
|
|
|
|
view,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
view: ReviewSidebarView
|
|
|
|
|
onChange: (view: ReviewSidebarView) => void
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-0.5 rounded-md border border-[var(--ui-border)] p-0.5">
|
|
|
|
|
<ReviewViewToggleButton
|
|
|
|
|
active={view === "ai"}
|
|
|
|
|
label="AI sorted"
|
|
|
|
|
onClick={() => onChange("ai")}
|
|
|
|
|
>
|
|
|
|
|
<ListBulletsIcon className="size-3.5" />
|
|
|
|
|
</ReviewViewToggleButton>
|
|
|
|
|
<ReviewViewToggleButton
|
|
|
|
|
active={view === "files"}
|
|
|
|
|
label="File tree"
|
|
|
|
|
onClick={() => onChange("files")}
|
|
|
|
|
>
|
|
|
|
|
<TreeViewIcon className="size-3.5" />
|
|
|
|
|
</ReviewViewToggleButton>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReviewViewToggleButton({
|
|
|
|
|
active,
|
|
|
|
|
label,
|
|
|
|
|
onClick,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
active: boolean
|
|
|
|
|
label: string
|
|
|
|
|
onClick: () => void
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
aria-label={label}
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
title={label}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex size-5 items-center justify-center rounded text-[var(--ui-text-dim)] transition-colors",
|
|
|
|
|
active
|
|
|
|
|
? "bg-[var(--ui-sidebar-hover)] text-[var(--ui-text)]"
|
|
|
|
|
: "hover:text-[var(--ui-text)]"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReviewGroupList({
|
|
|
|
|
groups,
|
|
|
|
|
onSelectGroup,
|
|
|
|
|
onSelectFile,
|
|
|
|
|
onLocationClick,
|
|
|
|
|
}: {
|
|
|
|
|
groups: Array<ReviewSidebarGroup>
|
|
|
|
|
onSelectGroup: (index: number) => void
|
|
|
|
|
onSelectFile: (path: string) => void
|
|
|
|
|
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-0 flex-1 divide-y divide-[var(--ui-border-subtle)] overflow-y-auto">
|
|
|
|
|
{groups.map((group) => (
|
|
|
|
|
<ReviewGroupRow
|
|
|
|
|
key={group.index}
|
|
|
|
|
group={group}
|
|
|
|
|
onSelect={() => onSelectGroup(group.index)}
|
|
|
|
|
onSelectFile={onSelectFile}
|
|
|
|
|
onLocationClick={onLocationClick}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function splitPath(path: string): { dir: string; base: string } {
|
|
|
|
|
const idx = path.lastIndexOf("/")
|
|
|
|
|
if (idx === -1) return { dir: "", base: path }
|
|
|
|
|
return { dir: path.slice(0, idx), base: path.slice(idx + 1) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render a title with `backtick`-delimited spans as inline code chips, matching
|
|
|
|
|
// the Markdown component's inline-code styling, without pulling in the full
|
|
|
|
|
// block renderer for a single line.
|
|
|
|
|
function renderInlineCode(text: string): Array<ReactNode> {
|
|
|
|
|
return text.split(/(`[^`]+`)/g).map((part, i) => {
|
|
|
|
|
if (part.length >= 2 && part.startsWith("`") && part.endsWith("`")) {
|
|
|
|
|
return (
|
|
|
|
|
<code
|
|
|
|
|
key={i}
|
|
|
|
|
className="rounded bg-[var(--ui-panel-2)] px-1 py-0.5 font-mono text-[0.9em] text-[var(--ui-accent)]"
|
|
|
|
|
>
|
|
|
|
|
{part.slice(1, -1)}
|
|
|
|
|
</code>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return <span key={i}>{part}</span>
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ReviewGroupRow({
|
|
|
|
|
group,
|
|
|
|
|
onSelect,
|
|
|
|
|
onSelectFile,
|
|
|
|
|
onLocationClick,
|
|
|
|
|
}: {
|
|
|
|
|
group: ReviewSidebarGroup
|
|
|
|
|
onSelect: () => void
|
|
|
|
|
onSelectFile: (path: string) => void
|
|
|
|
|
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [expanded, setExpanded] = useState(false)
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-3 py-3 transition-colors hover:bg-[var(--ui-sidebar-hover)]">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onSelect}
|
|
|
|
|
className="flex w-full items-start gap-2 text-left"
|
|
|
|
|
>
|
|
|
|
|
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded bg-[var(--ui-panel-2)] text-[11px] font-medium text-[var(--ui-text-dim)]">
|
|
|
|
|
{group.index}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="min-w-0 flex-1">
|
|
|
|
|
<span className="block text-xs leading-5 font-medium text-[var(--ui-text)]">
|
|
|
|
|
{renderInlineCode(group.title)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-[var(--ui-text-dim)]">
|
|
|
|
|
<span>
|
|
|
|
|
{group.fileCount} file{group.fileCount === 1 ? "" : "s"}
|
|
|
|
|
</span>
|
|
|
|
|
{group.additions > 0 && (
|
|
|
|
|
<span className="text-emerald-500">+{group.additions}</span>
|
|
|
|
|
)}
|
|
|
|
|
{group.deletions > 0 && (
|
|
|
|
|
<span className="text-red-500">-{group.deletions}</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{group.files.length > 0 && (
|
|
|
|
|
<div className="mt-2 space-y-0.5 pl-7">
|
|
|
|
|
{group.files.map((path) => {
|
|
|
|
|
const { dir, base } = splitPath(path)
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={path}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onSelectFile(path)}
|
|
|
|
|
title={path}
|
|
|
|
|
className="flex w-full items-baseline gap-1.5 text-left text-[11px] hover:text-[var(--ui-accent)]"
|
|
|
|
|
>
|
|
|
|
|
<span className="shrink-0 font-medium text-[var(--ui-text-muted)]">
|
|
|
|
|
{base}
|
|
|
|
|
</span>
|
|
|
|
|
{dir && (
|
|
|
|
|
<span className="min-w-0 truncate text-[var(--ui-text-dim)]">
|
|
|
|
|
{dir}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{group.summary && (
|
|
|
|
|
<div className="mt-2 pl-7">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setExpanded((value) => !value)}
|
|
|
|
|
className="inline-flex items-center gap-1 text-[11px] font-medium text-[var(--ui-accent)]"
|
|
|
|
|
>
|
|
|
|
|
<CaretRightIcon
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 transition-transform",
|
|
|
|
|
expanded && "rotate-90"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
Read explanation
|
|
|
|
|
</button>
|
|
|
|
|
{expanded && (
|
|
|
|
|
<div className="mt-1.5">
|
|
|
|
|
<Markdown
|
|
|
|
|
content={group.summary}
|
|
|
|
|
onLocationClick={onLocationClick}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:11:10 -07:00
|
|
|
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>>(
|
2026-06-12 11:03:51 -07:00
|
|
|
() =>
|
|
|
|
|
files.map((file) => ({
|
|
|
|
|
path: file.path,
|
|
|
|
|
status: reviewFileGitStatus(file.status),
|
|
|
|
|
})),
|
2026-06-11 16:11:10 -07:00
|
|
|
[files]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const { model } = useFileTree({
|
|
|
|
|
paths,
|
|
|
|
|
gitStatus,
|
|
|
|
|
flattenEmptyDirectories: true,
|
2026-06-16 10:54:30 -07:00
|
|
|
density: "default",
|
|
|
|
|
icons: "complete",
|
|
|
|
|
unsafeCSS: TREE_UNSAFE_CSS,
|
2026-06-11 16:11:10 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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(() => {
|
2026-06-16 10:54:30 -07:00
|
|
|
if (!selected) return
|
|
|
|
|
const segments = selected.split("/")
|
|
|
|
|
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
|
|
|
const item = model.getItem(segments.slice(0, depth).join("/"))
|
|
|
|
|
if (item?.isDirectory()) (item as FileTreeDirectoryHandle).expand()
|
2026-06-11 16:11:10 -07:00
|
|
|
}
|
2026-06-16 10:54:30 -07:00
|
|
|
model.scrollToPath(selected, { focus: false })
|
2026-06-11 16:11:10 -07:00
|
|
|
}, [model, selected])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-0 flex-1">
|
|
|
|
|
<FileTree
|
|
|
|
|
model={model}
|
|
|
|
|
style={
|
|
|
|
|
{
|
|
|
|
|
height: "100%",
|
|
|
|
|
...treeThemeStyle(),
|
fix: Reviews tab — anchored finding card, paginated list, file tree truncation (#1507)
* fix: Reviews tab — anchored finding card, paginated list, file tree truncation
- Finding card now tracks the diff anchor while scrolling instead of staying
frozen in the viewport; auto-hides when its diff card collapses (including
collapse via mark-as-viewed) and on click outside
- Checks section capped with max height + scroll
- /reviews paginated (page size 20) with has_more, filtered to the current
user's PRs by default with an All toggle; PR author login now stored in
reviewer thread metadata
- File tree truncation marker overlapped filenames because the sidebar bg
was transparent; use the opaque sidebar color
* fix: finding card tracks anchor 1:1 while scrolling
Drop the vertical viewport clamp — it pinned the card at the clamp
boundary while the highlighted lines kept scrolling, breaking the
attachment.
* fix: anchor finding card with Base UI popover
Replace manual fixed-position tracking (laggy: setState per scroll
frame) with a Popover anchored to the finding's diff row. Floating UI
tracks the anchor outside React renders, so the card moves 1:1 with
the content and scrolls out of view with it. Unanchored findings keep
the fixed top-right card.
* fix: lock finding card to diff scroll
Replace the Base UI popover (async repositioning, paints a frame behind
native scroll) with a card absolutely positioned inside the scroll
container, so it scrolls with the diff in the same compositor frame.
Scroll moves to the ReviewBody root, side panel becomes sticky.
Position recomputes only on layout shifts via ResizeObserver.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-11 17:52:20 -07:00
|
|
|
// Must stay opaque: the tree's truncation marker ("…") paints
|
|
|
|
|
// this color behind itself to hide the overflowing filename.
|
|
|
|
|
"--trees-theme-sidebar-bg": "var(--ui-sidebar)",
|
2026-06-11 16:11:10 -07:00
|
|
|
} as React.CSSProperties
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|