import { useCallback, useEffect, useMemo, useState } from "react"; import { api } from "../api"; import { useConfirm } from "../hooks/useConfirm"; import "./StashRecoveryView.css"; type RecordItem = { sha: string; sourceTaskId: string | null; createdAt: string | null; classification: "subsumed" | "live" | "unknown"; changedPaths: string[]; }; type DiffResponse = { diff: string; truncated: boolean; }; export function StashRecoveryView() { const { confirm } = useConfirm(); const [records, setRecords] = useState([]); const [error, setError] = useState(null); const [applyState, setApplyState] = useState>({}); const [diffState, setDiffState] = useState<{ sha: string; diff: string; truncated: boolean; loading: boolean; error: string | null } | null>(null); const load = useCallback(async () => { try { setError(null); const data = await api<{ records: RecordItem[] }>("/stash-recovery/orphans"); setRecords(data.records ?? []); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load orphans"); } }, []); useEffect(() => { void load(); }, [load]); const groups = useMemo(() => { const map = new Map(); for (const record of records) { const key = record.sourceTaskId ?? "Unknown source"; const existing = map.get(key) ?? []; existing.push(record); map.set(key, existing); } return Array.from(map.entries()); }, [records]); const handleApply = useCallback(async (sha: string) => { const result = await api<{ ok: boolean; reason?: string; stderr?: string }>(`/stash-recovery/orphans/${sha}/apply`, { method: "POST" }); setApplyState((prev) => ({ ...prev, [sha]: result.ok ? "Applied" : result.stderr ?? result.reason ?? "Apply failed" })); }, []); const handleDrop = useCallback(async (sha: string) => { const shouldDrop = await confirm({ title: "Drop orphaned stash?", message: "This removes the stash entry permanently.", confirmLabel: "Drop", danger: true, }); if (!shouldDrop) return; await api(`/stash-recovery/orphans/${sha}/drop`, { method: "POST", body: JSON.stringify({ confirm: true }) }); await load(); }, [confirm, load]); const handleInspectDiff = useCallback(async (sha: string) => { setDiffState({ sha, diff: "", truncated: false, loading: true, error: null }); try { const data = await api(`/stash-recovery/orphans/${sha}/diff`); setDiffState({ sha, diff: data.diff ?? "", truncated: Boolean(data.truncated), loading: false, error: null }); } catch (err) { setDiffState({ sha, diff: "", truncated: false, loading: false, error: err instanceof Error ? err.message : "Failed to load diff" }); } }, []); if (records.length === 0 && !error) { return

No orphaned merger autostashes found.

; } return (

Stash Recovery

{records.length} orphans
{error &&
{error}
} {groups.map(([group, items]) => (

{group}

{items.map((item) => (
SHA {item.sha.slice(0, 7)}
Classification {item.classification}
Changed paths {item.changedPaths.length} files
{applyState[item.sha] &&
{applyState[item.sha]}
}
))}
))} {diffState && (
setDiffState(null)}>
event.stopPropagation()}>

Diff for {diffState.sha.slice(0, 7)}

{diffState.loading &&

Loading diff…

} {diffState.error &&
{diffState.error}
} {!diffState.loading && !diffState.error && ( <>
{diffState.diff || "No diff output available."}
{diffState.truncated &&

Diff output truncated.

} )}
)}
); }