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}): LegacyTerminalSessionState {
32 const canRead = useEnvironmentScope(input.environmentId, AuthTerminalReadScope);
33 const canOperate = useEnvironmentScope(input.environmentId, AuthTerminalOperateScope);
34 const attach = useEnvironmentQuery(
35 input.environmentId !== null && input.terminal !== null
36 ? canOperate
37 ? terminalEnvironment.attach({
38 environmentId: input.environmentId,
39 input: input.terminal,
40 })
41 : canRead
42 ? terminalEnvironment.observe({
43 environmentId: input.environmentId,
44 input: { threadId: input.terminal.threadId, terminalId: input.terminal.terminalId },
45 })
46 : null
47 : null,
48 );
49 const metadata = useEnvironmentQuery(
50 input.environmentId === null || !canRead
51 ? null
52 : terminalEnvironment.metadata({
53 environmentId: input.environmentId,
54 input: null,
55 }),
56 );
57 const output = attach.data?.output ?? EMPTY_TERMINAL_BUFFER_STATE.output;
58 // Installed native binaries still accept initialBuffer. Keep materialization
59 // at this mobile boundary until the native streaming API is released.
60 const buffer = useMemo(() => terminalOutputText(output), [output]);
61
62 return useMemo(() => {
63 if (input.environmentId === null || input.terminal === null) {
64 return EMPTY_LEGACY_TERMINAL_SESSION_STATE;
65 }
66 const summary =
67 metadata.data?.find(
68 (terminal) =>
69 terminal.threadId === input.terminal?.threadId &&
70 terminal.terminalId === input.terminal?.terminalId,
71 ) ?? null;
72 const state = {
73 ...combineTerminalSessionState(summary, attach.data ?? EMPTY_TERMINAL_BUFFER_STATE),
74 buffer,
75 };
76 return attach.error === null ? state : { ...state, error: attach.error, status: "error" };
77 }, [attach.data, attach.error, buffer, input.environmentId, input.terminal, metadata.data]);
78}
79
80export function useKnownTerminalSessions(input: {
81 readonly environmentId: EnvironmentId | null;
82 readonly threadId: ThreadId | null;
83}) {
84 const canRead = useEnvironmentScope(input.environmentId, AuthTerminalReadScope);
85 const metadata = useEnvironmentQuery(
86 input.environmentId === null || !canRead
87 ? null
88 : terminalEnvironment.metadata({
89 environmentId: input.environmentId,
90 input: null,
91 }),
92 );
93 const sessions = useMemo((): ReadonlyArray<KnownTerminalSession> | null => {
94 if (input.environmentId === null || metadata.data === null || metadata.error !== null) {
95 return null;
96 }
97 return metadata.data
98 .filter((summary) => input.threadId === null || summary.threadId === input.threadId)
99 .map((summary) => ({
100 target: {
101 environmentId: input.environmentId!,
102 threadId: ThreadId.make(summary.threadId),
103 terminalId: summary.terminalId,
104 },
105 state: combineTerminalSessionState(summary, EMPTY_TERMINAL_BUFFER_STATE),
106 }))
107 .sort((left, right) =>
108 left.target.terminalId.localeCompare(right.target.terminalId, undefined, {
109 numeric: true,
110 }),
111 );
112 }, [input.environmentId, input.threadId, metadata.data, metadata.error]);
113 return { sessions, isPending: metadata.isPending, error: metadata.error };
114}