Welcome back! In the previous chapter, Smart Output Line Rendering, we learned how to make static text look beautiful by formatting JSON and creating clickable links.
However, a shell isn't just about showing resultsβit's about running tasks. Some tasks take a long time.
Imagine running a script that installs dependencies or compiles code. It takes 2 minutes.
Without Progress Feedback: You see a blinking cursor. You stare at it.
With Standard Output: The script prints 5,000 lines of "Installing x..." "Verifying y..." Your terminal scrollbar goes crazy. You lose context of what you were doing before.
The Solution: We need a Dashboard. We want a component that sits at the bottom of the screen, updates in real-time to show us the command is alive, but only shows the most recent activity (the "tail") so it doesn't flood our history.
To build this dashboard, we need three specific elements:
(12s)) so you know the shell is responsive.
The main component is <ShellProgressMessage />. It acts as the manager for active processes.
You pass the current output and the elapsed time to the component.
import { ShellProgressMessage } from './ShellProgressMessage';
function ActiveTask() {
// Imagine this output is growing every second
const currentOutput = "Step 1... \nStep 2... \nStep 3...";
return (
<ShellProgressMessage
output={currentOutput}
elapsedTimeSeconds={12}
/>
);
}
What the user sees:
Step 1...
Step 2...
Step 3...
(12s)
If currentOutput grew to 100 lines, the user would still only see the last 5 lines, keeping the UI clean.
How does the shell manage this "Live Dashboard"? It relies on a render loop that checks the output length and the clock.
Let's look at the implementation. We have two files: one for the clock, and one for the message logic.
ShellTimeDisplay.tsx)This component is simple. It takes a number (seconds) and formats it into a human-readable string.
import { Text } from '../../ink.js';
import { formatDuration } from '../../utils/format.js';
export function ShellTimeDisplay({ elapsedTimeSeconds, timeoutMs }) {
if (elapsedTimeSeconds === undefined) return null;
// Convert seconds to milliseconds for formatting
const elapsed = formatDuration(elapsedTimeSeconds * 1000);
// Render text in a dim color so it's not distracting
return <Text dimColor>({elapsed})</Text>;
}
Explanation:
elapsedTimeSeconds exists.formatDuration to turn 12 into "12s" or 65 into "1m 5s".dimColor (grey) because the time is metadata, not the main content.ShellProgressMessage.tsx)This is where the magic happens. We need to decide what to show based on the Output Visibility Context (from Chapter 1).
Step A: Preparing the Data First, we clean the input. Raw output often contains hidden "ANSI" codes (colors, cursor movements) that can mess up our line counting.
import stripAnsi from 'strip-ansi';
export function ShellProgressMessage({ output, verbose }) {
// 1. Clean the text so we can count lines accurately
const strippedOutput = stripAnsi(output.trim());
// 2. Split into an array of lines
const lines = strippedOutput.split("\n");
// ... continued below
Step B: The "Tail" Logic Here is the core decision: Do we show everything, or just the tail?
// ... inside ShellProgressMessage
// If 'verbose' is true (Chapter 1), show EVERYTHING.
// Otherwise, slice the array to take only the last 5 elements.
const displayLines = verbose
? output
: lines.slice(-5).join("\n");
const extraLines = Math.max(0, lines.length - 5);
Explanation:
verbose is true (the user wants full details), we ignore the tail logic.verbose is false (default), lines.slice(-5) throws away the history and keeps the active bottom section.extraLines to tell the user what they are missing (e.g., "+ 45 lines").Step C: Rendering the Dashboard Finally, we combine the text, the "hidden lines" counter, and the clock.
return (
<Box flexDirection="column">
{/* 1. The Output Window */}
<Box height={verbose ? undefined : 5} overflow="hidden">
<Text dimColor>{displayLines}</Text>
</Box>
{/* 2. The Status Footer */}
<Box flexDirection="row" gap={1}>
{/* Show how many lines are hidden */}
{!verbose && extraLines > 0 && (
<Text dimColor>+{extraLines} lines</Text>
)}
{/* Show the Clock */}
<ShellTimeDisplay elapsedTimeSeconds={elapsedTimeSeconds} />
</Box>
</Box>
);
}
Explanation:
Box to hold our output. If not verbose, we lock the height to 5.displayLines.+X lines so the user knows, "Hey, there's more data here hidden to save space."<ShellTimeDisplay /> we built earlier.In this chapter, we built Execution Progress Feedback.
At this point, you have a fully functional shell UI system! You can control visibility, render pretty text, and provide real-time feedback during long tasks.
This concludes the beginner tutorial for the Shell project.
Generated by Code IQ