๐Ÿ“‹ components/tasks/ ยท 03_visual_status_system.md

Chapter 3: Visual Status System

๐Ÿ“„ components/tasks/03_visual_status_system.md

Chapter 3: Visual Status System

Welcome back! In Chapter 2: Task Detail Dialogs, we built detailed windows to inspect our tasks. Before that, in Chapter 1: Background Task Footer, we built a summary bar.

The Problem: The "Traffic Light" Confusion

Imagine you are driving. You see a Red light. You stop. Now imagine you drive to the next town, and there, Blue means stop. You would crash immediately!

In user interfaces, we face a similar problem.

If we hard-code color="green" in the Footer and color="blue" in the Dialog, our app becomes inconsistent and confusing.

The Solution: A Shared Visual System

We solve this by creating a Visual Status System. This is a collection of helper functions that act as the "Source of Truth" for how things look.

Instead of asking: "What color is a failed task?" The UI asks: "Hey System, here is a task status. Give me the correct color and icon."


Key Concepts

1. Semantic Colors

We don't talk in hex codes (like #FF0000). We talk in Meanings:

2. Iconography

We use symbols to represent state quickly without reading text:

3. The Flow

Here is how a raw text status is converted into a visual element:

sequenceDiagram participant Task as Raw Task Data participant Utils as Visual Utils participant UI as Component (Footer/Dialog) Task->>Utils: Status is "failed" Utils->>Utils: Map "failed" to Icon (โœ–) Utils->>Utils: Map "failed" to Color ("error") Utils->>UI: Returns { icon: โœ–, color: "error" } UI->>UI: Render <Text color="error">โœ–</Text>

Internal Implementation

The heart of this system lives in taskStatusUtils.tsx. Let's look at how it makes decisions.

1. Determining the Icon

The function getTaskStatusIcon takes the status and returns a character string (an emoji or symbol).

import figures from 'figures'; // A library for cross-platform symbols

export function getTaskStatusIcon(status, options) {
  if (options?.hasError) return figures.cross; // โœ–
  
  if (status === 'running') {
    return figures.play; // โ–ถ
  }
  
  if (status === 'completed') return figures.tick; // โœ”
  
  return figures.bullet; // โ€ข (Default fallback)
}

2. Determining the Color

Similarly, getTaskStatusColor returns the semantic color name recognized by our terminal rendering engine (Ink).

export function getTaskStatusColor(status, options) {
  if (options?.hasError) return 'error'; // Red
  
  if (status === 'completed') return 'success'; // Green
  
  if (status === 'killed') return 'warning'; // Yellow
  
  return 'background'; // Default/Dim color
}

3. Describing AI Activity

AI agents are complex. They don't just "run"; they "think," "search," or "write." We use describeTeammateActivity to generate a one-line summary.

export function describeTeammateActivity(task) {
  if (task.shutdownRequested) return 'stopping';
  
  // If the AI has a plan, summarize the recent activity
  // e.g., "reading file..." or "searching google..."
  const activity = task.progress?.lastActivity?.activityDescription;
  
  return activity ?? 'working';
}

Usage Example: Building a Component

Now, let's look at ShellProgress.tsx. This component is used in both the Footer and the Dialogs. It uses our Visual System to render consistent text.

The Component Logic

Instead of manually checking if (status === 'failed'), we use the utility helpers.

import { Text } from 'src/ink.js';

// A reusable sub-component for text coloring
export function TaskStatusText({ status, label }) {
  // 1. Determine color based on status
  const color = status === 'completed' ? 'success' 
              : status === 'failed' ? 'error' 
              : undefined;

  // 2. Render text with that color
  return (
    <Text color={color} dimColor>
      ({label ?? status})
    </Text>
  );
}

Handling Different States

The main ShellProgress component switches between states but relies on the styling logic above.

export function ShellProgress({ shell }) {
  switch (shell.status) {
    case 'completed':
      return <TaskStatusText status="completed" label="done" />;
      
    case 'failed':
      return <TaskStatusText status="failed" label="error" />;
      
    case 'running':
      return <TaskStatusText status="running" />;
      
    default:
      return null;
  }
}

What just happened?

  1. We pass a shell object.
  2. If the status is failed, we ask for the "error" label.
  3. TaskStatusText sets the color to 'error' (Red).
  4. The user sees a Red (error) text.

Sometimes, the Visual System decides to show nothing.

In taskStatusUtils.tsx, there is a function shouldHideTasksFooter. If we are looking at a detailed "Spinner Tree" (a hierarchical view of tasks), we might not want to show the footer at all to save space.

export function shouldHideTasksFooter(tasks, showSpinnerTree) {
  // If spinner tree is off, always show footer
  if (!showSpinnerTree) return false;

  // If we have active background tasks that aren't 
  // part of the main tree, keep the footer.
  // ... (logic to check task types) ...
  
  return true; // Hide footer
}

This ensures the screen doesn't get cluttered with duplicate information.


Conclusion

By moving our colors and icons into a centralized system (taskStatusUtils), we achieved:

  1. Consistency: "Success" always looks the same.
  2. Simplicity: UI components don't need complex if/else chains.
  3. Maintainability: If we want to change the "Success" icon from โœ” to โ˜…, we change it in one place.

Now that our local tasks look great, what happens when we connect to a server somewhere else?

Next Chapter: Remote Session Visualization


Generated by Code IQ