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
| Icon | Status | Color |
|---|---|---|
| ○ | pending | dim |
| ● | running | blue |
| ✓ | done | green |
| ✗ | failed | red |
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