detached 11ffa7d

feat(protocol): select an attached tab in place

Add the SelectTab client message so the client picker can switch the
live attachment to another tab of the same session without
reconnecting. The daemon moves only the requesting attachment's view
and refuses an unknown tab without repairing the view.

Bumps protocol.Version 58 -> 59; this is the plan's single bump, so
later layout changes in this plan fold into 59.
commit
11ffa7d3869a6541396c757091e306e6007e336d
author
brice · · 2026-09-22 20:07 UTC
parent
ee2521c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +375 -115
M internal/adapters/sessionwire/client_test.go +1 -0
@@ -34,6 +34,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
3434 {name: "reset", message: protocol.OutputResetRequest{}},
3535 {name: "UI fence", message: protocol.UIFence{ActionID: 7}},
3636 {name: "UI fence pointer", message: &protocol.UIFence{ActionID: 7}},
37+ {name: "select tab", message: protocol.SelectTab{TabID: "tab-2"}},
3738 {name: "preview", message: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
3839 {name: "attention", message: protocol.RouteAttentionSubscription{}},
3940 {name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}},
@@ -34,6 +34,7 @@ func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
34 {name: "reset", message: protocol.OutputResetRequest{}},34 {name: "reset", message: protocol.OutputResetRequest{}},
35 {name: "UI fence", message: protocol.UIFence{ActionID: 7}},35 {name: "UI fence", message: protocol.UIFence{ActionID: 7}},
36 {name: "UI fence pointer", message: &protocol.UIFence{ActionID: 7}},36 {name: "UI fence pointer", message: &protocol.UIFence{ActionID: 7}},
37+ {name: "select tab", message: protocol.SelectTab{TabID: "tab-2"}},
37 {name: "preview", message: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},38 {name: "preview", message: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
38 {name: "attention", message: protocol.RouteAttentionSubscription{}},39 {name: "attention", message: protocol.RouteAttentionSubscription{}},
39 {name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}},40 {name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}},
M internal/adapters/sessionwire/proto_convert_misc.go +18 -0
@@ -325,6 +325,24 @@ func uiFenceFromWire(message *wire.UIFence) (protocol.UIFence, error) {
325325 return protocol.UIFence{ActionID: message.GetActionId()}, nil
326326 }
327327
328+func selectTabToWire(message protocol.SelectTab) (*wire.SelectTab, error) {
329+ if err := message.Validate(); err != nil {
330+ return nil, err
331+ }
332+ return &wire.SelectTab{TabId: string(message.TabID)}, nil
333+}
334+
335+func selectTabFromWire(message *wire.SelectTab) (protocol.SelectTab, error) {
336+ if message == nil {
337+ return protocol.SelectTab{}, errProtoConvertRange
338+ }
339+ selected := protocol.SelectTab{TabID: domain.TabStableID(message.GetTabId())}
340+ if err := selected.Validate(); err != nil {
341+ return protocol.SelectTab{}, err
342+ }
343+ return selected, nil
344+}
345+
328346 func uiReceiptToWire(message protocol.UIReceipt) (*wire.UIReceipt, error) {
329347 if err := message.Validate(); err != nil {
330348 return nil, err
@@ -325,6 +325,24 @@ func uiFenceFromWire(message *wire.UIFence) (protocol.UIFence, error) {
325 return protocol.UIFence{ActionID: message.GetActionId()}, nil325 return protocol.UIFence{ActionID: message.GetActionId()}, nil
326 }326 }
327 327
328+func selectTabToWire(message protocol.SelectTab) (*wire.SelectTab, error) {
329+ if err := message.Validate(); err != nil {
330+ return nil, err
331+ }
332+ return &wire.SelectTab{TabId: string(message.TabID)}, nil
333+}
334+
335+func selectTabFromWire(message *wire.SelectTab) (protocol.SelectTab, error) {
336+ if message == nil {
337+ return protocol.SelectTab{}, errProtoConvertRange
338+ }
339+ selected := protocol.SelectTab{TabID: domain.TabStableID(message.GetTabId())}
340+ if err := selected.Validate(); err != nil {
341+ return protocol.SelectTab{}, err
342+ }
343+ return selected, nil
344+}
345+
328 func uiReceiptToWire(message protocol.UIReceipt) (*wire.UIReceipt, error) {346 func uiReceiptToWire(message protocol.UIReceipt) (*wire.UIReceipt, error) {
329 if err := message.Validate(); err != nil {347 if err := message.Validate(); err != nil {
330 return nil, err348 return nil, err
M internal/adapters/sessionwire/proto_convert_test.go +42 -0
@@ -50,6 +50,7 @@ func TestProtoClientRoundTrips(t *testing.T) {
5050 protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"},
5151 protocol.OutputResetRequest{},
5252 protocol.UIFence{ActionID: 7},
53+ protocol.SelectTab{TabID: "tab-2"},
5354 protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1},
5455 protocol.RouteAttentionSubscription{},
5556 protocol.SamePeerSwitchRequest{RequestID: 1, Target: protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}},
@@ -250,6 +251,47 @@ func TestProtoScanNegatives(t *testing.T) {
250251 require.NoError(t, proto.Unmarshal(duplicate, envelope))
251252 }
252253
254+// TestProtoSelectTabWire pins the SelectTab payload byte for byte and refuses
255+// a malformed tab identity, a truncated prefix, and trailing garbage.
256+func TestProtoSelectTabWire(t *testing.T) {
257+ envelope, err := encodeProtoClient(protocol.SelectTab{TabID: "t1"})
258+ require.NoError(t, err)
259+ raw, err := proto.Marshal(envelope)
260+ require.NoError(t, err)
261+ // field 31 (length-delimited) -> SelectTab{field 1 = "t1"}.
262+ require.Equal(t, []byte{0xFA, 0x01, 0x04, 0x0A, 0x02, 't', '1'}, raw)
263+
264+ tests := []struct {
265+ name string
266+ raw []byte
267+ wantErr bool
268+ }{
269+ {name: "exact", raw: raw},
270+ {name: "truncated prefix", raw: raw[:len(raw)-1], wantErr: true},
271+ {name: "trailing garbage", raw: append(append([]byte(nil), raw...), 0xFF), wantErr: true},
272+ }
273+ for _, tt := range tests {
274+ t.Run(tt.name, func(t *testing.T) {
275+ err := wire.ScanEnvelope(&wire.ClientEnvelope{}, tt.raw)
276+ if tt.wantErr {
277+ require.Error(t, err)
278+ return
279+ }
280+ require.NoError(t, err)
281+ decoded := &wire.ClientEnvelope{}
282+ require.NoError(t, proto.Unmarshal(tt.raw, decoded))
283+ got, err := decodeProtoClient(decoded)
284+ require.NoError(t, err)
285+ require.Equal(t, protocol.SelectTab{TabID: "t1"}, got)
286+ })
287+ }
288+
289+ _, err = encodeProtoClient(protocol.SelectTab{})
290+ require.Error(t, err, "an empty tab identity is never encoded")
291+ _, err = decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_SelectTab{SelectTab: &wire.SelectTab{}}})
292+ require.Error(t, err, "an empty tab identity is never decoded")
293+}
294+
253295 func protoMessageName(message any) string {
254296 return protoShortName(message)
255297 }
@@ -50,6 +50,7 @@ func TestProtoClientRoundTrips(t *testing.T) {
50 protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"},50 protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"},
51 protocol.OutputResetRequest{},51 protocol.OutputResetRequest{},
52 protocol.UIFence{ActionID: 7},52 protocol.UIFence{ActionID: 7},
53+ protocol.SelectTab{TabID: "tab-2"},
53 protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1},54 protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1},
54 protocol.RouteAttentionSubscription{},55 protocol.RouteAttentionSubscription{},
55 protocol.SamePeerSwitchRequest{RequestID: 1, Target: protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}},56 protocol.SamePeerSwitchRequest{RequestID: 1, Target: protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}},
@@ -250,6 +251,47 @@ func TestProtoScanNegatives(t *testing.T) {
250 require.NoError(t, proto.Unmarshal(duplicate, envelope))251 require.NoError(t, proto.Unmarshal(duplicate, envelope))
251 }252 }
252 253
254+// TestProtoSelectTabWire pins the SelectTab payload byte for byte and refuses
255+// a malformed tab identity, a truncated prefix, and trailing garbage.
256+func TestProtoSelectTabWire(t *testing.T) {
257+ envelope, err := encodeProtoClient(protocol.SelectTab{TabID: "t1"})
258+ require.NoError(t, err)
259+ raw, err := proto.Marshal(envelope)
260+ require.NoError(t, err)
261+ // field 31 (length-delimited) -> SelectTab{field 1 = "t1"}.
262+ require.Equal(t, []byte{0xFA, 0x01, 0x04, 0x0A, 0x02, 't', '1'}, raw)
263+
264+ tests := []struct {
265+ name string
266+ raw []byte
267+ wantErr bool
268+ }{
269+ {name: "exact", raw: raw},
270+ {name: "truncated prefix", raw: raw[:len(raw)-1], wantErr: true},
271+ {name: "trailing garbage", raw: append(append([]byte(nil), raw...), 0xFF), wantErr: true},
272+ }
273+ for _, tt := range tests {
274+ t.Run(tt.name, func(t *testing.T) {
275+ err := wire.ScanEnvelope(&wire.ClientEnvelope{}, tt.raw)
276+ if tt.wantErr {
277+ require.Error(t, err)
278+ return
279+ }
280+ require.NoError(t, err)
281+ decoded := &wire.ClientEnvelope{}
282+ require.NoError(t, proto.Unmarshal(tt.raw, decoded))
283+ got, err := decodeProtoClient(decoded)
284+ require.NoError(t, err)
285+ require.Equal(t, protocol.SelectTab{TabID: "t1"}, got)
286+ })
287+ }
288+
289+ _, err = encodeProtoClient(protocol.SelectTab{})
290+ require.Error(t, err, "an empty tab identity is never encoded")
291+ _, err = decodeProtoClient(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_SelectTab{SelectTab: &wire.SelectTab{}}})
292+ require.Error(t, err, "an empty tab identity is never decoded")
293+}
294+
253 func protoMessageName(message any) string {295 func protoMessageName(message any) string {
254 return protoShortName(message)296 return protoShortName(message)
255 }297 }
M internal/adapters/sessionwire/proto_envelope.go +13 -0
@@ -148,6 +148,17 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
148148 return nil, ErrInvalidMessage
149149 }
150150 return encodeProtoClient(*m)
151+ case protocol.SelectTab:
152+ converted, err := selectTabToWire(m)
153+ if err != nil {
154+ return nil, err
155+ }
156+ return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_SelectTab{SelectTab: converted}}, nil
157+ case *protocol.SelectTab:
158+ if m == nil {
159+ return nil, ErrInvalidMessage
160+ }
161+ return encodeProtoClient(*m)
151162 case protocol.UIFence:
152163 return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_UiFence{UiFence: uiFenceToWire(m)}}, nil
153164 case *protocol.UIFence:
@@ -652,6 +663,8 @@ func decodeProtoClient(envelope *wire.ClientEnvelope) (protocol.ClientMessage, e
652663 return protocol.OutputResetRequest{}, nil
653664 case *wire.ClientEnvelope_UiFence:
654665 return uiFenceFromWire(payload.UiFence)
666+ case *wire.ClientEnvelope_SelectTab:
667+ return selectTabFromWire(payload.SelectTab)
655668 case *wire.ClientEnvelope_RemotePreviewRequest:
656669 return remotePreviewRequestFromWire(payload.RemotePreviewRequest)
657670 case *wire.ClientEnvelope_RouteAttentionSubscription:
@@ -148,6 +148,17 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
148 return nil, ErrInvalidMessage148 return nil, ErrInvalidMessage
149 }149 }
150 return encodeProtoClient(*m)150 return encodeProtoClient(*m)
151+ case protocol.SelectTab:
152+ converted, err := selectTabToWire(m)
153+ if err != nil {
154+ return nil, err
155+ }
156+ return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_SelectTab{SelectTab: converted}}, nil
157+ case *protocol.SelectTab:
158+ if m == nil {
159+ return nil, ErrInvalidMessage
160+ }
161+ return encodeProtoClient(*m)
151 case protocol.UIFence:162 case protocol.UIFence:
152 return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_UiFence{UiFence: uiFenceToWire(m)}}, nil163 return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_UiFence{UiFence: uiFenceToWire(m)}}, nil
153 case *protocol.UIFence:164 case *protocol.UIFence:
@@ -652,6 +663,8 @@ func decodeProtoClient(envelope *wire.ClientEnvelope) (protocol.ClientMessage, e
652 return protocol.OutputResetRequest{}, nil663 return protocol.OutputResetRequest{}, nil
653 case *wire.ClientEnvelope_UiFence:664 case *wire.ClientEnvelope_UiFence:
654 return uiFenceFromWire(payload.UiFence)665 return uiFenceFromWire(payload.UiFence)
666+ case *wire.ClientEnvelope_SelectTab:
667+ return selectTabFromWire(payload.SelectTab)
655 case *wire.ClientEnvelope_RemotePreviewRequest:668 case *wire.ClientEnvelope_RemotePreviewRequest:
656 return remotePreviewRequestFromWire(payload.RemotePreviewRequest)669 return remotePreviewRequestFromWire(payload.RemotePreviewRequest)
657 case *wire.ClientEnvelope_RouteAttentionSubscription:670 case *wire.ClientEnvelope_RouteAttentionSubscription:
M internal/adapters/sessionwire/server_test.go +1 -0
@@ -34,6 +34,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
3434 {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},
3535 {name: "command", want: protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"}},
3636 {name: "reset", want: protocol.OutputResetRequest{}},
37+ {name: "select tab", want: protocol.SelectTab{TabID: "tab-2"}},
3738 {name: "preview", want: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
3839 {name: "attention", want: protocol.RouteAttentionSubscription{}},
3940 {name: "same peer", want: protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact}},
@@ -34,6 +34,7 @@ func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
34 {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},34 {name: "notice", want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},
35 {name: "command", want: protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"}},35 {name: "command", want: protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"}},
36 {name: "reset", want: protocol.OutputResetRequest{}},36 {name: "reset", want: protocol.OutputResetRequest{}},
37+ {name: "select tab", want: protocol.SelectTab{TabID: "tab-2"}},
37 {name: "preview", want: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},38 {name: "preview", want: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
38 {name: "attention", want: protocol.RouteAttentionSubscription{}},39 {name: "attention", want: protocol.RouteAttentionSubscription{}},
39 {name: "same peer", want: protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact}},40 {name: "same peer", want: protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact}},
M internal/protocol/messages.go +1 -0
@@ -19,6 +19,7 @@ func (ImagePush) clientMessage() {}
1919 func (ClientNotice) clientMessage() {}
2020 func (CommandRequest) clientMessage() {}
2121 func (OutputResetRequest) clientMessage() {}
22+func (SelectTab) clientMessage() {}
2223 func (RemotePreviewRequest) clientMessage() {}
2324 func (RouteAttentionSubscription) clientMessage() {}
2425 func (SamePeerSwitchRequest) clientMessage() {}
@@ -19,6 +19,7 @@ func (ImagePush) clientMessage() {}
19 func (ClientNotice) clientMessage() {}19 func (ClientNotice) clientMessage() {}
20 func (CommandRequest) clientMessage() {}20 func (CommandRequest) clientMessage() {}
21 func (OutputResetRequest) clientMessage() {}21 func (OutputResetRequest) clientMessage() {}
22+func (SelectTab) clientMessage() {}
22 func (RemotePreviewRequest) clientMessage() {}23 func (RemotePreviewRequest) clientMessage() {}
23 func (RouteAttentionSubscription) clientMessage() {}24 func (RouteAttentionSubscription) clientMessage() {}
24 func (SamePeerSwitchRequest) clientMessage() {}25 func (SamePeerSwitchRequest) clientMessage() {}
M internal/protocol/session.go +14 -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
@@ -344,6 +344,19 @@ type SessionInfo struct {
344344 type Sessions struct{ Sessions []SessionInfo }
345345 type OutputResetRequest struct{}
346346
347+// SelectTab asks the daemon to show one exact tab of the attached session on
348+// this attachment's own view. It is the client picker's in-place tab switch:
349+// the attachment and its stream stay live, and a tab that no longer exists is
350+// refused without changing the view.
351+type SelectTab struct {
352+ TabID domain.TabStableID
353+}
354+
355+// Validate requires a well-formed stable tab identity.
356+func (m SelectTab) Validate() error {
357+ return domain.ValidateTabStableID(m.TabID)
358+}
359+
347360 func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
348361 return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
349362 }
@@ -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
@@ -344,6 +344,19 @@ type SessionInfo struct {
344 type Sessions struct{ Sessions []SessionInfo }344 type Sessions struct{ Sessions []SessionInfo }
345 type OutputResetRequest struct{}345 type OutputResetRequest struct{}
346 346
347+// SelectTab asks the daemon to show one exact tab of the attached session on
348+// this attachment's own view. It is the client picker's in-place tab switch:
349+// the attachment and its stream stay live, and a tab that no longer exists is
350+// refused without changing the view.
351+type SelectTab struct {
352+ TabID domain.TabStableID
353+}
354+
355+// Validate requires a well-formed stable tab identity.
356+func (m SelectTab) Validate() error {
357+ return domain.ValidateTabStableID(m.TabID)
358+}
359+
347 func validEnvironmentPolicy(policy EnvironmentPolicy) bool {360 func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
348 return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned361 return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
349 }362 }
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.pb.go +87 -66 generated file: collapsed
M internal/protocol/wire/envelope_inventory_test.go +3 -3
@@ -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)
@@ -40,7 +40,7 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
4040 "ui_fence": true, "navigation_inventory_request": true,
4141 "navigation_inventory_publication": true, "navigation_inventory_failure": true,
4242 "picker_close": true, "picker_selection": true,
43- "picker_preview_request": true,
43+ "picker_preview_request": true, "select_tab": true,
4444 }
4545 serverSemantic := map[string]bool{
4646 "attachment_suspended": true, "attachment_activated": true,
@@ -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)
@@ -40,7 +40,7 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
40 "ui_fence": true, "navigation_inventory_request": true,40 "ui_fence": true, "navigation_inventory_request": true,
41 "navigation_inventory_publication": true, "navigation_inventory_failure": true,41 "navigation_inventory_publication": true, "navigation_inventory_failure": true,
42 "picker_close": true, "picker_selection": true,42 "picker_close": true, "picker_selection": true,
43- "picker_preview_request": true,43+ "picker_preview_request": true, "select_tab": true,
44 }44 }
45 serverSemantic := map[string]bool{45 serverSemantic := map[string]bool{
46 "attachment_suspended": true, "attachment_activated": true,46 "attachment_suspended": true, "attachment_activated": true,
M internal/protocol/wire/schema/envelope.proto +1 -0
@@ -96,6 +96,7 @@ message ClientEnvelope {
9696 PickerPreviewRequest picker_preview_request = 27;
9797 SuspendAttachment suspend_attachment = 29;
9898 ActivateAttachment activate_attachment = 30;
99+ SelectTab select_tab = 31;
99100 }
100101 }
101102
@@ -96,6 +96,7 @@ message ClientEnvelope {
96 PickerPreviewRequest picker_preview_request = 27;96 PickerPreviewRequest picker_preview_request = 27;
97 SuspendAttachment suspend_attachment = 29;97 SuspendAttachment suspend_attachment = 29;
98 ActivateAttachment activate_attachment = 30;98 ActivateAttachment activate_attachment = 30;
99+ SelectTab select_tab = 31;
99 }100 }
100 }101 }
101 102
M internal/protocol/wire/schema/terminal.proto +5 -0
@@ -86,6 +86,11 @@ message ClientNotice {
8686 // OutputResetRequest restarts the output chain. Empty marker.
8787 message OutputResetRequest {}
8888
89+// SelectTab shows one exact tab of the attached session on this attachment.
90+message SelectTab {
91+ string tab_id = 1;
92+}
93+
8994 // UIFence orders UI operations by action.
9095 message UIFence {
9196 uint64 action_id = 1;
@@ -86,6 +86,11 @@ message ClientNotice {
86 // OutputResetRequest restarts the output chain. Empty marker.86 // OutputResetRequest restarts the output chain. Empty marker.
87 message OutputResetRequest {}87 message OutputResetRequest {}
88 88
89+// SelectTab shows one exact tab of the attached session on this attachment.
90+message SelectTab {
91+ string tab_id = 1;
92+}
93+
89 // UIFence orders UI operations by action.94 // UIFence orders UI operations by action.
90 message UIFence {95 message UIFence {
91 uint64 action_id = 1;96 uint64 action_id = 1;
M internal/protocol/wire/terminal.pb.go +92 -44
@@ -665,6 +665,51 @@ func (*OutputResetRequest) Descriptor() ([]byte, []int) {
665665 return file_terminal_proto_rawDescGZIP(), []int{8}
666666 }
667667
668+// SelectTab shows one exact tab of the attached session on this attachment.
669+type SelectTab struct {
670+ state protoimpl.MessageState `protogen:"open.v1"`
671+ TabId string `protobuf:"bytes,1,opt,name=tab_id,json=tabId,proto3" json:"tab_id,omitempty"`
672+ unknownFields protoimpl.UnknownFields
673+ sizeCache protoimpl.SizeCache
674+}
675+
676+func (x *SelectTab) Reset() {
677+ *x = SelectTab{}
678+ mi := &file_terminal_proto_msgTypes[9]
679+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
680+ ms.StoreMessageInfo(mi)
681+}
682+
683+func (x *SelectTab) String() string {
684+ return protoimpl.X.MessageStringOf(x)
685+}
686+
687+func (*SelectTab) ProtoMessage() {}
688+
689+func (x *SelectTab) ProtoReflect() protoreflect.Message {
690+ mi := &file_terminal_proto_msgTypes[9]
691+ if x != nil {
692+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
693+ if ms.LoadMessageInfo() == nil {
694+ ms.StoreMessageInfo(mi)
695+ }
696+ return ms
697+ }
698+ return mi.MessageOf(x)
699+}
700+
701+// Deprecated: Use SelectTab.ProtoReflect.Descriptor instead.
702+func (*SelectTab) Descriptor() ([]byte, []int) {
703+ return file_terminal_proto_rawDescGZIP(), []int{9}
704+}
705+
706+func (x *SelectTab) GetTabId() string {
707+ if x != nil {
708+ return x.TabId
709+ }
710+ return ""
711+}
712+
668713 // UIFence orders UI operations by action.
669714 type UIFence struct {
670715 state protoimpl.MessageState `protogen:"open.v1"`
@@ -675,7 +720,7 @@ type UIFence struct {
675720
676721 func (x *UIFence) Reset() {
677722 *x = UIFence{}
678- mi := &file_terminal_proto_msgTypes[9]
723+ mi := &file_terminal_proto_msgTypes[10]
679724 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
680725 ms.StoreMessageInfo(mi)
681726 }
@@ -687,7 +732,7 @@ func (x *UIFence) String() string {
687732 func (*UIFence) ProtoMessage() {}
688733
689734 func (x *UIFence) ProtoReflect() protoreflect.Message {
690- mi := &file_terminal_proto_msgTypes[9]
735+ mi := &file_terminal_proto_msgTypes[10]
691736 if x != nil {
692737 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
693738 if ms.LoadMessageInfo() == nil {
@@ -700,7 +745,7 @@ func (x *UIFence) ProtoReflect() protoreflect.Message {
700745
701746 // Deprecated: Use UIFence.ProtoReflect.Descriptor instead.
702747 func (*UIFence) Descriptor() ([]byte, []int) {
703- return file_terminal_proto_rawDescGZIP(), []int{9}
748+ return file_terminal_proto_rawDescGZIP(), []int{10}
704749 }
705750
706751 func (x *UIFence) GetActionId() uint64 {
@@ -725,7 +770,7 @@ type UIReceipt struct {
725770
726771 func (x *UIReceipt) Reset() {
727772 *x = UIReceipt{}
728- mi := &file_terminal_proto_msgTypes[10]
773+ mi := &file_terminal_proto_msgTypes[11]
729774 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
730775 ms.StoreMessageInfo(mi)
731776 }
@@ -737,7 +782,7 @@ func (x *UIReceipt) String() string {
737782 func (*UIReceipt) ProtoMessage() {}
738783
739784 func (x *UIReceipt) ProtoReflect() protoreflect.Message {
740- mi := &file_terminal_proto_msgTypes[10]
785+ mi := &file_terminal_proto_msgTypes[11]
741786 if x != nil {
742787 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
743788 if ms.LoadMessageInfo() == nil {
@@ -750,7 +795,7 @@ func (x *UIReceipt) ProtoReflect() protoreflect.Message {
750795
751796 // Deprecated: Use UIReceipt.ProtoReflect.Descriptor instead.
752797 func (*UIReceipt) Descriptor() ([]byte, []int) {
753- return file_terminal_proto_rawDescGZIP(), []int{10}
798+ return file_terminal_proto_rawDescGZIP(), []int{11}
754799 }
755800
756801 func (x *UIReceipt) GetActionId() uint64 {
@@ -800,7 +845,7 @@ type UIViewUpdate struct {
800845
801846 func (x *UIViewUpdate) Reset() {
802847 *x = UIViewUpdate{}
803- mi := &file_terminal_proto_msgTypes[11]
848+ mi := &file_terminal_proto_msgTypes[12]
804849 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
805850 ms.StoreMessageInfo(mi)
806851 }
@@ -812,7 +857,7 @@ func (x *UIViewUpdate) String() string {
812857 func (*UIViewUpdate) ProtoMessage() {}
813858
814859 func (x *UIViewUpdate) ProtoReflect() protoreflect.Message {
815- mi := &file_terminal_proto_msgTypes[11]
860+ mi := &file_terminal_proto_msgTypes[12]
816861 if x != nil {
817862 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
818863 if ms.LoadMessageInfo() == nil {
@@ -825,7 +870,7 @@ func (x *UIViewUpdate) ProtoReflect() protoreflect.Message {
825870
826871 // Deprecated: Use UIViewUpdate.ProtoReflect.Descriptor instead.
827872 func (*UIViewUpdate) Descriptor() ([]byte, []int) {
828- return file_terminal_proto_rawDescGZIP(), []int{11}
873+ return file_terminal_proto_rawDescGZIP(), []int{12}
829874 }
830875
831876 func (x *UIViewUpdate) GetEpoch() uint64 {
@@ -862,7 +907,7 @@ type RemotePreviewRequest struct {
862907
863908 func (x *RemotePreviewRequest) Reset() {
864909 *x = RemotePreviewRequest{}
865- mi := &file_terminal_proto_msgTypes[12]
910+ mi := &file_terminal_proto_msgTypes[13]
866911 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
867912 ms.StoreMessageInfo(mi)
868913 }
@@ -874,7 +919,7 @@ func (x *RemotePreviewRequest) String() string {
874919 func (*RemotePreviewRequest) ProtoMessage() {}
875920
876921 func (x *RemotePreviewRequest) ProtoReflect() protoreflect.Message {
877- mi := &file_terminal_proto_msgTypes[12]
922+ mi := &file_terminal_proto_msgTypes[13]
878923 if x != nil {
879924 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
880925 if ms.LoadMessageInfo() == nil {
@@ -887,7 +932,7 @@ func (x *RemotePreviewRequest) ProtoReflect() protoreflect.Message {
887932
888933 // Deprecated: Use RemotePreviewRequest.ProtoReflect.Descriptor instead.
889934 func (*RemotePreviewRequest) Descriptor() ([]byte, []int) {
890- return file_terminal_proto_rawDescGZIP(), []int{12}
935+ return file_terminal_proto_rawDescGZIP(), []int{13}
891936 }
892937
893938 func (x *RemotePreviewRequest) GetVersion() uint32 {
@@ -937,7 +982,7 @@ type RemotePreview struct {
937982
938983 func (x *RemotePreview) Reset() {
939984 *x = RemotePreview{}
940- mi := &file_terminal_proto_msgTypes[13]
985+ mi := &file_terminal_proto_msgTypes[14]
941986 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
942987 ms.StoreMessageInfo(mi)
943988 }
@@ -949,7 +994,7 @@ func (x *RemotePreview) String() string {
949994 func (*RemotePreview) ProtoMessage() {}
950995
951996 func (x *RemotePreview) ProtoReflect() protoreflect.Message {
952- mi := &file_terminal_proto_msgTypes[13]
997+ mi := &file_terminal_proto_msgTypes[14]
953998 if x != nil {
954999 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9551000 if ms.LoadMessageInfo() == nil {
@@ -962,7 +1007,7 @@ func (x *RemotePreview) ProtoReflect() protoreflect.Message {
9621007
9631008 // Deprecated: Use RemotePreview.ProtoReflect.Descriptor instead.
9641009 func (*RemotePreview) Descriptor() ([]byte, []int) {
965- return file_terminal_proto_rawDescGZIP(), []int{13}
1010+ return file_terminal_proto_rawDescGZIP(), []int{14}
9661011 }
9671012
9681013 func (x *RemotePreview) GetVersion() uint32 {
@@ -1036,7 +1081,7 @@ type PickerPreviewRequest struct {
10361081
10371082 func (x *PickerPreviewRequest) Reset() {
10381083 *x = PickerPreviewRequest{}
1039- mi := &file_terminal_proto_msgTypes[14]
1084+ mi := &file_terminal_proto_msgTypes[15]
10401085 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10411086 ms.StoreMessageInfo(mi)
10421087 }
@@ -1048,7 +1093,7 @@ func (x *PickerPreviewRequest) String() string {
10481093 func (*PickerPreviewRequest) ProtoMessage() {}
10491094
10501095 func (x *PickerPreviewRequest) ProtoReflect() protoreflect.Message {
1051- mi := &file_terminal_proto_msgTypes[14]
1096+ mi := &file_terminal_proto_msgTypes[15]
10521097 if x != nil {
10531098 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10541099 if ms.LoadMessageInfo() == nil {
@@ -1061,7 +1106,7 @@ func (x *PickerPreviewRequest) ProtoReflect() protoreflect.Message {
10611106
10621107 // Deprecated: Use PickerPreviewRequest.ProtoReflect.Descriptor instead.
10631108 func (*PickerPreviewRequest) Descriptor() ([]byte, []int) {
1064- return file_terminal_proto_rawDescGZIP(), []int{14}
1109+ return file_terminal_proto_rawDescGZIP(), []int{15}
10651110 }
10661111
10671112 func (x *PickerPreviewRequest) GetVersion() uint32 {
@@ -1125,7 +1170,7 @@ type PickerPreview struct {
11251170
11261171 func (x *PickerPreview) Reset() {
11271172 *x = PickerPreview{}
1128- mi := &file_terminal_proto_msgTypes[15]
1173+ mi := &file_terminal_proto_msgTypes[16]
11291174 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11301175 ms.StoreMessageInfo(mi)
11311176 }
@@ -1137,7 +1182,7 @@ func (x *PickerPreview) String() string {
11371182 func (*PickerPreview) ProtoMessage() {}
11381183
11391184 func (x *PickerPreview) ProtoReflect() protoreflect.Message {
1140- mi := &file_terminal_proto_msgTypes[15]
1185+ mi := &file_terminal_proto_msgTypes[16]
11411186 if x != nil {
11421187 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11431188 if ms.LoadMessageInfo() == nil {
@@ -1150,7 +1195,7 @@ func (x *PickerPreview) ProtoReflect() protoreflect.Message {
11501195
11511196 // Deprecated: Use PickerPreview.ProtoReflect.Descriptor instead.
11521197 func (*PickerPreview) Descriptor() ([]byte, []int) {
1153- return file_terminal_proto_rawDescGZIP(), []int{15}
1198+ return file_terminal_proto_rawDescGZIP(), []int{16}
11541199 }
11551200
11561201 func (x *PickerPreview) GetVersion() uint32 {
@@ -1267,7 +1312,9 @@ const file_terminal_proto_rawDesc = "" +
12671312 "\x04data\x18\x03 \x01(\fR\x04data\"&\n" +
12681313 "\fClientNotice\x12\x16\n" +
12691314 "\x06action\x18\x01 \x01(\rR\x06action\"\x14\n" +
1270- "\x12OutputResetRequest\"&\n" +
1315+ "\x12OutputResetRequest\"\"\n" +
1316+ "\tSelectTab\x12\x15\n" +
1317+ "\x06tab_id\x18\x01 \x01(\tR\x05tabId\"&\n" +
12711318 "\aUIFence\x12\x1b\n" +
12721319 "\taction_id\x18\x01 \x01(\x04R\bactionId\"\x99\x01\n" +
12731320 "\tUIReceipt\x12\x1b\n" +
@@ -1323,7 +1370,7 @@ func file_terminal_proto_rawDescGZIP() []byte {
13231370 return file_terminal_proto_rawDescData
13241371 }
13251372
1326-var file_terminal_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
1373+var file_terminal_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
13271374 var file_terminal_proto_goTypes = []any{
13281375 (*ViewContext)(nil), // 0: vev.wire.v1.ViewContext
13291376 (*Output)(nil), // 1: vev.wire.v1.Output
@@ -1334,30 +1381,31 @@ var file_terminal_proto_goTypes = []any{
13341381 (*ImagePush)(nil), // 6: vev.wire.v1.ImagePush
13351382 (*ClientNotice)(nil), // 7: vev.wire.v1.ClientNotice
13361383 (*OutputResetRequest)(nil), // 8: vev.wire.v1.OutputResetRequest
1337- (*UIFence)(nil), // 9: vev.wire.v1.UIFence
1338- (*UIReceipt)(nil), // 10: vev.wire.v1.UIReceipt
1339- (*UIViewUpdate)(nil), // 11: vev.wire.v1.UIViewUpdate
1340- (*RemotePreviewRequest)(nil), // 12: vev.wire.v1.RemotePreviewRequest
1341- (*RemotePreview)(nil), // 13: vev.wire.v1.RemotePreview
1342- (*PickerPreviewRequest)(nil), // 14: vev.wire.v1.PickerPreviewRequest
1343- (*PickerPreview)(nil), // 15: vev.wire.v1.PickerPreview
1344- (*CommittedRouteIdentity)(nil), // 16: vev.wire.v1.CommittedRouteIdentity
1345- (*RGB)(nil), // 17: vev.wire.v1.RGB
1346- (*RemoteTarget)(nil), // 18: vev.wire.v1.RemoteTarget
1347- (*LifecycleID)(nil), // 19: vev.wire.v1.LifecycleID
1348- (*PreviewCell)(nil), // 20: vev.wire.v1.PreviewCell
1384+ (*SelectTab)(nil), // 9: vev.wire.v1.SelectTab
1385+ (*UIFence)(nil), // 10: vev.wire.v1.UIFence
1386+ (*UIReceipt)(nil), // 11: vev.wire.v1.UIReceipt
1387+ (*UIViewUpdate)(nil), // 12: vev.wire.v1.UIViewUpdate
1388+ (*RemotePreviewRequest)(nil), // 13: vev.wire.v1.RemotePreviewRequest
1389+ (*RemotePreview)(nil), // 14: vev.wire.v1.RemotePreview
1390+ (*PickerPreviewRequest)(nil), // 15: vev.wire.v1.PickerPreviewRequest
1391+ (*PickerPreview)(nil), // 16: vev.wire.v1.PickerPreview
1392+ (*CommittedRouteIdentity)(nil), // 17: vev.wire.v1.CommittedRouteIdentity
1393+ (*RGB)(nil), // 18: vev.wire.v1.RGB
1394+ (*RemoteTarget)(nil), // 19: vev.wire.v1.RemoteTarget
1395+ (*LifecycleID)(nil), // 20: vev.wire.v1.LifecycleID
1396+ (*PreviewCell)(nil), // 21: vev.wire.v1.PreviewCell
13491397 }
13501398 var file_terminal_proto_depIdxs = []int32{
1351- 16, // 0: vev.wire.v1.ViewContext.route:type_name -> vev.wire.v1.CommittedRouteIdentity
1399+ 17, // 0: vev.wire.v1.ViewContext.route:type_name -> vev.wire.v1.CommittedRouteIdentity
13521400 0, // 1: vev.wire.v1.Output.context:type_name -> vev.wire.v1.ViewContext
1353- 17, // 2: vev.wire.v1.Theme.foreground:type_name -> vev.wire.v1.RGB
1354- 17, // 3: vev.wire.v1.Theme.background:type_name -> vev.wire.v1.RGB
1355- 17, // 4: vev.wire.v1.Theme.palette:type_name -> vev.wire.v1.RGB
1401+ 18, // 2: vev.wire.v1.Theme.foreground:type_name -> vev.wire.v1.RGB
1402+ 18, // 3: vev.wire.v1.Theme.background:type_name -> vev.wire.v1.RGB
1403+ 18, // 4: vev.wire.v1.Theme.palette:type_name -> vev.wire.v1.RGB
13561404 0, // 5: vev.wire.v1.UIViewUpdate.context:type_name -> vev.wire.v1.ViewContext
1357- 18, // 6: vev.wire.v1.RemotePreviewRequest.target:type_name -> vev.wire.v1.RemoteTarget
1358- 19, // 7: vev.wire.v1.RemotePreview.lifecycle_id:type_name -> vev.wire.v1.LifecycleID
1359- 20, // 8: vev.wire.v1.RemotePreview.cells:type_name -> vev.wire.v1.PreviewCell
1360- 20, // 9: vev.wire.v1.PickerPreview.cells:type_name -> vev.wire.v1.PreviewCell
1405+ 19, // 6: vev.wire.v1.RemotePreviewRequest.target:type_name -> vev.wire.v1.RemoteTarget
1406+ 20, // 7: vev.wire.v1.RemotePreview.lifecycle_id:type_name -> vev.wire.v1.LifecycleID
1407+ 21, // 8: vev.wire.v1.RemotePreview.cells:type_name -> vev.wire.v1.PreviewCell
1408+ 21, // 9: vev.wire.v1.PickerPreview.cells:type_name -> vev.wire.v1.PreviewCell
13611409 10, // [10:10] is the sub-list for method output_type
13621410 10, // [10:10] is the sub-list for method input_type
13631411 10, // [10:10] is the sub-list for extension type_name
@@ -1378,7 +1426,7 @@ func file_terminal_proto_init() {
13781426 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
13791427 RawDescriptor: unsafe.Slice(unsafe.StringData(file_terminal_proto_rawDesc), len(file_terminal_proto_rawDesc)),
13801428 NumEnums: 0,
1381- NumMessages: 16,
1429+ NumMessages: 17,
13821430 NumExtensions: 0,
13831431 NumServices: 0,
13841432 },
@@ -665,6 +665,51 @@ func (*OutputResetRequest) Descriptor() ([]byte, []int) {
665 return file_terminal_proto_rawDescGZIP(), []int{8}665 return file_terminal_proto_rawDescGZIP(), []int{8}
666 }666 }
667 667
668+// SelectTab shows one exact tab of the attached session on this attachment.
669+type SelectTab struct {
670+ state protoimpl.MessageState `protogen:"open.v1"`
671+ TabId string `protobuf:"bytes,1,opt,name=tab_id,json=tabId,proto3" json:"tab_id,omitempty"`
672+ unknownFields protoimpl.UnknownFields
673+ sizeCache protoimpl.SizeCache
674+}
675+
676+func (x *SelectTab) Reset() {
677+ *x = SelectTab{}
678+ mi := &file_terminal_proto_msgTypes[9]
679+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
680+ ms.StoreMessageInfo(mi)
681+}
682+
683+func (x *SelectTab) String() string {
684+ return protoimpl.X.MessageStringOf(x)
685+}
686+
687+func (*SelectTab) ProtoMessage() {}
688+
689+func (x *SelectTab) ProtoReflect() protoreflect.Message {
690+ mi := &file_terminal_proto_msgTypes[9]
691+ if x != nil {
692+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
693+ if ms.LoadMessageInfo() == nil {
694+ ms.StoreMessageInfo(mi)
695+ }
696+ return ms
697+ }
698+ return mi.MessageOf(x)
699+}
700+
701+// Deprecated: Use SelectTab.ProtoReflect.Descriptor instead.
702+func (*SelectTab) Descriptor() ([]byte, []int) {
703+ return file_terminal_proto_rawDescGZIP(), []int{9}
704+}
705+
706+func (x *SelectTab) GetTabId() string {
707+ if x != nil {
708+ return x.TabId
709+ }
710+ return ""
711+}
712+
668 // UIFence orders UI operations by action.713 // UIFence orders UI operations by action.
669 type UIFence struct {714 type UIFence struct {
670 state protoimpl.MessageState `protogen:"open.v1"`715 state protoimpl.MessageState `protogen:"open.v1"`
@@ -675,7 +720,7 @@ type UIFence struct {
675 720
676 func (x *UIFence) Reset() {721 func (x *UIFence) Reset() {
677 *x = UIFence{}722 *x = UIFence{}
678- mi := &file_terminal_proto_msgTypes[9]723+ mi := &file_terminal_proto_msgTypes[10]
679 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))724 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
680 ms.StoreMessageInfo(mi)725 ms.StoreMessageInfo(mi)
681 }726 }
@@ -687,7 +732,7 @@ func (x *UIFence) String() string {
687 func (*UIFence) ProtoMessage() {}732 func (*UIFence) ProtoMessage() {}
688 733
689 func (x *UIFence) ProtoReflect() protoreflect.Message {734 func (x *UIFence) ProtoReflect() protoreflect.Message {
690- mi := &file_terminal_proto_msgTypes[9]735+ mi := &file_terminal_proto_msgTypes[10]
691 if x != nil {736 if x != nil {
692 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))737 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
693 if ms.LoadMessageInfo() == nil {738 if ms.LoadMessageInfo() == nil {
@@ -700,7 +745,7 @@ func (x *UIFence) ProtoReflect() protoreflect.Message {
700 745
701 // Deprecated: Use UIFence.ProtoReflect.Descriptor instead.746 // Deprecated: Use UIFence.ProtoReflect.Descriptor instead.
702 func (*UIFence) Descriptor() ([]byte, []int) {747 func (*UIFence) Descriptor() ([]byte, []int) {
703- return file_terminal_proto_rawDescGZIP(), []int{9}748+ return file_terminal_proto_rawDescGZIP(), []int{10}
704 }749 }
705 750
706 func (x *UIFence) GetActionId() uint64 {751 func (x *UIFence) GetActionId() uint64 {
@@ -725,7 +770,7 @@ type UIReceipt struct {
725 770
726 func (x *UIReceipt) Reset() {771 func (x *UIReceipt) Reset() {
727 *x = UIReceipt{}772 *x = UIReceipt{}
728- mi := &file_terminal_proto_msgTypes[10]773+ mi := &file_terminal_proto_msgTypes[11]
729 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))774 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
730 ms.StoreMessageInfo(mi)775 ms.StoreMessageInfo(mi)
731 }776 }
@@ -737,7 +782,7 @@ func (x *UIReceipt) String() string {
737 func (*UIReceipt) ProtoMessage() {}782 func (*UIReceipt) ProtoMessage() {}
738 783
739 func (x *UIReceipt) ProtoReflect() protoreflect.Message {784 func (x *UIReceipt) ProtoReflect() protoreflect.Message {
740- mi := &file_terminal_proto_msgTypes[10]785+ mi := &file_terminal_proto_msgTypes[11]
741 if x != nil {786 if x != nil {
742 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))787 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
743 if ms.LoadMessageInfo() == nil {788 if ms.LoadMessageInfo() == nil {
@@ -750,7 +795,7 @@ func (x *UIReceipt) ProtoReflect() protoreflect.Message {
750 795
751 // Deprecated: Use UIReceipt.ProtoReflect.Descriptor instead.796 // Deprecated: Use UIReceipt.ProtoReflect.Descriptor instead.
752 func (*UIReceipt) Descriptor() ([]byte, []int) {797 func (*UIReceipt) Descriptor() ([]byte, []int) {
753- return file_terminal_proto_rawDescGZIP(), []int{10}798+ return file_terminal_proto_rawDescGZIP(), []int{11}
754 }799 }
755 800
756 func (x *UIReceipt) GetActionId() uint64 {801 func (x *UIReceipt) GetActionId() uint64 {
@@ -800,7 +845,7 @@ type UIViewUpdate struct {
800 845
801 func (x *UIViewUpdate) Reset() {846 func (x *UIViewUpdate) Reset() {
802 *x = UIViewUpdate{}847 *x = UIViewUpdate{}
803- mi := &file_terminal_proto_msgTypes[11]848+ mi := &file_terminal_proto_msgTypes[12]
804 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))849 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
805 ms.StoreMessageInfo(mi)850 ms.StoreMessageInfo(mi)
806 }851 }
@@ -812,7 +857,7 @@ func (x *UIViewUpdate) String() string {
812 func (*UIViewUpdate) ProtoMessage() {}857 func (*UIViewUpdate) ProtoMessage() {}
813 858
814 func (x *UIViewUpdate) ProtoReflect() protoreflect.Message {859 func (x *UIViewUpdate) ProtoReflect() protoreflect.Message {
815- mi := &file_terminal_proto_msgTypes[11]860+ mi := &file_terminal_proto_msgTypes[12]
816 if x != nil {861 if x != nil {
817 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))862 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
818 if ms.LoadMessageInfo() == nil {863 if ms.LoadMessageInfo() == nil {
@@ -825,7 +870,7 @@ func (x *UIViewUpdate) ProtoReflect() protoreflect.Message {
825 870
826 // Deprecated: Use UIViewUpdate.ProtoReflect.Descriptor instead.871 // Deprecated: Use UIViewUpdate.ProtoReflect.Descriptor instead.
827 func (*UIViewUpdate) Descriptor() ([]byte, []int) {872 func (*UIViewUpdate) Descriptor() ([]byte, []int) {
828- return file_terminal_proto_rawDescGZIP(), []int{11}873+ return file_terminal_proto_rawDescGZIP(), []int{12}
829 }874 }
830 875
831 func (x *UIViewUpdate) GetEpoch() uint64 {876 func (x *UIViewUpdate) GetEpoch() uint64 {
@@ -862,7 +907,7 @@ type RemotePreviewRequest struct {
862 907
863 func (x *RemotePreviewRequest) Reset() {908 func (x *RemotePreviewRequest) Reset() {
864 *x = RemotePreviewRequest{}909 *x = RemotePreviewRequest{}
865- mi := &file_terminal_proto_msgTypes[12]910+ mi := &file_terminal_proto_msgTypes[13]
866 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))911 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
867 ms.StoreMessageInfo(mi)912 ms.StoreMessageInfo(mi)
868 }913 }
@@ -874,7 +919,7 @@ func (x *RemotePreviewRequest) String() string {
874 func (*RemotePreviewRequest) ProtoMessage() {}919 func (*RemotePreviewRequest) ProtoMessage() {}
875 920
876 func (x *RemotePreviewRequest) ProtoReflect() protoreflect.Message {921 func (x *RemotePreviewRequest) ProtoReflect() protoreflect.Message {
877- mi := &file_terminal_proto_msgTypes[12]922+ mi := &file_terminal_proto_msgTypes[13]
878 if x != nil {923 if x != nil {
879 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))924 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
880 if ms.LoadMessageInfo() == nil {925 if ms.LoadMessageInfo() == nil {
@@ -887,7 +932,7 @@ func (x *RemotePreviewRequest) ProtoReflect() protoreflect.Message {
887 932
888 // Deprecated: Use RemotePreviewRequest.ProtoReflect.Descriptor instead.933 // Deprecated: Use RemotePreviewRequest.ProtoReflect.Descriptor instead.
889 func (*RemotePreviewRequest) Descriptor() ([]byte, []int) {934 func (*RemotePreviewRequest) Descriptor() ([]byte, []int) {
890- return file_terminal_proto_rawDescGZIP(), []int{12}935+ return file_terminal_proto_rawDescGZIP(), []int{13}
891 }936 }
892 937
893 func (x *RemotePreviewRequest) GetVersion() uint32 {938 func (x *RemotePreviewRequest) GetVersion() uint32 {
@@ -937,7 +982,7 @@ type RemotePreview struct {
937 982
938 func (x *RemotePreview) Reset() {983 func (x *RemotePreview) Reset() {
939 *x = RemotePreview{}984 *x = RemotePreview{}
940- mi := &file_terminal_proto_msgTypes[13]985+ mi := &file_terminal_proto_msgTypes[14]
941 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))986 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
942 ms.StoreMessageInfo(mi)987 ms.StoreMessageInfo(mi)
943 }988 }
@@ -949,7 +994,7 @@ func (x *RemotePreview) String() string {
949 func (*RemotePreview) ProtoMessage() {}994 func (*RemotePreview) ProtoMessage() {}
950 995
951 func (x *RemotePreview) ProtoReflect() protoreflect.Message {996 func (x *RemotePreview) ProtoReflect() protoreflect.Message {
952- mi := &file_terminal_proto_msgTypes[13]997+ mi := &file_terminal_proto_msgTypes[14]
953 if x != nil {998 if x != nil {
954 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))999 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
955 if ms.LoadMessageInfo() == nil {1000 if ms.LoadMessageInfo() == nil {
@@ -962,7 +1007,7 @@ func (x *RemotePreview) ProtoReflect() protoreflect.Message {
962 1007
963 // Deprecated: Use RemotePreview.ProtoReflect.Descriptor instead.1008 // Deprecated: Use RemotePreview.ProtoReflect.Descriptor instead.
964 func (*RemotePreview) Descriptor() ([]byte, []int) {1009 func (*RemotePreview) Descriptor() ([]byte, []int) {
965- return file_terminal_proto_rawDescGZIP(), []int{13}1010+ return file_terminal_proto_rawDescGZIP(), []int{14}
966 }1011 }
967 1012
968 func (x *RemotePreview) GetVersion() uint32 {1013 func (x *RemotePreview) GetVersion() uint32 {
@@ -1036,7 +1081,7 @@ type PickerPreviewRequest struct {
1036 1081
1037 func (x *PickerPreviewRequest) Reset() {1082 func (x *PickerPreviewRequest) Reset() {
1038 *x = PickerPreviewRequest{}1083 *x = PickerPreviewRequest{}
1039- mi := &file_terminal_proto_msgTypes[14]1084+ mi := &file_terminal_proto_msgTypes[15]
1040 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1085 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1041 ms.StoreMessageInfo(mi)1086 ms.StoreMessageInfo(mi)
1042 }1087 }
@@ -1048,7 +1093,7 @@ func (x *PickerPreviewRequest) String() string {
1048 func (*PickerPreviewRequest) ProtoMessage() {}1093 func (*PickerPreviewRequest) ProtoMessage() {}
1049 1094
1050 func (x *PickerPreviewRequest) ProtoReflect() protoreflect.Message {1095 func (x *PickerPreviewRequest) ProtoReflect() protoreflect.Message {
1051- mi := &file_terminal_proto_msgTypes[14]1096+ mi := &file_terminal_proto_msgTypes[15]
1052 if x != nil {1097 if x != nil {
1053 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1098 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1054 if ms.LoadMessageInfo() == nil {1099 if ms.LoadMessageInfo() == nil {
@@ -1061,7 +1106,7 @@ func (x *PickerPreviewRequest) ProtoReflect() protoreflect.Message {
1061 1106
1062 // Deprecated: Use PickerPreviewRequest.ProtoReflect.Descriptor instead.1107 // Deprecated: Use PickerPreviewRequest.ProtoReflect.Descriptor instead.
1063 func (*PickerPreviewRequest) Descriptor() ([]byte, []int) {1108 func (*PickerPreviewRequest) Descriptor() ([]byte, []int) {
1064- return file_terminal_proto_rawDescGZIP(), []int{14}1109+ return file_terminal_proto_rawDescGZIP(), []int{15}
1065 }1110 }
1066 1111
1067 func (x *PickerPreviewRequest) GetVersion() uint32 {1112 func (x *PickerPreviewRequest) GetVersion() uint32 {
@@ -1125,7 +1170,7 @@ type PickerPreview struct {
1125 1170
1126 func (x *PickerPreview) Reset() {1171 func (x *PickerPreview) Reset() {
1127 *x = PickerPreview{}1172 *x = PickerPreview{}
1128- mi := &file_terminal_proto_msgTypes[15]1173+ mi := &file_terminal_proto_msgTypes[16]
1129 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1174 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1130 ms.StoreMessageInfo(mi)1175 ms.StoreMessageInfo(mi)
1131 }1176 }
@@ -1137,7 +1182,7 @@ func (x *PickerPreview) String() string {
1137 func (*PickerPreview) ProtoMessage() {}1182 func (*PickerPreview) ProtoMessage() {}
1138 1183
1139 func (x *PickerPreview) ProtoReflect() protoreflect.Message {1184 func (x *PickerPreview) ProtoReflect() protoreflect.Message {
1140- mi := &file_terminal_proto_msgTypes[15]1185+ mi := &file_terminal_proto_msgTypes[16]
1141 if x != nil {1186 if x != nil {
1142 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1187 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1143 if ms.LoadMessageInfo() == nil {1188 if ms.LoadMessageInfo() == nil {
@@ -1150,7 +1195,7 @@ func (x *PickerPreview) ProtoReflect() protoreflect.Message {
1150 1195
1151 // Deprecated: Use PickerPreview.ProtoReflect.Descriptor instead.1196 // Deprecated: Use PickerPreview.ProtoReflect.Descriptor instead.
1152 func (*PickerPreview) Descriptor() ([]byte, []int) {1197 func (*PickerPreview) Descriptor() ([]byte, []int) {
1153- return file_terminal_proto_rawDescGZIP(), []int{15}1198+ return file_terminal_proto_rawDescGZIP(), []int{16}
1154 }1199 }
1155 1200
1156 func (x *PickerPreview) GetVersion() uint32 {1201 func (x *PickerPreview) GetVersion() uint32 {
@@ -1267,7 +1312,9 @@ const file_terminal_proto_rawDesc = "" +
1267 "\x04data\x18\x03 \x01(\fR\x04data\"&\n" +1312 "\x04data\x18\x03 \x01(\fR\x04data\"&\n" +
1268 "\fClientNotice\x12\x16\n" +1313 "\fClientNotice\x12\x16\n" +
1269 "\x06action\x18\x01 \x01(\rR\x06action\"\x14\n" +1314 "\x06action\x18\x01 \x01(\rR\x06action\"\x14\n" +
1270- "\x12OutputResetRequest\"&\n" +1315+ "\x12OutputResetRequest\"\"\n" +
1316+ "\tSelectTab\x12\x15\n" +
1317+ "\x06tab_id\x18\x01 \x01(\tR\x05tabId\"&\n" +
1271 "\aUIFence\x12\x1b\n" +1318 "\aUIFence\x12\x1b\n" +
1272 "\taction_id\x18\x01 \x01(\x04R\bactionId\"\x99\x01\n" +1319 "\taction_id\x18\x01 \x01(\x04R\bactionId\"\x99\x01\n" +
1273 "\tUIReceipt\x12\x1b\n" +1320 "\tUIReceipt\x12\x1b\n" +
@@ -1323,7 +1370,7 @@ func file_terminal_proto_rawDescGZIP() []byte {
1323 return file_terminal_proto_rawDescData1370 return file_terminal_proto_rawDescData
1324 }1371 }
1325 1372
1326-var file_terminal_proto_msgTypes = make([]protoimpl.MessageInfo, 16)1373+var file_terminal_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
1327 var file_terminal_proto_goTypes = []any{1374 var file_terminal_proto_goTypes = []any{
1328 (*ViewContext)(nil), // 0: vev.wire.v1.ViewContext1375 (*ViewContext)(nil), // 0: vev.wire.v1.ViewContext
1329 (*Output)(nil), // 1: vev.wire.v1.Output1376 (*Output)(nil), // 1: vev.wire.v1.Output
@@ -1334,30 +1381,31 @@ var file_terminal_proto_goTypes = []any{
1334 (*ImagePush)(nil), // 6: vev.wire.v1.ImagePush1381 (*ImagePush)(nil), // 6: vev.wire.v1.ImagePush
1335 (*ClientNotice)(nil), // 7: vev.wire.v1.ClientNotice1382 (*ClientNotice)(nil), // 7: vev.wire.v1.ClientNotice
1336 (*OutputResetRequest)(nil), // 8: vev.wire.v1.OutputResetRequest1383 (*OutputResetRequest)(nil), // 8: vev.wire.v1.OutputResetRequest
1337- (*UIFence)(nil), // 9: vev.wire.v1.UIFence1384+ (*SelectTab)(nil), // 9: vev.wire.v1.SelectTab
1338- (*UIReceipt)(nil), // 10: vev.wire.v1.UIReceipt1385+ (*UIFence)(nil), // 10: vev.wire.v1.UIFence
1339- (*UIViewUpdate)(nil), // 11: vev.wire.v1.UIViewUpdate1386+ (*UIReceipt)(nil), // 11: vev.wire.v1.UIReceipt
1340- (*RemotePreviewRequest)(nil), // 12: vev.wire.v1.RemotePreviewRequest1387+ (*UIViewUpdate)(nil), // 12: vev.wire.v1.UIViewUpdate
1341- (*RemotePreview)(nil), // 13: vev.wire.v1.RemotePreview1388+ (*RemotePreviewRequest)(nil), // 13: vev.wire.v1.RemotePreviewRequest
1342- (*PickerPreviewRequest)(nil), // 14: vev.wire.v1.PickerPreviewRequest1389+ (*RemotePreview)(nil), // 14: vev.wire.v1.RemotePreview
1343- (*PickerPreview)(nil), // 15: vev.wire.v1.PickerPreview1390+ (*PickerPreviewRequest)(nil), // 15: vev.wire.v1.PickerPreviewRequest
1344- (*CommittedRouteIdentity)(nil), // 16: vev.wire.v1.CommittedRouteIdentity1391+ (*PickerPreview)(nil), // 16: vev.wire.v1.PickerPreview
1345- (*RGB)(nil), // 17: vev.wire.v1.RGB1392+ (*CommittedRouteIdentity)(nil), // 17: vev.wire.v1.CommittedRouteIdentity
1346- (*RemoteTarget)(nil), // 18: vev.wire.v1.RemoteTarget1393+ (*RGB)(nil), // 18: vev.wire.v1.RGB
1347- (*LifecycleID)(nil), // 19: vev.wire.v1.LifecycleID1394+ (*RemoteTarget)(nil), // 19: vev.wire.v1.RemoteTarget
1348- (*PreviewCell)(nil), // 20: vev.wire.v1.PreviewCell1395+ (*LifecycleID)(nil), // 20: vev.wire.v1.LifecycleID
1396+ (*PreviewCell)(nil), // 21: vev.wire.v1.PreviewCell
1349 }1397 }
1350 var file_terminal_proto_depIdxs = []int32{1398 var file_terminal_proto_depIdxs = []int32{
1351- 16, // 0: vev.wire.v1.ViewContext.route:type_name -> vev.wire.v1.CommittedRouteIdentity1399+ 17, // 0: vev.wire.v1.ViewContext.route:type_name -> vev.wire.v1.CommittedRouteIdentity
1352 0, // 1: vev.wire.v1.Output.context:type_name -> vev.wire.v1.ViewContext1400 0, // 1: vev.wire.v1.Output.context:type_name -> vev.wire.v1.ViewContext
1353- 17, // 2: vev.wire.v1.Theme.foreground:type_name -> vev.wire.v1.RGB1401+ 18, // 2: vev.wire.v1.Theme.foreground:type_name -> vev.wire.v1.RGB
1354- 17, // 3: vev.wire.v1.Theme.background:type_name -> vev.wire.v1.RGB1402+ 18, // 3: vev.wire.v1.Theme.background:type_name -> vev.wire.v1.RGB
1355- 17, // 4: vev.wire.v1.Theme.palette:type_name -> vev.wire.v1.RGB1403+ 18, // 4: vev.wire.v1.Theme.palette:type_name -> vev.wire.v1.RGB
1356 0, // 5: vev.wire.v1.UIViewUpdate.context:type_name -> vev.wire.v1.ViewContext1404 0, // 5: vev.wire.v1.UIViewUpdate.context:type_name -> vev.wire.v1.ViewContext
1357- 18, // 6: vev.wire.v1.RemotePreviewRequest.target:type_name -> vev.wire.v1.RemoteTarget1405+ 19, // 6: vev.wire.v1.RemotePreviewRequest.target:type_name -> vev.wire.v1.RemoteTarget
1358- 19, // 7: vev.wire.v1.RemotePreview.lifecycle_id:type_name -> vev.wire.v1.LifecycleID1406+ 20, // 7: vev.wire.v1.RemotePreview.lifecycle_id:type_name -> vev.wire.v1.LifecycleID
1359- 20, // 8: vev.wire.v1.RemotePreview.cells:type_name -> vev.wire.v1.PreviewCell1407+ 21, // 8: vev.wire.v1.RemotePreview.cells:type_name -> vev.wire.v1.PreviewCell
1360- 20, // 9: vev.wire.v1.PickerPreview.cells:type_name -> vev.wire.v1.PreviewCell1408+ 21, // 9: vev.wire.v1.PickerPreview.cells:type_name -> vev.wire.v1.PreviewCell
1361 10, // [10:10] is the sub-list for method output_type1409 10, // [10:10] is the sub-list for method output_type
1362 10, // [10:10] is the sub-list for method input_type1410 10, // [10:10] is the sub-list for method input_type
1363 10, // [10:10] is the sub-list for extension type_name1411 10, // [10:10] is the sub-list for extension type_name
@@ -1378,7 +1426,7 @@ func file_terminal_proto_init() {
1378 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),1426 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
1379 RawDescriptor: unsafe.Slice(unsafe.StringData(file_terminal_proto_rawDesc), len(file_terminal_proto_rawDesc)),1427 RawDescriptor: unsafe.Slice(unsafe.StringData(file_terminal_proto_rawDesc), len(file_terminal_proto_rawDesc)),
1380 NumEnums: 0,1428 NumEnums: 0,
1381- NumMessages: 16,1429+ NumMessages: 17,
1382 NumExtensions: 0,1430 NumExtensions: 0,
1383 NumServices: 0,1431 NumServices: 0,
1384 },1432 },
A internal/usecase/daemon/attachment_select_tab.go +40 -0
@@ -0,0 +1,40 @@
1+package daemon
2+
3+import (
4+ "github.com/bnema/vev/internal/domain"
5+ "github.com/bnema/vev/internal/protocol"
6+)
7+
8+// selectTabForAttachment shows one exact tab of the attached session on this
9+// attachment's own view. It is the client picker's in-place tab switch: the
10+// attachment stays live and only its view moves. A tab the session no longer
11+// holds is refused without touching the view, so the picker never lands on a
12+// repaired neighbour it did not choose.
13+func (d *Daemon) selectTabForAttachment(effect *attachmentEffect, request protocol.SelectTab) {
14+ if request.Validate() != nil || effect == nil || !effect.current() || effect.sess == nil || effect.ac == nil {
15+ return
16+ }
17+ sess, ac := effect.sess, effect.ac
18+ var selected *tab
19+ _ = sess.runMutation(func() error {
20+ sess.mu.Lock()
21+ for _, candidate := range sess.tabs {
22+ if candidate == nil || domain.TabStableID(candidate.stableID) != request.TabID {
23+ continue
24+ }
25+ if sess.selectAttachmentTabLocked(ac, request.TabID) {
26+ selected = candidate
27+ }
28+ break
29+ }
30+ sess.mu.Unlock()
31+ if selected != nil {
32+ d.activateTabAfterResizeForLease(sess, selected, false, ac, nil)
33+ }
34+ return nil
35+ })
36+ if selected == nil {
37+ return
38+ }
39+ d.invalidateRender(sess, ac, true, "attachment_select_tab.go")
40+}
@@ -0,0 +1,40 @@
1+package daemon
2+
3+import (
4+ "github.com/bnema/vev/internal/domain"
5+ "github.com/bnema/vev/internal/protocol"
6+)
7+
8+// selectTabForAttachment shows one exact tab of the attached session on this
9+// attachment's own view. It is the client picker's in-place tab switch: the
10+// attachment stays live and only its view moves. A tab the session no longer
11+// holds is refused without touching the view, so the picker never lands on a
12+// repaired neighbour it did not choose.
13+func (d *Daemon) selectTabForAttachment(effect *attachmentEffect, request protocol.SelectTab) {
14+ if request.Validate() != nil || effect == nil || !effect.current() || effect.sess == nil || effect.ac == nil {
15+ return
16+ }
17+ sess, ac := effect.sess, effect.ac
18+ var selected *tab
19+ _ = sess.runMutation(func() error {
20+ sess.mu.Lock()
21+ for _, candidate := range sess.tabs {
22+ if candidate == nil || domain.TabStableID(candidate.stableID) != request.TabID {
23+ continue
24+ }
25+ if sess.selectAttachmentTabLocked(ac, request.TabID) {
26+ selected = candidate
27+ }
28+ break
29+ }
30+ sess.mu.Unlock()
31+ if selected != nil {
32+ d.activateTabAfterResizeForLease(sess, selected, false, ac, nil)
33+ }
34+ return nil
35+ })
36+ if selected == nil {
37+ return
38+ }
39+ d.invalidateRender(sess, ac, true, "attachment_select_tab.go")
40+}
A internal/usecase/daemon/attachment_select_tab_test.go +54 -0
@@ -0,0 +1,54 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+)
11+
12+// TestSelectTabMovesOnlyThisAttachmentView proves the picker's in-place tab
13+// switch: an exact tab selects it on the requesting attachment, a peer keeps
14+// its own view, and an unknown or malformed tab leaves the view untouched.
15+func TestSelectTabMovesOnlyThisAttachmentView(t *testing.T) {
16+ tests := []struct {
17+ name string
18+ request func(second domain.TabStableID) protocol.SelectTab
19+ moves bool
20+ }{
21+ {name: "exact tab", request: func(second domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{TabID: second} }, moves: true},
22+ {name: "unknown tab", request: func(domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{TabID: "t_missing"} }},
23+ {name: "malformed tab", request: func(domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{} }},
24+ }
25+ for _, tt := range tests {
26+ t.Run(tt.name, func(t *testing.T) {
27+ d, sess, ac, _, releases := newManualTabSession(t, 2)
28+ for _, release := range releases {
29+ defer release()
30+ }
31+ peer := addMultiplexTestAttachment(t, sess, domain.Geometry{Size: domain.Size{Cols: 90, Rows: 30}})
32+ rc := d.attachCoordinator(sess, nil, ac, true)
33+ d.attachCoordinator(sess, nil, peer, true)
34+ token := sess.captureAttachmentCapability(ac, ac.transport())
35+ token.lease = rc.attachmentLease(ac)
36+ ac.installTestAttachmentCapability(token)
37+
38+ first := domain.TabStableID(sess.tabs[0].stableID)
39+ second := domain.TabStableID(sess.tabs[1].stableID)
40+ require.True(t, sess.selectAttachmentTab(ac, first))
41+ require.True(t, sess.selectAttachmentTab(peer, first))
42+ before := ac.viewSnapshot()
43+
44+ require.False(t, d.handleAttachmentClientMessage(token, tt.request(second)))
45+
46+ if tt.moves {
47+ require.Equal(t, second, ac.viewSnapshot().tabID)
48+ } else {
49+ require.Equal(t, before, ac.viewSnapshot(), "a refused selection never repairs the view")
50+ }
51+ require.Equal(t, first, peer.viewSnapshot().tabID, "the peer attachment keeps its own view")
52+ })
53+ }
54+}
@@ -0,0 +1,54 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+)
11+
12+// TestSelectTabMovesOnlyThisAttachmentView proves the picker's in-place tab
13+// switch: an exact tab selects it on the requesting attachment, a peer keeps
14+// its own view, and an unknown or malformed tab leaves the view untouched.
15+func TestSelectTabMovesOnlyThisAttachmentView(t *testing.T) {
16+ tests := []struct {
17+ name string
18+ request func(second domain.TabStableID) protocol.SelectTab
19+ moves bool
20+ }{
21+ {name: "exact tab", request: func(second domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{TabID: second} }, moves: true},
22+ {name: "unknown tab", request: func(domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{TabID: "t_missing"} }},
23+ {name: "malformed tab", request: func(domain.TabStableID) protocol.SelectTab { return protocol.SelectTab{} }},
24+ }
25+ for _, tt := range tests {
26+ t.Run(tt.name, func(t *testing.T) {
27+ d, sess, ac, _, releases := newManualTabSession(t, 2)
28+ for _, release := range releases {
29+ defer release()
30+ }
31+ peer := addMultiplexTestAttachment(t, sess, domain.Geometry{Size: domain.Size{Cols: 90, Rows: 30}})
32+ rc := d.attachCoordinator(sess, nil, ac, true)
33+ d.attachCoordinator(sess, nil, peer, true)
34+ token := sess.captureAttachmentCapability(ac, ac.transport())
35+ token.lease = rc.attachmentLease(ac)
36+ ac.installTestAttachmentCapability(token)
37+
38+ first := domain.TabStableID(sess.tabs[0].stableID)
39+ second := domain.TabStableID(sess.tabs[1].stableID)
40+ require.True(t, sess.selectAttachmentTab(ac, first))
41+ require.True(t, sess.selectAttachmentTab(peer, first))
42+ before := ac.viewSnapshot()
43+
44+ require.False(t, d.handleAttachmentClientMessage(token, tt.request(second)))
45+
46+ if tt.moves {
47+ require.Equal(t, second, ac.viewSnapshot().tabID)
48+ } else {
49+ require.Equal(t, before, ac.viewSnapshot(), "a refused selection never repairs the view")
50+ }
51+ require.Equal(t, first, peer.viewSnapshot().tabID, "the peer attachment keeps its own view")
52+ })
53+ }
54+}
M internal/usecase/daemon/client_frame_routing.go +2 -0
@@ -167,6 +167,8 @@ func (d *Daemon) handleActiveAttachmentClientMessage(capability attachmentCapabi
167167 d.resetOutput(effect)
168168 case protocol.SamePeerSwitchRequest:
169169 d.switchSamePeerForAttachment(effect, message)
170+ case protocol.SelectTab:
171+ d.selectTabForAttachment(effect, message)
170172 case protocol.RecentRouteSnapshot:
171173 replayIdentity := effect.ac.setRouteSnapshot(message)
172174 d.pokeAttentionTicker()
@@ -167,6 +167,8 @@ func (d *Daemon) handleActiveAttachmentClientMessage(capability attachmentCapabi
167 d.resetOutput(effect)167 d.resetOutput(effect)
168 case protocol.SamePeerSwitchRequest:168 case protocol.SamePeerSwitchRequest:
169 d.switchSamePeerForAttachment(effect, message)169 d.switchSamePeerForAttachment(effect, message)
170+ case protocol.SelectTab:
171+ d.selectTabForAttachment(effect, message)
170 case protocol.RecentRouteSnapshot:172 case protocol.RecentRouteSnapshot:
171 replayIdentity := effect.ac.setRouteSnapshot(message)173 replayIdentity := effect.ac.setRouteSnapshot(message)
172 d.pokeAttentionTicker()174 d.pokeAttentionTicker()

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