fix(palette): honor effective recent jump code
- commit
- e2e231b41ffb38d4b32cfb51129fe86750044717
- author
- brice · · 2026-07-11 12:30 UTC
- parent
- 9818e1d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 144 | 144 | // fuzzy selection can never turn an unrelated query into a jump. | |
| 145 | 145 | // Static commands retain normal palette behavior and run the selected | |
| 146 | 146 | // fuzzy match. | |
| 147 | - | if cmd.Code == "JRS" { | |
| 147 | + | if cmd.Slug == "jump-recent-session" { | |
| 148 | 148 | action, valid := palette.ParseAction([]command.Command{cmd}, rawQuery) | |
| 149 | 149 | if !valid { | |
| 150 | 150 | ac.overlays.paletteFeedback = "invalid command arguments" | |
| @@ -160,7 +160,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 160 | 160 | }) | |
| 161 | 161 | if changed { | |
| 162 | 162 | active, ok := ac.overlays.palette.ArgumentCommand() | |
| 163 | - | if ok && active.Code == "JRS" { | |
| 163 | + | if ok && active.Slug == "jump-recent-session" { | |
| 164 | 164 | hints := recentSessionHints(ac.overlays.paletteRecent, paletteArgs(ac.overlays.palette.Query(), active)) | |
| 165 | 165 | ac.overlays.paletteHints = hints | |
| 166 | 166 | } else { | |
| @@ -184,7 +184,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 184 | 184 | ||
| 185 | 185 | // Revalidate live target without paletteMu. A captured ID is never replaced | |
| 186 | 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 | 188 | rank, err := command.ParsePositiveDecimal(args) | |
| 189 | 189 | if err != nil { | |
| 190 | 190 | 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 selected | 145 | // 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 = hints | 165 | 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 replaced | 185 | // 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) { | |||
| 70 | 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 | 104 | func TestPaletteFuzzySelectedStaticCommandExecutes(t *testing.T) { | |
| 74 | 105 | d, sess, ac, sends, releases := newManualTabSession(t, 2) | |
| 75 | 106 | defer releases[0]() | |
| @@ -100,6 +131,21 @@ func TestPaletteJRSUsesCapturedRankAfterMRUChanges(t *testing.T) { | |||
| 100 | 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 | 149 | func TestPaletteJRSDisplacedTargetKeepsInteractionOpen(t *testing.T) { | |
| 104 | 150 | p, release := newBlockingPTY(t) | |
| 105 | 151 | 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() | ||