import { useAtomValue } from "@effect/atom-react";import { type VcsActionState, type VcsActionTarget } from "@t3tools/client-runtime/state/vcs";import { Atom } from "effect/unstable/reactivity";import { useCallback, useEffect, useRef, useState } from "react"; import { appAtomRegistry } from "./atom-registry";import { vcsActionManager } from "./vcs"; export function useVcsActionState(target: VcsActionTarget): VcsActionState { return useAtomValue(vcsActionManager.stateAtom(target));} export interface GitActionResultNotification { readonly type: "success" | "error"; readonly title: string; readonly description?: string; readonly prUrl?: string;} const RESULT_DISMISS_MS = 5_000; const gitActionResultAtom = Atom.make<GitActionResultNotification | null>(null).pipe( Atom.keepAlive, Atom.withLabel("mobile:git-action-result"),);let dismissTimer: ReturnType<typeof setTimeout> | null = null; function broadcast(result: GitActionResultNotification | null): void { appAtomRegistry.set(gitActionResultAtom, result);} export function showGitActionResult(result: GitActionResultNotification): void { if (dismissTimer) clearTimeout(dismissTimer); broadcast(result); dismissTimer = setTimeout(() => { dismissTimer = null; broadcast(null); }, RESULT_DISMISS_MS);} export function dismissGitActionResult(): void { if (dismissTimer) clearTimeout(dismissTimer); dismissTimer = null; broadcast(null);} export function useGitActionResultNotification(): { readonly result: GitActionResultNotification | null; readonly dismiss: () => void;} { const result = useAtomValue(gitActionResultAtom); return { result, dismiss: dismissGitActionResult };} export type GitActionProgressPhase = "idle" | "running" | "success" | "error"; export interface GitActionProgress { readonly phase: GitActionProgressPhase; readonly label: string | null; readonly description: string | null; readonly prUrl?: string;} const EMPTY_PROGRESS: GitActionProgress = { phase: "idle", label: null, description: null,}; function formatElapsedSeconds(ms: number | null): string | null { if (ms === null) return null; const elapsed = Math.max(0, Math.floor((Date.now() - ms) / 1000)); if (elapsed < 2) return null; return `Running for ${elapsed}s`;} export function useGitActionProgress(target: VcsActionTarget): GitActionProgress { const actionState = useVcsActionState(target); const { result } = useGitActionResultNotification(); const [, forceUpdate] = useState(0); const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); const startElapsedTimer = useCallback(() => { if (intervalRef.current) return; intervalRef.current = setInterval(() => forceUpdate((n) => n + 1), 1000); }, []); const stopElapsedTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); useEffect(() => { if (actionState.isRunning) { startElapsedTimer(); } else { stopElapsedTimer(); } return stopElapsedTimer; }, [actionState.isRunning, startElapsedTimer, stopElapsedTimer]); if (actionState.isRunning) { const description = actionState.lastOutputLine ?? formatElapsedSeconds(actionState.hookStartedAtMs ?? actionState.phaseStartedAtMs); return { phase: "running", label: actionState.currentLabel, description, }; } if (result) { return { phase: result.type, label: result.title, description: result.description ?? null, prUrl: result.prUrl, }; } return EMPTY_PROGRESS;}
import { useAtomValue } from "@effect/atom-react";
import { type VcsActionState, type VcsActionTarget } from "@t3tools/client-runtime/state/vcs";
import { Atom } from "effect/unstable/reactivity";
import { useCallback, useEffect, useRef, useState } from "react";
import { appAtomRegistry } from "./atom-registry";
import { vcsActionManager } from "./vcs";
export function useVcsActionState(target: VcsActionTarget): VcsActionState {
return useAtomValue(vcsActionManager.stateAtom(target));
}
export interface GitActionResultNotification {
readonly type: "success" | "error";
readonly title: string;
readonly description?: string;
readonly prUrl?: string;
}
const RESULT_DISMISS_MS = 5_000;
const gitActionResultAtom = Atom.make<GitActionResultNotification | null>(null).pipe(
Atom.keepAlive,
Atom.withLabel("mobile:git-action-result"),
);
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
function broadcast(result: GitActionResultNotification | null): void {
appAtomRegistry.set(gitActionResultAtom, result);
}
export function showGitActionResult(result: GitActionResultNotification): void {
if (dismissTimer) clearTimeout(dismissTimer);
broadcast(result);
dismissTimer = setTimeout(() => {
dismissTimer = null;
broadcast(null);
}, RESULT_DISMISS_MS);
}
export function dismissGitActionResult(): void {
if (dismissTimer) clearTimeout(dismissTimer);
dismissTimer = null;
broadcast(null);
}
export function useGitActionResultNotification(): {
readonly result: GitActionResultNotification | null;
readonly dismiss: () => void;
} {
const result = useAtomValue(gitActionResultAtom);
return { result, dismiss: dismissGitActionResult };
}
export type GitActionProgressPhase = "idle" | "running" | "success" | "error";
export interface GitActionProgress {
readonly phase: GitActionProgressPhase;
readonly label: string | null;
readonly description: string | null;
readonly prUrl?: string;
}
const EMPTY_PROGRESS: GitActionProgress = {
phase: "idle",
label: null,
description: null,
};
function formatElapsedSeconds(ms: number | null): string | null {
if (ms === null) return null;
const elapsed = Math.max(0, Math.floor((Date.now() - ms) / 1000));
if (elapsed < 2) return null;
return `Running for ${elapsed}s`;
}
export function useGitActionProgress(target: VcsActionTarget): GitActionProgress {
const actionState = useVcsActionState(target);
const { result } = useGitActionResultNotification();
const [, forceUpdate] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const startElapsedTimer = useCallback(() => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => forceUpdate((n) => n + 1), 1000);
}, []);
const stopElapsedTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);
useEffect(() => {
if (actionState.isRunning) {
startElapsedTimer();
} else {
stopElapsedTimer();
}
return stopElapsedTimer;
}, [actionState.isRunning, startElapsedTimer, stopElapsedTimer]);
if (actionState.isRunning) {
const description =
actionState.lastOutputLine ??
formatElapsedSeconds(actionState.hookStartedAtMs ?? actionState.phaseStartedAtMs);
return {
phase: "running",
label: actionState.currentLabel,
description,
};
}
if (result) {
return {
phase: result.type,
label: result.title,
description: result.description ?? null,
prUrl: result.prUrl,
};
}
return EMPTY_PROGRESS;
}