import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useFocusEffect } from "@react-navigation/native";
import {
ActivityIndicator,
Pressable,
ScrollView,
Text,
View,
} from "react-native";
import { Plus, X } from "lucide-react-native";
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated";
import Svg, {
Defs,
LinearGradient as SvgLinearGradient,
Rect,
Stop,
} from "react-native-svg";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import type { ListTerminalsResponse } from "@server/shared/messages";
import { encodeTerminalKeyInput } from "@server/shared/terminal-key-input";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import {
hasPendingTerminalModifiers,
normalizeTerminalTransportKey,
resolvePendingModifierDataInput,
} from "@/utils/terminal-keys";
import { upsertTerminalListEntry } from "@/utils/terminal-list";
import { confirmDialog } from "@/utils/confirm-dialog";
import {
TerminalOutputPump,
type TerminalOutputChunk,
} from "@/terminal/runtime/terminal-output-pump";
import { TerminalOutputDeliveryQueue } from "@/terminal/runtime/terminal-output-delivery-queue";
import {
TerminalStreamController,
type TerminalStreamControllerStatus,
} from "@/terminal/runtime/terminal-stream-controller";
import {
summarizeTerminalText,
terminalDebugLog,
} from "@/terminal/runtime/terminal-debug";
import { usePanelStore } from "@/stores/panel-store";
import TerminalEmulator from "./terminal-emulator";
interface TerminalPaneProps {
serverId: string;
cwd: string;
selectedTerminalId?: string | null;
onSelectedTerminalIdChange?: (terminalId: string | null) => void;
hideHeader?: boolean;
manageTerminalDirectorySubscription?: boolean;
}
const MAX_OUTPUT_CHARS = 200_000;
const TERMINAL_TAB_MAX_WIDTH = 220;
const TERMINAL_REFIT_DELAYS_MS = [0, 48, 144, 320];
const MODIFIER_LABELS = {
ctrl: "Ctrl",
shift: "Shift",
alt: "Alt",
} as const;
const KEY_BUTTONS: Array<{ id: string; label: string; key: string }> = [
{ id: "esc", label: "Esc", key: "Escape" },
{ id: "tab", label: "Tab", key: "Tab" },
{ id: "up", label: "↑", key: "ArrowUp" },
{ id: "down", label: "↓", key: "ArrowDown" },
{ id: "left", label: "←", key: "ArrowLeft" },
{ id: "right", label: "→", key: "ArrowRight" },
{ id: "enter", label: "Enter", key: "Enter" },
{ id: "backspace", label: "⌫", key: "Backspace" },
{ id: "c", label: "C", key: "c" },
];
type ModifierState = {
ctrl: boolean;
shift: boolean;
alt: boolean;
};
type TerminalOutputChunkState = {
sequence: number;
text: string;
};
type PendingTerminalInput =
| {
type: "data";
data: string;
}
| {
type: "key";
input: {
key: string;
ctrl: boolean;
shift: boolean;
alt: boolean;
meta?: boolean;
};
};
type ListTerminalsPayload = ListTerminalsResponse["payload"];
const EMPTY_MODIFIERS: ModifierState = {
ctrl: false,
shift: false,
alt: false,
};
function terminalScopeKey(input: { serverId: string; cwd: string }): string {
return `${input.serverId}:${input.cwd}`;
}
function TerminalCloseGradient({ color, gradientId }: { color: string; gradientId: string }) {
return (
);
}
export function TerminalPane({
serverId,
cwd,
selectedTerminalId: selectedTerminalIdProp,
onSelectedTerminalIdChange,
hideHeader = false,
manageTerminalDirectorySubscription = true,
}: TerminalPaneProps) {
const { theme } = useUnistyles();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const openAgentList = usePanelStore((state) => state.openAgentList);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
const swipeGesturesEnabled = isMobile && mobileView === "agent";
const { shift: keyboardShift, style: keyboardPaddingStyle } = useKeyboardShiftStyle({
mode: "padding",
enabled: isMobile,
});
const queryClient = useQueryClient();
const { client, isConnected } = useHostRuntimeSession(serverId);
const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]);
const terminalsQueryKey = useMemo(() => ["terminals", serverId, cwd] as const, [cwd, serverId]);
const selectedTerminalByScopeRef = useRef