detached d9b9a0a

feat(picker): show sort mode in modal title

commit
d9b9a0afe38df5ab668689c68e92249fe344ae89
author
brice · · 2026-07-29 13:57 UTC
parent
18c1193
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +33 -3
M internal/usecase/daemon/move_picker.go +1 -0
@@ -179,6 +179,7 @@ func (d *Daemon) refreshPickerOpts(ac *attachedClient, opts pickerRefreshOptions
179179 updated := rt.picker != nil && rt.pickerIntent == intent && rt.pickerSource == source
180180 if updated {
181181 rt.picker = model
182+ rt.pickerTitle = pickerTitle(pickerSortMode(d.pickerSort.Load()))
182183 }
183184 rt.pickerMu.Unlock()
184185 if updated {
@@ -179,6 +179,7 @@ func (d *Daemon) refreshPickerOpts(ac *attachedClient, opts pickerRefreshOptions
179 updated := rt.picker != nil && rt.pickerIntent == intent && rt.pickerSource == source179 updated := rt.picker != nil && rt.pickerIntent == intent && rt.pickerSource == source
180 if updated {180 if updated {
181 rt.picker = model181 rt.picker = model
182+ rt.pickerTitle = pickerTitle(pickerSortMode(d.pickerSort.Load()))
182 }183 }
183 rt.pickerMu.Unlock()184 rt.pickerMu.Unlock()
184 if updated {185 if updated {
M internal/usecase/daemon/overlay_runtime.go +3 -0
@@ -18,6 +18,7 @@ type overlayRuntime struct {
1818
1919 pickerMu sync.Mutex
2020 picker *picker.Model
21+ pickerTitle string
2122 pickerIntent pickerIntent
2223 pickerSource moveSourceLocator
2324 pickerPreview *tab
@@ -381,6 +382,7 @@ type overlayRenderSnapshot struct {
381382
382383 pickerActive bool
383384 pickerModel *picker.Model
385+ pickerTitle string
384386 previewTab *tab
385387
386388 noticesOverlayActive bool
@@ -451,6 +453,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot {
451453 rt.pickerMu.Lock()
452454 snap.pickerActive = rt.picker != nil
453455 snap.pickerModel = rt.picker.Clone()
456+ snap.pickerTitle = rt.pickerTitle
454457 snap.previewTab = rt.pickerPreview
455458 rt.pickerMu.Unlock()
456459
@@ -18,6 +18,7 @@ type overlayRuntime struct {
18 18
19 pickerMu sync.Mutex19 pickerMu sync.Mutex
20 picker *picker.Model20 picker *picker.Model
21+ pickerTitle string
21 pickerIntent pickerIntent22 pickerIntent pickerIntent
22 pickerSource moveSourceLocator23 pickerSource moveSourceLocator
23 pickerPreview *tab24 pickerPreview *tab
@@ -381,6 +382,7 @@ type overlayRenderSnapshot struct {
381 382
382 pickerActive bool383 pickerActive bool
383 pickerModel *picker.Model384 pickerModel *picker.Model
385+ pickerTitle string
384 previewTab *tab386 previewTab *tab
385 387
386 noticesOverlayActive bool388 noticesOverlayActive bool
@@ -451,6 +453,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot {
451 rt.pickerMu.Lock()453 rt.pickerMu.Lock()
452 snap.pickerActive = rt.picker != nil454 snap.pickerActive = rt.picker != nil
453 snap.pickerModel = rt.picker.Clone()455 snap.pickerModel = rt.picker.Clone()
456+ snap.pickerTitle = rt.pickerTitle
454 snap.previewTab = rt.pickerPreview457 snap.previewTab = rt.pickerPreview
455 rt.pickerMu.Unlock()458 rt.pickerMu.Unlock()
456 459
M internal/usecase/daemon/picker.go +2 -0
@@ -48,6 +48,7 @@ func (d *Daemon) publishPicker(sess *session, ac *attachedClient, model *picker.
4848 rt.pickerPreviewSession = nil
4949 rt.pickerPreview = nil
5050 rt.picker = model
51+ rt.pickerTitle = pickerTitle(pickerSortMode(d.pickerSort.Load()))
5152 rt.pickerIntent = intent
5253 rt.pickerSource = source
5354 rt.pickerPending = nil
@@ -452,6 +453,7 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) {
452453 func (d *Daemon) closePicker(ac *attachedClient) {
453454 ac.overlays.pickerMu.Lock()
454455 ac.overlays.picker = nil
456+ ac.overlays.pickerTitle = ""
455457 ac.overlays.pickerIntent = pickerNavigate
456458 ac.overlays.pickerSource = moveSourceLocator{}
457459 ac.overlays.pickerPending = nil
@@ -48,6 +48,7 @@ func (d *Daemon) publishPicker(sess *session, ac *attachedClient, model *picker.
48 rt.pickerPreviewSession = nil48 rt.pickerPreviewSession = nil
49 rt.pickerPreview = nil49 rt.pickerPreview = nil
50 rt.picker = model50 rt.picker = model
51+ rt.pickerTitle = pickerTitle(pickerSortMode(d.pickerSort.Load()))
51 rt.pickerIntent = intent52 rt.pickerIntent = intent
52 rt.pickerSource = source53 rt.pickerSource = source
53 rt.pickerPending = nil54 rt.pickerPending = nil
@@ -452,6 +453,7 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) {
452 func (d *Daemon) closePicker(ac *attachedClient) {453 func (d *Daemon) closePicker(ac *attachedClient) {
453 ac.overlays.pickerMu.Lock()454 ac.overlays.pickerMu.Lock()
454 ac.overlays.picker = nil455 ac.overlays.picker = nil
456+ ac.overlays.pickerTitle = ""
455 ac.overlays.pickerIntent = pickerNavigate457 ac.overlays.pickerIntent = pickerNavigate
456 ac.overlays.pickerSource = moveSourceLocator{}458 ac.overlays.pickerSource = moveSourceLocator{}
457 ac.overlays.pickerPending = nil459 ac.overlays.pickerPending = nil
M internal/usecase/daemon/picker_test.go +20 -0
@@ -237,9 +237,11 @@ func TestPickerSortToggleFlipsModeAndKeepsSelection(t *testing.T) {
237237 require.True(t, ac.overlays.pickerActive())
238238 ac.overlays.pickerMu.Lock()
239239 selected, ok := ac.overlays.picker.Selected()
240+ title := ac.overlays.pickerTitle
240241 ac.overlays.pickerMu.Unlock()
241242 require.True(t, ok)
242243 require.Equal(t, before, selected)
244+ require.Equal(t, " Sessions · grouped ", title)
243245
244246 d.handleInput(sess, ac, []byte("s"))
245247 awaitFrame(t, sends, ports.MsgOutput)
@@ -248,9 +250,27 @@ func TestPickerSortToggleFlipsModeAndKeepsSelection(t *testing.T) {
248250 require.True(t, ac.overlays.pickerActive())
249251 ac.overlays.pickerMu.Lock()
250252 selected, ok = ac.overlays.picker.Selected()
253+ title = ac.overlays.pickerTitle
251254 ac.overlays.pickerMu.Unlock()
252255 require.True(t, ok)
253256 require.Equal(t, before, selected)
257+ require.Equal(t, " Sessions · recent ", title)
258+ })
259+ }
260+}
261+
262+func TestPickerTitleReflectsSortMode(t *testing.T) {
263+ tests := []struct {
264+ name string
265+ mode pickerSortMode
266+ want string
267+ }{
268+ {name: "recent", mode: pickerSortRecent, want: " Sessions · recent "},
269+ {name: "grouped", mode: pickerSortGrouped, want: " Sessions · grouped "},
270+ }
271+ for _, tc := range tests {
272+ t.Run(tc.name, func(t *testing.T) {
273+ require.Equal(t, tc.want, pickerTitle(tc.mode))
254274 })
255275 }
256276 }
@@ -237,9 +237,11 @@ func TestPickerSortToggleFlipsModeAndKeepsSelection(t *testing.T) {
237 require.True(t, ac.overlays.pickerActive())237 require.True(t, ac.overlays.pickerActive())
238 ac.overlays.pickerMu.Lock()238 ac.overlays.pickerMu.Lock()
239 selected, ok := ac.overlays.picker.Selected()239 selected, ok := ac.overlays.picker.Selected()
240+ title := ac.overlays.pickerTitle
240 ac.overlays.pickerMu.Unlock()241 ac.overlays.pickerMu.Unlock()
241 require.True(t, ok)242 require.True(t, ok)
242 require.Equal(t, before, selected)243 require.Equal(t, before, selected)
244+ require.Equal(t, " Sessions · grouped ", title)
243 245
244 d.handleInput(sess, ac, []byte("s"))246 d.handleInput(sess, ac, []byte("s"))
245 awaitFrame(t, sends, ports.MsgOutput)247 awaitFrame(t, sends, ports.MsgOutput)
@@ -248,9 +250,27 @@ func TestPickerSortToggleFlipsModeAndKeepsSelection(t *testing.T) {
248 require.True(t, ac.overlays.pickerActive())250 require.True(t, ac.overlays.pickerActive())
249 ac.overlays.pickerMu.Lock()251 ac.overlays.pickerMu.Lock()
250 selected, ok = ac.overlays.picker.Selected()252 selected, ok = ac.overlays.picker.Selected()
253+ title = ac.overlays.pickerTitle
251 ac.overlays.pickerMu.Unlock()254 ac.overlays.pickerMu.Unlock()
252 require.True(t, ok)255 require.True(t, ok)
253 require.Equal(t, before, selected)256 require.Equal(t, before, selected)
257+ require.Equal(t, " Sessions · recent ", title)
258+ })
259+ }
260+}
261+
262+func TestPickerTitleReflectsSortMode(t *testing.T) {
263+ tests := []struct {
264+ name string
265+ mode pickerSortMode
266+ want string
267+ }{
268+ {name: "recent", mode: pickerSortRecent, want: " Sessions · recent "},
269+ {name: "grouped", mode: pickerSortGrouped, want: " Sessions · grouped "},
270+ }
271+ for _, tc := range tests {
272+ t.Run(tc.name, func(t *testing.T) {
273+ require.Equal(t, tc.want, pickerTitle(tc.mode))
254 })274 })
255 }275 }
256 }276 }
M internal/usecase/daemon/render_pipeline.go +5 -1
@@ -230,7 +230,11 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
230230 }
231231 if snap.pickerActive && snap.pickerModel != nil {
232232 presentation := pickerModal.Resolve(size)
233- o.picker = capturedModal{active: true, title: pickerModal.Title, presentation: presentation, focused: true}
233+ title := snap.pickerTitle
234+ if title == "" {
235+ title = pickerModal.Title
236+ }
237+ o.picker = capturedModal{active: true, title: title, presentation: presentation, focused: true}
234238 renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PickerDescription, Base: styles.PickerBase, Separator: styles.PickerSeparator}
235239 o.picker.inner = snap.pickerModel.Render(rectSize(presentation.Inner), state.preview, renderStyles)
236240 }
@@ -230,7 +230,11 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho
230 }230 }
231 if snap.pickerActive && snap.pickerModel != nil {231 if snap.pickerActive && snap.pickerModel != nil {
232 presentation := pickerModal.Resolve(size)232 presentation := pickerModal.Resolve(size)
233- o.picker = capturedModal{active: true, title: pickerModal.Title, presentation: presentation, focused: true}233+ title := snap.pickerTitle
234+ if title == "" {
235+ title = pickerModal.Title
236+ }
237+ o.picker = capturedModal{active: true, title: title, presentation: presentation, focused: true}
234 renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PickerDescription, Base: styles.PickerBase, Separator: styles.PickerSeparator}238 renderStyles := picker.RenderStyles{Background: styles.PickerBase, Selection: styles.PickerSelection, SelectionName: styles.PickerSelectionName, SelectionMuted: styles.PickerSelectionMuted, Name: styles.PickerName, Detail: styles.PickerDescription, Base: styles.PickerBase, Separator: styles.PickerSeparator}
235 o.picker.inner = snap.pickerModel.Render(rectSize(presentation.Inner), state.preview, renderStyles)239 o.picker.inner = snap.pickerModel.Render(rectSize(presentation.Inner), state.preview, renderStyles)
236 }240 }
M internal/usecase/daemon/render_test.go +2 -2
@@ -107,7 +107,7 @@ func TestPaletteBackdropDimsSimultaneousPicker(t *testing.T) {
107107
108108 d.enterPicker(sess, ac)
109109 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
110- pickerTitle := client.Frame.At(36, 2)
110+ pickerTitle := client.Frame.At(31, 2)
111111 require.Equal(t, 'S', pickerTitle.Rune, "fixture must address the picker title")
112112 undimmedPane := client.Frame.At(0, 1)
113113
@@ -115,7 +115,7 @@ func TestPaletteBackdropDimsSimultaneousPicker(t *testing.T) {
115115 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
116116 dimmedPickerTitle := pickerTitle
117117 dimmedPickerTitle.Style = themeui.NewDimmer(backdropTheme()).Dim(pickerTitle.Style)
118- require.Equal(t, dimmedPickerTitle, client.Frame.At(36, 2), "the lower-priority picker must be part of the palette backdrop")
118+ require.Equal(t, dimmedPickerTitle, client.Frame.At(31, 2), "the lower-priority picker must be part of the palette backdrop")
119119 require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmedPane.Style), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style")
120120 }
121121
@@ -107,7 +107,7 @@ func TestPaletteBackdropDimsSimultaneousPicker(t *testing.T) {
107 107
108 d.enterPicker(sess, ac)108 d.enterPicker(sess, ac)
109 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))109 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
110- pickerTitle := client.Frame.At(36, 2)110+ pickerTitle := client.Frame.At(31, 2)
111 require.Equal(t, 'S', pickerTitle.Rune, "fixture must address the picker title")111 require.Equal(t, 'S', pickerTitle.Rune, "fixture must address the picker title")
112 undimmedPane := client.Frame.At(0, 1)112 undimmedPane := client.Frame.At(0, 1)
113 113
@@ -115,7 +115,7 @@ func TestPaletteBackdropDimsSimultaneousPicker(t *testing.T) {
115 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))115 mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput))
116 dimmedPickerTitle := pickerTitle116 dimmedPickerTitle := pickerTitle
117 dimmedPickerTitle.Style = themeui.NewDimmer(backdropTheme()).Dim(pickerTitle.Style)117 dimmedPickerTitle.Style = themeui.NewDimmer(backdropTheme()).Dim(pickerTitle.Style)
118- require.Equal(t, dimmedPickerTitle, client.Frame.At(36, 2), "the lower-priority picker must be part of the palette backdrop")118+ require.Equal(t, dimmedPickerTitle, client.Frame.At(31, 2), "the lower-priority picker must be part of the palette backdrop")
119 require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmedPane.Style), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style")119 require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmedPane.Style), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style")
120 }120 }
121 121

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