detached 4eaa45d

feat(status): show only this client's visited sessions in the recent history

commit
4eaa45ddbe63e65974bbee95fbc4d2535016454a
author
brice · · 2026-09-23 19:46 UTC
parent
80cdd12
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
17 files · +95 -18
M internal/adapters/sessionwire/proto_convert_navigation.go +2 -0
@@ -72,6 +72,7 @@ func recentRouteEntryToWire(entry protocol.RecentRouteEntry) (*wire.RecentRouteE
7272 Attention: entry.Attention,
7373 Reachability: uint32(entry.Reachability),
7474 AttentionSeq: entry.AttentionSeq,
75+ Visited: entry.Visited,
7576 }, nil
7677 }
7778
@@ -115,6 +116,7 @@ func recentRouteEntryFromWire(message *wire.RecentRouteEntry) (protocol.RecentRo
115116 return protocol.RecentRouteEntry{}, err
116117 }
117118 entry.AttentionSeq = message.GetAttentionSeq()
119+ entry.Visited = message.GetVisited()
118120 return entry, nil
119121 }
120122
@@ -72,6 +72,7 @@ func recentRouteEntryToWire(entry protocol.RecentRouteEntry) (*wire.RecentRouteE
72 Attention: entry.Attention,72 Attention: entry.Attention,
73 Reachability: uint32(entry.Reachability),73 Reachability: uint32(entry.Reachability),
74 AttentionSeq: entry.AttentionSeq,74 AttentionSeq: entry.AttentionSeq,
75+ Visited: entry.Visited,
75 }, nil76 }, nil
76 }77 }
77 78
@@ -115,6 +116,7 @@ func recentRouteEntryFromWire(message *wire.RecentRouteEntry) (protocol.RecentRo
115 return protocol.RecentRouteEntry{}, err116 return protocol.RecentRouteEntry{}, err
116 }117 }
117 entry.AttentionSeq = message.GetAttentionSeq()118 entry.AttentionSeq = message.GetAttentionSeq()
119+ entry.Visited = message.GetVisited()
118 return entry, nil120 return entry, nil
119 }121 }
120 122
M internal/ports/broker_acquisition_test.go +1 -1
@@ -9,7 +9,7 @@ import (
99 )
1010
1111 func acquisitionTestPolicy() BrokerPolicy {
12- return BrokerPolicy{ProtocolVersion: 58, CatalogSchemaVersion: 3, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "openssh", Launch: "explicit", Isolation: "user"}
12+ return BrokerPolicy{ProtocolVersion: 59, CatalogSchemaVersion: 3, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "openssh", Launch: "explicit", Isolation: "user"}
1313 }
1414
1515 func TestBrokerDialTargetFirstContactAndBoundValidation(t *testing.T) {
@@ -9,7 +9,7 @@ import (
9 )9 )
10 10
11 func acquisitionTestPolicy() BrokerPolicy {11 func acquisitionTestPolicy() BrokerPolicy {
12- return BrokerPolicy{ProtocolVersion: 58, CatalogSchemaVersion: 3, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "openssh", Launch: "explicit", Isolation: "user"}12+ return BrokerPolicy{ProtocolVersion: 59, CatalogSchemaVersion: 3, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "openssh", Launch: "explicit", Isolation: "user"}
13 }13 }
14 14
15 func TestBrokerDialTargetFirstContactAndBoundValidation(t *testing.T) {15 func TestBrokerDialTargetFirstContactAndBoundValidation(t *testing.T) {
M internal/protocol/routes.go +3 -0
@@ -318,6 +318,9 @@ type RecentRouteEntry struct {
318318 // AttentionSeq orders attention onsets across every daemon the client
319319 // observes: a smaller value is older. It is zero without attention.
320320 AttentionSeq uint64
321+ // Visited reports that this client attached to the route. The status-bar
322+ // history and jump-recent ranks show only visited routes.
323+ Visited bool
321324 }
322325
323326 // RouteHost is one daemon the client can create a session on, other than
@@ -318,6 +318,9 @@ type RecentRouteEntry struct {
318 // AttentionSeq orders attention onsets across every daemon the client318 // AttentionSeq orders attention onsets across every daemon the client
319 // observes: a smaller value is older. It is zero without attention.319 // observes: a smaller value is older. It is zero without attention.
320 AttentionSeq uint64320 AttentionSeq uint64
321+ // Visited reports that this client attached to the route. The status-bar
322+ // history and jump-recent ranks show only visited routes.
323+ Visited bool
321 }324 }
322 325
323 // RouteHost is one daemon the client can create a session on, other than326 // RouteHost is one daemon the client can create a session on, other than
M internal/protocol/session.go +1 -1
@@ -16,7 +16,7 @@ import (
1616 // daemonmux physical preamble conversation (multiplex.proto), whose
1717 // negotiated ceilings and accepted daemon binding ride the same wire
1818 // version as the session and broker conversations.
19-const Version uint16 = 58
19+const Version uint16 = 59
2020
2121 // HandshakeTimeout bounds every transport handshake from connect through the
2222 // first committed publication. It excludes the preceding client-local
@@ -16,7 +16,7 @@ import (
16 // daemonmux physical preamble conversation (multiplex.proto), whose16 // daemonmux physical preamble conversation (multiplex.proto), whose
17 // negotiated ceilings and accepted daemon binding ride the same wire17 // negotiated ceilings and accepted daemon binding ride the same wire
18 // version as the session and broker conversations.18 // version as the session and broker conversations.
19-const Version uint16 = 5819+const Version uint16 = 59
20 20
21 // HandshakeTimeout bounds every transport handshake from connect through the21 // HandshakeTimeout bounds every transport handshake from connect through the
22 // first committed publication. It excludes the preceding client-local22 // first committed publication. It excludes the preceding client-local
M internal/protocol/session_test.go +1 -1
@@ -10,7 +10,7 @@ import (
1010 )
1111
1212 func TestVersionRemainsWireCompatible(t *testing.T) {
13- require.Equal(t, uint16(58), Version)
13+ require.Equal(t, uint16(59), Version)
1414 require.Equal(t, (16<<20)-407, MaxOutputDataLen)
1515 }
1616
@@ -10,7 +10,7 @@ import (
10 )10 )
11 11
12 func TestVersionRemainsWireCompatible(t *testing.T) {12 func TestVersionRemainsWireCompatible(t *testing.T) {
13- require.Equal(t, uint16(58), Version)13+ require.Equal(t, uint16(59), Version)
14 require.Equal(t, (16<<20)-407, MaxOutputDataLen)14 require.Equal(t, (16<<20)-407, MaxOutputDataLen)
15 }15 }
16 16
M internal/protocol/wire/envelope_inventory_test.go +2 -2
@@ -15,8 +15,8 @@ import (
1515 // shares a payload type except route navigation failure, and the preamble
1616 // constants agree with the schema authority.
1717 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
18- if protocol.Version != 58 {
19- t.Fatalf("protocol.Version = %d, want 58", protocol.Version)
18+ if protocol.Version != 59 {
19+ t.Fatalf("protocol.Version = %d, want 59", protocol.Version)
2020 }
2121 if ProtocolEpoch != 1 {
2222 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)
@@ -15,8 +15,8 @@ import (
15 // shares a payload type except route navigation failure, and the preamble15 // shares a payload type except route navigation failure, and the preamble
16 // constants agree with the schema authority.16 // constants agree with the schema authority.
17 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {17 func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
18- if protocol.Version != 58 {18+ if protocol.Version != 59 {
19- t.Fatalf("protocol.Version = %d, want 58", protocol.Version)19+ t.Fatalf("protocol.Version = %d, want 59", protocol.Version)
20 }20 }
21 if ProtocolEpoch != 1 {21 if ProtocolEpoch != 1 {
22 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)22 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)
M internal/protocol/wire/schema/session.proto +3 -0
@@ -162,6 +162,9 @@ message RecentRouteEntry {
162162 uint32 reachability = 9;
163163 // Client-assigned attention onset order; zero without attention.
164164 uint64 attention_seq = 10;
165+ // True once this client attached to the route; the status-bar history
166+ // shows only visited routes.
167+ bool visited = 11;
165168 }
166169
167170 // RouteHost is one creation destination other than the serving daemon.
@@ -162,6 +162,9 @@ message RecentRouteEntry {
162 uint32 reachability = 9;162 uint32 reachability = 9;
163 // Client-assigned attention onset order; zero without attention.163 // Client-assigned attention onset order; zero without attention.
164 uint64 attention_seq = 10;164 uint64 attention_seq = 10;
165+ // True once this client attached to the route; the status-bar history
166+ // shows only visited routes.
167+ bool visited = 11;
165 }168 }
166 169
167 // RouteHost is one creation destination other than the serving daemon.170 // RouteHost is one creation destination other than the serving daemon.
M internal/protocol/wire/session.pb.go +14 -3 generated file: collapsed
M internal/usecase/client/route_ledger.go +2 -0
@@ -201,6 +201,7 @@ func (l *routeLedger) build(snapshot ports.BrokerSnapshot, active routeActive) (
201201 entry := routeActiveEntry(candidates, activeID, active, activeObservation)
202202 entry.Key, entry.Generation = l.keyFor(activeID), 1
203203 entry.AttentionSeq = l.attention[activeID]
204+ entry.Visited = true
204205 if validateRouteEntryShape(entry) == nil {
205206 next.ActiveEntry = entry
206207 next.Active = protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
@@ -220,6 +221,7 @@ func (l *routeLedger) build(snapshot ports.BrokerSnapshot, active routeActive) (
220221 entry := candidate.entry
221222 entry.Key, entry.Generation = l.keyFor(candidate.id), 1
222223 entry.AttentionSeq = l.attention[candidate.id]
224+ entry.Visited = l.attached[candidate.id] != 0
223225 ref := protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
224226 if next.Previous.IsZero() && l.attached[candidate.id] != 0 {
225227 next.Previous = ref
@@ -201,6 +201,7 @@ func (l *routeLedger) build(snapshot ports.BrokerSnapshot, active routeActive) (
201 entry := routeActiveEntry(candidates, activeID, active, activeObservation)201 entry := routeActiveEntry(candidates, activeID, active, activeObservation)
202 entry.Key, entry.Generation = l.keyFor(activeID), 1202 entry.Key, entry.Generation = l.keyFor(activeID), 1
203 entry.AttentionSeq = l.attention[activeID]203 entry.AttentionSeq = l.attention[activeID]
204+ entry.Visited = true
204 if validateRouteEntryShape(entry) == nil {205 if validateRouteEntryShape(entry) == nil {
205 next.ActiveEntry = entry206 next.ActiveEntry = entry
206 next.Active = protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}207 next.Active = protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
@@ -220,6 +221,7 @@ func (l *routeLedger) build(snapshot ports.BrokerSnapshot, active routeActive) (
220 entry := candidate.entry221 entry := candidate.entry
221 entry.Key, entry.Generation = l.keyFor(candidate.id), 1222 entry.Key, entry.Generation = l.keyFor(candidate.id), 1
222 entry.AttentionSeq = l.attention[candidate.id]223 entry.AttentionSeq = l.attention[candidate.id]
224+ entry.Visited = l.attached[candidate.id] != 0
223 ref := protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}225 ref := protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
224 if next.Previous.IsZero() && l.attached[candidate.id] != 0 {226 if next.Previous.IsZero() && l.attached[candidate.id] != 0 {
225 next.Previous = ref227 next.Previous = ref
M internal/usecase/client/route_ledger_test.go +8 -0
@@ -147,6 +147,14 @@ func TestRouteLedgerActivePreviousAndHome(t *testing.T) {
147147 previous, ok := ledger.resolve(snapshot.Previous)
148148 require.True(t, ok)
149149 require.Equal(t, tt.wantPrevious, previous.target.SessionName)
150+ require.True(t, snapshot.ActiveEntry.Visited)
151+ var visited []string
152+ for _, entry := range snapshot.Entries {
153+ if entry.Visited {
154+ visited = append(visited, entry.Name)
155+ }
156+ }
157+ require.Equal(t, []string{tt.wantPrevious}, visited, "only attached routes are visited")
150158 kinds := make([]protocol.RouteKind, 0, len(snapshot.Hosts))
151159 for _, host := range snapshot.Hosts {
152160 kinds = append(kinds, host.Kind)
@@ -147,6 +147,14 @@ func TestRouteLedgerActivePreviousAndHome(t *testing.T) {
147 previous, ok := ledger.resolve(snapshot.Previous)147 previous, ok := ledger.resolve(snapshot.Previous)
148 require.True(t, ok)148 require.True(t, ok)
149 require.Equal(t, tt.wantPrevious, previous.target.SessionName)149 require.Equal(t, tt.wantPrevious, previous.target.SessionName)
150+ require.True(t, snapshot.ActiveEntry.Visited)
151+ var visited []string
152+ for _, entry := range snapshot.Entries {
153+ if entry.Visited {
154+ visited = append(visited, entry.Name)
155+ }
156+ }
157+ require.Equal(t, []string{tt.wantPrevious}, visited, "only attached routes are visited")
150 kinds := make([]protocol.RouteKind, 0, len(snapshot.Hosts))158 kinds := make([]protocol.RouteKind, 0, len(snapshot.Hosts))
151 for _, host := range snapshot.Hosts {159 for _, host := range snapshot.Hosts {
152 kinds = append(kinds, host.Kind)160 kinds = append(kinds, host.Kind)
M internal/usecase/daemon/palette.go +4 -2
@@ -282,6 +282,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
282282 }
283283
284284 func recentRouteHints(snapshot protocol.RecentRouteSnapshot, args []string) palette.ContextualHints {
285+ snapshot = visitedRouteSnapshot(snapshot)
285286 formatted := formatRecentRouteSnapshot(snapshot)
286287 names := make([]string, len(formatted))
287288 for i, entry := range formatted {
@@ -1123,10 +1124,11 @@ func (e paletteExec) YankLastNotification() error {
11231124 }
11241125
11251126 func (e paletteExec) JumpRecentSession(rank int) error {
1126- if e.routeSnapshot.Generation == 0 || rank < 1 || rank > len(e.routeSnapshot.Entries) {
1127+ visited := visitedRouteSnapshot(e.routeSnapshot)
1128+ if visited.Generation == 0 || rank < 1 || rank > len(visited.Entries) {
11271129 return command.ErrInvalidArguments
11281130 }
1129- entry := e.routeSnapshot.Entries[rank-1]
1131+ entry := visited.Entries[rank-1]
11301132 return e.NavigateRecentRoute(protocol.RouteNavigationAction{
11311133 SnapshotGeneration: e.routeSnapshot.Generation,
11321134 Key: entry.Key,
@@ -282,6 +282,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
282 }282 }
283 283
284 func recentRouteHints(snapshot protocol.RecentRouteSnapshot, args []string) palette.ContextualHints {284 func recentRouteHints(snapshot protocol.RecentRouteSnapshot, args []string) palette.ContextualHints {
285+ snapshot = visitedRouteSnapshot(snapshot)
285 formatted := formatRecentRouteSnapshot(snapshot)286 formatted := formatRecentRouteSnapshot(snapshot)
286 names := make([]string, len(formatted))287 names := make([]string, len(formatted))
287 for i, entry := range formatted {288 for i, entry := range formatted {
@@ -1123,10 +1124,11 @@ func (e paletteExec) YankLastNotification() error {
1123 }1124 }
1124 1125
1125 func (e paletteExec) JumpRecentSession(rank int) error {1126 func (e paletteExec) JumpRecentSession(rank int) error {
1126- if e.routeSnapshot.Generation == 0 || rank < 1 || rank > len(e.routeSnapshot.Entries) {1127+ visited := visitedRouteSnapshot(e.routeSnapshot)
1128+ if visited.Generation == 0 || rank < 1 || rank > len(visited.Entries) {
1127 return command.ErrInvalidArguments1129 return command.ErrInvalidArguments
1128 }1130 }
1129- entry := e.routeSnapshot.Entries[rank-1]1131+ entry := visited.Entries[rank-1]
1130 return e.NavigateRecentRoute(protocol.RouteNavigationAction{1132 return e.NavigateRecentRoute(protocol.RouteNavigationAction{
1131 SnapshotGeneration: e.routeSnapshot.Generation,1133 SnapshotGeneration: e.routeSnapshot.Generation,
1132 Key: entry.Key,1134 Key: entry.Key,
M internal/usecase/daemon/recent_route_presentation_test.go +1 -1
@@ -90,7 +90,7 @@ func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T)
9090 snapshot := protocol.RecentRouteSnapshot{
9191 Generation: 1,
9292 Entries: []protocol.RecentRouteEntry{
93- {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true},
93+ {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true, Visited: true},
9494 testRouteEntry(2, 1, "logs", 2, protocol.RouteKindLocal),
9595 },
9696 }
@@ -90,7 +90,7 @@ func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T)
90 snapshot := protocol.RecentRouteSnapshot{90 snapshot := protocol.RecentRouteSnapshot{
91 Generation: 1,91 Generation: 1,
92 Entries: []protocol.RecentRouteEntry{92 Entries: []protocol.RecentRouteEntry{
93- {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true},93+ {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true, Visited: true},
94 testRouteEntry(2, 1, "logs", 2, protocol.RouteKindLocal),94 testRouteEntry(2, 1, "logs", 2, protocol.RouteKindLocal),
95 },95 },
96 }96 }
M internal/usecase/daemon/recent_route_snapshot.go +18 -0
@@ -35,6 +35,24 @@ func recentRoutePresentationsFromSnapshot(snapshot protocol.RecentRouteSnapshot)
3535 return out
3636 }
3737
38+// visitedRouteSnapshot keeps only the routes this client attached to. The
39+// status-bar history and jump-recent ranks start from the current session and
40+// grow as the client visits others; the palette and attention keep the full
41+// snapshot.
42+func visitedRouteSnapshot(snapshot protocol.RecentRouteSnapshot) protocol.RecentRouteSnapshot {
43+ if snapshot.Entries == nil {
44+ return snapshot
45+ }
46+ visited := make([]protocol.RecentRouteEntry, 0, len(snapshot.Entries))
47+ for _, entry := range snapshot.Entries {
48+ if entry.Visited {
49+ visited = append(visited, entry)
50+ }
51+ }
52+ snapshot.Entries = visited
53+ return snapshot
54+}
55+
3856 func formatRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
3957 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))
4058 }
@@ -35,6 +35,24 @@ func recentRoutePresentationsFromSnapshot(snapshot protocol.RecentRouteSnapshot)
35 return out35 return out
36 }36 }
37 37
38+// visitedRouteSnapshot keeps only the routes this client attached to. The
39+// status-bar history and jump-recent ranks start from the current session and
40+// grow as the client visits others; the palette and attention keep the full
41+// snapshot.
42+func visitedRouteSnapshot(snapshot protocol.RecentRouteSnapshot) protocol.RecentRouteSnapshot {
43+ if snapshot.Entries == nil {
44+ return snapshot
45+ }
46+ visited := make([]protocol.RecentRouteEntry, 0, len(snapshot.Entries))
47+ for _, entry := range snapshot.Entries {
48+ if entry.Visited {
49+ visited = append(visited, entry)
50+ }
51+ }
52+ snapshot.Entries = visited
53+ return snapshot
54+}
55+
38 func formatRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {56 func formatRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
39 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))57 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))
40 }58 }
M internal/usecase/daemon/recent_route_snapshot_test.go +31 -3
@@ -13,7 +13,7 @@ func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {
1313 Generation: 2,
1414 Active: protocol.RouteRef{Key: 3, Generation: 2},
1515 Entries: []protocol.RecentRouteEntry{
16- {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote},
16+ {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote, Visited: true},
1717 testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),
1818 },
1919 })
@@ -30,7 +30,7 @@ func TestAttachmentStatusUsesClientPublishedAttention(t *testing.T) {
3030 Generation: 2,
3131 Active: protocol.RouteRef{Key: 3, Generation: 2},
3232 Entries: []protocol.RecentRouteEntry{{
33- Key: 2, Generation: 1, Target: testRouteTarget("local", 9), Name: "local", Kind: protocol.RouteKindLocal, Attention: true,
33+ Key: 2, Generation: 1, Target: testRouteTarget("local", 9), Name: "local", Kind: protocol.RouteKindLocal, Attention: true, Visited: true,
3434 }},
3535 })
3636
@@ -104,7 +104,7 @@ func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
104104 snapshot := protocol.RecentRouteSnapshot{
105105 Generation: 8,
106106 Entries: []protocol.RecentRouteEntry{{
107- Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,
107+ Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote, Visited: true,
108108 }},
109109 }
110110
@@ -116,3 +116,31 @@ func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
116116 require.Equal(t, uint64(11), hints.Recent[0].Key)
117117 require.Equal(t, uint64(7), hints.Recent[0].Generation)
118118 }
119+
120+func TestStatusHistoryShowsOnlyVisitedRoutes(t *testing.T) {
121+ unvisited := testRouteEntry(4, 1, "other", 4, protocol.RouteKindLocal)
122+ unvisited.Visited = false
123+ tests := []struct {
124+ name string
125+ entries []protocol.RecentRouteEntry
126+ want []string
127+ }{
128+ {name: "fresh client shows no history", entries: []protocol.RecentRouteEntry{unvisited}, want: nil},
129+ {name: "visited routes keep their order", entries: []protocol.RecentRouteEntry{testRouteEntry(2, 1, "work", 2, protocol.RouteKindLocal), unvisited, testRouteEntry(3, 1, "logs", 3, protocol.RouteKindLocal)}, want: []string{"work", "logs"}},
130+ }
131+ for _, tt := range tests {
132+ t.Run(tt.name, func(t *testing.T) {
133+ d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
134+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 2, Entries: tt.entries})
135+
136+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
137+
138+ var names []string
139+ for _, entry := range state.mru {
140+ names = append(names, entry.name)
141+ }
142+ require.Equal(t, tt.want, names)
143+ require.Equal(t, len(tt.want), len(recentRouteHints(ac.routeSnapshotCopy(), nil).Recent))
144+ })
145+ }
146+}
@@ -13,7 +13,7 @@ func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {
13 Generation: 2,13 Generation: 2,
14 Active: protocol.RouteRef{Key: 3, Generation: 2},14 Active: protocol.RouteRef{Key: 3, Generation: 2},
15 Entries: []protocol.RecentRouteEntry{15 Entries: []protocol.RecentRouteEntry{
16- {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote},16+ {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote, Visited: true},
17 testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),17 testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),
18 },18 },
19 })19 })
@@ -30,7 +30,7 @@ func TestAttachmentStatusUsesClientPublishedAttention(t *testing.T) {
30 Generation: 2,30 Generation: 2,
31 Active: protocol.RouteRef{Key: 3, Generation: 2},31 Active: protocol.RouteRef{Key: 3, Generation: 2},
32 Entries: []protocol.RecentRouteEntry{{32 Entries: []protocol.RecentRouteEntry{{
33- Key: 2, Generation: 1, Target: testRouteTarget("local", 9), Name: "local", Kind: protocol.RouteKindLocal, Attention: true,33+ Key: 2, Generation: 1, Target: testRouteTarget("local", 9), Name: "local", Kind: protocol.RouteKindLocal, Attention: true, Visited: true,
34 }},34 }},
35 })35 })
36 36
@@ -104,7 +104,7 @@ func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
104 snapshot := protocol.RecentRouteSnapshot{104 snapshot := protocol.RecentRouteSnapshot{
105 Generation: 8,105 Generation: 8,
106 Entries: []protocol.RecentRouteEntry{{106 Entries: []protocol.RecentRouteEntry{{
107- Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,107+ Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote, Visited: true,
108 }},108 }},
109 }109 }
110 110
@@ -116,3 +116,31 @@ func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
116 require.Equal(t, uint64(11), hints.Recent[0].Key)116 require.Equal(t, uint64(11), hints.Recent[0].Key)
117 require.Equal(t, uint64(7), hints.Recent[0].Generation)117 require.Equal(t, uint64(7), hints.Recent[0].Generation)
118 }118 }
119+
120+func TestStatusHistoryShowsOnlyVisitedRoutes(t *testing.T) {
121+ unvisited := testRouteEntry(4, 1, "other", 4, protocol.RouteKindLocal)
122+ unvisited.Visited = false
123+ tests := []struct {
124+ name string
125+ entries []protocol.RecentRouteEntry
126+ want []string
127+ }{
128+ {name: "fresh client shows no history", entries: []protocol.RecentRouteEntry{unvisited}, want: nil},
129+ {name: "visited routes keep their order", entries: []protocol.RecentRouteEntry{testRouteEntry(2, 1, "work", 2, protocol.RouteKindLocal), unvisited, testRouteEntry(3, 1, "logs", 3, protocol.RouteKindLocal)}, want: []string{"work", "logs"}},
130+ }
131+ for _, tt := range tests {
132+ t.Run(tt.name, func(t *testing.T) {
133+ d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
134+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 2, Entries: tt.entries})
135+
136+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
137+
138+ var names []string
139+ for _, entry := range state.mru {
140+ names = append(names, entry.name)
141+ }
142+ require.Equal(t, tt.want, names)
143+ require.Equal(t, len(tt.want), len(recentRouteHints(ac.routeSnapshotCopy(), nil).Recent))
144+ })
145+ }
146+}
M internal/usecase/daemon/route_identity_test_helpers_test.go +1 -1
@@ -12,6 +12,6 @@ func testRouteTarget(name string, marker byte) protocol.ExactSessionTarget {
1212 func testRouteEntry(key, generation uint64, name string, marker byte, kind protocol.RouteKind) protocol.RecentRouteEntry {
1313 return protocol.RecentRouteEntry{
1414 Key: key, Generation: generation, Target: testRouteTarget(name, marker),
15- Name: name, Kind: kind,
15+ Name: name, Kind: kind, Visited: true,
1616 }
1717 }
@@ -12,6 +12,6 @@ func testRouteTarget(name string, marker byte) protocol.ExactSessionTarget {
12 func testRouteEntry(key, generation uint64, name string, marker byte, kind protocol.RouteKind) protocol.RecentRouteEntry {12 func testRouteEntry(key, generation uint64, name string, marker byte, kind protocol.RouteKind) protocol.RecentRouteEntry {
13 return protocol.RecentRouteEntry{13 return protocol.RecentRouteEntry{
14 Key: key, Generation: generation, Target: testRouteTarget(name, marker),14 Key: key, Generation: generation, Target: testRouteTarget(name, marker),
15- Name: name, Kind: kind,15+ Name: name, Kind: kind, Visited: true,
16 }16 }
17 }17 }
M internal/usecase/daemon/status.go +2 -2
@@ -237,7 +237,7 @@ func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot p
237237 }
238238 var formatted []recentRouteDisplay
239239 if snapshot.Generation != 0 {
240- formatted = formatRecentRouteSnapshot(snapshot)
240+ formatted = formatRecentRouteSnapshot(visitedRouteSnapshot(snapshot))
241241 }
242242 entries := make([]rankedRecent, 0, len(hints.Recent))
243243 for i, hint := range hints.Recent {
@@ -296,7 +296,7 @@ func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attached
296296 if ranked != nil {
297297 state.rankedRecent = ranked
298298 } else if routeSnapshot.Generation != 0 {
299- state.mru = formatRecentRouteSnapshot(routeSnapshot)
299+ state.mru = formatRecentRouteSnapshot(visitedRouteSnapshot(routeSnapshot))
300300 }
301301 return state
302302 }
@@ -237,7 +237,7 @@ func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot p
237 }237 }
238 var formatted []recentRouteDisplay238 var formatted []recentRouteDisplay
239 if snapshot.Generation != 0 {239 if snapshot.Generation != 0 {
240- formatted = formatRecentRouteSnapshot(snapshot)240+ formatted = formatRecentRouteSnapshot(visitedRouteSnapshot(snapshot))
241 }241 }
242 entries := make([]rankedRecent, 0, len(hints.Recent))242 entries := make([]rankedRecent, 0, len(hints.Recent))
243 for i, hint := range hints.Recent {243 for i, hint := range hints.Recent {
@@ -296,7 +296,7 @@ func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attached
296 if ranked != nil {296 if ranked != nil {
297 state.rankedRecent = ranked297 state.rankedRecent = ranked
298 } else if routeSnapshot.Generation != 0 {298 } else if routeSnapshot.Generation != 0 {
299- state.mru = formatRecentRouteSnapshot(routeSnapshot)299+ state.mru = formatRecentRouteSnapshot(visitedRouteSnapshot(routeSnapshot))
300 }300 }
301 return state301 return state
302 }302 }
M internal/usecase/daemon/status_test.go +1 -1
@@ -990,7 +990,7 @@ func TestBarStateForContextualRecentUsesClientSnapshot(t *testing.T) {
990990
991991 snapshot := protocol.RecentRouteSnapshot{
992992 Generation: 1,
993- Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true}},
993+ Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true, Visited: true}},
994994 }
995995 hints := palette.ContextualHints{
996996 Kind: command.ContextHintRecentSessions,
@@ -990,7 +990,7 @@ func TestBarStateForContextualRecentUsesClientSnapshot(t *testing.T) {
990 990
991 snapshot := protocol.RecentRouteSnapshot{991 snapshot := protocol.RecentRouteSnapshot{
992 Generation: 1,992 Generation: 1,
993- Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true}},993+ Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true, Visited: true}},
994 }994 }
995 hints := palette.ContextualHints{995 hints := palette.ContextualHints{
996 Kind: command.ContextHintRecentSessions,996 Kind: command.ContextHintRecentSessions,

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