Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 73 additions & 81 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { useTheme, type ThemeMode } from "./lib/theme";
import { HooksModal, type HooksMap } from "./components/HooksModal";
import { CreateWorktreeModal, type CreateFormState } from "./components/CreateWorktreeModal";
import { DeleteExecutionModal, type DeleteExecutionState, type DeleteExecutionPhase } from "./components/DeleteExecutionModal";
import { ModalShell } from "./components/ModalShell";
import type {
ActionResponse,
BootstrapResponse,
Expand Down Expand Up @@ -162,12 +163,12 @@ export default function App({ repoPath }: { repoPath: string }) {
const [configText, setConfigText] = useState("");
const [createForm, setCreateForm] = useState<CreateFormState>(createInitialForm());
const [logs, setLogs] = useState<TaggedLog[]>([]);
const [prunePreview, setPrunePreview] = useState<string[]>([]);
const [isBusy, setIsBusy] = useState(false);
const [error, setError] = useState<string | null>(null);

const [selectedWorktreeId, setSelectedWorktreeId] = useState<string | null>(null);
const [deleteExecution, setDeleteExecution] = useState<DeleteExecutionState | null>(null);
const [pruneModal, setPruneModal] = useState<{ candidates: string[]; loading: boolean } | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [view, setView] = useState<"repository" | "worktrees" | "hooks" | "settings">("worktrees");
const [showActionLog, setShowActionLog] = useState(false);
Expand Down Expand Up @@ -549,29 +550,6 @@ export default function App({ repoPath }: { repoPath: string }) {
}
}

async function handlePreviewPrune() {
if (!repo) return;
setIsBusy(true);
setError(null);
try {
const preview = await previewRepoPrune(repo.repoRoot);
setPrunePreview(preview);
appendLogs([
{
level: preview.length > 0 ? "info" : "success",
message:
preview.length > 0
? t.logPruneCandidates(preview.length)
: t.logNoPruneCandidates,
},
]);
} catch (reason) {
setError(String(reason));
} finally {
setIsBusy(false);
}
}

async function handleSetDefaultTerminal(terminalId: string) {
setDefaultTerminalId(terminalId);
await setDefaultTerminal(terminalId);
Expand All @@ -582,10 +560,21 @@ export default function App({ repoPath }: { repoPath: string }) {
await setDefaultShell(shell);
}

async function handlePrune() {
async function handlePrunePreview() {
if (!repo) return;
setPruneModal({ candidates: [], loading: true });
try {
const candidates = await previewRepoPrune(repo.repoRoot);
setPruneModal({ candidates, loading: false });
} catch {
setPruneModal(null);
}
}

async function handlePruneConfirm() {
if (!repo) return;
setPruneModal(null);
await runAction(() => pruneRepoMetadata(repo.repoRoot));
setPrunePreview([]);
}

async function handleSaveCustomLauncher(input: SaveCustomLauncherInput) {
Expand Down Expand Up @@ -637,18 +626,7 @@ export default function App({ repoPath }: { repoPath: string }) {
<span className="topbar-path-text">{repo.repoRoot}</span>
</div>
)}
<div className="topbar-right">
<button
className="topbar-new-btn"
onClick={() => {
setCreateForm(createInitialForm(repo ?? undefined));
setShowCreateModal(true);
}}
disabled={!repo || isBusy}
>
+ {t.newWorktree}
</button>
</div>
<div className="topbar-right" />
</nav>

<div className="body">
Expand Down Expand Up @@ -759,6 +737,26 @@ export default function App({ repoPath }: { repoPath: string }) {
) : (
<div className="worktrees-layout">
<div className="worktrees-panel">
<div className="repo-name-label">{repo.repoRoot.split("/").pop()}</div>
<div className="worktree-toolbar">
<button
className="primary-button btn-sm"
onClick={() => {
setCreateForm(createInitialForm(repo));
setShowCreateModal(true);
}}
disabled={isBusy}
>
+ {t.newWorktree}
</button>
<button
className="ghost-button btn-sm"
onClick={() => void handlePrunePreview()}
disabled={isBusy}
>
{t.prune}
</button>
</div>
<div className="worktree-list">
{repo.worktrees.map((wt) => (
<WorktreeListItem
Expand Down Expand Up @@ -849,9 +847,6 @@ export default function App({ repoPath }: { repoPath: string }) {
configText={configText}
onConfigChange={setConfigText}
onSaveConfig={() => void handleSaveConfig()}
prunePreview={prunePreview}
onPreviewPrune={() => void handlePreviewPrune()}
onPrune={() => void handlePrune()}
isBusy={isBusy}
t={t}
defaultTerminal={defaultTerminalId}
Expand Down Expand Up @@ -905,6 +900,43 @@ export default function App({ repoPath }: { repoPath: string }) {
/>
)}

{/* Prune Confirmation Modal */}
{pruneModal && (
<ModalShell
title={t.pruneConfirmTitle}
onClose={() => setPruneModal(null)}
className="prune-confirm-modal"
>
<p className="prune-description">{t.pruneDescription}</p>
{pruneModal.loading ? (
<p className="subtle">{t.loading}</p>
) : pruneModal.candidates.length === 0 ? (
<div className="prune-preview">
<p>{t.pruneNoCandidates}</p>
</div>
) : (
<div className="prune-preview">
<p>{t.pruneCandidatesFound(pruneModal.candidates.length)}</p>
<ul>
{pruneModal.candidates.map((c) => (
<li key={c}><code>{c}</code></li>
))}
</ul>
</div>
)}
<div className="modal-actions">
<button className="ghost-button" onClick={() => setPruneModal(null)}>
{pruneModal.candidates.length > 0 ? t.cancel : t.close}
</button>
{pruneModal.candidates.length > 0 && !pruneModal.loading && (
<button className="danger-button" onClick={() => void handlePruneConfirm()}>
{t.pruneConfirmAction}
</button>
)}
</div>
</ModalShell>
)}

{/* Create Worktree Modal */}
{showCreateModal && repo && (
<CreateWorktreeModal
Expand Down Expand Up @@ -1437,9 +1469,6 @@ function SettingsPage({
configText,
onConfigChange,
onSaveConfig,
prunePreview,
onPreviewPrune,
onPrune,
isBusy,
t,
defaultTerminal,
Expand Down Expand Up @@ -1467,9 +1496,6 @@ function SettingsPage({
configText: string;
onConfigChange: (v: string) => void;
onSaveConfig: () => void;
prunePreview: string[];
onPreviewPrune: () => void;
onPrune: () => void;
isBusy: boolean;
t: Translations;
defaultTerminal: string;
Expand Down Expand Up @@ -1587,40 +1613,6 @@ function SettingsPage({
)}
</section>

{/* Maintenance */}
{repo && (
<section className="card stack">
<div className="section-heading">
<span>{t.prune}</span>
<div className="overview-actions">
<button className="ghost-button" onClick={onPreviewPrune} disabled={isBusy}>
{t.previewPrune}
</button>
<button className="primary-button" onClick={onPrune} disabled={isBusy}>
{t.prune}
</button>
</div>
</div>
{repo.configErrors.length > 0 && (
<div className="warning-panel">
{repo.configErrors.map((msg) => (
<p key={msg}>{msg}</p>
))}
</div>
)}
{prunePreview.length > 0 && (
<div className="prune-preview">
<h3>{t.prunePreview}</h3>
<ul>
{prunePreview.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</div>
)}
</section>
)}

{/* Default Terminal */}
<section className="card stack">
<div className="section-heading">
Expand Down
11 changes: 6 additions & 5 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const en: Translations = {
branchPlaceholder: "branch name",
newBranchName: "New branch name",
create: "Create",
newWorktree: "New Worktree",
newWorktree: "New",
createWorktree: "Create Worktree",
pathPreview: "Target Path",
mode: "Mode",
Expand Down Expand Up @@ -86,9 +86,12 @@ export const en: Translations = {
allLogs: "All Logs",
worktreeCount: (n) => `${n} worktrees`,
baseBranch: "base",
previewPrune: "Preview Prune",
prune: "Prune",
prunePreview: "Prune preview",
pruneConfirmTitle: "Prune Worktrees",
pruneDescription: "Prune removes stale worktree metadata — entries left behind when a worktree directory was deleted manually without using git worktree remove.",
pruneNoCandidates: "No stale worktree metadata found. Everything is clean.",
pruneConfirmAction: "Confirm Prune",
pruneCandidatesFound: (n) => `Found ${n} stale worktree record(s) to clean up:`,
tooling: "Tooling",
logs: "Logs",
clear: "Clear",
Expand All @@ -113,8 +116,6 @@ export const en: Translations = {
logLoaded: (path) => `Loaded ${path}`,
logSavedConfig: "Saved local repo config.",
logSavedHooks: "Saved hooks.",
logPruneCandidates: (n) => `Found ${n} prune candidate(s).`,
logNoPruneCandidates: "No prune candidates detected.",
openInFinder: "Open in Finder",
lastCommit: "Last Commit",
branchLabel: "Branch",
Expand Down
9 changes: 5 additions & 4 deletions src/locales/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ export interface Translations {
allLogs: string;
worktreeCount: (n: number) => string;
baseBranch: string;
previewPrune: string;
prune: string;
prunePreview: string;
pruneConfirmTitle: string;
pruneDescription: string;
pruneNoCandidates: string;
pruneConfirmAction: string;
pruneCandidatesFound: (n: number) => string;
tooling: string;
logs: string;
clear: string;
Expand All @@ -109,8 +112,6 @@ export interface Translations {
logLoaded: (path: string) => string;
logSavedConfig: string;
logSavedHooks: string;
logPruneCandidates: (n: number) => string;
logNoPruneCandidates: string;
openInFinder: string;
lastCommit: string;
branchLabel: string;
Expand Down
33 changes: 17 additions & 16 deletions src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const zhCN: Translations = {
branchPlaceholder: "分支名",
newBranchName: "新分支名",
create: "创建",
newWorktree: "新建 Worktree",
createWorktree: "创建工作树",
newWorktree: "New",
createWorktree: "创建 Worktree",
pathPreview: "目标路径",
mode: "模式",
modeNewBranch: "新分支",
Expand All @@ -23,7 +23,7 @@ export const zhCN: Translations = {
fetchRemote: "拉取远程",
fetching: "拉取中...",
selectBranch: "选择分支",
worktreeRootLabel: "工作树目录",
worktreeRootLabel: "Worktree 目录",
setDefaultDirectory: "设置默认目录",
hooks: "Hooks",
addHook: "新增 Hook",
Expand Down Expand Up @@ -57,7 +57,7 @@ export const zhCN: Translations = {
cancel: "取消",
force: "强制删除",
delete: "删除",
noWorktrees: "未发现工作树。",
noWorktrees: "未发现 Worktree。",
detached: "(游离 HEAD)",
detachedShort: "(游离)",
justNow: "刚刚",
Expand All @@ -69,7 +69,7 @@ export const zhCN: Translations = {
dirty: "✗ Dirty",
clean: "✓ Clean",
locked: "锁定",
prunable: "可清理",
prunable: "Prunable",
enabled: "已启用",
disabled: "已禁用",
ready: "就绪",
Expand All @@ -84,11 +84,14 @@ export const zhCN: Translations = {
settings: "设置",
comingSoon: "开发中",
allLogs: "全部日志",
worktreeCount: (n) => `${n} 个工作树`,
worktreeCount: (n) => `${n} 个 Worktree`,
baseBranch: "基础分支",
previewPrune: "预览清理",
prune: "清理",
prunePreview: "清理预览",
prune: "Prune",
pruneConfirmTitle: "Prune Worktree",
pruneDescription: "Prune 会移除过期的 Worktree 元数据记录——当 Worktree 目录被手动删除(未使用 git worktree remove)时残留的条目。",
pruneNoCandidates: "没有发现过期的 Worktree 元数据,一切正常。",
pruneConfirmAction: "确认 Prune",
pruneCandidatesFound: (n) => `发现 ${n} 条过期的 Worktree 记录:`,
tooling: "工具检测",
logs: "日志",
clear: "清除",
Expand All @@ -102,17 +105,15 @@ export const zhCN: Translations = {
saveConfig: "保存配置",
close: "关闭",
heroTitle: "从一个本地 Git 仓库开始",
heroDescription: "选择任意仓库,扫描工作树、管理启动器、一站式运行Hooks。",
heroDescription: "选择任意仓库,扫描 Worktree、管理启动器、一站式运行 Hooks。",
heroPoint1: "使用原生 git worktree porcelain 输出。",
heroPoint2: "可通过 Hooks 在创建或启动后自动预热工作树。",
heroPoint2: "可通过 Hooks 在创建或启动后自动预热 Worktree。",
heroPoint3: "仓库自动化配置只保存在 Grove 本机存储里,不写入仓库。",
noWorktreeSelected: "未选择工作树",
selectWorktreeHint: "选择一个工作树,或创建新的工作树。",
noWorktreeSelected: "未选择 Worktree",
selectWorktreeHint: "选择一个 Worktree,或创建新的 Worktree。",
logLoaded: (path) => `已加载 ${path}`,
logSavedConfig: "已保存本机仓库配置。",
logSavedHooks: "已保存 Hooks。",
logPruneCandidates: (n) => `发现 ${n} 个可清理项。`,
logNoPruneCandidates: "未检测到可清理项。",
openInFinder: "在 Finder 中打开",
lastCommit: "最近提交",
branchLabel: "分支",
Expand Down Expand Up @@ -160,7 +161,7 @@ export const zhCN: Translations = {
cliUninstall: "卸载",
cliDescription: "将 grove 命令安装到 PATH 中,方便在终端快速操作。用法:grove open .、grove run post-create、grove list",
tabRepository: "仓库",
tabWorktrees: "工作树",
tabWorktrees: "Worktree",
themeLabel: "外观",
themeLight: "浅色",
themeDark: "深色",
Expand Down
Loading
Loading