fix: bash mode input locking, ESC cancellation, and no timeout (#642)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-22 14:55:16 -08:00
committed by GitHub
parent 7eb576c626
commit 8d1ad50142
5 changed files with 104 additions and 25 deletions

View File

@@ -1,5 +1,6 @@
import { Box, Text } from "ink";
import { memo } from "react";
import { INTERRUPTED_BY_USER } from "../../constants";
import type { StreamingState } from "../helpers/accumulator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { BlinkDot } from "./BlinkDot.js";
@@ -60,13 +61,26 @@ export const BashCommandMessage = memo(
{/* Streaming output during execution */}
{line.phase === "running" && line.streaming && (
<StreamingOutputDisplay streaming={line.streaming} />
<StreamingOutputDisplay
streaming={line.streaming}
showInterruptHint
/>
)}
{/* Full output after completion (no collapse for bash mode) */}
{line.phase === "finished" && line.output && (
<CollapsedOutputDisplay output={line.output} maxLines={Infinity} />
)}
{line.phase === "finished" &&
line.output &&
(line.output === INTERRUPTED_BY_USER ? (
// Red styling for interrupted commands (LET-7199)
<Box flexDirection="row">
<Box width={5} flexShrink={0}>
<Text>{" ⎿ "}</Text>
</Box>
<Text color={colors.status.interrupt}>{INTERRUPTED_BY_USER}</Text>
</Box>
) : (
<CollapsedOutputDisplay output={line.output} maxLines={Infinity} />
))}
{/* Fallback: show output when phase is undefined (legacy bash commands before streaming) */}
{!line.phase && line.output && (

View File

@@ -122,6 +122,8 @@ export function Input({
thinkingMessage,
onSubmit,
onBashSubmit,
bashRunning = false,
onBashInterrupt,
permissionMode: externalMode,
onPermissionModeChange,
onExit,
@@ -149,6 +151,8 @@ export function Input({
thinkingMessage: string;
onSubmit: (message?: string) => Promise<{ submitted: boolean }>;
onBashSubmit?: (command: string) => Promise<void>;
bashRunning?: boolean;
onBashInterrupt?: () => void;
permissionMode?: PermissionMode;
onPermissionModeChange?: (mode: PermissionMode) => void;
onExit?: () => void;
@@ -289,7 +293,13 @@ export function Input({
if (onEscapeCancel) return;
if (key.escape) {
// When streaming, use Esc to interrupt
// When bash command running, use Esc to interrupt (LET-7199)
if (bashRunning && onBashInterrupt) {
onBashInterrupt();
return;
}
// When agent streaming, use Esc to interrupt
if (streaming && onInterrupt && !interruptRequested) {
onInterrupt();
// Don't load queued messages into input - let the dequeue effect
@@ -609,6 +619,9 @@ export function Input({
if (isBashMode) {
if (!previousValue.trim()) return;
// Input locking - don't accept new commands while one is running (LET-7199)
if (bashRunning) return;
// Add to history if not empty and not a duplicate of the last entry
if (previousValue.trim() !== history[history.length - 1]) {
setHistory([...history, previousValue]);

View File

@@ -4,6 +4,8 @@ import type { StreamingState } from "../helpers/accumulator";
interface StreamingOutputDisplayProps {
streaming: StreamingState;
/** Show "(esc to interrupt)" hint - used by bash mode (LET-7199) */
showInterruptHint?: boolean;
}
/**
@@ -11,7 +13,7 @@ interface StreamingOutputDisplayProps {
* Shows a rolling window of the last 5 lines with elapsed time.
*/
export const StreamingOutputDisplay = memo(
({ streaming }: StreamingOutputDisplayProps) => {
({ streaming, showInterruptHint }: StreamingOutputDisplayProps) => {
// Force re-render every second for elapsed timer
const [, forceUpdate] = useState(0);
useEffect(() => {
@@ -24,10 +26,13 @@ export const StreamingOutputDisplay = memo(
const hiddenCount = Math.max(0, totalLineCount - tailLines.length);
const firstLine = tailLines[0];
const interruptHint = showInterruptHint ? " (esc to interrupt)" : "";
if (!firstLine) {
return (
<Box>
<Text dimColor>{` ⎿ Running... (${elapsed}s)`}</Text>
<Text
dimColor
>{` ⎿ Running... (${elapsed}s)${interruptHint}`}</Text>
</Box>
);
}
@@ -59,7 +64,7 @@ export const StreamingOutputDisplay = memo(
{/* Hidden count + elapsed time */}
{hiddenCount > 0 && (
<Text dimColor>
{" "} +{hiddenCount} more lines ({elapsed}s)
{" "} +{hiddenCount} more lines ({elapsed}s){interruptHint}
</Text>
)}
</Box>