detached e2e231b

fix(palette): honor effective recent jump code

commit
e2e231b41ffb38d4b32cfb51129fe86750044717
author
brice · · 2026-07-11 12:30 UTC
parent
9818e1d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +49 -3
M internal/usecase/daemon/palette.go +3 -3
@@ -144,7 +144,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
144144 // fuzzy selection can never turn an unrelated query into a jump.
145145 // Static commands retain normal palette behavior and run the selected
146146 // fuzzy match.
147- if cmd.Code == "JRS" {
147+ if cmd.Slug == "jump-recent-session" {
148148 action, valid := palette.ParseAction([]command.Command{cmd}, rawQuery)
149149 if !valid {
150150 ac.overlays.paletteFeedback = "invalid command arguments"
@@ -160,7 +160,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
160160 })
161161 if changed {
162162 active, ok := ac.overlays.palette.ArgumentCommand()
163- if ok && active.Code == "JRS" {
163+ if ok && active.Slug == "jump-recent-session" {
164164 hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active))
165165 ac.overlays.paletteHints = hints
166166 } else {
@@ -184,7 +184,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
184184
185185 // Revalidate live target without paletteMu. A captured ID is never replaced
186186 // by a newly-ranked session, so MRU changes cannot shift a requested rank.
187- if cmd.Code == "JRS" {
187+ if cmd.Slug == "jump-recent-session" {
188188 rank, err := command.ParsePositiveDecimal(args)
189189 if err != nil {
190190 d.paletteFailure(ac, generation, rawQuery, "requested recent session is unavailable")
@@ -144,7 +144,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
144 // fuzzy selection can never turn an unrelated query into a jump.144 // fuzzy selection can never turn an unrelated query into a jump.
145 // Static commands retain normal palette behavior and run the selected145 // Static commands retain normal palette behavior and run the selected
146 // fuzzy match.146 // fuzzy match.
147- if cmd.Code == "JRS" {147+ if cmd.Slug == "jump-recent-session" {
148 action, valid := palette.ParseAction([]command.Command{cmd}, rawQuery)148 action, valid := palette.ParseAction([]command.Command{cmd}, rawQuery)
149 if !valid {149 if !valid {
150 ac.overlays.paletteFeedback = "invalid command arguments"150 ac.overlays.paletteFeedback = "invalid command arguments"
@@ -160,7 +160,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
160 })160 })
161 if changed {161 if changed {
162 active, ok := ac.overlays.palette.ArgumentCommand()162 active, ok := ac.overlays.palette.ArgumentCommand()
163- if ok && active.Code == "JRS" {163+ if ok && active.Slug == "jump-recent-session" {
164 hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active))164 hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active))
165 ac.overlays.paletteHints = hints165 ac.overlays.paletteHints = hints
166 } else {166 } else {
@@ -184,7 +184,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
184 184
185 // Revalidate live target without paletteMu. A captured ID is never replaced185 // Revalidate live target without paletteMu. A captured ID is never replaced
186 // by a newly-ranked session, so MRU changes cannot shift a requested rank.186 // by a newly-ranked session, so MRU changes cannot shift a requested rank.
187- if cmd.Code == "JRS" {187+ if cmd.Slug == "jump-recent-session" {
188 rank, err := command.ParsePositiveDecimal(args)188 rank, err := command.ParsePositiveDecimal(args)
189 if err != nil {189 if err != nil {
190 d.paletteFailure(ac, generation, rawQuery, "requested recent session is unavailable")190 d.paletteFailure(ac, generation, rawQuery, "requested recent session is unavailable")
M internal/usecase/daemon/palette_test.go +46 -0
@@ -70,6 +70,37 @@ func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
7070 ac.overlays.paletteMu.Unlock()
7171 }
7272
73+func TestPaletteJRSUsesEffectiveOverrideOnly(t *testing.T) {
74+ d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
75+ defer releaseAll(releases)
76+ d.ApplyConfig(domain.Config{Codes: map[string]string{"jump-recent-session": "RJS"}})
77+
78+ d.handleInput(current, ac, []byte("\x1b "))
79+ awaitFrame(t, sends, ports.MsgOutput)
80+ d.handleInput(current, ac, []byte("RJS"))
81+ awaitFrame(t, sends, ports.MsgOutput)
82+ ac.overlays.paletteMu.Lock()
83+ require.Equal(t, command.ContextHintRecentSessions, ac.overlays.paletteHints.Kind)
84+ ac.overlays.paletteMu.Unlock()
85+ d.handleInput(current, ac, []byte(" 1\r"))
86+
87+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
88+ require.False(t, ac.overlays.paletteActive())
89+ awaitFrame(t, sends, ports.MsgOutput)
90+ awaitFrame(t, sends, ports.MsgOutput)
91+
92+ d.handleInput(ac.currentSession(), ac, []byte("\x1b "))
93+ awaitFrame(t, sends, ports.MsgOutput)
94+ d.handleInput(ac.currentSession(), ac, []byte("JRS"))
95+ awaitFrame(t, sends, ports.MsgOutput)
96+ ac.overlays.paletteMu.Lock()
97+ require.Equal(t, command.ContextHint(0), ac.overlays.paletteHints.Kind)
98+ ac.overlays.paletteMu.Unlock()
99+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
100+ d.handleInput(ac.currentSession(), ac, []byte("\r"))
101+ require.True(t, ac.overlays.paletteActive(), "literal JRS must not execute an overridden jump command")
102+}
103+
73104 func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) {
74105 d, sess, ac, sends, releases := newManualTabSession(t, 2)
75106 defer releases[0]()
@@ -100,6 +131,21 @@ func TestPaletteJRSUsesCapturedRankAfterMRUChanges(t *testing.T) {
100131 awaitFrame(t, sends, ports.MsgOutput)
101132 }
102133
134+func TestPaletteJRSThenBSKReversesJump(t *testing.T) {
135+ d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
136+ defer releaseAll(releases)
137+
138+ d.handleInput(current, ac, []byte("\x1b "))
139+ awaitFrame(t, sends, ports.MsgOutput)
140+ d.handleInput(current, ac, []byte("JRS 1\r"))
141+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
142+ awaitFrame(t, sends, ports.MsgOutput)
143+ awaitFrame(t, sends, ports.MsgOutput)
144+
145+ runPaletteCommand(t, d, ac.currentSession(), ac, "BSK")
146+ require.Same(t, current, ac.currentSession())
147+}
148+
103149 func TestPaletteJRSDisplacedTargetKeepsInteractionOpen(t *testing.T) {
104150 p, release := newBlockingPTY(t)
105151 defer release()
@@ -70,6 +70,37 @@ func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
70 ac.overlays.paletteMu.Unlock()70 ac.overlays.paletteMu.Unlock()
71 }71 }
72 72
73+func TestPaletteJRSUsesEffectiveOverrideOnly(t *testing.T) {
74+ d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
75+ defer releaseAll(releases)
76+ d.ApplyConfig(domain.Config{Codes: map[string]string{"jump-recent-session": "RJS"}})
77+
78+ d.handleInput(current, ac, []byte("\x1b "))
79+ awaitFrame(t, sends, ports.MsgOutput)
80+ d.handleInput(current, ac, []byte("RJS"))
81+ awaitFrame(t, sends, ports.MsgOutput)
82+ ac.overlays.paletteMu.Lock()
83+ require.Equal(t, command.ContextHintRecentSessions, ac.overlays.paletteHints.Kind)
84+ ac.overlays.paletteMu.Unlock()
85+ d.handleInput(current, ac, []byte(" 1\r"))
86+
87+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
88+ require.False(t, ac.overlays.paletteActive())
89+ awaitFrame(t, sends, ports.MsgOutput)
90+ awaitFrame(t, sends, ports.MsgOutput)
91+
92+ d.handleInput(ac.currentSession(), ac, []byte("\x1b "))
93+ awaitFrame(t, sends, ports.MsgOutput)
94+ d.handleInput(ac.currentSession(), ac, []byte("JRS"))
95+ awaitFrame(t, sends, ports.MsgOutput)
96+ ac.overlays.paletteMu.Lock()
97+ require.Equal(t, command.ContextHint(0), ac.overlays.paletteHints.Kind)
98+ ac.overlays.paletteMu.Unlock()
99+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
100+ d.handleInput(ac.currentSession(), ac, []byte("\r"))
101+ require.True(t, ac.overlays.paletteActive(), "literal JRS must not execute an overridden jump command")
102+}
103+
73 func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) {104 func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) {
74 d, sess, ac, sends, releases := newManualTabSession(t, 2)105 d, sess, ac, sends, releases := newManualTabSession(t, 2)
75 defer releases[0]()106 defer releases[0]()
@@ -100,6 +131,21 @@ func TestPaletteJRSUsesCapturedRankAfterMRUChanges(t *testing.T) {
100 awaitFrame(t, sends, ports.MsgOutput)131 awaitFrame(t, sends, ports.MsgOutput)
101 }132 }
102 133
134+func TestPaletteJRSThenBSKReversesJump(t *testing.T) {
135+ d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
136+ defer releaseAll(releases)
137+
138+ d.handleInput(current, ac, []byte("\x1b "))
139+ awaitFrame(t, sends, ports.MsgOutput)
140+ d.handleInput(current, ac, []byte("JRS 1\r"))
141+ require.Same(t, d.sessions[domain.SessionID("recent")], ac.currentSession())
142+ awaitFrame(t, sends, ports.MsgOutput)
143+ awaitFrame(t, sends, ports.MsgOutput)
144+
145+ runPaletteCommand(t, d, ac.currentSession(), ac, "BSK")
146+ require.Same(t, current, ac.currentSession())
147+}
148+
103 func TestPaletteJRSDisplacedTargetKeepsInteractionOpen(t *testing.T) {149 func TestPaletteJRSDisplacedTargetKeepsInteractionOpen(t *testing.T) {
104 p, release := newBlockingPTY(t)150 p, release := newBlockingPTY(t)
105 defer release()151 defer release()

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