fix(ui): add 30s timeout to progress bar to prevent stuck state
- commit
- 5544875d757518148f3d430603b4c26b4697c619
- author
- bnema · · 2025-12-27 22:45 UTC
- committer
- bnema · · 2025-12-27 22:48 UTC
- parent
- a9e245c
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
2 files · +40 -1
M CHANGELOG.md +2 -0
| @@ -35,6 +35,8 @@ All notable changes to this project will be documented in this file. | |||
| 35 | 35 | - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging. | |
| 36 | 36 | ||
| 37 | 37 | ### Fixed | |
| 38 | + | - **Fullscreen video tab bar**: Fixed tab bar remaining visible during fullscreen video playback. Now hides automatically when entering fullscreen and restores based on normal visibility logic when exiting. | |
| 39 | + | - **Progress bar stuck state**: Added 30-second timeout to progress bar to auto-hide if page load stalls and never completes. | |
| 38 | 40 | - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews. | |
| 39 | 41 | - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency. | |
| 40 | 42 | - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag). | |
| @@ -35,6 +35,8 @@ All notable changes to this project will be documented in this file. | |||||
| 35 | - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging. | 35 | - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging. | ||
| 36 | 36 | ||||
| 37 | ### Fixed | 37 | ### Fixed | ||
| 38 | + | - **Fullscreen video tab bar**: Fixed tab bar remaining visible during fullscreen video playback. Now hides automatically when entering fullscreen and restores based on normal visibility logic when exiting. | |||
| 39 | + | - **Progress bar stuck state**: Added 30-second timeout to progress bar to auto-hide if page load stalls and never completes. | |||
| 38 | - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews. | 40 | - **OAuth popup login**: Fixed parent page going blank after OAuth popup closes (e.g., Google login on claude.ai, notion.com). Related WebViews share a web process with their parent; destroying the popup was terminating the shared process, killing the parent. Now skips `TerminateWebProcess()` for popup WebViews. | ||
| 39 | - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency. | 41 | - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency. | ||
| 40 | - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag). | 42 | - README CLI examples: updated to match current commands/flags (`purge --force` only, `logs` usage, `sessions list --limit`, removed deprecated `--dmenu` root flag). | ||
M internal/ui/component/progress_bar.go +38 -1
| @@ -15,11 +15,14 @@ const ( | |||
| 15 | 15 | progressStep = 0.02 | |
| 16 | 16 | // Animation interval in milliseconds | |
| 17 | 17 | progressIntervalMs = 16 // ~60fps | |
| 18 | + | // Timeout to auto-hide progress bar if stuck (30 seconds) | |
| 19 | + | progressTimeoutMs = 30000 | |
| 18 | 20 | ) | |
| 19 | 21 | ||
| 20 | 22 | // ProgressBar displays a slim loading progress indicator at the bottom of a pane. | |
| 21 | 23 | // Uses native GtkProgressBar with "osd" styling for overlay appearance. | |
| 22 | 24 | // Implements smooth animation by incrementing towards the target value. | |
| 25 | + | // Includes a 30-second timeout to auto-hide if the page load stalls. | |
| 23 | 26 | type ProgressBar struct { | |
| 24 | 27 | progressBar layout.ProgressBarWidget | |
| 25 | 28 | ||
| @@ -27,6 +30,7 @@ type ProgressBar struct { | |||
| 27 | 30 | currentValue float64 // Current displayed value | |
| 28 | 31 | targetValue float64 // Target value to animate towards | |
| 29 | 32 | animationTimer uint // Timer source ID for animation | |
| 33 | + | timeoutTimer uint // Timer source ID for auto-hide timeout | |
| 30 | 34 | ||
| 31 | 35 | mu sync.Mutex | |
| 32 | 36 | } | |
| @@ -127,7 +131,7 @@ func (pb *ProgressBar) startAnimation() { | |||
| 127 | 131 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | |
| 128 | 132 | } | |
| 129 | 133 | ||
| 130 | - | // Show makes the progress bar visible. | |
| 134 | + | // Show makes the progress bar visible and starts the auto-hide timeout. | |
| 131 | 135 | func (pb *ProgressBar) Show() { | |
| 132 | 136 | pb.mu.Lock() | |
| 133 | 137 | defer pb.mu.Unlock() | |
| @@ -136,6 +140,33 @@ func (pb *ProgressBar) Show() { | |||
| 136 | 140 | pb.visible = true | |
| 137 | 141 | pb.progressBar.SetVisible(true) | |
| 138 | 142 | } | |
| 143 | + | ||
| 144 | + | // Reset timeout timer on every Show call | |
| 145 | + | pb.resetTimeout() | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // resetTimeout cancels any existing timeout and starts a new one. | |
| 149 | + | // Must be called with lock held. | |
| 150 | + | func (pb *ProgressBar) resetTimeout() { | |
| 151 | + | // Cancel existing timeout | |
| 152 | + | if pb.timeoutTimer != 0 { | |
| 153 | + | glib.SourceRemove(pb.timeoutTimer) | |
| 154 | + | pb.timeoutTimer = 0 | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // Start new timeout | |
| 158 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 159 | + | pb.mu.Lock() | |
| 160 | + | // Clear timer ID before unlocking (timer is being removed) | |
| 161 | + | pb.timeoutTimer = 0 | |
| 162 | + | pb.mu.Unlock() | |
| 163 | + | ||
| 164 | + | // Hide the progress bar (acquires lock internally) | |
| 165 | + | pb.Hide() | |
| 166 | + | return false // Don't repeat | |
| 167 | + | }) | |
| 168 | + | ||
| 169 | + | pb.timeoutTimer = glib.TimeoutAdd(progressTimeoutMs, &cb, 0) | |
| 139 | 170 | } | |
| 140 | 171 | ||
| 141 | 172 | // Hide makes the progress bar invisible and resets state. | |
| @@ -153,6 +184,12 @@ func (pb *ProgressBar) Hide() { | |||
| 153 | 184 | pb.animationTimer = 0 | |
| 154 | 185 | } | |
| 155 | 186 | ||
| 187 | + | // Stop timeout timer | |
| 188 | + | if pb.timeoutTimer != 0 { | |
| 189 | + | glib.SourceRemove(pb.timeoutTimer) | |
| 190 | + | pb.timeoutTimer = 0 | |
| 191 | + | } | |
| 192 | + | ||
| 156 | 193 | // Reset values | |
| 157 | 194 | pb.currentValue = 0 | |
| 158 | 195 | pb.targetValue = 0 | |
| @@ -15,11 +15,14 @@ const ( | |||||
| 15 | progressStep = 0.02 | 15 | progressStep = 0.02 | ||
| 16 | // Animation interval in milliseconds | 16 | // Animation interval in milliseconds | ||
| 17 | progressIntervalMs = 16 // ~60fps | 17 | progressIntervalMs = 16 // ~60fps | ||
| 18 | + | // Timeout to auto-hide progress bar if stuck (30 seconds) | |||
| 19 | + | progressTimeoutMs = 30000 | |||
| 18 | ) | 20 | ) | ||
| 19 | 21 | ||||
| 20 | // ProgressBar displays a slim loading progress indicator at the bottom of a pane. | 22 | // ProgressBar displays a slim loading progress indicator at the bottom of a pane. | ||
| 21 | // Uses native GtkProgressBar with "osd" styling for overlay appearance. | 23 | // Uses native GtkProgressBar with "osd" styling for overlay appearance. | ||
| 22 | // Implements smooth animation by incrementing towards the target value. | 24 | // Implements smooth animation by incrementing towards the target value. | ||
| 25 | + | // Includes a 30-second timeout to auto-hide if the page load stalls. | |||
| 23 | type ProgressBar struct { | 26 | type ProgressBar struct { | ||
| 24 | progressBar layout.ProgressBarWidget | 27 | progressBar layout.ProgressBarWidget | ||
| 25 | 28 | ||||
| @@ -27,6 +30,7 @@ type ProgressBar struct { | |||||
| 27 | currentValue float64 // Current displayed value | 30 | currentValue float64 // Current displayed value | ||
| 28 | targetValue float64 // Target value to animate towards | 31 | targetValue float64 // Target value to animate towards | ||
| 29 | animationTimer uint // Timer source ID for animation | 32 | animationTimer uint // Timer source ID for animation | ||
| 33 | + | timeoutTimer uint // Timer source ID for auto-hide timeout | |||
| 30 | 34 | ||||
| 31 | mu sync.Mutex | 35 | mu sync.Mutex | ||
| 32 | } | 36 | } | ||
| @@ -127,7 +131,7 @@ func (pb *ProgressBar) startAnimation() { | |||||
| 127 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | 131 | pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0) | ||
| 128 | } | 132 | } | ||
| 129 | 133 | ||||
| 130 | - | // Show makes the progress bar visible. | 134 | + | // Show makes the progress bar visible and starts the auto-hide timeout. |
| 131 | func (pb *ProgressBar) Show() { | 135 | func (pb *ProgressBar) Show() { | ||
| 132 | pb.mu.Lock() | 136 | pb.mu.Lock() | ||
| 133 | defer pb.mu.Unlock() | 137 | defer pb.mu.Unlock() | ||
| @@ -136,6 +140,33 @@ func (pb *ProgressBar) Show() { | |||||
| 136 | pb.visible = true | 140 | pb.visible = true | ||
| 137 | pb.progressBar.SetVisible(true) | 141 | pb.progressBar.SetVisible(true) | ||
| 138 | } | 142 | } | ||
| 143 | + | ||||
| 144 | + | // Reset timeout timer on every Show call | |||
| 145 | + | pb.resetTimeout() | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // resetTimeout cancels any existing timeout and starts a new one. | |||
| 149 | + | // Must be called with lock held. | |||
| 150 | + | func (pb *ProgressBar) resetTimeout() { | |||
| 151 | + | // Cancel existing timeout | |||
| 152 | + | if pb.timeoutTimer != 0 { | |||
| 153 | + | glib.SourceRemove(pb.timeoutTimer) | |||
| 154 | + | pb.timeoutTimer = 0 | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // Start new timeout | |||
| 158 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 159 | + | pb.mu.Lock() | |||
| 160 | + | // Clear timer ID before unlocking (timer is being removed) | |||
| 161 | + | pb.timeoutTimer = 0 | |||
| 162 | + | pb.mu.Unlock() | |||
| 163 | + | ||||
| 164 | + | // Hide the progress bar (acquires lock internally) | |||
| 165 | + | pb.Hide() | |||
| 166 | + | return false // Don't repeat | |||
| 167 | + | }) | |||
| 168 | + | ||||
| 169 | + | pb.timeoutTimer = glib.TimeoutAdd(progressTimeoutMs, &cb, 0) | |||
| 139 | } | 170 | } | ||
| 140 | 171 | ||||
| 141 | // Hide makes the progress bar invisible and resets state. | 172 | // Hide makes the progress bar invisible and resets state. | ||
| @@ -153,6 +184,12 @@ func (pb *ProgressBar) Hide() { | |||||
| 153 | pb.animationTimer = 0 | 184 | pb.animationTimer = 0 | ||
| 154 | } | 185 | } | ||
| 155 | 186 | ||||
| 187 | + | // Stop timeout timer | |||
| 188 | + | if pb.timeoutTimer != 0 { | |||
| 189 | + | glib.SourceRemove(pb.timeoutTimer) | |||
| 190 | + | pb.timeoutTimer = 0 | |||
| 191 | + | } | |||
| 192 | + | ||||
| 156 | // Reset values | 193 | // Reset values | ||
| 157 | pb.currentValue = 0 | 194 | pb.currentValue = 0 | ||
| 158 | pb.targetValue = 0 | 195 | pb.targetValue = 0 | ||