detached 59a790d

fix(palette): render recent jump guidance

commit
59a790dbc6e8cade564d208c320f57d2ac0a35c6
author
brice · · 2026-07-11 12:11 UTC
parent
1697b4c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +51 -3
M internal/usecase/daemon/palette.go +10 -2
@@ -39,7 +39,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
3939 ac.overlays.paletteGeneration++
4040 ac.overlays.palette = palette.New(commands)
4141 ac.overlays.paletteRecent = recent
42- ac.overlays.paletteHints = recentSessionHints(recent, nil)
42+ ac.overlays.paletteHints = palette.ContextualHints{}
4343 ac.overlays.paletteFeedback = ""
4444 ac.overlays.palettePending = nil
4545 ac.overlays.paletteMu.Unlock()
@@ -150,7 +150,15 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
150150 },
151151 })
152152 if changed {
153- ac.overlays.paletteHints = recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), cmd))
153+ active, ok := ac.overlays.palette.ArgumentCommand()
154+ if ok && active.Code == "JRS" {
155+ hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active))
156+ ac.overlays.paletteHints = hints
157+ ac.overlays.palette.SetFeedback(hints.Feedback)
158+ } else {
159+ ac.overlays.paletteHints = palette.ContextualHints{}
160+ ac.overlays.palette.SetFeedback("")
161+ }
154162 }
155163 if cancel {
156164 d.clearPaletteLocked(ac)
@@ -39,7 +39,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
39 ac.overlays.paletteGeneration++39 ac.overlays.paletteGeneration++
40 ac.overlays.palette = palette.New(commands)40 ac.overlays.palette = palette.New(commands)
41 ac.overlays.paletteRecent = recent41 ac.overlays.paletteRecent = recent
42- ac.overlays.paletteHints = recentSessionHints(recent, nil)42+ ac.overlays.paletteHints = palette.ContextualHints{}
43 ac.overlays.paletteFeedback = ""43 ac.overlays.paletteFeedback = ""
44 ac.overlays.palettePending = nil44 ac.overlays.palettePending = nil
45 ac.overlays.paletteMu.Unlock()45 ac.overlays.paletteMu.Unlock()
@@ -150,7 +150,15 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
150 },150 },
151 })151 })
152 if changed {152 if changed {
153- ac.overlays.paletteHints = recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), cmd))153+ active, ok := ac.overlays.palette.ArgumentCommand()
154+ if ok && active.Code == "JRS" {
155+ hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active))
156+ ac.overlays.paletteHints = hints
157+ ac.overlays.palette.SetFeedback(hints.Feedback)
158+ } else {
159+ ac.overlays.paletteHints = palette.ContextualHints{}
160+ ac.overlays.palette.SetFeedback("")
161+ }
154 }162 }
155 if cancel {163 if cancel {
156 d.clearPaletteLocked(ac)164 d.clearPaletteLocked(ac)
M internal/usecase/daemon/palette_test.go +22 -0
@@ -48,6 +48,28 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
4848 awaitFrame(t, sends, ports.MsgOutput)
4949 }
5050
51+func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
52+ p, release := newBlockingPTY(t)
53+ defer release()
54+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
55+
56+ d.handleInput(sess, ac, []byte("\x1b "))
57+ awaitFrame(t, sends, ports.MsgOutput)
58+ d.handleInput(sess, ac, []byte("JRS"))
59+ awaitFrame(t, sends, ports.MsgOutput)
60+
61+ ac.overlays.paletteMu.Lock()
62+ require.Equal(t, command.ContextHintRecentSessions, ac.overlays.paletteHints.Kind)
63+ require.Equal(t, "no recent sessions", ac.overlays.paletteHints.Feedback)
64+ ac.overlays.paletteMu.Unlock()
65+
66+ d.handleInput(sess, ac, []byte("\b\b\bRNS"))
67+ awaitFrame(t, sends, ports.MsgOutput)
68+ ac.overlays.paletteMu.Lock()
69+ require.Equal(t, command.ContextHint(0), ac.overlays.paletteHints.Kind)
70+ ac.overlays.paletteMu.Unlock()
71+}
72+
5173 func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
5274 p, release := newBlockingPTY(t)
5375 defer release()
@@ -48,6 +48,28 @@ func TestPaletteOpenTypeEnterRunAndEscClose(t *testing.T) {
48 awaitFrame(t, sends, ports.MsgOutput)48 awaitFrame(t, sends, ports.MsgOutput)
49 }49 }
50 50
51+func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
52+ p, release := newBlockingPTY(t)
53+ defer release()
54+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
55+
56+ d.handleInput(sess, ac, []byte("\x1b "))
57+ awaitFrame(t, sends, ports.MsgOutput)
58+ d.handleInput(sess, ac, []byte("JRS"))
59+ awaitFrame(t, sends, ports.MsgOutput)
60+
61+ ac.overlays.paletteMu.Lock()
62+ require.Equal(t, command.ContextHintRecentSessions, ac.overlays.paletteHints.Kind)
63+ require.Equal(t, "no recent sessions", ac.overlays.paletteHints.Feedback)
64+ ac.overlays.paletteMu.Unlock()
65+
66+ d.handleInput(sess, ac, []byte("\b\b\bRNS"))
67+ awaitFrame(t, sends, ports.MsgOutput)
68+ ac.overlays.paletteMu.Lock()
69+ require.Equal(t, command.ContextHint(0), ac.overlays.paletteHints.Kind)
70+ ac.overlays.paletteMu.Unlock()
71+}
72+
51 func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {73 func TestPaletteFLTExecutesFloatingToggle(t *testing.T) {
52 p, release := newBlockingPTY(t)74 p, release := newBlockingPTY(t)
53 defer release()75 defer release()
M internal/usecase/palette/model.go +9 -1
@@ -84,6 +84,14 @@ func (m *Model) Selected() (command.Command, bool) {
8484 }
8585 return m.matches[m.selected].Command, true
8686 }
87+
88+// ArgumentCommand returns the exact argument-taking command being entered.
89+func (m *Model) ArgumentCommand() (command.Command, bool) {
90+ if m == nil {
91+ return command.Command{}, false
92+ }
93+ return ArgumentCommand(m.commands, m.Query())
94+}
8795 func (m *Model) Matches() []Match {
8896 if m == nil {
8997 return nil
@@ -201,7 +209,7 @@ func (m *Model) Render(inner domain.Size, styles RenderStyles) renderer.Frame {
201209 x++
202210 }
203211 for x < codeWidth+1 && x < frame.Width {
204- frame.Set(x, y+1, renderer.Cell{Rune: ' ', Style: style})
212+ frame.Set(x, y+start, renderer.Cell{Rune: ' ', Style: style})
205213 x++
206214 }
207215 ui.DrawText(frame, x, y+start, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc))
@@ -84,6 +84,14 @@ func (m *Model) Selected() (command.Command, bool) {
84 }84 }
85 return m.matches[m.selected].Command, true85 return m.matches[m.selected].Command, true
86 }86 }
87+
88+// ArgumentCommand returns the exact argument-taking command being entered.
89+func (m *Model) ArgumentCommand() (command.Command, bool) {
90+ if m == nil {
91+ return command.Command{}, false
92+ }
93+ return ArgumentCommand(m.commands, m.Query())
94+}
87 func (m *Model) Matches() []Match {95 func (m *Model) Matches() []Match {
88 if m == nil {96 if m == nil {
89 return nil97 return nil
@@ -201,7 +209,7 @@ func (m *Model) Render(inner domain.Size, styles RenderStyles) renderer.Frame {
201 x++209 x++
202 }210 }
203 for x < codeWidth+1 && x < frame.Width {211 for x < codeWidth+1 && x < frame.Width {
204- frame.Set(x, y+1, renderer.Cell{Rune: ' ', Style: style})212+ frame.Set(x, y+start, renderer.Cell{Rune: ' ', Style: style})
205 x++213 x++
206 }214 }
207 ui.DrawText(frame, x, y+start, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc))215 ui.DrawText(frame, x, y+start, frame.Width, match.Command.Desc, mergePaletteDescStyle(style, desc))
M internal/usecase/palette/model_test.go +10 -0
@@ -185,3 +185,13 @@ func TestRenderUsesConfiguredStyles(t *testing.T) {
185185 })
186186 }
187187 }
188+
189+func TestRenderPreservesFeedbackAboveCommandRows(t *testing.T) {
190+ m := New([]command.Command{cmd("JRS", "Jump", "Jump to recent session")})
191+ m.SetFeedback("rank 1")
192+
193+ frame := m.Render(domain.Size{Cols: 28, Rows: 4}, DefaultRenderStyles())
194+ if got := frameRow(frame, 1); got[:6] != "rank 1" {
195+ t.Fatalf("feedback row = %q, want prefix %q", got, "rank 1")
196+ }
197+}
@@ -185,3 +185,13 @@ func TestRenderUsesConfiguredStyles(t *testing.T) {
185 })185 })
186 }186 }
187 }187 }
188+
189+func TestRenderPreservesFeedbackAboveCommandRows(t *testing.T) {
190+ m := New([]command.Command{cmd("JRS", "Jump", "Jump to recent session")})
191+ m.SetFeedback("rank 1")
192+
193+ frame := m.Render(domain.Size{Cols: 28, Rows: 4}, DefaultRenderStyles())
194+ if got := frameRow(frame, 1); got[:6] != "rank 1" {
195+ t.Fatalf("feedback row = %q, want prefix %q", got, "rank 1")
196+ }
197+}

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