"use client"; import { memo, useMemo, useState, useEffect, useRef, useCallback } from "react"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@kandev/ui/drawer"; import { Button } from "@kandev/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@kandev/ui/tabs"; import { TaskChangesPanel } from "../task-changes-panel"; import { CommitDiffView } from "../commit-detail-panel"; import type { ReviewSource, SourceCounts } from "@/hooks/domains/session/use-review-sources"; import type { SelectedDiff } from "../task-layout"; import type { DiffSheetMode } from "../changes-diff-target"; const MOBILE_DIFF_SOURCE_FILTER_KEY = "mobile-diff-source-filter"; type MobileDiffSheetProps = { mode: DiffSheetMode | null; onClose: () => void; onOpenFile?: (filePath: string) => void; selectedDiff: SelectedDiff | null; onClearSelected: () => void; sourceCounts: SourceCounts; }; function pickFirstNonEmpty(sourceCounts: SourceCounts): ReviewSource | null { if (sourceCounts.uncommitted > 0) return "uncommitted"; if (sourceCounts.committed > 0) return "committed"; if (sourceCounts.pr > 0) return "pr"; return null; } function useAutoSelectSource( modeKind: string | undefined, sourceCounts: SourceCounts, activeSource: ReviewSource, setActiveSource: (s: ReviewSource) => void, ) { const userPickedRef = useRef(false); const prevModeKindRef = useRef(undefined); // Reset the "user picked" flag whenever the sheet closes/reopens. useEffect(() => { if (modeKind !== "all" && prevModeKindRef.current === "all") { userPickedRef.current = false; } prevModeKindRef.current = modeKind; }, [modeKind]); // Auto-select highest-priority non-empty source when the active source is // empty. Stops once the user makes an explicit choice. Does not override a // non-empty active source — so switching tabs then seeing uncommitted files // arrive does not forcibly reset the user's view. useEffect(() => { if (modeKind !== "all") return; if (userPickedRef.current) return; const pick = pickFirstNonEmpty(sourceCounts); if (pick === null) return; if (sourceCounts[activeSource] > 0) return; if (pick === activeSource) return; setActiveSource(pick); }, [modeKind, sourceCounts, activeSource, setActiveSource]); const handleUserPick = useCallback( (s: ReviewSource) => { userPickedRef.current = true; setActiveSource(s); }, [setActiveSource], ); return { handleUserPick }; } type SourceTab = { key: ReviewSource; label: string; count: number }; function buildSourceTabs(sourceCounts: SourceCounts): SourceTab[] { return [ { key: "uncommitted" as ReviewSource, label: "Uncommitted", count: sourceCounts.uncommitted }, { key: "committed" as ReviewSource, label: "Committed", count: sourceCounts.committed }, { key: "pr" as ReviewSource, label: "PR", count: sourceCounts.pr }, ].filter((t) => t.count > 0); } function deriveTitle(mode: DiffSheetMode | null, sourceTabs: SourceTab[]): string { if (!mode) return ""; if (mode.kind === "all") { if (sourceTabs.length === 1) return `${sourceTabs[0].label} Changes`; return "All Changes"; } if (mode.kind === "file") return "File Changes"; if (mode.kind === "commit") return "Commit Changes"; return ""; } function SheetHeader({ title, showSourceChip, sourceLabel, onClose, }: { title: string; showSourceChip: boolean; sourceLabel: string | null; onClose: () => void; }) { return ( {title} {showSourceChip && sourceLabel && ( {sourceLabel} )} ); } function SourceTabBar({ tabs, activeSource, onPick, }: { tabs: SourceTab[]; activeSource: ReviewSource; onPick: (s: ReviewSource) => void; }) { return (
onPick(v as ReviewSource)}> {tabs.map((tab) => ( {tab.label} ({tab.count}) ))}
); } function renderPanel( mode: DiffSheetMode | null, activeSource: ReviewSource, selectedDiff: SelectedDiff | null, onClearSelected: () => void, onOpenFile?: (filePath: string) => void, ): React.ReactNode { if (!mode) return null; if (mode.kind === "commit") { return ; } const panelMode = mode.kind; const filePath = mode.kind === "file" ? mode.path : undefined; const fileRepositoryName = mode.kind === "file" ? mode.repositoryName : undefined; const effectiveSourceFilter = mode.kind === "all" ? activeSource : (mode.sourceFilter ?? "all"); return ( ); } /** * Full-screen mobile diff viewer sheet. Shows either merged diffs (mode=all), * single-file diffs (mode=file), or commit diffs (mode=commit). * Uses Drawer with full-screen height to give diff viewer maximum space. */ export const MobileDiffSheet = memo(function MobileDiffSheet({ mode, onClose, onOpenFile, selectedDiff, onClearSelected, sourceCounts, }: MobileDiffSheetProps) { const [activeSource, setActiveSource] = useState(() => { if (typeof window === "undefined") return "uncommitted"; const saved = localStorage.getItem(MOBILE_DIFF_SOURCE_FILTER_KEY); if (saved === "uncommitted" || saved === "committed" || saved === "pr") return saved; return "uncommitted"; }); const { handleUserPick } = useAutoSelectSource( mode?.kind, sourceCounts, activeSource, setActiveSource, ); useEffect(() => { if (mode?.kind !== "all") return; localStorage.setItem(MOBILE_DIFF_SOURCE_FILTER_KEY, activeSource); }, [mode?.kind, activeSource]); const sourceTabs = useMemo(() => buildSourceTabs(sourceCounts), [sourceCounts]); const activeSourceLabel = useMemo( () => sourceTabs.find((t) => t.key === activeSource)?.label ?? null, [sourceTabs, activeSource], ); const title = useMemo(() => deriveTitle(mode, sourceTabs), [mode, sourceTabs]); const panelContent = useMemo( () => renderPanel(mode, activeSource, selectedDiff, onClearSelected, onOpenFile), [mode, activeSource, selectedDiff, onClearSelected, onOpenFile], ); return ( (!open ? onClose() : undefined)}> {mode?.kind === "all" && sourceTabs.length > 1 && ( )}
{panelContent}
); });