/** * The worktree manager dialog: every linked worktree of the repository with * its branch/head, plus removal of managed worktrees. Dirty worktrees reject * once and surface an inline force-confirm; the wt/ branch survives removal * unless the row's delete-branch checkbox is on. The primary checkout row is * display-only. * @module dsh-git-graph/client/worktrees/WorktreeManager */ import { useCallback, useEffect, useState } from 'react' import { IconBranchOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { Translate } from '@deepseek-ai/dsh-client-ui-slots' import { WORKTREE_BRANCH_PREFIX } from '../../core/git-command.ts' import type { WorktreeInfo, WorktreeListView, WorktreeRemoveResult } from '../../core/types.ts' import type { GitGraphKey } from '../locales.ts' import { errorMessage } from '../chips/error-copy.ts' import { Backdrop, cx } from '../chips/Chip.tsx' import css from '../chips/context.module.css' /** Props of the worktree manager dialog. */ export interface WorktreeManagerProps { /** Fetch the fresh worktree list. */ fetchWorktrees: () => Promise /** Remove one managed worktree (force/deleteBranch options ride through). */ onRemove: (worktreePath: string, opts: { force?: boolean; deleteBranch?: boolean }) => Promise /** Close the dialog. */ onClose: () => void t: Translate } /** * The worktree manager dialog. * @param props - see {@link WorktreeManagerProps}. */ export function WorktreeManager({ fetchWorktrees, onRemove, onClose, t }: WorktreeManagerProps) { const [view, setView] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) /** Row awaiting an inline force-confirm after a worktree-dirty rejection. */ const [forcePath, setForcePath] = useState(null) /** Rows whose wt/ branch should be deleted together with the worktree. */ const [branchDelete, setBranchDelete] = useState>(new Set()) const [pending, setPending] = useState(null) const reload = useCallback(() => { let live = true setLoading(true) void fetchWorktrees().then((fresh) => { if (!live) return setView(fresh) setLoading(false) }) return () => { live = false } }, [fetchWorktrees]) useEffect(() => reload(), [reload]) const remove = (item: WorktreeInfo, force: boolean): void => { if (pending !== null) return setPending(item.path) setError(null) void onRemove(item.path, { force, deleteBranch: branchDelete.has(item.path) }).then((result) => { if (result.ok) { setForcePath(null) reload() return } if (result.error.code === 'worktree-dirty' && !force) { setForcePath(item.path) return } setError(errorMessage(result.error, t)) }).finally(() => { setPending(null) }) } const toggleBranchDelete = (path: string): void => { setBranchDelete((previous) => { const next = new Set(previous) if (next.has(path)) next.delete(path) else next.add(path) return next }) } const rows = view?.worktrees ?? [] return ( <>

{t('worktree.manager.title')}

{loading &&
{t('worktree.manager.loading')}
} {!loading && rows.length <= 1 &&
{t('worktree.manager.empty')}
} {!loading && rows.map(item => (
{item.branch === '' ? t('worktree.manager.detached') : item.branch} {item.main && {t('worktree.manager.main')}} {item.head.slice(0, 7)}
{item.path}
{forcePath === item.path && (
{t('worktree.manager.forceConfirm')}
)}
{!item.main && (
{item.branch.startsWith(WORKTREE_BRANCH_PREFIX) && ( )}
)}
))}
{error !== null &&
{error}
}
) }