import { useEffect, useMemo, useRef, useState } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { ChevronRight, ExternalLink, Folder, MessageSquare, Plus, Settings as SettingsIcon, X, Code } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { sessionEvents } from '@/hooks/sessionEvents';
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from '@/components/ui/context-menu';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
} from '@/components/ui/dialog';
import { AddProjectModal } from './AddProjectModal';
import { api, ApiError } from '@/api/client';
import { useSidebar } from '@/hooks/useSidebar';
import { useSidebarDrawer } from '@/hooks/useSidebarDrawer';
import { useViewport } from '@/hooks/useViewport';
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import type { SidebarProject, WorktreeRiskReport } from '@/api/types';
import { giteaUrlFor } from '@/lib/projectUrls';
import { isCoderSessionName } from '@/lib/coder-session';
import { cn } from '@/lib/utils';

const EXPANDED_KEY = 'boocode.sidebar.expanded';
const MAX_VISIBLE_SESSIONS = 5;

function readExpanded(): Set<string> {
  try {
    const raw = localStorage.getItem(EXPANDED_KEY);
    if (!raw) return new Set();
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return new Set();
    return new Set(parsed.filter((v): v is string => typeof v === 'string'));
  } catch {
    return new Set();
  }
}

function writeExpanded(ids: Set<string>): void {
  try {
    localStorage.setItem(EXPANDED_KEY, JSON.stringify(Array.from(ids)));
  } catch {
    /* quota or disabled storage — ignore */
  }
}

