import { DEFAULT_TERMINAL_ID, EnvironmentId, type TerminalAttachStreamEvent, ThreadId,} from "@t3tools/contracts";import type { KnownTerminalSession } from "@t3tools/client-runtime";import { SymbolView } from "expo-symbols";import { Stack, useLocalSearchParams, useRouter } from "expo-router";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { Pressable, ScrollView, Text as RNText, View, useColorScheme, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent,} from "react-native";import { KeyboardController, KeyboardEvents, KeyboardStickyView, useKeyboardState,} from "react-native-keyboard-controller"; import { EmptyState } from "../../components/EmptyState";import { GlassSurface } from "../../components/GlassSurface";import { LoadingScreen } from "../../components/LoadingScreen";import { buildThreadTerminalNavigation } from "../../lib/routes";import { getEnvironmentClient } from "../../state/environment-session-registry";import { useRemoteEnvironmentState } from "../../state/use-remote-environment-registry";import { attachTerminalSession, useKnownTerminalSessions, useTerminalSession, useTerminalSessionTarget,} from "../../state/use-terminal-session";import { useThreadSelection } from "../../state/use-thread-selection";import { useSelectedThreadDetail } from "../../state/use-thread-detail";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 { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap";import { resolveTerminalOpenLocation, stagePendingTerminalLaunch, takePendingTerminalLaunch,} 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;const TERMINAL_ACCESSORY_FADE_WIDTH = 18; 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 getTerminalStatusTone(input: { readonly status: TerminalMenuSession["status"]; readonly hasRunningSubprocess?: boolean;}): { readonly tintColor: string; readonly textColor: string;} { if (input.status === "running") { if (input.hasRunningSubprocess) { return { tintColor: "#fbbf24", textColor: "#a3a3a3", }; } return { tintColor: "#34d399", textColor: "#a3a3a3", }; } if (input.status === "starting") { return { tintColor: "#f59e0b", textColor: "#a3a3a3", }; } if (input.status === "error") { return { tintColor: "#ef4444", textColor: "#fca5a5", }; } return { tintColor: "#ef4444", textColor: "#a3a3a3", };} 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 withAlpha(hexColor: string, alpha: string): string { return /^#[0-9a-f]{6}$/i.test(hexColor) ? `${hexColor}${alpha}` : hexColor;} function pickRunningTerminalSessionForBootstrap( sessions: ReadonlyArray<KnownTerminalSession>,): 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 appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { isLoadingSavedConnection } = useRemoteEnvironmentState(); 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 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 [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 toolbarScrollGeometryRef = useRef({ contentWidth: 0, offsetX: 0, viewportWidth: 0, }); const [toolbarScrollEdges, setToolbarScrollEdges] = useState({ showLeftFade: false, showRightFade: false, }); const hasOpenedRef = useRef(false); const bufferReplayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const attachStreamLogCountRef = useRef(0); const firstNonEmptyBufferLoggedRef = useRef(false); const lastBufferReplayKeyRef = useRef<string | null>(null); const [readyBufferReplayKey, setReadyBufferReplayKey] = useState<string | null>(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 target = useTerminalSessionTarget({ environmentId: selectedThread?.environmentId ?? null, threadId: selectedThread?.id ?? null, terminalId, }); const terminal = useTerminalSession(target); 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 runningSession = useMemo( () => pickRunningTerminalSessionForBootstrap(knownSessions), [knownSessions], ); const activeKnownSession = useMemo( () => knownSessions.find((session) => session.target.terminalId === terminalId) ?? null, [knownSessions, terminalId], ); const terminalAttachLaunchHintsRef = useRef({ terminalSummary: terminal.summary, activeKnownSummary: activeKnownSession?.state.summary ?? null, }); terminalAttachLaunchHintsRef.current = { terminalSummary: terminal.summary, activeKnownSummary: activeKnownSession?.state.summary ?? null, }; const terminalStatusTone = useMemo( () => getTerminalStatusTone({ status: terminal.status, hasRunningSubprocess: terminal.hasRunningSubprocess, }), [terminal.hasRunningSubprocess, terminal.status], ); 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<ReadonlyArray<TerminalToolbarAction>>(() => { const modifierActions: ReadonlyArray<TerminalToolbarAction> = 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<ReadonlyArray<TerminalMenuSession>>( () => 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, ], ); const logAttachStreamEvent = useCallback((event: TerminalAttachStreamEvent) => { const n = ++attachStreamLogCountRef.current; if (event.type === "output" && n > 32 && n % 64 !== 0) { return; } if (event.type === "snapshot") { terminalDebugLog("attach:stream", { n, type: event.type, status: event.snapshot.status, historyLen: event.snapshot.history.length, cwd: event.snapshot.cwd, }); return; } if (event.type === "output") { terminalDebugLog("attach:stream", { n, type: event.type, dataLen: event.data.length }); return; } terminalDebugLog("attach:stream", { n, type: event.type }); }, []); const attachTerminal = useCallback(() => { if (!selectedThread || !selectedThreadProject?.workspaceRoot) { terminalDebugLog("attach:abort", { reason: "no-thread-or-workspace" });Lines 1–500 of 1334. Load more
import { DEFAULT_TERMINAL_ID, EnvironmentId, type TerminalAttachStreamEvent, ThreadId, } from "@t3tools/contracts"; import type { KnownTerminalSession } from "@t3tools/client-runtime"; import { SymbolView } from "expo-symbols"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Pressable, ScrollView, Text as RNText, View, useColorScheme, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, } from "react-native"; import { KeyboardController, KeyboardEvents, KeyboardStickyView, useKeyboardState, } from "react-native-keyboard-controller"; import { EmptyState } from "../../components/EmptyState"; import { GlassSurface } from "../../components/GlassSurface"; import { LoadingScreen } from "../../components/LoadingScreen"; import { buildThreadTerminalNavigation } from "../../lib/routes"; import { getEnvironmentClient } from "../../state/environment-session-registry"; import { useRemoteEnvironmentState } from "../../state/use-remote-environment-registry"; import { attachTerminalSession, useKnownTerminalSessions, useTerminalSession, useTerminalSessionTarget, } from "../../state/use-terminal-session"; import { useThreadSelection } from "../../state/use-thread-selection"; import { useSelectedThreadDetail } from "../../state/use-thread-detail"; 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 { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; import { resolveTerminalOpenLocation, stagePendingTerminalLaunch, takePendingTerminalLaunch, } 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; const TERMINAL_ACCESSORY_FADE_WIDTH = 18; 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 getTerminalStatusTone(input: { readonly status: TerminalMenuSession["status"]; readonly hasRunningSubprocess?: boolean; }): { readonly tintColor: string; readonly textColor: string; } { if (input.status === "running") { if (input.hasRunningSubprocess) { return { tintColor: "#fbbf24", textColor: "#a3a3a3", }; } return { tintColor: "#34d399", textColor: "#a3a3a3", }; } if (input.status === "starting") { return { tintColor: "#f59e0b", textColor: "#a3a3a3", }; } if (input.status === "error") { return { tintColor: "#ef4444", textColor: "#fca5a5", }; } return { tintColor: "#ef4444", textColor: "#a3a3a3", }; } 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 withAlpha(hexColor: string, alpha: string): string { return /^#[0-9a-f]{6}$/i.test(hexColor) ? `${hexColor}${alpha}` : hexColor; } function pickRunningTerminalSessionForBootstrap( sessions: ReadonlyArray<KnownTerminalSession>, ): 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 appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { isLoadingSavedConnection } = useRemoteEnvironmentState(); 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 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 [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 toolbarScrollGeometryRef = useRef({ contentWidth: 0, offsetX: 0, viewportWidth: 0, }); const [toolbarScrollEdges, setToolbarScrollEdges] = useState({ showLeftFade: false, showRightFade: false, }); const hasOpenedRef = useRef(false); const bufferReplayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const attachStreamLogCountRef = useRef(0); const firstNonEmptyBufferLoggedRef = useRef(false); const lastBufferReplayKeyRef = useRef<string | null>(null); const [readyBufferReplayKey, setReadyBufferReplayKey] = useState<string | null>(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 target = useTerminalSessionTarget({ environmentId: selectedThread?.environmentId ?? null, threadId: selectedThread?.id ?? null, terminalId, }); const terminal = useTerminalSession(target); 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 runningSession = useMemo( () => pickRunningTerminalSessionForBootstrap(knownSessions), [knownSessions], ); const activeKnownSession = useMemo( () => knownSessions.find((session) => session.target.terminalId === terminalId) ?? null, [knownSessions, terminalId], ); const terminalAttachLaunchHintsRef = useRef({ terminalSummary: terminal.summary, activeKnownSummary: activeKnownSession?.state.summary ?? null, }); terminalAttachLaunchHintsRef.current = { terminalSummary: terminal.summary, activeKnownSummary: activeKnownSession?.state.summary ?? null, }; const terminalStatusTone = useMemo( () => getTerminalStatusTone({ status: terminal.status, hasRunningSubprocess: terminal.hasRunningSubprocess, }), [terminal.hasRunningSubprocess, terminal.status], ); 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<ReadonlyArray<TerminalToolbarAction>>(() => { const modifierActions: ReadonlyArray<TerminalToolbarAction> = 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<ReadonlyArray<TerminalMenuSession>>( () => 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, ], ); const logAttachStreamEvent = useCallback((event: TerminalAttachStreamEvent) => { const n = ++attachStreamLogCountRef.current; if (event.type === "output" && n > 32 && n % 64 !== 0) { return; } if (event.type === "snapshot") { terminalDebugLog("attach:stream", { n, type: event.type, status: event.snapshot.status, historyLen: event.snapshot.history.length, cwd: event.snapshot.cwd, }); return; } if (event.type === "output") { terminalDebugLog("attach:stream", { n, type: event.type, dataLen: event.data.length }); return; } terminalDebugLog("attach:stream", { n, type: event.type }); }, []); const attachTerminal = useCallback(() => { if (!selectedThread || !selectedThreadProject?.workspaceRoot) { terminalDebugLog("attach:abort", { reason: "no-thread-or-workspace" });