"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { Archive, ArchiveRestore, ChevronRight, Loader2 } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu" import { ScrollArea } from "@/components/ui/scroll-area" import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable" import { FileTree, FileTreeFile, FileTreeFolder, } from "@/components/ai-elements/file-tree" import { DiffViewer } from "@/components/diff/diff-viewer" import { gitStashList, gitStashShow, gitStashApply, gitStashDrop, gitShowFile, } from "@/lib/tauri" import { toErrorMessage } from "@/lib/app-error" import { languageFromPath } from "@/lib/language-detect" import type { GitStashEntry, GitStatusEntry } from "@/lib/types" import { cn } from "@/lib/utils" // --- File tree types & builder (same pattern as commit-dialog) --- interface TreeFileNode { kind: "file" name: string path: string entry: GitStatusEntry } interface TreeDirNode { kind: "dir" name: string path: string children: TreeNode[] } type TreeNode = TreeFileNode | TreeDirNode function buildFileTree(entries: GitStatusEntry[]): TreeNode[] { type BuildDir = { name: string path: string dirs: Map files: TreeFileNode[] } const root: BuildDir = { name: "", path: "", dirs: new Map(), files: [], } for (const entry of entries) { const parts = entry.file.split("/").filter(Boolean) if (parts.length === 0) continue let current = root let currentPath = "" for (let i = 0; i < parts.length; i += 1) { const part = parts[i] const isLeaf = i === parts.length - 1 currentPath = currentPath ? `${currentPath}/${part}` : part if (isLeaf) { current.files.push({ kind: "file", name: part, path: currentPath, entry, }) } else { const found = current.dirs.get(part) if (found) { current = found } else { const next: BuildDir = { name: part, path: currentPath, dirs: new Map(), files: [], } current.dirs.set(part, next) current = next } } } } function sortNodes(nodes: TreeNode[]) { return nodes.sort((a, b) => { if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1 return a.name.localeCompare(b.name) }) } function toNodes(dir: BuildDir): TreeNode[] { const dirs: TreeNode[] = Array.from(dir.dirs.values()).map((child) => ({ kind: "dir", name: child.name, path: child.path, children: toNodes(child), })) return sortNodes([...dirs, ...dir.files]) } return toNodes(root) } function collectDirPaths(entries: GitStatusEntry[]) { const paths = new Set() for (const entry of entries) { const parts = entry.file.split("/").filter(Boolean) if (parts.length < 2) continue let p = "" for (let i = 0; i < parts.length - 1; i += 1) { p = p ? `${p}/${parts[i]}` : parts[i] paths.add(p) } } return paths } function statusColor(status: string) { switch (status.charAt(0).toUpperCase()) { case "A": return "text-green-500" case "D": return "text-red-500" case "M": return "text-blue-500" default: return "text-muted-foreground" } } // --- Main component --- interface StashWorkspaceProps { folderPath: string } export function StashWorkspace({ folderPath }: StashWorkspaceProps) { const t = useTranslations("Folder.branchDropdown.unstashDialog") const [stashes, setStashes] = useState([]) const [expandedStash, setExpandedStash] = useState(null) const [stashFiles, setStashFiles] = useState< Record >({}) const [filesLoading, setFilesLoading] = useState(null) const [selectedFile, setSelectedFile] = useState(null) const [selectedStashRef, setSelectedStashRef] = useState(null) const [originalContent, setOriginalContent] = useState("") const [modifiedContent, setModifiedContent] = useState("") const [listLoading, setListLoading] = useState(false) const [diffLoading, setDiffLoading] = useState(false) const [actionLoading, setActionLoading] = useState(false) const loadStashes = useCallback(async () => { setListLoading(true) try { const list = await gitStashList(folderPath) setStashes(list) } catch (err) { toast.error(toErrorMessage(err)) } finally { setListLoading(false) } }, [folderPath]) useEffect(() => { loadStashes() }, [loadStashes]) async function handleToggleStash(stashRef: string) { if (expandedStash === stashRef) { setExpandedStash(null) return } setExpandedStash(stashRef) if (!stashFiles[stashRef]) { setFilesLoading(stashRef) try { const fileList = await gitStashShow(folderPath, stashRef) setStashFiles((prev) => ({ ...prev, [stashRef]: fileList })) } catch (err) { toast.error(toErrorMessage(err)) } finally { setFilesLoading(null) } } } async function handleSelectFile(stashRef: string, file: string) { setSelectedFile(file) setSelectedStashRef(stashRef) setDiffLoading(true) try { const [orig, mod] = await Promise.all([ gitShowFile(folderPath, file, stashRef + "^").catch(() => ""), gitShowFile(folderPath, file, stashRef).catch(() => ""), ]) setOriginalContent(orig) setModifiedContent(mod) } catch { setOriginalContent("") setModifiedContent("") } finally { setDiffLoading(false) } } async function handleApply(stashRef: string) { setActionLoading(true) try { await gitStashApply(folderPath, stashRef) toast.success(t("applySuccess")) } catch (err) { toast.error(toErrorMessage(err)) } finally { setActionLoading(false) } } async function handleDrop(stashRef: string) { setActionLoading(true) try { await gitStashDrop(folderPath, stashRef) toast.success(t("dropSuccess")) if (expandedStash === stashRef) { setExpandedStash(null) } if (selectedStashRef === stashRef) { setSelectedFile(null) setSelectedStashRef(null) setOriginalContent("") setModifiedContent("") } setStashFiles((prev) => { const next = { ...prev } delete next[stashRef] return next }) await loadStashes() } catch (err) { toast.error(toErrorMessage(err)) } finally { setActionLoading(false) } } // Render file tree nodes function renderNode(node: TreeNode, stashRef: string): React.ReactNode { if (node.kind === "dir") { return ( {node.children.map((child) => renderNode(child, stashRef))} ) } return ( {node.name} {node.entry.status.charAt(0)} handleSelectFile(stashRef, node.path)} > {t("viewDiff")} ) } return ( {/* Left panel: stash cards */} {listLoading ? (
) : stashes.length === 0 ? (
{t("noStashes")}
) : (
{stashes.map((stash) => ( handleToggleStash(stash.ref_name)} onApply={() => handleApply(stash.ref_name)} onDrop={() => handleDrop(stash.ref_name)} onSelectFile={(file) => handleSelectFile(stash.ref_name, file) } renderNode={(node) => renderNode(node, stash.ref_name)} /> ))}
)}
{/* Right panel: diff viewer */} {diffLoading ? (
) : selectedFile && selectedStashRef ? ( ) : (
{t("selectFile")}
)}
) } // --- Stash Card Component --- interface StashCardProps { stash: GitStashEntry isExpanded: boolean isLoadingFiles: boolean actionLoading: boolean files?: GitStatusEntry[] selectedFile: string | null onToggle: () => void onApply: () => void onDrop: () => void onSelectFile: (file: string) => void renderNode: (node: TreeNode) => React.ReactNode } function StashCard({ stash, isExpanded, isLoadingFiles, actionLoading, files, selectedFile, onToggle, onApply, onDrop, onSelectFile, renderNode, }: StashCardProps) { const t = useTranslations("Folder.branchDropdown.unstashDialog") const [confirmApplyOpen, setConfirmApplyOpen] = useState(false) const tree = useMemo(() => (files ? buildFileTree(files) : []), [files]) const defaultExpanded = useMemo( () => (files ? collectDirPaths(files) : new Set()), [files] ) return ( <>
{/* File tree */} {isLoadingFiles ? (
) : tree.length > 0 ? (
{tree.map(renderNode)}
) : null}
setConfirmApplyOpen(true)}> {t("apply")} {t("drop")}
{t("apply")} {t("confirmApply", { ref: stash.ref_name })} {t("cancel")} { setConfirmApplyOpen(false) onApply() }} > {t("apply")} ) }