1import {
2 combineTerminalSessionState,
3 EMPTY_TERMINAL_BUFFER_STATE,
4 EMPTY_TERMINAL_SESSION_STATE,
5 terminalOutputText,
6 type KnownTerminalSession,
7 type TerminalSessionState,
8} from "@t3tools/client-runtime/state/terminal";
9import {
10 AuthTerminalReadScope,
11 AuthTerminalOperateScope,
12 ThreadId,
13 type EnvironmentId,
14 type TerminalAttachInput,
15} from "@t3tools/contracts";
16import { useMemo } from "react";
17
18import { useEnvironmentQuery } from "./query";
19import { terminalEnvironment } from "./terminal";
20import { useEnvironmentScope } from "./session";
21
22type LegacyTerminalSessionState = TerminalSessionState & { readonly buffer: string };
23const EMPTY_LEGACY_TERMINAL_SESSION_STATE: LegacyTerminalSessionState = {
24 ...EMPTY_TERMINAL_SESSION_STATE,
25 buffer: "",
26};
27
28export function useAttachedTerminalSession(input: {
29 readonly environmentId: EnvironmentId | null;
30 readonly terminal: TerminalAttachInput | null;
31 readonly materializeBuffer?: boolean;
32}): LegacyTerminalSessionState {
33 const canRead = useEnvironmentScope(input.environmentId, AuthTerminalReadScope);
34 const canOperate = useEnvironmentScope(input.environmentId, AuthTerminalOperateScope);
35 const attach = useEnvironmentQuery(
36 input.environmentId !== null && input.terminal !== null
37 ? canOperate
38 ? terminalEnvironment.attach({
39 environmentId: input.environmentId,
40 input: input.terminal,
41 })
42 : canRead
43 ? terminalEnvironment.observe({
44 environmentId: input.environmentId,
45 input: { threadId: input.terminal.threadId, terminalId: input.terminal.terminalId },
46 })
47 : null
48 : null,
49 );
50 const metadata = useEnvironmentQuery(
51 input.environmentId === null || !canRead
52 ? null
53 : terminalEnvironment.metadata({
54 environmentId: input.environmentId,
55 input: null,
56 }),
57 );
58 const output = attach.data?.output ?? EMPTY_TERMINAL_BUFFER_STATE.output;
59 const materializeBuffer = input.materializeBuffer ?? true;
60 // Older installed binaries and the text fallback still need initialBuffer.
61 const buffer = materializeBuffer ? terminalOutputText(output) : "";
62
63 return useMemo(() => {
64 if (input.environmentId === null || input.terminal === null) {
65 return EMPTY_LEGACY_TERMINAL_SESSION_STATE;
66 }
67 const summary =
68 metadata.data?.find(
69 (terminal) =>
70 terminal.threadId === input.terminal?.threadId &&
71 terminal.terminalId === input.terminal?.terminalId,
72 ) ?? null;
73 const state = {
74 ...combineTerminalSessionState(summary, attach.data ?? EMPTY_TERMINAL_BUFFER_STATE),
75 buffer,
76 };
77 return attach.error === null ? state : { ...state, error: attach.error, status: "error" };
78 }, [attach.data, attach.error, buffer, input.environmentId, input.terminal, metadata.data]);
79}
80
81export function useKnownTerminalSessions(input: {
82 readonly environmentId: EnvironmentId | null;
83 readonly threadId: ThreadId | null;
84}) {
85 const canRead = useEnvironmentScope(input.environmentId, AuthTerminalReadScope);
86 const metadata = useEnvironmentQuery(
87 input.environmentId === null || !canRead
88 ? null
89 : terminalEnvironment.metadata({
90 environmentId: input.environmentId,
91 input: null,
92 }),
93 );
94 const sessions = useMemo((): ReadonlyArray<KnownTerminalSession> | null => {
95 if (input.environmentId === null || metadata.data === null || metadata.error !== null) {
96 return null;
97 }
98 return metadata.data
99 .filter((summary) => input.threadId === null || summary.threadId === input.threadId)
100 .map((summary) => ({
101 target: {
102 environmentId: input.environmentId!,
103 threadId: ThreadId.make(summary.threadId),
104 terminalId: summary.terminalId,
105 },
106 state: combineTerminalSessionState(summary, EMPTY_TERMINAL_BUFFER_STATE),
107 }))
108 .sort((left, right) =>
109 left.target.terminalId.localeCompare(right.target.terminalId, undefined, {
110 numeric: true,
111 }),
112 );
113 }, [input.environmentId, input.threadId, metadata.data, metadata.error]);
114 return { sessions, isPending: metadata.isPending, error: metadata.error };
115}