detached 3dbe137

fix(picker): show dim remote host

commit
3dbe1376a057799cf1154ae710144f166da357fc
author
brice · · 2026-08-07 07:45 UTC
parent
e7d0da2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +44 -4
M internal/usecase/picker/model.go +19 -2
@@ -267,10 +267,21 @@ func rowsForSession(session SessionView, config SelectionConfig) []row {
267267 common.remote = common.hasRemoteKey || common.hasRemoteTarget || session.RemoteHost != ""
268268 header := common
269269 header.kind, header.dispName, header.tabIndex = rowSession, session.Name, -1
270+ if common.hasRemoteKey {
271+ header.dispName = common.remoteKey.Name
272+ display := common.remoteKey.Display()
273+ header.detail = display[len(common.remoteKey.Name):]
274+ }
270275 header.selectable = header.kind.selectable(config.Mode)
271276 header.focusable = header.selectable
272277 if common.remote {
273- header.detail = remoteRowDetail(session)
278+ if remoteDetail := remoteRowDetail(session); remoteDetail != "" {
279+ if header.detail != "" {
280+ header.detail += " (" + remoteDetail + ")"
281+ } else {
282+ header.detail = remoteDetail
283+ }
284+ }
274285 // Rich catalog rows use tab targets. Legacy count-only/connect-only
275286 // callers retain their header target until all peers carry the richer
276287 // identity contract.
@@ -623,7 +634,13 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render
623634
624635 if r.kind.rendersAsHeader() {
625636 if r.stopped {
626- ui.DrawText(frame, x, rect.Y+y, clipX, " (stopped)", base)
637+ detail := r.detail
638+ if detail == "" {
639+ detail = " "
640+ } else {
641+ detail += " "
642+ }
643+ ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(stopped)", base)
627644 } else {
628645 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)
629646 }
@@ -267,10 +267,21 @@ func rowsForSession(session SessionView, config SelectionConfig) []row {
267 common.remote = common.hasRemoteKey || common.hasRemoteTarget || session.RemoteHost != ""267 common.remote = common.hasRemoteKey || common.hasRemoteTarget || session.RemoteHost != ""
268 header := common268 header := common
269 header.kind, header.dispName, header.tabIndex = rowSession, session.Name, -1269 header.kind, header.dispName, header.tabIndex = rowSession, session.Name, -1
270+ if common.hasRemoteKey {
271+ header.dispName = common.remoteKey.Name
272+ display := common.remoteKey.Display()
273+ header.detail = display[len(common.remoteKey.Name):]
274+ }
270 header.selectable = header.kind.selectable(config.Mode)275 header.selectable = header.kind.selectable(config.Mode)
271 header.focusable = header.selectable276 header.focusable = header.selectable
272 if common.remote {277 if common.remote {
273- header.detail = remoteRowDetail(session)278+ if remoteDetail := remoteRowDetail(session); remoteDetail != "" {
279+ if header.detail != "" {
280+ header.detail += " (" + remoteDetail + ")"
281+ } else {
282+ header.detail = remoteDetail
283+ }
284+ }
274 // Rich catalog rows use tab targets. Legacy count-only/connect-only285 // Rich catalog rows use tab targets. Legacy count-only/connect-only
275 // callers retain their header target until all peers carry the richer286 // callers retain their header target until all peers carry the richer
276 // identity contract.287 // identity contract.
@@ -623,7 +634,13 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render
623 634
624 if r.kind.rendersAsHeader() {635 if r.kind.rendersAsHeader() {
625 if r.stopped {636 if r.stopped {
626- ui.DrawText(frame, x, rect.Y+y, clipX, " (stopped)", base)637+ detail := r.detail
638+ if detail == "" {
639+ detail = " "
640+ } else {
641+ detail += " "
642+ }
643+ ui.DrawText(frame, x, rect.Y+y, clipX, detail+"(stopped)", base)
627 } else {644 } else {
628 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)645 ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle)
629 }646 }
M internal/usecase/picker/model_test.go +2 -2
@@ -748,9 +748,9 @@ func TestRemoteRowsCarryStructuredIdentityAndRemainNonSelectable(t *testing.T) {
748748 require.True(t, pickerRow.hasRemoteKey)
749749 require.False(t, pickerRow.selectable, "remote state %d must remain gated in phase 5", availability[i])
750750 require.True(t, pickerRow.dim, "remote state %d is visibly unavailable", availability[i])
751- wantDetail := "proxy activation pending"
751+ wantDetail := "@mule (proxy activation pending)"
752752 if availability[i] == RemoteStale || availability[i] == RemoteVersionMismatch {
753- wantDetail = "remote state detail"
753+ wantDetail = "@mule (remote state detail)"
754754 }
755755 require.Equal(t, wantDetail, pickerRow.detail)
756756 }
@@ -748,9 +748,9 @@ func TestRemoteRowsCarryStructuredIdentityAndRemainNonSelectable(t *testing.T) {
748 require.True(t, pickerRow.hasRemoteKey)748 require.True(t, pickerRow.hasRemoteKey)
749 require.False(t, pickerRow.selectable, "remote state %d must remain gated in phase 5", availability[i])749 require.False(t, pickerRow.selectable, "remote state %d must remain gated in phase 5", availability[i])
750 require.True(t, pickerRow.dim, "remote state %d is visibly unavailable", availability[i])750 require.True(t, pickerRow.dim, "remote state %d is visibly unavailable", availability[i])
751- wantDetail := "proxy activation pending"751+ wantDetail := "@mule (proxy activation pending)"
752 if availability[i] == RemoteStale || availability[i] == RemoteVersionMismatch {752 if availability[i] == RemoteStale || availability[i] == RemoteVersionMismatch {
753- wantDetail = "remote state detail"753+ wantDetail = "@mule (remote state detail)"
754 }754 }
755 require.Equal(t, wantDetail, pickerRow.detail)755 require.Equal(t, wantDetail, pickerRow.detail)
756 }756 }
M internal/usecase/picker/remote_parity_test.go +23 -0
@@ -6,6 +6,7 @@ import (
66 "github.com/stretchr/testify/require"
77
88 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/pkg/renderer"
910 )
1011
1112 func remoteParityLifecycle(seed byte) domain.SessionLifecycleID {
@@ -14,6 +15,28 @@ func remoteParityLifecycle(seed byte) domain.SessionLifecycleID {
1415 return id
1516 }
1617
18+func TestRemoteSessionHeaderIncludesOrigin(t *testing.T) {
19+ lifecycle := remoteParityLifecycle(1)
20+ key := domain.RemoteSessionKey{Host: "vev@arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"}
21+ target := domain.RemoteSessionTarget{Endpoint: key.Host, DisplayOrigin: key.DisplayOrigin, LifecycleID: lifecycle, SessionName: key.Name, LiveTabID: "tab-1"}
22+ model := New([]SessionView{{
23+ ID: key.ID(), Name: key.Name, Tabs: []TabEntry{{TabID: "tab-1", Name: "main"}},
24+ RemoteKey: &key, RemoteTarget: &target, RemoteAvailability: RemoteFresh, RemoteAttachReady: true,
25+ }}, SelectionConfig{Mode: SelectNavigationTab})
26+
27+ require.Equal(t, "work", model.rows[0].dispName)
28+ require.Equal(t, "@arch", model.rows[0].detail)
29+ frame := model.Render(domain.Size{Cols: 32, Rows: 4}, Preview{}, RenderStyles{
30+ Name: renderer.Style{Bold: true}, Detail: renderer.Style{Attrs: renderer.AttrDim},
31+ })
32+ require.Equal(t, '@', frame.At(4, 0).Rune)
33+ require.True(t, frame.At(4, 0).Style.Equal(renderer.Style{Attrs: renderer.AttrDim}))
34+ selected, ok := model.Selected()
35+ require.True(t, ok)
36+ require.Equal(t, key.ID(), selected.Session)
37+ require.Equal(t, &target, selected.RemoteTarget)
38+}
39+
1740 func TestRemoteRowsWithDuplicateLabelsKeepDistinctRoutingIdentity(t *testing.T) {
1841 firstLifecycle := remoteParityLifecycle(1)
1942 secondLifecycle := remoteParityLifecycle(2)
@@ -6,6 +6,7 @@ import (
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/pkg/renderer"
9 )10 )
10 11
11 func remoteParityLifecycle(seed byte) domain.SessionLifecycleID {12 func remoteParityLifecycle(seed byte) domain.SessionLifecycleID {
@@ -14,6 +15,28 @@ func remoteParityLifecycle(seed byte) domain.SessionLifecycleID {
14 return id15 return id
15 }16 }
16 17
18+func TestRemoteSessionHeaderIncludesOrigin(t *testing.T) {
19+ lifecycle := remoteParityLifecycle(1)
20+ key := domain.RemoteSessionKey{Host: "vev@arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"}
21+ target := domain.RemoteSessionTarget{Endpoint: key.Host, DisplayOrigin: key.DisplayOrigin, LifecycleID: lifecycle, SessionName: key.Name, LiveTabID: "tab-1"}
22+ model := New([]SessionView{{
23+ ID: key.ID(), Name: key.Name, Tabs: []TabEntry{{TabID: "tab-1", Name: "main"}},
24+ RemoteKey: &key, RemoteTarget: &target, RemoteAvailability: RemoteFresh, RemoteAttachReady: true,
25+ }}, SelectionConfig{Mode: SelectNavigationTab})
26+
27+ require.Equal(t, "work", model.rows[0].dispName)
28+ require.Equal(t, "@arch", model.rows[0].detail)
29+ frame := model.Render(domain.Size{Cols: 32, Rows: 4}, Preview{}, RenderStyles{
30+ Name: renderer.Style{Bold: true}, Detail: renderer.Style{Attrs: renderer.AttrDim},
31+ })
32+ require.Equal(t, '@', frame.At(4, 0).Rune)
33+ require.True(t, frame.At(4, 0).Style.Equal(renderer.Style{Attrs: renderer.AttrDim}))
34+ selected, ok := model.Selected()
35+ require.True(t, ok)
36+ require.Equal(t, key.ID(), selected.Session)
37+ require.Equal(t, &target, selected.RemoteTarget)
38+}
39+
17 func TestRemoteRowsWithDuplicateLabelsKeepDistinctRoutingIdentity(t *testing.T) {40 func TestRemoteRowsWithDuplicateLabelsKeepDistinctRoutingIdentity(t *testing.T) {
18 firstLifecycle := remoteParityLifecycle(1)41 firstLifecycle := remoteParityLifecycle(1)
19 secondLifecycle := remoteParityLifecycle(2)42 secondLifecycle := remoteParityLifecycle(2)

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