diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index b0bc42f..da581e5 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -584,6 +584,7 @@ const APPROVAL_PREVIEW_BUFFER = 4;
const MIN_WRAP_WIDTH = 10;
const TEXT_WRAP_GUTTER = 6;
const DIFF_WRAP_GUTTER = 12;
+const SHELL_PREVIEW_MAX_LINES = 3;
function countWrappedLines(text: string, width: number): number {
if (!text) return 0;
@@ -2611,7 +2612,10 @@ export default function App({
}
let lines = 3; // solid line + header + blank line
- lines += countWrappedLines(command, wrapWidth);
+ lines += Math.min(
+ countWrappedLines(command, wrapWidth),
+ SHELL_PREVIEW_MAX_LINES,
+ );
if (description) {
lines += countWrappedLines(description, wrapWidth);
}
diff --git a/src/cli/components/InlineBashApproval.tsx b/src/cli/components/InlineBashApproval.tsx
index 84c0b7d..bb85bd5 100644
--- a/src/cli/components/InlineBashApproval.tsx
+++ b/src/cli/components/InlineBashApproval.tsx
@@ -28,6 +28,7 @@ type Props = {
// Horizontal line character for Claude Code style
const SOLID_LINE = "─";
+const BASH_PREVIEW_MAX_LINES = 3;
/**
* InlineBashApproval - Renders bash/shell approval UI inline (Claude Code style)
@@ -152,7 +153,12 @@ export const InlineBashApproval = memo(
{/* Command preview */}
-
+
{bashInfo.description && (
{bashInfo.description}
@@ -161,7 +167,7 @@ export const InlineBashApproval = memo(
>
),
- [bashInfo.command, bashInfo.description, solidLine],
+ [bashInfo.command, bashInfo.description, solidLine, columns],
);
// Hint text based on state
diff --git a/src/cli/components/StreamingOutputDisplay.tsx b/src/cli/components/StreamingOutputDisplay.tsx
index a8427fe..891982a 100644
--- a/src/cli/components/StreamingOutputDisplay.tsx
+++ b/src/cli/components/StreamingOutputDisplay.tsx
@@ -1,6 +1,7 @@
import { Box } from "ink";
import { memo, useEffect, useState } from "react";
import type { StreamingState } from "../helpers/accumulator";
+import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { Text } from "./Text";
interface StreamingOutputDisplayProps {
@@ -15,6 +16,7 @@ interface StreamingOutputDisplayProps {
*/
export const StreamingOutputDisplay = memo(
({ streaming, showInterruptHint }: StreamingOutputDisplayProps) => {
+ const columns = useTerminalWidth();
// Force re-render every second for elapsed timer
const [, forceUpdate] = useState(0);
useEffect(() => {
@@ -25,6 +27,17 @@ export const StreamingOutputDisplay = memo(
const elapsed = Math.floor((Date.now() - streaming.startTime) / 1000);
const { tailLines, totalLineCount } = streaming;
const hiddenCount = Math.max(0, totalLineCount - tailLines.length);
+ const contentWidth = Math.max(10, columns - 5);
+
+ const clipToWidth = (text: string): string => {
+ if (text.length <= contentWidth) {
+ return text;
+ }
+ if (contentWidth <= 1) {
+ return "…";
+ }
+ return `${text.slice(0, contentWidth - 1)}…`;
+ };
const firstLine = tailLines[0];
const interruptHint = showInterruptHint ? " (esc to interrupt)" : "";
@@ -47,7 +60,7 @@ export const StreamingOutputDisplay = memo(
dimColor={!firstLine.isStderr}
color={firstLine.isStderr ? "red" : undefined}
>
- {firstLine.text}
+ {clipToWidth(firstLine.text)}
{/* Remaining lines with indent (5 spaces to align with content after bracket) */}
@@ -59,7 +72,7 @@ export const StreamingOutputDisplay = memo(
color={line.isStderr ? "red" : undefined}
>
{" "}
- {line.text}
+ {clipToWidth(line.text)}
))}
{/* Hidden count + elapsed time */}
diff --git a/src/cli/components/SyntaxHighlightedCommand.tsx b/src/cli/components/SyntaxHighlightedCommand.tsx
index 85e883f..5ed9bb6 100644
--- a/src/cli/components/SyntaxHighlightedCommand.tsx
+++ b/src/cli/components/SyntaxHighlightedCommand.tsx
@@ -14,6 +14,9 @@ type Props = {
showPrompt?: boolean;
prefix?: string;
suffix?: string;
+ maxLines?: number;
+ maxColumns?: number;
+ showTruncationHint?: boolean;
};
type ShellSyntaxPalette = typeof colors.shellSyntax;
@@ -21,6 +24,39 @@ type ShellSyntaxPalette = typeof colors.shellSyntax;
/** Styled text span with a resolved color. */
export type StyledSpan = { text: string; color: string };
+type ClippedSpans = {
+ spans: StyledSpan[];
+ clipped: boolean;
+};
+
+function clipStyledSpans(
+ spans: StyledSpan[],
+ maxColumns: number,
+): ClippedSpans {
+ if (maxColumns <= 0) {
+ return { spans: [], clipped: spans.length > 0 };
+ }
+
+ let remaining = maxColumns;
+ const clipped: StyledSpan[] = [];
+
+ for (const span of spans) {
+ if (remaining <= 0) {
+ return { spans: clipped, clipped: true };
+ }
+ if (span.text.length <= remaining) {
+ clipped.push(span);
+ remaining -= span.text.length;
+ continue;
+ }
+
+ clipped.push({ text: span.text.slice(0, remaining), color: span.color });
+ return { spans: clipped, clipped: true };
+ }
+
+ return { spans: clipped, clipped: false };
+}
+
/** Map file extension to a lowlight language name. */
const EXT_TO_LANG: Record = {
ts: "typescript",
@@ -317,13 +353,47 @@ export function highlightCode(
}
export const SyntaxHighlightedCommand = memo(
- ({ command, showPrompt = true, prefix, suffix }: Props) => {
+ ({
+ command,
+ showPrompt = true,
+ prefix,
+ suffix,
+ maxLines,
+ maxColumns,
+ showTruncationHint = false,
+ }: Props) => {
const palette = colors.shellSyntax;
- const lines = highlightCommand(command, palette);
+ const highlightedLines = highlightCommand(command, palette);
+
+ const hasLineCap = typeof maxLines === "number" && maxLines >= 0;
+ const visibleLines = hasLineCap
+ ? highlightedLines.slice(0, maxLines)
+ : highlightedLines;
+ const hiddenLineCount = Math.max(
+ 0,
+ highlightedLines.length - visibleLines.length,
+ );
+
+ const renderedLines: StyledSpan[][] = [];
+ let anyColumnClipping = false;
+ for (let i = 0; i < visibleLines.length; i++) {
+ const spans = visibleLines[i] ?? [];
+ if (typeof maxColumns === "number") {
+ const prefixLen = i === 0 && prefix ? prefix.length : 0;
+ const suffixLen =
+ i === visibleLines.length - 1 && suffix ? suffix.length : 0;
+ const textBudget = Math.max(0, maxColumns - prefixLen - suffixLen);
+ const clipped = clipStyledSpans(spans, textBudget);
+ renderedLines.push(clipped.spans);
+ anyColumnClipping = anyColumnClipping || clipped.clipped;
+ } else {
+ renderedLines.push(spans);
+ }
+ }
return (
- {lines.map((spans, lineIdx) => {
+ {renderedLines.map((spans, lineIdx) => {
const lineKey = spans.map((s) => s.text).join("");
return (
@@ -339,11 +409,17 @@ export const SyntaxHighlightedCommand = memo(
{span.text}
))}
- {lineIdx === lines.length - 1 && suffix ? suffix : null}
+ {lineIdx === renderedLines.length - 1 && suffix ? suffix : null}
);
})}
+ {showTruncationHint && hiddenLineCount > 0 && (
+ {`… +${hiddenLineCount} more lines`}
+ )}
+ {showTruncationHint && hiddenLineCount === 0 && anyColumnClipping && (
+ … output clipped
+ )}
);
},
diff --git a/src/cli/components/ToolCallMessageRich.tsx b/src/cli/components/ToolCallMessageRich.tsx
index 3db47a8..cf17e45 100644
--- a/src/cli/components/ToolCallMessageRich.tsx
+++ b/src/cli/components/ToolCallMessageRich.tsx
@@ -102,6 +102,8 @@ import { StreamingOutputDisplay } from "./StreamingOutputDisplay";
import { SyntaxHighlightedCommand } from "./SyntaxHighlightedCommand";
import { TodoRenderer } from "./TodoRenderer.js";
+const LIVE_SHELL_ARGS_MAX_LINES = 2;
+
type ToolCallLine = {
kind: "tool_call";
id: string;
@@ -932,6 +934,11 @@ export const ToolCallMessage = memo(
showPrompt={false}
prefix="("
suffix=")"
+ maxLines={LIVE_SHELL_ARGS_MAX_LINES}
+ maxColumns={Math.max(
+ 10,
+ rightWidth - displayName.length,
+ )}
/>
) : args ? (
diff --git a/src/cli/components/previews/BashPreview.tsx b/src/cli/components/previews/BashPreview.tsx
index d1d84cc..f9c0edb 100644
--- a/src/cli/components/previews/BashPreview.tsx
+++ b/src/cli/components/previews/BashPreview.tsx
@@ -6,6 +6,7 @@ import { SyntaxHighlightedCommand } from "../SyntaxHighlightedCommand";
import { Text } from "../Text";
const SOLID_LINE = "─";
+const BASH_PREVIEW_MAX_LINES = 3;
type Props = {
command: string;
@@ -37,7 +38,12 @@ export const BashPreview = memo(({ command, description }: Props) => {
{/* Command preview */}
-
+
{description && {description}}
>