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
488 changes: 488 additions & 0 deletions apps/desktop/plans/20260413-1600-v2-review-tab.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { Search } from "lucide-react";
import { useMemo, useState } from "react";
import { useGitStatus } from "renderer/hooks/host-service/useGitStatus";
import type { CommentPaneData } from "../../types";
import { FilesTab } from "./components/FilesTab";
import { SidebarHeader } from "./components/SidebarHeader";
import { useChangesTab } from "./hooks/useChangesTab";
import { useReviewTab } from "./hooks/useReviewTab";
import type { SidebarTabDefinition } from "./types";

interface WorkspaceSidebarProps {
onSelectFile: (absolutePath: string, openInNewTab?: boolean) => void;
onSelectDiffFile?: (path: string) => void;
onOpenComment?: (comment: CommentPaneData) => void;
onSearch?: () => void;
selectedFilePath?: string;
workspaceId: string;
Expand Down Expand Up @@ -46,6 +49,7 @@ function IconButton({
export function WorkspaceSidebar({
onSelectFile,
onSelectDiffFile,
onOpenComment,
onSearch,
selectedFilePath,
workspaceId,
Expand All @@ -61,6 +65,8 @@ export function WorkspaceSidebar({
onSelectFile: onSelectDiffFile,
});

const reviewTab = useReviewTab({ workspaceId, onOpenComment });

const filesTab: SidebarTabDefinition = useMemo(
() => ({
id: "files",
Expand All @@ -86,20 +92,7 @@ export function WorkspaceSidebar({
],
);

const checksTab: SidebarTabDefinition = useMemo(
() => ({
id: "checks",
label: "Checks",
content: (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Coming soon
</div>
),
}),
[],
);

const tabs = [filesTab, changesTab, checksTab];
const tabs = [filesTab, changesTab, reviewTab];
const activeTabDef = tabs.find((t) => t.id === activeTab);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function SidebarHeader({
)}
>
{tab.label}
{tab.badge != null && tab.badge > 0 && (
{tab.badge != null && (
<span className="text-xs tabular-nums">{tab.badge}</span>
)}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@superset/ui/collapsible";
import { cn } from "@superset/ui/utils";
import { useMemo, useState } from "react";
import {
LuArrowUpRight,
LuCheck,
LuLoaderCircle,
LuMinus,
LuX,
} from "react-icons/lu";
import { VscChevronRight } from "react-icons/vsc";
import type { NormalizedCheck, NormalizedPR } from "../../types";

const checkIconConfig = {
success: {
icon: LuCheck,
className: "text-emerald-600 dark:text-emerald-400",
},
failure: { icon: LuX, className: "text-red-600 dark:text-red-400" },
pending: {
icon: LuLoaderCircle,
className: "text-amber-600 dark:text-amber-400",
},
skipped: { icon: LuMinus, className: "text-muted-foreground" },
cancelled: { icon: LuMinus, className: "text-muted-foreground" },
} as const;

const checkSummaryIconConfig = {
success: checkIconConfig.success,
failure: checkIconConfig.failure,
pending: checkIconConfig.pending,
none: { icon: LuMinus, className: "text-muted-foreground" },
} as const;

interface ChecksSectionProps {
checks: NormalizedCheck[];
checksStatus: NormalizedPR["checksStatus"];
prUrl: string;
}

export function ChecksSection({
checks,
checksStatus,
prUrl,
}: ChecksSectionProps) {
const [open, setOpen] = useState(true);

const relevantChecks = useMemo(
() =>
checks.filter(
(check) => check.status !== "skipped" && check.status !== "cancelled",
),
[checks],
);

const passingChecks = relevantChecks.filter(
(check) => check.status === "success",
).length;
const checksSummary =
relevantChecks.length > 0
? `${passingChecks}/${relevantChecks.length} checks passing`
: "No checks reported";
const checksStatusConfig = checkSummaryIconConfig[checksStatus];
const ChecksStatusIcon = checksStatusConfig.icon;

return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger
className={cn(
"flex w-full min-w-0 items-center justify-between gap-2 px-2 py-1.5 text-left",
"cursor-pointer transition-colors hover:bg-accent/30",
)}
>
<div className="flex min-w-0 items-center gap-1.5">
<VscChevronRight
className={cn(
"size-3 shrink-0 text-muted-foreground transition-transform duration-150",
open && "rotate-90",
)}
/>
<span className="truncate text-xs font-medium">Checks</span>
<span className="shrink-0 text-[10px] text-muted-foreground">
{relevantChecks.length}
</span>
</div>
<div
className={cn(
"flex shrink-0 items-center gap-1",
checksStatusConfig.className,
)}
>
<ChecksStatusIcon
className={cn(
"size-3.5 shrink-0",
checksStatus === "pending" && "animate-spin",
)}
/>
<span className="max-w-[140px] truncate text-[10px] normal-case">
{checksSummary}
</span>
</div>
</CollapsibleTrigger>
<CollapsibleContent className="min-w-0 overflow-hidden px-0.5 pb-1">
{relevantChecks.length === 0 ? (
<div className="px-1.5 py-1 text-xs text-muted-foreground">
No checks reported.
</div>
) : (
relevantChecks.map((check, index) => (
<CheckRow
key={`${check.name}-${index}`}
check={check}
prUrl={prUrl}
/>
))
)}
</CollapsibleContent>
</Collapsible>
);
}

function resolveCheckUrl(
check: NormalizedCheck,
prUrl: string,
): string | undefined {
if (check.url) return check.url;
const name = check.name.trim().toLowerCase();
if (name.includes("coderabbit") || name.includes("code rabbit")) return prUrl;
return undefined;
}

function CheckRow({ check, prUrl }: { check: NormalizedCheck; prUrl: string }) {
const { icon: CheckIcon, className } = checkIconConfig[check.status];
const checkUrl = resolveCheckUrl(check, prUrl);

const inner = (
<div className="flex min-w-0 items-center gap-1 rounded-sm px-1.5 py-1 text-xs transition-colors hover:bg-accent/50">
<CheckIcon
className={cn(
"size-3 shrink-0",
className,
check.status === "pending" && "animate-spin",
)}
/>
<div className="flex min-w-0 flex-1 items-center gap-1">
<span className="min-w-0 truncate">{check.name}</span>
{checkUrl && (
<LuArrowUpRight className="size-3.5 shrink-0 text-muted-foreground/70 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
)}
</div>
{check.durationText && (
<span className="shrink-0 text-[10px] text-muted-foreground">
{check.durationText}
</span>
)}
</div>
);

return checkUrl ? (
<a
href={checkUrl}
target="_blank"
rel="noopener noreferrer"
className="group block"
>
{inner}
</a>
) : (
inner
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChecksSection } from "./ChecksSection";
Loading
Loading