Skip to content
3 changes: 3 additions & 0 deletions packages/web/src/components/chapter/chapter-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function ChapterNavigator({
<Link
to="/runs/$runId/chapters/$chapterNumber"
params={{ runId, chapterNumber: String(prevChapter.order) }}
resetScroll={false}
className="inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<ChevronLeft className="size-4" />
Expand Down Expand Up @@ -105,6 +106,7 @@ export function ChapterNavigator({
<Link
to="/runs/$runId/chapters/$chapterNumber"
params={{ runId, chapterNumber: String(ch.order) }}
resetScroll={false}
className={cn("cursor-pointer", isActive && "bg-accent")}
>
<StatusBadge
Expand Down Expand Up @@ -146,6 +148,7 @@ export function ChapterNavigator({
<Link
to="/runs/$runId/chapters/$chapterNumber"
params={{ runId, chapterNumber: String(nextChapter.order) }}
resetScroll={false}
className="inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<ChevronRight className="size-4" />
Expand Down
51 changes: 29 additions & 22 deletions packages/web/src/components/chapter/file-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import {
} from "lucide-react";
import type { MouseEvent } from "react";
import { useCallback } from "react";
import { ShortcutLabel } from "@/components/keyboard/shortcut-label";
import { LineCounts } from "@/components/shared/line-counts";
import { ShortcutTooltip } from "@/components/shared/shortcut-tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { FILE_STATUS, type PullRequestFile } from "@/lib/diff-types";
import { FILE_STATUS_ICONS, FILE_STATUS_LABELS, FILE_STATUS_TEXT_COLORS } from "@/lib/file-status";
import { SHORTCUT_KEY } from "@/lib/keyboard-shortcuts";
import { useIsMac } from "@/lib/use-is-mac";
import { useShortcut } from "@/lib/use-shortcut";
import { cn } from "@/lib/utils";

function CopyableFilename({
Expand Down Expand Up @@ -71,6 +75,7 @@ export function FileHeader({
}: FileHeaderProps) {
const isMac = useIsMac();
const altLabel = isMac ? "⌥" : "Alt";
const { label: collapseShortcutLabel } = useShortcut(SHORTCUT_KEY.TOGGLE_FILE_COLLAPSED);

const copyPath = useCallback(
(path: string, label: string) => {
Expand Down Expand Up @@ -146,7 +151,10 @@ export function FileHeader({
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-center">
<p>{isCollapsed ? "Expand file" : "Collapse file"}</p>
<p className="flex items-center justify-center gap-1">
{isCollapsed ? "Expand file" : "Collapse file"}
<ShortcutLabel label={collapseShortcutLabel} />
</p>
<p className="text-muted-foreground">
{altLabel}-click to {isCollapsed ? "expand" : "collapse"} all files
</p>
Expand Down Expand Up @@ -213,27 +221,26 @@ export function FileHeader({
className="relative z-10 shrink-0"
/>
{onToggleViewed && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onToggleViewed();
}}
className={cn(
"relative z-10 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-accent",
isViewed
? "text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400"
: "text-muted-foreground hover:text-foreground",
)}
aria-label={isViewed ? "Mark file as unviewed" : "Mark file as viewed"}
>
{isViewed ? <CircleCheck className="size-3.5" /> : <Circle className="size-3.5" />}
</button>
</TooltipTrigger>
<TooltipContent>{isViewed ? "Mark as unviewed" : "Mark as viewed"}</TooltipContent>
</Tooltip>
<ShortcutTooltip
shortcutKey={SHORTCUT_KEY.MARK_FILE_AS_VIEWED}
label={isViewed ? "Mark as unviewed" : "Mark as viewed"}
>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onToggleViewed();
}}
className={cn(
"relative z-10 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-accent",
isViewed
? "text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400"
: "text-muted-foreground hover:text-foreground",
)}
>
{isViewed ? <CircleCheck className="size-3.5" /> : <Circle className="size-3.5" />}
</button>
</ShortcutTooltip>
)}
{handleCommentClick && (
<Tooltip>
Expand Down
22 changes: 20 additions & 2 deletions packages/web/src/components/files/file-diff-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PierreDiffViewer } from "@/components/chapter/pierre-diff-viewer";
import { findRenderedDiffLine } from "@/components/chapter/rendered-line-target";
import type { AnnotatedLineRef, DiffSide, LineRef } from "@/lib/diff-types";
import type { FileDiffEntry } from "@/lib/parse-diff";
import { cn } from "@/lib/utils";

export interface FileDiffListHandle {
scrollToFile: (filePath: string) => void;
Expand Down Expand Up @@ -41,14 +42,24 @@ interface FileDiffListProps {
onToggleViewed?: (path: string) => void;
collapseState: CollapseState;
chapterOverlay?: ChapterOverlayProps;
/** The keyboard-focused file, outlined to mark it as the active diff. */
focusedFilePath?: string;
}

const FILE_TOP_PADDING = 16;
const SCROLL_TO_LINE_POLL_MS = 100;
const SCROLL_TO_LINE_TIMEOUT_MS = 3000;

export const FileDiffList = forwardRef<FileDiffListHandle, FileDiffListProps>(function FileDiffList(
{ entries, emptyMessage, viewedPathSet, onToggleViewed, collapseState, chapterOverlay },
{
entries,
emptyMessage,
viewedPathSet,
onToggleViewed,
collapseState,
chapterOverlay,
focusedFilePath,
},
ref,
) {
const scrollRequestRef = useRef(0);
Expand Down Expand Up @@ -197,6 +208,7 @@ export const FileDiffList = forwardRef<FileDiffListHandle, FileDiffListProps>(fu
key={entry.file.path}
entry={entry}
isViewed={viewedPathSet?.has(entry.file.path) ?? false}
isFocused={entry.file.path === focusedFilePath}
onToggleViewed={onToggleViewed}
collapseState={collapseState}
chapterOverlay={chapterOverlay}
Expand All @@ -209,6 +221,7 @@ export const FileDiffList = forwardRef<FileDiffListHandle, FileDiffListProps>(fu
interface FileDiffSectionProps {
entry: FileDiffEntry;
isViewed: boolean;
isFocused: boolean;
onToggleViewed?: (path: string) => void;
collapseState: CollapseState;
chapterOverlay?: ChapterOverlayProps;
Expand All @@ -217,6 +230,7 @@ interface FileDiffSectionProps {
function FileDiffSection({
entry,
isViewed,
isFocused,
onToggleViewed,
collapseState,
chapterOverlay,
Expand All @@ -239,7 +253,11 @@ function FileDiffSection({
}, [onToggleViewed, file.path]);

return (
<div id={`file-${file.path}`}>
<div
id={`file-${file.path}`}
data-focused-file={isFocused ? "true" : undefined}
className={cn("rounded-lg", isFocused && "outline-2 outline-primary/70")}
>
<FileHeader
file={file}
isCollapsed={isCollapsed}
Expand Down
Loading