1import type { TerminalSessionState } from "@t3tools/client-runtime/state/terminal";
2import { useEffect, useEffectEvent, useRef } from "react";
3
4/** Keep observed history passive while preserving explicit terminal visits and live exits. */
5export function useTerminalLifecycle({
6 terminalKey,
7 canOperate,
8 observing,
9 attached,
10 terminal,
11 reopen,
12 onRunning,
13 onExit,
14}: {
15 readonly terminalKey: string;
16 readonly canOperate: boolean;
17 readonly observing: boolean;
18 readonly attached: boolean;
19 readonly terminal: Pick<TerminalSessionState, "status" | "version">;
20 readonly reopen: () => Promise<boolean>;
21 readonly onRunning: () => void;
22 readonly onExit: () => void;
23}) {
24 const lifecycle = useRef({
25 key: terminalKey,
26 wasRunning: false,
27 observed: false,
28 reopened: false,
29 });
30 const reopenTerminal = useEffectEvent(reopen);
31 const handleRunning = useEffectEvent(onRunning);
32 const handleExit = useEffectEvent(onExit);
33
34 useEffect(() => {
35 if (lifecycle.current.key !== terminalKey) {
36 lifecycle.current = {
37 key: terminalKey,
38 wasRunning: false,
39 observed: false,
40 reopened: false,
41 };
42 }
43 const current = lifecycle.current;
44 if (!canOperate) {
45 // An observe request stays passive before its first snapshot arrives.
46 if (observing || terminal.version > 0 || current.wasRunning) current.observed = true;
47 current.wasRunning = false;
48 return;
49 }
50 if (!attached) {
51 current.wasRunning = false;
52 return;
53 }
54 // An attachment without its first snapshot has no known process status yet.
55 if (terminal.version === 0) return;
56
57 if (terminal.status === "running" || terminal.status === "starting") {
58 current.wasRunning = true;
59 current.reopened = false;
60 handleRunning();
61 return;
62 }
63 if (terminal.status !== "closed" && terminal.status !== "exited") return;
64
65 if (current.wasRunning) {
66 current.wasRunning = false;
67 current.reopened = true;
68 handleExit();
69 return;
70 }
71 // An explicit visit can reopen a cached ended session. Granting a viewer
72 // operate permission must leave the history they were reading intact.
73 if (current.observed || current.reopened) return;
74 current.reopened = true;
75 void reopenTerminal().then((succeeded) => {
76 if (!succeeded && lifecycle.current === current) current.reopened = false;
77 });
78 }, [attached, canOperate, observing, terminal.status, terminal.version, terminalKey]);
79}