From c98a86b8f4e949c7b77525fb3a76658ffa3b6f18 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 06:30:29 +0100 Subject: [PATCH] feat(tui): add RalphPanel component for loop tracking - Display Ralph loop status (active/idle) - Show completion phrase, cycle count, elapsed time - Display todo list with status icons (pending/in_progress/completed) - Truncate long todo items, limit to 5 visible - Use magenta border to match web UI styling Co-Authored-By: Claude Opus 4.5 --- src/tui/components/RalphPanel.tsx | 115 ++++++++++++++++++++++++++++++ src/tui/components/index.ts | 1 + 2 files changed, 116 insertions(+) create mode 100644 src/tui/components/RalphPanel.tsx diff --git a/src/tui/components/RalphPanel.tsx b/src/tui/components/RalphPanel.tsx new file mode 100644 index 00000000..d18a7960 --- /dev/null +++ b/src/tui/components/RalphPanel.tsx @@ -0,0 +1,115 @@ +/** + * @fileoverview RalphPanel component + * + * Displays Ralph Wiggum loop tracking information: + * - Loop status and completion phrase + * - Todo list with progress + * - Cycle count and elapsed time + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import type { InnerLoopState, InnerTodoItem } from '../../types.js'; + +interface RalphPanelProps { + loopState: InnerLoopState | null; + todos: InnerTodoItem[]; + visible?: boolean; +} + +/** + * Status icon for todo items + */ +function getStatusIcon(status: string): { icon: string; color: string } { + switch (status) { + case 'completed': + return { icon: '\u2713', color: 'green' }; + case 'in_progress': + return { icon: '\u25CF', color: 'yellow' }; + case 'pending': + default: + return { icon: '\u25CB', color: 'gray' }; + } +} + +/** + * Formats elapsed hours to human-readable string + */ +function formatElapsed(hours: number | null): string { + if (hours === null) return '-'; + if (hours < 1) { + const minutes = Math.round(hours * 60); + return `${minutes}m`; + } + return `${hours.toFixed(1)}h`; +} + +/** + * RalphPanel component showing loop tracking + */ +export function RalphPanel({ + loopState, + todos, + visible = true, +}: RalphPanelProps): React.ReactElement | null { + // Don't render if not visible or no loop state + if (!visible || !loopState || !loopState.enabled) { + return null; + } + + const completedCount = todos.filter((t) => t.status === 'completed').length; + const totalCount = todos.length; + const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + + return ( + + {/* Header */} + + + Ralph Loop {loopState.active ? '(active)' : '(idle)'} + + {loopState.completionPhrase && ( + + phrase: {loopState.completionPhrase} + + )} + + + {/* Stats row */} + + + cycles: {loopState.cycleCount} + {loopState.maxIterations && /{loopState.maxIterations}} + | + elapsed: {formatElapsed(loopState.elapsedHours)} + | + todos: {completedCount}/{totalCount} ({progressPercent}%) + + + + {/* Todo list (max 5 items) */} + {todos.length > 0 && ( + + {todos.slice(0, 5).map((todo) => { + const { icon, color } = getStatusIcon(todo.status); + const content = todo.content.length > 50 + ? todo.content.slice(0, 47) + '...' + : todo.content; + + return ( + + {icon} + + {content} + + + ); + })} + {todos.length > 5 && ( + ... and {todos.length - 5} more + )} + + )} + + ); +} diff --git a/src/tui/components/index.ts b/src/tui/components/index.ts index 02f5c687..005f9c84 100644 --- a/src/tui/components/index.ts +++ b/src/tui/components/index.ts @@ -7,3 +7,4 @@ export { TabBar } from './TabBar.js'; export { TerminalView } from './TerminalView.js'; export { StatusBar } from './StatusBar.js'; export { HelpOverlay } from './HelpOverlay.js'; +export { RalphPanel } from './RalphPanel.js';