fix(palette): render recent jump guidance
- commit
- 59a790dbc6e8cade564d208c320f57d2ac0a35c6
- author
- brice · · 2026-07-11 12:11 UTC
- parent
- 1697b4c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 39 | 39 | ac.overlays.paletteGeneration++ | |
| 40 | 40 | ac.overlays.palette = palette.New(commands) | |
| 41 | 41 | ac.overlays.paletteRecent = recent | |
| 42 | - | ac.overlays.paletteHints = recentSessionHints(recent, nil) | |
| 42 | + | ac.overlays.paletteHints = palette.ContextualHints{} | |
| 43 | 43 | ac.overlays.paletteFeedback = "" | |
| 44 | 44 | ac.overlays.palettePending = nil | |
| 45 | 45 | ac.overlays.paletteMu.Unlock() | |
| @@ -150,7 +150,15 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 150 | 150 | }, | |
| 151 | 151 | }) | |
| 152 | 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 | 163 | if cancel { | |
| 156 | 164 | 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 = recent | 41 | 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 = nil | 44 | 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) { | |||
| 48 | 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 | 73 | func TestPaletteFLTExecutesFloatingToggle(t *testing.T) { | |
| 52 | 74 | p, release := newBlockingPTY(t) | |
| 53 | 75 | 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) { | |||
| 84 | 84 | } | |
| 85 | 85 | 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 | 95 | func (m *Model) Matches() []Match { | |
| 88 | 96 | if m == nil { | |
| 89 | 97 | return nil | |
| @@ -201,7 +209,7 @@ func (m *Model) Render(inner domain.Size, styles RenderStyles) renderer.Frame { | |||
| 201 | 209 | x++ | |
| 202 | 210 | } | |
| 203 | 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 | 213 | x++ | |
| 206 | 214 | } | |
| 207 | 215 | 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, true | 85 | 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 nil | 97 | 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) { | |||
| 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 | + | } | |
| @@ -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 | + | } | |||