| 1 | import { useState } from "react"; |
| 2 | import { useCommittedCommand } from "../lib/useCommittedCommand"; |
| 3 | import { runWorktreeMergeLifecycle } from "../lib/worktreeMergeLifecycle"; |
| 4 | import type { WorktreeMergeResult } from "../lib/types"; |
| 5 | import type { Translator } from "../lib/i18n"; |
| 6 | |
| 7 | /** |
| 8 | * Owns the worktree merge coordination: the worktreeMergeTabId overlay state |
| 9 | * with its open/close commands and the merged-receipt handler that runs the |
| 10 | * navigation-intent-gated close/finalize lifecycle. Callers only wire the |
| 11 | * returned state and commands into the topicbar and overlay regions. |
| 12 | */ |
| 13 | export function useWorktreeMergeCommands(input: { |
| 14 | noteNavigationIntent: () => number; |
| 15 | registeredNavigationIntent: (seq: number) => Promise<string | null>; |
| 16 | isNavigationIntentCurrent: (seq: number) => boolean; |
| 17 | ensureBlankSurface: (scope: string, workspace: string, seq: number) => Promise<any>; |
| 18 | seedSource: (tab: any) => void; |
| 19 | listTabs: () => Promise<any[]>; |
| 20 | closeWorktree: (request: any) => Promise<any>; |
| 21 | finalize: (request: any) => Promise<any>; |
| 22 | showToast: (message: string, level: "error", options?: { durationMs?: number }) => void; |
| 23 | t: Translator; |
| 24 | showCleanup: (cleanup: any, t: Translator) => void; |
| 25 | }) { |
| 26 | const [worktreeMergeTabId, setWorktreeMergeTabId] = useState<string | null>(null); |
| 27 | |
| 28 | const openWorktreeMerge = useCommittedCommand((tabId: string) => setWorktreeMergeTabId(tabId)); |
| 29 | const closeWorktreeMerge = useCommittedCommand(() => setWorktreeMergeTabId(null)); |
| 30 | |
| 31 | const handleWorktreeMerged = useCommittedCommand(async (result: WorktreeMergeResult) => { |
| 32 | const tabToClose = worktreeMergeTabId; |
| 33 | if (!tabToClose || !result.sourceRoot || !result.worktreeRoot || !result.targetBranch || !result.mergedCommit || !result.worktreeBranch || !result.worktreeHead) { |
| 34 | throw new Error(result.error || input.t("worktree.mergeReceiptInvalid")); |
| 35 | } |
| 36 | const seq = input.noteNavigationIntent(); |
| 37 | try { |
| 38 | const token = await input.registeredNavigationIntent(seq); |
| 39 | if (!token || !input.isNavigationIntentCurrent(seq)) { |
| 40 | input.showToast(input.t("worktree.navigationChangedPreserved"), "error", { durationMs: 9000 }); |
| 41 | return; |
| 42 | } |
| 43 | const lifecycle = await runWorktreeMergeLifecycle(result, tabToClose, token, { |
| 44 | ensureSource: (root) => input.ensureBlankSurface("project", root, seq), |
| 45 | isNavigationCurrent: () => input.isNavigationIntentCurrent(seq), |
| 46 | seedSource: input.seedSource, |
| 47 | listTabs: input.listTabs, |
| 48 | closeWorktree: input.closeWorktree, |
| 49 | finalize: input.finalize, |
| 50 | onNavigationPreserved: () => input.showToast(input.t("worktree.navigationChangedPreserved"), "error", { durationMs: 9000 }), |
| 51 | onCloseBlocked: () => input.showToast(input.t("worktree.cleanupViewBlocked"), "error", { durationMs: 8000 }), |
| 52 | }); |
| 53 | if (lifecycle.phase === "finalized") input.showCleanup(lifecycle.cleanup, input.t); |
| 54 | } catch (error) { |
| 55 | input.showToast(`${input.t("worktree.mergeDoneCleanupFailed")} ${error instanceof Error ? error.message : String(error)}`, "error", { durationMs: 9000 }); |
| 56 | } |
| 57 | }); |
| 58 | return { worktreeMergeTabId, openWorktreeMerge, closeWorktreeMerge, handleWorktreeMerged }; |
| 59 | } |
| 60 |