detached 0c2bd95

fix(picker): bound stopped tab selectors

commit
0c2bd9538de5829586b365332b186841b9c00c00
author
brice · · 2026-08-11 18:23 UTC
parent
0380a23
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +20 -3
M internal/usecase/daemon/remote_picker.go +3 -1
@@ -5,6 +5,7 @@ import (
55 "encoding/base64"
66 "errors"
77 "fmt"
8+ "math"
89 "sort"
910 "time"
1011
@@ -166,7 +167,8 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
166167 if first.ID != "" {
167168 target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID))
168169 } else {
169- target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(len(tabs)))
170+ tabCount := min(len(tabs), math.MaxUint16)
171+ target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(tabCount))
170172 }
171173 } else {
172174 target.LiveTabID = domain.TabStableID(tabs[active].ID)
@@ -5,6 +5,7 @@ import (
5 "encoding/base64"5 "encoding/base64"
6 "errors"6 "errors"
7 "fmt"7 "fmt"
8+ "math"
8 "sort"9 "sort"
9 "time"10 "time"
10 11
@@ -166,7 +167,8 @@ func remotePickerView(key domain.RemoteSessionKey, session ports.RemoteCatalogSe
166 if first.ID != "" {167 if first.ID != "" {
167 target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID))168 target.StoppedTab = domain.NewStableTabSelector(domain.TabStableID(first.ID))
168 } else {169 } else {
169- target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(len(tabs)))170+ tabCount := min(len(tabs), math.MaxUint16)
171+ target.StoppedTab = domain.NewOrdinalTabSelector(0, first.Name, uint16(tabCount))
170 }172 }
171 } else {173 } else {
172 target.LiveTabID = domain.TabStableID(tabs[active].ID)174 target.LiveTabID = domain.TabStableID(tabs[active].ID)
M internal/usecase/daemon/remote_picker_test.go +13 -1
@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "io"
88 "log/slog"
9+ "math"
910 "sync"
1011 "testing"
1112 "time"
@@ -84,6 +85,17 @@ func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T)
8485 }
8586 }
8687
88+func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) {
89+ tabs := make([]ports.RemoteCatalogTab, math.MaxUint16+1)
90+ tabs[0] = ports.RemoteCatalogTab{Name: "main"}
91+ view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, ports.RemoteCatalogSession{
92+ LifecycleID: remoteLifecycleForTest(), Name: "work", State: "down", Tabs: tabs,
93+ }, remoteHostFresh, time.Unix(100, 0))
94+
95+ require.NotNil(t, view.RemoteTarget)
96+ require.Equal(t, uint16(math.MaxUint16), view.RemoteTarget.StoppedTab.ExpectedCount)
97+}
98+
8799 func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) {
88100 for _, attached := range []bool{false, true} {
89101 view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{
@@ -750,9 +762,9 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) {
750762 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
751763 }, transport)
752764 require.NoError(t, err)
765+ t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
753766 require.Equal(t, lifecycle, restored.incarnation)
754767 require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID)
755- remote.clientGone(restored, attachment, transport, false)
756768 }
757769
758770 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
@@ -6,6 +6,7 @@ import (
6 "fmt"6 "fmt"
7 "io"7 "io"
8 "log/slog"8 "log/slog"
9+ "math"
9 "sync"10 "sync"
10 "testing"11 "testing"
11 "time"12 "time"
@@ -84,6 +85,17 @@ func TestRemotePickerStoppedRowsUseCanonicalStateAndSafeSelection(t *testing.T)
84 }85 }
85 }86 }
86 87
88+func TestRemotePickerClampsStoppedOrdinalTabCount(t *testing.T) {
89+ tabs := make([]ports.RemoteCatalogTab, math.MaxUint16+1)
90+ tabs[0] = ports.RemoteCatalogTab{Name: "main"}
91+ view := remotePickerView(domain.RemoteSessionKey{Host: "arch", Name: "work"}, ports.RemoteCatalogSession{
92+ LifecycleID: remoteLifecycleForTest(), Name: "work", State: "down", Tabs: tabs,
93+ }, remoteHostFresh, time.Unix(100, 0))
94+
95+ require.NotNil(t, view.RemoteTarget)
96+ require.Equal(t, uint16(math.MaxUint16), view.RemoteTarget.StoppedTab.ExpectedCount)
97+}
98+
87 func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) {99 func TestRemotePickerUsesCompactUpDetailRegardlessOfAttachment(t *testing.T) {
88 for _, attached := range []bool{false, true} {100 for _, attached := range []bool{false, true} {
89 view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{101 view := remotePickerView(domain.RemoteSessionKey{Host: "remote", Name: "work"}, ports.RemoteCatalogSession{
@@ -750,9 +762,9 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) {
750 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,762 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
751 }, transport)763 }, transport)
752 require.NoError(t, err)764 require.NoError(t, err)
765+ t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
753 require.Equal(t, lifecycle, restored.incarnation)766 require.Equal(t, lifecycle, restored.incarnation)
754 require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID)767 require.Equal(t, domain.TabStableID("tab-b"), attachment.viewSnapshot().tabID)
755- remote.clientGone(restored, attachment, transport, false)
756 }768 }
757 769
758 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {770 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
M internal/usecase/picker/model.go +4 -1
@@ -1,6 +1,8 @@
11 package picker
22
33 import (
4+ "math"
5+
46 renderer "github.com/bnema/vev-vt"
57 "github.com/bnema/vev/internal/domain"
68 "github.com/bnema/vev/internal/usecase/ui"
@@ -329,7 +331,8 @@ func rowsForSession(session SessionView, config SelectionConfig) []row {
329331 if tab.TabID != "" {
330332 remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID)
331333 } else {
332- remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(len(session.Tabs)))
334+ tabCount := min(len(session.Tabs), math.MaxUint16)
335+ remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(tabCount))
333336 }
334337 } else {
335338 remoteTarget.StoppedTab = domain.TabSelector{}
@@ -1,6 +1,8 @@
1 package picker1 package picker
2 2
3 import (3 import (
4+ "math"
5+
4 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
5 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
6 "github.com/bnema/vev/internal/usecase/ui"8 "github.com/bnema/vev/internal/usecase/ui"
@@ -329,7 +331,8 @@ func rowsForSession(session SessionView, config SelectionConfig) []row {
329 if tab.TabID != "" {331 if tab.TabID != "" {
330 remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID)332 remoteTarget.StoppedTab = domain.NewStableTabSelector(tab.TabID)
331 } else {333 } else {
332- remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(len(session.Tabs)))334+ tabCount := min(len(session.Tabs), math.MaxUint16)
335+ remoteTarget.StoppedTab = domain.NewOrdinalTabSelector(uint16(i), tab.RawName, uint16(tabCount))
333 }336 }
334 } else {337 } else {
335 remoteTarget.StoppedTab = domain.TabSelector{}338 remoteTarget.StoppedTab = domain.TabSelector{}

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