Files
letta-code/src/cli/components/BashCommandMessage.tsx
2026-03-23 15:18:00 -07:00

106 lines
3.5 KiB
TypeScript

import { Box } from "ink";
import { memo } from "react";
import { INTERRUPTED_BY_USER } from "../../constants";
import { clipToolReturn } from "../../tools/manager";
import type { StreamingState } from "../helpers/accumulator";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { BlinkDot } from "./BlinkDot.js";
import { CollapsedOutputDisplay } from "./CollapsedOutputDisplay";
import { colors } from "./colors.js";
import { MarkdownDisplay } from "./MarkdownDisplay.js";
import { StreamingOutputDisplay } from "./StreamingOutputDisplay";
import { Text } from "./Text";
type BashCommandLine = {
kind: "bash_command";
id: string;
input: string;
output: string;
phase?: "running" | "finished";
success?: boolean;
streaming?: StreamingState;
};
/**
* BashCommandMessage - Renders bash mode command output
* Similar to CommandMessage but with red ! indicator instead of dot
*
* Features:
* - Two-column layout with left gutter (2 chars) and right content area
* - Red ! indicator (blinking when running)
* - Proper terminal width calculation and wrapping
* - Markdown rendering for output
*/
export const BashCommandMessage = memo(
({ line }: { line: BashCommandLine }) => {
const columns = useTerminalWidth();
const rightWidth = Math.max(0, columns - 2); // gutter is 2 cols
// Determine indicator state based on phase and success
const getIndicatorElement = () => {
if (!line.phase || line.phase === "finished") {
// Show red ! for both success and failure (it's user-run, not agent-run)
return <Text color={colors.bash.dot}>!</Text>;
}
if (line.phase === "running") {
return <BlinkDot color={colors.bash.dot} symbol="!" />;
}
return <Text color={colors.bash.dot}>!</Text>;
};
return (
<Box flexDirection="column">
{/* Command input */}
<Box flexDirection="row">
<Box width={2} flexShrink={0}>
{getIndicatorElement()}
<Text> </Text>
</Box>
<Box flexGrow={1} width={rightWidth}>
<Text>{line.input}</Text>
</Box>
</Box>
{/* Streaming output during execution */}
{line.phase === "running" && line.streaming && (
<StreamingOutputDisplay
streaming={line.streaming}
showInterruptHint
/>
)}
{/* Full output after completion (no collapse for bash mode) */}
{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 && (
<Box flexDirection="row">
<Box width={5} flexShrink={0}>
<Text>{" ⎿ "}</Text>
</Box>
<Box flexGrow={1} width={Math.max(0, columns - 5)}>
<MarkdownDisplay
text={clipToolReturn(line.output).replace(/\n+$/, "")}
/>
</Box>
</Box>
)}
</Box>
);
},
);
BashCommandMessage.displayName = "BashCommandMessage";