import { log, isLoggingEnabled } from "../../utils/agent/log.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; // Retaining a single static placeholder text for potential future use. The // more elaborate randomised thinking prompts were removed to streamline the // UI – the elapsed‑time counter now provides sufficient feedback. export default function TerminalChatInputThinking({ onInterrupt, active, thinkingSeconds, }: { onInterrupt: () => void; active: boolean; thinkingSeconds: number; }): React.ReactElement { const [awaitingConfirm, setAwaitingConfirm] = useState(false); const [dots, setDots] = useState(""); // Animate the ellipsis useInterval(() => { setDots((prev) => (prev.length < 3 ? prev + "." : "")); }, 500); const { stdin, setRawMode } = useStdin(); React.useEffect(() => { if (!active) { return; } setRawMode?.(true); const onData = (data: Buffer | string) => { if (awaitingConfirm) { return; } const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { if (isLoggingEnabled()) { log( "raw stdin: received collapsed ESC ESC – starting confirmation timer", ); } setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } }; stdin?.on("data", onData); return () => { stdin?.off("data", onData); }; }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); // No timers required beyond tracking the elapsed seconds supplied via props. useInput( (_input, key) => { if (!key.escape) { return; } if (awaitingConfirm) { if (isLoggingEnabled()) { log("useInput: second ESC detected – triggering onInterrupt()"); } onInterrupt(); setAwaitingConfirm(false); } else { if (isLoggingEnabled()) { log("useInput: first ESC detected – waiting for confirmation"); } setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } }, { isActive: active }, ); // Custom ball animation including the elapsed seconds const ballFrames = [ "( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )", "(● )", ]; const [frame, setFrame] = useState(0); useInterval(() => { setFrame((idx) => (idx + 1) % ballFrames.length); }, 80); // Preserve the spinner (ball) animation while keeping the elapsed seconds // text static. We achieve this by rendering the bouncing ball inside the // parentheses and appending the seconds counter *after* the spinner rather // than injecting it directly next to the ball (which caused the counter to // move horizontally together with the ball). const frameTemplate = ballFrames[frame] ?? ballFrames[0]; const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; return ( {frameWithSeconds} Thinking {dots} {awaitingConfirm && ( Press Esc again to interrupt and enter a new instruction )} ); }