detached 9edd30e

fix(daemon): preserve history navigation under code overrides

commit
9edd30e1b861398f7b71b5f21b0a4b71af84c122
author
brice · · 2026-07-06 17:51 UTC
parent
3b83428
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +53 -44
M internal/usecase/daemon/client.go +21 -19
@@ -39,25 +39,27 @@ import (
3939 )
4040
4141 type attachedClient struct {
42- tr ports.Transport
43- rend *renderer.Renderer
44- overlays *overlayRuntime
45- clientID [16]byte
46- resumeCapable bool
47- resumeToken uint64
48- parked bool
49- nextStateNum uint64
50- echoAck atomic.Uint64
51- bars barCache // only touched while sendMu is held
52- size domain.Size
53- keys *keys.Router
54- sess Guarded[*session]
55- mouseScan mouse.Scanner
56- themeMu sync.Mutex
57- theme themeui.Theme
58- clientTheme themeui.Theme
59- lastCursor cursorOut
60- recentNav recentSessionNavigator
42+ tr ports.Transport
43+ rend *renderer.Renderer
44+ overlays *overlayRuntime
45+ clientID [16]byte
46+ resumeCapable bool
47+ resumeToken uint64
48+ parked bool
49+ nextStateNum uint64
50+ echoAck atomic.Uint64
51+ bars barCache // only touched while sendMu is held
52+ size domain.Size
53+ keys *keys.Router
54+ sess Guarded[*session]
55+ mouseScan mouse.Scanner
56+ themeMu sync.Mutex
57+ theme themeui.Theme
58+ clientTheme themeui.Theme
59+ lastCursor cursorOut
60+ recentNav recentSessionNavigator
61+ // historyNavMu protects historyNav and historyNavTimer. When paint needs
62+ // several locks, take sendMu before historyNavMu, then Daemon.mu/session.mu.
6163 historyNavMu sync.Mutex
6264 historyNav historyNavDisplay
6365 historyNavTimer pendingByteTimer
@@ -39,25 +39,27 @@ import (
39 )39 )
40 40
41 type attachedClient struct {41 type attachedClient struct {
42- tr ports.Transport42+ tr ports.Transport
43- rend *renderer.Renderer43+ rend *renderer.Renderer
44- overlays *overlayRuntime44+ overlays *overlayRuntime
45- clientID [16]byte45+ clientID [16]byte
46- resumeCapable bool46+ resumeCapable bool
47- resumeToken uint6447+ resumeToken uint64
48- parked bool48+ parked bool
49- nextStateNum uint6449+ nextStateNum uint64
50- echoAck atomic.Uint6450+ echoAck atomic.Uint64
51- bars barCache // only touched while sendMu is held51+ bars barCache // only touched while sendMu is held
52- size domain.Size52+ size domain.Size
53- keys *keys.Router53+ keys *keys.Router
54- sess Guarded[*session]54+ sess Guarded[*session]
55- mouseScan mouse.Scanner55+ mouseScan mouse.Scanner
56- themeMu sync.Mutex56+ themeMu sync.Mutex
57- theme themeui.Theme57+ theme themeui.Theme
58- clientTheme themeui.Theme58+ clientTheme themeui.Theme
59- lastCursor cursorOut59+ lastCursor cursorOut
60- recentNav recentSessionNavigator60+ recentNav recentSessionNavigator
61+ // historyNavMu protects historyNav and historyNavTimer. When paint needs
62+ // several locks, take sendMu before historyNavMu, then Daemon.mu/session.mu.
61 historyNavMu sync.Mutex63 historyNavMu sync.Mutex
62 historyNav historyNavDisplay64 historyNav historyNavDisplay
63 historyNavTimer pendingByteTimer65 historyNavTimer pendingByteTimer
M internal/usecase/daemon/input_test.go +8 -0
@@ -310,6 +310,14 @@ func TestHistoryNavDisplay(t *testing.T) {
310310 require.Equal(t, uint64(7), h.gen)
311311 }
312312
313+func TestHistoryNavigationCommandDetectionUsesSlug(t *testing.T) {
314+ require.True(t, isHistoryNavigationCommand("back-session"))
315+ require.True(t, isHistoryNavigationCommand("forward-session"))
316+ require.False(t, isHistoryNavigationCommand("BSK"))
317+ require.False(t, isHistoryNavigationCommand("FSK"))
318+ require.False(t, isHistoryNavigationCommand("next-tab"))
319+}
320+
313321 func TestHistoryNavActivation(t *testing.T) {
314322 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
315323 defer releaseAll(releases)
@@ -310,6 +310,14 @@ func TestHistoryNavDisplay(t *testing.T) {
310 require.Equal(t, uint64(7), h.gen)310 require.Equal(t, uint64(7), h.gen)
311 }311 }
312 312
313+func TestHistoryNavigationCommandDetectionUsesSlug(t *testing.T) {
314+ require.True(t, isHistoryNavigationCommand("back-session"))
315+ require.True(t, isHistoryNavigationCommand("forward-session"))
316+ require.False(t, isHistoryNavigationCommand("BSK"))
317+ require.False(t, isHistoryNavigationCommand("FSK"))
318+ require.False(t, isHistoryNavigationCommand("next-tab"))
319+}
320+
313 func TestHistoryNavActivation(t *testing.T) {321 func TestHistoryNavActivation(t *testing.T) {
314 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)322 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
315 defer releaseAll(releases)323 defer releaseAll(releases)
M internal/usecase/daemon/palette.go +3 -3
@@ -122,7 +122,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
122122 ac.overlays.paletteMu.Unlock()
123123
124124 if run && ok {
125- if !isHistoryNavigationCommand(cmd.Code) {
125+ if !isHistoryNavigationCommand(cmd.Slug) {
126126 d.clearHistoryNav(ac)
127127 }
128128 if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil {
@@ -140,8 +140,8 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
140140 }
141141 }
142142
143-func isHistoryNavigationCommand(code string) bool {
144- return code == "BSK" || code == "FSK"
143+func isHistoryNavigationCommand(slug string) bool {
144+ return slug == "back-session" || slug == "forward-session"
145145 }
146146
147147 type paletteExec struct {
@@ -122,7 +122,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
122 ac.overlays.paletteMu.Unlock()122 ac.overlays.paletteMu.Unlock()
123 123
124 if run && ok {124 if run && ok {
125- if !isHistoryNavigationCommand(cmd.Code) {125+ if !isHistoryNavigationCommand(cmd.Slug) {
126 d.clearHistoryNav(ac)126 d.clearHistoryNav(ac)
127 }127 }
128 if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil {128 if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}); err != nil {
@@ -140,8 +140,8 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
140 }140 }
141 }141 }
142 142
143-func isHistoryNavigationCommand(code string) bool {143+func isHistoryNavigationCommand(slug string) bool {
144- return code == "BSK" || code == "FSK"144+ return slug == "back-session" || slug == "forward-session"
145 }145 }
146 146
147 type paletteExec struct {147 type paletteExec struct {
M internal/usecase/daemon/recent_nav.go +3 -0
@@ -120,6 +120,9 @@ func (d *Daemon) showHistoryNav(ac *attachedClient) {
120120 }
121121
122122 func (d *Daemon) clearHistoryNav(ac *attachedClient) {
123+ if ac == nil {
124+ return
125+ }
123126 ac.clearHistoryNav()
124127 }
125128
@@ -120,6 +120,9 @@ func (d *Daemon) showHistoryNav(ac *attachedClient) {
120 }120 }
121 121
122 func (d *Daemon) clearHistoryNav(ac *attachedClient) {122 func (d *Daemon) clearHistoryNav(ac *attachedClient) {
123+ if ac == nil {
124+ return
125+ }
123 ac.clearHistoryNav()126 ac.clearHistoryNav()
124 }127 }
125 128
M internal/usecase/daemon/status.go +18 -22
@@ -19,9 +19,11 @@
1919 //
2020 // Locking: a pane's screen/scrollback and per-client renderer shadow are
2121 // guarded by pane.mu/tab.mu as appropriate; the attached-client pointer by
22-// session.mu; the registry by Daemon.mu. When more than one is held the order
23-// is always attachedClient.sendMu > Daemon.mu > session.mu > tab.mu > pane.mu.
24-// The PTY reader only ever takes pane.mu, so it never blocks on a slow client.
22+// session.mu; the registry by Daemon.mu. History-navigation display state is
23+// guarded by attachedClient.historyNavMu. When more than one is held the order
24+// is always attachedClient.sendMu > attachedClient.historyNavMu > Daemon.mu >
25+// session.mu > tab.mu > pane.mu. The PTY reader only ever takes pane.mu, so it
26+// never blocks on a slow client.
2527 package daemon
2628
2729 import (
@@ -91,16 +93,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
9193 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
9294 for i, sess := range fittedMRU {
9395 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))
94- name := sess.name
95- if sess.ephemeral {
96- name += "*"
97- }
98- writeStatusText(row, &x, " "+name, style)
99- if sess.attention {
100- writeStatusText(row, &x, " ", style)
101- writeBell(row, &x, state.attentionFrame)
102- }
103- writeStatusText(row, &x, " ", style)
96+ drawStatusSessionEntry(row, &x, sess.name, sess.ephemeral, sess.attention, style, state.attentionFrame)
10497 }
10598 drawRightPlainText(row, rightText, x, styles.statusBar)
10699 }
@@ -111,17 +104,20 @@ func drawHistoryNavSessions(row []renderer.Cell, x *int, entries []historyNavSes
111104 if sess.active {
112105 style = styles.accent
113106 }
114- name := sess.name
115- if sess.ephemeral {
116- name += "*"
117- }
118- writeStatusText(row, x, " "+name, style)
119- if sess.attention {
120- writeStatusText(row, x, " ", style)
121- writeBell(row, x, attentionFrame)
122- }
107+ drawStatusSessionEntry(row, x, sess.name, sess.ephemeral, sess.attention, style, attentionFrame)
108+ }
109+}
110+
111+func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, ephemeral, attention bool, style renderer.Style, attentionFrame int) {
112+ if ephemeral {
113+ name += "*"
114+ }
115+ writeStatusText(row, x, " "+name, style)
116+ if attention {
123117 writeStatusText(row, x, " ", style)
118+ writeBell(row, x, attentionFrame)
124119 }
120+ writeStatusText(row, x, " ", style)
125121 }
126122
127123 func composeBottomRightText(scriptText, copyFeedback string) string {
@@ -19,9 +19,11 @@
19 //19 //
20 // Locking: a pane's screen/scrollback and per-client renderer shadow are20 // Locking: a pane's screen/scrollback and per-client renderer shadow are
21 // guarded by pane.mu/tab.mu as appropriate; the attached-client pointer by21 // guarded by pane.mu/tab.mu as appropriate; the attached-client pointer by
22-// session.mu; the registry by Daemon.mu. When more than one is held the order22+// session.mu; the registry by Daemon.mu. History-navigation display state is
23-// is always attachedClient.sendMu > Daemon.mu > session.mu > tab.mu > pane.mu.23+// guarded by attachedClient.historyNavMu. When more than one is held the order
24-// The PTY reader only ever takes pane.mu, so it never blocks on a slow client.24+// is always attachedClient.sendMu > attachedClient.historyNavMu > Daemon.mu >
25+// session.mu > tab.mu > pane.mu. The PTY reader only ever takes pane.mu, so it
26+// never blocks on a slow client.
25 package daemon27 package daemon
26 28
27 import (29 import (
@@ -91,16 +93,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
91 fittedMRU := fitMRU(state.mru, len(row), x, rightText)93 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
92 for i, sess := range fittedMRU {94 for i, sess := range fittedMRU {
93 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))95 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))
94- name := sess.name96+ drawStatusSessionEntry(row, &x, sess.name, sess.ephemeral, sess.attention, style, state.attentionFrame)
95- if sess.ephemeral {
96- name += "*"
97- }
98- writeStatusText(row, &x, " "+name, style)
99- if sess.attention {
100- writeStatusText(row, &x, " ", style)
101- writeBell(row, &x, state.attentionFrame)
102- }
103- writeStatusText(row, &x, " ", style)
104 }97 }
105 drawRightPlainText(row, rightText, x, styles.statusBar)98 drawRightPlainText(row, rightText, x, styles.statusBar)
106 }99 }
@@ -111,17 +104,20 @@ func drawHistoryNavSessions(row []renderer.Cell, x *int, entries []historyNavSes
111 if sess.active {104 if sess.active {
112 style = styles.accent105 style = styles.accent
113 }106 }
114- name := sess.name107+ drawStatusSessionEntry(row, x, sess.name, sess.ephemeral, sess.attention, style, attentionFrame)
115- if sess.ephemeral {108+ }
116- name += "*"109+}
117- }110+
118- writeStatusText(row, x, " "+name, style)111+func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, ephemeral, attention bool, style renderer.Style, attentionFrame int) {
119- if sess.attention {112+ if ephemeral {
120- writeStatusText(row, x, " ", style)113+ name += "*"
121- writeBell(row, x, attentionFrame)114+ }
122- }115+ writeStatusText(row, x, " "+name, style)
116+ if attention {
123 writeStatusText(row, x, " ", style)117 writeStatusText(row, x, " ", style)
118+ writeBell(row, x, attentionFrame)
124 }119 }
120+ writeStatusText(row, x, " ", style)
125 }121 }
126 122
127 func composeBottomRightText(scriptText, copyFeedback string) string {123 func composeBottomRightText(scriptText, copyFeedback string) string {

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