detached 5544875

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 Unverified2B82207D370127A0 The 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.
3535 - **Console logging**: Standardized console timestamps to `HH:MM:SS` across bootstrap and session logging.
3636
3737 ### 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.
3840 - **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.
3941 - Startup: defer WebView pool prewarm until after initial tab creation to reduce cold-start navigation latency.
4042 - 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 ### Fixed37 ### 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 (
1515 progressStep = 0.02
1616 // Animation interval in milliseconds
1717 progressIntervalMs = 16 // ~60fps
18+ // Timeout to auto-hide progress bar if stuck (30 seconds)
19+ progressTimeoutMs = 30000
1820 )
1921
2022 // ProgressBar displays a slim loading progress indicator at the bottom of a pane.
2123 // Uses native GtkProgressBar with "osd" styling for overlay appearance.
2224 // Implements smooth animation by incrementing towards the target value.
25+// Includes a 30-second timeout to auto-hide if the page load stalls.
2326 type ProgressBar struct {
2427 progressBar layout.ProgressBarWidget
2528
@@ -27,6 +30,7 @@ type ProgressBar struct {
2730 currentValue float64 // Current displayed value
2831 targetValue float64 // Target value to animate towards
2932 animationTimer uint // Timer source ID for animation
33+ timeoutTimer uint // Timer source ID for auto-hide timeout
3034
3135 mu sync.Mutex
3236 }
@@ -127,7 +131,7 @@ func (pb *ProgressBar) startAnimation() {
127131 pb.animationTimer = glib.TimeoutAdd(progressIntervalMs, &cb, 0)
128132 }
129133
130-// Show makes the progress bar visible.
134+// Show makes the progress bar visible and starts the auto-hide timeout.
131135 func (pb *ProgressBar) Show() {
132136 pb.mu.Lock()
133137 defer pb.mu.Unlock()
@@ -136,6 +140,33 @@ func (pb *ProgressBar) Show() {
136140 pb.visible = true
137141 pb.progressBar.SetVisible(true)
138142 }
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)
139170 }
140171
141172 // Hide makes the progress bar invisible and resets state.
@@ -153,6 +184,12 @@ func (pb *ProgressBar) Hide() {
153184 pb.animationTimer = 0
154185 }
155186
187+ // Stop timeout timer
188+ if pb.timeoutTimer != 0 {
189+ glib.SourceRemove(pb.timeoutTimer)
190+ pb.timeoutTimer = 0
191+ }
192+
156193 // Reset values
157194 pb.currentValue = 0
158195 pb.targetValue = 0
@@ -15,11 +15,14 @@ const (
15 progressStep = 0.0215 progressStep = 0.02
16 // Animation interval in milliseconds16 // Animation interval in milliseconds
17 progressIntervalMs = 16 // ~60fps17 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.ProgressBarWidget27 progressBar layout.ProgressBarWidget
25 28
@@ -27,6 +30,7 @@ type ProgressBar struct {
27 currentValue float64 // Current displayed value30 currentValue float64 // Current displayed value
28 targetValue float64 // Target value to animate towards31 targetValue float64 // Target value to animate towards
29 animationTimer uint // Timer source ID for animation32 animationTimer uint // Timer source ID for animation
33+ timeoutTimer uint // Timer source ID for auto-hide timeout
30 34
31 mu sync.Mutex35 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 = true140 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 = 0184 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 values193 // Reset values
157 pb.currentValue = 0194 pb.currentValue = 0
158 pb.targetValue = 0195 pb.targetValue = 0

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close