@@ -229,9 +229,9 @@ func (m Model) Init() tea.Cmd {
229229 )
230230}
231231
232- // logTickCmd returns a command that sends a tick every 500ms for log updates
232+ // logTickCmd returns a command that sends a tick every second for log updates
233233func logTickCmd () tea.Cmd {
234- return tea .Tick (500 * time .Millisecond , func (t time.Time ) tea.Msg {
234+ return tea .Tick (time .Second , func (t time.Time ) tea.Msg {
235235 return logTickMsg (t )
236236 })
237237}
@@ -1654,6 +1654,13 @@ func (m Model) renderRunsList(width int) string {
16541654 // Stats summary line
16551655 statsLine := m .formatStatsLine ()
16561656 lines = append (lines , mutedStyle .Render (statsLine ))
1657+
1658+ // Progress bar (only shown when job is running with avg duration available)
1659+ progressBar := m .renderProgressBar (width )
1660+ if progressBar != "" {
1661+ lines = append (lines , progressBar )
1662+ }
1663+
16571664 lines = append (lines , "" )
16581665
16591666 // Calculate column widths based on panel width
@@ -2003,6 +2010,62 @@ func (m Model) formatStatsLine() string {
20032010 avgDuration )
20042011}
20052012
2013+ // renderProgressBar renders a progress bar for a running job
2014+ // Returns empty string if no progress bar should be shown
2015+ func (m Model ) renderProgressBar (width int ) string {
2016+ // Only show progress bar when:
2017+ // 1. We have a selected job that is running
2018+ // 2. We have stats with an average duration
2019+ if len (m .jobs ) == 0 || m .jobScroll .Cursor >= len (m .jobs ) {
2020+ return ""
2021+ }
2022+
2023+ job := m .jobs [m .jobScroll .Cursor ]
2024+ if ! job .Running {
2025+ return ""
2026+ }
2027+
2028+ if m .stats == nil || m .stats .AvgDurationMs <= 0 {
2029+ return ""
2030+ }
2031+
2032+ // Calculate elapsed time
2033+ elapsed := time .Since (job .StartedAt )
2034+ avgDuration := time .Duration (m .stats .AvgDurationMs ) * time .Millisecond
2035+
2036+ // Calculate progress percentage (capped at 100%)
2037+ progress := float64 (elapsed ) / float64 (avgDuration )
2038+ if progress > 1.0 {
2039+ progress = 1.0
2040+ }
2041+
2042+ // Calculate bar dimensions
2043+ // Reserve space for: percentage (5) + space (1) + times display (~20)
2044+ barWidth := width - 26
2045+ if barWidth < 10 {
2046+ barWidth = 10
2047+ }
2048+
2049+ filledWidth := int (float64 (barWidth ) * progress )
2050+ emptyWidth := barWidth - filledWidth
2051+
2052+ // Build the bar using Unicode block characters
2053+ // ▓ (U+2593) for filled, ▒ (U+2592) for empty
2054+ filled := progressBarFillStyle .Render (strings .Repeat ("▓" , filledWidth ))
2055+ empty := progressBarEmptyStyle .Render (strings .Repeat ("▒" , emptyWidth ))
2056+
2057+ // Format elapsed / avg time
2058+ elapsedStr := formatDuration (elapsed )
2059+ avgStr := formatDuration (avgDuration )
2060+ timeInfo := progressBarTextStyle .Render (fmt .Sprintf ("%s / %s" , elapsedStr , avgStr ))
2061+
2062+ // Format percentage
2063+ pct := int (progress * 100 )
2064+ pctStr := progressBarTextStyle .Render (fmt .Sprintf ("%3d%%" , pct ))
2065+
2066+ return fmt .Sprintf ("%s%s %s %s" , filled , empty , pctStr , timeInfo )
2067+ }
2068+
20062069// formatRelativeTime formats a time as a relative duration from now
20072070func formatRelativeTime (t time.Time ) string {
20082071 d := time .Since (t )
0 commit comments