import { DEFAULT_TERMINAL_ID, type EnvironmentId, type ThreadId } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { memo, useCallback, useEffect, useMemo, useRef } from "react"; import { Pressable, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useAttachedTerminalSession } from "../../state/use-terminal-session"; import { TerminalSurface } from "./NativeTerminalSurface"; import { hasNativeTerminalSurface } from "./nativeTerminalModule"; import { buildThreadTerminalAttachInput, type TerminalGridSize, type ThreadTerminalSubscriptionIdentity, } from "./threadTerminalPanelModel"; interface ThreadTerminalPanelProps { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly cwd: string; readonly worktreePath: string | null; readonly visible: boolean; readonly onClose: () => void; } const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( props: ThreadTerminalPanelProps, ) { const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); const nativeTerminalAvailable = hasNativeTerminalSurface(); const terminalId = DEFAULT_TERMINAL_ID; const lastGridSizeRef = useRef({ cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }); const subscriptionIdentity = useMemo( () => ({ environmentId: props.environmentId, threadId: props.threadId, terminalId, cwd: props.cwd, worktreePath: props.worktreePath, }), [props.cwd, props.environmentId, props.threadId, props.worktreePath, terminalId], ); const attachInput = useMemo( () => props.visible ? buildThreadTerminalAttachInput(subscriptionIdentity, lastGridSizeRef.current) : null, [props.visible, subscriptionIdentity], ); const terminal = useAttachedTerminalSession({ environmentId: props.environmentId, terminal: attachInput, }); const terminalKey = `${props.environmentId}:${props.threadId}:${terminalId}`; const isRunning = terminal.status === "running" || terminal.status === "starting"; const sendResize = useCallback( (size: TerminalGridSize) => { void resizeTerminal({ environmentId: props.environmentId, input: { threadId: props.threadId, terminalId, cols: size.cols, rows: size.rows, }, }); }, [props.environmentId, props.threadId, resizeTerminal, terminalId], ); useEffect(() => { if (isRunning) { sendResize(lastGridSizeRef.current); } }, [isRunning, sendResize]); const handleInput = useCallback( (data: string) => { if (!isRunning) { return; } void writeTerminal({ environmentId: props.environmentId, input: { threadId: props.threadId, terminalId, data, }, }); }, [isRunning, props.environmentId, props.threadId, terminalId, writeTerminal], ); const handleResize = useCallback( (size: TerminalGridSize) => { const previousSize = lastGridSizeRef.current; if (size.cols === previousSize.cols && size.rows === previousSize.rows) { return; } lastGridSizeRef.current = size; if (!isRunning) { return; } sendResize(size); }, [isRunning, sendResize], ); if (!props.visible) { return null; } return ( Terminal {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"} {terminal.error ? ( {terminal.error} ) : null} ); });