In the previous Character Animation Engine chapter, we brought our mascot, Clawd, to life. Now, our welcome screen has a personality!
However, a mascot alone isn't enough. We need to show the user useful informationβlike what they were working on last, tips for using the tool, or new updates.
In this chapter, we will build the Feed Component System. Think of this as the "Bulletin Board" next to the receptionist. It organizes messy lists of data into clean, uniform "cards" or widgets.
Displaying text in a terminal is tricky.
The Feed Component System solves this by acting as a "layout manager." It calculates exactly how much space is needed and cuts off (truncates) text that is too long.
The system is built around three main ideas:
A single "Feed" looks like this:
Recent Activity <-- Title
2h ago Fix bug <-- Line (Timestamp + Text)
4h ago Update docs <-- Line
/resume for more <-- Footer
To create a new section on the dashboard, you don't write complex UI code. You just create a function that returns a Configuration Object.
Let's look at feedConfigs.tsx. Here is how we create the "Recent Activity" feed.
We transform raw log data into a list of standardized lines.
// From feedConfigs.tsx
export function createRecentActivityFeed(activities) {
// Map raw data to "FeedLine" objects
const lines = activities.map(log => {
return {
text: log.summary, // "Fix bug in login"
timestamp: '2h ago' // Calculated time
};
});
return {
title: 'Recent activity', // The Header
lines: lines, // The content
footer: '/resume for more' // The bottom hint
};
}
In our main parent component, we use <FeedColumn />. We pass it an array of these configurations.
// Inside LogoV2.tsx (Conceptual)
<FeedColumn
feeds={[
createRecentActivityFeed(logs),
createProjectOnboardingFeed(steps)
]}
maxWidth={50} // Don't let it get wider than 50 chars
/>
FeedColumn takes care of the rest. It stacks "Activity" on top of "Onboarding" and aligns them perfectly.How does the system know how wide to draw the box? It uses a two-step process: Measure then Render.
Before drawing a single pixel, the system runs a calculation to find the "Ideal Width."
Feed.tsx)
The function calculateFeedWidth looks at every piece of text to find the widest point.
// From Feed.tsx (Simplified)
export function calculateFeedWidth(config) {
let maxWidth = stringWidth(config.title);
// Check every line in the list
for (const line of config.lines) {
// Calculate: Text Length + Timestamp Length + Gap
const lineWidth = stringWidth(line.text) +
stringWidth(line.timestamp) + 2;
// Keep the largest number found
maxWidth = Math.max(maxWidth, lineWidth);
}
return maxWidth;
}
stringWidth (which handles special characters correctly). We compare the title, every line, and the footer. The largest number wins.FeedColumn.tsx)This component acts as the manager. It ensures uniformity. If the "Tips" section is wide, the "Activity" section above it stretches to match, creating a clean vertical look.
// From FeedColumn.tsx (Simplified)
export function FeedColumn({ feeds, maxWidth }) {
// 1. Ask everyone how wide they want to be
const widths = feeds.map(calculateFeedWidth);
// 2. Find the widest feed
const maxOfAll = Math.max(...widths);
// 3. Cap it at the screen limit
const actualWidth = Math.min(maxOfAll, maxWidth);
// 4. Render all feeds with the SAME width
return (
<Box flexDirection="column">
{feeds.map((feed, index) => (
<Feed config={feed} actualWidth={actualWidth} />
))}
</Box>
);
}
actualWidth is passed down to every child. This forces them to align.Feed.tsx)
Finally, the Feed component renders the text. This is where we handle truncation (cutting off text with ...) if it exceeds the calculated width.
// From Feed.tsx (Inside the render loop)
return (
<Text>
{/* 1. Render Timestamp (Gray color) */}
<Text dimColor>
{line.timestamp.padEnd(maxTimestampWidth)}
</Text>
{/* 2. Gap */}
<Text> </Text>
{/* 3. Render Main Text (Cut off if too long) */}
<Text>
{truncate(line.text, availableWidth)}
</Text>
</Text>
);
truncate ensures that if a user has a very small terminal window, the text won't wrap securely to the next line and break the layout. It will just show Fix bug in....Sometimes, a simple list of text isn't enough. For example, we might want to show a graphical reward or "Guest Passes."
The FeedConfig supports a special property called customContent.
// From feedConfigs.tsx (Guest Passes)
export function createGuestPassesFeed() {
return {
title: '3 guest passes',
lines: [], // No text lines
// Inject custom React components directly
customContent: {
content: (
<Box marginY={1}>
<Text color="claude">[β»] [β»] [β»]</Text>
</Box>
),
width: 48
}
};
}
Feed component checks if customContent exists. If it does, it ignores the lines array and renders your custom Box instead. This allows the system to remain modular but flexible.The Feed Component System provides a structured way to display data.
FeedColumn) calculates the perfect width to keep everything aligned.Feed) handles colors, padding, and cutting off long text.Now our welcome screen looks professional: moving character on the left, organized data on the right.
But what if we want to show a special notice only if a user hasn't configured something yet? We need conditional logic.
Next Chapter: Conditional Feature Notices
Generated by Code IQ