fix(picker): show dim remote host
- commit
- 3dbe1376a057799cf1154ae710144f166da357fc
- author
- brice · · 2026-08-07 07:45 UTC
- parent
- e7d0da2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 267 | 267 | common.remote = common.hasRemoteKey || common.hasRemoteTarget || session.RemoteHost != "" | |
| 268 | 268 | header := common | |
| 269 | 269 | 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 | 275 | header.selectable = header.kind.selectable(config.Mode) | |
| 271 | 276 | header.focusable = header.selectable | |
| 272 | 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 | 285 | // Rich catalog rows use tab targets. Legacy count-only/connect-only | |
| 275 | 286 | // callers retain their header target until all peers carry the richer | |
| 276 | 287 | // identity contract. | |
| @@ -623,7 +634,13 @@ func (m *Model) renderList(frame renderer.Frame, rect domain.Rect, styles Render | |||
| 623 | 634 | ||
| 624 | 635 | if r.kind.rendersAsHeader() { | |
| 625 | 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 | 644 | } else { | |
| 628 | 645 | ui.DrawText(frame, x, rect.Y+y, clipX, ui.TruncateText(r.detail, clipX-x), detailStyle) | |
| 629 | 646 | } | |
| @@ -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 := common | 268 | header := common | ||
| 269 | header.kind, header.dispName, header.tabIndex = rowSession, session.Name, -1 | 269 | 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.selectable | 276 | 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-only | 285 | // Rich catalog rows use tab targets. Legacy count-only/connect-only | ||
| 275 | // callers retain their header target until all peers carry the richer | 286 | // 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) { | |||
| 748 | 748 | require.True(t, pickerRow.hasRemoteKey) | |
| 749 | 749 | require.False(t, pickerRow.selectable, "remote state %d must remain gated in phase 5", availability[i]) | |
| 750 | 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 | 752 | if availability[i] == RemoteStale || availability[i] == RemoteVersionMismatch { | |
| 753 | - | wantDetail = "remote state detail" | |
| 753 | + | wantDetail = "@mule (remote state detail)" | |
| 754 | 754 | } | |
| 755 | 755 | require.Equal(t, wantDetail, pickerRow.detail) | |
| 756 | 756 | } | |
| @@ -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 ( | |||
| 6 | 6 | "github.com/stretchr/testify/require" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/vev/internal/domain" | |
| 9 | + | "github.com/bnema/vev/pkg/renderer" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | func remoteParityLifecycle(seed byte) domain.SessionLifecycleID { | |
| @@ -14,6 +15,28 @@ func remoteParityLifecycle(seed byte) domain.SessionLifecycleID { | |||
| 14 | 15 | 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 | 40 | func TestRemoteRowsWithDuplicateLabelsKeepDistinctRoutingIdentity(t *testing.T) { | |
| 18 | 41 | firstLifecycle := remoteParityLifecycle(1) | |
| 19 | 42 | 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 id | 15 | 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) | ||