Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/git/CommitList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { cn } from "~/lib/utils";
/** Visible strip rows before the list scrolls (`h-10` per row → 12.5rem total). */
const STRIP_VISIBLE_ROWS = 5;

export function sortCommitsNewestFirst(commits: GitCommit[]): GitCommit[] {
return [...commits].sort(
(a, b) => b.committedAt.getTime() - a.committedAt.getTime()
);
}

export function CommitList({
commits,
selectedSha,
Expand Down
2 changes: 1 addition & 1 deletion src/components/git/DiffFileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function DiffFileTree({
paths,
gitStatus,
flattenEmptyDirectories: true,
initialExpansion: 1,
initialExpansion: "open",
icons: "standard",
density: "compact",
initialSelectedPaths: activePath ? [activePath] : [],
Expand Down
57 changes: 46 additions & 11 deletions src/components/git/GitHubMarkdownBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,42 @@ const markdownComponents: Components = {
td: ({ children }) => (
<td className="border-border/60 border px-2 py-1">{children}</td>
),
details: ({ children }) => (
<details className="bg-muted/30 my-2 rounded-md border border-border/60">
{children}
</details>
),
summary: ({ children }) => (
<summary className="cursor-pointer px-2.5 py-2 text-xs font-medium select-none">
{children}
</summary>
),
details: ({ className, children, ...props }) => {
const isAddressed = String(className ?? "").includes(
"gh-bot-finding-addressed"
);
return (
<details
{...props}
className={cn(
"my-2 rounded-md border",
isAddressed
? "border-emerald-500/40 bg-emerald-50/50 dark:bg-emerald-950/20"
: "border-border/60 bg-muted/30",
className
)}
>
{children}
</details>
);
},
summary: ({ className, children, ...props }) => {
const isAddressed = String(className ?? "").includes(
"gh-bot-finding-addressed"
);
return (
<summary
{...props}
className={cn(
"cursor-pointer px-2.5 py-2 text-xs font-medium select-none",
isAddressed && "text-emerald-800 dark:text-emerald-200",
className
)}
>
{children}
</summary>
);
},
hr: () => <hr className="border-border/60 my-3" />,
ul: ({ children }) => (
<ul className="my-1.5 list-disc space-y-0.5 pl-5">{children}</ul>
Expand Down Expand Up @@ -151,13 +177,22 @@ export function GitHubMarkdownBody({
body,
className,
collapsible = true,
collapseAddressedFindings = false,
}: {
body: string;
className?: string;
collapsible?: boolean;
/** Wrap CodeRabbit "Addressed" finding blocks in closed &lt;details&gt;. */
collapseAddressedFindings?: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const processed = useMemo(() => preprocessGitHubMarkdown(body), [body]);
const processed = useMemo(
() =>
preprocessGitHubMarkdown(body, {
collapseAddressedFindings,
}),
[body, collapseAddressedFindings]
);
const hasNestedCollapsibles = useMemo(
() => hasGitHubCollapsibleSections(processed),
[processed]
Expand Down
70 changes: 63 additions & 7 deletions src/components/git/PrReviewFeedItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { ReactNode } from "react";
import {
AlertCircle,
Check,
GitPullRequest,
MessageSquare,
MessageSquarePlus,
X,
} from "lucide-react";
import { summarizeBotReviewIssueComment } from "~/lib/git/bot-review-comment-summary";
import { Badge } from "~/components/ui/badge";
import { DiffLineLocationLink } from "~/components/git/DiffLineLocationLink";
import { GitHubMarkdownBody } from "~/components/git/GitHubMarkdownBody";
import { PrReviewFeedAttachedComments } from "~/components/git/PrReviewFeedAttachedComments";
Expand Down Expand Up @@ -198,8 +201,18 @@ export function PrReviewFeedItemIssueComment({
const body = comment.body?.trim() ?? "";
if (!body) return null;

const botSummary = summarizeBotReviewIssueComment(comment);

return (
<FeedCard>
<FeedCard
className={cn(
botSummary?.needsAction &&
"border-amber-400/70 ring-1 ring-amber-400/30",
botSummary &&
!botSummary.needsAction &&
"border-emerald-500/50 ring-1 ring-emerald-500/20"
)}
>
<FeedHeader
login={comment.authorLogin}
avatarUrl={comment.authorAvatarUrl}
Expand All @@ -209,15 +222,58 @@ export function PrReviewFeedItemIssueComment({
githubUrl={comment.url}
occurredAt={occurredAt}
trailing={
comment.isBot ? (
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px]">
Bot
</span>
) : null
<span className="flex shrink-0 flex-wrap items-center justify-end gap-1.5">
{botSummary ? (
<Badge
className={cn(
"h-5 border-0 text-[10px] font-semibold",
botSummary.needsAction
? "bg-amber-500 text-white"
: "bg-emerald-600 text-white"
)}
>
{botSummary.needsAction ? "Needs action" : "Addressed"}
</Badge>
) : null}
{comment.isBot ? (
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px]">
Bot
</span>
) : null}
</span>
}
/>
{botSummary ? (
<div
className={cn(
"mt-2 flex items-start gap-2 rounded-md border-2 px-2.5 py-2 text-xs font-medium",
botSummary.needsAction
? "border-amber-400 bg-amber-100 text-amber-950 dark:border-amber-600 dark:bg-amber-950/50 dark:text-amber-50"
: "border-emerald-500 bg-emerald-100 text-emerald-950 dark:border-emerald-600 dark:bg-emerald-950/50 dark:text-emerald-50"
)}
>
{botSummary.needsAction ? (
<AlertCircle className="mt-0.5 size-4 shrink-0" aria-hidden />
) : (
<Check className="mt-0.5 size-4 shrink-0" aria-hidden />
)}
<span>
<span className="block">{botSummary.label}</span>
{botSummary.actionableCount != null ? (
<span className="mt-0.5 block text-[10px] font-normal opacity-90">
{botSummary.addressedCount} of {botSummary.actionableCount}{" "}
marked addressed on GitHub
</span>
) : null}
</span>
</div>
) : null}
<div className="mt-2 pl-8">
<GitHubMarkdownBody body={body} collapsible={false} />
<GitHubMarkdownBody
body={body}
collapsible={false}
collapseAddressedFindings={Boolean(botSummary)}
/>
</div>
</FeedCard>
);
Expand Down
20 changes: 15 additions & 5 deletions src/components/git/TaskDevelopmentWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { PhaseBanner } from "~/components/git/PhaseBanner";
import { CommitList } from "~/components/git/CommitList";
import {
CommitList,
sortCommitsNewestFirst,
} from "~/components/git/CommitList";
import { DiffViewer } from "~/components/git/DiffViewer";
import { DiffReviewPanel } from "~/components/git/DiffReviewPanel";
import { DiffLoadingSkeleton } from "~/components/git/review/DiffLoadingSkeleton";
Expand Down Expand Up @@ -92,9 +95,16 @@ export function TaskDevelopmentWorkspace({
usePrCommitsList
);

const commits = usePrCommitsList
? (prCommitsQuery.data?.commits ?? [])
: (branchCommitsQuery.data?.commits ?? []);
const commits = useMemo(() => {
const raw = usePrCommitsList
? (prCommitsQuery.data?.commits ?? [])
: (branchCommitsQuery.data?.commits ?? []);
return sortCommitsNewestFirst(raw);
}, [
usePrCommitsList,
prCommitsQuery.data?.commits,
branchCommitsQuery.data?.commits,
]);

const prDiffQuery = useQuery({
queryKey: ["git", "diff", taskId, "pr", reviewPr?.id],
Expand Down
102 changes: 86 additions & 16 deletions src/components/git/TaskPullRequestReviewFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLayoutEffect, useRef, useState } from "react";
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import {
PrReviewFeedItemCommentThread,
Expand All @@ -16,7 +16,12 @@ import {
} from "~/db/queries/git";
import { useGitTaskLiveSync } from "~/hooks/use-git-task-live-sync";
import { formatClientError } from "~/lib/git/errors";
import {
aggregateBotReviewDigests,
feedEntryNeedsAction,
} from "~/lib/git/bot-review-comment-summary";
import { buildPrReviewFeedEntries } from "~/lib/git/pr-review-feed-entries";
import { Button } from "~/components/ui/button";
import { getLatestPr, getOpenPr } from "~/lib/git/task-dev-phase";
import { cn } from "~/lib/utils";

Expand Down Expand Up @@ -51,6 +56,7 @@ export function TaskPullRequestReviewFeed({
const [resolvingThreadNodeId, setResolvingThreadNodeId] = useState<
string | null
>(null);
const [needsActionOnly, setNeedsActionOnly] = useState(false);

const canResolve = Boolean(connectionData?.userLink) && Boolean(reviewPr);

Expand Down Expand Up @@ -81,22 +87,44 @@ export function TaskPullRequestReviewFeed({
setResolvingThreadNodeId(null);
}
};
const entries = reviewData
? buildPrReviewFeedEntries(
reviewData.reviews,
reviewData.comments,
reviewData.pendingComments,
reviewData.issueComments
)
: [];
const entries = useMemo(
() =>
reviewData
? buildPrReviewFeedEntries(
reviewData.reviews,
reviewData.comments,
reviewData.pendingComments,
reviewData.issueComments
)
: [],
[reviewData]
);

const botDigestRollup = useMemo(
() =>
reviewData
? aggregateBotReviewDigests(reviewData.issueComments)
: null,
[reviewData]
);

const visibleEntries = useMemo(
() =>
needsActionOnly
? entries.filter((entry) => feedEntryNeedsAction(entry))
: entries,
[entries, needsActionOnly]
);

const hiddenByFilterCount = entries.length - visibleEntries.length;

const scrollRef = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
const el = scrollRef.current;
if (!el || isLoading || entries.length === 0) return;
if (!el || isLoading || visibleEntries.length === 0) return;
el.scrollTop = el.scrollHeight;
}, [entries, isLoading]);
}, [visibleEntries, isLoading]);

return (
<div
Expand All @@ -105,11 +133,25 @@ export function TaskPullRequestReviewFeed({
className
)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<TaskLabel>Pull request</TaskLabel>
{isFetching && !isLoading ? (
<LoadingSpinner isActive className="size-3.5" />
) : null}
<div className="flex items-center gap-2">
{botDigestRollup &&
(botDigestRollup.digestCount > 0 || needsActionOnly) ? (
<Button
type="button"
variant={needsActionOnly ? "default" : "outline"}
size="sm"
className="h-7 text-xs"
onClick={() => setNeedsActionOnly((v) => !v)}
>
Needs action only
</Button>
) : null}
{isFetching && !isLoading ? (
<LoadingSpinner isActive className="size-3.5" />
) : null}
</div>
</div>

<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto">
Expand All @@ -130,6 +172,14 @@ export function TaskPullRequestReviewFeed({
? "No reviews or comments yet."
: "No reviews or comments yet. Connect your GitHub account in settings to see pending reviews and post comments."}
</p>
) : visibleEntries.length === 0 ? (
<p className="text-muted-foreground text-sm">
Nothing needs action right now
{hiddenByFilterCount > 0
? ` (${hiddenByFilterCount} addressed item${hiddenByFilterCount === 1 ? "" : "s"} hidden)`
: ""}
.
</p>
) : (
<>
{!connectionData.userLink ? (
Expand All @@ -138,8 +188,28 @@ export function TaskPullRequestReviewFeed({
comments.
</p>
) : null}
{botDigestRollup && botDigestRollup.digestCount > 0 ? (
<div
className={cn(
"mb-3 rounded-md border-2 px-3 py-2 text-xs font-medium",
botDigestRollup.openTotal > 0
? "border-amber-400 bg-amber-100 text-amber-950"
: "border-emerald-500 bg-emerald-100 text-emerald-950"
)}
>
{botDigestRollup.openTotal > 0
? `${botDigestRollup.openTotal} CodeRabbit finding${botDigestRollup.openTotal === 1 ? "" : "s"} still need action across ${botDigestRollup.digestCount} review comment${botDigestRollup.digestCount === 1 ? "" : "s"}`
: `All CodeRabbit findings addressed (${botDigestRollup.digestCount} review comment${botDigestRollup.digestCount === 1 ? "" : "s"})`}
</div>
) : null}
{needsActionOnly && hiddenByFilterCount > 0 ? (
<p className="text-muted-foreground mb-2 text-xs">
Hiding {hiddenByFilterCount} addressed or resolved item
{hiddenByFilterCount === 1 ? "" : "s"}.
</p>
) : null}
<ul className="space-y-2.5">
{entries.map((entry) => {
{visibleEntries.map((entry) => {
if (entry.kind === "issue_comment") {
return (
<li key={`issue-comment-${entry.comment.id}`}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/git/github-markdown-sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const githubMarkdownSanitizeSchema = {
],
attributes: {
...defaultSchema.attributes,
details: [...(defaultSchema.attributes?.details ?? []), "open"],
summary: defaultSchema.attributes?.summary ?? [],
details: [...(defaultSchema.attributes?.details ?? []), "open", "class"],
summary: [...(defaultSchema.attributes?.summary ?? []), "class"],
},
} as const;
Loading