2025-04-16 12:56:08 -04:00
|
|
|
|
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";
|
|
|
|
|
|
|
2025-04-18 18:13:34 -07:00
|
|
|
|
// 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.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
export default function TerminalChatInputThinking({
|
|
|
|
|
|
onInterrupt,
|
|
|
|
|
|
active,
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}: {
|
|
|
|
|
|
onInterrupt: () => void;
|
|
|
|
|
|
active: boolean;
|
2025-04-18 18:13:34 -07:00
|
|
|
|
thinkingSeconds: number;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}): React.ReactElement {
|
|
|
|
|
|
const [awaitingConfirm, setAwaitingConfirm] = useState(false);
|
2025-04-18 18:13:34 -07:00
|
|
|
|
const [dots, setDots] = useState("");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-18 18:13:34 -07:00
|
|
|
|
// Animate the ellipsis
|
|
|
|
|
|
useInterval(() => {
|
|
|
|
|
|
setDots((prev) => (prev.length < 3 ? prev + "." : ""));
|
|
|
|
|
|
}, 500);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
2025-04-18 18:13:34 -07:00
|
|
|
|
// No timers required beyond tracking the elapsed seconds supplied via props.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-04-18 18:13:34 -07:00
|
|
|
|
// 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`;
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return (
|
|
|
|
|
|
<Box flexDirection="column" gap={1}>
|
|
|
|
|
|
<Box gap={2}>
|
2025-04-18 18:13:34 -07:00
|
|
|
|
<Text>{frameWithSeconds}</Text>
|
2025-04-16 12:56:08 -04:00
|
|
|
|
<Text>
|
2025-04-18 18:13:34 -07:00
|
|
|
|
Thinking
|
2025-04-16 12:56:08 -04:00
|
|
|
|
{dots}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
{awaitingConfirm && (
|
|
|
|
|
|
<Text dimColor>
|
|
|
|
|
|
Press <Text bold>Esc</Text> again to interrupt and enter a new
|
|
|
|
|
|
instruction
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|