import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import { SymbolView } from "expo-symbols"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Pressable, Text as RNText, View, useColorScheme } from "react-native"; import { KeyboardController, KeyboardEvents, KeyboardStickyView, useKeyboardState, } from "react-native-keyboard-controller"; import { ComposerToolbarButton, ComposerToolbarRow, ComposerToolbarScroller, } from "../../components/ComposerToolbarTrigger"; import { EmptyState } from "../../components/EmptyState"; import { GlassSurface } from "../../components/GlassSurface"; import { LoadingScreen } from "../../components/LoadingScreen"; import { environmentCatalog } from "../../connection/catalog"; import { useEnvironmentPresentation } from "../../state/presentation"; import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; import { buildThreadTerminalNavigation } from "../../lib/routes"; import { useAttachedTerminalSession, useKnownTerminalSessions, } from "../../state/use-terminal-session"; import { useThreadSelection } from "../../state/use-thread-selection"; import { useSelectedThreadDetail } from "../../state/use-thread-detail"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; import { TerminalSurface } from "./NativeTerminalSurface"; import { getPierreTerminalTheme } from "./terminalTheme"; import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; import { terminalDebugLog } from "./terminalDebugLog"; import { getTerminalBufferReplayKey, getTerminalSurfaceReplayBuffer, TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS, } from "./terminalBufferReplay"; import { resolveTerminalOpenLocation, takePendingTerminalLaunch, type PendingTerminalLaunch, } from "./terminalLaunchContext"; import { basename, buildTerminalMenuSessions, getTerminalStatusLabel, nextOpenTerminalId, resolveTerminalSessionLabel, type TerminalMenuSession, } from "./terminalMenu"; import { DEFAULT_TERMINAL_FONT_SIZE, MAX_TERMINAL_FONT_SIZE, MIN_TERMINAL_FONT_SIZE, TERMINAL_FONT_SIZE_STEP, normalizeTerminalFontSize, } from "./terminalPreferences"; import { cacheTerminalFontSize, cacheTerminalGridSize, getCachedTerminalFontSize, getCachedTerminalGridSize, } from "./terminalUiState"; const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; const TERMINAL_ACCESSORY_HEIGHT = 52; type PendingModifier = "ctrl" | "meta"; type HostPlatform = "mac" | "linux" | "windows" | "unknown"; type TerminalToolbarAction = | { readonly kind: "send"; readonly key: string; readonly label: string; readonly data: string } | { readonly kind: "clear"; readonly key: string; readonly label: string } | { readonly kind: "modifier"; readonly key: string; readonly label: string; readonly modifier: PendingModifier; }; function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null; } return value ?? null; } function inferHostPlatform(environmentLabel: string | null): HostPlatform { const value = environmentLabel?.toLowerCase() ?? ""; if ( value.includes("mac") || value.includes("macbook") || value.includes("mac mini") || value.includes("imac") || value.includes("darwin") ) { return "mac"; } if (value.includes("windows") || value.includes("win")) { return "windows"; } if (value.includes("linux") || value.includes("ubuntu") || value.includes("debian")) { return "linux"; } return "unknown"; } function applyCtrlModifier(input: string): string { const firstCharacter = input[0]; if (!firstCharacter) { return input; } const lowerCharacter = firstCharacter.toLowerCase(); if (lowerCharacter >= "a" && lowerCharacter <= "z") { return String.fromCharCode(lowerCharacter.charCodeAt(0) - 96); } if (firstCharacter === "@") return "\u0000"; if (firstCharacter === "[") return "\u001b"; if (firstCharacter === "\\") return "\u001c"; if (firstCharacter === "]") return "\u001d"; if (firstCharacter === "^") return "\u001e"; if (firstCharacter === "_") return "\u001f"; if (firstCharacter === "?") return "\u007f"; return input; } function pickRunningTerminalSessionForBootstrap( sessions: ReadonlyArray, ): KnownTerminalSession | null { const running = sessions.filter( (session) => session.state.status === "running" || session.state.status === "starting", ); if (running.length === 0) { return null; } return ( running.find((session) => session.target.terminalId === DEFAULT_TERMINAL_ID) ?? running[0] ?? null ); } export function ThreadTerminalRouteScreen() { const router = useRouter(); const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); const clearTerminal = useAtomCommand(terminalEnvironment.clear, "terminal clear"); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { state: workspaceState } = useWorkspaceState(); const params = useLocalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; terminalId?: string | string[]; }>(); const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); const selectedThreadDetail = useSelectedThreadDetail(); const routeEnvironmentIdRaw = firstRouteParam(params.environmentId); const routeThreadIdRaw = firstRouteParam(params.threadId); const routeEnvironmentId = routeEnvironmentIdRaw ? EnvironmentId.make(routeEnvironmentIdRaw) : null; const routeThreadId = routeThreadIdRaw ? ThreadId.make(routeThreadIdRaw) : null; const environment = useEnvironmentPresentation(routeEnvironmentId); const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const requestedTerminalId = firstRouteParam(params.terminalId); const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID; const cachedFontSize = getCachedTerminalFontSize(); const cachedRouteGridSize = routeEnvironmentId && routeThreadId ? getCachedTerminalGridSize({ environmentId: routeEnvironmentId, threadId: routeThreadId, terminalId, }) : null; const knownSessions = useKnownTerminalSessions({ environmentId: selectedThread?.environmentId ?? null, threadId: selectedThread?.id ?? null, }); const runningSession = useMemo( () => pickRunningTerminalSessionForBootstrap(knownSessions), [knownSessions], ); const activeKnownSession = useMemo( () => knownSessions.find((session) => session.target.terminalId === terminalId) ?? null, [knownSessions, terminalId], ); const launchTarget = useMemo( () => selectedThread ? { environmentId: selectedThread.environmentId, threadId: selectedThread.id, terminalId, } : null, [selectedThread, terminalId], ); const launchTargetKey = launchTarget ? `${launchTarget.environmentId}:${launchTarget.threadId}:${launchTarget.terminalId}` : null; const [pendingLaunchEntry, setPendingLaunchEntry] = useState<{ readonly key: string | null; readonly launch: PendingTerminalLaunch | null; }>(() => ({ key: launchTargetKey, launch: launchTarget === null ? null : takePendingTerminalLaunch(launchTarget), })); const pendingLaunch = pendingLaunchEntry.key === launchTargetKey ? pendingLaunchEntry.launch : null; const hasResolvedPendingLaunch = pendingLaunchEntry.key === launchTargetKey; const [initialAttachGridEntry, setInitialAttachGridEntry] = useState(() => ({ key: launchTargetKey, size: cachedRouteGridSize ?? { cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }, })); const initialAttachGridSize = initialAttachGridEntry.key === launchTargetKey ? initialAttachGridEntry.size : null; const [lastGridSize, setLastGridSize] = useState( cachedRouteGridSize ?? { cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }, ); const [fontSize, setFontSize] = useState(cachedFontSize ?? DEFAULT_TERMINAL_FONT_SIZE); const [keyboardFocusRequest, setKeyboardFocusRequest] = useState(0); const [isAccessoryDismissed, setIsAccessoryDismissed] = useState(false); const bufferReplayTimerRef = useRef | null>(null); const firstNonEmptyBufferLoggedRef = useRef(false); const lastBufferReplayKeyRef = useRef(null); const sentInitialInputKeyRef = useRef(null); const [readyBufferReplayKey, setReadyBufferReplayKey] = useState(null); const [hasResolvedFontPreference, setHasResolvedFontPreference] = useState( cachedFontSize !== null, ); /** Default grid is always valid for attach; onResize refines cols/rows. Requiring a cached size blocked bootstrap for new terminal routes. */ const [hasMeasuredSurface, setHasMeasuredSurface] = useState(true); const [pendingModifierState, setPendingModifierState] = useState<{ readonly terminalId: string; readonly value: PendingModifier | null; }>({ terminalId, value: null, }); const shouldRedirectToRunningTerminal = requestedTerminalId === null && runningSession !== null && runningSession.target.terminalId !== terminalId; const launchLocationCandidate = useMemo(() => { if (!selectedThread || !selectedThreadProject?.workspaceRoot) { return null; } if (pendingLaunch) { return { cwd: pendingLaunch.cwd, worktreePath: pendingLaunch.worktreePath, }; } return resolveTerminalOpenLocation({ terminalLocation: activeKnownSession?.state.summary ?? null, activeSessionLocation: activeKnownSession?.state.summary ?? null, workspaceRoot: selectedThreadProject.workspaceRoot, threadShellWorktreePath: selectedThread.worktreePath ?? null, threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null, }); }, [ activeKnownSession?.state.summary, pendingLaunch, selectedThread, selectedThreadDetail?.worktreePath, selectedThreadProject?.workspaceRoot, ]); const [initialLaunchLocationEntry, setInitialLaunchLocationEntry] = useState(() => ({ key: launchTargetKey, location: launchLocationCandidate, })); const launchLocation = initialLaunchLocationEntry.key === launchTargetKey ? initialLaunchLocationEntry.location : null; const terminalAttachInput = useMemo( () => selectedThread !== null && launchLocation !== null && hasResolvedPendingLaunch && initialAttachGridSize !== null && hasResolvedFontPreference && hasMeasuredSurface && isEnvironmentReady && !shouldRedirectToRunningTerminal ? { threadId: selectedThread.id, terminalId, cwd: launchLocation.cwd, worktreePath: launchLocation.worktreePath, cols: initialAttachGridSize.cols, rows: initialAttachGridSize.rows, ...(pendingLaunch?.env ? { env: pendingLaunch.env } : {}), ...(pendingLaunch ? { restartIfNotRunning: true } : {}), } : null, [ hasMeasuredSurface, hasResolvedFontPreference, hasResolvedPendingLaunch, initialAttachGridSize, isEnvironmentReady, launchLocation, pendingLaunch, selectedThread, shouldRedirectToRunningTerminal, terminalId, ], ); const terminal = useAttachedTerminalSession({ environmentId: selectedThread?.environmentId ?? null, terminal: terminalAttachInput, }); const terminalKey = selectedThread ? `${selectedThread.environmentId}:${selectedThread.id}:${terminalId}` : terminalId; const bufferReplayKey = useMemo( () => getTerminalBufferReplayKey({ terminalKey, fontSize }), [fontSize, terminalKey], ); if (lastBufferReplayKeyRef.current === null) { lastBufferReplayKeyRef.current = bufferReplayKey; } const terminalSurfaceBuffer = getTerminalSurfaceReplayBuffer({ buffer: terminal.buffer, replayKey: bufferReplayKey, readyReplayKey: readyBufferReplayKey, }); const isRunning = terminal.status === "running" || terminal.status === "starting"; useEffect(() => { terminalDebugLog("surface:props", { terminalKey, atomBufferLen: terminal.buffer.length, surfaceBufferLen: terminalSurfaceBuffer.length, replayKey: bufferReplayKey, readyReplayKey: readyBufferReplayKey, status: terminal.status, version: terminal.version, }); }, [ bufferReplayKey, readyBufferReplayKey, terminal.buffer.length, terminal.status, terminal.version, terminalKey, terminalSurfaceBuffer.length, ]); useEffect(() => { terminalDebugLog("session:status", { terminalKey, status: terminal.status, error: terminal.error, summary: terminal.summary?.cwd ?? null, bufferLen: terminal.buffer.length, version: terminal.version, }); }, [ terminal.buffer.length, terminal.error, terminal.status, terminal.summary?.cwd, terminal.version, terminalKey, ]); useEffect(() => { if (terminal.buffer.length === 0 || firstNonEmptyBufferLoggedRef.current) { return; } firstNonEmptyBufferLoggedRef.current = true; terminalDebugLog("session:first-nonempty-buffer", { terminalKey, length: terminal.buffer.length, preview: terminal.buffer.slice(0, 160), }); }, [terminal.buffer, terminal.buffer.length, terminalKey]); const cwd = terminal.summary?.cwd ?? selectedThreadProject?.workspaceRoot ?? null; const hostPlatform = useMemo( () => inferHostPlatform(selectedEnvironmentConnection?.environmentLabel ?? null), [selectedEnvironmentConnection?.environmentLabel], ); const terminalTheme = getPierreTerminalTheme(appearanceScheme); const pendingModifier = pendingModifierState.terminalId === terminalId ? pendingModifierState.value : null; const headerTitle = useMemo(() => { const topLineParts = [ selectedEnvironmentConnection?.environmentLabel ?? null, selectedThreadProject?.title ?? null, ].filter((value): value is string => Boolean(value)); return { topLine: topLineParts.join(" \u00b7 "), bottomLine: cwd ?? selectedThreadProject?.workspaceRoot ?? "", }; }, [ cwd, selectedEnvironmentConnection?.environmentLabel, selectedThreadProject?.title, selectedThreadProject?.workspaceRoot, ]); const terminalToolbarActions = useMemo>(() => { const modifierActions: ReadonlyArray = hostPlatform === "mac" ? [ { kind: "modifier", key: "cmd", label: "cmd", modifier: "meta" }, { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" }, ] : [ { kind: "modifier", key: "ctrl", label: "ctrl", modifier: "ctrl" }, { kind: "modifier", key: "alt", label: "alt", modifier: "meta" }, ]; return [ { kind: "send", key: "esc", label: "esc", data: "\u001b" }, ...modifierActions, { kind: "send", key: "tab", label: "tab", data: "\t" }, { kind: "clear", key: "clear", label: "clear" }, { kind: "send", key: "up", label: "↑", data: "\u001b[A" }, { kind: "send", key: "down", label: "↓", data: "\u001b[B" }, { kind: "send", key: "left", label: "←", data: "\u001b[D" }, { kind: "send", key: "right", label: "→", data: "\u001b[C" }, { kind: "send", key: "tilde", label: "~", data: "~" }, { kind: "send", key: "pipe", label: "|", data: "|" }, { kind: "send", key: "slash", label: "/", data: "/" }, { kind: "send", key: "dash", label: "-", data: "-" }, ]; }, [hostPlatform]); const keyboardState = useKeyboardState((state) => ({ height: state.height, isVisible: state.isVisible, })); const isAccessoryVisible = keyboardState.isVisible && !isAccessoryDismissed; const terminalBottomInset = (keyboardState.isVisible ? keyboardState.height : 0) + (isAccessoryVisible ? TERMINAL_ACCESSORY_HEIGHT : 0); useEffect(() => { const keyboardWillShow = KeyboardEvents.addListener("keyboardWillShow", () => { setIsAccessoryDismissed(false); }); const keyboardWillHide = KeyboardEvents.addListener("keyboardWillHide", () => { setIsAccessoryDismissed(true); }); return () => { keyboardWillShow.remove(); keyboardWillHide.remove(); }; }, []); const terminalMenuSessions = useMemo>( () => buildTerminalMenuSessions({ knownSessions, workspaceRoot: selectedThreadProject?.workspaceRoot ?? null, currentSession: { terminalId, cwd: cwd ?? null, status: terminal.status, hasRunningSubprocess: terminal.hasRunningSubprocess, displayLabel: resolveTerminalSessionLabel(terminalId, terminal.summary), updatedAt: terminal.updatedAt, }, }), [ cwd, knownSessions, selectedThreadProject?.workspaceRoot, terminal.hasRunningSubprocess, terminal.summary, terminal.status, terminal.updatedAt, terminalId, ], ); useEffect(() => { if (pendingLaunchEntry.key === launchTargetKey) { return; } setPendingLaunchEntry({ key: launchTargetKey, launch: launchTarget === null ? null : takePendingTerminalLaunch(launchTarget), }); }, [launchTarget, launchTargetKey, pendingLaunchEntry.key]); useEffect(() => { if (initialAttachGridEntry.key === launchTargetKey) { return; } setInitialAttachGridEntry({ key: launchTargetKey, size: cachedRouteGridSize ?? { cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }, }); }, [cachedRouteGridSize, initialAttachGridEntry.key, launchTargetKey]); useEffect(() => { if ( initialLaunchLocationEntry.key === launchTargetKey && initialLaunchLocationEntry.location !== null ) { return; } if (initialLaunchLocationEntry.key === launchTargetKey && launchLocationCandidate === null) { return; } setInitialLaunchLocationEntry({ key: launchTargetKey, location: launchLocationCandidate, }); }, [ initialLaunchLocationEntry.key, initialLaunchLocationEntry.location, launchLocationCandidate, launchTargetKey, ]); useEffect(() => { if (!shouldRedirectToRunningTerminal || !selectedThread || !runningSession) { return; } router.replace(buildThreadTerminalNavigation(selectedThread, runningSession.target.terminalId)); }, [router, runningSession, selectedThread, shouldRedirectToRunningTerminal]); useEffect(() => { const initialInput = pendingLaunch?.initialInput; if ( !initialInput || !selectedThread || terminal.version === 0 || sentInitialInputKeyRef.current === launchTargetKey ) { return; } sentInitialInputKeyRef.current = launchTargetKey; void writeTerminal({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, terminalId, data: initialInput, }, }); }, [ launchTargetKey, pendingLaunch?.initialInput, selectedThread, terminal.version, terminalId, writeTerminal, ]); useEffect(() => { firstNonEmptyBufferLoggedRef.current = false; sentInitialInputKeyRef.current = null; }, [terminalKey]); const clearBufferReplayTimer = useCallback(() => { if (bufferReplayTimerRef.current !== null) { clearTimeout(bufferReplayTimerRef.current); bufferReplayTimerRef.current = null; } }, []); const scheduleBufferReplayReady = useCallback(() => { clearBufferReplayTimer(); const replayKey = bufferReplayKey; terminalDebugLog("replay:schedule-ready", { replayKey, delayMs: TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS, }); bufferReplayTimerRef.current = setTimeout(() => { bufferReplayTimerRef.current = null; setReadyBufferReplayKey(replayKey); terminalDebugLog("replay:ready", { replayKey }); }, TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS); }, [bufferReplayKey, clearBufferReplayTimer]); useEffect(() => { if (lastBufferReplayKeyRef.current === bufferReplayKey) { return; } lastBufferReplayKeyRef.current = bufferReplayKey; clearBufferReplayTimer(); setReadyBufferReplayKey(null); }, [bufferReplayKey, clearBufferReplayTimer]); useEffect(() => clearBufferReplayTimer, [clearBufferReplayTimer]); useEffect(() => { if (!routeEnvironmentId || !routeThreadId) { setLastGridSize({ cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }); return; } setLastGridSize( getCachedTerminalGridSize({ environmentId: routeEnvironmentId, threadId: routeThreadId, terminalId, }) ?? { cols: DEFAULT_TERMINAL_COLS, rows: DEFAULT_TERMINAL_ROWS, }, ); setHasMeasuredSurface(true); }, [routeEnvironmentId, routeThreadId, terminalId]); useEffect(() => { let cancelled = false; void loadPreferences() .then((preferences) => { if (cancelled) { return; } setFontSize(cacheTerminalFontSize(preferences.terminalFontSize)); setHasResolvedFontPreference(true); }) .catch(() => { if (cancelled) { return; } setHasResolvedFontPreference(true); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (!hasResolvedFontPreference) { return; } cacheTerminalFontSize(fontSize); void savePreferencesPatch({ terminalFontSize: normalizeTerminalFontSize(fontSize), }); }, [fontSize, hasResolvedFontPreference]); const writeInput = useCallback( (data: string) => { if (!selectedThread || !isRunning) { return; } void writeTerminal({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, terminalId, data, }, }); }, [isRunning, selectedThread, terminalId, writeTerminal], ); const handleInput = useCallback( (data: string) => { if (data.length === 0) { return; } if (pendingModifier === "ctrl") { setPendingModifierState({ terminalId, value: null }); writeInput(applyCtrlModifier(data)); } else if (pendingModifier === "meta") { setPendingModifierState({ terminalId, value: null }); writeInput(`\u001b${data}`); } else { writeInput(data); } }, [pendingModifier, terminalId, writeInput], ); const handleResize = useCallback( (size: { readonly cols: number; readonly rows: number }) => { terminalDebugLog("native:onResize", { cols: size.cols, rows: size.rows, terminalKey, }); setHasMeasuredSurface(true); if (readyBufferReplayKey !== bufferReplayKey) { scheduleBufferReplayReady(); } if (routeEnvironmentId && routeThreadId) { cacheTerminalGridSize( { environmentId: routeEnvironmentId, threadId: routeThreadId, terminalId, }, size, ); } if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { return; } setLastGridSize(size); if (!selectedThread || !isRunning) { return; } void resizeTerminal({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, terminalId, cols: size.cols, rows: size.rows, }, }); }, [ isRunning, lastGridSize.cols, lastGridSize.rows, bufferReplayKey, readyBufferReplayKey, routeEnvironmentId, routeThreadId, resizeTerminal, scheduleBufferReplayReady, selectedThread, terminalId, terminalKey, ], ); const handleSelectTerminal = useCallback( (nextTerminalId: string) => { if (!selectedThread || nextTerminalId === terminalId) { return; } router.replace(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); }, [router, selectedThread, terminalId], ); const handleOpenNewTerminal = useCallback(() => { if (!selectedThread) { return; } router.replace( buildThreadTerminalNavigation( selectedThread, nextOpenTerminalId({ listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), activeRouteTerminalId: terminalId, }), ), ); }, [router, selectedThread, terminalId, terminalMenuSessions]); const adjustFontSize = useCallback((delta: number) => { setTimeout(() => { setFontSize((current) => cacheTerminalFontSize(current + delta)); }, 0); }, []); const handleDecreaseFontSize = useCallback(() => { adjustFontSize(-TERMINAL_FONT_SIZE_STEP); }, [adjustFontSize]); const handleIncreaseFontSize = useCallback(() => { adjustFontSize(TERMINAL_FONT_SIZE_STEP); }, [adjustFontSize]); const handleClearTerminal = useCallback(() => { if (!selectedThread) { return; } setPendingModifierState({ terminalId, value: null }); void clearTerminal({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, terminalId, }, }); }, [clearTerminal, selectedThread, terminalId]); const handleToolbarActionPress = useCallback( (action: TerminalToolbarAction) => { if (action.kind === "modifier") { setPendingModifierState((current) => ({ terminalId, value: (current.terminalId === terminalId ? current.value : null) === action.modifier ? null : action.modifier, })); return; } if (action.kind === "clear") { handleClearTerminal(); return; } setPendingModifierState({ terminalId, value: null }); if (pendingModifier === "ctrl") { writeInput(applyCtrlModifier(action.data)); } else if (pendingModifier === "meta") { writeInput(`\u001b${action.data}`); } else { writeInput(action.data); } }, [handleClearTerminal, pendingModifier, terminalId, writeInput], ); const handleDismissKeyboard = useCallback(() => { setIsAccessoryDismissed(true); void KeyboardController.dismiss(); }, []); const handleShowKeyboard = useCallback(() => { setKeyboardFocusRequest((current) => current + 1); }, []); const handleRetryEnvironment = useCallback(() => { if (routeEnvironmentId !== null) { void retryEnvironment(routeEnvironmentId); } }, [retryEnvironment, routeEnvironmentId]); if (!selectedThread) { if (workspaceState.isLoadingConnections) { return ; } return ( ); } if (!selectedThreadProject?.workspaceRoot) { return ( ); } if (!environment.isReady && environment.presentation === null) { return ; } return ( <> ( {headerTitle.topLine} {headerTitle.bottomLine} ), }} /> {isEnvironmentReady ? ( {getTerminalStatusLabel({ status: terminal.status, hasRunningSubprocess: terminal.hasRunningSubprocess, })} Text size {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} = MAX_TERMINAL_FONT_SIZE} discoverabilityLabel="Increase terminal text size" onPress={handleIncreaseFontSize} > {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} {terminalMenuSessions.map((session) => ( handleSelectTerminal(session.terminalId)} subtitle={[ getTerminalStatusLabel({ status: session.status }), basename(session.cwd), ] .filter(Boolean) .join(" · ")} > {session.displayLabel} ))} Open new terminal ) : null} {!isEnvironmentReady ? ( ) : ( <> {isAccessoryVisible ? ( {terminalToolbarActions.map((action) => { const active = action.kind === "modifier" && pendingModifier === action.modifier; return ( 1 ? 56 : 44} onPress={() => handleToolbarActionPress(action)} showChevron={false} textTransform={ action.kind === "modifier" || action.kind === "clear" ? "uppercase" : "none" } /> ); })} ) : !keyboardState.isVisible ? ( ({ bottom: 16, borderRadius: 28, opacity: pressed ? 0.72 : 1, position: "absolute", right: 16, })} > ) : null} )} ); }