1import { useAtomValue } from "@effect/atom-react";
2import { type VcsActionState, type VcsActionTarget } from "@t3tools/client-runtime/state/vcs";
3import { Atom } from "effect/reactivity";
4import { useCallback, useEffect, useRef, useState } from "react";
5
6import { appAtomRegistry } from "./atom-registry";
7import { vcsActionManager } from "./vcs";
8
9export function useVcsActionState(target: VcsActionTarget): VcsActionState {
10 return useAtomValue(vcsActionManager.stateAtom(target));
11}
12
13export interface GitActionResultNotification {
14 readonly type: "success" | "error";
15 readonly title: string;
16 readonly description?: string;
17 readonly prUrl?: string;
18}
19
20const RESULT_DISMISS_MS = 5_000;
21
22const gitActionResultAtom = Atom.make<GitActionResultNotification | null>(null).pipe(
23 Atom.keepAlive,
24 Atom.withLabel("mobile:git-action-result"),
25);
26let dismissTimer: ReturnType<typeof setTimeout> | null = null;
27
28function broadcast(result: GitActionResultNotification | null): void {
29 appAtomRegistry.set(gitActionResultAtom, result);
30}
31
32export function showGitActionResult(result: GitActionResultNotification): void {
33 if (dismissTimer) clearTimeout(dismissTimer);
34 broadcast(result);
35 dismissTimer = setTimeout(() => {
36 dismissTimer = null;
37 broadcast(null);
38 }, RESULT_DISMISS_MS);
39}
40
41export function dismissGitActionResult(): void {
42 if (dismissTimer) clearTimeout(dismissTimer);
43 dismissTimer = null;
44 broadcast(null);
45}
46
47export function useGitActionResultNotification(): {
48 readonly result: GitActionResultNotification | null;
49 readonly dismiss: () => void;
50} {
51 const result = useAtomValue(gitActionResultAtom);
52 return { result, dismiss: dismissGitActionResult };
53}
54
55export type GitActionProgressPhase = "idle" | "running" | "success" | "error";
56
57export interface GitActionProgress {
58 readonly phase: GitActionProgressPhase;
59 readonly label: string | null;
60 readonly description: string | null;
61 readonly prUrl?: string;
62}
63
64const EMPTY_PROGRESS: GitActionProgress = {
65 phase: "idle",
66 label: null,
67 description: null,
68};
69
70function formatElapsedSeconds(ms: number | null): string | null {
71 if (ms === null) return null;
72 const elapsed = Math.max(0, Math.floor((Date.now() - ms) / 1000));
73 if (elapsed < 2) return null;
74 return `Running for ${elapsed}s`;
75}
76
77export function useGitActionProgress(target: VcsActionTarget): GitActionProgress {
78 const actionState = useVcsActionState(target);
79 const { result } = useGitActionResultNotification();
80
81 const [, forceUpdate] = useState(0);
82 const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
83
84 const startElapsedTimer = useCallback(() => {
85 if (intervalRef.current) return;
86 intervalRef.current = setInterval(() => forceUpdate((n) => n + 1), 1000);
87 }, []);
88
89 const stopElapsedTimer = useCallback(() => {
90 if (intervalRef.current) {
91 clearInterval(intervalRef.current);
92 intervalRef.current = null;
93 }
94 }, []);
95
96 useEffect(() => {
97 if (actionState.isRunning) {
98 startElapsedTimer();
99 } else {
100 stopElapsedTimer();
101 }
102 return stopElapsedTimer;
103 }, [actionState.isRunning, startElapsedTimer, stopElapsedTimer]);
104
105 if (actionState.isRunning) {
106 const description =
107 actionState.lastOutputLine ??
108 formatElapsedSeconds(actionState.hookStartedAtMs ?? actionState.phaseStartedAtMs);
109 return {
110 phase: "running",
111 label: actionState.currentLabel,
112 description,
113 };
114 }
115
116 if (result) {
117 return {
118 phase: result.type,
119 label: result.title,
120 description: result.description ?? null,
121 prUrl: result.prUrl,
122 };
123 }
124
125 return EMPTY_PROGRESS;
126}