import { DEFAULT_TERMINAL_ID, type EnvironmentId, type ThreadId } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { Pressable, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { getEnvironmentClient } from "../../state/environment-session-registry"; import { attachTerminalSession, useTerminalSession, useTerminalSessionTarget, } from "../../state/use-terminal-session"; import { TerminalSurface } from "./NativeTerminalSurface"; import { hasNativeTerminalSurface } from "./nativeTerminalModule"; import { terminalDebugLog } from "./terminalDebugLog"; 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 nativeTerminalAvailable = hasNativeTerminalSurface(); const terminalId = DEFAULT_TERMINAL_ID; const target = useTerminalSessionTarget({ environmentId: props.environmentId, threadId: props.threadId, terminalId, }); const terminal = useTerminalSession(target); const [lastGridSize, setLastGridSize] = useState({ cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }); const lastGridSizeRef = useRef(lastGridSize); lastGridSizeRef.current = lastGridSize; const terminalKey = `${props.environmentId}:${props.threadId}:${terminalId}`; const isRunning = terminal.status === "running" || terminal.status === "starting"; useEffect(() => { if (!props.visible) { return; } const client = getEnvironmentClient(props.environmentId); if (!client) { terminalDebugLog("panel:attach-skip", { reason: "no-environment-client", environmentId: props.environmentId, }); return; } terminalDebugLog("panel:attach", { environmentId: props.environmentId, threadId: props.threadId, terminalId, }); return attachTerminalSession({ environmentId: props.environmentId, client, terminal: { threadId: props.threadId, terminalId, cwd: props.cwd, worktreePath: props.worktreePath, cols: lastGridSizeRef.current.cols, rows: lastGridSizeRef.current.rows, }, }); }, [ props.cwd, props.environmentId, props.threadId, props.worktreePath, props.visible, terminalId, ]); const handleInput = useCallback( (data: string) => { const client = getEnvironmentClient(props.environmentId); if (!client || !isRunning) { return; } void client.terminal.write({ threadId: props.threadId, terminalId, data, }); }, [isRunning, props.environmentId, props.threadId, terminalId], ); const handleResize = useCallback( (size: { readonly cols: number; readonly rows: number }) => { if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { return; } setLastGridSize(size); const client = getEnvironmentClient(props.environmentId); if (!client || !isRunning) { return; } void client.terminal.resize({ threadId: props.threadId, terminalId, cols: size.cols, rows: size.rows, }); }, [ isRunning, lastGridSize.cols, lastGridSize.rows, props.environmentId, props.threadId, terminalId, ], ); if (!props.visible) { return null; } return ( Terminal {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"} {terminal.error ? ( {terminal.error} ) : null} ); });