arial
GitHub

Terminal User Interface

Arial includes a TUI (Terminal User Interface) for real-time monitoring of workstream execution. This document covers the TUI architecture and implementation.

Overview

The TUI provides:

  • Real-time workstream status updates
  • Activity indicators for each workstream
  • Output streaming from the active workstream
  • Keyboard navigation between workstreams

Architecture

The TUI (ui/tui.ts) uses a pure text rendering approach without external TUI libraries.

interface TUI {
  start(): void
  stop(): void
  updateStatus(workstreamId: string, status: WorkstreamStatus): void
  updateActivity(workstreamId: string, activity: string): void
  addOutput(workstreamId: string, line: string): void
}

Layout

┌──────────────────────┬────────────────────────────────────────────┐
│ Workstreams          │ auth-system                                │
│                      │                                            │
│ ● auth-system        │ Activity: Running tests...                 │
│ ○ payment-flow       │                                            │
│ ○ user-profile       │ Output:                                    │
│                      │ > npm test                                 │
│                      │ PASS src/auth.test.ts                      │
│                      │ PASS src/jwt.test.ts                       │
│                      │ All tests passed                           │
└──────────────────────┴────────────────────────────────────────────┘

Left Panel (22 chars)

  • Lists all workstreams
  • Shows status icon for each
  • Highlights selected workstream
  • Fixed width for consistency

Right Panel

  • Shows selected workstream name
  • Current activity (tool being used)
  • Recent output lines
  • Scrolls as new output arrives

Status Icons

IconStatusColor
pendingdim
runningblue
donegreen
failedred

Implementation

Rendering

The TUI uses ANSI escape codes for rendering:

// Clear screen and move cursor
const CLEAR = '\x1b[2J\x1b[H'

// Colors
const DIM = '\x1b[2m'
const BLUE = '\x1b[34m'
const GREEN = '\x1b[32m'
const RED = '\x1b[31m'
const RESET = '\x1b[0m'

Render Loop

function render() {
  const lines: string[] = []

  // Build left panel
  const leftPanel = buildWorkstreamList()

  // Build right panel
  const rightPanel = buildActivityPanel()

  // Combine panels
  for (let i = 0; i < maxHeight; i++) {
    const left = leftPanel[i] || ''
    const right = rightPanel[i] || ''
    lines.push(`${left}│${right}`)
  }

  // Output
  process.stdout.write(CLEAR + lines.join('\n'))
}

Spinner Animation

Running workstreams show an animated spinner:

const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
let spinnerIndex = 0

setInterval(() => {
  spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length
  render()
}, 80)

Keyboard Navigation

process.stdin.setRawMode(true)
process.stdin.on('data', (key) => {
  if (key[0] === 0x1b) {  // Escape sequence
    if (key[1] === 0x5b) {  // Arrow keys
      if (key[2] === 0x41) selectPrevious()  // Up
      if (key[2] === 0x42) selectNext()      // Down
    }
  }
  if (key[0] === 0x03) {  // Ctrl+C
    stop()
    process.exit(0)
  }
})

Event Handling

The TUI subscribes to orchestrator events:

orchestrator.on('workstream.status', (id, status) => {
  tui.updateStatus(id, status)
})

orchestrator.on('workstream.activity', (id, activity) => {
  tui.updateActivity(id, activity)
})

orchestrator.on('workstream.output', (id, line) => {
  tui.addOutput(id, line)
})

Output Buffering

Each workstream maintains an output buffer:

interface OutputBuffer {
  lines: string[]
  maxLines: number  // Default: 100

  add(line: string) {
    this.lines.push(line)
    if (this.lines.length > this.maxLines) {
      this.lines.shift()
    }
  }

  getRecent(count: number): string[] {
    return this.lines.slice(-count)
  }
}

TTY Detection

The TUI only runs in interactive terminals:

function shouldUseTUI(): boolean {
  return process.stdout.isTTY && process.stdin.isTTY
}

// In non-TTY mode, fall back to simple logging
if (!shouldUseTUI()) {
  console.log(`[${workstreamId}] ${status}`)
}

Terminal Restoration

On exit, the TUI restores terminal state:

function stop() {
  // Restore cursor
  process.stdout.write('\x1b[?25h')

  // Restore input mode
  process.stdin.setRawMode(false)

  // Clear alternate screen
  process.stdout.write('\x1b[?1049l')
}

Design Decisions

Why Not React/Ink?

Arial originally planned to use React/Ink but chose pure text rendering for:

  • Simplicity - No additional dependencies
  • Performance - Direct ANSI output is faster
  • Portability - Works in any terminal
  • Debugging - Easy to inspect raw output

Lane-Based Display

The two-column layout was chosen because:

  • Shows all workstreams at a glance
  • Provides detail for the selected workstream
  • Scales well with many workstreams
  • Familiar pattern from IDE panels

Fixed Left Panel Width

The 22-character left panel width:

  • Accommodates typical workstream names
  • Leaves room for status icons
  • Prevents layout shifts during execution
  • Works well in standard 80-column terminals

Future Improvements

Potential enhancements:

  • Mouse support for workstream selection
  • Resizable panels
  • Search/filter workstreams
  • Log export to file
  • Split view for multiple workstreams