feat: configurable status lines for CLI footer (#904)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
jnjpng
2026-02-11 17:35:34 -08:00
committed by GitHub
parent 74b369d1ca
commit c3a7f6c646
16 changed files with 1689 additions and 15 deletions

View File

@@ -4,10 +4,12 @@ import { EventEmitter } from "node:events";
import { stdin } from "node:process";
import chalk from "chalk";
import { Box, useInput } from "ink";
import Link from "ink-link";
import SpinnerLib from "ink-spinner";
import {
type ComponentType,
memo,
type ReactNode,
useCallback,
useEffect,
useMemo,
@@ -108,6 +110,85 @@ function findCursorLine(
};
}
// Matches OSC 8 hyperlink sequences: \x1b]8;;URL\x1b\DISPLAY\x1b]8;;\x1b\
// biome-ignore lint/suspicious/noControlCharactersInRegex: OSC 8 escape sequences require \x1b
const OSC8_REGEX = /\x1b\]8;;([^\x1b]*)\x1b\\([^\x1b]*)\x1b\]8;;\x1b\\/g;
function parseOsc8Line(line: string, keyPrefix: string): ReactNode[] {
const parts: ReactNode[] = [];
let lastIndex = 0;
const regex = new RegExp(OSC8_REGEX.source, "g");
for (let match = regex.exec(line); match !== null; match = regex.exec(line)) {
if (match.index > lastIndex) {
parts.push(
<Text key={`${keyPrefix}-${lastIndex}`}>
{line.slice(lastIndex, match.index)}
</Text>,
);
}
const url = match[1] ?? "";
const display = match[2] ?? "";
parts.push(
<Link key={`${keyPrefix}-${match.index}`} url={url}>
<Text>{display}</Text>
</Link>,
);
lastIndex = match.index + match[0].length;
}
if (lastIndex < line.length) {
parts.push(
<Text key={`${keyPrefix}-${lastIndex}`}>{line.slice(lastIndex)}</Text>,
);
}
if (parts.length === 0) {
parts.push(<Text key={keyPrefix}>{line}</Text>);
}
return parts;
}
function StatusLineContent({
text,
padding,
modeName,
modeColor,
showExitHint,
}: {
text: string;
padding: number;
modeName: string | null;
modeColor: string | null;
showExitHint: boolean;
}) {
const lines = text.split("\n");
const paddingStr = padding > 0 ? " ".repeat(padding) : "";
const parts: ReactNode[] = [];
for (let i = 0; i < lines.length; i++) {
if (i > 0) {
parts.push("\n");
}
if (paddingStr) {
parts.push(paddingStr);
}
parts.push(...parseOsc8Line(lines[i] ?? "", `l${i}`));
}
return (
<Text wrap="wrap">
<Text>{parts}</Text>
{modeName && modeColor && (
<>
{"\n"}
<Text color={modeColor}> {modeName}</Text>
<Text color={modeColor} dimColor>
{" "}
(shift+tab to {showExitHint ? "exit" : "cycle"})
</Text>
</>
)}
</Text>
);
}
/**
* Memoized footer component to prevent re-renders during high-frequency
* shimmer/timer updates. Only updates when its specific props change.
@@ -125,6 +206,9 @@ const InputFooter = memo(function InputFooter({
isByokProvider,
hideFooter,
rightColumnWidth,
statusLineText,
statusLineRight,
statusLinePadding,
}: {
ctrlCPressed: boolean;
escapePressed: boolean;
@@ -138,6 +222,9 @@ const InputFooter = memo(function InputFooter({
isByokProvider: boolean;
hideFooter: boolean;
rightColumnWidth: number;
statusLineText?: string;
statusLineRight?: string;
statusLinePadding?: number;
}) {
const hideFooterContent = hideFooter;
const maxAgentChars = Math.max(10, Math.floor(rightColumnWidth * 0.45));
@@ -188,6 +275,14 @@ const InputFooter = memo(function InputFooter({
(backspace to exit)
</Text>
</Text>
) : statusLineText ? (
<StatusLineContent
text={statusLineText}
padding={statusLinePadding ?? 0}
modeName={modeName}
modeColor={modeColor}
showExitHint={showExitHint}
/>
) : modeName && modeColor ? (
<Text>
<Text color={modeColor}> {modeName}</Text>
@@ -200,9 +295,26 @@ const InputFooter = memo(function InputFooter({
<Text dimColor>Press / for commands</Text>
)}
</Box>
<Box width={rightColumnWidth} flexShrink={0}>
<Box
flexDirection={
statusLineRight && !hideFooterContent ? "column" : undefined
}
alignItems={
statusLineRight && !hideFooterContent ? "flex-end" : undefined
}
width={
statusLineRight && !hideFooterContent ? undefined : rightColumnWidth
}
flexShrink={0}
>
{hideFooterContent ? (
<Text>{" ".repeat(rightColumnWidth)}</Text>
) : statusLineRight ? (
statusLineRight.split("\n").map((line) => (
<Text key={line} wrap="truncate-end">
{parseOsc8Line(line, line)}
</Text>
))
) : (
<Text>{rightLabel}</Text>
)}
@@ -423,6 +535,9 @@ export function Input({
networkPhase = null,
terminalWidth,
shouldAnimate = true,
statusLineText,
statusLineRight,
statusLinePadding = 0,
}: {
visible?: boolean;
streaming: boolean;
@@ -458,6 +573,9 @@ export function Input({
networkPhase?: "upload" | "download" | "error" | null;
terminalWidth: number;
shouldAnimate?: boolean;
statusLineText?: string;
statusLineRight?: string;
statusLinePadding?: number;
}) {
const [value, setValue] = useState("");
const [escapePressed, setEscapePressed] = useState(false);
@@ -1192,6 +1310,9 @@ export function Input({
}
hideFooter={hideFooter}
rightColumnWidth={footerRightColumnWidth}
statusLineText={statusLineText}
statusLineRight={statusLineRight}
statusLinePadding={statusLinePadding}
/>
</Box>
) : reserveInputSpace ? (
@@ -1232,6 +1353,9 @@ export function Input({
footerRightColumnWidth,
reserveInputSpace,
inputChromeHeight,
statusLineText,
statusLineRight,
statusLinePadding,
]);
// If not visible, render nothing but keep component mounted to preserve state