detached 212c6b7

fix(daemon): address lifecycle review findings

commit
212c6b7f7a2afc535634ca7b0b67b4bc73ff6ca4
author
brice · · 2026-07-28 17:22 UTC
parent
4441ea3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +33 -10
M internal/usecase/daemon/input.go +6 -0
@@ -384,6 +384,12 @@ func (h daemonKeyHandler) Action(action keys.Action) {
384384 case keys.ActionOpenPalette:
385385 h.d.enterPalette(sess, h.ac)
386386 case keys.ActionJumpAttention:
387+ if effect == nil {
388+ if err := h.d.jumpAttention(sess, h.ac); err != nil {
389+ h.d.reportError(sess, err)
390+ }
391+ return
392+ }
387393 effect.bindActionEnd(h.d, "jump-attention")
388394 token := h.roleToken
389395 token.effect = effect
@@ -384,6 +384,12 @@ func (h daemonKeyHandler) Action(action keys.Action) {
384 case keys.ActionOpenPalette:384 case keys.ActionOpenPalette:
385 h.d.enterPalette(sess, h.ac)385 h.d.enterPalette(sess, h.ac)
386 case keys.ActionJumpAttention:386 case keys.ActionJumpAttention:
387+ if effect == nil {
388+ if err := h.d.jumpAttention(sess, h.ac); err != nil {
389+ h.d.reportError(sess, err)
390+ }
391+ return
392+ }
387 effect.bindActionEnd(h.d, "jump-attention")393 effect.bindActionEnd(h.d, "jump-attention")
388 token := h.roleToken394 token := h.roleToken
389 token.effect = effect395 token.effect = effect
M internal/usecase/daemon/layout_mutation.go +6 -1
@@ -69,7 +69,12 @@ func (d *Daemon) mutateTargetLayoutChanged(target daemonActionTarget, requirePan
6969 d.mu.Unlock()
7070
7171 if !d.applyTabLayout(target.session, target.tab) {
72- return false, layout.ErrNotFound
72+ target.tab.mu.Lock()
73+ applied := target.tab.tree == candidate
74+ target.tab.mu.Unlock()
75+ if !applied {
76+ return false, layout.ErrNotFound
77+ }
7378 }
7479 return true, nil
7580 }
@@ -69,7 +69,12 @@ func (d *Daemon) mutateTargetLayoutChanged(target daemonActionTarget, requirePan
69 d.mu.Unlock()69 d.mu.Unlock()
70 70
71 if !d.applyTabLayout(target.session, target.tab) {71 if !d.applyTabLayout(target.session, target.tab) {
72- return false, layout.ErrNotFound72+ target.tab.mu.Lock()
73+ applied := target.tab.tree == candidate
74+ target.tab.mu.Unlock()
75+ if !applied {
76+ return false, layout.ErrNotFound
77+ }
73 }78 }
74 return true, nil79 return true, nil
75 }80 }
M internal/usecase/daemon/move.go +5 -1
@@ -154,7 +154,11 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
154154 }
155155 source.mu.Unlock()
156156 d.mu.Unlock()
157- frozen = freezeRoleEffectGatesWith(roleEffectFreezeOptions{interrupts: interrupts, nonblocking: true}, participants...)
157+ frozen = freezeRoleEffectGatesWith(roleEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
158+ if d.afterRoleEffectGateFrozen != nil {
159+ d.afterRoleEffectGateFrozen("move-pane", ac)
160+ }
161+ }}, participants...)
158162 if !frozen.acquired || !frozen.drained {
159163 frozen.unfreeze()
160164 return errMovePaneInvalid
@@ -154,7 +154,11 @@ func (d *Daemon) movePane(req movePaneRequest) (result error) {
154 }154 }
155 source.mu.Unlock()155 source.mu.Unlock()
156 d.mu.Unlock()156 d.mu.Unlock()
157- frozen = freezeRoleEffectGatesWith(roleEffectFreezeOptions{interrupts: interrupts, nonblocking: true}, participants...)157+ frozen = freezeRoleEffectGatesWith(roleEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
158+ if d.afterRoleEffectGateFrozen != nil {
159+ d.afterRoleEffectGateFrozen("move-pane", ac)
160+ }
161+ }}, participants...)
158 if !frozen.acquired || !frozen.drained {162 if !frozen.acquired || !frozen.drained {
159 frozen.unfreeze()163 frozen.unfreeze()
160 return errMovePaneInvalid164 return errMovePaneInvalid
M internal/usecase/daemon/pane_lifetime_test.go +1 -1
@@ -311,7 +311,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
311311 require.NotNil(t, p)
312312 pty, openCtx := factory.opened(0)
313313 require.NotNil(t, p.ctx)
314- require.Equal(t, openCtx, p.ctx)
314+ require.Same(t, openCtx, p.ctx)
315315 require.NotNil(t, p.cancel)
316316
317317 cancelSession()
@@ -311,7 +311,7 @@ func TestPaneProcessLifetimeIsDaemonRooted(t *testing.T) {
311 require.NotNil(t, p)311 require.NotNil(t, p)
312 pty, openCtx := factory.opened(0)312 pty, openCtx := factory.opened(0)
313 require.NotNil(t, p.ctx)313 require.NotNil(t, p.ctx)
314- require.Equal(t, openCtx, p.ctx)314+ require.Same(t, openCtx, p.ctx)
315 require.NotNil(t, p.cancel)315 require.NotNil(t, p.cancel)
316 316
317 cancelSession()317 cancelSession()
M internal/usecase/daemon/render_resize_deadline.go +1 -1
@@ -112,7 +112,7 @@ type resizeRetryReservation struct {
112112 func (c *renderCoordinator) reserveResizeRetryForLease(epoch uint64, source *attachedClient, lease *attachmentLease, run func()) (*resizeRetryReservation, bool) {
113113 c.mu.Lock()
114114 defer c.mu.Unlock()
115- if !c.leaseCurrentLocked(lease, false) || lease.attachment != source || c.resize.lease != lease || c.resize.source != source || c.resize.epoch != epoch || c.resize.committed != epoch || c.resize.retryAttempts >= maxResizeRetryAttempts {
115+ if c.torndown || !c.leaseCurrentLocked(lease, false) || lease.attachment != source || c.resize.lease != lease || c.resize.source != source || c.resize.epoch != epoch || c.resize.committed != epoch || c.resize.retryAttempts >= maxResizeRetryAttempts {
116116 return nil, false
117117 }
118118 c.resize.retryAttempts++
@@ -112,7 +112,7 @@ type resizeRetryReservation struct {
112 func (c *renderCoordinator) reserveResizeRetryForLease(epoch uint64, source *attachedClient, lease *attachmentLease, run func()) (*resizeRetryReservation, bool) {112 func (c *renderCoordinator) reserveResizeRetryForLease(epoch uint64, source *attachedClient, lease *attachmentLease, run func()) (*resizeRetryReservation, bool) {
113 c.mu.Lock()113 c.mu.Lock()
114 defer c.mu.Unlock()114 defer c.mu.Unlock()
115- if !c.leaseCurrentLocked(lease, false) || lease.attachment != source || c.resize.lease != lease || c.resize.source != source || c.resize.epoch != epoch || c.resize.committed != epoch || c.resize.retryAttempts >= maxResizeRetryAttempts {115+ if c.torndown || !c.leaseCurrentLocked(lease, false) || lease.attachment != source || c.resize.lease != lease || c.resize.source != source || c.resize.epoch != epoch || c.resize.committed != epoch || c.resize.retryAttempts >= maxResizeRetryAttempts {
116 return nil, false116 return nil, false
117 }117 }
118 c.resize.retryAttempts++118 c.resize.retryAttempts++
M internal/usecase/daemon/resume.go +1 -1
@@ -78,10 +78,10 @@ func (d *Daemon) parkAttachmentAs(sess *session, ac *attachedClient, role attach
7878 oldSame *parkedAttachment
7979 )
8080 if old := d.parked[token]; old != nil {
81- retirement := d.retireParkedAttachmentLocked(token, old)
8281 if old.ac == ac {
8382 oldSame = old
8483 } else {
84+ retirement := d.retireParkedAttachmentLocked(token, old)
8585 oldRetirement = &retirement
8686 }
8787 }
@@ -78,10 +78,10 @@ func (d *Daemon) parkAttachmentAs(sess *session, ac *attachedClient, role attach
78 oldSame *parkedAttachment78 oldSame *parkedAttachment
79 )79 )
80 if old := d.parked[token]; old != nil {80 if old := d.parked[token]; old != nil {
81- retirement := d.retireParkedAttachmentLocked(token, old)
82 if old.ac == ac {81 if old.ac == ac {
83 oldSame = old82 oldSame = old
84 } else {83 } else {
84+ retirement := d.retireParkedAttachmentLocked(token, old)
85 oldRetirement = &retirement85 oldRetirement = &retirement
86 }86 }
87 }87 }
M internal/usecase/daemon/role_effect_gate_test.go +12 -4
@@ -1178,10 +1178,17 @@ func TestRoleEffectGateAdmittedSnatchedPanelFinishesBeforePromotion(t *testing.T
11781178 token := replaced.displaced
11791179
11801180 admitted := make(chan struct{})
1181- release := make(chan struct{})
1181+ releaseGate := make(chan struct{})
1182+ release := releaseTestGate(t, releaseGate)
1183+ afterFrozen := make(chan struct{})
11821184 d.afterRoleEffectAdmitted = func(attachmentRoleToken) {
11831185 close(admitted)
1184- <-release
1186+ <-releaseGate
1187+ }
1188+ d.afterRoleEffectGateFrozen = func(action string, ac *attachedClient) {
1189+ if action == "" && ac == old {
1190+ close(afterFrozen)
1191+ }
11851192 }
11861193 effectDone := make(chan struct{})
11871194 wantSize := domain.Size{Cols: 100, Rows: 30}
@@ -1199,12 +1206,13 @@ func TestRoleEffectGateAdmittedSnatchedPanelFinishesBeforePromotion(t *testing.T
11991206 })
12001207 transitionDone <- err
12011208 }()
1209+ <-afterFrozen
12021210 select {
12031211 case err := <-transitionDone:
12041212 t.Fatalf("promotion overtook admitted snatched panel: %v", err)
1205- case <-time.After(20 * time.Millisecond):
1213+ default:
12061214 }
1207- close(release)
1215+ release()
12081216 <-effectDone
12091217 require.NoError(t, <-transitionDone)
12101218 old.sendMu.Lock()
@@ -1178,10 +1178,17 @@ func TestRoleEffectGateAdmittedSnatchedPanelFinishesBeforePromotion(t *testing.T
1178 token := replaced.displaced1178 token := replaced.displaced
1179 1179
1180 admitted := make(chan struct{})1180 admitted := make(chan struct{})
1181- release := make(chan struct{})1181+ releaseGate := make(chan struct{})
1182+ release := releaseTestGate(t, releaseGate)
1183+ afterFrozen := make(chan struct{})
1182 d.afterRoleEffectAdmitted = func(attachmentRoleToken) {1184 d.afterRoleEffectAdmitted = func(attachmentRoleToken) {
1183 close(admitted)1185 close(admitted)
1184- <-release1186+ <-releaseGate
1187+ }
1188+ d.afterRoleEffectGateFrozen = func(action string, ac *attachedClient) {
1189+ if action == "" && ac == old {
1190+ close(afterFrozen)
1191+ }
1185 }1192 }
1186 effectDone := make(chan struct{})1193 effectDone := make(chan struct{})
1187 wantSize := domain.Size{Cols: 100, Rows: 30}1194 wantSize := domain.Size{Cols: 100, Rows: 30}
@@ -1199,12 +1206,13 @@ func TestRoleEffectGateAdmittedSnatchedPanelFinishesBeforePromotion(t *testing.T
1199 })1206 })
1200 transitionDone <- err1207 transitionDone <- err
1201 }()1208 }()
1209+ <-afterFrozen
1202 select {1210 select {
1203 case err := <-transitionDone:1211 case err := <-transitionDone:
1204 t.Fatalf("promotion overtook admitted snatched panel: %v", err)1212 t.Fatalf("promotion overtook admitted snatched panel: %v", err)
1205- case <-time.After(20 * time.Millisecond):1213+ default:
1206 }1214 }
1207- close(release)1215+ release()
1208 <-effectDone1216 <-effectDone
1209 require.NoError(t, <-transitionDone)1217 require.NoError(t, <-transitionDone)
1210 old.sendMu.Lock()1218 old.sendMu.Lock()
M internal/usecase/daemon/transactional_resize.go +1 -1
@@ -291,7 +291,7 @@ func (d *Daemon) applyTabLayoutTransactionWithNotice(sess *session, tb *tab, rep
291291 failed = append(failed, member)
292292 }
293293 }
294- if reportFailure && len(failed) != 0 {
294+ if reportFailure && len(failed) != 0 && resizeMembersOwnerCurrent(failed) {
295295 d.notify(sess, domain.NoticeWarn, domain.NoticeResizeFailed,
296296 "pane resize failed; retrying in background", failed[len(failed)-1].err)
297297 }
@@ -291,7 +291,7 @@ func (d *Daemon) applyTabLayoutTransactionWithNotice(sess *session, tb *tab, rep
291 failed = append(failed, member)291 failed = append(failed, member)
292 }292 }
293 }293 }
294- if reportFailure && len(failed) != 0 {294+ if reportFailure && len(failed) != 0 && resizeMembersOwnerCurrent(failed) {
295 d.notify(sess, domain.NoticeWarn, domain.NoticeResizeFailed,295 d.notify(sess, domain.NoticeWarn, domain.NoticeResizeFailed,
296 "pane resize failed; retrying in background", failed[len(failed)-1].err)296 "pane resize failed; retrying in background", failed[len(failed)-1].err)
297 }297 }

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 12

Tags 11

↑↓ moveenter openesc close