function relTime(iso: string): string {
  const now = Date.now();
  const t = Date.parse(iso);
  if (Number.isNaN(t)) return '';
  const sec = Math.max(0, Math.floor((now - t) / 1000));
  if (sec < 60) return `${sec}s`;
  const min = Math.floor(sec / 60);
  if (min < 60) return `${min}m`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h`;
  const day = Math.floor(hr / 24);
  if (day < 30) return `${day}d`;
  const mo = Math.floor(day / 30);
  if (mo < 12) return `${mo}mo`;
  return `${Math.floor(mo / 12)}y`;
}

function activeProjectId(
  pathname: string,
  projects: SidebarProject[],
  activeSession: { session_id: string; project_id: string } | null
): string | null {
  const pm = pathname.match(/^\/project\/([^/]+)/);
  if (pm?.[1]) return pm[1];
  const sm = pathname.match(/^\/session\/([^/]+)/);
  const sid = sm?.[1];
  if (!sid) return null;
  // Prefer the cache lookup so we resolve correctly even when an older
  // activeSession (from a prior route) hasn't been cleared yet.
  const fromCache = projects.find((p) =>
    p.recent_sessions.some((s) => s.id === sid)
  )?.id;
  if (fromCache) return fromCache;
  // Fallback: the session was loaded via deep link (not in cache) and
  // emitted session_loaded — use that. Guard against stale values by
  // matching the current URL sid.
  if (activeSession && activeSession.session_id === sid) {
    return activeSession.project_id;
  }
  return null;
}

function activeSessionId(pathname: string): string | null {
  const m = pathname.match(/^\/session\/([^/]+)/);
  return m?.[1] ?? null;
}

export function ProjectSidebar() {
  const { data, error, loading, retry, activeSession: loadedActiveSession } =
    useSidebar();
  const [addOpen, setAddOpen] = useState(false);
  const [expanded, setExpanded] = useState<Set<string>>(() => readExpanded());
  const [renamingSession, setRenamingSession] = useState<string | null>(null);
  const [renameValue, setRenameValue] = useState('');
  const [deleteConfirm, setDeleteConfirm] = useState<{ id: string; name: string } | null>(null);
  const [renamingProject, setRenamingProject] = useState<string | null>(null);
  const [renameProjectValue, setRenameProjectValue] = useState('');
  const [archiveProjectConfirm, setArchiveProjectConfirm] = useState<{ id: string; name: string } | null>(null);
  // Work-at-risk dialog: shown when a delete is blocked (409) because the
  // session's worktree holds uncommitted/unpushed/unmerged work.
  const [riskState, setRiskState] = useState<{
    sessionId: string;
    projectId: string;
    name: string;
    message: string;
    reports: WorktreeRiskReport[];
  } | null>(null);
  const [riskBusy, setRiskBusy] = useState(false);
  const navigate = useNavigate();
  const location = useLocation();
  const lastToastedError = useRef<string | null>(null);

  useEffect(() => {
    if (error && !data && error !== lastToastedError.current) {
      toast.error(error);
      lastToastedError.current = error;
    }
    if (!error) lastToastedError.current = null;
  }, [error, data]);

  const projects = data?.projects ?? [];
  const activeProject = useMemo(
    () => activeProjectId(location.pathname, projects, loadedActiveSession),
    [location.pathname, projects, loadedActiveSession]
  );
  const activeSession = useMemo(
    () => activeSessionId(location.pathname),
    [location.pathname]
  );

  function toggle(id: string) {
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      writeExpanded(next);
      return next;
    });
  }

  async function handleArchiveProject(id: string) {
    try {
      await api.projects.archive(id);
      // Server publishes project_archived via WS.
      if (activeProject === id) navigate('/');
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'failed to archive project');
    }
  }

  async function handleRenameProject(id: string) {
    const trimmed = renameProjectValue.trim();
    setRenamingProject(null);
    if (!trimmed) return;
    try {
      await api.projects.update(id, { name: trimmed });
      // Server publishes project_updated via WS.
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'failed to rename project');
    }
  }

  async function handleArchiveSession(sessionId: string, projectId: string) {
    try {
      await api.sessions.archive(sessionId);
      // Server publishes session_archived via WS; useUserEvents delivers it.
      if (activeSession === sessionId) navigate(`/project/${projectId}`);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'failed to archive session');
    }
  }

  async function handleDeleteSession(
    sessionId: string,
    projectId: string,
    name: string,
    force = false,
  ) {
    try {
      await api.sessions.remove(sessionId, force);
      // Server publishes session_deleted via WS; useUserEvents delivers it.
      setRiskState(null);
      if (activeSession === sessionId) navigate(`/project/${projectId}`);
    } catch (err) {
      // 409 => the server's work-loss guard blocked the delete. Open the
      // work-at-risk dialog with the per-worktree reports instead of toasting.
      if (
        err instanceof ApiError &&
        err.status === 409 &&
        err.body && typeof err.body === 'object' && 'reports' in err.body
      ) {
        const body = err.body as { error?: string; reports?: WorktreeRiskReport[] };
        setRiskState({
          sessionId,
          projectId,
          name,
          message: body.error ?? 'This session has work at risk.',
          reports: body.reports ?? [],
        });
        return;
      }
      toast.error(err instanceof Error ? err.message : 'failed to delete session');
    }
  }

  // Stash the worktree's uncommitted changes (recoverable), then re-attempt the
  // delete. If unpushed/unmerged commits remain, the retry 409s again and the
  // dialog re-renders with the narrowed risk.
  async function handleStashAndRetry() {
    if (!riskState || riskBusy) return;
    setRiskBusy(true);
    try {
      const { results } = await api.sessions.worktreeStash(riskState.sessionId);
      const failed = results.find((r) => r.error);
      if (failed) {
        toast.error(`stash failed: ${failed.error}`);
        return;
      }
      await handleDeleteSession(riskState.sessionId, riskState.projectId, riskState.name, false);
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'stash failed');
    } finally {
      setRiskBusy(false);
    }
  }

  // Explicit, destructive override — deletes despite work at risk.
  async function handleForceDelete() {
    if (!riskState || riskBusy) return;
    setRiskBusy(true);
    try {
      await handleDeleteSession(riskState.sessionId, riskState.projectId, riskState.name, true);
    } finally {
      setRiskBusy(false);
    }
  }

  // Route the user to commit it themselves — never auto-commit. Opens the
  // session workspace where they can use a terminal or agent pane.
  function handleGoCommit() {
    if (!riskState) return;
    const sessionId = riskState.sessionId;
    setRiskState(null);
    navigate(`/session/${sessionId}`);
    toast.info('Open a terminal or agent in this session, commit and push your work, then delete again.');
  }

  async function handleRenameSession(sessionId: string) {
    const trimmed = renameValue.trim();
    setRenamingSession(null);
    if (!trimmed) return;
    try {
      await api.sessions.update(sessionId, { name: trimmed });
      // Server publishes session_renamed via broker.publishUser; useUserEvents
      // forwards onto the bus. No local emit needed.
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'failed to rename session');
    }
  }

  const rowCls = (active: boolean) =>
    active ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'hover:bg-sidebar-accent/60';

  const { open: drawerOpen, setOpen: setDrawerOpen } = useSidebarDrawer();
  const { isMobile } = useViewport();
  const pull = usePullToRefresh(() => retry(), { enabled: isMobile });

  // On mobile the sidebar is a slide-in drawer (fixed, z-40, off-screen by
  // default). On desktop it sits inline as a normal flex column. The
  // backdrop is rendered by AppShell; drawer-open state lives in
  // SidebarDrawerProvider.
  const asideCls = isMobile
    ? cn(
        'fixed inset-y-0 left-0 z-40 w-60 border-r bg-sidebar text-sidebar-foreground flex flex-col',
        'transition-transform duration-200 ease-out',
        drawerOpen ? 'translate-x-0' : '-translate-x-full',
      )
    : 'w-60 shrink-0 border-r bg-sidebar text-sidebar-foreground flex flex-col h-screen';

  // Work-at-risk dialog framing. The server returns 409 in two distinct
  // situations: (1) work genuinely at risk (reports has ≥1 atRisk entry), or
  // (2) it couldn't verify (BooCoder down/errored → reports is empty). These
  // are different user stories — "your work is in danger" vs "the checker is
  // offline" — so the dialog must not show one generic message for both.
  const atRiskReports = riskState?.reports.filter((r) => r.atRisk) ?? [];
  const verifyFailed = riskState !== null && atRiskReports.length === 0;
  const anyDirty = atRiskReports.some((r) => r.dirty);
  // Commit-based risk (unpushed/unmerged) that stash can NOT clear. When this is
  // all that remains (e.g. after a stash cleared the dirty changes), the dialog
  // explains why it re-blocked and hides the Stash button so it doesn't look
  // like stash "didn't work".
  const anyCommits = atRiskReports.some((r) => r.unpushed !== 0 || r.unmerged > 0);

  return (
    <aside className={asideCls}>
      <div className="px-4 py-3 border-b flex items-center justify-between">
        <NavLink to="/" className="font-semibold tracking-tight text-base">
          BooCode
        </NavLink>
        <div className="flex items-center gap-1">
          <Button size="icon-sm" variant="ghost" onClick={() => setAddOpen(true)} aria-label="Add project">
            <Plus />
          </Button>
          {isMobile && (
            <Button
              size="icon-sm"
              variant="ghost"
              onClick={() => setDrawerOpen(false)}
              aria-label="Close sidebar"
            >
              <X />
            </Button>
          )}
        </div>
      </div>

      {isMobile && (pull.pullDist > 0 || pull.refreshing) && (
        <div
          className="flex items-center justify-center text-[10px] uppercase tracking-wide text-muted-foreground border-b overflow-hidden shrink-0"
          style={{
            height: pull.refreshing ? 32 : Math.min(pull.pullDist, 80),
            transition: pull.pullDist === 0 && !pull.refreshing ? 'height 0.2s ease' : undefined,
          }}
          aria-live="polite"
        >
          {pull.refreshing
            ? 'Refreshing…'
            : pull.pullDist >= 80
            ? 'Release to refresh'
            : 'Pull to refresh'}
        </div>
      )}

      <nav
        className="flex-1 overflow-y-auto py-2"
        onTouchStart={isMobile ? pull.onTouchStart : undefined}
        onTouchMove={isMobile ? pull.onTouchMove : undefined}
        onTouchEnd={isMobile ? pull.onTouchEnd : undefined}
        onTouchCancel={isMobile ? pull.onTouchEnd : undefined}
      >
        {loading && data == null && (
          <div className="space-y-2 px-2">
            {[0, 1, 2, 3].map((i) => (
              <div key={i} className="bg-muted/40 animate-pulse rounded h-6" />
            ))}
          </div>
        )}
        {data != null && projects.length === 0 && (
          <div className="px-4 py-2 text-xs text-muted-foreground">
            No projects yet. Click + to add one.
          </div>
        )}
        {error != null && !data && (
          <div className="px-4 py-2 space-y-2">
            <div className="text-xs text-muted-foreground">{error}</div>
            <Button size="sm" variant="outline" onClick={retry}>
              Retry
            </Button>
          </div>
        )}
        {data != null &&
          projects.map((p) => {
            const isActiveProject = activeProject === p.id;
            const isExpanded = isActiveProject || expanded.has(p.id);
            const visible = p.recent_sessions.slice(0, MAX_VISIBLE_SESSIONS);
            return (
              <div key={p.id} className="px-2">
                <ContextMenu>
                  <ContextMenuTrigger asChild>
                    <div
                      className={`group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm ${rowCls(isActiveProject)}`}
                    >
                      <button
                        type="button"
                        aria-label={isExpanded ? 'Collapse' : 'Expand'}
                        aria-expanded={isExpanded}
                        disabled={isActiveProject}
                        onClick={(e) => {
                          e.stopPropagation();
                          if (isActiveProject) return;
                          toggle(p.id);
                        }}
                        className={cn(
                          'flex items-center justify-center size-4 shrink-0 opacity-70 hover:opacity-100',
                          isActiveProject &&
                            'opacity-50 cursor-not-allowed hover:opacity-50'
                        )}
                      >
                        <ChevronRight
                          className={`size-3.5 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
                        />
                      </button>
                      {renamingProject === p.id ? (
                        <div className="flex items-center gap-2 min-w-0 flex-1">
                          <Folder className="size-3.5 shrink-0 opacity-70" />
                          <input
                            autoFocus
                            value={renameProjectValue}
                            onChange={(e) => setRenameProjectValue(e.target.value)}
                            onBlur={() => void handleRenameProject(p.id)}
                            onKeyDown={(e) => {
                              if (e.key === 'Enter') void handleRenameProject(p.id);
                              if (e.key === 'Escape') setRenamingProject(null);
                            }}
                            className="bg-transparent border-b border-border text-sm outline-none flex-1 min-w-0"
                          />
                        </div>
                      ) : (
                        <NavLink to={`/project/${p.id}`} className="flex items-center gap-2 min-w-0 flex-1">
                          <Folder className="size-3.5 shrink-0 opacity-70" />
                          <span className="truncate" title={p.name}>{p.name}</span>
                        </NavLink>
                      )}
                    </div>
                  </ContextMenuTrigger>
                  <ContextMenuContent>
                    <ContextMenuItem onSelect={() => {
                      setRenamingProject(p.id);
                      setRenameProjectValue(p.name);
                    }}>
                      Rename
                    </ContextMenuItem>
                    <ContextMenuItem onSelect={() => setArchiveProjectConfirm({ id: p.id, name: p.name })}>
                      Archive
                    </ContextMenuItem>
                    <ContextMenuSeparator />
                    <ContextMenuItem onSelect={() => {
                      const url = giteaUrlFor({ path: p.path, gitea_remote: p.gitea_remote });
                      window.open(url, '_blank', 'noopener');
                    }}>
                      <ExternalLink size={12} /> Open in Gitea
                    </ContextMenuItem>
                  </ContextMenuContent>
                </ContextMenu>

                {isExpanded && (
                  <div className="ml-5 mt-0.5 space-y-0.5">
                    {visible.map((s) => (
                      <ContextMenu key={s.id}>
                        <ContextMenuTrigger asChild>
                          {renamingSession === s.id ? (
                            <div className={`flex items-center gap-2 rounded-md px-2 py-1 text-sm min-w-0 ${rowCls(activeSession === s.id)}`}>
                              <MessageSquare className="size-3.5 shrink-0 opacity-70" />
                              <input
                                autoFocus
                                value={renameValue}
                                onChange={(e) => setRenameValue(e.target.value)}
                                onBlur={() => void handleRenameSession(s.id)}
                                onKeyDown={(e) => {
                                  if (e.key === 'Enter') void handleRenameSession(s.id);
                                  if (e.key === 'Escape') setRenamingSession(null);
                                }}
                                className="bg-transparent border-b border-border text-sm outline-none flex-1 min-w-0"
                              />
                            </div>
                          ) : (
                            <NavLink
                              to={`/session/${s.id}`}
                              className={`flex items-center gap-2 rounded-md px-2 py-1 text-sm min-w-0 ${rowCls(activeSession === s.id)}`}
                            >
                              {isCoderSessionName(s.name) ? (
                                <Code className="size-3.5 shrink-0 opacity-70" />
                              ) : (
                                <MessageSquare className="size-3.5 shrink-0 opacity-70" />
                              )}
                              <span className="truncate flex-1" title={s.name}>{s.name}</span>
                              <span className="text-xs text-muted-foreground shrink-0 tabular-nums">
                                {relTime(s.updated_at)}
                              </span>
                            </NavLink>
                          )}
                        </ContextMenuTrigger>
                        <ContextMenuContent>
                          <ContextMenuItem onSelect={() => {
                            setRenamingSession(s.id);
                            setRenameValue(s.name);
                          }}>
                            Rename
                          </ContextMenuItem>
                          <ContextMenuItem onSelect={() => void handleArchiveSession(s.id, p.id)}>
                            Archive
                          </ContextMenuItem>
                          <ContextMenuSeparator />
                          <ContextMenuItem
                            variant="destructive"
                            onSelect={() => setDeleteConfirm({ id: s.id, name: s.name })}
                          >
                            Delete
                          </ContextMenuItem>
                        </ContextMenuContent>
                      </ContextMenu>
                    ))}
                    {p.total_sessions > MAX_VISIBLE_SESSIONS && (
                      <NavLink
                        to={`/project/${p.id}`}
                        className="block rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent/60"
                      >
                        View all ({p.total_sessions})
                      </NavLink>
                    )}
                  </div>
                )}
              </div>
            );
          })}
      </nav>

      {/* v1.9: bottom-pinned Settings button. In a session, opens/focuses the
          workspace settings pane via the sessionEvents bus (Session.tsx owns
          the panesHook). Outside a session there's no workspace to mount the
          pane in, so we navigate to /settings (themes page) instead. */}
      <div className="border-t shrink-0 p-2">
        <button
          type="button"
          onClick={() => {
            if (activeSession) {
              sessionEvents.emit({ type: 'open_settings_pane' });
              if (isMobile) setDrawerOpen(false);
            } else {
              navigate('/settings');
              if (isMobile) setDrawerOpen(false);
            }
          }}
          className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm hover:bg-sidebar-accent/60 text-sidebar-foreground"
          aria-label="Settings"
        >
          <SettingsIcon className="size-3.5 shrink-0 opacity-70" />
          <span className="flex-1 text-left">Settings</span>
        </button>
      </div>

      <AddProjectModal open={addOpen} onOpenChange={setAddOpen} onAdded={() => {}} />

      <Dialog open={archiveProjectConfirm !== null} onOpenChange={(open) => { if (!open) setArchiveProjectConfirm(null); }}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Archive project?</DialogTitle>
            <DialogDescription>
              Removes {archiveProjectConfirm ? `"${archiveProjectConfirm.name}"` : 'this project'} from the sidebar. Files on disk are untouched. You can restore it later from the Archived Projects view.
            </DialogDescription>
          </DialogHeader>
          <div className="flex gap-2 justify-end pt-2">
            <Button variant="outline" onClick={() => setArchiveProjectConfirm(null)}>
              Cancel
            </Button>
            <Button
              onClick={() => {
                if (archiveProjectConfirm) void handleArchiveProject(archiveProjectConfirm.id);
                setArchiveProjectConfirm(null);
              }}
            >
              Archive
            </Button>
          </div>
        </DialogContent>
      </Dialog>

      <Dialog open={deleteConfirm !== null} onOpenChange={(open) => { if (!open) setDeleteConfirm(null); }}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Delete session?</DialogTitle>
            <DialogDescription>
              This will permanently delete {deleteConfirm ? `"${deleteConfirm.name}"` : 'this session'} and all its chats and messages. This cannot be undone.
            </DialogDescription>
          </DialogHeader>
          <div className="flex gap-2 justify-end pt-2">
            <Button variant="outline" onClick={() => setDeleteConfirm(null)}>
              Cancel
            </Button>
            <Button
              variant="destructive"
              onClick={() => {
                if (deleteConfirm) {
                  const projectId = projects.find((p) =>
                    p.recent_sessions.some((s) => s.id === deleteConfirm.id)
                  )?.id;
                  if (projectId) void handleDeleteSession(deleteConfirm.id, projectId, deleteConfirm.name);
                }
                setDeleteConfirm(null);
              }}
            >
              Delete
            </Button>
          </div>
        </DialogContent>
      </Dialog>

      <Dialog open={riskState !== null} onOpenChange={(open) => { if (!open && !riskBusy) setRiskState(null); }}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>
              {verifyFailed ? 'Could not verify worktree safety' : 'This session has work at risk'}
            </DialogTitle>
            <DialogDescription>
              {verifyFailed ? (
                <>
                  {riskState?.message ?? 'The worktree safety check is unavailable.'} Your work may be
                  fine, but it couldn&apos;t be checked — only force-delete if you&apos;re sure.
                </>
              ) : anyDirty && anyCommits ? (
                <>
                  Deleting {riskState ? `"${riskState.name}"` : 'this session'} would orphan uncommitted
                  changes <em>and</em> commits that aren&apos;t pushed or merged. Stash clears the
                  changes (recoverable), but the commits will still block — push them or force-delete.
                </>
              ) : anyDirty ? (
                <>
                  Deleting {riskState ? `"${riskState.name}"` : 'this session'} would orphan uncommitted
                  changes in its worktree. Stash them (recoverable), commit them, or force-delete.
                </>
              ) : (
                <>
                  Deleting {riskState ? `"${riskState.name}"` : 'this session'} would orphan commits that
                  aren&apos;t pushed or merged. Stashing won&apos;t recover these — push them, or
                  force-delete.
                </>
              )}
            </DialogDescription>
          </DialogHeader>
          {!verifyFailed && (
            <div className="flex flex-col gap-2 py-1 text-sm">
              {atRiskReports.map((r) => (
                <div key={r.worktreePath} className="rounded border border-border/60 px-3 py-2">
                  <div className="font-mono text-xs text-muted-foreground truncate" title={r.worktreePath}>
                    {r.branch || r.worktreePath}
                  </div>
                  <ul className="mt-1 list-disc pl-5 text-foreground/90">
                    {r.error && <li className="text-destructive">git error: {r.error}</li>}
                    {r.dirty && <li>uncommitted changes</li>}
                    {r.unpushed === -1 && <li>local-only branch (no upstream)</li>}
                    {r.unpushed > 0 && <li>{r.unpushed} unpushed commit{r.unpushed === 1 ? '' : 's'}</li>}
                    {r.unmerged > 0 && <li>{r.unmerged} unmerged commit{r.unmerged === 1 ? '' : 's'}</li>}
                  </ul>
                </div>
              ))}
            </div>
          )}
          <div className="flex flex-wrap gap-2 justify-end pt-2">
            <Button variant="outline" disabled={riskBusy} onClick={() => setRiskState(null)}>
              Cancel
            </Button>
            {!verifyFailed && (
              <Button variant="outline" disabled={riskBusy} onClick={handleGoCommit}>
                Commit&hellip;
              </Button>
            )}
            {!verifyFailed && anyDirty && (
              <Button variant="outline" disabled={riskBusy} onClick={() => void handleStashAndRetry()}>
                Stash &amp; delete
              </Button>
            )}
            <Button variant="destructive" disabled={riskBusy} onClick={() => void handleForceDelete()}>
              Force delete
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </aside>
  );
}
