detached 391596f

fix(session): clear invalid toggle targets

commit
391596fd8fc92966bf722297d2d4d47028c1ce52
author
brice · · 2026-07-11 11:47 UTC
parent
316c07f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +32 -0
M internal/usecase/daemon/client.go +1 -0
@@ -644,6 +644,7 @@ func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed por
644644 return
645645 }
646646 d.resetScreenDefaultColors(sess)
647+ ac.clearPreviousSession()
647648 _ = ac.closeCapturedTransport(failed)
648649 d.log.Warn("detached client after send error", "session", sess.name)
649650 }
@@ -644,6 +644,7 @@ func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed por
644 return644 return
645 }645 }
646 d.resetScreenDefaultColors(sess)646 d.resetScreenDefaultColors(sess)
647+ ac.clearPreviousSession()
647 _ = ac.closeCapturedTransport(failed)648 _ = ac.closeCapturedTransport(failed)
648 d.log.Warn("detached client after send error", "session", sess.name)649 d.log.Warn("detached client after send error", "session", sess.name)
649 }650 }
M internal/usecase/daemon/input_test.go +25 -0
@@ -445,6 +445,31 @@ func TestPaletteBackSessionDoesNotMoveWithoutValidTarget(t *testing.T) {
445445 tc.prepare()
446446 runPaletteCommand(t, d, current, ac, "BSK")
447447 require.Same(t, current, ac.currentSession())
448+ if tc.name != "no target" {
449+ require.Nil(t, ac.previousSession.Get(), "invalid target must not remain toggleable")
450+ }
451+ })
452+ }
453+}
454+
455+func TestTerminalTeardownClearsPreviousSession(t *testing.T) {
456+ for _, tc := range []struct {
457+ name string
458+ teardown func(*Daemon, *session, *attachedClient)
459+ }{
460+ {name: "send error", teardown: func(d *Daemon, sess *session, ac *attachedClient) { d.detachOnSendError(sess, ac, ac.transport()) }},
461+ {name: "session kill", teardown: func(d *Daemon, sess *session, _ *attachedClient) {
462+ require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
463+ }},
464+ } {
465+ t.Run(tc.name, func(t *testing.T) {
466+ d, current, ac, _, releases := newRecentNavigationTestSessions(t)
467+ defer releaseAll(releases)
468+
469+ ac.previousSession.Set(d.sessions[domain.SessionID("recent")])
470+ tc.teardown(d, current, ac)
471+
472+ require.Nil(t, ac.previousSession.Get())
448473 })
449474 }
450475 }
@@ -445,6 +445,31 @@ func TestPaletteBackSessionDoesNotMoveWithoutValidTarget(t *testing.T) {
445 tc.prepare()445 tc.prepare()
446 runPaletteCommand(t, d, current, ac, "BSK")446 runPaletteCommand(t, d, current, ac, "BSK")
447 require.Same(t, current, ac.currentSession())447 require.Same(t, current, ac.currentSession())
448+ if tc.name != "no target" {
449+ require.Nil(t, ac.previousSession.Get(), "invalid target must not remain toggleable")
450+ }
451+ })
452+ }
453+}
454+
455+func TestTerminalTeardownClearsPreviousSession(t *testing.T) {
456+ for _, tc := range []struct {
457+ name string
458+ teardown func(*Daemon, *session, *attachedClient)
459+ }{
460+ {name: "send error", teardown: func(d *Daemon, sess *session, ac *attachedClient) { d.detachOnSendError(sess, ac, ac.transport()) }},
461+ {name: "session kill", teardown: func(d *Daemon, sess *session, _ *attachedClient) {
462+ require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
463+ }},
464+ } {
465+ t.Run(tc.name, func(t *testing.T) {
466+ d, current, ac, _, releases := newRecentNavigationTestSessions(t)
467+ defer releaseAll(releases)
468+
469+ ac.previousSession.Set(d.sessions[domain.SessionID("recent")])
470+ tc.teardown(d, current, ac)
471+
472+ require.Nil(t, ac.previousSession.Get())
448 })473 })
449 }474 }
450 }475 }
M internal/usecase/daemon/session.go +1 -0
@@ -734,6 +734,7 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
734734 if ac != nil {
735735 ac.cancelResizePaint()
736736 d.unregisterPreview(ac)
737+ ac.clearPreviousSession()
737738 ac.setSession(nil)
738739 }
739740
@@ -734,6 +734,7 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error {
734 if ac != nil {734 if ac != nil {
735 ac.cancelResizePaint()735 ac.cancelResizePaint()
736 d.unregisterPreview(ac)736 d.unregisterPreview(ac)
737+ ac.clearPreviousSession()
737 ac.setSession(nil)738 ac.setSession(nil)
738 }739 }
739 740
M internal/usecase/daemon/session_back.go +5 -0
@@ -10,6 +10,11 @@ func (d *Daemon) backSession(current *session, ac *attachedClient) {
1010 }
1111 target := ac.previousSession.Get()
1212 if target == nil || target == current || d.sessionByID(target.id) != target {
13+ ac.previousSession.With(func(previous **session) {
14+ if *previous == target {
15+ *previous = nil
16+ }
17+ })
1318 d.paint(current, ac, true)
1419 return
1520 }
@@ -10,6 +10,11 @@ func (d *Daemon) backSession(current *session, ac *attachedClient) {
10 }10 }
11 target := ac.previousSession.Get()11 target := ac.previousSession.Get()
12 if target == nil || target == current || d.sessionByID(target.id) != target {12 if target == nil || target == current || d.sessionByID(target.id) != target {
13+ ac.previousSession.With(func(previous **session) {
14+ if *previous == target {
15+ *previous = nil
16+ }
17+ })
13 d.paint(current, ac, true)18 d.paint(current, ac, true)
14 return19 return
15 }20 }

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 2

Tags 13

↑↓ moveenter openesc close